Repository: meshtastic/firmware Branch: develop Commit: eb80d3141d2f Files: 1572 Total size: 9.3 MB Directory structure: gitextract_d2spliar/ ├── .clusterfuzzlite/ │ ├── Dockerfile │ ├── README.md │ ├── build.sh │ ├── platformio-clusterfuzzlite-post.py │ ├── platformio-clusterfuzzlite-pre.py │ ├── project.yaml │ ├── router_fuzzer.cpp │ ├── router_fuzzer.options │ └── router_fuzzer_seed_corpus.py ├── .devcontainer/ │ ├── 99-platformio-udev.rules │ ├── Dockerfile │ ├── devcontainer.json │ └── setup.sh ├── .envrc ├── .gitattributes ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── Bug Report.yml │ │ ├── New Board.yml │ │ └── feature.yml │ ├── actionlint.yaml │ ├── actions/ │ │ ├── build-variant/ │ │ │ └── action.yml │ │ ├── setup-base/ │ │ │ └── action.yml │ │ └── setup-native/ │ │ └── action.yml │ ├── copilot-instructions.md │ ├── pull_request_template.md │ └── workflows/ │ ├── build_debian_src.yml │ ├── build_firmware.yml │ ├── build_one_target.yml │ ├── daily_packaging.yml │ ├── docker_build.yml │ ├── docker_manifest.yml │ ├── first_time_contributor.yml │ ├── hook_copr.yml │ ├── main_matrix.yml │ ├── merge_queue.yml │ ├── models_issue_triage.yml │ ├── models_pr_triage.yml │ ├── nightly.yml │ ├── package_obs.yml │ ├── package_pio_deps.yml │ ├── package_ppa.yml │ ├── pr_enforce_labels.yml │ ├── pr_tests.yml │ ├── release_channels.yml │ ├── sec_sast_semgrep_cron.yml │ ├── sec_sast_semgrep_pull.yml │ ├── stale_bot.yml │ ├── test_native.yml │ ├── tests.yml │ ├── trunk_annotate_pr.yml │ ├── trunk_check.yml │ └── update_protobufs.yml ├── .gitignore ├── .gitmodules ├── .gitpod.yml ├── .semgrepignore ├── .trunk/ │ ├── .gitignore │ ├── configs/ │ │ ├── .bandit │ │ ├── .clang-format │ │ ├── .flake8 │ │ ├── .hadolint.yaml │ │ ├── .isort.cfg │ │ ├── .markdownlint.yaml │ │ ├── .prettierignore │ │ ├── .prettierrc │ │ ├── .shellcheckrc │ │ ├── .yamllint.yaml │ │ ├── ruff.toml │ │ └── svgo.config.js │ └── trunk.yaml ├── .vscode/ │ ├── settings.json │ └── tasks.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── Dockerfile.test ├── LICENSE ├── README.md ├── SECURITY.md ├── alpine.Dockerfile ├── bin/ │ ├── .gitignore │ ├── 99-meshtasticd-udev.rules │ ├── Meshtastic_nRF52_factory_erase_v3_S140_6.1.0.uf2 │ ├── Meshtastic_nRF52_factory_erase_v3_S140_7.3.0.uf2 │ ├── analyze_map.py │ ├── base64_to_hex.py │ ├── build-esp32.sh │ ├── build-firmware.sh │ ├── build-native.sh │ ├── build-nrf52.sh │ ├── build-rp2xx0.sh │ ├── build-stm32wl.sh │ ├── build-userprefs-json.py │ ├── buildinfo.py │ ├── bump_metainfo/ │ │ ├── bump_metainfo.py │ │ └── requirements.txt │ ├── bump_version.py │ ├── check-all.sh │ ├── check-dependencies.sh │ ├── config-dist.yaml │ ├── config.d/ │ │ ├── MUI/ │ │ │ └── X11_480x480.yaml │ │ ├── OpenWRT/ │ │ │ ├── BananaPi-BPI-R4-sx1262.yaml │ │ │ ├── OpenWRT-One-mikroBUS-LR-IOT-CLICK.yaml │ │ │ └── OpenWRT_One_mikroBUS_sx1262.yaml │ │ ├── display-waveshare-1-44.yaml │ │ ├── display-waveshare-2.8.yaml │ │ ├── femtofox/ │ │ │ ├── femtofox_LR1121_TCXO.yaml │ │ │ ├── femtofox_SX1262_TCXO.yaml │ │ │ └── femtofox_SX1262_XTAL.yaml │ │ ├── lora-Adafruit-RFM9x.yaml │ │ ├── lora-MeshAdv-900M30S.yaml │ │ ├── lora-MeshAdv-Mini-900M22S.yaml │ │ ├── lora-RAK6421-13300-slot1.yaml │ │ ├── lora-RAK6421-13300-slot2.yaml │ │ ├── lora-RAK6421-13302-slot1.yaml │ │ ├── lora-RAK6421-13302-slot2.yaml │ │ ├── lora-hat-rak-6421-pi-hat.yaml │ │ ├── lora-lyra-picocalc-wio-sx1262.yaml │ │ ├── lora-lyra-ultra_1w.yaml │ │ ├── lora-lyra-ultra_2w.yaml │ │ ├── lora-lyra-ws-raspberry-pi-pico-hat.yaml │ │ ├── lora-meshstick-1262.yaml │ │ ├── lora-piggystick-lr1121.yaml │ │ ├── lora-pinedio-usb-sx1262.yaml │ │ ├── lora-raxda-rock2f-starter-edition-hat.yaml │ │ ├── lora-starter-edition-sx1262-i2c.yaml │ │ ├── lora-usb-meshstick-1262.yaml │ │ ├── lora-usb-meshtoad-e22.yaml │ │ ├── lora-usb-umesh-1262-30dbm.yaml │ │ ├── lora-usb-umesh-1268-30dbm.yaml │ │ ├── lora-waveshare-sxxx.yaml │ │ ├── lora-ws-raspberry-pi-pico-to-rpi-adapter.yaml │ │ └── lora-ws-raspberry-pico-to-orangepi-03.yaml │ ├── device-install.bat │ ├── device-install.sh │ ├── device-install_test.ps1 │ ├── device-update.bat │ ├── device-update.sh │ ├── dump-ram-users.sh │ ├── exception_decoder.py │ ├── gen-images.sh │ ├── generate_ci_matrix.py │ ├── generate_release_notes.py │ ├── generic/ │ │ ├── Meshtastic_6.1.0_bootloader-0.9.2_s140_6.1.1.hex │ │ ├── Meshtastic_7.3.0_bootloader-0.9.2_s140_7.3.0.hex │ │ ├── update-Meshtastic_6.1.0_bootloader-0.9.2_nosd.uf2 │ │ └── update-Meshtastic_7.3.0_bootloader-0.9.2_nosd.uf2 │ ├── genpartitions.py │ ├── kill-github-actions.sh │ ├── mergehex │ ├── meshtasticd-start.sh │ ├── meshtasticd.service │ ├── native-gdbserver.sh │ ├── native-install.sh │ ├── native-run.sh │ ├── org.meshtastic.meshtasticd.desktop │ ├── org.meshtastic.meshtasticd.metainfo.xml │ ├── platformio-custom.py │ ├── platformio-pre.py │ ├── promote-release.sh │ ├── read-system-info.sh │ ├── readprops.py │ ├── regen-protos.bat │ ├── regen-protos.sh │ ├── rpkg.macros │ ├── s140_nrf52_7.3.0_softdevice.hex │ ├── setup-python-for-esp-debug.sh │ ├── shame.py │ ├── test-native-docker.sh │ ├── test-simulator.sh │ ├── uf2-convert.bat │ ├── uf2conv.py │ ├── update-lilygo_techo_bootloader-0.6.1_nosd.uf2 │ ├── view-map.sh │ └── web.version ├── boards/ │ ├── CDEBYTE_EoRa-Hub.json │ ├── CDEBYTE_EoRa-S3.json │ ├── ESP32-S3-WROOM-1-N4.json │ ├── ThinkNode-M1.json │ ├── ThinkNode-M3.json │ ├── ThinkNode-M4.json │ ├── ThinkNode-M6.json │ ├── bpi_picow_esp32_s3.json │ ├── canaryone.json │ ├── crowpanel.json │ ├── eink0.1.json │ ├── esp32-s3-pico.json │ ├── esp32-s3-zero.json │ ├── gat562_mesh_trial_tracker.json │ ├── hackaday-communicator.json │ ├── heltec_mesh_node_t114.json │ ├── heltec_mesh_pocket.json │ ├── heltec_mesh_solar.json │ ├── heltec_v4.json │ ├── heltec_vision_master_e213.json │ ├── heltec_vision_master_e290.json │ ├── heltec_vision_master_t190.json │ ├── heltec_wireless_tracker.json │ ├── heltec_wireless_tracker_v2.json │ ├── icarus.json │ ├── me25ls01-4y10td.json │ ├── mesh-tab.json │ ├── meshlink.json │ ├── meshtiny.json │ ├── mini-epaper-s3.json │ ├── minimesh_lite.json │ ├── ms24sf1.json │ ├── muzi-base.json │ ├── my-esp32s3-diy-oled.json │ ├── my_esp32s3_diy_eink.json │ ├── nano-g2-ultra.json │ ├── nordic_pca10059.json │ ├── nrf52840_dk.json │ ├── promicro-nrf52840.json │ ├── r1-neo.json │ ├── seeed-sensecap-indicator.json │ ├── seeed-xiao-s3.json │ ├── seeed_solar_node.json │ ├── seeed_wio_tracker_L1.json │ ├── seeed_xiao_nrf52840_kit.json │ ├── station-g2.json │ ├── t-beam-1w.json │ ├── t-deck-pro.json │ ├── t-deck.json │ ├── t-echo.json │ ├── t-watch-s3.json │ ├── tbeam-s3-core.json │ ├── tlora-t3s3-v1.json │ ├── tracker-t1000-e.json │ ├── unphone.json │ ├── wio-sdk-wm1110.json │ ├── wio-t1000-s.json │ ├── wio-tracker-wm1110.json │ ├── wiphone.json │ ├── wiscore_rak11200.json │ ├── wiscore_rak3172.json │ ├── wiscore_rak3312.json │ ├── wiscore_rak4600.json │ ├── wiscore_rak4631.json │ └── xiao_ble_sense.json ├── branding/ │ └── README.md ├── data/ │ └── static/ │ └── .gitkeep ├── debian/ │ ├── .gitignore │ ├── changelog │ ├── ci_changelog.sh │ ├── ci_pack_sdeb.sh │ ├── control │ ├── meshtasticd.dirs │ ├── meshtasticd.install │ ├── meshtasticd.postinst │ ├── meshtasticd.postrm │ ├── meshtasticd.udev │ ├── rules │ └── source/ │ ├── format │ ├── include-binaries │ └── options ├── docker-compose.yml ├── extra_scripts/ │ ├── README.md │ ├── disable_adafruit_usb.py │ ├── esp32_extra.py │ ├── esp32_pre.py │ ├── nrf52_extra.py │ └── stm32_extra.py ├── flake.nix ├── meshtasticd.spec.rpkg ├── monitor/ │ └── filter_c3_exception_decoder.py ├── partition-table-8MB.csv ├── partition-table.csv ├── platformio.ini ├── pyocd.yaml ├── renovate.json ├── rpkg.conf ├── shell.nix ├── src/ │ ├── AmbientLightingThread.h │ ├── AudioThread.h │ ├── BluetoothCommon.cpp │ ├── BluetoothCommon.h │ ├── BluetoothStatus.h │ ├── DebugConfiguration.cpp │ ├── DebugConfiguration.h │ ├── DisplayFormatters.cpp │ ├── DisplayFormatters.h │ ├── FSCommon.cpp │ ├── FSCommon.h │ ├── Fusion/ │ │ ├── Fusion.h │ │ ├── FusionAhrs.c │ │ ├── FusionAhrs.h │ │ ├── FusionAxes.h │ │ ├── FusionCalibration.h │ │ ├── FusionCompass.c │ │ ├── FusionCompass.h │ │ ├── FusionConvention.h │ │ ├── FusionMath.h │ │ ├── FusionOffset.c │ │ └── FusionOffset.h │ ├── GPSStatus.h │ ├── GpioLogic.cpp │ ├── GpioLogic.h │ ├── MessageStore.cpp │ ├── MessageStore.h │ ├── NodeStatus.h │ ├── Observer.cpp │ ├── Observer.h │ ├── Power.cpp │ ├── PowerFSM.cpp │ ├── PowerFSM.h │ ├── PowerFSMThread.h │ ├── PowerMon.cpp │ ├── PowerMon.h │ ├── PowerStatus.h │ ├── RF95Configuration.h │ ├── RedirectablePrint.cpp │ ├── RedirectablePrint.h │ ├── SPILock.cpp │ ├── SPILock.h │ ├── SafeFile.cpp │ ├── SafeFile.h │ ├── SerialConsole.cpp │ ├── SerialConsole.h │ ├── Status.h │ ├── airtime.cpp │ ├── airtime.h │ ├── buzz/ │ │ ├── BuzzerFeedbackThread.cpp │ │ ├── BuzzerFeedbackThread.h │ │ ├── buzz.cpp │ │ └── buzz.h │ ├── commands.h │ ├── concurrency/ │ │ ├── BinarySemaphoreFreeRTOS.cpp │ │ ├── BinarySemaphoreFreeRTOS.h │ │ ├── BinarySemaphorePosix.cpp │ │ ├── BinarySemaphorePosix.h │ │ ├── InterruptableDelay.cpp │ │ ├── InterruptableDelay.h │ │ ├── Lock.cpp │ │ ├── Lock.h │ │ ├── LockGuard.cpp │ │ ├── LockGuard.h │ │ ├── NotifiedWorkerThread.cpp │ │ ├── NotifiedWorkerThread.h │ │ ├── OSThread.cpp │ │ ├── OSThread.h │ │ └── Periodic.h │ ├── configuration.h │ ├── detect/ │ │ ├── LoRaRadioType.h │ │ ├── ScanI2C.cpp │ │ ├── ScanI2C.h │ │ ├── ScanI2CConsumer.cpp │ │ ├── ScanI2CConsumer.h │ │ ├── ScanI2CTwoWire.cpp │ │ ├── ScanI2CTwoWire.h │ │ ├── einkScan.h │ │ ├── reClockI2C.cpp │ │ └── reClockI2C.h │ ├── error.h │ ├── freertosinc.h │ ├── gps/ │ │ ├── GPS.cpp │ │ ├── GPS.h │ │ ├── GPSUpdateScheduling.cpp │ │ ├── GPSUpdateScheduling.h │ │ ├── GeoCoord.cpp │ │ ├── GeoCoord.h │ │ ├── NMEAWPL.cpp │ │ ├── NMEAWPL.h │ │ ├── RTC.cpp │ │ ├── RTC.h │ │ ├── cas.h │ │ └── ubx.h │ ├── graphics/ │ │ ├── EInkDisplay2.cpp │ │ ├── EInkDisplay2.h │ │ ├── EInkDynamicDisplay.cpp │ │ ├── EInkDynamicDisplay.h │ │ ├── EmoteRenderer.cpp │ │ ├── EmoteRenderer.h │ │ ├── GxEPD2Multi.h │ │ ├── NomadStarLED.h │ │ ├── Panel_sdl.cpp │ │ ├── Panel_sdl.hpp │ │ ├── PointStruct.h │ │ ├── Screen.cpp │ │ ├── Screen.h │ │ ├── ScreenFonts.h │ │ ├── ScreenGlobals.cpp │ │ ├── SharedUIDisplay.cpp │ │ ├── SharedUIDisplay.h │ │ ├── TFTDisplay.cpp │ │ ├── TFTDisplay.h │ │ ├── TimeFormatters.cpp │ │ ├── TimeFormatters.h │ │ ├── VirtualKeyboard.cpp │ │ ├── VirtualKeyboard.h │ │ ├── draw/ │ │ │ ├── ClockRenderer.cpp │ │ │ ├── ClockRenderer.h │ │ │ ├── CompassRenderer.cpp │ │ │ ├── CompassRenderer.h │ │ │ ├── DebugRenderer.cpp │ │ │ ├── DebugRenderer.h │ │ │ ├── DrawRenderers.h │ │ │ ├── MenuHandler.cpp │ │ │ ├── MenuHandler.h │ │ │ ├── MessageRenderer.cpp │ │ │ ├── MessageRenderer.h │ │ │ ├── NodeListRenderer.cpp │ │ │ ├── NodeListRenderer.h │ │ │ ├── NotificationRenderer.cpp │ │ │ ├── NotificationRenderer.h │ │ │ ├── UIRenderer.cpp │ │ │ └── UIRenderer.h │ │ ├── emotes.cpp │ │ ├── emotes.h │ │ ├── fonts/ │ │ │ ├── EinkDisplayFonts.cpp │ │ │ ├── EinkDisplayFonts.h │ │ │ ├── OLEDDisplayFontsCS.cpp │ │ │ ├── OLEDDisplayFontsCS.h │ │ │ ├── OLEDDisplayFontsGR.cpp │ │ │ ├── OLEDDisplayFontsGR.h │ │ │ ├── OLEDDisplayFontsPL.cpp │ │ │ ├── OLEDDisplayFontsPL.h │ │ │ ├── OLEDDisplayFontsRU.cpp │ │ │ ├── OLEDDisplayFontsRU.h │ │ │ ├── OLEDDisplayFontsUA.cpp │ │ │ └── OLEDDisplayFontsUA.h │ │ ├── images.h │ │ ├── img/ │ │ │ ├── icon.xbm │ │ │ └── icon_small.xbm │ │ ├── niche/ │ │ │ ├── Drivers/ │ │ │ │ ├── Backlight/ │ │ │ │ │ ├── LatchingBacklight.cpp │ │ │ │ │ └── LatchingBacklight.h │ │ │ │ ├── EInk/ │ │ │ │ │ ├── DEPG0213BNS800.cpp │ │ │ │ │ ├── DEPG0213BNS800.h │ │ │ │ │ ├── DEPG0290BNS800.cpp │ │ │ │ │ ├── DEPG0290BNS800.h │ │ │ │ │ ├── E0213A367.cpp │ │ │ │ │ ├── E0213A367.h │ │ │ │ │ ├── EInk.cpp │ │ │ │ │ ├── EInk.h │ │ │ │ │ ├── GDEW0102T4.cpp │ │ │ │ │ ├── GDEW0102T4.h │ │ │ │ │ ├── GDEY0154D67.cpp │ │ │ │ │ ├── GDEY0154D67.h │ │ │ │ │ ├── GDEY0213B74.cpp │ │ │ │ │ ├── GDEY0213B74.h │ │ │ │ │ ├── HINK_E0213A289.cpp │ │ │ │ │ ├── HINK_E0213A289.h │ │ │ │ │ ├── HINK_E042A87.cpp │ │ │ │ │ ├── HINK_E042A87.h │ │ │ │ │ ├── LCMEN2R13ECC1.cpp │ │ │ │ │ ├── LCMEN2R13ECC1.h │ │ │ │ │ ├── LCMEN2R13EFC1.cpp │ │ │ │ │ ├── LCMEN2R13EFC1.h │ │ │ │ │ ├── README.md │ │ │ │ │ ├── SSD1682.cpp │ │ │ │ │ ├── SSD1682.h │ │ │ │ │ ├── SSD16XX.cpp │ │ │ │ │ ├── SSD16XX.h │ │ │ │ │ ├── UC8175.cpp │ │ │ │ │ ├── UC8175.h │ │ │ │ │ ├── ZJY122250_0213BAAMFGN.cpp │ │ │ │ │ ├── ZJY122250_0213BAAMFGN.h │ │ │ │ │ ├── ZJY128296_029EAAMFGN.cpp │ │ │ │ │ ├── ZJY128296_029EAAMFGN.h │ │ │ │ │ └── ZJY200200_0154DAAMFGN.h │ │ │ │ └── README.md │ │ │ ├── Fonts/ │ │ │ │ ├── FreeSans12pt_Win1250.h │ │ │ │ ├── FreeSans12pt_Win1251.h │ │ │ │ ├── FreeSans12pt_Win1252.h │ │ │ │ ├── FreeSans12pt_Win1253.h │ │ │ │ ├── FreeSans6pt7b.h │ │ │ │ ├── FreeSans6pt_Win1250.h │ │ │ │ ├── FreeSans6pt_Win1251.h │ │ │ │ ├── FreeSans6pt_Win1252.h │ │ │ │ ├── FreeSans6pt_Win1253.h │ │ │ │ ├── FreeSans9pt_Win1250.h │ │ │ │ ├── FreeSans9pt_Win1251.h │ │ │ │ ├── FreeSans9pt_Win1252.h │ │ │ │ ├── FreeSans9pt_Win1253.h │ │ │ │ └── README.md │ │ │ ├── InkHUD/ │ │ │ │ ├── Applet.cpp │ │ │ │ ├── Applet.h │ │ │ │ ├── AppletFont.cpp │ │ │ │ ├── AppletFont.h │ │ │ │ ├── Applets/ │ │ │ │ │ ├── Bases/ │ │ │ │ │ │ ├── Map/ │ │ │ │ │ │ │ ├── MapApplet.cpp │ │ │ │ │ │ │ └── MapApplet.h │ │ │ │ │ │ └── NodeList/ │ │ │ │ │ │ ├── NodeListApplet.cpp │ │ │ │ │ │ └── NodeListApplet.h │ │ │ │ │ ├── Examples/ │ │ │ │ │ │ ├── BasicExample/ │ │ │ │ │ │ │ ├── BasicExampleApplet.cpp │ │ │ │ │ │ │ └── BasicExampleApplet.h │ │ │ │ │ │ ├── NewMsgExample/ │ │ │ │ │ │ │ ├── NewMsgExampleApplet.cpp │ │ │ │ │ │ │ └── NewMsgExampleApplet.h │ │ │ │ │ │ └── UserAppletInputExample/ │ │ │ │ │ │ ├── UserAppletInputExample.cpp │ │ │ │ │ │ └── UserAppletInputExample.h │ │ │ │ │ ├── System/ │ │ │ │ │ │ ├── AlignStick/ │ │ │ │ │ │ │ ├── AlignStickApplet.cpp │ │ │ │ │ │ │ └── AlignStickApplet.h │ │ │ │ │ │ ├── BatteryIcon/ │ │ │ │ │ │ │ ├── BatteryIconApplet.cpp │ │ │ │ │ │ │ └── BatteryIconApplet.h │ │ │ │ │ │ ├── Keyboard/ │ │ │ │ │ │ │ ├── KeyboardApplet.cpp │ │ │ │ │ │ │ └── KeyboardApplet.h │ │ │ │ │ │ ├── Logo/ │ │ │ │ │ │ │ ├── LogoApplet.cpp │ │ │ │ │ │ │ └── LogoApplet.h │ │ │ │ │ │ ├── Menu/ │ │ │ │ │ │ │ ├── MenuAction.h │ │ │ │ │ │ │ ├── MenuApplet.cpp │ │ │ │ │ │ │ ├── MenuApplet.h │ │ │ │ │ │ │ ├── MenuItem.h │ │ │ │ │ │ │ └── MenuPage.h │ │ │ │ │ │ ├── Notification/ │ │ │ │ │ │ │ ├── Notification.h │ │ │ │ │ │ │ ├── NotificationApplet.cpp │ │ │ │ │ │ │ └── NotificationApplet.h │ │ │ │ │ │ ├── Pairing/ │ │ │ │ │ │ │ ├── PairingApplet.cpp │ │ │ │ │ │ │ └── PairingApplet.h │ │ │ │ │ │ ├── Placeholder/ │ │ │ │ │ │ │ ├── PlaceholderApplet.cpp │ │ │ │ │ │ │ └── PlaceholderApplet.h │ │ │ │ │ │ └── Tips/ │ │ │ │ │ │ ├── TipsApplet.cpp │ │ │ │ │ │ └── TipsApplet.h │ │ │ │ │ └── User/ │ │ │ │ │ ├── AllMessage/ │ │ │ │ │ │ ├── AllMessageApplet.cpp │ │ │ │ │ │ └── AllMessageApplet.h │ │ │ │ │ ├── DM/ │ │ │ │ │ │ ├── DMApplet.cpp │ │ │ │ │ │ └── DMApplet.h │ │ │ │ │ ├── FavoritesMap/ │ │ │ │ │ │ ├── FavoritesMapApplet.cpp │ │ │ │ │ │ └── FavoritesMapApplet.h │ │ │ │ │ ├── Heard/ │ │ │ │ │ │ ├── HeardApplet.cpp │ │ │ │ │ │ └── HeardApplet.h │ │ │ │ │ ├── Positions/ │ │ │ │ │ │ ├── PositionsApplet.cpp │ │ │ │ │ │ └── PositionsApplet.h │ │ │ │ │ ├── RecentsList/ │ │ │ │ │ │ ├── RecentsListApplet.cpp │ │ │ │ │ │ └── RecentsListApplet.h │ │ │ │ │ └── ThreadedMessage/ │ │ │ │ │ ├── ThreadedMessageApplet.cpp │ │ │ │ │ └── ThreadedMessageApplet.h │ │ │ │ ├── DisplayHealth.cpp │ │ │ │ ├── DisplayHealth.h │ │ │ │ ├── Events.cpp │ │ │ │ ├── Events.h │ │ │ │ ├── InkHUD.cpp │ │ │ │ ├── InkHUD.h │ │ │ │ ├── MessageStore.cpp │ │ │ │ ├── MessageStore.h │ │ │ │ ├── Persistence.cpp │ │ │ │ ├── Persistence.h │ │ │ │ ├── PlatformioConfig.ini │ │ │ │ ├── Renderer.cpp │ │ │ │ ├── Renderer.h │ │ │ │ ├── SystemApplet.h │ │ │ │ ├── Tile.cpp │ │ │ │ ├── Tile.h │ │ │ │ ├── WindowManager.cpp │ │ │ │ ├── WindowManager.h │ │ │ │ └── docs/ │ │ │ │ └── README.md │ │ │ ├── Inputs/ │ │ │ │ ├── README.md │ │ │ │ ├── TwoButton.cpp │ │ │ │ ├── TwoButton.h │ │ │ │ ├── TwoButtonExtended.cpp │ │ │ │ └── TwoButtonExtended.h │ │ │ ├── README.md │ │ │ └── Utils/ │ │ │ ├── CannedMessageStore.cpp │ │ │ ├── CannedMessageStore.h │ │ │ └── FlashData.h │ │ └── tftSetup.cpp │ ├── input/ │ │ ├── BBQ10Keyboard.cpp │ │ ├── BBQ10Keyboard.h │ │ ├── ButtonThread.cpp │ │ ├── ButtonThread.h │ │ ├── CardputerKeyboard.cpp │ │ ├── CardputerKeyboard.h │ │ ├── ExpressLRSFiveWay.cpp │ │ ├── ExpressLRSFiveWay.h │ │ ├── HackadayCommunicatorKeyboard.cpp │ │ ├── HackadayCommunicatorKeyboard.h │ │ ├── InputBroker.cpp │ │ ├── InputBroker.h │ │ ├── LinuxInput.cpp │ │ ├── LinuxInput.h │ │ ├── LinuxInputImpl.cpp │ │ ├── LinuxInputImpl.h │ │ ├── MPR121Keyboard.cpp │ │ ├── MPR121Keyboard.h │ │ ├── RotaryEncoderImpl.cpp │ │ ├── RotaryEncoderImpl.h │ │ ├── RotaryEncoderInterruptBase.cpp │ │ ├── RotaryEncoderInterruptBase.h │ │ ├── RotaryEncoderInterruptImpl1.cpp │ │ ├── RotaryEncoderInterruptImpl1.h │ │ ├── SeesawRotary.cpp │ │ ├── SeesawRotary.h │ │ ├── SerialKeyboard.cpp │ │ ├── SerialKeyboard.h │ │ ├── SerialKeyboardImpl.cpp │ │ ├── SerialKeyboardImpl.h │ │ ├── TCA8418Keyboard.cpp │ │ ├── TCA8418Keyboard.h │ │ ├── TCA8418KeyboardBase.cpp │ │ ├── TCA8418KeyboardBase.h │ │ ├── TDeckProKeyboard.cpp │ │ ├── TDeckProKeyboard.h │ │ ├── TLoraPagerKeyboard.cpp │ │ ├── TLoraPagerKeyboard.h │ │ ├── TouchScreenBase.cpp │ │ ├── TouchScreenBase.h │ │ ├── TouchScreenImpl1.cpp │ │ ├── TouchScreenImpl1.h │ │ ├── TrackballInterruptBase.cpp │ │ ├── TrackballInterruptBase.h │ │ ├── TrackballInterruptImpl1.cpp │ │ ├── TrackballInterruptImpl1.h │ │ ├── UpDownInterruptBase.cpp │ │ ├── UpDownInterruptBase.h │ │ ├── UpDownInterruptImpl1.cpp │ │ ├── UpDownInterruptImpl1.h │ │ ├── cardKbI2cImpl.cpp │ │ ├── cardKbI2cImpl.h │ │ ├── i2cButton.cpp │ │ ├── i2cButton.h │ │ ├── kbI2cBase.cpp │ │ ├── kbI2cBase.h │ │ ├── kbMatrixBase.cpp │ │ ├── kbMatrixBase.h │ │ ├── kbMatrixImpl.cpp │ │ └── kbMatrixImpl.h │ ├── main.cpp │ ├── main.h │ ├── memGet.cpp │ ├── memGet.h │ ├── mesh/ │ │ ├── Channels.cpp │ │ ├── Channels.h │ │ ├── CryptoEngine.cpp │ │ ├── CryptoEngine.h │ │ ├── Default.cpp │ │ ├── Default.h │ │ ├── FloodingRouter.cpp │ │ ├── FloodingRouter.h │ │ ├── InterfacesTemplates.cpp │ │ ├── LLCC68Interface.cpp │ │ ├── LLCC68Interface.h │ │ ├── LR1110Interface.cpp │ │ ├── LR1110Interface.h │ │ ├── LR1120Interface.cpp │ │ ├── LR1120Interface.h │ │ ├── LR1121Interface.cpp │ │ ├── LR1121Interface.h │ │ ├── LR11x0Interface.cpp │ │ ├── LR11x0Interface.h │ │ ├── LoRaFEMInterface.cpp │ │ ├── LoRaFEMInterface.h │ │ ├── MemoryPool.h │ │ ├── MeshModule.cpp │ │ ├── MeshModule.h │ │ ├── MeshPacketQueue.cpp │ │ ├── MeshPacketQueue.h │ │ ├── MeshRadio.h │ │ ├── MeshService.cpp │ │ ├── MeshService.h │ │ ├── MeshTypes.h │ │ ├── NextHopRouter.cpp │ │ ├── NextHopRouter.h │ │ ├── NodeDB.cpp │ │ ├── NodeDB.h │ │ ├── PacketCache.cpp │ │ ├── PacketCache.h │ │ ├── PacketHistory.cpp │ │ ├── PacketHistory.h │ │ ├── PhoneAPI.cpp │ │ ├── PhoneAPI.h │ │ ├── PointerQueue.h │ │ ├── ProtobufModule.cpp │ │ ├── ProtobufModule.h │ │ ├── RF95Interface.cpp │ │ ├── RF95Interface.h │ │ ├── RadioInterface.cpp │ │ ├── RadioInterface.h │ │ ├── RadioLibInterface.cpp │ │ ├── RadioLibInterface.h │ │ ├── RadioLibRF95.cpp │ │ ├── RadioLibRF95.h │ │ ├── ReliableRouter.cpp │ │ ├── ReliableRouter.h │ │ ├── Router.cpp │ │ ├── Router.h │ │ ├── STM32WLE5JCInterface.cpp │ │ ├── STM32WLE5JCInterface.h │ │ ├── SX1262Interface.cpp │ │ ├── SX1262Interface.h │ │ ├── SX1268Interface.cpp │ │ ├── SX1268Interface.h │ │ ├── SX126xInterface.cpp │ │ ├── SX126xInterface.h │ │ ├── SX1280Interface.cpp │ │ ├── SX1280Interface.h │ │ ├── SX128xInterface.cpp │ │ ├── SX128xInterface.h │ │ ├── SinglePortModule.h │ │ ├── StaticPointerQueue.h │ │ ├── StreamAPI.cpp │ │ ├── StreamAPI.h │ │ ├── Throttle.cpp │ │ ├── Throttle.h │ │ ├── TransmitHistory.cpp │ │ ├── TransmitHistory.h │ │ ├── TypeConversions.cpp │ │ ├── TypeConversions.h │ │ ├── TypedQueue.h │ │ ├── aes-ccm.cpp │ │ ├── aes-ccm.h │ │ ├── api/ │ │ │ ├── PacketAPI.cpp │ │ │ ├── PacketAPI.h │ │ │ ├── ServerAPI.cpp │ │ │ ├── ServerAPI.h │ │ │ ├── WiFiServerAPI.cpp │ │ │ ├── WiFiServerAPI.h │ │ │ ├── ethServerAPI.cpp │ │ │ └── ethServerAPI.h │ │ ├── compression/ │ │ │ ├── unishox2.cpp │ │ │ └── unishox2.h │ │ ├── eth/ │ │ │ ├── ethClient.cpp │ │ │ └── ethClient.h │ │ ├── generated/ │ │ │ ├── .clang-format │ │ │ └── meshtastic/ │ │ │ ├── admin.pb.cpp │ │ │ ├── admin.pb.h │ │ │ ├── apponly.pb.cpp │ │ │ ├── apponly.pb.h │ │ │ ├── atak.pb.cpp │ │ │ ├── atak.pb.h │ │ │ ├── cannedmessages.pb.cpp │ │ │ ├── cannedmessages.pb.h │ │ │ ├── channel.pb.cpp │ │ │ ├── channel.pb.h │ │ │ ├── clientonly.pb.cpp │ │ │ ├── clientonly.pb.h │ │ │ ├── config.pb.cpp │ │ │ ├── config.pb.h │ │ │ ├── connection_status.pb.cpp │ │ │ ├── connection_status.pb.h │ │ │ ├── device_ui.pb.cpp │ │ │ ├── device_ui.pb.h │ │ │ ├── deviceonly.pb.cpp │ │ │ ├── deviceonly.pb.h │ │ │ ├── interdevice.pb.cpp │ │ │ ├── interdevice.pb.h │ │ │ ├── localonly.pb.cpp │ │ │ ├── localonly.pb.h │ │ │ ├── mesh.pb.cpp │ │ │ ├── mesh.pb.h │ │ │ ├── module_config.pb.cpp │ │ │ ├── module_config.pb.h │ │ │ ├── mqtt.pb.cpp │ │ │ ├── mqtt.pb.h │ │ │ ├── paxcount.pb.cpp │ │ │ ├── paxcount.pb.h │ │ │ ├── portnums.pb.cpp │ │ │ ├── portnums.pb.h │ │ │ ├── powermon.pb.cpp │ │ │ ├── powermon.pb.h │ │ │ ├── remote_hardware.pb.cpp │ │ │ ├── remote_hardware.pb.h │ │ │ ├── rtttl.pb.cpp │ │ │ ├── rtttl.pb.h │ │ │ ├── storeforward.pb.cpp │ │ │ ├── storeforward.pb.h │ │ │ ├── telemetry.pb.cpp │ │ │ ├── telemetry.pb.h │ │ │ ├── xmodem.pb.cpp │ │ │ └── xmodem.pb.h │ │ ├── http/ │ │ │ ├── ContentHandler.cpp │ │ │ ├── ContentHandler.h │ │ │ ├── ContentHelper.cpp │ │ │ ├── ContentHelper.h │ │ │ ├── WebServer.cpp │ │ │ └── WebServer.h │ │ ├── mesh-pb-constants.cpp │ │ ├── mesh-pb-constants.h │ │ ├── raspihttp/ │ │ │ ├── PiWebServer.cpp │ │ │ └── PiWebServer.h │ │ ├── udp/ │ │ │ └── UdpMulticastHandler.h │ │ └── wifi/ │ │ ├── WiFiAPClient.cpp │ │ └── WiFiAPClient.h │ ├── meshUtils.cpp │ ├── meshUtils.h │ ├── modules/ │ │ ├── AdminModule.cpp │ │ ├── AdminModule.h │ │ ├── AtakPluginModule.cpp │ │ ├── AtakPluginModule.h │ │ ├── CannedMessageModule.cpp │ │ ├── CannedMessageModule.h │ │ ├── DetectionSensorModule.cpp │ │ ├── DetectionSensorModule.h │ │ ├── DropzoneModule.cpp │ │ ├── DropzoneModule.h │ │ ├── ExternalNotificationModule.cpp │ │ ├── ExternalNotificationModule.h │ │ ├── GenericThreadModule.cpp │ │ ├── GenericThreadModule.h │ │ ├── KeyVerificationModule.cpp │ │ ├── KeyVerificationModule.h │ │ ├── ModuleDev.h │ │ ├── Modules.cpp │ │ ├── Modules.h │ │ ├── NeighborInfoModule.cpp │ │ ├── NeighborInfoModule.h │ │ ├── NodeInfoModule.cpp │ │ ├── NodeInfoModule.h │ │ ├── OnScreenKeyboardModule.cpp │ │ ├── OnScreenKeyboardModule.h │ │ ├── PositionModule.cpp │ │ ├── PositionModule.h │ │ ├── PowerStressModule.cpp │ │ ├── PowerStressModule.h │ │ ├── RangeTestModule.cpp │ │ ├── RangeTestModule.h │ │ ├── RemoteHardwareModule.cpp │ │ ├── RemoteHardwareModule.h │ │ ├── ReplyBotModule.cpp │ │ ├── ReplyBotModule.h │ │ ├── ReplyModule.cpp │ │ ├── ReplyModule.h │ │ ├── RoutingModule.cpp │ │ ├── RoutingModule.h │ │ ├── SerialModule.cpp │ │ ├── SerialModule.h │ │ ├── StatusLEDModule.cpp │ │ ├── StatusLEDModule.h │ │ ├── StatusMessageModule.cpp │ │ ├── StatusMessageModule.h │ │ ├── StoreForwardModule.cpp │ │ ├── StoreForwardModule.h │ │ ├── SystemCommandsModule.cpp │ │ ├── SystemCommandsModule.h │ │ ├── Telemetry/ │ │ │ ├── AirQualityTelemetry.cpp │ │ │ ├── AirQualityTelemetry.h │ │ │ ├── BaseTelemetryModule.h │ │ │ ├── DeviceTelemetry.cpp │ │ │ ├── DeviceTelemetry.h │ │ │ ├── EnvironmentTelemetry.cpp │ │ │ ├── EnvironmentTelemetry.h │ │ │ ├── HealthTelemetry.cpp │ │ │ ├── HealthTelemetry.h │ │ │ ├── HostMetrics.cpp │ │ │ ├── HostMetrics.h │ │ │ ├── PowerTelemetry.cpp │ │ │ ├── PowerTelemetry.h │ │ │ ├── Sensor/ │ │ │ │ ├── AHT10.cpp │ │ │ │ ├── AHT10.h │ │ │ │ ├── AddI2CSensorTemplate.h │ │ │ │ ├── BH1750Sensor.cpp │ │ │ │ ├── BH1750Sensor.h │ │ │ │ ├── BME280Sensor.cpp │ │ │ │ ├── BME280Sensor.h │ │ │ │ ├── BME680Sensor.cpp │ │ │ │ ├── BME680Sensor.h │ │ │ │ ├── BMP085Sensor.cpp │ │ │ │ ├── BMP085Sensor.h │ │ │ │ ├── BMP280Sensor.cpp │ │ │ │ ├── BMP280Sensor.h │ │ │ │ ├── BMP3XXSensor.cpp │ │ │ │ ├── BMP3XXSensor.h │ │ │ │ ├── CGRadSensSensor.cpp │ │ │ │ ├── CGRadSensSensor.h │ │ │ │ ├── CurrentSensor.h │ │ │ │ ├── DFRobotGravitySensor.cpp │ │ │ │ ├── DFRobotGravitySensor.h │ │ │ │ ├── DFRobotLarkSensor.cpp │ │ │ │ ├── DFRobotLarkSensor.h │ │ │ │ ├── DPS310Sensor.cpp │ │ │ │ ├── DPS310Sensor.h │ │ │ │ ├── INA219Sensor.cpp │ │ │ │ ├── INA219Sensor.h │ │ │ │ ├── INA226Sensor.cpp │ │ │ │ ├── INA226Sensor.h │ │ │ │ ├── INA260Sensor.cpp │ │ │ │ ├── INA260Sensor.h │ │ │ │ ├── INA3221Sensor.cpp │ │ │ │ ├── INA3221Sensor.h │ │ │ │ ├── IndicatorSensor.cpp │ │ │ │ ├── IndicatorSensor.h │ │ │ │ ├── LPS22HBSensor.cpp │ │ │ │ ├── LPS22HBSensor.h │ │ │ │ ├── LTR390UVSensor.cpp │ │ │ │ ├── LTR390UVSensor.h │ │ │ │ ├── MAX17048Sensor.cpp │ │ │ │ ├── MAX17048Sensor.h │ │ │ │ ├── MAX30102Sensor.cpp │ │ │ │ ├── MAX30102Sensor.h │ │ │ │ ├── MCP9808Sensor.cpp │ │ │ │ ├── MCP9808Sensor.h │ │ │ │ ├── MLX90614Sensor.cpp │ │ │ │ ├── MLX90614Sensor.h │ │ │ │ ├── MLX90632Sensor.cpp │ │ │ │ ├── MLX90632Sensor.h │ │ │ │ ├── NAU7802Sensor.cpp │ │ │ │ ├── NAU7802Sensor.h │ │ │ │ ├── OPT3001Sensor.cpp │ │ │ │ ├── OPT3001Sensor.h │ │ │ │ ├── PCT2075Sensor.cpp │ │ │ │ ├── PCT2075Sensor.h │ │ │ │ ├── PMSA003ISensor.cpp │ │ │ │ ├── PMSA003ISensor.h │ │ │ │ ├── RAK12035Sensor.cpp │ │ │ │ ├── RAK12035Sensor.h │ │ │ │ ├── RAK9154Sensor.cpp │ │ │ │ ├── RAK9154Sensor.h │ │ │ │ ├── RCWL9620Sensor.cpp │ │ │ │ ├── RCWL9620Sensor.h │ │ │ │ ├── SCD30Sensor.cpp │ │ │ │ ├── SCD30Sensor.h │ │ │ │ ├── SCD4XSensor.cpp │ │ │ │ ├── SCD4XSensor.h │ │ │ │ ├── SEN5XSensor.cpp │ │ │ │ ├── SEN5XSensor.h │ │ │ │ ├── SFA30Sensor.cpp │ │ │ │ ├── SFA30Sensor.h │ │ │ │ ├── SHT31Sensor.cpp │ │ │ │ ├── SHT31Sensor.h │ │ │ │ ├── SHT4XSensor.cpp │ │ │ │ ├── SHT4XSensor.h │ │ │ │ ├── SHTC3Sensor.cpp │ │ │ │ ├── SHTC3Sensor.h │ │ │ │ ├── T1000xSensor.cpp │ │ │ │ ├── T1000xSensor.h │ │ │ │ ├── TSL2561Sensor.cpp │ │ │ │ ├── TSL2561Sensor.h │ │ │ │ ├── TSL2591Sensor.cpp │ │ │ │ ├── TSL2591Sensor.h │ │ │ │ ├── TelemetrySensor.cpp │ │ │ │ ├── TelemetrySensor.h │ │ │ │ ├── VEML7700Sensor.cpp │ │ │ │ ├── VEML7700Sensor.h │ │ │ │ ├── VoltageSensor.h │ │ │ │ ├── nullSensor.cpp │ │ │ │ └── nullSensor.h │ │ │ ├── UnitConversions.cpp │ │ │ └── UnitConversions.h │ │ ├── TextMessageModule.cpp │ │ ├── TextMessageModule.h │ │ ├── TraceRouteModule.cpp │ │ ├── TraceRouteModule.h │ │ ├── TrafficManagementModule.cpp │ │ ├── TrafficManagementModule.h │ │ ├── WaypointModule.cpp │ │ ├── WaypointModule.h │ │ └── esp32/ │ │ ├── AudioModule.cpp │ │ ├── AudioModule.h │ │ ├── PaxcounterModule.cpp │ │ └── PaxcounterModule.h │ ├── motion/ │ │ ├── AccelerometerThread.h │ │ ├── BMA423Sensor.cpp │ │ ├── BMA423Sensor.h │ │ ├── BMI270Sensor.cpp │ │ ├── BMI270Sensor.h │ │ ├── BMM150Sensor.cpp │ │ ├── BMM150Sensor.h │ │ ├── BMX160Sensor.cpp │ │ ├── BMX160Sensor.h │ │ ├── ICM20948Sensor.cpp │ │ ├── ICM20948Sensor.h │ │ ├── LIS3DHSensor.cpp │ │ ├── LIS3DHSensor.h │ │ ├── LSM6DS3Sensor.cpp │ │ ├── LSM6DS3Sensor.h │ │ ├── MPU6050Sensor.cpp │ │ ├── MPU6050Sensor.h │ │ ├── MotionSensor.cpp │ │ ├── MotionSensor.h │ │ ├── QMA6100PSensor.cpp │ │ ├── QMA6100PSensor.h │ │ ├── STK8XXXSensor.cpp │ │ └── STK8XXXSensor.h │ ├── mqtt/ │ │ ├── MQTT.cpp │ │ ├── MQTT.h │ │ ├── ServiceEnvelope.cpp │ │ └── ServiceEnvelope.h │ ├── network-stubs.cpp │ ├── nimble/ │ │ ├── NimbleBluetooth.cpp │ │ └── NimbleBluetooth.h │ ├── platform/ │ │ ├── esp32/ │ │ │ ├── ESP32CryptoEngine.cpp │ │ │ ├── MeshtasticOTA.cpp │ │ │ ├── MeshtasticOTA.h │ │ │ ├── SimpleAllocator.cpp │ │ │ ├── SimpleAllocator.h │ │ │ ├── architecture.h │ │ │ ├── iram-quirk.c │ │ │ └── main-esp32.cpp │ │ ├── extra_variants/ │ │ │ ├── README.md │ │ │ ├── heltec_wireless_tracker/ │ │ │ │ └── variant.cpp │ │ │ ├── t_deck_pro/ │ │ │ │ └── variant.cpp │ │ │ ├── t_lora_pager/ │ │ │ │ └── variant.cpp │ │ │ └── tbeam_displayshield/ │ │ │ └── variant.cpp │ │ ├── nrf52/ │ │ │ ├── AsyncUDP.cpp │ │ │ ├── AsyncUDP.h │ │ │ ├── BLEDfuSecure.cpp │ │ │ ├── BLEDfuSecure.h │ │ │ ├── NRF52Bluetooth.cpp │ │ │ ├── NRF52Bluetooth.h │ │ │ ├── NRF52CryptoEngine.cpp │ │ │ ├── aes-256/ │ │ │ │ ├── tiny-aes.cpp │ │ │ │ └── tiny-aes.h │ │ │ ├── alloc.cpp │ │ │ ├── architecture.h │ │ │ ├── hardfault.cpp │ │ │ ├── main-bare.cpp │ │ │ ├── main-nrf52.cpp │ │ │ ├── nrf52840_s140_v7.ld │ │ │ └── softdevice/ │ │ │ ├── ble.h │ │ │ ├── ble_err.h │ │ │ ├── ble_gap.h │ │ │ ├── ble_gatt.h │ │ │ ├── ble_gattc.h │ │ │ ├── ble_gatts.h │ │ │ ├── ble_hci.h │ │ │ ├── ble_l2cap.h │ │ │ ├── ble_ranges.h │ │ │ ├── ble_types.h │ │ │ ├── nrf52/ │ │ │ │ └── nrf_mbr.h │ │ │ ├── nrf_error.h │ │ │ ├── nrf_error_sdm.h │ │ │ ├── nrf_error_soc.h │ │ │ ├── nrf_nvic.h │ │ │ ├── nrf_sdm.h │ │ │ ├── nrf_soc.h │ │ │ └── nrf_svc.h │ │ ├── portduino/ │ │ │ ├── PortduinoGlue.cpp │ │ │ ├── PortduinoGlue.h │ │ │ ├── SimRadio.cpp │ │ │ ├── SimRadio.h │ │ │ ├── USBHal.h │ │ │ └── architecture.h │ │ ├── rp2xx0/ │ │ │ ├── architecture.h │ │ │ ├── hardware_rosc/ │ │ │ │ ├── include/ │ │ │ │ │ └── hardware/ │ │ │ │ │ └── rosc.h │ │ │ │ └── rosc.c │ │ │ ├── main-rp2xx0.cpp │ │ │ └── pico_sleep/ │ │ │ ├── include/ │ │ │ │ └── pico/ │ │ │ │ └── sleep.h │ │ │ └── sleep.c │ │ └── stm32wl/ │ │ ├── LittleFS.cpp │ │ ├── LittleFS.h │ │ ├── STM32_LittleFS.cpp │ │ ├── STM32_LittleFS.h │ │ ├── STM32_LittleFS_File.cpp │ │ ├── STM32_LittleFS_File.h │ │ ├── architecture.h │ │ ├── littlefs/ │ │ │ ├── lfs.c │ │ │ ├── lfs.h │ │ │ ├── lfs_util.c │ │ │ └── lfs_util.h │ │ └── main-stm32wl.cpp │ ├── power/ │ │ ├── PowerHAL.cpp │ │ └── PowerHAL.h │ ├── power.h │ ├── serialization/ │ │ ├── JSON.cpp │ │ ├── JSON.h │ │ ├── JSONValue.cpp │ │ ├── JSONValue.h │ │ ├── MeshPacketSerializer.cpp │ │ ├── MeshPacketSerializer.h │ │ ├── MeshPacketSerializer_nRF52.cpp │ │ ├── cobs.cpp │ │ └── cobs.h │ ├── sleep.cpp │ ├── sleep.h │ ├── target_specific.h │ ├── watchdog/ │ │ ├── watchdogThread.cpp │ │ └── watchdogThread.h │ ├── xmodem.cpp │ └── xmodem.h ├── suppressions.txt ├── test/ │ ├── TestUtil.cpp │ ├── TestUtil.h │ ├── test_admin_radio/ │ │ └── test_main.cpp │ ├── test_atak/ │ │ └── test_main.cpp │ ├── test_crypto/ │ │ └── test_main.cpp │ ├── test_default/ │ │ └── test_main.cpp │ ├── test_http_content_handler/ │ │ └── test_main.cpp │ ├── test_mesh_module/ │ │ └── test_main.cpp │ ├── test_meshpacket_serializer/ │ │ ├── ports/ │ │ │ ├── test_encrypted.cpp │ │ │ ├── test_nodeinfo.cpp │ │ │ ├── test_position.cpp │ │ │ ├── test_telemetry.cpp │ │ │ ├── test_text_message.cpp │ │ │ └── test_waypoint.cpp │ │ ├── test_helpers.h │ │ └── test_serializer.cpp │ ├── test_mqtt/ │ │ └── MQTT.cpp │ ├── test_radio/ │ │ └── test_main.cpp │ ├── test_serial/ │ │ └── SerialModule.cpp │ ├── test_traffic_management/ │ │ └── test_main.cpp │ └── test_transmit_history/ │ └── test_main.cpp ├── userPrefs.jsonc ├── variants/ │ ├── esp32/ │ │ ├── betafpv_2400_tx_micro/ │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── betafpv_900_tx_nano/ │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── chatter2/ │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── diy/ │ │ │ ├── 9m2ibr_aprs_lora_tracker/ │ │ │ │ ├── platformio.ini │ │ │ │ ├── variant.cpp │ │ │ │ └── variant.h │ │ │ ├── dr-dev/ │ │ │ │ ├── platformio.ini │ │ │ │ └── variant.h │ │ │ ├── hydra/ │ │ │ │ ├── platformio.ini │ │ │ │ └── variant.h │ │ │ ├── v1/ │ │ │ │ ├── platformio.ini │ │ │ │ └── variant.h │ │ │ └── v1_1/ │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── esp32-common.ini │ │ ├── esp32.ini │ │ ├── hackerboxes_esp32_io/ │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── heltec_v1/ │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── heltec_v2/ │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── heltec_v2.1/ │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── heltec_wireless_bridge/ │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── heltec_wsl_v2.1/ │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── m5stack_core/ │ │ │ ├── pins_arduino.h │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── m5stack_coreink/ │ │ │ ├── pins_arduino.h │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── nano-g1/ │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── nano-g1-explorer/ │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── radiomaster_900_bandit/ │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── radiomaster_900_bandit_micro/ │ │ │ └── platformio.ini │ │ ├── radiomaster_900_bandit_nano/ │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── rak11200/ │ │ │ ├── pins_arduino.h │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── station-g1/ │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── tbeam/ │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── tbeam_v07/ │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── tlora_v1/ │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── tlora_v1_3/ │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── tlora_v2/ │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── tlora_v2_1_16/ │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── tlora_v2_1_16_tcxo/ │ │ │ └── platformio.ini │ │ ├── tlora_v2_1_18/ │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── tlora_v3_3_0_tcxo/ │ │ │ └── platformio.ini │ │ ├── trackerd/ │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ └── wiphone/ │ │ ├── pins_arduino.h │ │ ├── platformio.ini │ │ └── variant.h │ ├── esp32c3/ │ │ ├── ai-c3/ │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── diy/ │ │ │ └── esp32c3_super_mini/ │ │ │ ├── pins_arduino.h │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── esp32c3.ini │ │ ├── hackerboxes_esp32c3_oled/ │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── heltec_esp32c3/ │ │ │ ├── pins_arduino.h │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── heltec_hru_3601/ │ │ │ ├── pins_arduino.h │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ └── m5stack-stamp-c3/ │ │ ├── pins_arduino.h │ │ ├── platformio.ini │ │ └── variant.h │ ├── esp32c6/ │ │ ├── esp32c6.ini │ │ ├── m5stack_unitc6l/ │ │ │ ├── pins_arduino.h │ │ │ ├── platformio.ini │ │ │ ├── variant.cpp │ │ │ └── variant.h │ │ └── tlora_c6/ │ │ ├── platformio.ini │ │ └── variant.h │ ├── esp32s2/ │ │ ├── esp32s2.ini │ │ └── nugget_s2_lora/ │ │ ├── platformio.ini │ │ └── variant.h │ ├── esp32s3/ │ │ ├── CDEBYTE_EoRa-Hub/ │ │ │ ├── pins_arduino.h │ │ │ ├── platformio.ini │ │ │ ├── rfswitch.h │ │ │ └── variant.h │ │ ├── CDEBYTE_EoRa-S3/ │ │ │ ├── pins_arduino.h │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── EBYTE_ESP32-S3/ │ │ │ ├── pins_arduino.h │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── ELECROW-ThinkNode-M2/ │ │ │ ├── pins_arduino.h │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── ELECROW-ThinkNode-M5/ │ │ │ ├── pins_arduino.h │ │ │ ├── platformio.ini │ │ │ ├── variant.cpp │ │ │ └── variant.h │ │ ├── bpi_picow_esp32_s3/ │ │ │ ├── pins_arduino.h │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── crowpanel-esp32s3-5-epaper/ │ │ │ ├── pins_arduino.h │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── diy/ │ │ │ ├── my_esp32s3_diy_eink/ │ │ │ │ ├── pins_arduino.h │ │ │ │ ├── platformio.ini │ │ │ │ └── variant.h │ │ │ ├── my_esp32s3_diy_oled/ │ │ │ │ ├── pins_arduino.h │ │ │ │ ├── platformio.ini │ │ │ │ └── variant.h │ │ │ └── t-energy-s3_e22/ │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── dreamcatcher/ │ │ │ ├── platformio.ini │ │ │ ├── rfswitch.h │ │ │ └── variant.h │ │ ├── elecrow_panel/ │ │ │ ├── pins_arduino.h │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── esp32-s3-pico/ │ │ │ ├── pins_arduino.h │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── esp32s3.ini │ │ ├── hackaday-communicator/ │ │ │ ├── pins_arduino.h │ │ │ ├── platformio.ini │ │ │ ├── variant.cpp │ │ │ └── variant.h │ │ ├── heltec_capsule_sensor_v3/ │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── heltec_sensor_hub/ │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── heltec_v3/ │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── heltec_v4/ │ │ │ ├── pins_arduino.h │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── heltec_vision_master_e213/ │ │ │ ├── einkDetect.h │ │ │ ├── nicheGraphics.h │ │ │ ├── pins_arduino.h │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── heltec_vision_master_e290/ │ │ │ ├── nicheGraphics.h │ │ │ ├── pins_arduino.h │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── heltec_vision_master_t190/ │ │ │ ├── pins_arduino.h │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── heltec_wireless_paper/ │ │ │ ├── einkDetect.h │ │ │ ├── nicheGraphics.h │ │ │ ├── pins_arduino.h │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── heltec_wireless_paper_v1/ │ │ │ ├── pins_arduino.h │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── heltec_wireless_tracker/ │ │ │ ├── pins_arduino.h │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── heltec_wireless_tracker_V1_0/ │ │ │ ├── pins_arduino.h │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── heltec_wireless_tracker_v2/ │ │ │ ├── pins_arduino.h │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── heltec_wsl_v3/ │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── icarus/ │ │ │ ├── pins_arduino.h │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── link32_s3_v1/ │ │ │ ├── pins_arduino.h │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── m5stack_cardputer_adv/ │ │ │ ├── pins_arduino.h │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── m5stack_cores3/ │ │ │ ├── pins_arduino.h │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── mesh-tab/ │ │ │ ├── pins_arduino.h │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── mini-epaper-s3/ │ │ │ ├── nicheGraphics.h │ │ │ ├── pins_arduino.h │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── nibble_esp32/ │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── nugget_s3_lora/ │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── picomputer-s3/ │ │ │ ├── pins_arduino.h │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── rak3312/ │ │ │ ├── pins_arduino.h │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── rak_wismesh_tap_v2/ │ │ │ ├── pins_arduino.h │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── seeed-sensecap-indicator/ │ │ │ ├── pins_arduino.h │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── seeed_xiao_s3/ │ │ │ ├── pins_arduino.h │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── station-g2/ │ │ │ ├── pins_arduino.h │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── t-beam-1w/ │ │ │ ├── pins_arduino.h │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── t-deck/ │ │ │ ├── pins_arduino.h │ │ │ ├── platformio.ini │ │ │ ├── variant.cpp │ │ │ └── variant.h │ │ ├── t-deck-pro/ │ │ │ ├── pins_arduino.h │ │ │ ├── platformio.ini │ │ │ ├── variant.cpp │ │ │ └── variant.h │ │ ├── t-eth-elite/ │ │ │ ├── pins_arduino.h │ │ │ ├── platformio.ini │ │ │ ├── rfswitch.h │ │ │ └── variant.h │ │ ├── t-watch-s3/ │ │ │ ├── pins_arduino.h │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── tbeam-s3-core/ │ │ │ ├── pins_arduino.h │ │ │ ├── platformio.ini │ │ │ ├── rfswitch.h │ │ │ └── variant.h │ │ ├── tlora-pager/ │ │ │ ├── pins_arduino.h │ │ │ ├── platformio.ini │ │ │ ├── rfswitch.h │ │ │ ├── variant.cpp │ │ │ └── variant.h │ │ ├── tlora_t3s3_epaper/ │ │ │ ├── nicheGraphics.h │ │ │ ├── pins_arduino.h │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── tlora_t3s3_v1/ │ │ │ ├── pins_arduino.h │ │ │ ├── platformio.ini │ │ │ ├── rfswitch.h │ │ │ └── variant.h │ │ ├── tracksenger/ │ │ │ ├── internal/ │ │ │ │ ├── pins_arduino.h │ │ │ │ └── variant.h │ │ │ ├── lcd/ │ │ │ │ ├── pins_arduino.h │ │ │ │ └── variant.h │ │ │ ├── oled/ │ │ │ │ ├── pins_arduino.h │ │ │ │ └── variant.h │ │ │ └── platformio.ini │ │ └── unphone/ │ │ ├── pins_arduino.h │ │ ├── platformio.ini │ │ ├── variant.cpp │ │ └── variant.h │ ├── native/ │ │ ├── portduino/ │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── portduino-buildroot/ │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ └── portduino.ini │ ├── nrf52840/ │ │ ├── Dongle_nRF52840-pca10059-v1/ │ │ │ ├── platformio.ini │ │ │ ├── variant.cpp │ │ │ └── variant.h │ │ ├── ELECROW-ThinkNode-M1/ │ │ │ ├── nicheGraphics.h │ │ │ ├── platformio.ini │ │ │ ├── variant.cpp │ │ │ └── variant.h │ │ ├── ELECROW-ThinkNode-M3/ │ │ │ ├── platformio.ini │ │ │ ├── rfswitch.h │ │ │ ├── variant.cpp │ │ │ └── variant.h │ │ ├── ELECROW-ThinkNode-M4/ │ │ │ ├── platformio.ini │ │ │ ├── rfswitch.h │ │ │ ├── variant.cpp │ │ │ └── variant.h │ │ ├── ELECROW-ThinkNode-M6/ │ │ │ ├── platformio.ini │ │ │ ├── variant.cpp │ │ │ └── variant.h │ │ ├── ME25LS01-4Y10TD/ │ │ │ ├── platformio.ini │ │ │ ├── rfswitch.h │ │ │ ├── variant.cpp │ │ │ └── variant.h │ │ ├── ME25LS01-4Y10TD_e-ink/ │ │ │ ├── platformio.ini │ │ │ ├── rfswitch.h │ │ │ ├── variant.cpp │ │ │ └── variant.h │ │ ├── MS24SF1/ │ │ │ ├── platformio.ini │ │ │ ├── variant.cpp │ │ │ └── variant.h │ │ ├── MakePython_nRF52840_eink/ │ │ │ ├── platformio.ini │ │ │ ├── variant.cpp │ │ │ └── variant.h │ │ ├── MakePython_nRF52840_oled/ │ │ │ ├── platformio.ini │ │ │ ├── variant.cpp │ │ │ └── variant.h │ │ ├── TWC_mesh_v4/ │ │ │ ├── platformio.ini │ │ │ ├── variant.cpp │ │ │ └── variant.h │ │ ├── canaryone/ │ │ │ ├── platformio.ini │ │ │ ├── variant.cpp │ │ │ └── variant.h │ │ ├── cpp_overrides/ │ │ │ └── lfs_util.h │ │ ├── diy/ │ │ │ ├── WashTastic/ │ │ │ │ └── platformio.ini │ │ │ ├── nrf52_promicro_diy_tcxo/ │ │ │ │ ├── custom_build_tasks.py │ │ │ │ ├── nicheGraphics.h │ │ │ │ ├── platformio.ini │ │ │ │ ├── readme.md │ │ │ │ ├── rfswitch.h │ │ │ │ ├── variant.cpp │ │ │ │ └── variant.h │ │ │ ├── seeed-xiao-nrf52840-wio-sx1262/ │ │ │ │ ├── README.md │ │ │ │ └── platformio.ini │ │ │ ├── seeed_xiao_nrf52840_e22/ │ │ │ │ └── platformio.ini │ │ │ └── xiao_ble/ │ │ │ ├── README.md │ │ │ └── platformio.ini │ │ ├── dls_Minimesh_Lite/ │ │ │ ├── platformio.ini │ │ │ ├── variant.cpp │ │ │ └── variant.h │ │ ├── feather_diy/ │ │ │ ├── platformio.ini │ │ │ ├── variant.cpp │ │ │ └── variant.h │ │ ├── gat562_mesh_trial_tracker/ │ │ │ ├── platformio.ini │ │ │ ├── variant.cpp │ │ │ └── variant.h │ │ ├── heltec_mesh_node_t114/ │ │ │ ├── platformio.ini │ │ │ ├── variant.cpp │ │ │ └── variant.h │ │ ├── heltec_mesh_node_t114-inkhud/ │ │ │ ├── custom_build_tasks.py │ │ │ ├── nicheGraphics.h │ │ │ ├── platformio.ini │ │ │ ├── variant.cpp │ │ │ └── variant.h │ │ ├── heltec_mesh_pocket/ │ │ │ ├── nicheGraphics.h │ │ │ ├── platformio.ini │ │ │ ├── variant.cpp │ │ │ └── variant.h │ │ ├── heltec_mesh_solar/ │ │ │ ├── nicheGraphics.h │ │ │ ├── platformio.ini │ │ │ ├── variant.cpp │ │ │ └── variant.h │ │ ├── meshlink/ │ │ │ ├── platformio.ini │ │ │ ├── variant.cpp │ │ │ └── variant.h │ │ ├── meshtiny/ │ │ │ ├── platformio.ini │ │ │ ├── variant.cpp │ │ │ └── variant.h │ │ ├── monteops_hw1/ │ │ │ ├── platformio.ini │ │ │ ├── variant.cpp │ │ │ └── variant.h │ │ ├── muzi_base/ │ │ │ ├── platformio.ini │ │ │ ├── rfswitch.h │ │ │ ├── variant.cpp │ │ │ └── variant.h │ │ ├── nano-g2-ultra/ │ │ │ ├── platformio.ini │ │ │ ├── variant.cpp │ │ │ └── variant.h │ │ ├── nrf52.ini │ │ ├── nrf52832.ini │ │ ├── nrf52840.ini │ │ ├── r1-neo/ │ │ │ ├── platformio.ini │ │ │ ├── variant.cpp │ │ │ └── variant.h │ │ ├── rak2560/ │ │ │ ├── platformio.ini │ │ │ ├── variant.cpp │ │ │ └── variant.h │ │ ├── rak3401_1watt/ │ │ │ ├── platformio.ini │ │ │ ├── variant.cpp │ │ │ └── variant.h │ │ ├── rak4631/ │ │ │ ├── platformio.ini │ │ │ ├── variant.cpp │ │ │ └── variant.h │ │ ├── rak4631_epaper/ │ │ │ ├── platformio.ini │ │ │ ├── variant.cpp │ │ │ └── variant.h │ │ ├── rak4631_epaper_onrxtx/ │ │ │ ├── platformio.ini │ │ │ ├── variant.cpp │ │ │ └── variant.h │ │ ├── rak4631_eth_gw/ │ │ │ ├── platformio.ini │ │ │ ├── variant.cpp │ │ │ └── variant.h │ │ ├── rak4631_nomadstar_meteor_pro/ │ │ │ ├── platformio.ini │ │ │ ├── variant.cpp │ │ │ └── variant.h │ │ ├── rak_wismeshtag/ │ │ │ ├── platformio.ini │ │ │ ├── variant.cpp │ │ │ └── variant.h │ │ ├── rak_wismeshtap/ │ │ │ ├── platformio.ini │ │ │ ├── variant.cpp │ │ │ └── variant.h │ │ ├── seeed_solar_node/ │ │ │ ├── platformio.ini │ │ │ ├── variant.cpp │ │ │ └── variant.h │ │ ├── seeed_wio_tracker_L1/ │ │ │ ├── platformio.ini │ │ │ ├── variant.cpp │ │ │ └── variant.h │ │ ├── seeed_wio_tracker_L1_eink/ │ │ │ ├── nicheGraphics.h │ │ │ ├── platformio.ini │ │ │ ├── variant.cpp │ │ │ └── variant.h │ │ ├── seeed_xiao_nrf52840_kit/ │ │ │ ├── platformio.ini │ │ │ ├── variant.cpp │ │ │ └── variant.h │ │ ├── t-echo/ │ │ │ ├── nicheGraphics.h │ │ │ ├── platformio.ini │ │ │ ├── variant.cpp │ │ │ └── variant.h │ │ ├── t-echo-lite/ │ │ │ ├── platformio.ini │ │ │ ├── variant.cpp │ │ │ └── variant.h │ │ ├── t-echo-plus/ │ │ │ ├── nicheGraphics.h │ │ │ ├── platformio.ini │ │ │ ├── variant.cpp │ │ │ └── variant.h │ │ ├── tracker-t1000-e/ │ │ │ ├── platformio.ini │ │ │ ├── rfswitch.h │ │ │ ├── variant.cpp │ │ │ └── variant.h │ │ ├── wio-sdk-wm1110/ │ │ │ ├── platformio.ini │ │ │ ├── rfswitch.h │ │ │ ├── variant.cpp │ │ │ └── variant.h │ │ ├── wio-t1000-s/ │ │ │ ├── platformio.ini │ │ │ ├── rfswitch.h │ │ │ ├── variant.cpp │ │ │ └── variant.h │ │ └── wio-tracker-wm1110/ │ │ ├── platformio.ini │ │ ├── rfswitch.h │ │ ├── variant.cpp │ │ └── variant.h │ ├── rp2040/ │ │ ├── challenger_2040_lora/ │ │ │ ├── pins_arduino.h │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── ec_catsniffer/ │ │ │ ├── platformio.ini │ │ │ ├── variant.cpp │ │ │ └── variant.h │ │ ├── feather_rp2040_rfm95/ │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── nibble_rp2040/ │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── rak11310/ │ │ │ ├── pins_arduino.h │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── rp2040-lora/ │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── rp2040.ini │ │ ├── rpipico/ │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── rpipico-slowclock/ │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ ├── rpipicow/ │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ └── senselora_rp2040/ │ │ ├── pins_arduino.h │ │ ├── platformio.ini │ │ └── variant.h │ ├── rp2350/ │ │ ├── rp2350.ini │ │ ├── rpipico2/ │ │ │ ├── platformio.ini │ │ │ └── variant.h │ │ └── rpipico2w/ │ │ ├── platformio.ini │ │ └── variant.h │ └── stm32/ │ ├── CDEBYTE_E77-MBL/ │ │ ├── platformio.ini │ │ ├── rfswitch.h │ │ └── variant.h │ ├── milesight_gs301/ │ │ ├── platformio.ini │ │ ├── rfswitch.h │ │ ├── variant.cpp │ │ └── variant.h │ ├── rak3172/ │ │ ├── platformio.ini │ │ ├── rfswitch.h │ │ └── variant.h │ ├── russell/ │ │ ├── platformio.ini │ │ ├── rfswitch.h │ │ └── variant.h │ ├── stm32.ini │ └── wio-e5/ │ ├── platformio.ini │ ├── rfswitch.h │ └── variant.h └── version.properties ================================================ FILE CONTENTS ================================================ ================================================ FILE: .clusterfuzzlite/Dockerfile ================================================ # This container is used to build Meshtastic with the libraries required by the fuzzer. # ClusterFuzzLite starts the container, runs the build.sh script, and then exits. # As this is not a long running service, health-checks are not required. ClusterFuzzLite # also only works if the user remains unchanged from the base image (it expects to run # as root). # trunk-ignore-all(trivy/DS026): No healthcheck is needed for this builder container # trunk-ignore-all(checkov/CKV_DOCKER_2): No healthcheck is needed for this builder container # trunk-ignore-all(checkov/CKV_DOCKER_3): We must run as root for this container # trunk-ignore-all(trivy/DS002): We must run as root for this container # trunk-ignore-all(checkov/CKV_DOCKER_8): We must run as root for this container # trunk-ignore-all(hadolint/DL3002): We must run as root for this container FROM gcr.io/oss-fuzz-base/base-builder:v1 ENV PIP_ROOT_USER_ACTION=ignore # trunk-ignore(hadolint/DL3008): apt packages are not pinned. # trunk-ignore(terrascan/AC_DOCKER_0002): apt packages are not pinned. RUN apt-get update && apt-get install --no-install-recommends -y \ cmake git zip libgpiod-dev libbluetooth-dev libi2c-dev \ libunistring-dev libmicrohttpd-dev libgnutls28-dev libgcrypt20-dev \ libusb-1.0-0-dev libssl-dev pkg-config libsqlite3-dev libsdl2-dev && \ apt-get clean && rm -rf /var/lib/apt/lists/* && \ pip install --no-cache-dir -U \ platformio==6.1.16 \ grpcio-tools==1.68.1 \ meshtastic==2.5.9 # Ugly hack to avoid clang detecting a conflict between the math "log" function and the "log" function in framework-portduino/cores/portduino/logging.h RUN sed -i -e 's/__MATHCALL_VEC (log,, (_Mdouble_ __x));//' /usr/include/x86_64-linux-gnu/bits/mathcalls.h # A few dependencies are too old on the base-builder image. More recent versions are built from source. WORKDIR $SRC RUN git config --global advice.detachedHead false && \ git clone --depth 1 --branch 0.8.0 https://github.com/jbeder/yaml-cpp.git && \ git clone --depth 1 --branch v2.3.3 https://github.com/babelouest/orcania.git && \ git clone --depth 1 --branch v1.4.20 https://github.com/babelouest/yder.git && \ git clone --depth 1 --branch v2.7.15 https://github.com/babelouest/ulfius.git COPY ./.clusterfuzzlite/build.sh $SRC/ WORKDIR $SRC/firmware COPY . $SRC/firmware/ # https://docs.platformio.org/en/latest/envvars.html ENV PLATFORMIO_CORE_DIR=$SRC/pio/core \ PLATFORMIO_LIBDEPS_DIR=$SRC/pio/libdeps \ PLATFORMIO_PACKAGES_DIR=$SRC/pio/packages \ PLATFORMIO_SETTING_ENABLE_CACHE=No \ PIO_ENV=buildroot RUN platformio pkg install --environment $PIO_ENV ================================================ FILE: .clusterfuzzlite/README.md ================================================ # ClusterFuzzLite for Meshtastic This directory contains the fuzzer implementation for Meshtastic using the ClusterFuzzLite framework. See the [ClusterFuzzLite documentation](https://google.github.io/clusterfuzzlite/) for more details. ## Running locally ClusterFuzzLite uses the OSS-Fuzz toolchain. To build the fuzzer manually, first grab a copy of OSS-Fuzz. ```shell git clone https://github.com/google/oss-fuzz.git cd oss-fuzz ``` To build the fuzzer, run: ```shell python3 infra/helper.py build_image --external $PATH_TO_MESHTASTIC_FIRMWARE_DIRECTORY python3 infra/helper.py build_fuzzers --external $PATH_TO_MESHTASTIC_FIRMWARE_DIRECTORY --sanitizer address ``` To run the fuzzer, run: ```shell python3 infra/helper.py run_fuzzer --external --corpus-dir= $PATH_TO_MESHTASTIC_FIRMWARE_DIRECTORY router_fuzzer ``` More background on these commands can be found in the [ClusterFuzzLite documentation](https://google.github.io/clusterfuzzlite/build-integration/#testing-locally). ## router_fuzzer.cpp This fuzzer submits MeshPacket protos to the `Router::enqueueReceivedMessage` method. It takes the binary data from the fuzzer and decodes that data to a MeshPacket using nanopb. A few fields in the MeshPacket are modified by the fuzzer. - If the `to` field is 0, it will be replaced with the NodeID of the running node. - If the `from` field is 0, it will be replaced with the NodeID of the running node. - If the `id` field is 0, it will be replaced with an incrementing counter value. - If the `pki_encrypted` field is true, the `public_key` field will be populated with the first admin key. The `router_fuzzer_seed_corpus.py` file contains a list of MeshPackets. It is run from inside build.sh and writes the binary MeshPacket protos to files. These files are use used by the fuzzer as its initial seed data, helping the fuzzer to start off with a few known inputs. ### Interpreting a fuzzer crash If the fuzzer crashes, it'll write the input bytes used for the test case to a file and notify about the location of that file. The contents of the file are a binary serialized MeshPacket protobuf. The following snippet of Python code can be used to parse the file into a human readable form. ```python from meshtastic.protobuf import mesh_pb2 mesh_pb2.MeshPacket.FromString(open("crash-XXXX-file", "rb").read()) ``` Consider adding any such crash results to the `router_fuzzer_seed_corpus.py` file to ensure there a isn't a future regression for that crash test case. ================================================ FILE: .clusterfuzzlite/build.sh ================================================ #!/bin/bash -eu # Build Meshtastic and a few needed dependencies using clang++ # and the OSS-Fuzz required build flags. env cd "$SRC" NPROC=$(nproc || echo 1) LDFLAGS=-lpthread cmake -S "$SRC/yaml-cpp" -B "$WORK/yaml-cpp/$SANITIZER" \ -DBUILD_SHARED_LIBS=OFF cmake --build "$WORK/yaml-cpp/$SANITIZER" -j "$NPROC" cmake --install "$WORK/yaml-cpp/$SANITIZER" --prefix /usr cmake -S "$SRC/orcania" -B "$WORK/orcania/$SANITIZER" \ -DBUILD_STATIC=ON cmake --build "$WORK/orcania/$SANITIZER" -j "$NPROC" cmake --install "$WORK/orcania/$SANITIZER" --prefix /usr cmake -S "$SRC/yder" -B "$WORK/yder/$SANITIZER" \ -DBUILD_STATIC=ON -DWITH_JOURNALD=OFF cmake --build "$WORK/yder/$SANITIZER" -j "$NPROC" cmake --install "$WORK/yder/$SANITIZER" --prefix /usr cmake -S "$SRC/ulfius" -B "$WORK/ulfius/$SANITIZER" \ -DBUILD_STATIC=ON -DWITH_JANSSON=OFF -DWITH_CURL=OFF -DWITH_WEBSOCKET=OFF cmake --build "$WORK/ulfius/$SANITIZER" -j "$NPROC" cmake --install "$WORK/ulfius/$SANITIZER" --prefix /usr cd "$SRC/firmware" PLATFORMIO_EXTRA_SCRIPTS=$(echo -e "pre:.clusterfuzzlite/platformio-clusterfuzzlite-pre.py\npost:.clusterfuzzlite/platformio-clusterfuzzlite-post.py") STATIC_LIBS=$(pkg-config --libs --static libulfius openssl libgpiod yaml-cpp bluez --silence-errors) export PLATFORMIO_EXTRA_SCRIPTS export STATIC_LIBS export PLATFORMIO_WORKSPACE_DIR="$WORK/pio/$SANITIZER" export TARGET_CC=$CC export TARGET_CXX=$CXX export TARGET_LD=$CXX export TARGET_AR=llvm-ar export TARGET_AS=llvm-as export TARGET_OBJCOPY=llvm-objcopy export TARGET_RANLIB=llvm-ranlib mkdir -p "$OUT/lib" cp .clusterfuzzlite/*_fuzzer.options "$OUT/" for f in .clusterfuzzlite/*_fuzzer.cpp; do fuzzer=$(basename "$f" .cpp) cp -f "$f" src/fuzzer.cpp pio run -vvv --environment "$PIO_ENV" program="$PLATFORMIO_WORKSPACE_DIR/build/$PIO_ENV/meshtasticd" cp "$program" "$OUT/$fuzzer" # Copy shared libraries used by the fuzzer. read -d '' -ra shared_libs < <(ldd "$program" | sed -n 's/[^=]\+=> \([^ ]\+\).*/\1/p') || true cp -f "${shared_libs[@]}" "$OUT/lib/" # Build the initial fuzzer seed corpus. corpus_name="${fuzzer}_seed_corpus" corpus_generator="$PWD/.clusterfuzzlite/${corpus_name}.py" if [[ -f $corpus_generator ]]; then mkdir "$corpus_name" pushd "$corpus_name" python3 "$corpus_generator" popd zip -D "$OUT/${corpus_name}.zip" "$corpus_name"/* fi done ================================================ FILE: .clusterfuzzlite/platformio-clusterfuzzlite-post.py ================================================ """PlatformIO build script (post: runs after other Meshtastic scripts).""" import os import shlex from SCons.Script import DefaultEnvironment env = DefaultEnvironment() # Remove any static libraries from the LIBS environment. Static libraries are # handled in platformio-clusterfuzzlite-pre.py. static_libs = set(lib[2:] for lib in shlex.split(os.getenv("STATIC_LIBS"))) env.Replace( LIBS=[ lib for lib in env["LIBS"] if not (isinstance(lib, str) and lib in static_libs) ], ) # FrameworkArduino/portduino/main.cpp contains the "main" function the binary. # The fuzzing framework also provides a "main" function and needs to be run # before Meshtastic is started. We rename the "main" function for Meshtastic to # "portduino_main" here so that it can be called inside the fuzzer. env.AddPostAction( "$BUILD_DIR/FrameworkArduino/portduino/main.cpp.o", env.VerboseAction( " ".join( [ "$OBJCOPY", "--redefine-sym=main=portduino_main", "$BUILD_DIR/FrameworkArduino/portduino/main.cpp.o", ] ), "Renaming main symbol to portduino_main", ), ) ================================================ FILE: .clusterfuzzlite/platformio-clusterfuzzlite-pre.py ================================================ """PlatformIO build script (pre: runs before other Meshtastic scripts). ClusterFuzzLite executes in a different container from the build. During the build, attempt to link statically to as many dependencies as possible. For dependencies that do not have static libraries, the shared library files are copied to the output directory by the build.sh script. """ import glob import os import shlex from SCons.Script import DefaultEnvironment, Literal env = DefaultEnvironment() cxxflags = shlex.split(os.getenv("CXXFLAGS")) sanitizer_flags = shlex.split(os.getenv("SANITIZER_FLAGS")) lib_fuzzing_engine = shlex.split(os.getenv("LIB_FUZZING_ENGINE")) statics = glob.glob("/usr/lib/lib*.a") + glob.glob("/usr/lib/*/lib*.a") no_static = set(("-ldl",)) def replaceStatic(lib): """Replace -l with the static .a file for the library.""" if not lib.startswith("-l") or lib in no_static: return lib static_name = f"/lib{lib[2:]}.a" static = [s for s in statics if s.endswith(static_name)] if len(static) == 1: return static[0] return lib # Setup the environment for building with Clang and the OSS-Fuzz required build flags. env.Append( CFLAGS=os.getenv("CFLAGS"), CXXFLAGS=cxxflags, LIBSOURCE_DIRS=["/usr/lib/x86_64-linux-gnu"], LINKFLAGS=cxxflags + sanitizer_flags + lib_fuzzing_engine + ["-stdlib=libc++", "-std=c++17"], _LIBFLAGS=[replaceStatic(s) for s in shlex.split(os.getenv("STATIC_LIBS"))] + [ "/usr/lib/x86_64-linux-gnu/libunistring.a", # Needs to be at the end. # Find the shared libraries in a subdirectory named lib # within the same directory as the binary. Literal("-Wl,-rpath,$ORIGIN/lib"), "-Wl,-z,origin", ], ) ================================================ FILE: .clusterfuzzlite/project.yaml ================================================ language: c++ ================================================ FILE: .clusterfuzzlite/router_fuzzer.cpp ================================================ // Fuzzer implementation that sends MeshPackets to Router::enqueueReceivedMessage. #include #include #include #include #include #include #include #include "PortduinoGPIO.h" #include "PortduinoGlue.h" #include "PowerFSM.h" #include "mesh/MeshTypes.h" #include "mesh/NodeDB.h" #include "mesh/Router.h" #include "mesh/TypeConversions.h" #include "mesh/mesh-pb-constants.h" namespace { constexpr uint32_t nodeId = 0x12345678; // Set to true when lateInitVariant finishes. Used to ensure lateInitVariant was called during startup. bool hasBeenConfigured = false; // These are used to block the Arduino loop() function until a fuzzer input is ready. This is // an optimization that prevents a sleep from happening before the loop is run. The Arduino loop // function calls loopCanSleep() before sleeping. loopCanSleep is implemented here in the fuzzer // and blocks until runLoopOnce() is called to signal for the loop to run. bool fuzzerRunning = false; // Set to true once LLVMFuzzerTestOneInput has started running. bool loopCanRun = true; // The main Arduino loop() can run when this is true. bool loopIsWaiting = false; // The main Arduino loop() is waiting to be signaled to run. bool loopShouldExit = false; // Indicates that the main Arduino thread should exit by throwing ShouldExitException. std::mutex loopLock; std::condition_variable loopCV; std::thread meshtasticThread; // This exception is thrown when the portuino main thread should exit. class ShouldExitException : public std::runtime_error { public: using std::runtime_error::runtime_error; }; // Start the loop for one test case and wait till the loop has completed. This ensures fuzz // test cases do not overlap with one another. This helps the fuzzer attribute a crash to the // single, currently running, test case. void runLoopOnce() { realHardware = true; // Avoids delay(100) within portduino/main.cpp std::unique_lock lck(loopLock); fuzzerRunning = true; loopCanRun = true; loopCV.notify_one(); loopCV.wait(lck, [] { return !loopCanRun && loopIsWaiting; }); } } // namespace // Called in the main Arduino loop function to determine if the loop can delay/sleep before running again. // We use this as a way to block the loop from sleeping and to start the loop function immediately when a // fuzzer input is ready. bool loopCanSleep() { std::unique_lock lck(loopLock); loopIsWaiting = true; loopCV.notify_one(); loopCV.wait(lck, [] { return loopCanRun || loopShouldExit; }); loopIsWaiting = false; if (loopShouldExit) throw ShouldExitException("exit"); if (!fuzzerRunning) return true; // The loop can sleep before the fuzzer starts. loopCanRun = false; // Only run the loop once before waiting again. return false; } // Called just prior to starting Meshtastic. Allows for setting config values before startup. void lateInitVariant() { portduino_config.logoutputlevel = level_error; channelFile.channels[0] = meshtastic_Channel{ .has_settings = true, .settings = meshtastic_ChannelSettings{ .psk = {.size = 1, .bytes = {/*defaultpskIndex=*/1}}, .name = "LongFast", .uplink_enabled = true, .has_module_settings = true, .module_settings = {.position_precision = 16}, }, .role = meshtastic_Channel_Role_PRIMARY, }; config.security.admin_key[0] = { .size = 32, .bytes = {0xcd, 0xc0, 0xb4, 0x3c, 0x53, 0x24, 0xdf, 0x13, 0xca, 0x5a, 0xa6, 0x0c, 0x0d, 0xec, 0x85, 0x5a, 0x4c, 0xf6, 0x1a, 0x96, 0x04, 0x1a, 0x3e, 0xfc, 0xbb, 0x8e, 0x33, 0x71, 0xe5, 0xfc, 0xff, 0x3c}, }; config.security.admin_key_count = 1; config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_US; moduleConfig.has_mqtt = true; moduleConfig.mqtt = meshtastic_ModuleConfig_MQTTConfig{ .enabled = true, .proxy_to_client_enabled = true, }; moduleConfig.has_store_forward = true; moduleConfig.store_forward = meshtastic_ModuleConfig_StoreForwardConfig{ .enabled = true, .history_return_max = 4, .history_return_window = 600, .is_server = true, }; meshtastic_Position fixedGPS = meshtastic_Position{ .has_latitude_i = true, .latitude_i = static_cast(1 * 1e7), .has_longitude_i = true, .longitude_i = static_cast(3 * 1e7), .has_altitude = true, .altitude = 64, .location_source = meshtastic_Position_LocSource_LOC_MANUAL, }; nodeDB->setLocalPosition(fixedGPS); config.has_position = true; config.position.fixed_position = true; meshtastic_NodeInfoLite *info = nodeDB->getMeshNode(nodeDB->getNodeNum()); info->has_position = true; info->position = TypeConversions::ConvertToPositionLite(fixedGPS); hasBeenConfigured = true; } extern "C" { int portduino_main(int argc, char **argv); // Renamed "main" function from Meshtastic binary. // Start Meshtastic in a thread and wait till it has reached the ON state. int LLVMFuzzerInitialize(int *argc, char ***argv) { portduino_config.maxtophone = 5; meshtasticThread = std::thread([program = *argv[0]]() { char nodeIdStr[12]; strcpy(nodeIdStr, std::to_string(nodeId).c_str()); int argc = 7; char *argv[] = {program, "-d", "/tmp/meshtastic", "-h", nodeIdStr, "-p", "0", nullptr}; try { portduino_main(argc, argv); } catch (const ShouldExitException &) { } }); std::atexit([] { { const std::lock_guard lck(loopLock); loopShouldExit = true; loopCV.notify_one(); } meshtasticThread.join(); }); // Wait for startup. for (int i = 1; i < 20; ++i) { if (powerFSM.getState() == &stateON) { assert(hasBeenConfigured); assert(router); assert(nodeDB); return 0; } std::this_thread::sleep_for(std::chrono::seconds(1)); } return 1; } // This is the main entrypoint for the fuzzer (the fuzz target). The fuzzer will provide an array of bytes to be // interpreted by this method. To keep things simple, the bytes are interpreted as a binary serialized MeshPacket // proto. Any crashes discovered by the fuzzer will be written to a file. Unserialize that file to print the MeshPacket // that caused the failure. // // This guide provides best practices for writing a fuzzer target. // https://github.com/google/fuzzing/blob/master/docs/good-fuzz-target.md int LLVMFuzzerTestOneInput(const uint8_t *data, size_t length) { meshtastic_MeshPacket p = meshtastic_MeshPacket_init_default; pb_istream_t stream = pb_istream_from_buffer(data, length); // Ignore any inputs that fail to decode or have fields set that are not transmitted over LoRa. if (!pb_decode(&stream, &meshtastic_MeshPacket_msg, &p) || p.rx_time || p.rx_snr || p.priority || p.rx_rssi || p.delayed || p.public_key.size || p.next_hop || p.relay_node || p.tx_after) return -1; // Reject: The input will not be added to the corpus. if (p.which_payload_variant == meshtastic_MeshPacket_decoded_tag) { meshtastic_Data d; stream = pb_istream_from_buffer(p.decoded.payload.bytes, p.decoded.payload.size); if (!pb_decode(&stream, &meshtastic_Data_msg, &d)) return -1; // Reject: The input will not be added to the corpus. } // Provide default values for a few fields so the fuzzer doesn't need to guess them. if (p.from == 0) p.from = nodeDB->getNodeNum(); if (p.to == 0) p.to = nodeDB->getNodeNum(); static uint32_t packetId = 0; if (p.id == 0) p.id == ++packetId; if (p.pki_encrypted && config.security.admin_key_count) memcpy(&p.public_key, &config.security.admin_key[0], sizeof(p.public_key)); router->enqueueReceivedMessage(packetPool.allocCopy(p)); runLoopOnce(); return 0; // Accept: The input may be added to the corpus. } } ================================================ FILE: .clusterfuzzlite/router_fuzzer.options ================================================ [libfuzzer] max_len=256 ================================================ FILE: .clusterfuzzlite/router_fuzzer_seed_corpus.py ================================================ """Generate an initial set of MeshPackets. The fuzzer uses these MeshPackets as an initial seed of test candidates. It's also good to add any previously discovered crash test cases to this list to avoid future regressions. If left unset, the following values will be automatically set by the fuzzer. - to: automatically set to the running node's NodeID - from: automatically set to the running node's NodeID - id: automatically set to the value of an incrementing counter Additionally, if `pki_encrypted` is populated in the packet, the first admin key will be copied into the `public_key` field. """ import base64 from meshtastic import BROADCAST_NUM from meshtastic.protobuf import ( admin_pb2, atak_pb2, mesh_pb2, portnums_pb2, telemetry_pb2, ) def From(node: int = 9): """Return a dict suitable for **kwargs for populating the 'from' field. 'from' is a reserved keyword in Python. It can't be used directly as an argument to the MeshPacket constructor. Rather **From() can be used as the final argument to provide the from node as a **kwarg. Defaults to 9 if no value is provided. """ return {"from": node} packets = ( ( "position", mesh_pb2.MeshPacket( decoded=mesh_pb2.Data( portnum=portnums_pb2.PortNum.POSITION_APP, payload=mesh_pb2.Position( latitude_i=int(1 * 1e7), longitude_i=int(2 * 1e7), altitude=5, precision_bits=32, ).SerializeToString(), ), to=BROADCAST_NUM, **From(), ), ), ( "telemetry", mesh_pb2.MeshPacket( decoded=mesh_pb2.Data( portnum=portnums_pb2.PortNum.TELEMETRY_APP, payload=telemetry_pb2.Telemetry( time=1736192207, device_metrics=telemetry_pb2.DeviceMetrics( battery_level=101, channel_utilization=8, air_util_tx=2, uptime_seconds=42, ), ).SerializeToString(), ), to=BROADCAST_NUM, **From(), ), ), ( "text", mesh_pb2.MeshPacket( decoded=mesh_pb2.Data( portnum=portnums_pb2.PortNum.TEXT_MESSAGE_APP, payload=b"Hello world", ), to=BROADCAST_NUM, **From(), ), ), ( "user", mesh_pb2.MeshPacket( decoded=mesh_pb2.Data( portnum=portnums_pb2.PortNum.NODEINFO_APP, payload=mesh_pb2.User( id="!00000009", long_name="Node 9", short_name="N9", macaddr=b"\x00\x00\x00\x00\x00\x09", hw_model=mesh_pb2.HardwareModel.RAK4631, public_key=base64.b64decode( "L0ih/6F41itofdE8mYyHk1SdfOJ/QRM1KQ+pO4vEEjQ=" ), ).SerializeToString(), ), **From(), ), ), ( "traceroute", mesh_pb2.MeshPacket( decoded=mesh_pb2.Data( portnum=portnums_pb2.PortNum.TRACEROUTE_APP, payload=mesh_pb2.RouteDiscovery( route=[10], ).SerializeToString(), ), **From(), ), ), ( "routing", mesh_pb2.MeshPacket( decoded=mesh_pb2.Data( portnum=portnums_pb2.PortNum.ROUTING_APP, payload=mesh_pb2.Routing( error_reason=mesh_pb2.Routing.NO_RESPONSE, ).SerializeToString(), ), **From(), ), ), ( "admin", mesh_pb2.MeshPacket( decoded=mesh_pb2.Data( portnum=portnums_pb2.PortNum.ADMIN_APP, payload=admin_pb2.AdminMessage( get_owner_request=True, ).SerializeToString(), ), pki_encrypted=True, **From(), ), ), ( "atak", mesh_pb2.MeshPacket( decoded=mesh_pb2.Data( portnum=portnums_pb2.PortNum.ATAK_PLUGIN, payload=atak_pb2.TAKPacket( is_compressed=True, # Note, the strings are not valid for a compressed message, but will # give the fuzzer a starting point. contact=atak_pb2.Contact( callsign="callsign", device_callsign="device_callsign" ), chat=atak_pb2.GeoChat( message="message", to="to", to_callsign="to_callsign" ), ).SerializeToString(), ), **From(), ), ), ) for name, packet in packets: with open(f"{name}.MeshPacket", "wb") as f: f.write(packet.SerializeToString()) ================================================ FILE: .devcontainer/99-platformio-udev.rules ================================================ # 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. ##################################################################################### # # INSTALLATION # # Please visit > https://docs.platformio.org/en/latest/core/installation/udev-rules.html # ##################################################################################### # # Boards # # CP210X USB UART ATTRS{idVendor}=="10c4", ATTRS{idProduct}=="ea[67][013]", MODE:="0666", ENV{ID_MM_DEVICE_IGNORE}="1", ENV{ID_MM_PORT_IGNORE}="1" ATTRS{idVendor}=="10c4", ATTRS{idProduct}=="80a9", MODE:="0666", ENV{ID_MM_DEVICE_IGNORE}="1", ENV{ID_MM_PORT_IGNORE}="1" # FT231XS USB UART ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6015", MODE:="0666", ENV{ID_MM_DEVICE_IGNORE}="1", ENV{ID_MM_PORT_IGNORE}="1" # Prolific Technology, Inc. PL2303 Serial Port ATTRS{idVendor}=="067b", ATTRS{idProduct}=="2303", MODE:="0666", ENV{ID_MM_DEVICE_IGNORE}="1", ENV{ID_MM_PORT_IGNORE}="1" # QinHeng Electronics HL-340 USB-Serial adapter ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="7523", MODE:="0666", ENV{ID_MM_DEVICE_IGNORE}="1", ENV{ID_MM_PORT_IGNORE}="1" # QinHeng Electronics CH343 USB-Serial adapter ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="55d3", MODE:="0666", ENV{ID_MM_DEVICE_IGNORE}="1", ENV{ID_MM_PORT_IGNORE}="1" # QinHeng Electronics CH9102 USB-Serial adapter ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="55d4", MODE:="0666", ENV{ID_MM_DEVICE_IGNORE}="1", ENV{ID_MM_PORT_IGNORE}="1" # Arduino boards ATTRS{idVendor}=="2341", ATTRS{idProduct}=="[08][023]*", MODE:="0666", ENV{ID_MM_DEVICE_IGNORE}="1", ENV{ID_MM_PORT_IGNORE}="1" ATTRS{idVendor}=="2a03", ATTRS{idProduct}=="[08][02]*", MODE:="0666", ENV{ID_MM_DEVICE_IGNORE}="1", ENV{ID_MM_PORT_IGNORE}="1" # Arduino SAM-BA ATTRS{idVendor}=="03eb", ATTRS{idProduct}=="6124", MODE:="0666", ENV{ID_MM_DEVICE_IGNORE}="1", ENV{MTP_NO_PROBE}="1" # Digistump boards ATTRS{idVendor}=="16d0", ATTRS{idProduct}=="0753", MODE:="0666", ENV{ID_MM_DEVICE_IGNORE}="1", ENV{ID_MM_PORT_IGNORE}="1" # Maple with DFU ATTRS{idVendor}=="1eaf", ATTRS{idProduct}=="000[34]", MODE:="0666", ENV{ID_MM_DEVICE_IGNORE}="1", ENV{ID_MM_PORT_IGNORE}="1" # USBtiny ATTRS{idProduct}=="0c9f", ATTRS{idVendor}=="1781", MODE:="0666", ENV{ID_MM_DEVICE_IGNORE}="1", ENV{ID_MM_PORT_IGNORE}="1" # USBasp V2.0 ATTRS{idVendor}=="16c0", ATTRS{idProduct}=="05dc", MODE:="0666", ENV{ID_MM_DEVICE_IGNORE}="1", ENV{ID_MM_PORT_IGNORE}="1" # Teensy boards ATTRS{idVendor}=="16c0", ATTRS{idProduct}=="04[789B]?", ENV{ID_MM_DEVICE_IGNORE}="1", ENV{ID_MM_PORT_IGNORE}="1" ATTRS{idVendor}=="16c0", ATTRS{idProduct}=="04[789A]?", ENV{MTP_NO_PROBE}="1" SUBSYSTEMS=="usb", ATTRS{idVendor}=="16c0", ATTRS{idProduct}=="04[789ABCD]?", MODE:="0666" KERNEL=="ttyACM*", ATTRS{idVendor}=="16c0", ATTRS{idProduct}=="04[789B]?", MODE:="0666" # TI Stellaris Launchpad ATTRS{idVendor}=="1cbe", ATTRS{idProduct}=="00fd", MODE="0666", ENV{ID_MM_DEVICE_IGNORE}="1", ENV{ID_MM_PORT_IGNORE}="1" # TI MSP430 Launchpad ATTRS{idVendor}=="0451", ATTRS{idProduct}=="f432", MODE="0666", ENV{ID_MM_DEVICE_IGNORE}="1", ENV{ID_MM_PORT_IGNORE}="1" # GD32V DFU Bootloader ATTRS{idVendor}=="28e9", ATTRS{idProduct}=="0189", MODE="0666", ENV{ID_MM_DEVICE_IGNORE}="1", ENV{ID_MM_PORT_IGNORE}="1" # FireBeetle-ESP32 ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="7522", MODE="0666", ENV{ID_MM_DEVICE_IGNORE}="1", ENV{ID_MM_PORT_IGNORE}="1" # Wio Terminal ATTRS{idVendor}=="2886", ATTRS{idProduct}=="[08]02d", MODE="0666", ENV{ID_MM_DEVICE_IGNORE}="1", ENV{ID_MM_PORT_IGNORE}="1" # Raspberry Pi Pico ATTRS{idVendor}=="2e8a", ATTRS{idProduct}=="[01]*", MODE:="0666", ENV{ID_MM_DEVICE_IGNORE}="1", ENV{ID_MM_PORT_IGNORE}="1" # AIR32F103 ATTRS{idVendor}=="0d28", ATTRS{idProduct}=="0204", MODE="0666", ENV{ID_MM_DEVICE_IGNORE}="1", ENV{ID_MM_PORT_IGNORE}="1" # STM32 virtual COM port ATTRS{idVendor}=="0483", ATTRS{idProduct}=="5740", MODE="0666", ENV{ID_MM_DEVICE_IGNORE}="1", ENV{ID_MM_PORT_IGNORE}="1" # # Debuggers # # Black Magic Probe SUBSYSTEM=="tty", ATTRS{interface}=="Black Magic GDB Server", MODE="0666", ENV{ID_MM_DEVICE_IGNORE}="1", ENV{ID_MM_PORT_IGNORE}="1" SUBSYSTEM=="tty", ATTRS{interface}=="Black Magic UART Port", MODE="0666", ENV{ID_MM_DEVICE_IGNORE}="1", ENV{ID_MM_PORT_IGNORE}="1" # opendous and estick ATTRS{idVendor}=="03eb", ATTRS{idProduct}=="204f", MODE="0666", ENV{ID_MM_DEVICE_IGNORE}="1", ENV{ID_MM_PORT_IGNORE}="1" # Original FT232/FT245/FT2232/FT232H/FT4232 ATTRS{idVendor}=="0403", ATTRS{idProduct}=="60[01][104]", MODE="0666", ENV{ID_MM_DEVICE_IGNORE}="1", ENV{ID_MM_PORT_IGNORE}="1" # DISTORTEC JTAG-lock-pick Tiny 2 ATTRS{idVendor}=="0403", ATTRS{idProduct}=="8220", MODE="0666", ENV{ID_MM_DEVICE_IGNORE}="1", ENV{ID_MM_PORT_IGNORE}="1" # TUMPA, TUMPA Lite ATTRS{idVendor}=="0403", ATTRS{idProduct}=="8a9[89]", MODE="0666", ENV{ID_MM_DEVICE_IGNORE}="1", ENV{ID_MM_PORT_IGNORE}="1" # XDS100v2 ATTRS{idVendor}=="0403", ATTRS{idProduct}=="a6d0", MODE="0666", ENV{ID_MM_DEVICE_IGNORE}="1", ENV{ID_MM_PORT_IGNORE}="1" # Xverve Signalyzer Tool (DT-USB-ST), Signalyzer LITE (DT-USB-SLITE) ATTRS{idVendor}=="0403", ATTRS{idProduct}=="bca[01]", MODE="0666", ENV{ID_MM_DEVICE_IGNORE}="1", ENV{ID_MM_PORT_IGNORE}="1" # TI/Luminary Stellaris Evaluation Board FTDI (several) ATTRS{idVendor}=="0403", ATTRS{idProduct}=="bcd[9a]", MODE="0666", ENV{ID_MM_DEVICE_IGNORE}="1", ENV{ID_MM_PORT_IGNORE}="1" # egnite Turtelizer 2 ATTRS{idVendor}=="0403", ATTRS{idProduct}=="bdc8", MODE="0666", ENV{ID_MM_DEVICE_IGNORE}="1", ENV{ID_MM_PORT_IGNORE}="1" # Section5 ICEbear ATTRS{idVendor}=="0403", ATTRS{idProduct}=="c14[01]", MODE="0666", ENV{ID_MM_DEVICE_IGNORE}="1", ENV{ID_MM_PORT_IGNORE}="1" # Amontec JTAGkey and JTAGkey-tiny ATTRS{idVendor}=="0403", ATTRS{idProduct}=="cff8", MODE="0666", ENV{ID_MM_DEVICE_IGNORE}="1", ENV{ID_MM_PORT_IGNORE}="1" # TI ICDI ATTRS{idVendor}=="0451", ATTRS{idProduct}=="c32a", MODE="0666", ENV{ID_MM_DEVICE_IGNORE}="1", ENV{ID_MM_PORT_IGNORE}="1" # STLink probes ATTRS{idVendor}=="0483", MODE="0666", ENV{ID_MM_DEVICE_IGNORE}="1", ENV{ID_MM_PORT_IGNORE}="1" # Hilscher NXHX Boards ATTRS{idVendor}=="0640", ATTRS{idProduct}=="0028", MODE="0666", ENV{ID_MM_DEVICE_IGNORE}="1", ENV{ID_MM_PORT_IGNORE}="1" # Hitex probes ATTRS{idVendor}=="0640", MODE="0666", ENV{ID_MM_DEVICE_IGNORE}="1", ENV{ID_MM_PORT_IGNORE}="1" # Altera USB Blaster ATTRS{idVendor}=="09fb", ATTRS{idProduct}=="6001", MODE="0666", ENV{ID_MM_DEVICE_IGNORE}="1", ENV{ID_MM_PORT_IGNORE}="1" # Amontec JTAGkey-HiSpeed ATTRS{idVendor}=="0fbb", ATTRS{idProduct}=="1000", MODE="0666", ENV{ID_MM_DEVICE_IGNORE}="1", ENV{ID_MM_PORT_IGNORE}="1" # SEGGER J-Link ATTRS{idVendor}=="1366", MODE="0666", ENV{ID_MM_DEVICE_IGNORE}="1", ENV{ID_MM_PORT_IGNORE}="1" # Raisonance RLink ATTRS{idVendor}=="138e", ATTRS{idProduct}=="9000", MODE="0666", ENV{ID_MM_DEVICE_IGNORE}="1", ENV{ID_MM_PORT_IGNORE}="1" # Debug Board for Neo1973 ATTRS{idVendor}=="1457", ATTRS{idProduct}=="5118", MODE="0666", ENV{ID_MM_DEVICE_IGNORE}="1", ENV{ID_MM_PORT_IGNORE}="1" # Olimex probes ATTRS{idVendor}=="15ba", MODE="0666", ENV{ID_MM_DEVICE_IGNORE}="1", ENV{ID_MM_PORT_IGNORE}="1" # USBprog with OpenOCD firmware ATTRS{idVendor}=="1781", ATTRS{idProduct}=="0c63", MODE="0666", ENV{ID_MM_DEVICE_IGNORE}="1", ENV{ID_MM_PORT_IGNORE}="1" # TI/Luminary Stellaris In-Circuit Debug Interface (ICDI) Board ATTRS{idVendor}=="1cbe", ATTRS{idProduct}=="00fd", MODE="0666", ENV{ID_MM_DEVICE_IGNORE}="1", ENV{ID_MM_PORT_IGNORE}="1" # Marvell Sheevaplug ATTRS{idVendor}=="9e88", ATTRS{idProduct}=="9e8f", MODE="0666", ENV{ID_MM_DEVICE_IGNORE}="1", ENV{ID_MM_PORT_IGNORE}="1" # Keil Software, Inc. ULink ATTRS{idVendor}=="c251", ATTRS{idProduct}=="2710", MODE="0666", ENV{ID_MM_DEVICE_IGNORE}="1", ENV{ID_MM_PORT_IGNORE}="1" # CMSIS-DAP compatible adapters ATTRS{product}=="*CMSIS-DAP*", MODE="0666", ENV{ID_MM_DEVICE_IGNORE}="1", ENV{ID_MM_PORT_IGNORE}="1" # Atmel AVR Dragon ATTRS{idVendor}=="03eb", ATTRS{idProduct}=="2107", MODE="0666", ENV{ID_MM_DEVICE_IGNORE}="1", ENV{ID_MM_PORT_IGNORE}="1" # Espressif USB JTAG/serial debug unit ATTRS{idVendor}=="303a", ATTRS{idProduct}=="1001", MODE="0666", ENV{ID_MM_DEVICE_IGNORE}="1", ENV{ID_MM_PORT_IGNORE}="1" # Zephyr framework USB CDC-ACM ATTRS{idVendor}=="2fe3", ATTRS{idProduct}=="0100", MODE="0666", ENV{ID_MM_DEVICE_IGNORE}="1", ENV{ID_MM_PORT_IGNORE}="1" ================================================ FILE: .devcontainer/Dockerfile ================================================ # trunk-ignore-all(terrascan/AC_DOCKER_0002): Known terrascan issue # trunk-ignore-all(hadolint/DL3008): Do not pin apt package versions # trunk-ignore-all(hadolint/DL3013): Do not pin pip package versions FROM mcr.microsoft.com/devcontainers/cpp:2-debian-13 USER root RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ && apt-get -y install --no-install-recommends \ ca-certificates \ g++ \ git \ libbluetooth-dev \ libgpiod-dev \ liborcania-dev \ libssl-dev \ libulfius-dev \ libyaml-cpp-dev \ pipx \ pkg-config \ python3 \ python3-pip \ python3-venv \ python3-wheel \ wget \ zip \ usbutils \ hwdata \ gpg \ gnupg2 \ libusb-1.0-0-dev \ libuv1-dev \ libi2c-dev \ libxcb-xkb-dev \ libxkbcommon-dev \ libinput-dev \ && apt-get clean && rm -rf /var/lib/apt/lists/* RUN pipx install platformio COPY 99-platformio-udev.rules /etc/udev/rules.d/99-platformio-udev.rules USER vscode HEALTHCHECK NONE ================================================ FILE: .devcontainer/devcontainer.json ================================================ // For format details, see https://aka.ms/devcontainer.json. For config options, see the // README at: https://github.com/devcontainers/templates/tree/main/src/cpp { "name": "Meshtastic Firmware Dev", "build": { "dockerfile": "Dockerfile" }, "features": { "ghcr.io/devcontainers/features/python:1": { "installTools": true, "version": "3.14" } }, "customizations": { "vscode": { "extensions": [ "ms-vscode.cpptools", "platformio.platformio-ide", "Trunk.io" ], "unwantedRecommendations": ["ms-azuretools.vscode-docker"], "settings": { "extensions.ignoreRecommendations": true } } }, // Use 'forwardPorts' to make a list of ports inside the container available locally. "forwardPorts": [4403], // Use "--device=" to make a local device available inside the container. // "runArgs": ["--device=/dev/ttyACM0"], // Run commands to prepare the container for use "postCreateCommand": ".devcontainer/setup.sh" } ================================================ FILE: .devcontainer/setup.sh ================================================ #!/usr/bin/env sh git submodule update --init pip install --no-cache-dir setuptools pipx install esptool ================================================ FILE: .envrc ================================================ use nix ================================================ FILE: .gitattributes ================================================ * text=auto eol=lf *.cmd text eol=crlf *.bat text eol=crlf *.ps1 text eol=crlf *.{sh,[sS][hH]} text eol=lf ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms open_collective: meshtastic ================================================ FILE: .github/ISSUE_TEMPLATE/Bug Report.yml ================================================ name: Bug Report description: File a bug report title: "[Bug]: " labels: [bug, triage] body: - type: markdown attributes: value: | Thanks for taking the time to fill out this bug report! - type: dropdown id: category attributes: label: Category description: How would you catagorize this issue? multiple: true options: - Hardware Compatibility - BLE - Serial - WiFi - Other validations: required: true - type: dropdown id: hardware attributes: label: Hardware description: What hardware are you encountering this issue on? multiple: true options: - Not Applicable - T-Beam - T-Beam S3 - T-Beam 0.7 - T-Lora v1 - T-Lora v1.3 - T-Lora v2 1.6 - T-Deck - T-Echo - T-Watch - Rak4631 - Rak11200 - Rak11310 - Heltec v1 - Heltec v2 - Heltec v2.1 - Heltec V3 - Heltec Wireless Paper - Heltec Wireless Tracker - Heltec Mesh Node T114 - Heltec Vision Master E213 - Heltec Vision Master E290 - Heltec Vision Master T190 - Nano G1 - Nano G1 Explorer - Nano G2 Ultra - Raspberry Pi Pico (W) - Relay v1 - Relay v2 - Seeed Wio Tracker 1110 - Seeed Card Tracker T1000-E - Station G1 - Station G2 - unPhone - CanaryOne - Chatter - Linux Native - DIY - Other validations: required: true - type: checkboxes id: mui attributes: label: Is this bug report about any UI component firmware like InkHUD or Meshtatic UI (MUI)? options: - label: Meshtastic UI aka MUI colorTFT - label: InkHUD ePaper - label: OLED slide UI on any display - type: input id: version attributes: label: Firmware Version description: This can be found on the device's screen or via one of the apps. placeholder: x.x.x.yyyyyyy validations: required: true - type: textarea id: body attributes: label: Description description: Please provide details on what steps you performed for this to happen. validations: required: true - type: textarea id: logs attributes: label: Relevant log output description: If you have any log output to help in diagnosing your bug, please provide it here. render: Shell validations: required: false ================================================ FILE: .github/ISSUE_TEMPLATE/New Board.yml ================================================ name: New Board description: Request us to support new hardware title: "[Board]: " labels: [enhancement, triage] body: - type: markdown attributes: value: | Thanks for requesting a new board, this will not gurantee that we will support it, but will be on our radar. - type: dropdown id: soc attributes: label: SOC description: What SOC does your board have? multiple: true options: - NRF52 - ESP32 - Other validations: required: true - type: input id: lora attributes: label: Lora IC description: What LoRa IC does the board have? validations: required: true - type: input id: link attributes: label: Product Link description: Where can we find this product? validations: required: true - type: textarea id: body attributes: label: Description description: Please provide any further details you think we may need. validations: required: true ================================================ FILE: .github/ISSUE_TEMPLATE/feature.yml ================================================ name: Feature Request description: Request a new feature title: "[Feature Request]: " labels: [enhancement] body: - type: markdown attributes: value: | Thanks for your request this will not gurantee that we will implement it, but it will be reviewed. - type: dropdown id: soc attributes: label: Platform description: What device platform will support your feature? multiple: true options: - NRF52 - ESP32 - RP2040 - Linux Native - Cross-Platform - other validations: required: true - type: textarea id: body attributes: label: Description description: Please provide details about your enhancement. validations: required: true ================================================ FILE: .github/actionlint.yaml ================================================ # Configuration related to self-hosted runner. self-hosted-runner: # Labels of self-hosted runner in array of strings. labels: - arctastic - test-runner ================================================ FILE: .github/actions/build-variant/action.yml ================================================ name: Setup Build Variant Composite Action description: Variant build actions for Meshtastic Platform IO steps inputs: board: description: The board to build for required: true github_token: description: GitHub token required: true build-script-path: description: Path to the build script required: true remove-debug-flags: description: A space separated list of files to remove debug flags from required: false default: "" ota-firmware-source: description: The OTA firmware file to pull required: false default: "" ota-firmware-target: description: The target path to store the OTA firmware file required: false default: "" artifact-paths: description: A newline separated list of paths to store as artifacts required: false default: "" # include-web-ui: # description: Include the web UI in the build # required: false # default: "false" arch: description: Processor arch name required: true default: esp32 runs: using: composite steps: - name: Build base id: base uses: ./.github/actions/setup-base # - name: Get web ui version # if: inputs.include-web-ui == 'true' # id: webver # shell: bash # run: | # echo "ver=$(cat bin/web.version)" >> $GITHUB_OUTPUT # - name: Pull web ui # if: inputs.include-web-ui == 'true' # uses: dsaltares/fetch-gh-release-asset@master # with: # repo: meshtastic/web # file: build.tar # target: build.tar # token: ${{ inputs.github_token }} # version: tags/v${{ steps.webver.outputs.ver }} # - name: Unpack web ui # if: inputs.include-web-ui == 'true' # shell: bash # run: | # tar -xf build.tar -C data/static # rm build.tar - name: Remove debug flags for release shell: bash if: inputs.remove-debug-flags != '' run: | for INI_FILE in ${{ inputs.remove-debug-flags }}; do sed -i '/DDEBUG_HEAP/d' ${INI_FILE} done - name: PlatformIO ${{ inputs.arch }} download cache uses: actions/cache@v5 with: path: ~/.platformio/.cache key: pio-cache-${{ inputs.arch }}-${{ hashFiles('.github/actions/**', '**.ini') }} - name: Build ${{ inputs.board }} shell: bash run: ${{ inputs.build-script-path }} ${{ inputs.board }} - name: Pull OTA Firmware if: inputs.ota-firmware-source != '' && inputs.ota-firmware-target != '' uses: dsaltares/fetch-gh-release-asset@master with: repo: meshtastic/firmware-ota file: ${{ inputs.ota-firmware-source }} target: ${{ inputs.ota-firmware-target }} token: ${{ inputs.github_token }} - name: Get release version string shell: bash run: echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT id: version - name: Store binaries as an artifact uses: actions/upload-artifact@v7 with: name: firmware-${{ inputs.arch }}-${{ inputs.board }}-${{ steps.version.outputs.long }} overwrite: true path: | ${{ inputs.artifact-paths }} ================================================ FILE: .github/actions/setup-base/action.yml ================================================ name: Setup Build Base Composite Action description: Base build actions for Meshtastic Platform IO steps runs: using: composite steps: - name: Checkout code uses: actions/checkout@v6 with: submodules: recursive ref: ${{github.event.pull_request.head.ref}} repository: ${{github.event.pull_request.head.repo.full_name}} - name: Install dependencies shell: bash run: | sudo apt-get -y update --fix-missing sudo apt-get install -y cppcheck libbluetooth-dev libgpiod-dev libyaml-cpp-dev lsb-release - name: Setup Python uses: actions/setup-python@v6 with: python-version: 3.x cache: pip cache-dependency-path: | .github/actions/** **.ini - name: Upgrade python tools shell: bash run: | python -m pip install --upgrade pip pip install -U --no-build-isolation --no-cache-dir "setuptools<72" pip install -U platformio adafruit-nrfutil --no-build-isolation pip install -U poetry --no-build-isolation pip install -U meshtastic --pre --no-build-isolation - name: Upgrade platformio shell: bash run: | pio upgrade ================================================ FILE: .github/actions/setup-native/action.yml ================================================ name: Setup native build description: Install libraries needed for building the Native/Portduino build runs: using: composite steps: - name: Setup base id: base uses: ./.github/actions/setup-base - name: Install libs needed for native build shell: bash run: | sudo apt-get install -y libbluetooth-dev libgpiod-dev libyaml-cpp-dev openssl libssl-dev libulfius-dev liborcania-dev libusb-1.0-0-dev libi2c-dev libuv1-dev ================================================ FILE: .github/copilot-instructions.md ================================================ # Meshtastic Firmware - Copilot Instructions This document provides context and guidelines for AI assistants working with the Meshtastic firmware codebase. ## Project Overview Meshtastic is an open-source LoRa mesh networking project for long-range, low-power communication without relying on internet or cellular infrastructure. The firmware enables text messaging, location sharing, and telemetry over a decentralized mesh network. ### Supported Hardware Platforms - **ESP32** (ESP32, ESP32-S3, ESP32-C3) - Most common platform - **nRF52** (nRF52840, nRF52833) - Low power Nordic chips - **RP2040/RP2350** - Raspberry Pi Pico variants - **STM32WL** - STM32 with integrated LoRa - **Linux/Portduino** - Native Linux builds (Raspberry Pi, etc.) ### Supported Radio Chips - **SX1262/SX1268** - Sub-GHz LoRa (868/915 MHz regions) - **SX1280** - 2.4 GHz LoRa - **LR1110/LR1120/LR1121** - Wideband radios (sub-GHz and 2.4 GHz capable, but not simultaneously) - **RF95** - Legacy RFM95 modules - **LLCC68** - Low-cost LoRa ### MQTT Integration MQTT provides a bridge between Meshtastic mesh networks and the internet, enabling nodes with network connectivity to share messages with remote meshes or external services. #### Key Components - **`src/mqtt/MQTT.cpp`** - Main MQTT client singleton, handles connection and message routing - **`src/mqtt/ServiceEnvelope.cpp`** - Protobuf wrapper for mesh packets sent over MQTT - **`moduleConfig.mqtt`** - MQTT module configuration #### MQTT Topic Structure Messages are published/subscribed using a hierarchical topic format: ``` {root}/{channel_id}/{gateway_id} ``` - `root` - Configurable prefix (default: `msh`) - `channel_id` - Channel name/identifier - `gateway_id` - Node ID of the publishing gateway #### Configuration Defaults (from `Default.h`) ```cpp #define default_mqtt_address "mqtt.meshtastic.org" #define default_mqtt_username "meshdev" #define default_mqtt_password "large4cats" #define default_mqtt_root "msh" #define default_mqtt_encryption_enabled true #define default_mqtt_tls_enabled false ``` #### Key Concepts - **Uplink** - Mesh packets sent TO the MQTT broker (controlled by `uplink_enabled` per channel) - **Downlink** - MQTT messages received and injected INTO the mesh (controlled by `downlink_enabled` per channel) - **Encryption** - When `encryption_enabled` is true, only encrypted packets are sent; plaintext JSON is disabled - **ServiceEnvelope** - Protobuf wrapper containing packet + channel_id + gateway_id for routing - **JSON Support** - Optional JSON encoding for integration with external systems (disabled on nRF52 by default) #### PKI Messages PKI (Public Key Infrastructure) messages have special handling: - Accepted on a special "PKI" channel - Allow encrypted DMs between nodes that discovered each other on downlink-enabled channels ## Project Structure ``` firmware/ ├── src/ # Main source code │ ├── main.cpp # Application entry point │ ├── mesh/ # Core mesh networking │ │ ├── NodeDB.* # Node database management │ │ ├── Router.* # Packet routing │ │ ├── Channels.* # Channel management │ │ ├── *Interface.* # Radio interface implementations │ │ └── generated/ # Protobuf generated code │ ├── modules/ # Feature modules (Position, Telemetry, etc.) │ ├── gps/ # GPS handling │ ├── graphics/ # Display drivers and UI │ ├── platform/ # Platform-specific code │ ├── input/ # Input device handling │ └── concurrency/ # Threading utilities ├── variants/ # Hardware variant definitions │ ├── esp32/ # ESP32 variants │ ├── esp32s3/ # ESP32-S3 variants │ ├── nrf52/ # nRF52 variants │ └── rp2xxx/ # RP2040/RP2350 variants ├── protobufs/ # Protocol buffer definitions ├── boards/ # Custom PlatformIO board definitions └── bin/ # Build and utility scripts ``` ## Coding Conventions ### General Style - Follow existing code style - run `trunk fmt` before commits - Prefer `LOG_DEBUG`, `LOG_INFO`, `LOG_WARN`, `LOG_ERROR` for logging - Use `assert()` for invariants that should never fail ### Naming Conventions - Classes: `PascalCase` (e.g., `PositionModule`, `NodeDB`) - Functions/Methods: `camelCase` (e.g., `sendOurPosition`, `getNodeNum`) - Constants/Defines: `UPPER_SNAKE_CASE` (e.g., `MAX_INTERVAL`, `ONE_DAY`) - Member variables: `camelCase` (e.g., `lastGpsSend`, `nodeDB`) - Config defines: `USERPREFS_*` for user-configurable options ### Key Patterns #### Module System Modules inherit from `MeshModule` or `ProtobufModule` and implement: - `handleReceivedProtobuf()` - Process incoming packets - `allocReply()` - Generate response packets - `runOnce()` - Periodic task execution (returns next run interval in ms) ```cpp class MyModule : public ProtobufModule { protected: virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_MyMessage *msg) override; virtual int32_t runOnce() override; }; ``` #### Configuration Access - `config.*` - Device configuration (LoRa, position, power, etc.) - `moduleConfig.*` - Module-specific configuration - `channels.*` - Channel configuration and management #### Default Values Use the `Default` class helpers in `src/mesh/Default.h`: - `Default::getConfiguredOrDefaultMs(configured, default)` - Returns ms, using default if configured is 0 - `Default::getConfiguredOrMinimumValue(configured, min)` - Enforces minimum values - `Default::getConfiguredOrDefaultMsScaled(configured, default, numNodes)` - Scales based on network size #### Thread Safety - Use `concurrency::Lock` for mutex protection - Radio SPI access uses `SPILock` - Prefer `OSThread` for background tasks ### Hardware Variants Each hardware variant has: - `variant.h` - Pin definitions and hardware capabilities - `platformio.ini` - Build configuration - Optional: `pins_arduino.h`, `rfswitch.h` Key defines in variant.h: ```cpp #define USE_SX1262 // Radio chip selection #define HAS_GPS 1 // Hardware capabilities #define LORA_CS 36 // Pin assignments #define SX126X_DIO1 14 // Radio-specific pins ``` ### Protobuf Messages - Defined in `protobufs/meshtastic/*.proto` - Generated code in `src/mesh/generated/` - Regenerate with `bin/regen-protos.sh` - Message types prefixed with `meshtastic_` ### Conditional Compilation ```cpp #if !MESHTASTIC_EXCLUDE_GPS // Feature exclusion #ifdef ARCH_ESP32 // Architecture-specific #if defined(USE_SX1262) // Radio-specific #ifdef HAS_SCREEN // Hardware capability #if USERPREFS_EVENT_MODE // User preferences ``` ## Build System Uses **PlatformIO** with custom scripts: - `bin/platformio-pre.py` - Pre-build script - `bin/platformio-custom.py` - Custom build logic Build commands: ```bash pio run -e tbeam # Build specific target pio run -e tbeam -t upload # Build and upload pio run -e native # Build native/Linux version ``` ## Common Tasks ### Adding a New Module 1. Create `src/modules/MyModule.cpp` and `.h` 2. Inherit from appropriate base class 3. Register in `src/modules/Modules.cpp` 4. Add protobuf messages if needed in `protobufs/` ### Adding a New Hardware Variant 1. Create directory under `variants///` 2. Add `variant.h` with pin definitions 3. Add `platformio.ini` with build config 4. Reference common configs with `extends` ### Modifying Configuration Defaults - Check `src/mesh/Default.h` for default value defines - Check `src/mesh/NodeDB.cpp` for initialization logic - Consider `isDefaultChannel()` checks for public channel restrictions ## Important Considerations ### Traffic Management The mesh network has limited bandwidth. When modifying broadcast intervals: - Respect minimum intervals on default/public channels - Use `Default::getConfiguredOrMinimumValue()` to enforce minimums - Consider `numOnlineNodes` scaling for congestion control ### Power Management Many devices are battery-powered: - Use `IF_ROUTER(routerVal, normalVal)` for role-based defaults - Check `config.power.is_power_saving` for power-saving modes - Implement proper `sleep()` methods in radio interfaces ### Channel Security - `channels.isDefaultChannel(index)` - Check if using default/public settings - Default channels get stricter rate limits to prevent abuse - Private channels may have relaxed limits ## GitHub Actions CI/CD The project uses GitHub Actions extensively for CI/CD. Key workflows are in `.github/workflows/`: ### Core CI Workflows - **`main_matrix.yml`** - Main CI pipeline, runs on push to `master`/`develop` and PRs - Uses `bin/generate_ci_matrix.py` to dynamically generate build targets - Builds all supported hardware variants - PRs build a subset (`--level pr`) for faster feedback - **`trunk_check.yml`** - Code quality checks on PRs - Runs Trunk.io for linting and formatting - Must pass before merge - **`tests.yml`** - End-to-end and hardware tests - Runs daily on schedule - Includes native tests and hardware-in-the-loop testing - **`test_native.yml`** - Native platform unit tests - Runs `pio test -e native` ### Release Workflows - **`release_channels.yml`** - Triggered on GitHub release publish - Builds Docker images - Packages for PPA (Ubuntu), OBS (openSUSE), and COPR (Fedora) - Handles Alpha/Beta/Stable release channels - **`nightly.yml`** - Nightly builds from develop branch - **`docker_build.yml`** / **`docker_manifest.yml`** - Docker image builds ### Build Matrix Generation The CI uses `bin/generate_ci_matrix.py` to dynamically select which targets to build: ```bash # Generate full build matrix ./bin/generate_ci_matrix.py all # Generate PR-level matrix (subset for faster builds) ./bin/generate_ci_matrix.py all --level pr ``` Variants can specify their support level in `platformio.ini`: - `custom_meshtastic_support_level = 1` - Actively supported, built on every PR - `custom_meshtastic_support_level = 2` - Supported, built on merge to main branches - `board_level = extra` - Extra builds, only on full releases ### Running Workflows Locally Most workflows can be triggered manually via `workflow_dispatch` for testing. ## Testing - Unit tests in `test/` directory - Run with `pio test -e native` - Use `bin/test-simulator.sh` for simulation testing ## Resources - [Documentation](https://meshtastic.org/docs/) ================================================ FILE: .github/pull_request_template.md ================================================ ## 🙏 Thank you for sending in a pull request, here's some tips to get started! ### ❌ (Please delete all these tips and replace them with your text) ❌ - Before starting on some new big chunk of code, it it is optional but highly recommended to open an issue first to say "Hey, I think this idea X should be implemented and I'm starting work on it. My general plan is Y, any feedback is appreciated." This will allow other devs to potentially save you time by not accidentally duplicating work etc... - Please do not check in files that don't have real changes - Please do not reformat lines that you didn't have to change the code on - We recommend using the [Visual Studio Code](https://platformio.org/install/ide?install=vscode) editor along with the ['Trunk Check' extension](https://marketplace.visualstudio.com/items?itemName=trunk.io) (In beta for windows, WSL2 for the Linux version), because it automatically follows our indentation rules and its auto reformatting will not cause spurious changes to lines. - If your PR fixes a bug, mention "fixes #bugnum" somewhere in your pull request description. - If your other co-developers have comments on your PR please tweak as needed. - Please also enable "Allow edits by maintainers". - Please do not submit untested code. - If you do not have the affected hardware to test your code changes adequately against regressions, please indicate this, so that contributors and community members can help test your changes. - If your PR gets accepted you can request a "Contributor" role in the Meshtastic Discord ## 🤝 Attestations - [ ] I have tested that my proposed changes behave as described. - [ ] I have tested that my proposed changes do not cause any obvious regressions on the following devices: - [ ] Heltec (Lora32) V3 - [ ] LilyGo T-Deck - [ ] LilyGo T-Beam - [ ] RAK WisBlock 4631 - [ ] Seeed Studio T-1000E tracker card - [ ] Other (please specify below) ================================================ FILE: .github/workflows/build_debian_src.yml ================================================ name: Build Debian Source Package on: workflow_call: secrets: PPA_GPG_PRIVATE_KEY: required: false inputs: series: description: Ubuntu/Debian series to target required: true type: string build_location: description: Location where build will execute required: true type: string permissions: contents: write packages: write jobs: build-debian-src: runs-on: ubuntu-24.04 steps: - name: Checkout code uses: actions/checkout@v6 with: submodules: recursive path: meshtasticd ref: ${{github.event.pull_request.head.ref}} repository: ${{github.event.pull_request.head.repo.full_name}} - name: Install deps shell: bash working-directory: meshtasticd run: | sudo apt-get update -y --fix-missing sudo apt-get install -y software-properties-common build-essential devscripts equivs sudo add-apt-repository ppa:meshtastic/build-tools -y sudo apt-get update -y --fix-missing sudo mk-build-deps --install --remove --tool='apt-get -o Debug::pkgProblemResolver=yes --no-install-recommends --yes' debian/control - name: Import GPG key uses: crazy-max/ghaction-import-gpg@v7 with: gpg_private_key: ${{ secrets.PPA_GPG_PRIVATE_KEY }} id: gpg - name: Get release version string working-directory: meshtasticd run: | echo "deb=$(./bin/buildinfo.py deb)" >> $GITHUB_OUTPUT env: BUILD_LOCATION: ${{ inputs.build_location }} id: version - name: Fetch libdeps, package debian source working-directory: meshtasticd run: debian/ci_pack_sdeb.sh env: SERIES: ${{ inputs.series }} GPG_KEY_ID: ${{ steps.gpg.outputs.keyid }} PKG_VERSION: ${{ steps.version.outputs.deb }} - name: Store binaries as an artifact uses: actions/upload-artifact@v7 with: name: firmware-debian-${{ steps.version.outputs.deb }}~${{ inputs.series }}-src overwrite: true path: | meshtasticd_${{ steps.version.outputs.deb }}* ================================================ FILE: .github/workflows/build_firmware.yml ================================================ name: Build on: workflow_call: inputs: version: required: true type: string platform: required: true type: string pio_env: required: true type: string permissions: read-all jobs: pio-build: name: build-${{ inputs.platform }} # Use 'arctastic' self-hosted runner pool when building in the main repo runs-on: ${{ github.repository_owner == 'meshtastic' && 'arctastic' || 'ubuntu-latest' }} outputs: artifact-id: ${{ steps.upload-firmware.outputs.artifact-id }} steps: - uses: actions/checkout@v6 with: submodules: recursive ref: ${{github.event.pull_request.head.ref}} repository: ${{github.event.pull_request.head.repo.full_name}} - name: Build ${{ inputs.platform }} id: build uses: meshtastic/gh-action-firmware@main with: pio_platform: ${{ inputs.platform }} pio_env: ${{ inputs.pio_env }} pio_target: build - name: ESP32 - Download Unified OTA firmware # Currently only esp32 and esp32s3 use the unified ota if: inputs.platform == 'esp32' || inputs.platform == 'esp32s3' id: dl-ota-unified env: PIO_PLATFORM: ${{ inputs.platform }} PIO_ENV: ${{ inputs.pio_env }} OTA_URL: https://github.com/meshtastic/esp32-unified-ota/releases/latest/download/mt-${{ inputs.platform }}-ota.bin working-directory: release run: | curl -L -o "mt-$PIO_PLATFORM-ota.bin" $OTA_URL - name: ESP32-C* - Download BLE-Only OTA firmware if: inputs.platform == 'esp32c3' || inputs.platform == 'esp32c6' id: dl-ota-ble env: PIO_ENV: ${{ inputs.pio_env }} OTA_URL: https://github.com/meshtastic/firmware-ota/releases/latest/download/firmware-c3.bin working-directory: release run: | curl -L -o bleota-c3.bin $OTA_URL - name: Update manifest with OTA file if: inputs.platform == 'esp32' || inputs.platform == 'esp32s3' || inputs.platform == 'esp32c3' || inputs.platform == 'esp32c6' working-directory: release env: PIO_PLATFORM: ${{ inputs.platform }} run: | # Determine OTA filename based on platform if [[ "$PIO_PLATFORM" == "esp32" || "$PIO_PLATFORM" == "esp32s3" ]]; then OTA_FILE="mt-${PIO_PLATFORM}-ota.bin" else OTA_FILE="bleota-c3.bin" fi # Check if OTA file exists if [[ ! -f "$OTA_FILE" ]]; then echo "OTA file $OTA_FILE not found, skipping manifest update" exit 0 fi # Calculate MD5 and size if command -v md5sum &> /dev/null; then OTA_MD5=$(md5sum "$OTA_FILE" | cut -d' ' -f1) else OTA_MD5=$(md5 -q "$OTA_FILE") fi OTA_SIZE=$(stat -f%z "$OTA_FILE" 2>/dev/null || stat -c%s "$OTA_FILE") # Find and update manifest file for manifest in firmware-*.mt.json; do if [[ -f "$manifest" ]]; then echo "Updating $manifest with $OTA_FILE (md5: $OTA_MD5, size: $OTA_SIZE)" # Add OTA entry to files array if not already present jq --arg name "$OTA_FILE" --arg md5 "$OTA_MD5" --argjson bytes "$OTA_SIZE" --arg part "app1" \ 'if .files | map(select(.name == $name)) | length == 0 then .files += [{"name": $name, "md5": $md5, "bytes": $bytes, "part_name": $part}] else . end' \ "$manifest" > "${manifest}.tmp" && mv "${manifest}.tmp" "$manifest" fi done - name: Job summary env: PIO_ENV: ${{ inputs.pio_env }} run: | echo "## $PIO_ENV" >> $GITHUB_STEP_SUMMARY echo "
Manifest" >> $GITHUB_STEP_SUMMARY echo '' >> $GITHUB_STEP_SUMMARY echo '```json' >> $GITHUB_STEP_SUMMARY cat release/firmware-*.mt.json >> $GITHUB_STEP_SUMMARY echo '' >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY echo "
" >> $GITHUB_STEP_SUMMARY - name: Store binaries as an artifact uses: actions/upload-artifact@v7 id: upload-firmware with: name: firmware-${{ inputs.platform }}-${{ inputs.pio_env }}-${{ inputs.version }} overwrite: true path: | release/*.mt.json release/*.bin release/*.elf release/*.uf2 release/*.hex release/*.zip release/device-*.sh release/device-*.bat - name: Store manifests as an artifact uses: actions/upload-artifact@v7 id: upload-manifest with: name: manifest-${{ inputs.platform }}-${{ inputs.pio_env }}-${{ inputs.version }} overwrite: true path: | release/*.mt.json ================================================ FILE: .github/workflows/build_one_target.yml ================================================ name: Build One Target on: workflow_dispatch: inputs: # trunk-ignore(checkov/CKV_GHA_7) arch: type: choice options: - esp32 - esp32s3 - esp32c3 - esp32c6 - nrf52840 - rp2040 - rp2350 - stm32 target: type: string required: false description: Choose the target board, e.g. nrf52_promicro_diy_tcxo. If blank, will find available targets. # find-target: # type: boolean # default: true # description: 'Find the available targets' permissions: read-all jobs: find-targets: if: ${{ inputs.target == '' }} strategy: fail-fast: false matrix: arch: - esp32 - esp32s3 - esp32c3 - esp32c6 - nrf52840 - rp2040 - rp2350 - stm32 runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v6 - uses: actions/setup-python@v6 with: python-version: 3.x cache: pip - run: pip install -U platformio - name: Generate matrix id: jsonStep run: | TARGETS=$(./bin/generate_ci_matrix.py ${{matrix.arch}} --level extra) echo "Name: $GITHUB_REF_NAME" >> $GITHUB_STEP_SUMMARY echo "Base: $GITHUB_BASE_REF" >> $GITHUB_STEP_SUMMARY echo "Arch: ${{matrix.arch}}" >> $GITHUB_STEP_SUMMARY echo "Ref: $GITHUB_REF" >> $GITHUB_STEP_SUMMARY echo "Targets:" >> $GITHUB_STEP_SUMMARY echo $TARGETS | jq -r 'sort_by(.board) |.[] | "- " + .board' >> $GITHUB_STEP_SUMMARY version: if: ${{ inputs.target != '' }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Get release version string run: | echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT echo "deb=$(./bin/buildinfo.py deb)" >> $GITHUB_OUTPUT id: version env: BUILD_LOCATION: local outputs: long: ${{ steps.version.outputs.long }} deb: ${{ steps.version.outputs.deb }} build: if: ${{ inputs.target != '' && inputs.arch != 'native' }} needs: [version] uses: ./.github/workflows/build_firmware.yml with: version: ${{ needs.version.outputs.long }} pio_env: ${{ inputs.target }} platform: ${{ inputs.arch }} gather-artifacts: permissions: contents: write pull-requests: write runs-on: ubuntu-latest needs: [version, build] steps: - name: Checkout code uses: actions/checkout@v6 with: ref: ${{github.event.pull_request.head.ref}} repository: ${{github.event.pull_request.head.repo.full_name}} - uses: actions/download-artifact@v8 with: path: ./ pattern: firmware-*-* merge-multiple: true - name: Display structure of downloaded files run: ls -R - name: Move files up run: mv -b -t ./ ./bin/device-*.sh ./bin/device-*.bat - name: Repackage in single firmware zip uses: actions/upload-artifact@v7 with: name: firmware-${{inputs.target}}-${{ needs.version.outputs.long }} overwrite: true path: | ./firmware-*.bin ./firmware-*.uf2 ./firmware-*.hex ./firmware-*.zip ./device-*.sh ./device-*.bat ./littlefs-*.bin ./bleota*bin ./Meshtastic_nRF52_factory_erase*.uf2 retention-days: 30 - uses: actions/download-artifact@v8 with: pattern: firmware-*-${{ needs.version.outputs.long }} merge-multiple: true path: ./output # For diagnostics - name: Show artifacts run: ls -lR - name: Device scripts permissions run: | chmod +x ./output/device-install.sh || true chmod +x ./output/device-update.sh || true - name: Zip firmware run: zip -j -9 -r ./firmware-${{inputs.target}}-${{ needs.version.outputs.long }}.zip ./output - name: Repackage in single elfs zip uses: actions/upload-artifact@v7 with: name: debug-elfs-${{inputs.target}}-${{ needs.version.outputs.long }}.zip overwrite: true path: ./*.elf retention-days: 30 - uses: scruplelesswizard/comment-artifact@main if: ${{ github.event_name == 'pull_request' }} with: name: firmware-${{inputs.target}}-${{ needs.version.outputs.long }} description: "Download firmware-${{inputs.target}}-${{ needs.version.outputs.long }}.zip. This artifact will be available for 90 days from creation" github-token: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/daily_packaging.yml ================================================ name: Daily Packaging on: schedule: - cron: 0 2 * * * workflow_dispatch: push: branches: - master paths: - debian/** - "*.rpkg" - .github/workflows/nightly_packaging.yml - .github/workflows/build_debian_src.yml - .github/workflows/package_ppa.yml - .github/workflows/package_obs.yml - .github/workflows/hook_copr.yml permissions: contents: write packages: write jobs: docker-multiarch: if: github.repository == 'meshtastic/firmware' uses: ./.github/workflows/docker_manifest.yml with: release_channel: daily secrets: inherit package-ppa: if: github.repository == 'meshtastic/firmware' strategy: fail-fast: false matrix: series: - jammy # 22.04 LTS - noble # 24.04 LTS - questing # 25.10 - resolute # 26.04 LTS uses: ./.github/workflows/package_ppa.yml with: ppa_repo: ppa:meshtastic/daily series: ${{ matrix.series }} secrets: inherit package-obs: if: github.repository == 'meshtastic/firmware' uses: ./.github/workflows/package_obs.yml with: obs_project: network:Meshtastic:daily series: unstable secrets: inherit hook-copr: if: github.repository == 'meshtastic/firmware' uses: ./.github/workflows/hook_copr.yml with: copr_project: daily secrets: inherit ================================================ FILE: .github/workflows/docker_build.yml ================================================ name: Build Docker # Build Docker image, push untagged (digest-only) on: workflow_call: secrets: DOCKER_FIRMWARE_TOKEN: required: false # Only required for push inputs: distro: description: Distro to target required: true type: string # choices: [debian, alpine] platform: description: Platform to target required: true type: string runs-on: description: Runner to use required: true type: string push: description: Push images to registry required: false type: boolean default: false pio_env: description: PlatformIO environment to build required: false type: string default: native outputs: digest: description: Digest of built image value: ${{ jobs.docker-build.outputs.digest }} permissions: contents: write packages: write jobs: docker-build: outputs: digest: ${{ steps.docker_variant.outputs.digest }} runs-on: ${{ inputs.runs-on }} steps: - name: Checkout code uses: actions/checkout@v6 with: submodules: recursive ref: ${{github.event.pull_request.head.ref}} repository: ${{github.event.pull_request.head.repo.full_name}} - name: Get release version string run: | echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT id: version - name: Docker login if: ${{ inputs.push }} uses: docker/login-action@v4 with: username: meshtastic password: ${{ secrets.DOCKER_FIRMWARE_TOKEN }} - name: Set up QEMU uses: docker/setup-qemu-action@v4 - name: Docker setup uses: docker/setup-buildx-action@v4 - name: Sanitize platform string id: sanitize_platform # Replace slashes with underscores run: echo "cleaned_platform=${{ inputs.platform }}" | sed 's/\//_/g' >> $GITHUB_OUTPUT - name: Docker tag id: meta uses: docker/metadata-action@v6 with: images: meshtastic/meshtasticd tags: | GHA-${{ steps.version.outputs.long }}-${{ inputs.distro }}-${{ steps.sanitize_platform.outputs.cleaned_platform }} flavor: latest=false - name: Docker build and push uses: docker/build-push-action@v7 id: docker_variant with: context: . file: | ${{ contains(inputs.distro, 'debian') && './Dockerfile' || contains(inputs.distro, 'alpine') && './alpine.Dockerfile' }} push: ${{ inputs.push }} tags: ${{ steps.meta.outputs.tags }} # Tag is only meant to be consumed by the "manifest" job platforms: ${{ inputs.platform }} build-args: | PIO_ENV=${{ inputs.pio_env }} ================================================ FILE: .github/workflows/docker_manifest.yml ================================================ name: Build Docker Multi-Arch Manifest on: workflow_call: secrets: DOCKER_FIRMWARE_TOKEN: required: true inputs: release_channel: description: Release channel to target required: true type: string permissions: contents: write packages: write jobs: docker-debian-amd64: uses: ./.github/workflows/docker_build.yml with: distro: debian platform: linux/amd64 runs-on: ubuntu-24.04 push: true secrets: inherit docker-debian-arm64: uses: ./.github/workflows/docker_build.yml with: distro: debian platform: linux/arm64 runs-on: ubuntu-24.04-arm push: true secrets: inherit docker-debian-armv7: uses: ./.github/workflows/docker_build.yml with: distro: debian platform: linux/arm/v7 runs-on: ubuntu-24.04-arm push: true secrets: inherit docker-alpine-amd64: uses: ./.github/workflows/docker_build.yml with: distro: alpine platform: linux/amd64 runs-on: ubuntu-24.04 push: true secrets: inherit docker-alpine-arm64: uses: ./.github/workflows/docker_build.yml with: distro: alpine platform: linux/arm64 runs-on: ubuntu-24.04-arm push: true secrets: inherit docker-alpine-armv7: uses: ./.github/workflows/docker_build.yml with: distro: alpine platform: linux/arm/v7 runs-on: ubuntu-24.04-arm push: true secrets: inherit docker-manifest: needs: # Debian - docker-debian-amd64 - docker-debian-arm64 - docker-debian-armv7 # Alpine - docker-alpine-amd64 - docker-alpine-arm64 - docker-alpine-armv7 runs-on: ubuntu-24.04 steps: - name: Checkout code uses: actions/checkout@v6 with: submodules: recursive ref: ${{github.event.pull_request.head.ref}} repository: ${{github.event.pull_request.head.repo.full_name}} - name: Get release version string run: | echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT echo "short=$(./bin/buildinfo.py short)" >> $GITHUB_OUTPUT id: version - name: Enumerate tags shell: python run: | import os short = "${{ steps.version.outputs.short }}" long = "${{ steps.version.outputs.long }}" release_channel = "${{ inputs.release_channel }}" tags = { "beta": { "debian": [ f"{short}", f"{long}", f"{short}-beta", f"{long}-beta", "beta", "latest", f"{short}-debian", f"{long}-debian", f"{short}-beta-debian", f"{long}-beta-debian", "beta-debian" ], "alpine": [ f"{short}-alpine", f"{long}-alpine", f"{short}-beta-alpine", f"{long}-beta-alpine", "beta-alpine" ] }, "alpha": { "debian": [ f"{short}-alpha", f"{long}-alpha", "alpha", f"{short}-alpha-debian", f"{long}-alpha-debian", "alpha-debian" ], "alpine": [ f"{short}-alpha-alpine", f"{long}-alpha-alpine", "alpha-alpine" ] }, "daily": { "debian": ["daily", "daily-debian"], "alpine": ["daily-alpine"] } } with open(os.environ["GITHUB_OUTPUT"], "a") as fh: fh.write("debian<> $GITHUB_OUTPUT echo "$TARGETS" >> $GITHUB_STEP_SUMMARY outputs: all: ${{ steps.jsonStep.outputs.all }} check: ${{ steps.jsonStep.outputs.check }} version: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Get release version string run: | echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT echo "deb=$(./bin/buildinfo.py deb)" >> $GITHUB_OUTPUT id: version env: BUILD_LOCATION: local outputs: long: ${{ steps.version.outputs.long }} deb: ${{ steps.version.outputs.deb }} check: needs: setup strategy: fail-fast: false matrix: check: ${{ fromJson(needs.setup.outputs.check) }} # Use 'arctastic' self-hosted runner pool when checking in the main repo runs-on: ${{ github.repository_owner == 'meshtastic' && 'arctastic' || 'ubuntu-latest' }} if: ${{ github.event_name != 'workflow_dispatch' && github.repository == 'meshtastic/firmware' }} steps: - uses: actions/checkout@v6 with: submodules: recursive ref: ${{github.event.pull_request.head.ref}} repository: ${{github.event.pull_request.head.repo.full_name}} - name: Check ${{ matrix.check.board }} uses: meshtastic/gh-action-firmware@main with: pio_platform: ${{ matrix.check.platform }} pio_env: ${{ matrix.check.board }} pio_target: check build: needs: [setup, version] strategy: fail-fast: false matrix: build: ${{ fromJson(needs.setup.outputs.all) }} uses: ./.github/workflows/build_firmware.yml with: version: ${{ needs.version.outputs.long }} pio_env: ${{ matrix.build.board }} platform: ${{ matrix.build.platform }} build-debian-src: if: github.repository == 'meshtastic/firmware' uses: ./.github/workflows/build_debian_src.yml with: series: UNRELEASED build_location: local secrets: inherit package-pio-deps-native-tft: if: ${{ github.repository == 'meshtastic/firmware' && github.event_name == 'workflow_dispatch' }} uses: ./.github/workflows/package_pio_deps.yml with: pio_env: native-tft secrets: inherit test-native: if: ${{ !contains(github.ref_name, 'event/') && github.repository == 'meshtastic/firmware' }} uses: ./.github/workflows/test_native.yml docker: strategy: fail-fast: false matrix: distro: [debian, alpine] platform: [linux/amd64, linux/arm64, linux/arm/v7] pio_env: [native, native-tft] exclude: - distro: alpine platform: linux/arm/v7 - pio_env: native-tft platform: linux/arm64 - pio_env: native-tft platform: linux/arm/v7 uses: ./.github/workflows/docker_build.yml with: distro: ${{ matrix.distro }} platform: ${{ matrix.platform }} runs-on: ${{ contains(matrix.platform, 'arm') && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }} pio_env: ${{ matrix.pio_env }} push: false gather-artifacts: # trunk-ignore(checkov/CKV2_GHA_1) if: github.repository == 'meshtastic/firmware' permissions: contents: write pull-requests: write strategy: fail-fast: false matrix: arch: - esp32 - esp32s3 - esp32c3 - esp32c6 - nrf52840 - rp2040 - rp2350 - stm32 runs-on: ubuntu-latest needs: [version, build] steps: - name: Checkout code uses: actions/checkout@v6 with: ref: ${{github.event.pull_request.head.ref}} repository: ${{github.event.pull_request.head.repo.full_name}} - uses: actions/download-artifact@v8 with: path: ./ pattern: firmware-${{matrix.arch}}-* merge-multiple: true - name: Display structure of downloaded files run: ls -R - name: Repackage in single firmware zip uses: actions/upload-artifact@v7 with: name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }} overwrite: true path: | ./firmware-*.mt.json ./firmware-*.bin ./firmware-*.uf2 ./firmware-*.hex ./firmware-*.zip ./device-*.sh ./device-*.bat ./littlefs-*.bin ./bleota*bin ./mt-*-ota.bin ./Meshtastic_nRF52_factory_erase*.uf2 retention-days: 30 - uses: actions/download-artifact@v8 with: name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }} merge-multiple: true path: ./output # For diagnostics - name: Show artifacts run: ls -lR - name: Device scripts permissions run: | chmod +x ./output/device-install.sh || true chmod +x ./output/device-update.sh || true - name: Zip firmware run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output - name: Repackage in single elfs zip uses: actions/upload-artifact@v7 with: name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }} overwrite: true path: ./*.elf retention-days: 30 - uses: scruplelesswizard/comment-artifact@main if: ${{ github.event_name == 'pull_request' }} with: name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }} description: "Download firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip. This artifact will be available for 90 days from creation" github-token: ${{ secrets.GITHUB_TOKEN }} shame: if: github.repository == 'meshtastic/firmware' continue-on-error: true runs-on: ubuntu-latest needs: [build] steps: - uses: actions/checkout@v6 if: github.event_name == 'pull_request_target' with: filter: blob:none # means we download all the git history but none of the commit (except ones with checkout like the head) fetch-depth: 0 - name: Download the current manifests uses: actions/download-artifact@v8 with: path: ./manifests-new/ pattern: manifest-* merge-multiple: true - name: Upload combined manifests for later commit and global stats crunching. uses: actions/upload-artifact@v7 id: upload-manifest with: name: manifests-${{ github.sha }} overwrite: true path: manifests-new/*.mt.json - name: Find the merge base if: github.event_name == 'pull_request_target' run: echo "MERGE_BASE=$(git merge-base "origin/$base" "$head")" >> $GITHUB_ENV env: base: ${{ github.base_ref }} head: ${{ github.sha }} # Currently broken (for-loop through EVERY artifact -- rate limiting) # - name: Download the old manifests # if: github.event_name == 'pull_request_target' # run: gh run download -R "$repo" --name "manifests-$merge_base" --dir manifest-old/ # env: # GH_TOKEN: ${{ github.token }} # merge_base: ${{ env.MERGE_BASE }} # repo: ${{ github.repository }} # - name: Do scan and post comment # if: github.event_name == 'pull_request_target' # run: python3 bin/shame.py ${{ github.event.pull_request.number }} manifests-old/ manifests-new/ release-artifacts: runs-on: ubuntu-latest if: ${{ github.event_name == 'workflow_dispatch' && github.repository == 'meshtastic/firmware' }} outputs: upload_url: ${{ steps.create_release.outputs.upload_url }} needs: - setup - version - gather-artifacts - build-debian-src - package-pio-deps-native-tft steps: - name: Checkout uses: actions/checkout@v6 with: fetch-depth: 0 - name: Setup Python uses: actions/setup-python@v6 with: python-version: 3.x - name: Generate release notes id: release_notes run: | chmod +x ./bin/generate_release_notes.py NOTES=$(./bin/generate_release_notes.py ${{ needs.version.outputs.long }}) echo "notes<> $GITHUB_OUTPUT echo "$NOTES" >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Create release uses: softprops/action-gh-release@v2 id: create_release with: draft: true prerelease: true name: Meshtastic Firmware ${{ needs.version.outputs.long }} Alpha tag_name: v${{ needs.version.outputs.long }} body: ${{ steps.release_notes.outputs.notes }} - name: Download source deb uses: actions/download-artifact@v8 with: pattern: firmware-debian-${{ needs.version.outputs.deb }}~UNRELEASED-src merge-multiple: true path: ./output/debian-src - name: Download `native-tft` pio deps uses: actions/download-artifact@v8 with: pattern: platformio-deps-native-tft-${{ needs.version.outputs.long }} merge-multiple: true path: ./output/pio-deps-native-tft - name: Zip Linux sources working-directory: output run: | zip -j -9 -r ./meshtasticd-${{ needs.version.outputs.deb }}-src.zip ./debian-src zip -9 -r ./platformio-deps-native-tft-${{ needs.version.outputs.long }}.zip ./pio-deps-native-tft # For diagnostics - name: Display structure of downloaded files run: ls -lR - name: Generate Release manifest run: | jq -n --arg ver "${{ needs.version.outputs.long }}" --argjson targets ${{ toJson(needs.setup.outputs.all) }} '{ "version": $ver, "targets": $targets }' > firmware-${{ needs.version.outputs.long }}.json - name: Save Release manifest artifact uses: actions/upload-artifact@v7 with: name: manifest-${{ needs.version.outputs.long }} overwrite: true path: firmware-${{ needs.version.outputs.long }}.json - name: Add sources to GitHub Release # Only run when targeting master branch with workflow_dispatch if: ${{ github.ref_name == 'master' }} run: | gh release upload v${{ needs.version.outputs.long }} ./firmware-${{ needs.version.outputs.long }}.json gh release upload v${{ needs.version.outputs.long }} ./output/meshtasticd-${{ needs.version.outputs.deb }}-src.zip gh release upload v${{ needs.version.outputs.long }} ./output/platformio-deps-native-tft-${{ needs.version.outputs.long }}.zip env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} release-firmware: strategy: fail-fast: false matrix: arch: - esp32 - esp32s3 - esp32c3 - esp32c6 - nrf52840 - rp2040 - rp2350 - stm32 runs-on: ubuntu-latest if: ${{ github.event_name == 'workflow_dispatch' && github.repository == 'meshtastic/firmware'}} needs: [release-artifacts, version] steps: - name: Checkout uses: actions/checkout@v6 - name: Setup Python uses: actions/setup-python@v6 with: python-version: 3.x - uses: actions/download-artifact@v8 with: pattern: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }} merge-multiple: true path: ./output - name: Display structure of downloaded files run: ls -lR - name: Device scripts permissions run: | chmod +x ./output/device-install.sh || true chmod +x ./output/device-update.sh || true - name: Zip firmware run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output - uses: actions/download-artifact@v8 with: name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }} merge-multiple: true path: ./elfs - name: Zip debug elfs run: zip -j -9 -r ./debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./elfs # For diagnostics - name: Display structure of downloaded files run: ls -lR - name: Add bins and debug elfs to GitHub Release # Only run when targeting master branch with workflow_dispatch if: ${{ github.ref_name == 'master' }} run: | gh release upload v${{ needs.version.outputs.long }} ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip gh release upload v${{ needs.version.outputs.long }} ./debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} publish-firmware: runs-on: ubuntu-24.04 if: ${{ github.event_name == 'workflow_dispatch' }} needs: [release-firmware, version] env: targets: |- esp32,esp32s3,esp32c3,esp32c6,nrf52840,rp2040,rp2350,stm32 steps: - name: Checkout uses: actions/checkout@v6 with: fetch-depth: 0 - name: Setup Python uses: actions/setup-python@v6 with: python-version: 3.x - name: Get firmware artifacts uses: actions/download-artifact@v8 with: pattern: firmware-{${{ env.targets }}}-${{ needs.version.outputs.long }} merge-multiple: true path: ./publish - name: Get manifest artifact uses: actions/download-artifact@v8 with: pattern: manifest-${{ needs.version.outputs.long }} path: ./publish - name: Generate release notes run: | chmod +x ./bin/generate_release_notes.py ./bin/generate_release_notes.py ${{ needs.version.outputs.long }} > ./publish/release_notes.md env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Publish firmware to meshtastic.github.io uses: peaceiris/actions-gh-pages@v4 env: # On event/* branches, use the event name as the destination prefix DEST_PREFIX: ${{ contains(github.ref_name, 'event/') && format('{0}/', github.ref_name) || '' }} with: deploy_key: ${{ secrets.DIST_PAGES_DEPLOY_KEY }} external_repository: meshtastic/meshtastic.github.io publish_branch: master publish_dir: ./publish destination_dir: ${{ env.DEST_PREFIX }}firmware-${{ needs.version.outputs.long }} keep_files: true user_name: github-actions[bot] user_email: github-actions[bot]@users.noreply.github.com commit_message: ${{ needs.version.outputs.long }} enable_jekyll: true ================================================ FILE: .github/workflows/merge_queue.yml ================================================ name: Merge Queue # Not sure how concurrency works in merge_queue, removing for now. # concurrency: # group: merge-queue-${{ github.head_ref || github.run_id }} # cancel-in-progress: true on: # Merge group is a special trigger that is used to trigger the workflow when a merge group is created. merge_group: jobs: setup: strategy: fail-fast: true matrix: arch: - all - check runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v6 - uses: actions/setup-python@v6 with: python-version: 3.x cache: pip - run: pip install -U platformio - name: Generate matrix id: jsonStep run: | if [[ "$GITHUB_HEAD_REF" == "" ]]; then TARGETS=$(./bin/generate_ci_matrix.py ${{matrix.arch}}) else TARGETS=$(./bin/generate_ci_matrix.py ${{matrix.arch}} --level pr) fi echo "Name: $GITHUB_REF_NAME Base: $GITHUB_BASE_REF Ref: $GITHUB_REF" echo "${{matrix.arch}}=$TARGETS" >> $GITHUB_OUTPUT outputs: all: ${{ steps.jsonStep.outputs.all }} check: ${{ steps.jsonStep.outputs.check }} version: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Get release version string run: | echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT echo "deb=$(./bin/buildinfo.py deb)" >> $GITHUB_OUTPUT id: version env: BUILD_LOCATION: local outputs: long: ${{ steps.version.outputs.long }} deb: ${{ steps.version.outputs.deb }} check: needs: setup strategy: fail-fast: true matrix: check: ${{ fromJson(needs.setup.outputs.check) }} runs-on: ubuntu-latest if: ${{ github.event_name != 'workflow_dispatch' }} steps: - uses: actions/checkout@v6 - name: Build base id: base uses: ./.github/actions/setup-base - name: Check ${{ matrix.check.board }} run: bin/check-all.sh ${{ matrix.check.board }} build: needs: [setup, version] strategy: matrix: build: ${{ fromJson(needs.setup.outputs.all) }} uses: ./.github/workflows/build_firmware.yml with: version: ${{ needs.version.outputs.long }} pio_env: ${{ matrix.build.board }} platform: ${{ matrix.build.platform }} build-debian-src: if: github.repository == 'meshtastic/firmware' uses: ./.github/workflows/build_debian_src.yml with: series: UNRELEASED build_location: local secrets: inherit package-pio-deps-native-tft: if: ${{ github.event_name == 'workflow_dispatch' }} uses: ./.github/workflows/package_pio_deps.yml with: pio_env: native-tft secrets: inherit test-native: if: ${{ !contains(github.ref_name, 'event/') }} uses: ./.github/workflows/test_native.yml docker: strategy: fail-fast: false matrix: distro: [debian, alpine] platform: [linux/amd64, linux/arm64, linux/arm/v7] pio_env: [native, native-tft] exclude: - distro: alpine platform: linux/arm/v7 - pio_env: native-tft platform: linux/arm64 - pio_env: native-tft platform: linux/arm/v7 uses: ./.github/workflows/docker_build.yml with: distro: ${{ matrix.distro }} platform: ${{ matrix.platform }} runs-on: ${{ contains(matrix.platform, 'arm') && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }} pio_env: ${{ matrix.pio_env }} push: false gather-artifacts: # trunk-ignore(checkov/CKV2_GHA_1) permissions: contents: write pull-requests: write strategy: fail-fast: false matrix: arch: - esp32 - esp32s3 - esp32c3 - esp32c6 - nrf52840 - rp2040 - rp2350 - stm32 runs-on: ubuntu-latest needs: [version, build] steps: - name: Checkout code uses: actions/checkout@v6 with: ref: ${{github.event.pull_request.head.ref}} repository: ${{github.event.pull_request.head.repo.full_name}} - uses: actions/download-artifact@v8 with: path: ./ pattern: firmware-${{matrix.arch}}-* merge-multiple: true - name: Display structure of downloaded files run: ls -R - name: Move files up run: mv -b -t ./ ./bin/device-*.sh ./bin/device-*.bat - name: Repackage in single firmware zip uses: actions/upload-artifact@v7 with: name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }} overwrite: true path: | ./firmware-*.bin ./firmware-*.uf2 ./firmware-*.hex ./firmware-*.zip ./device-*.sh ./device-*.bat ./littlefs-*.bin ./bleota*bin ./Meshtastic_nRF52_factory_erase*.uf2 retention-days: 30 - uses: actions/download-artifact@v8 with: name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }} merge-multiple: true path: ./output # For diagnostics - name: Show artifacts run: ls -lR - name: Device scripts permissions run: | chmod +x ./output/device-install.sh || true chmod +x ./output/device-update.sh || true - name: Zip firmware run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output - name: Repackage in single elfs zip uses: actions/upload-artifact@v7 with: name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }} overwrite: true path: ./*.elf retention-days: 30 - uses: scruplelesswizard/comment-artifact@main if: ${{ github.event_name == 'pull_request' }} with: name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }} description: "Download firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip. This artifact will be available for 90 days from creation" github-token: ${{ secrets.GITHUB_TOKEN }} release-artifacts: runs-on: ubuntu-latest if: ${{ github.event_name == 'workflow_dispatch' }} outputs: upload_url: ${{ steps.create_release.outputs.upload_url }} needs: - version - gather-artifacts - build-debian-src - package-pio-deps-native-tft steps: - name: Checkout uses: actions/checkout@v6 - name: Create release uses: softprops/action-gh-release@v2 id: create_release with: draft: true prerelease: true name: Meshtastic Firmware ${{ needs.version.outputs.long }} Alpha tag_name: v${{ needs.version.outputs.long }} body: | Autogenerated by github action, developer should edit as required before publishing... - name: Download source deb uses: actions/download-artifact@v8 with: pattern: firmware-debian-${{ needs.version.outputs.deb }}~UNRELEASED-src merge-multiple: true path: ./output/debian-src - name: Download `native-tft` pio deps uses: actions/download-artifact@v8 with: pattern: platformio-deps-native-tft-${{ needs.version.outputs.long }} merge-multiple: true path: ./output/pio-deps-native-tft - name: Zip Linux sources working-directory: output run: | zip -j -9 -r ./meshtasticd-${{ needs.version.outputs.deb }}-src.zip ./debian-src zip -9 -r ./platformio-deps-native-tft-${{ needs.version.outputs.long }}.zip ./pio-deps-native-tft # For diagnostics - name: Display structure of downloaded files run: ls -lR - name: Add Linux sources to GtiHub Release # Only run when targeting master branch with workflow_dispatch if: ${{ github.ref_name == 'master' }} run: | gh release upload v${{ needs.version.outputs.long }} ./output/meshtasticd-${{ needs.version.outputs.deb }}-src.zip gh release upload v${{ needs.version.outputs.long }} ./output/platformio-deps-native-tft-${{ needs.version.outputs.long }}.zip env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} release-firmware: strategy: fail-fast: false matrix: arch: - esp32 - esp32s3 - esp32c3 - esp32c6 - nrf52840 - rp2040 - rp2350 - stm32 runs-on: ubuntu-latest if: ${{ github.event_name == 'workflow_dispatch' }} needs: [release-artifacts, version] steps: - name: Checkout uses: actions/checkout@v6 - name: Setup Python uses: actions/setup-python@v6 with: python-version: 3.x - uses: actions/download-artifact@v8 with: pattern: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }} merge-multiple: true path: ./output - name: Display structure of downloaded files run: ls -lR - name: Device scripts permissions run: | chmod +x ./output/device-install.sh || true chmod +x ./output/device-update.sh || true - name: Zip firmware run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output - uses: actions/download-artifact@v8 with: name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }} merge-multiple: true path: ./elfs - name: Zip debug elfs run: zip -j -9 -r ./debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./elfs # For diagnostics - name: Display structure of downloaded files run: ls -lR - name: Add bins and debug elfs to GitHub Release # Only run when targeting master branch with workflow_dispatch if: ${{ github.ref_name == 'master' }} run: | gh release upload v${{ needs.version.outputs.long }} ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip gh release upload v${{ needs.version.outputs.long }} ./debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} publish-firmware: runs-on: ubuntu-24.04 if: ${{ github.event_name == 'workflow_dispatch' }} needs: [release-firmware, version] env: targets: |- esp32,esp32s3,esp32c3,esp32c6,nrf52840,rp2040,rp2350,stm32 steps: - name: Checkout uses: actions/checkout@v6 - name: Setup Python uses: actions/setup-python@v6 with: python-version: 3.x - uses: actions/download-artifact@v8 with: pattern: firmware-{${{ env.targets }}}-${{ needs.version.outputs.long }} merge-multiple: true path: ./publish - name: Publish firmware to meshtastic.github.io uses: peaceiris/actions-gh-pages@v4 env: # On event/* branches, use the event name as the destination prefix DEST_PREFIX: ${{ contains(github.ref_name, 'event/') && format('{0}/', github.ref_name) || '' }} with: deploy_key: ${{ secrets.DIST_PAGES_DEPLOY_KEY }} external_repository: meshtastic/meshtastic.github.io publish_branch: master publish_dir: ./publish destination_dir: ${{ env.DEST_PREFIX }}firmware-${{ needs.version.outputs.long }} keep_files: true user_name: github-actions[bot] user_email: github-actions[bot]@users.noreply.github.com commit_message: ${{ needs.version.outputs.long }} enable_jekyll: true ================================================ FILE: .github/workflows/models_issue_triage.yml ================================================ name: Issue Triage (Models) on: issues: types: [opened] permissions: issues: write models: read concurrency: group: ${{ github.workflow }}-${{ github.event.issue.number }} cancel-in-progress: true jobs: triage: if: ${{ github.repository == 'meshtastic/firmware' && github.event.issue.user.type != 'Bot' }} runs-on: ubuntu-latest steps: # ───────────────────────────────────────────────────────────────────────── # Step 1: Quality check (spam/AI-slop detection) - runs first, exits early if spam # ───────────────────────────────────────────────────────────────────────── - name: Detect spam or low-quality content uses: actions/ai-inference@v2 id: quality continue-on-error: true with: max-tokens: 20 prompt: | Is this GitHub issue spam, AI-generated slop, or low quality? Title: ${{ github.event.issue.title }} Body: ${{ github.event.issue.body }} Respond with exactly one of: spam, ai-generated, needs-review, ok system-prompt: You detect spam and low-quality contributions. Be conservative - only flag obvious spam or AI slop. model: openai/gpt-4o-mini - name: Apply quality label if needed if: steps.quality.outputs.response != '' && steps.quality.outputs.response != 'ok' uses: actions/github-script@v8 env: QUALITY_LABEL: ${{ steps.quality.outputs.response }} with: script: | const label = (process.env.QUALITY_LABEL || '').trim().toLowerCase(); const labelMeta = { 'spam': { color: 'd73a4a', description: 'Possible spam' }, 'ai-generated': { color: 'fbca04', description: 'Possible AI-generated low-quality content' }, 'needs-review': { color: 'f9d0c4', description: 'Needs human review' }, }; const meta = labelMeta[label]; if (!meta) return; // Ensure label exists try { await github.rest.issues.getLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label }); } catch (e) { if (e.status !== 404) throw e; await github.rest.issues.createLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label, color: meta.color, description: meta.description }); } // Apply label await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.payload.issue.number, labels: [label] }); // Set output to skip remaining steps core.setOutput('is_spam', 'true'); # ───────────────────────────────────────────────────────────────────────── # Step 2: Duplicate detection - only if not spam # ───────────────────────────────────────────────────────────────────────── - name: Detect duplicate issues if: steps.quality.outputs.response == 'ok' || steps.quality.outputs.response == '' uses: pelikhan/action-genai-issue-dedup@bdb3b5d9451c1090ffcdf123d7447a5e7c7a2528 # v0.0.19 with: github_token: ${{ secrets.GITHUB_TOKEN }} # ───────────────────────────────────────────────────────────────────────── # Step 3: Completeness check + auto-labeling (combined into one AI call) # ───────────────────────────────────────────────────────────────────────── - name: Determine if completeness check should be skipped if: steps.quality.outputs.response == 'ok' || steps.quality.outputs.response == '' uses: actions/github-script@v8 id: check-skip with: script: | const title = (context.payload.issue.title || '').toLowerCase(); const labels = (context.payload.issue.labels || []).map(label => label.name); const hasFeatureRequest = title.includes('feature request'); const hasEnhancement = labels.includes('enhancement'); const shouldSkip = hasFeatureRequest && hasEnhancement; core.setOutput('should_skip', shouldSkip ? 'true' : 'false'); - name: Analyze issue completeness and determine labels if: (steps.quality.outputs.response == 'ok' || steps.quality.outputs.response == '') && steps.check-skip.outputs.should_skip != 'true' uses: actions/ai-inference@v2 id: analysis continue-on-error: true with: prompt: | Analyze this GitHub issue for completeness and determine if it needs labels. IMPORTANT: Distinguish between: - Device/firmware bugs (crashes, reboots, lockups, radio/GPS/display/power issues) - these need device logs - Build/release/packaging issues (missing files, CI failures, download problems) - these do NOT need device logs - Documentation or website issues - these do NOT need device logs If this is a device/firmware bug, request device logs and explain how to get them: Web Flasher logs: - Go to https://flasher.meshtastic.org - Connect the device via USB and click Connect - Open the device console/log output, reproduce the problem, then copy/download and attach/paste the logs Meshtastic CLI logs: - Run: meshtastic --port --noproto - Reproduce the problem, then copy/paste the terminal output Also request key context if missing: device model/variant, firmware version, region, steps to reproduce, expected vs actual. Respond ONLY with valid JSON (no markdown, no code fences): {"complete": true, "comment": "", "label": "none"} OR {"complete": false, "comment": "Your helpful comment", "label": "needs-logs"} Use "needs-logs" ONLY if this is a device/firmware bug AND no logs are attached. Use "needs-info" if basic info like firmware version or steps to reproduce are missing. Use "none" if the issue is complete, is a feature request, or is a build/CI/packaging issue. Title: ${{ github.event.issue.title }} Body: ${{ github.event.issue.body }} system-prompt: You are a helpful assistant that triages GitHub issues. Be conservative with labels. Only request device logs for actual device/firmware bugs, not for build/release/CI issues. model: openai/gpt-4o-mini - name: Process analysis result if: (steps.quality.outputs.response == 'ok' || steps.quality.outputs.response == '') && steps.check-skip.outputs.should_skip != 'true' && steps.analysis.outputs.response != '' uses: actions/github-script@v8 id: process env: AI_RESPONSE: ${{ steps.analysis.outputs.response }} with: script: | let raw = (process.env.AI_RESPONSE || '').trim(); // Strip markdown code fences if present raw = raw.replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/i, '').trim(); let complete = true; let comment = ''; let label = 'none'; try { const parsed = JSON.parse(raw); complete = !!parsed.complete; comment = (parsed.comment ?? '').toString().trim(); label = (parsed.label ?? 'none').toString().trim().toLowerCase(); } catch { // If JSON parse fails, log warning and don't comment (avoid posting raw JSON) console.log('Failed to parse AI response as JSON:', raw); complete = true; comment = ''; label = 'none'; } // Validate label const allowedLabels = new Set(['needs-logs', 'needs-info', 'none']); if (!allowedLabels.has(label)) label = 'none'; // Only comment if we have a valid parsed comment (not raw JSON) const shouldComment = !complete && comment.length > 0 && !comment.startsWith('{'); core.setOutput('should_comment', shouldComment ? 'true' : 'false'); core.setOutput('comment_body', comment); core.setOutput('label', label); - name: Apply triage label if: steps.process.outputs.label != '' && steps.process.outputs.label != 'none' uses: actions/github-script@v8 env: LABEL_NAME: ${{ steps.process.outputs.label }} with: script: | const label = process.env.LABEL_NAME; const labelMeta = { 'needs-logs': { color: 'cfd3d7', description: 'Device logs requested for triage' }, 'needs-info': { color: 'f9d0c4', description: 'More information requested for triage' }, }; const meta = labelMeta[label]; if (!meta) return; // Ensure label exists try { await github.rest.issues.getLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label }); } catch (e) { if (e.status !== 404) throw e; await github.rest.issues.createLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label, color: meta.color, description: meta.description }); } // Apply label await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.payload.issue.number, labels: [label] }); - name: Comment on issue if: steps.process.outputs.should_comment == 'true' uses: actions/github-script@v8 env: COMMENT_BODY: ${{ steps.process.outputs.comment_body }} with: script: | await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.payload.issue.number, body: process.env.COMMENT_BODY }); ================================================ FILE: .github/workflows/models_pr_triage.yml ================================================ name: PR Triage (Models) on: pull_request_target: types: [opened] permissions: pull-requests: write issues: write models: read concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number }} cancel-in-progress: true jobs: triage: if: ${{ github.repository == 'meshtastic/firmware' && github.event.pull_request.user.type != 'Bot' }} runs-on: ubuntu-latest steps: # ───────────────────────────────────────────────────────────────────────── # Step 1: Check if PR already has automation/type labels (skip if so) # ───────────────────────────────────────────────────────────────────────── - name: Check existing labels uses: actions/github-script@v8 id: check-labels with: script: | const skipLabels = new Set(['automation']); const typeLabels = new Set(['bugfix', 'hardware-support', 'enhancement', 'dependencies', 'submodules', 'github_actions', 'trunk', 'cleanup']); const prLabels = context.payload.pull_request.labels.map(l => l.name); const shouldSkipAll = prLabels.some(l => skipLabels.has(l)); const hasTypeLabel = prLabels.some(l => typeLabels.has(l)); core.setOutput('skip_all', shouldSkipAll ? 'true' : 'false'); core.setOutput('has_type_label', hasTypeLabel ? 'true' : 'false'); # ───────────────────────────────────────────────────────────────────────── # Step 2: Quality check (spam/AI-slop detection) # ───────────────────────────────────────────────────────────────────────── - name: Detect spam or low-quality content if: steps.check-labels.outputs.skip_all != 'true' uses: actions/ai-inference@v2 id: quality continue-on-error: true with: max-tokens: 20 prompt: | Is this GitHub pull request spam, AI-generated slop, or low quality? Title: ${{ github.event.pull_request.title }} Body: ${{ github.event.pull_request.body }} Respond with exactly one of: spam, ai-generated, needs-review, ok system-prompt: You detect spam and low-quality contributions. Be conservative - only flag obvious spam or AI slop. model: openai/gpt-4o-mini - name: Apply quality label if needed if: steps.check-labels.outputs.skip_all != 'true' && steps.quality.outputs.response != '' && steps.quality.outputs.response != 'ok' uses: actions/github-script@v8 id: quality-label env: QUALITY_LABEL: ${{ steps.quality.outputs.response }} with: script: | const label = (process.env.QUALITY_LABEL || '').trim().toLowerCase(); const labelMeta = { 'spam': { color: 'd73a4a', description: 'Possible spam' }, 'ai-generated': { color: 'fbca04', description: 'Possible AI-generated low-quality content' }, 'needs-review': { color: 'f9d0c4', description: 'Needs human review' }, }; const meta = labelMeta[label]; if (!meta) return; // Ensure label exists try { await github.rest.issues.getLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label }); } catch (e) { if (e.status !== 404) throw e; await github.rest.issues.createLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label, color: meta.color, description: meta.description }); } // Apply label await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.payload.pull_request.number, labels: [label] }); core.setOutput('is_spam', 'true'); # ───────────────────────────────────────────────────────────────────────── # Step 3: Auto-label PR type (bugfix/hardware-support/enhancement) # Only skip for spam/ai-generated; still classify needs-review PRs # ───────────────────────────────────────────────────────────────────────── - name: Classify PR for labeling if: steps.check-labels.outputs.skip_all != 'true' && steps.check-labels.outputs.has_type_label != 'true' && steps.quality.outputs.response != 'spam' && steps.quality.outputs.response != 'ai-generated' uses: actions/ai-inference@v2 id: classify continue-on-error: true with: max-tokens: 30 prompt: | Classify this pull request into exactly one category. Return exactly one of: bugfix, hardware-support, enhancement Use bugfix if it fixes a bug, crash, or incorrect behavior. Use hardware-support if it adds or improves support for a specific hardware device/variant. Use enhancement if it adds a new feature, improves performance, or refactors code. Title: ${{ github.event.pull_request.title }} Body: ${{ github.event.pull_request.body }} system-prompt: You classify pull requests into categories. Be conservative and pick the most appropriate single label. model: openai/gpt-4o-mini - name: Apply type label if: steps.check-labels.outputs.skip_all != 'true' && steps.check-labels.outputs.has_type_label != 'true' && steps.classify.outputs.response != '' uses: actions/github-script@v8 env: TYPE_LABEL: ${{ steps.classify.outputs.response }} with: script: | const label = (process.env.TYPE_LABEL || '').trim().toLowerCase(); const labelMeta = { 'bugfix': { color: 'd73a4a', description: 'Bug fix' }, 'hardware-support': { color: '0e8a16', description: 'Hardware support addition or improvement' }, 'enhancement': { color: 'a2eeef', description: 'New feature or enhancement' }, }; const meta = labelMeta[label]; if (!meta) return; // Ensure label exists try { await github.rest.issues.getLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label }); } catch (e) { if (e.status !== 404) throw e; await github.rest.issues.createLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label, color: meta.color, description: meta.description }); } // Apply label await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.payload.pull_request.number, labels: [label] }); ================================================ FILE: .github/workflows/nightly.yml ================================================ name: Nightly on: schedule: - cron: 0 8 * * 1-5 workflow_dispatch: {} permissions: read-all jobs: trunk_check: if: github.repository == 'meshtastic/firmware' name: Trunk Check and Upload runs-on: ubuntu-24.04 steps: - name: Checkout uses: actions/checkout@v6 - name: Trunk Check uses: trunk-io/trunk-action@v1 with: trunk-token: ${{ secrets.TRUNK_TOKEN }} trunk_upgrade: if: github.repository == 'meshtastic/firmware' # See: https://github.com/trunk-io/trunk-action/blob/v1/readme.md#automatic-upgrades name: Trunk Upgrade (PR) runs-on: ubuntu-24.04 permissions: contents: write # For trunk to create PRs pull-requests: write # For trunk to create PRs steps: - name: Checkout uses: actions/checkout@v6 - name: Trunk Upgrade uses: trunk-io/trunk-action/upgrade@v1 with: base: master ================================================ FILE: .github/workflows/package_obs.yml ================================================ name: Package for OpenSUSE Build Service on: workflow_call: secrets: OBS_PASSWORD: required: true PPA_GPG_PRIVATE_KEY: required: true inputs: obs_project: description: Meshtastic OBS project to target required: true type: string series: description: Debian series to target required: true type: string permissions: contents: write packages: write jobs: build-debian-src: uses: ./.github/workflows/build_debian_src.yml secrets: inherit with: series: ${{ inputs.series }} build_location: obs package-obs: runs-on: ubuntu-24.04 needs: build-debian-src steps: - name: Checkout code uses: actions/checkout@v6 with: submodules: recursive path: meshtasticd ref: ${{github.event.pull_request.head.ref}} repository: ${{github.event.pull_request.head.repo.full_name}} - name: Install OpenSUSE Build Service deps shell: bash run: | echo 'deb http://download.opensuse.org/repositories/openSUSE:/Tools/xUbuntu_24.04/ /' | sudo tee /etc/apt/sources.list.d/openSUSE:Tools.list curl -fsSL https://download.opensuse.org/repositories/openSUSE:Tools/xUbuntu_24.04/Release.key | gpg --dearmor | sudo tee /etc/apt/trusted.gpg.d/openSUSE_Tools.gpg > /dev/null sudo apt-get update -y --fix-missing sudo apt-get install -y osc - name: Get release version string working-directory: meshtasticd run: | echo "deb=$(./bin/buildinfo.py deb)" >> $GITHUB_OUTPUT env: BUILD_LOCATION: obs id: version - name: Download artifacts uses: actions/download-artifact@v8 with: name: firmware-debian-${{ steps.version.outputs.deb }}~${{ inputs.series }}-src merge-multiple: true - name: Display structure of downloaded files run: ls -lah - name: Configure osc env: OBS_USERNAME: meshtastic run: | # Setup OpenSUSE Build Service credentials mkdir -p ~/.config/osc echo "[general]" > ~/.config/osc/oscrc echo "apiurl=https://api.opensuse.org" >> ~/.config/osc/oscrc echo "[https://api.opensuse.org]" >> ~/.config/osc/oscrc echo "user=${{ env.OBS_USERNAME }}" >> ~/.config/osc/oscrc echo "pass=${{ secrets.OBS_PASSWORD }}" >> ~/.config/osc/oscrc echo "credentials_mgr_class=osc.credentials.PlaintextConfigFileCredentialsManager" >> ~/.config/osc/oscrc # Create a temporary directory for osc checkout mkdir -p osc # Intentionally fail if credentials are invalid # Update secrets if this returns `401` - name: Verify OBS authentication run: osc token - name: Upload package to OBS shell: bash working-directory: osc env: OBS_PROJECT: ${{ inputs.obs_project }} OBS_PACKAGE: meshtasticd run: | # Initialize the package in the current directory osc checkout --output-dir . $OBS_PROJECT $OBS_PACKAGE # Remove the existing package files rm -rf *.dsc *.tar.xz # Copy new package files to the directory cp $GITHUB_WORKSPACE/*.dsc . cp $GITHUB_WORKSPACE/*.tar.xz . # Add/Remove the files osc addremove # Commit changes and push to OpenSUSE Build Service osc commit -m "GitHub Actions: ${{ steps.version.outputs.deb }}~${{ inputs.series }}" ================================================ FILE: .github/workflows/package_pio_deps.yml ================================================ name: Package PlatformIO Library Dependencies # trunk-ignore-all(checkov/CKV_GHA_7): Allow workflow_dispatch inputs for testing on: workflow_call: inputs: pio_env: description: PlatformIO environment to target required: true type: string workflow_dispatch: inputs: pio_env: description: PlatformIO environment to target required: true type: string permissions: contents: write packages: write jobs: pkg-pio-libdeps: runs-on: ubuntu-24.04 steps: - name: Checkout code uses: actions/checkout@v6 with: submodules: recursive ref: ${{github.event.pull_request.head.ref}} repository: ${{github.event.pull_request.head.repo.full_name}} - name: Setup Python uses: actions/setup-python@v6 with: python-version: 3.x - name: Install deps shell: bash run: | pip install platformio - name: Get release version string run: | echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT id: version - name: Fetch libdeps shell: bash run: |- platformio pkg install -e ${{ inputs.pio_env }} platformio pkg install -e ${{ inputs.pio_env }} -t platformio/tool-scons@4.40502.0 env: PLATFORMIO_LIBDEPS_DIR: pio/libdeps PLATFORMIO_PACKAGES_DIR: pio/packages PLATFORMIO_CORE_DIR: pio/core PLATFORMIO_SETTING_ENABLE_TELEMETRY: 0 PLATFORMIO_SETTING_CHECK_PLATFORMIO_INTERVAL: 3650 PLATFORMIO_SETTING_CHECK_PRUNE_SYSTEM_THRESHOLD: 10240 - name: Mangle platformio cache # Add "1" to epoch-timestamps of all downloads in the cache. # This is a hack to prevent internet access at build-time. run: | cp pio/core/.cache/downloads/usage.db pio/core/.cache/downloads/usage.db.bak jq -c 'with_entries(.value |= (. | tostring + "1" | tonumber))' pio/core/.cache/downloads/usage.db.bak > pio/core/.cache/downloads/usage.db - name: Store binaries as an artifact uses: actions/upload-artifact@v7 with: name: platformio-deps-${{ inputs.pio_env }}-${{ steps.version.outputs.long }} overwrite: true include-hidden-files: true path: | pio/* ================================================ FILE: .github/workflows/package_ppa.yml ================================================ name: Package for Launchpad PPA on: workflow_call: secrets: PPA_GPG_PRIVATE_KEY: required: true inputs: ppa_repo: description: Meshtastic PPA to target required: true type: string series: description: Ubuntu series to target required: true type: string permissions: contents: write packages: write jobs: build-debian-src: uses: ./.github/workflows/build_debian_src.yml secrets: inherit with: series: ${{ inputs.series }} build_location: ppa package-ppa: runs-on: ubuntu-24.04 needs: build-debian-src steps: - name: Checkout code uses: actions/checkout@v6 with: submodules: recursive path: meshtasticd ref: ${{github.event.pull_request.head.ref}} repository: ${{github.event.pull_request.head.repo.full_name}} - name: Install deps shell: bash run: | sudo apt-get update -y --fix-missing sudo apt-get install -y dput - name: Import GPG key uses: crazy-max/ghaction-import-gpg@v7 with: gpg_private_key: ${{ secrets.PPA_GPG_PRIVATE_KEY }} id: gpg - name: Get release version string working-directory: meshtasticd run: | echo "deb=$(./bin/buildinfo.py deb)" >> $GITHUB_OUTPUT env: BUILD_LOCATION: ppa id: version - name: Download artifacts uses: actions/download-artifact@v8 with: name: firmware-debian-${{ steps.version.outputs.deb }}~${{ inputs.series }}-src merge-multiple: true - name: Display structure of downloaded files run: ls -lah - name: Publish with dput if: ${{ github.event_name != 'pull_request_target' && github.event_name != 'pull_request' }} run: | dput ${{ inputs.ppa_repo }} meshtasticd_${{ steps.version.outputs.deb }}~${{ inputs.series }}_source.changes ================================================ FILE: .github/workflows/pr_enforce_labels.yml ================================================ name: Check PR Labels on: pull_request: types: [opened, edited, labeled, unlabeled, synchronize, reopened] permissions: pull-requests: read contents: read jobs: check-label: runs-on: ubuntu-latest steps: - name: Check for PR labels uses: actions/github-script@v8 with: script: | const labels = context.payload.pull_request.labels.map(label => label.name); const requiredLabels = ['bugfix', 'enhancement', 'hardware-support', 'dependencies', 'submodules', 'github_actions', 'trunk', 'cleanup']; const hasRequiredLabel = labels.some(label => requiredLabels.includes(label)); if (!hasRequiredLabel) { core.setFailed(`PR must have at least one of the following labels before it can be merged: ${requiredLabels.join(', ')}.`); } ================================================ FILE: .github/workflows/pr_tests.yml ================================================ name: Tests # DISABLED: Changed from automatic PR triggers to manual only on: workflow_dispatch: inputs: reason: description: "Reason for manual test run" required: false default: "Manual test execution" concurrency: group: tests-${{ github.head_ref || github.run_id }} cancel-in-progress: true permissions: contents: read actions: read checks: write pull-requests: write jobs: native-tests: name: "🧪 Native Tests" if: github.repository == 'meshtastic/firmware' uses: ./.github/workflows/test_native.yml permissions: contents: read actions: read checks: write test-summary: name: "📊 Test Results" runs-on: ubuntu-latest needs: [native-tests] if: always() permissions: contents: read actions: read checks: write pull-requests: write steps: - uses: actions/checkout@v6 with: submodules: recursive - name: Get release version string run: echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT id: version - name: Download test artifacts if: needs.native-tests.result != 'skipped' uses: actions/download-artifact@v8 with: name: platformio-test-report-${{ steps.version.outputs.long }} merge-multiple: true - name: Parse test results and create detailed summary id: test-results run: | echo "## 🧪 Test Results Summary" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY # Check overall job status first if [[ "${{ needs.native-tests.result }}" == "success" ]]; then echo "✅ **Overall Status**: PASSED" >> $GITHUB_STEP_SUMMARY elif [[ "${{ needs.native-tests.result }}" == "failure" ]]; then echo "❌ **Overall Status**: FAILED" >> $GITHUB_STEP_SUMMARY elif [[ "${{ needs.native-tests.result }}" == "cancelled" ]]; then echo "⏸️ **Overall Status**: CANCELLED" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "Tests were cancelled before completion." >> $GITHUB_STEP_SUMMARY exit 0 else echo "⚠️ **Overall Status**: SKIPPED" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "Tests were skipped." >> $GITHUB_STEP_SUMMARY exit 0 fi echo "" >> $GITHUB_STEP_SUMMARY # Parse detailed test results if available if [ -f "testreport.xml" ]; then echo "### 🔍 Individual Test Results" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY python3 << 'EOF' import xml.etree.ElementTree as ET import os try: tree = ET.parse('testreport.xml') root = tree.getroot() total_tests = 0 passed_tests = 0 failed_tests = 0 skipped_tests = 0 # Parse testsuite elements for testsuite in root.findall('.//testsuite'): suite_name = testsuite.get('name', 'Unknown') suite_tests = int(testsuite.get('tests', '0')) suite_failures = int(testsuite.get('failures', '0')) suite_errors = int(testsuite.get('errors', '0')) suite_skipped = int(testsuite.get('skipped', '0')) total_tests += suite_tests failed_tests += suite_failures + suite_errors skipped_tests += suite_skipped passed_tests += suite_tests - suite_failures - suite_errors - suite_skipped if suite_tests > 0: status = "✅" if (suite_failures + suite_errors) == 0 else "❌" print(f"**{status} Test Suite: {suite_name}**") print(f"- Total: {suite_tests}") print(f"- Passed: ✅ {suite_tests - suite_failures - suite_errors - suite_skipped}") print(f"- Failed: ❌ {suite_failures + suite_errors}") if suite_skipped > 0: print(f"- Skipped: ⏭️ {suite_skipped}") print("") # Show individual test results for failed suites if suite_failures + suite_errors > 0: print("**Failed Tests:**") for testcase in testsuite.findall('testcase'): test_name = testcase.get('name', 'Unknown') failure = testcase.find('failure') error = testcase.find('error') if failure is not None: msg = failure.get('message', 'Unknown error')[:100] print(f"- ❌ `{test_name}`: {msg}") elif error is not None: msg = error.get('message', 'Unknown error')[:100] print(f"- ❌ `{test_name}`: ERROR - {msg}") print("") else: # Show passed tests for successful suites passed_count = 0 for testcase in testsuite.findall('testcase'): if testcase.find('failure') is None and testcase.find('error') is None: if passed_count < 5: # Limit to first 5 to avoid spam test_name = testcase.get('name', 'Unknown') print(f"- ✅ `{test_name}`: PASSED") passed_count += 1 if passed_count > 5: print(f"- ... and {passed_count - 5} more tests passed") print("") # Summary statistics print("### 📊 Test Statistics") print(f"- **Total Tests**: {total_tests}") print(f"- **Passed**: ✅ {passed_tests}") print(f"- **Failed**: ❌ {failed_tests}") if skipped_tests > 0: print(f"- **Skipped**: ⏭️ {skipped_tests}") if failed_tests > 0: print(f"\n❌ **{failed_tests} tests failed out of {total_tests} total**") else: print(f"\n✅ **All {total_tests} tests passed!**") except Exception as e: print(f"❌ Error parsing test results: {e}") EOF else echo "⚠️ **No detailed test report available**" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "Test artifacts may not have been generated properly." >> $GITHUB_STEP_SUMMARY fi echo "" >> $GITHUB_STEP_SUMMARY echo "---" >> $GITHUB_STEP_SUMMARY echo "View detailed logs in the [Actions tab](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})" >> $GITHUB_STEP_SUMMARY - name: Comment test results on PR if: github.event_name == 'pull_request' && needs.native-tests.result != 'skipped' uses: actions/github-script@v8 with: script: | const fs = require('fs'); // Read the step summary to use as PR comment let testSummary = "## 🧪 Test Results Summary\n\n"; if ("${{ needs.native-tests.result }}" === "success") { testSummary += "✅ **All tests passed!**\n\n"; } else if ("${{ needs.native-tests.result }}" === "failure") { testSummary += "❌ **Some tests failed.**\n\n"; } else { testSummary += "⚠️ **Tests did not complete normally.**\n\n"; } testSummary += `View detailed results: [Actions Run](${context.payload.repository.html_url}/actions/runs/${context.runId})\n\n`; testSummary += "---\n"; testSummary += "*This comment will be automatically updated when new commits are pushed.*"; // Find existing comment const comments = await github.rest.issues.listComments({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number }); const botComment = comments.data.find(comment => comment.user.type === 'Bot' && comment.body.includes('🧪 Test Results Summary') ); if (botComment) { // Update existing comment await github.rest.issues.updateComment({ owner: context.repo.owner, repo: context.repo.repo, comment_id: botComment.id, body: testSummary }); } else { // Create new comment await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, body: testSummary }); } - name: Set overall status run: | if [[ "${{ needs.native-tests.result }}" == "success" ]]; then echo "All tests passed! ✅" exit 0 else echo "Some tests failed! ❌" exit 1 fi ================================================ FILE: .github/workflows/release_channels.yml ================================================ name: Trigger release workflows upon Publish on: release: types: [published, released] permissions: contents: write packages: write jobs: build-docker: uses: ./.github/workflows/docker_manifest.yml with: release_channel: |- ${{ contains(github.event.release.name, 'Beta') && 'beta' || contains(github.event.release.name, 'Alpha') && 'alpha' }} secrets: inherit package-ppa: strategy: fail-fast: false matrix: series: - jammy # 22.04 LTS - noble # 24.04 LTS - questing # 25.10 - resolute # 26.04 LTS uses: ./.github/workflows/package_ppa.yml with: ppa_repo: |- ppa:meshtastic/${{ contains(github.event.release.name, 'Beta') && 'beta' || contains(github.event.release.name, 'Alpha') && 'alpha' }} series: ${{ matrix.series }} secrets: inherit package-obs: uses: ./.github/workflows/package_obs.yml with: obs_project: |- network:Meshtastic:${{ contains(github.event.release.name, 'Beta') && 'beta' || contains(github.event.release.name, 'Alpha') && 'alpha' }} series: |- ${{ contains(github.event.release.name, 'Beta') && 'beta' || contains(github.event.release.name, 'Alpha') && 'alpha' }} secrets: inherit hook-copr: uses: ./.github/workflows/hook_copr.yml with: copr_project: |- ${{ contains(github.event.release.name, 'Beta') && 'beta' || contains(github.event.release.name, 'Alpha') && 'alpha' }} secrets: inherit publish-release-notes: if: github.event.action == 'published' runs-on: ubuntu-latest steps: - name: Get release version id: version run: | # Extract version from tag (e.g., v2.7.15.567b8ea -> 2.7.15.567b8ea) VERSION=${GITHUB_REF#refs/tags/v} echo "version=$VERSION" >> $GITHUB_OUTPUT - name: Get release notes run: | mkdir -p ./publish gh release view ${{ github.event.release.tag_name }} --json body --jq '.body' > ./publish/release_notes.md env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Publish release notes to meshtastic.github.io uses: peaceiris/actions-gh-pages@v4 with: deploy_key: ${{ secrets.DIST_PAGES_DEPLOY_KEY }} external_repository: meshtastic/meshtastic.github.io publish_branch: master publish_dir: ./publish destination_dir: firmware-${{ steps.version.outputs.version }} user_name: github-actions[bot] user_email: github-actions[bot]@users.noreply.github.com commit_message: Release notes for ${{ steps.version.outputs.version }} enable_jekyll: true # Create a PR to bump version when a release is Published bump-version: if: github.event.action == 'published' runs-on: ubuntu-latest permissions: pull-requests: write contents: write defaults: run: shell: bash steps: - name: Checkout uses: actions/checkout@v6 with: # Always use master branch for version bumps ref: master - name: Setup Python uses: actions/setup-python@v6 with: python-version: 3.x - name: Bump version.properties run: | # Bump version.properties chmod +x ./bin/bump_version.py ./bin/bump_version.py - name: Get new release version string run: | echo "short=$(./bin/buildinfo.py short)" >> $GITHUB_OUTPUT id: new_version - name: Ensure debian deps are installed run: | sudo apt-get update -y --fix-missing sudo apt-get install -y devscripts - name: Update debian changelog run: | # Update debian changelog chmod +x ./debian/ci_changelog.sh ./debian/ci_changelog.sh - name: Bump org.meshtastic.meshtasticd.metainfo.xml run: | # Bump org.meshtastic.meshtasticd.metainfo.xml pip install -r bin/bump_metainfo/requirements.txt -q chmod +x ./bin/bump_metainfo/bump_metainfo.py ./bin/bump_metainfo/bump_metainfo.py --file bin/org.meshtastic.meshtasticd.metainfo.xml "${{ steps.new_version.outputs.short }}" env: PIP_DISABLE_PIP_VERSION_CHECK: 1 - name: Create Bumps pull request uses: peter-evans/create-pull-request@v8 with: base: ${{ github.event.repository.default_branch }} branch: create-pull-request/bump-version labels: github_actions title: Bump release version commit-message: Automated version bumps add-paths: | version.properties debian/changelog bin/org.meshtastic.meshtasticd.metainfo.xml ================================================ FILE: .github/workflows/sec_sast_semgrep_cron.yml ================================================ --- name: Semgrep Full Scan on: workflow_dispatch: schedule: - cron: 0 1 * * 6 permissions: actions: read contents: read security-events: write jobs: semgrep-full: if: github.repository == 'meshtastic/firmware' runs-on: ubuntu-24.04 container: image: semgrep/semgrep steps: # step 1 - name: clone application source code uses: actions/checkout@v6 # step 2 - name: full scan run: | semgrep \ --sarif --output report.sarif \ --metrics=off \ --config="p/default" # step 3 - name: save report as pipeline artifact uses: actions/upload-artifact@v7 with: name: report.sarif overwrite: true path: report.sarif # step 4 - name: publish code scanning alerts uses: github/codeql-action/upload-sarif@v4 with: sarif_file: report.sarif category: semgrep ================================================ FILE: .github/workflows/sec_sast_semgrep_pull.yml ================================================ --- name: Semgrep Differential Scan on: pull_request permissions: read-all jobs: semgrep-diff: runs-on: ubuntu-24.04 container: image: semgrep/semgrep steps: # step 1 - name: clone application source code uses: actions/checkout@v6 with: fetch-depth: 0 # step 2 - name: differential scan run: | semgrep scan \ --error \ --metrics=off \ --baseline-commit ${{ github.event.pull_request.base.sha }} \ --config="p/default" ================================================ FILE: .github/workflows/stale_bot.yml ================================================ name: process stale Issues and PR's on: schedule: - cron: 0 6 * * * workflow_dispatch: {} permissions: issues: write pull-requests: write actions: write jobs: stale_issues: if: github.repository == 'meshtastic/firmware' name: Close Stale Issues runs-on: ubuntu-latest steps: - name: Stale PR+Issues uses: actions/stale@v10.2.0 with: days-before-stale: 45 stale-issue-message: This issue has not had any comment or update in the last month. If it is still relevant, please post update comments. If no comments are made, this issue will be closed automagically in 7 days. close-issue-message: This issue has not had any comment since the last notice. It has been closed automatically. If this is incorrect, or the issue becomes relevant again, please request that it is reopened. exempt-issue-labels: pinned,3.0,triaged,backlog exempt-pr-labels: pinned,3.0,triaged,backlog ================================================ FILE: .github/workflows/test_native.yml ================================================ name: Run Tests on Native platform on: workflow_call: workflow_dispatch: permissions: {} env: LCOV_CAPTURE_FLAGS: --quiet --capture --include "${PWD}/src/*" --exclude '*/src/mesh/generated/*' --directory .pio/build/coverage/src --base-directory "${PWD}" jobs: simulator-tests: name: Native Simulator Tests runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 with: ref: ${{github.event.pull_request.head.ref}} repository: ${{github.event.pull_request.head.repo.full_name}} submodules: recursive - name: Setup native build id: base uses: ./.github/actions/setup-native - name: Install simulator dependencies run: pip install -U dotmap # We now run integration test before other build steps (to quickly see runtime failures) - name: Build for native/coverage run: platformio run -e coverage - name: Capture initial coverage information shell: bash run: | sudo apt-get install -y lcov lcov ${{ env.LCOV_CAPTURE_FLAGS }} --initial --output-file coverage_base.info sed -i -e "s#${PWD}#.#" coverage_base.info # Make paths relative. - name: Integration test run: | .pio/build/coverage/meshtasticd -s & PID=$! timeout 20 bash -c "until ls -al /proc/$PID/fd | grep socket; do sleep 1; done" echo "Simulator started, launching python test..." python3 -c 'from meshtastic.test import testSimulator; testSimulator()' wait - name: Capture coverage information if: always() # run this step even if previous step failed run: | lcov ${{ env.LCOV_CAPTURE_FLAGS }} --test-name integration --output-file coverage_integration.info sed -i -e "s#${PWD}#.#" coverage_integration.info # Make paths relative. - name: Get release version string if: always() # run this step even if previous step failed run: echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT id: version - name: Save coverage information uses: actions/upload-artifact@v7 if: always() # run this step even if previous step failed with: name: lcov-coverage-info-native-simulator-test-${{ steps.version.outputs.long }} overwrite: true path: ./coverage_*.info platformio-tests: name: Native PlatformIO Tests runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 with: ref: ${{github.event.pull_request.head.ref}} repository: ${{github.event.pull_request.head.repo.full_name}} submodules: recursive - name: Setup native build id: base uses: ./.github/actions/setup-native - name: Get release version string run: echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT id: version # Disable (comment-out) BUILD_EPOCH. It causes a full rebuild between tests and resets the # coverage information each time. - name: Disable BUILD_EPOCH run: sed -i 's/-DBUILD_EPOCH=$UNIX_TIME/#-DBUILD_EPOCH=$UNIX_TIME/' platformio.ini - name: PlatformIO Tests run: platformio test -e coverage -v --junit-output-path testreport.xml - name: Save test results if: always() # run this step even if previous step failed uses: actions/upload-artifact@v7 with: name: platformio-test-report-${{ steps.version.outputs.long }} overwrite: true path: ./testreport.xml - name: Capture coverage information if: always() # run this step even if previous step failed run: | sudo apt-get install -y lcov lcov ${{ env.LCOV_CAPTURE_FLAGS }} --test-name tests --output-file coverage_tests.info sed -i -e "s#${PWD}#.#" coverage_tests.info # Make paths relative. - name: Save coverage information uses: actions/upload-artifact@v7 if: always() # run this step even if previous step failed with: name: lcov-coverage-info-native-platformio-tests-${{ steps.version.outputs.long }} overwrite: true path: ./coverage_*.info generate-reports: name: Generate Test Reports runs-on: ubuntu-latest permissions: # Needed for dorny/test-reporter. contents: read actions: read checks: write needs: - simulator-tests - platformio-tests if: always() steps: - uses: actions/checkout@v6 with: ref: ${{github.event.pull_request.head.ref}} repository: ${{github.event.pull_request.head.repo.full_name}} - name: Get release version string run: echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT id: version - name: Download test artifacts uses: actions/download-artifact@v8 with: name: platformio-test-report-${{ steps.version.outputs.long }} merge-multiple: true - name: Test Report uses: dorny/test-reporter@v2.6.0 with: name: PlatformIO Tests path: testreport.xml reporter: java-junit - name: Download coverage artifacts uses: actions/download-artifact@v8 with: pattern: lcov-coverage-info-native-*-${{ steps.version.outputs.long }} path: code-coverage-report merge-multiple: true - name: Generate Code Coverage Report run: | sudo apt-get install -y lcov lcov --quiet --add-tracefile code-coverage-report/coverage_base.info --add-tracefile code-coverage-report/coverage_integration.info --add-tracefile code-coverage-report/coverage_tests.info --output-file code-coverage-report/coverage_src.info genhtml --quiet --legend --prefix "${PWD}" code-coverage-report/coverage_src.info --output-directory code-coverage-report - name: Save Code Coverage Report uses: actions/upload-artifact@v7 with: name: code-coverage-report-${{ steps.version.outputs.long }} path: code-coverage-report ================================================ FILE: .github/workflows/tests.yml ================================================ name: End to end tests on: schedule: - cron: 0 0 * * * # Run every day at midnight workflow_dispatch: {} permissions: contents: read actions: read checks: write jobs: native-tests: if: github.repository == 'meshtastic/firmware' uses: ./.github/workflows/test_native.yml hardware-tests: if: github.repository == 'meshtastic/firmware' runs-on: test-runner steps: - name: Checkout code uses: actions/checkout@v6 # - uses: actions/setup-python@v6 # with: # python-version: '3.10' # pipx install "setuptools<72" - name: Upgrade python tools shell: bash run: | pipx install adafruit-nrfutil pipx install poetry pipx install meshtastic --pip-args=--pre - name: Install PlatformIO from script shell: bash run: | curl -fsSL -o get-platformio.py https://raw.githubusercontent.com/platformio/platformio-core-installer/master/get-platformio.py python3 get-platformio.py - name: Upgrade platformio shell: bash run: | export PATH=$PATH:$HOME/.local/bin pio upgrade - name: Setup Node uses: actions/setup-node@v6 with: node-version: 24 - name: Setup pnpm uses: pnpm/action-setup@v5 with: version: latest - name: Install dependencies, setup devices and run shell: bash run: | git submodule update --init --recursive cd meshtestic/ pnpm install pnpm run setup pnpm run test ================================================ FILE: .github/workflows/trunk_annotate_pr.yml ================================================ name: Annotate PR with trunk issues # See: https://github.com/trunk-io/trunk-action/blob/v1/readme.md#getting-inline-annotations-for-fork-prs on: workflow_run: workflows: [Pull Request] # Name from `trunk_check.yml` types: [completed] permissions: read-all jobs: trunk_check: name: Trunk Code Quality Annotate runs-on: ubuntu-24.04 permissions: checks: write # For trunk to post annotations contents: read # For repo checkout steps: - name: Checkout uses: actions/checkout@v6 - name: Trunk Check uses: trunk-io/trunk-action@v1 with: post-annotations: true ================================================ FILE: .github/workflows/trunk_check.yml ================================================ name: Pull Request on: [pull_request] concurrency: group: ${{ github.head_ref || github.run_id }} cancel-in-progress: true permissions: read-all jobs: trunk_check: name: Trunk Check Runner runs-on: ubuntu-24.04 permissions: checks: write # For trunk to post annotations contents: read # For repo checkout steps: - name: Checkout uses: actions/checkout@v6 - name: Trunk Check uses: trunk-io/trunk-action@v1 with: save-annotations: true ================================================ FILE: .github/workflows/update_protobufs.yml ================================================ name: Update protobufs and regenerate classes on: workflow_dispatch permissions: read-all jobs: update-protobufs: runs-on: ubuntu-latest permissions: contents: write pull-requests: write steps: - name: Checkout code uses: actions/checkout@v6 with: submodules: true - name: Update submodule if: ${{ github.ref == 'refs/heads/master' || github.ref == 'refs/heads/develop' }} run: | git submodule update --remote protobufs - name: Download nanopb run: | wget https://jpa.kapsi.fi/nanopb/download/nanopb-0.4.9.1-linux-x86.tar.gz tar xvzf nanopb-0.4.9.1-linux-x86.tar.gz mv nanopb-0.4.9.1-linux-x86 nanopb-0.4.9 - name: Re-generate protocol buffers run: | ./bin/regen-protos.sh - name: Create pull request uses: peter-evans/create-pull-request@v8 with: branch: create-pull-request/update-protobufs labels: submodules title: Update protobufs and classes commit-message: Update protobufs add-paths: | protobufs src/mesh ================================================ FILE: .gitignore ================================================ .pio pio pio.tar web web.tar # ignore vscode IDE settings files .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/extensions.json *.code-workspace .idea .platformio .local .cache .DS_Store Thumbs.db .autotools .built .context .cproject .vagrant nanopb* flash.uf2 cmake-build* __pycache__ *.swp *.swo *~ venv/ .venv/ release/ .vscode/extensions.json /compile_commands.json src/mesh/raspihttp/certificate.pem src/mesh/raspihttp/private_key.pem # Ignore logo (set at build time with platformio-custom.py) data/boot/logo.* # pioarduino platform managed_components/* arduino-lib-builder* dependencies.lock idf_component.yml CMakeLists.txt /sdkconfig.* .dummy/* # PYTHONPATH used by the Nix shell .python3 ================================================ FILE: .gitmodules ================================================ [submodule "protobufs"] path = protobufs url = https://github.com/meshtastic/protobufs.git [submodule "meshtestic"] path = meshtestic url = https://github.com/meshtastic/meshTestic ================================================ FILE: .gitpod.yml ================================================ tasks: - init: pip install platformio && pip install --upgrade pip ================================================ FILE: .semgrepignore ================================================ .github/workflows/main_matrix.yml src/mesh/compression/unishox2.cpp ================================================ FILE: .trunk/.gitignore ================================================ *out *logs *actions *notifications *tools plugins user_trunk.yaml user.yaml tmp ================================================ FILE: .trunk/configs/.bandit ================================================ [bandit] skips = B101 ================================================ FILE: .trunk/configs/.clang-format ================================================ Language: Cpp IndentWidth: 4 ColumnLimit: 130 PointerAlignment: Right BreakBeforeBraces: Linux AllowShortFunctionsOnASingleLine: Inline ================================================ FILE: .trunk/configs/.flake8 ================================================ # Autoformatter friendly flake8 config (all formatting rules disabled) [flake8] extend-ignore = D1, D2, E1, E2, E3, E501, W1, W2, W3, W5 ================================================ FILE: .trunk/configs/.hadolint.yaml ================================================ # Following source doesn't work in most setups ignored: - SC1090 - SC1091 ================================================ FILE: .trunk/configs/.isort.cfg ================================================ [settings] profile=black ================================================ FILE: .trunk/configs/.markdownlint.yaml ================================================ # Autoformatter friendly markdownlint config (all formatting rules disabled) default: true blank_lines: false bullet: false html: false indentation: false line_length: false spaces: false url: false whitespace: false headings: false ================================================ FILE: .trunk/configs/.prettierignore ================================================ renovate.json ================================================ FILE: .trunk/configs/.prettierrc ================================================ { "overrides": [ { "files": "userPrefs.jsonc", "options": { "trailingComma": "none" } } ] } ================================================ FILE: .trunk/configs/.shellcheckrc ================================================ enable=all source-path=SCRIPTDIR disable=SC2154 disable=SC2248 disable=SC2250 # If you're having issues with shellcheck following source, disable the errors via: # disable=SC1090 # disable=SC1091 # ================================================ FILE: .trunk/configs/.yamllint.yaml ================================================ rules: quoted-strings: required: only-when-needed extra-allowed: ["{|}"] empty-values: forbid-in-block-mappings: false forbid-in-flow-mappings: true key-duplicates: {} octal-values: forbid-implicit-octal: true ================================================ FILE: .trunk/configs/ruff.toml ================================================ # Generic, formatter-friendly config. select = ["B", "D3", "D4", "E", "F"] # Never enforce `E501` (line length violations). This should be handled by formatters. ignore = ["E501"] ================================================ FILE: .trunk/configs/svgo.config.js ================================================ module.exports = { plugins: [ { name: "preset-default", params: { overrides: { removeViewBox: false, // https://github.com/svg/svgo/issues/1128 sortAttrs: true, removeOffCanvasPaths: true, }, }, }, ], }; ================================================ FILE: .trunk/trunk.yaml ================================================ version: 0.1 cli: version: 1.25.0 plugins: sources: - id: trunk ref: v1.7.6 uri: https://github.com/trunk-io/plugins lint: enabled: - checkov@3.2.510 - renovate@43.78.0 - prettier@3.8.1 - trufflehog@3.93.8 - yamllint@1.38.0 - bandit@1.9.4 - trivy@0.69.3 - taplo@0.10.0 - ruff@0.15.6 - isort@8.0.1 - markdownlint@0.48.0 - oxipng@10.1.0 - svgo@4.0.1 - actionlint@1.7.11 - flake8@7.3.0 - hadolint@2.14.0 - shfmt@3.6.0 - shellcheck@0.11.0 - black@26.3.1 - git-diff-check - gitleaks@8.30.0 - clang-format@16.0.3 ignore: - linters: [ALL] paths: - bin/** runtimes: enabled: - python@3.10.8 - go@1.21.0 - node@22.16.0 actions: disabled: - trunk-announce enabled: - trunk-fmt-pre-commit - trunk-check-pre-push - trunk-upgrade-available ================================================ FILE: .vscode/settings.json ================================================ { "editor.formatOnSave": true, "editor.defaultFormatter": "trunk.io", "trunk.enableWindows": true, "files.insertFinalNewline": false, "files.trimFinalNewlines": false, "cmake.configureOnOpen": false, "[cpp]": { "editor.defaultFormatter": "trunk.io" }, "[powershell]": { "editor.defaultFormatter": "ms-vscode.powershell" } } ================================================ FILE: .vscode/tasks.json ================================================ { "version": "2.0.0", "tasks": [ { "type": "PlatformIO", "task": "Build", "problemMatcher": ["$platformio"], "group": { "kind": "build", "isDefault": true }, "label": "PlatformIO: Build" } ] } ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct The Meshtastic Firmware project is subject to the code of conduct for the parent project, which can be found here: https://meshtastic.org/docs/legal/conduct/ ================================================ FILE: CONTRIBUTING.md ================================================ # 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! ================================================ FILE: Dockerfile ================================================ # trunk-ignore-all(trivy/DS002): We must run as root for this container # trunk-ignore-all(hadolint/DL3002): We must run as root for this container # trunk-ignore-all(hadolint/DL3008): Do not pin apt package versions # trunk-ignore-all(hadolint/DL3013): Do not pin pip package versions FROM python:3.14-slim-trixie AS builder ARG PIO_ENV=native ENV DEBIAN_FRONTEND=noninteractive ENV TZ=Etc/UTC # Install Dependencies ENV PIP_ROOT_USER_ACTION=ignore RUN apt-get update && apt-get install --no-install-recommends -y \ curl wget g++ zip git ca-certificates pkg-config \ libgpiod-dev libyaml-cpp-dev libbluetooth-dev libi2c-dev libuv1-dev \ libusb-1.0-0-dev libulfius-dev liborcania-dev libssl-dev \ libx11-dev libinput-dev libxkbcommon-x11-dev libsqlite3-dev libsdl2-dev \ && apt-get clean && rm -rf /var/lib/apt/lists/* \ && pip install --no-cache-dir -U platformio \ && mkdir /tmp/firmware # Copy source code WORKDIR /tmp/firmware COPY . /tmp/firmware # Build RUN bash ./bin/build-native.sh "$PIO_ENV" && \ cp "/tmp/firmware/release/meshtasticd_linux_$(uname -m)" "/tmp/firmware/release/meshtasticd" # Fetch web assets RUN curl -L "https://github.com/meshtastic/web/releases/download/v$(cat /tmp/firmware/bin/web.version)/build.tar" -o /tmp/web.tar \ && mkdir -p /tmp/web \ && tar -xf /tmp/web.tar -C /tmp/web/ \ && gzip -dr /tmp/web \ && rm /tmp/web.tar ##### PRODUCTION BUILD ############# FROM debian:trixie-slim LABEL org.opencontainers.image.title="Meshtastic" \ org.opencontainers.image.description="Debian Meshtastic daemon and web interface" \ org.opencontainers.image.url="https://meshtastic.org" \ org.opencontainers.image.documentation="https://meshtastic.org/docs/" \ org.opencontainers.image.authors="Meshtastic" \ org.opencontainers.image.licenses="GPL-3.0-or-later" \ org.opencontainers.image.source="https://github.com/meshtastic/firmware/" ENV DEBIAN_FRONTEND=noninteractive ENV TZ=Etc/UTC # nosemgrep: dockerfile.security.last-user-is-root.last-user-is-root USER root RUN apt-get update && apt-get --no-install-recommends -y install \ libc-bin libc6 libgpiod3 libyaml-cpp0.8 libi2c0 libuv1t64 libusb-1.0-0-dev \ liborcania2.3 libulfius2.7t64 libssl3t64 \ libx11-6 libinput10 libxkbcommon-x11-0 libsdl2-2.0-0 \ && apt-get clean && rm -rf /var/lib/apt/lists/* \ && mkdir -p /var/lib/meshtasticd \ && mkdir -p /etc/meshtasticd/config.d \ && mkdir -p /etc/meshtasticd/ssl # Fetch compiled binary from the builder COPY --from=builder /tmp/firmware/release/meshtasticd /usr/bin/ COPY --from=builder /tmp/web /usr/share/meshtasticd/web/ # Copy config templates COPY ./bin/config.d /etc/meshtasticd/available.d WORKDIR /var/lib/meshtasticd VOLUME /var/lib/meshtasticd # Expose Meshtastic TCP API port from the host EXPOSE 4403 # Expose Meshtastic Web UI port from the host EXPOSE 9443 CMD [ "sh", "-cx", "meshtasticd --fsdir=/var/lib/meshtasticd" ] HEALTHCHECK NONE ================================================ FILE: Dockerfile.test ================================================ # Lightweight container for running native PlatformIO tests on non-Linux hosts FROM python:3.14-slim-trixie ENV DEBIAN_FRONTEND=noninteractive ENV PIP_ROOT_USER_ACTION=ignore # hadolint ignore=DL3008 RUN apt-get update && apt-get install --no-install-recommends -y \ g++ git ca-certificates pkg-config \ libgpiod-dev libyaml-cpp-dev libbluetooth-dev libi2c-dev libuv1-dev \ libusb-1.0-0-dev libulfius-dev liborcania-dev libssl-dev \ libx11-dev libinput-dev libxkbcommon-x11-dev libsqlite3-dev libsdl2-dev \ && apt-get clean && rm -rf /var/lib/apt/lists/* \ && pip install --no-cache-dir platformio==6.1.19 \ && useradd --create-home --shell /usr/sbin/nologin meshtastic WORKDIR /firmware RUN chown -R meshtastic:meshtastic /firmware HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ CMD platformio --version || exit 1 USER meshtastic # Run tests by default; override with docker run args for specific filters CMD ["platformio", "test", "-e", "coverage", "-v"] ================================================ FILE: LICENSE ================================================ 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 . ================================================ FILE: README.md ================================================
Meshtastic Logo

Meshtastic Firmware

![GitHub release downloads](https://img.shields.io/github/downloads/meshtastic/firmware/total) [![CI](https://img.shields.io/github/actions/workflow/status/meshtastic/firmware/main_matrix.yml?branch=master&label=actions&logo=github&color=yellow)](https://github.com/meshtastic/firmware/actions/workflows/ci.yml) [![CLA assistant](https://cla-assistant.io/readme/badge/meshtastic/firmware)](https://cla-assistant.io/meshtastic/firmware) [![Fiscal Contributors](https://opencollective.com/meshtastic/tiers/badge.svg?label=Fiscal%20Contributors&color=deeppink)](https://opencollective.com/meshtastic/) [![Vercel](https://img.shields.io/static/v1?label=Powered%20by&message=Vercel&style=flat&logo=vercel&color=000000)](https://vercel.com?utm_source=meshtastic&utm_campaign=oss) meshtastic%2Ffirmware | Trendshift
## Overview This repository contains the official device firmware for Meshtastic, an open-source LoRa mesh networking project designed for long-range, low-power communication without relying on internet or cellular infrastructure. The firmware supports various hardware platforms, including ESP32, nRF52, RP2040/RP2350, and Linux-based devices. Meshtastic enables text messaging, location sharing, and telemetry over a decentralized mesh network, making it ideal for outdoor adventures, emergency preparedness, and remote operations. ### Get Started - 🔧 **[Building Instructions](https://meshtastic.org/docs/development/firmware/build)** – Learn how to compile the firmware from source. - ⚡ **[Flashing Instructions](https://meshtastic.org/docs/getting-started/flashing-firmware/)** – Install or update the firmware on your device. Join our community and help improve Meshtastic! 🚀 ## Stats ![Alt](https://repobeats.axiom.co/api/embed/8025e56c482ec63541593cc5bd322c19d5c0bdcf.svg "Repobeats analytics image") ================================================ FILE: SECURITY.md ================================================ # Security Policy ## Supported Versions | Firmware Version | Supported | | ---------------- | ------------------ | | 2.7.x | :white_check_mark: | | <= 2.6.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. ================================================ FILE: alpine.Dockerfile ================================================ # trunk-ignore-all(trivy/DS002): We must run as root for this container # trunk-ignore-all(hadolint/DL3002): We must run as root for this container # trunk-ignore-all(hadolint/DL3018): Do not pin apk package versions # trunk-ignore-all(hadolint/DL3013): Do not pin pip package versions FROM python:3.14-alpine3.22 AS builder ARG PIO_ENV=native ENV PIP_ROOT_USER_ACTION=ignore RUN apk --no-cache add \ bash g++ libstdc++-dev linux-headers zip git ca-certificates libbsd-dev \ libgpiod-dev yaml-cpp-dev bluez-dev \ libusb-dev i2c-tools-dev libuv-dev openssl-dev pkgconf argp-standalone \ libx11-dev libinput-dev libxkbcommon-dev sqlite-dev sdl2-dev \ && rm -rf /var/cache/apk/* \ && pip install --no-cache-dir -U platformio \ && mkdir /tmp/firmware WORKDIR /tmp/firmware COPY . /tmp/firmware # Create small package (no debugging symbols) # Add `argp` for musl ENV PLATFORMIO_BUILD_FLAGS="-Os -ffunction-sections -fdata-sections -Wl,--gc-sections -largp" RUN bash ./bin/build-native.sh "$PIO_ENV" && \ cp "/tmp/firmware/release/meshtasticd_linux_$(uname -m)" "/tmp/firmware/release/meshtasticd" # ##### PRODUCTION BUILD ############# FROM alpine:3.23 LABEL org.opencontainers.image.title="Meshtastic" \ org.opencontainers.image.description="Alpine Meshtastic daemon" \ org.opencontainers.image.url="https://meshtastic.org" \ org.opencontainers.image.documentation="https://meshtastic.org/docs/" \ org.opencontainers.image.authors="Meshtastic" \ org.opencontainers.image.licenses="GPL-3.0-or-later" \ org.opencontainers.image.source="https://github.com/meshtastic/firmware/" # nosemgrep: dockerfile.security.last-user-is-root.last-user-is-root USER root RUN apk --no-cache add \ shadow libstdc++ libbsd libgpiod yaml-cpp libusb \ i2c-tools libuv libx11 libinput libxkbcommon sdl2 \ && rm -rf /var/cache/apk/* \ && mkdir -p /var/lib/meshtasticd \ && mkdir -p /etc/meshtasticd/config.d \ && mkdir -p /etc/meshtasticd/ssl # Fetch compiled binary from the builder COPY --from=builder /tmp/firmware/release/meshtasticd /usr/bin/ # Copy config templates COPY ./bin/config.d /etc/meshtasticd/available.d WORKDIR /var/lib/meshtasticd VOLUME /var/lib/meshtasticd EXPOSE 4403 CMD [ "sh", "-cx", "meshtasticd --fsdir=/var/lib/meshtasticd" ] HEALTHCHECK NONE ================================================ FILE: bin/.gitignore ================================================ config.yaml ================================================ FILE: bin/99-meshtasticd-udev.rules ================================================ # Set spidev ownership to 'spi' group SUBSYSTEM=="spidev", KERNEL=="spidev*", GROUP="spi", MODE="0660" # Allow access to USB CH341 devices SUBSYSTEM=="usb", ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="5512", MODE="0666" # Set gpio ownership to 'gpio' group SUBSYSTEM=="*gpiomem*", GROUP="gpio", MODE="0660" SUBSYSTEM=="gpio", GROUP="gpio", MODE="0660" ================================================ FILE: bin/analyze_map.py ================================================ #!/usr/bin/env python3 """Summarise linker map output to highlight heavy object files and libraries. Usage: python bin/analyze_map.py --map .pio/build/rak4631/output.map --top 20 The script parses GNU ld map files and aggregates section sizes per object file and per archive/library, then prints sortable tables that make it easy to spot modules worth trimming or hiding behind feature flags. """ from __future__ import annotations import argparse import collections import os import re import sys from typing import DefaultDict, Dict, Tuple SECTION_LINE_RE = re.compile(r"^\s+(?P
\S+)\s+0x[0-9A-Fa-f]+\s+0x(?P[0-9A-Fa-f]+)\s+(?P.+)$") ARCHIVE_MEMBER_RE = re.compile(r"^(?P.+)\((?P[^)]+)\)$") def human_size(num_bytes: int) -> str: """Return a friendly size string with one decimal place.""" if num_bytes < 1024: return f"{num_bytes:,} B" num = float(num_bytes) for unit in ("KB", "MB", "GB"): num /= 1024.0 if num < 1024.0: return f"{num:.1f} {unit}" return f"{num:.1f} TB" def shorten_path(path: str, root: str) -> str: """Prefer repository-relative paths for readability.""" path = path.strip() if not path: return path # Normalise Windows archives (backslashes) to POSIX style for consistency. path = path.replace("\\", "/") # Attempt to strip the root when an absolute path lives inside the repo. if os.path.isabs(path): try: rel = os.path.relpath(path, root) if not rel.startswith(".."): return rel except ValueError: # relpath can fail on mixed drives on Windows; fall back to basename. pass return path def describe_object(raw_object: str, root: str) -> Tuple[str, str]: """Return a human friendly object label and the library it belongs to.""" raw_object = raw_object.strip() lib_label = "[app]" match = ARCHIVE_MEMBER_RE.match(raw_object) if match: archive = shorten_path(match.group("archive"), root) obj = match.group("object") lib_label = os.path.basename(archive) or archive label = f"{archive}:{obj}" else: label = shorten_path(raw_object, root) # If the object lives under libs, hint at the containing directory. parent = os.path.basename(os.path.dirname(label)) if parent: lib_label = parent return label, lib_label def parse_map(map_path: str, repo_root: str) -> Tuple[Dict[str, int], Dict[str, int], Dict[str, Dict[str, int]]]: per_object: DefaultDict[str, int] = collections.defaultdict(int) per_library: DefaultDict[str, int] = collections.defaultdict(int) per_object_sections: DefaultDict[str, DefaultDict[str, int]] = collections.defaultdict(lambda: collections.defaultdict(int)) try: with open(map_path, "r", encoding="utf-8", errors="ignore") as handle: for line in handle: match = SECTION_LINE_RE.match(line) if not match: continue section = match.group("section") if section.startswith("*") or section in {"LOAD", "ORIGIN"}: continue size = int(match.group("size"), 16) if size == 0: continue obj_token = match.group("object").strip() if not obj_token or obj_token.startswith("*") or "load address" in obj_token: continue label, lib_label = describe_object(obj_token, repo_root) per_object[label] += size per_library[lib_label] += size per_object_sections[label][section] += size except FileNotFoundError: raise SystemExit(f"error: map file '{map_path}' not found. Run a build first.") return per_object, per_library, per_object_sections def format_section_breakdown(section_sizes: Dict[str, int], total: int, limit: int = 3) -> str: items = sorted(section_sizes.items(), key=lambda kv: kv[1], reverse=True) parts = [] for section, size in items[:limit]: pct = (size / total) * 100 if total else 0 parts.append(f"{section} {pct:.1f}%") if len(items) > limit: remainder = total - sum(size for _, size in items[:limit]) pct = (remainder / total) * 100 if total else 0 parts.append(f"other {pct:.1f}%") return ", ".join(parts) def print_report(map_path: str, top_n: int, per_object: Dict[str, int], per_library: Dict[str, int], per_object_sections: Dict[str, Dict[str, int]]): total_bytes = sum(per_object.values()) if total_bytes == 0: print("No section data found in map file.") return print(f"Map file: {map_path}") print(f"Accounted size: {human_size(total_bytes)} across {len(per_object)} object files\n") sorted_objects = sorted(per_object.items(), key=lambda kv: kv[1], reverse=True) print(f"Top {min(top_n, len(sorted_objects))} object files by linked size:") for idx, (obj, size) in enumerate(sorted_objects[:top_n], 1): pct = (size / total_bytes) * 100 breakdown = format_section_breakdown(per_object_sections[obj], size) print(f"{idx:2}. {human_size(size):>9} ({size:,} B, {pct:5.2f}% of linked size)") print(f" {obj}") if breakdown: print(f" sections: {breakdown}") print() sorted_libs = sorted(per_library.items(), key=lambda kv: kv[1], reverse=True) print(f"Top {min(top_n, len(sorted_libs))} libraries or source roots:") for idx, (lib, size) in enumerate(sorted_libs[:top_n], 1): pct = (size / total_bytes) * 100 print(f"{idx:2}. {human_size(size):>9} ({size:,} B, {pct:5.2f}% of linked size) {lib}") def main() -> None: parser = argparse.ArgumentParser(description="Highlight heavy object files from a GNU ld map file.") parser.add_argument("--map", default=".pio/build/rak4631/output.map", help="Path to the map file (default: %(default)s)") parser.add_argument("--top", type=int, default=20, help="Number of entries to display per table (default: %(default)s)") args = parser.parse_args() map_path = os.path.abspath(args.map) repo_root = os.path.abspath(os.getcwd()) per_object, per_library, per_object_sections = parse_map(map_path, repo_root) print_report(os.path.relpath(map_path, repo_root), args.top, per_object, per_library, per_object_sections) if __name__ == "__main__": main() ================================================ FILE: bin/base64_to_hex.py ================================================ import sys import base64 def base64_to_hex_string(b64_string): try: # Decode the Base64 string to raw bytes decoded_bytes = base64.b64decode(b64_string) except Exception as e: raise ValueError(f"Invalid Base64 input: {e}") # Check if the decoded result is exactly 32 bytes if len(decoded_bytes) != 32: raise ValueError("Decoded Base64 input must be exactly 32 bytes.") # Convert each byte to its hex representation hex_values = [f"0x{byte:02x}" for byte in decoded_bytes] # Join the formatted hex values with commas formatted_output = "{ " + ", ".join(hex_values) + " };" return formatted_output if __name__ == "__main__": # Check if a Base64 string was provided in command line arguments if len(sys.argv) != 2: print("Usage: python script.py ") sys.exit(1) b64_string = sys.argv[1] try: formatted_hex = base64_to_hex_string(b64_string) print(formatted_hex) except ValueError as e: print(e) ================================================ FILE: bin/build-esp32.sh ================================================ #!/usr/bin/env bash set -e VERSION=`bin/buildinfo.py long` SHORT_VERSION=`bin/buildinfo.py short` BUILDDIR=.pio/build/$1 OUTDIR=release rm -f $OUTDIR/firmware* rm -r $OUTDIR/* || true # Important to pull latest version of libs into all device flavors, otherwise some devices might be stale platformio pkg install -e $1 echo "Building for $1 with $PLATFORMIO_BUILD_FLAGS" rm -f $BUILDDIR/firmware* # The shell vars the build tool expects to find export APP_VERSION=$VERSION basename=firmware-$1-$VERSION pio run --environment $1 -t mtjson # -v cp $BUILDDIR/$basename.elf $OUTDIR/$basename.elf echo "Copying ESP32 bin file" cp $BUILDDIR/$basename.factory.bin $OUTDIR/$basename.factory.bin echo "Copying ESP32 update bin file" cp $BUILDDIR/$basename.bin $OUTDIR/$basename.bin echo "Copying Filesystem for ESP32 targets" cp $BUILDDIR/littlefs-$1-$VERSION.bin $OUTDIR/littlefs-$1-$VERSION.bin cp bin/device-install.* $OUTDIR/ cp bin/device-update.* $OUTDIR/ echo "Copying manifest" cp $BUILDDIR/$basename.mt.json $OUTDIR/$basename.mt.json || true ================================================ FILE: bin/build-firmware.sh ================================================ #!/usr/bin/env bash export PIP_BREAK_SYSTEM_PACKAGES=1 if (echo $2 | grep -q "esp32"); then bin/build-esp32.sh $1 elif (echo $2 | grep -q "nrf52"); then bin/build-nrf52.sh $1 elif (echo $2 | grep -q "stm32"); then bin/build-stm32.sh $1 elif (echo $2 | grep -q "rpi2040"); then bin/build-rp2xx0.sh $1 else echo "Unknown target $2" exit 1 fi ================================================ FILE: bin/build-native.sh ================================================ #!/usr/bin/env bash set -e platformioFailed() { [[ $VIRTUAL_ENV != "" ]] && exit 1 # don't hint at virtualenv if it's already in use echo -e "\nThere were issues running platformio and you are not using a virtual environment." \ "\nYou may try setting up virtualenv and downloading the latest platformio from pip:" \ "\n\tvirtualenv venv" \ "\n\tsource venv/bin/activate" \ "\n\tpip install platformio" \ "\n\t./bin/build-native.sh # retry building" exit 1 } VERSION=$(bin/buildinfo.py long) SHORT_VERSION=$(bin/buildinfo.py short) PIO_ENV=${1:-native} BUILDDIR=.pio/build/$PIO_ENV OUTDIR=release rm -f $OUTDIR/meshtasticd* mkdir -p $OUTDIR/ rm -r $OUTDIR/* || true basename=meshtasticd-$1-$VERSION # Important to pull latest version of libs into all device flavors, otherwise some devices might be stale pio pkg install --environment "$PIO_ENV" || platformioFailed pio run --environment "$PIO_ENV" || platformioFailed cp "$BUILDDIR/meshtasticd" "$OUTDIR/meshtasticd_linux_$(uname -m)" cp bin/native-install.* $OUTDIR/ ================================================ FILE: bin/build-nrf52.sh ================================================ #!/usr/bin/env bash set -e VERSION=$(bin/buildinfo.py long) SHORT_VERSION=$(bin/buildinfo.py short) BUILDDIR=.pio/build/$1 OUTDIR=release rm -f $OUTDIR/firmware* rm -r $OUTDIR/* || true # Important to pull latest version of libs into all device flavors, otherwise some devices might be stale platformio pkg install -e $1 echo "Building for $1 with $PLATFORMIO_BUILD_FLAGS" rm -f $BUILDDIR/firmware* # The shell vars the build tool expects to find export APP_VERSION=$VERSION basename=firmware-$1-$VERSION ota_basename=${basename}-ota pio run --environment $1 -t mtjson # -v cp $BUILDDIR/$basename.elf $OUTDIR/$basename.elf echo "Copying NRF52 dfu (OTA) file" cp $BUILDDIR/$basename.zip $OUTDIR/$ota_basename.zip echo "Copying NRF52 UF2 file" cp $BUILDDIR/$basename.uf2 $OUTDIR/$basename.uf2 cp bin/*.uf2 $OUTDIR/ SRCHEX=$BUILDDIR/$basename.hex # if WM1110 target, copy the merged.hex if (echo $1 | grep -q "wio-sdk-wm1110"); then echo "Copying .merged.hex file" SRCHEX=$BUILDDIR/$basename.merged.hex cp $SRCHEX $OUTDIR/ fi if (echo $1 | grep -q "rak4631"); then echo "Copying .hex file" cp $SRCHEX $OUTDIR/ fi echo "Copying manifest" cp $BUILDDIR/$basename.mt.json $OUTDIR/$basename.mt.json || true ================================================ FILE: bin/build-rp2xx0.sh ================================================ #!/usr/bin/env bash set -e VERSION=`bin/buildinfo.py long` SHORT_VERSION=`bin/buildinfo.py short` BUILDDIR=.pio/build/$1 OUTDIR=release rm -f $OUTDIR/firmware* rm -r $OUTDIR/* || true # Important to pull latest version of libs into all device flavors, otherwise some devices might be stale platformio pkg install -e $1 echo "Building for $1 with $PLATFORMIO_BUILD_FLAGS" rm -f $BUILDDIR/firmware* # The shell vars the build tool expects to find export APP_VERSION=$VERSION basename=firmware-$1-$VERSION pio run --environment $1 -t mtjson # -v cp $BUILDDIR/$basename.elf $OUTDIR/$basename.elf echo "Copying uf2 file" cp $BUILDDIR/$basename.uf2 $OUTDIR/$basename.uf2 echo "Copying manifest" cp $BUILDDIR/$basename.mt.json $OUTDIR/$basename.mt.json || true ================================================ FILE: bin/build-stm32wl.sh ================================================ #!/usr/bin/env bash set -e VERSION=$(bin/buildinfo.py long) SHORT_VERSION=$(bin/buildinfo.py short) BUILDDIR=.pio/build/$1 OUTDIR=release rm -f $OUTDIR/firmware* rm -r $OUTDIR/* || true # Important to pull latest version of libs into all device flavors, otherwise some devices might be stale platformio pkg install -e $1 echo "Building for $1 with $PLATFORMIO_BUILD_FLAGS" rm -f $BUILDDIR/firmware* # The shell vars the build tool expects to find export APP_VERSION=$VERSION basename=firmware-$1-$VERSION pio run --environment $1 -t mtjson # -v cp $BUILDDIR/$basename.elf $OUTDIR/$basename.elf echo "Copying STM32 bin file" cp $BUILDDIR/$basename.bin $OUTDIR/$basename.bin echo "Copying manifest" cp $BUILDDIR/$basename.mt.json $OUTDIR/$basename.mt.json || true ================================================ FILE: bin/build-userprefs-json.py ================================================ import json import subprocess import re def get_macros_from_header(header_file): # Run clang to preprocess the header file and capture the output result = subprocess.run(['clang', '-E', '-dM', header_file], capture_output=True, text=True) if result.returncode != 0: raise RuntimeError(f"Clang preprocessing failed: {result.stderr}") # Extract macros from the output macros = {} macro_pattern = re.compile(r'#define\s+(\w+)\s+(.*)') for line in result.stdout.splitlines(): match = macro_pattern.match(line) if match and 'USERPREFS_' in line and '_USERPREFS_' not in line: macros[match.group(1)] = match.group(2) return macros def write_macros_to_json(macros, output_file): with open(output_file, 'w') as f: json.dump(macros, f, indent=4) def main(): header_file = 'userPrefs.h' output_file = 'userPrefs.jsonc' # Uncomment all macros in the header file with open(header_file, 'r') as file: lines = file.readlines() uncommented_lines = [] for line in lines: stripped_line = line.strip().replace('/*', '').replace('*/', '') if stripped_line.startswith('//') and 'USERPREFS_' in stripped_line: # Replace "//" stripped_line = stripped_line.replace('//', '') uncommented_lines.append(stripped_line + '\n') with open(header_file, 'w') as file: for line in uncommented_lines: file.write(line) macros = get_macros_from_header(header_file) write_macros_to_json(macros, output_file) print(f"Macros have been written to {output_file}") if __name__ == "__main__": main() ================================================ FILE: bin/buildinfo.py ================================================ #!/usr/bin/env python3 import sys from readprops import readProps verObj = readProps("version.properties") propName = sys.argv[1] print(f"{verObj[propName]}") ================================================ FILE: bin/bump_metainfo/bump_metainfo.py ================================================ #!/usr/bin/env python3 import argparse import xml.etree.ElementTree as ET from defusedxml.ElementTree import parse from datetime import datetime, timezone # Indent by 2 spaces to align with xml formatting. def indent(elem, level=0): i = "\n" + level * " " if len(elem): if not elem.text or not elem.text.strip(): elem.text = i + " " for child in elem: indent(child, level + 1) if not child.tail or not child.tail.strip(): child.tail = i if level and (not elem.tail or not elem.tail.strip()): elem.tail = i def main(): parser = argparse.ArgumentParser( description="Prepend new release entry to metainfo.xml file.") parser.add_argument("--file", help="Path to the metainfo.xml file", default="org.meshtastic.meshtasticd.metainfo.xml") parser.add_argument("version", help="Version string (e.g. 2.6.4)") parser.add_argument("--date", help="Release date (YYYY-MM-DD), defaults to today", default=datetime.now(timezone.utc).date().isoformat()) args = parser.parse_args() tree = parse(args.file) root = tree.getroot() releases = root.find('releases') if releases is None: raise RuntimeError(" element not found in XML.") existing_versions = { release.get('version'): release for release in releases.findall('release') } existing_release = existing_versions.get(args.version) if existing_release is not None: if not existing_release.get('date'): print(f"Version {args.version} found without date. Adding date...") existing_release.set('date', args.date) else: print( f"Version {args.version} is already present with date, skipping insertion.") else: new_release = ET.Element('release', { 'version': args.version, 'date': args.date }) url = ET.SubElement(new_release, 'url', {'type': 'details'}) url.text = f"https://github.com/meshtastic/firmware/releases?q=tag%3Av{args.version}" releases.insert(0, new_release) indent(releases, level=1) releases.tail = "\n" print(f"Inserted new release: {args.version}") tree.write(args.file, encoding='UTF-8', xml_declaration=True) if __name__ == "__main__": main() ================================================ FILE: bin/bump_metainfo/requirements.txt ================================================ defusedxml==0.7.1 ================================================ FILE: bin/bump_version.py ================================================ #!/usr/bin/env python """Bump the version number""" lines = None with open('version.properties', 'r', encoding='utf-8') as f: lines = f.readlines() with open('version.properties', 'w', encoding='utf-8') as f: for line in lines: if line.lstrip().startswith("build = "): words = line.split(" = ") ver = f'build = {int(words[1]) + 1}' f.write(f'{ver}\n') else: f.write(line) ================================================ FILE: bin/check-all.sh ================================================ #!/usr/bin/env bash # Note: This is a prototype for how we could add static code analysis to the CI. set -e VERSION=$(bin/buildinfo.py long) # The shell vars the build tool expects to find export APP_VERSION=$VERSION if [[ $# -gt 0 ]]; then # can override which environment by passing arg BOARDS="$@" else BOARDS="tlora-v2 tlora-v1 tlora_v1_3 tlora-v2-1-1.6 tbeam heltec-v2.0 heltec-v2.1 tbeam0.7 meshtastic-diy-v1 rak4631 rak4631_eink rak11200 t-echo canaryone pca10059_diy_eink" fi echo "BOARDS:${BOARDS}" CHECK="" for BOARD in $BOARDS; do CHECK="${CHECK} -e ${BOARD}" done pio check --flags "-DAPP_VERSION=${APP_VERSION} --suppressions-list=suppressions.txt --inline-suppr" $CHECK --skip-packages --pattern="src/" --fail-on-defect=low --fail-on-defect=medium --fail-on-defect=high ================================================ FILE: bin/check-dependencies.sh ================================================ #!/usr/bin/env bash # Note: This is a prototype for how we could add static code analysis to the CI. set -e if [[ $# -gt 0 ]]; then # can override which environment by passing arg BOARDS="$@" else BOARDS="rak4631 rak4631_eink t-echo canaryone pca10059_diy_eink pico rak11200 tlora-v2 tlora-v1 tlora_v1_3 tlora-v2-1-1.6 tbeam heltec-v2.0 heltec-v2.1 tbeam0.7 meshtastic-diy-v1 nano-g1 station-g1 m5stack-core m5stack-coreink tbeam-s3-core" fi echo "BOARDS:${BOARDS}" CHECK="" for BOARD in $BOARDS; do CHECK="${CHECK} -e ${BOARD}" done echo $CHECK pio pkg outdated -e $CHECK ================================================ FILE: bin/config-dist.yaml ================================================ ### Many device configs have been moved to /etc/meshtasticd/available.d ### To activate, simply copy or link the appropriate file into /etc/meshtasticd/config.d ### Define your devices here using Broadcom pin numbering ### Uncomment the block that corresponds to your hardware ### Including the "Module:" line! --- Lora: # Default to auto-detecting the module type # This will be overridden by configs from config.d Module: auto # # Uncomment to enable Simulation mode, or use --sim # Module: sim # Module: sx1262 # Waveshare SX1302 LISTEN ONLY AT THIS TIME! # CS: 7 # IRQ: 17 # Reset: 22 # Module: RF95 # Elecrow Lora RFM95 IOT https://www.elecrow.com/lora-rfm95-iot-board-for-rpi.html # Reset: 22 # CS: 7 # IRQ: 25 # Module: sx1280 # SX1280 # CS: 21 # IRQ: 16 # Busy: 20 # Reset: 18 ### The Radxa Zero 3E/W employs multiple gpio chips. ### Each gpio pin must be unique, but can be assigned to a specific gpio chip and line. ### In case solely a no. is given, the default gpio chip and pin == line will be employed. ### # Module: sx1262 # Radxa Zero 3E/W + Ebyte E22-900M30S # DIO2_AS_RF_SWITCH: true # DIO3_TCXO_VOLTAGE: 1.8 # CS: # NSS PIN_24 -> chip 4, line 22 # pin: 24 # gpiochip: 4 # line: 22 # SCK: # SCK PIN_23 -> chip 4, line 18 # pin: 23 # gpiochip: 4 # line: 18 # Busy: # BUSY PIN_29 -> chip 3!, line 11 # pin: 29 # gpiochip: 3 # line: 11 # MOSI: # MOSI PIN_19 -> chip 4, line 19 # pin: 19 # gpiochip: 4 # line: 19 # MISO: # MISO PIN_21 -> chip 4, line 21 # pin: 21 # gpiochip: 4 # line: 21 # Reset: # NRST PIN_27 -> chip 4, line 10 # pin: 27 # gpiochip: 4 # line: 10 # IRQ: # DIO1 PIN_28 -> chip 4, line 11 # pin: 28 # gpiochip: 4 # line: 11 # RXen: # RXEN PIN_22 -> chip 3!, line 17 # pin: 22 # gpiochip: 3 # line: 17 # TXen: RADIOLIB_NC # TXEN no PIN, no line, fallback to default gpio chip # Module: sx1268 # SX1268-based modules, tested with Ebyte E22 400M33S # CS: 21 # IRQ: 16 # Busy: 20 # Reset: 18 # TXen: 6 # RXen: 12 # DIO3_TCXO_VOLTAGE: true # DIO3_TCXO_VOLTAGE: true # the Waveshare Core1262 and others are known to need this setting # TXen: x # TX and RX enable pins # RXen: x # SX126X_MAX_POWER: 8 # Limit the output power to 8 dBm, useful for amped nodes # spiSpeed: 2000000 ### Set default/fallback gpio chip to use in /dev/. Defaults to 0. ### Notably the Raspberry Pi 5 puts the GPIO header on gpiochip4 # gpiochip: 4 ### Specify the SPI device to use in /dev/. Defaults to spidev0.0 ### Some devices, like the pinedio, may require spidev0.1 as a workaround. # spidev: spidev0.0 ### Deprecated location for User Button: #GPIO: # User: 6 ### Define GPS GPS: # SerialPath: /dev/ttyS0 # ExtraPins: # - 22 ### Specify I2C device, or leave blank for none I2C: # I2CDevice: /dev/i2c-1 ### Set up SPI displays here. Note that I2C displays are generally auto-detected. Display: ### Adafruit PiTFT 2.8 TFT+Touchscreen # Panel: ILI9341 # CS: 8 # DC: 25 # Width: 240 # Height: 320 # Rotate: true ### SHCHV 3.5 RPi TFT+Touchscreen # Panel: ILI9486 # spidev: spidev0.0 # BusFrequency: 30000000 # DC: 24 # Reset: 25 # Width: 320 # Height: 480 # OffsetRotate: 2 ### TZT 2.0 Inch TFT Display ST7789V 240RGBx320 # Panel: ST7789 # spidev: spidev0.0 # # CS: 8 # can be freely chosen # BusFrequency: 80000000 # DC: 24 # can be freely chosen # Width: 320 # Height: 240 # Reset: 25 # can be freely chosen # Rotate: true # OffsetRotate: 1 # Invert: true ### You can also specify the spi device for the display to use # spidev: spidev0.0 Touchscreen: ### Note, at least for now, the touchscreen must have a CS pin defined, even if you let Linux manage the CS switching. # Module: STMPE610 # Option 1 for Adafruit PiTFT 2.8 # CS: 7 # IRQ: 24 # Module: FT5x06 # Option 2 for Adafruit PiTFT 2.8 # IRQ: 24 # I2CAddr: 0x38 ### You can also specify the spi device for the touchscreen to use # spidev: spidev0.0 Input: ### Configure device for direct keyboard input # KeyboardDevice: /dev/input/by-id/usb-_Raspberry_Pi_Internal_Keyboard-event-kbd ### Standard User Button Config # UserButton: 6 ### Trackball/Joystick input # TrackballUp: 6 # TrackballDown: 19 # TrackballLeft: 5 # TrackballRight: 26 # TrackballPress: 13 ### Logging: LogLevel: info # debug, info, warn, error # TraceFile: /var/log/meshtasticd.json # JSONFile: /packets.json # File location for JSON output of decoded packets # JSONFileRotate: 60 # Rotate JSON file every N minutes, or 0 for no rotation # JSONFilter: position # filter for packets to save to JSON file # AsciiLogs: true # default if not specified is !isatty() on stdout Webserver: # Port: 9443 # Port for Webserver & Webservices # RootPath: /usr/share/meshtasticd/web # Root Dir of WebServer # SSLKey: /etc/meshtasticd/ssl/private_key.pem # Path to SSL Key, generated if not present # SSLCert: /etc/meshtasticd/ssl/certificate.pem # Path to SSL Certificate, generated if not present HostMetrics: # ReportInterval: 30 # Interval in minutes between HostMetrics report packets, or 0 for disabled # Channel: 0 # channel to send Host Metrics over. Defaults to the primary channel. # UserStringCommand: cat /sys/firmware/devicetree/base/serial-number # Command to execute, to send the results as the userString Config: # DisplayMode: TWOCOLOR # uncomment to force BaseUI # DisplayMode: COLOR # uncomment to force MUI General: MaxNodes: 200 MaxMessageQueue: 100 ConfigDirectory: /etc/meshtasticd/config.d/ AvailableDirectory: /etc/meshtasticd/available.d/ # MACAddress: AA:BB:CC:DD:EE:FF # MACAddressSource: eth0 # APIPort: 4403 ================================================ FILE: bin/config.d/MUI/X11_480x480.yaml ================================================ Display: Panel: X11 Width: 480 Height: 480 ================================================ FILE: bin/config.d/OpenWRT/BananaPi-BPI-R4-sx1262.yaml ================================================ Lora: Module: sx1262 # BananaPi-BPI-R4 SPI via 26p GPIO Header ## CS: 28 IRQ: 50 Busy: 62 Reset: 51 spidev: spidev1.0 DIO2_AS_RF_SWITCH: true DIO3_TCXO_VOLTAGE: true ================================================ FILE: bin/config.d/OpenWRT/OpenWRT-One-mikroBUS-LR-IOT-CLICK.yaml ================================================ ## https://www.mikroe.com/lr-iot-click Lora: Module: lr1110 # OpenWRT ONE mikroBUS with LR-IOT-CLICK # CS: 25 IRQ: 10 Busy: 12 # Reset: 2 spidev: spidev2.0 DIO3_TCXO_VOLTAGE: 1.6 ================================================ FILE: bin/config.d/OpenWRT/OpenWRT_One_mikroBUS_sx1262.yaml ================================================ Lora: Module: sx1262 IRQ: 10 Busy: 12 # Reset: 2 spidev: spidev2.0 DIO2_AS_RF_SWITCH: true DIO3_TCXO_VOLTAGE: true ================================================ FILE: bin/config.d/display-waveshare-1-44.yaml ================================================ ### Waveshare 1.44inch LCD HAT Display: Panel: ST7735S spidev: spidev0.0 # Specify either the spidev here, or the CS below # CS: 8 #Chip Select # Optional, as this is the default pin for spidev0.0 DC: 25 # Data/Command pin Backlight: 24 Width: 128 Height: 128 Reset: 27 OffsetX: 2 OffsetY: 1 # OffsetY: 31 # These two options are used to properly flip the screen 180 degrees # OffsetRotate: 3 Input: TrackballUp: 6 TrackballDown: 19 TrackballLeft: 5 TrackballRight: 26 TrackballPress: 13 TrackballDirection: FALLING # User: 21 ================================================ FILE: bin/config.d/display-waveshare-2.8.yaml ================================================ Display: ### Waveshare 2.8inch RPi LCD Panel: ST7789 CS: 8 DC: 22 # Data/Command pin Backlight: 18 Width: 240 Height: 320 Reset: 27 Rotate: true Invert: true Touchscreen: ### Note, at least for now, the touchscreen must have a CS pin defined, even if you let Linux manage the CS switching. Module: XPT2046 # Waveshare 2.8inch CS: 7 IRQ: 17 ================================================ FILE: bin/config.d/femtofox/femtofox_LR1121_TCXO.yaml ================================================ --- Lora: ## Ebyte E80-900M22S ## This is a bit experimental ## ## Module: lr1121 gpiochip: 1 # subtract 32 from the gpio numbers DIO3_TCXO_VOLTAGE: 1.8 CS: 16 #pin6 / GPIO48 1C0 IRQ: 23 #pin17 / GPIO55 1C7 Busy: 22 #pin16 / GPIO54 1C6 Reset: 25 #pin13 / GPIO57 1D1 spidev: spidev0.0 #pins are (CS=16, CLK=17, MOSI=18, MISO=19) spiSpeed: 2000000 General: MACAddressSource: eth0 ================================================ FILE: bin/config.d/femtofox/femtofox_SX1262_TCXO.yaml ================================================ --- Lora: ## Ebyte E22-900M30S, E22-900M22S with or without external RF switching setup ## HT-RA62 (Has internal switching, but whatever) ## Seeed WIO SX1262 (already has TXEN-DIO2 link, but needs RXEN) ## Will work with any module with or without RF switching, and with TCXO Module: sx1262 gpiochip: 1 # subtract 32 from the gpio numbers DIO2_AS_RF_SWITCH: true DIO3_TCXO_VOLTAGE: true CS: 16 #pin6 / GPIO48 1C0 IRQ: 23 #pin17 / GPIO55 1C7 Busy: 22 #pin16 / GPIO54 1C6 Reset: 25 #pin13 / GPIO57 1D1 RXen: 24 #pin12 / GPIO56 1D0 # Not strictly needed for auto-switching, but why complicate things? # TXen: bridge to DIO2 on E22 module spidev: spidev0.0 #pins are (CS=16, CLK=17, MOSI=18, MISO=19) spiSpeed: 2000000 General: MACAddressSource: eth0 ================================================ FILE: bin/config.d/femtofox/femtofox_SX1262_XTAL.yaml ================================================ --- Lora: ## Ebyte E22-900MM22S with no external RF switching setup ## Waveshare SX126X XXXM, AI Thinker RA-01SH ## Will work with any module with or without RF switching and no TCXO Module: sx1262 gpiochip: 1 # subtract 32 from the gpio numbers DIO2_AS_RF_SWITCH: true DIO3_TCXO_VOLTAGE: false CS: 16 #pin6 / GPIO48 1C0 IRQ: 23 #pin17 / GPIO55 1C7 Busy: 22 #pin16 / GPIO54 1C6 Reset: 25 #pin13 / GPIO57 1D1 RXen: 24 #pin12 / GPIO56 1D0 # Not strictly needed for auto-switching, but why complicate things? # TXen: bridge to DIO2 on E22 module spidev: spidev0.0 #pins are (CS=16, CLK=17, MOSI=18, MISO=19) spiSpeed: 2000000 General: MACAddressSource: eth0 ================================================ FILE: bin/config.d/lora-Adafruit-RFM9x.yaml ================================================ Lora: Module: RF95 # Adafruit RFM9x Reset: 25 CS: 7 IRQ: 22 # Busy: 23 ================================================ FILE: bin/config.d/lora-MeshAdv-900M30S.yaml ================================================ # MeshAdv-Pi E22-900M30S # https://github.com/chrismyers2000/MeshAdv-Pi-Hat Lora: Module: sx1262 CS: 21 IRQ: 16 Busy: 20 Reset: 18 TXen: 13 RXen: 12 DIO3_TCXO_VOLTAGE: true # Only for E22-900M33S: # Limit the output power to 8 dBm # SX126X_MAX_POWER: 8 ================================================ FILE: bin/config.d/lora-MeshAdv-Mini-900M22S.yaml ================================================ # MeshAdv Mini E22-900M22S # https://github.com/chrismyers2000/MeshAdv-Mini Lora: Module: sx1262 # Ebyte E22-900M22S CS: 8 IRQ: 16 Busy: 20 Reset: 24 RXen: 12 DIO2_AS_RF_SWITCH: true DIO3_TCXO_VOLTAGE: true ================================================ FILE: bin/config.d/lora-RAK6421-13300-slot1.yaml ================================================ Lora: ### RAK13300in Slot 1 Module: sx1262 IRQ: 22 #IO6 Reset: 16 # IO4 Busy: 24 # IO5 # Ant_sw: 13 # IO3 Enable_Pins: - 12 - 13 DIO3_TCXO_VOLTAGE: true DIO2_AS_RF_SWITCH: true spidev: spidev0.0 # CS: 8 ================================================ FILE: bin/config.d/lora-RAK6421-13300-slot2.yaml ================================================ Lora: ### RAK13300in Slot 2 pins IRQ: 18 #IO6 Reset: 24 # IO4 Busy: 19 # IO5 # Ant_sw: 23 # IO3 Enable_Pins: - 26 - 23 spidev: spidev0.1 # CS: 7 ================================================ FILE: bin/config.d/lora-RAK6421-13302-slot1.yaml ================================================ Lora: ### RAK13300in Slot 1 Module: sx1262 IRQ: 22 #IO6 Reset: 16 # IO4 Busy: 24 # IO5 # Ant_sw: 13 # IO3 Enable_Pins: - 12 - 13 DIO3_TCXO_VOLTAGE: true DIO2_AS_RF_SWITCH: true spidev: spidev0.0 # CS: 8 TX_GAIN_LORA: [9, 9, 10, 11, 9, 8, 9, 10, 10, 10, 11, 12, 12, 12, 12, 12, 12, 12, 12, 10, 9, 8] ================================================ FILE: bin/config.d/lora-RAK6421-13302-slot2.yaml ================================================ Lora: ### RAK13300in Slot 2 pins IRQ: 18 #IO6 Reset: 24 # IO4 Busy: 19 # IO5 # Ant_sw: 23 # IO3 Enable_Pins: - 26 - 23 spidev: spidev0.1 # CS: 7 TX_GAIN_LORA: [9, 9, 10, 11, 9, 8, 9, 10, 10, 10, 11, 12, 12, 12, 12, 12, 12, 12, 12, 10, 9, 8] ================================================ FILE: bin/config.d/lora-hat-rak-6421-pi-hat.yaml ================================================ Lora: ### RAK13300in Slot 1 Module: sx1262 IRQ: 22 #IO6 Reset: 16 # IO4 Busy: 24 # IO5 Enable_Pins: - 12 - 13 DIO3_TCXO_VOLTAGE: true DIO2_AS_RF_SWITCH: true spidev: spidev0.0 # GPIO_DETECT_PA: 13 TX_GAIN_LORA: [9, 9, 10, 11, 9, 8, 9, 10, 10, 10, 11, 12, 12, 12, 12, 12, 12, 12, 12, 10, 9, 8] ================================================ FILE: bin/config.d/lora-lyra-picocalc-wio-sx1262.yaml ================================================ Lora: Module: sx1262 DIO2_AS_RF_SWITCH: true DIO3_TCXO_VOLTAGE: true gpiochip: 0 MOSI: 12 MISO: 13 IRQ: 1 Busy: 23 Reset: 22 RXen: 0 gpiochip: 1 CS: 9 SCK: 11 # TXen: bridge to DIO2 on E22 module SX126X_MAX_POWER: 22 spidev: spidev1.0 spiSpeed: 2000000 ================================================ FILE: bin/config.d/lora-lyra-ultra_1w.yaml ================================================ # For use with Armbian luckfox-lyra-ultra-w # Enable overlay 'luckfox-lyra-ultra-w-spi0-cs0-spidev' with armbian-config # https://github.com/wehooper4/Meshtastic-Hardware/tree/main/Luckfox%20Ultra%20Hat # 1 Watt Lyra Ultra hat Lora: Module: sx1262 DIO2_AS_RF_SWITCH: true DIO3_TCXO_VOLTAGE: true CS: 10 IRQ: 5 Busy: 11 Reset: 9 RXen: 14 spidev: spidev0.0 #pins are (CS=10, CLK=8, MOSI=6, MISO=7) spiSpeed: 2000000 ================================================ FILE: bin/config.d/lora-lyra-ultra_2w.yaml ================================================ # For use with Armbian luckfox-lyra-ultra-w # Enable overlay 'luckfox-lyra-ultra-w-spi0-cs0-spidev' with armbian-config # https://github.com/wehooper4/Meshtastic-Hardware/tree/main/Luckfox%20Ultra%20Hat # 2 Watt Lyra Ultra hat Lora: Module: sx1262 DIO2_AS_RF_SWITCH: true DIO3_TCXO_VOLTAGE: true SX126X_MAX_POWER: 8 CS: 10 IRQ: 5 Busy: 11 Reset: 9 RXen: 14 spidev: spidev0.0 #pins are (CS=10, CLK=8, MOSI=6, MISO=7) spiSpeed: 2000000 ================================================ FILE: bin/config.d/lora-lyra-ws-raspberry-pi-pico-hat.yaml ================================================ # For use with Armbian luckfox-lyra // luckfox-lyra-plus # Enable overlay 'luckfox-lyra-plus-spi0-cs0_rmio13-spidev' with armbian-config # Waveshare LoRa HAT for Raspberry Pi Pico # https://www.waveshare.com/wiki/Pico-LoRa-SX1262 Lora: Module: sx1262 DIO2_AS_RF_SWITCH: true DIO3_TCXO_VOLTAGE: true spidev: spidev0.0 CS: # GPIO0_B5 pin: 13 gpiochip: 0 line: 13 IRQ: # GPIO1_C2 pin: 50 gpiochip: 1 line: 18 Busy: # GPIO0_B4 pin: 12 gpiochip: 0 line: 12 Reset: # GPIO0_A2 pin: 2 gpiochip: 0 line: 2 ================================================ FILE: bin/config.d/lora-meshstick-1262.yaml ================================================ Lora: Module: sx1262 CS: 0 IRQ: 6 Reset: 2 Busy: 4 spidev: ch341 DIO3_TCXO_VOLTAGE: true # USB_Serialnum: 12345678 USB_PID: 0x5512 USB_VID: 0x1A86 ================================================ FILE: bin/config.d/lora-piggystick-lr1121.yaml ================================================ Lora: Module: lr1121 CS: 0 IRQ: 6 Reset: 2 Busy: 4 spidev: ch341 DIO3_TCXO_VOLTAGE: 1.8 # USB_Serialnum: 12345678 USB_PID: 0x5512 USB_VID: 0x1A86 ================================================ FILE: bin/config.d/lora-pinedio-usb-sx1262.yaml ================================================ Lora: Module: sx1262 CS: 0 IRQ: 10 spidev: ch341 ================================================ FILE: bin/config.d/lora-raxda-rock2f-starter-edition-hat.yaml ================================================ Lora: ### Raxda Rock 2F running Armbian Linux 6.1.99-vendor-rk35xx ### https://github.com/markbirss/rock-2f ### https://github.com/markbirss/lora-starter-edition-sx1262-i2c ### https://github.com/radxa-pkg/radxa-overlays/blob/main/arch/arm64/boot/dts/rockchip/overlays/rk3528-spi0-cs1-spidev.dts ### Require install of https://github.com/radxa-pkg/radxa-overlays and rk3528-spi0-cs1-spidev.dtbo copied to /boot/dtb/rockchip/overlay and enabled ### in /boot/armbianEnv.txt - overlays=rk3528-spi0-cs1-spidev ### The Radxa Rock 2F employs multiple gpio chips. ### Each gpio pin must be unique, but can be assigned to a specific gpio chip and line. ### In case solely a no. is given, the default gpio chip and pin == line will be employed. ### Module: sx1262 # Radxa Rock 2F + Starter Edition SX1262 HAT by Mark Birss DIO2_AS_RF_SWITCH: true DIO3_TCXO_VOLTAGE: 1.8 spidev: spidev0.1 CS: # NSS PIN_24 -> chip 4, line 14 pin: 24 gpiochip: 4 line: 14 SCK: # SCK PIN_23 -> chip 4, line 12 pin: 23 gpiochip: 4 line: 12 Busy: # BUSY PIN_7 -> chip 4, line 6 pin: 7 gpiochip: 4 line: 6 MOSI: # MOSI PIN_19 -> chip 4, line 10 pin: 19 gpiochip: 4 line: 10 MISO: # MISO PIN_21 -> chip 4, line 11 pin: 21 gpiochip: 4 line: 11 Reset: # NRST PIN_12 -> chip 1, line 13 pin: 12 gpiochip: 1 line: 13 IRQ: # DIO1 PIN_15 -> chip 4, line 22 pin: 15 gpiochip: 4 line: 22 # RXen: # RXEN PIN_22 -> chip 3!, line 17 # pin: 22 # gpiochip: 3 # line: 17 # TXen: RADIOLIB_NC # TXEN no PIN, no line, fallback to default gpio chip ================================================ FILE: bin/config.d/lora-starter-edition-sx1262-i2c.yaml ================================================ # https://www.waveshare.com/core1262-868m.htm # https://github.com/markbirss/lora-starter-edition-sx1262-i2c Lora: Module: sx1262 # Starter Edition SX1262 I2C Raspberry Pi HAT DIO2_AS_RF_SWITCH: true DIO3_TCXO_VOLTAGE: true CS: 8 IRQ: 22 Busy: 4 Reset: 18 ================================================ FILE: bin/config.d/lora-usb-meshstick-1262.yaml ================================================ Lora: Module: sx1262 CS: 0 IRQ: 6 Reset: 2 Busy: 4 RXen: 1 DIO2_AS_RF_SWITCH: true spidev: ch341 DIO3_TCXO_VOLTAGE: true # USB_Serialnum: 12345678 USB_PID: 0x5512 USB_VID: 0x1A86 SX126X_MAX_POWER: 22 ================================================ FILE: bin/config.d/lora-usb-meshtoad-e22.yaml ================================================ Lora: Module: sx1262 CS: 0 IRQ: 6 Reset: 2 Busy: 4 RXen: 1 DIO2_AS_RF_SWITCH: true DIO3_TCXO_VOLTAGE: true spidev: ch341 USB_PID: 0x5512 USB_VID: 0x1A86 # Optional: Reduce power to 10 dBm to # avoid over-drawing the USB port # SX126X_MAX_POWER: 10 # Optional: Set the serial number for multi-radio support # USB_Serialnum: 13374201 ================================================ FILE: bin/config.d/lora-usb-umesh-1262-30dbm.yaml ================================================ Lora: Module: sx1262 CS: 0 IRQ: 6 Reset: 1 Busy: 4 RXen: 2 DIO2_AS_RF_SWITCH: true spidev: ch341 USB_PID: 0x5512 USB_VID: 0x1A86 DIO3_TCXO_VOLTAGE: true # USB_Serialnum: 12345678 SX126X_MAX_POWER: 22 # Reduce output power to improve EMI TX_GAIN_LORA: [12, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 8, 8, 7] # Note: This module integrates an additional PA to achieve higher output power. # The 'power' parameter here does not represent the actual RF output. # TX_GAIN_LORA defines the gain offset applied at each SX1262 input power step (1–22 dBm). # Each array element corresponds to the additional gain when that input level is set, # The effective RF output is: Pout ≈ Pset + TX_GAIN_LORA[index]. # Please refer to https://github.com/linser233/uMesh/blob/main/RF_Power.md for detailed information. ================================================ FILE: bin/config.d/lora-usb-umesh-1268-30dbm.yaml ================================================ Lora: Module: sx1268 CS: 0 IRQ: 6 Reset: 1 Busy: 4 RXen: 2 DIO2_AS_RF_SWITCH: true spidev: ch341 USB_PID: 0x5512 USB_VID: 0x1A86 DIO3_TCXO_VOLTAGE: true # USB_Serialnum: 12345678 SX126X_MAX_POWER: 22 # Reduce output power to improve EMI TX_GAIN_LORA: [12, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 8, 8, 7] # Note: This module integrates an additional PA to achieve higher output power. # The 'power' parameter here does not represent the actual RF output. # TX_GAIN_LORA defines the gain offset applied at each SX1262 input power step (1–22 dBm). # Each array element corresponds to the additional gain when that input level is set, # The effective RF output is: Pout ≈ Pset + TX_GAIN_LORA[index]. # Please refer to https://github.com/linser233/uMesh/blob/main/RF_Power.md for detailed information. ================================================ FILE: bin/config.d/lora-waveshare-sxxx.yaml ================================================ Lora: Module: sx1262 # Waveshare SX126X XXXM DIO2_AS_RF_SWITCH: true CS: 21 IRQ: 16 Busy: 20 Reset: 18 SX126X_ANT_SW: 6 ================================================ FILE: bin/config.d/lora-ws-raspberry-pi-pico-to-rpi-adapter.yaml ================================================ # https://www.waveshare.com/pico-lora-sx1262-868m.htm # https://github.com/markbirss/lora-ws-raspberry-pi-pico-to-rpi-adapter Lora: Module: sx1262 # Waveshare Raspberry Pi Pico to Raspberry Pi HAT Adapter DIO2_AS_RF_SWITCH: true DIO3_TCXO_VOLTAGE: true CS: 21 IRQ: 16 Busy: 20 Reset: 18 ================================================ FILE: bin/config.d/lora-ws-raspberry-pico-to-orangepi-03.yaml ================================================ # https://www.waveshare.com/pico-lora-sx1262-868m.htm # http://www.orangepi.org/html/hardWare/computerAndMicrocontrollers/details/Orange-Pi-Zero-3.html # # See Orange Pi Zero3 manual, chapter 3.16, page 124 for 26-pin header pinout # # Pin Connection # Waveshare Orange Pi Zero3 # 36 3.3V 17 # 15 MOSI 19 # 16 MISO 21 # 14 CLK 23 # 38 GND 25 # 4 BUSY 18 # 20 RESET 22 # 5 CS 24 # 26 DIO1/IRQ 26 Lora: Module: sx1262 # Waveshare Raspberry Pico Lora module DIO2_AS_RF_SWITCH: true DIO3_TCXO_VOLTAGE: true # Specify either the spidev1_1 or the CS below, not both! # On DietPi Linux, when using the user overlay dietpi-spi1_1.dtbo, CS will be configured with spidev1.1 spidev: spidev1.1 # See Orange Pi Zero3 manual, chapter 3.18.3, page 130 # CS: # CS PIN_24 -> chip 1, line 233 # pin: 24 # gpiochip: 1 # line: 233 SCK: # SCK PIN_23 -> chip 1, line 230 pin: 23 gpiochip: 1 line: 230 Busy: # BUSY PIN_18 -> chip 1, line 78 pin: 18 gpiochip: 1 line: 78 MOSI: # MOSI PIN_19 -> chip 1, line 231 pin: 19 gpiochip: 1 line: 231 MISO: # MISO PIN_21 -> chip 1, line 232 pin: 21 gpiochip: 1 line: 232 Reset: # NRST PIN_22 -> chip 1, line 71 pin: 22 gpiochip: 1 line: 71 IRQ: # DIO1 PIN_26 -> chip 1, line 74 pin: 26 gpiochip: 1 line: 74 ================================================ FILE: bin/device-install.bat ================================================ @ECHO OFF SETLOCAL EnableDelayedExpansion TITLE Meshtastic device-install SET "SCRIPT_NAME=%~nx0" SET "DEBUG=0" SET "PYTHON=" SET "ESPTOOL_BAUD=115200" SET "ESPTOOL_CMD=" SET "LOGCOUNTER=0" SET "BPS_RESET=0" @REM Default offsets. @REM https://github.com/meshtastic/web-flasher/blob/main/stores/firmwareStore.ts#L202 SET "OTA_OFFSET=0x260000" SET "SPIFFS_OFFSET=0x300000" GOTO getopts :help ECHO Flash image file to device, but first erasing and writing system information. ECHO. ECHO Usage: %SCRIPT_NAME% -f filename [-p PORT] [-P python] [--1200bps-reset] ECHO. ECHO Options: ECHO -f filename The firmware .factory.bin file to flash. Custom to your device type and region. (required) ECHO The file must be located in this current directory. ECHO -p PORT Set the environment variable for ESPTOOL_PORT. ECHO If not set, ESPTOOL iterates all ports (Dangerous). ECHO -P python Specify alternate python interpreter to use to invoke esptool. (default: python) ECHO If supplied the script will use python. ECHO If not supplied the script will try to find esptool in Path. ECHO --1200bps-reset Attempt to place the device in correct mode. (1200bps Reset) ECHO Some hardware requires this twice. ECHO. ECHO Example: %SCRIPT_NAME% -p COM17 --1200bps-reset ECHO Example: %SCRIPT_NAME% -f firmware-t-deck-tft-2.6.0.0b106d4.factory.bin -p COM11 ECHO Example: %SCRIPT_NAME% -f firmware-unphone-2.6.0.0b106d4.factory.bin -p COM11 GOTO eof :version ECHO %SCRIPT_NAME% [Version 2.7.0] ECHO Meshtastic GOTO eof :getopts IF "%~1"=="" GOTO endopts IF /I "%~1"=="-?" GOTO help IF /I "%~1"=="-h" GOTO help IF /I "%~1"=="--help" GOTO help IF /I "%~1"=="-v" GOTO version IF /I "%~1"=="--version" GOTO version IF /I "%~1"=="--debug" SET "DEBUG=1" & CALL :LOG_MESSAGE DEBUG "DEBUG mode: enabled." IF /I "%~1"=="-f" SET "FILENAME=%~2" & SHIFT IF "%~1"=="-p" SET "ESPTOOL_PORT=%~2" & SHIFT IF /I "%~1"=="--port" SET "ESPTOOL_PORT=%~2" & SHIFT IF "%~1"=="-P" SET "PYTHON=%~2" & SHIFT IF /I "%~1"=="--1200bps-reset" SET "BPS_RESET=1" SHIFT GOTO getopts :endopts IF %BPS_RESET% EQU 1 GOTO skip-filename CALL :LOG_MESSAGE DEBUG "Checking FILENAME parameter..." IF "__!FILENAME!__"=="____" ( CALL :LOG_MESSAGE DEBUG "Missing -f filename input." GOTO help ) ELSE ( CALL :LOG_MESSAGE DEBUG "Filename: !FILENAME!" IF NOT "__!FILENAME: =!__"=="__!FILENAME!__" ( CALL :LOG_MESSAGE ERROR "Filename containing spaces are not supported." GOTO help ) IF NOT "__!FILENAME:.factory.bin=!__"=="__!FILENAME!__" ( CALL :LOG_MESSAGE ERROR "Filename must be a firmware-*.factory.bin file." GOTO help ) @REM Remove ".\" or "./" file prefix if present. SET "FILENAME=!FILENAME:.\=!" SET "FILENAME=!FILENAME:./=!" ) CALL :LOG_MESSAGE DEBUG "Checking if !FILENAME! exists..." IF NOT EXIST !FILENAME! ( CALL :LOG_MESSAGE ERROR "File does not exist: !FILENAME!. Terminating." GOTO eof ) CALL :LOG_MESSAGE DEBUG "Checking for metadata..." @REM Derive metadata filename from firmware filename. SET "METAFILE=!FILENAME:.factory.bin=!.mt.json" IF EXIST !METAFILE! ( @REM Print parsed json with powershell CALL :LOG_MESSAGE INFO "Firmware metadata: !METAFILE!" powershell -NoProfile -Command "(Get-Content '!METAFILE!' | ConvertFrom-Json | Out-String).Trim()" @REM Save metadata values to variables for later use. FOR /f "usebackq" %%A IN (`powershell -NoProfile -Command ^ "(Get-Content '!METAFILE!' | ConvertFrom-Json).mcu"`) DO SET "MCU=%%A" FOR /f "usebackq" %%A IN (`powershell -NoProfile -Command ^ "(Get-Content '!METAFILE!' | ConvertFrom-Json).part | Where-Object { $_.subtype -eq 'ota_1' } | Select-Object -ExpandProperty offset"` ) DO SET "OTA_OFFSET=%%A" FOR /f "usebackq" %%A IN (`powershell -NoProfile -Command ^ "(Get-Content '!METAFILE!' | ConvertFrom-Json).part | Where-Object { $_.subtype -eq 'spiffs' } | Select-Object -ExpandProperty offset"` ) DO SET "SPIFFS_OFFSET=%%A" ) ELSE ( CALL :LOG_MESSAGE ERROR "No metadata file found: !METAFILE!" GOTO eof ) :skip-filename CALL :LOG_MESSAGE DEBUG "Determine the correct esptool command to use..." IF NOT "__%PYTHON%__"=="____" ( SET "ESPTOOL_CMD=!PYTHON! -m esptool" CALL :LOG_MESSAGE DEBUG "Python interpreter supplied." ) ELSE ( CALL :LOG_MESSAGE DEBUG "Python interpreter NOT supplied. Looking for esptool..." WHERE esptool >nul 2>&1 IF %ERRORLEVEL% EQU 0 ( @REM WHERE exits with code 0 if esptool is found. SET "ESPTOOL_CMD=esptool" ) ELSE ( SET "ESPTOOL_CMD=python -m esptool" CALL :RESET_ERROR ) ) CALL :LOG_MESSAGE DEBUG "Checking esptool command !ESPTOOL_CMD!..." !ESPTOOL_CMD! >nul 2>&1 IF %ERRORLEVEL% EQU 9009 ( @REM 9009 = command not found on Windows CALL :LOG_MESSAGE ERROR "esptool not found: !ESPTOOL_CMD!" EXIT /B 1 ) IF %DEBUG% EQU 1 ( CALL :LOG_MESSAGE DEBUG "Skipping ESPTOOL_CMD steps." SET "ESPTOOL_CMD=REM !ESPTOOL_CMD!" ) CALL :LOG_MESSAGE DEBUG "Using esptool command: !ESPTOOL_CMD!" IF "__!ESPTOOL_PORT!__" == "____" ( CALL :LOG_MESSAGE WARN "Using esptool port: UNSET." ) ELSE ( SET "ESPTOOL_CMD=!ESPTOOL_CMD! --port !ESPTOOL_PORT!" CALL :LOG_MESSAGE INFO "Using esptool port: !ESPTOOL_PORT!." ) CALL :LOG_MESSAGE INFO "Using esptool baud: !ESPTOOL_BAUD!." IF %BPS_RESET% EQU 1 ( @REM Attempt to change mode via 1200bps Reset. CALL :RUN_ESPTOOL 1200 --after no_reset read_flash_status GOTO eof ) @REM Extract PROGNAME from %FILENAME% for later use. SET "PROGNAME=!FILENAME:.factory.bin=!" CALL :LOG_MESSAGE DEBUG "Computed PROGNAME: !PROGNAME!" @REM Determine OTA filename based on MCU type (unified OTA format) SET "OTA_FILENAME=mt-!MCU!-ota.bin" CALL :LOG_MESSAGE DEBUG "Set OTA_FILENAME to: !OTA_FILENAME!" @REM Set SPIFFS filename with "littlefs-" prefix. SET "SPIFFS_FILENAME=littlefs-!PROGNAME:firmware-=!.bin" CALL :LOG_MESSAGE DEBUG "Set SPIFFS_FILENAME to: !SPIFFS_FILENAME!" CALL :LOG_MESSAGE DEBUG "Set OTA_OFFSET to: !OTA_OFFSET!" CALL :LOG_MESSAGE DEBUG "Set SPIFFS_OFFSET to: !SPIFFS_OFFSET!" @REM Ensure target files exist before flashing operations. IF NOT EXIST !FILENAME! CALL :LOG_MESSAGE ERROR "File does not exist: "!FILENAME!". Terminating." & EXIT /B 2 & GOTO eof IF NOT EXIST !OTA_FILENAME! CALL :LOG_MESSAGE ERROR "File does not exist: "!OTA_FILENAME!". Terminating." & EXIT /B 2 & GOTO eof IF NOT EXIST !SPIFFS_FILENAME! CALL :LOG_MESSAGE ERROR "File does not exist: "!SPIFFS_FILENAME!". Terminating." & EXIT /B 2 & GOTO eof @REM Flashing operations. CALL :LOG_MESSAGE INFO "Trying to flash "!FILENAME!", but first erasing and writing system information..." CALL :RUN_ESPTOOL !ESPTOOL_BAUD! erase_flash || GOTO eof CALL :RUN_ESPTOOL !ESPTOOL_BAUD! write_flash 0x00 "!FILENAME!" || GOTO eof CALL :LOG_MESSAGE INFO "Trying to flash BLEOTA "!OTA_FILENAME!" at OTA_OFFSET !OTA_OFFSET!..." CALL :RUN_ESPTOOL !ESPTOOL_BAUD! write_flash !OTA_OFFSET! "!OTA_FILENAME!" || GOTO eof CALL :LOG_MESSAGE INFO "Trying to flash SPIFFS "!SPIFFS_FILENAME!" at SPIFFS_OFFSET !SPIFFS_OFFSET!..." CALL :RUN_ESPTOOL !ESPTOOL_BAUD! write_flash !SPIFFS_OFFSET! "!SPIFFS_FILENAME!" || GOTO eof CALL :LOG_MESSAGE INFO "Script complete!." :eof ENDLOCAL EXIT /B %ERRORLEVEL% :RUN_ESPTOOL @REM Subroutine used to run ESPTOOL_CMD with arguments. @REM Also handles %ERRORLEVEL%. @REM CALL :RUN_ESPTOOL [Baud] [erase_flash|write_flash] [OFFSET] [Filename] @REM. @REM Example:: CALL :RUN_ESPTOOL 115200 write_flash 0x10000 "firmwarefile.bin" IF %DEBUG% EQU 1 CALL :LOG_MESSAGE DEBUG "About to run command: !ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4" CALL :RESET_ERROR !ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4 IF %BPS_RESET% EQU 1 GOTO :eof IF %ERRORLEVEL% NEQ 0 ( CALL :LOG_MESSAGE ERROR "Error running command: !ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4" EXIT /B %ERRORLEVEL% ) GOTO :eof :LOG_MESSAGE @REM Subroutine used to print log messages in four different levels. @REM DEBUG messages only get printed if [-d] flag is passed to script. @REM CALL :LOG_MESSAGE [ERROR|INFO|WARN|DEBUG] "Message" @REM. @REM Example:: CALL :LOG_MESSAGE INFO "Message." SET /A LOGCOUNTER=LOGCOUNTER+1 IF "%1" == "ERROR" CALL :GET_TIMESTAMP & ECHO %1 ^| !TIMESTAMP! !LOGCOUNTER! %~2 IF "%1" == "INFO" CALL :GET_TIMESTAMP & ECHO %1 ^| !TIMESTAMP! !LOGCOUNTER! %~2 IF "%1" == "WARN" CALL :GET_TIMESTAMP & ECHO %1 ^| !TIMESTAMP! !LOGCOUNTER! %~2 IF "%1" == "DEBUG" IF %DEBUG% EQU 1 CALL :GET_TIMESTAMP & ECHO %1 ^| !TIMESTAMP! !LOGCOUNTER! %~2 GOTO :eof :GET_TIMESTAMP @REM Subroutine used to set !TIMESTAMP! to HH:MM:ss. @REM CALL :GET_TIMESTAMP @REM. @REM Updates: !TIMESTAMP! FOR /F "tokens=1,2,3 delims=:,." %%a IN ("%TIME%") DO ( SET "HH=%%a" SET "MM=%%b" SET "ss=%%c" ) SET "TIMESTAMP=!HH!:!MM!:!ss!" GOTO :eof :RESET_ERROR @REM Subroutine to reset %ERRORLEVEL% to 0. @REM CALL :RESET_ERROR @REM. @REM Updates: %ERRORLEVEL% EXIT /B 0 GOTO :eof ================================================ FILE: bin/device-install.sh ================================================ #!/usr/bin/env bash PYTHON=${PYTHON:-$(which python3 python | head -n 1)} BPS_RESET=false MCU="" # Constants RESET_BAUD=1200 FIRMWARE_OFFSET=0x00 # Default littlefs* offset. OFFSET=0x300000 # Default OTA Offset OTA_OFFSET=0x260000 # Determine the correct esptool command to use if "$PYTHON" -m esptool version >/dev/null 2>&1; then ESPTOOL_CMD="$PYTHON -m esptool" elif command -v esptool >/dev/null 2>&1; then ESPTOOL_CMD="esptool" elif command -v esptool.py >/dev/null 2>&1; then ESPTOOL_CMD="esptool.py" else echo "Error: esptool not found" exit 1 fi # Check for jq if ! command -v jq >/dev/null 2>&1; then echo "Error: jq not found" >&2 echo "Install jq with your package manager." >&2 echo "e.g. 'apt install jq', 'dnf install jq', 'brew install jq', etc." >&2 exit 1 fi # esptool v5 supports commands with dashes and deprecates commands with # underscores. Prior versions only support commands with underscores if ${ESPTOOL_CMD} | grep --quiet write-flash then ESPTOOL_WRITE_FLASH=write-flash ESPTOOL_ERASE_FLASH=erase-flash ESPTOOL_READ_FLASH_STATUS=read-flash-status else ESPTOOL_WRITE_FLASH=write_flash ESPTOOL_ERASE_FLASH=erase_flash ESPTOOL_READ_FLASH_STATUS=read_flash_status fi set -e # Usage info show_help() { cat <&2 exit 1 ;; esac shift # Move to the next argument done if [[ $BPS_RESET == true ]]; then $ESPTOOL_CMD --baud $RESET_BAUD --after no_reset ${ESPTOOL_READ_FLASH_STATUS} exit 0 fi [ -z "$FILENAME" ] && [ -n "$1" ] && { FILENAME="$1" shift } if [[ $(basename "$FILENAME") != firmware-*.factory.bin ]]; then echo "Filename must be a firmware-*.factory.bin file." exit 1 fi # Extract PROGNAME from %FILENAME% for later use. PROGNAME="${FILENAME/.factory.bin/}" # Derive metadata filename from %PROGNAME%. METAFILE="${PROGNAME}.mt.json" if [[ -f "$FILENAME" && "$FILENAME" == *.factory.bin ]]; then # Display metadata if it exists if [[ -f "$METAFILE" ]]; then echo "Firmware metadata: ${METAFILE}" jq . "$METAFILE" # Extract relevant fields from metadata if [[ $(jq -r '.part' "$METAFILE") != "null" ]]; then OTA_OFFSET=$(jq -r '.part[] | select(.subtype == "ota_1") | .offset' "$METAFILE") SPIFFS_OFFSET=$(jq -r '.part[] | select(.subtype == "spiffs") | .offset' "$METAFILE") fi MCU=$(jq -r '.mcu' "$METAFILE") else echo "ERROR: No metadata file found at ${METAFILE}" exit 1 fi # Determine OTA filename based on MCU type (unified OTA format) OTAFILE="mt-${MCU}-ota.bin" # Set SPIFFS filename with "littlefs-" prefix. SPIFFSFILE="littlefs-${PROGNAME/firmware-/}.bin" if [[ ! -f "$FILENAME" ]]; then echo "Error: file ${FILENAME} wasn't found. Terminating." exit 1 fi if [[ ! -f "$OTAFILE" ]]; then echo "Error: file ${OTAFILE} wasn't found. Terminating." exit 1 fi if [[ ! -f "$SPIFFSFILE" ]]; then echo "Error: file ${SPIFFSFILE} wasn't found. Terminating." exit 1 fi echo "Trying to flash ${FILENAME}, but first erasing and writing system information" $ESPTOOL_CMD ${ESPTOOL_ERASE_FLASH} $ESPTOOL_CMD ${ESPTOOL_WRITE_FLASH} $FIRMWARE_OFFSET "${FILENAME}" echo "Trying to flash ${OTAFILE} at offset ${OTA_OFFSET}" $ESPTOOL_CMD ${ESPTOOL_WRITE_FLASH} $OTA_OFFSET "${OTAFILE}" echo "Trying to flash ${SPIFFSFILE}, at offset ${OFFSET}" $ESPTOOL_CMD ${ESPTOOL_WRITE_FLASH} $OFFSET "${SPIFFSFILE}" else show_help echo "Invalid file: ${FILENAME}" fi exit 0 ================================================ FILE: bin/device-install_test.ps1 ================================================ <# .SYNOPSIS Unit-test for .\device-install.bat. .DESCRIPTION This script performs a positive unit-test on .\device-install.bat by creating the expected .bin files for a device followed by running the .bat script without flashing the firmware (--debug). If any errors are hit they are presented in the standard output. Investigate accordingly. This script needs to be placed in the same directory as .\device-install.bat. .EXAMPLE .\device-install_test.ps1 .EXAMPLE .\device-install_test.ps1 -Verbose .LINK .\device-install.bat --help #> [CmdletBinding()] param() function New-EmptyFile() { [CmdletBinding()] param ( [Parameter(Position = 0, Mandatory = $true)] # Specifies the file name. [string]$FileName, [Parameter(Position = 1)] # Specifies the target path. (Get-Location).Path is the default. [string]$Directory = (Get-Location).Path ) $filePath = Join-Path -Path $Directory -ChildPath $FileName Write-Verbose -Message "Create empty test file if it doesn't exist: $($FileName)" New-Item -Path "$filePath" -ItemType File -ErrorAction SilentlyContinue | Out-Null } function Remove-EmptyFile() { [CmdletBinding()] param ( [Parameter(Position = 0, Mandatory = $true)] # Specifies the file name. [string]$FileName, [Parameter(Position = 1)] # Specifies the target path. (Get-Location).Path is the default. [string]$Directory = (Get-Location).Path ) $filePath = Join-Path -Path $Directory -ChildPath $FileName Write-Verbose -Message "Deleted empty test file: $($FileName)" Remove-Item -Path "$filePath" | Out-Null } $TestCases = New-Object -TypeName PSObject -Property @{ # Use this PSObject to define testcases according to this syntax: # "testname" = @("firmware-testname","bleota","littlefs-testname","args") "t-deck" = @("firmware-t-deck-2.6.0.0b106d4.bin", "bleota-s3.bin", "littlefs-t-deck-2.6.0.0b106d4.bin", "") "t-deck_web" = @("firmware-t-deck-2.6.0.0b106d4.bin", "bleota-s3.bin", "littlefswebui-t-deck-2.6.0.0b106d4.bin", "--web") "t-deck-tft" = @("firmware-t-deck-tft-2.6.0.0b106d4.bin", "bleota-s3.bin", "littlefs-t-deck-tft-2.6.0.0b106d4.bin", "") "heltec-ht62-esp32c3" = @("firmware-heltec-ht62-esp32c3-sx1262-2.6.0.0b106d4.bin", "bleota-c3.bin", "littlefs-heltec-ht62-esp32c3-sx1262-2.6.0.0b106d4.bin", "") "tlora-c6" = @("firmware-tlora-c6-2.6.0.0b106d4.bin", "bleota.bin", "littlefs-tlora-c6-2.6.0.0b106d4.bin", "") "heltec-v3_web" = @("firmware-heltec-v3-2.6.0.0b106d4.bin", "bleota-s3.bin", "littlefswebui-heltec-v3-2.6.0.0b106d4.bin", "--web") "seeed-sensecap-indicator-tft" = @("firmware-seeed-sensecap-indicator-tft-2.6.0.0b106d4.bin", "bleota.bin", "littlefs-seeed-sensecap-indicator-tft-2.6.0.0b106d4.bin", "") "picomputer-s3-tft" = @("firmware-picomputer-s3-tft-2.6.0.0b106d4.bin", "bleota-s3.bin", "littlefs-picomputer-s3-tft-2.6.0.0b106d4.bin", "") } foreach ($TestCase in $TestCases.PSObject.Properties) { $Name = $TestCase.Name $Files = $TestCase.Value $Errors = $null $Counter = 0 Write-Host -Object "Testcase: $Name`:" -ForegroundColor Green foreach ($File in $Files) { if ($File.EndsWith(".bin")) { New-EmptyFile -FileName $File } } Write-Host -Object "Performing test on $Name..." -ForegroundColor Blue $Test = Invoke-Expression -Command "cmd /c .\device-install.bat --debug -f $($TestCases."$Name"[0]) $($TestCases."$Name"[3])" foreach ($Line in $Test) { if ($Line -match "Set OTA_OFFSET to" -or ` $Line -match "Set SPIFFS_OFFSET to") { Write-Host -Object "$($Line -replace "^.*?Set","Set")" -ForegroundColor Blue } elseif ($VerbosePreference -eq "Continue") { Write-Host -Object $Line } if ($Line -match "ERROR") { $Errors += $Line $Counter++ } } if ($null -ne $Errors) { Write-Host -Object "$Counter ERROR(s) detected!" -ForegroundColor Red if (-not ($VerbosePreference -eq "Continue")) { Write-Host -Object $Errors } } foreach ($File in $Files) { if ($File.EndsWith(".bin")) { Remove-EmptyFile -FileName $File } } } ================================================ FILE: bin/device-update.bat ================================================ @ECHO OFF SETLOCAL EnableDelayedExpansion TITLE Meshtastic device-update SET "SCRIPT_NAME=%~nx0" SET "DEBUG=0" SET "PYTHON=" SET "ESPTOOL_BAUD=115200" SET "RESET_BAUD=1200" SET "UPDATE_OFFSET=0x10000" SET "ESPTOOL_CMD=" SET "LOGCOUNTER=0" SET "CHANGE_MODE=0" GOTO getopts :help ECHO Flash image file to device, but leave existing system intact. ECHO. ECHO Usage: %SCRIPT_NAME% -f filename [-p PORT] [-P python] [--change-mode] ECHO. ECHO Options: ECHO -f filename The update .bin file to flash. Custom to your device type and region. (required) ECHO The file must be located in this current directory. ECHO -p PORT Set the environment variable for ESPTOOL_PORT. ECHO If not set, ESPTOOL iterates all ports (Dangerous). ECHO -P python Specify alternate python interpreter to use to invoke esptool. (default: python) ECHO If supplied the script will use python. ECHO If not supplied the script will try to find esptool in Path. ECHO --change-mode Attempt to place the device in correct mode. (1200bps Reset) ECHO Some hardware requires this twice. ECHO. ECHO Example: %SCRIPT_NAME% -p COM17 --change-mode ECHO Example: %SCRIPT_NAME% -f firmware-t-deck-tft-2.6.0.0b106d4.bin -p COM11 GOTO eof :version ECHO %SCRIPT_NAME% [Version 2.7.0] ECHO Meshtastic GOTO eof :getopts IF "%~1"=="" GOTO endopts IF /I "%~1"=="-?" GOTO help IF /I "%~1"=="-h" GOTO help IF /I "%~1"=="--help" GOTO help IF /I "%~1"=="-v" GOTO version IF /I "%~1"=="--version" GOTO version IF /I "%~1"=="--debug" SET "DEBUG=1" & CALL :LOG_MESSAGE DEBUG "DEBUG mode: enabled." IF /I "%~1"=="-f" SET "FILENAME=%~2" & SHIFT IF "%~1"=="-p" SET "ESPTOOL_PORT=%~2" & SHIFT IF /I "%~1"=="--port" SET "ESPTOOL_PORT=%~2" & SHIFT IF "%~1"=="-P" SET "PYTHON=%~2" & SHIFT IF /I "%~1"=="--change-mode" SET "CHANGE_MODE=1" SHIFT GOTO getopts :endopts IF %CHANGE_MODE% EQU 1 GOTO skip-filename CALL :LOG_MESSAGE DEBUG "Checking FILENAME parameter..." IF "__!FILENAME!__"=="____" ( CALL :LOG_MESSAGE DEBUG "Missing -f filename input." GOTO help ) ELSE ( CALL :LOG_MESSAGE DEBUG "Filename: !FILENAME!" IF NOT "__!FILENAME: =!__"=="__!FILENAME!__" ( CALL :LOG_MESSAGE ERROR "Filename containing spaces are not supported." GOTO help ) @REM Remove ".\" or "./" file prefix if present. SET "FILENAME=!FILENAME:.\=!" SET "FILENAME=!FILENAME:./=!" ) CALL :LOG_MESSAGE DEBUG "Checking if !FILENAME! exists..." IF NOT EXIST !FILENAME! ( CALL :LOG_MESSAGE ERROR "File does not exist: !FILENAME!. Terminating." GOTO eof ) IF NOT "__!FILENAME:.factory.bin=!__"=="__!FILENAME!__" ( CALL :LOG_MESSAGE DEBUG "We are working with a *.factory.bin* file. !FILENAME!" CALL :LOG_MESSAGE INFO "Use script device-install.bat to flash !FILENAME!." GOTO eof ) ELSE ( CALL :LOG_MESSAGE DEBUG "We are not working with a *.factory.bin* file. !FILENAME!" ) :skip-filename CALL :LOG_MESSAGE DEBUG "Determine the correct esptool command to use..." IF NOT "__%PYTHON%__"=="____" ( SET "ESPTOOL_CMD=""!PYTHON!"" -m esptool" CALL :LOG_MESSAGE DEBUG "Python interpreter supplied." ) ELSE ( CALL :LOG_MESSAGE DEBUG "Python interpreter NOT supplied. Looking for esptool..." WHERE esptool >nul 2>&1 IF %ERRORLEVEL% EQU 0 ( @REM WHERE exits with code 0 if esptool is found. SET "ESPTOOL_CMD=esptool" ) ELSE ( SET "ESPTOOL_CMD=python -m esptool" CALL :RESET_ERROR ) ) CALL :LOG_MESSAGE DEBUG "Checking esptool command !ESPTOOL_CMD!..." !ESPTOOL_CMD! >nul 2>&1 CALL :LOG_MESSAGE DEBUG "esptool exit code: %ERRORLEVEL%" IF %ERRORLEVEL% EQU 9009 ( @REM 9009 = command not found on Windows CALL :LOG_MESSAGE ERROR "esptool not found: !ESPTOOL_CMD!" EXIT /B 1 ) IF %DEBUG% EQU 1 ( CALL :LOG_MESSAGE DEBUG "Skipping ESPTOOL_CMD steps." SET "ESPTOOL_CMD=REM !ESPTOOL_CMD!" ) CALL :LOG_MESSAGE DEBUG "Using esptool command: !ESPTOOL_CMD!" IF "__!ESPTOOL_PORT!__" == "____" ( CALL :LOG_MESSAGE WARN "Using esptool port: UNSET." ) ELSE ( SET "ESPTOOL_CMD=!ESPTOOL_CMD! --port !ESPTOOL_PORT!" CALL :LOG_MESSAGE INFO "Using esptool port: !ESPTOOL_PORT!." ) CALL :LOG_MESSAGE INFO "Using esptool baud: !ESPTOOL_BAUD!." IF %CHANGE_MODE% EQU 1 ( @REM Attempt to change mode via 1200bps Reset. CALL :RUN_ESPTOOL !RESET_BAUD! --after no_reset read_flash_status GOTO eof ) @REM Flashing operations. CALL :LOG_MESSAGE INFO "Trying to flash update "!FILENAME!" at OFFSET !UPDATE_OFFSET!..." CALL :RUN_ESPTOOL !ESPTOOL_BAUD! write-flash !UPDATE_OFFSET! "!FILENAME!" || GOTO eof CALL :LOG_MESSAGE INFO "Script complete!." :eof ENDLOCAL EXIT /B %ERRORLEVEL% :RUN_ESPTOOL @REM Subroutine used to run ESPTOOL_CMD with arguments. @REM Also handles %ERRORLEVEL%. @REM CALL :RUN_ESPTOOL [Baud] [erase-flash|write-flash] [OFFSET] [Filename] @REM. @REM Example:: CALL :RUN_ESPTOOL 115200 write-flash 0x10000 "firmwarefile.bin" IF %DEBUG% EQU 1 CALL :LOG_MESSAGE DEBUG "About to run command: !ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4" CALL :RESET_ERROR !ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4 IF %CHANGE_MODE% EQU 1 GOTO :eof IF %ERRORLEVEL% NEQ 0 ( CALL :LOG_MESSAGE ERROR "Error running command: !ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4" EXIT /B %ERRORLEVEL% ) GOTO :eof :LOG_MESSAGE @REM Subroutine used to print log messages in four different levels. @REM DEBUG messages only get printed if [-d] flag is passed to script. @REM CALL :LOG_MESSAGE [ERROR|INFO|WARN|DEBUG] "Message" @REM. @REM Example:: CALL :LOG_MESSAGE INFO "Message." SET /A LOGCOUNTER=LOGCOUNTER+1 IF "%1" == "ERROR" CALL :GET_TIMESTAMP & ECHO %1 ^| !TIMESTAMP! !LOGCOUNTER! %~2 IF "%1" == "INFO" CALL :GET_TIMESTAMP & ECHO %1 ^| !TIMESTAMP! !LOGCOUNTER! %~2 IF "%1" == "WARN" CALL :GET_TIMESTAMP & ECHO %1 ^| !TIMESTAMP! !LOGCOUNTER! %~2 IF "%1" == "DEBUG" IF %DEBUG% EQU 1 CALL :GET_TIMESTAMP & ECHO %1 ^| !TIMESTAMP! !LOGCOUNTER! %~2 GOTO :eof :GET_TIMESTAMP @REM Subroutine used to set !TIMESTAMP! to HH:MM:ss. @REM CALL :GET_TIMESTAMP @REM. @REM Updates: !TIMESTAMP! FOR /F "tokens=1,2,3 delims=:,." %%a IN ("%TIME%") DO ( SET "HH=%%a" SET "MM=%%b" SET "ss=%%c" ) SET "TIMESTAMP=!HH!:!MM!:!ss!" GOTO :eof :RESET_ERROR @REM Subroutine to reset %ERRORLEVEL% to 0. @REM CALL :RESET_ERROR @REM. @REM Updates: %ERRORLEVEL% EXIT /B 0 GOTO :eof ================================================ FILE: bin/device-update.sh ================================================ #!/usr/bin/env bash PYTHON=${PYTHON:-$(which python3 python|head -n 1)} CHANGE_MODE=false # Constants FLASH_BAUD=115200 RESET_BAUD=1200 UPDATE_OFFSET=0x10000 # Determine the correct esptool command to use if "$PYTHON" -m esptool version >/dev/null 2>&1; then ESPTOOL_CMD="$PYTHON -m esptool" elif command -v esptool >/dev/null 2>&1; then ESPTOOL_CMD="esptool" elif command -v esptool.py >/dev/null 2>&1; then ESPTOOL_CMD="esptool.py" else echo "Error: esptool not found" exit 1 fi # esptool v5 supports commands with dashes and deprecates commands with # underscores. Prior versions only support commands with underscores if ${ESPTOOL_CMD} | grep --quiet write-flash then ESPTOOL_WRITE_FLASH=write-flash ESPTOOL_READ_FLASH_STATUS=read-flash-status else ESPTOOL_WRITE_FLASH=write_flash ESPTOOL_READ_FLASH_STATUS=read_flash_status fi # Usage info show_help() { cat << EOF Usage: $(basename "$0") [-h] [-p ESPTOOL_PORT] [-P PYTHON] [-f FILENAME|FILENAME] [--change-mode] 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 (Dangerous). -P PYTHON Specify alternate python interpreter to use to invoke esptool. (Default: "$PYTHON") -f FILENAME The *.bin file to flash. Custom to your device type. --change-mode Attempt to place the device in correct mode. Some hardware requires this twice. (1200bps Reset) EOF } # Check for --change-mode and remove it from arguments NEW_ARGS=() for arg in "$@"; do if [ "$arg" = "--change-mode" ]; then CHANGE_MODE=true else NEW_ARGS+=("$arg") fi done set -- "${NEW_ARGS[@]}" while getopts ":hp:P:f:" opt; do case "${opt}" in h) show_help exit 0 ;; p) ESPTOOL_CMD="$ESPTOOL_CMD --port ${OPTARG}" ;; P) PYTHON=${OPTARG} ;; f) FILENAME=${OPTARG} ;; *) echo "Invalid flag." show_help >&2 exit 1 ;; esac done shift "$((OPTIND-1))" if [ "$CHANGE_MODE" = true ]; then $ESPTOOL_CMD --baud $RESET_BAUD --after no_reset ${ESPTOOL_READ_FLASH_STATUS} exit 0 fi [ -z "$FILENAME" ] && [ -n "$1" ] && { FILENAME="$1" shift } if [[ -f "$FILENAME" && "$FILENAME" != *.factory.bin ]]; then echo "Trying to flash update ${FILENAME}" $ESPTOOL_CMD --baud $FLASH_BAUD ${ESPTOOL_WRITE_FLASH} $UPDATE_OFFSET "${FILENAME}" else show_help echo "Invalid file: ${FILENAME}" fi exit 0 ================================================ FILE: bin/dump-ram-users.sh ================================================ #!/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' ================================================ FILE: bin/exception_decoder.py ================================================ #!/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"\b(0x4[0-9a-fA-F]{7,8}):0x[0-9a-fA-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) ================================================ FILE: bin/gen-images.sh ================================================ #!/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 ================================================ FILE: bin/generate_ci_matrix.py ================================================ #!/usr/bin/env python3 """Generate the CI matrix.""" import argparse import json import re from platformio.project.config import ProjectConfig parser = argparse.ArgumentParser(description="Generate the CI matrix") parser.add_argument("platform", help="Platform to build for") parser.add_argument( "--level", choices=["extra", "pr"], nargs="*", default=[], help="Board level to build for (omit for full release boards)", ) args = parser.parse_args() outlist = [] cfg = ProjectConfig.get_instance() pio_envs = cfg.envs() # Gather all PlatformIO environments for filtering later all_envs = [] for pio_env in pio_envs: env_build_flags = cfg.get(f"env:{pio_env}", "build_flags") env_platform = None for flag in env_build_flags: # Extract the platform from the build flags # Example flag: -I variants/esp32s3/heltec-v3 match = re.search(r"-I\s?variants/([^/]+)", flag) if match: env_platform = match.group(1) break # Intentionally fail if platform cannot be determined if not env_platform: print(f"Error: Could not determine platform for environment '{pio_env}'") exit(1) # Store env details as a dictionary, and add to 'all_envs' list env = { "ci": {"board": pio_env, "platform": env_platform}, "board_level": cfg.get(f"env:{pio_env}", "board_level", default=None), "board_check": bool(cfg.get(f"env:{pio_env}", "board_check", default=False)), } all_envs.append(env) # Filter outputs based on options # Check is mutually exclusive with other options (except 'pr') if "check" in args.platform: for env in all_envs: if env["board_check"]: if "pr" in args.level: if env["board_level"] == "pr": outlist.append(env["ci"]) else: outlist.append(env["ci"]) # Filter (non-check) builds by platform else: for env in all_envs: if args.platform == env["ci"]["platform"] or args.platform == "all": # Always include board_level = 'pr' if env["board_level"] == "pr": outlist.append(env["ci"]) # Include board_level = 'extra' when requested elif "extra" in args.level and env["board_level"] == "extra": outlist.append(env["ci"]) # If no board level is specified, include in release builds (not PR) elif "pr" not in args.level and not env["board_level"]: outlist.append(env["ci"]) # Return as a JSON list print(json.dumps(outlist)) ================================================ FILE: bin/generate_release_notes.py ================================================ #!/usr/bin/env python3 """ Generate release notes from merged PRs on develop and master branches. Categorizes PRs into Enhancements and Bug Fixes/Maintenance sections. """ import subprocess import re import json import sys from datetime import datetime def get_last_release_tag(): """Get the most recent release tag.""" result = subprocess.run( ["git", "describe", "--tags", "--abbrev=0"], capture_output=True, text=True, check=True, ) return result.stdout.strip() def get_tag_date(tag): """Get the commit date (ISO 8601) of the tag.""" result = subprocess.run( ["git", "show", "-s", "--format=%cI", tag], capture_output=True, text=True, check=True, ) return result.stdout.strip() def get_merged_prs_since_tag(tag, branch): """Get all merged PRs since the given tag on the specified branch.""" # Get commits since tag on the branch - look for PR numbers in parentheses result = subprocess.run( [ "git", "log", f"{tag}..origin/{branch}", "--oneline", ], capture_output=True, text=True, ) prs = [] seen_pr_numbers = set() for line in result.stdout.strip().split("\n"): if not line: continue # Extract PR number from commit message - format: "Title (#1234)" pr_match = re.search(r"\(#(\d+)\)", line) if pr_match: pr_number = pr_match.group(1) if pr_number not in seen_pr_numbers: seen_pr_numbers.add(pr_number) prs.append(pr_number) return prs def get_pr_details(pr_number): """Get PR details from GitHub API via gh CLI.""" try: result = subprocess.run( [ "gh", "pr", "view", pr_number, "--json", "title,author,labels,url", ], capture_output=True, text=True, check=True, ) return json.loads(result.stdout) except subprocess.CalledProcessError: return None def should_exclude_pr(pr_details): """Check if PR should be excluded from release notes.""" if not pr_details: return True title = pr_details.get("title", "").lower() # Exclude trunk update PRs if "upgrade trunk" in title or "update trunk" in title or "trunk update" in title: return True # Exclude protobuf update PRs if "update protobufs" in title or "update protobuf" in title: return True # Exclude automated version bump PRs if "bump release version" in title or "bump version" in title: return True return False def is_dependency_update(pr_details): """Check if PR is a dependency/chore update.""" if not pr_details: return False title = pr_details.get("title", "").lower() author = pr_details.get("author", {}).get("login", "").lower() labels = [label.get("name", "").lower() for label in pr_details.get("labels", [])] # Check for renovate or dependabot authors if "renovate" in author or "dependabot" in author: return True # Check for chore(deps) pattern if re.match(r"^chore\(deps\):", title): return True # Check for digest update patterns if re.match(r".*digest to [a-f0-9]+", title, re.IGNORECASE): return True # Check for dependency-related labels dependency_labels = ["dependencies", "deps", "renovate"] if any(dep in label for label in labels for dep in dependency_labels): return True return False def is_enhancement(pr_details): """Determine if PR is an enhancement based on labels and title.""" labels = [label.get("name", "").lower() for label in pr_details.get("labels", [])] # Check labels first enhancement_labels = ["enhancement", "feature", "feat", "new feature"] for label in labels: if any(enh in label for enh in enhancement_labels): return True # Check title prefixes title = pr_details.get("title", "") enhancement_prefixes = ["feat:", "feature:", "add:"] title_lower = title.lower() for prefix in enhancement_prefixes: if title_lower.startswith(prefix) or f" {prefix}" in title_lower: return True return False def clean_title(title): """Clean up PR title for release notes.""" # Remove common prefixes prefixes_to_remove = [ r"^fix:\s*", r"^feat:\s*", r"^feature:\s*", r"^bug:\s*", r"^bugfix:\s*", r"^chore:\s*", r"^chore\([^)]+\):\s*", r"^refactor:\s*", r"^docs:\s*", r"^ci:\s*", r"^build:\s*", r"^perf:\s*", r"^style:\s*", r"^test:\s*", ] cleaned = title for prefix in prefixes_to_remove: cleaned = re.sub(prefix, "", cleaned, flags=re.IGNORECASE) # Ensure first letter is capitalized if cleaned: cleaned = cleaned[0].upper() + cleaned[1:] return cleaned.strip() def format_pr_line(pr_details): """Format a PR as a markdown bullet point.""" title = clean_title(pr_details.get("title", "Unknown")) author = pr_details.get("author", {}).get("login", "unknown") url = pr_details.get("url", "") return f"- {title} by @{author} in {url}" def get_new_contributors(pr_details_list, tag, repo="meshtastic/firmware"): """Find contributors who made their first merged PR before this release. GitHub usernames do not necessarily match git commit authors, so we use the GitHub search API via `gh` to see if the user has any merged PRs before the tag date. This mirrors how GitHub's "Generate release notes" feature works. """ bot_authors = {"github-actions", "renovate", "dependabot", "app/renovate", "app/github-actions", "app/dependabot"} new_contributors = [] seen_authors = set() try: tag_date = get_tag_date(tag) except subprocess.CalledProcessError: print(f"Warning: Could not determine tag date for {tag}; skipping new contributor detection", file=sys.stderr) return [] for pr in pr_details_list: author = pr.get("author", {}).get("login", "") if not author or author in seen_authors: continue # Skip bots if author.lower() in bot_authors or author.startswith("app/"): continue seen_authors.add(author) try: # Search for merged PRs by this author created before the tag date search_query = f"is:pr author:{author} repo:{repo} closed:<=\"{tag_date}\"" search = subprocess.run( [ "gh", "search", "issues", "--json", "number,mergedAt,createdAt", "--state", "closed", "--limit", "200", search_query, ], capture_output=True, text=True, ) if search.returncode != 0: # If gh fails, be conservative and skip adding to new contributors print(f"Warning: gh search failed for author {author}: {search.stderr.strip()}", file=sys.stderr) continue results = json.loads(search.stdout or "[]") # If any merged PR exists before or on tag date, not a new contributor had_prior_pr = any(item.get("mergedAt") for item in results) if not had_prior_pr: new_contributors.append((author, pr.get("url", ""))) except Exception as e: print(f"Warning: Could not check contributor history for {author}: {e}", file=sys.stderr) continue return new_contributors def main(): if len(sys.argv) < 2: print("Usage: generate_release_notes.py ", file=sys.stderr) sys.exit(1) new_version = sys.argv[1] # Get last release tag try: last_tag = get_last_release_tag() except subprocess.CalledProcessError: print("Error: Could not find last release tag", file=sys.stderr) sys.exit(1) # Collect PRs from both branches all_pr_numbers = set() for branch in ["develop", "master"]: try: prs = get_merged_prs_since_tag(last_tag, branch) all_pr_numbers.update(prs) except Exception as e: print(f"Warning: Could not get PRs from {branch}: {e}", file=sys.stderr) # Get details for all PRs enhancements = [] bug_fixes = [] dependencies = [] all_pr_details = [] for pr_number in sorted(all_pr_numbers, key=int): details = get_pr_details(pr_number) if details and not should_exclude_pr(details): all_pr_details.append(details) if is_dependency_update(details): dependencies.append(details) elif is_enhancement(details): enhancements.append(details) else: bug_fixes.append(details) # Generate release notes output = [] if enhancements: output.append("## 🚀 Enhancements\n") for pr in enhancements: output.append(format_pr_line(pr)) output.append("") if bug_fixes: output.append("## 🐛 Bug fixes and maintenance\n") for pr in bug_fixes: output.append(format_pr_line(pr)) output.append("") if dependencies: output.append("## ⚙️ Dependencies\n") for pr in dependencies: output.append(format_pr_line(pr)) output.append("") # Find new contributors (GitHub-accurate check using merged PRs before tag date) new_contributors = get_new_contributors(all_pr_details, last_tag) if new_contributors: output.append("## New Contributors\n") for author, url in new_contributors: # Find first PR URL for this contributor first_pr_url = url for pr in all_pr_details: if pr.get("author", {}).get("login") == author: first_pr_url = pr.get("url", url) break output.append(f"- @{author} made their first contribution in {first_pr_url}") output.append("") # Add full changelog link output.append( f"**Full Changelog**: https://github.com/meshtastic/firmware/compare/{last_tag}...v{new_version}" ) print("\n".join(output)) if __name__ == "__main__": main() ================================================ FILE: bin/generic/Meshtastic_6.1.0_bootloader-0.9.2_s140_6.1.1.hex ================================================ :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 ================================================ FILE: bin/generic/Meshtastic_7.3.0_bootloader-0.9.2_s140_7.3.0.hex ================================================ :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 ================================================ FILE: bin/genpartitions.py ================================================ #!/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) ================================================ FILE: bin/kill-github-actions.sh ================================================ #!/bin/bash # Script to cancel all running GitHub Actions workflows # Requires GitHub CLI (gh) to be installed and authenticated set -e # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' # No Color # Function to print colored output print_status() { echo -e "${GREEN}[INFO]${NC} $1" } print_warning() { echo -e "${YELLOW}[WARN]${NC} $1" } print_error() { echo -e "${RED}[ERROR]${NC} $1" } # Check if gh CLI is installed if ! command -v gh &> /dev/null; then print_error "GitHub CLI (gh) is not installed. Please install it first:" echo " brew install gh" echo " Or visit: https://cli.github.com/" exit 1 fi # Check if authenticated if ! gh auth status &> /dev/null; then print_error "GitHub CLI is not authenticated. Please run:" echo " gh auth login" exit 1 fi # Get repository info REPO=$(gh repo view --json owner,name -q '.owner.login + "/" + .name') if [[ -z "$REPO" ]]; then print_error "Could not determine repository. Make sure you're in a GitHub repository." exit 1 fi print_status "Working with repository: $REPO" # Get all active workflows (both queued and in-progress) print_status "Fetching active workflows (queued and in-progress)..." QUEUED_WORKFLOWS=$(gh run list --status queued --json databaseId,displayTitle,headBranch,status --limit 100) IN_PROGRESS_WORKFLOWS=$(gh run list --status in_progress --json databaseId,displayTitle,headBranch,status --limit 100) # Combine both lists ALL_WORKFLOWS=$(echo "$QUEUED_WORKFLOWS $IN_PROGRESS_WORKFLOWS" | jq -s 'add | unique_by(.databaseId)') if [[ "$ALL_WORKFLOWS" == "[]" ]]; then print_status "No active workflows found." exit 0 fi # Parse and display active workflows echo print_warning "Found active workflows:" echo "$ALL_WORKFLOWS" | jq -r '.[] | " - \(.displayTitle) (Branch: \(.headBranch), Status: \(.status), ID: \(.databaseId))"' echo read -p "Do you want to cancel ALL these workflows? (y/N): " -n 1 -r echo if [[ ! $REPLY =~ ^[Yy]$ ]]; then print_status "Cancelled by user." exit 0 fi # Cancel each workflow print_status "Cancelling workflows..." CANCELLED_COUNT=0 FAILED_COUNT=0 while IFS= read -r WORKFLOW_ID; do if [[ -n "$WORKFLOW_ID" ]]; then print_status "Cancelling workflow ID: $WORKFLOW_ID" if gh run cancel "$WORKFLOW_ID" 2>/dev/null; then ((CANCELLED_COUNT++)) else print_error "Failed to cancel workflow ID: $WORKFLOW_ID" ((FAILED_COUNT++)) fi fi done < <(echo "$ALL_WORKFLOWS" | jq -r '.[].databaseId') echo print_status "Summary:" echo " - Cancelled: $CANCELLED_COUNT workflows" if [[ $FAILED_COUNT -gt 0 ]]; then echo " - Failed: $FAILED_COUNT workflows" fi print_status "Done!" # Optional: Show remaining active workflows echo print_status "Checking for any remaining active workflows..." REMAINING_QUEUED=$(gh run list --status queued --json databaseId --limit 10) REMAINING_IN_PROGRESS=$(gh run list --status in_progress --json databaseId --limit 10) REMAINING_ALL=$(echo "$REMAINING_QUEUED $REMAINING_IN_PROGRESS" | jq -s 'add | unique_by(.databaseId)') if [[ "$REMAINING_ALL" == "[]" ]]; then print_status "All workflows successfully cancelled." else REMAINING_COUNT=$(echo "$REMAINING_ALL" | jq '. | length') print_warning "Still $REMAINING_COUNT workflows active (may take a moment to update status)" fi ================================================ FILE: bin/meshtasticd-start.sh ================================================ #!/usr/bin/env sh INSTANCE=$1 CONF_DIR="/etc/meshtasticd/config.d" VFS_DIR="/var/lib" # If no instance ID provided, start bare daemon and exit echo "no instance ID provided, starting bare meshtasticd service" if [ -z "${INSTANCE}" ]; then /usr/bin/meshtasticd exit 0 fi # Make VFS dir if it does not exist if [ ! -d "${VFS_DIR}/meshtasticd-${INSTANCE}" ]; then echo "vfs for ${INSTANCE} does not exist, creating it." mkdir "${VFS_DIR}/meshtasticd-${INSTANCE}" fi # Abort if config for $INSTANCE does not exist if [ ! -f "${CONF_DIR}/config-${INSTANCE}.yaml" ]; then echo "no config for ${INSTANCE} found in ${CONF_DIR}. refusing to start" >&2 exit 1 fi # Start meshtasticd with instance parameters printf "starting meshtasticd-%s..., ${INSTANCE}" if /usr/bin/meshtasticd --config="${CONF_DIR}/config-${INSTANCE}.yaml" --fsdir="${VFS_DIR}/meshtasticd-${INSTANCE}"; then echo "ok" else echo "failed" fi ================================================ FILE: bin/meshtasticd.service ================================================ [Unit] Description=Meshtastic %i Daemon After=network-online.target StartLimitInterval=200 StartLimitBurst=5 [Service] AmbientCapabilities=CAP_NET_BIND_SERVICE User=meshtasticd Group=meshtasticd Type=simple ExecStart=/usr/bin/meshtasticd-start.sh %i Restart=always RestartSec=3 [Install] WantedBy=multi-user.target ================================================ FILE: bin/native-gdbserver.sh ================================================ #!/usr/bin/env bash set -e pio run --environment native gdbserver --once localhost:2345 .pio/build/native/meshtasticd "$@" ================================================ FILE: bin/native-install.sh ================================================ #!/usr/bin/env bash cp "release/meshtasticd_linux_$(uname -m)" /usr/bin/meshtasticd cp "bin/meshtasticd-start.sh" /usr/bin/meshtasticd-start.sh 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 ================================================ FILE: bin/native-run.sh ================================================ #!/usr/bin/env bash set -e pio run --environment native .pio/build/native/meshtasticd "$@" ================================================ FILE: bin/org.meshtastic.meshtasticd.desktop ================================================ [Desktop Entry] Name=Meshtastic Comment=Meshtastic App Exec=meshtasticd Icon=org.meshtastic.meshtasticd Terminal=true Type=Application Categories=Network;Chat;HamRadio; ================================================ FILE: bin/org.meshtastic.meshtasticd.metainfo.xml ================================================ org.meshtastic.meshtasticd Meshtastic Decentralized mesh communication CC-BY-4.0 GPL-3.0-or-later Meshtastic

Meshtastic is an open source project for creating off-grid, affordable, and resilient communication with LoRa mesh networks.

org.meshtastic.meshtasticd.desktop Network Chat HamRadio mesh LoRa keyboard pointing touch 360 #97be89 #206538 intense intense https://github.com/meshtastic/firmware/issues https://meshtastic.org/ https://opencollective.com/meshtastic https://meshtastic.org/docs/software/linux/usage/ https://github.com/meshtastic/firmware/ https://meshtastic.org/img/software/meshtastic-ui/mui_home_dashboard_dark.webp Home Dashboard https://meshtastic.org/img/software/meshtastic-ui/mui_initial_boot.webp Setup https://meshtastic.org/img/software/meshtastic-ui/mui_node_list_dark.webp Nodes List https://meshtastic.org/img/software/meshtastic-ui/mui_chat_list_dark.webp Chats List https://meshtastic.org/img/software/meshtastic-ui/mui_chat_message_dark.webp Messages https://meshtastic.org/img/software/meshtastic-ui/mui_map_dark.webp Map https://meshtastic.org/img/software/meshtastic-ui/mui_settings_dark.webp Settings https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.21 https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.20 https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.19 https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.18 https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.17 https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.16 https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.15 https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.14 https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.13 https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.12 https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.11 https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.10 https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.9 https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.8 https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.7 https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.6 https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.5 https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.4 https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.3 https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.2 https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.1 https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.0 https://github.com/meshtastic/firmware/releases?q=tag%3Av2.6.13 https://github.com/meshtastic/firmware/releases?q=tag%3Av2.6.12 https://github.com/meshtastic/firmware/releases?q=tag%3Av2.6.11 https://github.com/meshtastic/firmware/releases?q=tag%3Av2.6.10 https://github.com/meshtastic/firmware/releases?q=tag%3Av2.6.9 https://github.com/meshtastic/firmware/releases?q=tag%3Av2.6.8 https://github.com/meshtastic/firmware/releases?q=tag%3Av2.6.7 https://github.com/meshtastic/firmware/releases?q=tag%3Av2.6.6 https://github.com/meshtastic/firmware/releases?q=tag%3Av2.6.5 https://github.com/meshtastic/firmware/releases?q=tag%3Av2.6.4
================================================ FILE: bin/platformio-custom.py ================================================ #!/usr/bin/env python3 # trunk-ignore-all(ruff/F821) # trunk-ignore-all(flake8/F821): For SConstruct imports import sys from os.path import join import subprocess import json import re from datetime import datetime from typing import Dict from readprops import readProps Import("env") platform = env.PioPlatform() progname = env.get("PROGNAME") lfsbin = f"{progname.replace('firmware-', 'littlefs-')}.bin" manifest_ran = False def infer_architecture(board_cfg): try: mcu = board_cfg.get("build.mcu") if board_cfg else None except KeyError: mcu = None except Exception: mcu = None if not mcu: return None mcu_l = str(mcu).lower() if "esp32s3" in mcu_l: return "esp32-s3" if "esp32c6" in mcu_l: return "esp32-c6" if "esp32c3" in mcu_l: return "esp32-c3" if "esp32" in mcu_l: return "esp32" if "rp2040" in mcu_l: return "rp2040" if "rp2350" in mcu_l: return "rp2350" if "nrf52" in mcu_l or "nrf52840" in mcu_l: return "nrf52840" if "stm32" in mcu_l: return "stm32" return None def manifest_gather(source, target, env): global manifest_ran if manifest_ran: return # Skip manifest generation if we cannot determine architecture (host/native builds) board_arch = infer_architecture(env.BoardConfig()) if not board_arch: print(f"Skipping mtjson generation for unknown architecture (env={env.get('PIOENV')})") manifest_ran = True return manifest_ran = True out = [] board_platform = env.BoardConfig().get("platform") board_mcu = env.BoardConfig().get("build.mcu").lower() needs_ota_suffix = board_platform == "nordicnrf52" # Mapping of bin files to their target partition names # Maps the filename pattern to the partition name where it should be flashed partition_map = { f"{progname}.bin": "app0", # primary application slot (app0 / OTA_0) lfsbin: "spiffs", # filesystem image flashed to spiffs } check_paths = [ progname, f"{progname}.elf", f"{progname}.bin", f"{progname}.factory.bin", f"{progname}.hex", f"{progname}.merged.hex", f"{progname}.uf2", f"{progname}.factory.uf2", f"{progname}.zip", lfsbin, f"mt-{board_mcu}-ota.bin", "bleota-c3.bin" ] for p in check_paths: f = env.File(env.subst(f"$BUILD_DIR/{p}")) if f.exists(): manifest_name = p if needs_ota_suffix and p == f"{progname}.zip": manifest_name = f"{progname}-ota.zip" d = { "name": manifest_name, "md5": f.get_content_hash(), # Returns MD5 hash "bytes": f.get_size() # Returns file size in bytes } # Add part_name if this file represents a partition that should be flashed if p in partition_map: d["part_name"] = partition_map[p] out.append(d) print(d) manifest_write(out, env) def manifest_write(files, env): # Defensive: also skip manifest writing if we cannot determine architecture def get_project_option(name): try: return env.GetProjectOption(name) except Exception: return None def get_project_option_any(names): for name in names: val = get_project_option(name) if val is not None: return val return None def as_bool(val): return str(val).strip().lower() in ("1", "true", "yes", "on") def as_int(val): try: return int(str(val), 10) except (TypeError, ValueError): return None def as_list(val): return [item.strip() for item in str(val).split(",") if item.strip()] manifest = { "version": verObj["long"], "build_epoch": build_epoch, "platformioTarget": env.get("PIOENV"), "mcu": env.get("BOARD_MCU"), "repo": repo_owner, "files": files, "has_mui": False, "has_inkhud": False, } # Get partition table (generated in esp32_pre.py) if it exists if env.get("custom_mtjson_part"): # custom_mtjson_part is a JSON string, convert it back to a dict pj = json.loads(env.get("custom_mtjson_part")) manifest["part"] = pj # Enable has_mui for TFT builds if ("HAS_TFT", 1) in env.get("CPPDEFINES", []): manifest["has_mui"] = True if "MESHTASTIC_INCLUDE_INKHUD" in env.get("CPPDEFINES", []): manifest["has_inkhud"] = True pioenv = env.get("PIOENV") device_meta = {} device_meta_fields = [ ("hwModel", ["custom_meshtastic_hw_model"], as_int), ("hwModelSlug", ["custom_meshtastic_hw_model_slug"], str), ("architecture", ["custom_meshtastic_architecture"], str), ("activelySupported", ["custom_meshtastic_actively_supported"], as_bool), ("displayName", ["custom_meshtastic_display_name"], str), ("supportLevel", ["custom_meshtastic_support_level"], as_int), ("images", ["custom_meshtastic_images"], as_list), ("tags", ["custom_meshtastic_tags"], as_list), ("requiresDfu", ["custom_meshtastic_requires_dfu"], as_bool), ("partitionScheme", ["custom_meshtastic_partition_scheme"], str), ("url", ["custom_meshtastic_url"], str), ("key", ["custom_meshtastic_key"], str), ("variant", ["custom_meshtastic_variant"], str), ] for manifest_key, option_keys, caster in device_meta_fields: raw_val = get_project_option_any(option_keys) if raw_val is None: continue parsed = caster(raw_val) if callable(caster) else raw_val if parsed is not None and parsed != "": device_meta[manifest_key] = parsed # Determine architecture once; if we can't infer it, skip manifest generation board_arch = device_meta.get("architecture") or infer_architecture(env.BoardConfig()) if not board_arch: print(f"Skipping mtjson write for unknown architecture (env={env.get('PIOENV')})") return device_meta["architecture"] = board_arch # Always set requiresDfu: true for nrf52840 targets if board_arch == "nrf52840": device_meta["requiresDfu"] = True device_meta.setdefault("displayName", pioenv) device_meta.setdefault("activelySupported", False) if device_meta: manifest.update(device_meta) # Write the manifest to the build directory with open(env.subst("$BUILD_DIR/${PROGNAME}.mt.json"), "w") as f: json.dump(manifest, f, indent=2) Import("projenv") prefsLoc = projenv["PROJECT_DIR"] + "/version.properties" verObj = readProps(prefsLoc) print(f"Using meshtastic platformio-custom.py, firmware version {verObj['long']} on {env.get('PIOENV')}") # get repository owner if git is installed try: r_owner = ( subprocess.check_output(["git", "config", "--get", "remote.origin.url"]) .decode("utf-8") .strip().split("/") ) repo_owner = r_owner[-2] + "/" + r_owner[-1].replace(".git", "") except subprocess.CalledProcessError: repo_owner = "unknown" jsonLoc = env["PROJECT_DIR"] + "/userPrefs.jsonc" with open(jsonLoc) as f: jsonStr = re.sub("//.*","", f.read(), flags=re.MULTILINE) userPrefs = json.loads(jsonStr) pref_flags = [] # Pre-process the userPrefs for pref in userPrefs: if userPrefs[pref].startswith("{"): pref_flags.append("-D" + pref + "=" + userPrefs[pref]) elif userPrefs[pref].lstrip("-").replace(".", "").isdigit(): pref_flags.append("-D" + pref + "=" + userPrefs[pref]) elif userPrefs[pref] == "true" or userPrefs[pref] == "false": pref_flags.append("-D" + pref + "=" + userPrefs[pref]) elif userPrefs[pref].startswith("meshtastic_"): pref_flags.append("-D" + pref + "=" + userPrefs[pref]) # If the value is a string, we need to wrap it in quotes else: pref_flags.append("-D" + pref + "=" + env.StringifyMacro(userPrefs[pref]) + "") # General options that are passed to the C and C++ compilers # Calculate unix epoch for current day (midnight) current_date = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) build_epoch = int(current_date.timestamp()) flags = [ "-DAPP_VERSION=" + verObj["long"], "-DAPP_VERSION_SHORT=" + verObj["short"], "-DAPP_ENV=" + env.get("PIOENV"), "-DAPP_REPO=" + repo_owner, "-DBUILD_EPOCH=" + str(build_epoch), ] + pref_flags print("Using flags:") for flag in flags: print(flag) projenv.Append( CCFLAGS=flags, ) for lb in env.GetLibBuilders(): if lb.name == "meshtastic-device-ui": lb.env.Append(CPPDEFINES=[("APP_VERSION", verObj["long"])]) break # Get the display resolution from macros def get_display_resolution(build_flags): # Check "DISPLAY_SIZE" to determine the screen resolution for flag in build_flags: if isinstance(flag, tuple) and flag[0] == "DISPLAY_SIZE": screen_width, screen_height = map(int, flag[1].split("x")) return screen_width, screen_height print("No screen resolution defined in build_flags. Please define DISPLAY_SIZE.") exit(1) def load_boot_logo(source, target, env): build_flags = env.get("CPPDEFINES", []) logo_w, logo_h = get_display_resolution(build_flags) print(f"TFT build with {logo_w}x{logo_h} resolution detected") # Load the boot logo from `branding/logo_x.png` if it exists source_path = join(env["PROJECT_DIR"], "branding", f"logo_{logo_w}x{logo_h}.png") dest_dir = join(env["PROJECT_DIR"], "data", "boot") dest_path = join(dest_dir, "logo.png") if env.File(source_path).exists(): print(f"Loading boot logo from {source_path}") # Prepare the destination env.Execute(f"mkdir -p {dest_dir} && rm -f {dest_path}") # Copy the logo to the `data/boot` directory env.Execute(f"cp {source_path} {dest_path}") # Load the boot logo on TFT builds if ("HAS_TFT", 1) in env.get("CPPDEFINES", []): env.AddPreAction(f"$BUILD_DIR/{lfsbin}", load_boot_logo) board_arch = infer_architecture(env.BoardConfig()) should_skip_manifest = board_arch is None # For host/native envs, avoid depending on 'buildprog' (some targets don't define it) mtjson_deps = [] if should_skip_manifest else ["buildprog"] if not should_skip_manifest and platform.name == "espressif32": # Build littlefs image as part of mtjson target # Equivalent to `pio run -t buildfs` target_lfs = env.DataToBin( join("$BUILD_DIR", "${ESP32_FS_IMAGE_NAME}"), "$PROJECT_DATA_DIR" ) mtjson_deps.append(target_lfs) if should_skip_manifest: def skip_manifest(source, target, env): print(f"mtjson: skipped for native environment: {env.get('PIOENV')}") env.AddCustomTarget( name="mtjson", dependencies=mtjson_deps, actions=[skip_manifest], title="Meshtastic Manifest (skipped)", description="mtjson generation is skipped for native environments", always_build=True, ) else: env.AddCustomTarget( name="mtjson", dependencies=mtjson_deps, actions=[manifest_gather], title="Meshtastic Manifest", description="Generating Meshtastic manifest JSON + Checksums", always_build=True, ) # Run manifest generation as part of the default build pipeline for non-native builds. env.Default("mtjson") ================================================ FILE: bin/platformio-pre.py ================================================ #!/usr/bin/env python3 # trunk-ignore-all(ruff/F821) # trunk-ignore-all(flake8/F821): For SConstruct imports Import("env") platform = env.PioPlatform() if platform.name == "native": env.Replace(PROGNAME="meshtasticd") else: from readprops import readProps prefsLoc = env["PROJECT_DIR"] + "/version.properties" verObj = readProps(prefsLoc) env.Replace(PROGNAME=f"firmware-{env.get('PIOENV')}-{verObj['long']}") env.Replace(ESP32_FS_IMAGE_NAME=f"littlefs-{env.get('PIOENV')}-{verObj['long']}") # Print the new program name for verification print(f"PROGNAME: {env.get('PROGNAME')}") if platform.name == "espressif32": print(f"ESP32_FS_IMAGE_NAME: {env.get('ESP32_FS_IMAGE_NAME')}") ================================================ FILE: bin/promote-release.sh ================================================ #!/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" ================================================ FILE: bin/read-system-info.sh ================================================ #!/usr/bin/env bash esptool.py --baud 115200 read_flash 0x1000 0xf000 system-info.img ================================================ FILE: bin/readprops.py ================================================ import configparser import subprocess import os run_number = os.getenv('GITHUB_RUN_NUMBER', '0') build_location = os.getenv('BUILD_LOCATION', 'local') 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", deb="unset", ) # Try to find current build SHA if if the workspace is clean. This could fail if git is not installed try: # Pin abbreviation length to keep local builds and CI matching (avoid auto-shortening) sha = ( subprocess.check_output(["git", "rev-parse", "--short=7", "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(verObj["short"], suffix) verObj["deb"] = "{}.{}~{}{}".format(verObj["short"], run_number, build_location, sha) except: # print("Unexpected error:", sys.exc_info()[0]) # traceback.print_exc() verObj["long"] = verObj["short"] verObj["deb"] = "{}.{}~{}".format(verObj["short"], run_number, build_location) # print("firmware version " + verStr) return verObj # print("path is" + ','.join(sys.path)) ================================================ FILE: bin/regen-protos.bat ================================================ @ECHO OFF SETLOCAL 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 GOTO eof :eof ENDLOCAL EXIT /B %ERRORLEVEL% ================================================ FILE: bin/regen-protos.sh ================================================ #!/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 ================================================ FILE: bin/rpkg.macros ================================================ function meshtastic_version { meshtastic_version=$(python3 bin/buildinfo.py short) echo -n "$meshtastic_version" } function web_version { web_version=$(cat bin/web.version) echo -n "$web_version" } function git_commits_num { total_commits=$(git rev-list --all --count) echo -n "$total_commits" } function git_commit_sha { commit_sha=$(git rev-parse --short HEAD) echo -n "$commit_sha" } ================================================ FILE: bin/s140_nrf52_7.3.0_softdevice.hex ================================================ :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 ================================================ FILE: bin/setup-python-for-esp-debug.sh ================================================ # 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/ ================================================ FILE: bin/shame.py ================================================ import sys import os import json from github import Github def parseFile(path): with open(path, "r") as f: data = json.loads(f) for file in data["files"]: if file["name"].endswith(".bin"): return file["name"], file["bytes"] if len(sys.argv) != 4: print(f"expected usage: {sys.argv[0]} ") sys.exit(1) pr_number = int(sys.argv[1]) token = os.getenv("GITHUB_TOKEN") if not token: raise EnvironmentError("GITHUB_TOKEN not found in environment.") repo_name = os.getenv("GITHUB_REPOSITORY") # "owner/repo" if not repo_name: raise EnvironmentError("GITHUB_REPOSITORY not found in environment.") oldFiles = sys.argv[2] old = set(os.path.join(oldFiles, f) for f in os.listdir(oldFiles) if os.path.isfile(f)) newFiles = sys.argv[3] new = set(os.path.join(newFiles, f) for f in os.listdir(newFiles) if os.path.isfile(f)) startMarkdown = "# Target Size Changes\n\n" markdown = "" newlyIntroduced = new - old if len(newlyIntroduced) > 0: markdown += "## Newly Introduced Targets\n\n" # create a table markdown += "| File | Size |\n" markdown += "| ---- | ---- |\n" for f in newlyIntroduced: name, size = parseFile(f) markdown += f"| `{name}` | {size}b |\n" # do not log removed targets # PRs only run a small subset of builds, so removed targets are not meaningful # since they are very likely to just be not ran in PR CI both = old & new degradations = [] improvements = [] for f in both: oldName, oldSize = parseFile(f) _, newSize = parseFile(f) if oldSize != newSize: if newSize < oldSize: improvements.append((oldName, oldSize, newSize)) else: degradations.append((oldName, oldSize, newSize)) if len(degradations) > 0: markdown += "\n## Degradation\n\n" # create a table markdown += "| File | Difference | Old Size | New Size |\n" markdown += "| ---- | ---------- | -------- | -------- |\n" for oldName, oldSize, newSize in degradations: markdown += f"| `{oldName}` | **{oldSize - newSize}b** | {oldSize}b | {newSize}b |\n" if len(improvements) > 0: markdown += "\n## Improvement\n\n" # create a table markdown += "| File | Difference | Old Size | New Size |\n" markdown += "| ---- | ---------- | -------- | -------- |\n" for oldName, oldSize, newSize in improvements: markdown += f"| `{oldName}` | **{oldSize - newSize}b** | {oldSize}b | {newSize}b |\n" if len(markdown) == 0: markdown = "No changes in target sizes detected." g = Github(token) repo = g.get_repo(repo_name) pr = repo.get_pull(pr_number) existing_comment = None for comment in pr.get_issue_comments(): if comment.body.startswith(startMarkdown): existing_comment = comment break final_markdown = startMarkdown + markdown if existing_comment: existing_comment.edit(body=final_markdown) else: pr.create_issue_comment(body=final_markdown) ================================================ FILE: bin/test-native-docker.sh ================================================ #!/usr/bin/env bash # Run native PlatformIO tests inside Docker (for macOS / non-Linux hosts). # # Usage: # ./bin/test-native-docker.sh # run all native tests # ./bin/test-native-docker.sh -f test_transmit_history # run specific test filter # ./bin/test-native-docker.sh --rebuild # force rebuild the image # set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" IMAGE_NAME="meshtastic-native-test" REBUILD=false EXTRA_ARGS=() for arg in "$@"; do if [[ "$arg" == "--rebuild" ]]; then REBUILD=true else EXTRA_ARGS+=("$arg") fi done if $REBUILD || ! docker image inspect "$IMAGE_NAME" >/dev/null 2>&1; then echo "Building test image (first run may take a few minutes)..." docker build -t "$IMAGE_NAME" -f "$ROOT_DIR/Dockerfile.test" "$ROOT_DIR" fi # Disable BUILD_EPOCH to avoid full rebuilds between test runs (matches CI) sed_cmd='s/-DBUILD_EPOCH=$UNIX_TIME/#-DBUILD_EPOCH=$UNIX_TIME/' # Default: run all tests. Pass extra args (e.g. -f test_transmit_history) through. if [[ ${#EXTRA_ARGS[@]} -eq 0 ]]; then CMD=("platformio" "test" "-e" "coverage" "-v") else CMD=("platformio" "test" "-e" "coverage" "-v" "${EXTRA_ARGS[@]}") fi exec docker run --rm \ -v "$ROOT_DIR:/src:ro" \ "$IMAGE_NAME" \ bash -c "rm -rf /tmp/fw-test && cp -a /src /tmp/fw-test && cd /tmp/fw-test && sed -i '${sed_cmd}' platformio.ini && ${CMD[*]}" ================================================ FILE: bin/test-simulator.sh ================================================ #!/usr/bin/env bash set -e echo "Starting simulator" .pio/build/native/meshtasticd -s & sleep 20 # 5 seconds was not enough echo "Simulator started, launching python test..." python3 -c 'from meshtastic.test import testSimulator; testSimulator()' ================================================ FILE: bin/uf2-convert.bat ================================================ @ECHO OFF SETLOCAL EnableDelayedExpansion TITLE Meshtastic uf2-convert SET "SCRIPT_NAME=%~nx0" SET "DEBUG=0" SET "NRF=0" SET "UF2CONV_CMD=python3 .\bin\uf2conv.py" GOTO getopts :help ECHO. ECHO Usage: %SCRIPT_NAME% -t [t-echo^|rak4631^|nano-g2-ultra^|wio-tracker-wm1110^|canaryone^| ECHO heltec-mesh-node-t114^|tracker-t1000-e^|rak_wismeshtap^|rak2560^| ECHO nrf52_promicro_diy_tcxo] ECHO. ECHO Options: ECHO -t target Specify a platformio NRF target to build for. (required) ECHO. ECHO Example: %SCRIPT_NAME% -t rak4631 GOTO eof :version ECHO %SCRIPT_NAME% [Version 2.6.0] ECHO Meshtastic GOTO eof :getopts IF "%~1"=="" GOTO endopts IF /I "%~1"=="-?" GOTO help IF /I "%~1"=="-h" GOTO help IF /I "%~1"=="--help" GOTO help IF /I "%~1"=="-v" GOTO version IF /I "%~1"=="--version" GOTO version IF /I "%~1"=="--debug" SET "DEBUG=1" & CALL :LOG_MESSAGE DEBUG "DEBUG mode: enabled." IF /I "%~1"=="-t" SET "TARGETNAME=%~2" & SHIFT IF /I "%~1"=="--target" SET "TARGETNAME=%~2" & SHIFT SHIFT GOTO getopts :endopts CALL :LOG_MESSAGE DEBUG "Checking TARGETNAME parameter..." IF "__!TARGETNAME!__"=="____" ( CALL :LOG_MESSAGE DEBUG "Missing -t target input." GOTO help ) IF %DEBUG% EQU 1 SET "UF2CONV_CMD=REM python3 .\bin\uf2conv.py" SET "NRFTARGETS=t-echo rak4631 nano-g2-ultra wio-tracker-wm1110 canaryone heltec-mesh-node-t114 tracker-t1000-e rak_wismeshtap rak2560 nrf52_promicro_diy_tcxo" FOR %%a IN (%NRFTARGETS%) DO ( IF /I "%%a"=="!TARGETNAME!" ( @REM We are working with any of %NRFTARGETS%. SET "NRF=1" GOTO end_loop_nrf ) ) :end_loop_nrf @REM Building operations. IF !NRF! EQU 1 ( CALL :LOG_MESSAGE INFO "Trying to build for !TARGETNAME!..." CALL :RUN_UF2CONV !TARGETNAME! || GOTO eof ) ELSE ( CALL :LOG_MESSAGE WARN "!TARGETNAME! is not supported..." GOTO eof ) CALL :LOG_MESSAGE INFO "Script complete!." :eof ENDLOCAL EXIT /B %ERRORLEVEL% :RUN_UF2CONV @REM Subroutine used to run .\bin\uf2conv.py with arguments. @REM Also handles %ERRORLEVEL%. @REM CALL :RUN_UF2CONV [target] @REM. @REM Example:: CALL :RUN_UF2CONV rak4631 IF %DEBUG% EQU 1 CALL :LOG_MESSAGE DEBUG "About to run command: !UF2CONV_CMD! .\.pio\build\%~1\firmware.hex -c -o .\.pio\build\%~1\firmware.uf2 -f 0xADA52840" CALL :RESET_ERROR !UF2CONV_CMD! .\.pio\build\%~1\firmware.hex -c -o .\.pio\build\%~1\firmware.uf2 -f 0xADA52840 IF %ERRORLEVEL% NEQ 0 ( CALL :LOG_MESSAGE ERROR "Error running command: !UF2CONV_CMD! .\.pio\build\%~1\firmware.hex -c -o .\.pio\build\%~1\firmware.uf2 -f 0xADA52840" EXIT /B %ERRORLEVEL% ) GOTO :eof :LOG_MESSAGE @REM Subroutine used to print log messages in four different levels. @REM DEBUG messages only get printed if [-d] flag is passed to script. @REM CALL :LOG_MESSAGE [ERROR|INFO|WARN|DEBUG] "Message" @REM. @REM Example:: CALL :LOG_MESSAGE INFO "Message." SET /A LOGCOUNTER=LOGCOUNTER+1 IF "%1" == "ERROR" CALL :GET_TIMESTAMP & ECHO %1 ^| !TIMESTAMP! !LOGCOUNTER! %~2 IF "%1" == "INFO" CALL :GET_TIMESTAMP & ECHO %1 ^| !TIMESTAMP! !LOGCOUNTER! %~2 IF "%1" == "WARN" CALL :GET_TIMESTAMP & ECHO %1 ^| !TIMESTAMP! !LOGCOUNTER! %~2 IF "%1" == "DEBUG" IF %DEBUG% EQU 1 CALL :GET_TIMESTAMP & ECHO %1 ^| !TIMESTAMP! !LOGCOUNTER! %~2 GOTO :eof :GET_TIMESTAMP @REM Subroutine used to set !TIMESTAMP! to HH:MM:ss. @REM CALL :GET_TIMESTAMP @REM. @REM Updates: !TIMESTAMP! FOR /F "tokens=1,2,3 delims=:,." %%a IN ("%TIME%") DO ( SET "HH=%%a" SET "MM=%%b" SET "ss=%%c" ) SET "TIMESTAMP=!HH!:!MM!:!ss!" GOTO :eof :RESET_ERROR @REM Subroutine to reset %ERRORLEVEL% to 0. @REM CALL :RESET_ERROR @REM. @REM Updates: %ERRORLEVEL% EXIT /B 0 GOTO :eof ================================================ FILE: bin/uf2conv.py ================================================ #!/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() ================================================ FILE: bin/view-map.sh ================================================ #!/usr/bin/env bash echo using amap tool to display memory map amap .pio/build/output.map ================================================ FILE: bin/web.version ================================================ 2.6.7 ================================================ FILE: boards/CDEBYTE_EoRa-Hub.json ================================================ { "build": { "arduino": { "ldscript": "esp32s3_out.ld", "partitions": "default.csv", "memory_type": "qio_qspi" }, "core": "esp32", "extra_flags": [ "-DBOARD_HAS_PSRAM", "-DARDUINO_USB_CDC_ON_BOOT=0", "-DARDUINO_RUNNING_CORE=1", "-DARDUINO_EVENT_RUNNING_CORE=1" ], "f_cpu": "240000000L", "f_flash": "80000000L", "flash_mode": "qio", "mcu": "esp32s3", "variant": "esp32s3" }, "connectivity": ["wifi"], "debug": { "openocd_target": "esp32s3.cfg" }, "frameworks": ["arduino", "espidf"], "name": "CDEBYTE_EoRa-Hub", "upload": { "flash_size": "4MB", "maximum_ram_size": 327680, "maximum_size": 4194304, "use_1200bps_touch": true, "wait_for_upload_port": true, "require_upload_port": true, "speed": 921600 }, "url": "https://www.cdebyte.com/products/EoRa-HUB-900TB", "vendor": "CDEBYTE" } ================================================ FILE: boards/CDEBYTE_EoRa-S3.json ================================================ { "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" } ================================================ FILE: boards/ESP32-S3-WROOM-1-N4.json ================================================ { "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" } ================================================ FILE: boards/ThinkNode-M1.json ================================================ { "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": "elecrow_eink", "mcu": "nrf52840", "variant": "ELECROW-ThinkNode-M1", "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": "elecrow eink", "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.elecrow.com/thinknode-m1-meshtastic-lora-signal-transceiver-powered-by-nrf52840-with-154-screen-support-gps.html", "vendor": "ELECROW" } ================================================ FILE: boards/ThinkNode-M3.json ================================================ { "build": { "arduino": { "ldscript": "nrf52840_s140_v6.ld" }, "core": "nRF5", "cpu": "cortex-m4", "extra_flags": "-DNRF52840_XXAA", "f_cpu": "64000000L", "hwids": [ ["0x239A", "0x4405"], ["0x239A", "0x0029"], ["0x239A", "0x002A"] ], "usb_product": "elecrow_eink", "mcu": "nrf52840", "variant": "ELECROW-ThinkNode-M3", "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": "elecrow nrf", "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": "", "vendor": "ELECROW" } ================================================ FILE: boards/ThinkNode-M4.json ================================================ { "build": { "arduino": { "ldscript": "nrf52840_s140_v6.ld" }, "core": "nRF5", "cpu": "cortex-m4", "extra_flags": "-DARDUINO_NRF52840_ELECROW_M4 -DNRF52840_XXAA", "f_cpu": "64000000L", "hwids": [ ["0x239A", "0x4405"], ["0x239A", "0x0029"], ["0x239A", "0x002A"] ], "usb_product": "elecrow_thinknode_m4", "mcu": "nrf52840", "variant": "ELECROW-ThinkNode-M4", "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": "ELECROW ThinkNode m4", "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.elecrow.com/thinknode-m4-power-bank-lora-device-with-meshtastic-lora-tracker-function-powered-by-nrf52840.html", "vendor": "ELECROW" } ================================================ FILE: boards/ThinkNode-M6.json ================================================ { "build": { "arduino": { "ldscript": "nrf52840_s140_v6.ld" }, "core": "nRF5", "cpu": "cortex-m4", "extra_flags": "-DARDUINO_NRF52840_ELECROW_M6 -DNRF52840_XXAA", "f_cpu": "64000000L", "hwids": [ ["0x239A", "0x4405"], ["0x239A", "0x0029"], ["0x239A", "0x002A"] ], "usb_product": "elecrow_thinknode_m6", "mcu": "nrf52840", "variant": "ELECROW-ThinkNode-M6", "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": "ELECROW ThinkNode M6", "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.elecrow.com/thinknode-m6-outdoor-solar-power-for-lora-powered-by-nrf52840-supports-gps.html", "vendor": "ELECROW" } ================================================ FILE: boards/bpi_picow_esp32_s3.json ================================================ { "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" } ================================================ FILE: boards/canaryone.json ================================================ { "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" } ================================================ FILE: boards/crowpanel.json ================================================ { "build": { "arduino": { "ldscript": "esp32s3_out.ld", "memory_type": "qio_opi", "partitions": "default_16MB.csv" }, "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=0" ], "f_cpu": "240000000L", "f_flash": "80000000L", "flash_mode": "qio", "hwids": [["0x303A", "0x1001"]], "mcu": "esp32s3", "variant": "ESP32-S3-WROOM-1-N16R8" }, "connectivity": ["wifi", "bluetooth", "lora"], "debug": { "default_tool": "esp-builtin", "onboard_tools": ["esp-builtin"], "openocd_target": "esp32s3.cfg" }, "frameworks": ["arduino", "espidf"], "name": "ESP32-S3-WROOM-1-N16R8 (16 MB Flash, 8 MB PSRAM)", "upload": { "flash_size": "16MB", "maximum_ram_size": 524288, "maximum_size": 16777216, "require_upload_port": true, "speed": 921600 }, "monitor": { "speed": 115200 }, "url": "https://www.espressif.com/sites/default/files/documentation/esp32-s3-wroom-1_wroom-1u_datasheet_en.pdf", "vendor": "Espressif" } ================================================ FILE: boards/eink0.1.json ================================================ { "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" } ================================================ FILE: boards/esp32-s3-pico.json ================================================ { "build": { "arduino": { "ldscript": "esp32s3_out.ld", "partitions": "default_16MB.csv" }, "core": "esp32", "extra_flags": [ "-DARDUINO_ESP32S3_DEV", "-DARDUINO_RUNNING_CORE=1", "-DARDUINO_EVENT_RUNNING_CORE=1", "-DARDUINO_USB_CDC_ON_BOOT=1", "-DBOARD_HAS_PSRAM" ], "f_cpu": "240000000L", "f_flash": "80000000L", "flash_mode": "qio", "psram_type": "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" } ================================================ FILE: boards/esp32-s3-zero.json ================================================ { "build": { "arduino": { "partitions": "default.csv", "memory_type": "qio_qspi" }, "core": "esp32", "extra_flags": [ "-DARDUINO_ESP32S3_DEV", "-DARDUINO_RUNNING_CORE=1", "-DARDUINO_EVENT_RUNNING_CORE=1", "-DARDUINO_USB_CDC_ON_BOOT=1", "-DBOARD_HAS_PSRAM" ], "f_cpu": "240000000L", "f_flash": "80000000L", "flash_mode": "qio", "psram_type": "qio", "hwids": [["0x303A", "0x1001"]], "mcu": "esp32s3", "variant": "esp32s3" }, "connectivity": ["wifi", "bluetooth"], "debug": { "default_tool": "esp-builtin", "onboard_tools": ["esp-builtin"], "openocd_target": "esp32s3.cfg" }, "frameworks": ["arduino", "espidf"], "platforms": ["espressif32"], "name": "Espressif ESP32-S3-FH4R2 (4 MB QD, 2MB PSRAM)", "upload": { "flash_size": "4MB", "maximum_ram_size": 327680, "maximum_size": 4194304, "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" } ================================================ FILE: boards/gat562_mesh_trial_tracker.json ================================================ { "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": "GAT562 Mesh Trial Tracker", "mcu": "nrf52840", "variant": "gat562_mesh_trial_tracker", "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": "GAT562 Mesh Trial Tracker", "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": "http://www.gat-iot.com/", "vendor": "GAT-IOT" } ================================================ FILE: boards/hackaday-communicator.json ================================================ { "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=1" ], "f_cpu": "240000000L", "f_flash": "80000000L", "flash_mode": "qio", "hwids": [["0x303A", "0x1001"]], "mcu": "esp32s3", "variant": "hackaday-communicator" }, "connectivity": ["wifi", "bluetooth", "lora"], "debug": { "default_tool": "esp-builtin", "onboard_tools": ["esp-builtin"], "openocd_target": "esp32s3.cfg" }, "frameworks": ["arduino", "espidf"], "name": "hackaday-communicator (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": 1500000 }, "url": "hackaday.com", "vendor": "hackaday" } ================================================ FILE: boards/heltec_mesh_node_t114.json ================================================ { "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"], ["0x2886", "0x1667"] ], "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": "https://heltec.org/project/mesh-node-t114/", "vendor": "Heltec" } ================================================ FILE: boards/heltec_mesh_pocket.json ================================================ { "build": { "arduino": { "ldscript": "nrf52840_s140_v6.ld" }, "core": "nRF5", "cpu": "cortex-m4", "extra_flags": "-DNRF52840_XXAA", "f_cpu": "64000000L", "hwids": [ ["0x239A", "0x4405"], ["0x239A", "0x0029"], ["0x239A", "0x002A"] ], "usb_product": "HT-n5262", "mcu": "nrf52840", "variant": "heltec_mesh_pocket", "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": "https://heltec.org/project/meshpocket/", "vendor": "Heltec" } ================================================ FILE: boards/heltec_mesh_solar.json ================================================ { "build": { "arduino": { "ldscript": "nrf52840_s140_v6.ld" }, "core": "nRF5", "cpu": "cortex-m4", "extra_flags": "-DNRF52840_XXAA", "f_cpu": "64000000L", "hwids": [ ["0x239A", "0x4405"], ["0x239A", "0x0029"], ["0x239A", "0x002A"], ["0x239A", "0x0071"] ], "usb_product": "HT-n5262", "mcu": "nrf52840", "variant": "heltec_mesh_solar", "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": "https://heltec.org/project/meshsolar/", "vendor": "Heltec" } ================================================ FILE: boards/heltec_v4.json ================================================ { "build": { "arduino": { "ldscript": "esp32s3_out.ld", "partitions": "default_16MB.csv", "memory_type": "qio_qspi" }, "core": "esp32", "extra_flags": [ "-DBOARD_HAS_PSRAM", "-DARDUINO_USB_CDC_ON_BOOT=1", "-DARDUINO_USB_MODE=1", "-DARDUINO_RUNNING_CORE=1", "-DARDUINO_EVENT_RUNNING_CORE=1" ], "f_cpu": "240000000L", "f_flash": "80000000L", "flash_mode": "qio", "psram_type": "qspi", "hwids": [["0x303A", "0x1001"]], "mcu": "esp32s3", "variant": "heltec_v4" }, "connectivity": ["wifi", "bluetooth", "lora"], "debug": { "default_tool": "esp-builtin", "onboard_tools": ["esp-builtin"], "openocd_target": "esp32s3.cfg" }, "frameworks": ["arduino", "espidf"], "name": "heltec_wifi_lora_32 v4 (16 MB FLASH, 2 MB PSRAM)", "upload": { "flash_size": "16MB", "maximum_ram_size": 2097152, "maximum_size": 16777216, "use_1200bps_touch": true, "wait_for_upload_port": true, "require_upload_port": true, "speed": 921600 }, "url": "https://heltec.org/", "vendor": "heltec" } ================================================ FILE: boards/heltec_vision_master_e213.json ================================================ { "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=1", "-DARDUINO_USB_MODE=0", "-DARDUINO_RUNNING_CORE=1", "-DARDUINO_EVENT_RUNNING_CORE=1" ], "f_cpu": "240000000L", "f_flash": "80000000L", "flash_mode": "qio", "psram_type": "opi", "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" } ================================================ FILE: boards/heltec_vision_master_e290.json ================================================ { "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=1", "-DARDUINO_USB_MODE=0", "-DARDUINO_RUNNING_CORE=1", "-DARDUINO_EVENT_RUNNING_CORE=1" ], "f_cpu": "240000000L", "f_flash": "80000000L", "flash_mode": "qio", "psram_type": "opi", "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" } ================================================ FILE: boards/heltec_vision_master_t190.json ================================================ { "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=1", "-DARDUINO_USB_MODE=0", "-DARDUINO_RUNNING_CORE=1", "-DARDUINO_EVENT_RUNNING_CORE=1" ], "f_cpu": "240000000L", "f_flash": "80000000L", "flash_mode": "qio", "psram_type": "opi", "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" } ================================================ FILE: boards/heltec_wireless_tracker.json ================================================ { "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" } ================================================ FILE: boards/heltec_wireless_tracker_v2.json ================================================ { "build": { "arduino": { "ldscript": "esp32s3_out.ld", "partitions": "default_8MB.csv" }, "core": "esp32", "extra_flags": [ "-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_v2" }, "connectivity": ["wifi", "bluetooth", "lora"], "debug": { "openocd_target": "esp32s3.cfg" }, "frameworks": ["arduino", "espidf"], "name": "Heltec Wireless Tracker V2", "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", "vendor": "Heltec" } ================================================ FILE: boards/icarus.json ================================================ { "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" } ================================================ FILE: boards/me25ls01-4y10td.json ================================================ { "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", "openocd_target": "nrf52840-mdk-rs" }, "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" } ================================================ FILE: boards/mesh-tab.json ================================================ { "build": { "arduino": { "ldscript": "esp32s3_out.ld", "partitions": "default_16MB.csv", "memory_type": "qio_qspi" }, "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", "0x80D6"]], "mcu": "esp32s3", "variant": "mesh-tab" }, "connectivity": ["wifi", "bluetooth", "lora"], "debug": { "default_tool": "esp-builtin", "onboard_tools": ["esp-builtin"], "openocd_target": "esp32s3.cfg" }, "frameworks": ["arduino", "espidf"], "name": "ESP32-S3 WROOM-1 N16R2 (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": 460800 }, "url": "https://github.com/valzzu/Mesh-Tab", "vendor": "Espressif" } ================================================ FILE: boards/meshlink.json ================================================ { "build": { "arduino": { "ldscript": "nrf52840_s140_v6.ld" }, "core": "nRF5", "cpu": "cortex-m4", "extra_flags": "-DMESHLINK -DNRF52840_XXAA", "f_cpu": "64000000L", "hwids": [ ["0x239A", "0x00B3"], ["0x239A", "0x8029"], ["0x239A", "0x0029"], ["0x239A", "0x002A"], ["0x239A", "0x802A"] ], "usb_product": "MeshLink", "mcu": "nrf52840", "variant": "meshlink", "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": "MeshLink", "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.loraitalia.it", "vendor": "LoraItalia" } ================================================ FILE: boards/meshtiny.json ================================================ { "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": "MeshTiny", "mcu": "nrf52840", "variant": "meshtiny", "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": "MeshTiny", "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://github.com/meshtastic/firmware", "vendor": "MTools Tec" } ================================================ FILE: boards/mini-epaper-s3.json ================================================ { "build": { "arduino": { "ldscript": "esp32s3_out.ld", "partitions": "default.csv" }, "core": "esp32", "extra_flags": [ "-DBOARD_HAS_PSRAM", "-DARDUINO_ESP32S3_DEV", "-DARDUINO_USB_MODE=1", "-DARDUINO_USB_CDC_ON_BOOT=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"], "debug": { "default_tool": "esp-builtin", "onboard_tools": ["esp-builtin"], "openocd_target": "esp32s3.cfg" }, "frameworks": ["arduino", "espidf"], "name": "LilyGo Mini-Epaper-S3 (4 MB Flash, 2MB PSRAM)", "upload": { "flash_size": "4MB", "maximum_ram_size": 327680, "maximum_size": 4194304, "require_upload_port": true, "speed": 460800 }, "url": "https://www.lilygo.cc", "vendor": "LilyGo" } ================================================ FILE: boards/minimesh_lite.json ================================================ { "build": { "arduino": { "ldscript": "nrf52840_s140_v6.ld" }, "core": "nRF5", "cpu": "cortex-m4", "extra_flags": "-DMINIMESH_LITE -DNRF52840_XXAA", "f_cpu": "64000000L", "hwids": [ ["0x239A", "0x8029"], ["0x239A", "0x0029"], ["0x239A", "0x002A"] ], "usb_product": "Minimesh Lite", "mcu": "nrf52840", "variant": "dls_Minimesh_Lite", "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": "Minimesh Lite", "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://deeplabstudio.com", "vendor": "Deeplab Studio" } ================================================ FILE: boards/ms24sf1.json ================================================ { "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", "openocd_target": "nrf52840-mdk-rs" }, "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" } ================================================ FILE: boards/muzi-base.json ================================================ { "build": { "arduino": { "ldscript": "nrf52840_s140_v6.ld" }, "core": "nRF5", "cpu": "cortex-m4", "extra_flags": "-DARDUINO_NRF52840_MUZI_BASE -DNRF52840_XXAA", "f_cpu": "64000000L", "hwids": [["0x239A", "0xcafe"]], "mcu": "nrf52840", "variant": "muzi-base", "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": "Muzi Base", "url": "https://muzi.works/", "vendor": "MuziWorks", "upload": { "maximum_ram_size": 248832, "maximum_size": 815104, "speed": 115200, "protocol": "nrfutil", "protocols": [ "jlink", "nrfjprog", "nrfutil", "blackmagic", "cmsis-dap", "mbed", "stlink" ], "use_1200bps_touch": true, "require_upload_port": true, "wait_for_upload_port": true } } ================================================ FILE: boards/my-esp32s3-diy-oled.json ================================================ { "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" } ================================================ FILE: boards/my_esp32s3_diy_eink.json ================================================ { "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" } ================================================ FILE: boards/nano-g2-ultra.json ================================================ { "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", "openocd_target": "nrf52840-mdk-rs" }, "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" } ================================================ FILE: boards/nordic_pca10059.json ================================================ { "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" } ================================================ FILE: boards/nrf52840_dk.json ================================================ { "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" } ================================================ FILE: boards/promicro-nrf52840.json ================================================ { "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", "openocd_target": "nrf52840-mdk-rs" }, "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" } ================================================ FILE: boards/r1-neo.json ================================================ { "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": "Muzi R1 Neo", "mcu": "nrf52840", "variant": "r1-neo", "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://muzi.works/", "vendor": "Muzi Works" } ================================================ FILE: boards/seeed-sensecap-indicator.json ================================================ { "build": { "arduino": { "ldscript": "esp32s3_out.ld", "partitions": "partition-table-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", "f_boot": "120000000L", "boot": "qio", "flash_mode": "qio", "psram_type": "opi", "hwids": [["0x1A86", "0x7523"]], "mcu": "esp32s3", "variant": "esp32s3" }, "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": false, "use_1200bps_touch": true, "wait_for_upload_port": false, "speed": 921600 }, "url": "https://www.seeedstudio.com/Indicator-for-Meshtastic.html", "vendor": "Seeed Studio" } ================================================ FILE: boards/seeed-xiao-s3.json ================================================ { "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", "psram_type": "opi", "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" } ================================================ FILE: boards/seeed_solar_node.json ================================================ { "build": { "arduino": { "ldscript": "nrf52840_s140_v7.ld" }, "core": "nRF5", "cpu": "cortex-m4", "extra_flags": "-DARDUINO_MDBT50Q_RX -DNRF52840_XXAA", "f_cpu": "64000000L", "hwids": [["0x2886", "0x0059"]], "usb_product": "XIAO-BOOT", "mcu": "nrf52840", "variant": "seeed_solar_node", "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_solar_node", "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" } ================================================ FILE: boards/seeed_wio_tracker_L1.json ================================================ { "build": { "arduino": { "ldscript": "nrf52840_s140_v7.ld" }, "core": "nRF5", "cpu": "cortex-m4", "extra_flags": "-DARDUINO_MDBT50Q_RX -DNRF52840_XXAA", "f_cpu": "64000000L", "hwids": [ ["0x2886", "0x1668"], ["0x2886", "0x1667"] ], "usb_product": "TRACKER L1", "mcu": "nrf52840", "variant": "seeed_wio_tracker_L1", "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_wio_tracker_L1", "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-L1-p-6477.html", "vendor": "Seeed Studio" } ================================================ FILE: boards/seeed_xiao_nrf52840_kit.json ================================================ { "build": { "arduino": { "ldscript": "nrf52840_s140_v7.ld" }, "core": "nRF5", "cpu": "cortex-m4", "extra_flags": "-DARDUINO_MDBT50Q_RX -DNRF52840_XXAA", "f_cpu": "64000000L", "hwids": [["0x2886", "0x0166"]], "usb_product": "XIAO-BOOT", "mcu": "nrf52840", "variant": "seeed_xiao_nrf52840_kit", "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_nrf52840_kit", "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/XIAO-nRF52840-Wio-SX1262-Kit-for-Meshtastic-p-6400.html", "vendor": "seeed" } ================================================ FILE: boards/station-g2.json ================================================ { "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=1", "-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" } ================================================ FILE: boards/t-beam-1w.json ================================================ { "build": { "arduino": { "ldscript": "esp32s3_out.ld", "memory_type": "qio_opi" }, "core": "esp32", "extra_flags": [ "-DBOARD_HAS_PSRAM", "-DLILYGO_TBEAM_1W", "-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", "psram_type": "opi", "hwids": [["0x303A", "0x1001"]], "mcu": "esp32s3", "variant": "t-beam-1w" }, "connectivity": ["wifi", "bluetooth", "lora"], "debug": { "openocd_target": "esp32s3.cfg" }, "frameworks": ["arduino"], "name": "LilyGo TBeam-1W", "upload": { "flash_size": "16MB", "maximum_ram_size": 327680, "maximum_size": 16777216, "require_upload_port": true, "speed": 921600 }, "url": "http://www.lilygo.cn/", "vendor": "LilyGo" } ================================================ FILE: boards/t-deck-pro.json ================================================ { "build": { "arduino": { "ldscript": "esp32s3_out.ld", "memory_type": "qio_qspi", "partitions": "default_16MB.csv" }, "core": "esp32", "extra_flags": [ "-DBOARD_HAS_PSRAM", "-DARDUINO_USB_CDC_ON_BOOT=1", "-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": "LilyGo T-Deck Pro S3 (16M Flash 8M QSPI PSRAM )", "upload": { "flash_size": "16MB", "maximum_ram_size": 327680, "maximum_size": 16777216, "require_upload_port": true, "speed": 921600 }, "monitor": { "speed": 115200 }, "url": "https://lilygo.cc/products/t-deck-pro", "vendor": "LilyGo" } ================================================ FILE: boards/t-deck.json ================================================ { "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=1" ], "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" } ================================================ FILE: boards/t-echo.json ================================================ { "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": "https://lilygo.cc/products/t-echo-lilygo", "vendor": "LILYGO" } ================================================ FILE: boards/t-watch-s3.json ================================================ { "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=1", "-DARDUINO_RUNNING_CORE=1", "-DARDUINO_EVENT_RUNNING_CORE=1" ], "f_cpu": "240000000L", "f_flash": "80000000L", "flash_mode": "qio", "psram_type": "opi", "hwids": [ ["0x303A", "0x1001"], ["0x303A", "0x0002"] ], "mcu": "esp32s3", "variant": "t-watch-s3" }, "connectivity": ["wifi", "bluetooth", "lora"], "debug": { "openocd_target": "esp32s3.cfg" }, "frameworks": ["arduino"], "name": "LilyGo T-Watch 2020 V3", "upload": { "flash_size": "16MB", "maximum_ram_size": 327680, "maximum_size": 16777216, "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" } ================================================ FILE: boards/tbeam-s3-core.json ================================================ { "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" } ================================================ FILE: boards/tlora-t3s3-v1.json ================================================ { "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" } ================================================ FILE: boards/tracker-t1000-e.json ================================================ { "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"], ["0x2886", "0x0057"] ], "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", "openocd_target": "nrf52840-mdk-rs" }, "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" } ================================================ FILE: boards/unphone.json ================================================ { "build": { "arduino": { "ldscript": "esp32s3_out.ld", "memory_type": "qio_opi", "partitions": "partition-table-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" } ================================================ FILE: boards/wio-sdk-wm1110.json ================================================ { "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", "openocd_target": "nrf52840-mdk-rs" }, "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" } ================================================ FILE: boards/wio-t1000-s.json ================================================ { "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", "openocd_target": "nrf52840-mdk-rs" }, "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" } ================================================ FILE: boards/wio-tracker-wm1110.json ================================================ { "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", "openocd_target": "nrf52840-mdk-rs" }, "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" } ================================================ FILE: boards/wiphone.json ================================================ { "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" } ================================================ FILE: boards/wiscore_rak11200.json ================================================ { "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" } ================================================ FILE: boards/wiscore_rak3172.json ================================================ { "build": { "arduino": { "variant_h": "variant_RAK3172_MODULE.h" }, "core": "stm32", "cpu": "cortex-m4", "extra_flags": "-DSTM32WLxx -DSTM32WLE5xx -DARDUINO_RAK3172_MODULE", "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" } ================================================ FILE: boards/wiscore_rak3312.json ================================================ { "build": { "arduino": { "ldscript": "esp32s3_out.ld", "memory_type": "qio_opi", "partitions": "default_16MB.csv" }, "core": "esp32", "extra_flags": [ "-DRAK3312", "-DARDUINO_USB_CDC_ON_BOOT=1", "-DARDUINO_USB_MODE=1", "-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": "rak3312" }, "connectivity": ["wifi", "bluetooth"], "debug": { "openocd_target": "esp32s3.cfg" }, "frameworks": ["arduino", "espidf"], "name": "WisCore RAK3312 Board", "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.rakwireless.com/en-us", "vendor": "rakwireless" } ================================================ FILE: boards/wiscore_rak4600.json ================================================ { "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" } ================================================ FILE: boards/wiscore_rak4631.json ================================================ { "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" } ================================================ FILE: boards/xiao_ble_sense.json ================================================ { "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" } ================================================ FILE: branding/README.md ================================================ # Meshtastic Branding / Whitelabeling This directory is consumed during the creation of **event** firmware. `bin/platformio-custom.py` determines the display resolution, and locates the corresponding `logo_x.png`. Ex: - `logo_800x480.png` - `logo_480x480.png` - `logo_480x320.png` - `logo_320x480.png` - `logo_320x240.png` This file is copied to `data/boot/logo.png` before filesystem image compilation. For additional examples see the [`event/defcon33` branch](https://github.com/meshtastic/firmware/tree/event/defcon33). ================================================ FILE: data/static/.gitkeep ================================================ ================================================ FILE: debian/.gitignore ================================================ .debhelper debhelper-build-stamp meshtasticd files meshtasticd.substvars meshtasticd.postrm.debhelper ================================================ FILE: debian/changelog ================================================ meshtasticd (2.7.21.0) unstable; urgency=medium * Version 2.7.21 -- GitHub Actions Wed, 11 Mar 2026 11:45:36 +0000 meshtasticd (2.7.20.0) unstable; urgency=medium * Version 2.7.20 -- GitHub Actions Wed, 11 Feb 2026 12:19:54 +0000 meshtasticd (2.7.19.0) unstable; urgency=medium * Version 2.7.19 -- GitHub Actions Thu, 22 Jan 2026 22:17:40 +0000 meshtasticd (2.7.18.0) unstable; urgency=medium * Version 2.7.18 -- GitHub Actions Fri, 02 Jan 2026 12:45:36 +0000 meshtasticd (2.7.17.0) unstable; urgency=medium * Version 2.7.17 -- GitHub Actions Fri, 28 Nov 2025 15:11:34 +0000 meshtasticd (2.7.16.0) unstable; urgency=medium * Version 2.7.16 -- GitHub Actions Wed, 19 Nov 2025 16:12:32 +0000 meshtasticd (2.7.15.0) unstable; urgency=medium * Version 2.7.15 -- GitHub Actions Thu, 13 Nov 2025 12:31:57 +0000 meshtasticd (2.7.14.0) unstable; urgency=medium * Version 2.7.14 -- GitHub Actions Mon, 03 Nov 2025 16:11:31 +0000 meshtasticd (2.7.13.0) unstable; urgency=medium * Version 2.7.13 -- GitHub Actions Sat, 11 Oct 2025 15:27:28 +0000 meshtasticd (2.7.12.0) unstable; urgency=medium [ Austin Lane ] * Initial packaging * Version 2.5.19 [ ] * GitHub Actions Automatic version bump [ GitHub Actions ] * Version 2.7.12 -- GitHub Actions Wed, 01 Oct 2025 19:51:41 +0000 ================================================ FILE: debian/ci_changelog.sh ================================================ #!/usr/bin/bash export DEBFULLNAME="GitHub Actions" export DEBEMAIL="github-actions[bot]@users.noreply.github.com" PKG_VERSION=$(python3 bin/buildinfo.py short) dch --newversion "$PKG_VERSION.0" \ --distribution unstable \ "Version $PKG_VERSION" ================================================ FILE: debian/ci_pack_sdeb.sh ================================================ #!/usr/bin/bash export DEBEMAIL="jbennett@incomsystems.biz" export PLATFORMIO_LIBDEPS_DIR=pio/libdeps export PLATFORMIO_PACKAGES_DIR=pio/packages export PLATFORMIO_CORE_DIR=pio/core export PLATFORMIO_SETTING_ENABLE_TELEMETRY=0 export PLATFORMIO_SETTING_CHECK_PLATFORMIO_INTERVAL=3650 export PLATFORMIO_SETTING_CHECK_PRUNE_SYSTEM_THRESHOLD=10240 # Download libraries to `pio` platformio pkg install -e native-tft platformio pkg install -e native-tft -t platformio/tool-scons@4.40502.0 # Mangle PlatformIO cache to prevent internet access at build-time # Simply adds 1 to all expiry (epoch) timestamps, adding ~500 years to expiry date cp pio/core/.cache/downloads/usage.db pio/core/.cache/downloads/usage.db.bak jq -c 'with_entries(.value |= (. | tostring + "1" | tonumber))' pio/core/.cache/downloads/usage.db.bak >pio/core/.cache/downloads/usage.db # Compress `pio` directory to prevent dh_clean from sanitizing it tar -cf pio.tar pio/ rm -rf pio # Download the meshtastic/web release build.tar to `web.tar` web_ver=$(cat bin/web.version) curl -L "https://github.com/meshtastic/web/releases/download/v$web_ver/build.tar" -o web.tar package=$(dpkg-parsechangelog --show-field Source) rm -rf debian/changelog dch --create --distribution "$SERIES" --package "$package" --newversion "$PKG_VERSION~$SERIES" \ "GitHub Actions Automatic packaging for $PKG_VERSION~$SERIES" # Build the source deb debuild -S -nc -k"$GPG_KEY_ID" ================================================ FILE: debian/control ================================================ Source: meshtasticd Section: misc Priority: optional Maintainer: Austin Lane Build-Depends: debhelper-compat (= 13), libc6-dev (>= 2.38) | libbsd-dev, lsb-release, tar, gzip, platformio, python3-protobuf, python3-grpcio, git, g++, pkg-config, libyaml-cpp-dev, libgpiod-dev, libbluetooth-dev, libusb-1.0-0-dev, libi2c-dev, libuv1-dev, openssl, libssl-dev, libulfius-dev, liborcania-dev, libx11-dev, libinput-dev, libxkbcommon-x11-dev, libsqlite3-dev, libsdl2-dev Standards-Version: 4.6.2 Homepage: https://github.com/meshtastic/firmware Rules-Requires-Root: no Package: meshtasticd Architecture: any Depends: adduser, ${misc:Depends}, ${shlibs:Depends} Description: Meshtastic daemon for communicating with Meshtastic devices Meshtastic is an off-grid text communication platform that uses inexpensive LoRa radios. ================================================ FILE: debian/meshtasticd.dirs ================================================ var/lib/meshtasticd etc/meshtasticd etc/meshtasticd/config.d etc/meshtasticd/available.d usr/share/meshtasticd/web etc/meshtasticd/ssl ================================================ FILE: debian/meshtasticd.install ================================================ .pio/build/native-tft/meshtasticd usr/bin bin/config.yaml etc/meshtasticd bin/config.d/* etc/meshtasticd/available.d bin/meshtasticd.service lib/systemd/system bin/meshtasticd-start.sh usr/bin web/* usr/share/meshtasticd/web ================================================ FILE: debian/meshtasticd.postinst ================================================ #!/bin/sh # postinst script for meshtasticd # # see: dh_installdeb(1) set -e # summary of how this script can be called: # * `configure' # * `abort-upgrade' # * `abort-remove' `in-favour' # # * `abort-remove' # * `abort-deconfigure' `in-favour' # `removing' # # for details, see http://www.debian.org/doc/debian-policy/ or # the debian-policy package case "$1" in configure|reconfigure) # create spi, gpio groups (for udev rules) # these groups already exist on Raspberry Pi OS getent group spi >/dev/null 2>/dev/null || addgroup --system spi getent group gpio >/dev/null 2>/dev/null || addgroup --system gpio # create a meshtasticd group and user getent passwd meshtasticd >/dev/null 2>/dev/null || adduser --system --home /var/lib/meshtasticd --no-create-home meshtasticd getent group meshtasticd >/dev/null 2>/dev/null || addgroup --system meshtasticd adduser meshtasticd meshtasticd >/dev/null 2>/dev/null adduser meshtasticd spi >/dev/null 2>/dev/null adduser meshtasticd gpio >/dev/null 2>/dev/null # add meshtasticd user to appropriate groups (if they exist) getent group plugdev >/dev/null 2>/dev/null && adduser meshtasticd plugdev >/dev/null 2>/dev/null getent group dialout >/dev/null 2>/dev/null && adduser meshtasticd dialout >/dev/null 2>/dev/null getent group i2c >/dev/null 2>/dev/null && adduser meshtasticd i2c >/dev/null 2>/dev/null getent group video >/dev/null 2>/dev/null && adduser meshtasticd video >/dev/null 2>/dev/null getent group audio >/dev/null 2>/dev/null && adduser meshtasticd audio >/dev/null 2>/dev/null getent group input >/dev/null 2>/dev/null && adduser meshtasticd input >/dev/null 2>/dev/null # migrate /root/.portduino to /var/lib/meshtasticd/.portduino # should only run once, upon upgrade from < 2.6.9 if [ -n "$2" ] && dpkg --compare-versions "$2" lt 2.6.9; then if [ -d /root/.portduino ] && [ ! -e /var/lib/meshtasticd/.portduino ]; then cp -r /root/.portduino /var/lib/meshtasticd/.portduino echo "Migrated meshtasticd VFS from /root/.portduino to /var/lib/meshtasticd/.portduino" echo "meshtasticd now runs as the 'meshtasticd' user, not 'root'." echo "See https://github.com/meshtastic/firmware/pull/6718 for details" fi fi if [ -d /var/lib/meshtasticd ]; then chown -R meshtasticd:meshtasticd /var/lib/meshtasticd fi if [ -d /etc/meshtasticd ]; then chown -R meshtasticd:meshtasticd /etc/meshtasticd fi if [ -d /usr/share/meshtasticd ]; then chown -R meshtasticd:meshtasticd /usr/share/meshtasticd fi ;; abort-upgrade|abort-remove|abort-deconfigure) ;; *) echo "postinst called with unknown argument \`$1'" >&2 exit 1 ;; esac # dh_installdeb will replace this with shell code automatically # generated by other debhelper scripts. #DEBHELPER# exit 0 ================================================ FILE: debian/meshtasticd.postrm ================================================ #!/bin/sh # postrm script for meshtasticd # # see: dh_installdeb(1) set -e # summary of how this script can be called: # * `remove' # * `purge' # * `upgrade' # * `failed-upgrade' # * `abort-install' # * `abort-install' # * `abort-upgrade' # * `disappear' # # for details, see http://www.debian.org/doc/debian-policy/ or # the debian-policy package case "$1" in purge|remove|upgrade|failed-upgrade|abort-install|abort-upgrade|disappear) # Only remove /var/lib/meshtasticd on purge if [ "${1}" = "purge" ] ; then rm -rf /var/lib/meshtasticd fi ;; *) echo "postrm called with unknown argument \`$1'" >&2 exit 1 ;; esac # dh_installdeb will replace this with shell code automatically # generated by other debhelper scripts. #DEBHELPER# exit 0 ================================================ FILE: debian/meshtasticd.udev ================================================ # Set spidev ownership to 'spi' group SUBSYSTEM=="spidev", KERNEL=="spidev*", GROUP="spi", MODE="0660" # Allow access to USB CH341 devices SUBSYSTEM=="usb", ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="5512", MODE="0666" # Set gpio ownership to 'gpio' group SUBSYSTEM=="*gpiomem*", GROUP="gpio", MODE="0660" SUBSYSTEM=="gpio", GROUP="gpio", MODE="0660" ================================================ FILE: debian/rules ================================================ #!/usr/bin/make -f # export DH_VERBOSE = 1 # Use the "dh" sequencer %: dh $@ # https://docs.platformio.org/en/latest/envvars.html PIO_ENV:=\ PLATFORMIO_CORE_DIR=pio/core \ PLATFORMIO_LIBDEPS_DIR=pio/libdeps \ PLATFORMIO_PACKAGES_DIR=pio/packages \ PLATFORMIO_SETTING_ENABLE_TELEMETRY=0 \ PLATFORMIO_SETTING_CHECK_PLATFORMIO_INTERVAL=3650 \ PLATFORMIO_SETTING_CHECK_PRUNE_SYSTEM_THRESHOLD=10240 # Raspbian armhf builds should be compatible with armv6-hardfloat # https://www.valvers.com/open-software/raspberry-pi/bare-metal-programming-in-c-part-1/#rpi1-compiler-flags ifneq (,$(findstring Raspbian,$(shell lsb_release -is))) ifeq ($(DEB_BUILD_ARCH),armhf) PIO_ENV+=\ PLATFORMIO_BUILD_FLAGS="-mfloat-abi=hard -mfpu=vfp -march=armv6zk" endif endif override_dh_auto_build: # Extract tarballs within source deb tar -xf pio.tar mkdir -p web && tar -xf web.tar -C web gunzip web/ -r # Build with platformio $(PIO_ENV) platformio run -e native-tft # Move the binary and default config to the correct name cp bin/config-dist.yaml bin/config.yaml ================================================ FILE: debian/source/format ================================================ 3.0 (native) ================================================ FILE: debian/source/include-binaries ================================================ pio.tar web.tar ================================================ FILE: debian/source/options ================================================ extend-diff-ignore = "\.pio" ================================================ FILE: docker-compose.yml ================================================ # USB-Based Meshtastic container-node! # Copy .env.example to .env and set the USB_DEVICE and CONFIG_PATH variables services: meshtastic-node: build: . container_name: meshtasticd # Pass USB device through to the container devices: - "${USB_DEVICE}" # Mount local config file and named volume for data persistence volumes: - "${CONFIG_PATH}:/etc/meshtasticd/config.yaml:ro" - meshtastic_data:/var/lib/meshtasticd # Forward the container’s port 4403 to the host ports: - 4403:4403 restart: unless-stopped volumes: meshtastic_data: ================================================ FILE: extra_scripts/README.md ================================================ # 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. ================================================ FILE: extra_scripts/disable_adafruit_usb.py ================================================ #!/usr/bin/env python3 # trunk-ignore-all(flake8/F821) # trunk-ignore-all(ruff/F821) Import("env") # 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...) ================================================ FILE: extra_scripts/esp32_extra.py ================================================ #!/usr/bin/env python3 # trunk-ignore-all(ruff/F821) # trunk-ignore-all(flake8/F821): For SConstruct imports # trunk-ignore-all(ruff/E402): Hacky esptool import # trunk-ignore-all(flake8/E402): Hacky esptool import import sys from os.path import join Import("env") platform = env.PioPlatform() sys.path.append(join(platform.get_package_dir("tool-esptoolpy"))) # IntelHex workaround, remove after fixed upstream # https://github.com/platformio/platform-espressif32/issues/1632 try: import intelhex except ImportError: env.Execute("$PYTHONEXE -m pip install intelhex") import esptool 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") board = env.BoardConfig() flash_size = board.get("upload.flash_size") flash_freq = board.get("build.f_flash", "40m") flash_freq = flash_freq.replace("000000L", "m") flash_mode = board.get("build.flash_mode", "dio") memory_type = board.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) 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"]) ================================================ FILE: extra_scripts/esp32_pre.py ================================================ #!/usr/bin/env python3 # trunk-ignore-all(ruff/F821) # trunk-ignore-all(flake8/F821): For SConstruct imports import json import sys from os.path import isfile Import("env") # From https://github.com/platformio/platform-espressif32/blob/develop/builder/main.py def _parse_size(value): if isinstance(value, int): return value elif value.isdigit(): return int(value) elif value.startswith("0x"): return int(value, 16) elif value[-1].upper() in ("K", "M"): base = 1024 if value[-1].upper() == "K" else 1024 * 1024 return int(value[:-1]) * base return value def _parse_partitions(env): partitions_csv = env.subst("$PARTITIONS_TABLE_CSV") if not isfile(partitions_csv): sys.stderr.write( "Could not find the file %s with partitions " "table.\n" % partitions_csv ) env.Exit(1) return result = [] # The first offset is 0x9000 because partition table is flashed to 0x8000 and # occupies an entire flash sector, which size is 0x1000 next_offset = 0x9000 with open(partitions_csv) as fp: for line in fp.readlines(): line = line.strip() if not line or line.startswith("#"): continue tokens = [t.strip() for t in line.split(",")] if len(tokens) < 5: continue bound = 0x10000 if tokens[1] in ("0", "app") else 4 calculated_offset = (next_offset + bound - 1) & ~(bound - 1) partition = { "name": tokens[0], "type": tokens[1], "subtype": tokens[2], "offset": tokens[3] or calculated_offset, "size": tokens[4], "flags": tokens[5] if len(tokens) > 5 else None, } result.append(partition) next_offset = _parse_size(partition["offset"]) + _parse_size( partition["size"] ) return result def mtjson_esp32_part(target, source, env): part = _parse_partitions(env) pj = json.dumps(part) # print(f"JSON_PARTITIONS: {pj}") # Dump json string to 'custom_mtjson_part' variable to use later when writing the manifest env.Replace(custom_mtjson_part=pj) env.AddPreAction("mtjson", mtjson_esp32_part) ================================================ FILE: extra_scripts/nrf52_extra.py ================================================ #!/usr/bin/env python3 # trunk-ignore-all(ruff/F821) # trunk-ignore-all(flake8/F821): For SConstruct imports import sys from os.path import basename Import("env") # Custom HEX from ELF # Convert hex to uf2 for nrf52 def nrf52_hex_to_uf2(source, target, env): hex_path = target[0].get_abspath() # When using merged hex, drop 'merged' from uf2 filename uf2_path = hex_path.replace(".merged.", ".") uf2_path = uf2_path.replace(".hex", ".uf2") env.Execute( env.VerboseAction( f'"{sys.executable}" ./bin/uf2conv.py "{hex_path}" -c -f 0xADA52840 -o "{uf2_path}"', f"Generating UF2 file from {basename(hex_path)}", ) ) def nrf52_mergehex(source, target, env): hex_path = target[0].get_abspath() merged_hex_path = hex_path.replace(".hex", ".merged.hex") merge_with = None if "wio-sdk-wm1110" == str(env.get("PIOENV")): merge_with = env.subst("$PROJECT_DIR/bin/s140_nrf52_7.3.0_softdevice.hex") else: print("merge_with not defined for this target") if merge_with is not None: env.Execute( env.VerboseAction( f'"$PROJECT_DIR/bin/mergehex" -m "{hex_path}" "{merge_with}" -o "{merged_hex_path}"', "Merging HEX with SoftDevice", ) ) print(f'Merged file saved at "{basename(merged_hex_path)}"') nrf52_hex_to_uf2([hex_path, merge_with], [env.File(merged_hex_path)], env) # if WM1110 target, merge hex with softdevice 7.3.0 if "wio-sdk-wm1110" == env.get("PIOENV"): env.AddPostAction("$BUILD_DIR/${PROGNAME}.hex", nrf52_mergehex) else: env.AddPostAction("$BUILD_DIR/${PROGNAME}.hex", nrf52_hex_to_uf2) ================================================ FILE: extra_scripts/stm32_extra.py ================================================ #!/usr/bin/env python3 # trunk-ignore-all(ruff/F821) # trunk-ignore-all(flake8/F821): For SConstruct imports Import("env") # Custom HEX from ELF env.AddPostAction( "$BUILD_DIR/${PROGNAME}.elf", env.VerboseAction( " ".join( [ "$OBJCOPY", "-O", "ihex", "-R", ".eeprom", "$BUILD_DIR/${PROGNAME}.elf", "$BUILD_DIR/${PROGNAME}.hex", ] ), "Building $BUILD_DIR/${PROGNAME}.hex", ), ) ================================================ FILE: flake.nix ================================================ { description = "Nix flake to compile Meshtastic firmware"; inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; # Shim to make flake.nix work with stable Nix. flake-compat = { url = "github:NixOS/flake-compat"; flake = false; }; }; outputs = inputs: let lib = inputs.nixpkgs.lib; forAllSystems = fn: lib.genAttrs lib.systems.flakeExposed ( system: fn { pkgs = import inputs.nixpkgs { inherit system; }; inherit system; } ); in { devShells = forAllSystems ( { pkgs, ... }: let python3 = pkgs.python312.withPackages ( ps: with ps; [ google ] ); in { default = pkgs.mkShell { buildInputs = with pkgs; [ python3 platformio ]; shellHook = '' # Set up PlatformIO to use a local core directory. export PLATFORMIO_CORE_DIR=$PWD/.platformio # Tell pip to put packages into $PIP_PREFIX instead of the usual # location. This is especially necessary under NixOS to avoid having # pip trying to write to the read-only Nix store. For more info, # see https://wiki.nixos.org/wiki/Python export PIP_PREFIX=$PWD/.python3 export PYTHONPATH="$PIP_PREFIX/${python3.sitePackages}" export PATH="$PIP_PREFIX/bin:$PATH" # Avoids reproducibility issues with some Python packages # See https://nixos.org/manual/nixpkgs/stable/#python-setup.py-bdist_wheel-cannot-create-.whl unset SOURCE_DATE_EPOCH ''; }; } ); }; } ================================================ FILE: meshtasticd.spec.rpkg ================================================ # meshtasticd spec file for RPM-based distributions # # Build locally with: # ``` # sudo dnf install rpkg-util # rpkg local # ``` # # See: # - https://docs.pagure.org/rpkg-util/v3/index.html # - https://docs.fedoraproject.org/en-US/packaging-guidelines/Versioning/ %global meshtasticd_user meshtasticd Name: meshtasticd # Version Ex: 2.5.19 Version: {{{ meshtastic_version }}} # Release Ex: 9127.daily.gitd7f5f620.fc41 Release: {{{ git_commits_num }}}%{?copr_projectname:.%{copr_projectname}}.git{{{ git_commit_sha }}}%{?dist} VCS: {{{ git_dir_vcs }}} Summary: Meshtastic daemon for communicating with Meshtastic devices License: GPL-3.0 URL: https://github.com/meshtastic/firmware Source0: {{{ git_dir_pack }}} Source1: https://github.com/meshtastic/web/releases/download/v{{{ web_version }}}/build.tar BuildRequires: systemd-rpm-macros BuildRequires: python3-devel BuildRequires: platformio BuildRequires: python3dist(protobuf) BuildRequires: python3dist(grpcio[protobuf]) BuildRequires: python3dist(grpcio-tools) BuildRequires: git-core BuildRequires: gcc-c++ BuildRequires: pkgconfig(yaml-cpp) BuildRequires: pkgconfig(libgpiod) BuildRequires: pkgconfig(bluez) BuildRequires: pkgconfig(libusb-1.0) BuildRequires: libi2c-devel BuildRequires: pkgconfig(libuv) BuildRequires: pkgconfig(sqlite3) # Web components: BuildRequires: pkgconfig(openssl) BuildRequires: pkgconfig(liborcania) BuildRequires: pkgconfig(libyder) BuildRequires: pkgconfig(libulfius) # TFT components: BuildRequires: pkgconfig(x11) BuildRequires: pkgconfig(libinput) BuildRequires: pkgconfig(xkbcommon-x11) BuildRequires: pkgconfig(sdl2) # libbsd is needed on older Fedora/RHEL to provide 'strlcpy' %if 0%{?fedora} >= 39 || 0%{?rhel} >= 10 BuildRequires: glibc-devel >= 2.38 %else BuildRequires: pkgconfig(libbsd-overlay) %endif Requires: systemd-udev # Declare that this package provides the user/group it creates in %pre # Required for Fedora 43+ which tracks users/groups as RPM dependencies Provides: user(%{meshtasticd_user}) Provides: group(%{meshtasticd_user}) Provides: group(spi) %description Meshtastic daemon. Meshtastic is an off-grid text communication platform that uses inexpensive LoRa radios. %prep {{{ git_dir_setup_macro }}} # Unpack the web files mkdir -p web tar -xf %{SOURCE1} -C web gzip -dr web %build # Use the “native-tft” environment from platformio to build a Linux binary platformio run -e native-tft %install # Install meshtasticd binary mkdir -p %{buildroot}%{_bindir} install -m 0755 .pio/build/native-tft/meshtasticd %{buildroot}%{_bindir}/meshtasticd # Install portduino VFS dir install -p -d -m 0770 %{buildroot}%{_localstatedir}/lib/meshtasticd # Install udev rules mkdir -p %{buildroot}%{_udevrulesdir} install -m 0644 bin/99-meshtasticd-udev.rules %{buildroot}%{_udevrulesdir}/99-meshtasticd-udev.rules # Install config dirs mkdir -p %{buildroot}%{_sysconfdir}/meshtasticd install -m 0644 bin/config-dist.yaml %{buildroot}%{_sysconfdir}/meshtasticd/config.yaml mkdir -p %{buildroot}%{_sysconfdir}/meshtasticd/config.d mkdir -p %{buildroot}%{_sysconfdir}/meshtasticd/available.d cp -r bin/config.d/* %{buildroot}%{_sysconfdir}/meshtasticd/available.d # Install systemd service install -D -m 0644 bin/meshtasticd.service %{buildroot}%{_unitdir}/meshtasticd.service # Install meshtasticd start wrapper install -D -m 0755 bin/meshtasticd-start.sh %{buildroot}%{_bindir}/meshtasticd-start.sh # Install the web files under /usr/share/meshtasticd/web mkdir -p %{buildroot}%{_datadir}/meshtasticd/web cp -r web/* %{buildroot}%{_datadir}/meshtasticd/web # Install default SSL storage directory (for web) mkdir -p %{buildroot}%{_sysconfdir}/meshtasticd/ssl %pre # create spi group (for udev rules) getent group spi > /dev/null || groupadd -r spi # create a meshtasticd group and user getent group %{meshtasticd_user} > /dev/null || groupadd -r %{meshtasticd_user} getent passwd %{meshtasticd_user} > /dev/null || \ useradd -r -d %{_localstatedir}/lib/meshtasticd -g %{meshtasticd_user} -G spi \ -s /sbin/nologin -c "Meshtastic Daemon" %{meshtasticd_user} # add meshtasticd user to appropriate groups (if they exist) getent group gpio > /dev/null && usermod -a -G gpio %{meshtasticd_user} > /dev/null getent group plugdev > /dev/null && usermod -a -G plugdev %{meshtasticd_user} > /dev/null getent group dialout > /dev/null && usermod -a -G dialout %{meshtasticd_user} > /dev/null getent group i2c > /dev/null && usermod -a -G i2c %{meshtasticd_user} > /dev/null getent group video > /dev/null && usermod -a -G video %{meshtasticd_user} > /dev/null getent group audio > /dev/null && usermod -a -G audio %{meshtasticd_user} > /dev/null getent group input > /dev/null && usermod -a -G input %{meshtasticd_user} > /dev/null exit 0 %triggerin -- meshtasticd < 2.6.9 # migrate .portduino (if it exists and hasn’t already been copied) if [ -d /root/.portduino ] && [ ! -e /var/lib/meshtasticd/.portduino ]; then mkdir -p /var/lib/meshtasticd cp -r /root/.portduino /var/lib/meshtasticd/.portduino chown -R %{meshtasticd_user}:%{meshtasticd_user} \ %{_localstatedir}/lib/meshtasticd || : # Fix SELinux labels if present (no-op on non-SELinux systems) restorecon -R /var/lib/meshtasticd/.portduino 2>/dev/null || : echo "Migrated meshtasticd VFS from /root/.portduino to /var/lib/meshtasticd/.portduino" echo "meshtasticd now runs as the 'meshtasticd' user, not 'root'." echo "See https://github.com/meshtastic/firmware/pull/6718 for details" fi %post %systemd_post meshtasticd.service %preun %systemd_preun meshtasticd.service %postun %systemd_postun_with_restart meshtasticd.service %files %defattr(-,%{meshtasticd_user},%{meshtasticd_user}) %license LICENSE %doc README.md %{_bindir}/meshtasticd %{_bindir}/meshtasticd-start.sh %dir %{_localstatedir}/lib/meshtasticd %{_udevrulesdir}/99-meshtasticd-udev.rules %dir %{_sysconfdir}/meshtasticd %dir %{_sysconfdir}/meshtasticd/config.d %dir %{_sysconfdir}/meshtasticd/available.d %config(noreplace) %{_sysconfdir}/meshtasticd/config.yaml %config %{_sysconfdir}/meshtasticd/available.d/* %{_unitdir}/meshtasticd.service %dir %{_datadir}/meshtasticd %dir %{_datadir}/meshtasticd/web %{_datadir}/meshtasticd/web/* %dir %{_sysconfdir}/meshtasticd/ssl %changelog %autochangelog ================================================ FILE: monitor/filter_c3_exception_decoder.py ================================================ # trunk-ignore-all(bandit/B404): subprocess is used to call addr2line # trunk-ignore-all(bandit/B603): subprocess is used to call addr2line # 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 ================================================ FILE: partition-table-8MB.csv ================================================ # This is a layout for 8MB of flash for MUI devices # Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x9000, 0x5000, otadata, data, ota, 0xe000, 0x2000, app0, app, ota_0, 0x10000, 0x5C0000, flashApp, app, ota_1, 0x5D0000,0x0A0000, spiffs, data, spiffs, 0x670000,0x180000 ================================================ FILE: partition-table.csv ================================================ # 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, ================================================ FILE: platformio.ini ================================================ ; PlatformIO Project Configuration File ; https://docs.platformio.org/page/projectconf.html [platformio] default_envs = tbeam extra_configs = variants/*/*.ini variants/*/*/platformio.ini variants/*/diy/*/platformio.ini src/graphics/niche/InkHUD/PlatformioConfig.ini description = Meshtastic [env] test_build_src = true extra_scripts = pre:bin/platformio-pre.py 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,"${platformio.build_dir}"/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_REPLYBOT=1 -DMESHTASTIC_EXCLUDE_REMOTEHARDWARE=1 -DMESHTASTIC_EXCLUDE_HEALTH_TELEMETRY=1 -DMESHTASTIC_EXCLUDE_POWERSTRESS=1 ; exclude power stress test module from main firmware -DMESHTASTIC_EXCLUDE_GENERIC_THREAD_MODULE=1 -DMESHTASTIC_EXCLUDE_POWERMON=1 -DMESHTASTIC_EXCLUDE_STATUS=1 -D MAX_THREADS=40 ; As we've split modules, we have more threads to manage -DLED_BUILTIN=-1 #-DBUILD_EPOCH=$UNIX_TIME ; set in platformio-custom.py now #-D OLED_PL=1 #-D DEBUG_HEAP=1 ; uncomment to add free heap space / memory leak debugging logs #-D DEBUG_LOOP_TIMING=1 ; uncomment to add main loop timing logs monitor_speed = 115200 monitor_filters = direct lib_deps = # renovate: datasource=git-refs depName=meshtastic-esp8266-oled-ssd1306 packageName=https://github.com/meshtastic/esp8266-oled-ssd1306 gitBranch=master https://github.com/meshtastic/esp8266-oled-ssd1306/archive/21e484f409cde18d44012caef84c244eb5ca28f3.zip # renovate: datasource=git-refs depName=meshtastic-OneButton packageName=https://github.com/meshtastic/OneButton gitBranch=master https://github.com/meshtastic/OneButton/archive/fa352d668c53f290cfa480a5f79ad422cd828c70.zip # renovate: datasource=git-refs depName=meshtastic-arduino-fsm packageName=https://github.com/meshtastic/arduino-fsm gitBranch=master https://github.com/meshtastic/arduino-fsm/archive/7db3702bf0cfe97b783d6c72595e3f38e0b19159.zip # renovate: datasource=git-refs depName=meshtastic-TinyGPSPlus packageName=https://github.com/meshtastic/TinyGPSPlus gitBranch=master https://github.com/meshtastic/TinyGPSPlus/archive/71a82db35f3b973440044c476d4bcdc673b104f4.zip # renovate: datasource=git-refs depName=meshtastic-ArduinoThread packageName=https://github.com/meshtastic/ArduinoThread gitBranch=master https://github.com/meshtastic/ArduinoThread/archive/b841b0415721f1341ea41cccfb4adccfaf951567.zip # renovate: datasource=custom.pio depName=Nanopb packageName=nanopb/library/Nanopb nanopb/Nanopb@0.4.91 # renovate: datasource=custom.pio depName=ErriezCRC32 packageName=erriez/library/ErriezCRC32 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} # renovate: datasource=custom.pio depName=NonBlockingRTTTL packageName=end2endzone/library/NonBlockingRTTTL end2endzone/NonBlockingRTTTL@1.4.0 build_unflags = -std=c++11 -std=gnu++11 build_flags = ${env.build_flags} -Os -std=gnu++17 build_src_filter = ${env.build_src_filter} - - ; Common libs for communicating over TCP/IP networks such as MQTT [networking_base] lib_deps = # renovate: datasource=custom.pio depName=TBPubSubClient packageName=thingsboard/library/TBPubSubClient thingsboard/TBPubSubClient@2.12.1 # renovate: datasource=custom.pio depName=NTPClient packageName=arduino-libraries/library/NTPClient arduino-libraries/NTPClient@3.2.1 ; Extra TCP/IP networking libs for supported devices [networking_extra] lib_deps = # renovate: datasource=custom.pio depName=Syslog packageName=arcao/library/Syslog arcao/Syslog@2.0.0 [radiolib_base] lib_deps = # renovate: datasource=custom.pio depName=RadioLib packageName=jgromes/library/RadioLib jgromes/RadioLib@7.6.0 [device-ui_base] lib_deps = # renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master https://github.com/meshtastic/device-ui/archive/f36d2a953524e372b78c5b4147ec55f38716964e.zip ; Common libs for environmental measurements in telemetry module [environmental_base] lib_deps = # renovate: datasource=custom.pio depName=Adafruit BusIO packageName=adafruit/library/Adafruit BusIO adafruit/Adafruit BusIO@1.17.4 # renovate: datasource=custom.pio depName=Adafruit Unified Sensor packageName=adafruit/library/Adafruit Unified Sensor adafruit/Adafruit Unified Sensor@1.1.15 # renovate: datasource=custom.pio depName=Adafruit BMP280 packageName=adafruit/library/Adafruit BMP280 Library adafruit/Adafruit BMP280 Library@3.0.0 # renovate: datasource=custom.pio depName=Adafruit BMP085 packageName=adafruit/library/Adafruit BMP085 Library adafruit/Adafruit BMP085 Library@1.2.4 # renovate: datasource=custom.pio depName=Adafruit BME280 packageName=adafruit/library/Adafruit BME280 Library adafruit/Adafruit BME280 Library@2.3.0 # renovate: datasource=custom.pio depName=Adafruit DPS310 packageName=adafruit/library/Adafruit DPS310 adafruit/Adafruit DPS310@1.1.6 # renovate: datasource=custom.pio depName=Adafruit MCP9808 packageName=adafruit/library/Adafruit MCP9808 Library adafruit/Adafruit MCP9808 Library@2.0.2 # renovate: datasource=custom.pio depName=Adafruit INA260 packageName=adafruit/library/Adafruit INA260 Library adafruit/Adafruit INA260 Library@1.5.3 # renovate: datasource=custom.pio depName=Adafruit INA219 packageName=adafruit/library/Adafruit INA219 adafruit/Adafruit INA219@1.2.3 # renovate: datasource=custom.pio depName=Adafruit MPU6050 packageName=adafruit/library/Adafruit MPU6050 adafruit/Adafruit MPU6050@2.2.9 # renovate: datasource=custom.pio depName=Adafruit LIS3DH packageName=adafruit/library/Adafruit LIS3DH adafruit/Adafruit LIS3DH@1.3.0 # renovate: datasource=custom.pio depName=Adafruit AHTX0 packageName=adafruit/library/Adafruit AHTX0 adafruit/Adafruit AHTX0@2.0.6 # renovate: datasource=custom.pio depName=Adafruit LSM6DS packageName=adafruit/library/Adafruit LSM6DS adafruit/Adafruit LSM6DS@4.7.4 # renovate: datasource=custom.pio depName=Adafruit TSL2591 packageName=adafruit/library/Adafruit TSL2591 Library adafruit/Adafruit TSL2591 Library@1.4.5 # renovate: datasource=custom.pio depName=EmotiBit MLX90632 packageName=emotibit/library/EmotiBit MLX90632 emotibit/EmotiBit MLX90632@1.0.8 # renovate: datasource=custom.pio depName=Adafruit MLX90614 packageName=adafruit/library/Adafruit MLX90614 Library adafruit/Adafruit MLX90614 Library@2.1.6 # renovate: datasource=git-refs depName=INA3221 packageName=https://github.com/sgtwilko/INA3221 gitBranch=FixOverflow https://github.com/sgtwilko/INA3221/archive/bb03d7e9bfcc74fc798838a54f4f99738f29fc6a.zip # renovate: datasource=custom.pio depName=QMC5883L Compass packageName=mprograms/library/QMC5883LCompass mprograms/QMC5883LCompass@1.2.3 # renovate: datasource=custom.pio depName=DFRobot_RTU packageName=dfrobot/library/DFRobot_RTU dfrobot/DFRobot_RTU@1.0.6 # renovate: datasource=git-refs depName=DFRobot_RainfallSensor packageName=https://github.com/DFRobot/DFRobot_RainfallSensor gitBranch=master https://github.com/DFRobot/DFRobot_RainfallSensor/archive/38fea5e02b40a5430be6dab39a99a6f6347d667e.zip # renovate: datasource=custom.pio depName=INA226 packageName=robtillaart/library/INA226 robtillaart/INA226@0.6.6 # renovate: datasource=custom.pio depName=SparkFun MAX3010x packageName=sparkfun/library/SparkFun MAX3010x Pulse and Proximity Sensor Library sparkfun/SparkFun MAX3010x Pulse and Proximity Sensor Library@1.1.2 # renovate: datasource=custom.pio depName=SparkFun 9DoF IMU Breakout ICM 20948 packageName=sparkfun/library/SparkFun 9DoF IMU Breakout - ICM 20948 - Arduino Library sparkfun/SparkFun 9DoF IMU Breakout - ICM 20948 - Arduino Library@1.3.2 # renovate: datasource=custom.pio depName=Adafruit LTR390 Library packageName=adafruit/library/Adafruit LTR390 Library adafruit/Adafruit LTR390 Library@1.1.2 # renovate: datasource=custom.pio depName=Adafruit PCT2075 packageName=adafruit/library/Adafruit PCT2075 adafruit/Adafruit PCT2075@1.0.6 # renovate: datasource=custom.pio depName=DFRobot_BMM150 packageName=dfrobot/library/DFRobot_BMM150 dfrobot/DFRobot_BMM150@1.0.0 # renovate: datasource=custom.pio depName=Adafruit_TSL2561 packageName=adafruit/library/Adafruit TSL2561 adafruit/Adafruit TSL2561@1.1.3 # renovate: datasource=custom.pio depName=BH1750_WE packageName=wollewald/library/BH1750_WE wollewald/BH1750_WE@1.1.10 ; Common environmental sensor libraries (not included in native / portduino) [environmental_extra_common] lib_deps = # renovate: datasource=custom.pio depName=Adafruit BMP3XX packageName=adafruit/library/Adafruit BMP3XX Library adafruit/Adafruit BMP3XX Library@2.1.6 # renovate: datasource=custom.pio depName=Adafruit MAX1704X packageName=adafruit/library/Adafruit MAX1704X adafruit/Adafruit MAX1704X@1.0.3 # renovate: datasource=custom.pio depName=Adafruit SHTC3 packageName=adafruit/library/Adafruit SHTC3 Library adafruit/Adafruit SHTC3 Library@1.0.2 # renovate: datasource=custom.pio depName=Adafruit LPS2X packageName=adafruit/library/Adafruit LPS2X adafruit/Adafruit LPS2X@2.0.6 # renovate: datasource=custom.pio depName=Adafruit SHT31 packageName=adafruit/library/Adafruit SHT31 Library adafruit/Adafruit SHT31 Library@2.2.2 # renovate: datasource=custom.pio depName=Adafruit VEML7700 packageName=adafruit/library/Adafruit VEML7700 Library adafruit/Adafruit VEML7700 Library@2.1.6 # renovate: datasource=custom.pio depName=Adafruit SHT4x packageName=adafruit/library/Adafruit SHT4x Library adafruit/Adafruit SHT4x Library@1.0.5 # renovate: datasource=custom.pio depName=SparkFun Qwiic Scale NAU7802 packageName=sparkfun/library/SparkFun Qwiic Scale NAU7802 Arduino Library sparkfun/SparkFun Qwiic Scale NAU7802 Arduino Library@1.0.6 # renovate: datasource=custom.pio depName=ClosedCube OPT3001 packageName=closedcube/library/ClosedCube OPT3001 closedcube/ClosedCube OPT3001@1.1.2 # renovate: datasource=git-refs depName=meshtastic-DFRobot_LarkWeatherStation packageName=https://github.com/meshtastic/DFRobot_LarkWeatherStation gitBranch=master https://github.com/meshtastic/DFRobot_LarkWeatherStation/archive/4de3a9cadef0f6a5220a8a906cf9775b02b0040d.zip # renovate: datasource=custom.pio depName=Sensirion Core packageName=sensirion/library/Sensirion Core sensirion/Sensirion Core@0.7.3 # renovate: datasource=custom.pio depName=Sensirion I2C SCD4x packageName=sensirion/library/Sensirion I2C SCD4x sensirion/Sensirion I2C SCD4x@1.1.0 # renovate: datasource=custom.pio depName=Sensirion I2C SFA3x packageName=sensirion/library/Sensirion I2C SFA3x sensirion/Sensirion I2C SFA3x@1.0.0 # renovate: datasource=custom.pio depName=Sensirion I2C SCD30 packageName=sensirion/library/Sensirion I2C SCD30 sensirion/Sensirion I2C SCD30@1.0.0 ; Environmental sensors with BSEC2 (Bosch proprietary IAQ) [environmental_extra] lib_deps = ${environmental_extra_common.lib_deps} # renovate: datasource=custom.pio depName=Bosch BSEC2 packageName=boschsensortec/library/bsec2 boschsensortec/bsec2@1.10.2610 # renovate: datasource=custom.pio depName=Bosch BME68x packageName=boschsensortec/library/BME68x Sensor Library boschsensortec/BME68x Sensor Library@1.3.40408 ; Environmental sensors without BSEC (saves ~3.5KB DRAM for original ESP32 targets) [environmental_extra_no_bsec] lib_deps = ${environmental_extra_common.lib_deps} # renovate: datasource=custom.pio depName=adafruit/Adafruit BME680 Library packageName=adafruit/library/Adafruit BME680 adafruit/Adafruit BME680 Library@^2.0.5 ================================================ FILE: pyocd.yaml ================================================ # 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 ================================================ FILE: renovate.json ================================================ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ ":dependencyDashboard", ":semanticCommitTypeAll(chore)", ":ignoreModulesAndTests", "group:recommended", "replacements:all", "workarounds:all" ], "baseBranchPatterns": ["master"], "forkProcessing": "enabled", "ignoreDeps": [ "protobufs" ], "git-submodules": { "enabled": true }, "pip_requirements": { "managerFilePatterns": [ "/bin/bump_metainfo/requirements.txt/" ] }, "commitMessageTopic": "{{depName}}", "labels": [ "dependencies" ], "customDatasources": { "pio": { "description": "PlatformIO Registry", "defaultRegistryUrlTemplate": "https://api.registry.platformio.org/v3/packages/{{packageName}}", "format": "json", "transformTemplates": [ "{\"releases\": [$map($.versions, function($v) { { \"version\": $v.name, \"releaseTimestamp\": $v.released_at } })], \"homepage\": $encodeUrl($join([\"https://registry.platformio.org/\",$.type,\"/\",$.owner.username,\"/\",$.name])) }" ] } }, "customManagers": [ { "customType": "regex", "description": "Match meshtastic/web version", "managerFilePatterns": [ "/bin/web.version/" ], "matchStrings": [ "(?.+)$" ], "datasourceTemplate": "github-releases", "depNameTemplate": "meshtastic/web", "versioningTemplate": "semver-coerced" }, { "customType": "regex", "description": "Match normal PIO dependencies", "managerFilePatterns": [ "/.*\\.ini$/" ], "matchStrings": [ "# renovate: datasource=(?.*?)(?: depName=(?.+?))? packageName=(?.+?)(?: versioning=(?[a-z-]+?))?\\s+?.+?@(?.+?)\\s" ], "versioningTemplate": "{{#if versioning}}{{{versioning}}}{{else}}semver-coerced{{/if}}" }, { "customType": "regex", "description": "Match PIO zipped dependencies with github tag ref", "managerFilePatterns": [ "/.*\\.ini$/" ], "matchStrings": [ "# renovate: datasource=github-tags(?: depName=(?.+?))? packageName=(?.+?)(?: versioning=(?[a-z-]+?))?\\s+?https://.+?archive/(?.+?).zip\\s" ], "datasourceTemplate": "github-tags", "versioningTemplate": "{{#if versioning}}{{{versioning}}}{{else}}semver-coerced{{/if}}" }, { "customType": "regex", "description": "Match PIO zipped dependencies with git commit ref", "managerFilePatterns": [ "/.*\\.ini$/" ], "matchStrings": [ "# renovate: datasource=git-refs(?: depName=(?.+?))? packageName=(?.+?)(?: versioning=(?[a-z-]+?))?\\sgitBranch=(?.+?)\\s+?https://.+?archive/(?.+?).zip\\s" ], "datasourceTemplate": "git-refs", "currentValueTemplate": "{{{gitBranch}}}", "versioningTemplate": "{{#if versioning}}{{{versioning}}}{{else}}git{{/if}}" } ], "packageRules": [ { "matchDepNames": [ "meshtastic/device-ui" ], "reviewers": [ "mverch67" ], "changelogUrl": "https://github.com/meshtastic/device-ui/compare/{{currentDigest}}...{{newDigest}}" } ] } ================================================ FILE: rpkg.conf ================================================ [rpkg] user_macros = "${git_props:root}/bin/rpkg.macros" ================================================ FILE: shell.nix ================================================ (import ( let lock = builtins.fromJSON (builtins.readFile ./flake.lock); nodeName = lock.nodes.root.inputs.flake-compat; in fetchTarball { url = lock.nodes.${nodeName}.locked.url or "https://github.com/NixOS/flake-compat/archive/${lock.nodes.${nodeName}.locked.rev}.tar.gz"; sha256 = lock.nodes.${nodeName}.locked.narHash; } ) { src = ./.; }).shellNix ================================================ FILE: src/AmbientLightingThread.h ================================================ #ifndef AMBIENTLIGHTINGTHREAD_H #define AMBIENTLIGHTINGTHREAD_H #include "Observer.h" #include "configuration.h" #include "detect/ScanI2C.h" #include "sleep.h" #ifdef HAS_NCP5623 #include #include #endif #ifdef HAS_LP5562 #include #endif #ifdef HAS_NEOPIXEL #include #endif #ifdef UNPHONE #include "unPhone.h" extern unPhone unphone; #endif class AmbientLightingThread : public concurrency::OSThread { friend class StatusLEDModule; // Let the LEDStatusModule trigger the ambient lighting for notifications and battery status. friend class ExternalNotificationModule; // Let the ExternalNotificationModule trigger the ambient lighting for notifications. private: #ifdef HAS_NCP5623 NCP5623 rgb; #endif #ifdef HAS_LP5562 LP5562 rgbw; #endif #ifdef HAS_NEOPIXEL Adafruit_NeoPixel pixels = Adafruit_NeoPixel(NEOPIXEL_COUNT, NEOPIXEL_DATA, NEOPIXEL_TYPE); #endif public: explicit AmbientLightingThread(ScanI2C::DeviceType type) : OSThread("AmbientLighting") { notifyDeepSleepObserver.observe(¬ifyDeepSleep); // Let us know when shutdown() is issued. // Enables Ambient Lighting by default if conditions are meet. #ifdef HAS_RGB_LED #ifdef ENABLE_AMBIENTLIGHTING moduleConfig.ambient_lighting.led_state = true; #endif #endif #if AMBIENT_LIGHTING_TEST // define to enable test 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; #endif #if defined(HAS_NCP5623) || defined(HAS_LP5562) _type = type; if (_type == ScanI2C::DeviceType::NONE) { LOG_DEBUG("AmbientLighting Disable due to no RGB leds found on I2C bus"); disable(); return; } #endif #ifdef HAS_RGB_LED LOG_DEBUG("AmbientLighting init"); #ifdef HAS_NCP5623 if (_type == ScanI2C::NCP5623) { rgb.begin(); #endif #ifdef HAS_LP5562 if (_type == ScanI2C::LP5562) { rgbw.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 if (!moduleConfig.ambient_lighting.led_state) { LOG_DEBUG("AmbientLighting Disable due to moduleConfig.ambient_lighting.led_state OFF"); disable(); return; } setLighting(moduleConfig.ambient_lighting.current, moduleConfig.ambient_lighting.red, moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue); #endif #if defined(HAS_NCP5623) || defined(HAS_LP5562) } #endif } protected: int32_t runOnce() override { #ifdef HAS_RGB_LED #if defined(HAS_NCP5623) || defined(HAS_LP5562) if ((_type == ScanI2C::NCP5623 || _type == ScanI2C::LP5562) && moduleConfig.ambient_lighting.led_state) { #endif setLighting(moduleConfig.ambient_lighting.current, moduleConfig.ambient_lighting.red, moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue); return 30000; // 30 seconds to reset from any animations that may have been running from Ext. Notification #if defined(HAS_NCP5623) || defined(HAS_LP5562) } #endif #endif return disable(); } // 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("OFF: NCP5623 Ambient lighting"); #endif #ifdef HAS_LP5562 rgbw.setCurrent(0); rgbw.setRed(0); rgbw.setGreen(0); rgbw.setBlue(0); rgbw.setWhite(0); LOG_INFO("OFF: LP5562 Ambient lighting"); #endif #ifdef HAS_NEOPIXEL pixels.clear(); pixels.show(); LOG_INFO("OFF: NeoPixel Ambient lighting"); #endif #ifdef RGBLED_CA analogWrite(RGBLED_RED, 255 - 0); analogWrite(RGBLED_GREEN, 255 - 0); analogWrite(RGBLED_BLUE, 255 - 0); LOG_INFO("OFF: Ambient light RGB Common Anode"); #elif defined(RGBLED_RED) analogWrite(RGBLED_RED, 0); analogWrite(RGBLED_GREEN, 0); analogWrite(RGBLED_BLUE, 0); LOG_INFO("OFF: Ambient light RGB Common Cathode"); #endif #ifdef UNPHONE unphone.rgb(0, 0, 0); LOG_INFO("OFF: unPhone Ambient lighting"); #endif return 0; } protected: void setLighting(float current, uint8_t red, uint8_t green, uint8_t blue) { #ifdef HAS_NCP5623 rgb.setCurrent(current); rgb.setRed(red); rgb.setGreen(green); rgb.setBlue(blue); LOG_DEBUG("Init NCP5623 Ambient light w/ current=%f, red=%d, green=%d, blue=%d", current, red, green, blue); #endif #ifdef HAS_LP5562 rgbw.setCurrent(current); rgbw.setRed(red); rgbw.setGreen(green); rgbw.setBlue(blue); LOG_DEBUG("Init LP5562 Ambient light w/ current=%f, red=%d, green=%d, blue=%d", current, red, green, blue); #endif #ifdef HAS_NEOPIXEL pixels.fill(pixels.Color(red, green, 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. #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, BUTTON2_COLOR_INDEX, 1); #endif pixels.show(); // LOG_DEBUG("Init NeoPixel Ambient light w/ brightness(current)=%f, red=%d, green=%d, blue=%d", // current, red, green, blue); #endif #ifdef RGBLED_CA analogWrite(RGBLED_RED, 255 - red); analogWrite(RGBLED_GREEN, 255 - green); analogWrite(RGBLED_BLUE, 255 - blue); LOG_DEBUG("Init Ambient light RGB Common Anode w/ red=%d, green=%d, blue=%d", red, green, blue); #elif defined(RGBLED_RED) analogWrite(RGBLED_RED, red); analogWrite(RGBLED_GREEN, green); analogWrite(RGBLED_BLUE, blue); LOG_DEBUG("Init Ambient light RGB Common Cathode w/ red=%d, green=%d, blue=%d", red, green, blue); #endif #ifdef UNPHONE unphone.rgb(red, green, blue); LOG_DEBUG("Init unPhone Ambient light w/ red=%d, green=%d, blue=%d", red, green, blue); #endif } }; #endif // AMBIENTLIGHTINGTHREAD_H ================================================ FILE: src/AudioThread.h ================================================ #pragma once #include "PowerFSM.h" #include "concurrency/OSThread.h" #include "configuration.h" #include "main.h" #include "sleep.h" #include #ifdef HAS_I2S #include #include #include #include #ifdef USE_XL9555 #include "ExtensionIOXL9555.hpp" extern ExtensionIOXL9555 io; #endif #define AUDIO_THREAD_INTERVAL_MS 100 class AudioThread : public concurrency::OSThread { public: AudioThread() : OSThread("Audio") { initOutput(); } void beginRttl(const void *data, uint32_t len) { #ifdef T_LORA_PAGER io.digitalWrite(EXPANDS_AMP_EN, HIGH); #endif setCPUFast(true); rtttlFile = std::unique_ptr(new AudioFileSourcePROGMEM(data, len)); i2sRtttl = std::unique_ptr(new AudioGeneratorRTTTL()); i2sRtttl->begin(rtttlFile.get(), audioOut.get()); } // Also handles actually playing the RTTTL, needs to be called in loop bool isPlaying() { if (i2sRtttl != nullptr) { return i2sRtttl->isRunning() && i2sRtttl->loop(); } return false; } void stop() { if (i2sRtttl != nullptr) { i2sRtttl->stop(); i2sRtttl = nullptr; } rtttlFile = nullptr; setCPUFast(false); #ifdef T_LORA_PAGER io.digitalWrite(EXPANDS_AMP_EN, LOW); #endif } void readAloud(const char *text) { if (i2sRtttl != nullptr) { i2sRtttl->stop(); i2sRtttl = nullptr; } #ifdef T_LORA_PAGER io.digitalWrite(EXPANDS_AMP_EN, HIGH); #endif auto sam = std::unique_ptr(new ESP8266SAM); sam->Say(audioOut.get(), text); setCPUFast(false); #ifdef T_LORA_PAGER io.digitalWrite(EXPANDS_AMP_EN, LOW); #endif } 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 = std::unique_ptr(new AudioOutputI2S(1, AudioOutputI2S::EXTERNAL_I2S)); audioOut->SetPinout(DAC_I2S_BCK, DAC_I2S_WS, DAC_I2S_DOUT, DAC_I2S_MCLK); audioOut->SetGain(0.2); }; std::unique_ptr i2sRtttl = nullptr; std::unique_ptr audioOut = nullptr; std::unique_ptr rtttlFile = nullptr; }; #endif ================================================ FILE: src/BluetoothCommon.cpp ================================================ #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}; ================================================ FILE: src/BluetoothCommon.h ================================================ #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; }; ================================================ FILE: src/BluetoothStatus.h ================================================ #pragma once #include "Status.h" #include "assert.h" #include "configuration.h" #include "meshUtils.h" #include namespace meshtastic { // Describes the state of the Bluetooth connection // Allows display to handle pairing events without each UI needing to explicitly hook the Bluefruit / NimBLE code class BluetoothStatus : public Status { public: enum class ConnectionState { DISCONNECTED, PAIRING, CONNECTED, }; private: CallbackObserver statusObserver = CallbackObserver(this, &BluetoothStatus::updateStatus); ConnectionState state = ConnectionState::DISCONNECTED; std::string passkey; // Stored as string, because Bluefruit allows passkeys with a leading zero public: BluetoothStatus() { statusType = STATUS_TYPE_BLUETOOTH; } // New BluetoothStatus: connected or disconnected explicit BluetoothStatus(ConnectionState state) { assert(state != ConnectionState::PAIRING); // If pairing, use constructor which specifies passkey statusType = STATUS_TYPE_BLUETOOTH; this->state = state; } // New BluetoothStatus: pairing, with passkey explicit BluetoothStatus(const std::string &passkey) : Status() { statusType = STATUS_TYPE_BLUETOOTH; this->state = ConnectionState::PAIRING; this->passkey = passkey; } ConnectionState getConnectionState() const { return this->state; } std::string getPasskey() const { assert(state == ConnectionState::PAIRING); return this->passkey; } void observe(Observable *source) { statusObserver.observe(source); } bool matches(const BluetoothStatus *newStatus) const { if (this->state == newStatus->getConnectionState()) { // Same state: CONNECTED / DISCONNECTED if (this->state != ConnectionState::PAIRING) return true; // Same state: PAIRING, and passkey matches else if (this->getPasskey() == newStatus->getPasskey()) return true; } return false; } int updateStatus(const BluetoothStatus *newStatus) { // Has the status changed? if (!matches(newStatus)) { // Copy the members state = newStatus->getConnectionState(); if (state == ConnectionState::PAIRING) passkey = newStatus->getPasskey(); // Tell anyone interested that we have an update onNewStatus.notifyObservers(this); // Debug only: switch (state) { case ConnectionState::PAIRING: LOG_DEBUG("BluetoothStatus PAIRING, key=%s", passkey.c_str()); break; case ConnectionState::CONNECTED: LOG_DEBUG("BluetoothStatus CONNECTED"); #ifdef BLE_LED digitalWrite(BLE_LED, LED_STATE_ON); #endif break; case ConnectionState::DISCONNECTED: LOG_DEBUG("BluetoothStatus DISCONNECTED"); #ifdef BLE_LED digitalWrite(BLE_LED, LED_STATE_OFF); #endif break; } } return 0; } }; } // namespace meshtastic extern meshtastic::BluetoothStatus *bluetoothStatus; ================================================ FILE: src/DebugConfiguration.cpp ================================================ /* 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 namespace meshtastic { 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 = !portduino_config.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; } }; // namespace meshtastic #endif ================================================ FILE: src/DebugConfiguration.h ================================================ #pragma once #include "configuration.h" // Forward declarations #if defined(DEBUG_HEAP) class MemGet; extern MemGet memGet; #endif // 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" #define MESHTASTIC_LOG_LEVEL_HEAP "HEAP" #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) #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 #if defined(DEBUG_HEAP) #define LOG_HEAP(...) DEBUG_PORT.log(MESHTASTIC_LOG_LEVEL_HEAP, __VA_ARGS__) // Macro-based heap debugging #define DEBUG_HEAP_BEFORE auto heapBefore = memGet.getFreeHeap(); #define DEBUG_HEAP_AFTER(context, ptr) \ do { \ auto heapAfter = memGet.getFreeHeap(); \ if (heapBefore != heapAfter) { \ LOG_HEAP("Alloc in %s pointer 0x%x, size: %u, free: %u", context, ptr, heapBefore - heapAfter, heapAfter); \ } \ } while (0) #else #define LOG_HEAP(...) #define DEBUG_HEAP_BEFORE #define DEBUG_HEAP_AFTER(context, ptr) #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 && !defined(USE_WS5500) #include #endif // HAS_ETHERNET #if HAS_ETHERNET && defined(USE_WS5500) #include #define ETH ETH2 #endif // HAS_ETHERNET #if HAS_WIFI #include #endif // HAS_WIFI #if HAS_NETWORKING namespace meshtastic { 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))); }; }; // namespace meshtastic #endif // HAS_NETWORKING ================================================ FILE: src/DisplayFormatters.cpp ================================================ #include "DisplayFormatters.h" const char *DisplayFormatters::getModemPresetDisplayName(meshtastic_Config_LoRaConfig_ModemPreset preset, bool useShortName, bool usePreset) { // If use_preset is false, always return "Custom" if (!usePreset) { return "Custom"; } switch (preset) { case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO: return useShortName ? "ShortT" : "ShortTurbo"; break; case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW: return useShortName ? "ShortS" : "ShortSlow"; break; case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST: return useShortName ? "ShortF" : "ShortFast"; break; case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW: return useShortName ? "MedS" : "MediumSlow"; break; case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST: return useShortName ? "MedF" : "MediumFast"; break; case meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW: return useShortName ? "LongS" : "LongSlow"; break; case meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST: return useShortName ? "LongF" : "LongFast"; break; case meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO: return useShortName ? "LongT" : "LongTurbo"; break; case meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE: return useShortName ? "LongM" : "LongMod"; break; default: return useShortName ? "Custom" : "Invalid"; break; } } const char *DisplayFormatters::getDeviceRole(meshtastic_Config_DeviceConfig_Role role) { switch (role) { case meshtastic_Config_DeviceConfig_Role_CLIENT: return "Client"; break; case meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE: return "Client Mute"; break; case meshtastic_Config_DeviceConfig_Role_CLIENT_HIDDEN: return "Client Hidden"; break; case meshtastic_Config_DeviceConfig_Role_CLIENT_BASE: return "Client Base"; break; case meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND: return "Lost and Found"; break; case meshtastic_Config_DeviceConfig_Role_TRACKER: return "Tracker"; break; case meshtastic_Config_DeviceConfig_Role_SENSOR: return "Sensor"; break; case meshtastic_Config_DeviceConfig_Role_TAK: return "TAK"; break; case meshtastic_Config_DeviceConfig_Role_TAK_TRACKER: return "TAK Tracker"; break; case meshtastic_Config_DeviceConfig_Role_ROUTER: return "Router"; break; case meshtastic_Config_DeviceConfig_Role_ROUTER_LATE: return "Router Late"; break; default: return "Unknown"; break; } } ================================================ FILE: src/DisplayFormatters.h ================================================ #pragma once #include "NodeDB.h" class DisplayFormatters { public: static const char *getModemPresetDisplayName(meshtastic_Config_LoRaConfig_ModemPreset preset, bool useShortName, bool usePreset); static const char *getDeviceRole(meshtastic_Config_DeviceConfig_Role role); }; ================================================ FILE: src/FSCommon.cpp ================================================ /** * @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 "SPILock.h" #include "configuration.h" // Software SPI is used by MUI so disable SD card here until it's also implemented #if defined(HAS_SDCARD) && !defined(SDCARD_USE_SOFT_SPI) #include #include #ifdef SDCARD_USE_SPI1 SPIClass SPI_HSPI(HSPI); #define SDHandler SPI_HSPI #else #define SDHandler SPI #endif #ifndef SD_SPI_FREQUENCY #define SD_SPI_FREQUENCY 4000000U #endif #endif // HAS_SDCARD /** * @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 FSCom // take SPI Lock concurrency::LockGuard g(spiLock); unsigned char cbuffer[16]; File f1 = FSCom.open(from, FILE_O_READ); if (!f1) { LOG_ERROR("Failed to open source file %s", from); return false; } File 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 FSCom #ifdef ARCH_ESP32 // take SPI Lock spiLock->lock(); // rename was fixed for ESP32 IDF LittleFS in April bool result = FSCom.rename(pathFrom, pathTo); spiLock->unlock(); return result; #else // copyFile does its own locking. 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. * We can't use SPILOCK here because of recursion. Callers of this function should use SPILOCK. * * @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 = {"", static_cast(file.size())}; #ifdef ARCH_ESP32 strcpy(fileInfo.file_name, file.path()); #else strcpy(fileInfo.file_name, file.name()); #endif if (!String(fileInfo.file_name).endsWith(".")) { filenames.push_back(fileInfo); } file.close(); } file = root.openNextFile(); } root.close(); #endif return filenames; } /** * Lists the contents of a directory. * We can't use SPILOCK here because of recursion. Callers of this function should use SPILOCK. * * @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("Remove %s", file.path()); strncpy(buffer, file.path(), sizeof(buffer)); file.close(); FSCom.rmdir(buffer); } else { file.close(); } #elif (defined(ARCH_RP2040) || defined(ARCH_PORTDUINO)) listDir(file.name(), levels - 1, del); if (del) { LOG_DEBUG("Remove %s", file.name()); strncpy(buffer, file.name(), sizeof(buffer)); file.close(); FSCom.rmdir(buffer); } else { file.close(); } #else LOG_DEBUG(" %s (directory)", file.name()); listDir(file.name(), levels - 1, del); file.close(); #endif } } else { #ifdef ARCH_ESP32 if (del) { LOG_DEBUG("Delete %s", file.path()); strncpy(buffer, file.path(), sizeof(buffer)); file.close(); FSCom.remove(buffer); } else { LOG_DEBUG(" %s (%i Bytes)", file.path(), file.size()); file.close(); } #elif (defined(ARCH_RP2040) || defined(ARCH_PORTDUINO)) if (del) { LOG_DEBUG("Delete %s", file.name()); strncpy(buffer, file.name(), sizeof(buffer)); file.close(); FSCom.remove(buffer); } else { LOG_DEBUG(" %s (%i Bytes)", file.name(), file.size()); file.close(); } #else LOG_DEBUG(" %s (%i Bytes)", file.name(), file.size()); file.close(); #endif } file = root.openNextFile(); } #ifdef ARCH_ESP32 if (del) { LOG_DEBUG("Remove %s", root.path()); strncpy(buffer, root.path(), sizeof(buffer)); root.close(); FSCom.rmdir(buffer); } else { root.close(); } #elif (defined(ARCH_RP2040) || defined(ARCH_PORTDUINO)) if (del) { LOG_DEBUG("Remove %s", root.name()); strncpy(buffer, root.name(), sizeof(buffer)); root.close(); FSCom.rmdir(buffer); } else { root.close(); } #else root.close(); #endif #endif } /** * @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 } /** * Some platforms (nrf52) might need to do an extra step before FSBegin(). */ __attribute__((weak, noinline)) void preFSBegin() {} void fsInit() { #ifdef FSCom concurrency::LockGuard g(spiLock); preFSBegin(); if (!FSBegin()) { LOG_ERROR("Filesystem mount failed"); // assert(0); This auto-formats the partition, so no need to fail here. } #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() { #if defined(HAS_SDCARD) && !defined(SDCARD_USE_SOFT_SPI) concurrency::LockGuard g(spiLock); SDHandler.begin(SPI_SCK, SPI_MISO, SPI_MOSI); if (!SD.begin(SDCARD_CS, SDHandler, SD_SPI_FREQUENCY)) { LOG_DEBUG("No SD_MMC card detected"); return; } uint8_t cardType = SD.cardType(); if (cardType == CARD_NONE) { LOG_DEBUG("No SD_MMC card attached"); return; } LOG_DEBUG("SD_MMC Card Type: "); if (cardType == CARD_MMC) { LOG_DEBUG("MMC"); } else if (cardType == CARD_SD) { LOG_DEBUG("SDSC"); } else if (cardType == CARD_SDHC) { LOG_DEBUG("SDHC"); } else { LOG_DEBUG("UNKNOWN"); } 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 } ================================================ FILE: src/FSCommon.h ================================================ #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 #include "LittleFS.h" #define FSCom InternalFS #define FSBegin() FSCom.begin() using namespace STM32_LittleFS_Namespace; #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(); ================================================ FILE: src/Fusion/Fusion.h ================================================ /** * @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 ================================================ FILE: src/Fusion/FusionAhrs.c ================================================ /** * @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 ================================================ FILE: src/Fusion/FusionAhrs.h ================================================ /** * @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 ================================================ FILE: src/Fusion/FusionAxes.h ================================================ /** * @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 ================================================ FILE: src/Fusion/FusionCalibration.h ================================================ /** * @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 ================================================ FILE: src/Fusion/FusionCompass.c ================================================ /** * @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 ================================================ FILE: src/Fusion/FusionCompass.h ================================================ /** * @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 ================================================ FILE: src/Fusion/FusionConvention.h ================================================ /** * @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 ================================================ FILE: src/Fusion/FusionMath.h ================================================ /** * @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 ================================================ FILE: src/Fusion/FusionOffset.c ================================================ /** * @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 ================================================ FILE: src/Fusion/FusionOffset.h ================================================ /** * @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 ================================================ FILE: src/GPSStatus.h ================================================ #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; /// Time of last valid GPS fix (millis since boot) uint32_t lastFixMillis = 0; 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) { meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum()); return node->position.latitude_i; } else { return p.latitude_i; } } int32_t getLongitude() const { if (config.position.fixed_position) { meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum()); return node->position.longitude_i; } else { return p.longitude_i; } } int32_t getAltitude() const { if (config.position.fixed_position) { meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum()); return node->position.altitude; } else { return p.altitude; } } uint32_t getDOP() const { return p.PDOP; } uint32_t getHeading() const { return p.ground_track; } uint32_t getNumSatellites() const { return p.sats_in_view; } /// Return millis() when the last GPS fix occurred (0 = never) uint32_t getLastFixMillis() const { return lastFixMillis; } bool matches(const GPSStatus *newStatus) const { #ifdef GPS_DEBUG LOG_DEBUG("GPSStatus.match() new pos@%x to old pos@%x", newStatus->p.timestamp, p.timestamp); #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) { // Record time of last valid GPS fix lastFixMillis = millis(); // In debug logs, identify position by @timestamp:stage (stage 3 = notify) LOG_DEBUG("New GPS pos@%x:3 lat=%f lon=%f alt=%d pdop=%.2f track=%.2f speed=%.2f sats=%d", p.timestamp, p.latitude_i * 1e-7, p.longitude_i * 1e-7, p.altitude, p.PDOP * 1e-2, p.ground_track * 1e-5, p.ground_speed * 1e-2, p.sats_in_view); } else { LOG_DEBUG("No GPS lock"); } onNewStatus.notifyObservers(this); } return 0; } }; } // namespace meshtastic extern meshtastic::GPSStatus *gpsStatus; ================================================ FILE: src/GpioLogic.cpp ================================================ #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) { 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 initiated because // order of operations for global constructors is not defined. // update(); } void GpioBinaryTransformer::update() { auto p1 = inPin1->get(), p2 = inPin2->get(); GpioVirtPin::PinState newValue = GpioVirtPin::PinState::Unset; if (p1 == GpioVirtPin::PinState::Unset) newValue = p2; // Not yet fully initialized else if (p2 == GpioVirtPin::PinState::Unset) newValue = p1; // Not yet fully initialized // If we've already found our value just use it, otherwise need to do the operation if (newValue == GpioVirtPin::PinState::Unset) { switch (operation) { case And: newValue = (GpioVirtPin::PinState)(p1 && p2); break; case Or: newValue = (GpioVirtPin::PinState)(p1 || p2); break; case Xor: newValue = (GpioVirtPin::PinState)(p1 != p2); break; default: assert(false); } } set(newValue); } GpioSplitter::GpioSplitter(GpioPin *outPin1, GpioPin *outPin2) : outPin1(outPin1), outPin2(outPin2) {} ================================================ FILE: src/GpioLogic.h ================================================ #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; }; ================================================ FILE: src/MessageStore.cpp ================================================ #include "configuration.h" #if HAS_SCREEN #include "FSCommon.h" #include "MessageStore.h" #include "NodeDB.h" #include "SPILock.h" #include "SafeFile.h" #include "gps/RTC.h" #include "graphics/draw/MessageRenderer.h" #include // memcpy #ifndef MESSAGE_TEXT_POOL_SIZE #define MESSAGE_TEXT_POOL_SIZE (MAX_MESSAGES_SAVED * MAX_MESSAGE_SIZE) #endif // Default autosave interval 2 hours, override per device later with -DMESSAGE_AUTOSAVE_INTERVAL_SEC=300 (etc) #ifndef MESSAGE_AUTOSAVE_INTERVAL_SEC #define MESSAGE_AUTOSAVE_INTERVAL_SEC (2 * 60 * 60) #endif // Global message text pool and state static char *g_messagePool = nullptr; static size_t g_poolWritePos = 0; // Reset pool (called on boot or clear) static inline void resetMessagePool() { if (!g_messagePool) { g_messagePool = static_cast(malloc(MESSAGE_TEXT_POOL_SIZE)); if (!g_messagePool) { LOG_ERROR("MessageStore: Failed to allocate %d bytes for message pool", MESSAGE_TEXT_POOL_SIZE); return; } } g_poolWritePos = 0; memset(g_messagePool, 0, MESSAGE_TEXT_POOL_SIZE); } // Allocate text in pool and return offset // If not enough space remains, wrap around (ring buffer style) static inline uint16_t storeTextInPool(const char *src, size_t len) { if (len >= MAX_MESSAGE_SIZE) len = MAX_MESSAGE_SIZE - 1; // Wrap pool if out of space if (g_poolWritePos + len + 1 >= MESSAGE_TEXT_POOL_SIZE) { g_poolWritePos = 0; } uint16_t offset = g_poolWritePos; memcpy(&g_messagePool[g_poolWritePos], src, len); g_messagePool[g_poolWritePos + len] = '\0'; g_poolWritePos += (len + 1); return offset; } // Retrieve a const pointer to message text by offset static inline const char *getTextFromPool(uint16_t offset) { if (!g_messagePool || offset >= MESSAGE_TEXT_POOL_SIZE) return ""; return &g_messagePool[offset]; } // Helper: assign a timestamp (RTC if available, else boot-relative) static inline void assignTimestamp(StoredMessage &sm) { uint32_t nowSecs = getValidTime(RTCQuality::RTCQualityDevice, true); if (nowSecs) { sm.timestamp = nowSecs; sm.isBootRelative = false; } else { sm.timestamp = millis() / 1000; sm.isBootRelative = true; } } // Generic push with cap (used by live + persisted queues) template static inline void pushWithLimit(std::deque &queue, const T &msg) { if (queue.size() >= MAX_MESSAGES_SAVED) queue.pop_front(); queue.push_back(msg); } template static inline void pushWithLimit(std::deque &queue, T &&msg) { if (queue.size() >= MAX_MESSAGES_SAVED) queue.pop_front(); queue.emplace_back(std::move(msg)); } MessageStore::MessageStore(const std::string &label) { filename = "/Messages_" + label + ".msgs"; resetMessagePool(); // initialize text pool on boot } // Live message handling (RAM only) void MessageStore::addLiveMessage(StoredMessage &&msg) { pushWithLimit(liveMessages, std::move(msg)); } void MessageStore::addLiveMessage(const StoredMessage &msg) { pushWithLimit(liveMessages, msg); } #if ENABLE_MESSAGE_PERSISTENCE static bool g_messageStoreHasUnsavedChanges = false; static uint32_t g_lastAutoSaveMs = 0; // last time we actually saved static inline uint32_t autosaveIntervalMs() { uint32_t sec = (uint32_t)MESSAGE_AUTOSAVE_INTERVAL_SEC; if (sec < 60) sec = 60; return sec * 1000UL; } static inline bool reachedMs(uint32_t now, uint32_t target) { return (int32_t)(now - target) >= 0; } // Mark new messages in RAM that need to be saved later static inline void markMessageStoreUnsaved() { g_messageStoreHasUnsavedChanges = true; if (g_lastAutoSaveMs == 0) { g_lastAutoSaveMs = millis(); } } // Called periodically from the main loop in main.cpp static inline void autosaveTick(MessageStore *store) { if (!store) return; uint32_t now = millis(); if (g_lastAutoSaveMs == 0) { g_lastAutoSaveMs = now; return; } if (!reachedMs(now, g_lastAutoSaveMs + autosaveIntervalMs())) return; // Autosave interval reached, only save if there are unsaved messages. if (g_messageStoreHasUnsavedChanges) { LOG_INFO("Autosaving MessageStore to flash"); store->saveToFlash(); } else { LOG_INFO("Autosave skipped, no changes to save"); g_lastAutoSaveMs = now; } } #endif // Add from incoming/outgoing packet const StoredMessage &MessageStore::addFromPacket(const meshtastic_MeshPacket &packet) { StoredMessage sm; assignTimestamp(sm); sm.channelIndex = packet.channel; const char *payload = reinterpret_cast(packet.decoded.payload.bytes); size_t len = strnlen(payload, MAX_MESSAGE_SIZE - 1); sm.textOffset = storeTextInPool(payload, len); sm.textLength = len; // Determine sender uint32_t localNode = nodeDB->getNodeNum(); sm.sender = (packet.from == 0) ? localNode : packet.from; sm.dest = packet.to; bool isDM = (sm.dest != 0 && sm.dest != NODENUM_BROADCAST); if (packet.from == 0) { sm.type = isDM ? MessageType::DM_TO_US : MessageType::BROADCAST; sm.ackStatus = AckStatus::NONE; } else { sm.type = isDM ? MessageType::DM_TO_US : MessageType::BROADCAST; sm.ackStatus = AckStatus::ACKED; } addLiveMessage(sm); #if ENABLE_MESSAGE_PERSISTENCE markMessageStoreUnsaved(); #endif return liveMessages.back(); } // Outgoing/manual message void MessageStore::addFromString(uint32_t sender, uint8_t channelIndex, const std::string &text) { StoredMessage sm; // Always use our local time (helper handles RTC vs boot time) assignTimestamp(sm); sm.sender = sender; sm.channelIndex = channelIndex; sm.textOffset = storeTextInPool(text.c_str(), text.size()); sm.textLength = text.size(); // Use the provided destination sm.dest = sender; sm.type = MessageType::DM_TO_US; // Outgoing messages always start with unknown ack status sm.ackStatus = AckStatus::NONE; addLiveMessage(sm); #if ENABLE_MESSAGE_PERSISTENCE markMessageStoreUnsaved(); #endif } #if ENABLE_MESSAGE_PERSISTENCE // Compact, fixed-size on-flash representation using offset + length struct __attribute__((packed)) StoredMessageRecord { uint32_t timestamp; uint32_t sender; uint8_t channelIndex; uint32_t dest; uint8_t isBootRelative; uint8_t ackStatus; // static_cast(AckStatus) uint8_t type; // static_cast(MessageType) uint16_t textLength; // message length char text[MAX_MESSAGE_SIZE]; // store actual text here }; // Serialize one StoredMessage to flash static inline void writeMessageRecord(SafeFile &f, const StoredMessage &m) { StoredMessageRecord rec = {}; rec.timestamp = m.timestamp; rec.sender = m.sender; rec.channelIndex = m.channelIndex; rec.dest = m.dest; rec.isBootRelative = m.isBootRelative; rec.ackStatus = static_cast(m.ackStatus); rec.type = static_cast(m.type); rec.textLength = m.textLength; // Copy the actual text into the record from RAM pool const char *txt = getTextFromPool(m.textOffset); strncpy(rec.text, txt, MAX_MESSAGE_SIZE - 1); rec.text[MAX_MESSAGE_SIZE - 1] = '\0'; f.write(reinterpret_cast(&rec), sizeof(rec)); } // Deserialize one StoredMessage from flash; returns false on short read static inline bool readMessageRecord(File &f, StoredMessage &m) { StoredMessageRecord rec = {}; if (f.readBytes(reinterpret_cast(&rec), sizeof(rec)) != sizeof(rec)) return false; m.timestamp = rec.timestamp; m.sender = rec.sender; m.channelIndex = rec.channelIndex; m.dest = rec.dest; m.isBootRelative = rec.isBootRelative; m.ackStatus = static_cast(rec.ackStatus); m.type = static_cast(rec.type); m.textLength = rec.textLength; // 💡 Re-store text into pool and update offset m.textLength = strnlen(rec.text, MAX_MESSAGE_SIZE - 1); m.textOffset = storeTextInPool(rec.text, m.textLength); return true; } void MessageStore::saveToFlash() { #ifdef FSCom // Ensure root exists spiLock->lock(); FSCom.mkdir("/"); spiLock->unlock(); SafeFile f(filename.c_str(), false); spiLock->lock(); uint8_t count = static_cast(liveMessages.size()); if (count > MAX_MESSAGES_SAVED) count = MAX_MESSAGES_SAVED; f.write(&count, 1); for (uint8_t i = 0; i < count; ++i) { writeMessageRecord(f, liveMessages[i]); } spiLock->unlock(); f.close(); #endif // Reset autosave state after any save g_messageStoreHasUnsavedChanges = false; g_lastAutoSaveMs = millis(); } void MessageStore::loadFromFlash() { std::deque().swap(liveMessages); resetMessagePool(); // reset pool when loading #ifdef FSCom concurrency::LockGuard guard(spiLock); if (!FSCom.exists(filename.c_str())) return; auto f = FSCom.open(filename.c_str(), FILE_O_READ); if (!f) return; uint8_t count = 0; f.readBytes(reinterpret_cast(&count), 1); if (count > MAX_MESSAGES_SAVED) count = MAX_MESSAGES_SAVED; for (uint8_t i = 0; i < count; ++i) { StoredMessage m; if (!readMessageRecord(f, m)) break; liveMessages.push_back(m); } f.close(); #endif // Loading messages does not trigger an autosave g_messageStoreHasUnsavedChanges = false; g_lastAutoSaveMs = millis(); } #else // If persistence is disabled, these functions become no-ops void MessageStore::saveToFlash() {} void MessageStore::loadFromFlash() {} #endif // Clear all messages (RAM + persisted queue) void MessageStore::clearAllMessages() { std::deque().swap(liveMessages); resetMessagePool(); #ifdef FSCom SafeFile f(filename.c_str(), false); uint8_t count = 0; f.write(&count, 1); // write "0 messages" f.close(); #endif #if ENABLE_MESSAGE_PERSISTENCE g_messageStoreHasUnsavedChanges = false; g_lastAutoSaveMs = millis(); #endif } // Internal helper: erase first or last message matching a predicate template static void eraseIf(std::deque &deque, Predicate pred, bool fromBack = false) { if (fromBack) { // Iterate from the back and erase all matches from the end for (auto it = deque.rbegin(); it != deque.rend();) { if (pred(*it)) { it = std::deque::reverse_iterator(deque.erase(std::next(it).base())); } else { ++it; } } } else { // Manual forward search to erase all matches for (auto it = deque.begin(); it != deque.end();) { if (pred(*it)) { it = deque.erase(it); } else { ++it; } } } } // Delete oldest message (RAM + persisted queue) void MessageStore::deleteOldestMessage() { eraseIf(liveMessages, [](StoredMessage &) { return true; }); saveToFlash(); } // Delete oldest message in a specific channel void MessageStore::deleteOldestMessageInChannel(uint8_t channel) { auto pred = [channel](const StoredMessage &m) { return m.type == MessageType::BROADCAST && m.channelIndex == channel; }; eraseIf(liveMessages, pred); saveToFlash(); } void MessageStore::deleteAllMessagesInChannel(uint8_t channel) { auto pred = [channel](const StoredMessage &m) { return m.type == MessageType::BROADCAST && m.channelIndex == channel; }; eraseIf(liveMessages, pred, false /* delete ALL, not just first */); saveToFlash(); } void MessageStore::deleteAllMessagesWithPeer(uint32_t peer) { uint32_t local = nodeDB->getNodeNum(); auto pred = [&](const StoredMessage &m) { if (m.type != MessageType::DM_TO_US) return false; uint32_t other = (m.sender == local) ? m.dest : m.sender; return other == peer; }; eraseIf(liveMessages, pred, false); saveToFlash(); } // Delete oldest message in a direct chat with a node void MessageStore::deleteOldestMessageWithPeer(uint32_t peer) { auto pred = [peer](const StoredMessage &m) { if (m.type != MessageType::DM_TO_US) return false; uint32_t other = (m.sender == nodeDB->getNodeNum()) ? m.dest : m.sender; return other == peer; }; eraseIf(liveMessages, pred); saveToFlash(); } std::deque MessageStore::getChannelMessages(uint8_t channel) const { std::deque result; for (const auto &m : liveMessages) { if (m.type == MessageType::BROADCAST && m.channelIndex == channel) { result.push_back(m); } } return result; } std::deque MessageStore::getDirectMessages() const { std::deque result; for (const auto &m : liveMessages) { if (m.type == MessageType::DM_TO_US) { result.push_back(m); } } return result; } // Upgrade boot-relative timestamps once RTC is valid // Only same-boot boot-relative messages are healed. // Persisted boot-relative messages from old boots stay ??? forever. void MessageStore::upgradeBootRelativeTimestamps() { uint32_t nowSecs = getValidTime(RTCQuality::RTCQualityDevice, true); if (nowSecs == 0) return; // Still no valid RTC uint32_t bootNow = millis() / 1000; auto fix = [&](std::deque &dq) { for (auto &m : dq) { if (m.isBootRelative && m.timestamp <= bootNow) { uint32_t bootOffset = nowSecs - bootNow; m.timestamp += bootOffset; m.isBootRelative = false; } } }; fix(liveMessages); } const char *MessageStore::getText(const StoredMessage &msg) { // Wrapper around the internal helper return getTextFromPool(msg.textOffset); } uint16_t MessageStore::storeText(const char *src, size_t len) { // Wrapper around the internal helper return storeTextInPool(src, len); } #if ENABLE_MESSAGE_PERSISTENCE void messageStoreAutosaveTick() { // Called from the main loop to check autosave timing autosaveTick(&messageStore); } #endif // Global definition MessageStore messageStore("default"); #endif ================================================ FILE: src/MessageStore.h ================================================ #pragma once #if HAS_SCREEN // Disable debug logging entirely on release builds of HELTEC_MESH_SOLAR for space constraints #if defined(HELTEC_MESH_SOLAR) #define LOG_DEBUG(...) #endif // Enable or disable message persistence (flash storage) // Define -DENABLE_MESSAGE_PERSISTENCE=0 in build_flags to disable it entirely #ifndef ENABLE_MESSAGE_PERSISTENCE #define ENABLE_MESSAGE_PERSISTENCE 1 #endif #include "mesh/generated/meshtastic/mesh.pb.h" #include #include #include // How many messages are stored (RAM + flash). // Define -DMESSAGE_HISTORY_LIMIT=N in build_flags to control memory usage. #ifndef MESSAGE_HISTORY_LIMIT #define MESSAGE_HISTORY_LIMIT 20 #endif // Internal alias used everywhere in code – do NOT redefine elsewhere. #define MAX_MESSAGES_SAVED MESSAGE_HISTORY_LIMIT // Maximum text payload size per message in bytes. // This still defines the max message length, but we no longer reserve this space per message. #define MAX_MESSAGE_SIZE 220 // Total shared text pool size for all messages combined. // The text pool is RAM-only. Text is re-stored from flash into the pool on boot. #ifndef MESSAGE_TEXT_POOL_SIZE #define MESSAGE_TEXT_POOL_SIZE (MAX_MESSAGES_SAVED * MAX_MESSAGE_SIZE) #endif // Explicit message classification enum class MessageType : uint8_t { BROADCAST = 0, // broadcast message DM_TO_US = 1 // direct message addressed to this node }; // Delivery status for messages we sent enum class AckStatus : uint8_t { NONE = 0, // just sent, waiting (no symbol shown) ACKED = 1, // got a valid ACK from destination NACKED = 2, // explicitly failed TIMEOUT = 3, // no ACK after retry window RELAYED = 4 // got an ACK from relay, not destination }; struct StoredMessage { uint32_t timestamp; // When message was created (secs since boot or RTC) uint32_t sender; // NodeNum of sender uint8_t channelIndex; // Channel index used uint32_t dest; // Destination node (broadcast or direct) MessageType type; // Derived from dest (explicit classification) bool isBootRelative; // true = millis()/1000 fallback; false = epoch/RTC absolute AckStatus ackStatus; // Delivery status (only meaningful for our own sent messages) // Text storage metadata — rebuilt from flash at boot uint16_t textOffset; // Offset into global text pool (valid only after loadFromFlash()) uint16_t textLength; // Length of text in bytes // Default constructor initializes all fields safely StoredMessage() : timestamp(0), sender(0), channelIndex(0), dest(0xffffffff), type(MessageType::BROADCAST), isBootRelative(false), ackStatus(AckStatus::NONE), textOffset(0), textLength(0) { } }; class MessageStore { public: explicit MessageStore(const std::string &label); // Live RAM methods (always current, used by UI and runtime) void addLiveMessage(StoredMessage &&msg); void addLiveMessage(const StoredMessage &msg); // convenience overload const std::deque &getLiveMessages() const { return liveMessages; } // Add new messages from packets or manual input const StoredMessage &addFromPacket(const meshtastic_MeshPacket &mp); // Incoming/outgoing → RAM only void addFromString(uint32_t sender, uint8_t channelIndex, const std::string &text); // Manual add // Persistence methods (used only on boot/shutdown) void saveToFlash(); // Save messages to flash void loadFromFlash(); // Load messages from flash // Clear all messages (RAM + persisted queue + text pool) void clearAllMessages(); // Delete helpers void deleteOldestMessage(); // remove oldest from RAM (and flash on save) void deleteOldestMessageInChannel(uint8_t channel); void deleteOldestMessageWithPeer(uint32_t peer); void deleteAllMessagesInChannel(uint8_t channel); void deleteAllMessagesWithPeer(uint32_t peer); // Unified accessor (for UI code, defaults to RAM buffer) const std::deque &getMessages() const { return liveMessages; } // Helper filters for future use std::deque getChannelMessages(uint8_t channel) const; // Only broadcast messages on a channel std::deque getDirectMessages() const; // Only direct messages // Upgrade boot-relative timestamps once RTC is valid void upgradeBootRelativeTimestamps(); // Retrieve the C-string text for a stored message static const char *getText(const StoredMessage &msg); // Allocate text into pool (used by sender-side code) static uint16_t storeText(const char *src, size_t len); // Used when loading from flash to rebuild the text pool static uint16_t rebuildTextFromFlash(const char *src, size_t len); private: std::deque liveMessages; // Single in-RAM message buffer (also used for persistence) std::string filename; // Flash filename for persistence }; #if ENABLE_MESSAGE_PERSISTENCE // Called periodically from main loop to trigger time based autosave void messageStoreAutosaveTick(); #endif // Global instance (defined in MessageStore.cpp) extern MessageStore messageStore; #endif ================================================ FILE: src/NodeStatus.h ================================================ #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); uint16_t numOnline = 0; uint16_t numTotal = 0; uint16_t lastNumTotal = 0; public: bool forceUpdate = false; NodeStatus() { statusType = STATUS_TYPE_NODE; } NodeStatus(uint16_t numOnline, uint16_t numTotal, bool forceUpdate = false) : Status() { this->forceUpdate = forceUpdate; this->numOnline = numOnline; this->numTotal = numTotal; } NodeStatus(const NodeStatus &); NodeStatus &operator=(const NodeStatus &); void observe(Observable *source) { statusObserver.observe(source); } uint16_t getNumOnline() const { return numOnline; } uint16_t getNumTotal() const { return numTotal; } uint16_t getLastNumTotal() const { return lastNumTotal; } bool matches(const NodeStatus *newStatus) const { return (newStatus->getNumOnline() != numOnline || newStatus->getNumTotal() != numTotal); } int updateStatus(const NodeStatus *newStatus) { // Only update the status if values have actually changed lastNumTotal = numTotal; bool isDirty; { isDirty = matches(newStatus); initialized = true; numOnline = newStatus->getNumOnline(); numTotal = newStatus->getNumTotal(); } if (isDirty || newStatus->forceUpdate) { LOG_DEBUG("Node status update: %u online, %u total", numOnline, numTotal); onNewStatus.notifyObservers(this); } return 0; } }; } // namespace meshtastic extern meshtastic::NodeStatus *nodeStatus; ================================================ FILE: src/Observer.cpp ================================================ #include "Observer.h" #include "configuration.h" ================================================ FILE: src/Observer.h ================================================ #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); } ================================================ FILE: src/Power.cpp ================================================ /** * @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 "MessageStore.h" #include "NodeDB.h" #include "PowerFSM.h" #include "Throttle.h" #include "buzz/buzz.h" #include "configuration.h" #include "main.h" #include "meshUtils.h" #include "power/PowerHAL.h" #include "sleep.h" #if defined(ARCH_PORTDUINO) #include "api/WiFiServerAPI.h" #include "input/LinuxInputImpl.h" #endif // 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 #if HAS_ETHERNET && defined(USE_WS5500) #include #define ETH ETH2 #endif // HAS_ETHERNET #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 #if __has_include() INA219Sensor ina219Sensor; #else NullSensor ina219Sensor; #endif #if __has_include() INA226Sensor ina226Sensor; #else NullSensor ina226Sensor; #endif #if __has_include() INA260Sensor ina260Sensor; #else NullSensor ina260Sensor; #endif #if __has_include() INA3221Sensor ina3221Sensor; #else NullSensor ina3221Sensor; #endif #endif #if !MESHTASTIC_EXCLUDE_I2C #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) #if __has_include() MAX17048Sensor max17048Sensor; #else NullSensor max17048Sensor; #endif #endif #endif #if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && HAS_RAKPROT RAK9154Sensor rak9154Sensor; #endif #ifdef HAS_PPM // note: XPOWERS_CHIP_XXX must be defined in variant.h #include XPowersPPM *PPM = NULL; #endif #ifdef HAS_BQ27220 #include "bq27220.h" #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; // NRF52 has AREF_VOLTAGE defined in architecture.h but // make sure it's included. If something is wrong with NRF52 // definition - compilation will fail on missing definition #if !defined(AREF_VOLTAGE) && !defined(ARCH_NRF52) #define AREF_VOLTAGE 3.3 #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 void battery_adcEnable() { #ifdef ADC_CTRL // enable adc voltage divider when we need to read #ifdef ADC_USE_PULLUP pinMode(ADC_CTRL, INPUT_PULLUP); #else #ifdef HELTEC_V3 pinMode(ADC_CTRL, INPUT); uint8_t adc_ctl_enable_value = !(digitalRead(ADC_CTRL)); pinMode(ADC_CTRL, OUTPUT); digitalWrite(ADC_CTRL, adc_ctl_enable_value); #else pinMode(ADC_CTRL, OUTPUT); digitalWrite(ADC_CTRL, ADC_CTRL_ENABLED); #endif #endif delay(10); #endif } static void battery_adcDisable() { #ifdef ADC_CTRL // disable adc voltage divider when we need to read #ifdef ADC_USE_PULLUP pinMode(ADC_CTRL, INPUT_PULLDOWN); #else #ifdef HELTEC_V3 pinMode(ADC_CTRL, ANALOG); #else digitalWrite(ADC_CTRL, !ADC_CTRL_ENABLED); #endif #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(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; } } #if defined(BATTERY_CHARGING_INV) // bit of trickery to show 99% up until the charge finishes if (!digitalRead(BATTERY_CHARGING_INV) && battery_SOC > 99) battery_SOC = 99; #endif return clamp((int)(battery_SOC), 0, 100); } /** * The raw voltage of the batteryin millivolts or NAN if unknown */ virtual uint16_t getBattVoltage() override { #if HAS_TELEMETRY && defined(HAS_RAKPROT) && !defined(HAS_PMU) && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR if (hasRAK()) { return getRAKVoltage(); } #endif #if HAS_TELEMETRY && !defined(HAS_PMU) && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR if (hasINA()) { 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 (!initial_read_done || !Throttle::isWithinTimespanMs(last_read_time_ms, min_read_interval)) { last_read_time_ms = millis(); uint32_t raw = 0; float scaled = 0; battery_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 battery_adcDisable(); if (!initial_read_done) { // Flush the smoothing filter with an ADC reading, if the reading is // plausibly correct if (scaled > last_read_value) last_read_value = scaled; initial_read_done = true; } else { // Already initialized - filter this reading last_read_value += (scaled - last_read_value) * 0.5; // Virtual LPF } // 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; } #elif defined(ADC_V) virtual bool isBatteryConnect() override { int lastReading = digitalRead(ADC_V); // 判断值是否变化 for (int i = 2; i < 500; i++) { int reading = digitalRead(ADC_V); if (reading != lastReading) { return false; // 有变化,USB供电, 没接电池 } } return true; } #else virtual bool isBatteryConnect() override { return getBatteryPercent() != -1; } #endif /// If we see a battery voltage higher than physics allows - assume charger is /// pumping in power On some boards we don't have the power management chip /// (like AXPxxxx) so we use EXT_PWR_DETECT GPIO pin to detect external power /// source virtual bool isVbusIn() override { #ifdef EXT_PWR_DETECT #if defined(HELTEC_CAPSULE_SENSOR_V3) || defined(HELTEC_SENSOR_HUB) // if external powered that pin will be pulled down if (digitalRead(EXT_PWR_DETECT) == LOW) { return true; } // if it's not LOW - check the battery #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 // If we have an EXT_PWR_DETECT pin and it indicates no external power, believe it. return false; // technically speaking this should work for all(?) NRF52 boards // but needs testing across multiple devices. NRF52 USB would not even work if // VBUS was not properly connected and detected by the CPU #elif defined(MUZI_BASE) || defined(PROMICRO_DIY_TCXO) return powerHAL_isVBUSConnected(); #endif return getBattVoltage() > chargingVolt; } /// Assume charging if we have a battery and external power is connected. /// we can't be smart enough to say 'full'? virtual bool isCharging() override { #if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && defined(HAS_RAKPROT) && !defined(HAS_PMU) if (hasRAK()) { return (rak9154Sensor.isCharging()) ? OptTrue : OptFalse; } #endif #if defined(ELECROW_ThinkNode_M6) return digitalRead(EXT_CHRG_DETECT) == ext_chrg_detect_value || isVbusIn(); #elif EXT_CHRG_DETECT return digitalRead(EXT_CHRG_DETECT) == ext_chrg_detect_value; #elif defined(BATTERY_CHARGING_INV) return !digitalRead(BATTERY_CHARGING_INV); #else #if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && !defined(DISABLE_INA_CHARGING_DETECTION) if (hasINA()) { // get current flow from INA sensor - negative value means power flowing // into the battery default assuming BATTERY+ <--> INA_VIN+ <--> SHUNT // RESISTOR <--> INA_VIN- <--> LOAD LOG_DEBUG("Using INA on I2C addr 0x%x for charging detection", config.power.device_battery_ina_address); #if defined(INA_CHARGING_DETECTION_INVERT) return getINACurrent() > 0; #else return getINACurrent() < 0; #endif } return isBatteryConnect() && isVbusIn(); #endif #endif // by default, we check the battery voltage only return isVbusIn(); } private: /// If we see a battery voltage higher than physics allows - assume charger is /// pumping in power /// For heltecs with no battery connected, the measured voltage is 2204, so // need to be higher than that, in this case is 2500mV (3000-500) const uint16_t OCV[NUM_OCV_POINTS] = {OCV_ARRAY}; const float chargingVolt = (OCV[0] + 10) * NUM_CELLS; const float noBatVolt = (OCV[NUM_OCV_POINTS - 1] - 500) * NUM_CELLS; // Start value from minimum voltage for the filter to not start from 0 // that could trigger some events. // This value is over-written by the first ADC reading, it the voltage seems // reasonable. bool initial_read_done = false; float last_read_value = (OCV[NUM_OCV_POINTS - 1] * NUM_CELLS); uint32_t last_read_time_ms = 0; #if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && defined(HAS_RAKPROT) uint16_t getRAKVoltage() { return rak9154Sensor.getBusVoltageMv(); } bool hasRAK() { if (!rak9154Sensor.isInitialized()) return rak9154Sensor.runOnce() > 0; return rak9154Sensor.isRunning(); } #endif #if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR uint16_t getINAVoltage() { if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA219].first == config.power.device_battery_ina_address) { return ina219Sensor.getBusVoltageMv(); } else if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA226].first == config.power.device_battery_ina_address) { return ina226Sensor.getBusVoltageMv(); } else if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA260].first == config.power.device_battery_ina_address) { return ina260Sensor.getBusVoltageMv(); } else if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA3221].first == config.power.device_battery_ina_address) { return ina3221Sensor.getBusVoltageMv(); } return 0; } int16_t getINACurrent() { if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA219].first == config.power.device_battery_ina_address) { return ina219Sensor.getCurrentMa(); } else if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA226].first == config.power.device_battery_ina_address) { return ina226Sensor.getCurrentMa(); } else if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA3221].first == config.power.device_battery_ina_address) { return ina3221Sensor.getCurrentMa(); } return 0; } bool hasINA() { if (!config.power.device_battery_ina_address) { return false; } if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA219].first == config.power.device_battery_ina_address) { if (!ina219Sensor.isInitialized()) return ina219Sensor.runOnce() > 0; return ina219Sensor.isRunning(); } else if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA226].first == config.power.device_battery_ina_address) { if (!ina226Sensor.isInitialized()) return ina226Sensor.runOnce() > 0; return ina226Sensor.isRunning(); } else if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA260].first == config.power.device_battery_ina_address) { if (!ina260Sensor.isInitialized()) return ina260Sensor.runOnce() > 0; return ina260Sensor.isRunning(); } else if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA3221].first == config.power.device_battery_ina_address) { if (!ina3221Sensor.isInitialized()) return ina3221Sensor.runOnce() > 0; return ina3221Sensor.isRunning(); } return false; } #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 #if defined(HELTEC_CAPSULE_SENSOR_V3) || defined(HELTEC_SENSOR_HUB) 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("Use 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("ADC config based on Two Point values stored in eFuse"); } else if (val_type == ESP_ADC_CAL_VAL_EFUSE_VREF) { LOG_INFO("ADC config based on reference voltage stored in eFuse"); } #ifdef CONFIG_IDF_TARGET_ESP32S3 // ESP32S3 else if (val_type == ESP_ADC_CAL_VAL_EFUSE_TP_FIT) { LOG_INFO("ADC config based on Two Point values and fitting curve " "coefficients stored in eFuse"); } #endif else { LOG_INFO("ADC config based on default reference voltage"); } #endif // ARCH_ESP32 // NRF52 ADC init moved to powerHAL_init in nrf52 platform #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() { bool found = false; if (axpChipInit()) { found = true; } else if (cw2015Init()) { found = true; } else if (max17048Init()) { found = true; } else if (lipoChargerInit()) { found = true; } else if (serialBatteryInit()) { found = true; } else if (meshSolarInit()) { found = true; } else if (analogInit()) { found = true; } else { #ifdef NRF_APM found = true; #endif } #ifdef EXT_PWR_DETECT attachInterrupt( EXT_PWR_DETECT, []() { power->setIntervalFromNow(0); runASAP = true; }, CHANGE); #endif #ifdef BATTERY_CHARGING_INV attachInterrupt( BATTERY_CHARGING_INV, []() { power->setIntervalFromNow(0); runASAP = true; }, CHANGE); #endif #ifdef EXT_CHRG_DETECT attachInterrupt( EXT_CHRG_DETECT, []() { power->setIntervalFromNow(0); runASAP = true; BaseType_t higherWake = 0; }, CHANGE); #endif enabled = found; low_voltage_counter = 0; return found; } void Power::powerCommandsCheck() { if (rebootAtMsec && millis() > rebootAtMsec) { LOG_INFO("Rebooting"); reboot(); } if (shutdownAtMsec && millis() > shutdownAtMsec) { shutdownAtMsec = 0; shutdown(); } } void Power::reboot() { notifyReboot.notifyObservers(NULL); #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; screen = nullptr; } LOG_DEBUG("final reboot!"); ::reboot(); #elif defined(ARCH_STM32WL) HAL_NVIC_SystemReset(); #else rebootAtMsec = -1; LOG_WARN("FIXME implement reboot for this platform. Note that some settings " "require a restart to be applied"); #endif } void Power::shutdown() { #if HAS_SCREEN if (screen) { #ifdef T_DECK_PRO screen->showSimpleBanner("Device is powered off.\nConnect USB to start!", 0); // T-Deck Pro has no power button #elif defined(USE_EINK) screen->showSimpleBanner("Shutting Down...", 2250); // dismiss after 3 seconds to avoid the // banner on the sleep screen #else screen->showSimpleBanner("Shutting Down...", 0); // stays on screen #endif } #endif #if !defined(ARCH_STM32WL) playShutdownMelody(); #endif nodeDB->saveToDisk(); #if HAS_SCREEN messageStore.saveToFlash(); #endif #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 #ifdef LED_NOTIFICATION ledOff(LED_NOTIFICATION); #endif doDeepSleep(DELAY_FOREVER, true, true); #elif defined(ARCH_PORTDUINO) exit(EXIT_SUCCESS); #else LOG_WARN("FIXME implement shutdown for this platform"); #endif } /// Reads power status to powerStatus singleton. // // TODO(girts): move this and other axp stuff to power.h/power.cpp. void Power::readPowerStatus() { int32_t batteryVoltageMv = -1; // Assume unknown int8_t batteryChargePercent = -1; OptionalBool usbPowered = OptUnknown; OptionalBool hasBattery = OptUnknown; // These must be static because NRF_APM // code doesn't run every time OptionalBool isChargingNow = OptUnknown; if (batteryLevel) { hasBattery = batteryLevel->isBatteryConnect() ? OptTrue : OptFalse; #ifndef NRF_APM usbPowered = batteryLevel->isVbusIn() ? OptTrue : OptFalse; isChargingNow = batteryLevel->isCharging() ? OptTrue : OptFalse; #endif if (hasBattery) { batteryVoltageMv = batteryLevel->getBattVoltage(); // If the AXP192 returns a valid battery percentage, use it if (batteryLevel->getBatteryPercent() >= 0) { batteryChargePercent = batteryLevel->getBatteryPercent(); } else { // If the AXP192 returns a percentage less than 0, the feature is either // not supported or there is an error In that case, we compute an // estimate of the charge percent based on open circuit voltage table // defined in power.h batteryChargePercent = clamp((int)(((batteryVoltageMv - (OCV[NUM_OCV_POINTS - 1] * NUM_CELLS)) * 1e2) / ((OCV[0] * NUM_CELLS) - (OCV[NUM_OCV_POINTS - 1] * NUM_CELLS))), 0, 100); } } } // FIXME: IMO we shouldn't be littering our code with all these ifdefs. Way // better instead to make a Nrf52IsUsbPowered subclass (which shares a // superclass with the BatteryLevel stuff) that just provides a few methods. But // in the interest of fixing this bug I'm going to follow current practice. #ifdef NRF_APM // Section of code detects USB power on the RAK4631 and updates // the power states. Takes 20 seconds or so to detect changes. nrfx_power_usb_state_t nrf_usb_state = nrfx_power_usbstatus_get(); // LOG_DEBUG("NRF Power %d", nrf_usb_state); // If changed to DISCONNECTED if (nrf_usb_state == NRFX_POWER_USB_STATE_DISCONNECTED) isChargingNow = usbPowered = OptFalse; // If changed to CONNECTED / READY else isChargingNow = usbPowered = OptTrue; #endif // Notify any status instances that are observing us const PowerStatus powerStatus2 = PowerStatus(hasBattery, usbPowered, isChargingNow, batteryVoltageMv, batteryChargePercent); if (millis() > lastLogTime + 50 * 1000) { LOG_DEBUG("Battery: usbPower=%d, isCharging=%d, batMv=%d, batPct=%d", powerStatus2.getHasUSB(), powerStatus2.getIsCharging(), powerStatus2.getBatteryVoltageMv(), powerStatus2.getBatteryChargePercent()); lastLogTime = millis(); } newStatus.notifyObservers(&powerStatus2); #ifdef DEBUG_HEAP if (lastheap != memGet.getFreeHeap()) { // Use stack-allocated buffer to avoid heap allocations in monitoring code char threadlist[256] = "Threads running:"; int threadlistLen = strlen(threadlist); int running = 0; for (int i = 0; i < MAX_THREADS; i++) { auto thread = concurrency::mainController.get(i); if ((thread != nullptr) && (thread->enabled)) { // Use snprintf to safely append to stack buffer without heap allocation int remaining = sizeof(threadlist) - threadlistLen - 1; if (remaining > 0) { int written = snprintf(threadlist + threadlistLen, remaining, " %s", thread->ThreadName.c_str()); if (written > 0 && written < remaining) { threadlistLen += written; } } running++; } } LOG_HEAP(threadlist); LOG_HEAP("Heap status: %d/%d bytes free (%d), running %d/%d threads", memGet.getFreeHeap(), memGet.getHeapSize(), memGet.getFreeHeap() - lastheap, running, concurrency::mainController.size(false)); lastheap = memGet.getFreeHeap(); } #ifdef DEBUG_HEAP_MQTT if (mqtt) { // send MQTT-Packet with Heap-Size uint8_t dmac[6]; getMacAddr(dmac); // Get our hardware ID char mac[18]; sprintf(mac, "!%02x%02x%02x%02x", dmac[2], dmac[3], dmac[4], dmac[5]); auto newHeap = memGet.getFreeHeap(); // Use stack-allocated buffers to avoid heap allocations in monitoring code char heapTopic[128]; snprintf(heapTopic, sizeof(heapTopic), "%s/2/heap/%s", (*moduleConfig.mqtt.root ? moduleConfig.mqtt.root : "msh"), mac); char heapString[16]; snprintf(heapString, sizeof(heapString), "%u", newHeap); mqtt->pubSub.publish(heapTopic, heapString, false); auto wifiRSSI = WiFi.RSSI(); char wifiTopic[128]; snprintf(wifiTopic, sizeof(wifiTopic), "%s/2/wifi/%s", (*moduleConfig.mqtt.root ? moduleConfig.mqtt.root : "msh"), mac); char wifiString[16]; snprintf(wifiString, sizeof(wifiString), "%d", wifiRSSI); mqtt->pubSub.publish(wifiTopic, wifiString, false); } #endif #endif // If we have a battery at all and it is less than 0%, force deep sleep if we // have more than 10 low readings in a row. NOTE: min LiIon/LiPo voltage // is 2.0 to 2.5V, current OCV min is set to 3100 that is large enough. // if (batteryLevel && powerStatus2.getHasBattery() && !powerStatus2.getHasUSB()) { if (batteryLevel->getBattVoltage() < OCV[NUM_OCV_POINTS - 1]) { low_voltage_counter++; LOG_DEBUG("Low voltage counter: %d/10", low_voltage_counter); if (low_voltage_counter > 10) { LOG_INFO("Low voltage detected, trigger deep sleep"); powerFSM.trigger(EVENT_LOW_BATTERY); } } else { low_voltage_counter = 0; } } } 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"); if (screen) 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("No AXP2101 power management"); delete PMU; PMU = NULL; } else { LOG_INFO("AXP2101 PMU init succeeded"); } } if (!PMU) { PMU = new XPowersAXP192(*w); if (!PMU->init()) { LOG_WARN("No AXP192 power management"); delete PMU; PMU = NULL; } else { LOG_INFO("AXP192 PMU init succeeded"); } } if (!PMU) { /* * 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 (T-Beam S3) / gnns (T-Watch S3 Plus) power channel PMU->setPowerChannelVoltage(XPOWERS_BLDO1, 3300); #ifndef T_WATCH_S3 PMU->enablePowerOutput(XPOWERS_BLDO1); #else // 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. // // Unfortunately 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 && __has_include() /** * Wrapper class for an I2C MAX17048 Lipo battery sensor. */ class MAX17048BatteryLevel : 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(); } }; MAX17048BatteryLevel max17048Level; /** * Init the Lipo battery level sensor */ bool Power::max17048Init() { bool result = max17048Level.runOnce(); LOG_DEBUG("Power::max17048Init lipo sensor is %s", result ? "ready" : "not ready yet"); if (!result) return false; batteryLevel = &max17048Level; return true; } #else /** * The Lipo battery level sensor is unavailable - default to AnalogBatteryLevel */ bool Power::max17048Init() { return false; } #endif #if !MESHTASTIC_EXCLUDE_I2C && HAS_CW2015 class CW2015BatteryLevel : public AnalogBatteryLevel { public: /** * Battery state of charge, from 0 to 100 or -1 for unknown */ virtual int getBatteryPercent() override { int data = -1; Wire.beginTransmission(CW2015_ADDR); Wire.write(0x04); if (Wire.endTransmission() == 0) { if (Wire.requestFrom(CW2015_ADDR, (uint8_t)1)) { data = Wire.read(); } } return data; } /** * The raw voltage of the battery in millivolts, or NAN if unknown */ virtual uint16_t getBattVoltage() override { uint16_t mv = 0; Wire.beginTransmission(CW2015_ADDR); Wire.write(0x02); if (Wire.endTransmission() == 0) { if (Wire.requestFrom(CW2015_ADDR, (uint8_t)2)) { mv = Wire.read(); mv <<= 8; mv |= Wire.read(); // Voltage is read in 305uV units, convert to mV mv = mv * 305 / 1000; } } return mv; } }; CW2015BatteryLevel cw2015Level; /** * Init the CW2015 battery level sensor */ bool Power::cw2015Init() { Wire.beginTransmission(CW2015_ADDR); uint8_t getInfo[] = {0x0a, 0x00}; Wire.write(getInfo, 2); Wire.endTransmission(); delay(10); Wire.beginTransmission(CW2015_ADDR); Wire.write(0x00); bool result = false; if (Wire.endTransmission() == 0) { if (Wire.requestFrom(CW2015_ADDR, (uint8_t)1)) { uint8_t data = Wire.read(); LOG_DEBUG("CW2015 init read data: 0x%x", data); if (data == 0x73) { result = true; batteryLevel = &cw2015Level; } } } return result; } #else /** * The CW2015 battery level sensor is unavailable - default to AnalogBatteryLevel */ bool Power::cw2015Init() { return false; } #endif #if defined(HAS_PPM) && HAS_PPM /** * Adapter class for BQ25896/BQ27220 Lipo battery charger. */ class LipoCharger : public HasBatteryLevel { private: BQ27220 *bq = nullptr; public: /** * Init the I2C BQ25896 Lipo battery charger */ bool runOnce() { if (PPM == nullptr) { PPM = new XPowersPPM; bool result = PPM->init(Wire, I2C_SDA, I2C_SCL, BQ25896_ADDR); if (result) { LOG_INFO("PPM BQ25896 init succeeded"); // Set the minimum operating voltage. Below this voltage, the PPM will // protect PPM->setSysPowerDownVoltage(3100); // Set input current limit, default is 500mA // PPM->setInputCurrentLimit(800); // Disable current limit pin // PPM->disableCurrentLimitPin(); // Set the charging target voltage, Range:3840 ~ 4608mV ,step:16 mV PPM->setChargeTargetVoltage(4288); // Set the precharge current , Range: 64mA ~ 1024mA ,step:64mA // PPM->setPrechargeCurr(64); // The premise is that limit pin is disabled, or it will // only follow the maximum charging current set by limit pin. // Set the charging current , Range:0~5056mA ,step:64mA PPM->setChargerConstantCurr(1024); // To obtain voltage data, the ADC must be enabled first PPM->enableMeasure(); // Turn on charging function // If there is no battery connected, do not turn on the charging // function PPM->enableCharge(); } else { LOG_WARN("PPM BQ25896 init failed"); delete PPM; PPM = nullptr; return false; } } if (bq == nullptr) { bq = new BQ27220; bq->setDefaultCapacity(BQ27220_DESIGN_CAPACITY); bool result = bq->init(); if (result) { LOG_DEBUG("BQ27220 design capacity: %d", bq->getDesignCapacity()); LOG_DEBUG("BQ27220 fullCharge capacity: %d", bq->getFullChargeCapacity()); LOG_DEBUG("BQ27220 remaining capacity: %d", bq->getRemainingCapacity()); return true; } else { LOG_WARN("BQ27220 init failed"); delete bq; bq = nullptr; return false; } } return false; } /** * Battery state of charge, from 0 to 100 or -1 for unknown */ virtual int getBatteryPercent() override { return -1; // return bq->getChargePercent(); // don't use BQ27220 for battery percent, // it is not calibrated } /** * The raw voltage of the battery in millivolts, or NAN if unknown */ virtual uint16_t getBattVoltage() override { return bq->getVoltage(); } /** * return true if there is a battery installed in this unit */ virtual bool isBatteryConnect() override { return PPM->getBattVoltage() > 0; } /** * return true if there is an external power source detected */ virtual bool isVbusIn() override { return PPM->isVbusIn(); } /** * return true if the battery is currently charging */ virtual bool isCharging() override { bool isCharging = PPM->isCharging(); if (isCharging) { LOG_DEBUG("BQ27220 time to full charge: %d min", bq->getTimeToFull()); } else { if (!PPM->isVbusIn()) { LOG_DEBUG("BQ27220 time to empty: %d min (%d mAh)", bq->getTimeToEmpty(), bq->getRemainingCapacity()); } } return isCharging; } }; LipoCharger lipoCharger; /** * Init the Lipo battery charger */ bool Power::lipoChargerInit() { bool result = lipoCharger.runOnce(); LOG_DEBUG("Power::lipoChargerInit lipo sensor is %s", result ? "ready" : "not ready yet"); if (!result) return false; batteryLevel = &lipoCharger; return true; } #else /** * The Lipo battery level sensor is unavailable - default to AnalogBatteryLevel */ bool Power::lipoChargerInit() { return false; } #endif #ifdef HELTEC_MESH_SOLAR #include "meshSolarApp.h" /** * meshSolar class for an SMBUS battery sensor. */ class meshSolarBatteryLevel : public HasBatteryLevel { public: /** * Init the I2C meshSolar battery level sensor */ bool runOnce() { meshSolarStart(); return true; } /** * Battery state of charge, from 0 to 100 or -1 for unknown */ virtual int getBatteryPercent() override { return meshSolarGetBatteryPercent(); } /** * The raw voltage of the battery in millivolts, or NAN if unknown */ virtual uint16_t getBattVoltage() override { return meshSolarGetBattVoltage(); } /** * return true if there is a battery installed in this unit */ virtual bool isBatteryConnect() override { return meshSolarIsBatteryConnect(); } /** * return true if there is an external power source detected */ virtual bool isVbusIn() override { return meshSolarIsVbusIn(); } /** * return true if the battery is currently charging */ virtual bool isCharging() override { return meshSolarIsCharging(); } }; meshSolarBatteryLevel meshSolarLevel; /** * Init the meshSolar battery level sensor */ bool Power::meshSolarInit() { bool result = meshSolarLevel.runOnce(); LOG_DEBUG("Power::meshSolarInit mesh solar sensor is %s", result ? "ready" : "not ready yet"); if (!result) return false; batteryLevel = &meshSolarLevel; return true; } #else /** * The meshSolar battery level sensor is unavailable - default to * AnalogBatteryLevel */ bool Power::meshSolarInit() { return false; } #endif #ifdef HAS_SERIAL_BATTERY_LEVEL #include /** * SerialBatteryLevel class for pulling battery information from a secondary MCU over serial. */ class SerialBatteryLevel : public HasBatteryLevel { public: /** * Init the I2C meshSolar battery level sensor */ bool runOnce() { BatterySerial.begin(4800); return true; } /** * Battery state of charge, from 0 to 100 or -1 for unknown */ virtual int getBatteryPercent() override { return v_percent; } /** * The raw voltage of the battery in millivolts, or NAN if unknown */ virtual uint16_t getBattVoltage() override { return voltage * 1000; } /** * return true if there is a battery installed in this unit */ virtual bool isBatteryConnect() override { // definitely need to gobble up more bytes at once if (BatterySerial.available() > 5) { // LOG_WARN("SerialBatteryLevel: %u bytes available", BatterySerial.available()); while (BatterySerial.available() > 11) { BatterySerial.read(); // flush old data } // LOG_WARN("SerialBatteryLevel: %u bytes now available", BatterySerial.available()); int tries = 0; while (BatterySerial.read() != 0xFE) { tries++; // wait for start byte if (tries > 10) { LOG_WARN("SerialBatteryLevel: no start byte found"); return 1; } } Data[1] = BatterySerial.read(); Data[2] = BatterySerial.read(); Data[3] = BatterySerial.read(); Data[4] = BatterySerial.read(); Data[5] = BatterySerial.read(); if (Data[5] != 0xFD) { LOG_WARN("SerialBatteryLevel: invalid end byte %02x", Data[5]); return true; } v_percent = Data[1]; voltage = Data[2] + (((float)Data[3]) / 100) + (((float)Data[4]) / 10000); voltage *= 2; // LOG_WARN("SerialBatteryLevel: received data %u, %f, %02x", v_percent, voltage, Data[5]); return true; } // This function runs first, so use it to grab the latest data from the secondary MCU return true; } /** * return true if there is an external power source detected */ virtual bool isVbusIn() override { #if defined(EXT_CHRG_DETECT) return digitalRead(EXT_CHRG_DETECT) == ext_chrg_detect_value; #endif return false; } virtual bool isCharging() override { #ifdef EXT_CHRG_DETECT return digitalRead(EXT_CHRG_DETECT) == ext_chrg_detect_value; #endif // by default, we check the battery voltage only return isVbusIn(); } private: SoftwareSerial BatterySerial = SoftwareSerial(SERIAL_BATTERY_RX, SERIAL_BATTERY_TX); uint8_t Data[6] = {0}; int v_percent = 0; float voltage = 0.0; }; SerialBatteryLevel serialBatteryLevel; /** * Init the serial battery level sensor */ bool Power::serialBatteryInit() { #ifdef EXT_PWR_DETECT pinMode(EXT_PWR_DETECT, INPUT); #endif #ifdef EXT_CHRG_DETECT pinMode(EXT_CHRG_DETECT, ext_chrg_detect_mode); #endif bool result = serialBatteryLevel.runOnce(); LOG_DEBUG("Power::serialBatteryInit serial battery sensor is %s", result ? "ready" : "not ready yet"); if (!result) return false; batteryLevel = &serialBatteryLevel; return true; } #else /** * If this device has no serial battery level sensor, don't try to use it. */ bool Power::serialBatteryInit() { return false; } #endif ================================================ FILE: src/PowerFSM.cpp ================================================ /** * @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 "MeshService.h" #include "NodeDB.h" #include "PowerMon.h" #include "configuration.h" #include "graphics/Screen.h" #include "main.h" #include "modules/StatusLEDModule.h" #include "sleep.h" #include "target_specific.h" #if HAS_WIFI && !defined(ARCH_PORTDUINO) || defined(MESHTASTIC_EXCLUDE_WIFI) #include "mesh/wifi/WiFiAPClient.h" #endif #ifndef SLEEP_TIME #define SLEEP_TIME 30 #endif #if MESHTASTIC_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 || config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE) ? 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_POWERFSM("State: SDS"); // FIXME - make sure GPS and LORA radio are off first - because we want close to zero current draw doDeepSleep(Default::getConfiguredOrDefaultMs(config.power.sds_secs), false, false); } static void lowBattSDSEnter() { LOG_POWERFSM("State: Lower batt SDS"); doDeepSleep(Default::getConfiguredOrDefaultMs(config.power.sds_secs), false, true); } extern Power *power; static void shutdownEnter() { LOG_POWERFSM("State: SHUTDOWN"); shutdownAtMsec = millis(); } #include "error.h" static uint32_t secsSlept; static void lsEnter() { LOG_POWERFSM("lsEnter begin, ls_secs=%u", config.power.ls_secs); if (screen) screen->setOn(false); secsSlept = 0; // How long have we been sleeping this time // 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); statusLEDModule->setPowerLED(false); 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 statusLEDModule->setPowerLED(true); wakeCause2 = doLightSleep(100); // leave led on for 1ms secsSlept += sleepTime; // LOG_INFO("Sleep, flash led!"); break; case ESP_SLEEP_WAKEUP_UART: // Not currently used (because uart triggers in hw have problems) powerFSM.trigger(EVENT_SERIAL_CONNECTED); break; 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! statusLEDModule->setPowerLED(false); LOG_INFO("Reached ls_secs, service loop()"); powerFSM.trigger(EVENT_WAKE_TIMER); } #endif } static void lsExit() { LOG_POWERFSM("State: lsExit"); } static void nbEnter() { LOG_POWERFSM("State: nbEnter"); if (screen) screen->setOn(false); #ifdef ARCH_ESP32 // Only ESP32 should turn off bluetooth setBluetoothEnable(false); #endif // FIXME - check if we already have packets for phone and immediately trigger EVENT_PACKETS_FOR_PHONE } static void darkEnter() { LOG_POWERFSM("State: darkEnter"); setBluetoothEnable(true); if (screen) screen->setOn(false); } static void serialEnter() { LOG_POWERFSM("State: serialEnter"); setBluetoothEnable(false); if (screen) { screen->setOn(true); } } static void serialExit() { LOG_POWERFSM("State: serialExit"); // Turn bluetooth back on when we leave serial stream API setBluetoothEnable(true); } static void powerEnter() { LOG_POWERFSM("State: powerEnter"); if (!isPowered()) { // If we got here, we are in the wrong state - we should be in powered, let that state handle things LOG_INFO("Loss of power in Powered"); powerFSM.trigger(EVENT_POWER_DISCONNECTED); } else { if (screen) screen->setOn(true); setBluetoothEnable(true); // within enter() the function getState() returns the state we came from } } static void powerIdle() { // LOG_POWERFSM("State: powerIdle"); // very chatty if (!isPowered()) { // If we got here, we are in the wrong state LOG_INFO("Loss of power in Powered"); powerFSM.trigger(EVENT_POWER_DISCONNECTED); } } static void powerExit() { LOG_POWERFSM("State: powerExit"); setBluetoothEnable(true); } static void onEnter() { LOG_POWERFSM("State: onEnter"); if (screen) screen->setOn(true); setBluetoothEnable(true); } static void onIdle() { LOG_POWERFSM("State: onIdle"); if (isPowered()) { // If we got here, we are in the wrong state - we should be in powered, let that state handle things powerFSM.trigger(EVENT_POWER_CONNECTED); } } static void bootEnter() { LOG_POWERFSM("State: bootEnter"); } State stateSHUTDOWN(shutdownEnter, NULL, NULL, "SHUTDOWN"); State stateSDS(sdsEnter, NULL, NULL, "SDS"); State stateLowBattSDS(lowBattSDSEnter, 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 || config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE) ? 1 : 0); bool hasPower = isPowered(); LOG_INFO("PowerFSM init, USB power=%d", hasPower ? 1 : 0); powerFSM.add_timed_transition(&stateBOOT, hasPower ? &statePOWER : &stateON, 3 * 1000, NULL, "boot timeout"); // 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, NULL, "Press"); powerFSM.add_transition(&stateON, &stateON, EVENT_PRESS, NULL, "Press"); // reenter On to restart our timers powerFSM.add_transition(&stateSERIAL, &stateSERIAL, EVENT_PRESS, NULL, "Press"); // Allow button to work while in serial API // Handle critically low power battery by forcing deep sleep powerFSM.add_transition(&stateBOOT, &stateLowBattSDS, EVENT_LOW_BATTERY, NULL, "LowBat"); powerFSM.add_transition(&stateLS, &stateLowBattSDS, EVENT_LOW_BATTERY, NULL, "LowBat"); powerFSM.add_transition(&stateNB, &stateLowBattSDS, EVENT_LOW_BATTERY, NULL, "LowBat"); powerFSM.add_transition(&stateDARK, &stateLowBattSDS, EVENT_LOW_BATTERY, NULL, "LowBat"); powerFSM.add_transition(&stateON, &stateLowBattSDS, EVENT_LOW_BATTERY, NULL, "LowBat"); powerFSM.add_transition(&stateSERIAL, &stateLowBattSDS, EVENT_LOW_BATTERY, NULL, "LowBat"); // Handle 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 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 or have Wifi enabled. Sleep will be initiated // through the modules #if HAS_WIFI && !defined(MESHTASTIC_EXCLUDE_WIFI) bool isTrackerOrSensor = config.device.role == meshtastic_Config_DeviceConfig_Role_TRACKER || config.device.role == meshtastic_Config_DeviceConfig_Role_TAK_TRACKER || config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR; if ((isRouter || config.power.is_power_saving) && !isWifiAvailable() && !isTrackerOrSensor) { powerFSM.add_timed_transition(&stateNB, &stateLS, Default::getConfiguredOrDefaultMs(config.power.min_wake_secs, default_min_wake_secs), NULL, "Min wake timeout"); // If ESP32 and using power-saving, timer mover from DARK to light-sleep // Also serves purpose of the old DARK to DARK transition(?) See https://github.com/meshtastic/firmware/issues/3517 powerFSM.add_timed_transition( &stateDARK, &stateLS, Default::getConfiguredOrDefaultMs(config.power.wait_bluetooth_secs, default_wait_bluetooth_secs), NULL, "Bluetooth timeout"); } else { // If ESP32, but not using power-saving, check periodically if config has drifted out of stateDark powerFSM.add_timed_transition(&stateDARK, &stateDARK, Default::getConfiguredOrDefaultMs(config.display.screen_on_secs, default_screen_on_secs), NULL, "Screen-on timeout"); } #endif // HAS_WIFI || !defined(MESHTASTIC_EXCLUDE_WIFI) #else // (not) ARCH_ESP32 // If not ESP32, light-sleep not used. Check periodically if config has drifted out of stateDark powerFSM.add_timed_transition(&stateDARK, &stateDARK, Default::getConfiguredOrDefaultMs(config.display.screen_on_secs, default_screen_on_secs), NULL, "Screen-on timeout"); #endif powerFSM.run_machine(); // run one iteration of the state machine, so we run our on enter tasks for the initial DARK state } #endif ================================================ FILE: src/PowerFSM.h ================================================ #pragma once #include "configuration.h" #ifdef PowerFSMDebug #define LOG_POWERFSM(...) LOG_DEBUG(__VA_ARGS__) #else #define LOG_POWERFSM(...) #endif // 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 // Now defunct: NodeDB has a big enough change that we think you should turn on the screen #define EVENT_CONTACT_FROM_PHONE 9 // the phone just talked to us over bluetooth #define EVENT_LOW_BATTERY 10 // Battery is critically low, go to sleep #define EVENT_SERIAL_CONNECTED 11 #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 MESHTASTIC_EXCLUDE_POWER_FSM class FakeFsm { public: void trigger(int event) { if (event == EVENT_SERIAL_CONNECTED) { serialConnected = true; } else if (event == EVENT_SERIAL_DISCONNECTED) { serialConnected = false; } }; bool getState() { return serialConnected; }; 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 ================================================ FILE: src/PowerFSMThread.h ================================================ #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 !MESHTASTIC_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 ================================================ FILE: src/PowerMon.cpp ================================================ #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(); } ================================================ FILE: src/PowerMon.h ================================================ #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(); ================================================ FILE: src/PowerStatus.h ================================================ #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; ================================================ FILE: src/RF95Configuration.h ================================================ // 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 ================================================ FILE: src/RedirectablePrint.cpp ================================================ #include "RedirectablePrint.h" #include "NodeDB.h" #include "RTC.h" #include "concurrency/OSThread.h" #include "configuration.h" #include "main.h" #include "memGet.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 meshtastic::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 = !portduino_config.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", 5); if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_INFO) == 0) Print::write("\u001b[32m", 5); if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_WARN) == 0) Print::write("\u001b[33m", 5); if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_ERROR) == 0) Print::write("\u001b[31m", 5); } len = Print::write(printBuf, len); if (color && logLevel != nullptr) { Print::write("\u001b[0m", 4); } return len; } void RedirectablePrint::log_to_serial(const char *logLevel, const char *format, va_list arg) { size_t r = 0; #ifdef ARCH_PORTDUINO bool color = !portduino_config.ascii_logs; #else bool color = true; #endif // include the header if (color) { if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_DEBUG) == 0) Print::write("\u001b[34m", 5); if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_INFO) == 0) Print::write("\u001b[32m", 5); if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_WARN) == 0) Print::write("\u001b[33m", 5); if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_ERROR) == 0) Print::write("\u001b[31m", 5); if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_TRACE) == 0) Print::write("\u001b[35m", 5); } 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("] "); } #ifdef DEBUG_HEAP // Add heap free space bytes prefix before every log message #ifdef ARCH_PORTDUINO ::printf("[heap %u] ", memGet.getFreeHeap()); #else printf("[heap %u] ", memGet.getFreeHeap()); #endif #endif // DEBUG_HEAP r += vprintf(logLevel, format, arg); } void RedirectablePrint::log_to_syslog(const char *logLevel, const char *format, va_list arg) { #if HAS_NETWORKING && !defined(ARCH_PORTDUINO) // if syslog is in use, collect the log messages and send them to syslog if (syslog.isEnabled()) { int ll = 0; switch (logLevel[0]) { case 'D': ll = SYSLOG_DEBUG; break; case 'I': ll = SYSLOG_INFO; break; case 'W': ll = SYSLOG_WARN; break; case 'E': ll = SYSLOG_ERR; break; case 'C': ll = SYSLOG_CRIT; break; default: ll = 0; } auto thread = concurrency::OSThread::currentThread; if (thread) { syslog.vlogf(ll, thread->ThreadName.c_str(), format, arg); } else { syslog.vlogf(ll, format, arg); } } #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) { auto thread = concurrency::OSThread::currentThread; meshtastic_LogRecord logRecord = meshtastic_LogRecord_init_zero; logRecord.level = getLogLevel(logLevel); vsprintf(logRecord.message, format, arg); if (thread) strcpy(logRecord.source, thread->ThreadName.c_str()); logRecord.time = getValidTime(RTCQuality::RTCQualityDevice, true); auto buffer = std::unique_ptr(new uint8_t[meshtastic_LogRecord_size]); size_t size = pb_encode_to_bytes(buffer.get(), meshtastic_LogRecord_size, meshtastic_LogRecord_fields, &logRecord); #ifdef ARCH_ESP32 nimbleBluetooth->sendLog(buffer.get(), size); #elif defined(ARCH_NRF52) nrf52Bluetooth->sendLog(buffer.get(), size); #endif } } #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); auto newFormat = std::unique_ptr(new char[len + 2]); strcpy(newFormat.get(), format); newFormat[len] = '\n'; newFormat[len + 1] = '\0'; #if ARCH_PORTDUINO // level trace is special, two possible ways to handle it. if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_TRACE) == 0) { if (portduino_config.traceFilename != "") { va_list arg; va_start(arg, format); try { traceFile << va_arg(arg, char *) << std::endl; } catch (const std::ios_base::failure &e) { } va_end(arg); } if (portduino_config.logoutputlevel < level_trace && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_TRACE) == 0) { return; } } if (portduino_config.logoutputlevel < level_debug && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_DEBUG) == 0) { return; } else if (portduino_config.logoutputlevel < level_info && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_INFO) == 0) { return; } else if (portduino_config.logoutputlevel < level_warn && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_WARN) == 0) { return; } #endif if (moduleConfig.serial.override_console_serial_port && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_DEBUG) == 0) { return; } #ifdef HAS_FREE_RTOS if (inDebugPrint != nullptr && xSemaphoreTake(inDebugPrint, portMAX_DELAY) == pdTRUE) { #else if (!inDebugPrint) { inDebugPrint = true; #endif va_list arg; va_list arg_copy; va_start(arg, format); va_copy(arg_copy, arg); log_to_serial(logLevel, newFormat.get(), arg_copy); va_end(arg_copy); va_copy(arg_copy, arg); log_to_syslog(logLevel, newFormat.get(), arg_copy); va_end(arg_copy); log_to_ble(logLevel, newFormat.get(), arg); va_end(arg); #ifdef HAS_FREE_RTOS xSemaphoreGive(inDebugPrint); #else inDebugPrint = false; #endif } return; } void RedirectablePrint::hexDump(const char *logLevel, const unsigned char *buf, uint16_t len) { const char alphabet[17] = "0123456789abcdef"; log(logLevel, " +------------------------------------------------+ +----------------+"); log(logLevel, " |.0 .1 .2 .3 .4 .5 .6 .7 .8 .9 .a .b .c .d .e .f | | ASCII |"); for (uint16_t i = 0; i < len; i += 16) { if (i % 128 == 0) log(logLevel, " +------------------------------------------------+ +----------------+"); char s[] = " | | | |\n"; uint8_t ix = 5, iy = 56; for (uint8_t j = 0; j < 16; j++) { if (i + j < len) { uint8_t c = buf[i + j]; s[ix++] = alphabet[(c >> 4) & 0x0F]; s[ix++] = alphabet[c & 0x0F]; ix++; if (c > 31 && c < 128) s[iy++] = c; else s[iy++] = '.'; } } uint8_t index = i / 16; sprintf(s, "%03x", index); s[3] = '.'; log(logLevel, s); } log(logLevel, " +------------------------------------------------+ +----------------+"); } std::string RedirectablePrint::mt_sprintf(const std::string fmt_str, ...) { int n = ((int)fmt_str.size()) * 2; /* Reserve two times as much as the length of the fmt_str */ std::unique_ptr formatted; va_list ap; while (1) { formatted.reset(new char[n]); /* Wrap the plain char array into the unique_ptr */ strcpy(&formatted[0], fmt_str.c_str()); va_start(ap, fmt_str); int final_n = vsnprintf(&formatted[0], n, fmt_str.c_str(), ap); va_end(ap); if (final_n < 0 || final_n >= n) n += abs(final_n - n + 1); else break; } return std::string(formatted.get()); } ================================================ FILE: src/RedirectablePrint.h ================================================ #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, const 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); }; ================================================ FILE: src/SPILock.cpp ================================================ #include "SPILock.h" #include "configuration.h" #include #include concurrency::Lock *spiLock; void initSPI() { assert(!spiLock); spiLock = new concurrency::Lock(); } ================================================ FILE: src/SPILock.h ================================================ #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(); ================================================ FILE: src/SafeFile.cpp ================================================ #include "SafeFile.h" #ifdef FSCom // Only way to work on both esp32 and nrf52 static File openFile(const char *filename, bool fullAtomic) { concurrency::LockGuard g(spiLock); LOG_DEBUG("Opening %s, fullAtomic=%d", filename, fullAtomic); #ifdef ARCH_NRF52 FSCom.remove(filename); return FSCom.open(filename, FILE_O_WRITE); #endif if (!fullAtomic) { FSCom.remove(filename); // Nuke the old file to make space (ignore if it !exists) } String filenameTmp = filename; filenameTmp += ".tmp"; // FIXME: If we are doing a full atomic write, we may need to remove the old tmp file now // if (fullAtomic) { // FSCom.remove(filename); // } // clear any previous LFS errors return FSCom.open(filenameTmp.c_str(), FILE_O_WRITE); } SafeFile::SafeFile(const char *_filename, bool fullAtomic) : filename(_filename), f(openFile(_filename, fullAtomic)), fullAtomic(fullAtomic) { } size_t SafeFile::write(uint8_t ch) { if (!f) return 0; hash ^= ch; return f.write(ch); } 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 (overwriting any old version) and readback the contents to confirm the hash matches * * @return false for failure */ bool SafeFile::close() { if (!f) return false; spiLock->lock(); f.close(); spiLock->unlock(); #ifdef ARCH_NRF52 return true; #endif if (!testReadback()) return false; // Rename or overwrite (atomic operation) 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() { concurrency::LockGuard g(spiLock); String filenameTmp = filename; filenameTmp += ".tmp"; auto f2 = FSCom.open(filenameTmp.c_str(), FILE_O_READ); if (!f2) { LOG_ERROR("Can't open tmp file for readback"); return false; } 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 true; } #endif ================================================ FILE: src/SafeFile.h ================================================ #pragma once #include "FSCommon.h" #include "SPILock.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 ================================================ FILE: src/SerialConsole.cpp ================================================ #include "SerialConsole.h" #include "Default.h" #include "NodeDB.h" #include "PowerFSM.h" #include "Throttle.h" #include "configuration.h" #include "time.h" #if defined(ARDUINO_USB_CDC_ON_BOOT) && ARDUINO_USB_CDC_ON_BOOT #define IS_USB_SERIAL #ifdef SERIAL_HAS_ON_RECEIVE #undef SERIAL_HAS_ON_RECEIVE #endif #include "HWCDC.h" #endif #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() { auto sc = new SerialConsole(); // Must be dynamically allocated because we are now inheriting from thread #if defined(SERIAL_HAS_ON_RECEIVE) // onReceive does only exist for HardwareSerial not for USB CDC serial Port.onReceive([sc]() { sc->rxInt(); }); #else (void)sc; #endif 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") { api_type = TYPE_SERIAL; assert(!console); console = this; canWrite = false; // We don't send packets to our port until it has talked to us first #ifdef RP2040_SLOW_CLOCK Port.setTX(SERIAL2_TX); Port.setRX(SERIAL2_RX); #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() { #ifdef HELTEC_MESH_SOLAR // After enabling the mesh solar serial port module configuration, command processing is handled by the serial port module. if (moduleConfig.serial.enabled && moduleConfig.serial.override_console_serial_port && moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MS_CONFIG) { return 250; } #endif int32_t delay = runOncePart(); #if defined(SERIAL_HAS_ON_RECEIVE) || defined(CONFIG_IDF_TARGET_ESP32S2) return Port.available() ? delay : INT32_MAX; #elif defined(IS_USB_SERIAL) return HWCDC::isPlugged() ? delay : (1000 * 20); #else return delay; #endif } void SerialConsole::flush() { Port.flush(); } // trigger tx of serial data void SerialConsole::onNowHasData(uint32_t fromRadioNum) { setIntervalFromNow(0); } // trigger rx of serial data void SerialConsole::rxInt() { setIntervalFromNow(0); } // For the serial port we can't really detect if any client is on the other side, so instead just look for recent messages bool SerialConsole::checkIsConnected() { return Throttle::isWithinTimespanMs(lastContactMsec, SERIAL_CONNECTION_TIMEOUT); } /** * 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); } ================================================ FILE: src/SerialConsole.h ================================================ #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(); void rxInt(); protected: /// Check the current underlying physical link to see if the client is currently connected virtual bool checkIsConnected() override; virtual void onNowHasData(uint32_t fromRadioNum) override; /// 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; ================================================ FILE: src/Status.h ================================================ #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 #define STATUS_TYPE_BLUETOOTH 4 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 ================================================ FILE: src/airtime.cpp ================================================ #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 TX: %ums", airtime_ms); this->airtimes.periodTX[0] = this->airtimes.periodTX[0] + airtime_ms; air_period_tx[0] = air_period_tx[0] + airtime_ms; this->utilizationTX[this->getPeriodUtilHour()] = this->utilizationTX[this->getPeriodUtilHour()] + airtime_ms; } else if (reportType == RX_LOG) { LOG_DEBUG("Packet RX: %ums", airtime_ms); this->airtimes.periodRX[0] = this->airtimes.periodRX[0] + airtime_ms; air_period_rx[0] = air_period_rx[0] + airtime_ms; } else if (reportType == RX_ALL_LOG) { LOG_DEBUG("Packet RX (noise?) : %ums", airtime_ms); this->airtimes.periodRX_ALL[0] = this->airtimes.periodRX_ALL[0] + airtime_ms; } // Log all airtime type for channel utilization this->channelUtilization[this->getPeriodUtilMinute()] = channelUtilization[this->getPeriodUtilMinute()] + airtime_ms; } uint8_t AirTime::currentPeriodIndex() { return ((getSecondsSinceBoot() / SECONDS_PER_PERIOD) % PERIODS_TO_LOG); } uint8_t AirTime::getPeriodUtilMinute() { return (getSecondsSinceBoot() / 10) % CHANNEL_UTILIZATION_PERIODS; } uint8_t AirTime::getPeriodUtilHour() { return (getSecondsSinceBoot() / 60) % MINUTES_IN_HOUR; } void AirTime::airtimeRotatePeriod() { if (this->airtimes.lastPeriodIndex != this->currentPeriodIndex()) { LOG_DEBUG("Rotate airtimes to a new period = %u", this->currentPeriodIndex()); for (int i = PERIODS_TO_LOG - 2; i >= 0; --i) { this->airtimes.periodTX[i + 1] = this->airtimes.periodTX[i]; this->airtimes.periodRX[i + 1] = this->airtimes.periodRX[i]; this->airtimes.periodRX_ALL[i + 1] = this->airtimes.periodRX_ALL[i]; air_period_tx[i + 1] = this->airtimes.periodTX[i]; air_period_rx[i + 1] = this->airtimes.periodRX[i]; } this->airtimes.periodTX[0] = 0; this->airtimes.periodRX[0] = 0; this->airtimes.periodRX_ALL[0] = 0; air_period_tx[0] = 0; air_period_rx[0] = 0; this->airtimes.lastPeriodIndex = this->currentPeriodIndex(); } } uint32_t *AirTime::airtimeReport(reportTypes reportType) { if (reportType == TX_LOG) { return this->airtimes.periodTX; } else if (reportType == RX_LOG) { return this->airtimes.periodRX; } else if (reportType == RX_ALL_LOG) { return this->airtimes.periodRX_ALL; } return 0; } uint8_t AirTime::getPeriodsToLog() { return PERIODS_TO_LOG; } uint32_t AirTime::getSecondsPerPeriod() { return SECONDS_PER_PERIOD; } uint32_t AirTime::getSecondsSinceBoot() { return this->secSinceBoot; } float AirTime::channelUtilizationPercent() { uint32_t sum = 0; for (uint32_t i = 0; i < CHANNEL_UTILIZATION_PERIODS; i++) { sum += this->channelUtilization[i]; } return (float(sum) / float(CHANNEL_UTILIZATION_PERIODS * 10 * 1000)) * 100; } float AirTime::utilizationTXPercent() { uint32_t sum = 0; for (uint32_t i = 0; i < MINUTES_IN_HOUR; i++) { sum += this->utilizationTX[i]; } return (float(sum) / float(MS_IN_HOUR)) * 100; } bool AirTime::isTxAllowedChannelUtil(bool polite) { uint8_t percentage = (polite ? polite_channel_util_percent : max_channel_util_percent); if (channelUtilizationPercent() < percentage) { return true; } else { LOG_WARN("Ch. util >%d%%. Skip send", percentage); return false; } } bool AirTime::isTxAllowedAirUtil() { if (!config.lora.override_duty_cycle && myRegion->dutyCycle < 100) { if (utilizationTXPercent() < myRegion->dutyCycle * polite_duty_cycle_percent / 100) { return true; } else { LOG_WARN("TX air util. >%f%%. Skip send", myRegion->dutyCycle * polite_duty_cycle_percent / 100); return false; } } 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; } } return (1000 * 1); } ================================================ FILE: src/airtime.h ================================================ #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; ================================================ FILE: src/buzz/BuzzerFeedbackThread.cpp ================================================ #include "BuzzerFeedbackThread.h" #include "NodeDB.h" #include "buzz.h" #include "configuration.h" BuzzerFeedbackThread *buzzerFeedbackThread; BuzzerFeedbackThread::BuzzerFeedbackThread() { if (inputBroker) inputObserver.observe(inputBroker); } int BuzzerFeedbackThread::handleInputEvent(const InputEvent *event) { // Only provide feedback if buzzer is enabled for notifications if (config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_DISABLED || config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_NOTIFICATIONS_ONLY || config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_DIRECT_MSG_ONLY) { return 0; // Let other handlers process the event } // Handle different input events with appropriate buzzer feedback switch (event->inputEvent) { #ifdef INPUTDRIVER_ENCODER_TYPE case INPUT_BROKER_SELECT: case INPUT_BROKER_SELECT_LONG: playClick(); break; #else case INPUT_BROKER_USER_PRESS: case INPUT_BROKER_ALT_PRESS: case INPUT_BROKER_SELECT: case INPUT_BROKER_SELECT_LONG: playBeep(); break; #endif case INPUT_BROKER_UP: case INPUT_BROKER_UP_LONG: case INPUT_BROKER_DOWN: case INPUT_BROKER_DOWN_LONG: case INPUT_BROKER_LEFT: case INPUT_BROKER_RIGHT: playChirp(); // Navigation feedback break; case INPUT_BROKER_CANCEL: case INPUT_BROKER_BACK: playBoop(); // Cancel/back feedback break; case INPUT_BROKER_SEND_PING: playComboTune(); // Ping sent feedback break; default: // For other events, check if it's a printable character if (event->kbchar >= 32 && event->kbchar <= 126) { // Typing feedback - very short boop // Removing this for now, too chatty // playChirp(); } break; } return 0; // Allow other handlers to process the event } ================================================ FILE: src/buzz/BuzzerFeedbackThread.h ================================================ #pragma once #include "Observer.h" #include "concurrency/OSThread.h" #include "input/InputBroker.h" class BuzzerFeedbackThread { CallbackObserver inputObserver = CallbackObserver(this, &BuzzerFeedbackThread::handleInputEvent); public: BuzzerFeedbackThread(); int handleInputEvent(const InputEvent *event); }; extern BuzzerFeedbackThread *buzzerFeedbackThread; ================================================ FILE: src/buzz/buzz.cpp ================================================ #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_SILENT 1 #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 #define NOTE_B4 494 #define NOTE_F5 698 #define NOTE_G6 1568 #define NOTE_E7 2637 #define NOTE_C4 262 #define NOTE_E4 330 #define NOTE_G4 392 #define NOTE_A4 440 #define NOTE_C5 523 #define NOTE_E5 659 #define NOTE_G5 784 const int DURATION_1_16 = 62; // 1/16 note const int DURATION_1_8 = 125; // 1/8 note const int DURATION_1_4 = 250; // 1/4 note const int DURATION_1_2 = 500; // 1/2 note const int DURATION_3_4 = 750; // 3/4 note const int DURATION_1_1 = 1000; // 1/1 note void playTones(const ToneDuration *tone_durations, int size) { if (config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_DISABLED || config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_NOTIFICATIONS_ONLY) { // Buzzer is disabled or not set to system tones return; } #ifdef PIN_BUZZER if (!config.device.buzzer_gpio) config.device.buzzer_gpio = PIN_BUZZER; #endif if (config.device.buzzer_gpio) { for (int i = 0; i < size; i++) { const auto &tone_duration = tone_durations[i]; tone(config.device.buzzer_gpio, tone_duration.frequency_khz, tone_duration.duration_ms); // to distinguish the notes, set a minimum time between them. delay(1.3 * tone_duration.duration_ms); } } } void playBeep() { ToneDuration melody[] = {{NOTE_B3, DURATION_1_16}}; playTones(melody, sizeof(melody) / sizeof(ToneDuration)); } void playLongBeep() { ToneDuration melody[] = {{NOTE_B3, DURATION_1_1}}; playTones(melody, sizeof(melody) / sizeof(ToneDuration)); } void playGPSEnableBeep() { #if defined(R1_NEO) || defined(MUZI_BASE) ToneDuration melody[] = { {NOTE_F5, DURATION_1_2}, {NOTE_G6, DURATION_1_8}, {NOTE_E7, DURATION_1_4}, {NOTE_SILENT, DURATION_1_2}}; #else ToneDuration melody[] = {{NOTE_C3, DURATION_1_8}, {NOTE_FS3, DURATION_1_4}, {NOTE_CS4, DURATION_1_4}}; #endif playTones(melody, sizeof(melody) / sizeof(ToneDuration)); } void playGPSDisableBeep() { #if defined(R1_NEO) || defined(MUZI_BASE) ToneDuration melody[] = {{NOTE_B4, DURATION_1_16}, {NOTE_B4, DURATION_1_16}, {NOTE_SILENT, DURATION_1_8}, {NOTE_F3, DURATION_1_16}, {NOTE_F3, DURATION_1_16}, {NOTE_SILENT, DURATION_1_8}, {NOTE_C3, DURATION_1_1}, {NOTE_SILENT, DURATION_1_1}}; #else ToneDuration melody[] = {{NOTE_CS4, DURATION_1_8}, {NOTE_FS3, DURATION_1_4}, {NOTE_C3, DURATION_1_4}}; #endif playTones(melody, sizeof(melody) / sizeof(ToneDuration)); } void playStartMelody() { ToneDuration melody[] = {{NOTE_FS3, DURATION_1_8}, {NOTE_AS3, DURATION_1_8}, {NOTE_CS4, DURATION_1_4}}; playTones(melody, sizeof(melody) / sizeof(ToneDuration)); } void playShutdownMelody() { ToneDuration melody[] = {{NOTE_CS4, DURATION_1_8}, {NOTE_AS3, DURATION_1_8}, {NOTE_FS3, DURATION_1_4}}; playTones(melody, sizeof(melody) / sizeof(ToneDuration)); } void playChirp() { // A short, friendly "chirp" sound for key presses ToneDuration melody[] = {{NOTE_AS3, 20}}; // Short AS3 note playTones(melody, sizeof(melody) / sizeof(ToneDuration)); } void playClick() { // A very short "click" sound with minimum delay; ideal for rotary encoder events ToneDuration melody[] = {{NOTE_AS3, 1}}; // Very Short AS3 playTones(melody, sizeof(melody) / sizeof(ToneDuration)); } void playBoop() { // A short, friendly "boop" sound for button presses ToneDuration melody[] = {{NOTE_A3, 50}}; // Very short A3 note playTones(melody, sizeof(melody) / sizeof(ToneDuration)); } void playLongPressLeadUp() { // An ascending lead-up sequence for long press - builds anticipation ToneDuration melody[] = { {NOTE_C3, 100}, // Start low {NOTE_E3, 100}, // Step up {NOTE_G3, 100}, // Keep climbing {NOTE_B3, 150} // Peak with longer note for emphasis }; playTones(melody, sizeof(melody) / sizeof(ToneDuration)); } // Static state for progressive lead-up notes static int leadUpNoteIndex = 0; static const ToneDuration leadUpNotes[] = { {NOTE_C3, 100}, // Start low {NOTE_E3, 100}, // Step up {NOTE_G3, 100}, // Keep climbing {NOTE_B3, 150} // Peak with longer note for emphasis }; static const int leadUpNotesCount = sizeof(leadUpNotes) / sizeof(ToneDuration); bool playNextLeadUpNote() { if (leadUpNoteIndex >= leadUpNotesCount) { return false; // All notes have been played } // Use playTones to handle buzzer logic consistently const auto ¬e = leadUpNotes[leadUpNoteIndex]; playTones(¬e, 1); // Play single note using existing playTones function leadUpNoteIndex++; if (leadUpNoteIndex >= leadUpNotesCount) { return false; // this was the final note } return true; // Note was played (playTones handles buzzer availability internally) } void resetLeadUpSequence() { leadUpNoteIndex = 0; } void playComboTune() { // Quick high-pitched notes with trills ToneDuration melody[] = { {NOTE_G3, 80}, // Quick chirp {NOTE_B3, 60}, // Higher chirp {NOTE_CS4, 80}, // Even higher {NOTE_G3, 60}, // Quick trill down {NOTE_CS4, 60}, // Quick trill up {NOTE_B3, 120} // Ending chirp }; playTones(melody, sizeof(melody) / sizeof(ToneDuration)); } void play4ClickDown() { ToneDuration melody[] = {{NOTE_G5, 55}, {NOTE_E5, 55}, {NOTE_C5, 60}, {NOTE_A4, 55}, {NOTE_G4, 55}, {NOTE_E4, 65}, {NOTE_C4, 80}, {NOTE_G3, 120}, {NOTE_E3, 160}, {NOTE_SILENT, 120}}; playTones(melody, sizeof(melody) / sizeof(ToneDuration)); } void play4ClickUp() { // Quick high-pitched notes with trills ToneDuration melody[] = {{NOTE_F5, 50}, {NOTE_G6, 45}, {NOTE_E7, 60}}; playTones(melody, sizeof(melody) / sizeof(ToneDuration)); } ================================================ FILE: src/buzz/buzz.h ================================================ #pragma once void playBeep(); void playLongBeep(); void playStartMelody(); void playShutdownMelody(); void playGPSEnableBeep(); void playGPSDisableBeep(); void playComboTune(); void play4ClickDown(); void play4ClickUp(); void playBoop(); void playChirp(); void playClick(); void playLongPressLeadUp(); bool playNextLeadUpNote(); // Play the next note in the lead-up sequence void resetLeadUpSequence(); // Reset the lead-up sequence to start from beginning ================================================ FILE: src/commands.h ================================================ /** * @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, SHOW_PREV_FRAME, SHOW_NEXT_FRAME, NOOP }; ================================================ FILE: src/concurrency/BinarySemaphoreFreeRTOS.cpp ================================================ #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 ================================================ FILE: src/concurrency/BinarySemaphoreFreeRTOS.h ================================================ #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 ================================================ FILE: src/concurrency/BinarySemaphorePosix.cpp ================================================ #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 ================================================ FILE: src/concurrency/BinarySemaphorePosix.h ================================================ #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 ================================================ FILE: src/concurrency/InterruptableDelay.cpp ================================================ #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 ================================================ FILE: src/concurrency/InterruptableDelay.h ================================================ #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 ================================================ FILE: src/concurrency/Lock.cpp ================================================ #include "Lock.h" #include "configuration.h" #include namespace concurrency { #ifdef HAS_FREE_RTOS Lock::Lock() : handle(xSemaphoreCreateBinary()) { assert(handle); if (xSemaphoreGive(handle) == false) { abort(); } } void Lock::lock() { if (xSemaphoreTake(handle, portMAX_DELAY) == false) { abort(); } } void Lock::unlock() { if (xSemaphoreGive(handle) == false) { abort(); } } #else Lock::Lock() {} void Lock::lock() {} void Lock::unlock() {} #endif } // namespace concurrency ================================================ FILE: src/concurrency/Lock.h ================================================ #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 ================================================ FILE: src/concurrency/LockGuard.cpp ================================================ #include "LockGuard.h" #include "configuration.h" namespace concurrency { LockGuard::LockGuard(Lock *lock) : lock(lock) { lock->lock(); } LockGuard::~LockGuard() { lock->unlock(); } } // namespace concurrency ================================================ FILE: src/concurrency/LockGuard.h ================================================ #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 ================================================ FILE: src/concurrency/NotifiedWorkerThread.cpp ================================================ #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("Set notification %d", v); } return true; } else { if (debugNotification) { LOG_DEBUG("Drop 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("Delay notification %u", delay); } } return didIt; } void NotifiedWorkerThread::checkNotification() { // Atomically read and clear. (This avoids a potential race condition where an interrupt handler could set a new notification // after checkNotification reads but before it clears, which would cause us to miss that notification until the next one comes // in.) auto n = notification.exchange(0); // read+clear atomically: like `n = notification; notification = 0;` but interrupt-safe if (n) { onNotify(n); } } int32_t NotifiedWorkerThread::runOnce() { enabled = false; // Only run once per notification checkNotification(); return RUN_SAME; } } // namespace concurrency ================================================ FILE: src/concurrency/NotifiedWorkerThread.h ================================================ #pragma once #include "OSThread.h" #include 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() */ std::atomic 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 ================================================ FILE: src/concurrency/OSThread.cpp ================================================ #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_HEAP("------ Thread %s leaked heap %d -> %d (%d) ------", ThreadName.c_str(), heap, newHeap, newHeap - heap); if (heap < newHeap) LOG_HEAP("++++++ Thread %s freed heap %d -> %d (%d) ++++++", ThreadName.c_str(), heap, newHeap, newHeap - heap); #endif #ifdef DEBUG_LOOP_TIMING LOG_DEBUG("====== Thread next run in: %d", newDelay); #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 ================================================ FILE: src/concurrency/OSThread.h ================================================ #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 ================================================ FILE: src/concurrency/Periodic.h ================================================ #pragma once #include #include #include "concurrency/OSThread.h" namespace concurrency { /** * @brief Periodically invoke a callback. * Supports both legacy function pointers and modern callables. */ class Periodic : public OSThread { 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 (*cb)()) : OSThread(name), callback(cb) {} Periodic(const char *name, std::function cb) : OSThread(name), callback(std::move(cb)) {} protected: int32_t runOnce() override { return callback ? callback() : 0; } private: std::function callback; }; } // namespace concurrency ================================================ FILE: src/configuration.h ================================================ /* 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 #if __has_include("Melopero_RV3028.h") #include "Melopero_RV3028.h" #endif #if __has_include("SensorRtcHelper.hpp") #include "SensorRtcHelper.hpp" #endif /* Offer chance for variant-specific defines */ #include "variant.h" // ----------------------------------------------------------------------------- // Display feature overrides // ----------------------------------------------------------------------------- // Allow build environments to opt-in explicitly to the E-Ink UI stack while // keeping headless targets slim by default. Existing variants that already // define USE_EINK continue to work without additional flags. #ifndef MESHTASTIC_USE_EINK_UI #ifdef USE_EINK #define MESHTASTIC_USE_EINK_UI 1 #else #define MESHTASTIC_USE_EINK_UI 0 #endif #endif #if MESHTASTIC_USE_EINK_UI #ifndef USE_EINK #define USE_EINK #endif #else #undef USE_EINK #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 // ----------------------------------------------------------------------------- // Pre-hop drop handling (compile-time flag). #ifndef MESHTASTIC_PREHOP_DROP #define MESHTASTIC_PREHOP_DROP 0 #endif /// 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 and Tx PA limits // The value consists of PA gain + antenna gain (if variant has a non-removable antenna) // TX_GAIN_LORA should be set with definitions below for common modules, or in variant.h. // Gain for common modules with transmit PAs #ifdef EBYTE_E22_900M30S // 10dB PA gain and 30dB rated output; based on measurements from // https://github.com/S5NC/EBYTE_ESP32-S3/blob/main/E22-900M30S%20power%20output%20testing.txt #define TX_GAIN_LORA 7 #define SX126X_MAX_POWER 22 #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 TX_GAIN_LORA 25 #define SX126X_MAX_POWER 8 #endif #ifdef NICERF_MINIF27 // Note that datasheet power level of 9 corresponds with SX1262 at 22dBm // Maximum output power of 29dBm with VCC_PA = 5V #define TX_GAIN_LORA 7 #define SX126X_MAX_POWER 22 #endif #ifdef NICERF_F30_HF // Maximum output power of 29.6dBm with VCC = 5V and SX1262 at 22dBm #define TX_GAIN_LORA 8 #define SX126X_MAX_POWER 22 #endif #ifdef NICERF_F30_LF // Maximum output power of 32.0dBm with VCC = 5V and SX1262 at 22dBm #define TX_GAIN_LORA 10 #define SX126X_MAX_POWER 22 #endif #ifdef USE_GC1109_PA // Power Amps are often non-linear, so we can use an array of values for the power curve #define NUM_PA_POINTS 22 #define TX_GAIN_LORA 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 9, 8, 7 #endif #ifdef USE_KCT8103L_PA // Power Amps are often non-linear, so we can use an array of values for the power curve #define NUM_PA_POINTS 22 #define TX_GAIN_LORA 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 13, 13, 13, 12, 12, 11, 10, 9, 8, 7 #endif #ifdef RAK13302 #define NUM_PA_POINTS 22 #define TX_GAIN_LORA 7, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 8 #endif // Default system gain to 0 if not defined #ifndef NUM_PA_POINTS #define NUM_PA_POINTS 1 #endif #ifndef TX_GAIN_LORA #define TX_GAIN_LORA 0 #endif #ifndef HAS_LORA_FEM #define HAS_LORA_FEM 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_L 0x3C // Addr = 0 #define SSD1306_ADDRESS_H 0x3D // Addr = 1 #if defined(SEEED_WIO_TRACKER_L1) && !defined(SEEED_WIO_TRACKER_L1_EINK) #define SSD1306_ADDRESS SSD1306_ADDRESS_H #define USE_SH1106 #endif #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, T-Deck Pro, T-Lora Pager, CardKB, BBQ10, MPR121, TCA8418) #define CARDKB_ADDR 0x5F #define TDECK_KB_ADDR 0x55 #define BBQ10_KB_ADDR 0x1F #define MPR121_KB_ADDR 0x5A #define TCA8418_KB_ADDR 0x34 // ----------------------------------------------------------------------------- // 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 QMC6310U_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 SFA30_ADDR 0x5D #define SHT31_4x_ADDR 0x44 #define SHT31_4x_ADDR_ALT 0x45 #define PMSA003I_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 DFROBOT_RAIN_ADDR 0x1d #define NAU7802_ADDR 0x2A #define MAX30102_ADDR 0x57 #define SCD4X_ADDR 0x62 #define CW2015_ADDR 0x62 #define MLX90614_ADDR_DEF 0x5A #define CGRADSENS_ADDR 0x66 #define LTR390UV_ADDR 0x53 #define XPOWERS_AXP192_AXP2101_ADDRESS 0x34 // same adress as TCA8418_KB #define PCT2075_ADDR 0x37 #define BQ27220_ADDR 0x55 // same address as TDECK_KB #define BQ25896_ADDR 0x6B #define LTR553ALS_ADDR 0x23 #define SEN5X_ADDR 0x69 #define SCD30_ADDR 0x61 // ----------------------------------------------------------------------------- // ACCELEROMETER // ----------------------------------------------------------------------------- #define MPU6050_ADDR 0x68 #define STK8BXX_ADDR 0x18 #define LIS3DH_ADDR 0x18 #define LIS3DH_ADDR_ALT 0x19 #define BMA423_ADDR 0x19 #define LSM6DS3_ADDR 0x6A #define BMX160_ADDR 0x69 #define ICM20948_ADDR 0x69 #define ICM20948_ADDR_ALT 0x68 #define BHI260AP_ADDR 0x28 #define BMM150_ADDR 0x13 #define DA217_ADDR 0x26 #define BMI270_ADDR 0x68 #define BMI270_ADDR_ALT 0x69 // ----------------------------------------------------------------------------- // LED // ----------------------------------------------------------------------------- #define NCP5623_ADDR 0x38 #define LP5562_ADDR 0x30 // ----------------------------------------------------------------------------- // Security // ----------------------------------------------------------------------------- // ----------------------------------------------------------------------------- // IO Expander // ----------------------------------------------------------------------------- #define TCA9535_ADDR 0x20 #define TCA9555_ADDR 0x26 // ----------------------------------------------------------------------------- // Touchscreen // ----------------------------------------------------------------------------- #define FT6336U_ADDR 0x48 #define CST328_ADDR 0x1A // same address as CST226SE #define CHSC6X_ADDR 0x2E #define CST226SE_ADDR_ALT 0x5A // ----------------------------------------------------------------------------- // RAK12035VB Soil Monitor (using RAK12023 up to 3 RAK12035 monitors can be connected) // - the default i2c address for this sensor is 0x20, and users are instructed to // set 0x21 and 0x22 for the second and third sensor if present. // ----------------------------------------------------------------------------- #define RAK120351_ADDR 0x20 #define RAK120352_ADDR 0x21 #define RAK120353_ADDR 0x22 // ----------------------------------------------------------------------------- // 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)) #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 // ----------------------------------------------------------------------------- // Rotary encoder // ----------------------------------------------------------------------------- #ifndef ROTARY_DELAY #define ROTARY_DELAY 5 #endif // ----------------------------------------------------------------------------- // GPS // ----------------------------------------------------------------------------- #ifndef GPS_BAUDRATE #define GPS_BAUDRATE 9600 #define GPS_BAUDRATE_FIXED 0 #else #define GPS_BAUDRATE_FIXED 1 #endif #ifndef GPS_THREAD_INTERVAL #define GPS_THREAD_INTERVAL 200 #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_TFT #define HAS_TFT 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_CPU_SHUTDOWN #define HAS_CPU_SHUTDOWN 0 #endif #ifndef HAS_BLUETOOTH #define HAS_BLUETOOTH 0 #endif #ifndef USE_TFTDISPLAY #define USE_TFTDISPLAY 0 #endif #ifndef HW_VENDOR #error HW_VENDOR must be defined #endif #ifndef TB_DOWN #define TB_DOWN 255 #endif #ifndef TB_UP #define TB_UP 255 #endif #ifndef TB_LEFT #define TB_LEFT 255 #endif #ifndef TB_RIGHT #define TB_RIGHT 255 #endif #ifndef TB_PRESS #define TB_PRESS 255 #endif // Support multiple RGB LED configuration #if defined(HAS_NCP5623) || defined(HAS_LP5562) || defined(RGBLED_RED) || defined(HAS_NEOPIXEL) || defined(UNPHONE) #define HAS_RGB_LED #endif #ifndef LED_STATE_ON #define LED_STATE_ON 1 #endif #ifndef LED_STATE_OFF #define LED_STATE_OFF (LED_STATE_ON ^ 1) #endif #ifndef ledOff #define ledOff(pin) pinMode(pin, INPUT) #endif // default mapping of pins #if defined(PIN_BUTTON2) && !defined(CANCEL_BUTTON_PIN) #define ALT_BUTTON_PIN PIN_BUTTON2 #endif #if defined ALT_BUTTON_PIN #ifndef ALT_BUTTON_ACTIVE_LOW #define ALT_BUTTON_ACTIVE_LOW true #endif #ifndef ALT_BUTTON_ACTIVE_PULLUP #define ALT_BUTTON_ACTIVE_PULLUP true #endif #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_AIR_QUALITY_SENSOR 1 #define MESHTASTIC_EXCLUDE_HEALTH_TELEMETRY 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_TRAFFIC_MANAGEMENT 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" ================================================ FILE: src/detect/LoRaRadioType.h ================================================ #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; ================================================ FILE: src/detect/ScanI2C.cpp ================================================ #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, RTC_PCF85063, RTC_RX8130CE}; return firstOfOrNONE(4, types); } ScanI2C::FoundDevice ScanI2C::firstKeyboard() const { ScanI2C::DeviceType types[] = {CARDKB, TDECKKB, BBQ10KB, RAK14004, MPR121KB, TCA8418KB}; return firstOfOrNONE(6, types); } ScanI2C::FoundDevice ScanI2C::firstAccelerometer() const { ScanI2C::DeviceType types[] = {MPU6050, LIS3DH, BMA423, LSM6DS3, BMX160, STK8BAXX, ICM20948, QMA6100P, BMM150, BMI270}; return firstOfOrNONE(10, types); } ScanI2C::FoundDevice ScanI2C::firstAQI() const { ScanI2C::DeviceType types[] = {PMSA003I, SEN5X, SCD4X, SFA30}; return firstOfOrNONE(4, types); } ScanI2C::FoundDevice ScanI2C::firstRGBLED() const { ScanI2C::DeviceType types[] = {NCP5623, LP5562}; return firstOfOrNONE(2, types); } ScanI2C::FoundDevice ScanI2C::find(ScanI2C::DeviceType) const { return DEVICE_NONE; } bool ScanI2C::exists(ScanI2C::DeviceType) const { return false; } ScanI2C::FoundDevice ScanI2C::firstOfOrNONE(size_t count, ScanI2C::DeviceType *types) const { return DEVICE_NONE; } size_t ScanI2C::countDevices() const { return 0; } ScanI2C::DeviceAddress::DeviceAddress(ScanI2C::I2CPort port, uint8_t address) : port(port), address(address) {} ScanI2C::DeviceAddress::DeviceAddress() : DeviceAddress(I2CPort::NO_I2C, 0) {} bool ScanI2C::DeviceAddress::operator<(const ScanI2C::DeviceAddress &other) const { return // If this one has no port and other has a port (port == NO_I2C && other.port != NO_I2C) // if both have a port and this one's address is lower || (port != NO_I2C && other.port != NO_I2C && (address < other.address)); } ScanI2C::FoundDevice::FoundDevice(ScanI2C::DeviceType type, ScanI2C::DeviceAddress address) : type(type), address(address) {} ================================================ FILE: src/detect/ScanI2C.h ================================================ #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, RTC_RV3028, RTC_PCF8563, RTC_PCF85063, RTC_RX8130CE, CARDKB, TDECKKB, BBQ10KB, RAK14004, PMU_AXP192_AXP2101, // has the same adress as the TCA8418KB BME_680, BME_280, BMP_280, BMP_085, BMP_3XX, INA260, INA219, INA3221, MAX17048, MCP9808, SHT31, SHT4X, SHTC3, LPS22HB, QMC6310U, QMC6310N, QMI8658, QMC5883L, HMC5883L, PMSA003I, QMA6100P, MPU6050, LIS3DH, BMA423, BQ24295, LSM6DS3, TCA9535, TCA9555, VEML7700, RCWL9620, NCP5623, LP5562, TSL2591, OPT3001, MLX90632, MLX90614, AHT10, BMX160, DFROBOT_LARK, NAU7802, FT6336U, STK8BAXX, ICM20948, SCD4X, MAX30102, TPS65233, MPR121KB, CGRADSENS, INA226, NXP_SE050, DFROBOT_RAIN, DPS310, LTR390UV, RAK12035, TCA8418KB, PCT2075, CST328, BQ25896, BQ27220, LTR553ALS, BHI260AP, BMM150, TSL2561, DRV2605, BH1750, DA217, CHSC6X, CST226SE, BMI270, SEN5X, SFA30, CW2015, SCD30, ADS1115 } 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; FoundDevice firstAQI() const; FoundDevice firstRGBLED() 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; }; ================================================ FILE: src/detect/ScanI2CConsumer.cpp ================================================ #include "ScanI2CConsumer.h" #include static std::forward_list ScanI2CConsumers; ScanI2CConsumer::ScanI2CConsumer() { ScanI2CConsumers.push_front(this); } void ScanI2CCompleted(ScanI2C *i2cScanner) { for (ScanI2CConsumer *consumer : ScanI2CConsumers) { consumer->i2cScanFinished(i2cScanner); } } ================================================ FILE: src/detect/ScanI2CConsumer.h ================================================ #pragma once #include "ScanI2C.h" #include class ScanI2CConsumer { public: ScanI2CConsumer(); virtual void i2cScanFinished(ScanI2C *i2cScanner) = 0; }; void ScanI2CCompleted(ScanI2C *i2cScanner); ================================================ FILE: src/detect/ScanI2CTwoWire.cpp ================================================ #include "ScanI2CTwoWire.h" #include "configuration.h" #include "detect/ScanI2C.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 "meshUtils.h" // vformat #endif bool in_array(uint8_t *array, int size, uint8_t lookfor) { int i; for (i = 0; i < size; i++) if (lookfor == array[i]) return true; return false; } 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(); } if (r == 0x80) { LOG_INFO("QMC6310N found at address 0x%02X", addr.address); return ScanI2C::DeviceType::QMC6310N; } r &= 0x0f; if (r == 0x08 || r == 0x00) { logFoundDevice("SH1106", (uint8_t)addr.address); o_probe = SCREEN_SH1106; // SH1106 } else if (r == 0x03 || r == 0x04 || r == 0x06 || r == 0x07 || r == 0x05) { logFoundDevice("SSD1306", (uint8_t)addr.address); o_probe = SCREEN_SSD1306; // SSD1306 } c++; } while ((r != r_prev) && (c < 4)); LOG_DEBUG("0x%x subtype probed in %i tries ", r, c); return o_probe; } uint16_t ScanI2CTwoWire::getRegisterValue(const ScanI2CTwoWire::RegisterLocation ®isterLocation, ScanI2CTwoWire::ResponseWidth responseWidth, bool zeropad = false) const { uint16_t value = 0x00; TwoWire *i2cBus = fetchI2CBus(registerLocation.i2cAddress); i2cBus->beginTransmission(registerLocation.i2cAddress.address); i2cBus->write(registerLocation.registerAddress); if (zeropad) { // Lark Commands need the argument list length in 2 bytes. i2cBus->write((int)0); i2cBus->write((int)0); } i2cBus->endTransmission(); delay(20); i2cBus->requestFrom(registerLocation.i2cAddress.address, responseWidth); if (i2cBus->available() > 1) { // Read MSB, then LSB value = (uint16_t)i2cBus->read() << 8; value |= i2cBus->read(); } else if (i2cBus->available()) { value = i2cBus->read(); } // Drain excess bytes for (uint8_t i = 0; i < responseWidth - 1; i++) { if (i2cBus->available()) i2cBus->read(); } LOG_DEBUG("Register value from 0x%x: 0x%x", registerLocation.i2cAddress.address, value); return value; } bool ScanI2CTwoWire::i2cCommandResponseLength(ScanI2C::DeviceAddress addr, uint16_t command, uint8_t expectedLength) const { TwoWire *i2cBus = fetchI2CBus(addr); i2cBus->beginTransmission(addr.address); if (command > 0xFF) { i2cBus->write((uint8_t)(command >> 8)); } i2cBus->write((uint8_t)(command & 0xFF)); if (i2cBus->endTransmission() != 0) { return false; } delay(20); uint8_t received = i2cBus->requestFrom(addr.address, expectedLength); bool match = (received == expectedLength); while (i2cBus->available()) i2cBus->read(); return match; } /// for SEN5X detection // Note, this code needs to be called before setting the I2C bus speed // for the screen at high speed. The speed needs to be at 100kHz, otherwise // detection will not work String readSEN5xProductName(TwoWire *i2cBus, uint8_t address) { uint8_t cmd[] = {0xD0, 0x14}; uint8_t response[48] = {0}; i2cBus->beginTransmission(address); i2cBus->write(cmd, 2); if (i2cBus->endTransmission() != 0) return ""; delay(20); if (i2cBus->requestFrom(address, (uint8_t)48) != 48) return ""; for (int i = 0; i < 48 && i2cBus->available(); ++i) { response[i] = i2cBus->read(); } char productName[33] = {0}; int j = 0; for (int i = 0; i < 48 && j < 32; i += 3) { if (response[i] >= 32 && response[i] <= 126) productName[j++] = response[i]; else break; if (response[i + 1] >= 32 && response[i + 1] <= 126) productName[j++] = response[i + 1]; else break; } return String(productName); } #define SCAN_SIMPLE_CASE(ADDR, T, ...) \ case ADDR: \ logFoundDevice(__VA_ARGS__); \ type = T; \ break; void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) { concurrency::LockGuard guard((concurrency::Lock *)&lock); LOG_DEBUG("Scan for I2C devices on port %d", port); uint8_t err; DeviceAddress addr(port, 0x00); uint16_t registerValue = 0x00; ScanI2C::DeviceType type; TwoWire *i2cBus; #ifdef RV3028_RTC Melopero_RV3028 rtc; #endif #if WIRE_INTERFACES_COUNT == 2 if (port == I2CPort::WIRE1) { i2cBus = &Wire1; } else { #endif i2cBus = &Wire; #if WIRE_INTERFACES_COUNT == 2 } #endif // We only need to scan 112 addresses, the rest is reserved for special purposes // 0x00 General Call // 0x01 CBUS addresses // 0x02 Reserved for different bus formats // 0x03 Reserved for future purposes // 0x04-0x07 High Speed Master Code // 0x78-0x7B 10-bit slave addressing // 0x7C-0x7F Reserved for future purposes for (addr.address = 8; addr.address < 120; addr.address++) { if (asize != 0) { if (!in_array(address, asize, (uint8_t)addr.address)) continue; LOG_DEBUG("Scan address 0x%x", (uint8_t)addr.address); } i2cBus->beginTransmission(addr.address); #ifdef ARCH_PORTDUINO err = 2; if ((addr.address >= 0x30 && addr.address <= 0x37) || (addr.address >= 0x50 && addr.address <= 0x5F)) { if (i2cBus->read() != -1) err = 0; } else { err = i2cBus->writeQuick((uint8_t)0); } if (err != 0) err = 2; #else err = i2cBus->endTransmission(); #endif type = NONE; if (err == 0) { switch (addr.address) { case SSD1306_ADDRESS_H: case SSD1306_ADDRESS_L: type = probeOLED(addr); break; #ifdef RV3028_RTC case RV3028_RTC: // foundDevices[addr] = RTC_RV3028; type = RTC_RV3028; logFoundDevice("RV3028", (uint8_t)addr.address); rtc.initI2C(*i2cBus); // Update RTC EEPROM settings, if necessary if (rtc.readEEPROMRegister(0x35) != 0x07) { rtc.writeEEPROMRegister(0x35, 0x07); // no Clkout } if (rtc.readEEPROMRegister(0x37) != 0xB4) { rtc.writeEEPROMRegister(0x37, 0xB4); } break; #endif #ifdef PCF8563_RTC SCAN_SIMPLE_CASE(PCF8563_RTC, RTC_PCF8563, "PCF8563", (uint8_t)addr.address) #endif #ifdef RX8130CE_RTC SCAN_SIMPLE_CASE(RX8130CE_RTC, RTC_RX8130CE, "RX8130CE", (uint8_t)addr.address) #endif #ifdef PCF85063_RTC SCAN_SIMPLE_CASE(PCF85063_RTC, RTC_PCF85063, "PCF85063", (uint8_t)addr.address) #endif case CARDKB_ADDR: // Do we have the RAK14006 instead? registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x04), 1); if (registerValue == 0x02) { // KEYPAD_VERSION logFoundDevice("RAK14004", (uint8_t)addr.address); type = RAK14004; } else { logFoundDevice("M5 cardKB", (uint8_t)addr.address); type = CARDKB; } break; case TDECK_KB_ADDR: // Do we have the T-Deck keyboard or the T-Deck Pro battery sensor? registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x04), 1); if (registerValue != 0) { logFoundDevice("BQ27220", (uint8_t)addr.address); type = BQ27220; } else { logFoundDevice("TDECKKB", (uint8_t)addr.address); type = TDECKKB; } break; SCAN_SIMPLE_CASE(BBQ10_KB_ADDR, BBQ10KB, "BB Q10", (uint8_t)addr.address); SCAN_SIMPLE_CASE(ST7567_ADDRESS, SCREEN_ST7567, "ST7567", (uint8_t)addr.address); #ifdef HAS_NCP5623 SCAN_SIMPLE_CASE(NCP5623_ADDR, NCP5623, "NCP5623", (uint8_t)addr.address); #endif #ifdef HAS_LP5562 SCAN_SIMPLE_CASE(LP5562_ADDR, LP5562, "LP5562", (uint8_t)addr.address); #endif case XPOWERS_AXP192_AXP2101_ADDRESS: // Do we have the axp2101/192 or the TCA8418 registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x90), 1); if (registerValue == 0x0) { logFoundDevice("TCA8418", (uint8_t)addr.address); type = TCA8418KB; } else { logFoundDevice("AXP192/AXP2101", (uint8_t)addr.address); type = PMU_AXP192_AXP2101; } break; case BME_ADDR: case BME_ADDR_ALTERNATE: registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xD0), 1); // GET_ID switch (registerValue) { case 0x61: logFoundDevice("BME680", (uint8_t)addr.address); type = BME_680; break; case 0x60: logFoundDevice("BME280", (uint8_t)addr.address); type = BME_280; break; case 0x55: logFoundDevice("BMP085/BMP180", (uint8_t)addr.address); type = BMP_085; break; case 0x00: // do we have a DPS310 instead? registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0D), 1); switch (registerValue) { case 0x10: logFoundDevice("DPS310", (uint8_t)addr.address); type = DPS310; break; } if (type == DPS310) { break; } default: registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1); // GET_ID switch (registerValue) { case 0x50: // BMP-388 should be 0x50 logFoundDevice("BMP-388", (uint8_t)addr.address); type = BMP_3XX; break; case 0x60: // BMP-390 should be 0x60 logFoundDevice("BMP-390", (uint8_t)addr.address); type = BMP_3XX; break; case 0x58: // BMP-280 should be 0x58 default: logFoundDevice("BMP-280", (uint8_t)addr.address); type = BMP_280; break; } break; } break; #ifndef HAS_NCP5623 case AHT10_ADDR: logFoundDevice("AHT10", (uint8_t)addr.address); type = AHT10; break; #endif #if !defined(M5STACK_UNITC6L) case INA_ADDR: case INA_ADDR_ALTERNATE: case INA_ADDR_WAVESHARE_UPS: registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xFE), 2); LOG_DEBUG("Register MFG_UID: 0x%x", registerValue); if (registerValue == 0x5449) { registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xFF), 2); LOG_DEBUG("Register DIE_UID: 0x%x", registerValue); if (registerValue == 0x2260) { logFoundDevice("INA226", (uint8_t)addr.address); type = INA226; } else { logFoundDevice("INA260", (uint8_t)addr.address); type = INA260; } } else { // Assume INA219 if INA260 ID is not found logFoundDevice("INA219", (uint8_t)addr.address); type = INA219; } break; case INA3221_ADDR: registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xFE), 2); LOG_DEBUG("Register MFG_UID FE: 0x%x", registerValue); if (registerValue == 0x5449) { logFoundDevice("INA3221", (uint8_t)addr.address); type = INA3221; } else { /* check the first 2 bytes of the 6 byte response register LARK FW 1.0 should return: RESPONSE_STATUS STATUS_SUCCESS (0x53) RESPONSE_CMD CMD_GET_VERSION (0x05) RESPONSE_LEN_L 0x02 RESPONSE_LEN_H 0x00 RESPONSE_PAYLOAD 0x01 RESPONSE_PAYLOAD+1 0x00 */ registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x05), 6, true); LOG_DEBUG("Register MFG_UID 05: 0x%x", registerValue); if (registerValue == 0x5305) { logFoundDevice("DFRobot Lark", (uint8_t)addr.address); type = DFROBOT_LARK; } // else: probably a RAK12500/UBLOX GPS on I2C } break; #endif case MCP9808_ADDR: // We need to check for STK8BAXX first, since register 0x07 is new data flag for the z-axis and can produce some // weird result. and register 0x00 doesn't seems to be colliding with MCP9808 and LIS3DH chips. { #ifdef HAS_STK8XXX // Check register 0x00 for 0x8700 response to ID STK8BA53 chip. registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 2); if (registerValue == 0x8700) { type = STK8BAXX; logFoundDevice("STK8BAXX", (uint8_t)addr.address); break; } #endif // Check register 0x07 for 0x0400 response to ID MCP9808 chip. registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x07), 2); if (registerValue == 0x0400) { type = MCP9808; logFoundDevice("MCP9808", (uint8_t)addr.address); break; } // Check register 0x0F for 0x3300 response to ID LIS3DH chip. registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0F), 2); if (registerValue == 0x3300 || registerValue == 0x3333) { // RAK4631 WisBlock has LIS3DH register at 0x3333 type = LIS3DH; logFoundDevice("LIS3DH", (uint8_t)addr.address); } break; } case SHT31_4x_ADDR: // same as OPT3001_ADDR_ALT case SHT31_4x_ADDR_ALT: // same as OPT3001_ADDR if (getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x7E), 2) == 0x5449) { type = OPT3001; logFoundDevice("OPT3001", (uint8_t)addr.address); } else if (i2cCommandResponseLength(addr, 0x89, 6)) { // SHT4x serial number (6 bytes inc. CRC) type = SHT4X; logFoundDevice("SHT4X", (uint8_t)addr.address); } else { type = SHT31; logFoundDevice("SHT31", (uint8_t)addr.address); } break; SCAN_SIMPLE_CASE(SHTC3_ADDR, SHTC3, "SHTC3", (uint8_t)addr.address) case RCWL9620_ADDR: // get MAX30102 PARTID registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xFF), 1); if (registerValue == 0x15) { type = MAX30102; logFoundDevice("MAX30102", (uint8_t)addr.address); break; } else { type = RCWL9620; logFoundDevice("RCWL9620", (uint8_t)addr.address); } break; case LPS22HB_ADDR_ALT: // SFA30 detection: send 2-byte command 0xD060 (Get Device Marking) and check for 48-byte response if (i2cCommandResponseLength(addr, 0xD060, 48)) { type = SFA30; logFoundDevice("SFA30", (uint8_t)addr.address); break; } // Fallback: LPS22HB detection at alternate address using WHO_AM_I register (0x0F == 0xB1) registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0F), 1); if (registerValue == 0xB1) { type = LPS22HB; logFoundDevice("LPS22HB", (uint8_t)addr.address); } break; SCAN_SIMPLE_CASE(LPS22HB_ADDR, LPS22HB, "LPS22HB", (uint8_t)addr.address) SCAN_SIMPLE_CASE(QMC6310U_ADDR, QMC6310U, "QMC6310U", (uint8_t)addr.address) case QMI8658_ADDR: registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0A), 1); // get ID if (registerValue == 0xC0) { type = BQ24295; logFoundDevice("BQ24295", (uint8_t)addr.address); break; } registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x14), 1); // get ID if ((registerValue & 0b00000011) == 0b00000010) { type = BQ25896; logFoundDevice("BQ25896", (uint8_t)addr.address); break; } registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0F), 1); // get ID if (registerValue == 0x6A) { type = LSM6DS3; logFoundDevice("LSM6DS3", (uint8_t)addr.address); } else { type = QMI8658; logFoundDevice("QMI8658", (uint8_t)addr.address); } break; SCAN_SIMPLE_CASE(QMC5883L_ADDR, QMC5883L, "QMC5883L", (uint8_t)addr.address) SCAN_SIMPLE_CASE(HMC5883L_ADDR, HMC5883L, "HMC5883L", (uint8_t)addr.address) #ifdef HAS_QMA6100P SCAN_SIMPLE_CASE(QMA6100P_ADDR, QMA6100P, "QMA6100P", (uint8_t)addr.address) #else SCAN_SIMPLE_CASE(PMSA003I_ADDR, PMSA003I, "PMSA003I", (uint8_t)addr.address) #endif case BMA423_ADDR: // this can also be LIS3DH_ADDR_ALT registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0F), 2); if (registerValue == 0x3300 || registerValue == 0x3333) { // RAK4631 WisBlock has LIS3DH register at 0x3333 type = LIS3DH; logFoundDevice("LIS3DH", (uint8_t)addr.address); } else { type = BMA423; logFoundDevice("BMA423", (uint8_t)addr.address); } break; case TCA9535_ADDR: case RAK120352_ADDR: case RAK120353_ADDR: registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x02), 1); if (registerValue == addr.address) { // RAK12035 returns its I2C address at 0x02 (eg 0x20) type = RAK12035; logFoundDevice("RAK12035", (uint8_t)addr.address); } else { type = TCA9535; logFoundDevice("TCA9535", (uint8_t)addr.address); } break; SCAN_SIMPLE_CASE(LSM6DS3_ADDR, LSM6DS3, "LSM6DS3", (uint8_t)addr.address); SCAN_SIMPLE_CASE(VEML7700_ADDR, VEML7700, "VEML7700", (uint8_t)addr.address); case TCA9555_ADDR: registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x01), 1); if (registerValue == 0x13) { registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1); if (registerValue == 0x81) { type = DA217; logFoundDevice("DA217", (uint8_t)addr.address); } else { type = TCA9555; logFoundDevice("TCA9555", (uint8_t)addr.address); } } else { type = TCA9555; logFoundDevice("TCA9555", (uint8_t)addr.address); } break; case TSL25911_ADDR: registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xA0 | 0x12), 1); if (registerValue == 0x50) { type = TSL2591; logFoundDevice("TSL25911", (uint8_t)addr.address); } else { type = TSL2561; logFoundDevice("TSL2561", (uint8_t)addr.address); } break; SCAN_SIMPLE_CASE(MLX90632_ADDR, MLX90632, "MLX90632", (uint8_t)addr.address); SCAN_SIMPLE_CASE(NAU7802_ADDR, NAU7802, "NAU7802", (uint8_t)addr.address); SCAN_SIMPLE_CASE(MAX1704X_ADDR, MAX17048, "MAX17048", (uint8_t)addr.address); SCAN_SIMPLE_CASE(DFROBOT_RAIN_ADDR, DFROBOT_RAIN, "DFRobot Rain Gauge", (uint8_t)addr.address); SCAN_SIMPLE_CASE(LTR390UV_ADDR, LTR390UV, "LTR390UV", (uint8_t)addr.address); SCAN_SIMPLE_CASE(PCT2075_ADDR, PCT2075, "PCT2075", (uint8_t)addr.address); SCAN_SIMPLE_CASE(SCD30_ADDR, SCD30, "SCD30", (uint8_t)addr.address); case CST328_ADDR: // Do we have the CST328 or the CST226SE registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xAB), 1); if (registerValue == 0xA9) { type = CST226SE; logFoundDevice("CST226SE", (uint8_t)addr.address); } else { type = CST328; logFoundDevice("CST328", (uint8_t)addr.address); } break; SCAN_SIMPLE_CASE(CHSC6X_ADDR, CHSC6X, "CHSC6X", (uint8_t)addr.address); case LTR553ALS_ADDR: registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x86), 1); // Part ID register if (registerValue == 0x92) { // LTR553ALS Part ID type = LTR553ALS; logFoundDevice("LTR553ALS", (uint8_t)addr.address); } else { // Test BH1750 - send power on command i2cBus->beginTransmission(addr.address); i2cBus->write(0x01); // Power On command uint8_t bh1750_error = i2cBus->endTransmission(); if (bh1750_error == 0) { type = BH1750; logFoundDevice("BH1750", (uint8_t)addr.address); } else { LOG_INFO("Device found at address 0x%x was not able to be enumerated", (uint8_t)addr.address); } } break; SCAN_SIMPLE_CASE(BHI260AP_ADDR, BHI260AP, "BHI260AP", (uint8_t)addr.address); case SCD4X_ADDR: { registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x8), 1); if (registerValue == 0x18) { logFoundDevice("CW2015", (uint8_t)addr.address); type = CW2015; } else { logFoundDevice("SCD4X", (uint8_t)addr.address); type = SCD4X; } break; } SCAN_SIMPLE_CASE(BMM150_ADDR, BMM150, "BMM150", (uint8_t)addr.address); #ifdef HAS_TPS65233 SCAN_SIMPLE_CASE(TPS65233_ADDR, TPS65233, "TPS65233", (uint8_t)addr.address); #endif case MLX90614_ADDR_DEF: // Do we have the MLX90614 or the MPR121KB or the CST226SE registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x06), 1); if (registerValue == 0xAB) { type = CST226SE; logFoundDevice("CST226SE", (uint8_t)addr.address); } else if (getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0e), 1) == 0x5a) { type = MLX90614; logFoundDevice("MLX90614", (uint8_t)addr.address); } else { registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1); // DRV2605_REG_STATUS if (registerValue == 0xe0) { type = DRV2605; logFoundDevice("DRV2605", (uint8_t)addr.address); } else { type = MPR121KB; logFoundDevice("MPR121KB", (uint8_t)addr.address); } } break; case ICM20948_ADDR: // same as BMX160_ADDR, BMI270_ADDR_ALT, and SEN5X_ADDR case ICM20948_ADDR_ALT: // same as MPU6050_ADDR, BMI270_ADDR registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1); #ifdef HAS_ICM20948 type = ICM20948; logFoundDevice("ICM20948", (uint8_t)addr.address); break; #endif if (registerValue == 0xEA) { type = ICM20948; logFoundDevice("ICM20948", (uint8_t)addr.address); break; } else if (registerValue == 0x24) { type = BMI270; logFoundDevice("BMI270", (uint8_t)addr.address); break; } else if (registerValue == 0xD8) { // BMX160 chip ID at register 0x00 type = BMX160; logFoundDevice("BMX160", (uint8_t)addr.address); break; } else { String prod = ""; prod = readSEN5xProductName(i2cBus, addr.address); if (prod.startsWith("SEN55")) { type = SEN5X; logFoundDevice("Sensirion SEN55", addr.address); break; } else if (prod.startsWith("SEN54")) { type = SEN5X; logFoundDevice("Sensirion SEN54", addr.address); break; } else if (prod.startsWith("SEN50")) { type = SEN5X; logFoundDevice("Sensirion SEN50", addr.address); break; } if (addr.address == BMX160_ADDR) { type = BMX160; logFoundDevice("BMX160", (uint8_t)addr.address); break; } else { type = MPU6050; logFoundDevice("MPU6050", (uint8_t)addr.address); break; } } break; case CGRADSENS_ADDR: // Register 0x00 of the RadSens sensor contains is product identifier 0x7D // Undocumented, but some devices return a product identifier of 0x7A registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1); if (registerValue == 0x7D || registerValue == 0x7A) { type = CGRADSENS; logFoundDevice("ClimateGuard RadSens", (uint8_t)addr.address); break; } else { LOG_DEBUG("Unexpected Device ID for RadSense: addr=0x%x id=0x%x", CGRADSENS_ADDR, registerValue); } break; case 0x48: { i2cBus->beginTransmission(addr.address); uint8_t getInfo[] = {0x5A, 0xC0, 0x00, 0xFF, 0xFC}; uint8_t expectedInfo[] = {0xa5, 0xE0, 0x00, 0x3F, 0x19}; uint8_t info[5]; size_t len = 0; i2cBus->write(getInfo, 5); i2cBus->endTransmission(); len = i2cBus->readBytes(info, 5); if (len == 5 && memcmp(expectedInfo, info, len) == 0) { LOG_INFO("NXP SE050 crypto chip found"); type = NXP_SE050; break; } registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x01), 2); if (registerValue == 0x8583 || registerValue == 0x8580) { type = ADS1115; logFoundDevice("ADS1115 ADC", (uint8_t)addr.address); break; } LOG_INFO("FT6336U touchscreen found"); type = FT6336U; break; } default: LOG_INFO("Device found at address 0x%x was not able to be enumerated", (uint8_t)addr.address); } } else if (err == 4) { LOG_ERROR("Unknown error at address 0x%x", (uint8_t)addr.address); } // Check if a type was found for the enumerated device - save, if so if (type != NONE) { deviceAddresses[type] = addr; foundDevices[addr] = type; } } } void ScanI2CTwoWire::scanPort(I2CPort port) { scanPort(port, nullptr, 0); } TwoWire *ScanI2CTwoWire::fetchI2CBus(ScanI2C::DeviceAddress address) { 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(); } void ScanI2CTwoWire::logFoundDevice(const char *device, uint8_t address) { LOG_INFO("%s found at address 0x%x", device, address); } #endif ================================================ FILE: src/detect/ScanI2CTwoWire.h ================================================ #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; bool exists(ScanI2C::DeviceType) const override; size_t countDevices() const override; static TwoWire *fetchI2CBus(ScanI2C::DeviceAddress); 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; uint16_t getRegisterValue(const RegisterLocation &, ResponseWidth, bool) const; bool i2cCommandResponseLength(DeviceAddress addr, uint16_t command, uint8_t expectedLength) const; DeviceType probeOLED(ScanI2C::DeviceAddress) const; static void logFoundDevice(const char *device, uint8_t address); }; #endif ================================================ FILE: src/detect/einkScan.h ================================================ #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 ================================================ FILE: src/detect/reClockI2C.cpp ================================================ #include "reClockI2C.h" #include "ScanI2CTwoWire.h" uint32_t reClockI2C(uint32_t desiredClock, TwoWire *i2cBus, bool force) { uint32_t currentClock = 0; /* See https://github.com/arduino/Arduino/issues/11457 Currently, only ESP32 can getClock() While all cores can setClock() https://github.com/sandeepmistry/arduino-nRF5/blob/master/libraries/Wire/Wire.h#L50 https://github.com/earlephilhower/arduino-pico/blob/master/libraries/Wire/src/Wire.h#L60 https://github.com/stm32duino/Arduino_Core_STM32/blob/main/libraries/Wire/src/Wire.h#L103 For cases when I2C speed is different to the ones defined by sensors (see defines in sensor classes) we need to reclock I2C and set it back to the previous desired speed. Only for cases where we can know OR predefine the speed, we can do this. */ // TODO add getClock function or return a predefined clock speed per variant? #ifdef CAN_RECLOCK_I2C currentClock = i2cBus->getClock(); #endif if ((currentClock != desiredClock) || force) { LOG_DEBUG("Changing I2C clock to %u", desiredClock); i2cBus->setClock(desiredClock); } return currentClock; } ================================================ FILE: src/detect/reClockI2C.h ================================================ #ifndef RECLOCK_I2C_ #define RECLOCK_I2C_ #include "ScanI2CTwoWire.h" #include #include uint32_t reClockI2C(uint32_t desiredClock, TwoWire *i2cBus, bool force); #endif ================================================ FILE: src/error.h ================================================ #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); ================================================ FILE: src/freertosinc.h ================================================ #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 ================================================ FILE: src/gps/GPS.cpp ================================================ #include // Include for strstr #include #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 "concurrency/Periodic.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 // Not all platforms have std::size(). template std::size_t array_count(const T (&)[N]) { return N; } #ifndef GPS_SERIAL_PORT #define GPS_SERIAL_PORT Serial1 #endif #if defined(ARCH_NRF52) Uart *GPS::_serial_gps = &GPS_SERIAL_PORT; #elif defined(ARCH_ESP32) || defined(ARCH_PORTDUINO) || defined(ARCH_STM32WL) HardwareSerial *GPS::_serial_gps = &GPS_SERIAL_PORT; #elif defined(ARCH_RP2040) SerialUART *GPS::_serial_gps = &GPS_SERIAL_PORT; #else HardwareSerial *GPS::_serial_gps = nullptr; #endif std::unique_ptr gps = nullptr; static GPSUpdateScheduling scheduling; /// Multiple GPS instances might use the same serial port (in sequence), but we can /// only init that port once. static bool didSerialInit; static struct uBloxGnssModelInfo { char swVersion[30]; char hwVersion[10]; uint8_t extensionNo; char extension[10][30]; uint8_t protocol_version; } ublox_info; #define GPS_SOL_EXPIRY_MS 5000 // in millis. give 1 second time to combine different sentences. NMEA Frequency isn't higher anyway #define NMEA_MSG_GXGSA "GNGSA" // GSA message (GPGSA, GNGSA etc) // For logging static const char *getGPSPowerStateString(GPSPowerState state) { switch (state) { case GPS_ACTIVE: return "ACTIVE"; case GPS_IDLE: return "IDLE"; case GPS_SOFTSLEEP: return "SOFTSLEEP"; case GPS_HARDSLEEP: return "HARDSLEEP"; case GPS_OFF: return "OFF"; default: assert(false); // Unhandled enum value.. return "FALSE"; // to make new ESP-IDF happy } } #ifdef PIN_GPS_SWITCH // If we have a hardware switch, define a periodic watcher outside of the GPS runOnce thread, since this can be sleeping // indefinitely int lastState = LOW; bool firstrun = true; static int32_t gpsSwitch() { if (gps) { int currentState = digitalRead(PIN_GPS_SWITCH); // Respect explicit NOT_PRESENT mode and do not let the hardware switch re-enable GPS. if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT) { gps->disable(); lastState = currentState; firstrun = false; return 1000; } // if the switch is set to zero, disable the GPS Thread if (firstrun) if (currentState == LOW) lastState = HIGH; if (currentState != lastState) { if (currentState == LOW) { config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_DISABLED; if (!firstrun) playGPSDisableBeep(); gps->disable(); } else { config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_ENABLED; if (!firstrun) playGPSEnableBeep(); gps->enable(); } lastState = currentState; } firstrun = false; } return 1000; } static std::unique_ptr gpsPeriodic; #endif static void UBXChecksum(uint8_t *message, size_t length) { uint8_t CK_A = 0, CK_B = 0; // Calculate the checksum, starting from the CLASS field (which is message[2]) for (size_t i = 2; i < length - 2; i++) { CK_A = (CK_A + message[i]) & 0xFF; CK_B = (CK_B + CK_A) & 0xFF; } // Place the calculated checksum values in the message message[length - 2] = CK_A; message[length - 1] = CK_B; } // Calculate the checksum for a CAS packet static void CASChecksum(uint8_t *message, size_t length) { uint32_t cksum = ((uint32_t)message[5] << 24); // Message ID cksum += ((uint32_t)message[4]) << 16; // Class cksum += message[2]; // Payload Len // 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("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')) { #ifdef GPS_DEBUG LOG_DEBUG(debugmsg.c_str()); #endif 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; } } } } 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 %dms", class_id, msg_id, millis() - startTime); #endif return GNSS_RESPONSE_OK; } // Check for an ACK-NACK for the specified class and message id if ((msg_cls == 0x05) && (msg_msg_id == 0x00) && payload_cls == class_id && payload_msg == msg_id) { #ifdef GPS_DEBUG LOG_WARN("Got NACK for class %02X message %02X in %dms", class_id, msg_id, millis() - startTime); #endif return GNSS_RESPONSE_NAK; } // This isn't the frame we are looking for, clear the buffer // and try again until we run out of time. memset(buffer, 0x0, sizeof(buffer)); bufferPos = 0; } } return GNSS_RESPONSE_NONE; } GPS_RESPONSE GPS::getACK(uint8_t class_id, uint8_t msg_id, uint32_t waitMillis) { uint8_t b; uint8_t ack = 0; const uint8_t ackP[2] = {class_id, msg_id}; uint8_t buf[10] = {0xB5, 0x62, 0x05, 0x01, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00}; uint32_t startTime = millis(); const char frame_errors[] = "More than 100 frame errors"; int sCounter = 0; #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_INFO("Got ACK for class %02X message %02X in %dms", class_id, msg_id, millis() - startTime); #endif return GNSS_RESPONSE_OK; // ACK received } if (_serial_gps->available()) { b = _serial_gps->read(); if (b == frame_errors[sCounter]) { sCounter++; if (sCounter == 26) { #ifdef GPS_DEBUG LOG_DEBUG(debugmsg.c_str()); #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 %dms", requestedClass, requestedID, millis() - startTime); #endif return needRead; } break; default: break; } } } return 0; } #if GPS_BAUDRATE_FIXED // if GPS_BAUDRATE is specified in variant, only try that. static const int serialSpeeds[1] = {GPS_BAUDRATE}; static const int rareSerialSpeeds[1] = {GPS_BAUDRATE}; #else static const int serialSpeeds[3] = {9600, 115200, 38400}; static const int rareSerialSpeeds[3] = {4800, 57600, GPS_BAUDRATE}; #endif #ifndef GPS_PROBETRIES #define GPS_PROBETRIES 2 #endif /** * @brief Setup the GPS based on the model detected. * We detect the GPS by cycling through a set of baud rates, first common then rare. * For each baud rate, we run GPS::Probe to send commands and match the responses * to known GPS responses. * @retval Whether setup reached the end of its potential to configure the GPS. */ bool GPS::setup() { if (!didSerialInit) { int msglen = 0; if (tx_gpio && gnssModel == GNSS_MODEL_UNKNOWN) { if (probeTries < GPS_PROBETRIES) { gnssModel = probe(serialSpeeds[speedSelect]); if (gnssModel == GNSS_MODEL_UNKNOWN) { if (currentStep == 0 && ++speedSelect == array_count(serialSpeeds)) { speedSelect = 0; ++probeTries; } } } // Rare Serial Speeds #ifndef CONFIG_IDF_TARGET_ESP32C6 if (probeTries == GPS_PROBETRIES) { gnssModel = probe(rareSerialSpeeds[speedSelect]); if (gnssModel == GNSS_MODEL_UNKNOWN) { if (currentStep == 0 && ++speedSelect == array_count(rareSerialSpeeds)) { LOG_WARN("Give up on GPS probe and set to %d", GPS_BAUDRATE); return true; } } } #endif } if (gnssModel != GNSS_MODEL_UNKNOWN) { setConnected(); } else { return false; } if (gnssModel == GNSS_MODEL_MTK) { /* * t-beam-s3-core uses the same L76K GNSS module as t-echo. * Unlike t-echo, L76K uses 9600 baud rate for communication by default. * */ // Initialize the L76K Chip, use GPS + GLONASS + BEIDOU _serial_gps->write("$PCAS04,7*1E\r\n"); delay(250); // only ask for RMC and GGA _serial_gps->write("$PCAS03,1,0,0,0,1,0,0,0,0,0,,,0,0*02\r\n"); delay(250); // Switch to Vehicle Mode, since SoftRF enables Aviation < 2g _serial_gps->write("$PCAS11,3*1E\r\n"); delay(250); } else if (gnssModel == GNSS_MODEL_MTK_L76B) { // Waveshare Pico-GPS hat uses the L76B with 9600 baud // Initialize the L76B Chip, use GPS + GLONASS // See note in L76_Series_GNSS_Protocol_Specification, chapter 3.29 _serial_gps->write("$PMTK353,1,1,0,0,0*2B\r\n"); // Above command will reset the GPS and takes longer before it will accept new commands delay(1000); // only ask for RMC and GGA (GNRMC and GNGGA) // See note in L76_Series_GNSS_Protocol_Specification, chapter 2.1 _serial_gps->write("$PMTK314,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0*28\r\n"); delay(250); // Enable SBAS _serial_gps->write("$PMTK301,2*2E\r\n"); delay(250); // Enable PPS for 2D/3D fix only _serial_gps->write("$PMTK285,3,100*3F\r\n"); delay(250); // Switch to Fitness Mode, for running and walking purpose with low speed (<5 m/s) _serial_gps->write("$PMTK886,1*29\r\n"); delay(250); } else if (gnssModel == GNSS_MODEL_MTK_PA1010D) { // PA1010D is used in the Pimoroni GPS board. // Enable all constellations. _serial_gps->write("$PMTK353,1,1,1,1,1*2A\r\n"); // Above command will reset the GPS and takes longer before it will accept new commands delay(1000); // Only ask for RMC and GGA (GNRMC and GNGGA) _serial_gps->write("$PMTK314,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0*28\r\n"); delay(250); // Enable SBAS / WAAS _serial_gps->write("$PMTK301,2*2E\r\n"); delay(250); } else if (gnssModel == GNSS_MODEL_MTK_PA1616S) { // PA1616S is used in some GPS breakout boards from Adafruit // PA1616S does not have GLONASS capability. PA1616D does, but is not implemented here. _serial_gps->write("$PMTK353,1,0,0,0,0*2A\r\n"); // Above command will reset the GPS and takes longer before it will accept new commands delay(1000); // Only ask for RMC and GGA (GNRMC and GNGGA) _serial_gps->write("$PMTK314,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0*28\r\n"); delay(250); // Enable SBAS / WAAS _serial_gps->write("$PMTK301,2*2E\r\n"); delay(250); } else if (gnssModel == GNSS_MODEL_ATGM336H) { // Set the initial configuration of the device - these _should_ work for most AT6558 devices msglen = makeCASPacket(0x06, 0x07, sizeof(_message_CAS_CFG_NAVX_CONF), _message_CAS_CFG_NAVX_CONF); _serial_gps->write(UBXscratch, msglen); if (getACKCas(0x06, 0x07, 250) != GNSS_RESPONSE_OK) { LOG_WARN("ATGM336H: Could not set Config"); } // Set the update frequency to 1Hz msglen = makeCASPacket(0x06, 0x04, sizeof(_message_CAS_CFG_RATE_1HZ), _message_CAS_CFG_RATE_1HZ); _serial_gps->write(UBXscratch, msglen); if (getACKCas(0x06, 0x04, 250) != GNSS_RESPONSE_OK) { LOG_WARN("ATGM336H: Could not set Update Frequency"); } // Set the NEMA output messages // Ask for only RMC and GGA uint8_t fields[] = {CAS_NEMA_RMC, CAS_NEMA_GGA}; for (unsigned int i = 0; i < sizeof(fields); i++) { // Construct a CAS-CFG-MSG packet uint8_t cas_cfg_msg_packet[] = {0x4e, fields[i], 0x01, 0x00}; msglen = makeCASPacket(0x06, 0x01, sizeof(cas_cfg_msg_packet), cas_cfg_msg_packet); _serial_gps->write(UBXscratch, msglen); if (getACKCas(0x06, 0x01, 250) != GNSS_RESPONSE_OK) { LOG_WARN("ATGM336H: Could not enable NMEA MSG: %d", fields[i]); } } } else if (gnssModel == GNSS_MODEL_UC6580) { // The Unicore UC6580 can use a lot of sat systems, enable it to // use GPS L1 & L5 + BDS B1I & B2a + GLONASS L1 + GALILEO E1 & E5a + SBAS + QZSS // This will reset the receiver, so wait a bit afterwards // The paranoid will wait for the OK*04 confirmation response after each command. _serial_gps->write("$CFGSYS,h35155\r\n"); delay(750); // Must be done after the CFGSYS command // Turn off GSV messages, we don't really care about which and where the sats are, maybe someday. _serial_gps->write("$CFGMSG,0,3,0\r\n"); delay(250); // Turn off GSA messages, TinyGPS++ doesn't use this message. _serial_gps->write("$CFGMSG,0,2,0\r\n"); delay(250); // Turn off NOTICE __TXT messages, these may provide Unicore some info but we don't care. _serial_gps->write("$CFGMSG,6,0,0\r\n"); delay(250); _serial_gps->write("$CFGMSG,6,1,0\r\n"); delay(250); } else if (IS_ONE_OF(gnssModel, GNSS_MODEL_AG3335, GNSS_MODEL_AG3352)) { if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_IN || config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_NP_865) { _serial_gps->write("$PAIR066,1,0,1,0,0,1*3B\r\n"); // Enable GPS+GALILEO+NAVIC // GPS GLONASS GALILEO BDS QZSS NAVIC // 1 0 1 0 0 1 } else { _serial_gps->write("$PAIR066,1,1,1,1,0,0*3A\r\n"); // Enable GPS+GLONASS+GALILEO+BDS // GPS GLONASS GALILEO BDS QZSS NAVIC // 1 1 1 1 0 0 } // Configure NMEA (sentences will output once per fix) _serial_gps->write("$PAIR062,0,1*3F\r\n"); // GGA ON _serial_gps->write("$PAIR062,1,0*3F\r\n"); // GLL OFF _serial_gps->write("$PAIR062,2,0*3C\r\n"); // GSA OFF _serial_gps->write("$PAIR062,3,0*3D\r\n"); // GSV OFF _serial_gps->write("$PAIR062,4,1*3B\r\n"); // RMC ON _serial_gps->write("$PAIR062,5,0*3B\r\n"); // VTG OFF _serial_gps->write("$PAIR062,6,0*38\r\n"); // ZDA ON delay(250); _serial_gps->write("$PAIR513*3D\r\n"); // save configuration } else if (gnssModel == GNSS_MODEL_UBLOX6) { clearBuffer(); SEND_UBX_PACKET(0x06, 0x02, _message_DISABLE_TXT_INFO, "disable text info messages", 500); SEND_UBX_PACKET(0x06, 0x39, _message_JAM_6_7, "enable interference resistance", 500); SEND_UBX_PACKET(0x06, 0x23, _message_NAVX5, "configure NAVX5 settings", 500); // Turn off unwanted NMEA messages, set update rate SEND_UBX_PACKET(0x06, 0x08, _message_1HZ, "set GPS update rate", 500); SEND_UBX_PACKET(0x06, 0x01, _message_GLL, "disable NMEA GLL", 500); SEND_UBX_PACKET(0x06, 0x01, _message_GSA, "enable NMEA GSA", 500); SEND_UBX_PACKET(0x06, 0x01, _message_GSV, "disable NMEA GSV", 500); SEND_UBX_PACKET(0x06, 0x01, _message_VTG, "disable NMEA VTG", 500); SEND_UBX_PACKET(0x06, 0x01, _message_RMC, "enable NMEA RMC", 500); SEND_UBX_PACKET(0x06, 0x01, _message_GGA, "enable NMEA GGA", 500); clearBuffer(); SEND_UBX_PACKET(0x06, 0x11, _message_CFG_RXM_ECO, "enable powersave ECO mode for Neo-6", 500); SEND_UBX_PACKET(0x06, 0x3B, _message_CFG_PM2, "enable powersave details for GPS", 500); SEND_UBX_PACKET(0x06, 0x01, _message_AID, "disable UBX-AID", 500); msglen = makeUBXPacket(0x06, 0x09, sizeof(_message_SAVE), _message_SAVE); _serial_gps->write(UBXscratch, msglen); if (getACK(0x06, 0x09, 2000) != GNSS_RESPONSE_OK) { LOG_WARN("Unable to save GNSS module config"); } else { LOG_INFO("GNSS module config saved!"); } } else if (IS_ONE_OF(gnssModel, GNSS_MODEL_UBLOX7, GNSS_MODEL_UBLOX8, GNSS_MODEL_UBLOX9)) { if (gnssModel == GNSS_MODEL_UBLOX7) { LOG_DEBUG("Set GPS+SBAS"); msglen = makeUBXPacket(0x06, 0x3e, sizeof(_message_GNSS_7), _message_GNSS_7); _serial_gps->write(UBXscratch, msglen); } else { // 8,9 msglen = makeUBXPacket(0x06, 0x3e, sizeof(_message_GNSS_8), _message_GNSS_8); _serial_gps->write(UBXscratch, msglen); } if (getACK(0x06, 0x3e, 800) == GNSS_RESPONSE_NAK) { // It's not critical if the module doesn't acknowledge this configuration. LOG_DEBUG("reconfigure GNSS - defaults maintained. Is this module GPS-only?"); } else { if (gnssModel == GNSS_MODEL_UBLOX7) { LOG_INFO("GPS+SBAS configured"); } else { // 8,9 LOG_INFO("GPS+SBAS+GLONASS+Galileo configured"); } // Documentation say, we need wait at least 0.5s after reconfiguration of GNSS module, before sending next // commands for the M8 it tends to be more... 1 sec should be enough ;>) delay(1000); } // Disable Text Info messages //6,7,8,9 clearBuffer(); SEND_UBX_PACKET(0x06, 0x02, _message_DISABLE_TXT_INFO, "disable text info messages", 500); if (gnssModel == GNSS_MODEL_UBLOX8) { // 8 clearBuffer(); SEND_UBX_PACKET(0x06, 0x39, _message_JAM_8, "enable interference resistance", 500); clearBuffer(); SEND_UBX_PACKET(0x06, 0x23, _message_NAVX5_8, "configure NAVX5_8 settings", 500); } else { // 6,7,9 SEND_UBX_PACKET(0x06, 0x39, _message_JAM_6_7, "enable interference resistance", 500); SEND_UBX_PACKET(0x06, 0x23, _message_NAVX5, "configure NAVX5 settings", 500); } // Turn off unwanted NMEA messages, set update rate SEND_UBX_PACKET(0x06, 0x08, _message_1HZ, "set GPS update rate", 500); SEND_UBX_PACKET(0x06, 0x01, _message_GLL, "disable NMEA GLL", 500); SEND_UBX_PACKET(0x06, 0x01, _message_GSA, "enable NMEA GSA", 500); SEND_UBX_PACKET(0x06, 0x01, _message_GSV, "disable NMEA GSV", 500); SEND_UBX_PACKET(0x06, 0x01, _message_VTG, "disable NMEA VTG", 500); SEND_UBX_PACKET(0x06, 0x01, _message_RMC, "enable NMEA RMC", 500); SEND_UBX_PACKET(0x06, 0x01, _message_GGA, "enable NMEA GGA", 500); if (ublox_info.protocol_version >= 18) { clearBuffer(); SEND_UBX_PACKET(0x06, 0x86, _message_PMS, "enable powersave for GPS", 500); SEND_UBX_PACKET(0x06, 0x3B, _message_CFG_PM2, "enable powersave details for GPS", 500); // For M8 we want to enable NMEA version 4.10 so we can see the additional sats. if (gnssModel == GNSS_MODEL_UBLOX8) { clearBuffer(); SEND_UBX_PACKET(0x06, 0x17, _message_NMEA, "enable NMEA 4.10", 500); } } else { SEND_UBX_PACKET(0x06, 0x11, _message_CFG_RXM_PSM, "enable powersave mode for GPS", 500); SEND_UBX_PACKET(0x06, 0x3B, _message_CFG_PM2, "enable powersave details for GPS", 500); } msglen = makeUBXPacket(0x06, 0x09, sizeof(_message_SAVE), _message_SAVE); _serial_gps->write(UBXscratch, msglen); if (getACK(0x06, 0x09, 2000) != GNSS_RESPONSE_OK) { LOG_WARN("Unable to save GNSS module config"); } else { LOG_INFO("GNSS module configuration saved!"); } } else if (gnssModel == GNSS_MODEL_UBLOX10) { delay(1000); clearBuffer(); SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_DISABLE_NMEA_RAM, "disable NMEA messages in M10 RAM", 300); delay(750); clearBuffer(); SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_DISABLE_NMEA_BBR, "disable NMEA messages in M10 BBR", 300); delay(750); clearBuffer(); SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_DISABLE_TXT_INFO_RAM, "disable Info messages for M10 GPS RAM", 300); delay(750); // Next disable Info txt messages in BBR layer clearBuffer(); SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_DISABLE_TXT_INFO_BBR, "disable Info messages for M10 GPS BBR", 300); delay(750); // Do M10 configuration for Power Management. SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_PM_RAM, "enable powersave for M10 GPS RAM", 300); delay(750); SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_PM_BBR, "enable powersave for M10 GPS BBR", 300); delay(750); SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_ITFM_RAM, "enable jam detection M10 GPS RAM", 300); delay(750); SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_ITFM_BBR, "enable jam detection M10 GPS BBR", 300); delay(750); // Here is where the init commands should go to do further M10 initialization. SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_DISABLE_SBAS_RAM, "disable SBAS M10 GPS RAM", 300); delay(750); // will cause a receiver restart so wait a bit SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_DISABLE_SBAS_BBR, "disable SBAS M10 GPS BBR", 300); delay(750); // will cause a receiver restart so wait a bit // Done with initialization, Now enable wanted NMEA messages in BBR layer so they will survive a periodic // sleep. SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_ENABLE_NMEA_BBR, "enable messages for M10 GPS BBR", 300); delay(750); // Next enable wanted NMEA messages in RAM layer SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_ENABLE_NMEA_RAM, "enable messages for M10 GPS RAM", 500); delay(750); // As the M10 has no flash, the best we can do to preserve the config is to set it in RAM and BBR. // BBR will survive a restart, and power off for a while, but modules with small backup // batteries or super caps will not retain the config for a long power off time. msglen = makeUBXPacket(0x06, 0x09, sizeof(_message_SAVE_10), _message_SAVE_10); _serial_gps->write(UBXscratch, msglen); if (getACK(0x06, 0x09, 2000) != GNSS_RESPONSE_OK) { LOG_WARN("Unable to save GNSS module config"); } else { LOG_INFO("GNSS module configuration saved!"); } } else if (gnssModel == GNSS_MODEL_CM121) { // only ask for RMC and GGA // enable GGA _serial_gps->write("$CFGMSG,0,0,1,1*1B\r\n"); delay(250); // enable RMC _serial_gps->write("$CFGMSG,0,4,1,1*1F\r\n"); delay(250); } didSerialInit = true; } notifyDeepSleepObserver.observe(¬ifyDeepSleep); return true; } GPS::~GPS() { // we really should unregister our sleep observer notifyDeepSleepObserver.unobserve(¬ifyDeepSleep); } // Put the GPS hardware into a specified state void GPS::setPowerState(GPSPowerState newState, uint32_t sleepTime) { // Update the stored GPSPowerstate, and create local copies GPSPowerState oldState = powerState; powerState = newState; LOG_INFO("GPS power state move from %s to %s", getGPSPowerStateString(oldState), getGPSPowerStateString(newState)); 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 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 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 || HW_VENDOR == meshtastic_HardwareModel_WISMESH_TAP) && (rotaryEncoderInterruptImpl1 || upDownInterruptImpl1)) return; // Write and log enablePin->set(on); #ifdef GPS_DEBUG LOG_DEBUG("Pin EN %s", on == HIGH ? "HI" : "LOW"); #endif } // Set the value of the STANDBY pin, if relevant // true for standby state, false for awake void GPS::writePinStandby(bool standby) { #ifdef PIN_GPS_STANDBY // Specifically the standby pin for L76B, L76K and clones bool val; if (standby) val = GPS_STANDBY_ACTIVE; else val = !GPS_STANDBY_ACTIVE; // Write and log pinMode(PIN_GPS_STANDBY, OUTPUT); digitalWrite(PIN_GPS_STANDBY, val); // Enter backup mode on PA1010D; TODO: may be applicable to other MTK GPS too if (IS_ONE_OF(gnssModel, GNSS_MODEL_MTK_PA1010D)) { _serial_gps->write("$PMTK225,4*2F\r\n"); } #ifdef GPS_DEBUG LOG_DEBUG("Pin STANDBY %s", val == HIGH ? "HI" : "LOW"); #endif #endif } // Enable / Disable GPS with PMU, if present void GPS::setPowerPMU(bool on) { // We only have PMUs on the T-Beam, and that board has a tiny battery to save GPS ephemera, // so treat as a standby. #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 (HW_VENDOR == meshtastic_HardwareModel_T_WATCH_S3) { // t-watch-s3-plus GNSS power channel on ? PMU->enablePowerOutput(XPOWERS_BLDO1) : PMU->disablePowerOutput(XPOWERS_BLDO1); } } else if (model == XPOWERS_AXP192) { // t-beam v1.1 GNSS power channel on ? PMU->enablePowerOutput(XPOWERS_LDO3) : PMU->disablePowerOutput(XPOWERS_LDO3); } #ifdef GPS_DEBUG LOG_DEBUG("PMU %s", on ? "on" : "off"); #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 } // 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++) _message_PMREQ[0 + i] = sleepMs >> (i * 8); // Record the message length msglen = gps->makeUBXPacket(0x02, 0x41, sizeof(_message_PMREQ), _message_PMREQ); } else { // Encode the sleep time in millis into the packet for (int i = 0; i < 4; i++) _message_PMREQ_10[4 + i] = sleepMs >> (i * 8); // Record the message length msglen = gps->makeUBXPacket(0x02, 0x41, sizeof(_message_PMREQ_10), _message_PMREQ_10); } // Send the UBX packet gps->_serial_gps->write(gps->UBXscratch, msglen); #ifdef GPS_DEBUG LOG_DEBUG("UBLOX: sleep for %dmS", sleepMs); #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 <= GPS_UPDATE_ALWAYS_ON_THRESHOLD_MS || sleepTime == 0) setPowerState(GPS_IDLE); else { // Check whether the GPS hardware is capable of GPS_SOFTSLEEP // If not, fallback to GPS_HARDSLEEP instead #ifdef PIN_GPS_STANDBY // L76B, L76K and clones have a standby pin bool softsleepSupported = true; #else bool softsleepSupported = false; #endif // U-blox is supported via PMREQ if (IS_ONE_OF(gnssModel, GNSS_MODEL_UBLOX6, GNSS_MODEL_UBLOX7, GNSS_MODEL_UBLOX8, GNSS_MODEL_UBLOX9, GNSS_MODEL_UBLOX10)) softsleepSupported = true; if (softsleepSupported) { // How long does gps_update_interval need to be, for GPS_HARDSLEEP to become more efficient than // GPS_SOFTSLEEP? Heuristic equation. A compromise manually fitted to power observations from U-blox NEO-6M // and M10050 https://www.desmos.com/calculator/6gvjghoumr This is not particularly accurate, but probably an // improvement over a single, fixed threshold uint32_t hardsleepThreshold = (2750 * pow(predictedSearchDuration / 1000, 1.22)); LOG_DEBUG("gps_update_interval >= %us needed to justify hardsleep", hardsleepThreshold / 1000); // If update interval too short: softsleep (if supported by hardware) if (updateInterval < hardsleepThreshold) { setPowerState(GPS_SOFTSLEEP, sleepTime); return; } } // If update interval long enough (or softsleep unsupported): hardsleep instead setPowerState(GPS_HARDSLEEP, sleepTime); // Reset the fix quality to 0, since we're off. fixQual = 0; } } void GPS::publishUpdate() { if (shouldPublish) { shouldPublish = false; // In debug logs, identify position by @timestamp:stage (stage 2 = publish) LOG_DEBUG("Publish pos@%x:2, hasVal=%d, Sats=%d, GPSlock=%d", p.timestamp, hasValidLocation, p.sats_in_view, hasLock()); // Notify any status instances that are observing us const meshtastic::GPSStatus status = meshtastic::GPSStatus(hasValidLocation, isConnected(), isPowerSaving(), p); newStatus.notifyObservers(&status); if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) { positionModule->handleNewPosition(); } } } int32_t GPS::runOnce() { if (!GPSInitFinished) { if (!_serial_gps || config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT) { LOG_INFO("GPS set to not-present. Skip probe"); return disable(); } if (!setup()) return currentDelay; // Setup failed, re-run in two seconds // We have now loaded our saved preferences from flash if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) { return disable(); } GPSInitFinished = true; publishUpdate(); } // ======================== GPS_ACTIVE state ======================== // In GPS_ACTIVE state, GPS is powered on and we're receiving NMEA messages. // We use the following logic to determine when to update the local position // or time by running GPS::publishUpdate. // Note: Local position update is asynchronous to position broadcast. We // generally run this state every gps_update_interval seconds, and in most cases // gps_update_interval is faster than the position broadcast interval so there's a // fresh position ready when the device wants to broadcast one on the mesh. // // 1. Got a time for the first time --> set the time, don't publish. // 2. Got a lock for the first time // --> If gps_update_interval is <= 10s --> publishUpdate // --> Otherwise, hold for MIN(gps_update_interval - GPS_UPDATE_ALWAYS_ON_THRESHOLD_MS, 20s) // 3. Got a lock after turning back on // --> If gps_update_interval is <= 10s --> publishUpdate // --> Otherwise, hold for MIN(gps_update_interval - GPS_UPDATE_ALWAYS_ON_THRESHOLD_MS, 20s) // 4. Hold has expired // --> If we have a time and a location --> publishUpdate // --> down() // 5. Search time has expired // --> If we have a time and a location --> publishUpdate // --> If we had a location before but don't now --> publishUpdate // --> down() if (whileActive()) { // if we have received valid NMEA claim we are connected setConnected(); } // If we're due for an update, wake the GPS if (!config.position.fixed_position && powerState != GPS_ACTIVE && scheduling.isUpdateDue()) up(); // quality of the previous fix. We set it to 0 when we go down, so it's a way // to check if we're getting a lock after being GPS_OFF. uint8_t prev_fixQual = fixQual; if (powerState == GPS_ACTIVE) { // if gps_update_interval is <=10s, GPS never goes off, so we treat that differently uint32_t updateInterval = Default::getConfiguredOrDefaultMs(config.position.gps_update_interval); // 1. Got a time for the first time bool gotTime = (getRTCQuality() >= RTCQualityGPS); if (!gotTime && lookForTime()) { // Note: we count on this && short-circuiting and not resetting the RTC time gotTime = true; } // 2. Got a lock for the first time, or 3. Got a lock after turning back on bool gotLoc = lookForLocation(); if (gotLoc) { #ifdef GPS_DEBUG if (!hasValidLocation) { // declare that we have location ASAP LOG_DEBUG("hasValidLocation RISING EDGE"); } #endif if (updateInterval <= GPS_UPDATE_ALWAYS_ON_THRESHOLD_MS) { hasValidLocation = true; shouldPublish = true; } else if (!hasValidLocation || prev_fixQual == 0 || (fixHoldEnds + GPS_THREAD_INTERVAL) < millis()) { hasValidLocation = true; // Hold for up to 20secs after getting a lock to download ephemeris etc uint32_t holdTime = updateInterval - GPS_UPDATE_ALWAYS_ON_THRESHOLD_MS; if (holdTime > GPS_FIX_HOLD_MAX_MS) holdTime = GPS_FIX_HOLD_MAX_MS; fixHoldEnds = millis() + holdTime; #ifdef GPS_DEBUG LOG_DEBUG("Holding for %ums after lock", holdTime); #endif } } bool tooLong = scheduling.searchedTooLong(); if (tooLong && !gotLoc) { LOG_WARN("Couldn't publish a valid location: didn't get a GPS lock in time"); // we didn't get a location during this ack window, therefore declare loss of lock if (hasValidLocation) { p = meshtastic_Position_init_default; hasValidLocation = false; shouldPublish = true; #ifdef GPS_DEBUG LOG_DEBUG("hasValidLocation FALLING EDGE"); #endif } } // Hold has expired , Search time has expired, we got a time only, or we never needed to hold. bool holdExpired = (fixHoldEnds != 0 && millis() > fixHoldEnds); if (shouldPublish || tooLong || holdExpired) { if (gotTime && hasValidLocation) { shouldPublish = true; } if (shouldPublish) { fixHoldEnds = 0; publishUpdate(); } // There's a chance we just got a time, so keep going to see if we can get a location too if (tooLong || holdExpired) { down(); } #ifdef GPS_DEBUG } else if (fixHoldEnds != 0) { LOG_DEBUG("Holding for GPS data download: %d ms (numSats=%d)", fixHoldEnds - millis(), p.sats_in_view); #endif } } // ===================== end GPS_ACTIVE state ======================== 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 infrequently (once every 5 secs?) to see if we need to wake. return (powerState == GPS_ACTIVE) ? GPS_THREAD_INTERVAL : 5000; } // clear the GPS rx/tx buffer as quickly as possible void GPS::clearBuffer() { #ifdef ARCH_ESP32 _serial_gps->flush(false); #else int x = _serial_gps->available(); while (x--) _serial_gps->read(); #endif } /// Prepare the GPS for the cpu entering deep or light sleep, expect to be gone for at least 100s of msecs int GPS::prepareDeepSleep(void *unused) { LOG_INFO("GPS deep sleep!"); disable(); return 0; } static const char *PROBE_MESSAGE = "Trying %s (%s)..."; static const char *DETECTED_MESSAGE = "%s detected"; #define PROBE_SIMPLE(CHIP, TOWRITE, RESPONSE, DRIVER, TIMEOUT, ...) \ do { \ LOG_DEBUG(PROBE_MESSAGE, TOWRITE, CHIP); \ clearBuffer(); \ _serial_gps->write(TOWRITE "\r\n"); \ if (getACK(RESPONSE, TIMEOUT) == GNSS_RESPONSE_OK) { \ LOG_INFO(DETECTED_MESSAGE, CHIP); \ return DRIVER; \ } \ } while (0) #define PROBE_FAMILY(FAMILY_NAME, COMMAND, RESPONSE_MAP, TIMEOUT) \ do { \ LOG_DEBUG(PROBE_MESSAGE, COMMAND, FAMILY_NAME); \ clearBuffer(); \ _serial_gps->write(COMMAND "\r\n"); \ GnssModel_t detectedDriver = getProbeResponse(TIMEOUT, RESPONSE_MAP, serialSpeed); \ if (detectedDriver != GNSS_MODEL_UNKNOWN) { \ return detectedDriver; \ } \ } while (0) GnssModel_t GPS::probe(int serialSpeed) { uint8_t buffer[768] = {0}; switch (currentStep) { case 0: { #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("Set GPS Baud to %i", serialSpeed); _serial_gps->updateBaudRate(serialSpeed); } #endif memset(&ublox_info, 0, sizeof(ublox_info)); delay(100); #if defined(PIN_GPS_RESET) && PIN_GPS_RESET != -1 digitalWrite(PIN_GPS_RESET, GPS_RESET_MODE); // assert for 10ms delay(10); digitalWrite(PIN_GPS_RESET, !GPS_RESET_MODE); // attempt to detect the chip based on boot messages std::vector passive_detect = { {"AG3335", "$PAIR021,AG3335", GNSS_MODEL_AG3335}, {"AG3352", "$PAIR021,AG3352", GNSS_MODEL_AG3352}, {"RYS3520", "$PAIR021,REYAX_RYS3520_V2", GNSS_MODEL_AG3352}, {"UC6580", "UC6580", GNSS_MODEL_UC6580}, // as L76K is sort of a last ditch effort, we won't attempt to detect it by startup messages for now. /*{"L76K", "SW=URANUS", GNSS_MODEL_MTK}*/}; GnssModel_t detectedDriver = getProbeResponse(500, passive_detect, serialSpeed); if (detectedDriver != GNSS_MODEL_UNKNOWN) { return detectedDriver; } #endif // Close all NMEA sentences, valid for L76K, ATGM336H (and likely other AT6558 devices) _serial_gps->write("$PCAS03,0,0,0,0,0,0,0,0,0,0,,,0,0*02\r\n"); delay(20); // Close NMEA sequences on Ublox _serial_gps->write("$PUBX,40,GLL,0,0,0,0,0,0*5C\r\n"); _serial_gps->write("$PUBX,40,GSV,0,0,0,0,0,0*59\r\n"); _serial_gps->write("$PUBX,40,VTG,0,0,0,0,0,0*5E\r\n"); delay(20); // Close NMEA sequences on CM121 _serial_gps->write("$CFGMSG,0,1,0,1*1B\r\n"); _serial_gps->write("$CFGMSG,0,2,0,1*18\r\n"); _serial_gps->write("$CFGMSG,0,3,0,1*19\r\n"); currentDelay = 20; currentStep = 1; return GNSS_MODEL_UNKNOWN; } case 1: { // Unicore UFirebirdII Series: UC6580, UM620, UM621, UM670A, UM680A, or UM681A,or CM121 std::vector unicore = { {"UC6580", "UC6580", GNSS_MODEL_UC6580}, {"UM600", "UM600", GNSS_MODEL_UC6580}, {"CM121", "CM121", GNSS_MODEL_CM121}}; PROBE_FAMILY("Unicore Family", "$PDTINFO", unicore, 500); currentDelay = 20; currentStep = 2; return GNSS_MODEL_UNKNOWN; } case 2: { std::vector atgm = { {"ATGM336H", "$GPTXT,01,01,02,HW=ATGM336H", GNSS_MODEL_ATGM336H}, /* ATGM332D series (-11(GPS), -21(BDS), -31(GPS+BDS), -51(GPS+GLONASS), -71-0(GPS+BDS+GLONASS)) based on AT6558 */ {"ATGM332D", "$GPTXT,01,01,02,HW=ATGM332D", GNSS_MODEL_ATGM336H}}; PROBE_FAMILY("ATGM33xx Family", "$PCAS06,1*1A", atgm, 500); currentDelay = 20; currentStep = 3; return GNSS_MODEL_UNKNOWN; } case 3: { /* Airoha (Mediatek) AG3335A/M/S, A3352Q, Quectel L89 2.0, SimCom SIM65M */ _serial_gps->write("$PAIR062,2,0*3C\r\n"); // GSA OFF to reduce volume _serial_gps->write("$PAIR062,3,0*3D\r\n"); // GSV OFF to reduce volume _serial_gps->write("$PAIR513*3D\r\n"); // save configuration std::vector airoha = {{"AG3335", "$PAIR021,AG3335", GNSS_MODEL_AG3335}, {"AG3352", "$PAIR021,AG3352", GNSS_MODEL_AG3352}, {"RYS3520", "$PAIR021,REYAX_RYS3520_V2", GNSS_MODEL_AG3352}}; PROBE_FAMILY("Airoha Family", "$PAIR021*39", airoha, 1000); currentDelay = 20; currentStep = 4; return GNSS_MODEL_UNKNOWN; } case 4: { PROBE_SIMPLE("LC86", "$PQTMVERNO*58", "$PQTMVERNO,LC86", GNSS_MODEL_AG3352, 500); PROBE_SIMPLE("L76K", "$PCAS06,0*1B", "$GPTXT,01,01,02,SW=", GNSS_MODEL_MTK, 500); currentDelay = 20; currentStep = 5; return GNSS_MODEL_UNKNOWN; } case 5: { // Close all NMEA sentences, valid for MTK3333 and MTK3339 platforms _serial_gps->write("$PMTK514,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0*2E\r\n"); delay(20); std::vector mtk = {{"L76B", "Quectel-L76B", GNSS_MODEL_MTK_L76B}, {"PA1010D", "1010D", GNSS_MODEL_MTK_PA1010D}, {"PA1616S", "1616S", GNSS_MODEL_MTK_PA1616S}, {"LS20031", "MC-1513", GNSS_MODEL_MTK_L76B}, {"L96", "Quectel-L96", GNSS_MODEL_MTK_L76B}, {"L80-R", "_3337_", GNSS_MODEL_MTK_L76B}, {"L80", "_3339_", GNSS_MODEL_MTK_L76B}}; PROBE_FAMILY("MTK Family", "$PMTK605*31", mtk, 500); currentDelay = 20; currentStep = 6; return GNSS_MODEL_UNKNOWN; } case 6: { uint8_t cfg_rate[] = {0xB5, 0x62, 0x06, 0x08, 0x00, 0x00, 0x00, 0x00}; UBXChecksum(cfg_rate, sizeof(cfg_rate)); clearBuffer(); _serial_gps->write(cfg_rate, sizeof(cfg_rate)); // Check that the returned response class and message ID are correct GPS_RESPONSE response = getACK(0x06, 0x08, 750); if (response == GNSS_RESPONSE_NONE) { LOG_WARN("No GNSS Module (baudrate %d)", serialSpeed); currentDelay = 2000; currentStep = 0; return GNSS_MODEL_UNKNOWN; } else if (response == GNSS_RESPONSE_FRAME_ERRORS) { LOG_INFO("UBlox Frame Errors (baudrate %d)", serialSpeed); } memset(buffer, 0, sizeof(buffer)); uint8_t _message_MONVER[8] = { 0xB5, 0x62, // Sync message for UBX protocol 0x0A, 0x04, // Message class and ID (UBX-MON-VER) 0x00, 0x00, // Length of payload (we're asking for an answer, so no payload) 0x00, 0x00 // Checksum }; // Get Ublox gnss module hardware and software info UBXChecksum(_message_MONVER, sizeof(_message_MONVER)); clearBuffer(); _serial_gps->write(_message_MONVER, sizeof(_message_MONVER)); uint16_t len = getACK(buffer, sizeof(buffer), 0x0A, 0x04, 1200); if (len) { uint16_t position = 0; for (int i = 0; i < 30; i++) { ublox_info.swVersion[i] = buffer[position]; position++; } for (int i = 0; i < 10; i++) { ublox_info.hwVersion[i] = buffer[position]; position++; } while (len >= position + 30) { for (int i = 0; i < 30; i++) { ublox_info.extension[ublox_info.extensionNo][i] = buffer[position]; position++; } ublox_info.extensionNo++; if (ublox_info.extensionNo > 9) break; } LOG_DEBUG("Module Info : "); LOG_DEBUG("Soft version: %s", ublox_info.swVersion); LOG_DEBUG("Hard version: %s", ublox_info.hwVersion); LOG_DEBUG("Extensions:%d", ublox_info.extensionNo); for (int i = 0; i < ublox_info.extensionNo; i++) { LOG_DEBUG(" %s", ublox_info.extension[i]); } memset(buffer, 0, sizeof(buffer)); // tips: extensionNo field is 0 on some 6M GNSS modules for (int i = 0; i < ublox_info.extensionNo; ++i) { if (!strncmp(ublox_info.extension[i], "MOD=", 4)) { strncpy((char *)buffer, &(ublox_info.extension[i][4]), sizeof(buffer)); } else if (!strncmp(ublox_info.extension[i], "PROTVER", 7)) { char *ptr = nullptr; memset(buffer, 0, sizeof(buffer)); strncpy((char *)buffer, &(ublox_info.extension[i][8]), sizeof(buffer)); LOG_DEBUG("Protocol Version:%s", (char *)buffer); if (strlen((char *)buffer)) { ublox_info.protocol_version = strtoul((char *)buffer, &ptr, 10); LOG_DEBUG("ProtVer=%d", ublox_info.protocol_version); } else { ublox_info.protocol_version = 0; } } } if (strncmp(ublox_info.hwVersion, "00040007", 8) == 0) { LOG_INFO(DETECTED_MESSAGE, "U-blox 6", "6"); return GNSS_MODEL_UBLOX6; } else if (strncmp(ublox_info.hwVersion, "00070000", 8) == 0) { LOG_INFO(DETECTED_MESSAGE, "U-blox 7", "7"); return GNSS_MODEL_UBLOX7; } else if (strncmp(ublox_info.hwVersion, "00080000", 8) == 0) { LOG_INFO(DETECTED_MESSAGE, "U-blox 8", "8"); return GNSS_MODEL_UBLOX8; } else if (strncmp(ublox_info.hwVersion, "00190000", 8) == 0) { LOG_INFO(DETECTED_MESSAGE, "U-blox 9", "9"); return GNSS_MODEL_UBLOX9; } else if (strncmp(ublox_info.hwVersion, "000A0000", 8) == 0) { LOG_INFO(DETECTED_MESSAGE, "U-blox 10", "10"); return GNSS_MODEL_UBLOX10; } } } } LOG_WARN("No GNSS Module (baudrate %d)", serialSpeed); currentDelay = 2000; currentStep = 0; return GNSS_MODEL_UNKNOWN; } GnssModel_t GPS::getProbeResponse(unsigned long timeout, const std::vector &responseMap, int serialSpeed) { // Calculate buffer size based on baud rate - 256 bytes for 9600 baud as baseline // Higher baud rates get proportionally larger buffers to handle more data int bufferSize = (serialSpeed * 256) / 9600; // Clamp buffer size between reasonable limits if (bufferSize < 128) bufferSize = 128; if (bufferSize > 2048) bufferSize = 2048; auto response = std::unique_ptr(new char[bufferSize]); // Dynamically allocate based on baud rate uint16_t responseLen = 0; unsigned long start = millis(); while (millis() - start < timeout) { if (_serial_gps->available()) { char c = _serial_gps->read(); // Add char to buffer if there's space if (responseLen < bufferSize - 1) { response[responseLen++] = c; response[responseLen] = '\0'; } if (c == ',' || (responseLen >= 2 && response[responseLen - 2] == '\r' && response[responseLen - 1] == '\n')) { // check if we can see our chips for (const auto &chipInfo : responseMap) { if (strstr(response.get(), chipInfo.detectionString.c_str()) != nullptr) { #ifdef GPS_DEBUG LOG_DEBUG(response.get()); #endif LOG_INFO("%s detected", chipInfo.chipName.c_str()); return chipInfo.driver; } } } if (responseLen >= 2 && response[responseLen - 2] == '\r' && response[responseLen - 1] == '\n') { #ifdef GPS_DEBUG LOG_DEBUG(response.get()); #endif // Reset the response buffer for the next potential message responseLen = 0; response[0] = '\0'; } } } #ifdef GPS_DEBUG LOG_DEBUG(response.get()); #endif return GNSS_MODEL_UNKNOWN; // Return unknown on timeout } std::unique_ptr GPS::createGps() { int8_t _rx_gpio = config.position.rx_gpio; int8_t _tx_gpio = config.position.tx_gpio; int8_t _en_gpio = config.position.gps_en_gpio; #if defined(GPS_RX_PIN) if (!_rx_gpio) _rx_gpio = GPS_RX_PIN; #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 (!portduino_config.has_gps) return nullptr; #endif if (!_rx_gpio || !_serial_gps) // Configured to have no GPS at all return nullptr; auto new_gps = std::unique_ptr(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 #ifdef PIN_GPS_SWITCH // toggle GPS via external GPIO switch pinMode(PIN_GPS_SWITCH, INPUT); gpsPeriodic = std::unique_ptr(new concurrency::Periodic("GPSSwitch", gpsSwitch)); #endif // Currently disabled per issue #525 (TinyGPS++ crash bug) // when fixed upstream, can be un-disabled to enable 3D FixType and PDOP #ifndef TINYGPS_OPTION_NO_CUSTOM_FIELDS // see NMEAGPS.h gsafixtype.begin(reader, NMEA_MSG_GXGSA, 2); gsapdop.begin(reader, NMEA_MSG_GXGSA, 15); LOG_DEBUG("Use " NMEA_MSG_GXGSA " for 3DFIX and PDOP"); #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); #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 LOG_DEBUG("Use GPIO%d for GPS RX", new_gps->rx_gpio); LOG_DEBUG("Use GPIO%d for GPS TX", new_gps->tx_gpio); // ESP32 has a special set of parameters vs other arduino ports #if defined(ARCH_ESP32) _serial_gps->begin(GPS_BAUDRATE, SERIAL_8N1, new_gps->rx_gpio, new_gps->tx_gpio); #elif defined(ARCH_RP2040) _serial_gps->setPinout(new_gps->tx_gpio, new_gps->rx_gpio); _serial_gps->setFIFOSize(256); _serial_gps->begin(GPS_BAUDRATE); #elif defined(ARCH_NRF52) _serial_gps->setPins(new_gps->rx_gpio, new_gps->tx_gpio); _serial_gps->begin(GPS_BAUDRATE); #elif defined(ARCH_STM32WL) _serial_gps->setTx(new_gps->tx_gpio); _serial_gps->setRx(new_gps->rx_gpio); _serial_gps->begin(GPS_BAUDRATE); #elif defined(ARCH_PORTDUINO) // Portduino can't set the GPS pins directly. _serial_gps->begin(GPS_BAUDRATE); #else #error Unsupported architecture! #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; } /** * 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 set a new time */ bool GPS::lookForTime() { auto ti = reader.time; auto d = reader.date; if (ti.isValid() && d.isValid()) { // Note: we don't check for updated, because we'll only be called if needed /* Convert to unix time The Unix epoch (or Unix time or POSIX time or Unix timestamp) is the number of seconds that have elapsed since January 1, 1970 (midnight UTC/GMT), not counting leap seconds (in ISO 8601: 1970-01-01T00:00:00Z). */ struct tm t; t.tm_sec = ti.second() + round(ti.age() / 1000); t.tm_min = ti.minute(); t.tm_hour = ti.hour(); t.tm_mday = d.day(); t.tm_mon = d.month() - 1; t.tm_year = d.year() - 1900; t.tm_isdst = false; if (t.tm_mon > -1) { if (perhapsSetRTC(RTCQualityGPS, t) == RTCSetResultSuccess) { LOG_DEBUG("NMEA GPS time set %02d-%02d-%02d %02d:%02d:%02d age %d", d.year(), d.month(), t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, ti.age()); return true; } else { return false; } } else return false; } else return false; } /** * 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() { // 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) { // In a GPS_DEBUG build we want to log all of these. In production, we only care if there are many of them. #ifndef GPS_DEBUG if (reader.failedChecksum() > 4) #endif 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 #endif // check if GPS has an acceptable lock if (!hasLock()) return false; #ifdef GPS_DEBUG LOG_DEBUG("AGE: LOC=%d FIX=%d DATE=%d TIME=%d", reader.location.age(), #ifndef TINYGPS_OPTION_NO_CUSTOM_FIELDS gsafixtype.age(), #else 0, #endif reader.date.age(), reader.time.age()); #endif // GPS_DEBUG // Is this a new point or are we re-reading the previous one? if (!reader.location.isUpdated() && !reader.altitude.isUpdated()) return false; // check if a complete GPS solution set is available for reading // tinyGPSDatum::age() also includes isValid() test // FIXME if (!((reader.location.age() < GPS_SOL_EXPIRY_MS) && #ifndef TINYGPS_OPTION_NO_CUSTOM_FIELDS (gsafixtype.age() < GPS_SOL_EXPIRY_MS) && #endif (reader.time.age() < GPS_SOL_EXPIRY_MS) && (reader.date.age() < GPS_SOL_EXPIRY_MS))) { LOG_WARN("SOME data is TOO OLD: LOC %u, TIME %u, DATE %u", reader.location.age(), reader.time.age(), reader.date.age()); return false; } // We know the solution is fresh and valid, so just read the data auto loc = reader.location.value(); // Bail out EARLY to avoid overwriting previous good data (like #857) if (toDegInt(loc.lat) > 900000000) { #ifdef GPS_DEBUG LOG_DEBUG("Bail out EARLY on LAT %i", toDegInt(loc.lat)); #endif return false; } if (toDegInt(loc.lng) > 1800000000) { #ifdef GPS_DEBUG LOG_DEBUG("Bail out EARLY on LNG %i", toDegInt(loc.lng)); #endif return false; } p.location_source = meshtastic_Position_LocSource_LOC_INTERNAL; // Dilution of precision (an accuracy metric) is reported in 10^2 units, so we need to scale down when we use it #ifndef TINYGPS_OPTION_NO_CUSTOM_FIELDS p.HDOP = reader.hdop.value(); p.PDOP = TinyGPSPlus::parseDecimal(gsapdop.value()); #else // FIXME! naive PDOP emulation (assumes VDOP==HDOP) // correct formula is PDOP = SQRT(HDOP^2 + VDOP^2) p.HDOP = reader.hdop.value(); p.PDOP = 1.41 * reader.hdop.value(); #endif // Discard incomplete or erroneous readings if (reader.hdop.value() == 0) { LOG_WARN("BOGUS hdop.value() REJECTED: %d", reader.hdop.value()); return false; } p.latitude_i = toDegInt(loc.lat); p.longitude_i = toDegInt(loc.lng); p.altitude_geoidal_separation = reader.geoidHeight.meters(); p.altitude_hae = reader.altitude.meters() + p.altitude_geoidal_separation; p.altitude = reader.altitude.meters(); p.fix_quality = fixQual; #ifndef TINYGPS_OPTION_NO_CUSTOM_FIELDS p.fix_type = fixType; #endif // positional timestamp struct tm t; t.tm_sec = reader.time.second(); t.tm_min = reader.time.minute(); t.tm_hour = reader.time.hour(); t.tm_mday = reader.date.day(); t.tm_mon = reader.date.month() - 1; t.tm_year = reader.date.year() - 1900; t.tm_isdst = false; p.timestamp = gm_mktime(&t); // Nice to have, if available if (reader.satellites.isUpdated()) { p.sats_in_view = reader.satellites.value(); } if (reader.course.isUpdated() && reader.course.isValid()) { if (reader.course.value() < 36000) { // sanity check p.ground_track = reader.course.value() * 1e3; // Scale the heading (in degrees * 10^-2) to match the expected degrees * 10^-5 } else { LOG_WARN("BOGUS course.value() REJECTED: %d", reader.course.value()); } } if (reader.speed.isUpdated() && reader.speed.isValid()) { p.ground_speed = reader.speed.kmph(); } return true; } bool GPS::hasLock() { // Using GPGGA fix quality indicator if (fixQual >= 1 && fixQual <= 5) { #ifndef TINYGPS_OPTION_NO_CUSTOM_FIELDS // Use GPGSA fix type 2D/3D (better) if available if (fixType == 3 || fixType == 0) // zero means "no data received" #endif return true; } return false; } bool GPS::hasFlow() { return reader.passedChecksum() > 0; } bool GPS::whileActive() { unsigned int charsInBuf = 0; bool isValid = false; #ifdef GPS_DEBUG std::string debugmsg = ""; #endif if (powerState != GPS_ACTIVE) { clearBuffer(); return false; } #ifdef SERIAL_BUFFER_SIZE if (_serial_gps->available() >= SERIAL_BUFFER_SIZE - 1) { LOG_WARN("GPS Buffer full with %u bytes waiting. Flush to avoid corruption", _serial_gps->available()); clearBuffer(); } #endif // First consume any chars that have piled up at the receiver while (_serial_gps->available() > 0) { int c = _serial_gps->read(); UBXscratch[charsInBuf] = c; #ifdef GPS_DEBUG debugmsg += vformat("%c", (c >= 32 && c <= 126) ? c : '.'); #endif isValid |= reader.encode(c); if (charsInBuf > sizeof(UBXscratch) - 10 || c == '\r') { if (strnstr((char *)UBXscratch, "$GPTXT,01,01,02,u-blox ag - www.u-blox.com*50", charsInBuf)) { rebootsSeen++; } charsInBuf = 0; } else { charsInBuf++; } } #ifdef GPS_DEBUG if (debugmsg != "") { LOG_DEBUG(debugmsg.c_str()); } #endif return isValid; } void GPS::enable() { // Clear the old scheduling info (reset the lock-time prediction) scheduling.reset(); 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 ================================================ FILE: src/gps/GPS.h ================================================ #pragma once #include "configuration.h" #if !MESHTASTIC_EXCLUDE_GPS #include #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 // Allow defining the polarity of the STANDBY output. default is LOW for standby #ifndef GPS_STANDBY_ACTIVE #define GPS_STANDBY_ACTIVE LOW #endif static constexpr uint32_t GPS_UPDATE_ALWAYS_ON_THRESHOLD_MS = 10 * 1000UL; static constexpr uint32_t GPS_FIX_HOLD_MAX_MS = 20000; typedef enum { GNSS_MODEL_ATGM336H, GNSS_MODEL_MTK, GNSS_MODEL_UBLOX6, GNSS_MODEL_UBLOX7, GNSS_MODEL_UBLOX8, GNSS_MODEL_UBLOX9, GNSS_MODEL_UBLOX10, GNSS_MODEL_UC6580, GNSS_MODEL_UNKNOWN, GNSS_MODEL_MTK_L76B, GNSS_MODEL_MTK_PA1010D, GNSS_MODEL_MTK_PA1616S, GNSS_MODEL_AG3335, GNSS_MODEL_AG3352, GNSS_MODEL_LS20031, GNSS_MODEL_CM121 } 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 }; struct ChipInfo { String chipName; // The name of the chip (for logging) String detectionString; // The string to match in the response GnssModel_t driver; // The driver to use }; /** * A gps class that only reads from the GPS periodically and keeps the gps powered down except when reading * * When new data is available it will notify observers. */ class GPS : private concurrency::OSThread { public: meshtastic_Position p = meshtastic_Position_init_default; /** This is normally bound to config.position.gps_en_gpio but some rare boards (like heltec tracker) need more advanced * implementations. Those boards will set this public variable to a custom implementation. * * Normally set by GPS::createGPS() */ GpioVirtPin *enablePin = NULL; virtual ~GPS(); /** 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(); // Creates an instance of the GPS class. // Returns the new instance or null if the GPS is not present. static std::unique_ptr createGps(); // Wake the GPS hardware - ready for an update void up(); // Let the GPS hardware save power between updates void down(); private: GPS() : concurrency::OSThread("GPS") {} /// 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(); GnssModel_t gnssModel = GNSS_MODEL_UNKNOWN; TinyGPSPlus reader; uint8_t fixQual = 0; // fix quality from GPGGA uint32_t lastChecksumFailCount = 0; uint8_t currentStep = 0; int32_t currentDelay = 2000; #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 uint32_t fixHoldEnds = 0; uint32_t rx_gpio = 0; uint32_t tx_gpio = 0; uint8_t speedSelect = 0; uint8_t probeTries = 0; /** * hasValidLocation - indicates that the position variables contain a complete * GPS location, valid and fresh (< gps_update_interval + position_broadcast_secs) */ bool hasValidLocation = false; // default to false, until we complete our first read 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); /** If !NULL we will use this serial port to construct our GPS */ #if defined(ARCH_RP2040) static SerialUART *_serial_gps; #elif defined(ARCH_NRF52) static Uart *_serial_gps; #else static HardwareSerial *_serial_gps; #endif // Create a ublox packet for editing in memory uint8_t makeUBXPacket(uint8_t class_id, uint8_t msg_id, uint8_t payload_size, const uint8_t *msg); uint8_t makeCASPacket(uint8_t class_id, uint8_t msg_id, uint8_t payload_size, const uint8_t *msg); // 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); /// Prepare the GPS for the cpu entering deep sleep, expect to be gone for at least 100s of msecs /// always returns 0 to indicate okay to sleep int prepareDeepSleep(void *unused); /** Set power with EN pin, if relevant */ void writePinEN(bool on); /** Set 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; GnssModel_t getProbeResponse(unsigned long timeout, const std::vector &responseMap, int serialSpeed); // Get GNSS model GnssModel_t probe(int serialSpeed); // delay counter to allow more sats before fixed position stops GPS thread uint8_t fixeddelayCtr = 0; }; extern std::unique_ptr gps; #endif // Exclude GPS ================================================ FILE: src/gps/GPSUpdateScheduling.cpp ================================================ #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 unacceptably 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 initialize the smoothing filter if (searchCount == 1) predictedMsToGetLock = lockTime; // Third locktime and after: predict using exponential smoothing. Respond slowly to changes else if (searchCount > 1) predictedMsToGetLock = (lockTime * weighting) + (predictedMsToGetLock * (1 - weighting)); searchCount++; // Only tracked so we can disregard initial lock-times LOG_DEBUG("Predict %us to get next lock", predictedMsToGetLock / 1000); } // How long do we expect to spend searching for a lock? uint32_t GPSUpdateScheduling::predictedSearchDurationMs() { return GPSUpdateScheduling::predictedMsToGetLock; } ================================================ FILE: src/gps/GPSUpdateScheduling.h ================================================ #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". }; ================================================ FILE: src/gps/GeoCoord.cpp ================================================ #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 decimal 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 decimal 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 based on the passed-in point * Ported from http://www.edwilliams.org/avform147.htm#LL * @param bearing * The bearing in radians * @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"; } double GeoCoord::pow_neg(double base, double exponent) { if (exponent == 0) { return 1; } else if (exponent > 0) { return pow(base, exponent); } return 1 / pow(base, -exponent); } double GeoCoord::toRadians(double deg) { return deg * PI / 180; } double GeoCoord::toDegrees(double r) { return r * 180 / PI; } ================================================ FILE: src/gps/GeoCoord.h ================================================ #pragma once #include #include #include #include #include #include #include #include #define PI 3.1415926535897932384626433832795 #define OLC_CODE_LEN 11 #define DEG_CONVERT (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); // Raises a number to an exponent, handling negative exponents. static double pow_neg(double base, double exponent); static double toRadians(double deg); static double toDegrees(double r); // Point to point conversions int32_t distanceTo(const GeoCoord &pointB); int32_t bearingTo(const GeoCoord &pointB); std::shared_ptr pointAtDistance(double bearing, double range); // 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 }; ================================================ FILE: src/gps/NMEAWPL.cpp ================================================ #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, "\r\n$G%cWPL,%02d%07.4f,%c,%03d%07.4f,%c,%s", type, geoCoord.getDMSLatDeg(), (abs(geoCoord.getLatitude()) - geoCoord.getDMSLatDeg() * 1e+7) * 6e-6, geoCoord.getDMSLatCP(), geoCoord.getDMSLonDeg(), (abs(geoCoord.getLongitude()) - geoCoord.getDMSLonDeg() * 1e+7) * 6e-6, geoCoord.getDMSLonCP(), name); uint32_t chk = 0; for (uint32_t i = 1; i < len; i++) { chk ^= buf[i]; } len += snprintf(buf + len, bufsz - len, "*%02X\r\n", chk); return len; } uint32_t printWPL(char *buf, size_t bufsz, const meshtastic_Position &pos, const char *name, bool isCaltopoMode) { GeoCoord geoCoord(pos.latitude_i, pos.longitude_i, pos.altitude); char type = isCaltopoMode ? 'P' : 'N'; uint32_t len = snprintf(buf, bufsz, "$G%cWPL,%02d%07.4f,%c,%03d%07.4f,%c,%s", type, geoCoord.getDMSLatDeg(), (abs(geoCoord.getLatitude()) - geoCoord.getDMSLatDeg() * 1e+7) * 6e-6, geoCoord.getDMSLatCP(), geoCoord.getDMSLonDeg(), (abs(geoCoord.getLongitude()) - geoCoord.getDMSLonDeg() * 1e+7) * 6e-6, geoCoord.getDMSLonCP(), name); uint32_t chk = 0; for (uint32_t i = 1; i < len; i++) { chk ^= buf[i]; } len += snprintf(buf + len, bufsz - len, "*%02X\r\n", chk); return len; } /* ------------------------------------------- * 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 ================================================ FILE: src/gps/NMEAWPL.h ================================================ #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); ================================================ FILE: src/gps/RTC.cpp ================================================ #include "RTC.h" #include "configuration.h" #include "detect/ScanI2C.h" #include "main.h" #include #include #include static RTCQuality currentQuality = RTCQualityNone; uint32_t lastSetFromPhoneNtpOrGps = 0; static uint32_t lastTimeValidationWarning = 0; static const uint32_t TIME_VALIDATION_WARNING_INTERVAL_MS = 15000; // 15 seconds RTCQuality getRTCQuality() { return currentQuality; } // 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. */ RTCSetResult readFromRTC() { struct timeval tv; /* btw settimeofday() is helpful here too*/ #ifdef RV3028_RTC if (rtc_found.address == RV3028_RTC) { uint32_t now = millis(); Melopero_RV3028 rtc; #if 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 #ifdef BUILD_EPOCH if (tv.tv_sec < BUILD_EPOCH) { if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) { LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); } return RTCSetResultInvalidTime; } #endif LOG_DEBUG("Read RTC time from RV3028 getTime as %02d-%02d-%02d %02d:%02d:%02d (%ld)", t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, printableEpoch); if (currentQuality == RTCQualityNone) { timeStartMsec = now; zeroOffsetSecs = tv.tv_sec; currentQuality = RTCQualityDevice; } return RTCSetResultSuccess; } else { LOG_WARN("RTC not found (found address 0x%02X)", rtc_found.address); } #elif defined(PCF8563_RTC) || defined(PCF85063_RTC) #if defined(PCF8563_RTC) if (rtc_found.address == PCF8563_RTC) { SensorPCF8563 rtc; #elif defined(PCF85063_RTC) if (rtc_found.address == PCF85063_RTC) { SensorPCF85063 rtc; #endif uint32_t now = millis(); #if WIRE_INTERFACES_COUNT == 2 rtc.begin(rtc_found.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire); #else rtc.begin(Wire); #endif RTC_DateTime datetime = rtc.getDateTime(); tm t = datetime.toUnixTime(); tv.tv_sec = gm_mktime(&t); tv.tv_usec = 0; uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms #ifdef BUILD_EPOCH if (tv.tv_sec < BUILD_EPOCH) { if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) { LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); lastTimeValidationWarning = millis(); } return RTCSetResultInvalidTime; } #endif LOG_DEBUG("Read RTC time from %s getDateTime as %02d-%02d-%02d %02d:%02d:%02d (%ld)", rtc.getChipName(), t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, printableEpoch); if (currentQuality == RTCQualityNone) { timeStartMsec = now; zeroOffsetSecs = tv.tv_sec; currentQuality = RTCQualityDevice; } return RTCSetResultSuccess; } else { LOG_WARN("RTC not found (found address 0x%02X)", rtc_found.address); } #elif defined(RX8130CE_RTC) if (rtc_found.address == RX8130CE_RTC) { uint32_t now = millis(); #ifdef MUZI_BASE ArtronShop_RX8130CE rtc(&Wire1); #else ArtronShop_RX8130CE rtc(&Wire); #endif tm t; if (rtc.getTime(&t)) { tv.tv_sec = gm_mktime(&t); tv.tv_usec = 0; uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms LOG_DEBUG("Read RTC time from RX8130CE getDateTime as %02d-%02d-%02d %02d:%02d:%02d (%ld)", t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, printableEpoch); #ifdef BUILD_EPOCH if (tv.tv_sec < BUILD_EPOCH) { if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) { LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); lastTimeValidationWarning = millis(); } return RTCSetResultInvalidTime; } #endif if (currentQuality == RTCQualityNone) { timeStartMsec = now; zeroOffsetSecs = tv.tv_sec; currentQuality = RTCQualityDevice; } return RTCSetResultSuccess; } } #else if (!gettimeofday(&tv, NULL)) { uint32_t now = millis(); uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms LOG_DEBUG("Read RTC time as %ld", printableEpoch); timeStartMsec = now; zeroOffsetSecs = tv.tv_sec; return RTCSetResultSuccess; } #endif return RTCSetResultNotSet; } /** * 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 RTCSetResult * * If we haven't yet set our RTC this boot, set it from a GPS derived time */ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpdate) { static uint32_t lastSetMsec = 0; uint32_t now = millis(); uint32_t printableEpoch = tv->tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms #ifdef BUILD_EPOCH if (tv->tv_sec < BUILD_EPOCH) { if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) { LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); lastTimeValidationWarning = millis(); } return RTCSetResultInvalidTime; } else if ((uint64_t)tv->tv_sec > ((uint64_t)BUILD_EPOCH + FORTY_YEARS)) { if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) { // Calculate max allowed time safely to avoid overflow in logging uint64_t maxAllowedTime = (uint64_t)BUILD_EPOCH + FORTY_YEARS; uint32_t maxAllowedPrintable = (maxAllowedTime > UINT32_MAX) ? UINT32_MAX : (uint32_t)maxAllowedTime; LOG_WARN("Ignore time (%ld) too far in the future (build epoch: %ld, max allowed: %ld)!", printableEpoch, (uint32_t)BUILD_EPOCH, maxAllowedPrintable); lastTimeValidationWarning = millis(); } return RTCSetResultInvalidTime; } #endif bool shouldSet; if (forceUpdate) { shouldSet = true; LOG_DEBUG("Override current RTC quality (%s) with incoming time of RTC quality of %s", RtcName(currentQuality), RtcName(q)); } else if (q > currentQuality) { shouldSet = true; LOG_DEBUG("Upgrade time to quality %s", RtcName(q)); } else if (q == RTCQualityGPS) { shouldSet = true; LOG_DEBUG("Reapply GPS time: %ld secs", printableEpoch); } else if (q == RTCQualityNTP && !Throttle::isWithinTimespanMs(lastSetMsec, (12 * 60 * 60 * 1000UL))) { // Every 12 hrs we will slam in a new NTP or Phone GPS / NTP time, to correct for local RTC clock drift shouldSet = true; LOG_DEBUG("Reapply external time to correct clock drift %ld secs", printableEpoch); } else { shouldSet = false; LOG_DEBUG("Current RTC quality: %s. Ignore time of RTC quality of %s", RtcName(currentQuality), RtcName(q)); } if (shouldSet) { currentQuality = q; lastSetMsec = now; if (currentQuality >= RTCQualityNTP) { lastSetFromPhoneNtpOrGps = now; } // This delta value works on all platforms timeStartMsec = now; zeroOffsetSecs = tv->tv_sec; // If this platform has a settable RTC, set it #ifdef RV3028_RTC if (rtc_found.address == RV3028_RTC) { Melopero_RV3028 rtc; #if WIRE_INTERFACES_COUNT == 2 rtc.initI2C(rtc_found.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire); #else rtc.initI2C(); #endif tm *t = gmtime(&tv->tv_sec); rtc.setTime(t->tm_year + 1900, t->tm_mon + 1, t->tm_wday, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec); LOG_DEBUG("RV3028_RTC setTime %02d-%02d-%02d %02d:%02d:%02d (%ld)", t->tm_year + 1900, t->tm_mon + 1, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec, printableEpoch); } else { LOG_WARN("RTC not found (found address 0x%02X)", rtc_found.address); } #elif defined(PCF8563_RTC) || defined(PCF85063_RTC) #if defined(PCF8563_RTC) if (rtc_found.address == PCF8563_RTC) { SensorPCF8563 rtc; #elif defined(PCF85063_RTC) if (rtc_found.address == PCF85063_RTC) { SensorPCF85063 rtc; #endif #if WIRE_INTERFACES_COUNT == 2 rtc.begin(rtc_found.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire); #else rtc.begin(Wire); #endif tm *t = gmtime(&tv->tv_sec); rtc.setDateTime(*t); LOG_DEBUG("%s setDateTime %02d-%02d-%02d %02d:%02d:%02d (%ld)", rtc.getChipName(), t->tm_year + 1900, t->tm_mon + 1, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec, printableEpoch); } else { LOG_WARN("RTC not found (found address 0x%02X)", rtc_found.address); } #elif defined(RX8130CE_RTC) if (rtc_found.address == RX8130CE_RTC) { #ifdef MUZI_BASE ArtronShop_RX8130CE rtc(&Wire1); #else ArtronShop_RX8130CE rtc(&Wire); #endif tm *t = gmtime(&tv->tv_sec); if (rtc.setTime(*t)) { LOG_DEBUG("RX8130CE setDateTime %02d-%02d-%02d %02d:%02d:%02d (%ld)", t->tm_year + 1900, t->tm_mon + 1, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec, printableEpoch); } else { LOG_WARN("Failed to set time for RX8130CE"); } } #elif defined(ARCH_ESP32) settimeofday(tv, NULL); #endif readFromRTC(); return RTCSetResultSuccess; } else { return RTCSetResultNotSet; // RTC was already set with a higher quality time } } const char *RtcName(RTCQuality quality) { switch (quality) { case RTCQualityNone: return "None"; case RTCQualityDevice: return "Device"; case RTCQualityFromNet: return "Net"; case RTCQualityNTP: return "NTP"; case RTCQualityGPS: return "GPS"; default: return "Unknown"; } } /** * 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. */ RTCSetResult perhapsSetRTC(RTCQuality q, const struct tm &t) { /* Convert to unix time The Unix epoch (or Unix time or POSIX time or Unix timestamp) is the number of seconds that have elapsed since January 1, 1970 (midnight UTC/GMT), not counting leap seconds (in ISO 8601: 1970-01-01T00:00:00Z). */ // horrible hack to make mktime TZ agnostic - best practise according to // https://www.gnu.org/software/libc/manual/html_node/Broken_002ddown-Time.html time_t res = gm_mktime(&t); struct timeval tv; tv.tv_sec = res; tv.tv_usec = 0; // time.centisecond() * (10 / 1000); uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms #ifdef BUILD_EPOCH if (tv.tv_sec < BUILD_EPOCH) { if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) { LOG_WARN("Ignore time (%lu) before build epoch (%lu)!", printableEpoch, BUILD_EPOCH); lastTimeValidationWarning = millis(); } return RTCSetResultInvalidTime; } else if ((uint64_t)tv.tv_sec > ((uint64_t)BUILD_EPOCH + FORTY_YEARS)) { if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) { // Calculate max allowed time safely to avoid overflow in logging uint64_t maxAllowedTime = (uint64_t)BUILD_EPOCH + FORTY_YEARS; uint32_t maxAllowedPrintable = (maxAllowedTime > UINT32_MAX) ? UINT32_MAX : (uint32_t)maxAllowedTime; LOG_WARN("Ignore time (%lu) too far in the future (build epoch: %lu, max allowed: %lu)!", printableEpoch, (uint32_t)BUILD_EPOCH, maxAllowedPrintable); lastTimeValidationWarning = millis(); } return RTCSetResultInvalidTime; } #endif // LOG_DEBUG("Got time from GPS month=%d, year=%d, unixtime=%ld", t.tm_mon, t.tm_year, tv.tv_sec); if (t.tm_year < 0 || t.tm_year >= 300) { // LOG_DEBUG("Ignore invalid GPS month=%d, year=%d, unixtime=%ld", t.tm_mon, t.tm_year, tv.tv_sec); return RTCSetResultInvalidTime; } else { return perhapsSetRTC(q, &tv); } } /** * Returns the timezone offset in seconds. * * @return The timezone offset in seconds. */ int32_t getTZOffset() { #if MESHTASTIC_EXCLUDE_TZ return 0; #else time_t now = getTime(false); struct tm *gmt; gmt = gmtime(&now); gmt->tm_isdst = -1; return (int32_t)difftime(now, mktime(gmt)); #endif } /** * 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(const struct tm *tm) { #if !MESHTASTIC_EXCLUDE_TZ time_t result = 0; // First, get us to the start of tm->year, by calculating the number of days since the Unix epoch. int year = 1900 + tm->tm_year; // tm_year is years since 1900 int year_minus_one = year - 1; int days_before_this_year = 0; days_before_this_year += year_minus_one * 365; // leap days: every 4 years, except 100s, but including 400s. days_before_this_year += year_minus_one / 4 - year_minus_one / 100 + year_minus_one / 400; // subtract from 1970-01-01 to get days since epoch days_before_this_year -= 719162; // (1969 * 365 + 1969 / 4 - 1969 / 100 + 1969 / 400); // Now, within this tm->year, compute the days *before* this tm->month starts. static const int days_before_month[12] = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334}; // non-leap year int days_this_year_before_this_month = days_before_month[tm->tm_mon]; // tm->tm_mon is 0..11 // If this is a leap year, and we're past February, add a day: if (tm->tm_mon >= 2 && (year % 4) == 0 && ((year % 100) != 0 || (year % 400) == 0)) { days_this_year_before_this_month += 1; } // And within this month: int days_this_month_before_today = tm->tm_mday - 1; // tm->tm_mday is 1..31 // Now combine them all together, and convert days to seconds: result += (days_before_this_year + days_this_year_before_this_month + days_this_month_before_today); result *= 86400L; // Finally, add in the hours, minutes, and seconds of today: result += tm->tm_hour * 3600; result += tm->tm_min * 60; result += tm->tm_sec; return result; #else struct tm tmCopy = *tm; return mktime(&tmCopy); #endif } ================================================ FILE: src/gps/RTC.h ================================================ #pragma once #include "configuration.h" #include "sys/time.h" #include #ifdef RX8130CE_RTC #include #endif 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 }; /// The RTC set result codes /// Used to indicate the result of an attempt to set the RTC. enum RTCSetResult { RTCSetResultNotSet = 0, ///< RTC was set successfully RTCSetResultSuccess = 1, ///< RTC was set successfully RTCSetResultInvalidTime = 3, ///< The provided time was invalid (e.g., before the build epoch) RTCSetResultError = 4 ///< An error occurred while setting the RTC }; RTCQuality getRTCQuality(); extern uint32_t lastSetFromPhoneNtpOrGps; /// If we haven't yet set our RTC this boot, set it from a GPS derived time RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpdate = false); RTCSetResult perhapsSetRTC(RTCQuality q, const 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); RTCSetResult readFromRTC(); time_t gm_mktime(const struct tm *tm); #define SEC_PER_DAY 86400 #define SEC_PER_HOUR 3600 #define SEC_PER_MIN 60 #ifdef BUILD_EPOCH static constexpr uint64_t FORTY_YEARS = (40ULL * 365 * SEC_PER_DAY); // Use 64-bit arithmetic to prevent overflow #endif ================================================ FILE: src/gps/cas.h ================================================ #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 static const uint8_t _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 static const uint8_t _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 // Quirk: 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. static const uint8_t _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 }; ================================================ FILE: src/gps/ubx.h ================================================ static const char *failMessage = "Unable to %s"; #define SEND_UBX_PACKET(TYPE, ID, DATA, ERRMSG, TIMEOUT) \ do { \ msglen = makeUBXPacket(TYPE, ID, sizeof(DATA), DATA); \ _serial_gps->write(UBXscratch, msglen); \ if (getACK(TYPE, ID, TIMEOUT) != GNSS_RESPONSE_OK) { \ LOG_WARN(failMessage, #ERRMSG); \ } \ } while (0) // Power Management static uint8_t _message_PMREQ[] PROGMEM = { 0x00, 0x00, 0x00, 0x00, // 4 bytes duration of request task (milliseconds) 0x02, 0x00, 0x00, 0x00 // Bitfield, set backup = 1 }; static uint8_t _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 }; static const uint8_t _message_CFG_RXM_PSM[] PROGMEM = { 0x08, // Reserved 0x01 // Power save mode }; // only for Neo-6 static const uint8_t _message_CFG_RXM_ECO[] PROGMEM = { 0x08, // Reserved 0x04 // eco mode }; static const uint8_t _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 }; // Constellation setup, none required for Neo-6 // For Neo-7 GPS & SBAS static const uint8_t _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 static const uint8_t _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 static const uint8_t _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 static const uint8_t _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 static const uint8_t _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 static const uint8_t _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 static const uint8_t _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 (Precise 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 static const uint8_t _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 (Precise 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 static const uint8_t _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. static const uint8_t _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) static const uint8_t _message_GSA[] = { 0xF0, 0x02, // NMEA ID for GSA 0x00, // Rate for DDC 0x00, // Rate for UART1 0x00, // Rate for UART2 0x00, // Rate for USB useful for native linux 0x00, // Rate for SPI 0x00 // Reserved }; // Disable GSV. GSV - Satellites in view, details the number and location of satellites in view. static const uint8_t _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. static const uint8_t _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. static const uint8_t _message_RMC[] = { 0xF0, 0x04, // NMEA ID for RMC 0x00, // Rate for DDC 0x01, // Rate for UART1 0x00, // Rate for UART2 0x01, // Rate for USB useful for native linux 0x00, // Rate for SPI 0x00 // Reserved }; // Enable GGA. GGA - Global Positioning System Fix Data, which provides 3D location and accuracy data. static const uint8_t _message_GGA[] = { 0xF0, 0x00, // NMEA ID for GGA 0x00, // Rate for DDC 0x01, // Rate for UART1 0x00, // Rate for UART2 0x01, // Rate for USB, useful 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 static const uint8_t _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 static const uint8_t _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 static const uint8_t _message_PMS[] = { 0x00, // Version (0) 0x03, // Power setup value 3 = Agressive 1Hz 0x00, 0x00, // period: not applicable, set to 0 0x00, 0x00, // onTime: not applicable, set to 0 0x00, 0x00 // reserved, generated by u-center }; static const uint8_t _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 }; static const uint8_t _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 acquisition 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 */ static const uint8_t _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}; static const uint8_t _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 */ static const uint8_t _message_VALSET_ITFM_RAM[] = {0x00, 0x01, 0x00, 0x00, 0x0d, 0x00, 0x41, 0x10, 0x01, 0x13, 0x00, 0x41, 0x10, 0x01}; static const uint8_t _message_VALSET_ITFM_BBR[] = {0x00, 0x02, 0x00, 0x00, 0x0d, 0x00, 0x41, 0x10, 0x01, 0x13, 0x00, 0x41, 0x10, 0x01}; // Turn off all NMEA messages: // Ram layer config message: // b5 62 06 8a 22 00 00 01 00 00 c0 00 91 20 00 ca 00 91 20 00 c5 00 91 20 00 ac 00 91 20 00 b1 00 91 20 00 bb 00 91 20 00 40 8f // Disable GLL, GSV, VTG messages in BBR layer // BBR layer config message: // b5 62 06 8a 13 00 00 02 00 00 ca 00 91 20 00 c5 00 91 20 00 b1 00 91 20 00 f8 4e static const uint8_t _message_VALSET_DISABLE_NMEA_RAM[] = { /*0x00, 0x01, 0x00, 0x00, 0xca, 0x00, 0x91, 0x20, 0x00, 0xc5, 0x00, 0x91, 0x20, 0x00, 0xb1, 0x00, 0x91, 0x20, 0x00 */ 0x00, 0x01, 0x00, 0x00, 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}; static const uint8_t _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 static const uint8_t _message_VALSET_DISABLE_TXT_INFO_RAM[] = {0x00, 0x01, 0x00, 0x00, 0x07, 0x00, 0x92, 0x20, 0x03}; static const uint8_t _message_VALSET_DISABLE_TXT_INFO_BBR[] = {0x00, 0x02, 0x00, 0x00, 0x07, 0x00, 0x92, 0x20, 0x03}; static const uint8_t _message_VALSET_ENABLE_NMEA_RAM[] = {0x00, 0x01, 0x00, 0x00, 0xbb, 0x00, 0x91, 0x20, 0x01, 0xac, 0x00, 0x91, 0x20, 0x01}; static const uint8_t _message_VALSET_ENABLE_NMEA_BBR[] = {0x00, 0x02, 0x00, 0x00, 0xbb, 0x00, 0x91, 0x20, 0x01, 0xac, 0x00, 0x91, 0x20, 0x01}; static const uint8_t _message_VALSET_DISABLE_SBAS_RAM[] = {0x00, 0x01, 0x00, 0x00, 0x20, 0x00, 0x31, 0x10, 0x00, 0x05, 0x00, 0x31, 0x10, 0x00}; static const uint8_t _message_VALSET_DISABLE_SBAS_BBR[] = {0x00, 0x02, 0x00, 0x00, 0x20, 0x00, 0x31, 0x10, 0x00, 0x05, 0x00, 0x31, 0x10, 0x00}; /* 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 definition 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 visibility 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 */ ================================================ FILE: src/graphics/EInkDisplay2.cpp ================================================ #include "configuration.h" #ifdef USE_EINK #include "EInkDisplay2.h" #include "SPILock.h" #include "main.h" #include #ifdef GXEPD2_DRIVER_0 #include "einkDetect.h" #endif /* 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; // HACK for L1 EInk #if defined(SEEED_WIO_TRACKER_L1_EINK) // For SEEED_WIO_TRACKER_L1_EINK, setRotation(3) is correct but mirrored; flip both axes for (uint32_t y = 0; y < displayHeight; y++) { for (uint32_t x = 0; x < displayWidth; x++) { auto b = buffer[x + (y / 8) * displayWidth]; auto isset = b & (1 << (y & 7)); adafruitDisplay->drawPixel((displayWidth - 1) - x, (displayHeight - 1) - y, isset ? GxEPD_BLACK : GxEPD_WHITE); } } #else for (uint32_t y = 0; y < displayHeight; y++) { for (uint32_t x = 0; x < displayWidth; x++) { auto b = buffer[x + (y / 8) * displayWidth]; auto isset = b & (1 << (y & 7)); if (flipped) adafruitDisplay->drawPixel((displayWidth - 1) - x, (displayHeight - 1) - y, isset ? GxEPD_BLACK : GxEPD_WHITE); else adafruitDisplay->drawPixel(x, y, isset ? GxEPD_BLACK : GxEPD_WHITE); } } #endif // Trigger the refresh in GxEPD2 LOG_DEBUG("Update E-Paper"); adafruitDisplay->nextPage(); // End the update process endUpdate(); LOG_DEBUG("done"); return true; } // End the update process - virtual method, overridden 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("Do EInk init"); #ifdef PIN_EINK_EN // backlight power, HIGH is backlight on, LOW is off pinMode(PIN_EINK_EN, OUTPUT); #ifdef ELECROW_ThinkNode_M1 // ThinkNode M1 has a hardware dimmable backlight. Start enabled digitalWrite(PIN_EINK_EN, HIGH); #elif defined(MINI_EPAPER_S3) // T-Mini Epaper S3 requires panel power rail enabled before SPI transfer. digitalWrite(PIN_EINK_EN, HIGH); delay(10); #else digitalWrite(PIN_EINK_EN, LOW); #endif #endif #if defined(TTGO_T_ECHO) || defined(ELECROW_ThinkNode_M1) || defined(T_ECHO_LITE) || defined(TTGO_T_ECHO_PLUS) { auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY, SPI1); adafruitDisplay = new GxEPD2_BW(*lowLevel); adafruitDisplay->init(); #if defined(ELECROW_ThinkNode_M1) || defined(T_ECHO_LITE) adafruitDisplay->setRotation(4); #else adafruitDisplay->setRotation(3); #endif adafruitDisplay->setPartialWindow(0, 0, displayWidth, displayHeight); } #elif defined(ELECROW_ThinkNode_M5) { // Start HSPI hspi = new SPIClass(HSPI); hspi->begin(PIN_EINK_SCLK, -1, PIN_EINK_MOSI, PIN_EINK_CS); // SCLK, MISO, MOSI, SS auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY, *hspi); adafruitDisplay = new GxEPD2_BW(*lowLevel); adafruitDisplay->init(); adafruitDisplay->setRotation(4); adafruitDisplay->setPartialWindow(0, 0, displayWidth, displayHeight); } #elif defined(MESHLINK) { auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY, SPI1); adafruitDisplay = new GxEPD2_BW(*lowLevel); adafruitDisplay->init(); adafruitDisplay->setRotation(3); adafruitDisplay->setPartialWindow(0, 0, displayWidth, displayHeight); } #elif defined(RAK4630) || defined(MAKERPYTHON) { if (eink_found) { auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY); adafruitDisplay = new GxEPD2_BW(*lowLevel); adafruitDisplay->init(115200, true, 10, false, SPI1, SPISettings(4000000, MSBFIRST, SPI_MODE0)); // RAK14000 2.13 inch b/w 250x122 does actually now support fast refresh adafruitDisplay->setRotation(3); // Fast refresh support for 1.54, 2.13 RAK14000 b/w , 2.9 and 4.2 // adafruitDisplay->setRotation(1); adafruitDisplay->setPartialWindow(0, 0, displayWidth, displayHeight); } else { (void)adafruitDisplay; } } #elif defined(HELTEC_WIRELESS_PAPER_V1_0) || defined(HELTEC_VISION_MASTER_E290) || defined(TLORA_T3S3_EPAPER) || \ defined(CROWPANEL_ESP32S3_5_EPAPER) || defined(CROWPANEL_ESP32S3_4_EPAPER) || defined(CROWPANEL_ESP32S3_2_EPAPER) || \ defined(MINI_EPAPER_S3) { // 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(); #if defined(MINI_EPAPER_S3) adafruitDisplay->setRotation(3); #else adafruitDisplay->setRotation(3); #if defined(CROWPANEL_ESP32S3_5_EPAPER) || defined(CROWPANEL_ESP32S3_4_EPAPER) adafruitDisplay->setRotation(0); #endif #endif } #elif defined(PCA10059) || defined(ME25LS01) { auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY); adafruitDisplay = new GxEPD2_BW(*lowLevel); adafruitDisplay->init(115200, true, 40, false, SPI1, SPISettings(4000000, MSBFIRST, SPI_MODE0)); adafruitDisplay->setRotation(0); adafruitDisplay->setPartialWindow(0, 0, EINK_WIDTH, EINK_HEIGHT); } #elif defined(M5_COREINK) || defined(T_DECK_PRO) auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY); adafruitDisplay = new GxEPD2_BW(*lowLevel); adafruitDisplay->init(115200, true, 40, false, SPI, SPISettings(4000000, MSBFIRST, SPI_MODE0)); adafruitDisplay->setRotation(0); adafruitDisplay->setPartialWindow(0, 0, EINK_WIDTH, EINK_HEIGHT); #elif defined(my) || defined(ESP32_S3_PICO) { auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY); adafruitDisplay = new GxEPD2_BW(*lowLevel); adafruitDisplay->init(115200, true, 40, false, SPI, SPISettings(4000000, MSBFIRST, SPI_MODE0)); adafruitDisplay->setRotation(1); adafruitDisplay->setPartialWindow(0, 0, EINK_WIDTH, EINK_HEIGHT); } #elif defined(HELTEC_MESH_POCKET) || defined(SEEED_WIO_TRACKER_L1_EINK) || defined(HELTEC_MESH_SOLAR_EINK) { spi1 = &SPI1; spi1->begin(); // VExt already enabled in setup() // RTC GPIO hold disabled in setup() // Create GxEPD2 objects auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY, *spi1); adafruitDisplay = new GxEPD2_BW(*lowLevel); // Init GxEPD2 adafruitDisplay->init(); adafruitDisplay->setRotation(3); adafruitDisplay->setPartialWindow(0, 0, EINK_WIDTH, EINK_HEIGHT); } #elif defined(HELTEC_WIRELESS_PAPER) || defined(HELTEC_VISION_MASTER_E213) // Detect display model, before starting SPI EInkDetectionResult displayModel = detectEInk(); // Start HSPI hspi = new SPIClass(HSPI); hspi->begin(PIN_EINK_SCLK, -1, PIN_EINK_MOSI, PIN_EINK_CS); // SCLK, MISO, MOSI, SS // Create GxEPD2 object adafruitDisplay = new GxEPD2_Multi((uint8_t)displayModel, PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY, *hspi); // Init GxEPD2 adafruitDisplay->init(); adafruitDisplay->setRotation(3); #endif return true; } #endif ================================================ FILE: src/graphics/EInkDisplay2.h ================================================ #pragma once #ifdef USE_EINK #include "GxEPD2_BW.h" #include #ifdef GXEPD2_DRIVER_0 // If variant has multiple possible display models #include "GxEPD2Multi.h" #endif // Limit how often we push a full E-Ink refresh. T-Deck Pro needs faster updates for typing. #ifndef EINK_FORCE_DISPLAY_THROTTLE_MS #if defined(T_DECK_PRO) #define EINK_FORCE_DISPLAY_THROTTLE_MS 200 #else #define EINK_FORCE_DISPLAY_THROTTLE_MS 1000 #endif #endif /** * 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 = EINK_FORCE_DISPLAY_THROTTLE_MS); /** * 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; #ifdef GXEPD2_DRIVER_0 // AdafruitGFX display object - wrapper for multiple drivers // Allows runtime detection of multiple displays // Avoid this situation if possible! GxEPD2_Multi *adafruitDisplay = NULL; #else // AdafruitGFX display object (for single display model) - instantiated in connect(), variant specific GxEPD2_BW *adafruitDisplay = NULL; #endif // If display uses HSPI #if defined(HELTEC_WIRELESS_PAPER) || defined(HELTEC_WIRELESS_PAPER_V1_0) || defined(HELTEC_VISION_MASTER_E213) || \ defined(HELTEC_VISION_MASTER_E290) || defined(TLORA_T3S3_EPAPER) || defined(CROWPANEL_ESP32S3_5_EPAPER) || \ defined(CROWPANEL_ESP32S3_4_EPAPER) || defined(CROWPANEL_ESP32S3_2_EPAPER) || defined(ELECROW_ThinkNode_M5) || \ defined(MINI_EPAPER_S3) SPIClass *hspi = NULL; #endif #if defined(HELTEC_MESH_POCKET) || defined(SEEED_WIO_TRACKER_L1_EINK) || defined(HELTEC_MESH_SOLAR_EINK) SPIClass *spi1 = NULL; #endif private: // FIXME quick hack to limit drawing to a very slow rate uint32_t lastDrawMsec = 0; }; #endif ================================================ FILE: src/graphics/EInkDynamicDisplay.cpp ================================================ #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 = std::unique_ptr(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 dirtyPixels = nullptr; #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() { // Determine 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 (drop frames)"); notifyLater(intervalPollAsyncRefresh, DUE_POLL_ASYNCREFRESH, true); // Hand-off to NotifiedWorkerThread } } // Fast Refresh else if (previousRefresh == FAST) EInkDisplay::endUpdate(); // Still block while updating, but EInkDisplay needs us to call endUpdate() ourselves. // 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, 30000)) { 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, 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 occurred consecutively, since last full refresh? void EInkDynamicDisplay::checkConsecutiveFastRefreshes() { // If a decision was already reached, don't run the check if (refresh != UNSPECIFIED) return; // Bypass limit if UNLIMITED_FAST mode is active if (frameFlags & UNLIMITED_FAST) { refresh = FAST; reason = NO_OBJECTIONS; LOG_DEBUG("refresh=FAST, reason=UNLIMITED_FAST_MODE_ACTIVE, frameFlags=0x%x", frameFlags); return; } // 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.get(), 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("Join 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 ================================================ FILE: src/graphics/EInkDynamicDisplay.h ================================================ #pragma once #include "configuration.h" #include #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(); // Methods to enable or disable unlimited fast refresh mode void enableUnlimitedFastMode() { addFrameFlag(UNLIMITED_FAST); } void disableUnlimitedFastMode() { frameFlags = (frameFlagTypes)(frameFlags & ~UNLIMITED_FAST); } // What kind of frame is this enum frameFlagTypes : uint8_t { BACKGROUND = (1 << 0), // For frames via display() RESPONSIVE = (1 << 1), // For frames via forceDisplay() COSMETIC = (1 << 2), // For splashes DEMAND_FAST = (1 << 3), // Special case only BLOCKING = (1 << 4), // Modifier - block while refresh runs UNLIMITED_FAST = (1 << 5) }; void addFrameFlag(frameFlagTypes flag); // 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. std::unique_ptr 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 ================================================ FILE: src/graphics/EmoteRenderer.cpp ================================================ #include "configuration.h" #if HAS_SCREEN #include "graphics/EmoteRenderer.h" #include #include namespace graphics { namespace EmoteRenderer { static inline int getStringWidth(OLEDDisplay *display, const char *text, size_t len) { #if defined(OLED_UA) || defined(OLED_RU) return display->getStringWidth(text, len, true); #else (void)len; return display->getStringWidth(text); #endif } size_t utf8CharLen(uint8_t c) { if ((c & 0xE0) == 0xC0) return 2; if ((c & 0xF0) == 0xE0) return 3; if ((c & 0xF8) == 0xF0) return 4; return 1; } static inline bool isPossibleEmoteLead(uint8_t c) { // All supported emoji labels in emotes.cpp are currently in these UTF-8 lead ranges. return c == 0xE2 || c == 0xF0; } static inline int getUtf8ChunkWidth(OLEDDisplay *display, const char *text, size_t len) { char chunk[5] = {0, 0, 0, 0, 0}; if (len > 4) len = 4; memcpy(chunk, text, len); return getStringWidth(display, chunk, len); } static inline bool isFE0FAt(const char *s, size_t pos, size_t len) { return pos + 2 < len && static_cast(s[pos]) == 0xEF && static_cast(s[pos + 1]) == 0xB8 && static_cast(s[pos + 2]) == 0x8F; } static inline bool isSkinToneAt(const char *s, size_t pos, size_t len) { return pos + 3 < len && static_cast(s[pos]) == 0xF0 && static_cast(s[pos + 1]) == 0x9F && static_cast(s[pos + 2]) == 0x8F && (static_cast(s[pos + 3]) >= 0xBB && static_cast(s[pos + 3]) <= 0xBF); } static inline size_t ignorableModifierLenAt(const char *s, size_t pos, size_t len) { // Skip modifiers that do not change which bitmap we render. if (isFE0FAt(s, pos, len)) return 3; if (isSkinToneAt(s, pos, len)) return 4; return 0; } const Emote *findEmoteByLabel(const char *label, const Emote *emoteSet, int emoteCount) { if (!label || !*label) return nullptr; for (int i = 0; i < emoteCount; ++i) { if (emoteSet[i].label && strcmp(label, emoteSet[i].label) == 0) return &emoteSet[i]; } return nullptr; } static bool matchAtIgnoringModifiers(const char *text, size_t textLen, size_t pos, const char *label, size_t &textConsumed, size_t &matchScore) { // Treat FE0F and skin-tone modifiers as optional while matching. textConsumed = 0; matchScore = 0; if (!label || !*label || pos >= textLen) return false; const size_t labelLen = strlen(label); size_t ti = pos; size_t li = 0; while (true) { while (ti < textLen) { const size_t skipLen = ignorableModifierLenAt(text, ti, textLen); if (!skipLen) break; ti += skipLen; } while (li < labelLen) { const size_t skipLen = ignorableModifierLenAt(label, li, labelLen); if (!skipLen) break; li += skipLen; } if (li >= labelLen) { while (ti < textLen) { const size_t skipLen = ignorableModifierLenAt(text, ti, textLen); if (!skipLen) break; ti += skipLen; } textConsumed = ti - pos; return textConsumed > 0; } if (ti >= textLen) return false; const uint8_t tc = static_cast(text[ti]); const uint8_t lc = static_cast(label[li]); const size_t tlen = utf8CharLen(tc); const size_t llen = utf8CharLen(lc); if (tlen != llen || ti + tlen > textLen || li + llen > labelLen) return false; if (memcmp(text + ti, label + li, tlen) != 0) return false; ti += tlen; li += llen; matchScore += llen; } } const Emote *findEmoteAt(const char *text, size_t textLen, size_t pos, size_t &matchLen, const Emote *emoteSet, int emoteCount) { // Prefer the longest matching label at this byte offset. const Emote *matched = nullptr; matchLen = 0; size_t bestScore = 0; if (!text || pos >= textLen) return nullptr; if (!isPossibleEmoteLead(static_cast(text[pos]))) return nullptr; for (int i = 0; i < emoteCount; ++i) { const char *label = emoteSet[i].label; if (!label || !*label) continue; if (static_cast(label[0]) != static_cast(text[pos])) continue; const size_t labelLen = strlen(label); if (labelLen == 0) continue; size_t candidateLen = 0; size_t candidateScore = 0; if (pos + labelLen <= textLen && memcmp(text + pos, label, labelLen) == 0) { candidateLen = labelLen; candidateScore = labelLen; } else if (matchAtIgnoringModifiers(text, textLen, pos, label, candidateLen, candidateScore)) { // Matched with FE0F/skin tone modifiers treated as optional. } else { continue; } if (candidateScore > bestScore) { matched = &emoteSet[i]; matchLen = candidateLen; bestScore = candidateScore; } } return matched; } static LineMetrics analyzeLineInternal(OLEDDisplay *display, const char *line, size_t lineLen, int fallbackHeight, const Emote *emoteSet, int emoteCount, int emoteSpacing) { // Scan once to collect width and tallest emote for this line. LineMetrics metrics{0, fallbackHeight, false}; if (!line) return metrics; for (size_t i = 0; i < lineLen;) { size_t matchLen = 0; const Emote *matched = findEmoteAt(line, lineLen, i, matchLen, emoteSet, emoteCount); if (matched) { metrics.hasEmote = true; metrics.tallestHeight = std::max(metrics.tallestHeight, matched->height); if (display) metrics.width += matched->width + emoteSpacing; i += matchLen; continue; } const size_t skipLen = ignorableModifierLenAt(line, i, lineLen); if (skipLen) { i += skipLen; continue; } const size_t charLen = utf8CharLen(static_cast(line[i])); if (display) metrics.width += getUtf8ChunkWidth(display, line + i, charLen); i += charLen; } return metrics; } LineMetrics analyzeLine(OLEDDisplay *display, const char *line, int fallbackHeight, const Emote *emoteSet, int emoteCount, int emoteSpacing) { return analyzeLineInternal(display, line, line ? strlen(line) : 0, fallbackHeight, emoteSet, emoteCount, emoteSpacing); } int maxEmoteHeight(const Emote *emoteSet, int emoteCount) { int tallest = 0; for (int i = 0; i < emoteCount; ++i) { if (emoteSet[i].label && *emoteSet[i].label) tallest = std::max(tallest, emoteSet[i].height); } return tallest; } int measureStringWithEmotes(OLEDDisplay *display, const char *line, const Emote *emoteSet, int emoteCount, int emoteSpacing) { if (!display) return 0; if (!line || !*line) return 0; return analyzeLine(display, line, 0, emoteSet, emoteCount, emoteSpacing).width; } static int appendTextSpanAndMeasure(OLEDDisplay *display, int cursorX, int fontY, const char *text, size_t len, bool draw, bool fauxBold) { // Draw plain-text runs in chunks so UTF-8 stays intact. if (!text || len == 0) return cursorX; char chunk[33]; size_t pos = 0; while (pos < len) { size_t chunkLen = 0; while (pos + chunkLen < len) { const size_t charLen = utf8CharLen(static_cast(text[pos + chunkLen])); if (chunkLen + charLen >= sizeof(chunk)) break; chunkLen += charLen; } if (chunkLen == 0) { chunkLen = std::min(len - pos, sizeof(chunk) - 1); } memcpy(chunk, text + pos, chunkLen); chunk[chunkLen] = '\0'; if (draw) { if (fauxBold) display->drawString(cursorX + 1, fontY, chunk); display->drawString(cursorX, fontY, chunk); } cursorX += getStringWidth(display, chunk, chunkLen); pos += chunkLen; } return cursorX; } size_t truncateToWidth(OLEDDisplay *display, const char *line, char *out, size_t outSize, int maxWidth, const char *ellipsis, const Emote *emoteSet, int emoteCount, int emoteSpacing) { if (!out || outSize == 0) return 0; out[0] = '\0'; if (!display || !line || maxWidth <= 0) return 0; const size_t lineLen = strlen(line); const int suffixWidth = (ellipsis && *ellipsis) ? measureStringWithEmotes(display, ellipsis, emoteSet, emoteCount, emoteSpacing) : 0; const char *suffix = (ellipsis && suffixWidth <= maxWidth) ? ellipsis : ""; const size_t suffixLen = strlen(suffix); const int availableWidth = maxWidth - (*suffix ? suffixWidth : 0); if (measureStringWithEmotes(display, line, emoteSet, emoteCount, emoteSpacing) <= maxWidth) { strncpy(out, line, outSize - 1); out[outSize - 1] = '\0'; return strlen(out); } int used = 0; size_t cut = 0; for (size_t i = 0; i < lineLen;) { // Keep whole emotes together when deciding where to cut. int tokenWidth = 0; size_t advance = 0; if (isPossibleEmoteLead(static_cast(line[i]))) { size_t matchLen = 0; const Emote *matched = findEmoteAt(line, lineLen, i, matchLen, emoteSet, emoteCount); if (matched) { tokenWidth = matched->width + emoteSpacing; advance = matchLen; } } if (advance == 0) { const size_t skipLen = ignorableModifierLenAt(line, i, lineLen); if (skipLen) { i += skipLen; cut = i; continue; } const size_t charLen = utf8CharLen(static_cast(line[i])); tokenWidth = getUtf8ChunkWidth(display, line + i, charLen); advance = charLen; } if (used + tokenWidth > availableWidth) break; used += tokenWidth; i += advance; cut = i; } if (cut == 0) { strncpy(out, suffix, outSize - 1); out[outSize - 1] = '\0'; return strlen(out); } size_t copyLen = cut; if (copyLen > outSize - 1) copyLen = outSize - 1; if (suffixLen > 0 && copyLen + suffixLen > outSize - 1) { copyLen = (outSize - 1 > suffixLen) ? (outSize - 1 - suffixLen) : 0; } memcpy(out, line, copyLen); size_t totalLen = copyLen; if (suffixLen > 0 && totalLen < outSize - 1) { memcpy(out + totalLen, suffix, std::min(suffixLen, outSize - 1 - totalLen)); totalLen += std::min(suffixLen, outSize - 1 - totalLen); } out[totalLen] = '\0'; return totalLen; } void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const char *line, int fontHeight, const Emote *emoteSet, int emoteCount, int emoteSpacing, bool fauxBold) { if (!line) return; const size_t lineLen = strlen(line); // Center text vertically when any emote is taller than the font. const int maxIconHeight = analyzeLineInternal(nullptr, line, lineLen, fontHeight, emoteSet, emoteCount, emoteSpacing).tallestHeight; const int lineHeight = std::max(fontHeight, maxIconHeight); const int fontY = y + (lineHeight - fontHeight) / 2; int cursorX = x; bool inBold = false; for (size_t i = 0; i < lineLen;) { // Toggle faux bold. if (fauxBold && i + 1 < lineLen && line[i] == '*' && line[i + 1] == '*') { inBold = !inBold; i += 2; continue; } const size_t skipLen = ignorableModifierLenAt(line, i, lineLen); if (skipLen) { i += skipLen; continue; } size_t matchLen = 0; const Emote *matched = findEmoteAt(line, lineLen, i, matchLen, emoteSet, emoteCount); if (matched) { const int iconY = y + (lineHeight - matched->height) / 2; display->drawXbm(cursorX, iconY, matched->width, matched->height, matched->bitmap); cursorX += matched->width + emoteSpacing; i += matchLen; continue; } size_t next = i; while (next < lineLen) { // Stop the text run before the next emote or bold marker. if (fauxBold && next + 1 < lineLen && line[next] == '*' && line[next + 1] == '*') break; if (ignorableModifierLenAt(line, next, lineLen)) break; size_t nextMatchLen = 0; if (findEmoteAt(line, lineLen, next, nextMatchLen, emoteSet, emoteCount) != nullptr) break; next += utf8CharLen(static_cast(line[next])); } if (next == i) next += utf8CharLen(static_cast(line[i])); cursorX = appendTextSpanAndMeasure(display, cursorX, fontY, line + i, next - i, true, fauxBold && inBold); i = next; } } } // namespace EmoteRenderer } // namespace graphics #endif // HAS_SCREEN ================================================ FILE: src/graphics/EmoteRenderer.h ================================================ #pragma once #include "configuration.h" #if HAS_SCREEN #include "graphics/emotes.h" #include #include #include #include namespace graphics { namespace EmoteRenderer { struct LineMetrics { int width; int tallestHeight; bool hasEmote; }; size_t utf8CharLen(uint8_t c); const Emote *findEmoteByLabel(const char *label, const Emote *emoteSet = emotes, int emoteCount = numEmotes); const Emote *findEmoteAt(const char *text, size_t textLen, size_t pos, size_t &matchLen, const Emote *emoteSet = emotes, int emoteCount = numEmotes); inline const Emote *findEmoteAt(const std::string &text, size_t pos, size_t &matchLen, const Emote *emoteSet = emotes, int emoteCount = numEmotes) { return findEmoteAt(text.c_str(), text.length(), pos, matchLen, emoteSet, emoteCount); } LineMetrics analyzeLine(OLEDDisplay *display, const char *line, int fallbackHeight = 0, const Emote *emoteSet = emotes, int emoteCount = numEmotes, int emoteSpacing = 1); inline LineMetrics analyzeLine(OLEDDisplay *display, const std::string &line, int fallbackHeight = 0, const Emote *emoteSet = emotes, int emoteCount = numEmotes, int emoteSpacing = 1) { return analyzeLine(display, line.c_str(), fallbackHeight, emoteSet, emoteCount, emoteSpacing); } int maxEmoteHeight(const Emote *emoteSet = emotes, int emoteCount = numEmotes); int measureStringWithEmotes(OLEDDisplay *display, const char *line, const Emote *emoteSet = emotes, int emoteCount = numEmotes, int emoteSpacing = 1); inline int measureStringWithEmotes(OLEDDisplay *display, const std::string &line, const Emote *emoteSet = emotes, int emoteCount = numEmotes, int emoteSpacing = 1) { return measureStringWithEmotes(display, line.c_str(), emoteSet, emoteCount, emoteSpacing); } size_t truncateToWidth(OLEDDisplay *display, const char *line, char *out, size_t outSize, int maxWidth, const char *ellipsis = "...", const Emote *emoteSet = emotes, int emoteCount = numEmotes, int emoteSpacing = 1); inline std::string truncateToWidth(OLEDDisplay *display, const std::string &line, int maxWidth, const std::string &ellipsis = "...", const Emote *emoteSet = emotes, int emoteCount = numEmotes, int emoteSpacing = 1) { if (!display || maxWidth <= 0) return ""; if (measureStringWithEmotes(display, line.c_str(), emoteSet, emoteCount, emoteSpacing) <= maxWidth) return line; std::vector out(line.length() + ellipsis.length() + 1, '\0'); truncateToWidth(display, line.c_str(), out.data(), out.size(), maxWidth, ellipsis.c_str(), emoteSet, emoteCount, emoteSpacing); return std::string(out.data()); } void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const char *line, int fontHeight, const Emote *emoteSet = emotes, int emoteCount = numEmotes, int emoteSpacing = 1, bool fauxBold = true); inline void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string &line, int fontHeight, const Emote *emoteSet = emotes, int emoteCount = numEmotes, int emoteSpacing = 1, bool fauxBold = true) { drawStringWithEmotes(display, x, y, line.c_str(), fontHeight, emoteSet, emoteCount, emoteSpacing, fauxBold); } } // namespace EmoteRenderer } // namespace graphics #endif // HAS_SCREEN ================================================ FILE: src/graphics/GxEPD2Multi.h ================================================ // Wrapper class for GxEPD2_BW // Generic signature at build-time, so that we can detect display model at run-time // Workaround for issue of GxEPD2_BW objects not having a shared base class // Only exposes methods which we are actually using template class GxEPD2_Multi { public: void drawPixel(int16_t x, int16_t y, uint16_t color) { if (which == 0) driver0->drawPixel(x, y, color); else driver1->drawPixel(x, y, color); } bool nextPage() { if (which == 0) return driver0->nextPage(); else return driver1->nextPage(); } void hibernate() { if (which == 0) driver0->hibernate(); else driver1->hibernate(); } void init(uint32_t serial_diag_bitrate = 0) { if (which == 0) driver0->init(serial_diag_bitrate); else driver1->init(serial_diag_bitrate); } void init(uint32_t serial_diag_bitrate, bool initial, uint16_t reset_duration = 20, bool pulldown_rst_mode = false) { if (which == 0) driver0->init(serial_diag_bitrate, initial, reset_duration, pulldown_rst_mode); else driver1->init(serial_diag_bitrate, initial, reset_duration, pulldown_rst_mode); } void setRotation(uint8_t x) { if (which == 0) driver0->setRotation(x); else driver1->setRotation(x); } void setPartialWindow(uint16_t x, uint16_t y, uint16_t w, uint16_t h) { if (which == 0) driver0->setPartialWindow(x, y, w, h); else driver1->setPartialWindow(x, y, w, h); } void setFullWindow() { if (which == 0) driver0->setFullWindow(); else driver1->setFullWindow(); } int16_t width() { if (which == 0) return driver0->width(); else return driver1->width(); } int16_t height() { if (which == 0) return driver0->height(); else return driver1->height(); } void clearScreen(uint8_t value = 0xFF) { if (which == 0) driver0->clearScreen(); else driver1->clearScreen(); } void endAsyncFull() { if (which == 0) driver0->endAsyncFull(); else driver1->endAsyncFull(); } // Exposes methods of the GxEPD2_EPD object which is usually available as GxEPD2_BW::epd class Epd2Wrapper { public: bool isBusy() { return m_epd2->isBusy(); } GxEPD2_EPD *m_epd2; } epd2; // Constructor // Select driver by passing whichDriver as 0 or 1 GxEPD2_Multi(uint8_t whichDriver, int16_t cs, int16_t dc, int16_t rst, int16_t busy, SPIClass &spi) { assert(whichDriver == 0 || whichDriver == 1); which = whichDriver; LOG_DEBUG("GxEPD2_Multi driver: %d", which); if (which == 0) { driver0 = new GxEPD2_BW(Driver0(cs, dc, rst, busy, spi)); epd2.m_epd2 = &(driver0->epd2); } else if (which == 1) { driver1 = new GxEPD2_BW(Driver1(cs, dc, rst, busy, spi)); epd2.m_epd2 = &(driver1->epd2); } } private: uint8_t which; GxEPD2_BW *driver0; GxEPD2_BW *driver1; }; ================================================ FILE: src/graphics/NomadStarLED.h ================================================ #ifdef HAS_LP5562 #include #include extern LP5562 rgbw; #endif ================================================ FILE: src/graphics/Panel_sdl.cpp ================================================ /*----------------------------------------------------------------------------/ Lovyan GFX - Graphics library for embedded devices. Original Source: https://github.com/lovyan03/LovyanGFX/ Licence: [FreeBSD](https://github.com/lovyan03/LovyanGFX/blob/master/license.txt) Author: [lovyan03](https://twitter.com/lovyan03) Contributors: [ciniml](https://github.com/ciniml) [mongonta0716](https://github.com/mongonta0716) [tobozo](https://github.com/tobozo) Porting for SDL: [imliubo](https://github.com/imliubo) /----------------------------------------------------------------------------*/ #include "Panel_sdl.hpp" #if defined(SDL_h_) // #include "../common.hpp" // #include "../../misc/common_function.hpp" // #include "../../Bus.hpp" #include #include #include #ifndef M_PI #define M_PI 3.14159265358979323846 #endif namespace lgfx { inline namespace v1 { SDL_Keymod Panel_sdl::_keymod = KMOD_NONE; static SDL_semaphore *_update_in_semaphore = nullptr; static SDL_semaphore *_update_out_semaphore = nullptr; volatile static uint32_t _in_step_exec = 0; volatile static uint32_t _msec_step_exec = 512; static bool _inited = false; static bool _all_close = false; volatile uint8_t Panel_sdl::_gpio_dummy_values[EMULATED_GPIO_MAX]; static inline void *heap_alloc_dma(size_t length) { return malloc(length); } // aligned_alloc(16, length); static inline void heap_free(void *buf) { free(buf); } static std::list _list_monitor; static monitor_t *const getMonitorByWindowID(uint32_t windowID) { for (auto &m : _list_monitor) { if (SDL_GetWindowID(m->window) == windowID) { return m; } } return nullptr; } //---------------------------------------------------------------------------- static std::vector _key_code_map; void Panel_sdl::addKeyCodeMapping(SDL_KeyCode keyCode, uint8_t gpio) { if (gpio > EMULATED_GPIO_MAX) return; KeyCodeMapping_t map; map.keycode = keyCode; map.gpio = gpio; _key_code_map.push_back(map); } int Panel_sdl::getKeyCodeMapping(SDL_KeyCode keyCode) { for (const auto &i : _key_code_map) { if (i.keycode == keyCode) return i.gpio; } return -1; } void Panel_sdl::_event_proc(void) { SDL_Event event; while (SDL_PollEvent(&event)) { if ((event.type == SDL_KEYDOWN) || (event.type == SDL_KEYUP)) { auto mon = getMonitorByWindowID(event.button.windowID); int gpio = -1; /// Check key mapping gpio = getKeyCodeMapping((SDL_KeyCode)event.key.keysym.sym); if (gpio < 0) { switch (event.key.keysym.sym) { /// M5StackのBtnA~BtnCのエミュレート; // case SDLK_LEFT: gpio = 39; break; // case SDLK_DOWN: gpio = 38; break; // case SDLK_RIGHT: gpio = 37; break; // case SDLK_UP: gpio = 36; break; /// L/Rキーで画面回転 case SDLK_r: case SDLK_l: if (event.type == SDL_KEYDOWN && event.key.keysym.mod == _keymod) { if (mon != nullptr) { mon->frame_rotation = (mon->frame_rotation += event.key.keysym.sym == SDLK_r ? 1 : -1); int x, y, w, h; SDL_GetWindowSize(mon->window, &w, &h); SDL_GetWindowPosition(mon->window, &x, &y); SDL_SetWindowSize(mon->window, h, w); SDL_SetWindowPosition(mon->window, x + (w - h) / 2, y + (h - w) / 2); mon->panel->sdl_invalidate(); } } break; /// 1~6キーで画面拡大率変更 case SDLK_1: case SDLK_2: case SDLK_3: case SDLK_4: case SDLK_5: case SDLK_6: if (event.type == SDL_KEYDOWN && event.key.keysym.mod == _keymod) { if (mon != nullptr) { int size = 1 + (event.key.keysym.sym - SDLK_1); _update_scaling(mon, size, size); } } break; default: continue; } } if (event.type == SDL_KEYDOWN) { Panel_sdl::gpio_lo(gpio); } else { Panel_sdl::gpio_hi(gpio); } } else if (event.type == SDL_MOUSEBUTTONDOWN || event.type == SDL_MOUSEBUTTONUP || event.type == SDL_MOUSEMOTION) { auto mon = getMonitorByWindowID(event.button.windowID); if (mon != nullptr) { { int x, y, w, h; SDL_GetWindowSize(mon->window, &w, &h); SDL_GetMouseState(&x, &y); float sf = sinf(mon->frame_angle * M_PI / 180); float cf = cosf(mon->frame_angle * M_PI / 180); x -= w / 2.0f; y -= h / 2.0f; float nx = y * sf + x * cf; float ny = y * cf - x * sf; if (mon->frame_rotation & 1) { std::swap(w, h); } x = (nx * mon->frame_width / w) + (mon->frame_width >> 1); y = (ny * mon->frame_height / h) + (mon->frame_height >> 1); mon->touch_x = x - mon->frame_inner_x; mon->touch_y = y - mon->frame_inner_y; } if (event.type == SDL_MOUSEBUTTONDOWN && event.button.button == SDL_BUTTON_LEFT) { mon->touched = true; } if (event.type == SDL_MOUSEBUTTONUP && event.button.button == SDL_BUTTON_LEFT) { mon->touched = false; } } } else if (event.type == SDL_WINDOWEVENT) { auto monitor = getMonitorByWindowID(event.window.windowID); if (monitor) { if (event.window.event == SDL_WINDOWEVENT_RESIZED) { int mw, mh; SDL_GetRendererOutputSize(monitor->renderer, &mw, &mh); if (monitor->frame_rotation & 1) { std::swap(mw, mh); } monitor->scaling_x = (mw * 2 / monitor->frame_width) / 2.0f; monitor->scaling_y = (mh * 2 / monitor->frame_height) / 2.0f; monitor->panel->sdl_invalidate(); } else if (event.window.event == SDL_WINDOWEVENT_CLOSE) { monitor->closing = true; } } } else if (event.type == SDL_QUIT) { for (auto &m : _list_monitor) { m->closing = true; } } } } /// デバッガでステップ実行されていることを検出するスレッド用関数。 static int detectDebugger(bool *running) { uint32_t prev_ms = SDL_GetTicks(); do { SDL_Delay(1); uint32_t ms = SDL_GetTicks(); /// 時間間隔が広すぎる場合はステップ実行中 (ブレークポイントで止まった)と判断する。 /// また、解除されたと判断した後も1023msecほど状態を維持する。 if (ms - prev_ms > 64) { _in_step_exec = _msec_step_exec; } else if (_in_step_exec) { --_in_step_exec; } prev_ms = ms; } while (*running); return 0; } void Panel_sdl::_update_proc(void) { for (auto it = _list_monitor.begin(); it != _list_monitor.end();) { if ((*it)->closing) { if ((*it)->texture_frameimage) { SDL_DestroyTexture((*it)->texture_frameimage); } SDL_DestroyTexture((*it)->texture); SDL_DestroyRenderer((*it)->renderer); SDL_DestroyWindow((*it)->window); _list_monitor.erase(it++); if (_list_monitor.empty()) { _all_close = true; return; } continue; } (*it)->panel->sdl_update(); ++it; } } int Panel_sdl::setup(void) { if (_inited) return 1; _inited = true; /// Add default keycode mapping /// M5StackのBtnA~BtnCのエミュレート; addKeyCodeMapping(SDLK_LEFT, 39); addKeyCodeMapping(SDLK_DOWN, 38); addKeyCodeMapping(SDLK_RIGHT, 37); addKeyCodeMapping(SDLK_UP, 36); SDL_CreateThread((SDL_ThreadFunction)detectDebugger, "dbg", &_inited); _update_in_semaphore = SDL_CreateSemaphore(0); _update_out_semaphore = SDL_CreateSemaphore(0); for (size_t pin = 0; pin < EMULATED_GPIO_MAX; ++pin) { gpio_hi(pin); } /*Initialize the SDL*/ SDL_Init(SDL_INIT_VIDEO); SDL_StartTextInput(); // SDL_SetThreadPriority(SDL_ThreadPriority::SDL_THREAD_PRIORITY_HIGH); return 0; } int Panel_sdl::loop(void) { if (!_inited) return 1; _event_proc(); SDL_SemWaitTimeout(_update_in_semaphore, 1); _update_proc(); _event_proc(); if (SDL_SemValue(_update_out_semaphore) == 0) { SDL_SemPost(_update_out_semaphore); } return _all_close; } int Panel_sdl::close(void) { if (!_inited) return 1; _inited = false; SDL_StopTextInput(); SDL_DestroySemaphore(_update_in_semaphore); SDL_DestroySemaphore(_update_out_semaphore); SDL_Quit(); return 0; } int Panel_sdl::main(int (*fn)(bool *), uint32_t msec_step_exec) { _msec_step_exec = msec_step_exec; /// SDLの準備 if (0 != Panel_sdl::setup()) { return 1; } /// ユーザコード関数の動作・停止フラグ bool running = true; /// ユーザコード関数を起動する auto thread = SDL_CreateThread((SDL_ThreadFunction)fn, "fn", &running); /// 全部のウィンドウが閉じられるまでSDLのイベント・描画処理を継続 while (0 == Panel_sdl::loop()) { }; /// ユーザコード関数を終了する running = false; SDL_WaitThread(thread, nullptr); /// SDLを終了する return Panel_sdl::close(); } void Panel_sdl::setScaling(uint_fast8_t scaling_x, uint_fast8_t scaling_y) { monitor.scaling_x = scaling_x; monitor.scaling_y = scaling_y; } void Panel_sdl::setFrameImage(const void *frame_image, int frame_width, int frame_height, int inner_x, int inner_y) { monitor.frame_image = frame_image; monitor.frame_width = frame_width; monitor.frame_height = frame_height; monitor.frame_inner_x = inner_x; monitor.frame_inner_y = inner_y; } void Panel_sdl::setFrameRotation(uint_fast16_t frame_rotation) { monitor.frame_rotation = frame_rotation; monitor.frame_angle = (monitor.frame_rotation) * 90; } Panel_sdl::~Panel_sdl(void) { _list_monitor.remove(&monitor); SDL_DestroyMutex(_sdl_mutex); } Panel_sdl::Panel_sdl(void) : Panel_FrameBufferBase() { _sdl_mutex = SDL_CreateMutex(); _auto_display = true; monitor.panel = this; } bool Panel_sdl::init(bool use_reset) { initFrameBuffer(_cfg.panel_width * 4, _cfg.panel_height); bool res = Panel_FrameBufferBase::init(use_reset); _list_monitor.push_back(&monitor); return res; } color_depth_t Panel_sdl::setColorDepth(color_depth_t depth) { auto bits = depth & color_depth_t::bit_mask; if (bits >= 16) { depth = (bits > 16) ? rgb888_3Byte : rgb565_2Byte; } else { depth = (depth == color_depth_t::grayscale_8bit) ? grayscale_8bit : rgb332_1Byte; } _write_depth = depth; _read_depth = depth; return depth; } Panel_sdl::lock_t::lock_t(Panel_sdl *parent) : _parent{parent} { SDL_LockMutex(parent->_sdl_mutex); }; Panel_sdl::lock_t::~lock_t(void) { ++_parent->_modified_counter; SDL_UnlockMutex(_parent->_sdl_mutex); if (SDL_SemValue(_update_in_semaphore) < 2) { SDL_SemPost(_update_in_semaphore); if (!_in_step_exec) { SDL_SemWaitTimeout(_update_out_semaphore, 1); } } }; void Panel_sdl::drawPixelPreclipped(uint_fast16_t x, uint_fast16_t y, uint32_t rawcolor) { lock_t lock(this); Panel_FrameBufferBase::drawPixelPreclipped(x, y, rawcolor); } void Panel_sdl::writeFillRectPreclipped(uint_fast16_t x, uint_fast16_t y, uint_fast16_t w, uint_fast16_t h, uint32_t rawcolor) { lock_t lock(this); Panel_FrameBufferBase::writeFillRectPreclipped(x, y, w, h, rawcolor); } void Panel_sdl::writeBlock(uint32_t rawcolor, uint32_t length) { // lock_t lock(this); Panel_FrameBufferBase::writeBlock(rawcolor, length); } void Panel_sdl::writeImage(uint_fast16_t x, uint_fast16_t y, uint_fast16_t w, uint_fast16_t h, pixelcopy_t *param, bool use_dma) { lock_t lock(this); Panel_FrameBufferBase::writeImage(x, y, w, h, param, use_dma); } void Panel_sdl::writeImageARGB(uint_fast16_t x, uint_fast16_t y, uint_fast16_t w, uint_fast16_t h, pixelcopy_t *param) { lock_t lock(this); Panel_FrameBufferBase::writeImageARGB(x, y, w, h, param); } void Panel_sdl::writePixels(pixelcopy_t *param, uint32_t len, bool use_dma) { lock_t lock(this); Panel_FrameBufferBase::writePixels(param, len, use_dma); } void Panel_sdl::display(uint_fast16_t x, uint_fast16_t y, uint_fast16_t w, uint_fast16_t h) { (void)x; (void)y; (void)w; (void)h; if (_in_step_exec) { if (_display_counter != _modified_counter) { do { SDL_SemPost(_update_in_semaphore); SDL_SemWaitTimeout(_update_out_semaphore, 1); } while (_display_counter != _modified_counter); SDL_Delay(1); } } } uint_fast8_t Panel_sdl::getTouchRaw(touch_point_t *tp, uint_fast8_t count) { (void)count; tp->x = monitor.touch_x; tp->y = monitor.touch_y; tp->size = monitor.touched ? 1 : 0; tp->id = 0; return monitor.touched; } void Panel_sdl::setWindowTitle(const char *title) { _window_title = title; if (monitor.window) { SDL_SetWindowTitle(monitor.window, _window_title); } } void Panel_sdl::_update_scaling(monitor_t *mon, float sx, float sy) { mon->scaling_x = sx; mon->scaling_y = sy; int nw = mon->frame_width; int nh = mon->frame_height; if (mon->frame_rotation & 1) { std::swap(nw, nh); } int x, y, w, h; int rw, rh; SDL_GetRendererOutputSize(mon->renderer, &rw, &rh); SDL_GetWindowSize(mon->window, &w, &h); nw = nw * sx * w / rw; nh = nh * sy * h / rh; SDL_GetWindowPosition(mon->window, &x, &y); SDL_SetWindowSize(mon->window, nw, nh); SDL_SetWindowPosition(mon->window, x + (w - nw) / 2, y + (h - nh) / 2); mon->panel->sdl_invalidate(); } void Panel_sdl::sdl_create(monitor_t *m) { int flag = SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI; #if SDL_FULLSCREEN flag |= SDL_WINDOW_FULLSCREEN; #endif if (m->frame_width < _cfg.panel_width) { m->frame_width = _cfg.panel_width; } if (m->frame_height < _cfg.panel_height) { m->frame_height = _cfg.panel_height; } int window_width = m->frame_width * m->scaling_x; int window_height = m->frame_height * m->scaling_y; int scaling_x = m->scaling_x; int scaling_y = m->scaling_y; if (m->frame_rotation & 1) { std::swap(window_width, window_height); std::swap(scaling_x, scaling_y); } { m->window = SDL_CreateWindow(_window_title, SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, window_width, window_height, flag); /*last param. SDL_WINDOW_BORDERLESS to hide borders*/ } m->renderer = SDL_CreateRenderer(m->window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC); m->texture = SDL_CreateTexture(m->renderer, SDL_PIXELFORMAT_RGB24, SDL_TEXTUREACCESS_STREAMING, _cfg.panel_width, _cfg.panel_height); SDL_SetTextureBlendMode(m->texture, SDL_BLENDMODE_NONE); if (m->frame_image) { // 枠画像用のサーフェイスを作成 auto sf = SDL_CreateRGBSurfaceFrom((void *)m->frame_image, m->frame_width, m->frame_height, 32, m->frame_width * 4, 0xFF000000, 0xFF0000, 0xFF00, 0xFF); if (sf != nullptr) { // 枠画像からテクスチャを作成 m->texture_frameimage = SDL_CreateTextureFromSurface(m->renderer, sf); SDL_FreeSurface(sf); } } SDL_SetTextureBlendMode(m->texture_frameimage, SDL_BLENDMODE_BLEND); _update_scaling(m, scaling_x, scaling_y); } void Panel_sdl::sdl_update(void) { if (monitor.renderer == nullptr) { sdl_create(&monitor); } bool step_exec = _in_step_exec; if (_texupdate_counter != _modified_counter) { pixelcopy_t pc(nullptr, color_depth_t::rgb888_3Byte, _write_depth, false); if (_write_depth == rgb565_2Byte) { pc.fp_copy = pixelcopy_t::copy_rgb_fast; } else if (_write_depth == rgb888_3Byte) { pc.fp_copy = pixelcopy_t::copy_rgb_fast; } else if (_write_depth == rgb332_1Byte) { pc.fp_copy = pixelcopy_t::copy_rgb_fast; } else if (_write_depth == grayscale_8bit) { pc.fp_copy = pixelcopy_t::copy_rgb_fast; } if (0 == SDL_LockMutex(_sdl_mutex)) { _texupdate_counter = _modified_counter; for (int y = 0; y < _cfg.panel_height; ++y) { pc.src_x32 = 0; pc.src_data = _lines_buffer[y]; pc.fp_copy(&_texturebuf[y * _cfg.panel_width], 0, _cfg.panel_width, &pc); } SDL_UnlockMutex(_sdl_mutex); SDL_UpdateTexture(monitor.texture, nullptr, _texturebuf, _cfg.panel_width * sizeof(rgb888_t)); } } int angle = monitor.frame_angle; int target = (monitor.frame_rotation) * 90; angle = (((target * 4) + (angle * 4) + (angle < target ? 8 : 0)) >> 3); if (monitor.frame_angle != angle) { // 表示する向きを変える monitor.frame_angle = angle; sdl_invalidate(); } else if (monitor.frame_rotation & ~3u) { monitor.frame_rotation &= 3; monitor.frame_angle = (monitor.frame_rotation) * 90; sdl_invalidate(); } if (_invalidated || (_display_counter != _texupdate_counter)) { SDL_RendererInfo info; if (0 == SDL_GetRendererInfo(monitor.renderer, &info)) { // ステップ実行中はVSYNCを待機しない if (((bool)(info.flags & SDL_RENDERER_PRESENTVSYNC)) == step_exec) { SDL_RenderSetVSync(monitor.renderer, !step_exec); } } { int red = 0; int green = 0; int blue = 0; #if defined(M5GFX_BACK_COLOR) red = ((M5GFX_BACK_COLOR) >> 16) & 0xFF; green = ((M5GFX_BACK_COLOR) >> 8) & 0xFF; blue = ((M5GFX_BACK_COLOR)) & 0xFF; #endif SDL_SetRenderDrawColor(monitor.renderer, red, green, blue, 0xFF); } SDL_RenderClear(monitor.renderer); if (_invalidated) { _invalidated = false; int mw, mh; SDL_GetRendererOutputSize(monitor.renderer, &mw, &mh); } render_texture(monitor.texture, monitor.frame_inner_x, monitor.frame_inner_y, _cfg.panel_width, _cfg.panel_height, angle); render_texture(monitor.texture_frameimage, 0, 0, monitor.frame_width, monitor.frame_height, angle); SDL_RenderPresent(monitor.renderer); _display_counter = _texupdate_counter; if (_invalidated) { _invalidated = false; SDL_SetRenderDrawColor(monitor.renderer, 0, 0, 0, 0xFF); SDL_RenderClear(monitor.renderer); render_texture(monitor.texture, monitor.frame_inner_x, monitor.frame_inner_y, _cfg.panel_width, _cfg.panel_height, angle); render_texture(monitor.texture_frameimage, 0, 0, monitor.frame_width, monitor.frame_height, angle); SDL_RenderPresent(monitor.renderer); } } } void Panel_sdl::render_texture(SDL_Texture *texture, int tx, int ty, int tw, int th, float angle) { SDL_Point pivot; pivot.x = (monitor.frame_width / 2.0f - tx) * (float)monitor.scaling_x; pivot.y = (monitor.frame_height / 2.0f - ty) * (float)monitor.scaling_y; SDL_Rect dstrect; dstrect.w = tw * monitor.scaling_x; dstrect.h = th * monitor.scaling_y; int mw, mh; SDL_GetRendererOutputSize(monitor.renderer, &mw, &mh); dstrect.x = mw / 2.0f - pivot.x; dstrect.y = mh / 2.0f - pivot.y; SDL_RenderCopyEx(monitor.renderer, texture, nullptr, &dstrect, angle, &pivot, SDL_RendererFlip::SDL_FLIP_NONE); } bool Panel_sdl::initFrameBuffer(size_t width, size_t height) { uint8_t **lineArray = (uint8_t **)heap_alloc_dma(height * sizeof(uint8_t *)); if (nullptr == lineArray) { return false; } _texturebuf = (rgb888_t *)heap_alloc_dma(width * height * sizeof(rgb888_t)); /// 8byte alignment; width = (width + 7) & ~7u; _lines_buffer = lineArray; memset(lineArray, 0, height * sizeof(uint8_t *)); uint8_t *framebuffer = (uint8_t *)heap_alloc_dma(width * height + 16); auto fb = framebuffer; { for (size_t y = 0; y < height; ++y) { lineArray[y] = fb; fb += width; } } return true; } void Panel_sdl::deinitFrameBuffer(void) { auto lines = _lines_buffer; _lines_buffer = nullptr; if (lines != nullptr) { heap_free(lines[0]); heap_free(lines); } if (_texturebuf) { heap_free(_texturebuf); _texturebuf = nullptr; } } //---------------------------------------------------------------------------- } // namespace v1 } // namespace lgfx #endif ================================================ FILE: src/graphics/Panel_sdl.hpp ================================================ /*----------------------------------------------------------------------------/ Lovyan GFX - Graphics library for embedded devices. Original Source: https://github.com/lovyan03/LovyanGFX/ Licence: [FreeBSD](https://github.com/lovyan03/LovyanGFX/blob/master/license.txt) Author: [lovyan03](https://twitter.com/lovyan03) Contributors: [ciniml](https://github.com/ciniml) [mongonta0716](https://github.com/mongonta0716) [tobozo](https://github.com/tobozo) Porting for SDL: [imliubo](https://github.com/imliubo) /----------------------------------------------------------------------------*/ #pragma once #define SDL_MAIN_HANDLED // cppcheck-suppress preprocessorErrorDirective #if __has_include() #include #include #elif __has_include() #include #include #endif #if defined(SDL_h_) #include "lgfx/v1/Touch.hpp" #include "lgfx/v1/misc/range.hpp" #include "lgfx/v1/panel/Panel_FrameBufferBase.hpp" #include namespace lgfx { inline namespace v1 { struct Panel_sdl; struct monitor_t { SDL_Window *window = nullptr; SDL_Renderer *renderer = nullptr; SDL_Texture *texture = nullptr; SDL_Texture *texture_frameimage = nullptr; Panel_sdl *panel = nullptr; // 外枠 const void *frame_image = 0; uint_fast16_t frame_width = 0; uint_fast16_t frame_height = 0; uint_fast16_t frame_inner_x = 0; uint_fast16_t frame_inner_y = 0; int_fast16_t frame_rotation = 0; int_fast16_t frame_angle = 0; float scaling_x = 1; float scaling_y = 1; int_fast16_t touch_x, touch_y; bool touched = false; bool closing = false; }; //---------------------------------------------------------------------------- struct Touch_sdl : public ITouch { bool init(void) override { return true; } void wakeup(void) override {} void sleep(void) override {} bool isEnable(void) override { return true; }; uint_fast8_t getTouchRaw(touch_point_t *tp, uint_fast8_t count) override { return 0; } }; //---------------------------------------------------------------------------- struct Panel_sdl : public Panel_FrameBufferBase { static constexpr size_t EMULATED_GPIO_MAX = 128; static volatile uint8_t _gpio_dummy_values[EMULATED_GPIO_MAX]; public: Panel_sdl(void); virtual ~Panel_sdl(void); bool init(bool use_reset) override; color_depth_t setColorDepth(color_depth_t depth) override; void display(uint_fast16_t x, uint_fast16_t y, uint_fast16_t w, uint_fast16_t h) override; // void setInvert(bool invert) override {} void drawPixelPreclipped(uint_fast16_t x, uint_fast16_t y, uint32_t rawcolor) override; void writeFillRectPreclipped(uint_fast16_t x, uint_fast16_t y, uint_fast16_t w, uint_fast16_t h, uint32_t rawcolor) override; void writeBlock(uint32_t rawcolor, uint32_t length) override; void writeImage(uint_fast16_t x, uint_fast16_t y, uint_fast16_t w, uint_fast16_t h, pixelcopy_t *param, bool use_dma) override; void writeImageARGB(uint_fast16_t x, uint_fast16_t y, uint_fast16_t w, uint_fast16_t h, pixelcopy_t *param) override; void writePixels(pixelcopy_t *param, uint32_t len, bool use_dma) override; uint_fast8_t getTouchRaw(touch_point_t *tp, uint_fast8_t count) override; void setWindowTitle(const char *title); void setScaling(uint_fast8_t scaling_x, uint_fast8_t scaling_y); void setFrameImage(const void *frame_image, int frame_width, int frame_height, int inner_x, int inner_y); void setFrameRotation(uint_fast16_t frame_rotaion); void setBrightness(uint8_t brightness) override{}; static volatile void gpio_hi(uint32_t pin) { _gpio_dummy_values[pin & (EMULATED_GPIO_MAX - 1)] = 1; } static volatile void gpio_lo(uint32_t pin) { _gpio_dummy_values[pin & (EMULATED_GPIO_MAX - 1)] = 0; } static volatile bool gpio_in(uint32_t pin) { return _gpio_dummy_values[pin & (EMULATED_GPIO_MAX - 1)]; } static int setup(void); static int loop(void); static int close(void); static int main(int (*fn)(bool *), uint32_t msec_step_exec = 512); static void setShortcutKeymod(SDL_Keymod keymod) { _keymod = keymod; } struct KeyCodeMapping_t { SDL_KeyCode keycode = SDLK_UNKNOWN; uint8_t gpio = 0; }; static void addKeyCodeMapping(SDL_KeyCode keyCode, uint8_t gpio); static int getKeyCodeMapping(SDL_KeyCode keyCode); protected: const char *_window_title = "LGFX Simulator"; SDL_mutex *_sdl_mutex = nullptr; void sdl_create(monitor_t *m); void sdl_update(void); touch_point_t _touch_point; monitor_t monitor; rgb888_t *_texturebuf = nullptr; uint_fast16_t _modified_counter; uint_fast16_t _texupdate_counter; uint_fast16_t _display_counter; bool _invalidated; static void _event_proc(void); static void _update_proc(void); static void _update_scaling(monitor_t *m, float sx, float sy); void sdl_invalidate(void) { _invalidated = true; } void render_texture(SDL_Texture *texture, int tx, int ty, int tw, int th, float angle); bool initFrameBuffer(size_t width, size_t height); void deinitFrameBuffer(void); static SDL_Keymod _keymod; struct lock_t { lock_t(Panel_sdl *parent); ~lock_t(); protected: Panel_sdl *_parent; }; }; //---------------------------------------------------------------------------- } // namespace v1 } // namespace lgfx #endif ================================================ FILE: src/graphics/PointStruct.h ================================================ struct PointStruct { int x; int y; }; ================================================ FILE: src/graphics/Screen.cpp ================================================ /* BaseUI Developed and Maintained By: - Ronald Garcia (HarukiToreda) – Lead development and implementation. - JasonP (Xaositek) – Screen layout and icon design, UI improvements and testing. - TonyG (Tropho) – Project management, structural planning, and testing 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 "NodeDB.h" #include "PowerMon.h" #include "Throttle.h" #include "configuration.h" #include "meshUtils.h" #if HAS_SCREEN #include #include "DisplayFormatters.h" #include "TimeFormatters.h" #include "draw/ClockRenderer.h" #include "draw/DebugRenderer.h" #include "draw/MenuHandler.h" #include "draw/MessageRenderer.h" #include "draw/NodeListRenderer.h" #include "draw/NotificationRenderer.h" #include "draw/UIRenderer.h" #include "modules/CannedMessageModule.h" #if !MESHTASTIC_EXCLUDE_GPS #include "GPS.h" #include "buzz.h" #endif #include "FSCommon.h" #include "MeshService.h" #include "MessageStore.h" #include "RadioLibInterface.h" #include "error.h" #include "gps/GeoCoord.h" #include "gps/RTC.h" #include "graphics/ScreenFonts.h" #include "graphics/SharedUIDisplay.h" #include "graphics/emotes.h" #include "graphics/images.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 "modules/ExternalNotificationModule.h" #include "modules/TextMessageModule.h" #include "modules/WaypointModule.h" #include "sleep.h" #include "target_specific.h" extern MessageStore messageStore; #if USE_TFTDISPLAY extern uint16_t TFT_MESH; #else uint16_t TFT_MESH = COLOR565(0x67, 0xEA, 0x94); #endif #if HAS_WIFI && !defined(ARCH_PORTDUINO) #include "mesh/wifi/WiFiAPClient.h" #endif #ifdef ARCH_ESP32 #endif #if ARCH_PORTDUINO #include "modules/StoreForwardModule.h" #include "platform/portduino/PortduinoGlue.h" #endif #if defined(T_LORA_PAGER) // KB backlight control #include "input/cardKbI2cImpl.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 #define ASCII_BELL '\x07' // A text message frame + debug frame + all the node infos FrameCallback *normalFrames; static uint32_t targetFramerate = IDLE_FRAMERATE; // Global variables for alert banner - explicitly define with extern "C" linkage to prevent optimization uint32_t logo_timeout = 5000; // 4 seconds for EACH logo // 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; #if HAS_GPS // GeoCoord object for the screen GeoCoord geoCoord; #endif #ifdef SHOW_REDRAWS static bool heartbeat = false; #endif #include "graphics/ScreenFonts.h" #include // Usage: int stringWidth = formatDateTime(datetimeStr, sizeof(datetimeStr), rtc_sec, display); // End Functions to write date/time to the screen extern bool hasUnreadMessage; // ============================== // Overlay Alert Banner Renderer // ============================== // Displays a temporary centered banner message (e.g., warning, status, etc.) // The banner appears in the center of the screen and disappears after the specified duration void Screen::showSimpleBanner(const char *message, uint32_t durationMs) { BannerOverlayOptions options; options.message = message; options.durationMs = durationMs; options.notificationType = notificationTypeEnum::text_banner; showOverlayBanner(options); } // Called to trigger a banner with custom message and duration void Screen::showOverlayBanner(BannerOverlayOptions banner_overlay_options) { #ifdef USE_EINK EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip full refresh for all overlay menus #endif // Store the message and set the expiration timestamp strncpy(NotificationRenderer::alertBannerMessage, banner_overlay_options.message, 255); NotificationRenderer::alertBannerMessage[255] = '\0'; // Ensure null termination NotificationRenderer::alertBannerUntil = (banner_overlay_options.durationMs == 0) ? 0 : millis() + banner_overlay_options.durationMs; NotificationRenderer::optionsArrayPtr = banner_overlay_options.optionsArrayPtr; NotificationRenderer::optionsEnumPtr = banner_overlay_options.optionsEnumPtr; NotificationRenderer::alertBannerOptions = banner_overlay_options.optionsCount; NotificationRenderer::alertBannerCallback = banner_overlay_options.bannerCallback; NotificationRenderer::curSelected = banner_overlay_options.InitialSelected; NotificationRenderer::pauseBanner = false; NotificationRenderer::current_notification_type = notificationTypeEnum::selection_picker; static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback}; ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); ui->setTargetFPS(60); ui->update(); } // Called to trigger a banner with custom message and duration void Screen::showNodePicker(const char *message, uint32_t durationMs, std::function bannerCallback) { #ifdef USE_EINK EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip full refresh for all overlay menus #endif nodeDB->pause_sort(true); // Store the message and set the expiration timestamp strncpy(NotificationRenderer::alertBannerMessage, message, 255); NotificationRenderer::alertBannerMessage[255] = '\0'; // Ensure null termination NotificationRenderer::alertBannerUntil = (durationMs == 0) ? 0 : millis() + durationMs; NotificationRenderer::alertBannerCallback = bannerCallback; NotificationRenderer::pauseBanner = false; NotificationRenderer::curSelected = 0; NotificationRenderer::current_notification_type = notificationTypeEnum::node_picker; static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback}; ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); ui->setTargetFPS(60); ui->update(); } // Called to trigger a banner with custom message and duration void Screen::showNumberPicker(const char *message, uint32_t durationMs, uint8_t digits, std::function bannerCallback) { #ifdef USE_EINK EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip full refresh for all overlay menus #endif // Store the message and set the expiration timestamp strncpy(NotificationRenderer::alertBannerMessage, message, 255); NotificationRenderer::alertBannerMessage[255] = '\0'; // Ensure null termination NotificationRenderer::alertBannerUntil = (durationMs == 0) ? 0 : millis() + durationMs; NotificationRenderer::alertBannerCallback = bannerCallback; NotificationRenderer::pauseBanner = false; NotificationRenderer::curSelected = 0; NotificationRenderer::current_notification_type = notificationTypeEnum::number_picker; NotificationRenderer::numDigits = digits; NotificationRenderer::currentNumber = 0; static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback}; ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); ui->setTargetFPS(60); ui->update(); } void Screen::showTextInput(const char *header, const char *initialText, uint32_t durationMs, std::function textCallback) { LOG_INFO("showTextInput called with header='%s', durationMs=%d", header ? header : "NULL", durationMs); // Start OnScreenKeyboardModule session (non-touch variant) OnScreenKeyboardModule::instance().start(header, initialText, durationMs, textCallback); NotificationRenderer::textInputCallback = textCallback; // Store the message and set the expiration timestamp (use same pattern as other notifications) strncpy(NotificationRenderer::alertBannerMessage, header ? header : "Text Input", 255); NotificationRenderer::alertBannerMessage[255] = '\0'; NotificationRenderer::alertBannerUntil = (durationMs == 0) ? 0 : millis() + durationMs; NotificationRenderer::pauseBanner = false; NotificationRenderer::current_notification_type = notificationTypeEnum::text_input; // Set the overlay using the same pattern as other notification types static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback}; ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); ui->setTargetFPS(60); ui->update(); } static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { uint8_t module_frame; // there's a little but in the UI transition code // where it invokes the function at the correct offset // in the array of "drawScreen" functions; however, // the passed-state doesn't quite reflect the "current" // screen, so we have to detect it. if (state->frameState == IN_TRANSITION && state->transitionFrameRelationship == TransitionRelationship_INCOMING) { // if we're transitioning from the end of the frame list back around to the first // frame, then we want this to be `0` module_frame = state->transitionFrameTarget; } else { // otherwise, just display the module frame that's aligned with the current frame module_frame = state->currentFrame; } MeshModule &pi = *moduleFrames.at(module_frame); pi.drawFrame(display, state, x, y); } /** * 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) * RAD_TO_DEG; oldLat = lat; oldLon = lon; return b; } /// We will skip one node - the one for us, so we just blindly loop over all /// nodes static int8_t prevFrame = -1; // Combined dynamic node list frame cycling through LastHeard, HopSignal, and Distance modes // Uses a single frame and changes data every few seconds (E-Ink variant is separate) #if defined(ESP_PLATFORM) && (defined(USE_ST7789) || defined(USE_ST7796)) 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]; int32_t rawRGB = uiconfig.screen_rgb_color; // Only validate the combined value once if (rawRGB > 0 && rawRGB <= 255255255) { LOG_INFO("Setting screen RGB color to user chosen: 0x%06X", rawRGB); // Extract each component as a normal int first int r = (rawRGB >> 16) & 0xFF; int g = (rawRGB >> 8) & 0xFF; int b = rawRGB & 0xFF; if (r >= 0 && r <= 255 && g >= 0 && g <= 255 && b >= 0 && b <= 255) { TFT_MESH = COLOR565(static_cast(r), static_cast(g), static_cast(b)); } #ifdef TFT_MESH_OVERRIDE } else if (rawRGB == 0) { LOG_INFO("Setting screen RGB color to TFT_MESH_OVERRIDE: 0x%04X", TFT_MESH_OVERRIDE); // Default to TFT_MESH_OVERRIDE if available TFT_MESH = TFT_MESH_OVERRIDE; #endif } else { // Default best readable yellow color LOG_INFO("Setting screen RGB color to default: (255,255,128)"); TFT_MESH = COLOR565(255, 255, 128); } #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_ST7796) #ifdef ESP_PLATFORM dispdev = new ST7796Spi(&SPI1, ST7796_RESET, ST7796_RS, ST7796_NSS, GEOMETRY_RAWMODE, TFT_WIDTH, TFT_HEIGHT, ST7796_SDA, ST7796_MISO, ST7796_SCK, TFT_SPI_FREQUENCY); #else dispdev = new ST7796Spi(&SPI1, ST7796_RESET, ST7796_RS, ST7796_NSS, GEOMETRY_RAWMODE, TFT_WIDTH, TFT_HEIGHT); #endif #elif defined(USE_SSD1306) dispdev = new SSD1306Wire(address.address, -1, -1, geometry, (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); #elif defined(USE_SPISSD1306) dispdev = new SSD1306Spi(SSD1306_RESET, SSD1306_RS, SSD1306_NSS, GEOMETRY_64_48); if (!dispdev->init()) { LOG_DEBUG("Error: SSD1306 not detected!"); } else { static_cast(dispdev)->setHorizontalOffset(32); LOG_INFO("SSD1306 init success"); } #elif defined(ST7735_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7789_CS) || \ defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) || defined(HACKADAY_COMMUNICATOR) dispdev = new TFTDisplay(address.address, -1, -1, geometry, (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); #elif defined(USE_EINK) && !defined(USE_EINK_DYNAMICDISPLAY) dispdev = new EInkDisplay(address.address, -1, -1, geometry, (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); #elif defined(USE_EINK) && defined(USE_EINK_DYNAMICDISPLAY) dispdev = new EInkDynamicDisplay(address.address, -1, -1, geometry, (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); #elif defined(USE_ST7567) dispdev = new ST7567Wire(address.address, -1, -1, geometry, (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); #elif ARCH_PORTDUINO if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { if (portduino_config.displayPanel != no_screen) { LOG_DEBUG("Make TFTDisplay!"); dispdev = new TFTDisplay(address.address, -1, -1, geometry, (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); } 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 #if defined(USE_ST7789) static_cast(dispdev)->setRGB(TFT_MESH); #elif defined(USE_ST7796) static_cast(dispdev)->setRGB(TFT_MESH); #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, graphics::UIRenderer::drawDeepSleepFrame); #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("Turn on screen"); powerMon->setState(meshtastic_PowerMon_State_Screen_On); #ifdef T_WATCH_S3 PMU->enablePowerOutput(XPOWERS_ALDO2); #endif // some screens seem to need a kick in the pants to turn back on #if defined(MUZI_BASE) || defined(M5STACK_CARDPUTER_ADV) dispdev->init(); dispdev->setBrightness(brightness); dispdev->flipScreenVertically(); dispdev->resetDisplay(); #ifdef SCREEN_12V_ENABLE digitalWrite(SCREEN_12V_ENABLE, HIGH); #endif delay(100); #endif #if !ARCH_PORTDUINO dispdev->displayOn(); #endif #ifdef PIN_EINK_EN if (uiconfig.screen_brightness == 1) digitalWrite(PIN_EINK_EN, HIGH); #elif defined(PCA_PIN_EINK_EN) if (uiconfig.screen_brightness > 0) io.digitalWrite(PCA_PIN_EINK_EN, HIGH); #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(); #if defined(HELTEC_TRACKER_V1_X) || defined(HELTEC_WIRELESS_TRACKER_V2) ui->init(); #endif #if defined(USE_ST7789) && defined(VTFT_LEDA) #ifdef VTFT_CTRL pinMode(VTFT_CTRL, OUTPUT); digitalWrite(VTFT_CTRL, LOW); #endif ui->init(); #ifdef ESP_PLATFORM analogWrite(VTFT_LEDA, BRIGHTNESS_DEFAULT); #else pinMode(VTFT_LEDA, OUTPUT); digitalWrite(VTFT_LEDA, TFT_BACKLIGHT_ON); #endif #endif #ifdef USE_ST7796 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 #ifdef PIN_EINK_EN digitalWrite(PIN_EINK_EN, LOW); #elif defined(PCA_PIN_EINK_EN) io.digitalWrite(PCA_PIN_EINK_EN, LOW); #endif dispdev->displayOff(); #ifdef SCREEN_12V_ENABLE digitalWrite(SCREEN_12V_ENABLE, LOW); #endif #ifdef USE_ST7789 SPI1.end(); #if defined(ARCH_ESP32) #ifdef VTFT_LEDA pinMode(VTFT_LEDA, ANALOG); #endif #ifdef VTFT_CTRL pinMode(VTFT_CTRL, ANALOG); #endif 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 USE_ST7796 SPI1.end(); #if defined(ARCH_ESP32) pinMode(VTFT_LEDA, OUTPUT); digitalWrite(VTFT_LEDA, LOW); pinMode(ST7796_RESET, ANALOG); pinMode(ST7796_RS, ANALOG); pinMode(ST7796_NSS, ANALOG); #else nrf_gpio_cfg_default(VTFT_LEDA); nrf_gpio_cfg_default(ST7796_RESET); nrf_gpio_cfg_default(ST7796_RS); nrf_gpio_cfg_default(ST7796_NSS); #endif #endif #ifdef T_WATCH_S3 PMU->disablePowerOutput(XPOWERS_ALDO2); #endif enabled = false; } screenOn = on; } } void Screen::setup() { // Enable display rendering useDisplay = true; // Load saved brightness from UI config // For OLED displays (SSD1306), default brightness is 255 if not set if (uiconfig.screen_brightness == 0) { #if defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107) brightness = 255; // Default for OLED #else brightness = BRIGHTNESS_DEFAULT; #endif } else { brightness = uiconfig.screen_brightness; } // Detect OLED subtype (if supported by board variant) #ifdef AutoOLEDWire_h if (isAUTOOled) static_cast(dispdev)->setDetected(model); #endif #if defined(USE_SH1107_128_64) || defined(USE_SH1107) static_cast(dispdev)->setSubtype(7); #endif #if defined(USE_ST7789) && defined(TFT_MESH) // Apply custom RGB color (e.g. Heltec T114/T190) static_cast(dispdev)->setRGB(TFT_MESH); #endif #if defined(MUZI_BASE) dispdev->delayPoweron = true; #endif #if defined(USE_ST7796) && defined(TFT_MESH) // Custom text color, if defined in variant.h static_cast(dispdev)->setRGB(TFT_MESH); #endif // Initialize display and UI system ui->init(); displayWidth = dispdev->width(); displayHeight = dispdev->height(); ui->setTimePerTransition(0); // Disable animation delays ui->setIndicatorPosition(BOTTOM); // Not used (indicators disabled below) ui->setIndicatorDirection(LEFT_RIGHT); // Not used (indicators disabled below) ui->setFrameAnimation(SLIDE_LEFT); // Used only when indicators are active ui->disableAllIndicators(); // Disable page indicator dots ui->getUiState()->userData = this; // Allow static callbacks to access Screen instance // Apply loaded brightness #if defined(ST7789_CS) static_cast(dispdev)->setDisplayBrightness(brightness); #elif defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107) || defined(USE_SPISSD1306) dispdev->setBrightness(brightness); #endif LOG_INFO("Applied screen brightness: %d", brightness); // Set custom overlay callbacks static OverlayCallback overlays[] = { graphics::UIRenderer::drawNavigationBar // Custom indicator icons for each frame }; ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); // Enable UTF-8 to display mapping dispdev->setFontTableLookupFunction(customFontTableLookup); #ifdef USERPREFS_OEM_TEXT logo_timeout *= 2; // Give more time for branded boot logos #endif // Configure alert frames (e.g., "Resuming..." or region name) EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip slow refresh alertFrames[0] = [this](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { #ifdef ARCH_ESP32 if (wakeCause == ESP_SLEEP_WAKEUP_TIMER || wakeCause == ESP_SLEEP_WAKEUP_EXT1) graphics::UIRenderer::drawFrameText(display, state, x, y, "Resuming..."); else #endif { const char *region = myRegion ? myRegion->name : nullptr; graphics::UIRenderer::drawIconScreen(region, display, state, x, y); } }; ui->setFrames(alertFrames, 1); ui->disableAutoTransition(); // Require manual navigation between frames // Log buffer for on-screen logs (3 lines max) dispdev->setLogBuffer(3, 32); // Optional screen mirroring or flipping (e.g. for T-Beam orientation) #ifdef SCREEN_MIRROR dispdev->mirrorScreen(); #else if (!config.display.flip_screen) { #if defined(ST7701_CS) || defined(ST7735_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7789_CS) || \ defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) || defined(HACKADAY_COMMUNICATOR) static_cast(dispdev)->flipScreenVertically(); #elif defined(USE_ST7789) static_cast(dispdev)->flipScreenVertically(); #elif defined(USE_ST7796) static_cast(dispdev)->mirrorScreen(); #elif !defined(M5STACK_UNITC6L) dispdev->flipScreenVertically(); #endif } #endif // Generate device ID from MAC address uint8_t dmac[6]; getMacAddr(dmac); snprintf(screen->ourId, sizeof(screen->ourId), "%02x%02x", dmac[4], dmac[5]); #if ARCH_PORTDUINO handleSetOn(false); // Ensure proper init for Arduino targets #endif // Turn on display and trigger first draw handleSetOn(true); graphics::currentResolution = graphics::determineScreenResolution(dispdev->height(), dispdev->width()); ui->update(); #ifndef USE_EINK ui->update(); // Some SSD1306 clones drop the first draw, so run twice #endif serialSinceMsec = millis(); #if ARCH_PORTDUINO if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { if (portduino_config.touchscreenModule) { touchScreenImpl1 = new TouchScreenImpl1(dispdev->getWidth(), dispdev->getHeight(), static_cast(dispdev)->getTouch); touchScreenImpl1->init(); } } #elif HAS_TOUCHSCREEN && !defined(USE_EINK) && !HAS_CST226SE touchScreenImpl1 = new TouchScreenImpl1(dispdev->getWidth(), dispdev->getHeight(), static_cast(dispdev)->getTouch); touchScreenImpl1->init(); #endif // Subscribe to device status updates powerStatusObserver.observe(&powerStatus->onNewStatus); gpsStatusObserver.observe(&gpsStatus->onNewStatus); nodeStatusObserver.observe(&nodeStatus->onNewStatus); #if !MESHTASTIC_EXCLUDE_ADMIN adminMessageObserver.observe(adminModule); #endif if (inputBroker) inputObserver.observe(inputBroker); // Load persisted messages into RAM messageStore.loadFromFlash(); LOG_INFO("MessageStore loaded from flash"); // Notify modules that support UI events MeshModule::observeUIEvents(&uiFrameEventObserver); } void Screen::setOn(bool on, FrameCallback einkScreensaver) { #if defined(T_LORA_PAGER) if (cardKbI2cImpl) cardKbI2cImpl->toggleBacklight(on); #endif if (!on) // We handle off commands immediately, because they might be called because the CPU is shutting down handleSetOn(false, einkScreensaver); else enqueueCmd(ScreenCmd{.cmd = Cmd::SET_ON}); } 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(); #else // No delay between UI frame rendering if (forceUiUpdate) { setFastFramerate(); } #endif } static uint32_t lastScreenTransition; int32_t Screen::runOnce() { // If we don't have a screen, don't ever spend any CPU for us. if (!useDisplay) { enabled = false; return RUN_SAME; } if (displayHeight == 0) { displayHeight = dispdev->getHeight(); } // Detect frame transitions and clear message cache when leaving text message screen { static int8_t lastFrameIndex = -1; int8_t currentFrameIndex = ui->getUiState()->currentFrame; int8_t textMsgIndex = framesetInfo.positions.textMessage; if (lastFrameIndex != -1 && currentFrameIndex != lastFrameIndex) { if (lastFrameIndex == textMsgIndex && currentFrameIndex != textMsgIndex) { graphics::MessageRenderer::clearMessageCache(); } } lastFrameIndex = currentFrameIndex; } menuHandler::handleMenuSwitch(dispdev); // Show boot screen for first logo_timeout seconds, then switch to normal operation. // serialSinceMsec adjusts for additional serial wait time during nRF52 bootup static bool showingBootScreen = true; if (showingBootScreen && (millis() > (logo_timeout + serialSinceMsec))) { LOG_INFO("Done with boot screen"); stopBootScreen(); showingBootScreen = false; } #ifdef USERPREFS_OEM_TEXT static bool showingOEMBootScreen = true; if (showingOEMBootScreen && (millis() > ((logo_timeout / 2) + serialSinceMsec))) { LOG_INFO("Switch to OEM screen..."); // Change frames. static FrameCallback bootOEMFrames[] = {graphics::UIRenderer::drawOEMBootScreen}; static const int bootOEMFrameCount = sizeof(bootOEMFrames) / sizeof(bootOEMFrames[0]); ui->setFrames(bootOEMFrames, bootOEMFrameCount); ui->update(); #ifndef USE_EINK ui->update(); #endif showingOEMBootScreen = false; } #endif #ifndef DISABLE_WELCOME_UNSET if (!NotificationRenderer::isOverlayBannerShowing() && config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET) { #if defined(M5STACK_UNITC6L) menuHandler::LoraRegionPicker(); #else menuHandler::OnboardMessage(); #endif } #endif if (!NotificationRenderer::isOverlayBannerShowing() && rebootAtMsec != 0 && !suppressRebootBanner) { showSimpleBanner("Rebooting...", 0); } // Process incoming commands. for (;;) { ScreenCmd cmd; if (!cmdQueue.dequeue(&cmd, 0)) { break; } switch (cmd.cmd) { case Cmd::SET_ON: handleSetOn(true); break; case Cmd::SET_OFF: handleSetOn(false); break; case Cmd::ON_PRESS: if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { showFrame(FrameDirection::NEXT); } break; case Cmd::SHOW_PREV_FRAME: if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { showFrame(FrameDirection::PREVIOUS); } break; case Cmd::SHOW_NEXT_FRAME: if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { showFrame(FrameDirection::NEXT); } break; case Cmd::START_ALERT_FRAME: { showingBootScreen = false; // this should avoid the edge case where an alert triggers before the boot screen goes away showingNormalScreen = false; NotificationRenderer::pauseBanner = true; alertFrames[0] = alertFrame; #ifdef USE_EINK EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Use fast-refresh for next frame, no skip please EINK_ADD_FRAMEFLAG(dispdev, BLOCKING); // Edge case: if this frame is promoted to COSMETIC, wait for update handleSetOn(true); // Ensure power-on to receive deep-sleep screensaver (PowerFSM should handle?) #endif setFrameImmediateDraw(alertFrames); break; } case Cmd::START_FIRMWARE_UPDATE_SCREEN: handleStartFirmwareUpdateScreen(); break; case Cmd::STOP_ALERT_FRAME: NotificationRenderer::pauseBanner = false; // Return from one-off alert mode back to regular frames. if (!showingNormalScreen && NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { setFrames(); } break; case Cmd::STOP_BOOT_SCREEN: EINK_ADD_FRAMEFLAG(dispdev, COSMETIC); // E-Ink: Explicitly use full-refresh for next frame if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { setFrames(); } break; case Cmd::NOOP: break; default: LOG_ERROR("Invalid screen cmd"); } } if (!screenOn) { // If we didn't just wake and the screen is still off, then // stop updating until it is on again enabled = false; return 0; } // 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 && NotificationRenderer::current_notification_type != notificationTypeEnum::text_input && !Throttle::isWithinTimespanMs(lastScreenTransition, config.display.auto_screen_carousel_secs * 1000)) { // If an E-Ink display struggles with fast refresh, force carousel to use full refresh instead // Carousel is potentially a major source of E-Ink display wear #if !defined(EINK_BACKGROUND_USES_FAST) EINK_ADD_FRAMEFLAG(dispdev, COSMETIC); #endif LOG_DEBUG("LastScreenTransition exceeded %ums transition to next frame", (millis() - lastScreenTransition)); handleOnPress(); } } // LOG_DEBUG("want fps %d, fixed=%d", targetFramerate, // ui->getUiState()->frameState); If we are scrolling we need to be called // soon, otherwise just 1 fps (to save CPU) We also ask to be called twice // as fast as we really need so that any rounding errors still result with // the correct framerate return (1000 / targetFramerate); } /* show a message that the SSL cert is being built * it is expected that this will be used during the boot phase */ void Screen::setSSLFrames() { if (address_found.address) { // LOG_DEBUG("Show SSL frames"); static FrameCallback sslFrames[] = {NotificationRenderer::drawSSLScreen}; ui->setFrames(sslFrames, 1); ui->update(); } } #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 = graphics::UIRenderer::drawScreensaverOverlay; ui->setOverlays(&screensaverOverlay, 1); } // Request new frame, ASAP setFastFramerate(); uint64_t startUpdate; do { startUpdate = millis(); // Handle impossibly unlikely corner case of a millis() overflow.. delay(1); ui->update(); } while (ui->getUiState()->lastUpdate < startUpdate); // 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) { // Block setFrames calls when virtual keyboard is active to prevent overlay interference if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) { return; } uint8_t originalPosition = ui->getUiState()->currentFrame; uint8_t previousFrameCount = framesetInfo.frameCount; FramesetInfo fsi; // Location of specific frames, for applying focus parameter graphics::UIRenderer::rebuildFavoritedNodes(); LOG_DEBUG("Show standard frames"); showingNormalScreen = true; indicatorIcons.clear(); size_t numframes = 0; // If we have a critical fault, show it first fsi.positions.fault = numframes; if (error_code) { normalFrames[numframes++] = NotificationRenderer::drawCriticalFaultFrame; indicatorIcons.push_back(icon_error); focus = FOCUS_FAULT; // Change our "focus" parameter, to ensure we show the fault frame } #if defined(DISPLAY_CLOCK_FRAME) if (!hiddenFrames.clock) { fsi.positions.clock = numframes; #if defined(M5STACK_UNITC6L) normalFrames[numframes++] = graphics::ClockRenderer::drawAnalogClockFrame; #else normalFrames[numframes++] = uiconfig.is_clockface_analog ? graphics::ClockRenderer::drawAnalogClockFrame : graphics::ClockRenderer::drawDigitalClockFrame; #endif indicatorIcons.push_back(digital_icon_clock); } #endif if (!hiddenFrames.home) { fsi.positions.home = numframes; normalFrames[numframes++] = graphics::UIRenderer::drawDeviceFocused; indicatorIcons.push_back(icon_home); } fsi.positions.textMessage = numframes; normalFrames[numframes++] = graphics::MessageRenderer::drawTextMessageFrame; indicatorIcons.push_back(icon_mail); #ifndef USE_EINK if (!hiddenFrames.nodelist_nodes) { fsi.positions.nodelist_nodes = numframes; normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicListScreen_Nodes; indicatorIcons.push_back(icon_nodes); } if (!hiddenFrames.nodelist_location) { fsi.positions.nodelist_location = numframes; normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicListScreen_Location; indicatorIcons.push_back(icon_list); } #endif // Show detailed node views only on E-Ink builds #ifdef USE_EINK if (!hiddenFrames.nodelist_lastheard) { fsi.positions.nodelist_lastheard = numframes; normalFrames[numframes++] = graphics::NodeListRenderer::drawLastHeardScreen; indicatorIcons.push_back(icon_nodes); } if (!hiddenFrames.nodelist_hopsignal) { fsi.positions.nodelist_hopsignal = numframes; normalFrames[numframes++] = graphics::NodeListRenderer::drawHopSignalScreen; indicatorIcons.push_back(icon_signal); } if (!hiddenFrames.nodelist_distance) { fsi.positions.nodelist_distance = numframes; normalFrames[numframes++] = graphics::NodeListRenderer::drawDistanceScreen; indicatorIcons.push_back(icon_distance); } #endif #if HAS_GPS #ifdef USE_EINK if (!hiddenFrames.nodelist_bearings) { fsi.positions.nodelist_bearings = numframes; normalFrames[numframes++] = graphics::NodeListRenderer::drawNodeListWithCompasses; indicatorIcons.push_back(icon_list); } #endif if (!hiddenFrames.gps) { fsi.positions.gps = numframes; normalFrames[numframes++] = graphics::UIRenderer::drawCompassAndLocationScreen; indicatorIcons.push_back(icon_compass); } #endif if (RadioLibInterface::instance && !hiddenFrames.lora) { fsi.positions.lora = numframes; normalFrames[numframes++] = graphics::DebugRenderer::drawLoRaFocused; indicatorIcons.push_back(icon_radio); } if (!hiddenFrames.system) { fsi.positions.system = numframes; normalFrames[numframes++] = graphics::DebugRenderer::drawSystemScreen; indicatorIcons.push_back(icon_system); } #if !defined(DISPLAY_CLOCK_FRAME) if (!hiddenFrames.clock) { fsi.positions.clock = numframes; normalFrames[numframes++] = uiconfig.is_clockface_analog ? graphics::ClockRenderer::drawAnalogClockFrame : graphics::ClockRenderer::drawDigitalClockFrame; indicatorIcons.push_back(digital_icon_clock); } #endif if (!hiddenFrames.chirpy) { fsi.positions.chirpy = numframes; normalFrames[numframes++] = graphics::DebugRenderer::drawChirpy; indicatorIcons.push_back(chirpy_small); } #if HAS_WIFI && !defined(ARCH_PORTDUINO) if (!hiddenFrames.wifi && isWifiAvailable()) { fsi.positions.wifi = numframes; normalFrames[numframes++] = graphics::DebugRenderer::drawDebugInfoWiFiTrampoline; indicatorIcons.push_back(icon_wifi); } #endif // Beware of what changes you make in this code! // We pass numframes into GetMeshModulesWithUIFrames() which is highly important! // Inside of that callback, goes over to MeshModule.cpp and we run // modulesWithUIFrames.resize(startIndex, nullptr), to insert nullptr // entries until we're ready to start building the matching entries. // We are doing our best to keep the normalFrames vector // and the moduleFrames vector in lock step. moduleFrames = MeshModule::GetMeshModulesWithUIFrames(numframes); LOG_DEBUG("Show %d module frames", moduleFrames.size()); for (auto i = moduleFrames.begin(); i != moduleFrames.end(); ++i) { // Draw the module frame, using the hack described above if (*i != nullptr) { normalFrames[numframes] = drawModuleFrame; // Check if the module being drawn has requested focus // We will honor this request later, if setFrames was triggered by a UIFrameEvent MeshModule *m = *i; if (m && m->isRequestingFocus()) fsi.positions.focusedModule = numframes; if (m && m == waypointModule) fsi.positions.waypoint = numframes; indicatorIcons.push_back(icon_module); numframes++; } } LOG_DEBUG("Added modules. numframes: %d", numframes); // We don't show the node info of our node (if we have it yet - we should) size_t numMeshNodes = nodeDB->getNumMeshNodes(); if (numMeshNodes > 0) numMeshNodes--; if (!hiddenFrames.show_favorites) { // Temporary array to hold favorite node frames std::vector favoriteFrames; for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) { const meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i); if (n && n->num != nodeDB->getNodeNum() && n->is_favorite) { favoriteFrames.push_back(graphics::UIRenderer::drawNodeInfo); } } // Insert favorite frames *after* collecting them all if (!favoriteFrames.empty()) { fsi.positions.firstFavorite = numframes; for (const auto &f : favoriteFrames) { normalFrames[numframes++] = f; indicatorIcons.push_back(icon_node); } fsi.positions.lastFavorite = numframes - 1; } else { fsi.positions.firstFavorite = 255; fsi.positions.lastFavorite = 255; } } fsi.frameCount = numframes; // Total framecount is used to apply FOCUS_PRESERVE this->frameCount = numframes; // Save frame count for use in custom overlay LOG_DEBUG("Finished build frames. numframes: %d", numframes); ui->setFrames(normalFrames, numframes); ui->disableAllIndicators(); // Add overlays: frame icons and alert banner) static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback}; ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); prevFrame = -1; // Force drawNodeInfo to pick a new node (because our list just changed) // Focus on a specific frame, in the frame set we just created switch (focus) { case FOCUS_DEFAULT: ui->switchToFrame(fsi.positions.deviceFocused); break; case FOCUS_FAULT: ui->switchToFrame(fsi.positions.fault); break; case FOCUS_MODULE: // Whichever frame was marked by MeshModule::requestFocus(), if any // If no module requested focus, will show the first frame instead ui->switchToFrame(fsi.positions.focusedModule); break; case FOCUS_CLOCK: // Whichever frame was marked by MeshModule::requestFocus(), if any // If no module requested focus, will show the first frame instead ui->switchToFrame(fsi.positions.clock); break; case FOCUS_SYSTEM: ui->switchToFrame(fsi.positions.system); break; case FOCUS_PRESERVE: // No more adjustment — force stay on same index if (previousFrameCount > fsi.frameCount) { ui->switchToFrame(originalPosition - 1); } else if (previousFrameCount < fsi.frameCount) { ui->switchToFrame(originalPosition + 1); } else { ui->switchToFrame(originalPosition); } break; } // Store the info about this frameset, for future setFrames calls this->framesetInfo = fsi; setFastFramerate(); // Draw ASAP } void Screen::setFrameImmediateDraw(FrameCallback *drawFrames) { ui->disableAllIndicators(); ui->setFrames(drawFrames, 1); setFastFramerate(); } void Screen::toggleFrameVisibility(const std::string &frameName) { #ifndef USE_EINK if (frameName == "nodelist_nodes") { hiddenFrames.nodelist_nodes = !hiddenFrames.nodelist_nodes; } if (frameName == "nodelist_location") { hiddenFrames.nodelist_location = !hiddenFrames.nodelist_location; } #endif #ifdef USE_EINK if (frameName == "nodelist_lastheard") { hiddenFrames.nodelist_lastheard = !hiddenFrames.nodelist_lastheard; } if (frameName == "nodelist_hopsignal") { hiddenFrames.nodelist_hopsignal = !hiddenFrames.nodelist_hopsignal; } if (frameName == "nodelist_distance") { hiddenFrames.nodelist_distance = !hiddenFrames.nodelist_distance; } #endif #if HAS_GPS #ifdef USE_EINK if (frameName == "nodelist_bearings") { hiddenFrames.nodelist_bearings = !hiddenFrames.nodelist_bearings; } #endif if (frameName == "gps") { hiddenFrames.gps = !hiddenFrames.gps; } #endif if (frameName == "lora") { hiddenFrames.lora = !hiddenFrames.lora; } if (frameName == "clock") { hiddenFrames.clock = !hiddenFrames.clock; } if (frameName == "show_favorites") { hiddenFrames.show_favorites = !hiddenFrames.show_favorites; } if (frameName == "chirpy") { hiddenFrames.chirpy = !hiddenFrames.chirpy; } } bool Screen::isFrameHidden(const std::string &frameName) const { #ifndef USE_EINK if (frameName == "nodelist_nodes") return hiddenFrames.nodelist_nodes; if (frameName == "nodelist_location") return hiddenFrames.nodelist_location; #endif #ifdef USE_EINK if (frameName == "nodelist_lastheard") return hiddenFrames.nodelist_lastheard; if (frameName == "nodelist_hopsignal") return hiddenFrames.nodelist_hopsignal; if (frameName == "nodelist_distance") return hiddenFrames.nodelist_distance; #endif #if HAS_GPS #ifdef USE_EINK if (frameName == "nodelist_bearings") return hiddenFrames.nodelist_bearings; #endif if (frameName == "gps") return hiddenFrames.gps; #endif if (frameName == "lora") return hiddenFrames.lora; if (frameName == "clock") return hiddenFrames.clock; if (frameName == "show_favorites") return hiddenFrames.show_favorites; if (frameName == "chirpy") return hiddenFrames.chirpy; return false; } void Screen::handleStartFirmwareUpdateScreen() { LOG_DEBUG("Show firmware screen"); showingNormalScreen = false; EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // E-Ink: Explicitly use fast-refresh for next frame static FrameCallback frames[] = {graphics::NotificationRenderer::drawFrameFirmware}; setFrameImmediateDraw(frames); } void Screen::blink() { setFastFramerate(); uint8_t count = 10; dispdev->setBrightness(254); while (count > 0) { dispdev->fillRect(0, 0, dispdev->getWidth(), dispdev->getHeight()); dispdev->display(); delay(50); dispdev->clear(); dispdev->display(); delay(50); count = count - 1; } // The dispdev->setBrightness does not work for t-deck display, it seems to run the setBrightness function in // OLEDDisplay. dispdev->setBrightness(brightness); } void Screen::increaseBrightness() { brightness = ((brightness + 62) > 254) ? brightness : (brightness + 62); #if defined(ST7789_CS) // run the setDisplayBrightness function. This works on t-decks static_cast(dispdev)->setDisplayBrightness(brightness); #endif /* TO DO: add little popup in center of screen saying what brightness level it is set to*/ } void Screen::decreaseBrightness() { brightness = (brightness < 70) ? brightness : (brightness - 62); #if defined(ST7789_CS) static_cast(dispdev)->setDisplayBrightness(brightness); #endif /* TO DO: add little popup in center of screen saying what brightness level it is set to*/ } void Screen::handleOnPress() { // If screen was off, just wake it, otherwise advance to next frame // If we are in a transition, the press must have bounced, drop it. if (ui->getUiState()->frameState == FIXED) { ui->nextFrame(); lastScreenTransition = millis(); setFastFramerate(); } } void Screen::showFrame(FrameDirection direction) { // Only advance frames when UI is stable if (ui->getUiState()->frameState == FIXED) { if (direction == FrameDirection::NEXT) { ui->nextFrame(); } else { ui->previousFrame(); } lastScreenTransition = millis(); setFastFramerate(); } } #ifndef SCREEN_TRANSITION_FRAMERATE #define SCREEN_TRANSITION_FRAMERATE 30 // fps #endif void Screen::setFastFramerate() { #if defined(M5STACK_UNITC6L) dispdev->clear(); dispdev->display(); #endif // We are about to start a transition so speed up fps targetFramerate = SCREEN_TRANSITION_FRAMERATE; ui->setTargetFPS(targetFramerate); setInterval(0); // redraw ASAP runASAP = true; } int Screen::handleStatusUpdate(const meshtastic::Status *arg) { switch (arg->getStatusType()) { case STATUS_TYPE_NODE: if (showingNormalScreen && nodeStatus->getLastNumTotal() != nodeStatus->getNumTotal()) { setFrames(FOCUS_PRESERVE); // Regen the list of screen frames (returning to same frame, if possible) } nodeDB->updateGUI = false; break; case STATUS_TYPE_POWER: { bool currentUSB = powerStatus->getHasUSB(); if (currentUSB != lastPowerUSBState) { lastPowerUSBState = currentUSB; forceDisplay(true); } break; } } return 0; } // Handles when message is received; will jump to text message frame. int Screen::handleTextMessage(const meshtastic_MeshPacket *packet) { if (showingNormalScreen) { if (packet->from == 0) { // Outgoing message (likely sent from phone) devicestate.has_rx_text_message = false; memset(&devicestate.rx_text_message, 0, sizeof(devicestate.rx_text_message)); hiddenFrames.textMessage = true; hasUnreadMessage = false; // Clear unread state when user replies setFrames(FOCUS_PRESERVE); // Stay on same frame, silently update frame list } else { // Incoming message devicestate.has_rx_text_message = true; // Needed to include the message frame hasUnreadMessage = true; // Enables mail icon in the header setFrames(FOCUS_PRESERVE); // Refresh frame list without switching view (no-op during text_input) // Only wake/force display if the configuration allows it if (shouldWakeOnReceivedMessage()) { setOn(true); // Wake up the screen first forceDisplay(); // Forces screen redraw } // === Prepare banner/popup content === const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(packet->from); const meshtastic_Channel channel = channels.getByIndex(packet->channel ? packet->channel : channels.getPrimaryIndex()); const char *longName = (node && node->has_user) ? node->user.long_name : nullptr; const char *msgRaw = reinterpret_cast(packet->decoded.payload.bytes); char banner[256]; bool isAlert = false; if (moduleConfig.external_notification.alert_bell || moduleConfig.external_notification.alert_bell_vibra || moduleConfig.external_notification.alert_bell_buzzer) // Check for bell character to determine if this message is an alert for (size_t i = 0; i < packet->decoded.payload.size && i < 100; i++) { if (msgRaw[i] == ASCII_BELL) { isAlert = true; break; } } // Unlike generic messages, alerts (when enabled via the ext notif module) ignore any // 'mute' preferences set to any specific node or channel. // If on-screen keyboard is active, show a transient popup over keyboard instead of interrupting it if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) { // Wake and force redraw so popup is visible immediately if (shouldWakeOnReceivedMessage()) { setOn(true); forceDisplay(); } // Build popup: title = message source name, content = message text (sanitized) // Title char titleBuf[64] = {0}; if (longName && longName[0]) { // Sanitize sender name std::string t = sanitizeString(longName); strncpy(titleBuf, t.c_str(), sizeof(titleBuf) - 1); } else { strncpy(titleBuf, "Message", sizeof(titleBuf) - 1); } // Content: payload bytes may not be null-terminated, remove ASCII_BELL and sanitize char content[256] = {0}; { std::string raw; raw.reserve(packet->decoded.payload.size); for (size_t i = 0; i < packet->decoded.payload.size; ++i) { char c = msgRaw[i]; if (c == ASCII_BELL) continue; // strip bell raw.push_back(c); } std::string sanitized = sanitizeString(raw); strncpy(content, sanitized.c_str(), sizeof(content) - 1); } NotificationRenderer::showKeyboardMessagePopupWithTitle(titleBuf, content, 3000); // Maintain existing buzzer behavior on M5 if applicable #if defined(M5STACK_UNITC6L) if (config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_DIRECT_MSG_ONLY || (isAlert && moduleConfig.external_notification.alert_bell_buzzer) || (!isBroadcast(packet->to) && isToUs(packet))) { playLongBeep(); } #endif } else { // No keyboard active: use regular banner flow, respecting mute settings if (isAlert) { if (longName && longName[0]) { snprintf(banner, sizeof(banner), "Alert Received from\n%s", longName); } else { strcpy(banner, "Alert Received"); } screen->showSimpleBanner(banner, 3000); } else if (!channel.settings.has_module_settings || !channel.settings.module_settings.is_muted) { if (longName && longName[0]) { if (currentResolution == ScreenResolution::UltraLow) { strcpy(banner, "New Message"); } else { snprintf(banner, sizeof(banner), "New Message from\n%s", longName); } } else { strcpy(banner, "New Message"); } #if defined(M5STACK_UNITC6L) screen->setOn(true); screen->showSimpleBanner(banner, 1500); if (config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_DIRECT_MSG_ONLY || (isAlert && moduleConfig.external_notification.alert_bell_buzzer) || (!isBroadcast(packet->to) && isToUs(packet))) { // Beep if not in DIRECT_MSG_ONLY mode or if in DIRECT_MSG_ONLY mode and either // - packet contains an alert and alert bell buzzer is enabled // - packet is a non-broadcast that is addressed to this node playLongBeep(); } #else screen->showSimpleBanner(banner, 3000); #endif } } } } return 0; } // Triggered by MeshModules int Screen::handleUIFrameEvent(const UIFrameEvent *event) { // Block UI frame events when virtual keyboard is active if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) { return 0; } if (showingNormalScreen) { // Regenerate the frameset, potentially honoring a module's internal requestFocus() call if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET) { setFrames(FOCUS_MODULE); } // Regenerate the frameset, while attempting to maintain focus on the current frame else if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET_BACKGROUND) { setFrames(FOCUS_PRESERVE); } // Don't regenerate the frameset, just re-draw whatever is on screen ASAP else if (event->action == UIFrameEvent::Action::REDRAW_ONLY) { setFastFramerate(); } // Jump directly to the Text Message screen else if (event->action == UIFrameEvent::Action::SWITCH_TO_TEXTMESSAGE) { setFrames(FOCUS_PRESERVE); // preserve current frame ordering ui->switchToFrame(framesetInfo.positions.textMessage); setFastFramerate(); // force redraw ASAP } } return 0; } int Screen::handleInputEvent(const InputEvent *event) { LOG_INPUT("Screen Input event %u! kb %u", event->inputEvent, event->kbchar); if (!screenOn) return 0; // Handle text input notifications specially - pass input to virtual keyboard if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) { NotificationRenderer::inEvent = *event; static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback}; ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); setFastFramerate(); // Draw ASAP ui->update(); return 0; } #ifdef USE_EINK // the screen is the last input handler, so if an event makes it here, we can assume it will prompt a screen draw. EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Use fast-refresh for next frame, no skip please EINK_ADD_FRAMEFLAG(dispdev, BLOCKING); // Edge case: if this frame is promoted to COSMETIC, wait for update handleSetOn(true); // Ensure power-on to receive deep-sleep screensaver (PowerFSM should handle?) setFastFramerate(); // Draw ASAP #endif if (NotificationRenderer::isOverlayBannerShowing()) { NotificationRenderer::inEvent = *event; static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback}; ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); setFastFramerate(); // Draw ASAP ui->update(); menuHandler::handleMenuSwitch(dispdev); return 0; } // UP/DOWN in message screen scrolls through message threads if (ui->getUiState()->currentFrame == framesetInfo.positions.textMessage) { if (event->inputEvent == INPUT_BROKER_UP) { if (messageStore.getMessages().empty()) { cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST); } else { graphics::MessageRenderer::scrollUp(); setFastFramerate(); // match existing behavior return 0; } } if (event->inputEvent == INPUT_BROKER_DOWN) { if (messageStore.getMessages().empty()) { cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST); } else { graphics::MessageRenderer::scrollDown(); setFastFramerate(); return 0; } } } // UP/DOWN in node list screens scrolls through node pages if (ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_nodes || ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_location || ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_lastheard || ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_hopsignal || ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_distance || ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_bearings) { if (event->inputEvent == INPUT_BROKER_UP) { graphics::NodeListRenderer::scrollUp(); setFastFramerate(); return 0; } if (event->inputEvent == INPUT_BROKER_DOWN) { graphics::NodeListRenderer::scrollDown(); setFastFramerate(); return 0; } } // Use left or right input from a keyboard to move between frames, // so long as a mesh module isn't using these events for some other purpose if (showingNormalScreen) { // Ask any MeshModules if they're handling keyboard input right now bool inputIntercepted = false; for (MeshModule *module : moduleFrames) { if (module && module->interceptingKeyboardInput()) inputIntercepted = true; } // If no modules are using the input, move between frames if (!inputIntercepted) { #if defined(INPUTDRIVER_ENCODER_TYPE) && INPUTDRIVER_ENCODER_TYPE == 2 bool handledEncoderScroll = false; const bool isTextMessageFrame = (framesetInfo.positions.textMessage != 255 && this->ui->getUiState()->currentFrame == framesetInfo.positions.textMessage && !messageStore.getMessages().empty()); if (isTextMessageFrame) { if (event->inputEvent == INPUT_BROKER_UP_LONG) { graphics::MessageRenderer::nudgeScroll(-1); handledEncoderScroll = true; } else if (event->inputEvent == INPUT_BROKER_DOWN_LONG) { graphics::MessageRenderer::nudgeScroll(1); handledEncoderScroll = true; } } if (handledEncoderScroll) { setFastFramerate(); return 0; } #endif if (event->inputEvent == INPUT_BROKER_LEFT || event->inputEvent == INPUT_BROKER_ALT_PRESS) { showFrame(FrameDirection::PREVIOUS); } else if (event->inputEvent == INPUT_BROKER_RIGHT || event->inputEvent == INPUT_BROKER_USER_PRESS) { showFrame(FrameDirection::NEXT); } else if (event->inputEvent == INPUT_BROKER_FN_F1) { this->ui->switchToFrame(0); lastScreenTransition = millis(); setFastFramerate(); } else if (event->inputEvent == INPUT_BROKER_FN_F2) { this->ui->switchToFrame(1); lastScreenTransition = millis(); setFastFramerate(); } else if (event->inputEvent == INPUT_BROKER_FN_F3) { this->ui->switchToFrame(2); lastScreenTransition = millis(); setFastFramerate(); } else if (event->inputEvent == INPUT_BROKER_FN_F4) { this->ui->switchToFrame(3); lastScreenTransition = millis(); setFastFramerate(); } else if (event->inputEvent == INPUT_BROKER_FN_F5) { this->ui->switchToFrame(4); lastScreenTransition = millis(); setFastFramerate(); } else if (event->inputEvent == INPUT_BROKER_UP_LONG) { // Long press up button for fast frame switching showPrevFrame(); } else if (event->inputEvent == INPUT_BROKER_DOWN_LONG) { // Long press down button for fast frame switching showNextFrame(); } else if ((event->inputEvent == INPUT_BROKER_UP || event->inputEvent == INPUT_BROKER_DOWN) && this->ui->getUiState()->currentFrame == framesetInfo.positions.home) { cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST); } else if (event->inputEvent == INPUT_BROKER_SELECT) { if (this->ui->getUiState()->currentFrame == framesetInfo.positions.home) { menuHandler::homeBaseMenu(); } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.system) { menuHandler::systemBaseMenu(); #if HAS_GPS } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.gps && gps) { menuHandler::positionBaseMenu(); #endif } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.clock) { menuHandler::clockMenu(); } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.lora) { menuHandler::loraMenu(); } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.textMessage) { if (!messageStore.getMessages().empty()) { menuHandler::messageResponseMenu(); } else { if (currentResolution == ScreenResolution::UltraLow) { menuHandler::textMessageMenu(); } else { menuHandler::textMessageBaseMenu(); } } } else if (framesetInfo.positions.firstFavorite != 255 && this->ui->getUiState()->currentFrame >= framesetInfo.positions.firstFavorite && this->ui->getUiState()->currentFrame <= framesetInfo.positions.lastFavorite) { menuHandler::favoriteBaseMenu(); } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_nodes || this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_location || this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_lastheard || this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_hopsignal || this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_distance || this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_hopsignal || this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_bearings) { menuHandler::nodeListMenu(); } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.wifi) { menuHandler::wifiBaseMenu(); } } else if (event->inputEvent == INPUT_BROKER_BACK) { showFrame(FrameDirection::PREVIOUS); } else if (event->inputEvent == INPUT_BROKER_CANCEL) { setOn(false); } } } return 0; } int Screen::handleAdminMessage(AdminModule_ObserverData *arg) { switch (arg->request->which_payload_variant) { // Node removed manually (i.e. via app) case meshtastic_AdminMessage_remove_by_nodenum_tag: setFrames(FOCUS_PRESERVE); *arg->result = AdminMessageHandleResult::HANDLED; break; // Default no-op, in case the admin message observable gets used by other classes in future default: break; } return 0; } bool Screen::isOverlayBannerShowing() { return NotificationRenderer::isOverlayBannerShowing(); } } // namespace graphics #else graphics::Screen::Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY) {} #endif // HAS_SCREEN bool shouldWakeOnReceivedMessage() { /* The goal here is to determine when we do NOT wake up the screen on message received: - Any ext. notifications are turned on - If role is not CLIENT / CLIENT_MUTE / CLIENT_HIDDEN / CLIENT_BASE - If the battery level is very low */ if (moduleConfig.external_notification.enabled) { return false; } if (!IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_CLIENT, meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE, meshtastic_Config_DeviceConfig_Role_CLIENT_HIDDEN, meshtastic_Config_DeviceConfig_Role_CLIENT_BASE)) { return false; } if (powerStatus && powerStatus->getBatteryChargePercent() < 10) { return false; } return true; } ================================================ FILE: src/graphics/Screen.h ================================================ #pragma once #include "configuration.h" #include "detect/ScanI2C.h" #include "mesh/generated/meshtastic/config.pb.h" #include #include #include #include #define getStringCenteredX(s) ((SCREEN_WIDTH - display->getStringWidth(s)) / 2) namespace graphics { enum notificationTypeEnum { none, text_banner, selection_picker, node_picker, number_picker, text_input }; struct BannerOverlayOptions { const char *message; uint32_t durationMs = 30000; const char **optionsArrayPtr = nullptr; const int *optionsEnumPtr = nullptr; uint8_t optionsCount = 0; std::function bannerCallback = nullptr; int8_t InitialSelected = 0; notificationTypeEnum notificationType = notificationTypeEnum::text_banner; }; } // namespace graphics bool shouldWakeOnReceivedMessage(); #if !HAS_SCREEN #include "power.h" namespace graphics { // Noop class for boards without screen. class Screen { public: enum FrameFocus : uint8_t { FOCUS_DEFAULT, // No specific frame FOCUS_PRESERVE, // Return to the previous frame FOCUS_FAULT, FOCUS_MODULE, // Note: target module should call requestFocus(), otherwise no info about which module to focus FOCUS_CLOCK, FOCUS_SYSTEM, }; explicit Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY); void onPress() {} void setup() {} void setOn(bool) {} void doDeepSleep() {} void forceDisplay(bool forceUiUpdate = false) {} void startFirmwareUpdateScreen() {} void increaseBrightness() {} void decreaseBrightness() {} void startAlert(const char *) {} void showSimpleBanner(const char *message, uint32_t durationMs = 0) {} void showOverlayBanner(BannerOverlayOptions) {} void setFrames(FrameFocus focus) {} void endAlert() {} }; } // namespace graphics #else #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 #elif defined(USE_SPISSD1306) #include #elif defined(USE_ST7796) #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 "graphics/draw/MenuHandler.h" #include "input/InputBroker.h" #include "mesh/MeshModule.h" #include "modules/AdminModule.h" #include "power.h" #include #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) extern bool hasUnreadMessage; 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 { enum class FrameDirection { NEXT, PREVIOUS }; // 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 uiFrameEventObserver = CallbackObserver(this, &Screen::handleUIFrameEvent); // Sent by Mesh Modules CallbackObserver inputObserver = CallbackObserver(this, &Screen::handleInputEvent); CallbackObserver adminMessageObserver = CallbackObserver(this, &Screen::handleAdminMessage); public: OLEDDisplay *getDisplayDevice() { return dispdev; } explicit Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY); // Screen dimension accessors inline int getHeight() const { return displayHeight; } inline int getWidth() const { return displayWidth; } size_t frameCount = 0; // Total number of active frames ~Screen(); // Which frame we want to be displayed, after we regen the frameset by calling setFrames enum FrameFocus : uint8_t { FOCUS_DEFAULT, // No specific frame FOCUS_PRESERVE, // Return to the previous frame FOCUS_FAULT, FOCUS_MODULE, // Note: target module should call requestFocus(), otherwise no info about which module to focus FOCUS_CLOCK, FOCUS_SYSTEM, }; // Regenerate the normal set of frames, focusing a specific frame if requested // Call when a frame should be added / removed, or custom frames should be cleared void setFrames(FrameFocus focus = FOCUS_DEFAULT); std::vector indicatorIcons; // Per-frame custom icon pointers Screen(const Screen &) = delete; Screen &operator=(const Screen &) = delete; ScanI2C::DeviceAddress address_found; meshtastic_Config_DisplayConfig_OledType model; OLEDDISPLAY_GEOMETRY geometry; bool isOverlayBannerShowing(); bool isScreenOn() { return screenOn; } // Stores the last 4 of our hardware ID, to make finding the device for pairing easier // FIXME: Needs refactoring and getMacAddr needs to be moved to a utility class char ourId[5]; /// Initializes the UI, turns on the display, starts showing boot screen. // // Not thread safe - must be called before any other methods are called. void setup(); /// Turns the screen on/off. Optionally, pass a custom screensaver frame for E-Ink void setOn(bool on, FrameCallback einkScreensaver = NULL); /** * Prepare the display for the unit going to the lowest power mode possible. Most screens will just * poweroff, but eink screens will show a "I'm sleeping" graphic, possibly with a QR code */ void doDeepSleep(); void blink(); // Draw north float estimatedHeading(double lat, double lon); /// Handle button press, trackball or swipe action) void onPress() { enqueueCmd(ScreenCmd{.cmd = Cmd::ON_PRESS}); } void showPrevFrame() { enqueueCmd(ScreenCmd{.cmd = Cmd::SHOW_PREV_FRAME}); } void showNextFrame() { enqueueCmd(ScreenCmd{.cmd = Cmd::SHOW_NEXT_FRAME}); } void showFrame(FrameDirection direction); // generic alert start void startAlert(FrameCallback _alertFrame) { alertFrame = _alertFrame; ScreenCmd cmd; cmd.cmd = Cmd::START_ALERT_FRAME; enqueueCmd(cmd); } void startAlert(const char *_alertMessage) { startAlert([_alertMessage](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -> void { uint16_t x_offset = display->width() / 2; display->setTextAlignment(TEXT_ALIGN_CENTER); display->setFont(FONT_MEDIUM); display->drawString(x_offset + x, 26 + y, _alertMessage); }); } void endAlert() { ScreenCmd cmd; cmd.cmd = Cmd::STOP_ALERT_FRAME; enqueueCmd(cmd); } void showSimpleBanner(const char *message, uint32_t durationMs = 0); void showOverlayBanner(BannerOverlayOptions); void showNodePicker(const char *message, uint32_t durationMs, std::function bannerCallback); void showNumberPicker(const char *message, uint32_t durationMs, uint8_t digits, std::function bannerCallback); void showTextInput(const char *header, const char *initialText, uint32_t durationMs, std::function textCallback); void requestMenu(graphics::menuHandler::screenMenus menuToShow) { graphics::menuHandler::menuQueue = menuToShow; runNow(); } void startFirmwareUpdateScreen() { ScreenCmd cmd; cmd.cmd = Cmd::START_FIRMWARE_UPDATE_SCREEN; enqueueCmd(cmd); } // Function to allow the AccelerometerThread to set the heading if a sensor provides it // Mutex needed? void setHeading(long _heading) { hasCompass = true; compassHeading = fmod(_heading, 360); } bool hasHeading() { return hasCompass; } long getHeading() { return compassHeading; } void setEndCalibration(uint32_t _endCalibrationAt) { endCalibrationAt = _endCalibrationAt; } uint32_t getEndCalibration() { return endCalibrationAt; } // functions for display brightness void increaseBrightness(); void decreaseBrightness(); /// Stops showing the boot screen. void stopBootScreen() { enqueueCmd(ScreenCmd{.cmd = Cmd::STOP_BOOT_SCREEN}); } void runNow() { setFastFramerate(); enqueueCmd(ScreenCmd{.cmd = Cmd::NOOP}); } /// Overrides the default utf8 character conversion, to replace empty space with question marks static char customFontTableLookup(const uint8_t ch) { // UTF-8 to font table index converter // Code from http://playground.arduino.cc/Main/Utf8ascii static uint8_t LASTCHAR; static bool SKIPREST; // Only display a single unconvertable-character symbol per sequence of unconvertable characters if (ch < 128) { // Standard ASCII-set 0..0x7F handling LASTCHAR = 0; SKIPREST = false; return ch; } uint8_t last = LASTCHAR; // get last char LASTCHAR = ch; switch (last) { case 0xC2: { SKIPREST = false; return (uint8_t)ch; } case 0xC3: { SKIPREST = false; return (uint8_t)(ch | 0xC0); } } // We want to strip out prefix chars for two-byte char formats if (ch == 0xC2 || ch == 0xC3) return (uint8_t)0; #if defined(OLED_PL) switch (last) { case 0xC3: { 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 defined(OLED_CS) switch (last) { case 0xC2: { SKIPREST = false; return (uint8_t)ch; } case 0xC3: { SKIPREST = false; return (uint8_t)(ch | 0xC0); } case 0xC4: { SKIPREST = false; if (ch == 140) return (uint8_t)(129); // Č if (ch == 141) return (uint8_t)(138); // č if (ch == 142) return (uint8_t)(130); // Ď if (ch == 143) return (uint8_t)(139); // ď if (ch == 154) return (uint8_t)(131); // Ě if (ch == 155) return (uint8_t)(140); // ě // Slovak specific glyphs if (ch == 185) return (uint8_t)(147); // Ĺ if (ch == 186) return (uint8_t)(148); // ĺ if (ch == 189) return (uint8_t)(149); // Ľ if (ch == 190) return (uint8_t)(150); // ľ break; } case 0xC5: { SKIPREST = false; if (ch == 135) return (uint8_t)(132); // Ň if (ch == 136) return (uint8_t)(141); // ň if (ch == 152) return (uint8_t)(133); // Ř if (ch == 153) return (uint8_t)(142); // ř if (ch == 160) return (uint8_t)(134); // Š if (ch == 161) return (uint8_t)(143); // š if (ch == 164) return (uint8_t)(135); // Ť if (ch == 165) return (uint8_t)(144); // ť if (ch == 174) return (uint8_t)(136); // Ů if (ch == 175) return (uint8_t)(145); // ů if (ch == 189) return (uint8_t)(137); // Ž if (ch == 190) return (uint8_t)(146); // ž // Slovak specific glyphs if (ch == 148) return (uint8_t)(151); // Ŕ if (ch == 149) return (uint8_t)(152); // ŕ break; } } // We want to strip out prefix chars for two-byte char formats if (ch == 0xC2 || ch == 0xC3 || ch == 0xC4 || ch == 0xC5) return (uint8_t)0; #endif #if defined(OLED_GR) switch (last) { case 0xC3: { SKIPREST = false; return (uint8_t)(ch | 0xC0); } // Map UTF-8 Greek chars to Windows-1253 (CP-1253) ASCII codes case 0xCE: { SKIPREST = false; // Uppercase Greek: Α-Ρ (U+0391-U+03A1) -> CP-1253 193-209 if (ch >= 145 && ch <= 161) return (uint8_t)(ch + 48); // Uppercase Greek: Σ-Ω (U+03A3-U+03A9) -> CP-1253 211-217 else if (ch >= 163 && ch <= 169) return (uint8_t)(ch + 48); // Lowercase Greek: α-ρ (U+03B1-U+03C1) -> CP-1253 225-241 else if (ch >= 177 && ch <= 193) return (uint8_t)(ch + 48); break; } case 0xCF: { SKIPREST = false; // Lowercase Greek: ς-ω (U+03C2-U+03C9) -> CP-1253 242-249 if (ch >= 130 && ch <= 137) return (uint8_t)(ch + 112); break; } } // We want to strip out prefix chars for two-byte Greek char formats if (ch == 0xC2 || ch == 0xC3 || ch == 0xCE || ch == 0xCF) 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 *packet); int handleUIFrameEvent(const UIFrameEvent *arg); int handleInputEvent(const InputEvent *arg); int handleAdminMessage(AdminModule_ObserverData *arg); /// Used to force (super slow) eink displays to draw critical frames void forceDisplay(bool forceUiUpdate = false); /// Draws our SSL cert screen during boot (called from WebServer) void setSSLFrames(); // Menu-driven Show / Hide Toggle void toggleFrameVisibility(const std::string &frameName); bool isFrameHidden(const std::string &frameName) const; #ifdef USE_EINK /// Draw an image to remain on E-Ink display after screen off void setScreensaverFrames(FrameCallback einkScreensaver = NULL); #endif protected: /// Updates the UI. // // Called periodically from the main loop. int32_t runOnce() final; bool isAUTOOled = false; // Screen dimensions (for convenience) // Defined during Screen::setup uint16_t displayWidth = 0; uint16_t displayHeight = 0; private: FrameCallback alertFrames[1]; struct ScreenCmd { Cmd cmd; union { uint32_t bluetooth_pin; char *print_text; }; }; /// Enques given command item to be processed by main loop(). bool enqueueCmd(const ScreenCmd &cmd) { if (!useDisplay) return false; // not enqueued if our display is not in use else { bool success = cmdQueue.enqueue(cmd, 0); enabled = true; // handle ASAP (we are the registered reader for cmdQueue, but might have been disabled) return success; } } // Implementations of various commands, called from doTask(). void handleSetOn(bool on, FrameCallback einkScreensaver = NULL); void handleOnPress(); void handleStartFirmwareUpdateScreen(); // Info collected by setFrames method. // Index location of specific frames. // - Used to apply the FrameFocus parameter of setFrames // - Used to dismiss the currently shown frame (txt; waypoint) by CardKB combo struct FramesetInfo { struct FramePositions { uint8_t fault = 255; uint8_t waypoint = 255; uint8_t focusedModule = 255; uint8_t log = 255; uint8_t settings = 255; uint8_t wifi = 255; uint8_t deviceFocused = 255; uint8_t system = 255; uint8_t gps = 255; uint8_t home = 255; uint8_t textMessage = 255; uint8_t nodelist_nodes = 255; uint8_t nodelist_location = 255; uint8_t nodelist_lastheard = 255; uint8_t nodelist_hopsignal = 255; uint8_t nodelist_distance = 255; uint8_t nodelist_bearings = 255; uint8_t clock = 255; uint8_t chirpy = 255; uint8_t firstFavorite = 255; uint8_t lastFavorite = 255; uint8_t lora = 255; } positions; uint8_t frameCount = 0; } framesetInfo; struct hiddenFrames { bool textMessage = false; bool waypoint = false; bool wifi = false; bool system = false; bool home = false; bool clock = false; #ifndef USE_EINK bool nodelist_nodes = false; bool nodelist_location = false; #endif #ifdef USE_EINK bool nodelist_lastheard = false; bool nodelist_hopsignal = false; bool nodelist_distance = false; #endif #if HAS_GPS #ifdef USE_EINK bool nodelist_bearings = false; #endif bool gps = false; #endif bool lora = false; bool show_favorites = false; bool chirpy = true; } hiddenFrames; /// Try to start drawing ASAP void setFastFramerate(); // Sets frame up for immediate drawing void setFrameImmediateDraw(FrameCallback *drawFrames); /// 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; /// Track USB power state to only wake screen on actual power state changes bool lastPowerUSBState = false; // Implementation to Adjust Brightness uint8_t brightness = BRIGHTNESS_DEFAULT; // H = 254, MH = 192, ML = 130 L = 103 bool hasCompass = false; float compassHeading; uint32_t endCalibrationAt; /// Holds state for debug information DebugInfo debugInfo; /// Display device #ifdef USE_ST7789 ST7789Spi *dispdev; #else OLEDDisplay *dispdev; #endif /// UI helper for rendering to frames and switching between them OLEDDisplayUi *ui; }; } // namespace graphics // Extern declarations for function symbols used in UIRenderer extern std::vector functionSymbol; extern std::string functionSymbolString; extern graphics::Screen *screen; #endif ================================================ FILE: src/graphics/ScreenFonts.h ================================================ #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 #ifdef OLED_CS #include "graphics/fonts/OLEDDisplayFontsCS.h" #endif #ifdef OLED_GR #include "graphics/fonts/OLEDDisplayFontsGR.h" #endif #if defined(CROWPANEL_ESP32S3_5_EPAPER) && defined(USE_EINK) #include "graphics/fonts/EinkDisplayFonts.h" #endif #ifdef OLED_GR #define FONT_SMALL_LOCAL ArialMT_Plain_10_GR // Height: 13 #else #ifdef OLED_PL #define FONT_SMALL_LOCAL ArialMT_Plain_10_PL #else #ifdef OLED_RU #define FONT_SMALL_LOCAL ArialMT_Plain_10_RU #else #ifdef OLED_UA #define FONT_SMALL_LOCAL ArialMT_Plain_10_UA // Height: 13 #else #ifdef OLED_CS #define FONT_SMALL_LOCAL ArialMT_Plain_10_CS #else #define FONT_SMALL_LOCAL ArialMT_Plain_10 // Height: 13 #endif #endif #endif #endif #endif #ifdef OLED_GR #define FONT_MEDIUM_LOCAL ArialMT_Plain_16_GR // Height: 19 #else #ifdef OLED_PL #define FONT_MEDIUM_LOCAL ArialMT_Plain_16_PL // Height: 19 #else #ifdef OLED_RU #define FONT_MEDIUM_LOCAL ArialMT_Plain_16_RU // Height: 19 #else #ifdef OLED_UA #define FONT_MEDIUM_LOCAL ArialMT_Plain_16_UA // Height: 19 #else #ifdef OLED_CS #define FONT_MEDIUM_LOCAL ArialMT_Plain_16_CS #else #define FONT_MEDIUM_LOCAL ArialMT_Plain_16 // Height: 19 #endif #endif #endif #endif #endif #ifdef OLED_GR #define FONT_LARGE_LOCAL ArialMT_Plain_24_GR // Height: 28 #else #ifdef OLED_PL #define FONT_LARGE_LOCAL ArialMT_Plain_24_PL // Height: 28 #else #ifdef OLED_RU #define FONT_LARGE_LOCAL ArialMT_Plain_24_RU // Height: 28 #else #ifdef OLED_UA #define FONT_LARGE_LOCAL ArialMT_Plain_24_UA // Height: 28 #else #ifdef OLED_CS #define FONT_LARGE_LOCAL ArialMT_Plain_24_CS // Height: 28 #else #define FONT_LARGE_LOCAL ArialMT_Plain_24 // Height: 28 #endif #endif #endif #endif #endif #if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) || \ defined(HACKADAY_COMMUNICATOR) || defined(USE_ST7796)) && \ !defined(DISPLAY_FORCE_SMALL_FONTS) // The screen is bigger so use bigger fonts #define FONT_SMALL FONT_MEDIUM_LOCAL // Height: 19 #define FONT_MEDIUM FONT_LARGE_LOCAL // Height: 28 #define FONT_LARGE FONT_LARGE_LOCAL // Height: 28 #elif defined(M5STACK_UNITC6L) #define FONT_SMALL FONT_SMALL_LOCAL // Height: 13 #define FONT_MEDIUM FONT_SMALL_LOCAL // Height: 13 #define FONT_LARGE FONT_SMALL_LOCAL // Height: 13 #else #define FONT_SMALL FONT_SMALL_LOCAL // Height: 13 #define FONT_MEDIUM FONT_MEDIUM_LOCAL // Height: 19 #define FONT_LARGE FONT_LARGE_LOCAL // Height: 28 #endif #if defined(CROWPANEL_ESP32S3_5_EPAPER) && defined(USE_EINK) #undef FONT_SMALL #undef FONT_MEDIUM #undef FONT_LARGE #define FONT_SMALL Monospaced_plain_30 #define FONT_MEDIUM Monospaced_plain_30 #define FONT_LARGE Monospaced_plain_30 #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) ================================================ FILE: src/graphics/ScreenGlobals.cpp ================================================ #include #include // Global variables for screen function overlay std::vector functionSymbol; std::string functionSymbolString; ================================================ FILE: src/graphics/SharedUIDisplay.cpp ================================================ #include "configuration.h" #if HAS_SCREEN #include "MeshService.h" #include "RTC.h" #include "draw/NodeListRenderer.h" #include "graphics/ScreenFonts.h" #include "graphics/SharedUIDisplay.h" #include "graphics/draw/UIRenderer.h" #include "main.h" #include "meshtastic/config.pb.h" #include "modules/ExternalNotificationModule.h" #include "power.h" #include #include namespace graphics { ScreenResolution determineScreenResolution(int16_t screenheight, int16_t screenwidth) { #ifdef FORCE_LOW_RES return ScreenResolution::Low; #else // Unit C6L and other ultra low res screens if (screenwidth <= 64 || screenheight <= 48) { return ScreenResolution::UltraLow; } // Standard OLED screens if (screenwidth > 128 && screenheight <= 64) { return ScreenResolution::Low; } // High Resolutions screens like T114, TDeck, TLora Pager, etc if (screenwidth > 128) { return ScreenResolution::High; } // Default to low resolution return ScreenResolution::Low; #endif } void decomposeTime(uint32_t rtc_sec, int &hour, int &minute, int &second) { hour = 0; minute = 0; second = 0; if (rtc_sec == 0) return; uint32_t hms = (rtc_sec % SEC_PER_DAY + SEC_PER_DAY) % SEC_PER_DAY; hour = hms / SEC_PER_HOUR; minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN; second = hms % SEC_PER_MIN; } // === Shared External State === bool hasUnreadMessage = false; ScreenResolution currentResolution = ScreenResolution::Low; // === Internal State === bool isBoltVisibleShared = true; uint32_t lastBlinkShared = 0; bool isMailIconVisible = true; uint32_t lastMailBlink = 0; // ********************************* // * Rounded Header when inverted * // ********************************* void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w, int16_t h, int16_t r) { // Draw the center and side rectangles display->fillRect(x + r, y, w - 2 * r, h); // center bar display->fillRect(x, y + r, r, h - 2 * r); // left edge display->fillRect(x + w - r, y + r, r, h - 2 * r); // right edge // Draw the rounded corners using filled circles display->fillCircle(x + r + 1, y + r, r); // top-left display->fillCircle(x + w - r - 1, y + r, r); // top-right display->fillCircle(x + r + 1, y + h - r - 1, r); // bottom-left display->fillCircle(x + w - r - 1, y + h - r - 1, r); // bottom-right } // ************************* // * Common Header Drawing * // ************************* void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool force_no_invert, bool show_date) { constexpr int HEADER_OFFSET_Y = 1; y += HEADER_OFFSET_Y; display->setFont(FONT_SMALL); display->setTextAlignment(TEXT_ALIGN_LEFT); const int xOffset = 4; const int highlightHeight = FONT_HEIGHT_SMALL - 1; const bool isInverted = (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED); const bool isBold = config.display.heading_bold; const int screenW = display->getWidth(); const int screenH = display->getHeight(); if (!force_no_invert) { // === Inverted Header Background === if (isInverted) { display->setColor(BLACK); display->fillRect(0, 0, screenW, highlightHeight + 2); display->setColor(WHITE); drawRoundedHighlight(display, x, y, screenW, highlightHeight, 2); display->setColor(BLACK); } else { display->setColor(BLACK); display->fillRect(0, 0, screenW, highlightHeight + 2); display->setColor(WHITE); if (currentResolution == ScreenResolution::High) { display->drawLine(0, 20, screenW, 20); } else { display->drawLine(0, 14, screenW, 14); } } // === Screen Title === const char *headerTitle = titleStr ? titleStr : ""; const int titleWidth = UIRenderer::measureStringWithEmotes(display, headerTitle); const int titleX = (SCREEN_WIDTH - titleWidth) / 2; UIRenderer::drawStringWithEmotes(display, titleX, y, headerTitle, FONT_HEIGHT_SMALL, 1, config.display.heading_bold); } display->setTextAlignment(TEXT_ALIGN_LEFT); // === Battery State === int chargePercent = powerStatus->getBatteryChargePercent(); bool isCharging = powerStatus->getIsCharging(); bool usbPowered = powerStatus->getHasUSB(); if (chargePercent >= 100) { isCharging = false; } if (chargePercent == 101) { usbPowered = true; // Forcing this flag on for the express purpose that some devices have no concept of having a USB cable // plugged in } uint32_t now = millis(); #ifndef USE_EINK if (isCharging && now - lastBlinkShared > 500) { isBoltVisibleShared = !isBoltVisibleShared; lastBlinkShared = now; } #endif bool useHorizontalBattery = (currentResolution == ScreenResolution::High && screenW >= screenH); const int textY = y + (highlightHeight - FONT_HEIGHT_SMALL) / 2; int batteryX = 1; int batteryY = HEADER_OFFSET_Y + 1; #if !defined(M5STACK_UNITC6L) // === Battery Icons === if (usbPowered && !isCharging) { // This is a basic check to determine USB Powered is flagged but not charging batteryX += 1; batteryY += 2; if (currentResolution == ScreenResolution::High) { display->drawXbm(batteryX, batteryY, 19, 12, imgUSB_HighResolution); batteryX += 20; // Icon + 1 pixel } else { display->drawXbm(batteryX, batteryY, 10, 8, imgUSB); batteryX += 11; // Icon + 1 pixel } } else { if (useHorizontalBattery) { batteryX += 1; batteryY += 2; display->drawXbm(batteryX, batteryY, 9, 13, batteryBitmap_h_bottom); display->drawXbm(batteryX + 9, batteryY, 9, 13, batteryBitmap_h_top); if (isCharging && isBoltVisibleShared) display->drawXbm(batteryX + 4, batteryY, 9, 13, lightning_bolt_h); else { display->drawLine(batteryX + 5, batteryY, batteryX + 10, batteryY); display->drawLine(batteryX + 5, batteryY + 12, batteryX + 10, batteryY + 12); int fillWidth = 14 * chargePercent / 100; display->fillRect(batteryX + 1, batteryY + 1, fillWidth, 11); } batteryX += 18; // Icon + 2 pixels } else { #ifdef USE_EINK batteryY += 2; #endif display->drawXbm(batteryX, batteryY, 7, 11, batteryBitmap_v); if (isCharging && isBoltVisibleShared) display->drawXbm(batteryX + 1, batteryY + 3, 5, 5, lightning_bolt_v); else { display->drawXbm(batteryX - 1, batteryY + 4, 8, 3, batteryBitmap_sidegaps_v); int fillHeight = 8 * chargePercent / 100; int fillY = batteryY - fillHeight; display->fillRect(batteryX + 1, fillY + 10, 5, fillHeight); } batteryX += 9; // Icon + 2 pixels } } if (chargePercent != 101) { // === Battery % Display === char chargeStr[4]; snprintf(chargeStr, sizeof(chargeStr), "%d", chargePercent); int chargeNumWidth = display->getStringWidth(chargeStr); display->drawString(batteryX, textY, chargeStr); display->drawString(batteryX + chargeNumWidth - 1, textY, "%"); if (isBold) { display->drawString(batteryX + 1, textY, chargeStr); display->drawString(batteryX + chargeNumWidth, textY, "%"); } } // === Time and Right-aligned Icons === uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); char timeStr[10] = "--:--"; // Fallback display int timeStrWidth = display->getStringWidth("12:34"); // Default alignment int timeX = screenW - xOffset - timeStrWidth + 4; if (rtc_sec > 0) { // === Build Time String === int hour, minute, second; graphics::decomposeTime(rtc_sec, hour, minute, second); snprintf(timeStr, sizeof(timeStr), "%d:%02d", hour, minute); // === Build Date String === char datetimeStr[25]; UIRenderer::formatDateTime(datetimeStr, sizeof(datetimeStr), rtc_sec, display, false); char dateLine[40]; if (currentResolution == ScreenResolution::High) { snprintf(dateLine, sizeof(dateLine), "%s", datetimeStr); } else { if (hasUnreadMessage) { snprintf(dateLine, sizeof(dateLine), "%s", &datetimeStr[5]); } else { snprintf(dateLine, sizeof(dateLine), "%s", &datetimeStr[2]); } } if (config.display.use_12h_clock) { bool isPM = hour >= 12; hour %= 12; if (hour == 0) hour = 12; snprintf(timeStr, sizeof(timeStr), "%d:%02d%s", hour, minute, isPM ? "p" : "a"); } if (show_date) { timeStrWidth = display->getStringWidth(dateLine); } else { timeStrWidth = display->getStringWidth(timeStr); } timeX = screenW - xOffset - timeStrWidth + 3; // === Show Mail or Mute Icon to the Left of Time === int iconRightEdge = timeX - 2; bool showMail = false; #ifndef USE_EINK if (hasUnreadMessage) { if (now - lastMailBlink > 500) { isMailIconVisible = !isMailIconVisible; lastMailBlink = now; } showMail = isMailIconVisible; } #else if (hasUnreadMessage) { showMail = true; } #endif if (showMail) { if (useHorizontalBattery) { int iconW = 16, iconH = 12; int iconX = iconRightEdge - iconW; int iconY = textY + (FONT_HEIGHT_SMALL - iconH) / 2 - 1; if (isInverted && !force_no_invert) { display->setColor(WHITE); display->fillRect(iconX - 1, iconY - 1, iconW + 3, iconH + 2); display->setColor(BLACK); } else { display->setColor(BLACK); display->fillRect(iconX - 1, iconY - 1, iconW + 3, iconH + 2); display->setColor(WHITE); } display->drawRect(iconX, iconY, iconW + 1, iconH); display->drawLine(iconX, iconY, iconX + iconW / 2, iconY + iconH - 4); display->drawLine(iconX + iconW, iconY, iconX + iconW / 2, iconY + iconH - 4); } else { int iconX = iconRightEdge - (mail_width - 2); int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2; if (isInverted && !force_no_invert) { display->setColor(WHITE); display->fillRect(iconX - 1, iconY - 1, mail_width + 2, mail_height + 2); display->setColor(BLACK); } else { display->setColor(BLACK); display->fillRect(iconX - 1, iconY - 1, mail_width + 2, mail_height + 2); display->setColor(WHITE); } display->drawXbm(iconX, iconY, mail_width, mail_height, mail); } } else if (externalNotificationModule->getMute()) { if (currentResolution == ScreenResolution::High) { int iconX = iconRightEdge - mute_symbol_big_width; int iconY = textY + (FONT_HEIGHT_SMALL - mute_symbol_big_height) / 2; if (isInverted && !force_no_invert) { display->setColor(WHITE); display->fillRect(iconX - 1, iconY - 1, mute_symbol_big_width + 2, mute_symbol_big_height + 2); display->setColor(BLACK); } else { display->setColor(BLACK); display->fillRect(iconX - 1, iconY - 1, mute_symbol_big_width + 2, mute_symbol_big_height + 2); display->setColor(WHITE); } display->drawXbm(iconX, iconY, mute_symbol_big_width, mute_symbol_big_height, mute_symbol_big); } else { int iconX = iconRightEdge - mute_symbol_width; int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2; if (isInverted && !force_no_invert) { display->setColor(WHITE); display->fillRect(iconX - 1, iconY - 1, mute_symbol_width + 2, mute_symbol_height + 2); display->setColor(BLACK); } else { display->setColor(BLACK); display->fillRect(iconX - 1, iconY - 1, mute_symbol_width + 2, mute_symbol_height + 2); display->setColor(WHITE); } display->drawXbm(iconX, iconY, mute_symbol_width, mute_symbol_height, mute_symbol); } } if (show_date) { // === Draw Date === display->drawString(timeX, textY, dateLine); if (isBold) display->drawString(timeX - 1, textY, dateLine); } else { // === Draw Time === display->drawString(timeX, textY, timeStr); if (isBold) display->drawString(timeX - 1, textY, timeStr); } } else { // === No Time Available: Mail/Mute Icon Moves to Far Right === int iconRightEdge = screenW - xOffset; bool showMail = false; #ifndef USE_EINK if (hasUnreadMessage) { if (now - lastMailBlink > 500) { isMailIconVisible = !isMailIconVisible; lastMailBlink = now; } showMail = isMailIconVisible; } #else if (hasUnreadMessage) { showMail = true; } #endif if (showMail) { if (useHorizontalBattery) { int iconW = 16, iconH = 12; int iconX = iconRightEdge - iconW; int iconY = textY + (FONT_HEIGHT_SMALL - iconH) / 2 - 1; display->drawRect(iconX, iconY, iconW + 1, iconH); display->drawLine(iconX, iconY, iconX + iconW / 2, iconY + iconH - 4); display->drawLine(iconX + iconW, iconY, iconX + iconW / 2, iconY + iconH - 4); } else { int iconX = iconRightEdge - mail_width; int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2; display->drawXbm(iconX, iconY, mail_width, mail_height, mail); } } else if (externalNotificationModule->getMute()) { if (currentResolution == ScreenResolution::High) { int iconX = iconRightEdge - mute_symbol_big_width; int iconY = textY + (FONT_HEIGHT_SMALL - mute_symbol_big_height) / 2; display->drawXbm(iconX, iconY, mute_symbol_big_width, mute_symbol_big_height, mute_symbol_big); } else { int iconX = iconRightEdge - mute_symbol_width; int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2; display->drawXbm(iconX, iconY, mute_symbol_width, mute_symbol_height, mute_symbol); } } } #endif display->setColor(WHITE); // Reset for other UI } const int *getTextPositions(OLEDDisplay *display) { static int textPositions[7]; // Static array that persists beyond function scope if (currentResolution == ScreenResolution::High) { textPositions[0] = textZeroLine; textPositions[1] = textFirstLine_medium; textPositions[2] = textSecondLine_medium; textPositions[3] = textThirdLine_medium; textPositions[4] = textFourthLine_medium; textPositions[5] = textFifthLine_medium; textPositions[6] = textSixthLine_medium; } else { textPositions[0] = textZeroLine; textPositions[1] = textFirstLine; textPositions[2] = textSecondLine; textPositions[3] = textThirdLine; textPositions[4] = textFourthLine; textPositions[5] = textFifthLine; textPositions[6] = textSixthLine; } return textPositions; } // ************************* // * Common Footer Drawing * // ************************* void drawCommonFooter(OLEDDisplay *display, int16_t x, int16_t y) { if (!isAPIConnected(service->api_state)) return; const int scale = (currentResolution == ScreenResolution::High) ? 2 : 1; display->setColor(BLACK); display->fillRect(0, SCREEN_HEIGHT - (1 * scale) - (connection_icon_height * scale), (connection_icon_width * scale), (connection_icon_height * scale) + (2 * scale)); display->setColor(WHITE); if (currentResolution == ScreenResolution::High) { const int bytesPerRow = (connection_icon_width + 7) / 8; int iconX = 0; int iconY = SCREEN_HEIGHT - (connection_icon_height * 2); for (int yy = 0; yy < connection_icon_height; ++yy) { const uint8_t *rowPtr = connection_icon + yy * bytesPerRow; for (int xx = 0; xx < connection_icon_width; ++xx) { const uint8_t byteVal = pgm_read_byte(rowPtr + (xx >> 3)); const uint8_t bitMask = 1U << (xx & 7); // XBM is LSB-first if (byteVal & bitMask) { display->fillRect(iconX + xx * scale, iconY + yy * scale, scale, scale); } } } } else { display->drawXbm(0, SCREEN_HEIGHT - connection_icon_height, connection_icon_width, connection_icon_height, connection_icon); } } bool isAllowedPunctuation(char c) { const std::string allowed = ".,!?;:-_()[]{}'\"@#$/\\&+=%~^ "; return allowed.find(c) != std::string::npos; } static void replaceAll(std::string &s, const std::string &from, const std::string &to) { if (from.empty()) return; size_t pos = 0; while ((pos = s.find(from, pos)) != std::string::npos) { s.replace(pos, from.size(), to); pos += to.size(); } } std::string sanitizeString(const std::string &input) { std::string output; bool inReplacement = false; // Make a mutable copy so we can normalize UTF-8 “smart punctuation” into ASCII first. std::string s = input; // Curly single quotes: ‘ ’ replaceAll(s, "\xE2\x80\x98", "'"); // U+2018 replaceAll(s, "\xE2\x80\x99", "'"); // U+2019 // Curly double quotes: “ ” replaceAll(s, "\xE2\x80\x9C", "\""); // U+201C replaceAll(s, "\xE2\x80\x9D", "\""); // U+201D // En dash / Em dash: – — replaceAll(s, "\xE2\x80\x93", "-"); // U+2013 replaceAll(s, "\xE2\x80\x94", "-"); // U+2014 // Non-breaking space replaceAll(s, "\xC2\xA0", " "); // U+00A0 // Now do your original sanitize pass over the normalized string. for (unsigned char uc : s) { char c = static_cast(uc); if (std::isalnum(uc) || isAllowedPunctuation(c)) { output += c; inReplacement = false; } else { if (!inReplacement) { output += static_cast(0xBF); // ISO-8859-1 for inverted question mark inReplacement = true; } } } return output; } } // namespace graphics #endif ================================================ FILE: src/graphics/SharedUIDisplay.h ================================================ #pragma once #include #include namespace graphics { // ======================= // Shared UI Helpers // ======================= #define textZeroLine 0 // Consistent Line Spacing - this is standard for all display and the fall-back spacing #define textFirstLine (FONT_HEIGHT_SMALL - 1) #define textSecondLine (textFirstLine + (FONT_HEIGHT_SMALL - 5)) #define textThirdLine (textSecondLine + (FONT_HEIGHT_SMALL - 5)) #define textFourthLine (textThirdLine + (FONT_HEIGHT_SMALL - 5)) #define textFifthLine (textFourthLine + (FONT_HEIGHT_SMALL - 5)) #define textSixthLine (textFifthLine + (FONT_HEIGHT_SMALL - 5)) // Consistent Line Spacing for devices like T114 and TEcho/ThinkNode M1 of devices #define textFirstLine_medium (FONT_HEIGHT_SMALL + 1) #define textSecondLine_medium (textFirstLine_medium + FONT_HEIGHT_SMALL) #define textThirdLine_medium (textSecondLine_medium + FONT_HEIGHT_SMALL) #define textFourthLine_medium (textThirdLine_medium + FONT_HEIGHT_SMALL) #define textFifthLine_medium (textFourthLine_medium + FONT_HEIGHT_SMALL) #define textSixthLine_medium (textFifthLine_medium + FONT_HEIGHT_SMALL) // Consistent Line Spacing for devices like VisionMaster T190 #define textFirstLine_large (FONT_HEIGHT_SMALL + 1) #define textSecondLine_large (textFirstLine_large + (FONT_HEIGHT_SMALL + 5)) #define textThirdLine_large (textSecondLine_large + (FONT_HEIGHT_SMALL + 5)) #define textFourthLine_large (textThirdLine_large + (FONT_HEIGHT_SMALL + 5)) #define textFifthLine_large (textFourthLine_large + (FONT_HEIGHT_SMALL + 5)) #define textSixthLine_large (textFifthLine_large + (FONT_HEIGHT_SMALL + 5)) // Quick screen access #define SCREEN_WIDTH display->getWidth() #define SCREEN_HEIGHT display->getHeight() // Shared state (declare inside namespace) extern bool hasUnreadMessage; enum class ScreenResolution : uint8_t { UltraLow = 0, Low = 1, High = 2 }; extern ScreenResolution currentResolution; ScreenResolution determineScreenResolution(int16_t screenheight, int16_t screenwidth); void decomposeTime(uint32_t rtc_sec, int &hour, int &minute, int &second); // Rounded highlight (used for inverted headers) void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w, int16_t h, int16_t r); // Shared battery/time/mail header void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr = "", bool force_no_invert = false, bool show_date = false); // Shared battery/time/mail header void drawCommonFooter(OLEDDisplay *display, int16_t x, int16_t y); const int *getTextPositions(OLEDDisplay *display); bool isAllowedPunctuation(char c); std::string sanitizeString(const std::string &input); static inline bool isAPIConnected(uint8_t state) { static constexpr bool connectedStates[] = { /* STATE_NONE */ false, /* STATE_BLE */ true, /* STATE_WIFI */ true, /* STATE_SERIAL */ true, /* STATE_PACKET */ true, /* STATE_HTTP */ true, /* STATE_ETH */ true, }; return state < sizeof(connectedStates) ? connectedStates[state] : false; } } // namespace graphics ================================================ FILE: src/graphics/TFTDisplay.cpp ================================================ #include "configuration.h" #include "main.h" #if USE_TFTDISPLAY #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 #ifdef TFT_MESH_OVERRIDE uint16_t TFT_MESH = TFT_MESH_OVERRIDE; #else uint16_t 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(HACKADAY_COMMUNICATOR) #include Arduino_DataBus *bus = nullptr; Arduino_GFX *tft = nullptr; #elif defined(ST72xx_DE) #include #include #include #include TCA9534 ioex; class LGFX : public lgfx::LGFX_Device { lgfx::Bus_RGB _bus_instance; lgfx::Panel_RGB _panel_instance; lgfx::Touch_GT911 _touch_instance; public: const uint16_t screenWidth = TFT_WIDTH; const uint16_t screenHeight = TFT_HEIGHT; bool init_impl(bool use_reset, bool use_clear) override { ioex.attach(Wire); ioex.setDeviceAddress(0x18); ioex.config(1, TCA9534::Config::OUT); ioex.config(2, TCA9534::Config::OUT); ioex.config(3, TCA9534::Config::OUT); ioex.config(4, TCA9534::Config::OUT); ioex.output(1, TCA9534::Level::H); ioex.output(3, TCA9534::Level::L); ioex.output(4, TCA9534::Level::H); pinMode(1, OUTPUT); digitalWrite(1, LOW); ioex.output(2, TCA9534::Level::L); delay(20); ioex.output(2, TCA9534::Level::H); delay(100); pinMode(1, INPUT); return LGFX_Device::init_impl(use_reset, use_clear); } LGFX(void) { { auto cfg = _panel_instance.config(); cfg.memory_width = screenWidth; cfg.memory_height = screenHeight; cfg.panel_width = screenWidth; cfg.panel_height = screenHeight; cfg.offset_x = 0; cfg.offset_y = 0; cfg.offset_rotation = 0; _panel_instance.config(cfg); } { auto cfg = _panel_instance.config_detail(); cfg.use_psram = 0; _panel_instance.config_detail(cfg); } { auto cfg = _bus_instance.config(); cfg.panel = &_panel_instance; cfg.pin_d0 = ST72xx_B0; // B0 cfg.pin_d1 = ST72xx_B1; // B1 cfg.pin_d2 = ST72xx_B2; // B2 cfg.pin_d3 = ST72xx_B3; // B3 cfg.pin_d4 = ST72xx_B4; // B4 cfg.pin_d5 = ST72xx_G0; // G0 cfg.pin_d6 = ST72xx_G1; // G1 cfg.pin_d7 = ST72xx_G2; // G2 cfg.pin_d8 = ST72xx_G3; // G3 cfg.pin_d9 = ST72xx_G4; // G4 cfg.pin_d10 = ST72xx_G5; // G5 cfg.pin_d11 = ST72xx_R0; // R0 cfg.pin_d12 = ST72xx_R1; // R1 cfg.pin_d13 = ST72xx_R2; // R2 cfg.pin_d14 = ST72xx_R3; // R3 cfg.pin_d15 = ST72xx_R4; // R4 cfg.pin_henable = ST72xx_DE; cfg.pin_vsync = ST72xx_VSYNC; cfg.pin_hsync = ST72xx_HSYNC; cfg.pin_pclk = ST72xx_PCLK; cfg.freq_write = 13000000; #ifdef ST7265_HSYNC_POLARITY cfg.hsync_polarity = ST7265_HSYNC_POLARITY; cfg.hsync_front_porch = ST7265_HSYNC_FRONT_PORCH; // 8; cfg.hsync_pulse_width = ST7265_HSYNC_PULSE_WIDTH; // 4; cfg.hsync_back_porch = ST7265_HSYNC_BACK_PORCH; // 8; cfg.vsync_polarity = ST7265_VSYNC_POLARITY; // 0; cfg.vsync_front_porch = ST7265_VSYNC_FRONT_PORCH; // 8; cfg.vsync_pulse_width = ST7265_VSYNC_PULSE_WIDTH; // 4; cfg.vsync_back_porch = ST7265_VSYNC_BACK_PORCH; // 8; cfg.pclk_idle_high = 1; cfg.pclk_active_neg = ST7265_PCLK_ACTIVE_NEG; // 0; // cfg.pclk_idle_high = 0; // cfg.de_idle_high = 1; #endif #ifdef ST7262_HSYNC_POLARITY cfg.hsync_polarity = ST7262_HSYNC_POLARITY; cfg.hsync_front_porch = ST7262_HSYNC_FRONT_PORCH; // 8; cfg.hsync_pulse_width = ST7262_HSYNC_PULSE_WIDTH; // 4; cfg.hsync_back_porch = ST7262_HSYNC_BACK_PORCH; // 8; cfg.vsync_polarity = ST7262_VSYNC_POLARITY; // 0; cfg.vsync_front_porch = ST7262_VSYNC_FRONT_PORCH; // 8; cfg.vsync_pulse_width = ST7262_VSYNC_PULSE_WIDTH; // 4; cfg.vsync_back_porch = ST7262_VSYNC_BACK_PORCH; // 8; cfg.pclk_idle_high = 1; cfg.pclk_active_neg = ST7262_PCLK_ACTIVE_NEG; // 0; // cfg.pclk_idle_high = 0; // cfg.de_idle_high = 1; #endif #ifdef SC7277_HSYNC_POLARITY cfg.hsync_polarity = SC7277_HSYNC_POLARITY; cfg.hsync_front_porch = SC7277_HSYNC_FRONT_PORCH; // 8; cfg.hsync_pulse_width = SC7277_HSYNC_PULSE_WIDTH; // 4; cfg.hsync_back_porch = SC7277_HSYNC_BACK_PORCH; // 8; cfg.vsync_polarity = SC7277_VSYNC_POLARITY; // 0; cfg.vsync_front_porch = SC7277_VSYNC_FRONT_PORCH; // 8; cfg.vsync_pulse_width = SC7277_VSYNC_PULSE_WIDTH; // 4; cfg.vsync_back_porch = SC7277_VSYNC_BACK_PORCH; // 8; cfg.pclk_idle_high = 1; cfg.pclk_active_neg = SC7277_PCLK_ACTIVE_NEG; // 0; // cfg.pclk_idle_high = 0; // cfg.de_idle_high = 1; #endif _bus_instance.config(cfg); } _panel_instance.setBus(&_bus_instance); { auto cfg = _touch_instance.config(); cfg.x_min = 0; cfg.x_max = TFT_WIDTH; cfg.y_min = 0; cfg.y_max = TFT_HEIGHT; cfg.pin_int = -1; cfg.pin_rst = -1; cfg.bus_shared = true; cfg.offset_rotation = 0; cfg.i2c_port = 0; cfg.i2c_addr = 0x5D; cfg.pin_sda = I2C_SDA; cfg.pin_scl = I2C_SCL; cfg.freq = 400000; _touch_instance.config(cfg); _panel_instance.setTouch(&_touch_instance); } setPanel(&_panel_instance); } }; static LGFX *tft = nullptr; #elif defined(ILI9488_CS) #include // Graphics and font library for ILI9488 driver chip class LGFX : public lgfx::LGFX_Device { lgfx::Panel_ILI9488 _panel_instance; lgfx::Bus_SPI _bus_instance; lgfx::Light_PWM _light_instance; lgfx::Touch_GT911 _touch_instance; public: LGFX(void) { { auto cfg = _bus_instance.config(); // configure SPI cfg.spi_host = ILI9488_SPI_HOST; // ESP32-S2,S3,C3 : SPI2_HOST or SPI3_HOST / ESP32 : VSPI_HOST or HSPI_HOST cfg.spi_mode = 0; cfg.freq_write = SPI_FREQUENCY; // SPI clock for transmission (up to 80MHz, rounded to the value obtained by dividing // 80MHz by an integer) cfg.freq_read = SPI_READ_FREQUENCY; // SPI clock when receiving cfg.spi_3wire = false; // Set to true if reception is done on the MOSI pin cfg.use_lock = true; // Set to true to use transaction locking cfg.dma_channel = SPI_DMA_CH_AUTO; // SPI_DMA_CH_AUTO; // Set DMA channel to use (0=not use DMA / 1=1ch / 2=ch / // SPI_DMA_CH_AUTO=auto setting) cfg.pin_sclk = ILI9488_SCK; // Set SPI SCLK pin number cfg.pin_mosi = ILI9488_SDA; // Set SPI MOSI pin number cfg.pin_miso = ILI9488_MISO; // Set SPI MISO pin number (-1 = disable) cfg.pin_dc = ILI9488_RS; // Set SPI DC pin number (-1 = disable) _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 = ILI9488_CS; // Pin number where CS is connected (-1 = disable) cfg.pin_rst = -1; // Pin number where RST is connected (-1 = disable) cfg.pin_busy = -1; // Pin number where BUSY is connected (-1 = disable) // The following setting values ​​are general initial values ​​for each panel, so please comment out any // unknown items and try them. cfg.memory_width = TFT_WIDTH; // Maximum width supported by the driver IC cfg.memory_height = TFT_HEIGHT; // Maximum height supported by the driver IC cfg.panel_width = TFT_WIDTH; // actual displayable width cfg.panel_height = TFT_HEIGHT; // actual displayable height cfg.offset_x = TFT_OFFSET_X; // Panel offset amount in X direction cfg.offset_y = TFT_OFFSET_Y; // Panel offset amount in Y direction cfg.offset_rotation = TFT_OFFSET_ROTATION; // Rotation direction value offset 0~7 (4~7 is mirrored) #ifdef TFT_DUMMY_READ_PIXELS cfg.dummy_read_pixel = TFT_DUMMY_READ_PIXELS; // Number of bits for dummy read before pixel readout #else cfg.dummy_read_pixel = 9; // Number of bits for dummy read before pixel readout #endif cfg.dummy_read_bits = 1; // Number of bits for dummy read before non-pixel data read cfg.readable = true; // Set to true if data can be read cfg.invert = true; // Set to true if the light/darkness of the panel is reversed cfg.rgb_order = false; // Set to true if the panel's red and blue are swapped cfg.dlen_16bit = false; // Set to true for panels that transmit data length in 16-bit units with 16-bit parallel or SPI cfg.bus_shared = true; // If the bus is shared with the SD card, set to true (bus control with drawJpgFile etc.) // Set the following only when the display is shifted with a driver with a variable number of pixels, such as the // ST7735 or ILI9163. // cfg.memory_width = TFT_WIDTH; // Maximum width supported by the driver IC // cfg.memory_height = TFT_HEIGHT; // Maximum height supported by the driver IC _panel_instance.config(cfg); } #ifdef ILI9488_BL // Set the backlight control { auto cfg = _light_instance.config(); // Gets a structure for backlight settings. cfg.pin_bl = ILI9488_BL; // Pin number to which the backlight is connected cfg.invert = false; // true to invert the brightness of the backlight // cfg.freq = 44100; // PWM frequency of backlight // cfg.pwm_channel = 1; // PWM channel number to use _light_instance.config(cfg); _panel_instance.setLight(&_light_instance); // Set the backlight on the panel. } #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; #ifdef SCREEN_TOUCH_RST cfg.pin_rst = SCREEN_TOUCH_RST; #endif 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); } }; static LGFX *tft = nullptr; #elif defined(ST7789_CS) #include // Graphics and font library for ST7735 driver chip #ifdef HELTEC_V4_TFT #include "chsc6x.h" #include "lgfx/v1/Touch.hpp" namespace lgfx { inline namespace v1 { class TOUCH_CHSC6X : public ITouch { public: TOUCH_CHSC6X(void) { _cfg.i2c_addr = TOUCH_SLAVE_ADDRESS; _cfg.x_min = 0; _cfg.x_max = 240; _cfg.y_min = 0; _cfg.y_max = 320; }; bool init(void) override { if (chsc6xTouch == nullptr) { chsc6xTouch = new chsc6x(&Wire1, TOUCH_SDA_PIN, TOUCH_SCL_PIN, TOUCH_INT_PIN, TOUCH_RST_PIN); } chsc6xTouch->chsc6x_init(); return true; }; uint_fast8_t getTouchRaw(touch_point_t *tp, uint_fast8_t count) override { uint16_t raw_x, raw_y; if (chsc6xTouch->chsc6x_read_touch_info(&raw_x, &raw_y) == 0) { tp[0].x = 320 - 1 - raw_y; tp[0].y = 240 - 1 - raw_x; tp[0].size = 1; tp[0].id = 1; return 1; } tp[0].size = 0; return 0; }; void wakeup(void) override{}; void sleep(void) override{}; private: chsc6x *chsc6xTouch = nullptr; }; } // namespace v1 } // namespace lgfx #endif class LGFX : public lgfx::LGFX_Device { lgfx::Panel_ST7789 _panel_instance; lgfx::Bus_SPI _bus_instance; lgfx::Light_PWM _light_instance; #if HAS_TOUCHSCREEN #if defined(T_WATCH_S3) || defined(ELECROW) lgfx::Touch_FT5x06 _touch_instance; #elif defined(HELTEC_V4_TFT) lgfx::TOUCH_CHSC6X _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 = ST7789_RESET; // Pin number where RST is connected (-1 = disable) cfg.pin_busy = ST7789_BUSY; // Pin number where BUSY is connected (-1 = disable) // The following setting values ​​are general initial values ​​for each panel, so please comment out any // unknown items and try them. #if defined(T_WATCH_S3) cfg.panel_width = 240; cfg.panel_height = 240; cfg.memory_width = 240; cfg.memory_height = 320; cfg.offset_x = 0; cfg.offset_y = 0; // No vertical shift needed — panel is top-aligned cfg.offset_rotation = 2; // Rotate 180° to correct upside-down layout #else cfg.memory_width = TFT_WIDTH; // Maximum width supported by the driver IC cfg.memory_height = TFT_HEIGHT; // Maximum height supported by the driver IC cfg.panel_width = TFT_WIDTH; // actual displayable width cfg.panel_height = TFT_HEIGHT; // actual displayable height cfg.offset_x = TFT_OFFSET_X; // Panel offset amount in X direction cfg.offset_y = TFT_OFFSET_Y; // Panel offset amount in Y direction cfg.offset_rotation = TFT_OFFSET_ROTATION; // Rotation direction value offset 0~7 (4~7 is mirrored) #endif #ifdef TFT_DUMMY_READ_PIXELS cfg.dummy_read_pixel = TFT_DUMMY_READ_PIXELS; // Number of bits for dummy read before pixel readout #else cfg.dummy_read_pixel = 9; // Number of bits for dummy read before pixel readout #endif cfg.dummy_read_bits = 1; // Number of bits for dummy read before non-pixel data read cfg.readable = true; // Set to true if data can be read cfg.invert = true; // Set to true if the light/darkness of the panel is reversed cfg.rgb_order = false; // Set to true if the panel's red and blue are swapped cfg.dlen_16bit = false; // Set to true for panels that transmit data length in 16-bit units with 16-bit parallel or SPI cfg.bus_shared = true; // If the bus is shared with the SD card, set to true (bus control with drawJpgFile etc.) // 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; #ifdef SCREEN_TOUCH_RST cfg.pin_rst = SCREEN_TOUCH_RST; #endif 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(ST7796_CS) #include // Graphics and font library for ST7796 driver chip class LGFX : public lgfx::LGFX_Device { lgfx::Panel_ST7796 _panel_instance; lgfx::Bus_SPI _bus_instance; lgfx::Light_PWM _light_instance; public: LGFX(void) { { auto cfg = _bus_instance.config(); // SPI cfg.spi_host = ST7796_SPI_HOST; cfg.spi_mode = 0; cfg.freq_write = SPI_FREQUENCY; // SPI clock for transmission (up to 80MHz, rounded to the value obtained by dividing // 80MHz by an integer) cfg.freq_read = SPI_READ_FREQUENCY; // SPI clock when receiving cfg.spi_3wire = false; cfg.use_lock = true; // Set to true to use transaction locking cfg.dma_channel = SPI_DMA_CH_AUTO; // SPI_DMA_CH_AUTO; // Set DMA channel to use (0=not use DMA / 1=1ch / 2=ch / // SPI_DMA_CH_AUTO=auto setting) cfg.pin_sclk = ST7796_SCK; // Set SPI SCLK pin number cfg.pin_mosi = ST7796_SDA; // Set SPI MOSI pin number cfg.pin_miso = ST7796_MISO; // Set SPI MISO pin number (-1 = disable) cfg.pin_dc = ST7796_RS; // Set SPI DC pin number (-1 = disable) _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 = ST7796_CS; // Pin number where CS is connected (-1 = disable) cfg.pin_rst = ST7796_RESET; // Pin number where RST is connected (-1 = disable) cfg.pin_busy = ST7796_BUSY; // Pin number where BUSY is connected (-1 = disable) // cfg.memory_width = TFT_WIDTH; // Maximum width supported by the driver IC // cfg.memory_height = TFT_HEIGHT; // Maximum height supported by the driver IC cfg.panel_width = TFT_WIDTH; // actual displayable width cfg.panel_height = TFT_HEIGHT; // actual displayable height cfg.offset_x = TFT_OFFSET_X; // Panel offset amount in X direction cfg.offset_y = TFT_OFFSET_Y; // Panel offset amount in Y direction cfg.offset_rotation = TFT_OFFSET_ROTATION; // Rotation direction value offset 0~7 (4~7 is mirrored) #ifdef TFT_DUMMY_READ_PIXELS cfg.dummy_read_pixel = TFT_DUMMY_READ_PIXELS; // Number of bits for dummy read before pixel readout #else cfg.dummy_read_pixel = 8; // Number of bits for dummy read before pixel readout #endif cfg.dummy_read_bits = 1; // Number of bits for dummy read before non-pixel data read cfg.readable = true; // Set to true if data can be read cfg.invert = true; // Set to true if the light/darkness of the panel is reversed cfg.rgb_order = false; // Set to true if the panel's red and blue are swapped cfg.dlen_16bit = false; // Set to true for panels that transmit data length in 16-bit units with 16-bit parallel or SPI cfg.bus_shared = true; // If the bus is shared with the SD card, set to true (bus control with drawJpgFile etc.) _panel_instance.config(cfg); } #ifdef ST7796_BL // Set the backlight control. (delete if not necessary) { auto cfg = _light_instance.config(); // Gets a structure for backlight settings. cfg.pin_bl = ST7796_BL; // Pin number to which the backlight is connected cfg.invert = false; // true to invert the brightness of the backlight cfg.freq = 44100; cfg.pwm_channel = 7; _light_instance.config(cfg); _panel_instance.setLight(&_light_instance); // Set the backlight on the panel. } #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 #include "Panel_sdl.hpp" #include // Graphics and font library for ST7735 driver chip class LGFX : public lgfx::LGFX_Device { lgfx::Bus_SPI _bus_instance; lgfx::ITouch *_touch_instance; public: lgfx::Panel_Device *_panel_instance; LGFX(void) { if (portduino_config.displayPanel == st7789) _panel_instance = new lgfx::Panel_ST7789; else if (portduino_config.displayPanel == st7735) _panel_instance = new lgfx::Panel_ST7735; else if (portduino_config.displayPanel == st7735s) _panel_instance = new lgfx::Panel_ST7735S; else if (portduino_config.displayPanel == st7796) _panel_instance = new lgfx::Panel_ST7796; else if (portduino_config.displayPanel == ili9341) _panel_instance = new lgfx::Panel_ILI9341; else if (portduino_config.displayPanel == ili9342) _panel_instance = new lgfx::Panel_ILI9342; else if (portduino_config.displayPanel == ili9488) _panel_instance = new lgfx::Panel_ILI9488; else if (portduino_config.displayPanel == hx8357d) _panel_instance = new lgfx::Panel_HX8357D; #if defined(SDL_h_) else if (portduino_config.displayPanel == x11) _panel_instance = new lgfx::Panel_sdl; #endif else { _panel_instance = new lgfx::Panel_NULL; LOG_ERROR("Unknown display panel configured!"); } auto buscfg = _bus_instance.config(); buscfg.spi_mode = 0; buscfg.spi_host = portduino_config.display_spi_dev_int; buscfg.pin_dc = portduino_config.displayDC.pin; // Set SPI DC pin number (-1 = disable) _bus_instance.config(buscfg); // applies the set value to the bus. if (portduino_config.displayPanel != x11) _panel_instance->setBus(&_bus_instance); // set the bus on the panel. auto cfg = _panel_instance->config(); // Gets a structure for display panel settings. LOG_DEBUG("Width: %d, Height: %d", portduino_config.displayWidth, portduino_config.displayHeight); cfg.pin_cs = portduino_config.displayCS.pin; // Pin number where CS is connected (-1 = disable) cfg.pin_rst = portduino_config.displayReset.pin; if (portduino_config.displayRotate) { cfg.panel_width = portduino_config.displayHeight; // actual displayable width cfg.panel_height = portduino_config.displayWidth; // actual displayable height } else { cfg.panel_width = portduino_config.displayWidth; // actual displayable width cfg.panel_height = portduino_config.displayHeight; // actual displayable height } cfg.offset_x = portduino_config.displayOffsetX; // Panel offset amount in X direction cfg.offset_y = portduino_config.displayOffsetY; // Panel offset amount in Y direction cfg.offset_rotation = portduino_config.displayOffsetRotate; // Rotation direction value offset 0~7 (4~7 is mirrored) cfg.invert = portduino_config.displayInvert; // Set to true if the light/darkness of the panel is reversed _panel_instance->config(cfg); // Configure settings for touch control. if (portduino_config.touchscreenModule) { if (portduino_config.touchscreenModule == xpt2046) { _touch_instance = new lgfx::Touch_XPT2046; } else if (portduino_config.touchscreenModule == stmpe610) { _touch_instance = new lgfx::Touch_STMPE610; } else if (portduino_config.touchscreenModule == ft5x06) { _touch_instance = new lgfx::Touch_FT5x06; } auto touch_cfg = _touch_instance->config(); touch_cfg.pin_cs = portduino_config.touchscreenCS.pin; touch_cfg.x_min = 0; touch_cfg.x_max = portduino_config.displayHeight - 1; touch_cfg.y_min = 0; touch_cfg.y_max = portduino_config.displayWidth - 1; touch_cfg.pin_int = portduino_config.touchscreenIRQ.pin; touch_cfg.bus_shared = true; touch_cfg.offset_rotation = portduino_config.touchscreenRotate; if (portduino_config.touchscreenI2CAddr != -1) { touch_cfg.i2c_addr = portduino_config.touchscreenI2CAddr; } else { touch_cfg.spi_host = portduino_config.touchscreen_spi_dev_int; } _touch_instance->config(touch_cfg); _panel_instance->setTouch(_touch_instance); } #if defined(SDL_h_) if (portduino_config.displayPanel == x11) { lgfx::Panel_sdl *sdl_panel_ = (lgfx::Panel_sdl *)_panel_instance; sdl_panel_->setup(); sdl_panel_->addKeyCodeMapping(SDLK_RETURN, SDL_SCANCODE_KP_ENTER); } #endif setPanel(_panel_instance); // Sets the panel to use. } }; 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 PanelInit_ST7701 : public lgfx::Panel_ST7701 { public: const uint8_t *getInitCommands(uint8_t listno) const override { // 180 degree hw rotation: vertical flip, horizontal flip static constexpr const uint8_t list1[] = {0x36, 1, 0x10, // MADCTL for vertical flip 0xFF, 5, 0x77, 0x01, 0x00, 0x00, 0x10, // Command2 BK0 SEL 0xC7, 1, 0x04, // SDIR: X-direction Control (Horizontal Flip) 0xFF, 5, 0x77, 0x01, 0x00, 0x00, 0x00, // Command2 BK0 DIS 0xFF, 0xFF}; switch (listno) { case 1: return list1; default: return lgfx::Panel_ST7701::getInitCommands(listno); } } }; class LGFX : public lgfx::LGFX_Device { PanelInit_ST7701 _panel_instance; lgfx::Bus_RGB _bus_instance; lgfx::Light_PWM _light_instance; lgfx::Touch_FT5x06 _touch_instance; 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 #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 (portduino_config.displayRotate) { setGeometry(GEOMETRY_RAWMODE, portduino_config.displayWidth, portduino_config.displayWidth); } else { setGeometry(GEOMETRY_RAWMODE, portduino_config.displayHeight, portduino_config.displayHeight); } #elif defined(SCREEN_ROTATE) setGeometry(GEOMETRY_RAWMODE, TFT_HEIGHT, TFT_WIDTH); #else setGeometry(GEOMETRY_RAWMODE, TFT_WIDTH, TFT_HEIGHT); #endif } TFTDisplay::~TFTDisplay() { // Clean up allocated line pixel buffer to prevent memory leak if (linePixelBuffer != nullptr) { free(linePixelBuffer); linePixelBuffer = nullptr; } } // Write the buffer to the display memory void TFTDisplay::display(bool fromBlank) { if (fromBlank) tft->fillScreen(TFT_BLACK); concurrency::LockGuard g(spiLock); uint32_t x, y; uint32_t y_byteIndex; uint8_t y_byteMask; uint32_t x_FirstPixelUpdate; uint32_t x_LastPixelUpdate; bool isset, dblbuf_isset; uint16_t colorTftMesh, colorTftBlack; bool somethingChanged = false; // Store colors byte-reversed so that TFT_eSPI doesn't have to swap bytes in a separate step colorTftMesh = __builtin_bswap16(TFT_MESH); colorTftBlack = __builtin_bswap16(TFT_BLACK); y = 0; while (y < displayHeight) { y_byteIndex = (y / 8) * displayWidth; y_byteMask = (1 << (y & 7)); // Step 1: Do a quick scan of 8 rows together. This allows fast-forwarding over unchanged screen areas. if (y_byteMask == 1) { if (!fromBlank) { for (x = 0; x < displayWidth; x++) { if (buffer[x + y_byteIndex] != buffer_back[x + y_byteIndex]) break; } } else { for (x = 0; x < displayWidth; x++) { if (buffer[x + y_byteIndex] != 0) break; } } if (x >= displayWidth) { // No changed pixels found in these 8 rows, fast-forward to the next 8 y = y + 8; continue; } } // Step 2: Scan each of the 8 rows individually. Find the first pixel in each row that needs updating for (x_FirstPixelUpdate = 0; x_FirstPixelUpdate < displayWidth; x_FirstPixelUpdate++) { isset = buffer[x_FirstPixelUpdate + y_byteIndex] & y_byteMask; if (!fromBlank) { // get src pixel in the page based ordering the OLED lib uses dblbuf_isset = buffer_back[x_FirstPixelUpdate + y_byteIndex] & y_byteMask; if (isset != dblbuf_isset) { break; } } else if (isset) { break; } } // Did we find a pixel that needs updating on this row? if (x_FirstPixelUpdate < displayWidth) { // Quickly write out the first changed pixel (saves another array lookup) linePixelBuffer[x_FirstPixelUpdate] = isset ? colorTftMesh : colorTftBlack; x_LastPixelUpdate = x_FirstPixelUpdate; // Step 3: copy all remaining pixels in this row into the pixel line buffer, // while also recording the last pixel in the row that needs updating for (x = x_FirstPixelUpdate + 1; x < displayWidth; x++) { isset = buffer[x + y_byteIndex] & y_byteMask; linePixelBuffer[x] = isset ? colorTftMesh : colorTftBlack; if (!fromBlank) { dblbuf_isset = buffer_back[x + y_byteIndex] & y_byteMask; if (isset != dblbuf_isset) { x_LastPixelUpdate = x; } } else if (isset) { x_LastPixelUpdate = x; } } #if defined(HACKADAY_COMMUNICATOR) tft->draw16bitBeRGBBitmap(x_FirstPixelUpdate, y, &linePixelBuffer[x_FirstPixelUpdate], (x_LastPixelUpdate - x_FirstPixelUpdate + 1), 1); #else // Step 4: Send the changed pixels on this line to the screen as a single block transfer. // This function accepts pixel data MSB first so it can dump the memory straight out the SPI port. tft->pushRect(x_FirstPixelUpdate, y, (x_LastPixelUpdate - x_FirstPixelUpdate + 1), 1, &linePixelBuffer[x_FirstPixelUpdate]); #endif somethingChanged = true; } y++; } // Copy the Buffer to the Back Buffer if (somethingChanged) memcpy(buffer_back, buffer, displayBufferSize); } void TFTDisplay::sdlLoop() { #if defined(SDL_h_) static int lastPressed = 0; static int shuttingDown = false; if (portduino_config.displayPanel == x11) { lgfx::Panel_sdl *sdl_panel_ = (lgfx::Panel_sdl *)tft->_panel_instance; if (sdl_panel_->loop() && !shuttingDown) { LOG_WARN("Window Closed!"); InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_SHUTDOWN, .kbchar = 0, .touchX = 0, .touchY = 0}; inputBroker->injectInputEvent(&event); } // debounce if (lastPressed != 0 && !sdl_panel_->gpio_in(lastPressed)) return; if (!sdl_panel_->gpio_in(37)) { lastPressed = 37; InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_RIGHT, .kbchar = 0, .touchX = 0, .touchY = 0}; inputBroker->injectInputEvent(&event); } else if (!sdl_panel_->gpio_in(36)) { lastPressed = 36; InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_UP, .kbchar = 0, .touchX = 0, .touchY = 0}; inputBroker->injectInputEvent(&event); } else if (!sdl_panel_->gpio_in(38)) { lastPressed = 38; InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_DOWN, .kbchar = 0, .touchX = 0, .touchY = 0}; inputBroker->injectInputEvent(&event); } else if (!sdl_panel_->gpio_in(39)) { lastPressed = 39; InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_LEFT, .kbchar = 0, .touchX = 0, .touchY = 0}; inputBroker->injectInputEvent(&event); } else if (!sdl_panel_->gpio_in(SDL_SCANCODE_KP_ENTER)) { lastPressed = SDL_SCANCODE_KP_ENTER; InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_SELECT, .kbchar = 0, .touchX = 0, .touchY = 0}; inputBroker->injectInputEvent(&event); } else { lastPressed = 0; } } #endif } // Send a command to the display (low level function) void TFTDisplay::sendCommand(uint8_t com) { // handle display on/off directly switch (com) { case DISPLAYON: { // LOG_DEBUG("Display on"); backlightEnable->set(true); #if ARCH_PORTDUINO display(true); if (portduino_config.displayBacklight.pin > 0) digitalWrite(portduino_config.displayBacklight.pin, TFT_BACKLIGHT_ON); #elif defined(HACKADAY_COMMUNICATOR) tft->displayOn(); #elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE) tft->wakeup(); tft->powerSaveOff(); #endif #ifdef VTFT_CTRL digitalWrite(VTFT_CTRL, LOW); #endif #ifdef UNPHONE unphone.backlight(true); // using unPhone library #endif #ifdef RAK14014 #elif !defined(M5STACK) && !defined(ST7789_CS) && \ !defined(HACKADAY_COMMUNICATOR) // T-Deck gets brightness set in Screen.cpp in the handleSetOn function tft->setBrightness(172); #endif break; } case DISPLAYOFF: { // LOG_DEBUG("Display off"); backlightEnable->set(false); #if ARCH_PORTDUINO tft->clear(); if (portduino_config.displayBacklight.pin > 0) digitalWrite(portduino_config.displayBacklight.pin, !TFT_BACKLIGHT_ON); #elif defined(HACKADAY_COMMUNICATOR) tft->displayOff(); #elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE) tft->sleep(); tft->powerSaveOn(); #endif #ifdef VTFT_CTRL digitalWrite(VTFT_CTRL, HIGH); #endif #ifdef UNPHONE unphone.backlight(false); // using unPhone library #endif #ifdef RAK14014 #elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR) tft->setBrightness(0); #endif break; } default: break; } // Drop all other commands to device (we just update the buffer) } void TFTDisplay::setDisplayBrightness(uint8_t _brightness) { #ifdef RAK14014 // todo #elif !defined(HACKADAY_COMMUNICATOR) tft->setBrightness(_brightness); LOG_DEBUG("Brightness is set to value: %i ", _brightness); #endif } void TFTDisplay::flipScreenVertically() { #if defined(T_WATCH_S3) LOG_DEBUG("Flip TFT vertically"); // T-Watch S3 right-handed orientation tft->setRotation(0); #endif } bool TFTDisplay::hasTouch(void) { #ifdef RAK14014 return true; #elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR) return tft->touch() != nullptr; #else return false; #endif } bool TFTDisplay::getTouch(int16_t *x, int16_t *y) { #ifdef RAK14014 if (_rak14014_touch_int) { _rak14014_touch_int = false; /* The X and Y axes have to be switched */ *y = ft6336u.read_touch1_x(); *x = TFT_HEIGHT - ft6336u.read_touch1_y(); return true; } else { return false; } #elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR) return tft->getTouch(x, y); #else return false; #endif } void TFTDisplay::setDetected(uint8_t detected) { (void)detected; } // Connect to the display bool TFTDisplay::connect() { concurrency::LockGuard g(spiLock); LOG_INFO("Do TFT init"); #ifdef RAK14014 tft = new TFT_eSPI; #elif defined(HACKADAY_COMMUNICATOR) bus = new Arduino_ESP32SPI(TFT_DC, TFT_CS, 38 /* SCK */, 21 /* MOSI */, GFX_NOT_DEFINED /* MISO */, HSPI /* spi_num */); tft = new Arduino_NV3007(bus, 40, 0 /* rotation */, false /* IPS */, 142 /* width */, 428 /* height */, 12 /* col offset 1 */, 0 /* row offset 1 */, 14 /* col offset 2 */, 0 /* row offset 2 */, nv3007_279_init_operations, sizeof(nv3007_279_init_operations)); #else tft = new LGFX; #endif backlightEnable->set(true); LOG_INFO("Power to TFT Backlight"); #ifdef UNPHONE unphone.backlight(true); // using unPhone library #endif #ifdef HACKADAY_COMMUNICATOR bool beginStatus = tft->begin(); if (beginStatus) LOG_DEBUG("TFT Success!"); else LOG_ERROR("TFT Fail!"); #else tft->init(); #endif #if defined(M5STACK) tft->setRotation(0); #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) tft->setRotation(2); // T-Watch S3 left-handed orientation #elif ARCH_PORTDUINO || defined(SENSECAP_INDICATOR) || defined(T_LORA_PAGER) tft->setRotation(0); // use config.yaml to set rotation #else tft->setRotation(3); // Orient horizontal and wide underneath the silkscreen name label #endif tft->fillScreen(TFT_BLACK); if (this->linePixelBuffer == NULL) { this->linePixelBuffer = (uint16_t *)malloc(sizeof(uint16_t) * displayWidth); if (!this->linePixelBuffer) { LOG_ERROR("Not enough memory to create TFT line buffer\n"); return false; } } return true; } #endif // USE_TFTDISPLAY ================================================ FILE: src/graphics/TFTDisplay.h ================================================ #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); // Destructor to clean up allocated memory ~TFTDisplay(); // Write the buffer to the display memory virtual void display() override { display(false); }; virtual void display(bool fromBlank); void sdlLoop(); // 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; uint16_t *linePixelBuffer = nullptr; }; ================================================ FILE: src/graphics/TimeFormatters.cpp ================================================ #include "TimeFormatters.h" #include "configuration.h" #include "gps/RTC.h" #include "mesh/NodeDB.h" #include bool deltaToTimestamp(uint32_t secondsAgo, uint8_t *hours, uint8_t *minutes, int32_t *daysAgo) { // Cache the result - avoid frequent recalculation static uint8_t hoursCached = 0, minutesCached = 0; static uint32_t daysAgoCached = 0; static uint32_t secondsAgoCached = 0; static bool validCached = false; // 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; } void getTimeAgoStr(uint32_t agoSecs, char *timeStr, uint8_t maxLength) { // Use an absolute timestamp in some cases. // Particularly useful with E-Ink displays. Static UI, fewer refreshes. uint8_t timestampHours, timestampMinutes; int32_t daysAgo; bool useTimestamp = deltaToTimestamp(agoSecs, ×tampHours, ×tampMinutes, &daysAgo); if (agoSecs < 120) // last 2 mins? snprintf(timeStr, maxLength, "%u seconds ago", agoSecs); // -- if suitable for timestamp -- else if (useTimestamp && agoSecs < 15 * SECONDS_IN_MINUTE) // Last 15 minutes snprintf(timeStr, maxLength, "%u minutes ago", agoSecs / SECONDS_IN_MINUTE); else if (useTimestamp && daysAgo == 0) // Today snprintf(timeStr, maxLength, "Last seen: %02u:%02u", (unsigned int)timestampHours, (unsigned int)timestampMinutes); else if (useTimestamp && daysAgo == 1) // Yesterday snprintf(timeStr, maxLength, "Seen yesterday"); else if (useTimestamp && daysAgo > 1) // Last six months (capped by deltaToTimestamp method) snprintf(timeStr, maxLength, "%li days ago", (long)daysAgo); // -- if using time delta instead -- else if (agoSecs < 120 * 60) // last 2 hrs snprintf(timeStr, maxLength, "%u minutes ago", agoSecs / 60); // Only show hours ago if it's been less than 6 months. Otherwise, we may have bad data. else if ((agoSecs / 60 / 60) < (730 * 6)) snprintf(timeStr, maxLength, "%u hours ago", agoSecs / 60 / 60); else snprintf(timeStr, maxLength, "unknown age"); } void getUptimeStr(uint32_t uptimeMillis, const char *prefix, char *uptimeStr, uint8_t maxLength, bool includeSecs) { uint32_t days = uptimeMillis / 86400000; uint32_t hours = (uptimeMillis % 86400000) / 3600000; uint32_t mins = (uptimeMillis % 3600000) / 60000; uint32_t secs = (uptimeMillis % 60000) / 1000; if (days) { snprintf(uptimeStr, maxLength, "%s%ud %uh", prefix, days, hours); } else if (hours) { snprintf(uptimeStr, maxLength, "%s%uh %um", prefix, hours, mins); } else if (!includeSecs) { snprintf(uptimeStr, maxLength, "%s%um", prefix, mins); } else if (mins) { snprintf(uptimeStr, maxLength, "%s%um %us", prefix, mins, secs); } else { snprintf(uptimeStr, maxLength, "%s%us", prefix, secs); } } ================================================ FILE: src/graphics/TimeFormatters.h ================================================ #pragma once #include "configuration.h" #include "gps/RTC.h" #include #include /** * Convert a delta in seconds ago to timestamp information (hours, minutes, days ago). * * @param secondsAgo Number of seconds ago to convert * @param hours Pointer to store the hours (0-23) * @param minutes Pointer to store the minutes (0-59) * @param daysAgo Pointer to store the number of days ago * @return true if conversion was successful, false if invalid input or time not available */ bool deltaToTimestamp(uint32_t secondsAgo, uint8_t *hours, uint8_t *minutes, int32_t *daysAgo); /** * Get a human-readable string representing the time ago in a format like "2 days, 3 hours, 15 minutes". * * @param agoSecs Number of seconds ago to convert * @param timeStr Pointer to store the resulting string * @param maxLength Maximum length of the resulting string buffer */ void getTimeAgoStr(uint32_t agoSecs, char *timeStr, uint8_t maxLength); /** * Get a compact human-readable string that only shows the largest non-zero time components. * For example, 0 days 1 hour 2 minutes will display as "1h 2m" but 1 day 2 hours 3 minutes * will display as "1d 2h". */ void getUptimeStr(uint32_t uptimeMillis, const char *prefix, char *uptimeStr, uint8_t maxLength, bool includeSecs = false); ================================================ FILE: src/graphics/VirtualKeyboard.cpp ================================================ #include "configuration.h" #if HAS_SCREEN #include "VirtualKeyboard.h" #include "graphics/Screen.h" #include "graphics/ScreenFonts.h" #include "graphics/SharedUIDisplay.h" #include "main.h" #include #include namespace graphics { VirtualKeyboard::VirtualKeyboard() : cursorRow(0), cursorCol(0), lastActivityTime(millis()) { initializeKeyboard(); // Set cursor to H(2, 5) cursorRow = 2; cursorCol = 5; } VirtualKeyboard::~VirtualKeyboard() {} void VirtualKeyboard::initializeKeyboard() { // New 4 row, 11 column keyboard layout: static const char LAYOUT[KEYBOARD_ROWS][KEYBOARD_COLS] = {{'1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '\b'}, {'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '\n'}, {'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', ' '}, {'z', 'x', 'c', 'v', 'b', 'n', 'm', '.', ',', '?', '\x1b'}}; // Derive layout dimensions and assert they match the configured keyboard grid constexpr int LAYOUT_ROWS = (int)(sizeof(LAYOUT) / sizeof(LAYOUT[0])); constexpr int LAYOUT_COLS = (int)(sizeof(LAYOUT[0]) / sizeof(LAYOUT[0][0])); static_assert(LAYOUT_ROWS == KEYBOARD_ROWS, "LAYOUT rows must equal KEYBOARD_ROWS"); static_assert(LAYOUT_COLS == KEYBOARD_COLS, "LAYOUT cols must equal KEYBOARD_COLS"); // Initialize all keys to empty first for (int row = 0; row < LAYOUT_ROWS; row++) { for (int col = 0; col < LAYOUT_COLS; col++) { keyboard[row][col] = {0, VK_CHAR, 0, 0, 0, 0}; } } // Fill keyboard from the 2D layout for (int row = 0; row < LAYOUT_ROWS; row++) { for (int col = 0; col < LAYOUT_COLS; col++) { char ch = LAYOUT[row][col]; // No empty slots in the simplified layout VirtualKeyType type = VK_CHAR; if (ch == '\b') { type = VK_BACKSPACE; } else if (ch == '\n') { type = VK_ENTER; } else if (ch == '\x1b') { // ESC type = VK_ESC; } else if (ch == ' ') { type = VK_SPACE; } // Make action keys wider to fit text while keeping the last column aligned uint8_t width = (type == VK_BACKSPACE || type == VK_ENTER || type == VK_SPACE) ? (KEY_WIDTH * 3) : KEY_WIDTH; keyboard[row][col] = {ch, type, (uint8_t)(col * KEY_WIDTH), (uint8_t)(row * KEY_HEIGHT), width, KEY_HEIGHT}; } } } void VirtualKeyboard::draw(OLEDDisplay *display, int16_t offsetX, int16_t offsetY) { // Repeat ticking is driven by NotificationRenderer once per frame // Base styles display->setColor(WHITE); display->setFont(FONT_SMALL); // Screen geometry const int screenW = display->getWidth(); const int screenH = display->getHeight(); // Decide wide-screen mode: if there is comfortable width, allow taller keys and reserve fixed width for last column labels // Heuristic: if screen width >= 200px (e.g., 240x135), treat as wide const bool isWide = screenW >= 200; // Determine last-column label max width display->setFont(FONT_SMALL); const int wENTER = display->getStringWidth("ENTER"); int lastColLabelW = wENTER; // ENTER is usually the widest // Smaller padding on very small screens to avoid excessive whitespace const int lastColPad = (screenW <= 128 ? 2 : 6); const int reservedLastColW = lastColLabelW + lastColPad; // reserved width for last column keys // Always reserve width for the rightmost text column to avoid overlap on small screens int cellW = 0; int leftoverW = 0; { const int leftCols = KEYBOARD_COLS - 1; // 10 input characters int usableW = screenW - reservedLastColW; if (usableW < leftCols) { // Guard: ensure at least 1px per left cell if labels are extremely wide (unlikely) usableW = leftCols; } cellW = usableW / leftCols; leftoverW = usableW - cellW * leftCols; // distribute extra pixels over left columns (left to right) } // Dynamic key geometry int cellH = KEY_HEIGHT; int keyboardStartY = 0; if (screenH <= 64) { const int headerHeight = headerText.empty() ? 0 : (FONT_HEIGHT_SMALL - 2); const int gapBelowHeader = 0; const int singleLineBoxHeight = FONT_HEIGHT_SMALL; const int gapAboveKeyboard = 0; keyboardStartY = offsetY + headerHeight + gapBelowHeader + singleLineBoxHeight + gapAboveKeyboard; if (keyboardStartY < 0) keyboardStartY = 0; if (keyboardStartY > screenH) keyboardStartY = screenH; int keyboardHeight = screenH - keyboardStartY; cellH = std::max(1, keyboardHeight / KEYBOARD_ROWS); } else if (isWide) { // For wide screens (e.g., T114 240x135), prefer square keys: height equals left-column key width. cellH = std::max((int)KEY_HEIGHT, cellW); // Guarantee at least 2 lines of input are visible by reducing cell height minimally if needed. // Replicate the spacing used in drawInputArea(): headerGap=1, box-to-header gap=1, gap above keyboard=1 display->setFont(FONT_SMALL); const int headerHeight = headerText.empty() ? 0 : (FONT_HEIGHT_SMALL + 1); const int headerToBoxGap = 1; const int gapAboveKb = 1; const int minBoxHeightForTwoLines = 2 * FONT_HEIGHT_SMALL + 2; // inner 1px top/bottom int maxKeyboardHeight = screenH - (offsetY + headerHeight + headerToBoxGap + minBoxHeightForTwoLines + gapAboveKb); int maxCellHAllowed = maxKeyboardHeight / KEYBOARD_ROWS; if (maxCellHAllowed < (int)KEY_HEIGHT) maxCellHAllowed = KEY_HEIGHT; if (maxCellHAllowed > 0 && cellH > maxCellHAllowed) { cellH = maxCellHAllowed; } // Keyboard placement from bottom for wide screens int keyboardHeight = KEYBOARD_ROWS * cellH; keyboardStartY = screenH - keyboardHeight; if (keyboardStartY < 0) keyboardStartY = 0; } else { // Default (non-wide, non-64px) behavior: use key height heuristic and place at bottom cellH = KEY_HEIGHT; int keyboardHeight = KEYBOARD_ROWS * cellH; keyboardStartY = screenH - keyboardHeight; if (keyboardStartY < 0) keyboardStartY = 0; } // Draw input area above keyboard drawInputArea(display, offsetX, offsetY, keyboardStartY); // Precompute per-column x and width with leftover distributed over left columns for even spacing int colX[KEYBOARD_COLS]; int colW[KEYBOARD_COLS]; int runningX = offsetX; for (int col = 0; col < KEYBOARD_COLS - 1; ++col) { int wcol = cellW + (col < leftoverW ? 1 : 0); colX[col] = runningX; colW[col] = wcol; runningX += wcol; } // Last column colX[KEYBOARD_COLS - 1] = runningX; colW[KEYBOARD_COLS - 1] = reservedLastColW; // Draw keyboard grid for (int row = 0; row < KEYBOARD_ROWS; row++) { for (int col = 0; col < KEYBOARD_COLS; col++) { const VirtualKey &k = keyboard[row][col]; if (k.character != 0 || k.type != VK_CHAR) { const bool isLastCol = (col == KEYBOARD_COLS - 1); int x = colX[col]; int w = colW[col]; int y = offsetY + keyboardStartY + row * cellH; int h = cellH; bool selected = (row == cursorRow && col == cursorCol); drawKey(display, k, selected, x, y, (uint8_t)w, (uint8_t)h, isLastCol); } } } } void VirtualKeyboard::drawInputArea(OLEDDisplay *display, int16_t offsetX, int16_t offsetY, int16_t keyboardStartY) { display->setColor(WHITE); const int screenWidth = display->getWidth(); const int screenHeight = display->getHeight(); // Use the standard small font metrics for input box sizing (restore original size) const int inputLineH = FONT_HEIGHT_SMALL; // Header uses the standard small (which may be larger on big screens) display->setFont(FONT_SMALL); int headerHeight = 0; if (!headerText.empty()) { // Draw header and reserve exact font height (plus a tighter gap) to maximize input area display->drawString(offsetX + 2, offsetY, headerText.c_str()); if (screenHeight <= 64) { headerHeight = FONT_HEIGHT_SMALL - 2; // 11px } else { headerHeight = FONT_HEIGHT_SMALL; // no extra padding baked in } } const int boxX = offsetX; const int boxWidth = screenWidth; int boxY; int boxHeight; if (screenHeight <= 64) { const int gapBelowHeader = 0; const int fixedBoxHeight = inputLineH; const int gapAboveKeyboard = 0; boxY = offsetY + headerHeight + gapBelowHeader; boxHeight = fixedBoxHeight; if (boxY + boxHeight + gapAboveKeyboard > keyboardStartY) { int over = boxY + boxHeight + gapAboveKeyboard - keyboardStartY; boxHeight = std::max(1, fixedBoxHeight - over); } } else { const int gapBelowHeader = 1; int gapAboveKeyboard = 1; int tmpBoxY = offsetY + headerHeight + gapBelowHeader; const int minBoxHeight = inputLineH + 2; int availableH = keyboardStartY - tmpBoxY - gapAboveKeyboard; if (availableH < minBoxHeight) availableH = minBoxHeight; boxY = tmpBoxY; boxHeight = availableH; } // Draw box border display->drawRect(boxX, boxY, boxWidth, boxHeight); display->setFont(FONT_SMALL); // Text rendering: multi-line if space allows (>= 2 lines), else single-line with leading ellipsis const int textX = boxX + 2; const int maxTextWidth = boxWidth - 4; const int maxLines = (boxHeight - 2) / inputLineH; if (maxLines >= 2) { // Inner bounds for caret clamping const int innerLeft = boxX + 1; const int innerRight = boxX + boxWidth - 2; const int innerTop = boxY + 1; const int innerBottom = boxY + boxHeight - 2; // Wrap text greedily into lines that fit maxTextWidth std::vector lines; { std::string remaining = inputText; while (!remaining.empty()) { int bestLen = 0; for (int len = 1; len <= (int)remaining.size(); ++len) { int w = display->getStringWidth(remaining.substr(0, len).c_str()); if (w <= maxTextWidth) bestLen = len; else break; } if (bestLen == 0) { // At least show one character to make progress bestLen = 1; } lines.emplace_back(remaining.substr(0, bestLen)); remaining.erase(0, bestLen); } } const bool scrolledUp = ((int)lines.size() > maxLines); int caretX = textX; int caretY = innerTop; // Leave a small top gap to render '...' without replacing the first line const int topInset = 2; const int lineStep = std::max(1, inputLineH - 1); // slightly tighter than font height int lineY = innerTop + topInset; if (scrolledUp) { // Draw three small dots centered horizontally, vertically at the midpoint of the gap // between the inner top and the first line's top baseline. This avoids using a tall glyph. const int firstLineTop = lineY; // baseline top for the first visible line const int gapMidY = innerTop + (firstLineTop - innerTop) / 2 + 1; // shift down 1px as requested const int centerX = boxX + boxWidth / 2; const int dotSpacing = 3; // px between dots const int dotSize = 1; // small square dot display->fillRect(centerX - dotSpacing, gapMidY, dotSize, dotSize); display->fillRect(centerX, gapMidY, dotSize, dotSize); display->fillRect(centerX + dotSpacing, gapMidY, dotSize, dotSize); } // How many lines fit with our top inset and tighter step const int linesCapacity = std::max(1, (innerBottom - lineY + 1) / lineStep); const int linesToShow = std::min((int)lines.size(), linesCapacity); const int startIndex = scrolledUp ? ((int)lines.size() - linesToShow) : 0; for (int i = 0; i < linesToShow; ++i) { const std::string &chunk = lines[startIndex + i]; display->drawString(textX, lineY, chunk.c_str()); caretX = textX + display->getStringWidth(chunk.c_str()); caretY = lineY; lineY += lineStep; } // Draw caret at end of the last visible line int caretPadY = 2; if (boxHeight >= inputLineH + 4) caretPadY = 3; int cursorTop = caretY + caretPadY; // Use lineStep so caret height matches the row spacing int cursorH = lineStep - caretPadY * 2; if (cursorH < 1) cursorH = 1; // Clamp vertical bounds to stay inside the inner rect if (cursorTop < innerTop) cursorTop = innerTop; if (cursorTop + cursorH - 1 > innerBottom) cursorH = innerBottom - cursorTop + 1; if (cursorH < 1) cursorH = 1; // Only draw if cursor is inside inner bounds if (caretX >= innerLeft && caretX <= innerRight) { display->drawVerticalLine(caretX, cursorTop, cursorH); } } else { std::string displayText = inputText; int textW = display->getStringWidth(displayText.c_str()); std::string scrolled = displayText; if (textW > maxTextWidth) { // Trim from the left until it fits while (textW > maxTextWidth && !scrolled.empty()) { scrolled.erase(0, 1); textW = display->getStringWidth(scrolled.c_str()); } // Add leading ellipsis and ensure it still fits if (scrolled != displayText) { scrolled = "..." + scrolled; textW = display->getStringWidth(scrolled.c_str()); // If adding ellipsis causes overflow, trim more after the ellipsis while (textW > maxTextWidth && scrolled.size() > 3) { scrolled.erase(3, 1); // remove chars after the ellipsis textW = display->getStringWidth(scrolled.c_str()); } } } else { // Keep textW in sync with what we draw textW = display->getStringWidth(scrolled.c_str()); } int textY; if (screenHeight <= 64) { textY = boxY + (boxHeight - inputLineH) / 2; } else { const int innerTop = boxY + 1; const int innerBottom = boxY + boxHeight - 2; // Center text vertically within inner box for single-line, then clamp so it never overlaps borders int innerH = innerBottom - innerTop + 1; textY = innerTop + std::max(0, (innerH - inputLineH) / 2); // Clamp fully inside the inner rect if (textY < innerTop) textY = innerTop; int maxTop = innerBottom - inputLineH + 1; if (textY > maxTop) textY = maxTop; } if (!scrolled.empty()) { display->drawString(textX, textY, scrolled.c_str()); } int cursorX = textX + textW; if (screenHeight > 64) { const int innerRight = boxX + boxWidth - 2; if (cursorX > innerRight) cursorX = innerRight; } int cursorTop, cursorH; if (screenHeight <= 64) { cursorH = 10; cursorTop = boxY + (boxHeight - cursorH) / 2; } else { const int innerLeft = boxX + 1; const int innerRight = boxX + boxWidth - 2; const int innerTop = boxY + 1; const int innerBottom = boxY + boxHeight - 2; cursorTop = boxY + 2; cursorH = boxHeight - 4; if (cursorH < 1) cursorH = 1; if (cursorTop < innerTop) cursorTop = innerTop; if (cursorTop + cursorH - 1 > innerBottom) cursorH = innerBottom - cursorTop + 1; if (cursorH < 1) cursorH = 1; if (cursorX < innerLeft || cursorX > innerRight) return; } display->drawVerticalLine(cursorX, cursorTop, cursorH); } } void VirtualKeyboard::drawKey(OLEDDisplay *display, const VirtualKey &key, bool selected, int16_t x, int16_t y, uint8_t width, uint8_t height, bool isLastCol) { // Draw key content display->setFont(FONT_SMALL); const int fontH = FONT_HEIGHT_SMALL; // Build label and metrics first std::string keyText; if (key.type == VK_BACKSPACE || key.type == VK_ENTER || key.type == VK_SPACE || key.type == VK_ESC) { // Keep literal text labels for the action keys on the rightmost column keyText = (key.type == VK_BACKSPACE) ? "BACK" : (key.type == VK_ENTER) ? "ENTER" : (key.type == VK_SPACE) ? "SPACE" : (key.type == VK_ESC) ? "ESC" : ""; } else { char c = getCharForKey(key, false); if (c >= 'a' && c <= 'z') { c = c - 'a' + 'A'; } keyText = (key.character == ' ' || key.character == '_') ? "_" : std::string(1, c); // Show the common "/" pairing next to "?" like on a real keyboard if (key.type == VK_CHAR && key.character == '?') { keyText = "?/"; } } int textWidth = display->getStringWidth(keyText.c_str()); // Label alignment // - Rightmost action column: right-align text with a small right padding (~2px) so it hugs screen edge neatly. // - Other keys: center horizontally; use ceil-style rounding to avoid appearing left-biased on odd widths. int textX; if (isLastCol) { const int rightPad = 1; textX = x + width - textWidth - rightPad; if (textX < x) textX = x; // guard } else { if (display->getHeight() <= 64 && (key.character >= '0' && key.character <= '9')) { textX = x + (width - textWidth + 1) / 2; } else { textX = x + (width - textWidth) / 2; } } int contentTop = y; int contentH = height; if (selected) { display->setColor(WHITE); bool isAction = (key.type == VK_BACKSPACE || key.type == VK_ENTER || key.type == VK_SPACE || key.type == VK_ESC); if (display->getHeight() <= 64 && !isAction) { display->fillRect(x, y, width, height); } else if (isAction) { const int padX = 1; const int padY = 2; int hlW = textWidth + padX * 2; int hlX = textX - padX; if (hlX < x) { hlW -= (x - hlX); hlX = x; } int maxW = (x + width) - hlX; if (hlW > maxW) hlW = maxW; if (hlW < 1) hlW = 1; int hlH = std::min(fontH + padY * 2, (int)height); int hlY = y + (height - hlH) / 2; display->fillRect(hlX, hlY, hlW, hlH); contentTop = hlY; contentH = hlH; } else { display->fillRect(x, y, width, height); } display->setColor(BLACK); } else { display->setColor(WHITE); } int centeredTextY; if (display->getHeight() <= 64) { centeredTextY = y + (height - fontH) / 2; } else { centeredTextY = contentTop + (contentH - fontH) / 2; } if (display->getHeight() > 64) { if (centeredTextY < contentTop) centeredTextY = contentTop; if (centeredTextY + fontH > contentTop + contentH) centeredTextY = std::max(contentTop, contentTop + contentH - fontH); } if (display->getHeight() <= 64 && keyText.size() == 1) { char ch = keyText[0]; if (ch == '.' || ch == ',' || ch == ';') { centeredTextY -= 1; } } #ifdef MUZI_BASE // Correct issue with character vertical position on MUZI_BASE centeredTextY -= 2; #endif display->drawString(textX, centeredTextY, keyText.c_str()); } char VirtualKeyboard::getCharForKey(const VirtualKey &key, bool isLongPress) { if (key.type != VK_CHAR) { return key.character; } char c = key.character; // Long-press: letters become uppercase; for "?" provide "/" like a typical keyboard if (isLongPress) { if (c >= 'a' && c <= 'z') { c = (char)(c - 'a' + 'A'); } else if (c == '?') { c = '/'; } } return c; } void VirtualKeyboard::moveCursorDelta(int dRow, int dCol) { resetTimeout(); // wrap around rows and cols in the 4x11 grid int r = (int)cursorRow + dRow; int c = (int)cursorCol + dCol; if (r < 0) r = KEYBOARD_ROWS - 1; else if (r >= KEYBOARD_ROWS) r = 0; if (c < 0) c = KEYBOARD_COLS - 1; else if (c >= KEYBOARD_COLS) c = 0; cursorRow = (uint8_t)r; cursorCol = (uint8_t)c; } void VirtualKeyboard::moveCursorUp() { moveCursorDelta(-1, 0); } void VirtualKeyboard::moveCursorDown() { moveCursorDelta(1, 0); } void VirtualKeyboard::moveCursorLeft() { resetTimeout(); if (cursorCol > 0) { cursorCol--; } else { if (cursorRow > 0) { cursorRow--; cursorCol = KEYBOARD_COLS - 1; } else { cursorRow = KEYBOARD_ROWS - 1; cursorCol = KEYBOARD_COLS - 1; } } } void VirtualKeyboard::moveCursorRight() { resetTimeout(); if (cursorCol < KEYBOARD_COLS - 1) { cursorCol++; } else { if (cursorRow < KEYBOARD_ROWS - 1) { cursorRow++; cursorCol = 0; } else { cursorRow = 0; cursorCol = 0; } } } void VirtualKeyboard::handlePress() { resetTimeout(); // Reset timeout on any input activity const VirtualKey &key = keyboard[cursorRow][cursorCol]; // Don't handle press if the key is empty (but allow special keys) if (key.character == 0 && key.type == VK_CHAR) { return; } // For character keys, insert lowercase character if (key.type == VK_CHAR) { insertCharacter(getCharForKey(key, false)); // false = lowercase/normal char return; } // Handle non-character keys immediately switch (key.type) { case VK_BACKSPACE: deleteCharacter(); break; case VK_ENTER: submitText(); break; case VK_SPACE: insertCharacter(' '); break; case VK_ESC: if (onTextEntered) { std::function callback = onTextEntered; onTextEntered = nullptr; inputText = ""; callback(""); } return; default: break; } } void VirtualKeyboard::handleLongPress() { resetTimeout(); // Reset timeout on any input activity const VirtualKey &key = keyboard[cursorRow][cursorCol]; // Don't handle press if the key is empty (but allow special keys) if (key.character == 0 && key.type == VK_CHAR) { return; } // For character keys, insert uppercase/alternate character if (key.type == VK_CHAR) { insertCharacter(getCharForKey(key, true)); // true = uppercase/alternate char return; } switch (key.type) { case VK_BACKSPACE: // One-shot: delete up to 5 characters on long press for (int i = 0; i < 5; ++i) { if (inputText.empty()) break; deleteCharacter(); } break; case VK_ENTER: submitText(); break; case VK_SPACE: insertCharacter(' '); break; case VK_ESC: if (onTextEntered) { onTextEntered(""); } break; default: break; } } void VirtualKeyboard::insertCharacter(char c) { if (inputText.length() < 160) { // Reasonable text length limit inputText += c; } } void VirtualKeyboard::deleteCharacter() { if (!inputText.empty()) { inputText.pop_back(); } } void VirtualKeyboard::submitText() { LOG_INFO("Virtual keyboard: submitting text '%s'", inputText.c_str()); // Only submit if text is not empty if (!inputText.empty() && onTextEntered) { // Store callback and text to submit before clearing callback std::function callback = onTextEntered; std::string textToSubmit = inputText; onTextEntered = nullptr; // Don't clear inputText here - let the calling module handle cleanup // inputText = ""; // Removed: keep text visible until module cleans up callback(textToSubmit); } else if (inputText.empty()) { // For empty text, just ignore the submission - don't clear callback // This keeps the virtual keyboard responsive for further input LOG_INFO("Virtual keyboard: empty text submitted, ignoring - keyboard remains active"); } else { // No callback available if (screen) { screen->setFrames(graphics::Screen::FOCUS_PRESERVE); } } } void VirtualKeyboard::setInputText(const std::string &text) { inputText = text; } std::string VirtualKeyboard::getInputText() const { return inputText; } void VirtualKeyboard::setHeader(const std::string &header) { headerText = header; } void VirtualKeyboard::setCallback(std::function callback) { onTextEntered = callback; } void VirtualKeyboard::resetTimeout() { lastActivityTime = millis(); } bool VirtualKeyboard::isTimedOut() const { return (millis() - lastActivityTime) > TIMEOUT_MS; } } // namespace graphics #endif ================================================ FILE: src/graphics/VirtualKeyboard.h ================================================ #pragma once #include "configuration.h" #include #include #include namespace graphics { enum VirtualKeyType { VK_CHAR, VK_BACKSPACE, VK_ENTER, VK_SHIFT, VK_ESC, VK_SPACE }; struct VirtualKey { char character; VirtualKeyType type; uint8_t x; uint8_t y; uint8_t width; uint8_t height; }; class VirtualKeyboard { public: VirtualKeyboard(); ~VirtualKeyboard(); void draw(OLEDDisplay *display, int16_t offsetX, int16_t offsetY); void setInputText(const std::string &text); std::string getInputText() const; void setHeader(const std::string &header); void setCallback(std::function callback); // Navigation methods for encoder input void moveCursorUp(); void moveCursorDown(); void moveCursorLeft(); void moveCursorRight(); void handlePress(); void handleLongPress(); // Timeout management void resetTimeout(); bool isTimedOut() const; private: static const uint8_t KEYBOARD_ROWS = 4; static const uint8_t KEYBOARD_COLS = 11; static const uint8_t KEY_WIDTH = 9; static const uint8_t KEY_HEIGHT = 9; // Compressed to fit 4 rows on 64px displays static const uint8_t KEYBOARD_START_Y = 26; // Start just below input box bottom VirtualKey keyboard[KEYBOARD_ROWS][KEYBOARD_COLS]; std::string inputText; std::string headerText; std::function onTextEntered; uint8_t cursorRow; uint8_t cursorCol; // Timeout management for auto-exit uint32_t lastActivityTime; static const uint32_t TIMEOUT_MS = 60000; // 1 minute timeout void initializeKeyboard(); void drawKey(OLEDDisplay *display, const VirtualKey &key, bool selected, int16_t x, int16_t y, uint8_t w, uint8_t h, bool isLastCol); void drawInputArea(OLEDDisplay *display, int16_t offsetX, int16_t offsetY, int16_t keyboardStartY); // Unified cursor movement helper void moveCursorDelta(int dRow, int dCol); char getCharForKey(const VirtualKey &key, bool isLongPress = false); void insertCharacter(char c); void deleteCharacter(); void submitText(); }; } // namespace graphics ================================================ FILE: src/graphics/draw/ClockRenderer.cpp ================================================ #include "configuration.h" #if HAS_SCREEN #include "ClockRenderer.h" #include "gps/RTC.h" #include "graphics/ScreenFonts.h" #include "graphics/SharedUIDisplay.h" #include "graphics/draw/UIRenderer.h" #include "graphics/images.h" #include "main.h" #if !MESHTASTIC_EXCLUDE_BLUETOOTH #include "nimble/NimbleBluetooth.h" #endif namespace graphics { namespace ClockRenderer { // Segment bitmaps for numerals 0-9 stored in flash to save RAM. // Each row is a digit, each column is a segment state (1 = on, 0 = off). // Segment layout reference: // // ___1___ // 6 | | 2 // |_7___| // 5 | | 3 // |___4_| // // Segment order: [1, 2, 3, 4, 5, 6, 7] // static const uint8_t PROGMEM digitSegments[10][7] = { {1, 1, 1, 1, 1, 1, 0}, // 0 {0, 1, 1, 0, 0, 0, 0}, // 1 {1, 1, 0, 1, 1, 0, 1}, // 2 {1, 1, 1, 1, 0, 0, 1}, // 3 {0, 1, 1, 0, 0, 1, 1}, // 4 {1, 0, 1, 1, 0, 1, 1}, // 5 {1, 0, 1, 1, 1, 1, 1}, // 6 {1, 1, 1, 0, 0, 1, 0}, // 7 {1, 1, 1, 1, 1, 1, 1}, // 8 {1, 1, 1, 1, 0, 1, 1} // 9 }; void drawSegmentedDisplayColon(OLEDDisplay *display, int x, int y, float scale) { uint16_t segmentWidth = SEGMENT_WIDTH * scale; uint16_t segmentHeight = SEGMENT_HEIGHT * scale; uint16_t cellHeight = (segmentWidth * 2) + (segmentHeight * 3) + 8; uint16_t topAndBottomX = x + static_cast(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 drawSegmentedDisplayCharacter(OLEDDisplay *display, int x, int y, uint8_t number, float scale) { // Read 7-segment pattern for the digit from flash uint8_t seg[7]; for (uint8_t i = 0; i < 7; i++) { seg[i] = pgm_read_byte(&digitSegments[number][i]); } uint16_t segmentWidth = SEGMENT_WIDTH * scale; uint16_t segmentHeight = SEGMENT_HEIGHT * scale; // Precompute segment positions uint16_t segmentOneX = x + segmentHeight + 2; uint16_t segmentOneY = y; uint16_t segmentTwoX = segmentOneX + segmentWidth + 2; uint16_t segmentTwoY = segmentOneY + segmentHeight + 2; uint16_t segmentThreeX = segmentTwoX; uint16_t segmentThreeY = segmentTwoY + segmentWidth + 2 + segmentHeight + 2; uint16_t segmentFourX = segmentOneX; uint16_t segmentFourY = segmentThreeY + segmentWidth + 2; uint16_t segmentFiveX = x; uint16_t segmentFiveY = segmentThreeY; uint16_t segmentSixX = x; uint16_t segmentSixY = segmentTwoY; uint16_t segmentSevenX = segmentOneX; uint16_t segmentSevenY = segmentTwoY + segmentWidth + 2; // Draw only the active segments if (seg[0]) drawHorizontalSegment(display, segmentOneX, segmentOneY, segmentWidth, segmentHeight); if (seg[1]) drawVerticalSegment(display, segmentTwoX, segmentTwoY, segmentWidth, segmentHeight); if (seg[2]) drawVerticalSegment(display, segmentThreeX, segmentThreeY, segmentWidth, segmentHeight); if (seg[3]) drawHorizontalSegment(display, segmentFourX, segmentFourY, segmentWidth, segmentHeight); if (seg[4]) drawVerticalSegment(display, segmentFiveX, segmentFiveY, segmentWidth, segmentHeight); if (seg[5]) drawVerticalSegment(display, segmentSixX, segmentSixY, segmentWidth, segmentHeight); if (seg[6]) drawHorizontalSegment(display, segmentSevenX, segmentSevenY, segmentWidth, segmentHeight); } void drawHorizontalSegment(OLEDDisplay *display, int x, int y, int width, int height) { int halfHeight = height / 2; // draw central rectangle display->fillRect(x, y, width, height); // draw end triangles display->fillTriangle(x, y, x, y + height - 1, x - halfHeight, y + halfHeight); display->fillTriangle(x + width, y, x + width + halfHeight, y + halfHeight, x + width, y + height - 1); } void drawVerticalSegment(OLEDDisplay *display, int x, int y, int width, int height) { int halfHeight = height / 2; // draw central rectangle display->fillRect(x, y, height, width); // draw end triangles display->fillTriangle(x + halfHeight, y - halfHeight, x + height - 1, y, x, y); display->fillTriangle(x, y + width, x + height - 1, y + width, x + halfHeight, y + width + halfHeight); } void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { display->clear(); display->setTextAlignment(TEXT_ALIGN_LEFT); // === Set Title, Blank for Clock const char *titleStr = ""; // === Header === graphics::drawCommonHeader(display, x, y, titleStr, true, true); uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone char timeString[16]; int hour = 0; int minute = 0; int second = 0; if (rtc_sec > 0) { long hms = rtc_sec % SEC_PER_DAY; hms = (hms + SEC_PER_DAY) % SEC_PER_DAY; hour = hms / SEC_PER_HOUR; minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN; second = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN } bool isPM = hour >= 12; if (config.display.use_12h_clock) { hour %= 12; if (hour == 0) { hour = 12; } snprintf(timeString, sizeof(timeString), "%d:%02d", hour, minute); } else { snprintf(timeString, sizeof(timeString), "%02d:%02d", hour, minute); } // Format seconds string char secondString[8]; snprintf(secondString, sizeof(secondString), "%02d", second); static bool scaleInitialized = false; static float scale = 0.75f; static float segmentWidth = SEGMENT_WIDTH * 0.75f; static float segmentHeight = SEGMENT_HEIGHT * 0.75f; if (!scaleInitialized) { float screenwidth_target_ratio = 0.80f; // Target 80% of display width (adjustable) float max_scale = 3.5f; // Safety limit to avoid runaway scaling float step = 0.05f; // Step increment per iteration float target_width = display->getWidth() * screenwidth_target_ratio; float target_height = display->getHeight() - ((currentResolution == ScreenResolution::High) ? 46 : 33); // Be careful adjusting this number, we have to account for header and the text under the time float calculated_width_size = 0.0f; float calculated_height_size = 0.0f; while (true) { segmentWidth = SEGMENT_WIDTH * scale; segmentHeight = SEGMENT_HEIGHT * scale; calculated_width_size = segmentHeight + ((segmentWidth + (segmentHeight * 2) + 4) * 4); calculated_height_size = segmentHeight + ((segmentHeight + (segmentHeight * 2) + 4) * 2); if (calculated_width_size >= target_width || calculated_height_size >= target_height || scale >= max_scale) { break; } scale += step; } // If we overshot width, back off one step and recompute segment sizes if (calculated_width_size > target_width || calculated_height_size > target_height) { scale -= step; segmentWidth = SEGMENT_WIDTH * scale; segmentHeight = SEGMENT_HEIGHT * scale; } scaleInitialized = true; } // calculate hours:minutes string width size_t len = strlen(timeString); uint16_t timeStringWidth = len * 5; for (size_t i = 0; i < len; i++) { char character = timeString[i]; if (character == ':') { timeStringWidth += segmentHeight; } else { timeStringWidth += segmentWidth + (segmentHeight * 2) + 4; } } uint16_t hourMinuteTextX = (display->getWidth() / 2) - (timeStringWidth / 2); uint16_t startingHourMinuteTextX = hourMinuteTextX; uint16_t hourMinuteTextY = (display->getHeight() / 2) - (((segmentWidth * 2) + (segmentHeight * 3) + 8) / 2) + 2; // iterate over characters in hours:minutes string and draw segmented characters for (size_t i = 0; i < len; i++) { char character = timeString[i]; if (character == ':') { drawSegmentedDisplayColon(display, hourMinuteTextX, hourMinuteTextY, scale); hourMinuteTextX += segmentHeight + 6; if (scale >= 2.0f) { hourMinuteTextX += (uint16_t)(4.5f * scale); } } else { drawSegmentedDisplayCharacter(display, hourMinuteTextX, hourMinuteTextY, character - '0', scale); hourMinuteTextX += segmentWidth + (segmentHeight * 2) + 4; } hourMinuteTextX += 5; } // draw seconds string + AM/PM display->setFont(FONT_SMALL); int xOffset = -1; if (currentResolution == ScreenResolution::High) { xOffset = 0; } if (hour >= 10) { if (currentResolution == ScreenResolution::High) { xOffset += 32; } else { xOffset += 18; } } if (config.display.use_12h_clock) { display->drawString(startingHourMinuteTextX + xOffset, (display->getHeight() - hourMinuteTextY) - 1, isPM ? "pm" : "am"); } #ifndef USE_EINK xOffset = (currentResolution == ScreenResolution::High) ? 18 : 10; if (scale >= 2.0f) { xOffset -= (int)(4.5f * scale); } display->drawString(startingHourMinuteTextX + timeStringWidth - xOffset, (display->getHeight() - hourMinuteTextY) - 1, secondString); #endif graphics::drawCommonFooter(display, x, y); } // Draw an analog clock void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { display->setTextAlignment(TEXT_ALIGN_LEFT); // === Set Title, Blank for Clock const char *titleStr = ""; // === Header === graphics::drawCommonHeader(display, x, y, titleStr, true, true); // clock face center coordinates int16_t centerX = display->getWidth() / 2; int16_t centerY = display->getHeight() / 2; // clock face radius int16_t radius = (std::min(display->getWidth(), display->getHeight()) / 2) * 0.9; #ifdef T_WATCH_S3 radius = (display->getWidth() / 2) * 0.8; #endif // 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; double secondsTickMarkInnerNoonY = noonY + ((currentResolution == ScreenResolution::High) ? 8 : 4); double hoursTickMarkInnerNoonY = noonY + ((currentResolution == ScreenResolution::High) ? 16 : 6); // minute hand y coordinate int16_t minuteHandNoonY = secondsTickMarkInnerNoonY + 4; // hour string y coordinate int16_t hourStringNoonY = minuteHandNoonY + 18; // hour hand radius and y coordinate int16_t hourHandRadius = radius * 0.35; if (currentResolution == ScreenResolution::High) { hourHandRadius = radius * 0.55; } int16_t hourHandNoonY = centerY - hourHandRadius; display->setColor(OLEDDISPLAY_COLOR::WHITE); display->drawCircle(centerX, centerY, radius); uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone if (rtc_sec > 0) { int hour, minute, second; decomposeTime(rtc_sec, hour, minute, second); if (config.display.use_12h_clock) { bool isPM = hour >= 12; display->setFont(FONT_SMALL); int yOffset = (currentResolution == ScreenResolution::High) ? 1 : 0; #ifdef USE_EINK yOffset += 3; #endif display->drawString(centerX - (display->getStringWidth(isPM ? "pm" : "am") / 2), centerY + yOffset, isPM ? "pm" : "am"); } hour %= 12; if (hour == 0) hour = 12; 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; #ifdef T_WATCH_S3 // draw hour number display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt); #else #ifdef USE_EINK if (currentResolution == ScreenResolution::High) { // draw hour number display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt); } #else if (currentResolution == ScreenResolution::High && (hourInt == 3 || hourInt == 6 || hourInt == 9 || hourInt == 12)) { // draw hour number display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt); } #endif #endif } if (angle % degreesPerMinuteOrSecond == 0) { double startX = sineAngleInRadians * (secondsTickMarkInnerNoonY - centerY) + noonX; double startY = cosineAngleInRadians * (secondsTickMarkInnerNoonY - centerY) + centerY; if (currentResolution == ScreenResolution::High) { // draw minute tick mark display->drawLine(startX, startY, endX, endY); } } } // draw hour hand display->drawLine(centerX, centerY, hourX, hourY); // draw minute hand display->drawLine(centerX, centerY, minuteX, minuteY); #ifndef USE_EINK // draw second hand display->drawLine(centerX, centerY, secondX, secondY); #endif } graphics::drawCommonFooter(display, x, y); } } // namespace ClockRenderer } // namespace graphics #endif ================================================ FILE: src/graphics/draw/ClockRenderer.h ================================================ #pragma once #include #include namespace graphics { /// Forward declarations class Screen; namespace ClockRenderer { // Clock frame functions void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); // Segmented display functions void drawSegmentedDisplayCharacter(OLEDDisplay *display, int x, int y, uint8_t number, float scale = 1); void drawSegmentedDisplayColon(OLEDDisplay *display, int x, int y, float scale = 1); void drawHorizontalSegment(OLEDDisplay *display, int x, int y, int width, int height); void drawVerticalSegment(OLEDDisplay *display, int x, int y, int width, int height); // UI elements for clock displays // void drawWatchFaceToggleButton(OLEDDisplay *display, int16_t x, int16_t y, bool digitalMode = true, float scale = 1); } // namespace ClockRenderer } // namespace graphics ================================================ FILE: src/graphics/draw/CompassRenderer.cpp ================================================ #include "configuration.h" #if HAS_SCREEN #include "CompassRenderer.h" #include "NodeDB.h" #include "UIRenderer.h" #include "configuration.h" #include "gps/GeoCoord.h" #include "graphics/ScreenFonts.h" #include "graphics/SharedUIDisplay.h" #include namespace graphics { namespace CompassRenderer { // Point helper class for compass calculations struct Point { float x, y; Point(float x, float y) : x(x), y(y) {} void rotate(float angle) { float cos_a = cos(angle); float sin_a = sin(angle); float new_x = x * cos_a - y * sin_a; float new_y = x * sin_a + y * cos_a; x = new_x; y = new_y; } void scale(float factor) { x *= factor; y *= factor; } void translate(float dx, float dy) { x += dx; y += dy; } }; void drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t compassY, float myHeading, int16_t radius) { // Show the compass heading (not implemented in original) // This could draw a "N" indicator or north arrow // For now, we'll draw a simple north indicator // const float radius = 17.0f; if (currentResolution == ScreenResolution::High) { radius += 4; } Point north(0, -radius); if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING) north.rotate(-myHeading); north.translate(compassX, compassY); display->setFont(FONT_SMALL); display->setTextAlignment(TEXT_ALIGN_CENTER); display->setColor(BLACK); if (currentResolution == ScreenResolution::High) { display->fillRect(north.x - 8, north.y - 1, display->getStringWidth("N") + 3, FONT_HEIGHT_SMALL - 6); } else { display->fillRect(north.x - 4, north.y - 1, display->getStringWidth("N") + 2, FONT_HEIGHT_SMALL - 6); } display->setColor(WHITE); display->drawString(north.x, north.y - 3, "N"); } void drawNodeHeading(OLEDDisplay *display, int16_t compassX, int16_t compassY, uint16_t compassDiam, float headingRadian) { Point tip(0.0f, -0.5f), tail(0.0f, 0.35f); // pointing up initially float arrowOffsetX = 0.14f, arrowOffsetY = 0.9f; Point leftArrow(tip.x - arrowOffsetX, tip.y + arrowOffsetY), rightArrow(tip.x + arrowOffsetX, tip.y + arrowOffsetY); 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); } #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); } void drawArrowToNode(OLEDDisplay *display, int16_t x, int16_t y, int16_t size, float bearing) { float radians = bearing * DEG_TO_RAD; Point tip(0, -size / 2); Point left(-size / 6, size / 4); Point right(size / 6, size / 4); Point tail(0, size / 4.5); tip.rotate(radians); left.rotate(radians); right.rotate(radians); tail.rotate(radians); tip.translate(x, y); left.translate(x, y); right.translate(x, y); tail.translate(x, y); display->fillTriangle(tip.x, tip.y, left.x, left.y, tail.x, tail.y); display->fillTriangle(tip.x, tip.y, right.x, right.y, tail.x, tail.y); } float estimatedHeading(double lat, double lon) { // Simple magnetic declination estimation // This is a very basic implementation - the original might be more sophisticated return 0.0f; // Return 0 for now, indicating no heading available } uint16_t getCompassDiam(uint32_t displayWidth, uint32_t displayHeight) { // Calculate appropriate compass diameter based on display size uint16_t minDimension = (displayWidth < displayHeight) ? displayWidth : displayHeight; uint16_t maxDiam = minDimension / 3; // Use 1/3 of the smaller dimension // Ensure minimum and maximum bounds if (maxDiam < 16) maxDiam = 16; if (maxDiam > 64) maxDiam = 64; return maxDiam; } } // namespace CompassRenderer } // namespace graphics #endif ================================================ FILE: src/graphics/draw/CompassRenderer.h ================================================ #pragma once #include "graphics/Screen.h" #include "mesh/generated/meshtastic/mesh.pb.h" #include #include namespace graphics { /// Forward declarations class Screen; /** * @brief Compass and navigation drawing functions * * Contains all functions related to drawing compass elements, headings, * navigation arrows, and location-based UI components. */ namespace CompassRenderer { // Compass drawing functions void drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t compassY, float myHeading, int16_t radius); void drawNodeHeading(OLEDDisplay *display, int16_t compassX, int16_t compassY, uint16_t compassDiam, float headingRadian); void drawArrowToNode(OLEDDisplay *display, int16_t x, int16_t y, int16_t size, float bearing); // Navigation and location functions float estimatedHeading(double lat, double lon); uint16_t getCompassDiam(uint32_t displayWidth, uint32_t displayHeight); } // namespace CompassRenderer } // namespace graphics ================================================ FILE: src/graphics/draw/DebugRenderer.cpp ================================================ #include "configuration.h" #if HAS_SCREEN #include "../Screen.h" #include "DebugRenderer.h" #include "FSCommon.h" #include "MeshService.h" #include "NodeDB.h" #include "Throttle.h" #include "UIRenderer.h" #include "airtime.h" #include "gps/RTC.h" #include "graphics/ScreenFonts.h" #include "graphics/SharedUIDisplay.h" #include "graphics/TimeFormatters.h" #include "graphics/images.h" #include "main.h" #include "mesh/Channels.h" #include "mesh/generated/meshtastic/deviceonly.pb.h" #include "sleep.h" #if HAS_WIFI && !defined(ARCH_PORTDUINO) #include "mesh/wifi/WiFiAPClient.h" #include #ifdef ARCH_ESP32 #include "mesh/wifi/WiFiAPClient.h" #endif #endif #ifdef ARCH_ESP32 #include "modules/StoreForwardModule.h" #endif #include #include #include using namespace meshtastic; // External variables extern graphics::Screen *screen; extern PowerStatus *powerStatus; extern NodeStatus *nodeStatus; extern GPSStatus *gpsStatus; extern Channels channels; extern AirTime *airTime; // External functions from Screen.cpp extern bool heartbeat; #ifdef ARCH_ESP32 extern StoreForwardModule *storeForwardModule; #endif namespace graphics { namespace DebugRenderer { void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { display->setFont(FONT_SMALL); // The coordinates define the left starting point of the text display->setTextAlignment(TEXT_ALIGN_LEFT); if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { display->fillRect(0 + x, 0 + y, x + display->getWidth(), y + FONT_HEIGHT_SMALL); display->setColor(BLACK); } char channelStr[20]; snprintf(channelStr, sizeof(channelStr), "#%s", channels.getName(channels.getPrimaryIndex())); // Display nodes status if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) { UIRenderer::drawNodes(display, x + (SCREEN_WIDTH * 0.25), y + 2, nodeStatus); } else { UIRenderer::drawNodes(display, x + (SCREEN_WIDTH * 0.25), y + 3, nodeStatus); } #if HAS_GPS // Display GPS status if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) { UIRenderer::drawGpsPowerStatus(display, x, y + 2, gpsStatus); } else { if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) { UIRenderer::drawGps(display, x + (SCREEN_WIDTH * 0.63), y + 2, gpsStatus); } else { UIRenderer::drawGps(display, x + (SCREEN_WIDTH * 0.63), y + 3, gpsStatus); } } #endif display->setColor(WHITE); // Draw the channel name display->drawString(x, y + FONT_HEIGHT_SMALL, channelStr); // Draw our hardware ID to assist with bluetooth pairing. Either prefix with Info or S&F Logo if (moduleConfig.store_forward.enabled) { #ifdef ARCH_ESP32 if (!Throttle::isWithinTimespanMs(storeForwardModule->lastHeartbeat, (storeForwardModule->heartbeatInterval * 1200))) { // no heartbeat, overlap a bit #if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS) || \ defined(HACKADAY_COMMUNICATOR) || defined(USE_ST7796) || ARCH_PORTDUINO) && \ !defined(DISPLAY_FORCE_SMALL_FONTS) display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 12, 8, imgQuestionL1); display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(screen->ourId), y + 11 + FONT_HEIGHT_SMALL, 12, 8, imgQuestionL2); #else display->drawFastImage(x + SCREEN_WIDTH - 10 - display->getStringWidth(screen->ourId), y + 2 + FONT_HEIGHT_SMALL, 8, 8, imgQuestion); #endif } else { #if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS) || \ defined(HACKADAY_COMMUNICATOR) || defined(USE_ST7796)) && \ !defined(DISPLAY_FORCE_SMALL_FONTS) display->drawFastImage(x + SCREEN_WIDTH - 18 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 16, 8, imgSFL1); display->drawFastImage(x + SCREEN_WIDTH - 18 - display->getStringWidth(screen->ourId), y + 11 + FONT_HEIGHT_SMALL, 16, 8, imgSFL2); #else display->drawFastImage(x + SCREEN_WIDTH - 13 - display->getStringWidth(screen->ourId), y + 2 + FONT_HEIGHT_SMALL, 11, 8, imgSF); #endif } #endif } else { // TODO: Raspberry Pi supports more than just the one screen size #if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS) || \ defined(HACKADAY_COMMUNICATOR) || defined(USE_ST7796) || ARCH_PORTDUINO) && \ !defined(DISPLAY_FORCE_SMALL_FONTS) display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 12, 8, imgInfoL1); display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(screen->ourId), y + 11 + FONT_HEIGHT_SMALL, 12, 8, imgInfoL2); #else display->drawFastImage(x + SCREEN_WIDTH - 10 - display->getStringWidth(screen->ourId), y + 2 + FONT_HEIGHT_SMALL, 8, 8, imgInfo); #endif } display->drawString(x + SCREEN_WIDTH - display->getStringWidth(screen->ourId), y + FONT_HEIGHT_SMALL, screen->ourId); // 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 } // **************************** // * WiFi Screen * // **************************** void drawFrameWiFi(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { #if HAS_WIFI && !defined(ARCH_PORTDUINO) display->clear(); display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); int line = 1; // === Set Title const char *titleStr = "WiFi"; // === Header === graphics::drawCommonHeader(display, x, y, titleStr); const char *wifiName = config.network.wifi_ssid; if (WiFi.status() != WL_CONNECTED) { display->drawString(x, getTextPositions(display)[line++], "WiFi: Not Connected"); } else { display->drawString(x, getTextPositions(display)[line++], "WiFi: Connected"); char rssiStr[32]; snprintf(rssiStr, sizeof(rssiStr), "RSSI: %d", WiFi.RSSI()); display->drawString(x, getTextPositions(display)[line++], rssiStr); } /* - WL_CONNECTED: assigned when connected to a WiFi network; - WL_NO_SSID_AVAIL: assigned when no SSID are available; - WL_CONNECT_FAILED: assigned when the connection fails for all the attempts; - WL_CONNECTION_LOST: assigned when the connection is lost; - WL_DISCONNECTED: assigned when disconnected from a network; - WL_IDLE_STATUS: it is a temporary status assigned when WiFi.begin() is called and remains active until the number of attempts expires (resulting in WL_CONNECT_FAILED) or a connection is established (resulting in WL_CONNECTED); - WL_SCAN_COMPLETED: assigned when the scan networks is completed; - WL_NO_SHIELD: assigned when no WiFi shield is present; */ if (WiFi.status() == WL_CONNECTED) { char ipStr[64]; snprintf(ipStr, sizeof(ipStr), "IP: %s", WiFi.localIP().toString().c_str()); display->drawString(x, getTextPositions(display)[line++], ipStr); } else if (WiFi.status() == WL_NO_SSID_AVAIL) { display->drawString(x, getTextPositions(display)[line++], "SSID Not Found"); } else if (WiFi.status() == WL_CONNECTION_LOST) { display->drawString(x, getTextPositions(display)[line++], "Connection Lost"); } else if (WiFi.status() == WL_IDLE_STATUS) { display->drawString(x, getTextPositions(display)[line++], "Idle ... Reconnecting"); } else if (WiFi.status() == WL_CONNECT_FAILED) { display->drawString(x, getTextPositions(display)[line++], "Connection Failed"); } #ifdef ARCH_ESP32 else { // Codes: // https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/wifi.html#wi-fi-reason-code display->drawString(x, getTextPositions(display)[line++], WiFi.disconnectReasonName(static_cast(getWifiDisconnectReason()))); } #else else { char statusStr[32]; snprintf(statusStr, sizeof(statusStr), "Unknown status: %d", WiFi.status()); display->drawString(x, getTextPositions(display)[line++], statusStr); } #endif char ssidStr[64]; snprintf(ssidStr, sizeof(ssidStr), "SSID: %s", wifiName); display->drawString(x, getTextPositions(display)[line++], ssidStr); display->drawString(x, getTextPositions(display)[line++], "URL: http://meshtastic.local"); graphics::drawCommonFooter(display, x, y); /* 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 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, "USB"); if (config.display.heading_bold) display->drawString(x + 1, y, "USB"); } uint32_t currentMillis = millis(); uint32_t seconds = currentMillis / 1000; uint32_t minutes = seconds / 60; uint32_t hours = minutes / 60; uint32_t days = hours / 24; // currentMillis %= 1000; // seconds %= 60; // minutes %= 60; // hours %= 24; // Show uptime as days, hours, minutes OR seconds std::string uptime = UIRenderer::drawTimeDelta(days, hours, minutes, seconds); // Line 1 (Still) if (currentResolution != graphics::ScreenResolution::UltraLow) { display->drawString(x + SCREEN_WIDTH - display->getStringWidth(uptime.c_str()), y, uptime.c_str()); if (config.display.heading_bold) display->drawString(x - 1 + SCREEN_WIDTH - display->getStringWidth(uptime.c_str()), y, uptime.c_str()); display->setColor(WHITE); } // Setup string to assemble analogClock string std::string analogClock = ""; uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone if (rtc_sec > 0) { long hms = rtc_sec % SEC_PER_DAY; // hms += tz.tz_dsttime * SEC_PER_HOUR; // hms -= tz.tz_minuteswest * SEC_PER_MIN; // mod `hms` to ensure in positive range of [0...SEC_PER_DAY) hms = (hms + SEC_PER_DAY) % SEC_PER_DAY; // Tear apart hms into h:m:s int hour, min, sec; graphics::decomposeTime(rtc_sec, hour, min, sec); char timebuf[12]; if (config.display.use_12h_clock) { std::string meridiem = "am"; if (hour >= 12) { if (hour > 12) hour -= 12; meridiem = "pm"; } if (hour == 00) { hour = 12; } snprintf(timebuf, sizeof(timebuf), "%d:%02d:%02d%s", hour, min, sec, meridiem.c_str()); } else { snprintf(timebuf, sizeof(timebuf), "%02d:%02d:%02d", hour, min, sec); } analogClock += timebuf; } // Line 2 display->drawString(x, y + FONT_HEIGHT_SMALL * 1, analogClock.c_str()); // Display Channel Utilization char chUtil[13]; snprintf(chUtil, sizeof(chUtil), "ChUtil %2.0f%%", airTime->channelUtilizationPercent()); display->drawString(x + SCREEN_WIDTH - display->getStringWidth(chUtil), y + FONT_HEIGHT_SMALL * 1, chUtil); #if HAS_GPS if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) { // Line 3 if (uiconfig.gps_format != meshtastic_DeviceUIConfig_GpsCoordinateFormat_DMS) // if DMS then don't draw altitude UIRenderer::drawGpsAltitude(display, x, y + FONT_HEIGHT_SMALL * 2, gpsStatus); // Line 4 UIRenderer::drawGpsCoordinates(display, x, y + FONT_HEIGHT_SMALL * 3, gpsStatus); } else { UIRenderer::drawGpsPowerStatus(display, x, y + FONT_HEIGHT_SMALL * 2, gpsStatus); } #endif /* Display a heartbeat pixel that blinks every time the frame is redrawn */ #ifdef SHOW_REDRAWS if (heartbeat) display->setPixel(0, 0); heartbeat = !heartbeat; #endif } // Trampoline functions for DebugInfo class access void drawDebugInfoTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { drawFrame(display, state, x, y); } void drawDebugInfoSettingsTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { drawFrameSettings(display, state, x, y); } void drawDebugInfoWiFiTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { drawFrameWiFi(display, state, x, y); } // **************************** // * LoRa Focused Screen * // **************************** void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { display->clear(); display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); int line = 1; // === Set Title const char *titleStr = (currentResolution == ScreenResolution::High) ? "LoRa Info" : "LoRa"; // === Header === graphics::drawCommonHeader(display, x, y, titleStr); // === First Row: Region / BLE Name === graphics::UIRenderer::drawNodes(display, x, getTextPositions(display)[line] + 2, nodeStatus, 0, true, ""); uint8_t dmac[6]; char shortnameble[35]; getMacAddr(dmac); snprintf(screen->ourId, sizeof(screen->ourId), "%02x%02x", dmac[4], dmac[5]); if (currentResolution == ScreenResolution::UltraLow) { snprintf(shortnameble, sizeof(shortnameble), "%s", screen->ourId); } else { snprintf(shortnameble, sizeof(shortnameble), "BLE: %s", screen->ourId); } int textWidth = display->getStringWidth(shortnameble); int nameX = (SCREEN_WIDTH - textWidth); display->drawString(nameX, getTextPositions(display)[line++], shortnameble); // === Second Row: Role === auto role = DisplayFormatters::getDeviceRole(config.device.role); char device_role[25]; snprintf(device_role, sizeof(device_role), "Role: %s", role); textWidth = display->getStringWidth(device_role); nameX = (SCREEN_WIDTH - textWidth) / 2; display->drawString(nameX, getTextPositions(display)[line++], device_role); // === Third Row: Radio Preset === auto mode = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false, config.lora.use_preset); char regionradiopreset[25]; const char *region = myRegion ? myRegion->name : NULL; if (region != nullptr) { if (currentResolution == ScreenResolution::UltraLow) { snprintf(regionradiopreset, sizeof(regionradiopreset), "%s", region); } else { snprintf(regionradiopreset, sizeof(regionradiopreset), "%s/%s", region, mode); } } textWidth = display->getStringWidth(regionradiopreset); nameX = (SCREEN_WIDTH - textWidth) / 2; display->drawString(nameX, getTextPositions(display)[line++], regionradiopreset); // === Fourth Row: Frequency / ChanNum === char frequencyslot[35]; char freqStr[16]; float freq = RadioLibInterface::instance->getFreq(); snprintf(freqStr, sizeof(freqStr), "%.3f", freq); if (config.lora.channel_num == 0) { if (currentResolution == ScreenResolution::UltraLow) { snprintf(frequencyslot, sizeof(frequencyslot), "%sMHz", freqStr); } else { snprintf(frequencyslot, sizeof(frequencyslot), "Freq: %sMHz", freqStr); } } else { if (currentResolution == ScreenResolution::UltraLow) { snprintf(frequencyslot, sizeof(frequencyslot), "%sMHz (%d)", freqStr, config.lora.channel_num); } else { snprintf(frequencyslot, sizeof(frequencyslot), "Freq: %sMHz (%d)", freqStr, config.lora.channel_num); } } size_t len = strlen(frequencyslot); if (len >= 4 && strcmp(frequencyslot + len - 4, " (0)") == 0) { frequencyslot[len - 4] = '\0'; // Remove the last three characters } textWidth = display->getStringWidth(frequencyslot); nameX = (SCREEN_WIDTH - textWidth) / 2; display->drawString(nameX, getTextPositions(display)[line++], frequencyslot); #if !defined(M5STACK_UNITC6L) // === Fifth Row: Channel Utilization === const char *chUtil = "ChUtil:"; char chUtilPercentage[10]; snprintf(chUtilPercentage, sizeof(chUtilPercentage), "%2.0f%%", airTime->channelUtilizationPercent()); int chUtil_x = (currentResolution == ScreenResolution::High) ? display->getStringWidth(chUtil) + 10 : display->getStringWidth(chUtil) + 5; int chUtil_y = getTextPositions(display)[line] + 3; int chutil_bar_width = (currentResolution == ScreenResolution::High) ? 100 : 50; int chutil_bar_height = (currentResolution == ScreenResolution::High) ? 12 : 7; int extraoffset = (currentResolution == ScreenResolution::High) ? 6 : 3; int chutil_percent = airTime->channelUtilizationPercent(); int centerofscreen = SCREEN_WIDTH / 2; int total_line_content_width = (chUtil_x + chutil_bar_width + display->getStringWidth(chUtilPercentage) + extraoffset) / 2; int starting_position = centerofscreen - total_line_content_width; display->drawString(starting_position, getTextPositions(display)[line], chUtil); // Force 56% or higher to show a full 100% bar, text would still show related percent. if (chutil_percent >= 61) { chutil_percent = 100; } // Weighting for nonlinear segments float milestone1 = 25; float milestone2 = 40; float weight1 = 0.45; // Weight for 0–25% float weight2 = 0.35; // Weight for 25–40% float weight3 = 0.20; // Weight for 40–100% float totalWeight = weight1 + weight2 + weight3; int seg1 = chutil_bar_width * (weight1 / totalWeight); int seg2 = chutil_bar_width * (weight2 / totalWeight); int seg3 = chutil_bar_width * (weight3 / totalWeight); int fillRight = 0; if (chutil_percent <= milestone1) { fillRight = (seg1 * (chutil_percent / milestone1)); } else if (chutil_percent <= milestone2) { fillRight = seg1 + (seg2 * ((chutil_percent - milestone1) / (milestone2 - milestone1))); } else { fillRight = seg1 + seg2 + (seg3 * ((chutil_percent - milestone2) / (100 - milestone2))); } // Draw outline display->drawRect(starting_position + chUtil_x, chUtil_y, chutil_bar_width, chutil_bar_height); // Fill progress if (fillRight > 0) { display->fillRect(starting_position + chUtil_x, chUtil_y, fillRight, chutil_bar_height); } display->drawString(starting_position + chUtil_x + chutil_bar_width + extraoffset, getTextPositions(display)[line++], chUtilPercentage); #endif graphics::drawCommonFooter(display, x, y); } // **************************** // * System Screen * // **************************** void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { display->clear(); display->setFont(FONT_SMALL); display->setTextAlignment(TEXT_ALIGN_LEFT); // === Set Title const char *titleStr = "System"; // === Header === graphics::drawCommonHeader(display, x, y, titleStr); // === Layout === int line = 1; const int barHeight = 6; const int labelX = x; int barsOffset = (currentResolution == ScreenResolution::High) ? 24 : 0; #ifdef USE_EINK #ifndef T_DECK_PRO barsOffset -= 12; #endif #endif int barX = x + barsOffset; if (currentResolution == ScreenResolution::UltraLow) { barX += 45; } else { barX += 40; } auto drawUsageRow = [&](const char *label, uint32_t used, uint32_t total, bool isHeap = false) { if (total == 0) return; int percent = (used * 100) / total; char combinedStr[24]; if (currentResolution == ScreenResolution::High) { snprintf(combinedStr, sizeof(combinedStr), "%s%3d%% %u/%uKB", (percent > 80) ? "! " : "", percent, used / 1024, total / 1024); } else { snprintf(combinedStr, sizeof(combinedStr), "%s%3d%%", (percent > 80) ? "! " : "", percent); } int textWidth = display->getStringWidth(combinedStr); int adjustedBarWidth = SCREEN_WIDTH - barX - textWidth - 6; if (adjustedBarWidth < 10) adjustedBarWidth = 10; int fillWidth = (used * adjustedBarWidth) / total; // Label display->setTextAlignment(TEXT_ALIGN_LEFT); display->drawString(labelX, getTextPositions(display)[line], label); #if !defined(M5STACK_UNITC6L) // Bar int barY = getTextPositions(display)[line] + (FONT_HEIGHT_SMALL - barHeight) / 2; display->setColor(WHITE); display->drawRect(barX, barY, adjustedBarWidth, barHeight); display->fillRect(barX, barY, fillWidth, barHeight); display->setColor(WHITE); #endif // Value string display->setTextAlignment(TEXT_ALIGN_RIGHT); display->drawString(SCREEN_WIDTH, getTextPositions(display)[line], combinedStr); }; // === Memory values === uint32_t heapUsed = memGet.getHeapSize() - memGet.getFreeHeap(); uint32_t heapTotal = memGet.getHeapSize(); uint32_t psramUsed = memGet.getPsramSize() - memGet.getFreePsram(); uint32_t psramTotal = memGet.getPsramSize(); uint32_t flashUsed = 0, flashTotal = 0; #ifdef ESP32 flashUsed = FSCom.usedBytes(); flashTotal = FSCom.totalBytes(); #endif uint32_t sdUsed = 0, sdTotal = 0; bool hasSD = false; /* #ifdef HAS_SDCARD hasSD = SD.cardType() != CARD_NONE; if (hasSD) { sdUsed = SD.usedBytes(); sdTotal = SD.totalBytes(); } #endif */ // === Draw memory rows drawUsageRow("Heap:", heapUsed, heapTotal, true); #ifdef ESP32 if (psramUsed > 0) { line += 1; drawUsageRow("PSRAM:", psramUsed, psramTotal); } if (flashTotal > 0) { line += 1; drawUsageRow("Flash:", flashUsed, flashTotal); } #endif if (hasSD && sdTotal > 0) { line += 1; drawUsageRow("SD:", sdUsed, sdTotal); } display->setTextAlignment(TEXT_ALIGN_LEFT); // System Uptime if (line < 2) { line += 1; } line += 1; char appversionstr[35]; char appversionstr_formatted[40]; const char *ver = optstr(APP_VERSION); char verbuf[32]; strncpy(verbuf, ver, sizeof(verbuf) - 1); verbuf[sizeof(verbuf) - 1] = '\0'; char *lastDot = strrchr(verbuf, '.'); if (currentResolution == ScreenResolution::UltraLow) { if (lastDot != nullptr) { *lastDot = '\0'; } snprintf(appversionstr, sizeof(appversionstr), "Ver: %s", verbuf); } else { if (lastDot) { size_t prefixLen = (size_t)(lastDot - verbuf); snprintf(appversionstr_formatted, sizeof(appversionstr_formatted), "Ver: %.*s", (int)prefixLen, verbuf); strncat(appversionstr_formatted, " (", sizeof(appversionstr_formatted) - strlen(appversionstr_formatted) - 1); strncat(appversionstr_formatted, lastDot + 1, sizeof(appversionstr_formatted) - strlen(appversionstr_formatted) - 1); strncat(appversionstr_formatted, ")", sizeof(appversionstr_formatted) - strlen(appversionstr_formatted) - 1); strncpy(appversionstr, appversionstr_formatted, sizeof(appversionstr) - 1); appversionstr[sizeof(appversionstr) - 1] = '\0'; } else { snprintf(appversionstr, sizeof(appversionstr), "Ver: %s", verbuf); } } int textWidth = display->getStringWidth(appversionstr); int nameX = (SCREEN_WIDTH - textWidth) / 2; display->drawString(nameX, getTextPositions(display)[line++], appversionstr); if (SCREEN_HEIGHT > 64 || (SCREEN_HEIGHT <= 64 && line <= 5)) { // Only show uptime if the screen can show it char uptimeStr[32] = ""; getUptimeStr(millis(), "Up: ", uptimeStr, sizeof(uptimeStr)); textWidth = display->getStringWidth(uptimeStr); nameX = (SCREEN_WIDTH - textWidth) / 2; display->drawString(nameX, getTextPositions(display)[line++], uptimeStr); } if (SCREEN_HEIGHT > 64 || (SCREEN_HEIGHT <= 64 && line <= 5)) { // Only show API state if the screen can show it char api_state[32] = ""; const char *clientWord = nullptr; // Determine if narrow or wide screen if (currentResolution == ScreenResolution::High) { clientWord = "Client"; } else { clientWord = "App"; } snprintf(api_state, sizeof(api_state), "No %ss Connected", clientWord); if (service->api_state == service->STATE_BLE) { snprintf(api_state, sizeof(api_state), "%s Connected (BLE)", clientWord); } else if (service->api_state == service->STATE_WIFI) { snprintf(api_state, sizeof(api_state), "%s Connected (WiFi)", clientWord); } else if (service->api_state == service->STATE_SERIAL) { snprintf(api_state, sizeof(api_state), "%s Connected (Serial)", clientWord); } else if (service->api_state == service->STATE_PACKET) { snprintf(api_state, sizeof(api_state), "%s Connected (Internal)", clientWord); } else if (service->api_state == service->STATE_HTTP) { snprintf(api_state, sizeof(api_state), "%s Connected (HTTP)", clientWord); } else if (service->api_state == service->STATE_ETH) { snprintf(api_state, sizeof(api_state), "%s Connected (Ethernet)", clientWord); } if (api_state[0] != '\0') { display->drawString((SCREEN_WIDTH - display->getStringWidth(api_state)) / 2, getTextPositions(display)[line++], api_state); } } graphics::drawCommonFooter(display, x, y); } // **************************** // * Chirpy Screen * // **************************** void drawChirpy(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { display->clear(); display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); int line = 1; int iconX = SCREEN_WIDTH - chirpy_width - (chirpy_width / 3); int iconY = (SCREEN_HEIGHT - chirpy_height) / 2; int textX_offset = 10; if (currentResolution == ScreenResolution::High) { textX_offset = textX_offset * 4; const int scale = 2; const int bytesPerRow = (chirpy_width + 7) / 8; for (int yy = 0; yy < chirpy_height; ++yy) { iconX = SCREEN_WIDTH - (chirpy_width * 2) - ((chirpy_width * 2) / 3); iconY = (SCREEN_HEIGHT - (chirpy_height * 2)) / 2; const uint8_t *rowPtr = chirpy + yy * bytesPerRow; for (int xx = 0; xx < chirpy_width; ++xx) { const uint8_t byteVal = pgm_read_byte(rowPtr + (xx >> 3)); const uint8_t bitMask = 1U << (xx & 7); // XBM is LSB-first if (byteVal & bitMask) { display->fillRect(iconX + xx * scale, iconY + yy * scale, scale, scale); } } } } else { display->drawXbm(iconX, iconY, chirpy_width, chirpy_height, chirpy); } int textX = (display->getWidth() / 2) - textX_offset - (display->getStringWidth("Hello") / 2); display->drawString(textX, getTextPositions(display)[line++], "Hello"); textX = (display->getWidth() / 2) - textX_offset - (display->getStringWidth("World!") / 2); display->drawString(textX, getTextPositions(display)[line++], "World!"); } } // namespace DebugRenderer } // namespace graphics #endif ================================================ FILE: src/graphics/draw/DebugRenderer.h ================================================ #pragma once #include #include namespace graphics { /// Forward declarations class Screen; class DebugInfo; /** * @brief Debug and diagnostic drawing functions * * Contains all functions related to drawing debug information, * WiFi status, settings screens, and diagnostic data. */ namespace DebugRenderer { // Debug frame functions void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); void drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); void drawFrameWiFi(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); // Trampoline functions for framework callback compatibility void drawDebugInfoTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); void drawDebugInfoSettingsTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); void drawDebugInfoWiFiTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); // LoRa information display void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); // System screen display void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); // Chirpy screen display void drawChirpy(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); } // namespace DebugRenderer } // namespace graphics ================================================ FILE: src/graphics/draw/DrawRenderers.h ================================================ #pragma once /** * @brief Master include file for all Screen draw renderers * * This file includes all the individual renderer headers to provide * a convenient single include for accessing all draw functions. */ #include "graphics/draw/ClockRenderer.h" #include "graphics/draw/CompassRenderer.h" #include "graphics/draw/DebugRenderer.h" #include "graphics/draw/NodeListRenderer.h" #include "graphics/draw/UIRenderer.h" namespace graphics { /** * @brief Collection of all draw renderers * * This namespace provides access to all the specialized rendering * functions organized by category. */ namespace DrawRenderers { // Re-export all renderer namespaces for convenience using namespace ClockRenderer; using namespace CompassRenderer; using namespace DebugRenderer; using namespace NodeListRenderer; } // namespace DrawRenderers } // namespace graphics ================================================ FILE: src/graphics/draw/MenuHandler.cpp ================================================ #include "configuration.h" #if HAS_SCREEN #include "ClockRenderer.h" #include "Default.h" #include "GPS.h" #include "MenuHandler.h" #include "MeshRadio.h" #include "MeshService.h" #include "MessageStore.h" #include "NodeDB.h" #include "buzz.h" #include "graphics/Screen.h" #include "graphics/SharedUIDisplay.h" #include "graphics/draw/MessageRenderer.h" #include "graphics/draw/UIRenderer.h" #include "input/RotaryEncoderInterruptImpl1.h" #include "input/UpDownInterruptImpl1.h" #include "main.h" #include "mesh/Default.h" #include "mesh/MeshTypes.h" #include "modules/AdminModule.h" #include "modules/CannedMessageModule.h" #include "modules/ExternalNotificationModule.h" #include "modules/KeyVerificationModule.h" #include "modules/TraceRouteModule.h" #include #include #include #include #include extern uint16_t TFT_MESH; namespace graphics { namespace { // Caller must ensure the provided options array outlives the banner callback. template BannerOverlayOptions createStaticBannerOptions(const char *message, const MenuOption (&options)[N], std::array &labels, Callback &&onSelection) { for (size_t i = 0; i < N; ++i) { labels[i] = options[i].label; } const MenuOption *optionsPtr = options; auto callback = std::function &, int)>(std::forward(onSelection)); BannerOverlayOptions bannerOptions; bannerOptions.message = message; bannerOptions.optionsArrayPtr = labels.data(); bannerOptions.optionsCount = static_cast(N); bannerOptions.bannerCallback = [optionsPtr, callback](int selected) -> void { callback(optionsPtr[selected], selected); }; return bannerOptions; } } // namespace menuHandler::screenMenus menuHandler::menuQueue = MenuNone; uint32_t menuHandler::pickedNodeNum = 0; bool test_enabled = false; uint8_t test_count = 0; void menuHandler::loraMenu() { static const char *optionsArray[] = {"Back", "Device Role", "Radio Preset", "Frequency Slot", "LoRa Region"}; enum optionsNumbers { Back = 0, DeviceRolePicker = 1, RadioPresetPicker = 2, FrequencySlot = 3, LoraPicker = 4 }; BannerOverlayOptions bannerOptions; bannerOptions.message = "LoRa Actions"; bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsCount = 5; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == Back) { // No action } else if (selected == DeviceRolePicker) { menuHandler::menuQueue = menuHandler::DeviceRolePicker; } else if (selected == RadioPresetPicker) { menuHandler::menuQueue = menuHandler::RadioPresetPicker; } else if (selected == FrequencySlot) { menuHandler::menuQueue = menuHandler::FrequencySlot; } else if (selected == LoraPicker) { menuHandler::menuQueue = menuHandler::LoraPicker; } }; screen->showOverlayBanner(bannerOptions); } void menuHandler::OnboardMessage() { static const char *optionsArray[] = {"OK", "Got it!"}; enum optionsNumbers { OK, got }; BannerOverlayOptions bannerOptions; #if HAS_TFT bannerOptions.message = "Welcome to Meshtastic!\nSwipe to navigate and\nlong press to select\nor open a menu."; #elif defined(BUTTON_PIN) bannerOptions.message = "Welcome to Meshtastic!\nClick to navigate and\nlong press to select\nor open a menu."; #else bannerOptions.message = "Welcome to Meshtastic!\nUse the Select button\nto open menus\nand make selections."; #endif bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsCount = 2; bannerOptions.bannerCallback = [](int selected) -> void { menuHandler::menuQueue = menuHandler::NoTimeoutLoraPicker; screen->runNow(); }; screen->showOverlayBanner(bannerOptions); } void menuHandler::LoraRegionPicker(uint32_t duration) { static const LoraRegionOption regionOptions[] = { {"Back", OptionsAction::Back}, {"US", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_US}, {"EU_433", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_EU_433}, {"EU_868", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_EU_868}, {"CN", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_CN}, {"JP", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_JP}, {"ANZ", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_ANZ}, {"KR", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_KR}, {"TW", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_TW}, {"RU", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_RU}, {"IN", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_IN}, {"NZ_865", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_NZ_865}, {"TH", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_TH}, {"LORA_24", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_LORA_24}, {"UA_433", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_UA_433}, {"UA_868", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_UA_868}, {"MY_433", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_MY_433}, {"MY_919", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_MY_919}, {"SG_923", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_SG_923}, {"PH_433", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_PH_433}, {"PH_868", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_PH_868}, {"PH_915", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_PH_915}, {"ANZ_433", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_ANZ_433}, {"KZ_433", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_KZ_433}, {"KZ_863", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_KZ_863}, {"NP_865", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_NP_865}, {"BR_902", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_BR_902}, }; constexpr size_t regionCount = sizeof(regionOptions) / sizeof(regionOptions[0]); static std::array regionLabels{}; const char *bannerMessage = "Set the LoRa region"; if (currentResolution == ScreenResolution::UltraLow) { bannerMessage = "LoRa Region"; } auto bannerOptions = createStaticBannerOptions(bannerMessage, regionOptions, regionLabels, [](const LoraRegionOption &option, int) -> void { if (!option.hasValue) { return; } auto selectedRegion = option.value; if (config.lora.region == selectedRegion) { return; } config.lora.region = selectedRegion; auto changes = SEGMENT_CONFIG; #if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN || MESHTASTIC_EXCLUDE_PKI) if (crypto) { crypto->ensurePkiKeys(config.security, owner); } #endif config.lora.tx_enabled = true; initRegion(); if (myRegion->dutyCycle < 100) { config.lora.ignore_mqtt = true; // Ignore MQTT by default if region has a duty cycle limit } if (strncmp(moduleConfig.mqtt.root, default_mqtt_root, strlen(default_mqtt_root)) == 0) { // Default broker is in use, so subscribe to the appropriate MQTT root topic for this region sprintf(moduleConfig.mqtt.root, "%s/%s", default_mqtt_root, myRegion->name); changes |= SEGMENT_MODULECONFIG; } service->reloadConfig(changes); rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); }); bannerOptions.durationMs = duration; int initialSelection = 0; for (size_t i = 0; i < regionCount; ++i) { if (regionOptions[i].hasValue && regionOptions[i].value == config.lora.region) { initialSelection = static_cast(i); break; } } bannerOptions.InitialSelected = initialSelection; screen->showOverlayBanner(bannerOptions); } void menuHandler::deviceRolePicker() { static const char *optionsArray[] = {"Back", "Client", "Client Mute", "Lost and Found", "Tracker"}; enum optionsNumbers { Back = 0, devicerole_client = 1, devicerole_clientmute = 2, devicerole_lostandfound = 3, devicerole_tracker = 4 }; BannerOverlayOptions bannerOptions; bannerOptions.message = "Device Role"; bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsCount = 5; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == Back) { menuHandler::menuQueue = menuHandler::LoraMenu; screen->runNow(); return; } else if (selected == devicerole_client) { config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; } else if (selected == devicerole_clientmute) { config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE; } else if (selected == devicerole_lostandfound) { config.device.role = meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND; } else if (selected == devicerole_tracker) { config.device.role = meshtastic_Config_DeviceConfig_Role_TRACKER; } service->reloadConfig(SEGMENT_CONFIG); rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); }; screen->showOverlayBanner(bannerOptions); } void menuHandler::FrequencySlotPicker() { enum ReplyOptions : int { Back = -1 }; constexpr int MAX_CHANNEL_OPTIONS = 202; static const char *optionsArray[MAX_CHANNEL_OPTIONS]; static int optionsEnumArray[MAX_CHANNEL_OPTIONS]; static char channelText[MAX_CHANNEL_OPTIONS - 1][12]; int options = 0; optionsArray[options] = "Back"; optionsEnumArray[options++] = Back; optionsArray[options] = "Slot 0 (Auto)"; optionsEnumArray[options++] = 0; // Calculate number of channels (copied from RadioInterface::applyModemConfig()) meshtastic_Config_LoRaConfig &loraConfig = config.lora; double bw = loraConfig.use_preset ? modemPresetToBwKHz(loraConfig.modem_preset, myRegion->wideLora) : bwCodeToKHz(loraConfig.bandwidth); uint32_t numChannels = 0; if (myRegion) { // Match RadioInterface::applyModemConfig(): include padding, add spacing in numerator, and use round() const double spacing = myRegion->profile->spacing; const double padding = myRegion->profile->padding; const double channelBandwidthMHz = bw / 1000.0; const double numerator = (myRegion->freqEnd - myRegion->freqStart) + spacing; const double denominator = spacing + (padding * 2) + channelBandwidthMHz; if (denominator > 0.0) { numChannels = static_cast(round(numerator / denominator)); } else { LOG_WARN("Invalid region configuration: non-positive channel spacing/width"); } } else { LOG_WARN("Region not set, cannot calculate number of channels"); return; } if (numChannels > (uint32_t)(MAX_CHANNEL_OPTIONS - 2)) numChannels = (uint32_t)(MAX_CHANNEL_OPTIONS - 2); for (uint32_t ch = 1; ch <= numChannels; ch++) { snprintf(channelText[ch - 1], sizeof(channelText[ch - 1]), "Slot %lu", (unsigned long)ch); optionsArray[options] = channelText[ch - 1]; optionsEnumArray[options++] = (int)ch; } BannerOverlayOptions bannerOptions; bannerOptions.message = "Frequency Slot"; bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.optionsCount = options; // Start highlight on current channel if possible, otherwise on "1" int initial = (int)config.lora.channel_num + 1; if (initial < 2 || initial > (int)numChannels + 1) initial = 1; bannerOptions.InitialSelected = initial; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == Back) { menuHandler::menuQueue = menuHandler::LoraMenu; screen->runNow(); return; } config.lora.channel_num = selected; service->reloadConfig(SEGMENT_CONFIG); rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); }; screen->showOverlayBanner(bannerOptions); } void menuHandler::radioPresetPicker() { static const RadioPresetOption presetOptions[] = { {"Back", OptionsAction::Back}, {"LongTurbo", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO}, {"LongModerate", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE}, {"LongFast", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST}, {"MediumSlow", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW}, {"MediumFast", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST}, {"ShortSlow", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW}, {"ShortFast", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST}, {"ShortTurbo", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO}, }; constexpr size_t presetCount = sizeof(presetOptions) / sizeof(presetOptions[0]); static std::array presetLabels{}; auto bannerOptions = createStaticBannerOptions("Radio Preset", presetOptions, presetLabels, [](const RadioPresetOption &option, int) -> void { if (option.action == OptionsAction::Back) { menuHandler::menuQueue = menuHandler::LoraMenu; screen->runNow(); return; } if (!option.hasValue) { return; } config.lora.modem_preset = option.value; config.lora.channel_num = 0; // Reset to default channel for the preset config.lora.override_frequency = 0; // Clear any custom frequency service->reloadConfig(SEGMENT_CONFIG); rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); }); screen->showOverlayBanner(bannerOptions); } void menuHandler::twelveHourPicker() { static const char *optionsArray[] = {"Back", "12-hour", "24-hour"}; enum optionsNumbers { Back = 0, twelve = 1, twentyfour = 2 }; BannerOverlayOptions bannerOptions; bannerOptions.message = "Time Format"; bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsCount = 3; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == Back) { menuHandler::menuQueue = menuHandler::ClockMenu; screen->runNow(); } else if (selected == twelve) { config.display.use_12h_clock = true; } else { config.display.use_12h_clock = false; } service->reloadConfig(SEGMENT_CONFIG); }; screen->showOverlayBanner(bannerOptions); } // Reusable confirmation prompt function void menuHandler::showConfirmationBanner(const char *message, std::function onConfirm) { static const char *confirmOptions[] = {"No", "Yes"}; BannerOverlayOptions confirmBanner; confirmBanner.message = message; confirmBanner.optionsArrayPtr = confirmOptions; confirmBanner.optionsCount = 2; confirmBanner.bannerCallback = [onConfirm](int confirmSelected) -> void { if (confirmSelected == 1) { onConfirm(); } }; screen->showOverlayBanner(confirmBanner); } void menuHandler::clockFacePicker() { static const ClockFaceOption clockFaceOptions[] = { {"Back", OptionsAction::Back}, {"Digital", OptionsAction::Select, false}, {"Analog", OptionsAction::Select, true}, }; constexpr size_t clockFaceCount = sizeof(clockFaceOptions) / sizeof(clockFaceOptions[0]); static std::array clockFaceLabels{}; auto bannerOptions = createStaticBannerOptions("Which Face?", clockFaceOptions, clockFaceLabels, [](const ClockFaceOption &option, int) -> void { if (option.action == OptionsAction::Back) { menuHandler::menuQueue = menuHandler::ClockMenu; screen->runNow(); return; } if (!option.hasValue) { return; } if (uiconfig.is_clockface_analog == option.value) { return; } uiconfig.is_clockface_analog = option.value; saveUIConfig(); screen->setFrames(Screen::FOCUS_CLOCK); }); bannerOptions.InitialSelected = uiconfig.is_clockface_analog ? 2 : 1; screen->showOverlayBanner(bannerOptions); } void menuHandler::TZPicker() { static const TimezoneOption timezoneOptions[] = { {"Back", OptionsAction::Back}, {"US/Hawaii", OptionsAction::Select, "HST10"}, {"US/Alaska", OptionsAction::Select, "AKST9AKDT,M3.2.0,M11.1.0"}, {"US/Pacific", OptionsAction::Select, "PST8PDT,M3.2.0,M11.1.0"}, {"US/Arizona", OptionsAction::Select, "MST7"}, {"US/Mountain", OptionsAction::Select, "MST7MDT,M3.2.0,M11.1.0"}, {"US/Central", OptionsAction::Select, "CST6CDT,M3.2.0,M11.1.0"}, {"US/Eastern", OptionsAction::Select, "EST5EDT,M3.2.0,M11.1.0"}, {"BR/Brasilia", OptionsAction::Select, "BRT3"}, {"UTC", OptionsAction::Select, "UTC0"}, {"EU/Western", OptionsAction::Select, "GMT0BST,M3.5.0/1,M10.5.0"}, {"EU/Central", OptionsAction::Select, "CET-1CEST,M3.5.0,M10.5.0/3"}, {"EU/Eastern", OptionsAction::Select, "EET-2EEST,M3.5.0/3,M10.5.0/4"}, {"Asia/Kolkata", OptionsAction::Select, "IST-5:30"}, {"Asia/Hong_Kong", OptionsAction::Select, "HKT-8"}, {"AU/AWST", OptionsAction::Select, "AWST-8"}, {"AU/ACST", OptionsAction::Select, "ACST-9:30ACDT,M10.1.0,M4.1.0/3"}, {"AU/AEST", OptionsAction::Select, "AEST-10AEDT,M10.1.0,M4.1.0/3"}, {"Pacific/NZ", OptionsAction::Select, "NZST-12NZDT,M9.5.0,M4.1.0/3"}, }; constexpr size_t timezoneCount = sizeof(timezoneOptions) / sizeof(timezoneOptions[0]); static std::array timezoneLabels{}; auto bannerOptions = createStaticBannerOptions( "Pick Timezone", timezoneOptions, timezoneLabels, [](const TimezoneOption &option, int) -> void { if (option.action == OptionsAction::Back) { menuHandler::menuQueue = menuHandler::ClockMenu; screen->runNow(); return; } if (!option.hasValue) { return; } if (strncmp(config.device.tzdef, option.value, sizeof(config.device.tzdef)) == 0) { return; } strncpy(config.device.tzdef, option.value, sizeof(config.device.tzdef)); config.device.tzdef[sizeof(config.device.tzdef) - 1] = '\0'; setenv("TZ", config.device.tzdef, 1); service->reloadConfig(SEGMENT_CONFIG); }); int initialSelection = 0; for (size_t i = 0; i < timezoneCount; ++i) { if (timezoneOptions[i].hasValue && strncmp(config.device.tzdef, timezoneOptions[i].value, sizeof(config.device.tzdef)) == 0) { initialSelection = static_cast(i); break; } } bannerOptions.InitialSelected = initialSelection; screen->showOverlayBanner(bannerOptions); } void menuHandler::clockMenu() { #if defined(M5STACK_UNITC6L) static const char *optionsArray[] = {"Back", "Time Format", "Timezone"}; #else static const char *optionsArray[] = {"Back", "Clock Face", "Time Format", "Timezone"}; #endif enum optionsNumbers { Back = 0, Clock = 1, Time = 2, Timezone = 3 }; BannerOverlayOptions bannerOptions; bannerOptions.message = "Clock Action"; bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsCount = 4; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == Clock) { menuHandler::menuQueue = menuHandler::ClockFacePicker; screen->runNow(); } else if (selected == Time) { menuHandler::menuQueue = menuHandler::TwelveHourPicker; screen->runNow(); } else if (selected == Timezone) { menuHandler::menuQueue = menuHandler::TzPicker; screen->runNow(); } }; screen->showOverlayBanner(bannerOptions); } void menuHandler::messageResponseMenu() { enum optionsNumbers { Back = 0, ViewMode, DeleteMenu, ReplyMenu, MuteChannel, Aloud, enumEnd }; static const char *optionsArray[enumEnd]; static int optionsEnumArray[enumEnd]; int options = 0; auto mode = graphics::MessageRenderer::getThreadMode(); int threadChannel = graphics::MessageRenderer::getThreadChannel(); optionsArray[options] = "Back"; optionsEnumArray[options++] = Back; // New Reply submenu (replaces Preset and Freetext directly in this menu) optionsArray[options] = "Reply"; optionsEnumArray[options++] = ReplyMenu; optionsArray[options] = "View Chats"; optionsEnumArray[options++] = ViewMode; // If viewing ALL chats, hide “Mute Chat” if (mode != graphics::MessageRenderer::ThreadMode::ALL && mode != graphics::MessageRenderer::ThreadMode::DIRECT) { const uint8_t chIndex = (threadChannel != 0) ? (uint8_t)threadChannel : channels.getPrimaryIndex(); const auto &chan = channels.getByIndex(chIndex); optionsArray[options] = chan.settings.module_settings.is_muted ? "Unmute Channel" : "Mute Channel"; optionsEnumArray[options++] = MuteChannel; } // Delete submenu optionsArray[options] = "Delete"; optionsEnumArray[options++] = DeleteMenu; #ifdef HAS_I2S optionsArray[options] = "Read Aloud"; optionsEnumArray[options++] = Aloud; #endif BannerOverlayOptions bannerOptions; bannerOptions.message = "Message Action"; if (currentResolution == ScreenResolution::UltraLow) { bannerOptions.message = "Message"; } bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.optionsCount = options; bannerOptions.bannerCallback = [](int selected) -> void { LOG_DEBUG("messageResponseMenu: selected %d", selected); auto mode = graphics::MessageRenderer::getThreadMode(); int ch = graphics::MessageRenderer::getThreadChannel(); uint32_t peer = graphics::MessageRenderer::getThreadPeer(); LOG_DEBUG("[ReplyCtx] mode=%d ch=%d peer=0x%08x", (int)mode, ch, (unsigned int)peer); if (selected == ViewMode) { menuHandler::menuQueue = menuHandler::MessageViewModeMenu; screen->runNow(); // Reply submenu } else if (selected == ReplyMenu) { menuHandler::menuQueue = menuHandler::ReplyMenu; screen->runNow(); } else if (selected == MuteChannel) { const uint8_t chIndex = (ch != 0) ? (uint8_t)ch : channels.getPrimaryIndex(); auto &chan = channels.getByIndex(chIndex); if (chan.settings.has_module_settings) { chan.settings.module_settings.is_muted = !chan.settings.module_settings.is_muted; nodeDB->saveToDisk(); } } else if (selected == DeleteMenu) { menuHandler::menuQueue = menuHandler::DeleteMessagesMenu; screen->runNow(); #ifdef HAS_I2S } else if (selected == Aloud) { const meshtastic_MeshPacket &mp = devicestate.rx_text_message; const char *msg = reinterpret_cast(mp.decoded.payload.bytes); audioThread->readAloud(msg); #endif } }; screen->showOverlayBanner(bannerOptions); } void menuHandler::replyMenu() { enum replyOptions { Back = 0, ReplyPreset, ReplyFreetext, enumEnd }; static const char *optionsArray[enumEnd]; static int optionsEnumArray[enumEnd]; int options = 0; // Back optionsArray[options] = "Back"; optionsEnumArray[options++] = Back; // Preset reply optionsArray[options] = "With Preset"; optionsEnumArray[options++] = ReplyPreset; // Freetext reply (only when keyboard exists) if (kb_found) { optionsArray[options] = "With Freetext"; optionsEnumArray[options++] = ReplyFreetext; } BannerOverlayOptions bannerOptions; // Dynamic title based on thread mode auto mode = graphics::MessageRenderer::getThreadMode(); if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) { bannerOptions.message = "Reply to Channel"; } else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) { bannerOptions.message = "Reply to DM"; } else { // View All bannerOptions.message = "Reply to Last Msg"; } bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.optionsCount = options; bannerOptions.InitialSelected = 1; bannerOptions.bannerCallback = [](int selected) -> void { auto mode = graphics::MessageRenderer::getThreadMode(); int ch = graphics::MessageRenderer::getThreadChannel(); uint32_t peer = graphics::MessageRenderer::getThreadPeer(); if (selected == Back) { menuHandler::menuQueue = menuHandler::MessageResponseMenu; screen->runNow(); return; } // Preset reply if (selected == ReplyPreset) { if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) { cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST, ch); } else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) { cannedMessageModule->LaunchWithDestination(peer); } else { // Fallback for last received message if (devicestate.rx_text_message.to == NODENUM_BROADCAST) { cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST, devicestate.rx_text_message.channel); } else { cannedMessageModule->LaunchWithDestination(devicestate.rx_text_message.from); } } return; } // Freetext reply if (selected == ReplyFreetext) { if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) { cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST, ch); } else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) { cannedMessageModule->LaunchFreetextWithDestination(peer); } else { // Fallback for last received message if (devicestate.rx_text_message.to == NODENUM_BROADCAST) { cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST, devicestate.rx_text_message.channel); } else { cannedMessageModule->LaunchFreetextWithDestination(devicestate.rx_text_message.from); } } return; } }; screen->showOverlayBanner(bannerOptions); } void menuHandler::deleteMessagesMenu() { enum optionsNumbers { Back = 0, DeleteOldest, DeleteThis, DeleteAll, enumEnd }; static const char *optionsArray[enumEnd]; static int optionsEnumArray[enumEnd]; int options = 0; auto mode = graphics::MessageRenderer::getThreadMode(); optionsArray[options] = "Back"; optionsEnumArray[options++] = Back; optionsArray[options] = "Delete Oldest"; optionsEnumArray[options++] = DeleteOldest; // If viewing ALL chats → hide “Delete This Chat” if (mode != graphics::MessageRenderer::ThreadMode::ALL) { optionsArray[options] = "Delete This Chat"; optionsEnumArray[options++] = DeleteThis; } if (currentResolution == ScreenResolution::UltraLow) { optionsArray[options] = "Delete All"; } else { optionsArray[options] = "Delete All Chats"; } optionsEnumArray[options++] = DeleteAll; BannerOverlayOptions bannerOptions; bannerOptions.message = "Delete Messages"; bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.optionsCount = options; bannerOptions.bannerCallback = [mode](int selected) -> void { int ch = graphics::MessageRenderer::getThreadChannel(); uint32_t peer = graphics::MessageRenderer::getThreadPeer(); if (selected == Back) { menuHandler::menuQueue = menuHandler::MessageResponseMenu; screen->runNow(); return; } if (selected == DeleteAll) { LOG_INFO("Deleting all messages"); messageStore.clearAllMessages(); graphics::MessageRenderer::clearThreadRegistries(); graphics::MessageRenderer::clearMessageCache(); return; } if (selected == DeleteOldest) { LOG_INFO("Deleting oldest message"); if (mode == graphics::MessageRenderer::ThreadMode::ALL) { messageStore.deleteOldestMessage(); } else if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) { messageStore.deleteOldestMessageInChannel(ch); } else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) { messageStore.deleteOldestMessageWithPeer(peer); } return; } // This only appears in non-ALL modes if (selected == DeleteThis) { LOG_INFO("Deleting all messages in this thread"); if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) { messageStore.deleteAllMessagesInChannel(ch); } else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) { messageStore.deleteAllMessagesWithPeer(peer); } return; } }; screen->showOverlayBanner(bannerOptions); } void menuHandler::messageViewModeMenu() { auto encodeChannelId = [](int ch) -> int { return 100 + ch; }; auto isChannelSel = [](int id) -> bool { return id >= 100 && id < 200; }; static std::vector labels; static std::vector ids; static std::vector idToPeer; // DM lookup labels.clear(); ids.clear(); idToPeer.clear(); labels.push_back("Back"); ids.push_back(-1); labels.push_back("View All Chats"); ids.push_back(-2); // Channels with messages for (int ch = 0; ch < 8; ++ch) { auto msgs = messageStore.getChannelMessages((uint8_t)ch); if (!msgs.empty()) { char buf[40]; const char *cname = channels.getName(ch); snprintf(buf, sizeof(buf), cname && cname[0] ? "#%s" : "#Ch%d", cname ? cname : "", ch); labels.push_back(buf); ids.push_back(encodeChannelId(ch)); LOG_DEBUG("messageViewModeMenu: Added live channel %s (id=%d)", buf, encodeChannelId(ch)); } } // Registry channels for (int ch : graphics::MessageRenderer::getSeenChannels()) { if (ch < 0 || ch >= 8) continue; auto msgs = messageStore.getChannelMessages((uint8_t)ch); if (msgs.empty()) continue; int enc = encodeChannelId(ch); if (std::find(ids.begin(), ids.end(), enc) == ids.end()) { char buf[40]; const char *cname = channels.getName(ch); snprintf(buf, sizeof(buf), cname && cname[0] ? "#%s" : "#Ch%d", cname ? cname : "", ch); labels.push_back(buf); ids.push_back(enc); LOG_DEBUG("messageViewModeMenu: Added registry channel %s (id=%d)", buf, enc); } } // Gather unique peers auto dms = messageStore.getDirectMessages(); std::vector uniquePeers; for (const auto &m : dms) { uint32_t peer = (m.sender == nodeDB->getNodeNum()) ? m.dest : m.sender; if (peer != nodeDB->getNodeNum() && std::find(uniquePeers.begin(), uniquePeers.end(), peer) == uniquePeers.end()) uniquePeers.push_back(peer); } for (uint32_t peer : graphics::MessageRenderer::getSeenPeers()) { if (peer != nodeDB->getNodeNum() && std::find(uniquePeers.begin(), uniquePeers.end(), peer) == uniquePeers.end()) uniquePeers.push_back(peer); } std::sort(uniquePeers.begin(), uniquePeers.end()); // Encode peers for (size_t i = 0; i < uniquePeers.size(); ++i) { uint32_t peer = uniquePeers[i]; auto node = nodeDB->getMeshNode(peer); std::string name; if (node && node->has_user) name = sanitizeString(node->user.long_name).substr(0, 15); else { char buf[20]; snprintf(buf, sizeof(buf), "Node %08X", peer); name = buf; } labels.push_back("@" + name); int encPeer = 1000 + (int)idToPeer.size(); ids.push_back(encPeer); idToPeer.push_back(peer); LOG_DEBUG("messageViewModeMenu: Added DM %s peer=0x%08x id=%d", name.c_str(), (unsigned int)peer, encPeer); } // Active ID int activeId = -2; auto mode = graphics::MessageRenderer::getThreadMode(); if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) activeId = encodeChannelId(graphics::MessageRenderer::getThreadChannel()); else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) { uint32_t cur = graphics::MessageRenderer::getThreadPeer(); for (size_t i = 0; i < idToPeer.size(); ++i) if (idToPeer[i] == cur) { activeId = 1000 + (int)i; break; } } LOG_DEBUG("messageViewModeMenu: Active thread id=%d", activeId); // Build banner static std::vector options; static std::vector optionIds; options.clear(); optionIds.clear(); int initialIndex = 0; for (size_t i = 0; i < labels.size(); i++) { options.push_back(labels[i].c_str()); optionIds.push_back(ids[i]); if (ids[i] == activeId) initialIndex = (int)i; } BannerOverlayOptions bannerOptions; bannerOptions.message = "Select Conversation"; bannerOptions.optionsArrayPtr = options.data(); bannerOptions.optionsEnumPtr = optionIds.data(); bannerOptions.optionsCount = options.size(); bannerOptions.InitialSelected = initialIndex; bannerOptions.bannerCallback = [=](int selected) -> void { LOG_DEBUG("messageViewModeMenu: selected=%d", selected); if (selected == -1) { menuHandler::menuQueue = menuHandler::MessageResponseMenu; screen->runNow(); } else if (selected == -2) { graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::ALL); } else if (isChannelSel(selected)) { int ch = selected - 100; graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::CHANNEL, ch); } else if (selected >= 1000) { int idx = selected - 1000; if (idx >= 0 && (size_t)idx < idToPeer.size()) { uint32_t peer = idToPeer[idx]; graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::DIRECT, -1, peer); } } }; screen->showOverlayBanner(bannerOptions); } void menuHandler::homeBaseMenu() { enum optionsNumbers { Back, Mute, Backlight, Position, Preset, Freetext, Sleep, enumEnd }; static const char *optionsArray[enumEnd] = {"Back"}; static int optionsEnumArray[enumEnd] = {Back}; int options = 1; if (moduleConfig.external_notification.enabled && externalNotificationModule && config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_DISABLED) { if (!externalNotificationModule->getMute()) { optionsArray[options] = "Temporarily Mute"; } else { optionsArray[options] = "Unmute"; } optionsEnumArray[options++] = Mute; } #if defined(PIN_EINK_EN) || defined(PCA_PIN_EINK_EN) optionsArray[options] = "Toggle Backlight"; optionsEnumArray[options++] = Backlight; #else optionsArray[options] = "Sleep Screen"; optionsEnumArray[options++] = Sleep; #endif if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) { optionsArray[options] = "Send Position"; } else { optionsArray[options] = "Send Node Info"; } optionsEnumArray[options++] = Position; BannerOverlayOptions bannerOptions; bannerOptions.message = "Home Action"; if (currentResolution == ScreenResolution::UltraLow) { bannerOptions.message = "Home"; } bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.optionsCount = options; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == Mute) { if (moduleConfig.external_notification.enabled && externalNotificationModule) { externalNotificationModule->setMute(!externalNotificationModule->getMute()); IF_SCREEN(if (!externalNotificationModule->getMute()) externalNotificationModule->stopNow();) } } else if (selected == Backlight) { screen->setOn(false); #if defined(PIN_EINK_EN) if (uiconfig.screen_brightness == 1) { uiconfig.screen_brightness = 0; digitalWrite(PIN_EINK_EN, LOW); } else { uiconfig.screen_brightness = 1; digitalWrite(PIN_EINK_EN, HIGH); } saveUIConfig(); #elif defined(PCA_PIN_EINK_EN) if (uiconfig.screen_brightness > 0) { uiconfig.screen_brightness = 0; io.digitalWrite(PCA_PIN_EINK_EN, LOW); } else { uiconfig.screen_brightness = 1; io.digitalWrite(PCA_PIN_EINK_EN, HIGH); } saveUIConfig(); #endif } else if (selected == Sleep) { screen->setOn(false); } else if (selected == Position) { service->refreshLocalMeshNode(); if (service->trySendPosition(NODENUM_BROADCAST, true)) { IF_SCREEN(screen->showSimpleBanner("Position\nSent", 3000)); } else { IF_SCREEN(screen->showSimpleBanner("Node Info\nSent", 3000)); } } else if (selected == Preset) { cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST); } else if (selected == Freetext) { cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST); } }; screen->showOverlayBanner(bannerOptions); } void menuHandler::textMessageMenu() { cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST); } void menuHandler::textMessageBaseMenu() { enum optionsNumbers { Back, Preset, Freetext, enumEnd }; static const char *optionsArray[enumEnd] = {"Back"}; static int optionsEnumArray[enumEnd] = {Back}; int options = 1; optionsArray[options] = "New Preset Msg"; optionsEnumArray[options++] = Preset; if (kb_found) { optionsArray[options] = "New Freetext Msg"; optionsEnumArray[options++] = Freetext; } BannerOverlayOptions bannerOptions; bannerOptions.message = "Message Action"; bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.optionsCount = options; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == Preset) { cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST); } else if (selected == Freetext) { cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST); } }; screen->showOverlayBanner(bannerOptions); } void menuHandler::systemBaseMenu() { enum optionsNumbers { Back, Notifications, ScreenOptions, Bluetooth, WiFiToggle, PowerMenu, Test, enumEnd }; static const char *optionsArray[enumEnd] = {"Back"}; static int optionsEnumArray[enumEnd] = {Back}; int options = 1; optionsArray[options] = "Notifications"; optionsEnumArray[options++] = Notifications; optionsArray[options] = "Display Options"; optionsEnumArray[options++] = ScreenOptions; if (currentResolution == ScreenResolution::UltraLow) { optionsArray[options] = "Bluetooth"; } else { optionsArray[options] = "Bluetooth Toggle"; } optionsEnumArray[options++] = Bluetooth; #if HAS_WIFI && !defined(ARCH_PORTDUINO) optionsArray[options] = "WiFi Toggle"; optionsEnumArray[options++] = WiFiToggle; #endif if (currentResolution == ScreenResolution::UltraLow) { optionsArray[options] = "Power"; } else { optionsArray[options] = "Reboot/Shutdown"; } optionsEnumArray[options++] = PowerMenu; if (test_enabled) { optionsArray[options] = "Test Menu"; optionsEnumArray[options++] = Test; } BannerOverlayOptions bannerOptions; bannerOptions.message = "System Action"; if (currentResolution == ScreenResolution::UltraLow) { bannerOptions.message = "System"; } bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsCount = options; bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == Notifications) { menuHandler::menuQueue = menuHandler::BuzzerModeMenuPicker; screen->runNow(); } else if (selected == ScreenOptions) { menuHandler::menuQueue = menuHandler::ScreenOptionsMenu; screen->runNow(); } else if (selected == PowerMenu) { menuHandler::menuQueue = menuHandler::PowerMenu; screen->runNow(); } else if (selected == Test) { menuHandler::menuQueue = menuHandler::TestMenu; screen->runNow(); } else if (selected == Bluetooth) { menuQueue = BluetoothToggleMenu; screen->runNow(); #if HAS_WIFI && !defined(ARCH_PORTDUINO) } else if (selected == WiFiToggle) { menuQueue = WifiToggleMenu; screen->runNow(); #endif } else if (selected == Back && !test_enabled) { test_count++; if (test_count > 4) { test_enabled = true; } } }; screen->showOverlayBanner(bannerOptions); } void menuHandler::favoriteBaseMenu() { enum optionsNumbers { Back, Preset, Freetext, GoToChat, Remove, TraceRoute, enumEnd }; static const char *optionsArray[enumEnd] = {"Back"}; static int optionsEnumArray[enumEnd] = {Back}; int options = 1; // Only show "View Conversation" if a message exists with this node uint32_t peer = graphics::UIRenderer::currentFavoriteNodeNum; bool hasConversation = false; for (const auto &m : messageStore.getMessages()) { if ((m.sender == peer || m.dest == peer)) { hasConversation = true; break; } } if (hasConversation) { optionsArray[options] = "Go To Chat"; optionsEnumArray[options++] = GoToChat; } if (currentResolution == ScreenResolution::UltraLow) { optionsArray[options] = "New Preset"; } else { optionsArray[options] = "New Preset Msg"; } optionsEnumArray[options++] = Preset; if (kb_found) { optionsArray[options] = "New Freetext Msg"; optionsEnumArray[options++] = Freetext; } if (currentResolution != ScreenResolution::UltraLow) { optionsArray[options] = "Trace Route"; optionsEnumArray[options++] = TraceRoute; } optionsArray[options] = "Remove Favorite"; optionsEnumArray[options++] = Remove; BannerOverlayOptions bannerOptions; bannerOptions.message = "Favorites Action"; if (currentResolution == ScreenResolution::UltraLow) { bannerOptions.message = "Favorites"; } bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.optionsCount = options; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == Preset) { cannedMessageModule->LaunchWithDestination(graphics::UIRenderer::currentFavoriteNodeNum); } else if (selected == Freetext) { cannedMessageModule->LaunchFreetextWithDestination(graphics::UIRenderer::currentFavoriteNodeNum); } // Handle new Go To Thread action else if (selected == GoToChat) { // Switch thread to direct conversation with this node graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::DIRECT, -1, graphics::UIRenderer::currentFavoriteNodeNum); // Manually create and send a UIFrameEvent to trigger the jump UIFrameEvent evt; evt.action = UIFrameEvent::Action::SWITCH_TO_TEXTMESSAGE; screen->handleUIFrameEvent(&evt); } else if (selected == Remove) { menuHandler::menuQueue = menuHandler::RemoveFavorite; screen->runNow(); } else if (selected == TraceRoute) { if (traceRouteModule) { traceRouteModule->launch(graphics::UIRenderer::currentFavoriteNodeNum); } } }; screen->showOverlayBanner(bannerOptions); } void menuHandler::positionBaseMenu() { enum class PositionAction { GpsToggle, GpsFormat, CompassMenu, CompassCalibrate, GPSSmartPosition, GPSUpdateInterval, GPSPositionBroadcast }; static const PositionMenuOption baseOptions[] = { {"Back", OptionsAction::Back}, {"On/Off Toggle", OptionsAction::Select, static_cast(PositionAction::GpsToggle)}, {"Format", OptionsAction::Select, static_cast(PositionAction::GpsFormat)}, {"Smart Position", OptionsAction::Select, static_cast(PositionAction::GPSSmartPosition)}, {"Update Interval", OptionsAction::Select, static_cast(PositionAction::GPSUpdateInterval)}, {"Broadcast Interval", OptionsAction::Select, static_cast(PositionAction::GPSPositionBroadcast)}, {"Compass", OptionsAction::Select, static_cast(PositionAction::CompassMenu)}, }; static const PositionMenuOption calibrateOptions[] = { {"Back", OptionsAction::Back}, {"On/Off Toggle", OptionsAction::Select, static_cast(PositionAction::GpsToggle)}, {"Format", OptionsAction::Select, static_cast(PositionAction::GpsFormat)}, {"Smart Position", OptionsAction::Select, static_cast(PositionAction::GPSSmartPosition)}, {"Update Interval", OptionsAction::Select, static_cast(PositionAction::GPSUpdateInterval)}, {"Broadcast Interval", OptionsAction::Select, static_cast(PositionAction::GPSPositionBroadcast)}, {"Compass", OptionsAction::Select, static_cast(PositionAction::CompassMenu)}, {"Compass Calibrate", OptionsAction::Select, static_cast(PositionAction::CompassCalibrate)}, }; constexpr size_t baseCount = sizeof(baseOptions) / sizeof(baseOptions[0]); constexpr size_t calibrateCount = sizeof(calibrateOptions) / sizeof(calibrateOptions[0]); static std::array baseLabels{}; static std::array calibrateLabels{}; auto onSelection = [](const PositionMenuOption &option, int) -> void { if (option.action == OptionsAction::Back) { return; } if (!option.hasValue) { return; } auto action = static_cast(option.value); switch (action) { case PositionAction::GpsToggle: menuQueue = GpsToggleMenu; screen->runNow(); break; case PositionAction::GpsFormat: menuQueue = GpsFormatMenu; screen->runNow(); break; case PositionAction::CompassMenu: menuQueue = CompassPointNorthMenu; screen->runNow(); break; case PositionAction::CompassCalibrate: if (accelerometerThread) { accelerometerThread->calibrate(30); } break; case PositionAction::GPSSmartPosition: menuQueue = GpsSmartPositionMenu; screen->runNow(); break; case PositionAction::GPSUpdateInterval: menuQueue = GpsUpdateIntervalMenu; screen->runNow(); break; case PositionAction::GPSPositionBroadcast: menuQueue = GpsPositionBroadcastMenu; screen->runNow(); break; } }; BannerOverlayOptions bannerOptions; if (accelerometerThread) { bannerOptions = createStaticBannerOptions("GPS Action", calibrateOptions, calibrateLabels, onSelection); } else { bannerOptions = createStaticBannerOptions("GPS Action", baseOptions, baseLabels, onSelection); } screen->showOverlayBanner(bannerOptions); } void menuHandler::nodeListMenu() { enum optionsNumbers { Back, NodePicker, TraceRoute, Verify, Reset, NodeNameLength, enumEnd }; static const char *optionsArray[enumEnd] = {"Back"}; static int optionsEnumArray[enumEnd] = {Back}; int options = 1; optionsArray[options] = "Node Actions / Settings"; optionsEnumArray[options++] = NodePicker; if (currentResolution != ScreenResolution::UltraLow) { optionsArray[options] = "Show Long/Short Name"; optionsEnumArray[options++] = NodeNameLength; } optionsArray[options] = "Reset NodeDB"; optionsEnumArray[options++] = Reset; BannerOverlayOptions bannerOptions; bannerOptions.message = "Node Action"; bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsCount = options; bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == NodePicker) { menuQueue = NodePickerMenu; screen->runNow(); } else if (selected == Reset) { menuQueue = ResetNodeDbMenu; screen->runNow(); } else if (selected == NodeNameLength) { menuHandler::menuQueue = menuHandler::NodeNameLengthMenu; screen->runNow(); } }; screen->showOverlayBanner(bannerOptions); } void menuHandler::NodePicker() { const char *NODE_PICKER_TITLE; if (currentResolution == ScreenResolution::UltraLow) { NODE_PICKER_TITLE = "Pick Node"; } else { NODE_PICKER_TITLE = "Pick A Node"; } screen->showNodePicker(NODE_PICKER_TITLE, 30000, [](uint32_t nodenum) -> void { LOG_INFO("Nodenum: %u", nodenum); // Store the selection so the Manage Node menu knows which node to operate on menuHandler::pickedNodeNum = nodenum; // Keep UI favorite context in sync (used elsewhere for some node-based actions) graphics::UIRenderer::currentFavoriteNodeNum = nodenum; menuQueue = ManageNodeMenu; screen->runNow(); }); } void menuHandler::manageNodeMenu() { // If we don't have a node selected yet, go fast exit auto node = nodeDB->getMeshNode(menuHandler::pickedNodeNum); if (!node) { return; } enum optionsNumbers { Back, Favorite, Mute, TraceRoute, KeyVerification, Ignore, enumEnd }; static const char *optionsArray[enumEnd] = {"Back"}; static int optionsEnumArray[enumEnd] = {Back}; int options = 1; if (node->is_favorite) { optionsArray[options] = "Unfavorite"; } else { optionsArray[options] = "Favorite"; } optionsEnumArray[options++] = Favorite; bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0; if (isMuted) { optionsArray[options] = "Unmute Notifications"; } else { optionsArray[options] = "Mute Notifications"; } optionsEnumArray[options++] = Mute; optionsArray[options] = "Trace Route"; optionsEnumArray[options++] = TraceRoute; optionsArray[options] = "Key Verification"; optionsEnumArray[options++] = KeyVerification; if (node->is_ignored) { optionsArray[options] = "Unignore Node"; } else { optionsArray[options] = "Ignore Node"; } optionsEnumArray[options++] = Ignore; BannerOverlayOptions bannerOptions; std::string title = ""; if (node->has_user && node->user.long_name && node->user.long_name[0]) { title += sanitizeString(node->user.long_name).substr(0, 15); } else { char buf[20]; snprintf(buf, sizeof(buf), "%08X", (unsigned int)node->num); title += buf; } bannerOptions.message = title.c_str(); bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsCount = options; bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == Back) { menuQueue = NodeBaseMenu; screen->runNow(); return; } if (selected == Favorite) { const auto *n = nodeDB->getMeshNode(menuHandler::pickedNodeNum); if (!n) { return; } if (n->is_favorite) { LOG_INFO("Removing node %08X from favorites", menuHandler::pickedNodeNum); nodeDB->set_favorite(false, menuHandler::pickedNodeNum); } else { LOG_INFO("Adding node %08X to favorites", menuHandler::pickedNodeNum); nodeDB->set_favorite(true, menuHandler::pickedNodeNum); } screen->setFrames(graphics::Screen::FOCUS_PRESERVE); return; } if (selected == Mute) { auto n = nodeDB->getMeshNode(menuHandler::pickedNodeNum); if (!n) { return; } if (n->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) { n->bitfield &= ~NODEINFO_BITFIELD_IS_MUTED_MASK; LOG_INFO("Unmuted node %08X", menuHandler::pickedNodeNum); } else { n->bitfield |= NODEINFO_BITFIELD_IS_MUTED_MASK; LOG_INFO("Muted node %08X", menuHandler::pickedNodeNum); } nodeDB->notifyObservers(true); nodeDB->saveToDisk(); screen->setFrames(graphics::Screen::FOCUS_PRESERVE); return; } if (selected == TraceRoute) { LOG_INFO("Starting traceroute to %08X", menuHandler::pickedNodeNum); if (traceRouteModule) { traceRouteModule->startTraceRoute(menuHandler::pickedNodeNum); } return; } if (selected == KeyVerification) { LOG_INFO("Initiating key verification with %08X", menuHandler::pickedNodeNum); if (keyVerificationModule) { keyVerificationModule->sendInitialRequest(menuHandler::pickedNodeNum); } return; } if (selected == Ignore) { auto n = nodeDB->getMeshNode(menuHandler::pickedNodeNum); if (!n) { return; } if (n->is_ignored) { n->is_ignored = false; LOG_INFO("Unignoring node %08X", menuHandler::pickedNodeNum); } else { n->is_ignored = true; LOG_INFO("Ignoring node %08X", menuHandler::pickedNodeNum); } nodeDB->notifyObservers(true); nodeDB->saveToDisk(); screen->setFrames(graphics::Screen::FOCUS_PRESERVE); return; } }; screen->showOverlayBanner(bannerOptions); } void menuHandler::nodeNameLengthMenu() { static const NodeNameOption nodeNameOptions[] = { {"Back", OptionsAction::Back}, {"Long", OptionsAction::Select, true}, {"Short", OptionsAction::Select, false}, }; constexpr size_t nodeNameCount = sizeof(nodeNameOptions) / sizeof(nodeNameOptions[0]); static std::array nodeNameLabels{}; auto bannerOptions = createStaticBannerOptions("Node Name Length", nodeNameOptions, nodeNameLabels, [](const NodeNameOption &option, int) -> void { if (option.action == OptionsAction::Back) { menuQueue = NodeBaseMenu; screen->runNow(); return; } if (!option.hasValue) { return; } if (config.display.use_long_node_name == option.value) { return; } config.display.use_long_node_name = option.value; saveUIConfig(); service->reloadConfig(SEGMENT_CONFIG); LOG_INFO("Setting names to %s", option.value ? "long" : "short"); }); int initialSelection = config.display.use_long_node_name ? 1 : 2; bannerOptions.InitialSelected = initialSelection; screen->showOverlayBanner(bannerOptions); } void menuHandler::resetNodeDBMenu() { static const char *optionsArray[] = {"Back", "Reset All", "Preserve Favorites"}; BannerOverlayOptions bannerOptions; bannerOptions.message = "Confirm Reset NodeDB"; bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsCount = 3; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == 1 || selected == 2) { disableBluetooth(); screen->setFrames(Screen::FOCUS_DEFAULT); } if (selected == 1) { LOG_INFO("Initiate node-db reset"); nodeDB->resetNodes(); rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); } else if (selected == 2) { LOG_INFO("Initiate node-db reset but keeping favorites"); nodeDB->resetNodes(1); rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); } else if (selected == 0) { menuQueue = NodeBaseMenu; screen->runNow(); } }; screen->showOverlayBanner(bannerOptions); } void menuHandler::compassNorthMenu() { static const CompassOption compassOptions[] = { {"Back", OptionsAction::Back}, {"Dynamic", OptionsAction::Select, meshtastic_CompassMode_DYNAMIC}, {"Fixed Ring", OptionsAction::Select, meshtastic_CompassMode_FIXED_RING}, {"Freeze Heading", OptionsAction::Select, meshtastic_CompassMode_FREEZE_HEADING}, }; constexpr size_t compassCount = sizeof(compassOptions) / sizeof(compassOptions[0]); static std::array compassLabels{}; auto bannerOptions = createStaticBannerOptions("North Directions?", compassOptions, compassLabels, [](const CompassOption &option, int) -> void { if (option.action == OptionsAction::Back) { menuQueue = PositionBaseMenu; screen->runNow(); return; } if (!option.hasValue) { return; } if (uiconfig.compass_mode == option.value) { return; } uiconfig.compass_mode = option.value; saveUIConfig(); screen->setFrames(graphics::Screen::FOCUS_PRESERVE); }); int initialSelection = 0; for (size_t i = 0; i < compassCount; ++i) { if (compassOptions[i].hasValue && uiconfig.compass_mode == compassOptions[i].value) { initialSelection = static_cast(i); break; } } bannerOptions.InitialSelected = initialSelection; screen->showOverlayBanner(bannerOptions); } #if !MESHTASTIC_EXCLUDE_GPS void menuHandler::GPSToggleMenu() { static const GPSToggleOption gpsToggleOptions[] = { {"Back", OptionsAction::Back}, {"Enabled", OptionsAction::Select, meshtastic_Config_PositionConfig_GpsMode_ENABLED}, {"Disabled", OptionsAction::Select, meshtastic_Config_PositionConfig_GpsMode_DISABLED}, }; constexpr size_t toggleCount = sizeof(gpsToggleOptions) / sizeof(gpsToggleOptions[0]); static std::array toggleLabels{}; auto bannerOptions = createStaticBannerOptions("Toggle GPS", gpsToggleOptions, toggleLabels, [](const GPSToggleOption &option, int) -> void { if (option.action == OptionsAction::Back) { menuQueue = PositionBaseMenu; screen->runNow(); return; } if (!option.hasValue) { return; } if (config.position.gps_mode == option.value) { return; } config.position.gps_mode = option.value; if (option.value == meshtastic_Config_PositionConfig_GpsMode_ENABLED) { playGPSEnableBeep(); gps->enable(); } else { playGPSDisableBeep(); gps->disable(); } service->reloadConfig(SEGMENT_CONFIG); }); int initialSelection = 0; for (size_t i = 0; i < toggleCount; ++i) { if (gpsToggleOptions[i].hasValue && config.position.gps_mode == gpsToggleOptions[i].value) { initialSelection = static_cast(i); break; } } bannerOptions.InitialSelected = initialSelection; screen->showOverlayBanner(bannerOptions); } void menuHandler::GPSFormatMenu() { static const GPSFormatOption formatOptionsHigh[] = { {"Back", OptionsAction::Back}, {"Decimal Degrees", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_DEC}, {"Degrees Minutes Seconds", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_DMS}, {"Universal Transverse Mercator", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_UTM}, {"Military Grid Reference System", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_MGRS}, {"Open Location Code", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_OLC}, {"Ordnance Survey Grid Ref", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_OSGR}, {"Maidenhead Locator", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_MLS}, }; static const GPSFormatOption formatOptionsLow[] = { {"Back", OptionsAction::Back}, {"DEC", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_DEC}, {"DMS", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_DMS}, {"UTM", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_UTM}, {"MGRS", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_MGRS}, {"OLC", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_OLC}, {"OSGR", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_OSGR}, {"MLS", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_MLS}, }; constexpr size_t formatCount = sizeof(formatOptionsHigh) / sizeof(formatOptionsHigh[0]); static std::array formatLabelsHigh{}; static std::array formatLabelsLow{}; auto onSelection = [](const GPSFormatOption &option, int) -> void { if (option.action == OptionsAction::Back) { menuQueue = PositionBaseMenu; screen->runNow(); return; } if (!option.hasValue) { return; } if (uiconfig.gps_format == option.value) { return; } uiconfig.gps_format = option.value; saveUIConfig(); service->reloadConfig(SEGMENT_CONFIG); }; BannerOverlayOptions bannerOptions; int initialSelection = 0; if (currentResolution == ScreenResolution::High) { bannerOptions = createStaticBannerOptions("GPS Format", formatOptionsHigh, formatLabelsHigh, onSelection); for (size_t i = 0; i < formatCount; ++i) { if (formatOptionsHigh[i].hasValue && uiconfig.gps_format == formatOptionsHigh[i].value) { initialSelection = static_cast(i); break; } } } else { bannerOptions = createStaticBannerOptions("GPS Format", formatOptionsLow, formatLabelsLow, onSelection); for (size_t i = 0; i < formatCount; ++i) { if (formatOptionsLow[i].hasValue && uiconfig.gps_format == formatOptionsLow[i].value) { initialSelection = static_cast(i); break; } } } bannerOptions.InitialSelected = initialSelection; screen->showOverlayBanner(bannerOptions); } void menuHandler::GPSSmartPositionMenu() { static const char *optionsArray[] = {"Back", "Enabled", "Disabled"}; BannerOverlayOptions bannerOptions; bannerOptions.message = "Toggle Smart Position"; if (currentResolution == ScreenResolution::UltraLow) { bannerOptions.message = "Smrt Postn"; } bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsCount = 3; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == 0) { menuQueue = PositionBaseMenu; screen->runNow(); } else if (selected == 1) { config.position.position_broadcast_smart_enabled = true; saveUIConfig(); service->reloadConfig(SEGMENT_CONFIG); rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); } else if (selected == 2) { config.position.position_broadcast_smart_enabled = false; saveUIConfig(); service->reloadConfig(SEGMENT_CONFIG); rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); } }; bannerOptions.InitialSelected = config.position.position_broadcast_smart_enabled ? 1 : 2; screen->showOverlayBanner(bannerOptions); } void menuHandler::GPSUpdateIntervalMenu() { static const char *optionsArray[] = {"Back", "8 seconds", "20 seconds", "40 seconds", "1 minute", "80 seconds", "2 minutes", "5 minutes", "10 minutes", "15 minutes", "30 minutes", "1 hour", "6 hours", "12 hours", "24 hours", "At Boot Only"}; BannerOverlayOptions bannerOptions; bannerOptions.message = "Update Interval"; bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsCount = 16; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == 0) { menuQueue = PositionBaseMenu; screen->runNow(); } else if (selected == 1) { config.position.gps_update_interval = 8; } else if (selected == 2) { config.position.gps_update_interval = 20; } else if (selected == 3) { config.position.gps_update_interval = 40; } else if (selected == 4) { config.position.gps_update_interval = 60; } else if (selected == 5) { config.position.gps_update_interval = 80; } else if (selected == 6) { config.position.gps_update_interval = 120; } else if (selected == 7) { config.position.gps_update_interval = 300; } else if (selected == 8) { config.position.gps_update_interval = 600; } else if (selected == 9) { config.position.gps_update_interval = 900; } else if (selected == 10) { config.position.gps_update_interval = 1800; } else if (selected == 11) { config.position.gps_update_interval = 3600; } else if (selected == 12) { config.position.gps_update_interval = 21600; } else if (selected == 13) { config.position.gps_update_interval = 43200; } else if (selected == 14) { config.position.gps_update_interval = 86400; } else if (selected == 15) { config.position.gps_update_interval = 2147483647; // At Boot Only } if (selected != 0) { saveUIConfig(); service->reloadConfig(SEGMENT_CONFIG); rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); } }; if (config.position.gps_update_interval == 8) { bannerOptions.InitialSelected = 1; } else if (config.position.gps_update_interval == 20) { bannerOptions.InitialSelected = 2; } else if (config.position.gps_update_interval == 40) { bannerOptions.InitialSelected = 3; } else if (config.position.gps_update_interval == 60) { bannerOptions.InitialSelected = 4; } else if (config.position.gps_update_interval == 80) { bannerOptions.InitialSelected = 5; } else if (config.position.gps_update_interval == 120) { bannerOptions.InitialSelected = 6; } else if (config.position.gps_update_interval == 300) { bannerOptions.InitialSelected = 7; } else if (config.position.gps_update_interval == 600) { bannerOptions.InitialSelected = 8; } else if (config.position.gps_update_interval == 900) { bannerOptions.InitialSelected = 9; } else if (config.position.gps_update_interval == 1800) { bannerOptions.InitialSelected = 10; } else if (config.position.gps_update_interval == 3600) { bannerOptions.InitialSelected = 11; } else if (config.position.gps_update_interval == 21600) { bannerOptions.InitialSelected = 12; } else if (config.position.gps_update_interval == 43200) { bannerOptions.InitialSelected = 13; } else if (config.position.gps_update_interval == 86400) { bannerOptions.InitialSelected = 14; } else if (config.position.gps_update_interval == 2147483647) { // At Boot Only bannerOptions.InitialSelected = 15; } else { bannerOptions.InitialSelected = 0; } screen->showOverlayBanner(bannerOptions); } void menuHandler::GPSPositionBroadcastMenu() { static const char *optionsArray[] = {"Back", "1 minute", "90 seconds", "5 minutes", "15 minutes", "1 hour", "2 hours", "3 hours", "4 hours", "5 hours", "6 hours", "12 hours", "18 hours", "24 hours", "36 hours", "48 hours", "72 hours"}; BannerOverlayOptions bannerOptions; bannerOptions.message = "Broadcast Interval"; bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsCount = 17; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == 0) { menuQueue = PositionBaseMenu; screen->runNow(); } else if (selected == 1) { config.position.position_broadcast_secs = 60; } else if (selected == 2) { config.position.position_broadcast_secs = 90; } else if (selected == 3) { config.position.position_broadcast_secs = 300; } else if (selected == 4) { config.position.position_broadcast_secs = 900; } else if (selected == 5) { config.position.position_broadcast_secs = 3600; } else if (selected == 6) { config.position.position_broadcast_secs = 7200; } else if (selected == 7) { config.position.position_broadcast_secs = 10800; } else if (selected == 8) { config.position.position_broadcast_secs = 14400; } else if (selected == 9) { config.position.position_broadcast_secs = 18000; } else if (selected == 10) { config.position.position_broadcast_secs = 21600; } else if (selected == 11) { config.position.position_broadcast_secs = 43200; } else if (selected == 12) { config.position.position_broadcast_secs = 64800; } else if (selected == 13) { config.position.position_broadcast_secs = 86400; } else if (selected == 14) { config.position.position_broadcast_secs = 129600; } else if (selected == 15) { config.position.position_broadcast_secs = 172800; } else if (selected == 16) { config.position.position_broadcast_secs = 259200; } if (selected != 0) { saveUIConfig(); service->reloadConfig(SEGMENT_CONFIG); rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); } }; if (config.position.position_broadcast_secs == 60) { bannerOptions.InitialSelected = 1; } else if (config.position.position_broadcast_secs == 90) { bannerOptions.InitialSelected = 2; } else if (config.position.position_broadcast_secs == 300) { bannerOptions.InitialSelected = 3; } else if (config.position.position_broadcast_secs == 900) { bannerOptions.InitialSelected = 4; } else if (config.position.position_broadcast_secs == 3600) { bannerOptions.InitialSelected = 5; } else if (config.position.position_broadcast_secs == 7200) { bannerOptions.InitialSelected = 6; } else if (config.position.position_broadcast_secs == 10800) { bannerOptions.InitialSelected = 7; } else if (config.position.position_broadcast_secs == 14400) { bannerOptions.InitialSelected = 8; } else if (config.position.position_broadcast_secs == 18000) { bannerOptions.InitialSelected = 9; } else if (config.position.position_broadcast_secs == 21600) { bannerOptions.InitialSelected = 10; } else if (config.position.position_broadcast_secs == 43200) { bannerOptions.InitialSelected = 11; } else if (config.position.position_broadcast_secs == 64800) { bannerOptions.InitialSelected = 12; } else if (config.position.position_broadcast_secs == 86400) { bannerOptions.InitialSelected = 13; } else if (config.position.position_broadcast_secs == 129600) { bannerOptions.InitialSelected = 14; } else if (config.position.position_broadcast_secs == 172800) { bannerOptions.InitialSelected = 15; } else if (config.position.position_broadcast_secs == 259200) { bannerOptions.InitialSelected = 16; } else { bannerOptions.InitialSelected = 0; } screen->showOverlayBanner(bannerOptions); } #endif void menuHandler::bluetoothToggleMenu() { static const char *optionsArray[] = {"Back", "Enabled", "Disabled"}; BannerOverlayOptions bannerOptions; bannerOptions.message = "Toggle Bluetooth"; if (currentResolution == ScreenResolution::UltraLow) { bannerOptions.message = "Bluetooth"; } bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsCount = 3; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == 0) return; else if (selected != (config.bluetooth.enabled ? 1 : 2)) { InputEvent event = {.inputEvent = (input_broker_event)170, .kbchar = 170, .touchX = 0, .touchY = 0}; inputBroker->injectInputEvent(&event); } }; bannerOptions.InitialSelected = config.bluetooth.enabled ? 1 : 2; screen->showOverlayBanner(bannerOptions); } void menuHandler::BuzzerModeMenu() { static const char *optionsArray[] = {"All Enabled", "All Disabled", "Notifications", "System Only", "DMs Only"}; BannerOverlayOptions bannerOptions; bannerOptions.message = "Notification Sounds"; bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsCount = 5; bannerOptions.bannerCallback = [](int selected) -> void { config.device.buzzer_mode = (meshtastic_Config_DeviceConfig_BuzzerMode)selected; service->reloadConfig(SEGMENT_CONFIG); }; bannerOptions.InitialSelected = config.device.buzzer_mode; screen->showOverlayBanner(bannerOptions); } void menuHandler::BrightnessPickerMenu() { static const char *optionsArray[] = {"Back", "Low", "Medium", "High"}; // Get current brightness level to set initial selection int currentSelection = 1; // Default to Medium if (uiconfig.screen_brightness >= 255) { currentSelection = 3; // Very High } else if (uiconfig.screen_brightness >= 128) { currentSelection = 2; // High } else { currentSelection = 1; // Medium } BannerOverlayOptions bannerOptions; bannerOptions.message = "Brightness"; bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsCount = 4; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == 1) { // Medium uiconfig.screen_brightness = 64; } else if (selected == 2) { // High uiconfig.screen_brightness = 128; } else if (selected == 3) { // Very High uiconfig.screen_brightness = 255; } if (selected != 0) { // Not "Back" // Apply brightness immediately #if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) // For HELTEC devices, use analogWrite to control backlight analogWrite(VTFT_LEDA, uiconfig.screen_brightness); #elif defined(ST7789_CS) || defined(ST7796_CS) static_cast(screen->getDisplayDevice())->setDisplayBrightness(uiconfig.screen_brightness); #elif defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107) screen->getDisplayDevice()->setBrightness(uiconfig.screen_brightness); #endif // Save to device saveUIConfig(); LOG_INFO("Screen brightness set to %d", uiconfig.screen_brightness); } }; bannerOptions.InitialSelected = currentSelection; screen->showOverlayBanner(bannerOptions); } void menuHandler::switchToMUIMenu() { static const char *optionsArray[] = {"No", "Yes"}; BannerOverlayOptions bannerOptions; bannerOptions.message = "Switch to MUI?"; bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsCount = 2; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == 1) { config.display.displaymode = meshtastic_Config_DisplayConfig_DisplayMode_COLOR; config.bluetooth.enabled = false; service->reloadConfig(SEGMENT_CONFIG); rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); } }; screen->showOverlayBanner(bannerOptions); } void menuHandler::TFTColorPickerMenu(OLEDDisplay *display) { static const ScreenColorOption colorOptions[] = { {"Back", OptionsAction::Back}, {"Default", OptionsAction::Select, ScreenColor(0, 0, 0, true)}, {"Meshtastic Green", OptionsAction::Select, ScreenColor(0x67, 0xEA, 0x94)}, {"Yellow", OptionsAction::Select, ScreenColor(255, 255, 128)}, {"Red", OptionsAction::Select, ScreenColor(255, 64, 64)}, {"Orange", OptionsAction::Select, ScreenColor(255, 160, 20)}, {"Purple", OptionsAction::Select, ScreenColor(204, 153, 255)}, {"Blue", OptionsAction::Select, ScreenColor(0, 0, 255)}, {"Teal", OptionsAction::Select, ScreenColor(16, 102, 102)}, {"Cyan", OptionsAction::Select, ScreenColor(0, 255, 255)}, {"Ice", OptionsAction::Select, ScreenColor(173, 216, 230)}, {"Pink", OptionsAction::Select, ScreenColor(255, 105, 180)}, {"White", OptionsAction::Select, ScreenColor(255, 255, 255)}, {"Gray", OptionsAction::Select, ScreenColor(128, 128, 128)}, }; constexpr size_t colorCount = sizeof(colorOptions) / sizeof(colorOptions[0]); static std::array colorLabels{}; auto bannerOptions = createStaticBannerOptions( "Select Screen Color", colorOptions, colorLabels, [display](const ScreenColorOption &option, int) -> void { if (option.action == OptionsAction::Back) { menuQueue = SystemBaseMenu; screen->runNow(); return; } if (!option.hasValue) { return; } #if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || defined(T_DECK) || defined(T_LORA_PAGER) || \ HAS_TFT || defined(HACKADAY_COMMUNICATOR) const ScreenColor &color = option.value; if (color.useVariant) { LOG_INFO("Setting color to system default or defined variant"); } else { LOG_INFO("Setting color to %s", option.label); } uint8_t r = color.r; uint8_t g = color.g; uint8_t b = color.b; display->setColor(BLACK); display->fillRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT); display->setColor(WHITE); if (color.useVariant || (r == 0 && g == 0 && b == 0)) { #ifdef TFT_MESH_OVERRIDE TFT_MESH = TFT_MESH_OVERRIDE; #else TFT_MESH = COLOR565(255, 255, 128); #endif } else { TFT_MESH = COLOR565(r, g, b); } #if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) static_cast(screen->getDisplayDevice())->setRGB(TFT_MESH); #endif screen->setFrames(graphics::Screen::FOCUS_SYSTEM); if (color.useVariant || (r == 0 && g == 0 && b == 0)) { uiconfig.screen_rgb_color = 0; } else { uiconfig.screen_rgb_color = (static_cast(r) << 16) | (static_cast(g) << 8) | static_cast(b); } LOG_INFO("Storing Value of %d to uiconfig.screen_rgb_color", uiconfig.screen_rgb_color); saveUIConfig(); #endif }); int initialSelection = 0; if (uiconfig.screen_rgb_color == 0) { initialSelection = 1; } else { uint32_t currentColor = uiconfig.screen_rgb_color; for (size_t i = 0; i < colorCount; ++i) { if (!colorOptions[i].hasValue) { continue; } const ScreenColor &color = colorOptions[i].value; if (color.useVariant) { continue; } uint32_t encoded = (static_cast(color.r) << 16) | (static_cast(color.g) << 8) | static_cast(color.b); if (encoded == currentColor) { initialSelection = static_cast(i); break; } } } bannerOptions.InitialSelected = initialSelection; screen->showOverlayBanner(bannerOptions); } void menuHandler::rebootMenu() { static const char *optionsArray[] = {"Back", "Confirm"}; BannerOverlayOptions bannerOptions; bannerOptions.message = "Reboot Device?"; if (currentResolution == ScreenResolution::UltraLow) { bannerOptions.message = "Reboot"; } bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsCount = 2; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == 1) { IF_SCREEN(screen->showSimpleBanner("Rebooting...", 0)); nodeDB->saveToDisk(); messageStore.saveToFlash(); rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; } else { menuQueue = PowerMenu; screen->runNow(); } }; screen->showOverlayBanner(bannerOptions); } void menuHandler::shutdownMenu() { static const char *optionsArray[] = {"Back", "Confirm"}; BannerOverlayOptions bannerOptions; bannerOptions.message = "Shutdown Device?"; if (currentResolution == ScreenResolution::UltraLow) { bannerOptions.message = "Shutdown"; } bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsCount = 2; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == 1) { InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_SHUTDOWN, .kbchar = 0, .touchX = 0, .touchY = 0}; inputBroker->injectInputEvent(&event); } else { menuQueue = PowerMenu; screen->runNow(); } }; screen->showOverlayBanner(bannerOptions); } void menuHandler::removeFavoriteMenu() { static const char *optionsArray[] = {"Back", "Yes"}; BannerOverlayOptions bannerOptions; std::string message = "Unfavorite This Node?\n"; auto node = nodeDB->getMeshNode(graphics::UIRenderer::currentFavoriteNodeNum); if (node && node->has_user) { message += sanitizeString(node->user.long_name).substr(0, 15); } bannerOptions.message = message.c_str(); bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsCount = 2; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == 1) { LOG_INFO("Removing %x as favorite node", graphics::UIRenderer::currentFavoriteNodeNum); nodeDB->set_favorite(false, graphics::UIRenderer::currentFavoriteNodeNum); screen->setFrames(graphics::Screen::FOCUS_DEFAULT); } }; screen->showOverlayBanner(bannerOptions); } void menuHandler::traceRouteMenu() { screen->showNodePicker("Node to Trace", 30000, [](uint32_t nodenum) -> void { LOG_INFO("Menu: Node picker selected node 0x%08x, traceRouteModule=%p", nodenum, traceRouteModule); if (traceRouteModule) { traceRouteModule->startTraceRoute(nodenum); } }); } void menuHandler::testMenu() { enum optionsNumbers { Back, NumberPicker, ShowChirpy }; static const char *optionsArray[4] = {"Back"}; static int optionsEnumArray[4] = {Back}; int options = 1; optionsArray[options] = "Number Picker"; optionsEnumArray[options++] = NumberPicker; optionsArray[options] = screen->isFrameHidden("chirpy") ? "Show Chirpy" : "Hide Chirpy"; optionsEnumArray[options++] = ShowChirpy; BannerOverlayOptions bannerOptions; bannerOptions.message = "Hidden Test Menu"; bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsCount = options; bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == NumberPicker) { menuQueue = NumberTest; screen->runNow(); } else if (selected == ShowChirpy) { screen->toggleFrameVisibility("chirpy"); screen->setFrames(Screen::FOCUS_SYSTEM); } else { menuQueue = SystemBaseMenu; screen->runNow(); } }; screen->showOverlayBanner(bannerOptions); } void menuHandler::numberTest() { screen->showNumberPicker("Pick a number\n ", 30000, 4, [](int number_picked) -> void { LOG_WARN("Nodenum: %u", number_picked); }); } void menuHandler::wifiBaseMenu() { enum optionsNumbers { Back, Wifi_toggle }; static const char *optionsArray[] = {"Back", "WiFi Toggle"}; BannerOverlayOptions bannerOptions; bannerOptions.message = "WiFi Menu"; bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsCount = 2; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == Wifi_toggle) { menuQueue = WifiToggleMenu; screen->runNow(); } }; screen->showOverlayBanner(bannerOptions); } void menuHandler::wifiToggleMenu() { enum optionsNumbers { Back, Wifi_disable, Wifi_enable }; static const char *optionsArray[] = {"Back", "WiFi Disabled", "WiFi Enabled"}; BannerOverlayOptions bannerOptions; bannerOptions.message = "WiFi Actions"; bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsCount = 3; if (config.network.wifi_enabled == true) bannerOptions.InitialSelected = 2; else bannerOptions.InitialSelected = 1; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == Wifi_disable) { config.network.wifi_enabled = false; config.bluetooth.enabled = true; service->reloadConfig(SEGMENT_CONFIG); rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); } else if (selected == Wifi_enable) { config.network.wifi_enabled = true; config.bluetooth.enabled = false; service->reloadConfig(SEGMENT_CONFIG); rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); } }; screen->showOverlayBanner(bannerOptions); } void menuHandler::screenOptionsMenu() { // Check if brightness is supported #if defined(T_DECK) // TDeck Doesn't seem to support brightness at all, at least not reliably bool hasSupportBrightness = false; #elif defined(ST7789_CS) || defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107) bool hasSupportBrightness = true; #else bool hasSupportBrightness = false; #endif enum optionsNumbers { Back, Brightness, ScreenColor, FrameToggles, DisplayUnits, MessageBubbles }; static const char *optionsArray[6] = {"Back"}; static int optionsEnumArray[6] = {Back}; int options = 1; // Only show brightness for B&W displays if (hasSupportBrightness) { optionsArray[options] = "Brightness"; optionsEnumArray[options++] = Brightness; } // Only show screen color for TFT displays #if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || defined(T_DECK) || defined(T_LORA_PAGER) || \ HAS_TFT || defined(HACKADAY_COMMUNICATOR) optionsArray[options] = "Screen Color"; optionsEnumArray[options++] = ScreenColor; #endif optionsArray[options] = "Frame Visibility"; optionsEnumArray[options++] = FrameToggles; optionsArray[options] = "Display Units"; optionsEnumArray[options++] = DisplayUnits; optionsArray[options] = "Message Bubbles"; optionsEnumArray[options++] = MessageBubbles; BannerOverlayOptions bannerOptions; bannerOptions.message = "Display Options"; bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsCount = options; bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == Brightness) { menuHandler::menuQueue = menuHandler::BrightnessPicker; screen->runNow(); } else if (selected == ScreenColor) { menuHandler::menuQueue = menuHandler::TftColorMenuPicker; screen->runNow(); } else if (selected == FrameToggles) { menuHandler::menuQueue = menuHandler::FrameToggles; screen->runNow(); } else if (selected == DisplayUnits) { menuHandler::menuQueue = menuHandler::DisplayUnits; screen->runNow(); } else if (selected == MessageBubbles) { menuHandler::menuQueue = menuHandler::MessageBubblesMenu; screen->runNow(); } else { menuQueue = SystemBaseMenu; screen->runNow(); } }; screen->showOverlayBanner(bannerOptions); } void menuHandler::powerMenu() { enum optionsNumbers { Back, Reboot, Shutdown, MUI }; static const char *optionsArray[4] = {"Back"}; static int optionsEnumArray[4] = {Back}; int options = 1; optionsArray[options] = "Reboot"; optionsEnumArray[options++] = Reboot; optionsArray[options] = "Shutdown"; optionsEnumArray[options++] = Shutdown; #if HAS_TFT optionsArray[options] = "Switch to MUI"; optionsEnumArray[options++] = MUI; #endif BannerOverlayOptions bannerOptions; bannerOptions.message = "Reboot / Shutdown"; if (currentResolution == ScreenResolution::UltraLow) { bannerOptions.message = "Power"; } bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsCount = options; bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == Reboot) { menuHandler::menuQueue = menuHandler::RebootMenu; screen->runNow(); } else if (selected == Shutdown) { menuHandler::menuQueue = menuHandler::ShutdownMenu; screen->runNow(); } else if (selected == MUI) { menuHandler::menuQueue = menuHandler::MuiPicker; screen->runNow(); } else { menuQueue = SystemBaseMenu; screen->runNow(); } }; screen->showOverlayBanner(bannerOptions); } void menuHandler::keyVerificationInitMenu() { screen->showNodePicker("Node to Verify", 30000, [](uint32_t selected) -> void { keyVerificationModule->sendInitialRequest(selected); }); } void menuHandler::keyVerificationFinalPrompt() { char message[40] = {0}; memset(message, 0, sizeof(message)); sprintf(message, "Verification: \n"); keyVerificationModule->generateVerificationCode(message + 15); // send the toPhone packet if (screen) { static const char *optionsArray[] = {"Reject", "Accept"}; graphics::BannerOverlayOptions options; options.message = message; options.durationMs = 30000; options.optionsArrayPtr = optionsArray; options.optionsCount = 2; options.notificationType = graphics::notificationTypeEnum::selection_picker; options.bannerCallback = [=](int selected) { if (selected == 1) { auto remoteNodePtr = nodeDB->getMeshNode(keyVerificationModule->getCurrentRemoteNode()); remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK; } }; screen->showOverlayBanner(options); } } void menuHandler::frameTogglesMenu() { enum optionsNumbers { Finish, nodelist_nodes, nodelist_location, nodelist_lastheard, nodelist_hopsignal, nodelist_distance, nodelist_bearings, gps_position, lora, clock, show_favorites, show_env_telemetry, show_aq_telemetry, show_power, enumEnd }; static const char *optionsArray[enumEnd] = {"Finish"}; static int optionsEnumArray[enumEnd] = {Finish}; int options = 1; // Track last selected index (not enum value!) static int lastSelectedIndex = 0; #ifndef USE_EINK optionsArray[options] = screen->isFrameHidden("nodelist_nodes") ? "Show Node Lists" : "Hide Node Lists"; optionsEnumArray[options++] = nodelist_nodes; #else optionsArray[options] = screen->isFrameHidden("nodelist_lastheard") ? "Show NL - Last Heard" : "Hide NL - Last Heard"; optionsEnumArray[options++] = nodelist_lastheard; optionsArray[options] = screen->isFrameHidden("nodelist_hopsignal") ? "Show NL - Hops/Signal" : "Hide NL - Hops/Signal"; optionsEnumArray[options++] = nodelist_hopsignal; #endif #if HAS_GPS #ifndef USE_EINK optionsArray[options] = screen->isFrameHidden("nodelist_location") ? "Show Position Lists" : "Hide Position Lists"; optionsEnumArray[options++] = nodelist_location; #else optionsArray[options] = screen->isFrameHidden("nodelist_distance") ? "Show NL - Distance" : "Hide NL - Distance"; optionsEnumArray[options++] = nodelist_distance; optionsArray[options] = screen->isFrameHidden("nodelist_bearings") ? "Show NL - Bearings" : "Hide NL - Bearings"; optionsEnumArray[options++] = nodelist_bearings; #endif optionsArray[options] = screen->isFrameHidden("gps") ? "Show Position" : "Hide Position"; optionsEnumArray[options++] = gps_position; #endif optionsArray[options] = screen->isFrameHidden("lora") ? "Show LoRa" : "Hide LoRa"; optionsEnumArray[options++] = lora; optionsArray[options] = screen->isFrameHidden("clock") ? "Show Clock" : "Hide Clock"; optionsEnumArray[options++] = clock; optionsArray[options] = screen->isFrameHidden("show_favorites") ? "Show Favorites" : "Hide Favorites"; optionsEnumArray[options++] = show_favorites; optionsArray[options] = moduleConfig.telemetry.environment_screen_enabled ? "Hide Env. Telemetry" : "Show Env. Telemetry"; optionsEnumArray[options++] = show_env_telemetry; optionsArray[options] = moduleConfig.telemetry.air_quality_screen_enabled ? "Hide AQ Telemetry" : "Show AQ Telemetry"; optionsEnumArray[options++] = show_aq_telemetry; optionsArray[options] = moduleConfig.telemetry.power_screen_enabled ? "Hide Power" : "Show Power"; optionsEnumArray[options++] = show_power; BannerOverlayOptions bannerOptions; bannerOptions.message = "Show/Hide Frames"; bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsCount = options; bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.InitialSelected = lastSelectedIndex; // Use index, not enum value bannerOptions.bannerCallback = [options](int selected) mutable -> void { // Find the index of selected in optionsEnumArray int idx = 0; for (; idx < options; ++idx) { if (optionsEnumArray[idx] == selected) break; } lastSelectedIndex = idx; if (selected == Finish) { screen->setFrames(Screen::FOCUS_DEFAULT); } else if (selected == nodelist_nodes) { screen->toggleFrameVisibility("nodelist_nodes"); menuHandler::menuQueue = menuHandler::FrameToggles; screen->runNow(); } else if (selected == nodelist_location) { screen->toggleFrameVisibility("nodelist_location"); menuHandler::menuQueue = menuHandler::FrameToggles; screen->runNow(); } else if (selected == nodelist_lastheard) { screen->toggleFrameVisibility("nodelist_lastheard"); menuHandler::menuQueue = menuHandler::FrameToggles; screen->runNow(); } else if (selected == nodelist_hopsignal) { screen->toggleFrameVisibility("nodelist_hopsignal"); menuHandler::menuQueue = menuHandler::FrameToggles; screen->runNow(); } else if (selected == nodelist_distance) { screen->toggleFrameVisibility("nodelist_distance"); menuHandler::menuQueue = menuHandler::FrameToggles; screen->runNow(); } else if (selected == nodelist_bearings) { screen->toggleFrameVisibility("nodelist_bearings"); menuHandler::menuQueue = menuHandler::FrameToggles; screen->runNow(); } else if (selected == gps_position) { screen->toggleFrameVisibility("gps"); menuHandler::menuQueue = menuHandler::FrameToggles; screen->runNow(); } else if (selected == lora) { screen->toggleFrameVisibility("lora"); menuHandler::menuQueue = menuHandler::FrameToggles; screen->runNow(); } else if (selected == clock) { screen->toggleFrameVisibility("clock"); menuHandler::menuQueue = menuHandler::FrameToggles; screen->runNow(); } else if (selected == show_favorites) { screen->toggleFrameVisibility("show_favorites"); menuHandler::menuQueue = menuHandler::FrameToggles; screen->runNow(); } else if (selected == show_env_telemetry) { moduleConfig.telemetry.environment_screen_enabled = !moduleConfig.telemetry.environment_screen_enabled; menuHandler::menuQueue = menuHandler::FrameToggles; screen->runNow(); } else if (selected == show_aq_telemetry) { moduleConfig.telemetry.air_quality_screen_enabled = !moduleConfig.telemetry.air_quality_screen_enabled; menuHandler::menuQueue = menuHandler::FrameToggles; screen->runNow(); } else if (selected == show_power) { moduleConfig.telemetry.power_screen_enabled = !moduleConfig.telemetry.power_screen_enabled; menuHandler::menuQueue = menuHandler::FrameToggles; screen->runNow(); } }; screen->showOverlayBanner(bannerOptions); } void menuHandler::displayUnitsMenu() { enum optionsNumbers { Back, MetricUnits, ImperialUnits }; static const char *optionsArray[] = {"Back", "Metric", "Imperial"}; BannerOverlayOptions bannerOptions; bannerOptions.message = " Select display units"; bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsCount = 3; if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) bannerOptions.InitialSelected = 2; else bannerOptions.InitialSelected = 1; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == MetricUnits) { config.display.units = meshtastic_Config_DisplayConfig_DisplayUnits_METRIC; service->reloadConfig(SEGMENT_CONFIG); } else if (selected == ImperialUnits) { config.display.units = meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL; service->reloadConfig(SEGMENT_CONFIG); } else { menuHandler::menuQueue = menuHandler::ScreenOptionsMenu; screen->runNow(); } }; screen->showOverlayBanner(bannerOptions); } void menuHandler::messageBubblesMenu() { enum optionsNumbers { Back, ShowBubbles, HideBubbles }; static const char *optionsArray[] = {"Back", "Show Bubbles", "Hide Bubbles"}; BannerOverlayOptions bannerOptions; bannerOptions.message = "Message Bubbles"; bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsCount = 3; bannerOptions.InitialSelected = config.display.enable_message_bubbles ? 1 : 2; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == ShowBubbles) { config.display.enable_message_bubbles = true; service->reloadConfig(SEGMENT_CONFIG); LOG_INFO("Message bubbles enabled"); } else if (selected == HideBubbles) { config.display.enable_message_bubbles = false; service->reloadConfig(SEGMENT_CONFIG); LOG_INFO("Message bubbles disabled"); } else { menuHandler::menuQueue = menuHandler::ScreenOptionsMenu; screen->runNow(); } }; screen->showOverlayBanner(bannerOptions); } void menuHandler::handleMenuSwitch(OLEDDisplay *display) { if (menuQueue != MenuNone) test_count = 0; switch (menuQueue) { case MenuNone: break; case LoraMenu: loraMenu(); break; case LoraPicker: LoraRegionPicker(); break; case DeviceRolePicker: deviceRolePicker(); break; case RadioPresetPicker: radioPresetPicker(); break; case FrequencySlot: FrequencySlotPicker(); break; case NoTimeoutLoraPicker: LoraRegionPicker(0); break; case TzPicker: TZPicker(); break; case TwelveHourPicker: twelveHourPicker(); break; case ClockFacePicker: clockFacePicker(); break; case ClockMenu: clockMenu(); break; case SystemBaseMenu: systemBaseMenu(); break; case PositionBaseMenu: positionBaseMenu(); break; case NodeBaseMenu: nodeListMenu(); break; #if !MESHTASTIC_EXCLUDE_GPS case GpsToggleMenu: GPSToggleMenu(); break; case GpsFormatMenu: GPSFormatMenu(); break; case GpsSmartPositionMenu: GPSSmartPositionMenu(); break; case GpsUpdateIntervalMenu: GPSUpdateIntervalMenu(); break; case GpsPositionBroadcastMenu: GPSPositionBroadcastMenu(); break; #endif case CompassPointNorthMenu: compassNorthMenu(); break; case ResetNodeDbMenu: resetNodeDBMenu(); break; case BuzzerModeMenuPicker: BuzzerModeMenu(); break; case MuiPicker: switchToMUIMenu(); break; case TftColorMenuPicker: TFTColorPickerMenu(display); break; case BrightnessPicker: BrightnessPickerMenu(); break; case NodeNameLengthMenu: nodeNameLengthMenu(); break; case RebootMenu: rebootMenu(); break; case ShutdownMenu: shutdownMenu(); break; case NodePickerMenu: NodePicker(); break; case ManageNodeMenu: manageNodeMenu(); break; case RemoveFavorite: removeFavoriteMenu(); break; case TraceRouteMenu: traceRouteMenu(); break; case TestMenu: testMenu(); break; case NumberTest: numberTest(); break; case WifiToggleMenu: wifiToggleMenu(); break; case KeyVerificationInit: keyVerificationInitMenu(); break; case KeyVerificationFinalPrompt: keyVerificationFinalPrompt(); break; case BluetoothToggleMenu: bluetoothToggleMenu(); break; case ScreenOptionsMenu: screenOptionsMenu(); break; case PowerMenu: powerMenu(); break; case FrameToggles: frameTogglesMenu(); break; case DisplayUnits: displayUnitsMenu(); break; case ThrottleMessage: screen->showSimpleBanner("Too Many Attempts\nTry again in 60 seconds.", 5000); break; case MessageResponseMenu: messageResponseMenu(); break; case ReplyMenu: replyMenu(); break; case DeleteMessagesMenu: deleteMessagesMenu(); break; case MessageViewModeMenu: messageViewModeMenu(); break; case MessageBubblesMenu: messageBubblesMenu(); break; } menuQueue = MenuNone; } void menuHandler::saveUIConfig() { nodeDB->saveProto("/prefs/uiconfig.proto", meshtastic_DeviceUIConfig_size, &meshtastic_DeviceUIConfig_msg, &uiconfig); } } // namespace graphics #endif ================================================ FILE: src/graphics/draw/MenuHandler.h ================================================ #pragma once #if HAS_SCREEN #include "configuration.h" namespace graphics { class menuHandler { public: enum screenMenus { MenuNone, LoraMenu, LoraPicker, DeviceRolePicker, RadioPresetPicker, FrequencySlot, NoTimeoutLoraPicker, TzPicker, TwelveHourPicker, ClockFacePicker, ClockMenu, PositionBaseMenu, NodeBaseMenu, GpsToggleMenu, GpsFormatMenu, GpsSmartPositionMenu, GpsUpdateIntervalMenu, GpsPositionBroadcastMenu, CompassPointNorthMenu, ResetNodeDbMenu, BuzzerModeMenuPicker, MuiPicker, TftColorMenuPicker, BrightnessPicker, RebootMenu, ShutdownMenu, NodePickerMenu, ManageNodeMenu, RemoveFavorite, TestMenu, NumberTest, WifiToggleMenu, BluetoothToggleMenu, ScreenOptionsMenu, PowerMenu, SystemBaseMenu, KeyVerificationInit, KeyVerificationFinalPrompt, TraceRouteMenu, ThrottleMessage, MessageResponseMenu, MessageViewModeMenu, ReplyMenu, DeleteMessagesMenu, NodeNameLengthMenu, FrameToggles, DisplayUnits, MessageBubblesMenu }; static screenMenus menuQueue; static uint32_t pickedNodeNum; // node selected by NodePicker for ManageNodeMenu static void OnboardMessage(); static void LoraRegionPicker(uint32_t duration = 30000); static void loraMenu(); static void deviceRolePicker(); static void radioPresetPicker(); static void FrequencySlotPicker(); static void handleMenuSwitch(OLEDDisplay *display); static void showConfirmationBanner(const char *message, std::function onConfirm); static void clockMenu(); static void TZPicker(); static void twelveHourPicker(); static void clockFacePicker(); static void messageResponseMenu(); static void messageViewModeMenu(); static void replyMenu(); static void deleteMessagesMenu(); static void homeBaseMenu(); static void textMessageBaseMenu(); static void systemBaseMenu(); static void favoriteBaseMenu(); static void positionBaseMenu(); static void compassNorthMenu(); static void GPSToggleMenu(); static void GPSFormatMenu(); static void GPSSmartPositionMenu(); static void GPSUpdateIntervalMenu(); static void GPSPositionBroadcastMenu(); static void BuzzerModeMenu(); static void switchToMUIMenu(); static void TFTColorPickerMenu(OLEDDisplay *display); static void nodeListMenu(); static void resetNodeDBMenu(); static void BrightnessPickerMenu(); static void rebootMenu(); static void shutdownMenu(); static void NodePicker(); static void manageNodeMenu(); static void addFavoriteMenu(); static void removeFavoriteMenu(); static void traceRouteMenu(); static void testMenu(); static void numberTest(); static void wifiBaseMenu(); static void wifiToggleMenu(); static void screenOptionsMenu(); static void powerMenu(); static void nodeNameLengthMenu(); static void frameTogglesMenu(); static void displayUnitsMenu(); static void messageBubblesMenu(); static void textMessageMenu(); private: static void saveUIConfig(); static void keyVerificationInitMenu(); static void keyVerificationFinalPrompt(); static void bluetoothToggleMenu(); }; /* Generic Menu Options designations */ enum class OptionsAction { Back, Select }; template struct MenuOption { const char *label; OptionsAction action; bool hasValue; T value; MenuOption(const char *labelIn, OptionsAction actionIn, T valueIn) : label(labelIn), action(actionIn), hasValue(true), value(valueIn) { } MenuOption(const char *labelIn, OptionsAction actionIn) : label(labelIn), action(actionIn), hasValue(false), value() {} }; struct ScreenColor { uint8_t r; uint8_t g; uint8_t b; bool useVariant; explicit ScreenColor(uint8_t rIn = 0, uint8_t gIn = 0, uint8_t bIn = 0, bool variantIn = false) : r(rIn), g(gIn), b(bIn), useVariant(variantIn) { } }; using RadioPresetOption = MenuOption; using LoraRegionOption = MenuOption; using TimezoneOption = MenuOption; using CompassOption = MenuOption; using ScreenColorOption = MenuOption; using GPSToggleOption = MenuOption; using GPSFormatOption = MenuOption; using NodeNameOption = MenuOption; using PositionMenuOption = MenuOption; using ManageNodeOption = MenuOption; using ClockFaceOption = MenuOption; } // namespace graphics #endif ================================================ FILE: src/graphics/draw/MessageRenderer.cpp ================================================ #include "configuration.h" #if HAS_SCREEN #include "MessageRenderer.h" // Core includes #include "MessageStore.h" #include "NodeDB.h" #include "UIRenderer.h" #include "gps/RTC.h" #include "graphics/EmoteRenderer.h" #include "graphics/Screen.h" #include "graphics/ScreenFonts.h" #include "graphics/SharedUIDisplay.h" #include "graphics/TimeFormatters.h" #include "graphics/emotes.h" #include "main.h" #include "meshUtils.h" #include #include // External declarations extern bool hasUnreadMessage; extern graphics::Screen *screen; using graphics::Emote; using graphics::emotes; using graphics::numEmotes; namespace graphics { namespace MessageRenderer { static std::vector cachedLines; static std::vector cachedHeights; static bool manualScrolling = false; // Scroll state (file scope so we can reset on new message) float scrollY = 0.0f; uint32_t lastTime = 0; uint32_t scrollStartDelay = 0; uint32_t pauseStart = 0; bool waitingToReset = false; bool scrollStarted = false; static bool didReset = false; static constexpr int MESSAGE_BLOCK_GAP = 6; void scrollUp() { manualScrolling = true; scrollY -= 12; if (scrollY < 0) scrollY = 0; } void scrollDown() { manualScrolling = true; int totalHeight = 0; for (int h : cachedHeights) totalHeight += h; int visibleHeight = screen->getHeight() - (FONT_HEIGHT_SMALL * 2); int maxScroll = totalHeight - visibleHeight; if (maxScroll < 0) maxScroll = 0; scrollY += 12; if (scrollY > maxScroll) scrollY = maxScroll; } void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string &line, const Emote *emotes, int emoteCount) { graphics::EmoteRenderer::drawStringWithEmotes(display, x, y, line, FONT_HEIGHT_SMALL, emotes, emoteCount); } // Reset scroll state when new messages arrive void resetScrollState() { scrollY = 0.0f; scrollStarted = false; waitingToReset = false; scrollStartDelay = millis(); lastTime = millis(); manualScrolling = false; didReset = false; } void nudgeScroll(int8_t direction) { if (direction == 0) return; if (cachedHeights.empty()) { scrollY = 0.0f; return; } OLEDDisplay *display = (screen != nullptr) ? screen->getDisplayDevice() : nullptr; const int displayHeight = display ? display->getHeight() : 64; const int navHeight = FONT_HEIGHT_SMALL; const int usableHeight = std::max(0, displayHeight - navHeight); int totalHeight = 0; for (int h : cachedHeights) totalHeight += h; if (totalHeight <= usableHeight) { scrollY = 0.0f; return; } const int scrollStop = std::max(0, totalHeight - usableHeight + cachedHeights.back()); const int step = std::max(FONT_HEIGHT_SMALL, usableHeight / 3); float newScroll = scrollY + static_cast(direction) * static_cast(step); if (newScroll < 0.0f) newScroll = 0.0f; if (newScroll > scrollStop) newScroll = static_cast(scrollStop); if (newScroll != scrollY) { scrollY = newScroll; waitingToReset = false; scrollStarted = false; scrollStartDelay = millis(); lastTime = millis(); } } // Fully free cached message data from heap void clearMessageCache() { std::vector().swap(cachedLines); std::vector().swap(cachedHeights); // Reset scroll so we rebuild cleanly next time we enter the screen resetScrollState(); } // Current thread state static ThreadMode currentMode = ThreadMode::ALL; static int currentChannel = -1; static uint32_t currentPeer = 0; // Registry of seen threads for manual toggle static std::vector seenChannels; static std::vector seenPeers; // Public helper so menus / store can clear stale registries void clearThreadRegistries() { seenChannels.clear(); seenPeers.clear(); } // Setter so other code can switch threads void setThreadMode(ThreadMode mode, int channel /* = -1 */, uint32_t peer /* = 0 */) { currentMode = mode; currentChannel = channel; currentPeer = peer; didReset = false; // force reset when mode changes // Track channels we’ve seen if (mode == ThreadMode::CHANNEL && channel >= 0) { if (std::find(seenChannels.begin(), seenChannels.end(), channel) == seenChannels.end()) { seenChannels.push_back(channel); } } // Track DMs we’ve seen if (mode == ThreadMode::DIRECT && peer != 0) { if (std::find(seenPeers.begin(), seenPeers.end(), peer) == seenPeers.end()) { seenPeers.push_back(peer); } } } ThreadMode getThreadMode() { return currentMode; } int getThreadChannel() { return currentChannel; } uint32_t getThreadPeer() { return currentPeer; } // Accessors for menuHandler const std::vector &getSeenChannels() { return seenChannels; } const std::vector &getSeenPeers() { return seenPeers; } static int centerYForRow(int y, int size) { int midY = y + (FONT_HEIGHT_SMALL / 2); return midY - (size / 2); } // Helpers for drawing status marks (thickened strokes) static void drawCheckMark(OLEDDisplay *display, int x, int y, int size) { int topY = centerYForRow(y, size); display->setColor(WHITE); display->drawLine(x, topY + size / 2, x + size / 3, topY + size); display->drawLine(x, topY + size / 2 + 1, x + size / 3, topY + size + 1); display->drawLine(x + size / 3, topY + size, x + size, topY); display->drawLine(x + size / 3, topY + size + 1, x + size, topY + 1); } static void drawXMark(OLEDDisplay *display, int x, int y, int size = 8) { int topY = centerYForRow(y, size); display->setColor(WHITE); display->drawLine(x, topY, x + size, topY + size); display->drawLine(x, topY + 1, x + size, topY + size + 1); display->drawLine(x + size, topY, x, topY + size); display->drawLine(x + size, topY + 1, x, topY + size + 1); } static void drawRelayMark(OLEDDisplay *display, int x, int y, int size = 8) { int r = size / 2; int centerY = centerYForRow(y, size) + r; int centerX = x + r; display->setColor(WHITE); display->drawCircle(centerX, centerY, r); display->drawLine(centerX, centerY - 2, centerX, centerY); display->setPixel(centerX, centerY + 2); display->drawLine(centerX - 1, centerY - 4, centerX + 1, centerY - 4); } static inline int getRenderedLineWidth(OLEDDisplay *display, const std::string &line, const Emote *emotes, int emoteCount) { return graphics::EmoteRenderer::analyzeLine(display, line, 0, emotes, emoteCount).width; } struct MessageBlock { size_t start; size_t end; bool mine; }; static int getDrawnLinePixelBottom(int lineTopY, const std::string &line, bool isHeaderLine) { if (isHeaderLine) { return lineTopY + (FONT_HEIGHT_SMALL - 1); } const int tallest = graphics::EmoteRenderer::analyzeLine(nullptr, line, FONT_HEIGHT_SMALL, emotes, numEmotes).tallestHeight; const int lineHeight = std::max(FONT_HEIGHT_SMALL, tallest); const int iconTop = lineTopY + (lineHeight - tallest) / 2; return iconTop + tallest - 1; } static std::vector buildMessageBlocks(const std::vector &isHeaderVec, const std::vector &isMineVec) { std::vector blocks; if (isHeaderVec.empty()) return blocks; size_t start = 0; bool mine = isMineVec[0]; for (size_t i = 1; i < isHeaderVec.size(); ++i) { if (isHeaderVec[i]) { MessageBlock b; b.start = start; b.end = i - 1; b.mine = mine; blocks.push_back(b); start = i; mine = isMineVec[i]; } } MessageBlock last; last.start = start; last.end = isHeaderVec.size() - 1; last.mine = mine; blocks.push_back(last); return blocks; } static void drawMessageScrollbar(OLEDDisplay *display, int visibleHeight, int totalHeight, int scrollOffset, int startY) { if (totalHeight <= visibleHeight) return; // no scrollbar needed int scrollbarX = display->getWidth() - 2; int scrollbarHeight = visibleHeight; int thumbHeight = std::max(6, (scrollbarHeight * visibleHeight) / totalHeight); int maxScroll = std::max(1, totalHeight - visibleHeight); int thumbY = startY + (scrollbarHeight - thumbHeight) * scrollOffset / maxScroll; for (int i = 0; i < thumbHeight; i++) { display->setPixel(scrollbarX, thumbY + i); } } void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { // Ensure any boot-relative timestamps are upgraded if RTC is valid messageStore.upgradeBootRelativeTimestamps(); if (!didReset) { resetScrollState(); didReset = true; } // Clear the unread message indicator when viewing the message hasUnreadMessage = false; // Filter messages based on thread mode std::deque filtered; for (const auto &m : messageStore.getLiveMessages()) { bool include = false; switch (currentMode) { case ThreadMode::ALL: include = true; break; case ThreadMode::CHANNEL: if (m.type == MessageType::BROADCAST && (int)m.channelIndex == currentChannel) include = true; break; case ThreadMode::DIRECT: if (m.dest != NODENUM_BROADCAST && (m.sender == currentPeer || m.dest == currentPeer)) include = true; break; } if (include) filtered.push_back(m); } display->clear(); display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); const int navHeight = FONT_HEIGHT_SMALL; const int scrollBottom = SCREEN_HEIGHT - navHeight; const int usableHeight = scrollBottom; constexpr int LEFT_MARGIN = 2; constexpr int RIGHT_MARGIN = 2; constexpr int SCROLLBAR_WIDTH = 3; constexpr int BUBBLE_PAD_X = 3; constexpr int BUBBLE_PAD_Y = 4; constexpr int BUBBLE_RADIUS = 4; constexpr int BUBBLE_MIN_W = 24; constexpr int BUBBLE_TEXT_INDENT = 2; // Check if bubbles are enabled const bool showBubbles = config.display.enable_message_bubbles; const int textIndent = showBubbles ? (BUBBLE_PAD_X + BUBBLE_TEXT_INDENT) : LEFT_MARGIN; // Derived widths const int leftTextWidth = SCREEN_WIDTH - LEFT_MARGIN - RIGHT_MARGIN - (showBubbles ? (BUBBLE_PAD_X * 2) : 0); const int rightTextWidth = SCREEN_WIDTH - LEFT_MARGIN - RIGHT_MARGIN - SCROLLBAR_WIDTH; // Title string depending on mode char titleStr[48]; snprintf(titleStr, sizeof(titleStr), "Messages"); switch (currentMode) { case ThreadMode::ALL: snprintf(titleStr, sizeof(titleStr), "Messages"); break; case ThreadMode::CHANNEL: { const char *cname = channels.getName(currentChannel); if (cname && cname[0]) { snprintf(titleStr, sizeof(titleStr), "#%s", cname); } else { snprintf(titleStr, sizeof(titleStr), "Ch%d", currentChannel); } break; } case ThreadMode::DIRECT: { meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(currentPeer); if (node && node->has_user && node->user.short_name[0]) { snprintf(titleStr, sizeof(titleStr), "@%s", node->user.short_name); } else { snprintf(titleStr, sizeof(titleStr), "@%08x", currentPeer); } break; } } if (filtered.empty()) { // If current conversation is empty go back to ALL view if (currentMode != ThreadMode::ALL) { setThreadMode(ThreadMode::ALL); resetScrollState(); return; // Next draw will rerun in ALL mode } // Still in ALL mode and no messages at all → show placeholder graphics::drawCommonHeader(display, x, y, titleStr); didReset = false; const char *messageString = "No messages"; int center_text = (SCREEN_WIDTH / 2) - (display->getStringWidth(messageString) / 2); display->drawString(center_text, getTextPositions(display)[2], messageString); graphics::drawCommonFooter(display, x, y); return; } // Build lines for filtered messages (newest first) std::vector allLines; std::vector isMine; // track alignment std::vector isHeader; // track header lines std::vector ackForLine; for (auto it = filtered.rbegin(); it != filtered.rend(); ++it) { const auto &m = *it; // Channel / destination labeling char chanType[32] = ""; if (currentMode == ThreadMode::ALL) { if (m.dest == NODENUM_BROADCAST) { const char *name = channels.getName(m.channelIndex); if (currentResolution == ScreenResolution::Low || currentResolution == ScreenResolution::UltraLow) { if (strcmp(name, "ShortTurbo") == 0) name = "ShortT"; else if (strcmp(name, "ShortSlow") == 0) name = "ShortS"; else if (strcmp(name, "ShortFast") == 0) name = "ShortF"; else if (strcmp(name, "MediumSlow") == 0) name = "MedS"; else if (strcmp(name, "MediumFast") == 0) name = "MedF"; else if (strcmp(name, "LongSlow") == 0) name = "LongS"; else if (strcmp(name, "LongFast") == 0) name = "LongF"; else if (strcmp(name, "LongTurbo") == 0) name = "LongT"; else if (strcmp(name, "LongMod") == 0) name = "LongM"; } snprintf(chanType, sizeof(chanType), "#%s", name); } else { snprintf(chanType, sizeof(chanType), "(DM)"); } } // Calculate how long ago uint32_t nowSecs = getValidTime(RTCQuality::RTCQualityDevice, true); uint32_t seconds = 0; bool invalidTime = true; if (m.timestamp > 0 && nowSecs > 0) { if (nowSecs >= m.timestamp) { seconds = nowSecs - m.timestamp; invalidTime = (seconds > 315360000); // >10 years } else { uint32_t ahead = m.timestamp - nowSecs; if (ahead <= 600) { // allow small skew seconds = 0; invalidTime = false; } } } else if (m.timestamp > 0 && nowSecs == 0) { // RTC not valid: only trust boot-relative if same boot uint32_t bootNow = millis() / 1000; if (m.isBootRelative && m.timestamp <= bootNow) { seconds = bootNow - m.timestamp; invalidTime = false; } else { invalidTime = true; // old persisted boot-relative, ignore until healed } } char timeBuf[16]; if (invalidTime) { snprintf(timeBuf, sizeof(timeBuf), "???"); } else if (seconds < 60) { snprintf(timeBuf, sizeof(timeBuf), "%us", seconds); } else if (seconds < 3600) { snprintf(timeBuf, sizeof(timeBuf), "%um", seconds / 60); } else if (seconds < 86400) { snprintf(timeBuf, sizeof(timeBuf), "%uh", seconds / 3600); } else { snprintf(timeBuf, sizeof(timeBuf), "%ud", seconds / 86400); } // Build header line for this message meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(m.sender); meshtastic_NodeInfoLite *node_recipient = nodeDB->getMeshNode(m.dest); char senderName[64] = ""; if (node && node->has_user) { if (node->user.long_name[0]) { strncpy(senderName, node->user.long_name, sizeof(senderName) - 1); } else if (node->user.short_name[0]) { strncpy(senderName, node->user.short_name, sizeof(senderName) - 1); } senderName[sizeof(senderName) - 1] = '\0'; } if (!senderName[0]) { snprintf(senderName, sizeof(senderName), "(%08x)", m.sender); } // If this is *our own* message, override senderName to who the recipient was bool mine = (m.sender == nodeDB->getNodeNum()); if (mine && node_recipient && node_recipient->has_user) { if (node_recipient->user.long_name[0]) { strncpy(senderName, node_recipient->user.long_name, sizeof(senderName) - 1); senderName[sizeof(senderName) - 1] = '\0'; } else if (node_recipient->user.short_name[0]) { strncpy(senderName, node_recipient->user.short_name, sizeof(senderName) - 1); senderName[sizeof(senderName) - 1] = '\0'; } } // If recipient info is missing/empty, prefer a recipient identifier for outbound messages. if (mine && (!node_recipient || !node_recipient->has_user || (!node_recipient->user.long_name[0] && !node_recipient->user.short_name[0]))) { snprintf(senderName, sizeof(senderName), "(%08x)", m.dest); } // Shrink Sender name if needed int availWidth = (mine ? rightTextWidth : leftTextWidth) - display->getStringWidth(timeBuf) - display->getStringWidth(chanType) - graphics::UIRenderer::measureStringWithEmotes(display, " @..."); if (availWidth < 0) availWidth = 0; char truncatedSender[64]; graphics::UIRenderer::truncateStringWithEmotes(display, senderName, truncatedSender, sizeof(truncatedSender), availWidth); // Final header line char headerStr[128]; if (mine) { if (currentMode == ThreadMode::ALL) { if (strcmp(chanType, "(DM)") == 0) { snprintf(headerStr, sizeof(headerStr), "%s to %s", timeBuf, truncatedSender); } else { snprintf(headerStr, sizeof(headerStr), "%s to %s", timeBuf, chanType); } } else { snprintf(headerStr, sizeof(headerStr), "%s", timeBuf); } } else { snprintf(headerStr, sizeof(headerStr), chanType[0] ? "%s @%s %s" : "%s @%s", timeBuf, truncatedSender, chanType); } // Push header line allLines.push_back(headerStr); isMine.push_back(mine); isHeader.push_back(true); ackForLine.push_back(m.ackStatus); const char *msgText = MessageStore::getText(m); int wrapWidth = mine ? rightTextWidth : leftTextWidth; std::vector wrapped = generateLines(display, "", msgText, wrapWidth); for (auto &ln : wrapped) { allLines.push_back(ln); isMine.push_back(mine); isHeader.push_back(false); ackForLine.push_back(AckStatus::NONE); } } // Cache lines and heights cachedLines = allLines; cachedHeights = calculateLineHeights(cachedLines, emotes, isHeader); std::vector blocks = buildMessageBlocks(isHeader, isMine); // Scrolling logic (unchanged) int totalHeight = 0; for (size_t i = 0; i < cachedHeights.size(); ++i) totalHeight += cachedHeights[i]; int usableScrollHeight = usableHeight; int scrollStop = std::max(0, totalHeight - usableScrollHeight + cachedHeights.back()); #ifndef USE_EINK uint32_t now = millis(); float delta = (now - lastTime) / 400.0f; lastTime = now; const float scrollSpeed = 2.0f; if (scrollStartDelay == 0) scrollStartDelay = now; if (!scrollStarted && now - scrollStartDelay > 2000) scrollStarted = true; if (!manualScrolling && totalHeight > usableScrollHeight) { if (scrollStarted) { if (!waitingToReset) { scrollY += delta * scrollSpeed; if (scrollY >= scrollStop) { scrollY = scrollStop; waitingToReset = true; pauseStart = lastTime; } } else if (lastTime - pauseStart > 3000) { scrollY = 0; waitingToReset = false; scrollStarted = false; scrollStartDelay = lastTime; } } } else if (!manualScrolling) { scrollY = 0; } #else // E-Ink: disable autoscroll scrollY = 0.0f; waitingToReset = false; scrollStarted = false; lastTime = millis(); #endif int finalScroll = (int)scrollY; int yOffset = -finalScroll + getTextPositions(display)[1]; const int contentTop = getTextPositions(display)[1]; const int contentBottom = scrollBottom; // already excludes nav line const int rightEdge = SCREEN_WIDTH - SCROLLBAR_WIDTH - RIGHT_MARGIN; const int bubbleGapY = std::max(1, MESSAGE_BLOCK_GAP / 2); std::vector lineTop; lineTop.resize(cachedLines.size()); { int acc = 0; for (size_t i = 0; i < cachedLines.size(); ++i) { lineTop[i] = yOffset + acc; acc += cachedHeights[i]; } } // Draw bubbles (only if enabled) if (showBubbles) { for (size_t bi = 0; bi < blocks.size(); ++bi) { const auto &b = blocks[bi]; if (b.start >= cachedLines.size() || b.end >= cachedLines.size() || b.start > b.end) continue; int visualTop = lineTop[b.start]; int topY; if (isHeader[b.start]) { // Header start constexpr int BUBBLE_PAD_TOP_HEADER = 1; // try 1 or 2 topY = visualTop - BUBBLE_PAD_TOP_HEADER; } else { // Body start const bool thisLineHasEmote = graphics::EmoteRenderer::analyzeLine(nullptr, cachedLines[b.start].c_str(), 0, emotes, numEmotes).hasEmote; if (thisLineHasEmote) { constexpr int EMOTE_PADDING_ABOVE = 4; visualTop -= EMOTE_PADDING_ABOVE; } topY = visualTop - BUBBLE_PAD_Y; } int visualBottom = getDrawnLinePixelBottom(lineTop[b.end], cachedLines[b.end], isHeader[b.end]); int bottomY = visualBottom + BUBBLE_PAD_Y; if (bi + 1 < blocks.size()) { int nextHeaderIndex = (int)blocks[bi + 1].start; int nextTop = lineTop[nextHeaderIndex]; int maxBottom = nextTop - 1 - bubbleGapY; if (bottomY > maxBottom) bottomY = maxBottom; } if (bottomY <= topY + 2) continue; if (bottomY < contentTop || topY > contentBottom - 1) continue; int maxLineW = 0; for (size_t i = b.start; i <= b.end; ++i) { int w = 0; if (isHeader[i]) { w = graphics::UIRenderer::measureStringWithEmotes(display, cachedLines[i].c_str()); if (b.mine) w += 12; // room for ACK/NACK/relay mark } else { w = getRenderedLineWidth(display, cachedLines[i], emotes, numEmotes); } if (w > maxLineW) maxLineW = w; } int bubbleW = std::max(BUBBLE_MIN_W, maxLineW + (textIndent * 2)); int bubbleH = (bottomY - topY) + 1; int bubbleX = 0; if (b.mine) { bubbleX = rightEdge - bubbleW; } else { bubbleX = x; } if (bubbleX < x) bubbleX = x; if (bubbleX + bubbleW > rightEdge) bubbleW = std::max(1, rightEdge - bubbleX); // Draw rounded rectangle bubble if (bubbleW > BUBBLE_RADIUS * 2 && bubbleH > BUBBLE_RADIUS * 2) { const int r = BUBBLE_RADIUS; const int bx = bubbleX; const int by = topY; const int bw = bubbleW; const int bh = bubbleH; // Draw the 4 corner arcs using drawCircleQuads display->drawCircleQuads(bx + r, by + r, r, 0x2); // Top-left display->drawCircleQuads(bx + bw - r - 1, by + r, r, 0x1); // Top-right display->drawCircleQuads(bx + r, by + bh - r - 1, r, 0x4); // Bottom-left display->drawCircleQuads(bx + bw - r - 1, by + bh - r - 1, r, 0x8); // Bottom-right // Draw the 4 edges between corners display->drawHorizontalLine(bx + r, by, bw - 2 * r); // Top edge display->drawHorizontalLine(bx + r, by + bh - 1, bw - 2 * r); // Bottom edge display->drawVerticalLine(bx, by + r, bh - 2 * r); // Left edge display->drawVerticalLine(bx + bw - 1, by + r, bh - 2 * r); // Right edge } else if (bubbleW > 1 && bubbleH > 1) { // Fallback to simple rectangle for very small bubbles display->drawRect(bubbleX, topY, bubbleW, bubbleH); } } } // end if (showBubbles) // Render visible lines int lineY = yOffset; for (size_t i = 0; i < cachedLines.size(); ++i) { if (lineY > -cachedHeights[i] && lineY < scrollBottom) { if (isHeader[i]) { int w = graphics::UIRenderer::measureStringWithEmotes(display, cachedLines[i].c_str()); int headerX; if (isMine[i]) { // push header left to avoid overlap with scrollbar headerX = (SCREEN_WIDTH - SCROLLBAR_WIDTH - RIGHT_MARGIN) - w - (showBubbles ? textIndent : 0); if (headerX < LEFT_MARGIN) headerX = LEFT_MARGIN; } else { headerX = x + textIndent; } graphics::UIRenderer::drawStringWithEmotes(display, headerX, lineY, cachedLines[i].c_str(), FONT_HEIGHT_SMALL, 1, false); // Draw underline just under header text int underlineY = lineY + FONT_HEIGHT_SMALL; int underlineW = w; int maxW = rightEdge - headerX; if (maxW < 0) maxW = 0; if (underlineW > maxW) underlineW = maxW; for (int px = 0; px < underlineW; ++px) { display->setPixel(headerX + px, underlineY); } // Draw ACK/NACK mark for our own messages if (isMine[i]) { int markX = headerX - 10; int markY = lineY; if (ackForLine[i] == AckStatus::ACKED) { // Destination ACK drawCheckMark(display, markX, markY, 8); } else if (ackForLine[i] == AckStatus::NACKED || ackForLine[i] == AckStatus::TIMEOUT) { // Failure or timeout drawXMark(display, markX, markY, 8); } else if (ackForLine[i] == AckStatus::RELAYED) { // Relay ACK drawRelayMark(display, markX, markY, 8); } // AckStatus::NONE → show nothing } } else { // Render message line if (isMine[i]) { // Calculate actual rendered width including emotes int renderedWidth = getRenderedLineWidth(display, cachedLines[i], emotes, numEmotes); int rightX = (SCREEN_WIDTH - SCROLLBAR_WIDTH - RIGHT_MARGIN) - renderedWidth - (showBubbles ? textIndent : 0); if (rightX < LEFT_MARGIN) rightX = LEFT_MARGIN; drawStringWithEmotes(display, rightX, lineY, cachedLines[i], emotes, numEmotes); } else { drawStringWithEmotes(display, x + textIndent, lineY, cachedLines[i], emotes, numEmotes); } } } lineY += cachedHeights[i]; } // Draw scrollbar drawMessageScrollbar(display, usableHeight, totalHeight, finalScroll, getTextPositions(display)[1]); graphics::drawCommonHeader(display, x, y, titleStr); graphics::drawCommonFooter(display, x, y); } std::vector generateLines(OLEDDisplay *display, const char *headerStr, const char *messageBuf, int textWidth) { std::vector lines; // Only push headerStr if it's not empty (prevents extra blank line after headers) if (headerStr && headerStr[0] != '\0') { lines.push_back(std::string(headerStr)); } std::string line, word; for (int i = 0; messageBuf[i]; ++i) { char ch = messageBuf[i]; if ((unsigned char)messageBuf[i] == 0xE2 && (unsigned char)messageBuf[i + 1] == 0x80 && (unsigned char)messageBuf[i + 2] == 0x99) { ch = '\''; // plain apostrophe i += 2; // skip over the extra UTF-8 bytes } if (ch == '\n') { if (!word.empty()) line += word; if (!line.empty()) lines.push_back(line); line.clear(); word.clear(); } else if (ch == ' ') { line += word + ' '; word.clear(); } else { word += ch; std::string test = line + word; uint16_t strWidth = graphics::UIRenderer::measureStringWithEmotes(display, test.c_str()); if (strWidth > textWidth) { if (!line.empty()) lines.push_back(line); line = word; word.clear(); } } } if (!word.empty()) line += word; if (!line.empty()) lines.push_back(line); return lines; } std::vector calculateLineHeights(const std::vector &lines, const Emote *emotes, const std::vector &isHeaderVec) { // Tunables for layout control constexpr int HEADER_UNDERLINE_GAP = 0; // space between underline and first body line constexpr int HEADER_UNDERLINE_PIX = 1; // underline thickness (1px row drawn) constexpr int BODY_LINE_LEADING = -4; // default vertical leading for normal body lines constexpr int EMOTE_PADDING_ABOVE = 4; // space above emote line (added to line above) constexpr int EMOTE_PADDING_BELOW = 3; // space below emote line (added to emote line) std::vector rowHeights; rowHeights.reserve(lines.size()); std::vector lineMetrics; lineMetrics.reserve(lines.size()); for (const auto &line : lines) { lineMetrics.push_back(graphics::EmoteRenderer::analyzeLine(nullptr, line, FONT_HEIGHT_SMALL, emotes, numEmotes)); } for (size_t idx = 0; idx < lines.size(); ++idx) { const int baseHeight = FONT_HEIGHT_SMALL; int lineHeight = baseHeight; const int tallestEmote = lineMetrics[idx].tallestHeight; const bool hasEmote = lineMetrics[idx].hasEmote; const bool nextHasEmote = (idx + 1 < lines.size()) && lineMetrics[idx + 1].hasEmote; if (isHeaderVec[idx]) { // Header line spacing lineHeight = baseHeight + HEADER_UNDERLINE_PIX + HEADER_UNDERLINE_GAP; } else { // Base spacing for normal lines int desiredBody = baseHeight + BODY_LINE_LEADING; if (hasEmote) { // Emote line: add overshoot + bottom padding int overshoot = std::max(0, tallestEmote - baseHeight); lineHeight = desiredBody + overshoot + EMOTE_PADDING_BELOW; } else { // Regular line: no emote → standard spacing lineHeight = desiredBody; // If next line has an emote → add top padding *here* if (nextHasEmote) { lineHeight += EMOTE_PADDING_ABOVE; } } // Add block gap if next is a header if (idx + 1 < lines.size() && isHeaderVec[idx + 1]) { lineHeight += MESSAGE_BLOCK_GAP; } } rowHeights.push_back(lineHeight); } return rowHeights; } void handleNewMessage(OLEDDisplay *display, const StoredMessage &sm, const meshtastic_MeshPacket &packet) { if (packet.from != 0) { hasUnreadMessage = true; // Determine if message belongs to a muted channel bool isChannelMuted = false; if (sm.type == MessageType::BROADCAST) { const meshtastic_Channel channel = channels.getByIndex(packet.channel ? packet.channel : channels.getPrimaryIndex()); if (channel.settings.has_module_settings && channel.settings.module_settings.is_muted) isChannelMuted = true; } // Banner logic const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(packet.from); char longName[64] = "?"; if (node && node->has_user) { if (node->user.long_name[0]) { strncpy(longName, node->user.long_name, sizeof(longName) - 1); longName[sizeof(longName) - 1] = '\0'; } else if (node->user.short_name[0]) { strncpy(longName, node->user.short_name, sizeof(longName) - 1); longName[sizeof(longName) - 1] = '\0'; } } int availWidth = display->getWidth() - ((currentResolution == ScreenResolution::High) ? 40 : 20); if (availWidth < 0) availWidth = 0; char truncatedLongName[64]; graphics::UIRenderer::truncateStringWithEmotes(display, longName, truncatedLongName, sizeof(truncatedLongName), availWidth); const char *msgRaw = reinterpret_cast(packet.decoded.payload.bytes); char banner[256]; bool isAlert = false; // Check if alert detection is enabled via external notification module if (moduleConfig.external_notification.alert_bell || moduleConfig.external_notification.alert_bell_vibra || moduleConfig.external_notification.alert_bell_buzzer) { for (size_t i = 0; i < packet.decoded.payload.size && i < 100; i++) { if (msgRaw[i] == '\x07') { isAlert = true; break; } } } if (isAlert) { if (truncatedLongName[0]) snprintf(banner, sizeof(banner), "Alert Received from\n%s", truncatedLongName); else strcpy(banner, "Alert Received"); } else { // Skip muted channels unless it's an alert if (isChannelMuted) return; if (truncatedLongName[0]) { if (currentResolution == ScreenResolution::UltraLow) { strcpy(banner, "New Message"); } else { snprintf(banner, sizeof(banner), "New Message from\n%s", truncatedLongName); } } else strcpy(banner, "New Message"); } // Append context (which channel or DM) so the banner shows where the message arrived { char contextBuf[64] = ""; if (sm.type == MessageType::BROADCAST) { const char *cname = channels.getName(sm.channelIndex); if (cname && cname[0]) snprintf(contextBuf, sizeof(contextBuf), "in #%s", cname); else snprintf(contextBuf, sizeof(contextBuf), "in Ch%d", sm.channelIndex); } if (contextBuf[0]) { size_t cur = strlen(banner); if (cur + 1 < sizeof(banner)) { if (cur > 0 && banner[cur - 1] != '\n') { banner[cur] = '\n'; banner[cur + 1] = '\0'; cur++; } strncat(banner, contextBuf, sizeof(banner) - cur - 1); } } } // Shorter banner if already in a conversation (Channel or Direct) bool inThread = (getThreadMode() != ThreadMode::ALL); if (shouldWakeOnReceivedMessage()) { screen->setOn(true); } screen->showSimpleBanner(banner, inThread ? 1000 : 3000); } // Always focus into the correct conversation thread when a message with real text arrives const char *msgText = MessageStore::getText(sm); if (msgText && msgText[0] != '\0') { setThreadFor(sm, packet); } // Reset scroll for a clean start resetScrollState(); } void setThreadFor(const StoredMessage &sm, const meshtastic_MeshPacket &packet) { if (packet.to == 0 || packet.to == NODENUM_BROADCAST) { setThreadMode(ThreadMode::CHANNEL, sm.channelIndex); } else { uint32_t localNode = nodeDB->getNodeNum(); uint32_t peer = (sm.sender == localNode) ? packet.to : sm.sender; setThreadMode(ThreadMode::DIRECT, -1, peer); } } } // namespace MessageRenderer } // namespace graphics #endif ================================================ FILE: src/graphics/draw/MessageRenderer.h ================================================ #pragma once #include "MessageStore.h" // for StoredMessage #if HAS_SCREEN #include "OLEDDisplay.h" #include "OLEDDisplayUi.h" #include "graphics/emotes.h" #include "mesh/generated/meshtastic/mesh.pb.h" // for meshtastic_MeshPacket #include #include #include namespace graphics { namespace MessageRenderer { // Thread filter modes enum class ThreadMode { ALL, CHANNEL, DIRECT }; // Setter for switching thread mode void setThreadMode(ThreadMode mode, int channel = -1, uint32_t peer = 0); // Getter for current mode ThreadMode getThreadMode(); // Getter for current channel (valid if mode == CHANNEL) int getThreadChannel(); // Getter for current peer (valid if mode == DIRECT) uint32_t getThreadPeer(); // Registry accessors for menuHandler const std::vector &getSeenChannels(); const std::vector &getSeenPeers(); void clearThreadRegistries(); // Text and emote rendering void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string &line, const Emote *emotes, int emoteCount); /// Draws the text message frame for displaying received messages void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); // Function to generate lines with word wrapping std::vector generateLines(OLEDDisplay *display, const char *headerStr, const char *messageBuf, int textWidth); // Function to calculate heights for each line std::vector calculateLineHeights(const std::vector &lines, const Emote *emotes, const std::vector &isHeaderVec); // Reset scroll state when new messages arrive void resetScrollState(); // Manual scroll control for encoder-style inputs void nudgeScroll(int8_t direction); // Helper to auto-select the correct thread mode from a message void setThreadFor(const StoredMessage &sm, const meshtastic_MeshPacket &packet); // Handles a new incoming/outgoing message: banner, wake, thread select, scroll reset void handleNewMessage(OLEDDisplay *display, const StoredMessage &sm, const meshtastic_MeshPacket &packet); // Clear Message Line Cache from Message Renderer void clearMessageCache(); void scrollUp(); void scrollDown(); } // namespace MessageRenderer } // namespace graphics #endif ================================================ FILE: src/graphics/draw/NodeListRenderer.cpp ================================================ #include "configuration.h" #if HAS_SCREEN #include "CompassRenderer.h" #include "NodeDB.h" #include "NodeListRenderer.h" #include "UIRenderer.h" #include "gps/GeoCoord.h" #include "gps/RTC.h" // for getTime() function #include "graphics/ScreenFonts.h" #include "graphics/SharedUIDisplay.h" #include "graphics/images.h" #include "meshUtils.h" #include // Forward declarations for functions defined in Screen.cpp namespace graphics { extern bool haveGlyphs(const char *str); } // namespace graphics // Global screen instance extern graphics::Screen *screen; #if defined(M5STACK_UNITC6L) static uint32_t lastSwitchTime = 0; #endif namespace graphics { namespace NodeListRenderer { // Function moved from Screen.cpp to NodeListRenderer.cpp since it's primarily used here void drawScaledXBitmap16x16(int x, int y, int width, int height, const uint8_t *bitmapXBM, OLEDDisplay *display) { for (int row = 0; row < height; row++) { uint8_t rowMask = (1 << row); for (int col = 0; col < width; col++) { uint8_t colData = pgm_read_byte(&bitmapXBM[col]); if (colData & rowMask) { // Note: rows become X, columns become Y after transpose display->fillRect(x + row * 2, y + col * 2, 2, 2); } } } } // Static variables for dynamic cycling static ListMode_Node currentMode_Nodes = MODE_LAST_HEARD; static ListMode_Location currentMode_Location = MODE_DISTANCE; static int scrollIndex = 0; // Popup overlay state static uint32_t popupTime = 0; static int popupTotal = 0; static int popupStart = 0; static int popupEnd = 0; static int popupPage = 1; static int popupMaxPage = 1; static const uint32_t POPUP_DURATION_MS = 1000; // 1 second visible // ============================= // Scrolling Logic // ============================= void scrollUp() { if (scrollIndex > 0) scrollIndex--; popupTime = millis(); // show popup } void scrollDown() { scrollIndex++; popupTime = millis(); } // ============================= // Utility Functions // ============================= std::string getSafeNodeName(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int columnWidth) { (void)display; (void)columnWidth; auto fallbackId = [&] { char id[12]; std::snprintf(id, sizeof(id), "(%04X)", static_cast(node ? (node->num & 0xFFFF) : 0)); return std::string(id); }; // 1) Choose target candidate (long vs short) only if present const char *raw = nullptr; if (node && node->has_user) { raw = config.display.use_long_node_name ? node->user.long_name : node->user.short_name; } // 2) Preserve UTF-8 names so emotes can be detected and rendered. std::string nodeName = (raw && *raw) ? std::string(raw) : std::string{}; if (nodeName.empty()) { nodeName = fallbackId(); } return nodeName; } const char *getCurrentModeTitle_Nodes(int screenWidth) { switch (currentMode_Nodes) { case MODE_LAST_HEARD: return "Last Heard"; case MODE_HOP_SIGNAL: #ifdef USE_EINK return "Hops/Sig"; #else return (currentResolution == ScreenResolution::High) ? "Hops/Signal" : "Hops/Sig"; #endif default: return "Nodes"; } } const char *getCurrentModeTitle_Location(int screenWidth) { switch (currentMode_Location) { case MODE_DISTANCE: return "Distance"; case MODE_BEARING: return "Bearings"; default: return "Nodes"; } } static int getNodeNameMaxWidth(int columnWidth, int baseWidth) { if (!config.display.use_long_node_name) return baseWidth; const int legacyLongNameWidth = columnWidth - ((currentResolution == ScreenResolution::High) ? 65 : 38); return std::max(0, std::min(baseWidth, legacyLongNameWidth)); } // Use dynamic timing based on mode unsigned long getModeCycleIntervalMs() { return 3000; } int calculateMaxScroll(int totalEntries, int visibleRows) { return max(0, (totalEntries - 1) / (visibleRows * 2)); } void drawColumnSeparator(OLEDDisplay *display, int16_t x, int16_t yStart, int16_t yEnd) { x = (currentResolution == ScreenResolution::High) ? x - 2 : (currentResolution == ScreenResolution::Low) ? x - 1 : x; for (int y = yStart; y <= yEnd; y += 2) { display->setPixel(x, y); } } void drawScrollbar(OLEDDisplay *display, int visibleNodeRows, int totalEntries, int scrollIndex, int columns, int scrollStartY) { if (totalEntries <= visibleNodeRows * columns) return; int scrollbarHeight = display->getHeight() - scrollStartY - 10; int thumbHeight = max(4, (scrollbarHeight * visibleNodeRows * columns) / totalEntries); int thumbY = scrollStartY + (scrollIndex * (scrollbarHeight - thumbHeight)) / max(1, max(0, (totalEntries - 1) / (visibleNodeRows * columns))); int scrollbarX = display->getWidth() - 2; for (int i = 0; i < thumbHeight; i++) { display->setPixel(scrollbarX, thumbY + i); } } // ============================= // Entry Renderers // ============================= void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) { bool isLeftCol = (x < SCREEN_WIDTH / 2); int nameMaxWidth = getNodeNameMaxWidth(columnWidth, columnWidth - 25); int timeOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 7 : 10) : (isLeftCol ? 3 : 7); const int nameX = x + ((currentResolution == ScreenResolution::High) ? 6 : 3); char nodeName[96]; UIRenderer::truncateStringWithEmotes(display, getSafeNodeName(display, node, columnWidth).c_str(), nodeName, sizeof(nodeName), nameMaxWidth); bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0; char timeStr[10]; uint32_t seconds = sinceLastSeen(node); if (seconds == 0 || seconds == UINT32_MAX) { snprintf(timeStr, sizeof(timeStr), "?"); } else { uint32_t minutes = seconds / 60, hours = minutes / 60, days = hours / 24; snprintf(timeStr, sizeof(timeStr), (days > 365 ? "?" : "%d%c"), (days ? days : hours ? hours : minutes), (days ? 'd' : hours ? 'h' : 'm')); } display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); UIRenderer::drawStringWithEmotes(display, nameX, y, nodeName, FONT_HEIGHT_SMALL, 1, false); if (node->is_favorite) { if (currentResolution == ScreenResolution::High) { drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); } else { display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); } } if (node->is_ignored || isMuted) { if (currentResolution == ScreenResolution::High) { display->drawLine(x + 8, y + 8, (isLeftCol ? 0 : x - 4) + nameMaxWidth - 17, y + 8); } else { display->drawLine(x + 4, y + 6, (isLeftCol ? 0 : x - 3) + nameMaxWidth - 4, y + 6); } } int rightEdge = x + columnWidth - timeOffset; if (timeStr[strlen(timeStr) - 1] == 'm') // Fix the fact that our fonts don't line up well all the time rightEdge -= 1; int textWidth = display->getStringWidth(timeStr); display->drawString(rightEdge - textWidth, y, timeStr); } void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) { bool isLeftCol = (x < SCREEN_WIDTH / 2); int nameMaxWidth = getNodeNameMaxWidth(columnWidth, columnWidth - 25); int barsOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 20 : 24) : (isLeftCol ? 15 : 19); int hopOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 21 : 29) : (isLeftCol ? 13 : 17); int barsXOffset = columnWidth - barsOffset; const int nameX = x + ((currentResolution == ScreenResolution::High) ? 6 : 3); char nodeName[96]; UIRenderer::truncateStringWithEmotes(display, getSafeNodeName(display, node, columnWidth).c_str(), nodeName, sizeof(nodeName), nameMaxWidth); bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0; display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); UIRenderer::drawStringWithEmotes(display, nameX, y, nodeName, FONT_HEIGHT_SMALL, 1, false); if (node->is_favorite) { if (currentResolution == ScreenResolution::High) { drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); } else { display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); } } if (node->is_ignored || isMuted) { if (currentResolution == ScreenResolution::High) { display->drawLine(x + 8, y + 8, (isLeftCol ? 0 : x - 4) + nameMaxWidth - 17, y + 8); } else { display->drawLine(x + 4, y + 6, (isLeftCol ? 0 : x - 3) + nameMaxWidth - 4, y + 6); } } // Draw signal strength bars int bars = (node->snr > 5) ? 4 : (node->snr > 0) ? 3 : (node->snr > -5) ? 2 : (node->snr > -10) ? 1 : 0; int barWidth = 2; int barStartX = x + barsXOffset; int barStartY = y + 1 + (FONT_HEIGHT_SMALL / 2) + 2; for (int b = 0; b < 4; b++) { if (b < bars) { int height = (b * 2); display->fillRect(barStartX + (b * (barWidth + 1)), barStartY - height, barWidth, height); } } // Draw hop count char hopStr[6] = ""; if (node->has_hops_away && node->hops_away > 0) snprintf(hopStr, sizeof(hopStr), "[%d]", node->hops_away); if (hopStr[0] != '\0') { int rightEdge = x + columnWidth - hopOffset; int textWidth = display->getStringWidth(hopStr); display->drawString(rightEdge - textWidth, y, hopStr); } } void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) { bool isLeftCol = (x < SCREEN_WIDTH / 2); int nameMaxWidth = getNodeNameMaxWidth(columnWidth, columnWidth - ((currentResolution == ScreenResolution::High) ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22))); const int nameX = x + ((currentResolution == ScreenResolution::High) ? 6 : 3); char nodeName[96]; UIRenderer::truncateStringWithEmotes(display, getSafeNodeName(display, node, columnWidth).c_str(), nodeName, sizeof(nodeName), nameMaxWidth); bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0; char distStr[10] = ""; meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); if (nodeDB->hasValidPosition(ourNode) && nodeDB->hasValidPosition(node)) { double lat1 = ourNode->position.latitude_i * 1e-7; double lon1 = ourNode->position.longitude_i * 1e-7; double lat2 = node->position.latitude_i * 1e-7; double lon2 = node->position.longitude_i * 1e-7; double earthRadiusKm = 6371.0; double dLat = (lat2 - lat1) * DEG_TO_RAD; double dLon = (lon2 - lon1) * DEG_TO_RAD; double a = sin(dLat / 2) * sin(dLat / 2) + cos(lat1 * DEG_TO_RAD) * cos(lat2 * DEG_TO_RAD) * sin(dLon / 2) * sin(dLon / 2); double c = 2 * atan2(sqrt(a), sqrt(1 - a)); double distanceKm = earthRadiusKm * c; if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { double miles = distanceKm * 0.621371; if (miles < 0.1) { int feet = (int)(miles * 5280); if (feet < 1000) snprintf(distStr, sizeof(distStr), "%dft", feet); else snprintf(distStr, sizeof(distStr), "¼mi"); // 4-char max } else { int roundedMiles = (int)(miles + 0.5); if (roundedMiles < 1000) snprintf(distStr, sizeof(distStr), "%dmi", roundedMiles); else snprintf(distStr, sizeof(distStr), "999"); // Max display cap } } else { if (distanceKm < 1.0) { int meters = (int)(distanceKm * 1000); if (meters < 1000) snprintf(distStr, sizeof(distStr), "%dm", meters); else snprintf(distStr, sizeof(distStr), "1k"); } else { int km = (int)(distanceKm + 0.5); if (km < 1000) snprintf(distStr, sizeof(distStr), "%dk", km); else snprintf(distStr, sizeof(distStr), "999"); } } } display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); UIRenderer::drawStringWithEmotes(display, nameX, y, nodeName, FONT_HEIGHT_SMALL, 1, false); if (node->is_favorite) { if (currentResolution == ScreenResolution::High) { drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); } else { display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); } } if (node->is_ignored || isMuted) { if (currentResolution == ScreenResolution::High) { display->drawLine(x + 8, y + 8, (isLeftCol ? 0 : x - 4) + nameMaxWidth - 17, y + 8); } else { display->drawLine(x + 4, y + 6, (isLeftCol ? 0 : x - 3) + nameMaxWidth - 4, y + 6); } } if (strlen(distStr) > 0) { int offset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 7 : 10) // Offset for Wide Screens (Left Column:Right Column) : (isLeftCol ? 4 : 7); // Offset for Narrow Screens (Left Column:Right Column) int rightEdge = x + columnWidth - offset; int textWidth = display->getStringWidth(distStr); display->drawString(rightEdge - textWidth, y, distStr); } } void drawEntryDynamic_Nodes(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) { switch (currentMode_Nodes) { case MODE_LAST_HEARD: drawEntryLastHeard(display, node, x, y, columnWidth); break; case MODE_HOP_SIGNAL: drawEntryHopSignal(display, node, x, y, columnWidth); break; default: break; } } void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) { bool isLeftCol = (x < SCREEN_WIDTH / 2); // Adjust max text width depending on column and screen width int nameMaxWidth = getNodeNameMaxWidth(columnWidth, columnWidth - ((currentResolution == ScreenResolution::High) ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22))); const int nameX = x + ((currentResolution == ScreenResolution::High) ? 6 : 3); char nodeName[96]; UIRenderer::truncateStringWithEmotes(display, getSafeNodeName(display, node, columnWidth).c_str(), nodeName, sizeof(nodeName), nameMaxWidth); bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0; display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); UIRenderer::drawStringWithEmotes(display, nameX, y, nodeName, FONT_HEIGHT_SMALL, 1, false); if (node->is_favorite) { if (currentResolution == ScreenResolution::High) { drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); } else { display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); } } if (node->is_ignored || isMuted) { if (currentResolution == ScreenResolution::High) { display->drawLine(x + 8, y + 8, (isLeftCol ? 0 : x - 4) + nameMaxWidth - 17, y + 8); } else { display->drawLine(x + 4, y + 6, (isLeftCol ? 0 : x - 3) + nameMaxWidth - 4, y + 6); } } } void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth, float myHeading, double userLat, double userLon) { if (!nodeDB->hasValidPosition(node)) return; bool isLeftCol = (x < SCREEN_WIDTH / 2); int arrowXOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 22 : 24) : (isLeftCol ? 12 : 18); int centerX = x + columnWidth - arrowXOffset; int centerY = y + FONT_HEIGHT_SMALL / 2; double nodeLat = node->position.latitude_i * 1e-7; double nodeLon = node->position.longitude_i * 1e-7; float bearing = GeoCoord::bearing(userLat, userLon, nodeLat, nodeLon); float bearingToNode = RAD_TO_DEG * bearing; float relativeBearing = fmod((bearingToNode - myHeading + 360), 360); // Shrink size by 2px int size = FONT_HEIGHT_SMALL - 5; CompassRenderer::drawArrowToNode(display, centerX, centerY, size, relativeBearing); /* float angle = relativeBearing * DEG_TO_RAD; float halfSize = size / 2.0; // Point of the arrow int tipX = centerX + halfSize * cos(angle); int tipY = centerY - halfSize * sin(angle); float baseAngle = radians(35); float sideLen = halfSize * 0.95; float notchInset = halfSize * 0.35; // Left and right corners int leftX = centerX + sideLen * cos(angle + PI - baseAngle); int leftY = centerY - sideLen * sin(angle + PI - baseAngle); int rightX = centerX + sideLen * cos(angle + PI + baseAngle); int rightY = centerY - sideLen * sin(angle + PI + baseAngle); // Center notch (cut-in) int notchX = centerX - notchInset * cos(angle); int notchY = centerY + notchInset * sin(angle); // Draw the chevron-style arrowhead display->fillTriangle(tipX, tipY, leftX, leftY, notchX, notchY); display->fillTriangle(tipX, tipY, notchX, notchY, rightX, rightY); */ } // ============================= // Main Screen Functions // ============================= void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *title, EntryRenderer renderer, NodeExtrasRenderer extras, float heading, double lat, double lon) { const int COMMON_HEADER_HEIGHT = FONT_HEIGHT_SMALL - 1; const int rowYOffset = FONT_HEIGHT_SMALL - 3; bool locationScreen = false; if (strcmp(title, "Bearings") == 0) locationScreen = true; else if (strcmp(title, "Distance") == 0) locationScreen = true; display->clear(); // Draw the battery/time header graphics::drawCommonHeader(display, x, y, title); // Space below header y += COMMON_HEADER_HEIGHT; int totalColumns = 1; // Default to 1 column if (config.display.use_long_node_name) { if (SCREEN_WIDTH <= 240) { totalColumns = 1; } else if (SCREEN_WIDTH > 240) { totalColumns = 2; } } else { if (SCREEN_WIDTH <= 64) { totalColumns = 1; } else if (SCREEN_WIDTH > 64 && SCREEN_WIDTH <= 240) { totalColumns = 2; } else { totalColumns = 3; } } int columnWidth = display->getWidth() / totalColumns; int totalEntries = nodeDB->getNumMeshNodes(); int totalRowsAvailable = (display->getHeight() - y) / rowYOffset; int numskipped = 0; int visibleNodeRows = totalRowsAvailable; // Build filtered + ordered list std::vector drawList; drawList.reserve(totalEntries); for (int i = 0; i < totalEntries; i++) { auto *n = nodeDB->getMeshNodeByIndex(i); if (!n) continue; if (n->num == nodeDB->getNodeNum()) continue; if (locationScreen && !n->has_position) continue; drawList.push_back(n->num); } totalEntries = drawList.size(); int perPage = visibleNodeRows * totalColumns; int maxScroll = 0; if (perPage > 0) { maxScroll = max(0, (totalEntries - 1) / perPage); } if (scrollIndex > maxScroll) scrollIndex = maxScroll; int startIndex = scrollIndex * visibleNodeRows * totalColumns; int endIndex = min(startIndex + visibleNodeRows * totalColumns, totalEntries); int yOffset = 0; int col = 0; int lastNodeY = y; int shownCount = 0; int rowCount = 0; for (int idx = startIndex; idx < endIndex; idx++) { uint32_t nodeNum = drawList[idx]; auto *node = nodeDB->getMeshNode(nodeNum); int xPos = x + (col * columnWidth); int yPos = y + yOffset; renderer(display, node, xPos, yPos, columnWidth); if (extras) extras(display, node, xPos, yPos, columnWidth, heading, lat, lon); lastNodeY = max(lastNodeY, yPos + FONT_HEIGHT_SMALL); yOffset += rowYOffset; shownCount++; rowCount++; if (rowCount >= totalRowsAvailable) { yOffset = 0; rowCount = 0; col++; if (col > (totalColumns - 1)) break; } } // This should correct the scrollbar totalEntries -= numskipped; // Draw column separator if (currentResolution != ScreenResolution::UltraLow && shownCount > 0) { const int firstNodeY = y + 3; for (int horizontal_offset = 1; horizontal_offset < totalColumns; horizontal_offset++) { drawColumnSeparator(display, columnWidth * horizontal_offset, firstNodeY, lastNodeY); } } const int scrollStartY = y + 3; drawScrollbar(display, visibleNodeRows, totalEntries, scrollIndex, totalColumns, scrollStartY); graphics::drawCommonFooter(display, x, y); // Scroll Popup Overlay if (millis() - popupTime < POPUP_DURATION_MS) { popupTotal = totalEntries; popupStart = startIndex + 1; popupEnd = min(startIndex + perPage, totalEntries); popupPage = (scrollIndex + 1); popupMaxPage = max(1, (totalEntries + perPage - 1) / perPage); char buf[32]; snprintf(buf, sizeof(buf), "%d-%d/%d Pg %d/%d", popupStart, popupEnd, popupTotal, popupPage, popupMaxPage); display->setTextAlignment(TEXT_ALIGN_LEFT); // Box padding int padding = 2; int textW = display->getStringWidth(buf); int textH = FONT_HEIGHT_SMALL; int boxWidth = textW + padding * 3; int boxHeight = textH + padding * 2; // Center of usable screen area: int headerHeight = FONT_HEIGHT_SMALL - 1; int footerHeight = FONT_HEIGHT_SMALL + 2; int usableTop = headerHeight; int usableBottom = display->getHeight() - footerHeight; int usableHeight = usableBottom - usableTop; // Center point inside usable area int boxLeft = (display->getWidth() - boxWidth) / 2; int boxTop = usableTop + (usableHeight - boxHeight) / 2; // Draw Box display->setColor(BLACK); display->fillRect(boxLeft - 1, boxTop - 1, boxWidth + 2, boxHeight + 2); display->fillRect(boxLeft, boxTop - 2, boxWidth, 1); display->fillRect(boxLeft, boxTop + boxHeight + 1, boxWidth, 1); display->fillRect(boxLeft - 2, boxTop, 1, boxHeight); display->fillRect(boxLeft + boxWidth + 1, boxTop, 1, boxHeight); display->setColor(WHITE); display->drawRect(boxLeft, boxTop, boxWidth, boxHeight); display->setColor(BLACK); display->fillRect(boxLeft, boxTop, 1, 1); display->fillRect(boxLeft + boxWidth - 1, boxTop, 1, 1); display->fillRect(boxLeft, boxTop + boxHeight - 1, 1, 1); display->fillRect(boxLeft + boxWidth - 1, boxTop + boxHeight - 1, 1, 1); display->setColor(WHITE); // Text display->drawString(boxLeft + padding, boxTop + padding, buf); } } // ============================= // Screen Frame Functions // ============================= #ifndef USE_EINK // Node list for Last Heard and Hop Signal views void drawDynamicListScreen_Nodes(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { // Static variables to track mode and duration static ListMode_Node lastRenderedMode = MODE_COUNT_NODE; static unsigned long modeStartTime = 0; unsigned long now = millis(); #if defined(M5STACK_UNITC6L) display->clear(); if (now - lastSwitchTime >= 3000) { display->display(); lastSwitchTime = now; } #endif // On very first call (on boot or state enter) if (lastRenderedMode == MODE_COUNT_NODE) { currentMode_Nodes = MODE_LAST_HEARD; modeStartTime = now; } // Time to switch to next mode? if (now - modeStartTime >= getModeCycleIntervalMs()) { currentMode_Nodes = static_cast((currentMode_Nodes + 1) % MODE_COUNT_NODE); modeStartTime = now; } // Render screen based on currentMode const char *title = getCurrentModeTitle_Nodes(display->getWidth()); drawNodeListScreen(display, state, x, y, title, drawEntryDynamic_Nodes); // Track the last mode to avoid reinitializing modeStartTime lastRenderedMode = currentMode_Nodes; } // Node list for Distance and Bearings views void drawDynamicListScreen_Location(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { // Static variables to track mode and duration static ListMode_Location lastRenderedMode = MODE_COUNT_LOCATION; static unsigned long modeStartTime = 0; unsigned long now = millis(); #if defined(M5STACK_UNITC6L) display->clear(); if (now - lastSwitchTime >= 3000) { display->display(); lastSwitchTime = now; } #endif // On very first call (on boot or state enter) if (lastRenderedMode == MODE_COUNT_LOCATION) { currentMode_Location = MODE_DISTANCE; modeStartTime = now; } // Time to switch to next mode? if (now - modeStartTime >= getModeCycleIntervalMs()) { currentMode_Location = static_cast((currentMode_Location + 1) % MODE_COUNT_LOCATION); modeStartTime = now; } // Render screen based on currentMode const char *title = getCurrentModeTitle_Location(display->getWidth()); // Render screen based on currentMode_Location if (currentMode_Location == MODE_DISTANCE) { drawNodeListScreen(display, state, x, y, title, drawNodeDistance); } else if (currentMode_Location == MODE_BEARING) { drawNodeListWithCompasses(display, state, x, y); } // Track the last mode to avoid reinitializing modeStartTime lastRenderedMode = currentMode_Location; } #endif #ifdef USE_EINK void drawLastHeardScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { const char *title = "Last Heard"; drawNodeListScreen(display, state, x, y, title, drawEntryLastHeard); } void drawHopSignalScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { #ifdef USE_EINK const char *title = "Hops/Sig"; #else const char *title = "Hops/Signal"; #endif drawNodeListScreen(display, state, x, y, title, drawEntryHopSignal); } void drawDistanceScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { const char *title = "Distance"; drawNodeListScreen(display, state, x, y, title, drawNodeDistance); } #endif void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { float heading = 0; bool validHeading = false; auto ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); double lat = DegD(ourNode->position.latitude_i); double lon = DegD(ourNode->position.longitude_i); #if defined(M5STACK_UNITC6L) display->clear(); uint32_t now = millis(); if (now - lastSwitchTime >= 2000) { display->display(); lastSwitchTime = now; } #endif if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING) { #if HAS_GPS if (screen->hasHeading()) { heading = screen->getHeading(); // degrees validHeading = true; } else { heading = screen->estimatedHeading(lat, lon); validHeading = !isnan(heading); } #endif if (!validHeading) return; } drawNodeListScreen(display, state, x, y, "Bearings", drawEntryCompass, drawCompassArrow, heading, lat, lon); } /// Draw a series of fields in a column, wrapping to multiple columns if needed void drawColumns(OLEDDisplay *display, int16_t x, int16_t y, const char **fields) { // The coordinates define the left starting point of the text display->setTextAlignment(TEXT_ALIGN_LEFT); 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++; } } } // namespace NodeListRenderer } // namespace graphics #endif ================================================ FILE: src/graphics/draw/NodeListRenderer.h ================================================ #pragma once #include "graphics/Screen.h" #include "mesh/generated/meshtastic/mesh.pb.h" #include #include #include namespace graphics { /// Forward declarations class Screen; /** * @brief Node list and entry rendering functions * * Contains all functions related to drawing node lists and individual node entries * including last heard, hop signal, distance, and compass views. */ namespace NodeListRenderer { // Entry renderer function types typedef void (*EntryRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int); typedef void (*NodeExtrasRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int, float, double, double); // Node list mode enumeration for Last Heard and Hop Signal views enum ListMode_Node { MODE_LAST_HEARD = 0, MODE_HOP_SIGNAL = 1, MODE_COUNT_NODE = 2 }; // Node list mode enumeration for Distance and Bearings views enum ListMode_Location { MODE_DISTANCE = 0, MODE_BEARING = 1, MODE_COUNT_LOCATION = 2 }; // Main node list screen function void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *title, EntryRenderer renderer, NodeExtrasRenderer extras = nullptr, float heading = 0, double lat = 0, double lon = 0); // Entry renderers void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth); void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth); void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth); void drawEntryDynamic_Nodes(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth); void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth); // Extras renderers void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth, float myHeading, double userLat, double userLon); // Screen frame functions void drawLastHeardScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); void drawHopSignalScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); void drawDistanceScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); void drawDynamicListScreen_Nodes(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); void drawDynamicListScreen_Location(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); // Utility functions const char *getCurrentModeTitle_Nodes(int screenWidth); const char *getCurrentModeTitle_Location(int screenWidth); std::string getSafeNodeName(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int columnWidth); void drawColumns(OLEDDisplay *display, int16_t x, int16_t y, const char **fields); // Scrolling controls void scrollUp(); void scrollDown(); // Bitmap drawing function void drawScaledXBitmap16x16(int x, int y, int width, int height, const uint8_t *bitmapXBM, OLEDDisplay *display); } // namespace NodeListRenderer } // namespace graphics ================================================ FILE: src/graphics/draw/NotificationRenderer.cpp ================================================ #include "configuration.h" #if HAS_SCREEN #include "DisplayFormatters.h" #include "NodeDB.h" #include "NotificationRenderer.h" #include "UIRenderer.h" #include "graphics/ScreenFonts.h" #include "graphics/SharedUIDisplay.h" #include "graphics/images.h" #include "input/RotaryEncoderInterruptImpl1.h" #include "input/UpDownInterruptImpl1.h" #if HAS_BUTTON #include "input/ButtonThread.h" #endif #include "main.h" #include #include #include #if HAS_TRACKBALL #include "input/TrackballInterruptImpl1.h" #endif #ifdef ARCH_ESP32 #include "esp_task_wdt.h" #endif using namespace meshtastic; #if HAS_BUTTON // Global button thread pointer defined in main.cpp extern ::ButtonThread *UserButtonThread; #endif // External references to global variables from Screen.cpp extern std::vector functionSymbol; extern std::string functionSymbolString; extern bool hasUnreadMessage; namespace graphics { int bannerSignalBars = -1; InputEvent NotificationRenderer::inEvent; int8_t NotificationRenderer::curSelected = 0; char NotificationRenderer::alertBannerMessage[256] = {0}; uint32_t NotificationRenderer::alertBannerUntil = 0; // 0 is a special case meaning forever uint8_t NotificationRenderer::alertBannerOptions = 0; // last x lines are selectable options const char **NotificationRenderer::optionsArrayPtr = nullptr; const int *NotificationRenderer::optionsEnumPtr = nullptr; std::function NotificationRenderer::alertBannerCallback = NULL; bool NotificationRenderer::pauseBanner = false; notificationTypeEnum NotificationRenderer::current_notification_type = notificationTypeEnum::none; uint32_t NotificationRenderer::numDigits = 0; uint32_t NotificationRenderer::currentNumber = 0; VirtualKeyboard *NotificationRenderer::virtualKeyboard = nullptr; std::function NotificationRenderer::textInputCallback = nullptr; uint32_t pow_of_10(uint32_t n) { uint32_t ret = 1; for (uint32_t i = 0; i < n; i++) { ret *= 10; } return ret; } // Used on boot when a certificate is being created void NotificationRenderer::drawSSLScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { display->setTextAlignment(TEXT_ALIGN_CENTER); display->setFont(FONT_SMALL); display->drawString(64 + x, y, "Creating SSL certificate"); #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 . . "); } } void NotificationRenderer::resetBanner() { notificationTypeEnum previousType = current_notification_type; alertBannerMessage[0] = '\0'; current_notification_type = notificationTypeEnum::none; OnScreenKeyboardModule::instance().clearPopup(); inEvent.inputEvent = INPUT_BROKER_NONE; inEvent.kbchar = 0; curSelected = 0; alertBannerOptions = 0; // last x lines are selectable options optionsArrayPtr = nullptr; optionsEnumPtr = nullptr; alertBannerCallback = NULL; pauseBanner = false; numDigits = 0; currentNumber = 0; nodeDB->pause_sort(false); // If we're exiting from text_input (virtual keyboard), stop module and trigger frame update // to ensure any messages received during keyboard use are now displayed if (previousType == notificationTypeEnum::text_input && screen) { OnScreenKeyboardModule::instance().stop(false); screen->setFrames(graphics::Screen::FOCUS_PRESERVE); } } void NotificationRenderer::drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state) { // Handle text_input notifications first - they have their own timeout/banner logic if (current_notification_type == notificationTypeEnum::text_input) { // Check for timeout and reset if needed for text input if (millis() > alertBannerUntil && alertBannerUntil > 0) { resetBanner(); return; } drawTextInput(display, state); return; } if (millis() > alertBannerUntil && alertBannerUntil > 0) { resetBanner(); } // Exit if no banner is showing or banner is paused if (!isOverlayBannerShowing() || pauseBanner) { return; } switch (current_notification_type) { case notificationTypeEnum::none: // Do nothing - no notification to display break; case notificationTypeEnum::text_input: // Already handled above with dedicated logic (early return). Keep a case here to satisfy -Wswitch. break; case notificationTypeEnum::text_banner: case notificationTypeEnum::selection_picker: drawAlertBannerOverlay(display, state); break; case notificationTypeEnum::node_picker: drawNodePicker(display, state); break; case notificationTypeEnum::number_picker: drawNumberPicker(display, state); break; } } void NotificationRenderer::drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiState *state) { const char *lineStarts[MAX_LINES + 1] = {0}; uint16_t lineCount = 0; // Parse lines char *alertEnd = alertBannerMessage + strnlen(alertBannerMessage, sizeof(alertBannerMessage)); lineStarts[lineCount] = alertBannerMessage; // Find lines while ((lineCount < MAX_LINES) && (lineStarts[lineCount] < alertEnd)) { lineStarts[lineCount + 1] = std::find((char *)lineStarts[lineCount], alertEnd, '\n'); if (lineStarts[lineCount + 1][0] == '\n') lineStarts[lineCount + 1] += 1; lineCount++; } // modulo to extract uint8_t this_digit = (currentNumber % (pow_of_10(numDigits - curSelected))) / (pow_of_10(numDigits - curSelected - 1)); // Handle input if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_ALT_PRESS || inEvent.inputEvent == INPUT_BROKER_UP_LONG) { if (this_digit == 9) { currentNumber -= 9 * (pow_of_10(numDigits - curSelected - 1)); } else { currentNumber += (pow_of_10(numDigits - curSelected - 1)); } } else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_USER_PRESS || inEvent.inputEvent == INPUT_BROKER_DOWN_LONG) { if (this_digit == 0) { currentNumber += 9 * (pow_of_10(numDigits - curSelected - 1)); } else { currentNumber -= (pow_of_10(numDigits - curSelected - 1)); } } else if (inEvent.inputEvent == INPUT_BROKER_ANYKEY) { if (inEvent.kbchar > 47 && inEvent.kbchar < 58) { // have a digit currentNumber -= this_digit * (pow_of_10(numDigits - curSelected - 1)); currentNumber += (inEvent.kbchar - 48) * (pow_of_10(numDigits - curSelected - 1)); curSelected++; } } else if (inEvent.inputEvent == INPUT_BROKER_SELECT || inEvent.inputEvent == INPUT_BROKER_RIGHT) { curSelected++; } else if (inEvent.inputEvent == INPUT_BROKER_LEFT) { curSelected--; } else if ((inEvent.inputEvent == INPUT_BROKER_CANCEL || inEvent.inputEvent == INPUT_BROKER_ALT_LONG) && alertBannerUntil != 0) { resetBanner(); return; } if (curSelected == static_cast(numDigits)) { alertBannerCallback(currentNumber); resetBanner(); return; } inEvent.inputEvent = INPUT_BROKER_NONE; if (alertBannerMessage[0] == '\0') return; uint16_t totalLines = lineCount + 2; const char *linePointers[totalLines + 1] = {0}; // this is sort of a dynamic allocation // copy the linestarts to display to the linePointers holder for (uint16_t i = 0; i < lineCount; i++) { linePointers[i] = lineStarts[i]; } std::string digits = " "; std::string arrowPointer = " "; for (uint16_t i = 0; i < numDigits; i++) { // Modulo minus modulo to return just the current number digits += std::to_string((currentNumber % (pow_of_10(numDigits - i))) / (pow_of_10(numDigits - i - 1))) + " "; if (curSelected == i) { arrowPointer += "^ "; } else { arrowPointer += "_ "; } } linePointers[lineCount++] = digits.c_str(); linePointers[lineCount++] = arrowPointer.c_str(); drawNotificationBox(display, state, linePointers, totalLines, 0); } void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiState *state) { static uint32_t selectedNodenum = 0; // === Layout Configuration === constexpr uint16_t vPadding = 2; alertBannerOptions = nodeDB->getNumMeshNodes() - 1; // let the box drawing function calculate the widths? const char *lineStarts[MAX_LINES + 1] = {0}; uint16_t lineCount = 0; // Parse lines char *alertEnd = alertBannerMessage + strnlen(alertBannerMessage, sizeof(alertBannerMessage)); lineStarts[lineCount] = alertBannerMessage; while ((lineCount < MAX_LINES) && (lineStarts[lineCount] < alertEnd)) { lineStarts[lineCount + 1] = std::find((char *)lineStarts[lineCount], alertEnd, '\n'); if (lineStarts[lineCount + 1][0] == '\n') lineStarts[lineCount + 1] += 1; lineCount++; } // Handle input if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_LEFT || inEvent.inputEvent == INPUT_BROKER_ALT_PRESS || inEvent.inputEvent == INPUT_BROKER_UP_LONG) { curSelected--; } else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_RIGHT || inEvent.inputEvent == INPUT_BROKER_USER_PRESS || inEvent.inputEvent == INPUT_BROKER_DOWN_LONG) { curSelected++; } else if (inEvent.inputEvent == INPUT_BROKER_SELECT) { alertBannerCallback(selectedNodenum); resetBanner(); return; } else if ((inEvent.inputEvent == INPUT_BROKER_CANCEL || inEvent.inputEvent == INPUT_BROKER_ALT_LONG) && alertBannerUntil != 0) { resetBanner(); return; } if (curSelected == -1) curSelected = alertBannerOptions - 1; if (curSelected == alertBannerOptions) curSelected = 0; inEvent.inputEvent = INPUT_BROKER_NONE; if (alertBannerMessage[0] == '\0') return; uint16_t totalLines = lineCount + alertBannerOptions; uint16_t screenHeight = display->height(); uint8_t effectiveLineHeight = FONT_HEIGHT_SMALL - 3; uint8_t visibleTotalLines = std::min(totalLines, (screenHeight - vPadding * 2) / effectiveLineHeight); uint8_t linesShown = lineCount; const char *linePointers[visibleTotalLines + 1] = {0}; // this is sort of a dynamic allocation // copy the linestarts to display to the linePointers holder for (int i = 0; i < lineCount; i++) { linePointers[i] = lineStarts[i]; } char scratchLineBuffer[visibleTotalLines - lineCount][64]; uint8_t firstOptionToShow = 0; if (curSelected > 1 && alertBannerOptions > visibleTotalLines - lineCount) { if (curSelected > alertBannerOptions - visibleTotalLines + lineCount) firstOptionToShow = alertBannerOptions - visibleTotalLines + lineCount; else firstOptionToShow = curSelected - 1; } else { firstOptionToShow = 0; } int scratchLineNum = 0; for (int i = firstOptionToShow; i < alertBannerOptions && linesShown < visibleTotalLines; i++, linesShown++) { char tempName[48] = {0}; meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i + 1); if (node && node->has_user) { const char *rawName = nullptr; if (node->user.long_name[0]) { rawName = node->user.long_name; } else if (node->user.short_name[0]) { rawName = node->user.short_name; } if (rawName) { const int arrowWidth = (currentResolution == ScreenResolution::High) ? UIRenderer::measureStringWithEmotes(display, "> <") : UIRenderer::measureStringWithEmotes(display, "><"); const int maxTextWidth = std::max(0, display->getWidth() - 28 - arrowWidth); UIRenderer::truncateStringWithEmotes(display, rawName, tempName, sizeof(tempName), maxTextWidth); } } else { snprintf(tempName, sizeof(tempName), "(%04X)", (uint16_t)(node ? (node->num & 0xFFFF) : 0)); } if (!tempName[0]) { snprintf(tempName, sizeof(tempName), "(%04X)", (uint16_t)(node ? (node->num & 0xFFFF) : 0)); } if (i == curSelected) { selectedNodenum = node ? node->num : 0; if (currentResolution == ScreenResolution::High) { strncpy(scratchLineBuffer[scratchLineNum], "> ", 3); strncpy(scratchLineBuffer[scratchLineNum] + 2, tempName, sizeof(scratchLineBuffer[scratchLineNum]) - 3); scratchLineBuffer[scratchLineNum][sizeof(scratchLineBuffer[scratchLineNum]) - 1] = '\0'; const size_t used = strnlen(scratchLineBuffer[scratchLineNum], sizeof(scratchLineBuffer[scratchLineNum]) - 1); strncpy(scratchLineBuffer[scratchLineNum] + used, " <", sizeof(scratchLineBuffer[scratchLineNum]) - used - 1); } else { strncpy(scratchLineBuffer[scratchLineNum], ">", 2); strncpy(scratchLineBuffer[scratchLineNum] + 1, tempName, sizeof(scratchLineBuffer[scratchLineNum]) - 2); scratchLineBuffer[scratchLineNum][sizeof(scratchLineBuffer[scratchLineNum]) - 1] = '\0'; const size_t used = strnlen(scratchLineBuffer[scratchLineNum], sizeof(scratchLineBuffer[scratchLineNum]) - 1); strncpy(scratchLineBuffer[scratchLineNum] + used, "<", sizeof(scratchLineBuffer[scratchLineNum]) - used - 1); } scratchLineBuffer[scratchLineNum][sizeof(scratchLineBuffer[scratchLineNum]) - 1] = '\0'; } else { strncpy(scratchLineBuffer[scratchLineNum], tempName, sizeof(scratchLineBuffer[scratchLineNum]) - 1); scratchLineBuffer[scratchLineNum][sizeof(scratchLineBuffer[scratchLineNum]) - 1] = '\0'; } linePointers[linesShown] = scratchLineBuffer[scratchLineNum++]; } drawNotificationBox(display, state, linePointers, totalLines, firstOptionToShow); } void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state) { // === Layout Configuration === constexpr uint16_t vPadding = 2; uint16_t optionWidths[alertBannerOptions] = {0}; uint16_t maxWidth = 0; uint16_t arrowsWidth = display->getStringWidth("> <", 4, true); uint16_t lineWidths[MAX_LINES] = {0}; uint16_t lineLengths[MAX_LINES] = {0}; const char *lineStarts[MAX_LINES + 1] = {0}; uint16_t lineCount = 0; char lineBuffer[40] = {0}; // Parse lines char *alertEnd = alertBannerMessage + strnlen(alertBannerMessage, sizeof(alertBannerMessage)); lineStarts[lineCount] = alertBannerMessage; while ((lineCount < MAX_LINES) && (lineStarts[lineCount] < alertEnd)) { lineStarts[lineCount + 1] = std::find((char *)lineStarts[lineCount], alertEnd, '\n'); lineLengths[lineCount] = lineStarts[lineCount + 1] - lineStarts[lineCount]; if (lineStarts[lineCount + 1][0] == '\n') lineStarts[lineCount + 1] += 1; lineWidths[lineCount] = display->getStringWidth(lineStarts[lineCount], lineLengths[lineCount], true); if (lineWidths[lineCount] > maxWidth) maxWidth = lineWidths[lineCount]; lineCount++; } // Measure option widths for (int i = 0; i < alertBannerOptions; i++) { optionWidths[i] = display->getStringWidth(optionsArrayPtr[i], strlen(optionsArrayPtr[i]), true); if (optionWidths[i] > maxWidth) maxWidth = optionWidths[i]; if (optionWidths[i] + arrowsWidth > maxWidth) maxWidth = optionWidths[i] + arrowsWidth; } // Handle input if (alertBannerOptions > 0) { if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_LEFT || inEvent.inputEvent == INPUT_BROKER_ALT_PRESS || inEvent.inputEvent == INPUT_BROKER_UP_LONG) { curSelected--; } else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_RIGHT || inEvent.inputEvent == INPUT_BROKER_USER_PRESS || inEvent.inputEvent == INPUT_BROKER_DOWN_LONG) { curSelected++; } else if (inEvent.inputEvent == INPUT_BROKER_SELECT) { if (optionsEnumPtr != nullptr) { alertBannerCallback(optionsEnumPtr[curSelected]); optionsEnumPtr = nullptr; } else { alertBannerCallback(curSelected); } resetBanner(); return; } else if ((inEvent.inputEvent == INPUT_BROKER_CANCEL || inEvent.inputEvent == INPUT_BROKER_ALT_LONG) && alertBannerUntil != 0) { resetBanner(); return; } if (curSelected == -1) curSelected = alertBannerOptions - 1; if (curSelected == alertBannerOptions) curSelected = 0; } else { if (inEvent.inputEvent == INPUT_BROKER_SELECT || inEvent.inputEvent == INPUT_BROKER_ALT_LONG || inEvent.inputEvent == INPUT_BROKER_CANCEL) { resetBanner(); return; } } inEvent.inputEvent = INPUT_BROKER_NONE; if (alertBannerMessage[0] == '\0') return; uint16_t totalLines = lineCount + alertBannerOptions; uint16_t screenHeight = display->height(); uint8_t effectiveLineHeight = FONT_HEIGHT_SMALL - 3; uint8_t visibleTotalLines = std::min(totalLines, (screenHeight - vPadding * 2) / effectiveLineHeight); uint8_t linesShown = lineCount; const char *linePointers[visibleTotalLines + 1] = {0}; // this is sort of a dynamic allocation // copy the linestarts to display to the linePointers holder for (int i = 0; i < lineCount; i++) { linePointers[i] = lineStarts[i]; } uint8_t firstOptionToShow = 0; if (alertBannerOptions > 0) { if (visibleTotalLines - lineCount == 1) { firstOptionToShow = curSelected; } else if (curSelected > 1 && alertBannerOptions > visibleTotalLines - lineCount) { if (curSelected > alertBannerOptions - visibleTotalLines + lineCount) firstOptionToShow = alertBannerOptions - visibleTotalLines + lineCount; else firstOptionToShow = curSelected - 1; } else { firstOptionToShow = 0; } } // Useful log line for troubleshooting: /* LOG_WARN("alertBannerOptions: %u, curSelected: %u, visibleTotalLines: %u, lineCount: %u, firstOptionToShow: %u", alertBannerOptions, curSelected, visibleTotalLines, lineCount, firstOptionToShow); */ for (int i = firstOptionToShow; i < alertBannerOptions && linesShown < visibleTotalLines; i++, linesShown++) { if (i == curSelected) { if (currentResolution == ScreenResolution::High) { strncpy(lineBuffer, "> ", 3); strncpy(lineBuffer + 2, optionsArrayPtr[i], 36); strncpy(lineBuffer + strlen(optionsArrayPtr[i]) + 2, " <", 3); } else { strncpy(lineBuffer, ">", 2); strncpy(lineBuffer + 1, optionsArrayPtr[i], 37); strncpy(lineBuffer + strlen(optionsArrayPtr[i]) + 1, "<", 2); } lineBuffer[39] = '\0'; linePointers[linesShown] = lineBuffer; } else { linePointers[linesShown] = optionsArrayPtr[i]; } } if (alertBannerOptions > 0) { drawNotificationBox(display, state, linePointers, totalLines, firstOptionToShow, maxWidth); } else { drawNotificationBox(display, state, linePointers, totalLines, firstOptionToShow); } } void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplayUiState *state, const char *lines[], uint16_t totalLines, uint8_t firstOptionToShow, uint16_t maxWidth) { bool is_picker = false; uint16_t lineCount = 0; // Layout Configuration constexpr uint16_t hPadding = 5; constexpr uint16_t vPadding = 2; bool needs_bell = false; uint16_t lineWidths[totalLines] = {0}; uint16_t lineLengths[totalLines] = {0}; if (maxWidth != 0) is_picker = true; // Setup font and alignment display->setFont(FONT_SMALL); display->setTextAlignment(TEXT_ALIGN_LEFT); // Track widest line INCLUDING bars (but don't change per-line widths) uint16_t widestLineWithBars = 0; while (lines[lineCount] != nullptr) { auto newlinePointer = strchr(lines[lineCount], '\n'); if (newlinePointer) lineLengths[lineCount] = (newlinePointer - lines[lineCount]); // Check for newlines first else // if the newline wasn't found, then pull string length from strlen lineLengths[lineCount] = strlen(lines[lineCount]); if (current_notification_type == notificationTypeEnum::node_picker) { char measureBuffer[64] = {0}; strncpy(measureBuffer, lines[lineCount], std::min(lineLengths[lineCount], sizeof(measureBuffer) - 1)); lineWidths[lineCount] = UIRenderer::measureStringWithEmotes(display, measureBuffer); } else { lineWidths[lineCount] = display->getStringWidth(lines[lineCount], lineLengths[lineCount], true); } // Consider extra width for signal bars on lines that contain "Signal:" uint16_t potentialWidth = lineWidths[lineCount]; if (graphics::bannerSignalBars >= 0 && strncmp(lines[lineCount], "Signal:", 7) == 0) { const int totalBars = 5; const int barWidth = 3; const int barSpacing = 2; const int gap = 6; // space between text and bars int barsWidth = totalBars * barWidth + (totalBars - 1) * barSpacing + gap; potentialWidth += barsWidth; } if (potentialWidth > widestLineWithBars) widestLineWithBars = potentialWidth; if (!is_picker) { needs_bell |= (strstr(alertBannerMessage, "Alert Received") != nullptr); if (lineWidths[lineCount] > maxWidth) maxWidth = lineWidths[lineCount]; } lineCount++; } // count lines // Ensure box accounts for signal bars if present if (widestLineWithBars > maxWidth) maxWidth = widestLineWithBars; uint16_t boxWidth = hPadding * 2 + maxWidth; if (needs_bell) { if ((currentResolution == ScreenResolution::High) && boxWidth <= 150) boxWidth += 26; if ((currentResolution == ScreenResolution::Low || currentResolution == ScreenResolution::UltraLow) && boxWidth <= 100) boxWidth += 20; } uint16_t screenHeight = display->height(); uint8_t effectiveLineHeight = FONT_HEIGHT_SMALL - 3; uint8_t visibleTotalLines = std::min(lineCount, (screenHeight - vPadding * 2) / effectiveLineHeight); uint16_t contentHeight = visibleTotalLines * effectiveLineHeight; uint16_t boxHeight = contentHeight + vPadding * 2; if (visibleTotalLines == 1) { boxHeight += (currentResolution == ScreenResolution::High) ? 4 : 3; } int16_t boxLeft = (display->width() / 2) - (boxWidth / 2); if (totalLines > visibleTotalLines) { boxWidth += (currentResolution == ScreenResolution::High) ? 4 : 2; } int16_t boxTop = (display->height() / 2) - (boxHeight / 2); boxHeight += (currentResolution == ScreenResolution::High) ? 2 : 1; #if defined(M5STACK_UNITC6L) if (visibleTotalLines == 1) { boxTop += 25; } if (alertBannerOptions < 3) { int missingLines = 3 - alertBannerOptions; int moveUp = missingLines * (effectiveLineHeight / 2); boxTop -= moveUp; if (boxTop < 0) boxTop = 0; } #endif // Draw Box display->setColor(BLACK); display->fillRect(boxLeft - 1, boxTop - 1, boxWidth + 2, boxHeight + 2); display->fillRect(boxLeft, boxTop - 2, boxWidth, 1); display->fillRect(boxLeft, boxTop + boxHeight + 1, boxWidth, 1); display->fillRect(boxLeft - 2, boxTop, 1, boxHeight); display->fillRect(boxLeft + boxWidth + 1, boxTop, 1, boxHeight); display->setColor(WHITE); display->drawRect(boxLeft, boxTop, boxWidth, boxHeight); display->setColor(BLACK); display->fillRect(boxLeft, boxTop, 1, 1); display->fillRect(boxLeft + boxWidth - 1, boxTop, 1, 1); display->fillRect(boxLeft, boxTop + boxHeight - 1, 1, 1); display->fillRect(boxLeft + boxWidth - 1, boxTop + boxHeight - 1, 1, 1); display->setColor(WHITE); // Draw Content int16_t lineY = boxTop + vPadding; for (int i = 0; i < lineCount; i++) { int16_t textX = boxLeft + (boxWidth - lineWidths[i]) / 2; if (needs_bell && i == 0) { int bellY = lineY + (FONT_HEIGHT_SMALL - 8) / 2; display->drawXbm(textX - 10, bellY, 8, 8, bell_alert); display->drawXbm(textX + lineWidths[i] + 2, bellY, 8, 8, bell_alert); } char lineBuffer[lineLengths[i] + 1]; strncpy(lineBuffer, lines[i], lineLengths[i]); lineBuffer[lineLengths[i]] = '\0'; // Determine if this is a pop-up or a pick list if (alertBannerOptions > 0 && i == 0) { // Pick List display->setColor(WHITE); int background_yOffset = 1; // Determine if we have low hanging characters if (strchr(lineBuffer, 'p') || strchr(lineBuffer, 'g') || strchr(lineBuffer, 'y') || strchr(lineBuffer, 'j')) { background_yOffset = -1; } display->fillRect(boxLeft, boxTop + 1, boxWidth, effectiveLineHeight - background_yOffset); display->setColor(BLACK); int yOffset = 3; if (current_notification_type == notificationTypeEnum::node_picker) { UIRenderer::drawStringWithEmotes(display, textX, lineY - yOffset, lineBuffer, FONT_HEIGHT_SMALL, 1, false); } else { display->drawString(textX, lineY - yOffset, lineBuffer); } display->setColor(WHITE); lineY += (effectiveLineHeight - 2 - background_yOffset); } else { // Pop-up // If this is the Signal line, center text + bars as one group bool isSignalLine = (graphics::bannerSignalBars >= 0 && strstr(lineBuffer, "Signal:") != nullptr); if (isSignalLine) { const int totalBars = 5; const int barWidth = 3; const int barSpacing = 2; const int barHeightStep = 2; const int gap = 6; int textWidth = display->getStringWidth(lineBuffer, strlen(lineBuffer), true); int barsWidth = totalBars * barWidth + (totalBars - 1) * barSpacing + gap; int totalWidth = textWidth + barsWidth; int groupStartX = boxLeft + (boxWidth - totalWidth) / 2; if (current_notification_type == notificationTypeEnum::node_picker) { UIRenderer::drawStringWithEmotes(display, groupStartX, lineY, lineBuffer, FONT_HEIGHT_SMALL, 1, false); } else { display->drawString(groupStartX, lineY, lineBuffer); } int baseX = groupStartX + textWidth + gap; int baseY = lineY + effectiveLineHeight - 1; for (int b = 0; b < totalBars; b++) { int barHeight = (b + 1) * barHeightStep; int x = baseX + b * (barWidth + barSpacing); int y = baseY - barHeight; if (b < graphics::bannerSignalBars) { display->fillRect(x, y, barWidth, barHeight); } else { display->drawRect(x, y, barWidth, barHeight); } } } else { if (current_notification_type == notificationTypeEnum::node_picker) { UIRenderer::drawStringWithEmotes(display, textX, lineY, lineBuffer, FONT_HEIGHT_SMALL, 1, false); } else { display->drawString(textX, lineY, lineBuffer); } } lineY += (effectiveLineHeight); } } // Scroll Bar (Thicker, inside box, not over title) if (totalLines > visibleTotalLines) { const uint8_t scrollBarWidth = 5; int16_t scrollBarX = boxLeft + boxWidth - scrollBarWidth - 2; int16_t scrollBarY = boxTop + vPadding + effectiveLineHeight; uint16_t scrollBarHeight = boxHeight - vPadding * 2 - effectiveLineHeight; float ratio = (float)visibleTotalLines / totalLines; uint16_t indicatorHeight = std::max((int)(scrollBarHeight * ratio), 4); float scrollRatio = (float)(firstOptionToShow + lineCount - visibleTotalLines) / (totalLines - visibleTotalLines); uint16_t indicatorY = scrollBarY + scrollRatio * (scrollBarHeight - indicatorHeight); display->drawRect(scrollBarX, scrollBarY, scrollBarWidth, scrollBarHeight); display->fillRect(scrollBarX + 1, indicatorY, scrollBarWidth - 2, indicatorHeight); } } /// Draw the last text message we received void NotificationRenderer::drawCriticalFaultFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_MEDIUM); char tempBuf[24]; snprintf(tempBuf, sizeof(tempBuf), "Critical fault #%d", error_code); display->drawString(0 + x, 0 + y, tempBuf); display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); display->drawString(0 + x, FONT_HEIGHT_MEDIUM + y, "For help, please visit \nmeshtastic.org"); } void NotificationRenderer::drawFrameFirmware(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { display->setTextAlignment(TEXT_ALIGN_CENTER); display->setFont(FONT_MEDIUM); display->drawString(64 + x, y, "Updating"); display->setFont(FONT_SMALL); display->setTextAlignment(TEXT_ALIGN_LEFT); display->drawStringMaxWidth(0 + x, 2 + y + FONT_HEIGHT_SMALL * 2, x + display->getWidth(), "Please be patient and do not power off."); } void NotificationRenderer::drawTextInput(OLEDDisplay *display, OLEDDisplayUiState *state) { if (virtualKeyboard) { // Check for timeout and auto-exit if needed if (virtualKeyboard->isTimedOut()) { LOG_INFO("Virtual keyboard timeout - auto-exiting"); // Cancel virtual keyboard - call callback with empty string to indicate timeout auto callback = textInputCallback; // Store callback before clearing // Clean up first to prevent re-entry delete virtualKeyboard; virtualKeyboard = nullptr; textInputCallback = nullptr; resetBanner(); // Call callback after cleanup if (callback) { callback(""); } // Restore normal overlays if (screen) { screen->setFrames(graphics::Screen::FOCUS_PRESERVE); } return; } if (inEvent.inputEvent != INPUT_BROKER_NONE) { bool handled = OnScreenKeyboardModule::processVirtualKeyboardInput(inEvent, virtualKeyboard); if (!handled && inEvent.inputEvent == INPUT_BROKER_CANCEL) { auto callback = textInputCallback; delete virtualKeyboard; virtualKeyboard = nullptr; textInputCallback = nullptr; resetBanner(); if (callback) { callback(""); } if (screen) { screen->setFrames(graphics::Screen::FOCUS_PRESERVE); } return; } // Consume the event after processing for virtual keyboard inEvent.inputEvent = INPUT_BROKER_NONE; } // Re-check pointer before drawing to avoid use-after-free and crashes if (!virtualKeyboard) { // Ensure we exit text_input state and restore frames if (current_notification_type == notificationTypeEnum::text_input) { resetBanner(); } if (screen) { screen->setFrames(graphics::Screen::FOCUS_PRESERVE); } // If screen is null, do nothing (safe fallback) return; } // Clear the screen to avoid overlapping with underlying frames or overlays display->setColor(BLACK); display->fillRect(0, 0, display->getWidth(), display->getHeight()); display->setColor(WHITE); // Draw the virtual keyboard virtualKeyboard->draw(display, 0, 0); // Draw transient popup overlay (if any) managed by OnScreenKeyboardModule OnScreenKeyboardModule::instance().drawPopupOverlay(display); } else { // If virtualKeyboard is null, reset the banner to avoid getting stuck LOG_INFO("Virtual keyboard is null - resetting banner"); resetBanner(); } } bool NotificationRenderer::isOverlayBannerShowing() { return strlen(alertBannerMessage) > 0 && (alertBannerUntil == 0 || millis() <= alertBannerUntil); } void NotificationRenderer::showKeyboardMessagePopupWithTitle(const char *title, const char *content, uint32_t durationMs) { if (!title || !content || current_notification_type != notificationTypeEnum::text_input) return; OnScreenKeyboardModule::instance().showPopup(title, content, durationMs); } } // namespace graphics #endif ================================================ FILE: src/graphics/draw/NotificationRenderer.h ================================================ #pragma once #include "OLEDDisplay.h" #include "OLEDDisplayUi.h" #include "graphics/Screen.h" #include "graphics/VirtualKeyboard.h" #include "modules/OnScreenKeyboardModule.h" #include #include #define MAX_LINES 5 namespace graphics { class NotificationRenderer { public: static InputEvent inEvent; static char inKeypress; static int8_t curSelected; static char alertBannerMessage[256]; static uint32_t alertBannerUntil; // 0 is a special case meaning forever static const char **optionsArrayPtr; static const int *optionsEnumPtr; static uint8_t alertBannerOptions; // last x lines are selectable options static std::function alertBannerCallback; static uint32_t numDigits; static uint32_t currentNumber; static VirtualKeyboard *virtualKeyboard; static std::function textInputCallback; static bool pauseBanner; static void resetBanner(); static void showKeyboardMessagePopupWithTitle(const char *title, const char *content, uint32_t durationMs); static void drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state); static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state); static void drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiState *state); static void drawNodePicker(OLEDDisplay *display, OLEDDisplayUiState *state); static void drawTextInput(OLEDDisplay *display, OLEDDisplayUiState *state); static void drawNotificationBox(OLEDDisplay *display, OLEDDisplayUiState *state, const char *lines[MAX_LINES + 1], uint16_t totalLines, uint8_t firstOptionToShow, uint16_t maxWidth = 0); static void drawCriticalFaultFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); static void drawSSLScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); static void drawFrameFirmware(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); static bool isOverlayBannerShowing(); static graphics::notificationTypeEnum current_notification_type; }; } // namespace graphics ================================================ FILE: src/graphics/draw/UIRenderer.cpp ================================================ #include "configuration.h" #if HAS_SCREEN #include "CompassRenderer.h" #include "GPSStatus.h" #include "MeshService.h" #include "NodeDB.h" #include "NodeListRenderer.h" #include "UIRenderer.h" #include "airtime.h" #include "gps/GeoCoord.h" #include "graphics/EmoteRenderer.h" #include "graphics/SharedUIDisplay.h" #include "graphics/TimeFormatters.h" #include "graphics/images.h" #include "main.h" #include "target_specific.h" #include #include #include // External variables extern graphics::Screen *screen; #if defined(M5STACK_UNITC6L) static uint32_t lastSwitchTime = 0; #endif namespace graphics { NodeNum UIRenderer::currentFavoriteNodeNum = 0; std::vector graphics::UIRenderer::favoritedNodes; static inline void drawSatelliteIcon(OLEDDisplay *display, int16_t x, int16_t y) { int yOffset = (currentResolution == ScreenResolution::High) ? -5 : 1; if (currentResolution == ScreenResolution::High) { NodeListRenderer::drawScaledXBitmap16x16(x, y + yOffset, imgSatellite_width, imgSatellite_height, imgSatellite, display); } else { display->drawXbm(x + 1, y + yOffset, imgSatellite_width, imgSatellite_height, imgSatellite); } } void graphics::UIRenderer::rebuildFavoritedNodes() { favoritedNodes.clear(); size_t total = nodeDB->getNumMeshNodes(); for (size_t i = 0; i < total; i++) { meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i); if (!n || n->num == nodeDB->getNodeNum()) continue; if (n->is_favorite) favoritedNodes.push_back(n); } std::sort(favoritedNodes.begin(), favoritedNodes.end(), [](const meshtastic_NodeInfoLite *a, const meshtastic_NodeInfoLite *b) { return a->num < b->num; }); } #if !MESHTASTIC_EXCLUDE_GPS // GeoCoord object for coordinate conversions extern GeoCoord geoCoord; // Threshold values for the GPS lock accuracy bar display extern uint32_t dopThresholds[5]; // Draw GPS status summary void UIRenderer::drawGps(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gps) { // Draw satellite image if (currentResolution == ScreenResolution::High) { NodeListRenderer::drawScaledXBitmap16x16(x, y - 2, imgSatellite_width, imgSatellite_height, imgSatellite, display); } else { display->drawXbm(x + 1, y + 1, imgSatellite_width, imgSatellite_height, imgSatellite); } char textString[10]; if (config.position.fixed_position) { // GPS coordinates are currently fixed snprintf(textString, sizeof(textString), "Fixed"); } if (!gps->getIsConnected()) { snprintf(textString, sizeof(textString), "No Lock"); } if (!gps->getHasLock()) { // Draw "No sats" to the right of the icon with slightly more gap snprintf(textString, sizeof(textString), "No Sats"); } else { snprintf(textString, sizeof(textString), "%u sats", gps->getNumSatellites()); } if (currentResolution == ScreenResolution::High) { display->drawString(x + 18, y, textString); } else { display->drawString(x + 11, y, textString); } } // Draw status when GPS is disabled or not present void UIRenderer::drawGpsPowerStatus(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gps) { const char *displayLine; int pos; if (y < FONT_HEIGHT_SMALL) { // Line 1: use short string displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off"; pos = display->getWidth() - display->getStringWidth(displayLine); } else { displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "GPS not present" : "GPS is disabled"; pos = (display->getWidth() - display->getStringWidth(displayLine)) / 2; } display->drawString(x + pos, y, displayLine); } void UIRenderer::drawGpsAltitude(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gps) { char displayLine[32]; if (!gps->getIsConnected() && !config.position.fixed_position) { // displayLine = "No GPS Module"; // display->drawString(x + (SCREEN_WIDTH - (display->getStringWidth(displayLine))) / 2, y, displayLine); } else if (!gps->getHasLock() && !config.position.fixed_position) { // displayLine = "No GPS Lock"; // display->drawString(x + (SCREEN_WIDTH - (display->getStringWidth(displayLine))) / 2, y, displayLine); } else { geoCoord.updateCoords(int32_t(gps->getLatitude()), int32_t(gps->getLongitude()), int32_t(gps->getAltitude())); if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) snprintf(displayLine, sizeof(displayLine), "Altitude: %.0fft", geoCoord.getAltitude() * METERS_TO_FEET); else snprintf(displayLine, sizeof(displayLine), "Altitude: %.0im", geoCoord.getAltitude()); display->drawString(x + (display->getWidth() - (display->getStringWidth(displayLine))) / 2, y, displayLine); } } // Draw GPS status coordinates void UIRenderer::drawGpsCoordinates(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gps, const char *mode) { auto gpsFormat = uiconfig.gps_format; char displayLine[32]; if (!gps->getIsConnected() && !config.position.fixed_position) { if (strcmp(mode, "line1") == 0) { strcpy(displayLine, "No GPS present"); display->drawString(x, y, displayLine); } } else if (!gps->getHasLock() && !config.position.fixed_position) { if (strcmp(mode, "line1") == 0) { strcpy(displayLine, "No GPS Lock"); display->drawString(x, y, displayLine); } } else { geoCoord.updateCoords(int32_t(gps->getLatitude()), int32_t(gps->getLongitude()), int32_t(gps->getAltitude())); if (gpsFormat != meshtastic_DeviceUIConfig_GpsCoordinateFormat_DMS) { char coordinateLine_1[22]; char coordinateLine_2[22]; if (gpsFormat == meshtastic_DeviceUIConfig_GpsCoordinateFormat_DEC) { // Decimal Degrees snprintf(coordinateLine_1, sizeof(coordinateLine_1), "Lat: %f", geoCoord.getLatitude() * 1e-7); snprintf(coordinateLine_2, sizeof(coordinateLine_2), "Lon: %f", geoCoord.getLongitude() * 1e-7); } else if (gpsFormat == meshtastic_DeviceUIConfig_GpsCoordinateFormat_UTM) { // Universal Transverse Mercator snprintf(coordinateLine_1, sizeof(coordinateLine_1), "%2i%1c %06u E", geoCoord.getUTMZone(), geoCoord.getUTMBand(), geoCoord.getUTMEasting()); snprintf(coordinateLine_2, sizeof(coordinateLine_2), "%07u N", geoCoord.getUTMNorthing()); } else if (gpsFormat == meshtastic_DeviceUIConfig_GpsCoordinateFormat_MGRS) { // Military Grid Reference System snprintf(coordinateLine_1, sizeof(coordinateLine_1), "%2i%1c %1c%1c", geoCoord.getMGRSZone(), geoCoord.getMGRSBand(), geoCoord.getMGRSEast100k(), geoCoord.getMGRSNorth100k()); snprintf(coordinateLine_2, sizeof(coordinateLine_2), "%05u E %05u N", geoCoord.getMGRSEasting(), geoCoord.getMGRSNorthing()); } else if (gpsFormat == meshtastic_DeviceUIConfig_GpsCoordinateFormat_OLC) { // Open Location Code geoCoord.getOLCCode(coordinateLine_1); coordinateLine_2[0] = '\0'; } else if (gpsFormat == meshtastic_DeviceUIConfig_GpsCoordinateFormat_OSGR) { // Ordnance Survey Grid Reference if (geoCoord.getOSGRE100k() == 'I' || geoCoord.getOSGRN100k() == 'I') { // OSGR is only valid around the UK region snprintf(coordinateLine_1, sizeof(coordinateLine_1), "%s", "Out of Boundary"); coordinateLine_2[0] = '\0'; } else { snprintf(coordinateLine_1, sizeof(coordinateLine_1), "%1c%1c", geoCoord.getOSGRE100k(), geoCoord.getOSGRN100k()); snprintf(coordinateLine_2, sizeof(coordinateLine_2), "%05u E %05u N", geoCoord.getOSGREasting(), geoCoord.getOSGRNorthing()); } } else if (gpsFormat == meshtastic_DeviceUIConfig_GpsCoordinateFormat_MLS) { // Maidenhead Locator System double lat = geoCoord.getLatitude() * 1e-7; double lon = geoCoord.getLongitude() * 1e-7; // Normalize if (lat > 90.0) lat = 90.0; if (lat < -90.0) lat = -90.0; while (lon < -180.0) lon += 360.0; while (lon >= 180.0) lon -= 360.0; double adjLon = lon + 180.0; double adjLat = lat + 90.0; char maiden[10]; // enough for 8-char + null // Field (2 letters) int lonField = int(adjLon / 20.0); int latField = int(adjLat / 10.0); adjLon -= lonField * 20.0; adjLat -= latField * 10.0; // Square (2 digits) int lonSquare = int(adjLon / 2.0); int latSquare = int(adjLat / 1.0); adjLon -= lonSquare * 2.0; adjLat -= latSquare * 1.0; // Subsquare (2 letters) double lonUnit = 2.0 / 24.0; double latUnit = 1.0 / 24.0; int lonSub = int(adjLon / lonUnit); int latSub = int(adjLat / latUnit); snprintf(maiden, sizeof(maiden), "%c%c%c%c%c%c", 'A' + lonField, 'A' + latField, '0' + lonSquare, '0' + latSquare, 'A' + lonSub, 'A' + latSub); snprintf(coordinateLine_1, sizeof(coordinateLine_1), "MH: %s", maiden); coordinateLine_2[0] = '\0'; // only need one line } if (strcmp(mode, "line1") == 0) { display->drawString(x, y, coordinateLine_1); } else if (strcmp(mode, "line2") == 0) { display->drawString(x, y, coordinateLine_2); } else if (strcmp(mode, "combined") == 0) { display->drawString(x, y, coordinateLine_1); if (coordinateLine_2[0] != '\0') { display->drawString(x + display->getStringWidth(coordinateLine_1), y, coordinateLine_2); } } } else { char coordinateLine_1[22]; char coordinateLine_2[22]; snprintf(coordinateLine_1, sizeof(coordinateLine_1), "Lat: %2i° %2i' %2u\" %1c", geoCoord.getDMSLatDeg(), geoCoord.getDMSLatMin(), geoCoord.getDMSLatSec(), geoCoord.getDMSLatCP()); snprintf(coordinateLine_2, sizeof(coordinateLine_2), "Lon: %3i° %2i' %2u\" %1c", geoCoord.getDMSLonDeg(), geoCoord.getDMSLonMin(), geoCoord.getDMSLonSec(), geoCoord.getDMSLonCP()); if (strcmp(mode, "line1") == 0) { display->drawString(x, y, coordinateLine_1); } else if (strcmp(mode, "line2") == 0) { display->drawString(x, y, coordinateLine_2); } else { // both display->drawString(x, y, coordinateLine_1); display->drawString(x, y + 10, coordinateLine_2); } } } } #endif // !MESHTASTIC_EXCLUDE_GPS // Draw nodes status void UIRenderer::drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::NodeStatus *nodeStatus, int node_offset, bool show_total, const char *additional_words) { char usersString[20]; int nodes_online = (nodeStatus->getNumOnline() > 0) ? nodeStatus->getNumOnline() + node_offset : 0; snprintf(usersString, sizeof(usersString), "%d %s", nodes_online, additional_words); if (show_total) { int nodes_total = (nodeStatus->getNumTotal() > 0) ? nodeStatus->getNumTotal() + node_offset : 0; snprintf(usersString, sizeof(usersString), "%d/%d %s", nodes_online, nodes_total, additional_words); } #if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS) || \ defined(HACKADAY_COMMUNICATOR) || defined(USE_ST7796)) && \ !defined(DISPLAY_FORCE_SMALL_FONTS) if (currentResolution == ScreenResolution::High) { NodeListRenderer::drawScaledXBitmap16x16(x, y - 1, 8, 8, imgUser, display); } else { display->drawFastImage(x, y + 3, 8, 8, imgUser); } #else if (currentResolution == ScreenResolution::High) { NodeListRenderer::drawScaledXBitmap16x16(x, y - 1, 8, 8, imgUser, display); } else { display->drawFastImage(x, y + 1, 8, 8, imgUser); } #endif int string_offset = (currentResolution == ScreenResolution::High) ? 9 : 0; display->drawString(x + 10 + string_offset, y - 2, usersString); } // ********************** // * Favorite Node Info * // ********************** // cppcheck-suppress constParameterPointer; signature must match FrameCallback typedef from OLEDDisplayUi library void UIRenderer::drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { if (favoritedNodes.empty()) return; // --- Only display if index is valid --- int nodeIndex = state->currentFrame - (screen->frameCount - favoritedNodes.size()); if (nodeIndex < 0 || nodeIndex >= (int)favoritedNodes.size()) return; meshtastic_NodeInfoLite *node = favoritedNodes[nodeIndex]; if (!node || node->num == nodeDB->getNodeNum() || !node->is_favorite) return; display->clear(); #if defined(M5STACK_UNITC6L) uint32_t now = millis(); if (now - lastSwitchTime >= 10000) // 10000 ms = 10 秒 { display->display(); lastSwitchTime = now; } #endif currentFavoriteNodeNum = node->num; // === Create the shortName and title string === const char *shortName = (node->has_user && node->user.short_name[0]) ? node->user.short_name : "Node"; char titlestr[40]; snprintf(titlestr, sizeof(titlestr), "*%s*", shortName); // === Draw battery/time/mail header (common across screens) === graphics::drawCommonHeader(display, x, y, titlestr); // ===== DYNAMIC ROW STACKING WITH YOUR MACROS ===== // 1. Each potential info row has a macro-defined Y position (not regular increments!). // 2. Each row is only shown if it has valid data. // 3. Each row "moves up" if previous are empty, so there are never any blank rows. // 4. The first line is ALWAYS at your macro position; subsequent lines use the next available macro slot. // List of available macro Y positions in order, from top to bottom. int line = 1; // which slot to use next // === 1. Long Name (always try to show first) === const char *username; if (currentResolution == ScreenResolution::UltraLow) { username = (node->has_user && node->user.long_name[0]) ? node->user.short_name : nullptr; } else { username = (node->has_user && node->user.long_name[0]) ? node->user.long_name : nullptr; } if (username) { // Print node's long name (e.g. "Backpack Node") UIRenderer::drawStringWithEmotes(display, x, getTextPositions(display)[line++], username, FONT_HEIGHT_SMALL, 1, false); } // === 2. Signal and Hops (combined on one line, if available) === char signalHopsStr[32] = ""; bool haveSignal = false; int bars = 0; // Helper to get SNR limit based on modem preset auto getSnrLimit = [](meshtastic_Config_LoRaConfig_ModemPreset preset) -> float { switch (preset) { case meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW: case meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE: case meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST: return -6.0f; case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW: case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST: return -5.5f; case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW: case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST: case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO: return -4.5f; default: return -6.0f; } }; // Calculate signal grade using modem preset and SNR only float snrLimit = getSnrLimit(config.lora.modem_preset); float snr = node->snr; // Determine signal quality label and bars using SNR-only grading const char *qualityLabel = nullptr; if (snr > snrLimit + 10) { qualityLabel = "Good"; bars = 4; } else if (snr > snrLimit + 6) { qualityLabel = "Good"; bars = 3; } else if (snr > snrLimit + 2) { qualityLabel = "Good"; bars = 2; } else if (snr > snrLimit - 4) { qualityLabel = "Fair"; bars = 1; } else { qualityLabel = "Bad"; bars = 1; } // Add extra spacing on the left if we have an API connection to account for the common footer icons const char *leftSideSpacing = graphics::isAPIConnected(service->api_state) ? (currentResolution == ScreenResolution::High ? " " : " ") : " "; // --- Build the Signal/Hops line --- // Only show signal if we have valid SNR if (snr > -100 && snr != 0) { snprintf(signalHopsStr, sizeof(signalHopsStr), "%sSig:%s", leftSideSpacing, qualityLabel); haveSignal = true; } if (node->hops_away > 0) { size_t len = strlen(signalHopsStr); if (haveSignal) { snprintf(signalHopsStr + len, sizeof(signalHopsStr) - len, " [#]"); } else { snprintf(signalHopsStr, sizeof(signalHopsStr), "[#]"); } } if (signalHopsStr[0]) { int yPos = getTextPositions(display)[line++]; int curX = x; // Split combined string into signal text and hop suffix char sigPart[20] = ""; const char *hopPart = nullptr; char *bracket = strchr(signalHopsStr, '['); if (bracket) { size_t n = (size_t)(bracket - signalHopsStr); if (n >= sizeof(sigPart)) n = sizeof(sigPart) - 1; memcpy(sigPart, signalHopsStr, n); sigPart[n] = '\0'; // Trim trailing spaces while (strlen(sigPart) && sigPart[strlen(sigPart) - 1] == ' ') { sigPart[strlen(sigPart) - 1] = '\0'; } hopPart = bracket; // "[n Hop(s)]" } else { strncpy(sigPart, signalHopsStr, sizeof(sigPart) - 1); sigPart[sizeof(sigPart) - 1] = '\0'; } // Draw signal quality text display->drawString(curX, yPos, sigPart); curX += display->getStringWidth(sigPart) + 4; // Draw signal bars (skip on UltraLow, text only) if (currentResolution != ScreenResolution::UltraLow && haveSignal && bars > 0) { const int kMaxBars = 4; if (bars < 1) bars = 1; if (bars > kMaxBars) bars = kMaxBars; int barX = curX; const bool hi = (currentResolution == ScreenResolution::High); int barWidth = hi ? 2 : 1; int barGap = hi ? 2 : 1; int maxBarHeight = FONT_HEIGHT_SMALL - 7; if (!hi) maxBarHeight -= 1; int barY = yPos + (FONT_HEIGHT_SMALL - maxBarHeight) / 2; for (int bi = 0; bi < kMaxBars; bi++) { int barHeight = maxBarHeight * (bi + 1) / kMaxBars; if (barHeight < 2) barHeight = 2; int bx = barX + bi * (barWidth + barGap); int by = barY + maxBarHeight - barHeight; if (bi < bars) { display->fillRect(bx, by, barWidth, barHeight); } else { int baseY = barY + maxBarHeight - 1; display->drawHorizontalLine(bx, baseY, barWidth); } } curX += (kMaxBars * barWidth) + ((kMaxBars - 1) * barGap) + 2; } // Draw hops AFTER the bars as: [ number + hop icon ] if (hopPart && node->hops_away > 0) { // open bracket display->drawString(curX, yPos, "["); curX += display->getStringWidth("[") + 1; // hop count char hopCount[6]; snprintf(hopCount, sizeof(hopCount), "%d", node->hops_away); display->drawString(curX, yPos, hopCount); curX += display->getStringWidth(hopCount) + 2; // hop icon const int iconY = yPos + (FONT_HEIGHT_SMALL - hop_height) / 2; display->drawXbm(curX, iconY, hop_width, hop_height, hop); curX += hop_width + 1; // closing bracket display->drawString(curX, yPos, "]"); } } // === 3. Heard (last seen, skip if node never seen) === char seenStr[20] = ""; uint32_t seconds = sinceLastSeen(node); if (seconds != 0 && seconds != UINT32_MAX) { uint32_t minutes = seconds / 60, hours = minutes / 60, days = hours / 24; // Format as "Heard:Xm ago", "Heard:Xh ago", or "Heard:Xd ago" snprintf(seenStr, sizeof(seenStr), (days > 365 ? " Heard:?" : "%sHeard:%d%c ago"), leftSideSpacing, (days ? days : hours ? hours : minutes), (days ? 'd' : hours ? 'h' : 'm')); } if (seenStr[0]) { display->drawString(x, getTextPositions(display)[line++], seenStr); } #if !defined(M5STACK_UNITC6L) // === 4. Uptime (only show if metric is present) === char uptimeStr[32] = ""; if (node->has_device_metrics && node->device_metrics.has_uptime_seconds) { char upPrefix[12]; // enough for leftSideSpacing + "Up:" snprintf(upPrefix, sizeof(upPrefix), "%sUp:", leftSideSpacing); getUptimeStr(node->device_metrics.uptime_seconds * 1000, upPrefix, uptimeStr, sizeof(uptimeStr)); } if (uptimeStr[0]) { display->drawString(x, getTextPositions(display)[line++], uptimeStr); } // === 5. Distance (only if both nodes have GPS position) === meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); char distStr[24] = ""; // Make buffer big enough for any string bool haveDistance = false; if (nodeDB->hasValidPosition(ourNode) && nodeDB->hasValidPosition(node)) { double lat1 = ourNode->position.latitude_i * 1e-7; double lon1 = ourNode->position.longitude_i * 1e-7; double lat2 = node->position.latitude_i * 1e-7; double lon2 = node->position.longitude_i * 1e-7; double earthRadiusKm = 6371.0; double dLat = (lat2 - lat1) * DEG_TO_RAD; double dLon = (lon2 - lon1) * DEG_TO_RAD; double a = sin(dLat / 2) * sin(dLat / 2) + cos(lat1 * DEG_TO_RAD) * cos(lat2 * DEG_TO_RAD) * sin(dLon / 2) * sin(dLon / 2); double c = 2 * atan2(sqrt(a), sqrt(1 - a)); double distanceKm = earthRadiusKm * c; if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { double miles = distanceKm * 0.621371; if (miles < 0.1) { int feet = (int)(miles * 5280); if (feet > 0 && feet < 1000) { snprintf(distStr, sizeof(distStr), "%sDistance:%dft", leftSideSpacing, feet); haveDistance = true; } else if (feet >= 1000) { snprintf(distStr, sizeof(distStr), "%sDistance:¼mi", leftSideSpacing); haveDistance = true; } } else { int roundedMiles = (int)(miles + 0.5); if (roundedMiles > 0 && roundedMiles < 1000) { snprintf(distStr, sizeof(distStr), "%sDistance:%dmi", leftSideSpacing, roundedMiles); haveDistance = true; } } } else { if (distanceKm < 1.0) { int meters = (int)(distanceKm * 1000); if (meters > 0 && meters < 1000) { snprintf(distStr, sizeof(distStr), "%sDistance:%dm", leftSideSpacing, meters); haveDistance = true; } else if (meters >= 1000) { snprintf(distStr, sizeof(distStr), "%sDistance:1km", leftSideSpacing); haveDistance = true; } } else { int km = (int)(distanceKm + 0.5); if (km > 0 && km < 1000) { snprintf(distStr, sizeof(distStr), "%sDistance:%dkm", leftSideSpacing, km); haveDistance = true; } } } } if (haveDistance && distStr[0]) { display->drawString(x, getTextPositions(display)[line++], distStr); } // === 6. Battery after Distance line, otherwise next available line === char batLine[32] = ""; bool haveBatLine = false; if (node->has_device_metrics) { bool hasPct = node->device_metrics.has_battery_level; bool hasVolt = node->device_metrics.has_voltage && node->device_metrics.voltage > 0.001f; int pct = 0; float volt = 0.0f; if (hasPct) { pct = (int)node->device_metrics.battery_level; } if (hasVolt) { volt = node->device_metrics.voltage; } if (hasPct && pct > 0 && pct <= 100) { // Normal battery percentage if (hasVolt) { snprintf(batLine, sizeof(batLine), "%sBat:%d%% (%.2fV)", leftSideSpacing, pct, volt); } else { snprintf(batLine, sizeof(batLine), "%sBat:%d%%", leftSideSpacing, pct); } haveBatLine = true; } else if (hasPct && pct > 100) { // Plugged in if (hasVolt) { snprintf(batLine, sizeof(batLine), "%sPlugged In (%.2fV)", leftSideSpacing, volt); } else { snprintf(batLine, sizeof(batLine), "%sPlugged In", leftSideSpacing); } haveBatLine = true; } else if (!hasPct && hasVolt) { // Voltage only snprintf(batLine, sizeof(batLine), "%sBat:%.2fV", leftSideSpacing, volt); haveBatLine = true; } } const int maxTextLines = (currentResolution == ScreenResolution::High) ? 6 : 5; // Only draw battery if it fits within the allowed lines if (haveBatLine && line <= maxTextLines) { display->drawString(x, getTextPositions(display)[line++], batLine); } // --- Compass Rendering: landscape (wide) screens use the original side-aligned logic --- if (SCREEN_WIDTH > SCREEN_HEIGHT) { bool showCompass = false; if (ourNode && (nodeDB->hasValidPosition(ourNode) || screen->hasHeading()) && nodeDB->hasValidPosition(node)) { showCompass = true; } if (showCompass) { const int16_t topY = getTextPositions(display)[1]; const int16_t bottomY = SCREEN_HEIGHT - (FONT_HEIGHT_SMALL - 1); const int16_t usableHeight = bottomY - topY - 5; int16_t compassRadius = usableHeight / 2; if (compassRadius < 8) compassRadius = 8; const int16_t compassDiam = compassRadius * 2; const int16_t compassX = x + SCREEN_WIDTH - compassRadius - 8; const int16_t compassY = topY + (usableHeight / 2) + ((FONT_HEIGHT_SMALL - 1) / 2) + 2; const auto &op = ourNode->position; float myHeading = screen->hasHeading() ? screen->getHeading() * PI / 180 : screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); const auto &p = node->position; /* unused float d = GeoCoord::latLongToMeter(DegD(p.latitude_i), DegD(p.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i)); */ float bearing = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(p.latitude_i), DegD(p.longitude_i)); if (uiconfig.compass_mode == meshtastic_CompassMode_FREEZE_HEADING) { myHeading = 0; } else { bearing -= myHeading; } display->drawCircle(compassX, compassY, compassRadius); CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading, compassRadius); CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, bearing); } // else show nothing } else { // Portrait or square: put compass at the bottom and centered, scaled to fit available space bool showCompass = false; if (ourNode && (nodeDB->hasValidPosition(ourNode) || screen->hasHeading()) && nodeDB->hasValidPosition(node)) { showCompass = true; } if (showCompass) { int yBelowContent = (line > 0 && line <= 5) ? (getTextPositions(display)[line - 1] + FONT_HEIGHT_SMALL + 2) : getTextPositions(display)[1]; const int margin = 4; // --------- PATCH FOR EINK NAV BAR (ONLY CHANGE BELOW) ----------- #if defined(USE_EINK) const int iconSize = (currentResolution == ScreenResolution::High) ? 16 : 8; const int navBarHeight = iconSize + 6; #else const int navBarHeight = 0; #endif int availableHeight = SCREEN_HEIGHT - yBelowContent - navBarHeight - margin; // --------- END PATCH FOR EINK NAV BAR ----------- if (availableHeight < FONT_HEIGHT_SMALL * 2) return; int compassRadius = availableHeight / 2; if (compassRadius < 8) compassRadius = 8; if (compassRadius * 2 > SCREEN_WIDTH - 16) compassRadius = (SCREEN_WIDTH - 16) / 2; int compassX = x + SCREEN_WIDTH / 2; int compassY = yBelowContent + availableHeight / 2; const auto &op = ourNode->position; float myHeading = 0; if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING) { myHeading = screen->hasHeading() ? screen->getHeading() * PI / 180 : screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); } graphics::CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading, compassRadius); const auto &p = node->position; /* unused float d = GeoCoord::latLongToMeter(DegD(p.latitude_i), DegD(p.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i)); */ float bearing = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(p.latitude_i), DegD(p.longitude_i)); if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING) bearing -= myHeading; graphics::CompassRenderer::drawNodeHeading(display, compassX, compassY, compassRadius * 2, bearing); display->drawCircle(compassX, compassY, compassRadius); } // else show nothing } #endif graphics::drawCommonFooter(display, x, y); } // **************************** // * Device Focused Screen * // **************************** void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { display->clear(); display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); int line = 1; meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); // === Header === if (currentResolution == ScreenResolution::UltraLow) { graphics::drawCommonHeader(display, x, y, "Home"); } else { graphics::drawCommonHeader(display, x, y, ""); } // === Content below header === // Determine if we need to show 4 or 5 rows on the screen int rows = 4; if (!config.bluetooth.enabled) { rows = 5; } // === First Row: Region / Channel Utilization and Uptime === bool origBold = config.display.heading_bold; config.display.heading_bold = false; // Display Region and Channel Utilization if (currentResolution == ScreenResolution::UltraLow) { drawNodes(display, x, getTextPositions(display)[line] + 2, nodeStatus, -1, false, "online"); } else { drawNodes(display, x + 1, getTextPositions(display)[line] + 2, nodeStatus, -1, false, "online"); } char uptimeStr[32] = ""; if (currentResolution != ScreenResolution::UltraLow) { getUptimeStr(millis(), "Up: ", uptimeStr, sizeof(uptimeStr)); } display->drawString(SCREEN_WIDTH - display->getStringWidth(uptimeStr), getTextPositions(display)[line++], uptimeStr); // === Second Row: Satellites and Voltage === config.display.heading_bold = false; #if HAS_GPS if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) { const char *displayLine; if (config.position.fixed_position) { displayLine = "Fixed GPS"; } else { displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off"; } drawSatelliteIcon(display, x, getTextPositions(display)[line]); int xOffset = (currentResolution == ScreenResolution::High) ? 6 : 0; display->drawString(x + 11 + xOffset, getTextPositions(display)[line], displayLine); } else { UIRenderer::drawGps(display, 0, getTextPositions(display)[line], gpsStatus); } #endif #if defined(M5STACK_UNITC6L) line += 1; // === Node Identity === int textWidth = 0; int nameX = 0; const char *shortName = owner.short_name ? owner.short_name : ""; // === ShortName Centered === textWidth = UIRenderer::measureStringWithEmotes(display, shortName); nameX = (SCREEN_WIDTH - textWidth) / 2; UIRenderer::drawStringWithEmotes(display, nameX, getTextPositions(display)[line++], shortName, FONT_HEIGHT_SMALL, 1, false); #else if (powerStatus->getHasBattery()) { char batStr[20]; int batV = powerStatus->getBatteryVoltageMv() / 1000; int batCv = (powerStatus->getBatteryVoltageMv() % 1000) / 10; snprintf(batStr, sizeof(batStr), "%01d.%02dV", batV, batCv); display->drawString(x + SCREEN_WIDTH - display->getStringWidth(batStr), getTextPositions(display)[line++], batStr); } else { display->drawString(x + SCREEN_WIDTH - display->getStringWidth("USB"), getTextPositions(display)[line++], "USB"); } config.display.heading_bold = origBold; // === Third Row: Channel Utilization Bluetooth Off (Only If Actually Off) === const char *chUtil = "ChUtil:"; char chUtilPercentage[10]; snprintf(chUtilPercentage, sizeof(chUtilPercentage), "%2.0f%%", airTime->channelUtilizationPercent()); int chUtil_x = (currentResolution == ScreenResolution::High) ? display->getStringWidth(chUtil) + 10 : display->getStringWidth(chUtil) + 5; int chUtil_y = getTextPositions(display)[line] + 3; int chutil_bar_width = (currentResolution == ScreenResolution::High) ? 100 : 50; if (!config.bluetooth.enabled) { #if defined(USE_EINK) chutil_bar_width = (currentResolution == ScreenResolution::High) ? 50 : 30; #else chutil_bar_width = (currentResolution == ScreenResolution::High) ? 80 : 40; #endif } int chutil_bar_height = (currentResolution == ScreenResolution::High) ? 12 : 7; int extraoffset = (currentResolution == ScreenResolution::High) ? 6 : 3; if (!config.bluetooth.enabled) { extraoffset = (currentResolution == ScreenResolution::High) ? 6 : 1; } int chutil_percent = airTime->channelUtilizationPercent(); int centerofscreen = SCREEN_WIDTH / 2; int total_line_content_width = (chUtil_x + chutil_bar_width + display->getStringWidth(chUtilPercentage) + extraoffset) / 2; int starting_position = centerofscreen - total_line_content_width; if (!config.bluetooth.enabled) { starting_position = 0; } display->drawString(starting_position, getTextPositions(display)[line], chUtil); // Force 56% or higher to show a full 100% bar, text would still show related percent. if (chutil_percent >= 61) { chutil_percent = 100; } // Weighting for nonlinear segments float milestone1 = 25; float milestone2 = 40; float weight1 = 0.45; // Weight for 0–25% float weight2 = 0.35; // Weight for 25–40% float weight3 = 0.20; // Weight for 40–100% float totalWeight = weight1 + weight2 + weight3; int seg1 = chutil_bar_width * (weight1 / totalWeight); int seg2 = chutil_bar_width * (weight2 / totalWeight); int seg3 = chutil_bar_width * (weight3 / totalWeight); int fillRight = 0; if (chutil_percent <= milestone1) { fillRight = (seg1 * (chutil_percent / milestone1)); } else if (chutil_percent <= milestone2) { fillRight = seg1 + (seg2 * ((chutil_percent - milestone1) / (milestone2 - milestone1))); } else { fillRight = seg1 + seg2 + (seg3 * ((chutil_percent - milestone2) / (100 - milestone2))); } // Draw outline display->drawRect(starting_position + chUtil_x, chUtil_y, chutil_bar_width, chutil_bar_height); // Fill progress if (fillRight > 0) { display->fillRect(starting_position + chUtil_x, chUtil_y, fillRight, chutil_bar_height); } display->drawString(starting_position + chUtil_x + chutil_bar_width + extraoffset, getTextPositions(display)[line], chUtilPercentage); if (!config.bluetooth.enabled) { display->drawString(SCREEN_WIDTH - display->getStringWidth("BT off"), getTextPositions(display)[line], "BT off"); } line += 1; // === Fourth & Fifth Rows: Node Identity === int textWidth = 0; int nameX = 0; int yOffset = (currentResolution == ScreenResolution::High) ? 0 : 5; const char *longName = (ourNode && ourNode->has_user && ourNode->user.long_name[0]) ? ourNode->user.long_name : ""; const char *shortName = owner.short_name ? owner.short_name : ""; char combinedName[96]; if (longName[0] && shortName[0]) { snprintf(combinedName, sizeof(combinedName), "%s (%s)", longName, shortName); } else if (longName[0]) { strncpy(combinedName, longName, sizeof(combinedName) - 1); combinedName[sizeof(combinedName) - 1] = '\0'; } else { strncpy(combinedName, shortName, sizeof(combinedName) - 1); combinedName[sizeof(combinedName) - 1] = '\0'; } if (SCREEN_WIDTH - UIRenderer::measureStringWithEmotes(display, combinedName) > 10) { textWidth = UIRenderer::measureStringWithEmotes(display, combinedName); nameX = (SCREEN_WIDTH - textWidth) / 2; UIRenderer::drawStringWithEmotes( display, nameX, ((rows == 4) ? getTextPositions(display)[line++] : getTextPositions(display)[line++]) + yOffset, combinedName, FONT_HEIGHT_SMALL, 1, false); } else { // === LongName Centered === textWidth = UIRenderer::measureStringWithEmotes(display, longName); nameX = (SCREEN_WIDTH - textWidth) / 2; UIRenderer::drawStringWithEmotes(display, nameX, getTextPositions(display)[line++], longName, FONT_HEIGHT_SMALL, 1, false); // === ShortName Centered === textWidth = UIRenderer::measureStringWithEmotes(display, shortName); nameX = (SCREEN_WIDTH - textWidth) / 2; UIRenderer::drawStringWithEmotes(display, nameX, getTextPositions(display)[line++], shortName, FONT_HEIGHT_SMALL, 1, false); } #endif graphics::drawCommonFooter(display, x, y); } // Start Functions to write date/time to the screen // Helper function to check if a year is a leap year constexpr bool isLeapYear(int year) { return (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)); } // Array of days in each month (non-leap year) const int daysInMonth[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; // Fills the buffer with a formatted date/time string and returns pixel width int UIRenderer::formatDateTime(char *buf, size_t bufSize, uint32_t rtc_sec, OLEDDisplay *display, bool includeTime) { int sec = rtc_sec % 60; rtc_sec /= 60; int min = rtc_sec % 60; rtc_sec /= 60; int hour = rtc_sec % 24; rtc_sec /= 24; int year = 1970; while (true) { int daysInYear = isLeapYear(year) ? 366 : 365; if (rtc_sec >= (uint32_t)daysInYear) { rtc_sec -= daysInYear; year++; } else { break; } } int month = 0; while (month < 12) { int dim = daysInMonth[month]; if (month == 1 && isLeapYear(year)) dim++; if (rtc_sec >= (uint32_t)dim) { rtc_sec -= dim; month++; } else { break; } } int day = rtc_sec + 1; if (includeTime) { snprintf(buf, bufSize, "%04d-%02d-%02d %02d:%02d:%02d", year, month + 1, day, hour, min, sec); } else { snprintf(buf, bufSize, "%04d-%02d-%02d", year, month + 1, day); } return display->getStringWidth(buf); } // Check if the display can render a string (detect special chars; emoji) bool UIRenderer::haveGlyphs(const char *str) { #if defined(OLED_PL) || defined(OLED_UA) || defined(OLED_RU) || defined(OLED_CS) // Don't want to make any assumptions about custom language support return true; #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; } #ifdef USE_EINK /// Used on eink displays while in deep sleep void UIRenderer::drawDeepSleepFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { // Next frame should use full-refresh, and block while running, else device will sleep before async callback EINK_ADD_FRAMEFLAG(display, COSMETIC); EINK_ADD_FRAMEFLAG(display, BLOCKING); LOG_DEBUG("Draw deep sleep screen"); // Display displayStr on the screen graphics::UIRenderer::drawIconScreen("Sleeping", display, state, x, y); } /// Used on eink displays when screen updates are paused void UIRenderer::drawScreensaverOverlay(OLEDDisplay *display, OLEDDisplayUiState *state) { LOG_DEBUG("Draw screensaver overlay"); EINK_ADD_FRAMEFLAG(display, COSMETIC); // Full refresh for screensaver // Config display->setFont(FONT_SMALL); display->setTextAlignment(TEXT_ALIGN_LEFT); const char *pauseText = "Screen Paused"; const char *idText = owner.short_name; const bool useId = (idText && idText[0]); constexpr uint8_t padding = 2; constexpr uint8_t dividerGap = 1; // Text widths const uint16_t idTextWidth = useId ? UIRenderer::measureStringWithEmotes(display, idText) : 0; const uint16_t pauseTextWidth = display->getStringWidth(pauseText, strlen(pauseText)); const uint16_t boxWidth = padding + (useId ? idTextWidth + padding : 0) + pauseTextWidth + padding; const uint16_t boxHeight = FONT_HEIGHT_SMALL + (padding * 2); // Flush with bottom const int16_t boxLeft = (display->width() / 2) - (boxWidth / 2); const int16_t boxTop = display->height() - boxHeight; const int16_t boxBottom = display->height() - 1; const int16_t idTextLeft = boxLeft + padding; const int16_t idTextTop = boxTop + padding; const int16_t pauseTextLeft = boxLeft + (useId ? idTextWidth + (padding * 2) : 0) + padding; const int16_t pauseTextTop = boxTop + padding; const int16_t dividerX = boxLeft + padding + idTextWidth + padding; const int16_t dividerTop = boxTop + dividerGap; const int16_t dividerBottom = boxBottom - dividerGap; // Draw: box display->setColor(EINK_WHITE); display->fillRect(boxLeft, boxTop, boxWidth, boxHeight); display->setColor(EINK_BLACK); display->drawRect(boxLeft, boxTop, boxWidth, boxHeight); // Draw: text if (useId) UIRenderer::drawStringWithEmotes(display, idTextLeft, idTextTop, idText, FONT_HEIGHT_SMALL, 1, false); display->drawString(pauseTextLeft, pauseTextTop, pauseText); display->drawString(pauseTextLeft + 1, pauseTextTop, pauseText); // Faux bold // Draw: divider if (useId) display->drawLine(dividerX, dividerTop, dividerX, dividerBottom); } #endif /** * Draw the icon with extra info printed around the corners */ void UIRenderer::drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { // draw an xbm image. // Please note that everything that should be transitioned // needs to be drawn relative to x and y // draw centered icon left to right and centered above the one line of app text #if defined(M5STACK_UNITC6L) display->drawXbm(x + (SCREEN_WIDTH - 50) / 2, y + (SCREEN_HEIGHT - 28) / 2, icon_width, icon_height, icon_bits); display->setFont(FONT_MEDIUM); display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); // Draw region in upper left if (upperMsg) { int msgWidth = display->getStringWidth(upperMsg); int msgX = x + (SCREEN_WIDTH - msgWidth) / 2; int msgY = y; display->drawString(msgX, msgY, upperMsg); } // Draw version and short name in bottom middle char footer[64]; if (owner.short_name && owner.short_name[0]) { snprintf(footer, sizeof(footer), "%s %s", xstr(APP_VERSION_SHORT), owner.short_name); } else { snprintf(footer, sizeof(footer), "%s", xstr(APP_VERSION_SHORT)); } int footerW = UIRenderer::measureStringWithEmotes(display, footer); int footerX = x + ((SCREEN_WIDTH - footerW) / 2); UIRenderer::drawStringWithEmotes(display, footerX, y + SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM, footer, FONT_HEIGHT_SMALL, 1, false); screen->forceDisplay(); display->setTextAlignment(TEXT_ALIGN_LEFT); // Restore left align, just to be kind to any other unsuspecting code #else display->drawXbm(x + (SCREEN_WIDTH - icon_width) / 2, y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - icon_height) / 2 + 2, icon_width, icon_height, icon_bits); display->setFont(FONT_MEDIUM); display->setTextAlignment(TEXT_ALIGN_LEFT); const char *title = "meshtastic.org"; display->drawString(x + getStringCenteredX(title), y + SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM, title); display->setFont(FONT_SMALL); // Draw region in upper left if (upperMsg) display->drawString(x + 0, y + 0, upperMsg); // Draw version and short name in upper right const char *version = xstr(APP_VERSION_SHORT); int versionX = x + SCREEN_WIDTH - display->getStringWidth(version); display->drawString(versionX, y + 0, version); if (owner.short_name && owner.short_name[0]) { const char *shortName = owner.short_name; int shortNameW = UIRenderer::measureStringWithEmotes(display, shortName); int shortNameX = x + SCREEN_WIDTH - shortNameW; UIRenderer::drawStringWithEmotes(display, shortNameX, y + FONT_HEIGHT_SMALL, shortName, FONT_HEIGHT_SMALL, 1, false); } screen->forceDisplay(); display->setTextAlignment(TEXT_ALIGN_LEFT); // Restore left align, just to be kind to any other unsuspecting code #endif } // **************************** // * My Position Screen * // **************************** void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { display->clear(); display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); int line = 1; // === Set Title const char *titleStr = "Position"; // === Header === graphics::drawCommonHeader(display, x, y, titleStr); // === First Row: My Location === #if HAS_GPS bool origBold = config.display.heading_bold; config.display.heading_bold = false; const char *displayLine = ""; // Initialize to empty string by default if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) { if (config.position.fixed_position) { displayLine = "Fixed GPS"; } else { displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off"; } drawSatelliteIcon(display, x, getTextPositions(display)[line]); int xOffset = (currentResolution == ScreenResolution::High) ? 6 : 0; display->drawString(x + 11 + xOffset, getTextPositions(display)[line++], displayLine); } else { // Onboard GPS UIRenderer::drawGps(display, 0, getTextPositions(display)[line++], gpsStatus); } config.display.heading_bold = origBold; // === Update GeoCoord === geoCoord.updateCoords(int32_t(gpsStatus->getLatitude()), int32_t(gpsStatus->getLongitude()), int32_t(gpsStatus->getAltitude())); // === Determine Compass Heading === float heading = 0; bool validHeading = false; if (uiconfig.compass_mode == meshtastic_CompassMode_FREEZE_HEADING) { validHeading = true; } else { if (screen->hasHeading()) { heading = radians(screen->getHeading()); validHeading = true; } else { heading = screen->estimatedHeading(geoCoord.getLatitude() * 1e-7, geoCoord.getLongitude() * 1e-7); validHeading = !isnan(heading); } } // If GPS is off, no need to display these parts if (strcmp(displayLine, "GPS off") != 0 && strcmp(displayLine, "No GPS") != 0) { // === Second Row: Last GPS Fix === if (gpsStatus->getLastFixMillis() > 0) { uint32_t delta = millis() - gpsStatus->getLastFixMillis(); char uptimeStr[32]; #if defined(USE_EINK) // E-Ink: skip seconds, show only days/hours/mins getUptimeStr(delta, "Last: ", uptimeStr, sizeof(uptimeStr), false); #else // Non E-Ink: include seconds where useful getUptimeStr(delta, "Last: ", uptimeStr, sizeof(uptimeStr), true); #endif display->drawString(0, getTextPositions(display)[line++], uptimeStr); } else { display->drawString(0, getTextPositions(display)[line++], "Last: ?"); } // === Third Row: Line 1 GPS Info === UIRenderer::drawGpsCoordinates(display, x, getTextPositions(display)[line++], gpsStatus, "line1"); if (uiconfig.gps_format != meshtastic_DeviceUIConfig_GpsCoordinateFormat_OLC && uiconfig.gps_format != meshtastic_DeviceUIConfig_GpsCoordinateFormat_MLS) { // === Fourth Row: Line 2 GPS Info === UIRenderer::drawGpsCoordinates(display, x, getTextPositions(display)[line++], gpsStatus, "line2"); } // === Final Row: Altitude === char altitudeLine[32] = {0}; int32_t alt = geoCoord.getAltitude(); if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { snprintf(altitudeLine, sizeof(altitudeLine), "Alt: %.0fft", alt * METERS_TO_FEET); } else { snprintf(altitudeLine, sizeof(altitudeLine), "Alt: %.0im", alt); } display->drawString(x, getTextPositions(display)[line++], altitudeLine); } #if !defined(M5STACK_UNITC6L) // === Draw Compass if heading is valid === if (validHeading) { // --- Compass Rendering: landscape (wide) screens use original side-aligned logic --- if (SCREEN_WIDTH > SCREEN_HEIGHT) { const int16_t topY = getTextPositions(display)[1]; const int16_t bottomY = SCREEN_HEIGHT - (FONT_HEIGHT_SMALL - 1); // nav row height const int16_t usableHeight = bottomY - topY - 5; int16_t compassRadius = usableHeight / 2; if (compassRadius < 8) compassRadius = 8; const int16_t compassDiam = compassRadius * 2; const int16_t compassX = x + SCREEN_WIDTH - compassRadius - 8; // Center vertically and nudge down slightly to keep "N" clear of header const int16_t compassY = topY + (usableHeight / 2) + ((FONT_HEIGHT_SMALL - 1) / 2) + 2; CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, -heading); display->drawCircle(compassX, compassY, compassRadius); // "N" label float northAngle = 0; if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING) northAngle = -heading; float radius = compassRadius; int16_t nX = compassX + (radius - 1) * sin(northAngle); int16_t nY = compassY - (radius - 1) * cos(northAngle); int16_t nLabelWidth = display->getStringWidth("N") + 2; int16_t nLabelHeightBox = FONT_HEIGHT_SMALL + 1; display->setColor(BLACK); display->fillRect(nX - nLabelWidth / 2, nY - nLabelHeightBox / 2, nLabelWidth, nLabelHeightBox); display->setColor(WHITE); display->setFont(FONT_SMALL); display->setTextAlignment(TEXT_ALIGN_CENTER); display->drawString(nX, nY - FONT_HEIGHT_SMALL / 2, "N"); } else { // Portrait or square: put compass at the bottom and centered, scaled to fit available space // For E-Ink screens, account for navigation bar at the bottom! int yBelowContent = getTextPositions(display)[5] + FONT_HEIGHT_SMALL + 2; const int margin = 4; int availableHeight = #if defined(USE_EINK) SCREEN_HEIGHT - yBelowContent - 24; // Leave extra space for nav bar on E-Ink #else SCREEN_HEIGHT - yBelowContent - margin; #endif if (availableHeight < FONT_HEIGHT_SMALL * 2) return; int compassRadius = availableHeight / 2; if (compassRadius < 8) compassRadius = 8; if (compassRadius * 2 > SCREEN_WIDTH - 16) compassRadius = (SCREEN_WIDTH - 16) / 2; int compassX = x + SCREEN_WIDTH / 2; int compassY = yBelowContent + availableHeight / 2; CompassRenderer::drawNodeHeading(display, compassX, compassY, compassRadius * 2, -heading); display->drawCircle(compassX, compassY, compassRadius); // "N" label float northAngle = 0; if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING) northAngle = -heading; float radius = compassRadius; int16_t nX = compassX + (radius - 1) * sin(northAngle); int16_t nY = compassY - (radius - 1) * cos(northAngle); int16_t nLabelWidth = display->getStringWidth("N") + 2; int16_t nLabelHeightBox = FONT_HEIGHT_SMALL + 1; display->setColor(BLACK); display->fillRect(nX - nLabelWidth / 2, nY - nLabelHeightBox / 2, nLabelWidth, nLabelHeightBox); display->setColor(WHITE); display->setFont(FONT_SMALL); display->setTextAlignment(TEXT_ALIGN_CENTER); display->drawString(nX, nY - FONT_HEIGHT_SMALL / 2, "N"); } } #endif #endif // HAS_GPS graphics::drawCommonFooter(display, x, y); } #ifdef USERPREFS_OEM_TEXT void UIRenderer::drawOEMIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { static const uint8_t xbm[] = USERPREFS_OEM_IMAGE_DATA; if (currentResolution == ScreenResolution::High) { display->drawXbm(x + (SCREEN_WIDTH - USERPREFS_OEM_IMAGE_WIDTH) / 2, y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - USERPREFS_OEM_IMAGE_HEIGHT) / 2 + 2, USERPREFS_OEM_IMAGE_WIDTH, USERPREFS_OEM_IMAGE_HEIGHT, xbm); } else { display->drawXbm(x + (SCREEN_WIDTH - USERPREFS_OEM_IMAGE_WIDTH) / 2, y + (SCREEN_HEIGHT - USERPREFS_OEM_IMAGE_HEIGHT) / 2 + 2, USERPREFS_OEM_IMAGE_WIDTH, USERPREFS_OEM_IMAGE_HEIGHT, xbm); } switch (USERPREFS_OEM_FONT_SIZE) { case 0: display->setFont(FONT_SMALL); break; case 2: display->setFont(FONT_LARGE); break; default: display->setFont(FONT_MEDIUM); break; } display->setTextAlignment(TEXT_ALIGN_LEFT); const char *title = USERPREFS_OEM_TEXT; if (currentResolution == ScreenResolution::High) { display->drawString(x + getStringCenteredX(title), y + SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM, title); } display->setFont(FONT_SMALL); // Draw region in upper left if (upperMsg) display->drawString(x + 0, y + 0, upperMsg); // Draw version and shortname in upper right const char *version = xstr(APP_VERSION_SHORT); int versionX = x + SCREEN_WIDTH - display->getStringWidth(version); display->drawString(versionX, y + 0, version); if (owner.short_name && owner.short_name[0]) { const char *shortName = owner.short_name; int shortNameW = UIRenderer::measureStringWithEmotes(display, shortName); int shortNameX = x + SCREEN_WIDTH - shortNameW; UIRenderer::drawStringWithEmotes(display, shortNameX, y + FONT_HEIGHT_SMALL, shortName, FONT_HEIGHT_SMALL, 1, false); } screen->forceDisplay(); display->setTextAlignment(TEXT_ALIGN_LEFT); // Restore left align, just to be kind to any other unsuspecting code } void UIRenderer::drawOEMBootScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { // Draw region in upper left const char *region = myRegion ? myRegion->name : NULL; drawOEMIconScreen(region, display, state, x, y); } #endif // Navigation bar overlay implementation static int8_t lastFrameIndex = -1; static uint32_t lastFrameChangeTime = 0; constexpr uint32_t ICON_DISPLAY_DURATION_MS = 2000; // cppcheck-suppress constParameterPointer; signature must match OverlayCallback typedef from OLEDDisplayUi library void UIRenderer::drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *state) { int currentFrame = state->currentFrame; // Detect frame change and record time if (currentFrame != lastFrameIndex) { lastFrameIndex = currentFrame; lastFrameChangeTime = millis(); } const int iconSize = (currentResolution == ScreenResolution::High) ? 16 : 8; const int spacing = (currentResolution == ScreenResolution::High) ? 8 : 4; const int bigOffset = (currentResolution == ScreenResolution::High) ? 1 : 0; const size_t totalIcons = screen->indicatorIcons.size(); if (totalIcons == 0) return; const int navPadding = (currentResolution == ScreenResolution::High) ? 24 : 12; // padding per side int usableWidth = SCREEN_WIDTH - (navPadding * 2); if (usableWidth < iconSize) usableWidth = iconSize; const size_t iconsPerPage = usableWidth / (iconSize + spacing); const size_t currentPage = currentFrame / iconsPerPage; const size_t pageStart = currentPage * iconsPerPage; const size_t pageEnd = min(pageStart + iconsPerPage, totalIcons); const int totalWidth = (pageEnd - pageStart) * iconSize + (pageEnd - pageStart - 1) * spacing; const int xStart = (SCREEN_WIDTH - totalWidth) / 2; bool navBarVisible = millis() - lastFrameChangeTime <= ICON_DISPLAY_DURATION_MS; int y = navBarVisible ? (SCREEN_HEIGHT - iconSize - 1) : SCREEN_HEIGHT; #if defined(USE_EINK) // Only show bar briefly after switching frames static uint32_t navBarLastShown = 0; static bool cosmeticRefreshDone = false; static bool navBarPrevVisible = false; if (navBarVisible && !navBarPrevVisible) { EINK_ADD_FRAMEFLAG(display, DEMAND_FAST); // Fast refresh when showing nav bar cosmeticRefreshDone = false; navBarLastShown = millis(); } if (!navBarVisible && navBarPrevVisible) { EINK_ADD_FRAMEFLAG(display, DEMAND_FAST); // Fast refresh when hiding nav bar navBarLastShown = millis(); // Mark when it disappeared } if (!navBarVisible && navBarLastShown != 0 && !cosmeticRefreshDone) { if (millis() - navBarLastShown > 10000) { // 10s after hidden EINK_ADD_FRAMEFLAG(display, COSMETIC); // One-time ghost cleanup cosmeticRefreshDone = true; } } navBarPrevVisible = navBarVisible; #endif // Pre-calculate bounding rect const int rectX = xStart - 2 - bigOffset; const int rectWidth = totalWidth + 4 + (bigOffset * 2); const int rectHeight = iconSize + 6; // Clear background and draw border display->setColor(BLACK); display->fillRect(rectX + 1, y - 2, rectWidth - 2, rectHeight - 2); display->setColor(WHITE); display->drawRect(rectX, y - 2, rectWidth, rectHeight); // Icon drawing loop for the current page for (size_t i = pageStart; i < pageEnd; ++i) { const uint8_t *icon = screen->indicatorIcons[i]; const int x = xStart + (i - pageStart) * (iconSize + spacing); const bool isActive = (i == static_cast(currentFrame)); if (isActive) { display->setColor(WHITE); display->fillRect(x - 2, y - 2, iconSize + 4, iconSize + 4); display->setColor(BLACK); } if (currentResolution == ScreenResolution::High) { NodeListRenderer::drawScaledXBitmap16x16(x, y, 8, 8, icon, display); } else { display->drawXbm(x, y, iconSize, iconSize, icon); } if (isActive) { display->setColor(WHITE); } } // Compact arrow drawer auto drawArrow = [&](bool rightSide) { display->setColor(WHITE); const int offset = (currentResolution == ScreenResolution::High) ? 3 : 1; const int halfH = rectHeight / 2; const int top = (y - 2) + (rectHeight - halfH) / 2; const int bottom = top + halfH - 1; const int midY = top + (halfH / 2); const int maxW = 4; // Determine left X coordinate int baseX = rightSide ? (rectX + rectWidth + offset) : // right arrow (rectX - offset - 1); // left arrow for (int yy = top; yy <= bottom; yy++) { int dist = abs(yy - midY); int lineW = maxW - (dist * maxW / (halfH / 2)); if (lineW < 1) lineW = 1; if (rightSide) { display->drawHorizontalLine(baseX, yy, lineW); } else { display->drawHorizontalLine(baseX - lineW + 1, yy, lineW); } } }; // Right arrow if (pageEnd < totalIcons) { drawArrow(true); } // Left arrow if (pageStart > 0) { drawArrow(false); } // Knock the corners off the square display->setColor(BLACK); display->drawRect(rectX, y - 2, 1, 1); display->drawRect(rectX + rectWidth - 1, y - 2, 1, 1); display->setColor(WHITE); } void UIRenderer::drawFrameText(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *message) { uint16_t x_offset = display->width() / 2; display->setTextAlignment(TEXT_ALIGN_CENTER); display->setFont(FONT_MEDIUM); display->drawString(x_offset + x, 26 + y, message); } std::string UIRenderer::drawTimeDelta(uint32_t days, uint32_t hours, uint32_t minutes, uint32_t seconds) { std::string uptime; if (days > (HOURS_IN_MONTH * 6)) uptime = "?"; else if (days >= 2) uptime = std::to_string(days) + "d"; else if (hours >= 2) uptime = std::to_string(hours) + "h"; else if (minutes >= 1) uptime = std::to_string(minutes) + "m"; else uptime = std::to_string(seconds) + "s"; return uptime; } int UIRenderer::measureStringWithEmotes(OLEDDisplay *display, const char *line, int emoteSpacing) { return graphics::EmoteRenderer::measureStringWithEmotes(display, line, graphics::emotes, graphics::numEmotes, emoteSpacing); } size_t UIRenderer::truncateStringWithEmotes(OLEDDisplay *display, const char *line, char *out, size_t outSize, int maxWidth, const char *ellipsis, int emoteSpacing) { return graphics::EmoteRenderer::truncateToWidth(display, line, out, outSize, maxWidth, ellipsis, graphics::emotes, graphics::numEmotes, emoteSpacing); } void UIRenderer::drawStringWithEmotes(OLEDDisplay *display, int x, int y, const char *line, int fontHeight, int emoteSpacing, bool fauxBold) { graphics::EmoteRenderer::drawStringWithEmotes(display, x, y, line, fontHeight, graphics::emotes, graphics::numEmotes, emoteSpacing, fauxBold); } } // namespace graphics #endif // HAS_SCREEN ================================================ FILE: src/graphics/draw/UIRenderer.h ================================================ #pragma once #include "NodeDB.h" #include "graphics/EmoteRenderer.h" #include "graphics/Screen.h" #include "graphics/emotes.h" #include #include #include #define HOURS_IN_MONTH 730 // Forward declarations for status types namespace meshtastic { class PowerStatus; class NodeStatus; class GPSStatus; } // namespace meshtastic namespace graphics { /// Forward declarations class Screen; /** * @brief UI utility drawing functions * * Contains utility functions for drawing common UI elements, overlays, * battery indicators, and other shared graphical components. */ class UIRenderer { public: // Common UI elements static void drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::NodeStatus *nodeStatus, int node_offset = 0, bool show_total = true, const char *additional_words = ""); // GPS status functions static void drawGps(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus); static void drawGpsCoordinates(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus, const char *mode = "line1"); static void drawGpsAltitude(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus); static void drawGpsPowerStatus(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus); // Overlay and special screens static void drawFrameText(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *text); // Navigation bar overlay static void drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *state); static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); // Icon and screen drawing functions static void drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); // Compass and location screen static void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); static NodeNum currentFavoriteNodeNum; static std::vector favoritedNodes; static void rebuildFavoritedNodes(); // OEM screens #ifdef USERPREFS_OEM_TEXT static void drawOEMIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); static void drawOEMBootScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); #endif #ifdef USE_EINK /// Used on eink displays while in deep sleep static void drawDeepSleepFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); /// Used on eink displays when screen updates are paused static void drawScreensaverOverlay(OLEDDisplay *display, OLEDDisplayUiState *state); #endif static std::string drawTimeDelta(uint32_t days, uint32_t hours, uint32_t minutes, uint32_t seconds); static int formatDateTime(char *buffer, size_t bufferSize, uint32_t rtc_sec, OLEDDisplay *display, bool showTime); // Shared BaseUI emote helpers. static int measureStringWithEmotes(OLEDDisplay *display, const char *line, int emoteSpacing = 1); static inline int measureStringWithEmotes(OLEDDisplay *display, const std::string &line, int emoteSpacing = 1) { return measureStringWithEmotes(display, line.c_str(), emoteSpacing); } static size_t truncateStringWithEmotes(OLEDDisplay *display, const char *line, char *out, size_t outSize, int maxWidth, const char *ellipsis = "...", int emoteSpacing = 1); static inline std::string truncateStringWithEmotes(OLEDDisplay *display, const std::string &line, int maxWidth, const std::string &ellipsis = "...", int emoteSpacing = 1) { return graphics::EmoteRenderer::truncateToWidth(display, line, maxWidth, ellipsis, graphics::emotes, graphics::numEmotes, emoteSpacing); } static void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const char *line, int fontHeight, int emoteSpacing = 1, bool fauxBold = true); static inline void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string &line, int fontHeight, int emoteSpacing = 1, bool fauxBold = true) { drawStringWithEmotes(display, x, y, line.c_str(), fontHeight, emoteSpacing, fauxBold); } // Check if the display can render a string (detect special chars; emoji) static bool haveGlyphs(const char *str); }; // namespace UIRenderer } // namespace graphics ================================================ FILE: src/graphics/emotes.cpp ================================================ #include "configuration.h" #if HAS_SCREEN #include "emotes.h" namespace graphics { // Always define Emote list and count const Emote emotes[] = { #ifndef EXCLUDE_EMOJI // --- Thumbs --- {"\U0001F44D", thumbup, thumbs_width, thumbs_height}, // 👍 Thumbs Up {"\U0001F44E", thumbdown, thumbs_width, thumbs_height}, // 👎 Thumbs Down // --- Smileys (Multiple Unicode Aliases) --- {"\U0001F60A", smiling_eyes, smiling_eyes_width, smiling_eyes_height}, // 😊 Smiling Eyes {"\U0001F600", grinning, grinning_width, grinning_height}, // 😀 Grinning Face {"\U0001F642", slightly_smiling, slightly_smiling_width, slightly_smiling_height}, // 🙂 Slightly Smiling Face {"\U0001F609", winking_face, winking_face_width, winking_face_height}, // 😉 Winking Face {"\U0001F601", grinning_smiling_eyes, grinning_smiling_eyes_width, grinning_smiling_eyes_height}, // 😁 Grinning Smiling Eyes {"\U0001F60D", heart_eyes, heart_eyes_width, heart_eyes_height}, // 😍 Heart Eyes {"\U0001F970", heart_smile, heart_smile_width, heart_smile_height}, // 🥰 Smiling Face with Hearts // --- Question/Alert --- {"\u2753", question, question_width, question_height}, // ❓ Question Mark {"\u203C\uFE0F", bang, bang_width, bang_height}, // ‼️ Double Exclamation Mark {"\u26A0\uFE0F", caution, caution_width, caution_height}, // ⚠️ Warning Sign // --- Laughing Faces --- {"\U0001F602", haha, haha_width, haha_height}, // 😂 Face with Tears of Joy {"\U0001F923", rofl, rofl_width, rofl_height}, // 🤣 Rolling on the Floor Laughing {"\U0001F606", smiling_closed_eyes, smiling_closed_eyes_width, smiling_closed_eyes_height}, // 😆 Smiling Closed Eyes {"\U0001F605", haha, haha_width, haha_height}, // 😅 Smiling with Sweat {"\U0001F604", grinning_smiling_eyes_2, grinning_smiling_eyes_2_width, grinning_smiling_eyes_2_height}, // 😄 Grinning Face with Smiling Eyes {"\U0001F62D", loudly_crying_face, loudly_crying_face_width, loudly_crying_face_height}, // 😭 Loudly Crying Face {"\U0001F92E", vomiting, vomiting_width, vomiting_height}, // 🤮 Face Vomiting {"\U0001F60E", cool, cool_width, cool_height}, // 😎 Smiling Face with Sunglasses {"\U0001F440", eyes, eyes_width, eyes_height}, // 👀 Eyes {"\U0001F441\uFE0F", eye, eye_width, eye_height}, // 👁️ Eye // --- Gestures and People --- {"\U0001F44B", wave_icon, wave_icon_width, wave_icon_height}, // 👋 Waving Hand {"\u270C\uFE0F", peace_sign, peace_sign_width, peace_sign_height}, // ✌️ Victory Hand {"\U0001F596", vulcan_salute, vulcan_salute_width, vulcan_salute_height}, // 🖖 Vulcan Salute {"\U0001F64F", praying, praying_width, praying_height}, // 🙏 Praying Hands {"\U0001F4AA", strong, strong_width, strong_height}, // 💪 Flexed Biceps {"\U0001F937", shrug, shrug_width, shrug_height}, // 🤷 Person Shrugging {"\U0001F920", cowboy, cowboy_width, cowboy_height}, // 🤠 Cowboy Hat Face {"\U0001F3A7", deadmau5, deadmau5_width, deadmau5_height}, // 🎧 Headphones // --- Symbols --- {"\u2714\uFE0F", check_mark, check_mark_width, check_mark_height}, // ✔️ Check Mark {"\u2705", check_mark, check_mark_width, check_mark_height}, // ✅ Check Mark Button {"\u2611\uFE0F", check_mark, check_mark_width, check_mark_height}, // ☑️ Check Box with Check {"\U0001F3E0", house, house_width, house_height}, // 🏠 House // --- Weather --- {"\u2600", sun, sun_width, sun_height}, // ☀ Sun (without variation selector) {"\u2600\uFE0F", sun, sun_width, sun_height}, // ☀️ Sun (with variation selector) {"\U0001F327\uFE0F", rain, rain_width, rain_height}, // 🌧️ Cloud with Rain {"\u2601\uFE0F", cloud, cloud_width, cloud_height}, // ☁️ Cloud {"\U0001F32B\uFE0F", fog, fog_width, fog_height}, // 🌫️ Fog {"\u2744\uFE0F", snowflake, snowflake_width, snowflake_height}, // ❄️ Snowflake {"\U0001F4A7", drop, drop_width, drop_height}, // 💧 Droplet {"\U0001F321\uFE0F", thermometer, thermometer_width, thermometer_height}, // 🌡️ Thermometer {"\U0001F326\uFE0F", sun_behind_raincloud, sun_behind_raincloud_width, sun_behind_raincloud_height}, // 🌦️ Sun Behind Rain Cloud {"\u26C5", sun_behind_cloud, sun_behind_cloud_width, sun_behind_cloud_height}, // ⛅ Sun Behind Cloud {"\u26C5\uFE0F", sun_behind_cloud, sun_behind_cloud_width, sun_behind_cloud_height}, // ⛅️ Sun Behind Cloud {"\U0001F328\uFE0F", cloud_with_snow, cloud_with_snow_width, cloud_with_snow_height}, // 🌨️ Cloud with Snow {"\U0001F329\uFE0F", cloud_with_lightning, cloud_with_lightning_width, cloud_with_lightning_height}, // 🌩️ Cloud with Lightning {"\u26C8", cloud_with_lightning_rain, cloud_with_lightning_rain_width, cloud_with_lightning_rain_height}, // ⛈ Cloud with Lightning and Rain {"\u26C8\uFE0F", cloud_with_lightning_rain, cloud_with_lightning_rain_width, cloud_with_lightning_rain_height}, // ⛈️ Cloud with Lightning and Rain {"\U0001F32C\uFE0F", wind_face, wind_face_width, wind_face_height}, // 🌬️ Wind Face // --- Moon Phases --- {"\U0001F311", new_moon, new_moon_width, new_moon_height}, // 🌑 New Moon {"\U0001F312", waxing_crescent_moon, waxing_crescent_moon_width, waxing_crescent_moon_height}, // 🌒 Waxing Crescent Moon {"\U0001F313", first_quarter_moon, first_quarter_moon_width, first_quarter_moon_height}, // 🌓 First Quarter Moon {"\U0001F314", waxing_gibbous_moon, waxing_gibbous_moon_width, waxing_gibbous_moon_height}, // 🌔 Waxing Gibbous Moon {"\U0001F315", full_moon, full_moon_width, full_moon_height}, // 🌕 Full Moon {"\U0001F316", waning_gibbous_moon, waning_gibbous_moon_width, waning_gibbous_moon_height}, // 🌖 Waning Gibbous Moon {"\U0001F317", last_quarter_moon, last_quarter_moon_width, last_quarter_moon_height}, // 🌗 Last Quarter Moon {"\U0001F318", waning_crescent_moon, waning_crescent_moon_width, waning_crescent_moon_height}, // 🌘 Waning Crescent Moon {"\U0001F31B", first_quarter_moon_face, first_quarter_moon_face_width, first_quarter_moon_face_height}, // 🌛 First Quarter Moon Face // --- Misc Faces --- {"\U0001F608", devil, devil_width, devil_height}, // 😈 Smiling Face with Horns {"\U0001F921", clown, clown_width, clown_height}, // 🤡 Clown Face {"\U0001F916", robo, robo_width, robo_height}, // 🤖 Robot Face // --- Hearts (Multiple Unicode Aliases) --- {"\u2665", heart, heart_width, heart_height}, // ♥ Black Heart Suit {"\u2665\uFE0F", heart, heart_width, heart_height}, // ♥️ Black Heart Suit (emoji presentation) {"\u2764\uFE0F", heart, heart_width, heart_height}, // ❤️ Red Heart {"\U0001F9E1", heart, heart_width, heart_height}, // 🧡 Orange Heart {"\U00002763", heart, heart_width, heart_height}, // ❣ Heart Exclamation {"\U00002764", heart, heart_width, heart_height}, // ❤ Red Heart (legacy) {"\U0001F495", heart, heart_width, heart_height}, // 💕 Two Hearts {"\U0001F496", heart, heart_width, heart_height}, // 💖 Sparkling Heart {"\U0001F497", heart, heart_width, heart_height}, // 💗 Growing Heart {"\U0001F498", heart, heart_width, heart_height}, // 💘 Heart with Arrow // --- Objects --- {"\U0001F4A9", poo, poo_width, poo_height}, // 💩 Pile of Poo {"\U0001F514", bell_icon, bell_icon_width, bell_icon_height}, // 🔔 Bell {"\U0001F4CB", clipboard, clipboard_width, clipboard_height}, // 📋 Clipboard {"\U0001F36A", cookie, cookie_width, cookie_height}, // 🍪 Cookie {"\U0001F370", shortcake, shortcake_width, shortcake_height}, // 🍰 Shortcake {"\U0001F351", peach, peach_width, peach_height}, // 🍑 Peach {"\U0001F983", turkey, turkey_width, turkey_height}, // 🦃 Turkey {"\U0001F357", turkey_leg, turkey_leg_width, turkey_leg_height}, // 🍗 Poultry Leg {"\U0001F525", fire, fire_width, fire_height}, // 🔥 Fire {"\u2728", sparkles, sparkles_width, sparkles_height}, // ✨ Sparkles {"\U0001F573\uFE0F", hole, hole_width, hole_height}, // 🕳️ Hole {"\U0001F3B3", bowling, bowling_width, bowling_height}, // 🎳 Bowling // --- Arrows --- {"\u2193", downwards_arrow, downwards_arrow_width, downwards_arrow_height}, // ↓ Downwards Arrow {"\u2193\uFE0E", downwards_arrow, downwards_arrow_width, downwards_arrow_height}, // ↓︎ Downwards Arrow (text) {"\u2193\uFE0F", downwards_arrow, downwards_arrow_width, downwards_arrow_height}, // ↓️ Downwards Arrow (emoji) {"\u2199", south_west_arrow, south_west_arrow_width, south_west_arrow_height}, // ↙ South West Arrow {"\u2199\uFE0E", south_west_arrow, south_west_arrow_width, south_west_arrow_height}, // ↙︎ South West Arrow (text) {"\u2199\uFE0F", south_west_arrow, south_west_arrow_width, south_west_arrow_height}, // ↙️ South West Arrow (emoji) {"\u2190", leftwards_arrow, leftwards_arrow_width, leftwards_arrow_height}, // ← Leftwards Arrow {"\u2190\uFE0E", leftwards_arrow, leftwards_arrow_width, leftwards_arrow_height}, // ←︎ Leftwards Arrow (text) {"\u2190\uFE0F", leftwards_arrow, leftwards_arrow_width, leftwards_arrow_height}, // ←️ Leftwards Arrow (emoji) {"\u2196", north_west_arrow, north_west_arrow_width, north_west_arrow_height}, // ↖ North West Arrow {"\u2196\uFE0E", north_west_arrow, north_west_arrow_width, north_west_arrow_height}, // ↖︎ North West Arrow (text) {"\u2196\uFE0F", north_west_arrow, north_west_arrow_width, north_west_arrow_height}, // ↖️ North West Arrow (emoji) {"\u2191", upwards_arrow, upwards_arrow_width, upwards_arrow_height}, // ↑ Upwards Arrow {"\u2191\uFE0E", upwards_arrow, upwards_arrow_width, upwards_arrow_height}, // ↑︎ Upwards Arrow (text) {"\u2191\uFE0F", upwards_arrow, upwards_arrow_width, upwards_arrow_height}, // ↑️ Upwards Arrow (emoji) {"\u2197", north_east_arrow, north_east_arrow_width, north_east_arrow_height}, // ↗ North East Arrow {"\u2197\uFE0E", north_east_arrow, north_east_arrow_width, north_east_arrow_height}, // ↗︎ North East Arrow (text) {"\u2197\uFE0F", north_east_arrow, north_east_arrow_width, north_east_arrow_height}, // ↗️ North East Arrow (emoji) {"\u2192", rightwards_arrow, rightwards_arrow_width, rightwards_arrow_height}, // → Rightwards Arrow {"\u2192\uFE0E", rightwards_arrow, rightwards_arrow_width, rightwards_arrow_height}, // →︎ Rightwards Arrow (text) {"\u2192\uFE0F", rightwards_arrow, rightwards_arrow_width, rightwards_arrow_height}, // →️ Rightwards Arrow (emoji) {"\u2198", south_east_arrow, south_east_arrow_width, south_east_arrow_height}, // ↘ South East Arrow {"\u2198\uFE0E", south_east_arrow, south_east_arrow_width, south_east_arrow_height}, // ↘︎ South East Arrow (text) {"\u2198\uFE0F", south_east_arrow, south_east_arrow_width, south_east_arrow_height}, // ↘️ South East Arrow (emoji) // --- Halloween --- {"\U0001F383", jack_o_lantern, jack_o_lantern_width, jack_o_lantern_height}, // 🎃 Jack-O-Lantern {"\U0001F47B", ghost, ghost_width, ghost_height}, // 👻 Ghost {"\U0001F480", skull, skull_width, skull_height} // 💀 Skull #endif }; const int numEmotes = sizeof(emotes) / sizeof(emotes[0]); #ifndef EXCLUDE_EMOJI const unsigned char thumbup[] PROGMEM = {0x00, 0x03, 0x80, 0x04, 0x80, 0x04, 0x40, 0x04, 0x20, 0x02, 0x18, 0x02, 0x06, 0x3F, 0x06, 0x40, 0x06, 0x70, 0x06, 0x40, 0x06, 0x70, 0x06, 0x40, 0x06, 0x30, 0x08, 0x20, 0xF0, 0x1F, 0x00, 0x00}; const unsigned char thumbdown[] PROGMEM = {0xF0, 0x1F, 0x08, 0x20, 0x06, 0x30, 0x06, 0x40, 0x06, 0x70, 0x06, 0x40, 0x06, 0x70, 0x06, 0x40, 0x06, 0x3F, 0x18, 0x02, 0x20, 0x02, 0x40, 0x04, 0x80, 0x04, 0x80, 0x04, 0x00, 0x03, 0x00, 0x00}; const unsigned char smiling_eyes[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x24, 0x24, 0x52, 0x4A, 0x02, 0x40, 0x02, 0x40, 0x22, 0x44, 0x22, 0x44, 0xC2, 0x43, 0x04, 0x20, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; const unsigned char grinning[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x44, 0x22, 0x42, 0x42, 0x02, 0x40, 0x02, 0x40, 0xF2, 0x4F, 0x12, 0x48, 0x22, 0x44, 0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; const unsigned char slightly_smiling[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x44, 0x22, 0x42, 0x42, 0x02, 0x40, 0x02, 0x40, 0x12, 0x48, 0x12, 0x48, 0x22, 0x44, 0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; const unsigned char winking_face[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x44, 0x20, 0x42, 0x46, 0x02, 0x40, 0x02, 0x40, 0x12, 0x48, 0x12, 0x48, 0x22, 0x44, 0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; const unsigned char grinning_smiling_eyes[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x24, 0x24, 0x52, 0x4A, 0x02, 0x40, 0xFA, 0x5F, 0x0A, 0x50, 0x0A, 0x50, 0x12, 0x48, 0x24, 0x24, 0xC4, 0x23, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; const unsigned char heart_smile[] PROGMEM = {0x00, 0x00, 0x6C, 0x07, 0x7C, 0x18, 0x7C, 0x20, 0x38, 0x24, 0x52, 0x0A, 0x02, 0xD8, 0x02, 0xF8, 0x22, 0xFC, 0x20, 0x74, 0xDB, 0x23, 0x1F, 0x00, 0x1F, 0x20, 0x0E, 0x18, 0xE4, 0x07, 0x00, 0x00}; const unsigned char heart_eyes[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x54, 0x2A, 0xFA, 0x5F, 0x72, 0x4E, 0x22, 0x44, 0x02, 0x40, 0x12, 0x48, 0x12, 0x48, 0x24, 0x24, 0xC4, 0x23, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; const unsigned char question[] PROGMEM = {0xE0, 0x07, 0x10, 0x08, 0x08, 0x10, 0x88, 0x11, 0x48, 0x12, 0x48, 0x12, 0x48, 0x12, 0x30, 0x11, 0x80, 0x08, 0x40, 0x04, 0x40, 0x02, 0xC0, 0x03, 0x00, 0x00, 0xC0, 0x03, 0x40, 0x02, 0x80, 0x01}; const unsigned char bang[] PROGMEM = {0x30, 0x0C, 0x48, 0x12, 0x48, 0x12, 0x48, 0x12, 0x48, 0x12, 0x48, 0x12, 0x48, 0x12, 0x48, 0x12, 0x48, 0x12, 0x48, 0x12, 0x30, 0x0C, 0x00, 0x00, 0x30, 0x0C, 0x48, 0x12, 0x30, 0x0C, 0x00, 0x00}; const unsigned char haha[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x24, 0x24, 0x52, 0x4A, 0x0A, 0x50, 0x0E, 0x70, 0xF2, 0x4F, 0x12, 0x48, 0x32, 0x44, 0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; const unsigned char rofl[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x84, 0x21, 0x84, 0x20, 0x02, 0x4C, 0x02, 0x4A, 0x1A, 0x49, 0x8A, 0x48, 0x42, 0x48, 0x22, 0x44, 0xE4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; const unsigned char smiling_closed_eyes[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x24, 0x24, 0x42, 0x42, 0x22, 0x44, 0x02, 0x40, 0xF2, 0x4F, 0x12, 0x48, 0x22, 0x44, 0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; const unsigned char grinning_smiling_eyes_2[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x24, 0x24, 0x52, 0x4A, 0x02, 0x40, 0x02, 0x40, 0xF2, 0x4F, 0x12, 0x48, 0x22, 0x44, 0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; const unsigned char loudly_crying_face[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x34, 0x2C, 0x4A, 0x52, 0x12, 0x48, 0x12, 0x48, 0x92, 0x49, 0x52, 0x4A, 0x52, 0x4A, 0x54, 0x2A, 0x94, 0x29, 0x18, 0x18, 0xF0, 0x0F, 0x00, 0x00}; const unsigned char wave_icon[] PROGMEM = {0x00, 0x00, 0xC0, 0x18, 0x30, 0x21, 0x48, 0x5A, 0x94, 0x64, 0x24, 0x25, 0x4A, 0x24, 0x12, 0x44, 0x22, 0x44, 0x04, 0x40, 0x08, 0x40, 0x12, 0x40, 0x22, 0x20, 0xC4, 0x10, 0x18, 0x0F, 0x00, 0x00}; const unsigned char cowboy[] PROGMEM = {0x70, 0x0E, 0x8F, 0xF1, 0x11, 0x88, 0x21, 0x84, 0xC2, 0x43, 0x1E, 0x78, 0xE2, 0x47, 0x42, 0x42, 0x12, 0x48, 0x12, 0x48, 0x22, 0x44, 0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; const unsigned char deadmau5[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0xE4, 0x27, 0x12, 0x48, 0x0A, 0x50, 0x0E, 0x70, 0x11, 0x88, 0x19, 0x98, 0x19, 0x98, 0x19, 0x98, 0x19, 0x98, 0x19, 0x98, 0x11, 0x88, 0x0E, 0x70, 0x00, 0x00}; const unsigned char sun[] PROGMEM = {0x00, 0x00, 0x80, 0x01, 0xEC, 0x37, 0xFC, 0x3F, 0xF8, 0x1F, 0xFC, 0x3F, 0xFE, 0x7F, 0xFC, 0x3F, 0xFC, 0x3F, 0xFE, 0x7F, 0xFC, 0x3F, 0xF8, 0x1F, 0xFC, 0x3F, 0xEC, 0x37, 0x80, 0x01, 0x00, 0x00}; const unsigned char rain[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x38, 0x1F, 0xFC, 0x3F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFC, 0x3F, 0x00, 0x00, 0x48, 0x12, 0x48, 0x12, 0x24, 0x09, 0x24, 0x09, 0x00, 0x00, 0x00, 0x00}; const unsigned char cloud[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x38, 0x1F, 0xFC, 0x3F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFC, 0x3F, 0xF8, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; const unsigned char fog[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x88, 0x88, 0x54, 0x55, 0x22, 0x22, 0x00, 0x00, 0x44, 0x44, 0xAA, 0x2A, 0x11, 0x11, 0x00, 0x00, 0x88, 0x88, 0x54, 0x55, 0x22, 0x22, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; const unsigned char devil[] PROGMEM = {0x06, 0x60, 0xCA, 0x53, 0x32, 0x4C, 0x22, 0x44, 0x44, 0x22, 0x3A, 0x5C, 0x32, 0x4C, 0x52, 0x4A, 0x72, 0x4E, 0x02, 0x40, 0x22, 0x44, 0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; const unsigned char heart[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x3C, 0x3C, 0x7E, 0x7E, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFC, 0x3F, 0xF8, 0x1F, 0xF8, 0x1F, 0xF0, 0x0F, 0xE0, 0x07, 0xC0, 0x03, 0x80, 0x01, 0x00, 0x00}; const unsigned char poo[] PROGMEM = {0x00, 0x00, 0x80, 0x01, 0x40, 0x02, 0x20, 0x04, 0x10, 0x04, 0xF0, 0x08, 0x10, 0x10, 0x48, 0x12, 0x08, 0x18, 0xE8, 0x21, 0x1C, 0x40, 0x42, 0x42, 0x82, 0x41, 0x02, 0x30, 0xFC, 0x0F, 0x00, 0x00}; const unsigned char bell_icon[] PROGMEM = {0x00, 0x00, 0x80, 0x01, 0x80, 0x01, 0xE0, 0x07, 0xF0, 0x0F, 0xF0, 0x0F, 0xF8, 0x1F, 0xF8, 0x1F, 0xF8, 0x1F, 0xF8, 0x1F, 0xFC, 0x3F, 0xFC, 0x3F, 0xFE, 0x7F, 0xFE, 0x7F, 0x80, 0x01, 0x00, 0x00}; const unsigned char cookie[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x34, 0x22, 0x32, 0x40, 0x02, 0x58, 0x82, 0x5B, 0x92, 0x43, 0x82, 0x43, 0x02, 0x40, 0x64, 0x28, 0x64, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; const unsigned char fire[] PROGMEM = {0x30, 0x00, 0xF0, 0x00, 0xF8, 0x03, 0xF8, 0x07, 0xFC, 0x1F, 0xFC, 0x1F, 0xFE, 0x3E, 0x7E, 0x3E, 0x3E, 0x7C, 0x1E, 0x78, 0x1E, 0x70, 0x1C, 0x70, 0x1C, 0x70, 0x38, 0x38, 0x30, 0x38, 0x60, 0x0C}; const unsigned char peace_sign[] PROGMEM = {0xC0, 0x30, 0x40, 0x29, 0x40, 0x25, 0x40, 0x15, 0x40, 0x12, 0x38, 0x0A, 0x54, 0x68, 0x54, 0x58, 0x54, 0x44, 0x3C, 0x22, 0x04, 0x22, 0x04, 0x12, 0x08, 0x10, 0x10, 0x08, 0xE0, 0x07, 0x00, 0x00}; const unsigned char praying[] PROGMEM = {0x00, 0x00, 0x40, 0x02, 0xA0, 0x05, 0x90, 0x09, 0x90, 0x09, 0x90, 0x09, 0x98, 0x19, 0x94, 0x29, 0xA4, 0x25, 0xA4, 0x25, 0x84, 0x21, 0x84, 0x21, 0x86, 0x61, 0x4E, 0x72, 0x7F, 0x7E, 0x3F, 0xFC}; const unsigned char sparkles[] PROGMEM = {0x00, 0x00, 0x10, 0x00, 0x38, 0x04, 0x10, 0x04, 0x00, 0x0E, 0x00, 0x1F, 0x80, 0x3F, 0xE0, 0xFF, 0x80, 0x3F, 0x10, 0x1F, 0x10, 0x0E, 0x38, 0x04, 0xFE, 0x04, 0x38, 0x00, 0x10, 0x00, 0x10, 0x00}; const unsigned char clown[] PROGMEM = {0x00, 0x00, 0xEE, 0x77, 0x1A, 0x58, 0x06, 0x60, 0x24, 0x24, 0x72, 0x4E, 0x22, 0x44, 0x82, 0x41, 0x82, 0x41, 0x1A, 0x58, 0xF2, 0x4F, 0x14, 0x28, 0xE4, 0x27, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; const unsigned char robo[] PROGMEM = {0x80, 0x01, 0xC0, 0x03, 0x80, 0x01, 0xFC, 0x3F, 0x04, 0x20, 0x74, 0x2E, 0x52, 0x4A, 0x72, 0x4E, 0x02, 0x40, 0x02, 0x40, 0xA2, 0x4A, 0x52, 0x45, 0x04, 0x20, 0x04, 0x20, 0xFC, 0x3F, 0x00, 0x00}; const unsigned char hole[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x3C, 0x3C, 0x06, 0x60, 0x0C, 0x30, 0xF0, 0x0F, 0x00, 0x00, 0x00, 0x00}; const unsigned char bowling[] PROGMEM = {0x00, 0x38, 0x00, 0x44, 0x00, 0x44, 0x00, 0x44, 0x00, 0x28, 0x00, 0x38, 0x00, 0x28, 0x78, 0x44, 0x84, 0x82, 0x22, 0x83, 0x52, 0x83, 0x02, 0x83, 0x02, 0x45, 0x84, 0x44, 0x78, 0x38, 0x00, 0x00}; const unsigned char vulcan_salute[] PROGMEM = {0x08, 0x02, 0x16, 0x0D, 0x15, 0x15, 0x15, 0x15, 0xA9, 0x12, 0x4A, 0x0A, 0x02, 0x38, 0x04, 0x48, 0x04, 0x44, 0x04, 0x22, 0x04, 0x22, 0x04, 0x12, 0x08, 0x10, 0x10, 0x08, 0xE0, 0x07, 0x00, 0x00}; const unsigned char jack_o_lantern[] PROGMEM = {0xC0, 0x00, 0x80, 0x01, 0xB8, 0x1D, 0xC4, 0x23, 0x22, 0x44, 0x05, 0xA0, 0x31, 0x8C, 0x51, 0x8A, 0x61, 0x86, 0x09, 0x90, 0xB9, 0x9D, 0x49, 0x92, 0xB2, 0x4D, 0x42, 0x42, 0x04, 0x20, 0xF8, 0x1F}; const unsigned char ghost[] PROGMEM = {0xC0, 0x03, 0xF0, 0x0F, 0xF8, 0x1F, 0xDC, 0x3B, 0xBC, 0x3D, 0xDF, 0xFB, 0xFF, 0xFF, 0x1F, 0xF8, 0x1E, 0x78, 0x1C, 0x38, 0x3C, 0x3C, 0xFC, 0x3F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0x8C, 0x31}; const unsigned char skull[] PROGMEM = {0xE0, 0x07, 0xF8, 0x1F, 0xFC, 0x3F, 0xFE, 0x7F, 0xFE, 0x7F, 0xC7, 0xE3, 0x87, 0xE1, 0x87, 0xE1, 0x8F, 0xF1, 0xFE, 0x7F, 0x7C, 0x3E, 0xFC, 0x3F, 0xFC, 0x3F, 0xFC, 0x3F, 0xF8, 0x1F, 0xB0, 0x0D}; const unsigned char vomiting[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x04, 0x20, 0x22, 0x44, 0x42, 0x42, 0x22, 0x44, 0x02, 0x40, 0x02, 0x40, 0xC2, 0x43, 0x64, 0x26, 0x64, 0x26, 0x68, 0x16, 0x50, 0x0A, 0xF8, 0x1F}; const unsigned char cool[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0xFC, 0x3F, 0xFA, 0x5F, 0x72, 0x4E, 0x02, 0x40, 0x12, 0x48, 0x12, 0x48, 0x22, 0x44, 0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; const unsigned char shortcake[] PROGMEM = {0x00, 0x00, 0x00, 0x0F, 0x80, 0x3F, 0xE0, 0xFC, 0xE0, 0xE1, 0xF0, 0xB8, 0x10, 0x87, 0xC8, 0x80, 0x3C, 0xE0, 0x06, 0x98, 0x02, 0xC7, 0xE2, 0x30, 0x1A, 0x0E, 0xC6, 0x01, 0x32, 0x00, 0x0E, 0x00}; const unsigned char caution[] PROGMEM = {0x00, 0x00, 0x80, 0x01, 0xC0, 0x03, 0xC0, 0x03, 0x60, 0x06, 0x60, 0x06, 0x70, 0x0E, 0x70, 0x0E, 0x78, 0x1E, 0x78, 0x1E, 0x7C, 0x3E, 0xFC, 0x3F, 0x7E, 0x7E, 0x7E, 0x7E, 0xFC, 0x3F, 0x00, 0x00}; const unsigned char clipboard[] PROGMEM = {0xC0, 0x03, 0x7E, 0x7E, 0xC2, 0x43, 0xFA, 0x5F, 0x0A, 0x5B, 0xFA, 0x5F, 0x8A, 0x54, 0xFA, 0x5F, 0x4A, 0x58, 0xFA, 0x5F, 0x2A, 0x51, 0xFA, 0x5F, 0x0A, 0x59, 0xFA, 0x5F, 0x02, 0x40, 0xFE, 0x7F}; const unsigned char snowflake[] PROGMEM = {0x00, 0x00, 0x40, 0x01, 0x88, 0x08, 0x8C, 0x18, 0xD0, 0x05, 0x60, 0x03, 0x32, 0x26, 0x1C, 0x1C, 0x32, 0x26, 0x60, 0x03, 0xD0, 0x05, 0x8C, 0x18, 0x88, 0x08, 0x40, 0x01, 0x00, 0x00, 0x00, 0x00}; const unsigned char drop[] PROGMEM = {0x00, 0x00, 0x00, 0x01, 0x80, 0x03, 0xC0, 0x07, 0xE0, 0x0F, 0xE0, 0x0F, 0xF0, 0x1F, 0xF0, 0x1F, 0xF8, 0x3F, 0xF8, 0x3F, 0xF8, 0x3F, 0xF8, 0x3F, 0xF0, 0x1F, 0xE0, 0x0F, 0x80, 0x03, 0x00, 0x00}; const unsigned char thermometer[] PROGMEM = {0x00, 0x00, 0x0C, 0x00, 0x16, 0x00, 0x2E, 0x00, 0x5C, 0x00, 0xB8, 0x00, 0x70, 0x01, 0xE0, 0x02, 0xC0, 0x05, 0x80, 0x3B, 0x00, 0x47, 0x00, 0xBE, 0x00, 0x9E, 0x00, 0xBE, 0x00, 0x7C, 0x00, 0x38}; const unsigned char sun_behind_raincloud[] PROGMEM = {0xC0, 0x03, 0x20, 0x04, 0x10, 0x0E, 0x38, 0x1F, 0xFC, 0x37, 0xEE, 0x77, 0xDE, 0x7B, 0x3E, 0x7C, 0xFC, 0x3F, 0x00, 0x00, 0x48, 0x12, 0x48, 0x12, 0x24, 0x09, 0x24, 0x09, 0x00, 0x00, 0x00, 0x00}; const unsigned char sun_behind_cloud[] PROGMEM = {0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x04, 0x0E, 0x3C, 0x1B, 0xFC, 0x3B, 0xFE, 0x7B, 0xFA, 0x7B, 0xF6, 0x7D, 0x0C, 0x3E, 0xF8, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; const unsigned char cloud_with_snow[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x38, 0x1F, 0xFC, 0x3F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFC, 0x3F, 0x00, 0x00, 0x08, 0x02, 0x40, 0x10, 0x00, 0x00, 0x24, 0x09, 0x00, 0x00, 0x00, 0x00}; const unsigned char cloud_with_lightning[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x38, 0x1F, 0xFC, 0x3F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFC, 0x3F, 0x00, 0x01, 0x80, 0x01, 0x80, 0x01, 0xC0, 0x07, 0x00, 0x03, 0x00, 0x03, 0x00, 0x01}; const unsigned char cloud_with_lightning_rain[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x38, 0x1F, 0xFC, 0x3F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFC, 0x3F, 0x00, 0x01, 0x90, 0x21, 0x90, 0x21, 0xC8, 0x17, 0x08, 0x13, 0x00, 0x03, 0x00, 0x01}; const unsigned char wind_face[] PROGMEM = {0xFF, 0x00, 0x01, 0x01, 0x01, 0x01, 0xF9, 0x00, 0xF9, 0x01, 0xD9, 0x01, 0x99, 0x01, 0xF9, 0x01, 0xF9, 0x33, 0xFD, 0x4B, 0xFD, 0x85, 0xFD, 0x9A, 0xFD, 0x75, 0xFD, 0x09, 0xFD, 0x01, 0xFF, 0x00}; const unsigned char new_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x04, 0x20, 0x02, 0x40, 0x02, 0x40, 0x02, 0x40, 0x02, 0x40, 0x02, 0x40, 0x02, 0x40, 0x04, 0x20, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; const unsigned char waxing_crescent_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x1F, 0x04, 0x3E, 0x04, 0x3C, 0x02, 0x78, 0x02, 0x78, 0x02, 0x78, 0x02, 0x78, 0x02, 0x78, 0x02, 0x78, 0x04, 0x3C, 0x04, 0x3E, 0x18, 0x1F, 0xE0, 0x07, 0x00, 0x00}; const unsigned char first_quarter_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x1F, 0x04, 0x3F, 0x04, 0x3F, 0x02, 0x7F, 0x02, 0x7F, 0x02, 0x7F, 0x02, 0x7F, 0x02, 0x7F, 0x02, 0x7F, 0x04, 0x3F, 0x04, 0x3F, 0x18, 0x1F, 0xE0, 0x07, 0x00, 0x00}; const unsigned char waxing_gibbous_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x1F, 0x84, 0x3F, 0xC4, 0x3F, 0xC2, 0x7F, 0xC2, 0x7F, 0xC2, 0x7F, 0xC2, 0x7F, 0xC2, 0x7F, 0xC2, 0x7F, 0xC4, 0x3F, 0x84, 0x3F, 0x18, 0x1F, 0xE0, 0x07, 0x00, 0x00}; const unsigned char full_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0xF8, 0x1F, 0xFC, 0x3F, 0xFC, 0x3F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFC, 0x3F, 0xFC, 0x3F, 0xF8, 0x1F, 0xE0, 0x07, 0x00, 0x00}; const unsigned char waning_gibbous_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0xF8, 0x18, 0xFC, 0x21, 0xFC, 0x23, 0xFE, 0x43, 0xFE, 0x43, 0xFE, 0x43, 0xFE, 0x43, 0xFE, 0x43, 0xFE, 0x43, 0xFC, 0x23, 0xFC, 0x21, 0xF8, 0x18, 0xE0, 0x07, 0x00, 0x00}; const unsigned char last_quarter_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0xF8, 0x18, 0xFC, 0x20, 0xFC, 0x20, 0xFE, 0x40, 0xFE, 0x40, 0xFE, 0x40, 0xFE, 0x40, 0xFE, 0x40, 0xFE, 0x40, 0xFC, 0x20, 0xFC, 0x20, 0xF8, 0x18, 0xE0, 0x07, 0x00, 0x00}; const unsigned char waning_crescent_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0xF8, 0x18, 0x7C, 0x20, 0x3C, 0x20, 0x1E, 0x40, 0x1E, 0x40, 0x1E, 0x40, 0x1E, 0x40, 0x1E, 0x40, 0x1E, 0x40, 0x3C, 0x20, 0x7C, 0x20, 0xF8, 0x18, 0xE0, 0x07, 0x00, 0x00}; const unsigned char first_quarter_moon_face[] PROGMEM = {0x00, 0x0F, 0x00, 0x12, 0x00, 0x24, 0x00, 0x44, 0x00, 0x48, 0x00, 0x88, 0x00, 0x84, 0x80, 0x93, 0x80, 0x80, 0x03, 0x81, 0x8D, 0x80, 0x71, 0x40, 0x82, 0x41, 0x02, 0x20, 0x0C, 0x18, 0xF0, 0x07}; const unsigned char peach[] PROGMEM = {0x70, 0x0F, 0x88, 0x10, 0x78, 0x1F, 0x88, 0x11, 0x04, 0x22, 0x02, 0x44, 0x02, 0x44, 0x02, 0x44, 0x02, 0x44, 0x02, 0x42, 0x02, 0x40, 0x04, 0x20, 0x04, 0x20, 0x08, 0x10, 0x30, 0x0C, 0xC0, 0x03}; const unsigned char turkey[] PROGMEM = {0x00, 0x00, 0x38, 0x00, 0x44, 0x38, 0x56, 0x54, 0x45, 0x52, 0xE2, 0x21, 0x2C, 0x56, 0x14, 0x58, 0x0A, 0x37, 0x86, 0x68, 0x82, 0x50, 0x82, 0x20, 0x04, 0x41, 0xF8, 0x7F, 0x40, 0x02, 0xF0, 0x07}; const unsigned char turkey_leg[] PROGMEM = {0x0C, 0x00, 0x1E, 0x00, 0x1F, 0x00, 0x2F, 0x00, 0x46, 0x00, 0x88, 0x01, 0x10, 0x0E, 0x20, 0x30, 0x20, 0x40, 0x40, 0x40, 0x40, 0x80, 0x40, 0x80, 0x80, 0x80, 0x80, 0x80, 0x00, 0x43, 0x00, 0x3C}; const unsigned char south_west_arrow[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x1C, 0x00, 0x3E, 0x00, 0x1F, 0x80, 0x0F, 0xC2, 0x07, 0xE6, 0x03, 0xFE, 0x01, 0xFE, 0x00, 0x7E, 0x00, 0x7E, 0x00, 0xFE, 0x00, 0xFE, 0x01, 0x00, 0x00}; const unsigned char south_east_arrow[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x38, 0x00, 0x7C, 0x00, 0xF8, 0x00, 0xF0, 0x01, 0xE0, 0x43, 0xC0, 0x67, 0x80, 0x7F, 0x00, 0x7F, 0x00, 0x7E, 0x00, 0x7E, 0x00, 0x7F, 0x80, 0x7F, 0x00, 0x00}; const unsigned char north_west_arrow[] PROGMEM = {0x00, 0x00, 0xFE, 0x01, 0xFE, 0x00, 0x7E, 0x00, 0x7E, 0x00, 0xFE, 0x00, 0xFE, 0x01, 0xE6, 0x03, 0xC2, 0x07, 0x80, 0x0F, 0x00, 0x1F, 0x00, 0x3E, 0x00, 0x1C, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00}; const unsigned char north_east_arrow[] PROGMEM = {0x00, 0x00, 0x80, 0x7F, 0x00, 0x7F, 0x00, 0x7E, 0x00, 0x7E, 0x00, 0x7F, 0x80, 0x7F, 0xC0, 0x67, 0xE0, 0x43, 0xF0, 0x01, 0xF8, 0x00, 0x7C, 0x00, 0x38, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00}; const unsigned char downwards_arrow[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xFC, 0x3F, 0xF8, 0x1F, 0xF0, 0x0F, 0xE0, 0x07, 0xC0, 0x03, 0x80, 0x01}; const unsigned char leftwards_arrow[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x30, 0x00, 0x38, 0x00, 0x3C, 0x00, 0xFE, 0x3F, 0xFF, 0x3F, 0xFF, 0x3F, 0xFE, 0x3F, 0x3C, 0x00, 0x38, 0x00, 0x30, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00}; const unsigned char upwards_arrow[] PROGMEM = {0x80, 0x01, 0xC0, 0x03, 0xE0, 0x07, 0xF0, 0x0F, 0xF8, 0x1F, 0xFC, 0x3F, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0x00, 0x00, 0x00, 0x00}; const unsigned char rightwards_arrow[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x0C, 0x00, 0x1C, 0x00, 0x3C, 0xFC, 0x7F, 0xFC, 0xFF, 0xFC, 0xFF, 0xFC, 0x7F, 0x00, 0x3C, 0x00, 0x1C, 0x00, 0x0C, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00}; const unsigned char strong[] PROGMEM = {0x38, 0x00, 0x44, 0x00, 0x62, 0x00, 0x42, 0x00, 0x42, 0x00, 0x3A, 0x00, 0x11, 0x3C, 0x11, 0x42, 0xD1, 0x81, 0x31, 0x82, 0x11, 0x82, 0x21, 0x80, 0x01, 0x80, 0x01, 0x80, 0x02, 0x40, 0xFC, 0x3F}; const unsigned char check_mark[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x70, 0x00, 0x3C, 0x00, 0x1E, 0x00, 0x0F, 0x80, 0x07, 0xC3, 0x03, 0xEE, 0x03, 0xFC, 0x01, 0xF8, 0x00, 0xF0, 0x00, 0x70, 0x00, 0x60, 0x00, 0x20, 0x00}; const unsigned char house[] PROGMEM = {0x80, 0x01, 0x5C, 0x02, 0x34, 0x04, 0x14, 0x08, 0x0C, 0x10, 0x04, 0x20, 0x02, 0x40, 0xFF, 0xFF, 0x02, 0x40, 0x7A, 0x5F, 0x4A, 0x55, 0x4A, 0x5F, 0x6A, 0x55, 0x4A, 0x5F, 0x4A, 0x40, 0xFE, 0x7F}; const unsigned char shrug[] PROGMEM = {0xC0, 0x03, 0x20, 0x04, 0x10, 0x08, 0x50, 0x0A, 0x10, 0x08, 0x90, 0x09, 0x27, 0xE4, 0x49, 0x92, 0xAA, 0x55, 0x16, 0x68, 0x12, 0x48, 0x02, 0x40, 0x02, 0x40, 0x0C, 0x30, 0x08, 0x10, 0xF8, 0x1F}; const unsigned char eyes[] PROGMEM = {0x00, 0x00, 0x3C, 0x3C, 0x42, 0x42, 0x81, 0x81, 0x85, 0x85, 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, 0x85, 0x85, 0x81, 0x81, 0x42, 0x42, 0x3C, 0x3C, 0x00, 0x00}; const unsigned char eye[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0xF8, 0x1F, 0xF4, 0x2F, 0x7A, 0x5E, 0x39, 0x9C, 0x39, 0x9C, 0x7A, 0x5E, 0xF4, 0x2F, 0xF8, 0x1F, 0xE0, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; #endif } // namespace graphics #endif ================================================ FILE: src/graphics/emotes.h ================================================ #pragma once #include namespace graphics { // === Emote List === struct Emote { const char *label; const unsigned char *bitmap; int width; int height; }; extern const Emote emotes[/* numEmotes */]; extern const int numEmotes; #ifndef EXCLUDE_EMOJI // === Emote Bitmaps === #define thumbs_height 16 #define thumbs_width 16 extern const unsigned char thumbup[] PROGMEM; extern const unsigned char thumbdown[] PROGMEM; #define smiling_eyes_height 16 #define smiling_eyes_width 16 extern const unsigned char smiling_eyes[] PROGMEM; #define grinning_height 16 #define grinning_width 16 extern const unsigned char grinning[] PROGMEM; #define slightly_smiling_height 16 #define slightly_smiling_width 16 extern const unsigned char slightly_smiling[] PROGMEM; #define winking_face_height 16 #define winking_face_width 16 extern const unsigned char winking_face[] PROGMEM; #define grinning_smiling_eyes_height 16 #define grinning_smiling_eyes_width 16 extern const unsigned char grinning_smiling_eyes[] PROGMEM; #define heart_smile_height 16 #define heart_smile_width 16 extern const unsigned char heart_smile[] PROGMEM; #define heart_eyes_height 16 #define heart_eyes_width 16 extern const unsigned char heart_eyes[] PROGMEM; #define question_height 16 #define question_width 16 extern const unsigned char question[] PROGMEM; #define bang_height 16 #define bang_width 16 extern const unsigned char bang[] PROGMEM; #define haha_height 16 #define haha_width 16 extern const unsigned char haha[] PROGMEM; #define rofl_height 16 #define rofl_width 16 extern const unsigned char rofl[] PROGMEM; #define smiling_closed_eyes_height 16 #define smiling_closed_eyes_width 16 extern const unsigned char smiling_closed_eyes[] PROGMEM; #define grinning_smiling_eyes_2_height 16 #define grinning_smiling_eyes_2_width 16 extern const unsigned char grinning_smiling_eyes_2[] PROGMEM; #define loudly_crying_face_height 16 #define loudly_crying_face_width 16 extern const unsigned char loudly_crying_face[] PROGMEM; #define wave_icon_height 16 #define wave_icon_width 16 extern const unsigned char wave_icon[] PROGMEM; #define cowboy_height 16 #define cowboy_width 16 extern const unsigned char cowboy[] PROGMEM; #define deadmau5_height 16 #define deadmau5_width 16 extern const unsigned char deadmau5[] PROGMEM; #define sun_height 16 #define sun_width 16 extern const unsigned char sun[] PROGMEM; #define rain_height 16 #define rain_width 16 extern const unsigned char rain[] PROGMEM; #define cloud_height 16 #define cloud_width 16 extern const unsigned char cloud[] PROGMEM; #define fog_height 16 #define fog_width 16 extern const unsigned char fog[] PROGMEM; #define devil_height 16 #define devil_width 16 extern const unsigned char devil[] PROGMEM; #define heart_height 16 #define heart_width 16 extern const unsigned char heart[] PROGMEM; #define poo_height 16 #define poo_width 16 extern const unsigned char poo[] PROGMEM; #define bell_icon_width 16 #define bell_icon_height 16 extern const unsigned char bell_icon[] PROGMEM; #define cookie_width 16 #define cookie_height 16 extern const unsigned char cookie[] PROGMEM; #define fire_width 16 #define fire_height 16 extern const unsigned char fire[] PROGMEM; #define peace_sign_width 16 #define peace_sign_height 16 extern const unsigned char peace_sign[] PROGMEM; #define praying_width 16 #define praying_height 16 extern const unsigned char praying[] PROGMEM; #define sparkles_width 16 #define sparkles_height 16 extern const unsigned char sparkles[] PROGMEM; #define clown_width 16 #define clown_height 16 extern const unsigned char clown[] PROGMEM; #define robo_width 16 #define robo_height 16 extern const unsigned char robo[] PROGMEM; #define hole_width 16 #define hole_height 16 extern const unsigned char hole[] PROGMEM; #define bowling_width 16 #define bowling_height 16 extern const unsigned char bowling[] PROGMEM; #define vulcan_salute_width 16 #define vulcan_salute_height 16 extern const unsigned char vulcan_salute[] PROGMEM; #define jack_o_lantern_width 16 #define jack_o_lantern_height 16 extern const unsigned char jack_o_lantern[] PROGMEM; #define ghost_width 16 #define ghost_height 16 extern const unsigned char ghost[] PROGMEM; #define skull_width 16 #define skull_height 16 extern const unsigned char skull[] PROGMEM; #define vomiting_width 16 #define vomiting_height 16 extern const unsigned char vomiting[] PROGMEM; #define cool_width 16 #define cool_height 16 extern const unsigned char cool[] PROGMEM; #define shortcake_width 16 #define shortcake_height 16 extern const unsigned char shortcake[] PROGMEM; #define caution_width 16 #define caution_height 16 extern const unsigned char caution[] PROGMEM; #define clipboard_width 16 #define clipboard_height 16 extern const unsigned char clipboard[] PROGMEM; #define snowflake_width 16 #define snowflake_height 16 extern const unsigned char snowflake[] PROGMEM; #define drop_width 16 #define drop_height 16 extern const unsigned char drop[] PROGMEM; #define thermometer_width 16 #define thermometer_height 16 extern const unsigned char thermometer[] PROGMEM; #define sun_behind_raincloud_width 16 #define sun_behind_raincloud_height 16 extern const unsigned char sun_behind_raincloud[] PROGMEM; #define sun_behind_cloud_width 16 #define sun_behind_cloud_height 16 extern const unsigned char sun_behind_cloud[] PROGMEM; #define cloud_with_snow_width 16 #define cloud_with_snow_height 16 extern const unsigned char cloud_with_snow[] PROGMEM; #define cloud_with_lightning_width 16 #define cloud_with_lightning_height 16 extern const unsigned char cloud_with_lightning[] PROGMEM; #define cloud_with_lightning_rain_width 16 #define cloud_with_lightning_rain_height 16 extern const unsigned char cloud_with_lightning_rain[] PROGMEM; #define wind_face_width 16 #define wind_face_height 16 extern const unsigned char wind_face[] PROGMEM; #define new_moon_width 16 #define new_moon_height 16 extern const unsigned char new_moon[] PROGMEM; #define waxing_crescent_moon_width 16 #define waxing_crescent_moon_height 16 extern const unsigned char waxing_crescent_moon[] PROGMEM; #define first_quarter_moon_width 16 #define first_quarter_moon_height 16 extern const unsigned char first_quarter_moon[] PROGMEM; #define waxing_gibbous_moon_width 16 #define waxing_gibbous_moon_height 16 extern const unsigned char waxing_gibbous_moon[] PROGMEM; #define full_moon_width 16 #define full_moon_height 16 extern const unsigned char full_moon[] PROGMEM; #define waning_gibbous_moon_width 16 #define waning_gibbous_moon_height 16 extern const unsigned char waning_gibbous_moon[] PROGMEM; #define last_quarter_moon_width 16 #define last_quarter_moon_height 16 extern const unsigned char last_quarter_moon[] PROGMEM; #define waning_crescent_moon_width 16 #define waning_crescent_moon_height 16 extern const unsigned char waning_crescent_moon[] PROGMEM; #define first_quarter_moon_face_width 16 #define first_quarter_moon_face_height 16 extern const unsigned char first_quarter_moon_face[] PROGMEM; #define peach_width 16 #define peach_height 16 extern const unsigned char peach[] PROGMEM; #define turkey_width 16 #define turkey_height 16 extern const unsigned char turkey[] PROGMEM; #define turkey_leg_width 16 #define turkey_leg_height 16 extern const unsigned char turkey_leg[] PROGMEM; #define south_west_arrow_width 16 #define south_west_arrow_height 16 extern const unsigned char south_west_arrow[] PROGMEM; #define south_east_arrow_width 16 #define south_east_arrow_height 16 extern const unsigned char south_east_arrow[] PROGMEM; #define north_west_arrow_width 16 #define north_west_arrow_height 16 extern const unsigned char north_west_arrow[] PROGMEM; #define north_east_arrow_width 16 #define north_east_arrow_height 16 extern const unsigned char north_east_arrow[] PROGMEM; #define downwards_arrow_width 16 #define downwards_arrow_height 16 extern const unsigned char downwards_arrow[] PROGMEM; #define leftwards_arrow_width 16 #define leftwards_arrow_height 16 extern const unsigned char leftwards_arrow[] PROGMEM; #define upwards_arrow_width 16 #define upwards_arrow_height 16 extern const unsigned char upwards_arrow[] PROGMEM; #define rightwards_arrow_width 16 #define rightwards_arrow_height 16 extern const unsigned char rightwards_arrow[] PROGMEM; #define strong_width 16 #define strong_height 16 extern const unsigned char strong[] PROGMEM; #define check_mark_width 16 #define check_mark_height 16 extern const unsigned char check_mark[] PROGMEM; #define house_width 16 #define house_height 16 extern const unsigned char house[] PROGMEM; #define shrug_width 16 #define shrug_height 16 extern const unsigned char shrug[] PROGMEM; #define eyes_width 16 #define eyes_height 16 extern const unsigned char eyes[] PROGMEM; #define eye_width 16 #define eye_height 16 extern const unsigned char eye[] PROGMEM; #endif // EXCLUDE_EMOJI } // namespace graphics ================================================ FILE: src/graphics/fonts/EinkDisplayFonts.cpp ================================================ #ifdef USE_EINK #include "EinkDisplayFonts.h" // Created by https://oleddisplay.squix.ch/ Consider a donation // In case of problems make sure that you are using the font file with the correct version! const uint8_t Monospaced_plain_30[] PROGMEM = { 0x12, // Width: 18 0x24, // Height: 36 0x20, // First Char: 32 0xE0, // Numbers of Chars: 224 // Jump Table: 0xFF, 0xFF, 0x00, 0x12, // 32:65535 0x00, 0x00, 0x36, 0x12, // 33:0 0x00, 0x36, 0x3E, 0x12, // 34:54 0x00, 0x74, 0x57, 0x12, // 35:116 0x00, 0xCB, 0x54, 0x12, // 36:203 0x01, 0x1F, 0x54, 0x12, // 37:287 0x01, 0x73, 0x59, 0x12, // 38:371 0x01, 0xCC, 0x2F, 0x12, // 39:460 0x01, 0xFB, 0x40, 0x12, // 40:507 0x02, 0x3B, 0x3A, 0x12, // 41:571 0x02, 0x75, 0x4D, 0x12, // 42:629 0x02, 0xC2, 0x4E, 0x12, // 43:706 0x03, 0x10, 0x36, 0x12, // 44:784 0x03, 0x46, 0x3F, 0x12, // 45:838 0x03, 0x85, 0x36, 0x12, // 46:901 0x03, 0xBB, 0x51, 0x12, // 47:955 0x04, 0x0C, 0x4E, 0x12, // 48:1036 0x04, 0x5A, 0x54, 0x12, // 49:1114 0x04, 0xAE, 0x4F, 0x12, // 50:1198 0x04, 0xFD, 0x4E, 0x12, // 51:1277 0x05, 0x4B, 0x53, 0x12, // 52:1355 0x05, 0x9E, 0x4E, 0x12, // 53:1438 0x05, 0xEC, 0x4E, 0x12, // 54:1516 0x06, 0x3A, 0x4D, 0x12, // 55:1594 0x06, 0x87, 0x4F, 0x12, // 56:1671 0x06, 0xD6, 0x4E, 0x12, // 57:1750 0x07, 0x24, 0x36, 0x12, // 58:1828 0x07, 0x5A, 0x36, 0x12, // 59:1882 0x07, 0x90, 0x4F, 0x12, // 60:1936 0x07, 0xDF, 0x4E, 0x12, // 61:2015 0x08, 0x2D, 0x4E, 0x12, // 62:2093 0x08, 0x7B, 0x48, 0x12, // 63:2171 0x08, 0xC3, 0x59, 0x12, // 64:2243 0x09, 0x1C, 0x59, 0x12, // 65:2332 0x09, 0x75, 0x4F, 0x12, // 66:2421 0x09, 0xC4, 0x4F, 0x12, // 67:2500 0x0A, 0x13, 0x4E, 0x12, // 68:2579 0x0A, 0x61, 0x4F, 0x12, // 69:2657 0x0A, 0xB0, 0x4D, 0x12, // 70:2736 0x0A, 0xFD, 0x54, 0x12, // 71:2813 0x0B, 0x51, 0x4F, 0x12, // 72:2897 0x0B, 0xA0, 0x4F, 0x12, // 73:2976 0x0B, 0xEF, 0x44, 0x12, // 74:3055 0x0C, 0x33, 0x59, 0x12, // 75:3123 0x0C, 0x8C, 0x54, 0x12, // 76:3212 0x0C, 0xE0, 0x54, 0x12, // 77:3296 0x0D, 0x34, 0x4F, 0x12, // 78:3380 0x0D, 0x83, 0x53, 0x12, // 79:3459 0x0D, 0xD6, 0x52, 0x12, // 80:3542 0x0E, 0x28, 0x53, 0x12, // 81:3624 0x0E, 0x7B, 0x59, 0x12, // 82:3707 0x0E, 0xD4, 0x4F, 0x12, // 83:3796 0x0F, 0x23, 0x57, 0x12, // 84:3875 0x0F, 0x7A, 0x4E, 0x12, // 85:3962 0x0F, 0xC8, 0x51, 0x12, // 86:4040 0x10, 0x19, 0x57, 0x12, // 87:4121 0x10, 0x70, 0x59, 0x12, // 88:4208 0x10, 0xC9, 0x56, 0x12, // 89:4297 0x11, 0x1F, 0x54, 0x12, // 90:4383 0x11, 0x73, 0x45, 0x12, // 91:4467 0x11, 0xB8, 0x54, 0x12, // 92:4536 0x12, 0x0C, 0x3B, 0x12, // 93:4620 0x12, 0x47, 0x52, 0x12, // 94:4679 0x12, 0x99, 0x5A, 0x12, // 95:4761 0x12, 0xF3, 0x34, 0x12, // 96:4851 0x13, 0x27, 0x4F, 0x12, // 97:4903 0x13, 0x76, 0x53, 0x12, // 98:4982 0x13, 0xC9, 0x4F, 0x12, // 99:5065 0x14, 0x18, 0x4F, 0x12, // 100:5144 0x14, 0x67, 0x53, 0x12, // 101:5223 0x14, 0xBA, 0x4D, 0x12, // 102:5306 0x15, 0x07, 0x4F, 0x12, // 103:5383 0x15, 0x56, 0x4F, 0x12, // 104:5462 0x15, 0xA5, 0x4F, 0x12, // 105:5541 0x15, 0xF4, 0x3B, 0x12, // 106:5620 0x16, 0x2F, 0x54, 0x12, // 107:5679 0x16, 0x83, 0x4F, 0x12, // 108:5763 0x16, 0xD2, 0x54, 0x12, // 109:5842 0x17, 0x26, 0x4F, 0x12, // 110:5926 0x17, 0x75, 0x4E, 0x12, // 111:6005 0x17, 0xC3, 0x53, 0x12, // 112:6083 0x18, 0x16, 0x50, 0x12, // 113:6166 0x18, 0x66, 0x52, 0x12, // 114:6246 0x18, 0xB8, 0x4A, 0x12, // 115:6328 0x19, 0x02, 0x4A, 0x12, // 116:6402 0x19, 0x4C, 0x4F, 0x12, // 117:6476 0x19, 0x9B, 0x4D, 0x12, // 118:6555 0x19, 0xE8, 0x57, 0x12, // 119:6632 0x1A, 0x3F, 0x54, 0x12, // 120:6719 0x1A, 0x93, 0x52, 0x12, // 121:6803 0x1A, 0xE5, 0x4A, 0x12, // 122:6885 0x1B, 0x2F, 0x4B, 0x12, // 123:6959 0x1B, 0x7A, 0x32, 0x12, // 124:7034 0x1B, 0xAC, 0x4E, 0x12, // 125:7084 0x1B, 0xFA, 0x4E, 0x12, // 126:7162 0x1C, 0x48, 0x55, 0x12, // 127:7240 0x1C, 0x9D, 0x55, 0x12, // 128:7325 0x1C, 0xF2, 0x55, 0x12, // 129:7410 0x1D, 0x47, 0x55, 0x12, // 130:7495 0x1D, 0x9C, 0x55, 0x12, // 131:7580 0x1D, 0xF1, 0x55, 0x12, // 132:7665 0x1E, 0x46, 0x55, 0x12, // 133:7750 0x1E, 0x9B, 0x55, 0x12, // 134:7835 0x1E, 0xF0, 0x55, 0x12, // 135:7920 0x1F, 0x45, 0x55, 0x12, // 136:8005 0x1F, 0x9A, 0x55, 0x12, // 137:8090 0x1F, 0xEF, 0x55, 0x12, // 138:8175 0x20, 0x44, 0x55, 0x12, // 139:8260 0x20, 0x99, 0x55, 0x12, // 140:8345 0x20, 0xEE, 0x55, 0x12, // 141:8430 0x21, 0x43, 0x55, 0x12, // 142:8515 0x21, 0x98, 0x55, 0x12, // 143:8600 0x21, 0xED, 0x55, 0x12, // 144:8685 0x22, 0x42, 0x55, 0x12, // 145:8770 0x22, 0x97, 0x55, 0x12, // 146:8855 0x22, 0xEC, 0x55, 0x12, // 147:8940 0x23, 0x41, 0x55, 0x12, // 148:9025 0x23, 0x96, 0x55, 0x12, // 149:9110 0x23, 0xEB, 0x55, 0x12, // 150:9195 0x24, 0x40, 0x55, 0x12, // 151:9280 0x24, 0x95, 0x55, 0x12, // 152:9365 0x24, 0xEA, 0x55, 0x12, // 153:9450 0x25, 0x3F, 0x55, 0x12, // 154:9535 0x25, 0x94, 0x55, 0x12, // 155:9620 0x25, 0xE9, 0x55, 0x12, // 156:9705 0x26, 0x3E, 0x55, 0x12, // 157:9790 0x26, 0x93, 0x55, 0x12, // 158:9875 0x26, 0xE8, 0x55, 0x12, // 159:9960 0xFF, 0xFF, 0x00, 0x12, // 160:65535 0x27, 0x3D, 0x37, 0x12, // 161:10045 0x27, 0x74, 0x4A, 0x12, // 162:10100 0x27, 0xBE, 0x54, 0x12, // 163:10174 0x28, 0x12, 0x54, 0x12, // 164:10258 0x28, 0x66, 0x56, 0x12, // 165:10342 0x28, 0xBC, 0x32, 0x12, // 166:10428 0x28, 0xEE, 0x49, 0x12, // 167:10478 0x29, 0x37, 0x42, 0x12, // 168:10551 0x29, 0x79, 0x58, 0x12, // 169:10617 0x29, 0xD1, 0x49, 0x12, // 170:10705 0x2A, 0x1A, 0x4F, 0x12, // 171:10778 0x2A, 0x69, 0x4E, 0x12, // 172:10857 0x2A, 0xB7, 0x3F, 0x12, // 173:10935 0x2A, 0xF6, 0x58, 0x12, // 174:10998 0x2B, 0x4E, 0x43, 0x12, // 175:11086 0x2B, 0x91, 0x3E, 0x12, // 176:11153 0x2B, 0xCF, 0x4F, 0x12, // 177:11215 0x2C, 0x1E, 0x3F, 0x12, // 178:11294 0x2C, 0x5D, 0x43, 0x12, // 179:11357 0x2C, 0xA0, 0x42, 0x12, // 180:11424 0x2C, 0xE2, 0x59, 0x12, // 181:11490 0x2D, 0x3B, 0x4F, 0x12, // 182:11579 0x2D, 0x8A, 0x35, 0x12, // 183:11658 0x2D, 0xBF, 0x3C, 0x12, // 184:11711 0x2D, 0xFB, 0x3F, 0x12, // 185:11771 0x2E, 0x3A, 0x49, 0x12, // 186:11834 0x2E, 0x83, 0x53, 0x12, // 187:11907 0x2E, 0xD6, 0x54, 0x12, // 188:11990 0x2F, 0x2A, 0x4F, 0x12, // 189:12074 0x2F, 0x79, 0x54, 0x12, // 190:12153 0x2F, 0xCD, 0x4A, 0x12, // 191:12237 0x30, 0x17, 0x59, 0x12, // 192:12311 0x30, 0x70, 0x59, 0x12, // 193:12400 0x30, 0xC9, 0x59, 0x12, // 194:12489 0x31, 0x22, 0x59, 0x12, // 195:12578 0x31, 0x7B, 0x59, 0x12, // 196:12667 0x31, 0xD4, 0x59, 0x12, // 197:12756 0x32, 0x2D, 0x54, 0x12, // 198:12845 0x32, 0x81, 0x4F, 0x12, // 199:12929 0x32, 0xD0, 0x4F, 0x12, // 200:13008 0x33, 0x1F, 0x4F, 0x12, // 201:13087 0x33, 0x6E, 0x4F, 0x12, // 202:13166 0x33, 0xBD, 0x4F, 0x12, // 203:13245 0x34, 0x0C, 0x4F, 0x12, // 204:13324 0x34, 0x5B, 0x4F, 0x12, // 205:13403 0x34, 0xAA, 0x4F, 0x12, // 206:13482 0x34, 0xF9, 0x4F, 0x12, // 207:13561 0x35, 0x48, 0x4E, 0x12, // 208:13640 0x35, 0x96, 0x4F, 0x12, // 209:13718 0x35, 0xE5, 0x53, 0x12, // 210:13797 0x36, 0x38, 0x53, 0x12, // 211:13880 0x36, 0x8B, 0x53, 0x12, // 212:13963 0x36, 0xDE, 0x53, 0x12, // 213:14046 0x37, 0x31, 0x53, 0x12, // 214:14129 0x37, 0x84, 0x4F, 0x12, // 215:14212 0x37, 0xD3, 0x56, 0x12, // 216:14291 0x38, 0x29, 0x4E, 0x12, // 217:14377 0x38, 0x77, 0x4E, 0x12, // 218:14455 0x38, 0xC5, 0x4E, 0x12, // 219:14533 0x39, 0x13, 0x4E, 0x12, // 220:14611 0x39, 0x61, 0x56, 0x12, // 221:14689 0x39, 0xB7, 0x53, 0x12, // 222:14775 0x3A, 0x0A, 0x54, 0x12, // 223:14858 0x3A, 0x5E, 0x4F, 0x12, // 224:14942 0x3A, 0xAD, 0x4F, 0x12, // 225:15021 0x3A, 0xFC, 0x4F, 0x12, // 226:15100 0x3B, 0x4B, 0x4F, 0x12, // 227:15179 0x3B, 0x9A, 0x4F, 0x12, // 228:15258 0x3B, 0xE9, 0x4F, 0x12, // 229:15337 0x3C, 0x38, 0x59, 0x12, // 230:15416 0x3C, 0x91, 0x4F, 0x12, // 231:15505 0x3C, 0xE0, 0x53, 0x12, // 232:15584 0x3D, 0x33, 0x53, 0x12, // 233:15667 0x3D, 0x86, 0x53, 0x12, // 234:15750 0x3D, 0xD9, 0x53, 0x12, // 235:15833 0x3E, 0x2C, 0x4F, 0x12, // 236:15916 0x3E, 0x7B, 0x4F, 0x12, // 237:15995 0x3E, 0xCA, 0x4F, 0x12, // 238:16074 0x3F, 0x19, 0x4F, 0x12, // 239:16153 0x3F, 0x68, 0x4E, 0x12, // 240:16232 0x3F, 0xB6, 0x4F, 0x12, // 241:16310 0x40, 0x05, 0x4E, 0x12, // 242:16389 0x40, 0x53, 0x4E, 0x12, // 243:16467 0x40, 0xA1, 0x4E, 0x12, // 244:16545 0x40, 0xEF, 0x4E, 0x12, // 245:16623 0x41, 0x3D, 0x4E, 0x12, // 246:16701 0x41, 0x8B, 0x53, 0x12, // 247:16779 0x41, 0xDE, 0x52, 0x12, // 248:16862 0x42, 0x30, 0x4F, 0x12, // 249:16944 0x42, 0x7F, 0x4F, 0x12, // 250:17023 0x42, 0xCE, 0x4F, 0x12, // 251:17102 0x43, 0x1D, 0x4F, 0x12, // 252:17181 0x43, 0x6C, 0x52, 0x12, // 253:17260 0x43, 0xBE, 0x53, 0x12, // 254:17342 0x44, 0x11, 0x52, 0x12, // 255:17425 // Font Data: 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x1F, 0x0F, 0x00, 0xC0, 0xFF, 0x1F, 0x0F, 0x00, 0xC0, 0xFF, 0x1F, 0x0F, // 33 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x00, 0xC0, 0x3F, // 34 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x70, 0x38, 0x08, 0x00, 0x00, 0x70, 0xB8, 0x0F, 0x00, 0x00, 0x70, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x01, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x80, 0xFF, 0x38, 0x00, 0x00, 0xC0, 0x7F, 0x38, 0x08, 0x00, 0xC0, 0x70, 0xB8, 0x0F, 0x00, 0x00, 0x70, 0xFC, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xC0, 0xFF, 0x38, 0x00, 0x00, 0xC0, 0x7F, 0x38, 0x00, 0x00, 0xC0, 0x70, 0x38, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x70, // 35 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x01, 0x07, 0x00, 0x00, 0xF8, 0x03, 0x07, 0x00, 0x00, 0xFC, 0x03, 0x0E, 0x00, 0x00, 0x1E, 0x07, 0x0E, 0x00, 0x00, 0x0E, 0x06, 0x0E, 0x00, 0x00, 0x0E, 0x06, 0x0E, 0x00, 0xC0, 0xFF, 0xFF, 0xFF, 0x00, 0xC0, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x0E, 0x0C, 0x0E, 0x00, 0x00, 0x0E, 0x0C, 0x0E, 0x00, 0x00, 0x0E, 0x1C, 0x0F, 0x00, 0x00, 0x1C, 0xF8, 0x07, 0x00, 0x00, 0x1C, 0xF8, 0x03, 0x00, 0x00, 0x00, 0xE0, 0x01, // 36 0x00, 0x1F, 0x00, 0x00, 0x00, 0x80, 0x3F, 0x10, 0x00, 0x00, 0xC0, 0x71, 0x18, 0x00, 0x00, 0xC0, 0x60, 0x08, 0x00, 0x00, 0xC0, 0x60, 0x0C, 0x00, 0x00, 0xC0, 0x60, 0x04, 0x00, 0x00, 0xC0, 0x71, 0x06, 0x00, 0x00, 0x80, 0x3F, 0x02, 0x00, 0x00, 0x00, 0x1F, 0xE3, 0x03, 0x00, 0x00, 0x00, 0xF1, 0x07, 0x00, 0x00, 0x80, 0x39, 0x0E, 0x00, 0x00, 0x80, 0x18, 0x0C, 0x00, 0x00, 0xC0, 0x18, 0x0C, 0x00, 0x00, 0x40, 0x18, 0x0C, 0x00, 0x00, 0x60, 0x38, 0x0E, 0x00, 0x00, 0x20, 0xF0, 0x07, 0x00, 0x00, 0x00, 0xC0, 0x03, // 37 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0x00, 0xFF, 0x03, 0x00, 0x00, 0x1F, 0x87, 0x07, 0x00, 0x80, 0xFF, 0x03, 0x0F, 0x00, 0x80, 0xFF, 0x01, 0x0F, 0x00, 0xC0, 0xE3, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x07, 0x0E, 0x00, 0xC0, 0x01, 0x1F, 0x0E, 0x00, 0xC0, 0x01, 0x3E, 0x0E, 0x00, 0xC0, 0x01, 0x7C, 0x07, 0x00, 0x80, 0x03, 0xF0, 0x07, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0x00, 0x7E, 0x0E, 0x00, 0x00, 0x00, 0x1E, 0x08, // 38 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x00, 0xC0, 0x3F, // 39 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x03, 0x00, 0x00, 0xFE, 0xFF, 0x0F, 0x00, 0x80, 0x3F, 0x80, 0x3F, 0x00, 0xE0, 0x03, 0x00, 0xF8, 0x00, 0xE0, 0x00, 0x00, 0xE0, 0x00, 0x20, 0x00, 0x00, 0x80, // 40 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x80, 0x00, 0xE0, 0x00, 0x00, 0xE0, 0x00, 0xE0, 0x03, 0x00, 0xF8, 0x00, 0x80, 0x3F, 0x80, 0x3F, 0x00, 0x00, 0xFE, 0xFF, 0x0F, 0x00, 0x00, 0xF8, 0xFF, 0x03, 0x00, 0x00, 0xC0, 0x7F, // 41 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x84, 0x00, 0x00, 0x00, 0x00, 0x86, 0x01, 0x00, 0x00, 0x00, 0xCC, 0x00, 0x00, 0x00, 0x00, 0xCC, 0x00, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x0F, 0x00, 0x00, 0xC0, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x00, 0xCC, 0x00, 0x00, 0x00, 0x00, 0xCC, 0x00, 0x00, 0x00, 0x00, 0x86, 0x01, 0x00, 0x00, 0x00, 0x84, // 42 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x07, 0x00, 0x00, 0xF8, 0xFF, 0x07, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, // 43 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xFF, 0x01, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x0F, // 44 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, // 45 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0F, // 46 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0x40, // 47 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, 0xFF, 0xFF, 0x03, 0x00, 0x80, 0x0F, 0xC0, 0x07, 0x00, 0xC0, 0x03, 0x00, 0x0F, 0x00, 0xC0, 0x01, 0x06, 0x0E, 0x00, 0xC0, 0x01, 0x07, 0x0E, 0x00, 0xC0, 0x01, 0x07, 0x0E, 0x00, 0xC0, 0x01, 0x02, 0x0E, 0x00, 0xC0, 0x03, 0x00, 0x0F, 0x00, 0x80, 0x0F, 0xC0, 0x07, 0x00, 0x00, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, 0xF0, 0x3F, // 48 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x0E, 0x00, 0x80, 0x03, 0x00, 0x0E, 0x00, 0xC0, 0x03, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, // 49 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x0E, 0x00, 0x80, 0x03, 0x00, 0x0F, 0x00, 0xC0, 0x01, 0x80, 0x0F, 0x00, 0xC0, 0x01, 0xC0, 0x0F, 0x00, 0xC0, 0x01, 0xE0, 0x0E, 0x00, 0xC0, 0x01, 0xF0, 0x0E, 0x00, 0xC0, 0x01, 0x78, 0x0E, 0x00, 0xC0, 0x01, 0x3C, 0x0E, 0x00, 0xC0, 0x01, 0x1E, 0x0E, 0x00, 0xC0, 0x03, 0x0F, 0x0E, 0x00, 0x80, 0x87, 0x0F, 0x0E, 0x00, 0x80, 0xFF, 0x07, 0x0E, 0x00, 0x00, 0xFF, 0x01, 0x0E, 0x00, 0x00, 0xFC, 0x00, 0x0E, // 50 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x80, 0x03, 0x00, 0x07, 0x00, 0x80, 0x03, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0xC1, 0x03, 0x0E, 0x00, 0x80, 0xC3, 0x06, 0x0F, 0x00, 0x80, 0xFF, 0x8E, 0x07, 0x00, 0x00, 0x7F, 0xFE, 0x07, 0x00, 0x00, 0x3E, 0xFC, 0x03, 0x00, 0x00, 0x00, 0xF0, // 51 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x7F, 0x00, 0x00, 0x00, 0xC0, 0x77, 0x00, 0x00, 0x00, 0xE0, 0x71, 0x00, 0x00, 0x00, 0x78, 0x70, 0x00, 0x00, 0x00, 0x1E, 0x70, 0x00, 0x00, 0x00, 0x0F, 0x70, 0x00, 0x00, 0xC0, 0x03, 0x70, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x70, // 52 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0xC0, 0xFF, 0x03, 0x07, 0x00, 0xC0, 0xFF, 0x01, 0x0E, 0x00, 0xC0, 0xFF, 0x01, 0x0E, 0x00, 0xC0, 0xC1, 0x01, 0x0E, 0x00, 0xC0, 0xC1, 0x01, 0x0E, 0x00, 0xC0, 0xC1, 0x01, 0x0E, 0x00, 0xC0, 0xC1, 0x01, 0x0E, 0x00, 0xC0, 0xC1, 0x03, 0x0E, 0x00, 0xC0, 0xC1, 0x03, 0x07, 0x00, 0xC0, 0x81, 0x87, 0x07, 0x00, 0xC0, 0x81, 0xFF, 0x03, 0x00, 0x00, 0x00, 0xFF, 0x01, 0x00, 0x00, 0x00, 0xFC, // 53 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0xFC, 0xFF, 0x01, 0x00, 0x00, 0xFF, 0xFF, 0x03, 0x00, 0x80, 0x1F, 0x87, 0x07, 0x00, 0x80, 0x87, 0x03, 0x0F, 0x00, 0xC0, 0x83, 0x01, 0x0E, 0x00, 0xC0, 0xC1, 0x01, 0x0E, 0x00, 0xC0, 0xC1, 0x01, 0x0E, 0x00, 0xC0, 0xC1, 0x01, 0x0E, 0x00, 0xC0, 0xC1, 0x03, 0x0F, 0x00, 0xC0, 0x81, 0x87, 0x07, 0x00, 0x80, 0x83, 0xFF, 0x07, 0x00, 0x00, 0x00, 0xFF, 0x03, 0x00, 0x00, 0x00, 0xFC, // 54 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x08, 0x00, 0xC0, 0x01, 0x00, 0x0F, 0x00, 0xC0, 0x01, 0xE0, 0x0F, 0x00, 0xC0, 0x01, 0xF8, 0x07, 0x00, 0xC0, 0x01, 0xFF, 0x01, 0x00, 0xC0, 0xE1, 0x3F, 0x00, 0x00, 0xC0, 0xF9, 0x07, 0x00, 0x00, 0xC0, 0xFF, 0x01, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x00, 0xC0, 0x01, // 55 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x00, 0x3E, 0xFC, 0x03, 0x00, 0x00, 0x7F, 0xFE, 0x07, 0x00, 0x80, 0xFF, 0x8E, 0x07, 0x00, 0xC0, 0xC3, 0x06, 0x0F, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0xC3, 0x06, 0x0F, 0x00, 0x80, 0xFF, 0x8E, 0x07, 0x00, 0x00, 0x7F, 0xFE, 0x07, 0x00, 0x00, 0x3E, 0xFC, 0x03, 0x00, 0x00, 0x00, 0xF8, 0x01, // 56 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x03, 0x00, 0x00, 0x80, 0xFF, 0x07, 0x07, 0x00, 0x80, 0x87, 0x07, 0x0E, 0x00, 0xC0, 0x03, 0x0F, 0x0E, 0x00, 0xC0, 0x01, 0x0E, 0x0E, 0x00, 0xC0, 0x01, 0x0E, 0x0E, 0x00, 0xC0, 0x01, 0x0E, 0x0E, 0x00, 0xC0, 0x01, 0x0E, 0x0F, 0x00, 0xC0, 0x03, 0x87, 0x07, 0x00, 0x80, 0x87, 0xE1, 0x07, 0x00, 0x00, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0xFE, 0xFF, 0x00, 0x00, 0x00, 0xF0, 0x3F, // 57 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xF0, 0x00, 0x0F, // 58 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0xF0, 0x00, 0xFF, 0x01, 0x00, 0xF0, 0x00, 0xFF, 0x00, 0x00, 0xF0, 0x00, 0x3F, 0x00, 0x00, 0xF0, 0x00, 0x0F, // 59 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x00, 0x80, 0x3B, 0x00, 0x00, 0x00, 0x80, 0x33, 0x00, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x00, 0xC0, 0x71, 0x00, 0x00, 0x00, 0xC0, 0x60, 0x00, 0x00, 0x00, 0xE0, 0xE0, 0x00, 0x00, 0x00, 0xE0, 0xE0, 0x00, 0x00, 0x00, 0x70, 0xC0, 0x01, 0x00, 0x00, 0x70, 0xC0, 0x01, 0x00, 0x00, 0x30, 0x80, 0x01, 0x00, 0x00, 0x38, 0x80, 0x03, // 60 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x00, 0x80, 0x31, // 61 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x80, 0x03, 0x00, 0x00, 0x30, 0x80, 0x01, 0x00, 0x00, 0x70, 0xC0, 0x01, 0x00, 0x00, 0x70, 0xC0, 0x01, 0x00, 0x00, 0xE0, 0xE0, 0x00, 0x00, 0x00, 0xE0, 0xE0, 0x00, 0x00, 0x00, 0xC0, 0x60, 0x00, 0x00, 0x00, 0xC0, 0x71, 0x00, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x00, 0x80, 0x3B, 0x00, 0x00, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, // 62 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x3E, 0x0F, 0x00, 0xC0, 0x01, 0x3F, 0x0F, 0x00, 0xC0, 0xC1, 0x3F, 0x0F, 0x00, 0xC0, 0xC1, 0x03, 0x00, 0x00, 0xC0, 0xF3, 0x01, 0x00, 0x00, 0x80, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x1E, // 63 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x01, 0x00, 0x00, 0xE0, 0xFF, 0x07, 0x00, 0x00, 0xF0, 0x00, 0x1F, 0x00, 0x00, 0x38, 0x00, 0x38, 0x00, 0x00, 0x0C, 0x00, 0x70, 0x00, 0x00, 0x0E, 0x7C, 0xE0, 0x00, 0x00, 0x06, 0xFF, 0xC1, 0x00, 0x00, 0x83, 0x83, 0xC3, 0x00, 0x00, 0x83, 0x01, 0x87, 0x01, 0x00, 0xC3, 0x00, 0x86, 0x01, 0x00, 0xC3, 0x00, 0x86, 0x01, 0x00, 0xC3, 0x00, 0x86, 0x01, 0x00, 0xC7, 0x00, 0x86, 0x01, 0x00, 0x86, 0x01, 0xC3, 0x01, 0x00, 0x9E, 0x83, 0xC3, 0x01, 0x00, 0xFC, 0xFF, 0x07, 0x00, 0x00, 0xF0, 0xFF, 0x07, // 64 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xE0, 0xFF, 0x00, 0x00, 0x00, 0xFC, 0x7F, 0x00, 0x00, 0x80, 0xFF, 0x71, 0x00, 0x00, 0xC0, 0x3F, 0x70, 0x00, 0x00, 0xC0, 0x03, 0x70, 0x00, 0x00, 0xC0, 0x3F, 0x70, 0x00, 0x00, 0x80, 0xFF, 0x71, 0x00, 0x00, 0x00, 0xFC, 0x7F, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0C, // 65 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0xC3, 0x07, 0x0F, 0x00, 0x80, 0xFF, 0x0E, 0x07, 0x00, 0x00, 0x7F, 0xFE, 0x07, 0x00, 0x00, 0x3E, 0xFC, 0x03, 0x00, 0x00, 0x00, 0xF0, 0x01, // 66 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x00, 0x00, 0xFC, 0xFF, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, 0x1F, 0xE0, 0x03, 0x00, 0x80, 0x07, 0x80, 0x07, 0x00, 0x80, 0x03, 0x00, 0x07, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x03, 0x00, 0x0E, 0x00, 0x80, 0x03, 0x00, 0x07, 0x00, 0x00, 0x07, 0x80, 0x03, // 67 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0x80, 0x03, 0x00, 0x07, 0x00, 0x80, 0x07, 0x80, 0x07, 0x00, 0x00, 0x1F, 0xE0, 0x03, 0x00, 0x00, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0xFC, 0xFF, 0x00, 0x00, 0x00, 0xF0, 0x3F, // 68 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, // 69 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0x81, 0x03, 0x00, 0x00, 0xC0, 0x81, 0x03, 0x00, 0x00, 0xC0, 0x81, 0x03, 0x00, 0x00, 0xC0, 0x81, 0x03, 0x00, 0x00, 0xC0, 0x81, 0x03, 0x00, 0x00, 0xC0, 0x81, 0x03, 0x00, 0x00, 0xC0, 0x81, 0x03, 0x00, 0x00, 0xC0, 0x81, 0x03, 0x00, 0x00, 0xC0, 0x81, 0x03, 0x00, 0x00, 0xC0, 0x01, // 70 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x00, 0x00, 0xFC, 0xFF, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, 0x1F, 0xE0, 0x03, 0x00, 0x80, 0x07, 0x80, 0x07, 0x00, 0x80, 0x03, 0x00, 0x07, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x07, 0x0E, 0x00, 0xC0, 0x01, 0x07, 0x0E, 0x00, 0xC0, 0x01, 0x07, 0x0F, 0x00, 0x80, 0x03, 0xFF, 0x07, 0x00, 0x00, 0x07, 0xFF, 0x07, 0x00, 0x00, 0x00, 0xFF, 0x03, // 71 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, // 72 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, // 73 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x07, 0x00, 0xC0, 0xFF, 0xFF, 0x03, 0x00, 0xC0, 0xFF, 0xFF, // 74 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0x00, 0x3C, 0x3E, 0x00, 0x00, 0x00, 0x1E, 0xFC, 0x00, 0x00, 0x00, 0x0F, 0xF0, 0x01, 0x00, 0x80, 0x03, 0xE0, 0x07, 0x00, 0xC0, 0x01, 0x80, 0x0F, 0x00, 0xC0, 0x00, 0x00, 0x0F, 0x00, 0x40, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x08, // 75 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, // 76 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0x03, 0x00, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, // 77 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, // 78 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, 0xFF, 0xFF, 0x03, 0x00, 0x80, 0x0F, 0xC0, 0x07, 0x00, 0x80, 0x03, 0x00, 0x07, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0x80, 0x03, 0x00, 0x07, 0x00, 0x80, 0x0F, 0xC0, 0x07, 0x00, 0x00, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, 0xF0, 0x3F, // 79 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0x01, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x07, 0x00, 0x00, 0xC0, 0x83, 0x07, 0x00, 0x00, 0x80, 0xC7, 0x03, 0x00, 0x00, 0x80, 0xFF, 0x03, 0x00, 0x00, 0x00, 0xFF, 0x01, 0x00, 0x00, 0x00, 0x7C, // 80 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, 0xFF, 0xFF, 0x03, 0x00, 0x80, 0x0F, 0xC0, 0x07, 0x00, 0x80, 0x03, 0x00, 0x07, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x3E, 0x00, 0x80, 0x03, 0x00, 0x7F, 0x00, 0x80, 0x0F, 0xC0, 0xF7, 0x00, 0x00, 0xFF, 0xFF, 0x63, 0x00, 0x00, 0xFE, 0xFF, 0x41, 0x00, 0x00, 0xF0, 0x3F, // 81 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0x01, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x0F, 0x00, 0x00, 0xC0, 0x83, 0x1F, 0x00, 0x00, 0x80, 0xC7, 0x7D, 0x00, 0x00, 0x80, 0xFF, 0xF9, 0x01, 0x00, 0x00, 0xFF, 0xF0, 0x07, 0x00, 0x00, 0x7C, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x08, // 82 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7C, 0x00, 0x07, 0x00, 0x00, 0xFF, 0x00, 0x07, 0x00, 0x80, 0xFF, 0x01, 0x0E, 0x00, 0x80, 0xC7, 0x01, 0x0E, 0x00, 0xC0, 0xC3, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x07, 0x0E, 0x00, 0xC0, 0x01, 0x07, 0x0E, 0x00, 0xC0, 0x01, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x8F, 0x07, 0x00, 0x80, 0x03, 0xFE, 0x07, 0x00, 0x80, 0x03, 0xFC, 0x03, 0x00, 0x00, 0x00, 0xF8, 0x01, // 83 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, // 84 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x03, 0x00, 0xC0, 0xFF, 0xFF, 0x07, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0xC0, 0xFF, 0xFF, 0x07, 0x00, 0xC0, 0xFF, 0xFF, 0x03, 0x00, 0xC0, 0xFF, 0xFF, // 85 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x00, 0x80, 0xFF, 0x03, 0x00, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x01, 0x00, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x00, 0xC0, 0xFF, 0x01, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x80, 0xFF, 0x03, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x00, 0xC0, // 86 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x7F, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0x00, 0xFF, 0x0F, 0x00, 0x00, 0xFE, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0x7F, 0x00, 0x00, 0xC0, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x01, // 87 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x08, 0x00, 0xC0, 0x00, 0x00, 0x0E, 0x00, 0xC0, 0x03, 0x00, 0x0F, 0x00, 0xC0, 0x07, 0xC0, 0x0F, 0x00, 0x80, 0x1F, 0xE0, 0x03, 0x00, 0x00, 0x3E, 0xF8, 0x01, 0x00, 0x00, 0x7C, 0x7E, 0x00, 0x00, 0x00, 0xF0, 0x1F, 0x00, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x00, 0x7C, 0x7C, 0x00, 0x00, 0x00, 0x3E, 0xF8, 0x01, 0x00, 0x80, 0x0F, 0xE0, 0x03, 0x00, 0xC0, 0x07, 0xC0, 0x0F, 0x00, 0xC0, 0x03, 0x00, 0x0F, 0x00, 0xC0, 0x00, 0x00, 0x0C, // 88 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0x40, // 89 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x80, 0x0F, 0x00, 0xC0, 0x01, 0xC0, 0x0F, 0x00, 0xC0, 0x01, 0xF0, 0x0F, 0x00, 0xC0, 0x01, 0xF8, 0x0E, 0x00, 0xC0, 0x01, 0x7E, 0x0E, 0x00, 0xC0, 0x01, 0x1F, 0x0E, 0x00, 0xC0, 0xC1, 0x0F, 0x0E, 0x00, 0xC0, 0xE1, 0x03, 0x0E, 0x00, 0xC0, 0xF9, 0x01, 0x0E, 0x00, 0xC0, 0x7D, 0x00, 0x0E, 0x00, 0xC0, 0x3F, 0x00, 0x0E, 0x00, 0xC0, 0x0F, 0x00, 0x0E, 0x00, 0xC0, 0x07, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, // 90 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0xFF, 0x00, 0xE0, 0xFF, 0xFF, 0xFF, 0x00, 0xE0, 0xFF, 0xFF, 0xFF, 0x00, 0xE0, 0x00, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0x00, 0xE0, // 91 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x40, // 92 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0x00, 0xE0, 0x00, 0xE0, 0xFF, 0xFF, 0xFF, 0x00, 0xE0, 0xFF, 0xFF, 0xFF, 0x00, 0xE0, 0xFF, 0xFF, 0xFF, // 93 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x00, 0x20, // 94 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, // 95 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x02, // 96 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, 0xF1, 0x07, 0x00, 0x00, 0xE0, 0xF8, 0x07, 0x00, 0x00, 0xE0, 0x38, 0x0F, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x06, 0x00, 0x00, 0x70, 0x1C, 0x07, 0x00, 0x00, 0xE0, 0x9C, 0x01, 0x00, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0xC0, 0xFF, 0x0F, 0x00, 0x00, 0x80, 0xFF, 0x0F, // 97 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0xC0, 0x81, 0x03, 0x00, 0x00, 0xE0, 0x00, 0x07, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xE0, 0x81, 0x07, 0x00, 0x00, 0xE0, 0xFF, 0x07, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0x00, 0xFF, // 98 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x80, 0xFF, 0x01, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0xE0, 0xC3, 0x07, 0x00, 0x00, 0xE0, 0x00, 0x07, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0xE0, 0x00, 0x07, 0x00, 0x00, 0xC0, 0x81, 0x03, // 99 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0xE0, 0xFF, 0x07, 0x00, 0x00, 0xE0, 0x81, 0x07, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0xE0, 0x00, 0x07, 0x00, 0x00, 0xC0, 0x81, 0x03, 0x00, 0xE0, 0xFF, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0xFF, 0x0F, // 100 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x80, 0xFF, 0x01, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0xE0, 0x9D, 0x07, 0x00, 0x00, 0xE0, 0x1C, 0x07, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0xF0, 0x1C, 0x0E, 0x00, 0x00, 0xE0, 0x1C, 0x07, 0x00, 0x00, 0xE0, 0x1F, 0x07, 0x00, 0x00, 0xC0, 0x9F, 0x03, 0x00, 0x00, 0x00, 0x1F, // 101 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xE0, 0x71, 0x00, 0x00, 0x00, 0xE0, 0x70, 0x00, 0x00, 0x00, 0xE0, 0x70, 0x00, 0x00, 0x00, 0xE0, 0x70, 0x00, 0x00, 0x00, 0xE0, 0x70, 0x00, 0x00, 0x00, 0xE0, 0x70, // 102 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0xE0, 0xFF, 0xC7, 0x01, 0x00, 0xE0, 0x81, 0xC7, 0x01, 0x00, 0xF0, 0x00, 0x8F, 0x03, 0x00, 0x70, 0x00, 0x8E, 0x03, 0x00, 0x70, 0x00, 0x8E, 0x03, 0x00, 0x70, 0x00, 0x8E, 0x03, 0x00, 0x70, 0x00, 0x8E, 0x03, 0x00, 0xE0, 0x00, 0xC7, 0x03, 0x00, 0xC0, 0x81, 0xE3, 0x01, 0x00, 0xF0, 0xFF, 0xFF, 0x01, 0x00, 0xF0, 0xFF, 0xFF, 0x00, 0x00, 0xF0, 0xFF, 0x3F, // 103 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0x80, 0xFF, 0x0F, // 104 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0xE0, 0xF0, 0xFF, 0x0F, 0x00, 0xE0, 0xF0, 0xFF, 0x0F, 0x00, 0xE0, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, // 105 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x70, 0x00, 0x80, 0x03, 0x00, 0x70, 0x00, 0x80, 0x03, 0x00, 0x70, 0x00, 0x80, 0x03, 0x00, 0x70, 0x00, 0x80, 0x03, 0x00, 0x70, 0x00, 0xC0, 0x03, 0xE0, 0xF0, 0xFF, 0xFF, 0x01, 0xE0, 0xF0, 0xFF, 0xFF, 0x00, 0xE0, 0xF0, 0xFF, 0x7F, // 106 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x80, 0x7F, 0x00, 0x00, 0x00, 0xC0, 0xFB, 0x00, 0x00, 0x00, 0xE0, 0xF3, 0x03, 0x00, 0x00, 0xF0, 0xC1, 0x07, 0x00, 0x00, 0xF0, 0x80, 0x0F, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x30, 0x00, 0x0C, 0x00, 0x00, 0x10, 0x00, 0x08, // 107 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x01, 0x00, 0xE0, 0xFF, 0xFF, 0x03, 0x00, 0xE0, 0xFF, 0xFF, 0x07, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, // 108 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0xC0, 0xFF, 0x0F, // 109 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0x80, 0xFF, 0x0F, // 110 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0xE0, 0xFF, 0x07, 0x00, 0x00, 0xE0, 0x81, 0x07, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xE0, 0x81, 0x07, 0x00, 0x00, 0xE0, 0xFF, 0x07, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0x00, 0xFF, // 111 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0xFF, 0x03, 0x00, 0xF0, 0xFF, 0xFF, 0x03, 0x00, 0xF0, 0xFF, 0xFF, 0x03, 0x00, 0xC0, 0x81, 0x03, 0x00, 0x00, 0xE0, 0x00, 0x07, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xE0, 0x81, 0x07, 0x00, 0x00, 0xE0, 0xFF, 0x07, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0x00, 0xFF, // 112 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0xE0, 0xFF, 0x07, 0x00, 0x00, 0xE0, 0x81, 0x07, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0xE0, 0x00, 0x07, 0x00, 0x00, 0xC0, 0x81, 0x03, 0x00, 0x00, 0xF0, 0xFF, 0xFF, 0x03, 0x00, 0xF0, 0xFF, 0xFF, 0x03, 0x00, 0xF0, 0xFF, 0xFF, 0x03, // 113 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0xE0, // 114 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x83, 0x03, 0x00, 0x00, 0xE0, 0x07, 0x07, 0x00, 0x00, 0xE0, 0x0F, 0x07, 0x00, 0x00, 0xF0, 0x0E, 0x0E, 0x00, 0x00, 0x70, 0x0E, 0x0E, 0x00, 0x00, 0x70, 0x0E, 0x0E, 0x00, 0x00, 0x70, 0x1E, 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x0F, 0x00, 0x00, 0x70, 0xFC, 0x07, 0x00, 0x00, 0xE0, 0xF8, 0x07, 0x00, 0x00, 0x00, 0xF0, 0x01, // 115 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x03, 0x00, 0x80, 0xFF, 0xFF, 0x07, 0x00, 0x80, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x70, 0x00, 0x0F, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, // 116 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0x01, 0x00, 0x00, 0xF0, 0xFF, 0x07, 0x00, 0x00, 0xF0, 0xFF, 0x07, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, // 117 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x07, 0x00, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0x00, 0xFC, 0x07, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x10, // 118 0x00, 0x30, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x07, 0x00, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x00, 0xC0, 0xFF, 0x07, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x00, 0x30, // 119 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x10, 0x00, 0x0C, 0x00, 0x00, 0x30, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0F, 0x00, 0x00, 0xF0, 0xC1, 0x07, 0x00, 0x00, 0xE0, 0xE3, 0x03, 0x00, 0x00, 0xC0, 0xFF, 0x01, 0x00, 0x00, 0x00, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x7F, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x01, 0x00, 0x00, 0xE0, 0xE3, 0x03, 0x00, 0x00, 0xF0, 0xC1, 0x07, 0x00, 0x00, 0x70, 0x00, 0x0F, 0x00, 0x00, 0x30, 0x00, 0x0E, 0x00, 0x00, 0x10, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x08, // 120 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x80, 0x03, 0x00, 0xF0, 0x03, 0x80, 0x03, 0x00, 0xE0, 0x1F, 0x80, 0x03, 0x00, 0x80, 0x7F, 0xC0, 0x03, 0x00, 0x00, 0xFC, 0xF3, 0x01, 0x00, 0x00, 0xF0, 0xFF, 0x01, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0x80, 0x7F, 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x10, // 121 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0F, 0x00, 0x00, 0x70, 0xC0, 0x0F, 0x00, 0x00, 0x70, 0xE0, 0x0F, 0x00, 0x00, 0x70, 0xF8, 0x0F, 0x00, 0x00, 0x70, 0x7C, 0x0E, 0x00, 0x00, 0x70, 0x3F, 0x0E, 0x00, 0x00, 0xF0, 0x0F, 0x0E, 0x00, 0x00, 0xF0, 0x07, 0x0E, 0x00, 0x00, 0xF0, 0x03, 0x0E, 0x00, 0x00, 0xF0, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, // 122 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x80, 0xFF, 0xFB, 0x7F, 0x00, 0xC0, 0xFF, 0xFB, 0xFF, 0x00, 0xE0, 0xFF, 0xF1, 0xFF, 0x01, 0xE0, 0x01, 0x00, 0xE0, 0x01, 0xE0, 0x00, 0x00, 0xC0, 0x01, 0xE0, 0x00, 0x00, 0xC0, 0x01, 0xE0, 0x00, 0x00, 0xC0, 0x01, // 123 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0xFF, 0x07, 0xE0, 0xFF, 0xFF, 0xFF, 0x07, // 124 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0xC0, 0x01, 0xE0, 0x00, 0x00, 0xC0, 0x01, 0xE0, 0x00, 0x00, 0xC0, 0x01, 0xE0, 0x01, 0x00, 0xE0, 0x01, 0xE0, 0xFF, 0xF1, 0xFF, 0x00, 0xC0, 0xFF, 0xFB, 0xFF, 0x00, 0x80, 0xFF, 0xFB, 0x7F, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, // 125 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0E, // 126 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 127 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 128 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 129 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 130 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 131 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 132 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 133 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 134 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 135 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 136 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 137 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 138 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 139 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 140 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 141 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 142 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 143 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 144 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 145 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 146 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 147 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 148 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 149 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 150 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 151 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 152 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 153 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 154 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 155 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 156 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 157 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 158 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x80, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0x01, // 159 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xF8, 0xFF, 0x03, 0x00, 0xF0, 0xF8, 0xFF, 0x03, 0x00, 0xF0, 0xF8, 0xFF, 0x03, // 161 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x80, 0xFF, 0x01, 0x00, 0x00, 0xE0, 0xFF, 0x07, 0x00, 0x00, 0xE0, 0x81, 0x07, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0x01, 0x00, 0xFF, 0xFF, 0xFF, 0x01, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0xE0, 0x00, 0x07, // 162 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x0E, 0x00, 0x00, 0x00, 0x07, 0x0E, 0x00, 0x00, 0x00, 0x07, 0x0E, 0x00, 0x00, 0xFC, 0xFF, 0x0F, 0x00, 0x00, 0xFF, 0xFF, 0x0F, 0x00, 0x80, 0xFF, 0xFF, 0x0F, 0x00, 0x80, 0x07, 0x07, 0x0E, 0x00, 0xC0, 0x03, 0x07, 0x0E, 0x00, 0xC0, 0x01, 0x07, 0x0E, 0x00, 0xC0, 0x01, 0x07, 0x0E, 0x00, 0xC0, 0x01, 0x07, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0x80, 0x03, 0x00, 0x0E, // 163 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x01, 0x00, 0x00, 0x70, 0x80, 0x03, 0x00, 0x00, 0xE0, 0xDE, 0x01, 0x00, 0x00, 0xC0, 0xFF, 0x00, 0x00, 0x00, 0x80, 0x61, 0x00, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0x00, 0x80, 0x61, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x00, 0x00, 0x00, 0xE0, 0xDE, 0x01, 0x00, 0x00, 0x70, 0x80, 0x03, 0x00, 0x00, 0x20, 0x00, 0x01, // 164 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xC1, 0x18, 0x00, 0x00, 0xC0, 0xC3, 0x18, 0x00, 0x00, 0xC0, 0xCF, 0x18, 0x00, 0x00, 0x00, 0xFF, 0x18, 0x00, 0x00, 0x00, 0xFC, 0x18, 0x00, 0x00, 0x00, 0xF8, 0x1B, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0xF8, 0x1B, 0x00, 0x00, 0x00, 0xFC, 0x18, 0x00, 0x00, 0x00, 0xFF, 0x18, 0x00, 0x00, 0xC0, 0xCF, 0x18, 0x00, 0x00, 0xC0, 0xC3, 0x18, 0x00, 0x00, 0xC0, 0xC1, 0x18, 0x00, 0x00, 0x40, // 165 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xC3, 0xFF, 0x01, 0x80, 0xFF, 0xC3, 0xFF, 0x01, // 166 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x8F, 0x1F, 0x38, 0x00, 0x80, 0xDF, 0x1F, 0x70, 0x00, 0x80, 0xFF, 0x38, 0x60, 0x00, 0xC0, 0x71, 0x30, 0x60, 0x00, 0xC0, 0xE0, 0x60, 0x60, 0x00, 0xC0, 0xC0, 0xE0, 0x60, 0x00, 0xC0, 0xC0, 0xC1, 0x71, 0x00, 0xC0, 0x80, 0xE3, 0x3F, 0x00, 0xC0, 0x81, 0xBF, 0x3F, 0x00, 0x80, 0x03, 0x3F, 0x0E, 0x00, 0x00, 0x00, 0x1E, // 167 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0xE0, // 168 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0x38, 0x70, 0x00, 0x00, 0x00, 0x1C, 0xE0, 0x00, 0x00, 0x00, 0x8E, 0xCF, 0x01, 0x00, 0x00, 0xE6, 0x9F, 0x01, 0x00, 0x00, 0x63, 0x38, 0x03, 0x00, 0x00, 0x33, 0x30, 0x03, 0x00, 0x00, 0x33, 0x30, 0x03, 0x00, 0x00, 0x33, 0x30, 0x03, 0x00, 0x00, 0x33, 0x30, 0x03, 0x00, 0x00, 0x63, 0x18, 0x03, 0x00, 0x00, 0x06, 0x80, 0x01, 0x00, 0x00, 0x0E, 0xC0, 0x01, 0x00, 0x00, 0x1C, 0xE0, 0x00, 0x00, 0x00, 0x38, 0x70, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0xC0, 0x0F, // 169 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x30, 0x00, 0x00, 0x80, 0xF9, 0x31, 0x00, 0x00, 0xC0, 0x98, 0x33, 0x00, 0x00, 0xC0, 0x0C, 0x33, 0x00, 0x00, 0xC0, 0x0C, 0x33, 0x00, 0x00, 0xC0, 0x0C, 0x33, 0x00, 0x00, 0xC0, 0x0C, 0x33, 0x00, 0x00, 0xC0, 0x8C, 0x31, 0x00, 0x00, 0x80, 0xCD, 0x30, 0x00, 0x00, 0x80, 0xFF, 0x33, 0x00, 0x00, 0x00, 0xFF, 0x33, // 170 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x80, 0x3B, 0x00, 0x00, 0x00, 0xC0, 0x71, 0x00, 0x00, 0x00, 0xE0, 0xE0, 0x00, 0x00, 0x00, 0x70, 0xC0, 0x01, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x00, 0x80, 0x3B, 0x00, 0x00, 0x00, 0xC0, 0x71, 0x00, 0x00, 0x00, 0xE0, 0xE0, 0x00, 0x00, 0x00, 0x70, 0xC0, 0x01, // 171 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0x00, 0x80, 0x3F, // 172 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, // 173 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0x38, 0x70, 0x00, 0x00, 0x00, 0x1C, 0xE0, 0x00, 0x00, 0x00, 0x0E, 0xC0, 0x01, 0x00, 0x00, 0x06, 0x80, 0x01, 0x00, 0x00, 0xF3, 0x3F, 0x03, 0x00, 0x00, 0xF3, 0x3F, 0x03, 0x00, 0x00, 0x33, 0x07, 0x03, 0x00, 0x00, 0x33, 0x1F, 0x03, 0x00, 0x00, 0xF3, 0x3D, 0x03, 0x00, 0x00, 0xE3, 0x30, 0x03, 0x00, 0x00, 0x06, 0xA0, 0x01, 0x00, 0x00, 0x0E, 0xC0, 0x01, 0x00, 0x00, 0x1C, 0xE0, 0x00, 0x00, 0x00, 0x38, 0x70, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0xC0, 0x0F, // 174 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, // 175 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0x00, 0xC0, 0x71, 0x00, 0x00, 0x00, 0xC0, 0x60, 0x00, 0x00, 0x00, 0xC0, 0x60, 0x00, 0x00, 0x00, 0xC0, 0x60, 0x00, 0x00, 0x00, 0xC0, 0x71, 0x00, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x1F, // 176 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x0C, 0x00, 0x00, 0x00, 0x03, 0x0C, 0x00, 0x00, 0x00, 0x03, 0x0C, 0x00, 0x00, 0x00, 0x03, 0x0C, 0x00, 0x00, 0x00, 0x03, 0x0C, 0x00, 0x00, 0x00, 0x03, 0x0C, 0x00, 0x00, 0xF8, 0x7F, 0x0C, 0x00, 0x00, 0xF8, 0x7F, 0x0C, 0x00, 0x00, 0x00, 0x03, 0x0C, 0x00, 0x00, 0x00, 0x03, 0x0C, 0x00, 0x00, 0x00, 0x03, 0x0C, 0x00, 0x00, 0x00, 0x03, 0x0C, 0x00, 0x00, 0x00, 0x03, 0x0C, 0x00, 0x00, 0x00, 0x03, 0x0C, // 177 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x03, 0x00, 0x00, 0xC0, 0x80, 0x03, 0x00, 0x00, 0xC0, 0xC0, 0x03, 0x00, 0x00, 0xC0, 0xE0, 0x03, 0x00, 0x00, 0xC0, 0x70, 0x03, 0x00, 0x00, 0xC0, 0x39, 0x03, 0x00, 0x00, 0x80, 0x1F, 0x03, 0x00, 0x00, 0x00, 0x07, 0x03, // 178 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x81, 0x01, 0x00, 0x00, 0xC0, 0x00, 0x03, 0x00, 0x00, 0xC0, 0x0C, 0x03, 0x00, 0x00, 0xC0, 0x0C, 0x03, 0x00, 0x00, 0xC0, 0x0C, 0x03, 0x00, 0x00, 0xC0, 0x0C, 0x03, 0x00, 0x00, 0xC0, 0x9C, 0x03, 0x00, 0x00, 0x80, 0xFF, 0x01, 0x00, 0x00, 0x80, 0xF3, // 179 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x00, 0x10, // 180 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0xFF, 0x03, 0x00, 0xF0, 0xFF, 0xFF, 0x03, 0x00, 0xF0, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0xF0, 0xFF, 0x01, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, // 181 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x80, 0xFF, 0x01, 0x00, 0x00, 0x80, 0xFF, 0x01, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x7F, 0x00, 0xC0, 0xFF, 0xFF, 0x7F, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x7F, 0x00, 0xC0, 0xFF, 0xFF, 0x7F, // 182 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x80, 0x0F, // 183 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x30, 0x03, 0x00, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x00, 0xC0, 0x01, // 184 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x03, 0x00, 0x00, 0x80, 0x01, 0x03, 0x00, 0x00, 0xC0, 0x00, 0x03, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x03, // 185 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x30, 0x00, 0x00, 0x00, 0xFF, 0x30, 0x00, 0x00, 0x80, 0xC3, 0x31, 0x00, 0x00, 0xC0, 0x81, 0x33, 0x00, 0x00, 0xC0, 0x00, 0x33, 0x00, 0x00, 0xC0, 0x00, 0x33, 0x00, 0x00, 0xC0, 0x00, 0x33, 0x00, 0x00, 0xC0, 0x81, 0x33, 0x00, 0x00, 0x80, 0xC3, 0x31, 0x00, 0x00, 0x00, 0xFF, 0x30, 0x00, 0x00, 0x00, 0x7E, 0x30, // 186 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0xC0, 0x01, 0x00, 0x00, 0xE0, 0xE0, 0x00, 0x00, 0x00, 0xC0, 0x71, 0x00, 0x00, 0x00, 0x80, 0x3B, 0x00, 0x00, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x70, 0xC0, 0x01, 0x00, 0x00, 0xE0, 0xE0, 0x00, 0x00, 0x00, 0xC0, 0x71, 0x00, 0x00, 0x00, 0x80, 0x3B, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, // 187 0x00, 0x00, 0x18, 0x00, 0x00, 0x60, 0xC0, 0x18, 0x00, 0x00, 0x60, 0xC0, 0x0C, 0x00, 0x00, 0x30, 0xC0, 0x0C, 0x00, 0x00, 0xF0, 0xFF, 0x0C, 0x00, 0x00, 0xF0, 0xFF, 0x0C, 0x00, 0x00, 0x00, 0xC0, 0x06, 0x00, 0x00, 0x00, 0xC0, 0x06, 0x18, 0x00, 0x00, 0xC0, 0x06, 0x1E, 0x00, 0x00, 0x00, 0x06, 0x1B, 0x00, 0x00, 0x00, 0x83, 0x19, 0x00, 0x00, 0x00, 0xE3, 0x18, 0x00, 0x00, 0x00, 0x33, 0x18, 0x00, 0x00, 0x00, 0xF3, 0xFF, 0x00, 0x00, 0x80, 0xF1, 0xFF, 0x00, 0x00, 0x80, 0x01, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, // 188 0x00, 0x00, 0x18, 0x00, 0x00, 0x60, 0xC0, 0x18, 0x00, 0x00, 0x60, 0xC0, 0x0C, 0x00, 0x00, 0x30, 0xC0, 0x0C, 0x00, 0x00, 0xF0, 0xFF, 0x0C, 0x00, 0x00, 0xF0, 0xFF, 0x0C, 0x00, 0x00, 0x00, 0xC0, 0x06, 0x00, 0x00, 0x00, 0xC0, 0x06, 0x00, 0x00, 0x00, 0xC0, 0x66, 0xC0, 0x00, 0x00, 0x00, 0x36, 0xE0, 0x00, 0x00, 0x00, 0x33, 0xF0, 0x00, 0x00, 0x00, 0x33, 0xF8, 0x00, 0x00, 0x00, 0x33, 0xDC, 0x00, 0x00, 0x00, 0x73, 0xCE, 0x00, 0x00, 0x80, 0xE1, 0xC7, 0x00, 0x00, 0x80, 0xC1, 0xC1, // 189 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x60, 0x60, 0x0C, 0x00, 0x00, 0x30, 0xC0, 0x0C, 0x00, 0x00, 0x30, 0xC3, 0x0C, 0x00, 0x00, 0x30, 0xC3, 0x0C, 0x00, 0x00, 0x30, 0xC3, 0x06, 0x00, 0x00, 0x30, 0xC3, 0x06, 0x18, 0x00, 0x30, 0xE7, 0x06, 0x1E, 0x00, 0xE0, 0x7F, 0x06, 0x1B, 0x00, 0xE0, 0x3C, 0x83, 0x19, 0x00, 0x00, 0x00, 0xE3, 0x18, 0x00, 0x00, 0x00, 0x33, 0x18, 0x00, 0x00, 0x00, 0xF3, 0xFF, 0x00, 0x00, 0x80, 0xF1, 0xFF, 0x00, 0x00, 0x80, 0x01, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, // 190 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x01, 0x00, 0x00, 0x80, 0xCF, 0x03, 0x00, 0x00, 0xC0, 0x83, 0x03, 0x00, 0xF0, 0xFC, 0x81, 0x03, 0x00, 0xF0, 0xFC, 0x80, 0x03, 0x00, 0xF0, 0x7C, 0x80, 0x03, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xE0, // 191 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xE0, 0xFF, 0x00, 0x00, 0x02, 0xFC, 0x7F, 0x00, 0x00, 0x86, 0xFF, 0x71, 0x00, 0x00, 0xCE, 0x3F, 0x70, 0x00, 0x00, 0xDC, 0x03, 0x70, 0x00, 0x00, 0xD8, 0x3F, 0x70, 0x00, 0x00, 0x90, 0xFF, 0x71, 0x00, 0x00, 0x00, 0xFC, 0x7F, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0C, // 192 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xE0, 0xFF, 0x00, 0x00, 0x00, 0xFC, 0x7F, 0x00, 0x00, 0x90, 0xFF, 0x71, 0x00, 0x00, 0xD8, 0x3F, 0x70, 0x00, 0x00, 0xDC, 0x03, 0x70, 0x00, 0x00, 0xCE, 0x3F, 0x70, 0x00, 0x00, 0x86, 0xFF, 0x71, 0x00, 0x00, 0x02, 0xFC, 0x7F, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0C, // 193 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x10, 0xE0, 0xFF, 0x00, 0x00, 0x18, 0xFC, 0x7F, 0x00, 0x00, 0x9C, 0xFF, 0x71, 0x00, 0x00, 0xC6, 0x3F, 0x70, 0x00, 0x00, 0xC2, 0x03, 0x70, 0x00, 0x00, 0xC6, 0x3F, 0x70, 0x00, 0x00, 0x9C, 0xFF, 0x71, 0x00, 0x00, 0x18, 0xFC, 0x7F, 0x00, 0x00, 0x10, 0xE0, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0C, // 194 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x0C, 0xE0, 0xFF, 0x00, 0x00, 0x0E, 0xFC, 0x7F, 0x00, 0x00, 0x86, 0xFF, 0x71, 0x00, 0x00, 0xC6, 0x3F, 0x70, 0x00, 0x00, 0xCE, 0x03, 0x70, 0x00, 0x00, 0xCC, 0x3F, 0x70, 0x00, 0x00, 0x8C, 0xFF, 0x71, 0x00, 0x00, 0x0E, 0xFC, 0x7F, 0x00, 0x00, 0x06, 0xE0, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0C, // 195 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x0E, 0xE0, 0xFF, 0x00, 0x00, 0x0E, 0xFC, 0x7F, 0x00, 0x00, 0x8E, 0xFF, 0x71, 0x00, 0x00, 0xC0, 0x3F, 0x70, 0x00, 0x00, 0xC0, 0x03, 0x70, 0x00, 0x00, 0xC0, 0x3F, 0x70, 0x00, 0x00, 0x8E, 0xFF, 0x71, 0x00, 0x00, 0x0E, 0xFC, 0x7F, 0x00, 0x00, 0x0E, 0xE0, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0C, // 196 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x3C, 0xF0, 0x7F, 0x00, 0x00, 0x7E, 0xFE, 0x7F, 0x00, 0x00, 0xE7, 0xFF, 0x70, 0x00, 0x00, 0xC3, 0x1F, 0x70, 0x00, 0x00, 0xC3, 0x01, 0x70, 0x00, 0x00, 0xC3, 0x1F, 0x70, 0x00, 0x00, 0xE7, 0xFF, 0x70, 0x00, 0x00, 0x7E, 0xFE, 0x7F, 0x00, 0x00, 0x3C, 0xF0, 0x7F, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0C, // 197 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x00, 0x80, 0xFF, 0x03, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x80, 0xFF, 0x77, 0x00, 0x00, 0xC0, 0x7F, 0x70, 0x00, 0x00, 0xC0, 0x07, 0x70, 0x00, 0x00, 0xC0, 0x01, 0x70, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, // 198 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x00, 0x00, 0xFC, 0xFF, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, 0x1F, 0xE0, 0x03, 0x00, 0x80, 0x07, 0x80, 0x07, 0x00, 0x80, 0x03, 0x00, 0x07, 0x03, 0xC0, 0x01, 0x00, 0x0E, 0x03, 0xC0, 0x01, 0x00, 0x0E, 0x03, 0xC0, 0x01, 0x00, 0x3E, 0x03, 0xC0, 0x01, 0x00, 0xFE, 0x03, 0xC0, 0x01, 0x00, 0xCE, 0x01, 0xC0, 0x03, 0x00, 0x0E, 0x00, 0x80, 0x03, 0x00, 0x07, 0x00, 0x00, 0x07, 0x80, 0x03, // 199 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC2, 0x81, 0x03, 0x0E, 0x00, 0xC6, 0x81, 0x03, 0x0E, 0x00, 0xCE, 0x81, 0x03, 0x0E, 0x00, 0xDC, 0x81, 0x03, 0x0E, 0x00, 0xD8, 0x81, 0x03, 0x0E, 0x00, 0xD0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, // 200 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xD0, 0x81, 0x03, 0x0E, 0x00, 0xD8, 0x81, 0x03, 0x0E, 0x00, 0xDC, 0x81, 0x03, 0x0E, 0x00, 0xCE, 0x81, 0x03, 0x0E, 0x00, 0xC6, 0x81, 0x03, 0x0E, 0x00, 0xC2, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, // 201 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xD0, 0xFF, 0xFF, 0x0F, 0x00, 0xD8, 0x81, 0x03, 0x0E, 0x00, 0xDC, 0x81, 0x03, 0x0E, 0x00, 0xC6, 0x81, 0x03, 0x0E, 0x00, 0xC2, 0x81, 0x03, 0x0E, 0x00, 0xC6, 0x81, 0x03, 0x0E, 0x00, 0xDC, 0x81, 0x03, 0x0E, 0x00, 0xD8, 0x81, 0x03, 0x0E, 0x00, 0xD0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, // 202 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xCE, 0xFF, 0xFF, 0x0F, 0x00, 0xCE, 0x81, 0x03, 0x0E, 0x00, 0xCE, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xCE, 0x81, 0x03, 0x0E, 0x00, 0xCE, 0x81, 0x03, 0x0E, 0x00, 0xCE, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, // 203 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC2, 0x01, 0x00, 0x0E, 0x00, 0xC6, 0x01, 0x00, 0x0E, 0x00, 0xCE, 0xFF, 0xFF, 0x0F, 0x00, 0xDC, 0xFF, 0xFF, 0x0F, 0x00, 0xD8, 0xFF, 0xFF, 0x0F, 0x00, 0xD0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, // 204 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xD0, 0x01, 0x00, 0x0E, 0x00, 0xD8, 0xFF, 0xFF, 0x0F, 0x00, 0xDC, 0xFF, 0xFF, 0x0F, 0x00, 0xCE, 0xFF, 0xFF, 0x0F, 0x00, 0xC6, 0x01, 0x00, 0x0E, 0x00, 0xC2, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, // 205 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xD0, 0x01, 0x00, 0x0E, 0x00, 0xD8, 0x01, 0x00, 0x0E, 0x00, 0xDC, 0x01, 0x00, 0x0E, 0x00, 0xC6, 0xFF, 0xFF, 0x0F, 0x00, 0xC2, 0xFF, 0xFF, 0x0F, 0x00, 0xC6, 0xFF, 0xFF, 0x0F, 0x00, 0xDC, 0x01, 0x00, 0x0E, 0x00, 0xD8, 0x01, 0x00, 0x0E, 0x00, 0xD0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, // 206 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xCE, 0x01, 0x00, 0x0E, 0x00, 0xCE, 0x01, 0x00, 0x0E, 0x00, 0xCE, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xCE, 0x01, 0x00, 0x0E, 0x00, 0xCE, 0x01, 0x00, 0x0E, 0x00, 0xCE, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, // 207 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0x80, 0x03, 0x00, 0x07, 0x00, 0x80, 0x07, 0x80, 0x07, 0x00, 0x00, 0x1F, 0xE0, 0x03, 0x00, 0x00, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0xFC, 0xFF, 0x00, 0x00, 0x00, 0xF0, 0x3F, // 208 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC6, 0xFF, 0xFF, 0x0F, 0x00, 0xC7, 0x07, 0x00, 0x00, 0x00, 0x03, 0x3F, 0x00, 0x00, 0x00, 0x03, 0xFC, 0x00, 0x00, 0x00, 0x03, 0xE0, 0x07, 0x00, 0x00, 0x06, 0x80, 0x1F, 0x00, 0x00, 0x06, 0x00, 0xFC, 0x00, 0x00, 0x06, 0x00, 0xF0, 0x03, 0x00, 0x07, 0x00, 0x80, 0x0F, 0x00, 0xC3, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, // 209 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, 0xFF, 0xFF, 0x03, 0x00, 0x80, 0x0F, 0xC0, 0x07, 0x00, 0x82, 0x03, 0x00, 0x07, 0x00, 0xC6, 0x01, 0x00, 0x0E, 0x00, 0xCE, 0x01, 0x00, 0x0E, 0x00, 0xDC, 0x01, 0x00, 0x0E, 0x00, 0xD8, 0x01, 0x00, 0x0E, 0x00, 0xD0, 0x01, 0x00, 0x0E, 0x00, 0x80, 0x03, 0x00, 0x07, 0x00, 0x80, 0x0F, 0xC0, 0x07, 0x00, 0x00, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, 0xF0, 0x3F, // 210 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, 0xFF, 0xFF, 0x03, 0x00, 0x80, 0x0F, 0xC0, 0x07, 0x00, 0x80, 0x03, 0x00, 0x07, 0x00, 0xD0, 0x01, 0x00, 0x0E, 0x00, 0xD8, 0x01, 0x00, 0x0E, 0x00, 0xDC, 0x01, 0x00, 0x0E, 0x00, 0xCE, 0x01, 0x00, 0x0E, 0x00, 0xC6, 0x01, 0x00, 0x0E, 0x00, 0x82, 0x03, 0x00, 0x07, 0x00, 0x80, 0x0F, 0xC0, 0x07, 0x00, 0x00, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, 0xF0, 0x3F, // 211 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, 0xFF, 0xFF, 0x03, 0x00, 0x90, 0x0F, 0xC0, 0x07, 0x00, 0x98, 0x03, 0x00, 0x07, 0x00, 0xDC, 0x01, 0x00, 0x0E, 0x00, 0xC6, 0x01, 0x00, 0x0E, 0x00, 0xC2, 0x01, 0x00, 0x0E, 0x00, 0xC6, 0x01, 0x00, 0x0E, 0x00, 0xDC, 0x01, 0x00, 0x0E, 0x00, 0x98, 0x03, 0x00, 0x07, 0x00, 0x90, 0x0F, 0xC0, 0x07, 0x00, 0x00, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, 0xF0, 0x3F, // 212 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, 0xFF, 0xFF, 0x03, 0x00, 0x8C, 0x0F, 0xC0, 0x07, 0x00, 0x8E, 0x03, 0x00, 0x07, 0x00, 0xC6, 0x01, 0x00, 0x0E, 0x00, 0xC6, 0x01, 0x00, 0x0E, 0x00, 0xCE, 0x01, 0x00, 0x0E, 0x00, 0xCC, 0x01, 0x00, 0x0E, 0x00, 0xCC, 0x01, 0x00, 0x0E, 0x00, 0x8E, 0x03, 0x00, 0x07, 0x00, 0x86, 0x0F, 0xC0, 0x07, 0x00, 0x00, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, 0xF0, 0x3F, // 213 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, 0xFF, 0xFF, 0x03, 0x00, 0x8E, 0x0F, 0xC0, 0x07, 0x00, 0x8E, 0x03, 0x00, 0x07, 0x00, 0xCE, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xCE, 0x01, 0x00, 0x0E, 0x00, 0x8E, 0x03, 0x00, 0x07, 0x00, 0x8E, 0x0F, 0xC0, 0x07, 0x00, 0x00, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0x00, 0xF0, 0x3F, // 214 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x01, 0x00, 0x00, 0x70, 0x80, 0x03, 0x00, 0x00, 0xE0, 0xC0, 0x01, 0x00, 0x00, 0xC0, 0xE1, 0x00, 0x00, 0x00, 0x80, 0x73, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x80, 0x73, 0x00, 0x00, 0x00, 0xC0, 0xE1, 0x00, 0x00, 0x00, 0xE0, 0xC0, 0x01, 0x00, 0x00, 0x70, 0x80, 0x03, 0x00, 0x00, 0x20, 0x00, 0x01, // 215 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0xF0, 0x3F, 0x0F, 0x00, 0x00, 0xFE, 0xFF, 0x03, 0x00, 0x00, 0xFF, 0xFF, 0x03, 0x00, 0x80, 0x0F, 0xF0, 0x07, 0x00, 0x80, 0x03, 0x38, 0x07, 0x00, 0xC0, 0x01, 0x1C, 0x0E, 0x00, 0xC0, 0x01, 0x06, 0x0E, 0x00, 0xC0, 0x81, 0x03, 0x0E, 0x00, 0xC0, 0xC1, 0x01, 0x0E, 0x00, 0xC0, 0xE1, 0x00, 0x0E, 0x00, 0x80, 0x73, 0x00, 0x07, 0x00, 0x80, 0x1F, 0xC0, 0x07, 0x00, 0x00, 0xFE, 0xFF, 0x03, 0x00, 0x00, 0xFF, 0xFF, 0x01, 0x00, 0xC0, 0xF1, 0x3F, 0x00, 0x00, 0xC0, // 216 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x03, 0x00, 0xC0, 0xFF, 0xFF, 0x07, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x02, 0x00, 0x00, 0x0F, 0x00, 0x06, 0x00, 0x00, 0x0E, 0x00, 0x0E, 0x00, 0x00, 0x0E, 0x00, 0x1C, 0x00, 0x00, 0x0E, 0x00, 0x18, 0x00, 0x00, 0x0E, 0x00, 0x10, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0xC0, 0xFF, 0xFF, 0x07, 0x00, 0xC0, 0xFF, 0xFF, 0x03, 0x00, 0xC0, 0xFF, 0xFF, // 217 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x03, 0x00, 0xC0, 0xFF, 0xFF, 0x07, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x10, 0x00, 0x00, 0x0E, 0x00, 0x18, 0x00, 0x00, 0x0E, 0x00, 0x1C, 0x00, 0x00, 0x0E, 0x00, 0x0E, 0x00, 0x00, 0x0E, 0x00, 0x06, 0x00, 0x00, 0x0F, 0x00, 0x02, 0x00, 0x80, 0x07, 0x00, 0xC0, 0xFF, 0xFF, 0x07, 0x00, 0xC0, 0xFF, 0xFF, 0x03, 0x00, 0xC0, 0xFF, 0xFF, // 218 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x03, 0x00, 0xC0, 0xFF, 0xFF, 0x07, 0x00, 0x10, 0x00, 0x80, 0x07, 0x00, 0x18, 0x00, 0x00, 0x0F, 0x00, 0x0E, 0x00, 0x00, 0x0E, 0x00, 0x06, 0x00, 0x00, 0x0E, 0x00, 0x06, 0x00, 0x00, 0x0E, 0x00, 0x0E, 0x00, 0x00, 0x0E, 0x00, 0x18, 0x00, 0x00, 0x0F, 0x00, 0x10, 0x00, 0x80, 0x07, 0x00, 0xC0, 0xFF, 0xFF, 0x07, 0x00, 0xC0, 0xFF, 0xFF, 0x03, 0x00, 0xC0, 0xFF, 0xFF, // 219 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x03, 0x00, 0xC0, 0xFF, 0xFF, 0x07, 0x00, 0x0E, 0x00, 0x80, 0x07, 0x00, 0x0E, 0x00, 0x00, 0x0F, 0x00, 0x0E, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x0E, 0x00, 0x00, 0x0E, 0x00, 0x0E, 0x00, 0x00, 0x0F, 0x00, 0x0E, 0x00, 0x80, 0x07, 0x00, 0xC0, 0xFF, 0xFF, 0x07, 0x00, 0xC0, 0xFF, 0xFF, 0x03, 0x00, 0xC0, 0xFF, 0xFF, // 220 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0x10, 0xF8, 0x01, 0x00, 0x00, 0x18, 0xE0, 0xFF, 0x0F, 0x00, 0x1C, 0x80, 0xFF, 0x0F, 0x00, 0x0E, 0xE0, 0xFF, 0x0F, 0x00, 0x06, 0xF8, 0x01, 0x00, 0x00, 0x02, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0x40, // 221 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x1C, 0x70, 0x00, 0x00, 0x00, 0x1C, 0x70, 0x00, 0x00, 0x00, 0x1C, 0x70, 0x00, 0x00, 0x00, 0x1C, 0x70, 0x00, 0x00, 0x00, 0x1C, 0x70, 0x00, 0x00, 0x00, 0x1C, 0x70, 0x00, 0x00, 0x00, 0x3C, 0x78, 0x00, 0x00, 0x00, 0x78, 0x3C, 0x00, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x00, 0xF0, 0x1F, 0x00, 0x00, 0x00, 0xE0, 0x0F, // 222 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x0F, 0x00, 0x80, 0xFF, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x07, 0x00, 0xE0, 0xE0, 0x01, 0x0E, 0x00, 0xE0, 0xF0, 0x03, 0x0E, 0x00, 0xE0, 0xF8, 0x07, 0x0E, 0x00, 0xE0, 0x19, 0x06, 0x0E, 0x00, 0xC0, 0x0F, 0x0E, 0x0E, 0x00, 0x80, 0x0F, 0x1C, 0x0F, 0x00, 0x00, 0x0E, 0xF8, 0x07, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0x00, 0xE0, 0x01, // 223 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, 0xF1, 0x07, 0x00, 0x10, 0xE0, 0xF8, 0x07, 0x00, 0x30, 0xE0, 0x38, 0x0F, 0x00, 0x70, 0x70, 0x1C, 0x0E, 0x00, 0xE0, 0x70, 0x1C, 0x0E, 0x00, 0xC0, 0x73, 0x1C, 0x0E, 0x00, 0x00, 0x73, 0x1C, 0x0E, 0x00, 0x00, 0x72, 0x1C, 0x06, 0x00, 0x00, 0x70, 0x1C, 0x07, 0x00, 0x00, 0xE0, 0x9C, 0x01, 0x00, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0xC0, 0xFF, 0x0F, 0x00, 0x00, 0x80, 0xFF, 0x0F, // 224 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, 0xF1, 0x07, 0x00, 0x00, 0xE0, 0xF8, 0x07, 0x00, 0x00, 0xE0, 0x38, 0x0F, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x72, 0x1C, 0x0E, 0x00, 0x00, 0x73, 0x1C, 0x0E, 0x00, 0xC0, 0x73, 0x1C, 0x0E, 0x00, 0xE0, 0x70, 0x1C, 0x06, 0x00, 0x70, 0x70, 0x1C, 0x07, 0x00, 0x30, 0xE0, 0x9C, 0x01, 0x00, 0x10, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0xC0, 0xFF, 0x0F, 0x00, 0x00, 0x80, 0xFF, 0x0F, // 225 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, 0xF1, 0x07, 0x00, 0x00, 0xE2, 0xF8, 0x07, 0x00, 0x80, 0xE3, 0x38, 0x0F, 0x00, 0xC0, 0x71, 0x1C, 0x0E, 0x00, 0xF0, 0x70, 0x1C, 0x0E, 0x00, 0x30, 0x70, 0x1C, 0x0E, 0x00, 0xF0, 0x70, 0x1C, 0x0E, 0x00, 0xC0, 0x71, 0x1C, 0x06, 0x00, 0x80, 0x73, 0x1C, 0x07, 0x00, 0x00, 0xE2, 0x9C, 0x01, 0x00, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0xC0, 0xFF, 0x0F, 0x00, 0x00, 0x80, 0xFF, 0x0F, // 226 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, 0xF1, 0x07, 0x00, 0xC0, 0xE1, 0xF8, 0x07, 0x00, 0xE0, 0xE1, 0x38, 0x0F, 0x00, 0x60, 0x70, 0x1C, 0x0E, 0x00, 0x60, 0x70, 0x1C, 0x0E, 0x00, 0xE0, 0x70, 0x1C, 0x0E, 0x00, 0xC0, 0x71, 0x1C, 0x0E, 0x00, 0x80, 0x71, 0x1C, 0x06, 0x00, 0x80, 0x71, 0x1C, 0x07, 0x00, 0xE0, 0xE1, 0x9C, 0x01, 0x00, 0x60, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0xC0, 0xFF, 0x0F, 0x00, 0x00, 0x80, 0xFF, 0x0F, // 227 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, 0xF1, 0x07, 0x00, 0x00, 0xE0, 0xF8, 0x07, 0x00, 0xE0, 0xE0, 0x38, 0x0F, 0x00, 0xE0, 0x70, 0x1C, 0x0E, 0x00, 0xE0, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x06, 0x00, 0xE0, 0x70, 0x1C, 0x07, 0x00, 0xE0, 0xE0, 0x9C, 0x01, 0x00, 0xE0, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0xC0, 0xFF, 0x0F, 0x00, 0x00, 0x80, 0xFF, 0x0F, // 228 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, 0xF1, 0x07, 0x00, 0x00, 0xE0, 0xF8, 0x07, 0x00, 0xF0, 0xE0, 0x38, 0x0F, 0x00, 0xF8, 0x71, 0x1C, 0x0E, 0x00, 0x9C, 0x73, 0x1C, 0x0E, 0x00, 0x0C, 0x73, 0x1C, 0x0E, 0x00, 0x0C, 0x73, 0x1C, 0x0E, 0x00, 0x9C, 0x73, 0x1C, 0x06, 0x00, 0xF8, 0x71, 0x1C, 0x07, 0x00, 0xF0, 0xE0, 0x9C, 0x01, 0x00, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0xC0, 0xFF, 0x0F, 0x00, 0x00, 0x80, 0xFF, 0x0F, // 229 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0xE0, 0xF8, 0x07, 0x00, 0x00, 0x70, 0xF8, 0x0F, 0x00, 0x00, 0x70, 0x3C, 0x0F, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0xF0, 0x1C, 0x0F, 0x00, 0x00, 0xE0, 0xFF, 0x07, 0x00, 0x00, 0xC0, 0xFF, 0x01, 0x00, 0x00, 0xE0, 0xFF, 0x07, 0x00, 0x00, 0xF0, 0x1C, 0x07, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0xF0, 0x1C, 0x0E, 0x00, 0x00, 0xF0, 0x1F, 0x0E, 0x00, 0x00, 0xE0, 0x1F, 0x07, 0x00, 0x00, 0x80, 0x1F, 0x07, // 230 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x80, 0xFF, 0x01, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0xE0, 0xC3, 0x07, 0x00, 0x00, 0xE0, 0x00, 0x07, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x03, 0x00, 0x70, 0x00, 0x0E, 0x03, 0x00, 0x70, 0x00, 0x0E, 0x03, 0x00, 0x70, 0x00, 0x3E, 0x03, 0x00, 0x70, 0x00, 0xFE, 0x03, 0x00, 0x70, 0x00, 0xCE, 0x01, 0x00, 0xE0, 0x00, 0x07, 0x00, 0x00, 0xC0, 0x81, 0x03, // 231 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x80, 0xFF, 0x01, 0x00, 0x10, 0xC0, 0xFF, 0x03, 0x00, 0x30, 0xE0, 0x9D, 0x07, 0x00, 0x70, 0xE0, 0x1C, 0x07, 0x00, 0xE0, 0x70, 0x1C, 0x0E, 0x00, 0xC0, 0x73, 0x1C, 0x0E, 0x00, 0x00, 0x73, 0x1C, 0x0E, 0x00, 0x00, 0x72, 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0xF0, 0x1C, 0x0E, 0x00, 0x00, 0xE0, 0x1C, 0x07, 0x00, 0x00, 0xE0, 0x1F, 0x07, 0x00, 0x00, 0xC0, 0x9F, 0x03, 0x00, 0x00, 0x00, 0x1F, // 232 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x80, 0xFF, 0x01, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0xE0, 0x9D, 0x07, 0x00, 0x00, 0xE0, 0x1C, 0x07, 0x00, 0x00, 0x72, 0x1C, 0x0E, 0x00, 0x00, 0x73, 0x1C, 0x0E, 0x00, 0xC0, 0x73, 0x1C, 0x0E, 0x00, 0xE0, 0x70, 0x1C, 0x0E, 0x00, 0x70, 0x70, 0x1C, 0x0E, 0x00, 0x30, 0xF0, 0x1C, 0x0E, 0x00, 0x10, 0xE0, 0x1C, 0x07, 0x00, 0x00, 0xE0, 0x1F, 0x07, 0x00, 0x00, 0xC0, 0x9F, 0x03, 0x00, 0x00, 0x00, 0x1F, // 233 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x80, 0xFF, 0x01, 0x00, 0x00, 0xC2, 0xFF, 0x03, 0x00, 0x80, 0xE3, 0x9D, 0x07, 0x00, 0xC0, 0xE1, 0x1C, 0x07, 0x00, 0xF0, 0x70, 0x1C, 0x0E, 0x00, 0x30, 0x70, 0x1C, 0x0E, 0x00, 0xF0, 0x70, 0x1C, 0x0E, 0x00, 0xC0, 0x71, 0x1C, 0x0E, 0x00, 0x80, 0x73, 0x1C, 0x0E, 0x00, 0x00, 0xF2, 0x1C, 0x0E, 0x00, 0x00, 0xE0, 0x1C, 0x07, 0x00, 0x00, 0xE0, 0x1F, 0x07, 0x00, 0x00, 0xC0, 0x9F, 0x03, 0x00, 0x00, 0x00, 0x1F, // 234 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x80, 0xFF, 0x01, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0xE0, 0xE0, 0x9D, 0x07, 0x00, 0xE0, 0xE0, 0x1C, 0x07, 0x00, 0xE0, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0xE0, 0x70, 0x1C, 0x0E, 0x00, 0xE0, 0xF0, 0x1C, 0x0E, 0x00, 0xE0, 0xE0, 0x1C, 0x07, 0x00, 0x00, 0xE0, 0x1F, 0x07, 0x00, 0x00, 0xC0, 0x9F, 0x03, 0x00, 0x00, 0x00, 0x1F, // 235 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x10, 0x70, 0x00, 0x0E, 0x00, 0x30, 0x70, 0x00, 0x0E, 0x00, 0x70, 0x70, 0x00, 0x0E, 0x00, 0xE0, 0x70, 0x00, 0x0E, 0x00, 0xC0, 0xF3, 0xFF, 0x0F, 0x00, 0x00, 0xF3, 0xFF, 0x0F, 0x00, 0x00, 0xF2, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, // 236 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x72, 0x00, 0x0E, 0x00, 0x00, 0xF3, 0xFF, 0x0F, 0x00, 0xC0, 0xF3, 0xFF, 0x0F, 0x00, 0xE0, 0xF0, 0xFF, 0x0F, 0x00, 0x70, 0x00, 0x00, 0x0E, 0x00, 0x30, 0x00, 0x00, 0x0E, 0x00, 0x10, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, // 237 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x72, 0x00, 0x0E, 0x00, 0x80, 0x73, 0x00, 0x0E, 0x00, 0xC0, 0x71, 0x00, 0x0E, 0x00, 0xF0, 0x70, 0x00, 0x0E, 0x00, 0x30, 0xF0, 0xFF, 0x0F, 0x00, 0xF0, 0xF0, 0xFF, 0x0F, 0x00, 0xC0, 0xF1, 0xFF, 0x0F, 0x00, 0x80, 0x03, 0x00, 0x0E, 0x00, 0x00, 0x02, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, // 238 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0xE0, 0x70, 0x00, 0x0E, 0x00, 0xE0, 0x70, 0x00, 0x0E, 0x00, 0xE0, 0x70, 0x00, 0x0E, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0xE0, 0x00, 0x00, 0x0E, 0x00, 0xE0, 0x00, 0x00, 0x0E, 0x00, 0xE0, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, // 239 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x20, 0xE6, 0xFF, 0x07, 0x00, 0x60, 0xE2, 0x81, 0x07, 0x00, 0xE0, 0xF3, 0x00, 0x0F, 0x00, 0xE0, 0x73, 0x00, 0x0E, 0x00, 0xC0, 0x77, 0x00, 0x0E, 0x00, 0x80, 0x7F, 0x00, 0x0E, 0x00, 0x80, 0x7E, 0x00, 0x0E, 0x00, 0xC0, 0x7C, 0x00, 0x0F, 0x00, 0xC0, 0xF8, 0x81, 0x07, 0x00, 0x40, 0xF0, 0xFF, 0x07, 0x00, 0x00, 0xC0, 0xFF, 0x01, 0x00, 0x00, 0x00, 0x7F, // 240 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0xC0, 0xF1, 0xFF, 0x0F, 0x00, 0xE0, 0xF1, 0xFF, 0x0F, 0x00, 0x60, 0xC0, 0x01, 0x00, 0x00, 0x60, 0xE0, 0x00, 0x00, 0x00, 0xE0, 0x60, 0x00, 0x00, 0x00, 0xC0, 0x71, 0x00, 0x00, 0x00, 0x80, 0x71, 0x00, 0x00, 0x00, 0x80, 0x71, 0x00, 0x00, 0x00, 0xE0, 0xF1, 0x00, 0x00, 0x00, 0x60, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0x80, 0xFF, 0x0F, // 241 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x10, 0xE0, 0xFF, 0x07, 0x00, 0x30, 0xE0, 0x81, 0x07, 0x00, 0x70, 0xF0, 0x00, 0x0F, 0x00, 0xE0, 0x70, 0x00, 0x0E, 0x00, 0xC0, 0x73, 0x00, 0x0E, 0x00, 0x00, 0x73, 0x00, 0x0E, 0x00, 0x00, 0x72, 0x00, 0x0E, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xE0, 0x81, 0x07, 0x00, 0x00, 0xE0, 0xFF, 0x07, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0x00, 0xFF, // 242 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0xE0, 0xFF, 0x07, 0x00, 0x00, 0xE0, 0x81, 0x07, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0x72, 0x00, 0x0E, 0x00, 0x00, 0x73, 0x00, 0x0E, 0x00, 0xC0, 0x73, 0x00, 0x0E, 0x00, 0xE0, 0x70, 0x00, 0x0E, 0x00, 0x70, 0xF0, 0x00, 0x0F, 0x00, 0x30, 0xE0, 0x81, 0x07, 0x00, 0x10, 0xE0, 0xFF, 0x07, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0x00, 0xFF, // 243 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0xE0, 0xFF, 0x07, 0x00, 0x00, 0xE2, 0x81, 0x07, 0x00, 0x80, 0xF3, 0x00, 0x0F, 0x00, 0xE0, 0x71, 0x00, 0x0E, 0x00, 0x70, 0x70, 0x00, 0x0E, 0x00, 0x70, 0x70, 0x00, 0x0E, 0x00, 0xE0, 0x71, 0x00, 0x0E, 0x00, 0x80, 0xF3, 0x00, 0x0F, 0x00, 0x00, 0xE2, 0x81, 0x07, 0x00, 0x00, 0xE0, 0xFF, 0x07, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0x00, 0xFF, // 244 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0xC0, 0xE1, 0xFF, 0x07, 0x00, 0xE0, 0xE1, 0x81, 0x07, 0x00, 0x60, 0xF0, 0x00, 0x0F, 0x00, 0x60, 0x70, 0x00, 0x0E, 0x00, 0xE0, 0x70, 0x00, 0x0E, 0x00, 0xC0, 0x71, 0x00, 0x0E, 0x00, 0x80, 0x71, 0x00, 0x0E, 0x00, 0x80, 0xF1, 0x00, 0x0F, 0x00, 0xE0, 0xE1, 0x81, 0x07, 0x00, 0x60, 0xE0, 0xFF, 0x07, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0x00, 0xFF, // 245 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0xE0, 0xE0, 0xFF, 0x07, 0x00, 0xE0, 0xE0, 0x81, 0x07, 0x00, 0xE0, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0xE0, 0xF0, 0x00, 0x0F, 0x00, 0xE0, 0xE0, 0x81, 0x07, 0x00, 0xE0, 0xE0, 0xFF, 0x07, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0x00, 0xFF, // 246 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0xF0, 0xCC, 0x03, 0x00, 0x00, 0xF0, 0xCC, 0x03, 0x00, 0x00, 0xF0, 0xCC, 0x03, 0x00, 0x00, 0xF0, 0xCC, 0x03, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x0C, // 247 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x7F, 0x0E, 0x00, 0x00, 0xC0, 0xFF, 0x07, 0x00, 0x00, 0xE0, 0xFF, 0x03, 0x00, 0x00, 0xE0, 0xC1, 0x07, 0x00, 0x00, 0xF0, 0xE0, 0x07, 0x00, 0x00, 0x70, 0x70, 0x0E, 0x00, 0x00, 0x70, 0x38, 0x0E, 0x00, 0x00, 0x70, 0x1C, 0x0E, 0x00, 0x00, 0x70, 0x0E, 0x0E, 0x00, 0x00, 0xE0, 0x07, 0x0F, 0x00, 0x00, 0xE0, 0x83, 0x07, 0x00, 0x00, 0xC0, 0xFF, 0x07, 0x00, 0x00, 0xE0, 0xFF, 0x03, 0x00, 0x00, 0x38, 0xFE, 0x00, 0x00, 0x00, 0x10, // 248 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0x01, 0x00, 0x10, 0xF0, 0xFF, 0x07, 0x00, 0x30, 0xF0, 0xFF, 0x07, 0x00, 0x70, 0x00, 0x00, 0x0F, 0x00, 0xE0, 0x00, 0x00, 0x0E, 0x00, 0xC0, 0x03, 0x00, 0x0E, 0x00, 0x00, 0x03, 0x00, 0x0E, 0x00, 0x00, 0x02, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, // 249 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0x01, 0x00, 0x00, 0xF0, 0xFF, 0x07, 0x00, 0x00, 0xF0, 0xFF, 0x07, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x02, 0x00, 0x0E, 0x00, 0x00, 0x03, 0x00, 0x0E, 0x00, 0xC0, 0x03, 0x00, 0x0E, 0x00, 0xE0, 0x00, 0x00, 0x06, 0x00, 0x70, 0x00, 0x00, 0x07, 0x00, 0x30, 0x00, 0x80, 0x01, 0x00, 0x10, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, // 250 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0x01, 0x00, 0x00, 0xF0, 0xFF, 0x07, 0x00, 0x00, 0xF2, 0xFF, 0x07, 0x00, 0x80, 0x03, 0x00, 0x0F, 0x00, 0xC0, 0x01, 0x00, 0x0E, 0x00, 0xF0, 0x00, 0x00, 0x0E, 0x00, 0x30, 0x00, 0x00, 0x0E, 0x00, 0xF0, 0x00, 0x00, 0x06, 0x00, 0xC0, 0x01, 0x00, 0x07, 0x00, 0x80, 0x03, 0x80, 0x01, 0x00, 0x00, 0xF2, 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, // 251 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0x01, 0x00, 0x00, 0xF0, 0xFF, 0x07, 0x00, 0xE0, 0xF0, 0xFF, 0x07, 0x00, 0xE0, 0x00, 0x00, 0x0F, 0x00, 0xE0, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0xE0, 0x00, 0x00, 0x07, 0x00, 0xE0, 0x00, 0x80, 0x01, 0x00, 0xE0, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0x0F, // 252 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x80, 0x03, 0x00, 0xF0, 0x03, 0x80, 0x03, 0x00, 0xE0, 0x1F, 0x80, 0x03, 0x00, 0x80, 0x7F, 0xC0, 0x03, 0x00, 0x02, 0xFC, 0xF3, 0x01, 0x00, 0x03, 0xF0, 0xFF, 0x01, 0xC0, 0x03, 0xC0, 0x7F, 0x00, 0xE0, 0x00, 0xF0, 0x0F, 0x00, 0x70, 0x00, 0xFE, 0x03, 0x00, 0x30, 0x80, 0x7F, 0x00, 0x00, 0x10, 0xE0, 0x1F, 0x00, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x10, // 253 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0xFF, 0x03, 0xE0, 0xFF, 0xFF, 0xFF, 0x03, 0xE0, 0xFF, 0xFF, 0xFF, 0x03, 0x00, 0xC0, 0x81, 0x03, 0x00, 0x00, 0xE0, 0x00, 0x07, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0xE0, 0x81, 0x07, 0x00, 0x00, 0xE0, 0xFF, 0x07, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0x00, 0xFF, // 254 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x80, 0x03, 0x00, 0xF0, 0x03, 0x80, 0x03, 0xE0, 0xE0, 0x1F, 0x80, 0x03, 0xE0, 0x80, 0x7F, 0xC0, 0x03, 0xE0, 0x00, 0xFC, 0xF3, 0x01, 0x00, 0x00, 0xF0, 0xFF, 0x01, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0xE0, 0x00, 0xFE, 0x03, 0x00, 0xE0, 0x80, 0x7F, 0x00, 0x00, 0xE0, 0xE0, 0x1F, 0x00, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x10 // 255 }; #endif // USE_EINK ================================================ FILE: src/graphics/fonts/EinkDisplayFonts.h ================================================ #ifndef EINKDISPLAYFONTS_h #define EINKDISPLAYFONTS_h #ifdef USE_EINK #ifdef ARDUINO #include #elif __MBED__ #define PROGMEM #endif /** * Monospaced Plain 30 */ extern const uint8_t Monospaced_plain_30[] PROGMEM; #endif // USE_EINK #endif ================================================ FILE: src/graphics/fonts/OLEDDisplayFontsCS.cpp ================================================ #ifdef OLED_CS #include "OLEDDisplayFontsCS.h" // Font generated or edited with the glyphEditor const uint8_t ArialMT_Plain_10_CS[] PROGMEM = { 0x0A, // Width: 10 0x0D, // Height: 13 0x20, // First char: 32 0xE0, // Number of chars: 224 // Jump Table: 0xFF, 0xFF, 0x00, 0x03, // 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 0x03, 0x6E, 0x0C, 0x07, // 129 0x03, 0x7A, 0x0B, 0x07, // 130 0x03, 0x85, 0x0C, 0x07, // 131 0x03, 0x91, 0x0C, 0x07, // 132 0x03, 0x9D, 0x0C, 0x07, // 133 0x03, 0xA9, 0x0C, 0x07, // 134 0x03, 0xB5, 0x0B, 0x07, // 135 0x03, 0xC0, 0x0C, 0x07, // 136 0x03, 0xCC, 0x0C, 0x07, // 137 0x03, 0xD8, 0x0A, 0x06, // 138 0x03, 0xE2, 0x0D, 0x08, // 139 0x03, 0xEF, 0x0A, 0x06, // 140 0x03, 0xF9, 0x0A, 0x06, // 141 0x04, 0x03, 0x07, 0x05, // 142 0x04, 0x0A, 0x08, 0x05, // 143 0x04, 0x12, 0x07, 0x05, // 144 0x04, 0x19, 0x0A, 0x06, // 145 0x04, 0x23, 0x0A, 0x06, // 146 0x04, 0x2D, 0x0C, 0x07, // 147 0x04, 0x39, 0x05, 0x04, // 148 0x04, 0x3E, 0x0C, 0x07, // 149 0x04, 0x4A, 0x07, 0x05, // 150 0x04, 0x51, 0x0C, 0x07, // 151 0x04, 0x5D, 0x07, 0x05, // 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 0x04, 0x64, 0x04, 0x03, // 161 0x04, 0x68, 0x0A, 0x06, // 162 0x04, 0x72, 0x0C, 0x07, // 163 0x04, 0x7E, 0x0A, 0x06, // 164 0x04, 0x88, 0x0A, 0x06, // 165 0x04, 0x92, 0x04, 0x03, // 166 0x04, 0x96, 0x0A, 0x06, // 167 0x04, 0xA0, 0x05, 0x04, // 168 0x04, 0xA5, 0x0D, 0x08, // 169 0x04, 0xB2, 0x07, 0x05, // 170 0x04, 0xB9, 0x0A, 0x06, // 171 0x04, 0xC3, 0x09, 0x06, // 172 0x04, 0xCC, 0x03, 0x03, // 173 0x04, 0xCF, 0x0D, 0x08, // 174 0x04, 0xDC, 0x0B, 0x07, // 175 0x04, 0xE7, 0x07, 0x05, // 176 0x04, 0xEE, 0x0A, 0x06, // 177 0x04, 0xF8, 0x05, 0x04, // 178 0x04, 0xFD, 0x05, 0x04, // 179 0x05, 0x02, 0x05, 0x04, // 180 0x05, 0x07, 0x0A, 0x06, // 181 0x05, 0x11, 0x09, 0x06, // 182 0x05, 0x1A, 0x03, 0x03, // 183 0x05, 0x1D, 0x06, 0x04, // 184 0x05, 0x23, 0x05, 0x04, // 185 0x05, 0x28, 0x07, 0x05, // 186 0x05, 0x2F, 0x0A, 0x06, // 187 0x05, 0x39, 0x10, 0x09, // 188 0x05, 0x49, 0x10, 0x09, // 189 0x05, 0x59, 0x10, 0x09, // 190 0x05, 0x69, 0x0A, 0x06, // 191 0x05, 0x73, 0x0E, 0x08, // 192 0x05, 0x81, 0x0E, 0x08, // 193 0x05, 0x8F, 0x0E, 0x08, // 194 0x05, 0x9D, 0x0E, 0x08, // 195 0x05, 0xAB, 0x0E, 0x08, // 196 0x05, 0xB9, 0x0E, 0x08, // 197 0x05, 0xC7, 0x12, 0x0A, // 198 0x05, 0xD9, 0x0C, 0x07, // 199 0x05, 0xE5, 0x0C, 0x07, // 200 0x05, 0xF1, 0x0C, 0x07, // 201 0x05, 0xFD, 0x0C, 0x07, // 202 0x06, 0x09, 0x0C, 0x07, // 203 0x06, 0x15, 0x05, 0x04, // 204 0x06, 0x1A, 0x04, 0x03, // 205 0x06, 0x1E, 0x04, 0x03, // 206 0x06, 0x22, 0x05, 0x04, // 207 0x06, 0x27, 0x0B, 0x07, // 208 0x06, 0x32, 0x0C, 0x07, // 209 0x06, 0x3E, 0x0E, 0x08, // 210 0x06, 0x4C, 0x0E, 0x08, // 211 0x06, 0x5A, 0x0E, 0x08, // 212 0x06, 0x68, 0x0E, 0x08, // 213 0x06, 0x76, 0x0E, 0x08, // 214 0x06, 0x84, 0x0A, 0x06, // 215 0x06, 0x8E, 0x0D, 0x08, // 216 0x06, 0x9B, 0x0C, 0x07, // 217 0x06, 0xA7, 0x0C, 0x07, // 218 0x06, 0xB3, 0x0C, 0x07, // 219 0x06, 0xBF, 0x0C, 0x07, // 220 0x06, 0xCB, 0x0D, 0x08, // 221 0x06, 0xD8, 0x0B, 0x07, // 222 0x06, 0xE3, 0x0C, 0x07, // 223 0x06, 0xEF, 0x0A, 0x06, // 224 0x06, 0xF9, 0x0A, 0x06, // 225 0x07, 0x03, 0x0A, 0x06, // 226 0x07, 0x0D, 0x0A, 0x06, // 227 0x07, 0x17, 0x0A, 0x06, // 228 0x07, 0x21, 0x0A, 0x06, // 229 0x07, 0x2B, 0x10, 0x09, // 230 0x07, 0x3B, 0x0A, 0x06, // 231 0x07, 0x45, 0x0A, 0x06, // 232 0x07, 0x4F, 0x0A, 0x06, // 233 0x07, 0x59, 0x0A, 0x06, // 234 0x07, 0x63, 0x0A, 0x06, // 235 0x07, 0x6D, 0x05, 0x04, // 236 0x07, 0x72, 0x04, 0x03, // 237 0x07, 0x76, 0x05, 0x04, // 238 0x07, 0x7B, 0x05, 0x04, // 239 0x07, 0x80, 0x0A, 0x06, // 240 0x07, 0x8A, 0x0A, 0x06, // 241 0x07, 0x94, 0x0A, 0x06, // 242 0x07, 0x9E, 0x0A, 0x06, // 243 0x07, 0xA8, 0x0A, 0x06, // 244 0x07, 0xB2, 0x0A, 0x06, // 245 0x07, 0xBC, 0x0A, 0x06, // 246 0x07, 0xC6, 0x09, 0x06, // 247 0x07, 0xCF, 0x0A, 0x06, // 248 0x07, 0xD9, 0x0A, 0x06, // 249 0x07, 0xE3, 0x0A, 0x06, // 250 0x07, 0xED, 0x0A, 0x06, // 251 0x07, 0xF7, 0x0A, 0x06, // 252 0x08, 0x01, 0x09, 0x06, // 253 0x08, 0x0A, 0x0A, 0x06, // 254 0x08, 0x14, 0x09, 0x06, // 255 // Font Data: 0x00, 0x00, 0xF8, 0x02, // 33 0x38, 0x00, 0x00, 0x00, 0x38, // 34 0xA0, 0x03, 0xE0, 0x00, 0xB8, 0x03, 0xE0, 0x00, 0xB8, // 35 0x30, 0x01, 0x28, 0x02, 0xF8, 0x07, 0x48, 0x02, 0x90, 0x01, // 36 0x00, 0x00, 0x30, 0x00, 0x48, 0x00, 0x30, 0x03, 0xC0, 0x00, 0xB0, 0x01, 0x48, 0x02, 0x80, 0x01, // 37 0x80, 0x01, 0x50, 0x02, 0x68, 0x02, 0xA8, 0x02, 0x18, 0x01, 0x80, 0x03, 0x80, 0x02, // 38 0x38, // 39 0xE0, 0x03, 0x10, 0x04, 0x08, 0x08, // 40 0x08, 0x08, 0x10, 0x04, 0xE0, 0x03, // 41 0x28, 0x00, 0x18, 0x00, 0x28, // 42 0x40, 0x00, 0x40, 0x00, 0xF0, 0x01, 0x40, 0x00, 0x40, // 43 0x00, 0x00, 0x00, 0x06, // 44 0x80, 0x00, 0x80, // 45 0x00, 0x00, 0x00, 0x02, // 46 0x00, 0x03, 0xE0, 0x00, 0x18, // 47 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0xF0, 0x01, // 48 0x00, 0x00, 0x20, 0x00, 0x10, 0x00, 0xF8, 0x03, // 49 0x10, 0x02, 0x08, 0x03, 0x88, 0x02, 0x48, 0x02, 0x30, 0x02, // 50 0x10, 0x01, 0x08, 0x02, 0x48, 0x02, 0x48, 0x02, 0xB0, 0x01, // 51 0xC0, 0x00, 0xA0, 0x00, 0x90, 0x00, 0x88, 0x00, 0xF8, 0x03, 0x80, // 52 0x60, 0x01, 0x38, 0x02, 0x28, 0x02, 0x28, 0x02, 0xC8, 0x01, // 53 0xF0, 0x01, 0x28, 0x02, 0x28, 0x02, 0x28, 0x02, 0xD0, 0x01, // 54 0x08, 0x00, 0x08, 0x03, 0xC8, 0x00, 0x38, 0x00, 0x08, // 55 0xB0, 0x01, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0xB0, 0x01, // 56 0x70, 0x01, 0x88, 0x02, 0x88, 0x02, 0x88, 0x02, 0xF0, 0x01, // 57 0x00, 0x00, 0x20, 0x02, // 58 0x00, 0x00, 0x20, 0x06, // 59 0x00, 0x00, 0x40, 0x00, 0xA0, 0x00, 0xA0, 0x00, 0x10, 0x01, // 60 0xA0, 0x00, 0xA0, 0x00, 0xA0, 0x00, 0xA0, 0x00, 0xA0, // 61 0x00, 0x00, 0x10, 0x01, 0xA0, 0x00, 0xA0, 0x00, 0x40, // 62 0x10, 0x00, 0x08, 0x00, 0x08, 0x00, 0xC8, 0x02, 0x48, 0x00, 0x30, // 63 0x00, 0x00, 0xC0, 0x03, 0x30, 0x04, 0xD0, 0x09, 0x28, 0x0A, 0x28, 0x0A, 0xC8, 0x0B, 0x68, 0x0A, 0x10, 0x05, 0xE0, 0x04, // 64 0x00, 0x02, 0xC0, 0x01, 0xB0, 0x00, 0x88, 0x00, 0xB0, 0x00, 0xC0, 0x01, 0x00, 0x02, // 65 0x00, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0xF0, 0x01, // 66 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0x10, 0x01, // 67 0x00, 0x00, 0xF8, 0x03, 0x08, 0x02, 0x08, 0x02, 0x10, 0x01, 0xE0, // 68 0x00, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, // 69 0x00, 0x00, 0xF8, 0x03, 0x48, 0x00, 0x48, 0x00, 0x08, // 70 0x00, 0x00, 0xE0, 0x00, 0x10, 0x01, 0x08, 0x02, 0x48, 0x02, 0x50, 0x01, 0xC0, // 71 0x00, 0x00, 0xF8, 0x03, 0x40, 0x00, 0x40, 0x00, 0x40, 0x00, 0xF8, 0x03, // 72 0x00, 0x00, 0xF8, 0x03, // 73 0x00, 0x03, 0x00, 0x02, 0x00, 0x02, 0xF8, 0x01, // 74 0x00, 0x00, 0xF8, 0x03, 0x80, 0x00, 0x60, 0x00, 0x90, 0x00, 0x08, 0x01, 0x00, 0x02, // 75 0x00, 0x00, 0xF8, 0x03, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, // 76 0x00, 0x00, 0xF8, 0x03, 0x30, 0x00, 0xC0, 0x01, 0x00, 0x02, 0xC0, 0x01, 0x30, 0x00, 0xF8, 0x03, // 77 0x00, 0x00, 0xF8, 0x03, 0x30, 0x00, 0x40, 0x00, 0x80, 0x01, 0xF8, 0x03, // 78 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0xF0, 0x01, // 79 0x00, 0x00, 0xF8, 0x03, 0x48, 0x00, 0x48, 0x00, 0x48, 0x00, 0x30, // 80 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x03, 0x08, 0x03, 0xF0, 0x02, // 81 0x00, 0x00, 0xF8, 0x03, 0x48, 0x00, 0x48, 0x00, 0xC8, 0x00, 0x30, 0x03, // 82 0x00, 0x00, 0x30, 0x01, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0x90, 0x01, // 83 0x00, 0x00, 0x08, 0x00, 0x08, 0x00, 0xF8, 0x03, 0x08, 0x00, 0x08, // 84 0x00, 0x00, 0xF8, 0x01, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0xF8, 0x01, // 85 0x08, 0x00, 0x70, 0x00, 0x80, 0x01, 0x00, 0x02, 0x80, 0x01, 0x70, 0x00, 0x08, // 86 0x18, 0x00, 0xE0, 0x01, 0x00, 0x02, 0xF0, 0x01, 0x08, 0x00, 0xF0, 0x01, 0x00, 0x02, 0xE0, 0x01, 0x18, // 87 0x00, 0x02, 0x08, 0x01, 0x90, 0x00, 0x60, 0x00, 0x90, 0x00, 0x08, 0x01, 0x00, 0x02, // 88 0x08, 0x00, 0x10, 0x00, 0x20, 0x00, 0xC0, 0x03, 0x20, 0x00, 0x10, 0x00, 0x08, // 89 0x08, 0x03, 0x88, 0x02, 0xC8, 0x02, 0x68, 0x02, 0x38, 0x02, 0x18, 0x02, // 90 0x00, 0x00, 0xF8, 0x0F, 0x08, 0x08, // 91 0x18, 0x00, 0xE0, 0x00, 0x00, 0x03, // 92 0x08, 0x08, 0xF8, 0x0F, // 93 0x40, 0x00, 0x30, 0x00, 0x08, 0x00, 0x30, 0x00, 0x40, // 94 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, // 95 0x08, 0x00, 0x10, // 96 0x00, 0x00, 0x00, 0x03, 0xA0, 0x02, 0xA0, 0x02, 0xE0, 0x03, // 97 0x00, 0x00, 0xF8, 0x03, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // 98 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0x40, 0x01, // 99 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0xF8, 0x03, // 100 0x00, 0x00, 0xC0, 0x01, 0xA0, 0x02, 0xA0, 0x02, 0xC0, 0x02, // 101 0x20, 0x00, 0xF0, 0x03, 0x28, // 102 0x00, 0x00, 0xC0, 0x05, 0x20, 0x0A, 0x20, 0x0A, 0xE0, 0x07, // 103 0x00, 0x00, 0xF8, 0x03, 0x20, 0x00, 0x20, 0x00, 0xC0, 0x03, // 104 0x00, 0x00, 0xE8, 0x03, // 105 0x00, 0x08, 0xE8, 0x07, // 106 0xF8, 0x03, 0x80, 0x00, 0xC0, 0x01, 0x20, 0x02, // 107 0x00, 0x00, 0xF8, 0x03, // 108 0x00, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0xC0, 0x03, // 109 0x00, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0xC0, 0x03, // 110 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // 111 0x00, 0x00, 0xE0, 0x0F, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // 112 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0xE0, 0x0F, // 113 0x00, 0x00, 0xE0, 0x03, 0x20, // 114 0x40, 0x02, 0xA0, 0x02, 0xA0, 0x02, 0x20, 0x01, // 115 0x20, 0x00, 0xF8, 0x03, 0x20, 0x02, // 116 0x00, 0x00, 0xE0, 0x01, 0x00, 0x02, 0x00, 0x02, 0xE0, 0x03, // 117 0x20, 0x00, 0xC0, 0x01, 0x00, 0x02, 0xC0, 0x01, 0x20, // 118 0xE0, 0x01, 0x00, 0x02, 0xC0, 0x01, 0x20, 0x00, 0xC0, 0x01, 0x00, 0x02, 0xE0, 0x01, // 119 0x20, 0x02, 0x40, 0x01, 0x80, 0x00, 0x40, 0x01, 0x20, 0x02, // 120 0x20, 0x00, 0xC0, 0x09, 0x00, 0x06, 0xC0, 0x01, 0x20, // 121 0x20, 0x02, 0x20, 0x03, 0xA0, 0x02, 0x60, 0x02, 0x20, 0x02, // 122 0x80, 0x00, 0x78, 0x0F, 0x08, 0x08, // 123 0x00, 0x00, 0xF8, 0x0F, // 124 0x08, 0x08, 0x78, 0x0F, 0x80, // 125 0xC0, 0x00, 0x40, 0x00, 0xC0, 0x00, 0x80, 0x00, 0xC0, // 126 0x00, 0x00, 0xF0, 0x01, 0x09, 0x02, 0x0A, 0x02, 0x09, 0x02, 0x10, 0x01, // 129 0x00, 0x00, 0xF8, 0x03, 0x09, 0x02, 0x0A, 0x02, 0x11, 0x01, 0xE0, // 130 0x00, 0x00, 0xF8, 0x03, 0x49, 0x02, 0x4A, 0x02, 0x49, 0x02, 0x48, 0x02, // 131 0x00, 0x00, 0xF8, 0x03, 0x31, 0x00, 0x42, 0x00, 0x81, 0x01, 0xF8, 0x03, // 132 0x00, 0x00, 0xF8, 0x03, 0x49, 0x00, 0x4A, 0x00, 0xC9, 0x00, 0x30, 0x03, // 133 0x00, 0x00, 0x30, 0x01, 0x49, 0x02, 0x4A, 0x02, 0x49, 0x02, 0x90, 0x01, // 134 0x00, 0x00, 0x08, 0x00, 0x09, 0x00, 0xFA, 0x03, 0x09, 0x00, 0x08, // 135 0x00, 0x00, 0xF8, 0x01, 0x02, 0x02, 0x05, 0x02, 0x02, 0x02, 0xF8, 0x01, // 136 0x08, 0x03, 0x88, 0x02, 0xC9, 0x02, 0x6A, 0x02, 0x39, 0x02, 0x18, 0x02, // 137 0x00, 0x00, 0xC0, 0x01, 0x24, 0x02, 0x28, 0x02, 0x44, 0x01, // 138 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0xF8, 0x03, 0x00, 0x00, 0x18, // 139 0x00, 0x00, 0xC0, 0x01, 0xA4, 0x02, 0xA8, 0x02, 0xC4, 0x02, // 140 0x00, 0x00, 0xE0, 0x03, 0x24, 0x00, 0x28, 0x00, 0xC4, 0x03, // 141 0x00, 0x00, 0xE4, 0x03, 0x28, 0x00, 0x04, // 142 0x40, 0x02, 0xA4, 0x02, 0xA8, 0x02, 0x24, 0x01, // 143 0x20, 0x00, 0xF8, 0x03, 0x20, 0x02, 0x18, // 144 0x00, 0x00, 0xE0, 0x01, 0x08, 0x02, 0x14, 0x02, 0xE8, 0x03, // 145 0x20, 0x02, 0x24, 0x03, 0xA8, 0x02, 0x64, 0x02, 0x20, 0x02, // 146 0x00, 0x00, 0xFA, 0x03, 0x01, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, // 147 0x00, 0x00, 0xFA, 0x03, 0x01, // 148 0x00, 0x00, 0xF8, 0x03, 0x00, 0x02, 0x00, 0x02, 0x18, 0x02, 0x00, 0x02, // 149 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x18, // 150 0x00, 0x00, 0xF8, 0x03, 0x48, 0x00, 0x4A, 0x00, 0xC9, 0x00, 0x30, 0x03, // 151 0x00, 0x00, 0xE0, 0x03, 0x28, 0x00, 0x04, // 152 0x00, 0x00, 0xA0, 0x0F, // 161 0x00, 0x00, 0xC0, 0x01, 0xA0, 0x0F, 0x78, 0x02, 0x40, 0x01, // 162 0x40, 0x02, 0x70, 0x03, 0xC8, 0x02, 0x48, 0x02, 0x08, 0x02, 0x10, 0x02, // 163 0x00, 0x00, 0xE0, 0x01, 0x20, 0x01, 0x20, 0x01, 0xE0, 0x01, // 164 0x48, 0x01, 0x70, 0x01, 0xC0, 0x03, 0x70, 0x01, 0x48, 0x01, // 165 0x00, 0x00, 0x38, 0x0F, // 166 0xD0, 0x04, 0x28, 0x09, 0x48, 0x09, 0x48, 0x0A, 0x90, 0x05, // 167 0x08, 0x00, 0x00, 0x00, 0x08, // 168 0xE0, 0x00, 0x10, 0x01, 0x48, 0x02, 0xA8, 0x02, 0xA8, 0x02, 0x10, 0x01, 0xE0, // 169 0x68, 0x00, 0x68, 0x00, 0x68, 0x00, 0x78, // 170 0x00, 0x00, 0x80, 0x01, 0x40, 0x02, 0x80, 0x01, 0x40, 0x02, // 171 0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0xE0, // 172 0x80, 0x00, 0x80, // 173 0xE0, 0x00, 0x10, 0x01, 0xE8, 0x02, 0x68, 0x02, 0xC8, 0x02, 0x10, 0x01, 0xE0, // 174 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, // 175 0x00, 0x00, 0x38, 0x00, 0x28, 0x00, 0x38, // 176 0x40, 0x02, 0x40, 0x02, 0xF0, 0x03, 0x40, 0x02, 0x40, 0x02, // 177 0x48, 0x00, 0x68, 0x00, 0x58, // 178 0x48, 0x00, 0x58, 0x00, 0x68, // 179 0x00, 0x00, 0x10, 0x00, 0x08, // 180 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x02, 0x00, 0x02, 0xE0, 0x03, // 181 0x70, 0x00, 0xF8, 0x0F, 0x08, 0x00, 0xF8, 0x0F, 0x08, // 182 0x00, 0x00, 0x40, // 183 0x00, 0x00, 0x00, 0x14, 0x00, 0x18, // 184 0x00, 0x00, 0x10, 0x00, 0x78, // 185 0x30, 0x00, 0x48, 0x00, 0x48, 0x00, 0x30, // 186 0x00, 0x00, 0x40, 0x02, 0x80, 0x01, 0x40, 0x02, 0x80, 0x01, // 187 0x00, 0x00, 0x10, 0x02, 0x78, 0x01, 0xC0, 0x00, 0x20, 0x01, 0x90, 0x01, 0xC8, 0x03, 0x00, 0x01, // 188 0x00, 0x00, 0x10, 0x02, 0x78, 0x01, 0x80, 0x00, 0x60, 0x00, 0x50, 0x02, 0x48, 0x03, 0xC0, 0x02, // 189 0x48, 0x00, 0x58, 0x00, 0x68, 0x03, 0x80, 0x00, 0x60, 0x01, 0x90, 0x01, 0xC8, 0x03, 0x00, 0x01, // 190 0x00, 0x00, 0x00, 0x06, 0x00, 0x09, 0xA0, 0x09, 0x00, 0x04, // 191 0x00, 0x02, 0xC0, 0x01, 0xB0, 0x00, 0x89, 0x00, 0xB2, 0x00, 0xC0, 0x01, 0x00, 0x02, // 192 0x00, 0x02, 0xC0, 0x01, 0xB0, 0x00, 0x8A, 0x00, 0xB1, 0x00, 0xC0, 0x01, 0x00, 0x02, // 193 0x00, 0x02, 0xC0, 0x01, 0xB2, 0x00, 0x89, 0x00, 0xB2, 0x00, 0xC0, 0x01, 0x00, 0x02, // 194 0x00, 0x02, 0xC2, 0x01, 0xB1, 0x00, 0x8A, 0x00, 0xB1, 0x00, 0xC0, 0x01, 0x00, 0x02, // 195 0x00, 0x02, 0xC0, 0x01, 0xB2, 0x00, 0x88, 0x00, 0xB2, 0x00, 0xC0, 0x01, 0x00, 0x02, // 196 0x00, 0x02, 0xC0, 0x01, 0xBE, 0x00, 0x8A, 0x00, 0xBE, 0x00, 0xC0, 0x01, 0x00, 0x02, // 197 0x00, 0x03, 0xC0, 0x00, 0xE0, 0x00, 0x98, 0x00, 0x88, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, // 198 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x16, 0x08, 0x1A, 0x10, 0x01, // 199 0x00, 0x00, 0xF8, 0x03, 0x49, 0x02, 0x4A, 0x02, 0x48, 0x02, 0x48, 0x02, // 200 0x00, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x4A, 0x02, 0x49, 0x02, 0x48, 0x02, // 201 0x00, 0x00, 0xFA, 0x03, 0x49, 0x02, 0x4A, 0x02, 0x48, 0x02, 0x48, 0x02, // 202 0x00, 0x00, 0xF8, 0x03, 0x4A, 0x02, 0x48, 0x02, 0x4A, 0x02, 0x48, 0x02, // 203 0x00, 0x00, 0xF9, 0x03, 0x02, // 204 0x02, 0x00, 0xF9, 0x03, // 205 0x01, 0x00, 0xFA, 0x03, // 206 0x02, 0x00, 0xF8, 0x03, 0x02, // 207 0x40, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x10, 0x01, 0xE0, // 208 0x00, 0x00, 0xFA, 0x03, 0x31, 0x00, 0x42, 0x00, 0x81, 0x01, 0xF8, 0x03, // 209 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x09, 0x02, 0x0A, 0x02, 0x08, 0x02, 0xF0, 0x01, // 210 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x0A, 0x02, 0x09, 0x02, 0x08, 0x02, 0xF0, 0x01, // 211 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x0A, 0x02, 0x09, 0x02, 0x0A, 0x02, 0xF0, 0x01, // 212 0x00, 0x00, 0xF0, 0x01, 0x0A, 0x02, 0x09, 0x02, 0x0A, 0x02, 0x09, 0x02, 0xF0, 0x01, // 213 0x00, 0x00, 0xF0, 0x01, 0x0A, 0x02, 0x08, 0x02, 0x0A, 0x02, 0x08, 0x02, 0xF0, 0x01, // 214 0x10, 0x01, 0xA0, 0x00, 0xE0, 0x00, 0xA0, 0x00, 0x10, 0x01, // 215 0x00, 0x00, 0xF0, 0x02, 0x08, 0x03, 0xC8, 0x02, 0x28, 0x02, 0x18, 0x03, 0xE8, // 216 0x00, 0x00, 0xF8, 0x01, 0x01, 0x02, 0x02, 0x02, 0x00, 0x02, 0xF8, 0x01, // 217 0x00, 0x00, 0xF8, 0x01, 0x02, 0x02, 0x01, 0x02, 0x00, 0x02, 0xF8, 0x01, // 218 0x00, 0x00, 0xF8, 0x01, 0x02, 0x02, 0x01, 0x02, 0x02, 0x02, 0xF8, 0x01, // 219 0x00, 0x00, 0xF8, 0x01, 0x02, 0x02, 0x00, 0x02, 0x02, 0x02, 0xF8, 0x01, // 220 0x08, 0x00, 0x10, 0x00, 0x20, 0x00, 0xC2, 0x03, 0x21, 0x00, 0x10, 0x00, 0x08, // 221 0x00, 0x00, 0xF8, 0x03, 0x10, 0x01, 0x10, 0x01, 0x10, 0x01, 0xE0, // 222 0x00, 0x00, 0xF0, 0x03, 0x08, 0x01, 0x48, 0x02, 0xB0, 0x02, 0x80, 0x01, // 223 0x00, 0x00, 0x00, 0x03, 0xA4, 0x02, 0xA8, 0x02, 0xE0, 0x03, // 224 0x00, 0x00, 0x00, 0x03, 0xA8, 0x02, 0xA4, 0x02, 0xE0, 0x03, // 225 0x00, 0x00, 0x00, 0x03, 0xA8, 0x02, 0xA4, 0x02, 0xE8, 0x03, // 226 0x00, 0x00, 0x08, 0x03, 0xA4, 0x02, 0xA8, 0x02, 0xE4, 0x03, // 227 0x00, 0x00, 0x00, 0x03, 0xA8, 0x02, 0xA0, 0x02, 0xE8, 0x03, // 228 0x00, 0x00, 0x00, 0x03, 0xAE, 0x02, 0xAA, 0x02, 0xEE, 0x03, // 229 0x00, 0x00, 0x40, 0x03, 0xA0, 0x02, 0xA0, 0x02, 0xC0, 0x01, 0xA0, 0x02, 0xA0, 0x02, 0xC0, 0x02, // 230 0x00, 0x00, 0xC0, 0x01, 0x20, 0x16, 0x20, 0x1A, 0x40, 0x01, // 231 0x00, 0x00, 0xC0, 0x01, 0xA4, 0x02, 0xA8, 0x02, 0xC0, 0x02, // 232 0x00, 0x00, 0xC0, 0x01, 0xA8, 0x02, 0xA4, 0x02, 0xC0, 0x02, // 233 0x00, 0x00, 0xC0, 0x01, 0xA8, 0x02, 0xA4, 0x02, 0xC8, 0x02, // 234 0x00, 0x00, 0xC0, 0x01, 0xA8, 0x02, 0xA0, 0x02, 0xC8, 0x02, // 235 0x00, 0x00, 0xE4, 0x03, 0x08, // 236 0x08, 0x00, 0xE4, 0x03, // 237 0x08, 0x00, 0xE4, 0x03, 0x08, // 238 0x08, 0x00, 0xE0, 0x03, 0x08, // 239 0x00, 0x00, 0xC0, 0x01, 0x28, 0x02, 0x38, 0x02, 0xE0, 0x01, // 240 0x00, 0x00, 0xE8, 0x03, 0x24, 0x00, 0x28, 0x00, 0xC4, 0x03, // 241 0x00, 0x00, 0xC0, 0x01, 0x24, 0x02, 0x28, 0x02, 0xC0, 0x01, // 242 0x00, 0x00, 0xC0, 0x01, 0x28, 0x02, 0x24, 0x02, 0xC0, 0x01, // 243 0x00, 0x00, 0xC0, 0x01, 0x28, 0x02, 0x24, 0x02, 0xC8, 0x01, // 244 0x00, 0x00, 0xC8, 0x01, 0x24, 0x02, 0x28, 0x02, 0xC4, 0x01, // 245 0x00, 0x00, 0xC0, 0x01, 0x28, 0x02, 0x20, 0x02, 0xC8, 0x01, // 246 0x40, 0x00, 0x40, 0x00, 0x50, 0x01, 0x40, 0x00, 0x40, // 247 0x00, 0x00, 0xC0, 0x02, 0xA0, 0x03, 0x60, 0x02, 0xA0, 0x01, // 248 0x00, 0x00, 0xE0, 0x01, 0x04, 0x02, 0x08, 0x02, 0xE0, 0x03, // 249 0x00, 0x00, 0xE0, 0x01, 0x08, 0x02, 0x04, 0x02, 0xE0, 0x03, // 250 0x00, 0x00, 0xE8, 0x01, 0x04, 0x02, 0x08, 0x02, 0xE0, 0x03, // 251 0x00, 0x00, 0xE0, 0x01, 0x08, 0x02, 0x00, 0x02, 0xE8, 0x03, // 252 0x20, 0x00, 0xC0, 0x09, 0x08, 0x06, 0xC4, 0x01, 0x20, // 253 0x00, 0x00, 0xF8, 0x0F, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // 254 0x20, 0x00, 0xC8, 0x09, 0x00, 0x06, 0xC8, 0x01, 0x20, // 255 }; const uint8_t ArialMT_Plain_16_CS[] PROGMEM = { 0x10, // Width: 16 0x13, // Height: 19 0x20, // First char: 32 0xE0, // Number of chars: 224 // Jump Table: 0xFF, 0xFF, 0x00, 0x04, // 32 0x00, 0x00, 0x08, 0x04, // 33 0x00, 0x08, 0x0D, 0x06, // 34 0x00, 0x15, 0x1A, 0x0A, // 35 0x00, 0x2F, 0x17, 0x09, // 36 0x00, 0x46, 0x26, 0x0E, // 37 0x00, 0x6C, 0x1D, 0x0B, // 38 0x00, 0x89, 0x04, 0x03, // 39 0x00, 0x8D, 0x0C, 0x05, // 40 0x00, 0x99, 0x0B, 0x05, // 41 0x00, 0xA4, 0x0D, 0x06, // 42 0x00, 0xB1, 0x17, 0x09, // 43 0x00, 0xC8, 0x09, 0x04, // 44 0x00, 0xD1, 0x0B, 0x05, // 45 0x00, 0xDC, 0x08, 0x04, // 46 0x00, 0xE4, 0x0A, 0x05, // 47 0x00, 0xEE, 0x17, 0x09, // 48 0x01, 0x05, 0x11, 0x07, // 49 0x01, 0x16, 0x17, 0x09, // 50 0x01, 0x2D, 0x17, 0x09, // 51 0x01, 0x44, 0x17, 0x09, // 52 0x01, 0x5B, 0x17, 0x09, // 53 0x01, 0x72, 0x17, 0x09, // 54 0x01, 0x89, 0x16, 0x09, // 55 0x01, 0x9F, 0x17, 0x09, // 56 0x01, 0xB6, 0x17, 0x09, // 57 0x01, 0xCD, 0x05, 0x03, // 58 0x01, 0xD2, 0x06, 0x03, // 59 0x01, 0xD8, 0x17, 0x09, // 60 0x01, 0xEF, 0x17, 0x09, // 61 0x02, 0x06, 0x17, 0x09, // 62 0x02, 0x1D, 0x16, 0x09, // 63 0x02, 0x33, 0x2F, 0x11, // 64 0x02, 0x62, 0x1D, 0x0B, // 65 0x02, 0x7F, 0x1D, 0x0B, // 66 0x02, 0x9C, 0x20, 0x0C, // 67 0x02, 0xBC, 0x20, 0x0C, // 68 0x02, 0xDC, 0x1D, 0x0B, // 69 0x02, 0xF9, 0x19, 0x0A, // 70 0x03, 0x12, 0x20, 0x0C, // 71 0x03, 0x32, 0x1D, 0x0B, // 72 0x03, 0x4F, 0x05, 0x03, // 73 0x03, 0x54, 0x14, 0x08, // 74 0x03, 0x68, 0x1D, 0x0B, // 75 0x03, 0x85, 0x17, 0x09, // 76 0x03, 0x9C, 0x23, 0x0D, // 77 0x03, 0xBF, 0x1D, 0x0B, // 78 0x03, 0xDC, 0x20, 0x0C, // 79 0x03, 0xFC, 0x1C, 0x0B, // 80 0x04, 0x18, 0x20, 0x0C, // 81 0x04, 0x38, 0x1D, 0x0B, // 82 0x04, 0x55, 0x1D, 0x0B, // 83 0x04, 0x72, 0x19, 0x0A, // 84 0x04, 0x8B, 0x1D, 0x0B, // 85 0x04, 0xA8, 0x1C, 0x0B, // 86 0x04, 0xC4, 0x2B, 0x10, // 87 0x04, 0xEF, 0x20, 0x0C, // 88 0x05, 0x0F, 0x19, 0x0A, // 89 0x05, 0x28, 0x1A, 0x0A, // 90 0x05, 0x42, 0x0C, 0x05, // 91 0x05, 0x4E, 0x0B, 0x05, // 92 0x05, 0x59, 0x09, 0x04, // 93 0x05, 0x62, 0x14, 0x08, // 94 0x05, 0x76, 0x1B, 0x0A, // 95 0x05, 0x91, 0x07, 0x04, // 96 0x05, 0x98, 0x17, 0x09, // 97 0x05, 0xAF, 0x17, 0x09, // 98 0x05, 0xC6, 0x14, 0x08, // 99 0x05, 0xDA, 0x17, 0x09, // 100 0x05, 0xF1, 0x17, 0x09, // 101 0x06, 0x08, 0x0A, 0x05, // 102 0x06, 0x12, 0x17, 0x09, // 103 0x06, 0x29, 0x14, 0x08, // 104 0x06, 0x3D, 0x05, 0x03, // 105 0x06, 0x42, 0x06, 0x03, // 106 0x06, 0x48, 0x17, 0x09, // 107 0x06, 0x5F, 0x05, 0x03, // 108 0x06, 0x64, 0x23, 0x0D, // 109 0x06, 0x87, 0x14, 0x08, // 110 0x06, 0x9B, 0x17, 0x09, // 111 0x06, 0xB2, 0x17, 0x09, // 112 0x06, 0xC9, 0x18, 0x09, // 113 0x06, 0xE1, 0x0D, 0x06, // 114 0x06, 0xEE, 0x14, 0x08, // 115 0x07, 0x02, 0x0B, 0x05, // 116 0x07, 0x0D, 0x14, 0x08, // 117 0x07, 0x21, 0x13, 0x08, // 118 0x07, 0x34, 0x1F, 0x0C, // 119 0x07, 0x53, 0x14, 0x08, // 120 0x07, 0x67, 0x13, 0x08, // 121 0x07, 0x7A, 0x14, 0x08, // 122 0x07, 0x8E, 0x0F, 0x06, // 123 0x07, 0x9D, 0x06, 0x03, // 124 0x07, 0xA3, 0x0E, 0x06, // 125 0x07, 0xB1, 0x17, 0x09, // 126 0xFF, 0xFF, 0x00, 0x10, // 127 0xFF, 0xFF, 0x00, 0x10, // 128 0x07, 0xC8, 0x20, 0x0C, // 129 0x07, 0xE8, 0x20, 0x0C, // 130 0x08, 0x08, 0x1D, 0x0B, // 131 0x08, 0x25, 0x1D, 0x0B, // 132 0x08, 0x42, 0x1D, 0x0B, // 133 0x08, 0x5F, 0x1D, 0x0B, // 134 0x08, 0x7C, 0x19, 0x0A, // 135 0x08, 0x95, 0x1D, 0x0B, // 136 0x08, 0xB2, 0x1A, 0x0A, // 137 0x08, 0xCC, 0x14, 0x08, // 138 0x08, 0xE0, 0x1C, 0x0B, // 139 0x08, 0xFC, 0x17, 0x09, // 140 0x09, 0x13, 0x14, 0x08, // 141 0x09, 0x27, 0x0D, 0x06, // 142 0x09, 0x34, 0x14, 0x08, // 143 0x09, 0x48, 0x10, 0x07, // 144 0x09, 0x58, 0x14, 0x08, // 145 0x09, 0x6C, 0x14, 0x08, // 146 0x09, 0x80, 0x17, 0x09, // 147 0x09, 0x97, 0x07, 0x04, // 148 0x09, 0x9E, 0x17, 0x09, // 149 0x09, 0xB5, 0x0A, 0x05, // 150 0x09, 0xBF, 0x1D, 0x0B, // 151 0x09, 0xDC, 0x0D, 0x06, // 152 0xFF, 0xFF, 0x00, 0x10, // 153 0xFF, 0xFF, 0x00, 0x10, // 154 0xFF, 0xFF, 0x00, 0x10, // 155 0xFF, 0xFF, 0x00, 0x10, // 156 0xFF, 0xFF, 0x00, 0x10, // 157 0xFF, 0xFF, 0x00, 0x10, // 158 0xFF, 0xFF, 0x00, 0x10, // 159 0xFF, 0xFF, 0x00, 0x10, // 160 0x09, 0xE9, 0x09, 0x04, // 161 0x09, 0xF2, 0x17, 0x09, // 162 0x0A, 0x09, 0x17, 0x09, // 163 0x0A, 0x20, 0x14, 0x08, // 164 0x0A, 0x34, 0x1A, 0x0A, // 165 0x0A, 0x4E, 0x06, 0x03, // 166 0x0A, 0x54, 0x17, 0x09, // 167 0x0A, 0x6B, 0x07, 0x04, // 168 0x0A, 0x72, 0x23, 0x0D, // 169 0x0A, 0x95, 0x0E, 0x06, // 170 0x0A, 0xA3, 0x14, 0x08, // 171 0x0A, 0xB7, 0x17, 0x09, // 172 0x0A, 0xCE, 0x0B, 0x05, // 173 0x0A, 0xD9, 0x23, 0x0D, // 174 0x0A, 0xFC, 0x19, 0x0A, // 175 0x0B, 0x15, 0x0D, 0x06, // 176 0x0B, 0x22, 0x17, 0x09, // 177 0x0B, 0x39, 0x0E, 0x06, // 178 0x0B, 0x47, 0x0D, 0x06, // 179 0x0B, 0x54, 0x0A, 0x05, // 180 0x0B, 0x5E, 0x17, 0x09, // 181 0x0B, 0x75, 0x19, 0x0A, // 182 0x0B, 0x8E, 0x08, 0x04, // 183 0x0B, 0x96, 0x0C, 0x05, // 184 0x0B, 0xA2, 0x0B, 0x05, // 185 0x0B, 0xAD, 0x0D, 0x06, // 186 0x0B, 0xBA, 0x17, 0x09, // 187 0x0B, 0xD1, 0x26, 0x0E, // 188 0x0B, 0xF7, 0x26, 0x0E, // 189 0x0C, 0x1D, 0x26, 0x0E, // 190 0x0C, 0x43, 0x1A, 0x0A, // 191 0x0C, 0x5D, 0x1D, 0x0B, // 192 0x0C, 0x7A, 0x1D, 0x0B, // 193 0x0C, 0x97, 0x1D, 0x0B, // 194 0x0C, 0xB4, 0x1D, 0x0B, // 195 0x0C, 0xD1, 0x1D, 0x0B, // 196 0x0C, 0xEE, 0x1D, 0x0B, // 197 0x0D, 0x0B, 0x2C, 0x10, // 198 0x0D, 0x37, 0x20, 0x0C, // 199 0x0D, 0x57, 0x1D, 0x0B, // 200 0x0D, 0x74, 0x1D, 0x0B, // 201 0x0D, 0x91, 0x1D, 0x0B, // 202 0x0D, 0xAE, 0x1D, 0x0B, // 203 0x0D, 0xCB, 0x05, 0x03, // 204 0x0D, 0xD0, 0x07, 0x04, // 205 0x0D, 0xD7, 0x0A, 0x05, // 206 0x0D, 0xE1, 0x07, 0x04, // 207 0x0D, 0xE8, 0x20, 0x0C, // 208 0x0E, 0x08, 0x1D, 0x0B, // 209 0x0E, 0x25, 0x20, 0x0C, // 210 0x0E, 0x45, 0x20, 0x0C, // 211 0x0E, 0x65, 0x20, 0x0C, // 212 0x0E, 0x85, 0x20, 0x0C, // 213 0x0E, 0xA5, 0x20, 0x0C, // 214 0x0E, 0xC5, 0x17, 0x09, // 215 0x0E, 0xDC, 0x20, 0x0C, // 216 0x0E, 0xFC, 0x1D, 0x0B, // 217 0x0F, 0x19, 0x1D, 0x0B, // 218 0x0F, 0x36, 0x1D, 0x0B, // 219 0x0F, 0x53, 0x1D, 0x0B, // 220 0x0F, 0x70, 0x19, 0x0A, // 221 0x0F, 0x89, 0x1D, 0x0B, // 222 0x0F, 0xA6, 0x17, 0x09, // 223 0x0F, 0xBD, 0x17, 0x09, // 224 0x0F, 0xD4, 0x17, 0x09, // 225 0x0F, 0xEB, 0x17, 0x09, // 226 0x10, 0x02, 0x17, 0x09, // 227 0x10, 0x19, 0x17, 0x09, // 228 0x10, 0x30, 0x17, 0x09, // 229 0x10, 0x47, 0x29, 0x0F, // 230 0x10, 0x70, 0x14, 0x08, // 231 0x10, 0x84, 0x17, 0x09, // 232 0x10, 0x9B, 0x17, 0x09, // 233 0x10, 0xB2, 0x17, 0x09, // 234 0x10, 0xC9, 0x17, 0x09, // 235 0x10, 0xE0, 0x05, 0x03, // 236 0x10, 0xE5, 0x07, 0x04, // 237 0x10, 0xEC, 0x0A, 0x05, // 238 0x10, 0xF6, 0x07, 0x04, // 239 0x10, 0xFD, 0x17, 0x09, // 240 0x11, 0x14, 0x14, 0x08, // 241 0x11, 0x28, 0x17, 0x09, // 242 0x11, 0x3F, 0x17, 0x09, // 243 0x11, 0x56, 0x17, 0x09, // 244 0x11, 0x6D, 0x17, 0x09, // 245 0x11, 0x84, 0x17, 0x09, // 246 0x11, 0x9B, 0x17, 0x09, // 247 0x11, 0xB2, 0x17, 0x09, // 248 0x11, 0xC9, 0x14, 0x08, // 249 0x11, 0xDD, 0x14, 0x08, // 250 0x11, 0xF1, 0x14, 0x08, // 251 0x12, 0x05, 0x14, 0x08, // 252 0x12, 0x19, 0x13, 0x08, // 253 0x12, 0x2C, 0x17, 0x09, // 254 0x12, 0x43, 0x13, 0x08, // 255 // Font Data: 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x5F, // 33 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, // 34 0x80, 0x08, 0x00, 0x80, 0x78, 0x00, 0xC0, 0x0F, 0x00, 0xB8, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x78, 0x00, 0xC0, 0x0F, 0x00, 0xB8, 0x08, 0x00, 0x80, 0x08, // 35 0x00, 0x00, 0x00, 0xE0, 0x10, 0x00, 0x10, 0x21, 0x00, 0x08, 0x41, 0x00, 0xFC, 0xFF, 0x00, 0x08, 0x42, 0x00, 0x10, 0x22, 0x00, 0x20, 0x1C, // 36 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0x08, 0x61, 0x00, 0xF0, 0x18, 0x00, 0x00, 0x06, 0x00, 0xC0, 0x01, 0x00, 0x30, 0x3C, 0x00, 0x08, 0x42, 0x00, 0x00, 0x42, 0x00, 0x00, 0x42, 0x00, 0x00, 0x3C, // 37 0x00, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x70, 0x22, 0x00, 0x88, 0x41, 0x00, 0x08, 0x43, 0x00, 0x88, 0x44, 0x00, 0x70, 0x28, 0x00, 0x00, 0x10, 0x00, 0x00, 0x28, 0x00, 0x00, 0x44, // 38 0x00, 0x00, 0x00, 0x78, // 39 0x00, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x70, 0xC0, 0x01, 0x08, 0x00, 0x02, // 40 0x00, 0x00, 0x00, 0x08, 0x00, 0x02, 0x70, 0xC0, 0x01, 0x80, 0x3F, // 41 0x10, 0x00, 0x00, 0xD0, 0x00, 0x00, 0x38, 0x00, 0x00, 0xD0, 0x00, 0x00, 0x10, // 42 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, // 43 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, // 44 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, // 45 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, // 46 0x00, 0x60, 0x00, 0x00, 0x1E, 0x00, 0xE0, 0x01, 0x00, 0x18, // 47 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0xE0, 0x1F, // 48 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0x00, 0x10, 0x00, 0x00, 0xF8, 0x7F, // 49 0x00, 0x00, 0x00, 0x20, 0x40, 0x00, 0x10, 0x60, 0x00, 0x08, 0x50, 0x00, 0x08, 0x48, 0x00, 0x08, 0x44, 0x00, 0x10, 0x43, 0x00, 0xE0, 0x40, // 50 0x00, 0x00, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x88, 0x41, 0x00, 0xF0, 0x22, 0x00, 0x00, 0x1C, // 51 0x00, 0x0C, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x09, 0x00, 0xC0, 0x08, 0x00, 0x20, 0x08, 0x00, 0x10, 0x08, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x08, // 52 0x00, 0x00, 0x00, 0xC0, 0x11, 0x00, 0xB8, 0x20, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x08, 0x21, 0x00, 0x08, 0x1E, // 53 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x10, 0x21, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x10, 0x21, 0x00, 0x20, 0x1E, // 54 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x78, 0x00, 0x08, 0x07, 0x00, 0xC8, 0x00, 0x00, 0x28, 0x00, 0x00, 0x18, // 55 0x00, 0x00, 0x00, 0x60, 0x1C, 0x00, 0x90, 0x22, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x90, 0x22, 0x00, 0x60, 0x1C, // 56 0x00, 0x00, 0x00, 0xE0, 0x11, 0x00, 0x10, 0x22, 0x00, 0x08, 0x44, 0x00, 0x08, 0x44, 0x00, 0x08, 0x44, 0x00, 0x10, 0x22, 0x00, 0xE0, 0x1F, // 57 0x00, 0x00, 0x00, 0x40, 0x40, // 58 0x00, 0x00, 0x00, 0x40, 0xC0, 0x01, // 59 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x05, 0x00, 0x00, 0x05, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x40, 0x10, // 60 0x00, 0x00, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, // 61 0x00, 0x00, 0x00, 0x40, 0x10, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x00, 0x05, 0x00, 0x00, 0x05, 0x00, 0x00, 0x02, // 62 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x5C, 0x00, 0x08, 0x02, 0x00, 0x10, 0x01, 0x00, 0xE0, // 63 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0xC0, 0x40, 0x00, 0x20, 0x80, 0x00, 0x10, 0x1E, 0x01, 0x10, 0x21, 0x01, 0x88, 0x40, 0x02, 0x48, 0x40, 0x02, 0x48, 0x40, 0x02, 0x48, 0x20, 0x02, 0x88, 0x7C, 0x02, 0xC8, 0x43, 0x02, 0x10, 0x40, 0x02, 0x10, 0x20, 0x01, 0x60, 0x10, 0x01, 0x80, 0x8F, // 64 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x70, 0x04, 0x00, 0x08, 0x04, 0x00, 0x70, 0x04, 0x00, 0x80, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, // 65 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x90, 0x22, 0x00, 0x60, 0x1C, // 66 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, // 67 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 68 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x40, // 69 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, // 70 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, 0x10, 0x22, 0x00, 0x20, 0x12, 0x00, 0x00, 0x0E, // 71 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0xF8, 0x7F, // 72 0x00, 0x00, 0x00, 0xF8, 0x7F, // 73 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x3F, // 74 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x02, 0x00, 0x00, 0x01, 0x00, 0x80, 0x03, 0x00, 0x40, 0x04, 0x00, 0x20, 0x18, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, // 75 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, // 76 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x30, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x03, 0x00, 0xC0, 0x00, 0x00, 0x30, 0x00, 0x00, 0xF8, 0x7F, // 77 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x10, 0x00, 0x00, 0x60, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x04, 0x00, 0x00, 0x18, 0x00, 0x00, 0x20, 0x00, 0xF8, 0x7F, // 78 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 79 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x10, 0x01, 0x00, 0xE0, // 80 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x50, 0x00, 0x08, 0x50, 0x00, 0x10, 0x20, 0x00, 0x20, 0x70, 0x00, 0xC0, 0x4F, // 81 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x06, 0x00, 0x08, 0x1A, 0x00, 0x10, 0x21, 0x00, 0xE0, 0x40, // 82 0x00, 0x00, 0x00, 0x60, 0x10, 0x00, 0x90, 0x20, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, 0x10, 0x22, 0x00, 0x20, 0x1C, // 83 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, // 84 0x00, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0xF8, 0x1F, // 85 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x18, 0x00, 0x00, 0x60, 0x00, 0x00, 0x18, 0x00, 0x00, 0x07, 0x00, 0xE0, 0x00, 0x00, 0x18, // 86 0x18, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x03, 0x00, 0x70, 0x00, 0x00, 0x08, 0x00, 0x00, 0x70, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1E, 0x00, 0xE0, 0x01, 0x00, 0x18, // 87 0x00, 0x40, 0x00, 0x08, 0x20, 0x00, 0x10, 0x10, 0x00, 0x60, 0x0C, 0x00, 0x80, 0x02, 0x00, 0x00, 0x01, 0x00, 0x80, 0x02, 0x00, 0x60, 0x0C, 0x00, 0x10, 0x10, 0x00, 0x08, 0x20, 0x00, 0x00, 0x40, // 88 0x08, 0x00, 0x00, 0x30, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x7E, 0x00, 0x80, 0x01, 0x00, 0x40, 0x00, 0x00, 0x30, 0x00, 0x00, 0x08, // 89 0x00, 0x40, 0x00, 0x08, 0x60, 0x00, 0x08, 0x58, 0x00, 0x08, 0x44, 0x00, 0x08, 0x43, 0x00, 0x88, 0x40, 0x00, 0x68, 0x40, 0x00, 0x18, 0x40, 0x00, 0x08, 0x40, // 90 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x03, 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, // 91 0x18, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x60, // 92 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, 0xF8, 0xFF, 0x03, // 93 0x00, 0x01, 0x00, 0xC0, 0x00, 0x00, 0x30, 0x00, 0x00, 0x08, 0x00, 0x00, 0x30, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x01, // 94 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, // 95 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x10, // 96 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x42, 0x00, 0x40, 0x22, 0x00, 0x80, 0x7F, // 97 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 98 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, // 99 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0xF8, 0x7F, // 100 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x24, 0x00, 0x00, 0x17, // 101 0x40, 0x00, 0x00, 0xF0, 0x7F, 0x00, 0x48, 0x00, 0x00, 0x48, // 102 0x00, 0x00, 0x00, 0x00, 0x1F, 0x01, 0x80, 0x20, 0x02, 0x40, 0x40, 0x02, 0x40, 0x40, 0x02, 0x40, 0x40, 0x02, 0x80, 0x20, 0x01, 0xC0, 0xFF, // 103 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x7F, // 104 0x00, 0x00, 0x00, 0xC8, 0x7F, // 105 0x00, 0x00, 0x02, 0xC8, 0xFF, 0x01, // 106 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x06, 0x00, 0x00, 0x19, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, // 107 0x00, 0x00, 0x00, 0xF8, 0x7F, // 108 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x7F, // 109 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x7F, // 110 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 111 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 112 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0xC0, 0xFF, 0x03, // 113 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, // 114 0x00, 0x00, 0x00, 0x80, 0x23, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x38, // 115 0x40, 0x00, 0x00, 0xF0, 0x7F, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, // 116 0x00, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0xC0, 0x7F, // 117 0xC0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x03, 0x00, 0xC0, // 118 0xC0, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x03, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1F, 0x00, 0xC0, // 119 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, // 120 0xC0, 0x01, 0x00, 0x00, 0x06, 0x02, 0x00, 0x38, 0x02, 0x00, 0xE0, 0x01, 0x00, 0x38, 0x00, 0x00, 0x07, 0x00, 0xC0, // 121 0x40, 0x40, 0x00, 0x40, 0x60, 0x00, 0x40, 0x58, 0x00, 0x40, 0x44, 0x00, 0x40, 0x43, 0x00, 0xC0, 0x40, 0x00, 0x40, 0x40, // 122 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0xF0, 0xFB, 0x01, 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, // 123 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x03, // 124 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, 0xF0, 0xFB, 0x01, 0x00, 0x04, 0x00, 0x00, 0x04, // 125 0x00, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x01, // 126 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x09, 0x40, 0x00, 0x0A, 0x40, 0x00, 0x0A, 0x40, 0x00, 0x09, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, // 129 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x09, 0x40, 0x00, 0x0A, 0x40, 0x00, 0x0A, 0x40, 0x00, 0x09, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 130 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x09, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x09, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x40, // 131 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x10, 0x00, 0x00, 0x60, 0x00, 0x00, 0x81, 0x00, 0x00, 0x02, 0x03, 0x00, 0x02, 0x04, 0x00, 0x01, 0x18, 0x00, 0x00, 0x20, 0x00, 0xF8, 0x7F, // 132 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x09, 0x02, 0x00, 0x0A, 0x02, 0x00, 0x0A, 0x06, 0x00, 0x09, 0x1A, 0x00, 0x10, 0x21, 0x00, 0xE0, 0x40, // 133 0x00, 0x00, 0x00, 0x60, 0x10, 0x00, 0x90, 0x20, 0x00, 0x08, 0x41, 0x00, 0x09, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x0A, 0x42, 0x00, 0x09, 0x42, 0x00, 0x10, 0x22, 0x00, 0x20, 0x1C, // 134 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x09, 0x00, 0x00, 0xFA, 0x7F, 0x00, 0x0A, 0x00, 0x00, 0x09, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, // 135 0x00, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x06, 0x40, 0x00, 0x09, 0x40, 0x00, 0x09, 0x40, 0x00, 0x06, 0x40, 0x00, 0x00, 0x20, 0x00, 0xF8, 0x1F, // 136 0x00, 0x40, 0x00, 0x08, 0x60, 0x00, 0x08, 0x58, 0x00, 0x09, 0x44, 0x00, 0x0A, 0x43, 0x00, 0x8A, 0x40, 0x00, 0x69, 0x40, 0x00, 0x18, 0x40, 0x00, 0x08, 0x40, // 137 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x48, 0x40, 0x00, 0x50, 0x40, 0x00, 0x50, 0x40, 0x00, 0x88, 0x20, // 138 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x38, // 139 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x48, 0x44, 0x00, 0x50, 0x44, 0x00, 0x50, 0x44, 0x00, 0x88, 0x24, 0x00, 0x00, 0x17, // 140 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x88, 0x00, 0x00, 0x50, 0x00, 0x00, 0x50, 0x00, 0x00, 0x48, 0x00, 0x00, 0x80, 0x7F, // 141 0x00, 0x00, 0x00, 0xC8, 0x7F, 0x00, 0x90, 0x00, 0x00, 0x50, 0x00, 0x00, 0x48, // 142 0x00, 0x00, 0x00, 0x80, 0x23, 0x00, 0x48, 0x44, 0x00, 0x50, 0x44, 0x00, 0x50, 0x44, 0x00, 0x48, 0x44, 0x00, 0x80, 0x38, // 143 0x40, 0x00, 0x00, 0xF0, 0x7F, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x00, 0x00, 0x00, 0x70, // 144 0x00, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x0C, 0x40, 0x00, 0x12, 0x40, 0x00, 0x12, 0x40, 0x00, 0x0C, 0x20, 0x00, 0xC0, 0x7F, // 145 0x40, 0x40, 0x00, 0x40, 0x60, 0x00, 0x48, 0x58, 0x00, 0x50, 0x44, 0x00, 0x50, 0x43, 0x00, 0xC8, 0x40, 0x00, 0x40, 0x40, // 146 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x02, 0x40, 0x00, 0x01, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, // 147 0x00, 0x00, 0x00, 0xFA, 0x7F, 0x00, 0x01, // 148 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x38, 0x40, 0x00, 0x00, 0x40, // 149 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x38, // 150 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x0A, 0x02, 0x00, 0x09, 0x06, 0x00, 0x08, 0x1A, 0x00, 0x10, 0x21, 0x00, 0xE0, 0x40, // 151 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x50, 0x00, 0x00, 0x48, // 152 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0xFF, 0x03, // 161 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x03, 0x40, 0xF0, 0x00, 0x40, 0x4E, 0x00, 0xC0, 0x41, 0x00, 0xB8, 0x20, 0x00, 0x00, 0x11, // 162 0x00, 0x41, 0x00, 0xE0, 0x31, 0x00, 0x10, 0x2F, 0x00, 0x08, 0x21, 0x00, 0x08, 0x21, 0x00, 0x08, 0x40, 0x00, 0x10, 0x40, 0x00, 0x20, 0x20, // 163 0x00, 0x00, 0x00, 0x40, 0x0B, 0x00, 0x80, 0x04, 0x00, 0x40, 0x08, 0x00, 0x40, 0x08, 0x00, 0x80, 0x04, 0x00, 0x40, 0x0B, // 164 0x08, 0x0A, 0x00, 0x10, 0x0A, 0x00, 0x60, 0x0A, 0x00, 0x80, 0x0B, 0x00, 0x00, 0x7E, 0x00, 0x80, 0x0B, 0x00, 0x60, 0x0A, 0x00, 0x10, 0x0A, 0x00, 0x08, 0x0A, // 165 0x00, 0x00, 0x00, 0xF8, 0xF1, 0x03, // 166 0x00, 0x86, 0x00, 0x70, 0x09, 0x01, 0xC8, 0x10, 0x02, 0x88, 0x10, 0x02, 0x08, 0x21, 0x02, 0x08, 0x61, 0x02, 0x30, 0xD2, 0x01, 0x00, 0x0C, // 167 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, // 168 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0xC8, 0x47, 0x00, 0x28, 0x48, 0x00, 0x28, 0x48, 0x00, 0x28, 0x48, 0x00, 0x28, 0x48, 0x00, 0x48, 0x44, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 169 0xD0, 0x00, 0x00, 0x48, 0x01, 0x00, 0x28, 0x01, 0x00, 0x28, 0x01, 0x00, 0xF0, 0x01, // 170 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, 0x20, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, 0x20, // 171 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x0F, // 172 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, // 173 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0xE8, 0x4F, 0x00, 0x28, 0x41, 0x00, 0x28, 0x41, 0x00, 0x28, 0x43, 0x00, 0x28, 0x45, 0x00, 0xC8, 0x48, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 174 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, // 175 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x48, 0x00, 0x00, 0x48, 0x00, 0x00, 0x30, // 176 0x00, 0x00, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0xE0, 0x4F, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, // 177 0x10, 0x01, 0x00, 0x88, 0x01, 0x00, 0x48, 0x01, 0x00, 0x48, 0x01, 0x00, 0x30, 0x01, // 178 0x90, 0x00, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0x28, 0x01, 0x00, 0xD8, // 179 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, // 180 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0xC0, 0x7F, // 181 0xF0, 0x00, 0x00, 0xF8, 0x00, 0x00, 0xF8, 0x01, 0x00, 0xF8, 0x01, 0x00, 0xF8, 0xFF, 0x03, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0xFF, 0x03, 0x08, // 182 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, // 183 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x80, 0x02, 0x00, 0x00, 0x03, // 184 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x01, // 185 0xF0, 0x00, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0xF0, // 186 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x04, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x04, // 187 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x40, 0x00, 0xF8, 0x21, 0x00, 0x00, 0x10, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x02, 0x00, 0x80, 0x01, 0x00, 0x40, 0x30, 0x00, 0x30, 0x28, 0x00, 0x08, 0x24, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x20, // 188 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x40, 0x00, 0xF8, 0x31, 0x00, 0x00, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x03, 0x00, 0x80, 0x00, 0x00, 0x60, 0x44, 0x00, 0x10, 0x62, 0x00, 0x08, 0x52, 0x00, 0x00, 0x52, 0x00, 0x00, 0x4C, // 189 0x90, 0x00, 0x00, 0x08, 0x01, 0x00, 0x08, 0x41, 0x00, 0x28, 0x21, 0x00, 0xD8, 0x18, 0x00, 0x00, 0x04, 0x00, 0x00, 0x03, 0x00, 0x80, 0x00, 0x00, 0x40, 0x30, 0x00, 0x30, 0x28, 0x00, 0x08, 0x24, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x20, // 190 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x10, 0x01, 0x00, 0x08, 0x02, 0x40, 0x07, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x01, 0x00, 0xC0, // 191 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x71, 0x04, 0x00, 0x0A, 0x04, 0x00, 0x70, 0x04, 0x00, 0x80, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, // 192 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x70, 0x04, 0x00, 0x0A, 0x04, 0x00, 0x71, 0x04, 0x00, 0x80, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, // 193 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x72, 0x04, 0x00, 0x09, 0x04, 0x00, 0x71, 0x04, 0x00, 0x82, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, // 194 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x72, 0x04, 0x00, 0x09, 0x04, 0x00, 0x72, 0x04, 0x00, 0x81, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, // 195 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x72, 0x04, 0x00, 0x08, 0x04, 0x00, 0x72, 0x04, 0x00, 0x80, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, // 196 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x7E, 0x04, 0x00, 0x0A, 0x04, 0x00, 0x7E, 0x04, 0x00, 0x80, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, // 197 0x00, 0x60, 0x00, 0x00, 0x18, 0x00, 0x00, 0x06, 0x00, 0x80, 0x05, 0x00, 0x60, 0x04, 0x00, 0x18, 0x04, 0x00, 0x08, 0x04, 0x00, 0x08, 0x04, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, // 198 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x02, 0x08, 0xC0, 0x02, 0x08, 0x40, 0x03, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, // 199 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x09, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x40, // 200 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x09, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x40, // 201 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x09, 0x41, 0x00, 0x09, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x40, // 202 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x08, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x40, // 203 0x01, 0x00, 0x00, 0xFA, 0x7F, // 204 0x00, 0x00, 0x00, 0xFA, 0x7F, 0x00, 0x01, // 205 0x02, 0x00, 0x00, 0xF9, 0x7F, 0x00, 0x01, 0x00, 0x00, 0x02, // 206 0x02, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x02, // 207 0x00, 0x02, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 208 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x10, 0x00, 0x00, 0x60, 0x00, 0x00, 0x82, 0x00, 0x00, 0x01, 0x03, 0x00, 0x02, 0x04, 0x00, 0x01, 0x18, 0x00, 0x00, 0x20, 0x00, 0xF8, 0x7F, // 209 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x09, 0x40, 0x00, 0x0A, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 210 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x0A, 0x40, 0x00, 0x09, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 211 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x0A, 0x40, 0x00, 0x09, 0x40, 0x00, 0x09, 0x40, 0x00, 0x0A, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 212 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x0A, 0x40, 0x00, 0x09, 0x40, 0x00, 0x0A, 0x40, 0x00, 0x09, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 213 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x0A, 0x40, 0x00, 0x08, 0x40, 0x00, 0x0A, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 214 0x00, 0x00, 0x00, 0x40, 0x10, 0x00, 0x80, 0x08, 0x00, 0x00, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x05, 0x00, 0x80, 0x08, 0x00, 0x40, 0x10, // 215 0x00, 0x00, 0x00, 0xC0, 0x4F, 0x00, 0x20, 0x30, 0x00, 0x10, 0x30, 0x00, 0x08, 0x4C, 0x00, 0x08, 0x42, 0x00, 0x08, 0x41, 0x00, 0xC8, 0x40, 0x00, 0x30, 0x20, 0x00, 0x30, 0x10, 0x00, 0xC8, 0x0F, // 216 0x00, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x01, 0x40, 0x00, 0x02, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0xF8, 0x1F, // 217 0x00, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x02, 0x40, 0x00, 0x01, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0xF8, 0x1F, // 218 0x00, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x02, 0x40, 0x00, 0x01, 0x40, 0x00, 0x01, 0x40, 0x00, 0x02, 0x40, 0x00, 0x00, 0x20, 0x00, 0xF8, 0x1F, // 219 0x00, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x02, 0x40, 0x00, 0x00, 0x40, 0x00, 0x02, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0xF8, 0x1F, // 220 0x08, 0x00, 0x00, 0x30, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x01, 0x00, 0x02, 0x7E, 0x00, 0x81, 0x01, 0x00, 0x40, 0x00, 0x00, 0x30, 0x00, 0x00, 0x08, // 221 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x20, 0x10, 0x00, 0x20, 0x10, 0x00, 0x20, 0x10, 0x00, 0x20, 0x10, 0x00, 0x20, 0x10, 0x00, 0x20, 0x10, 0x00, 0x40, 0x08, 0x00, 0x80, 0x07, // 222 0x00, 0x00, 0x00, 0xE0, 0x7F, 0x00, 0x10, 0x00, 0x00, 0x08, 0x20, 0x00, 0x88, 0x43, 0x00, 0x70, 0x42, 0x00, 0x00, 0x44, 0x00, 0x00, 0x38, // 223 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x40, 0x44, 0x00, 0x48, 0x44, 0x00, 0x50, 0x42, 0x00, 0x40, 0x22, 0x00, 0x80, 0x7F, // 224 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x40, 0x44, 0x00, 0x50, 0x44, 0x00, 0x48, 0x42, 0x00, 0x40, 0x22, 0x00, 0x80, 0x7F, // 225 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x50, 0x44, 0x00, 0x48, 0x44, 0x00, 0x48, 0x42, 0x00, 0x50, 0x22, 0x00, 0x80, 0x7F, // 226 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x50, 0x44, 0x00, 0x48, 0x44, 0x00, 0x50, 0x42, 0x00, 0x48, 0x22, 0x00, 0x80, 0x7F, // 227 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x50, 0x44, 0x00, 0x40, 0x44, 0x00, 0x50, 0x42, 0x00, 0x40, 0x22, 0x00, 0x80, 0x7F, // 228 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x5C, 0x44, 0x00, 0x54, 0x44, 0x00, 0x5C, 0x42, 0x00, 0x40, 0x22, 0x00, 0x80, 0x7F, // 229 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x42, 0x00, 0x40, 0x22, 0x00, 0x80, 0x3F, 0x00, 0x80, 0x24, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x24, 0x00, 0x00, 0x17, // 230 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x02, 0x40, 0xC0, 0x02, 0x40, 0x40, 0x03, 0x80, 0x20, // 231 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x48, 0x44, 0x00, 0x50, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x24, 0x00, 0x00, 0x17, // 232 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x40, 0x44, 0x00, 0x50, 0x44, 0x00, 0x48, 0x44, 0x00, 0x80, 0x24, 0x00, 0x00, 0x17, // 233 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x50, 0x44, 0x00, 0x48, 0x44, 0x00, 0x48, 0x44, 0x00, 0x90, 0x24, 0x00, 0x00, 0x17, // 234 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x50, 0x44, 0x00, 0x40, 0x44, 0x00, 0x50, 0x44, 0x00, 0x80, 0x24, 0x00, 0x00, 0x17, // 235 0x08, 0x00, 0x00, 0xD0, 0x7F, // 236 0x00, 0x00, 0x00, 0xD0, 0x7F, 0x00, 0x08, // 237 0x10, 0x00, 0x00, 0xC8, 0x7F, 0x00, 0x08, 0x00, 0x00, 0x10, // 238 0x10, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x10, // 239 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0xA0, 0x20, 0x00, 0x68, 0x40, 0x00, 0x58, 0x40, 0x00, 0x70, 0x40, 0x00, 0xE8, 0x20, 0x00, 0x00, 0x1F, // 240 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x90, 0x00, 0x00, 0x48, 0x00, 0x00, 0x50, 0x00, 0x00, 0x48, 0x00, 0x00, 0x80, 0x7F, // 241 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x48, 0x40, 0x00, 0x50, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 242 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x50, 0x40, 0x00, 0x48, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 243 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x50, 0x40, 0x00, 0x48, 0x40, 0x00, 0x48, 0x40, 0x00, 0x90, 0x20, 0x00, 0x00, 0x1F, // 244 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x50, 0x40, 0x00, 0x48, 0x40, 0x00, 0x50, 0x40, 0x00, 0x88, 0x20, 0x00, 0x00, 0x1F, // 245 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x50, 0x40, 0x00, 0x40, 0x40, 0x00, 0x50, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 246 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x80, 0x0A, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, // 247 0x00, 0x00, 0x00, 0x00, 0x5F, 0x00, 0x80, 0x30, 0x00, 0x40, 0x48, 0x00, 0x40, 0x44, 0x00, 0x40, 0x42, 0x00, 0x80, 0x21, 0x00, 0x40, 0x1F, // 248 0x00, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x40, 0x00, 0x00, 0x20, 0x00, 0xC0, 0x7F, // 249 0x00, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x10, 0x40, 0x00, 0x08, 0x20, 0x00, 0xC0, 0x7F, // 250 0x00, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x10, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0xC0, 0x7F, // 251 0x00, 0x00, 0x00, 0xD0, 0x3F, 0x00, 0x00, 0x40, 0x00, 0x10, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0xC0, 0x7F, // 252 0xC0, 0x01, 0x00, 0x00, 0x06, 0x02, 0x00, 0x38, 0x02, 0x10, 0xE0, 0x01, 0x08, 0x38, 0x00, 0x00, 0x07, 0x00, 0xC0, // 253 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x03, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 254 0xC0, 0x01, 0x00, 0x00, 0x06, 0x02, 0x10, 0x38, 0x02, 0x00, 0xE0, 0x01, 0x10, 0x38, 0x00, 0x00, 0x07, 0x00, 0xC0, // 255 }; const uint8_t ArialMT_Plain_24_CS[] PROGMEM = { 0x18, // Width: 24 0x1C, // Height: 28 0x20, // First char: 32 0xE0, // Number of chars: 224 // Jump Table: 0xFF, 0xFF, 0x00, 0x06, // 32 0x00, 0x00, 0x13, 0x06, // 33 0x00, 0x13, 0x1A, 0x08, // 34 0x00, 0x2D, 0x33, 0x0E, // 35 0x00, 0x60, 0x2F, 0x0D, // 36 0x00, 0x8F, 0x4F, 0x15, // 37 0x00, 0xDE, 0x3B, 0x10, // 38 0x01, 0x19, 0x0A, 0x04, // 39 0x01, 0x23, 0x1C, 0x08, // 40 0x01, 0x3F, 0x1B, 0x08, // 41 0x01, 0x5A, 0x21, 0x0A, // 42 0x01, 0x7B, 0x32, 0x0E, // 43 0x01, 0xAD, 0x10, 0x05, // 44 0x01, 0xBD, 0x1B, 0x08, // 45 0x01, 0xD8, 0x0F, 0x05, // 46 0x01, 0xE7, 0x19, 0x08, // 47 0x02, 0x00, 0x2F, 0x0D, // 48 0x02, 0x2F, 0x23, 0x0A, // 49 0x02, 0x52, 0x2F, 0x0D, // 50 0x02, 0x81, 0x2F, 0x0D, // 51 0x02, 0xB0, 0x2F, 0x0D, // 52 0x02, 0xDF, 0x2F, 0x0D, // 53 0x03, 0x0E, 0x2F, 0x0D, // 54 0x03, 0x3D, 0x2D, 0x0D, // 55 0x03, 0x6A, 0x2F, 0x0D, // 56 0x03, 0x99, 0x2F, 0x0D, // 57 0x03, 0xC8, 0x0F, 0x05, // 58 0x03, 0xD7, 0x10, 0x05, // 59 0x03, 0xE7, 0x2F, 0x0D, // 60 0x04, 0x16, 0x2F, 0x0D, // 61 0x04, 0x45, 0x2E, 0x0D, // 62 0x04, 0x73, 0x2E, 0x0D, // 63 0x04, 0xA1, 0x5B, 0x18, // 64 0x04, 0xFC, 0x3B, 0x10, // 65 0x05, 0x37, 0x3B, 0x10, // 66 0x05, 0x72, 0x3F, 0x11, // 67 0x05, 0xB1, 0x3F, 0x11, // 68 0x05, 0xF0, 0x3B, 0x10, // 69 0x06, 0x2B, 0x35, 0x0F, // 70 0x06, 0x60, 0x43, 0x12, // 71 0x06, 0xA3, 0x3B, 0x10, // 72 0x06, 0xDE, 0x0F, 0x05, // 73 0x06, 0xED, 0x27, 0x0B, // 74 0x07, 0x14, 0x3F, 0x11, // 75 0x07, 0x53, 0x2F, 0x0D, // 76 0x07, 0x82, 0x43, 0x12, // 77 0x07, 0xC5, 0x3B, 0x10, // 78 0x08, 0x00, 0x47, 0x13, // 79 0x08, 0x47, 0x3A, 0x10, // 80 0x08, 0x81, 0x47, 0x13, // 81 0x08, 0xC8, 0x3F, 0x11, // 82 0x09, 0x07, 0x3B, 0x10, // 83 0x09, 0x42, 0x35, 0x0F, // 84 0x09, 0x77, 0x3B, 0x10, // 85 0x09, 0xB2, 0x39, 0x10, // 86 0x09, 0xEB, 0x59, 0x18, // 87 0x0A, 0x44, 0x3B, 0x10, // 88 0x0A, 0x7F, 0x3D, 0x11, // 89 0x0A, 0xBC, 0x37, 0x0F, // 90 0x0A, 0xF3, 0x14, 0x06, // 91 0x0B, 0x07, 0x1B, 0x08, // 92 0x0B, 0x22, 0x18, 0x07, // 93 0x0B, 0x3A, 0x2A, 0x0C, // 94 0x0B, 0x64, 0x34, 0x0E, // 95 0x0B, 0x98, 0x11, 0x06, // 96 0x0B, 0xA9, 0x2F, 0x0D, // 97 0x0B, 0xD8, 0x33, 0x0E, // 98 0x0C, 0x0B, 0x2B, 0x0C, // 99 0x0C, 0x36, 0x2F, 0x0D, // 100 0x0C, 0x65, 0x2F, 0x0D, // 101 0x0C, 0x94, 0x1A, 0x08, // 102 0x0C, 0xAE, 0x2F, 0x0D, // 103 0x0C, 0xDD, 0x2F, 0x0D, // 104 0x0D, 0x0C, 0x0F, 0x05, // 105 0x0D, 0x1B, 0x10, 0x05, // 106 0x0D, 0x2B, 0x2F, 0x0D, // 107 0x0D, 0x5A, 0x0F, 0x05, // 108 0x0D, 0x69, 0x47, 0x13, // 109 0x0D, 0xB0, 0x2F, 0x0D, // 110 0x0D, 0xDF, 0x2F, 0x0D, // 111 0x0E, 0x0E, 0x33, 0x0E, // 112 0x0E, 0x41, 0x30, 0x0D, // 113 0x0E, 0x71, 0x1E, 0x09, // 114 0x0E, 0x8F, 0x2B, 0x0C, // 115 0x0E, 0xBA, 0x1B, 0x08, // 116 0x0E, 0xD5, 0x2F, 0x0D, // 117 0x0F, 0x04, 0x2A, 0x0C, // 118 0x0F, 0x2E, 0x42, 0x12, // 119 0x0F, 0x70, 0x2B, 0x0C, // 120 0x0F, 0x9B, 0x2A, 0x0C, // 121 0x0F, 0xC5, 0x2B, 0x0C, // 122 0x0F, 0xF0, 0x1C, 0x08, // 123 0x10, 0x0C, 0x10, 0x05, // 124 0x10, 0x1C, 0x1B, 0x08, // 125 0x10, 0x37, 0x32, 0x0E, // 126 0xFF, 0xFF, 0x00, 0x18, // 127 0xFF, 0xFF, 0x00, 0x18, // 128 0x10, 0x69, 0x3F, 0x11, // 129 0x10, 0xA8, 0x3F, 0x11, // 130 0x10, 0xE7, 0x3B, 0x10, // 131 0x11, 0x22, 0x3B, 0x10, // 132 0x11, 0x5D, 0x3F, 0x11, // 133 0x11, 0x9C, 0x3B, 0x10, // 134 0x11, 0xD7, 0x35, 0x0F, // 135 0x12, 0x0C, 0x3B, 0x10, // 136 0x12, 0x47, 0x37, 0x0F, // 137 0x12, 0x7E, 0x2B, 0x0C, // 138 0x12, 0xA9, 0x3A, 0x10, // 139 0x12, 0xE3, 0x2F, 0x0D, // 140 0x13, 0x12, 0x2F, 0x0D, // 141 0x13, 0x41, 0x1E, 0x09, // 142 0x13, 0x5F, 0x2B, 0x0C, // 143 0x13, 0x8A, 0x26, 0x0B, // 144 0x13, 0xB0, 0x2F, 0x0D, // 145 0x13, 0xDF, 0x2B, 0x0C, // 146 0x14, 0x0A, 0x2F, 0x0D, // 147 0x14, 0x39, 0x15, 0x07, // 148 0x14, 0x4E, 0x2F, 0x0D, // 149 0x14, 0x7D, 0x1A, 0x08, // 150 0x14, 0x97, 0x3F, 0x11, // 151 0x14, 0xD6, 0x1E, 0x09, // 152 0xFF, 0xFF, 0x00, 0x18, // 153 0xFF, 0xFF, 0x00, 0x18, // 154 0xFF, 0xFF, 0x00, 0x18, // 155 0xFF, 0xFF, 0x00, 0x18, // 156 0xFF, 0xFF, 0x00, 0x18, // 157 0xFF, 0xFF, 0x00, 0x18, // 158 0xFF, 0xFF, 0x00, 0x18, // 159 0xFF, 0xFF, 0x00, 0x18, // 160 0x14, 0xF4, 0x14, 0x06, // 161 0x15, 0x08, 0x2B, 0x0C, // 162 0x15, 0x33, 0x2F, 0x0D, // 163 0x15, 0x62, 0x33, 0x0E, // 164 0x15, 0x95, 0x31, 0x0E, // 165 0x15, 0xC6, 0x10, 0x05, // 166 0x15, 0xD6, 0x2F, 0x0D, // 167 0x16, 0x05, 0x19, 0x08, // 168 0x16, 0x1E, 0x46, 0x13, // 169 0x16, 0x64, 0x1A, 0x08, // 170 0x16, 0x7E, 0x27, 0x0B, // 171 0x16, 0xA5, 0x2F, 0x0D, // 172 0x16, 0xD4, 0x1B, 0x08, // 173 0x16, 0xEF, 0x46, 0x13, // 174 0x17, 0x35, 0x31, 0x0E, // 175 0x17, 0x66, 0x1E, 0x09, // 176 0x17, 0x84, 0x33, 0x0E, // 177 0x17, 0xB7, 0x1A, 0x08, // 178 0x17, 0xD1, 0x1A, 0x08, // 179 0x17, 0xEB, 0x19, 0x08, // 180 0x18, 0x04, 0x2F, 0x0D, // 181 0x18, 0x33, 0x31, 0x0E, // 182 0x18, 0x64, 0x12, 0x06, // 183 0x18, 0x76, 0x18, 0x07, // 184 0x18, 0x8E, 0x16, 0x07, // 185 0x18, 0xA4, 0x1E, 0x09, // 186 0x18, 0xC2, 0x2E, 0x0D, // 187 0x18, 0xF0, 0x4F, 0x15, // 188 0x19, 0x3F, 0x4B, 0x14, // 189 0x19, 0x8A, 0x4B, 0x14, // 190 0x19, 0xD5, 0x33, 0x0E, // 191 0x1A, 0x08, 0x3B, 0x10, // 192 0x1A, 0x43, 0x3B, 0x10, // 193 0x1A, 0x7E, 0x3B, 0x10, // 194 0x1A, 0xB9, 0x3B, 0x10, // 195 0x1A, 0xF4, 0x3B, 0x10, // 196 0x1B, 0x2F, 0x3B, 0x10, // 197 0x1B, 0x6A, 0x5B, 0x18, // 198 0x1B, 0xC5, 0x3F, 0x11, // 199 0x1C, 0x04, 0x3B, 0x10, // 200 0x1C, 0x3F, 0x3B, 0x10, // 201 0x1C, 0x7A, 0x3B, 0x10, // 202 0x1C, 0xB5, 0x3B, 0x10, // 203 0x1C, 0xF0, 0x11, 0x06, // 204 0x1D, 0x01, 0x11, 0x06, // 205 0x1D, 0x12, 0x15, 0x07, // 206 0x1D, 0x27, 0x15, 0x07, // 207 0x1D, 0x3C, 0x3F, 0x11, // 208 0x1D, 0x7B, 0x3B, 0x10, // 209 0x1D, 0xB6, 0x47, 0x13, // 210 0x1D, 0xFD, 0x47, 0x13, // 211 0x1E, 0x44, 0x47, 0x13, // 212 0x1E, 0x8B, 0x47, 0x13, // 213 0x1E, 0xD2, 0x47, 0x13, // 214 0x1F, 0x19, 0x2B, 0x0C, // 215 0x1F, 0x44, 0x47, 0x13, // 216 0x1F, 0x8B, 0x3B, 0x10, // 217 0x1F, 0xC6, 0x3B, 0x10, // 218 0x20, 0x01, 0x3B, 0x10, // 219 0x20, 0x3C, 0x3B, 0x10, // 220 0x20, 0x77, 0x3D, 0x11, // 221 0x20, 0xB4, 0x3A, 0x10, // 222 0x20, 0xEE, 0x37, 0x0F, // 223 0x21, 0x25, 0x2F, 0x0D, // 224 0x21, 0x54, 0x2F, 0x0D, // 225 0x21, 0x83, 0x2F, 0x0D, // 226 0x21, 0xB2, 0x2F, 0x0D, // 227 0x21, 0xE1, 0x2F, 0x0D, // 228 0x22, 0x10, 0x2F, 0x0D, // 229 0x22, 0x3F, 0x53, 0x16, // 230 0x22, 0x92, 0x2B, 0x0C, // 231 0x22, 0xBD, 0x2F, 0x0D, // 232 0x22, 0xEC, 0x2F, 0x0D, // 233 0x23, 0x1B, 0x2F, 0x0D, // 234 0x23, 0x4A, 0x2F, 0x0D, // 235 0x23, 0x79, 0x11, 0x06, // 236 0x23, 0x8A, 0x11, 0x06, // 237 0x23, 0x9B, 0x15, 0x07, // 238 0x23, 0xB0, 0x15, 0x07, // 239 0x23, 0xC5, 0x2F, 0x0D, // 240 0x23, 0xF4, 0x2F, 0x0D, // 241 0x24, 0x23, 0x2F, 0x0D, // 242 0x24, 0x52, 0x2F, 0x0D, // 243 0x24, 0x81, 0x2F, 0x0D, // 244 0x24, 0xB0, 0x2F, 0x0D, // 245 0x24, 0xDF, 0x2F, 0x0D, // 246 0x25, 0x0E, 0x32, 0x0E, // 247 0x25, 0x40, 0x33, 0x0E, // 248 0x25, 0x73, 0x2F, 0x0D, // 249 0x25, 0xA2, 0x2F, 0x0D, // 250 0x25, 0xD1, 0x2F, 0x0D, // 251 0x26, 0x00, 0x2F, 0x0D, // 252 0x26, 0x2F, 0x2A, 0x0C, // 253 0x26, 0x59, 0x2F, 0x0D, // 254 0x26, 0x88, 0x2A, 0x0C, // 255 // Font Data: 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x33, 0x00, 0xE0, 0xFF, 0x33, // 33 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xE0, 0x07, // 34 0x00, 0x0C, 0x03, 0x00, 0x00, 0x0C, 0x33, 0x00, 0x00, 0x0C, 0x3F, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x80, 0xFF, 0x03, 0x00, 0xE0, 0x0F, 0x03, 0x00, 0x60, 0x0C, 0x33, 0x00, 0x00, 0x0C, 0x3F, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x80, 0xFF, 0x03, 0x00, 0xE0, 0x0F, 0x03, 0x00, 0x60, 0x0C, 0x03, 0x00, 0x00, 0x0C, 0x03, // 35 0x00, 0x00, 0x00, 0x00, 0x80, 0x07, 0x06, 0x00, 0xC0, 0x0F, 0x1E, 0x00, 0xC0, 0x18, 0x1C, 0x00, 0x60, 0x18, 0x38, 0x00, 0x60, 0x30, 0x30, 0x00, 0xF0, 0xFF, 0xFF, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x60, 0x38, 0x00, 0xC0, 0x60, 0x18, 0x00, 0xC0, 0xC1, 0x1F, 0x00, 0x00, 0x81, 0x07, // 36 0x00, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x20, 0x20, 0x00, 0x00, 0x20, 0x20, 0x20, 0x00, 0x60, 0x30, 0x38, 0x00, 0xC0, 0x1F, 0x1E, 0x00, 0x80, 0x8F, 0x0F, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x8F, 0x0F, 0x00, 0xC0, 0xC3, 0x1F, 0x00, 0xE0, 0x60, 0x30, 0x00, 0x20, 0x20, 0x20, 0x00, 0x00, 0x20, 0x20, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x80, 0x0F, // 37 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x80, 0xE3, 0x1C, 0x00, 0xC0, 0x77, 0x38, 0x00, 0xE0, 0x3C, 0x30, 0x00, 0x60, 0x38, 0x30, 0x00, 0x60, 0x78, 0x30, 0x00, 0xE0, 0xEC, 0x38, 0x00, 0xC0, 0x8F, 0x1B, 0x00, 0x80, 0x03, 0x1F, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xC0, 0x38, 0x00, 0x00, 0x00, 0x10, // 38 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xE0, 0x07, // 39 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0xFE, 0x7F, 0x00, 0x80, 0x0F, 0xF0, 0x01, 0xC0, 0x01, 0x80, 0x03, 0x60, 0x00, 0x00, 0x06, 0x20, 0x00, 0x00, 0x04, // 40 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x04, 0x60, 0x00, 0x00, 0x06, 0xC0, 0x01, 0x80, 0x03, 0x80, 0x0F, 0xF0, 0x01, 0x00, 0xFE, 0x7F, 0x00, 0x00, 0xF0, 0x0F, // 41 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x04, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x80, 0x04, 0x00, 0x00, 0x80, // 42 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xFF, 0x0F, 0x00, 0x00, 0xFF, 0x0F, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, // 43 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x03, 0x00, 0x00, 0xF0, 0x01, // 44 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, // 45 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, // 46 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x80, 0x3F, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x60, // 47 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x38, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0x00, 0xFE, 0x03, // 48 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 49 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x30, 0x00, 0xC0, 0x03, 0x38, 0x00, 0xC0, 0x00, 0x3C, 0x00, 0x60, 0x00, 0x36, 0x00, 0x60, 0x00, 0x33, 0x00, 0x60, 0x80, 0x31, 0x00, 0x60, 0xC0, 0x30, 0x00, 0x60, 0x60, 0x30, 0x00, 0xC0, 0x30, 0x30, 0x00, 0xC0, 0x1F, 0x30, 0x00, 0x00, 0x0F, 0x30, // 50 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x06, 0x00, 0xC0, 0x01, 0x0E, 0x00, 0xC0, 0x00, 0x1C, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0xC0, 0x38, 0x30, 0x00, 0xC0, 0x6F, 0x18, 0x00, 0x80, 0xC7, 0x0F, 0x00, 0x00, 0x80, 0x07, // 51 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x3C, 0x03, 0x00, 0x00, 0x0E, 0x03, 0x00, 0x80, 0x07, 0x03, 0x00, 0xC0, 0x01, 0x03, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, // 52 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x06, 0x00, 0x80, 0x3F, 0x0E, 0x00, 0xE0, 0x1F, 0x18, 0x00, 0x60, 0x08, 0x30, 0x00, 0x60, 0x0C, 0x30, 0x00, 0x60, 0x0C, 0x30, 0x00, 0x60, 0x0C, 0x30, 0x00, 0x60, 0x0C, 0x30, 0x00, 0x60, 0x18, 0x1C, 0x00, 0x60, 0xF0, 0x0F, 0x00, 0x00, 0xE0, 0x03, // 53 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x03, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0xC0, 0x63, 0x1C, 0x00, 0xC0, 0x30, 0x38, 0x00, 0x60, 0x18, 0x30, 0x00, 0x60, 0x18, 0x30, 0x00, 0x60, 0x18, 0x30, 0x00, 0x60, 0x18, 0x30, 0x00, 0xE0, 0x30, 0x18, 0x00, 0xC0, 0xF1, 0x0F, 0x00, 0x80, 0xC1, 0x07, // 54 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x3C, 0x00, 0x60, 0x80, 0x3F, 0x00, 0x60, 0xE0, 0x03, 0x00, 0x60, 0x78, 0x00, 0x00, 0x60, 0x0E, 0x00, 0x00, 0x60, 0x03, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x60, // 55 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x80, 0xC7, 0x1F, 0x00, 0xC0, 0x6F, 0x18, 0x00, 0xE0, 0x38, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0xE0, 0x38, 0x30, 0x00, 0xC0, 0x6F, 0x18, 0x00, 0x80, 0xC7, 0x1F, 0x00, 0x00, 0x80, 0x07, // 56 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x0C, 0x00, 0x80, 0x7F, 0x1C, 0x00, 0xC0, 0x61, 0x38, 0x00, 0x60, 0xC0, 0x30, 0x00, 0x60, 0xC0, 0x30, 0x00, 0x60, 0xC0, 0x30, 0x00, 0x60, 0xC0, 0x30, 0x00, 0x60, 0x60, 0x18, 0x00, 0xC0, 0x31, 0x1E, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0x00, 0xFE, 0x01, // 57 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, // 58 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x30, 0x03, 0x00, 0x06, 0xF0, 0x01, // 59 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0xD8, 0x00, 0x00, 0x00, 0xD8, 0x00, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x04, 0x01, 0x00, 0x00, 0x06, 0x03, 0x00, 0x00, 0x06, 0x03, 0x00, 0x00, 0x03, 0x06, // 60 0x00, 0x00, 0x00, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, // 61 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x06, 0x00, 0x00, 0x06, 0x03, 0x00, 0x00, 0x06, 0x03, 0x00, 0x00, 0x04, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0xD8, 0x00, 0x00, 0x00, 0xD8, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x20, // 62 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x60, 0x80, 0x33, 0x00, 0x60, 0xC0, 0x33, 0x00, 0x60, 0xE0, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0xC0, 0x38, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x00, 0x07, // 63 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x1E, 0xF0, 0x00, 0x00, 0x07, 0xC0, 0x01, 0x80, 0xC3, 0x87, 0x01, 0xC0, 0xF1, 0x9F, 0x03, 0xC0, 0x38, 0x18, 0x03, 0xC0, 0x0C, 0x30, 0x03, 0x60, 0x0E, 0x30, 0x06, 0x60, 0x06, 0x30, 0x06, 0x60, 0x06, 0x18, 0x06, 0x60, 0x06, 0x0C, 0x06, 0x60, 0x0C, 0x1E, 0x06, 0x60, 0xF8, 0x3F, 0x06, 0xE0, 0xFE, 0x31, 0x06, 0xC0, 0x0E, 0x30, 0x06, 0xC0, 0x01, 0x18, 0x03, 0x80, 0x03, 0x1C, 0x03, 0x00, 0x07, 0x8F, 0x01, 0x00, 0xFE, 0x87, 0x01, 0x00, 0xF8, 0xC1, 0x00, 0x00, 0x00, 0x40, // 64 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x80, 0x8F, 0x01, 0x00, 0xE0, 0x83, 0x01, 0x00, 0x60, 0x80, 0x01, 0x00, 0xE0, 0x83, 0x01, 0x00, 0x80, 0x8F, 0x01, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 65 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0xC0, 0x78, 0x30, 0x00, 0xC0, 0xFF, 0x18, 0x00, 0x80, 0xC7, 0x1F, 0x00, 0x00, 0x80, 0x07, // 66 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x03, 0x0F, 0x00, 0x00, 0x02, 0x03, // 67 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x03, 0x0E, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 68 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 69 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, // 70 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xE0, 0x00, 0x18, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x60, 0x30, 0x00, 0x60, 0x60, 0x30, 0x00, 0xE0, 0x60, 0x38, 0x00, 0xC0, 0x60, 0x18, 0x00, 0xC0, 0x61, 0x18, 0x00, 0x80, 0xE3, 0x0F, 0x00, 0x00, 0xE2, 0x0F, // 71 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 72 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 73 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x38, 0x00, 0xE0, 0xFF, 0x1F, 0x00, 0xE0, 0xFF, 0x0F, // 74 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0xE7, 0x01, 0x00, 0x80, 0x83, 0x07, 0x00, 0xC0, 0x01, 0x0F, 0x00, 0xE0, 0x00, 0x1E, 0x00, 0x60, 0x00, 0x38, 0x00, 0x20, 0x00, 0x30, 0x00, 0x00, 0x00, 0x20, // 75 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, // 76 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xFE, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 77 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 78 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x0F, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 79 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x00, 0x0F, // 80 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x0C, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xE0, 0x00, 0x18, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x36, 0x00, 0x60, 0x00, 0x36, 0x00, 0xE0, 0x00, 0x3C, 0x00, 0xC0, 0x00, 0x1C, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x3F, 0x00, 0x00, 0xFF, 0x77, 0x00, 0x00, 0xFC, 0x61, // 81 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x70, 0x00, 0x00, 0x60, 0xF0, 0x00, 0x00, 0x60, 0xF0, 0x03, 0x00, 0x60, 0xB0, 0x07, 0x00, 0xE0, 0x18, 0x1F, 0x00, 0xC0, 0x1F, 0x3C, 0x00, 0x80, 0x0F, 0x30, 0x00, 0x00, 0x00, 0x20, // 82 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x07, 0x0F, 0x00, 0xC0, 0x1F, 0x1C, 0x00, 0xC0, 0x18, 0x18, 0x00, 0x60, 0x38, 0x38, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x70, 0x30, 0x00, 0xC0, 0x60, 0x18, 0x00, 0xC0, 0xE1, 0x18, 0x00, 0x80, 0xC3, 0x0F, 0x00, 0x00, 0x83, 0x07, // 83 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, // 84 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x03, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0x03, // 85 0x20, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x20, // 86 0x60, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x80, 0xFF, 0x00, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x80, 0x1F, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x80, 0xFF, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x60, // 87 0x00, 0x00, 0x20, 0x00, 0x20, 0x00, 0x30, 0x00, 0x60, 0x00, 0x3C, 0x00, 0xE0, 0x01, 0x1E, 0x00, 0xC0, 0x83, 0x07, 0x00, 0x00, 0xCF, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x00, 0xCF, 0x03, 0x00, 0xC0, 0x03, 0x07, 0x00, 0xE0, 0x01, 0x1E, 0x00, 0x60, 0x00, 0x3C, 0x00, 0x20, 0x00, 0x30, 0x00, 0x00, 0x00, 0x20, // 88 0x20, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x20, // 89 0x00, 0x00, 0x30, 0x00, 0x60, 0x00, 0x38, 0x00, 0x60, 0x00, 0x3C, 0x00, 0x60, 0x00, 0x37, 0x00, 0x60, 0x80, 0x33, 0x00, 0x60, 0xC0, 0x31, 0x00, 0x60, 0xE0, 0x30, 0x00, 0x60, 0x38, 0x30, 0x00, 0x60, 0x1C, 0x30, 0x00, 0x60, 0x0E, 0x30, 0x00, 0x60, 0x07, 0x30, 0x00, 0xE0, 0x01, 0x30, 0x00, 0xE0, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, // 90 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x07, 0xE0, 0xFF, 0xFF, 0x07, 0x60, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0x06, // 91 0x60, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 92 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0x06, 0xE0, 0xFF, 0xFF, 0x07, 0xE0, 0xFF, 0xFF, 0x07, // 93 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x20, // 94 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, // 95 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x80, // 96 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0x00, 0x1C, 0x1F, 0x00, 0x00, 0x8C, 0x39, 0x00, 0x00, 0x86, 0x31, 0x00, 0x00, 0x86, 0x31, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x18, 0x00, 0x00, 0xCE, 0x0C, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x00, 0x20, // 97 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x18, 0x0C, 0x00, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xE0, 0x03, // 98 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x18, 0x0C, // 99 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x18, 0x0C, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 100 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xDC, 0x1C, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x00, 0xDC, 0x18, 0x00, 0x00, 0xF8, 0x0C, 0x00, 0x00, 0xF0, 0x04, // 101 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0xC0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x06, 0x00, 0x00, 0x60, 0x06, 0x00, 0x00, 0x60, 0x06, // 102 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x83, 0x01, 0x00, 0xF8, 0x8F, 0x03, 0x00, 0x1C, 0x1C, 0x07, 0x00, 0x0E, 0x38, 0x06, 0x00, 0x06, 0x30, 0x06, 0x00, 0x06, 0x30, 0x06, 0x00, 0x06, 0x30, 0x06, 0x00, 0x0C, 0x18, 0x07, 0x00, 0x18, 0x8C, 0x03, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0xFE, 0xFF, // 103 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xF8, 0x3F, // 104 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0xFE, 0x3F, 0x00, 0x60, 0xFE, 0x3F, // 105 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x60, 0xFE, 0xFF, 0x07, 0x60, 0xFE, 0xFF, 0x03, // 106 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0x98, 0x07, 0x00, 0x00, 0x0C, 0x0E, 0x00, 0x00, 0x06, 0x3C, 0x00, 0x00, 0x02, 0x30, 0x00, 0x00, 0x00, 0x20, // 107 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 108 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xF8, 0x3F, // 109 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xF8, 0x3F, // 110 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x07, // 111 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0x18, 0x0C, 0x00, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xE0, 0x03, // 112 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x18, 0x0C, 0x00, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0xFE, 0xFF, 0x07, // 113 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, // 114 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x0C, 0x00, 0x00, 0x7C, 0x1C, 0x00, 0x00, 0xEE, 0x38, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x31, 0x00, 0x00, 0xC6, 0x31, 0x00, 0x00, 0x8E, 0x39, 0x00, 0x00, 0x9C, 0x1F, 0x00, 0x00, 0x18, 0x0F, // 115 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0xC0, 0xFF, 0x1F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, // 116 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x0F, 0x00, 0x00, 0xFE, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, // 117 0x00, 0x06, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x06, // 118 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x0E, // 119 0x00, 0x02, 0x20, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x38, 0x0E, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x38, 0x0E, 0x00, 0x00, 0x1C, 0x3C, 0x00, 0x00, 0x0E, 0x30, 0x00, 0x00, 0x02, 0x20, // 120 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x06, 0x00, 0xF0, 0x01, 0x06, 0x00, 0x80, 0x0F, 0x07, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0xFC, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x06, // 121 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x06, 0x3C, 0x00, 0x00, 0x06, 0x3E, 0x00, 0x00, 0x06, 0x37, 0x00, 0x00, 0xC6, 0x33, 0x00, 0x00, 0xE6, 0x30, 0x00, 0x00, 0x76, 0x30, 0x00, 0x00, 0x3E, 0x30, 0x00, 0x00, 0x1E, 0x30, 0x00, 0x00, 0x06, 0x30, // 122 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xC0, 0x03, 0x00, 0xC0, 0x7F, 0xFE, 0x03, 0xE0, 0x3F, 0xFC, 0x07, 0x60, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0x06, // 123 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x0F, 0xE0, 0xFF, 0xFF, 0x0F, // 124 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0x06, 0xE0, 0x3F, 0xFC, 0x07, 0xC0, 0x7F, 0xFF, 0x03, 0x00, 0xC0, 0x03, 0x00, 0x00, 0x80, 0x01, // 125 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x60, // 126 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, 0x62, 0x00, 0x30, 0x00, 0x66, 0x00, 0x30, 0x00, 0x6C, 0x00, 0x30, 0x00, 0x6C, 0x00, 0x30, 0x00, 0x66, 0x00, 0x30, 0x00, 0x62, 0x00, 0x30, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x03, 0x0F, 0x00, 0x00, 0x02, 0x03, // 129 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x62, 0x00, 0x30, 0x00, 0x66, 0x00, 0x30, 0x00, 0x6C, 0x00, 0x30, 0x00, 0x6C, 0x00, 0x30, 0x00, 0x66, 0x00, 0x30, 0x00, 0xE2, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x03, 0x0E, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 130 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x62, 0x30, 0x30, 0x00, 0x66, 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x66, 0x30, 0x30, 0x00, 0x62, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 131 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x02, 0x0E, 0x00, 0x00, 0x06, 0x3C, 0x00, 0x00, 0x0C, 0x70, 0x00, 0x00, 0x0C, 0xE0, 0x01, 0x00, 0x06, 0x80, 0x03, 0x00, 0x02, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 132 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x62, 0x30, 0x00, 0x00, 0x66, 0x30, 0x00, 0x00, 0x6C, 0x70, 0x00, 0x00, 0x6C, 0xF0, 0x00, 0x00, 0x66, 0xF0, 0x03, 0x00, 0x62, 0xB0, 0x07, 0x00, 0xE0, 0x18, 0x1F, 0x00, 0xC0, 0x1F, 0x3C, 0x00, 0x80, 0x0F, 0x30, 0x00, 0x00, 0x00, 0x20, // 133 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x07, 0x0F, 0x00, 0xC0, 0x1F, 0x1C, 0x00, 0xC0, 0x18, 0x18, 0x00, 0x62, 0x38, 0x38, 0x00, 0x66, 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x66, 0x30, 0x30, 0x00, 0x62, 0x70, 0x30, 0x00, 0xC0, 0x60, 0x18, 0x00, 0xC0, 0xE1, 0x18, 0x00, 0x80, 0xC3, 0x0F, 0x00, 0x00, 0x83, 0x07, // 134 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x62, 0x00, 0x00, 0x00, 0x66, 0x00, 0x00, 0x00, 0xEC, 0xFF, 0x3F, 0x00, 0xEC, 0xFF, 0x3F, 0x00, 0x66, 0x00, 0x00, 0x00, 0x62, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, // 135 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x03, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x38, 0x00, 0x0E, 0x00, 0x30, 0x00, 0x1B, 0x00, 0x30, 0x00, 0x11, 0x00, 0x30, 0x00, 0x1B, 0x00, 0x30, 0x00, 0x0E, 0x00, 0x30, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0x03, // 136 0x00, 0x00, 0x30, 0x00, 0x60, 0x00, 0x38, 0x00, 0x60, 0x00, 0x3C, 0x00, 0x60, 0x00, 0x37, 0x00, 0x60, 0x80, 0x33, 0x00, 0x62, 0xC0, 0x31, 0x00, 0x66, 0xE0, 0x30, 0x00, 0x6C, 0x38, 0x30, 0x00, 0x6C, 0x1C, 0x30, 0x00, 0x66, 0x0E, 0x30, 0x00, 0x62, 0x07, 0x30, 0x00, 0xE0, 0x01, 0x30, 0x00, 0xE0, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, // 137 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x20, 0x0E, 0x38, 0x00, 0x60, 0x06, 0x30, 0x00, 0xC0, 0x06, 0x30, 0x00, 0xC0, 0x06, 0x30, 0x00, 0x60, 0x0E, 0x38, 0x00, 0x20, 0x1C, 0x1C, 0x00, 0x00, 0x18, 0x0C, // 138 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x18, 0x0C, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x03, 0x00, 0x00, 0xE0, 0x01, // 139 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xDC, 0x1C, 0x00, 0x20, 0xCE, 0x38, 0x00, 0x60, 0xC6, 0x30, 0x00, 0xC0, 0xC6, 0x30, 0x00, 0xC0, 0xC6, 0x30, 0x00, 0x60, 0xCE, 0x38, 0x00, 0x20, 0xDC, 0x18, 0x00, 0x00, 0xF8, 0x0C, 0x00, 0x00, 0xF0, 0x04, // 140 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x20, 0x18, 0x00, 0x00, 0x60, 0x0C, 0x00, 0x00, 0xC0, 0x06, 0x00, 0x00, 0xC0, 0x06, 0x00, 0x00, 0x60, 0x06, 0x00, 0x00, 0x20, 0x0E, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xF8, 0x3F, // 141 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0xFE, 0x3F, 0x00, 0x60, 0xFE, 0x3F, 0x00, 0xC0, 0x0C, 0x00, 0x00, 0xC0, 0x06, 0x00, 0x00, 0x60, 0x06, 0x00, 0x00, 0x20, 0x06, // 142 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x0C, 0x00, 0x00, 0x7C, 0x1C, 0x00, 0x20, 0xEE, 0x38, 0x00, 0x60, 0xC6, 0x30, 0x00, 0xC0, 0xC6, 0x30, 0x00, 0xC0, 0xC6, 0x31, 0x00, 0x60, 0xC6, 0x31, 0x00, 0x20, 0x8E, 0x39, 0x00, 0x00, 0x9C, 0x1F, 0x00, 0x00, 0x18, 0x0F, // 143 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0xC0, 0xFF, 0x1F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x03, 0x00, 0x00, 0xE0, 0x01, // 144 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x0F, 0x00, 0x00, 0xFE, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x70, 0x00, 0x30, 0x00, 0xD8, 0x00, 0x30, 0x00, 0x88, 0x00, 0x30, 0x00, 0xD8, 0x00, 0x18, 0x00, 0x70, 0x00, 0x0C, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, // 145 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x06, 0x3C, 0x00, 0x00, 0x06, 0x3E, 0x00, 0x20, 0x06, 0x37, 0x00, 0x60, 0xC6, 0x33, 0x00, 0xC0, 0xE6, 0x30, 0x00, 0xC0, 0x76, 0x30, 0x00, 0x60, 0x3E, 0x30, 0x00, 0x20, 0x1E, 0x30, 0x00, 0x00, 0x06, 0x30, // 146 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE8, 0xFF, 0x3F, 0x00, 0x0E, 0x00, 0x30, 0x00, 0x06, 0x00, 0x30, 0x00, 0x02, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, // 147 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE8, 0xFF, 0x3F, 0x00, 0xEE, 0xFF, 0x3F, 0x00, 0x06, 0x00, 0x00, 0x00, 0x02, // 148 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x60, 0x03, 0x30, 0x00, 0xE0, 0x01, 0x30, 0x00, 0x00, 0x00, 0x30, // 149 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x03, 0x00, 0x00, 0xE0, 0x01, // 150 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x68, 0x70, 0x00, 0x00, 0x6E, 0xF0, 0x00, 0x00, 0x66, 0xF0, 0x03, 0x00, 0x62, 0xB0, 0x07, 0x00, 0xE0, 0x18, 0x1F, 0x00, 0xC0, 0x1F, 0x3C, 0x00, 0x80, 0x0F, 0x30, 0x00, 0x00, 0x00, 0x20, // 151 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x80, 0x0C, 0x00, 0x00, 0xE0, 0x06, 0x00, 0x00, 0x60, 0x06, 0x00, 0x00, 0x20, 0x06, // 152 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE6, 0xFF, 0x07, 0x00, 0xE6, 0xFF, 0x07, // 161 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x9C, 0x07, 0x00, 0x0E, 0x78, 0x00, 0x00, 0x06, 0x3F, 0x00, 0x00, 0xF6, 0x30, 0x00, 0x00, 0x0E, 0x30, 0x00, 0xE0, 0x0D, 0x1C, 0x00, 0x00, 0x1C, 0x0E, 0x00, 0x00, 0x10, 0x06, // 162 0x00, 0x60, 0x10, 0x00, 0x00, 0x60, 0x38, 0x00, 0x00, 0x7F, 0x1C, 0x00, 0xC0, 0xFF, 0x1F, 0x00, 0xE0, 0xE0, 0x19, 0x00, 0x60, 0x60, 0x18, 0x00, 0x60, 0x60, 0x18, 0x00, 0x60, 0x60, 0x30, 0x00, 0xE0, 0x00, 0x30, 0x00, 0xC0, 0x01, 0x30, 0x00, 0x80, 0x01, 0x38, 0x00, 0x00, 0x00, 0x10, // 163 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x04, 0x00, 0x00, 0xF7, 0x0E, 0x00, 0x00, 0xFE, 0x07, 0x00, 0x00, 0x0C, 0x03, 0x00, 0x00, 0x06, 0x06, 0x00, 0x00, 0x06, 0x06, 0x00, 0x00, 0x06, 0x06, 0x00, 0x00, 0x06, 0x06, 0x00, 0x00, 0x0C, 0x03, 0x00, 0x00, 0xFE, 0x07, 0x00, 0x00, 0xF7, 0x0E, 0x00, 0x00, 0x02, 0x04, // 164 0xE0, 0x60, 0x06, 0x00, 0xC0, 0x61, 0x06, 0x00, 0x80, 0x67, 0x06, 0x00, 0x00, 0x7E, 0x06, 0x00, 0x00, 0x7C, 0x06, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x7C, 0x06, 0x00, 0x00, 0x7E, 0x06, 0x00, 0x80, 0x67, 0x06, 0x00, 0xC0, 0x61, 0x06, 0x00, 0xE0, 0x60, 0x06, 0x00, 0x20, // 165 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x7F, 0xF8, 0x0F, 0xE0, 0x7F, 0xF8, 0x0F, // 166 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x80, 0xF3, 0xC1, 0x00, 0xC0, 0x1F, 0xC3, 0x03, 0xE0, 0x0C, 0x07, 0x03, 0x60, 0x1C, 0x06, 0x06, 0x60, 0x18, 0x0C, 0x06, 0x60, 0x30, 0x1C, 0x06, 0xE0, 0x70, 0x38, 0x07, 0xC0, 0xE1, 0xF4, 0x03, 0x80, 0xC1, 0xE7, 0x01, 0x00, 0x80, 0x03, // 167 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, // 168 0x00, 0xF8, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0x07, 0x07, 0x00, 0x80, 0x01, 0x0C, 0x00, 0xC0, 0x79, 0x1C, 0x00, 0xC0, 0xFE, 0x19, 0x00, 0x60, 0x86, 0x31, 0x00, 0x60, 0x03, 0x33, 0x00, 0x60, 0x03, 0x33, 0x00, 0x60, 0x03, 0x33, 0x00, 0x60, 0x03, 0x33, 0x00, 0x60, 0x87, 0x33, 0x00, 0xC0, 0x86, 0x19, 0x00, 0xC0, 0x85, 0x1C, 0x00, 0x80, 0x01, 0x0C, 0x00, 0x00, 0x07, 0x07, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0xF8, // 169 0x00, 0x00, 0x00, 0x00, 0xC0, 0x1C, 0x00, 0x00, 0xE0, 0x3E, 0x00, 0x00, 0x60, 0x32, 0x00, 0x00, 0x60, 0x32, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0xC0, 0x3F, // 170 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x78, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x84, 0x10, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x78, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x04, 0x10, // 171 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFC, 0x01, // 172 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, // 173 0x00, 0xF8, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0x07, 0x07, 0x00, 0x80, 0x01, 0x0C, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0xFE, 0x1B, 0x00, 0x60, 0xFE, 0x33, 0x00, 0x60, 0x66, 0x30, 0x00, 0x60, 0x66, 0x30, 0x00, 0x60, 0xE6, 0x30, 0x00, 0x60, 0xFE, 0x31, 0x00, 0x60, 0x3C, 0x33, 0x00, 0xC0, 0x00, 0x1A, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x01, 0x0C, 0x00, 0x00, 0x07, 0x07, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0xF8, // 174 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, // 175 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x40, 0x04, 0x00, 0x00, 0x20, 0x08, 0x00, 0x00, 0x20, 0x08, 0x00, 0x00, 0x20, 0x08, 0x00, 0x00, 0x40, 0x04, 0x00, 0x00, 0x80, 0x03, // 176 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0xFF, 0x3F, 0x00, 0x00, 0xFF, 0x3F, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, // 177 0x40, 0x20, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x20, 0x38, 0x00, 0x00, 0x20, 0x2C, 0x00, 0x00, 0x20, 0x26, 0x00, 0x00, 0xE0, 0x23, 0x00, 0x00, 0xC0, 0x21, // 178 0x40, 0x10, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x20, 0x20, 0x00, 0x00, 0x20, 0x22, 0x00, 0x00, 0x20, 0x22, 0x00, 0x00, 0xE0, 0x3D, 0x00, 0x00, 0xC0, 0x1D, // 179 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x20, // 180 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, // 181 0x00, 0x0F, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0xE0, 0x7F, 0x00, 0x00, 0xE0, 0x7F, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x07, 0xE0, 0xFF, 0xFF, 0x07, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x07, 0xE0, 0xFF, 0xFF, 0x07, 0x60, 0x00, 0x00, 0x00, 0x60, // 182 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, // 183 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0xC0, 0x02, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x01, // 184 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0xE0, 0x3F, // 185 0x00, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xE0, 0x38, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0xE0, 0x38, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x80, 0x0F, // 186 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x10, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x78, 0x0F, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x84, 0x10, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x78, 0x0F, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x80, // 187 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x20, 0x00, 0xE0, 0x3F, 0x38, 0x00, 0xE0, 0x3F, 0x1C, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x07, 0x0C, 0x00, 0xC0, 0x01, 0x0E, 0x00, 0xE0, 0x80, 0x0B, 0x00, 0x60, 0xC0, 0x08, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x00, 0x08, // 188 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x20, 0x00, 0xE0, 0x3F, 0x30, 0x00, 0xE0, 0x3F, 0x1C, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x4E, 0x20, 0x00, 0x00, 0x67, 0x30, 0x00, 0xC0, 0x21, 0x38, 0x00, 0xE0, 0x20, 0x2C, 0x00, 0x60, 0x20, 0x26, 0x00, 0x00, 0xE0, 0x27, 0x00, 0x00, 0xC0, 0x21, // 189 0x40, 0x10, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x20, 0x20, 0x00, 0x00, 0x20, 0x22, 0x20, 0x00, 0x20, 0x22, 0x30, 0x00, 0xE0, 0x3D, 0x38, 0x00, 0xC0, 0x1D, 0x0E, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x0E, 0x0C, 0x00, 0x00, 0x07, 0x0E, 0x00, 0x80, 0x83, 0x0B, 0x00, 0xE0, 0xC0, 0x08, 0x00, 0x60, 0xE0, 0x3F, 0x00, 0x20, 0xE0, 0x3F, 0x00, 0x00, 0x00, 0x08, // 190 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x1E, 0x03, 0x00, 0x00, 0x07, 0x07, 0x00, 0xE6, 0x03, 0x06, 0x00, 0xE6, 0x01, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xC0, // 191 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x82, 0x8F, 0x01, 0x00, 0xE6, 0x83, 0x01, 0x00, 0x6E, 0x80, 0x01, 0x00, 0xE8, 0x83, 0x01, 0x00, 0x80, 0x8F, 0x01, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 192 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x80, 0x8F, 0x01, 0x00, 0xE8, 0x83, 0x01, 0x00, 0x6E, 0x80, 0x01, 0x00, 0xE6, 0x83, 0x01, 0x00, 0x82, 0x8F, 0x01, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 193 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x88, 0x8F, 0x01, 0x00, 0xEC, 0x83, 0x01, 0x00, 0x66, 0x80, 0x01, 0x00, 0xE6, 0x83, 0x01, 0x00, 0x8C, 0x8F, 0x01, 0x00, 0x08, 0xFE, 0x01, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 194 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x0C, 0xFE, 0x01, 0x00, 0x8E, 0x8F, 0x01, 0x00, 0xE6, 0x83, 0x01, 0x00, 0x66, 0x80, 0x01, 0x00, 0xEC, 0x83, 0x01, 0x00, 0x8C, 0x8F, 0x01, 0x00, 0x0E, 0xFE, 0x01, 0x00, 0x06, 0xF0, 0x03, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 195 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x8C, 0x8F, 0x01, 0x00, 0xEC, 0x83, 0x01, 0x00, 0x60, 0x80, 0x01, 0x00, 0xE0, 0x83, 0x01, 0x00, 0x8C, 0x8F, 0x01, 0x00, 0x0C, 0xFE, 0x01, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 196 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x9C, 0x8F, 0x01, 0x00, 0xE2, 0x83, 0x01, 0x00, 0x62, 0x80, 0x01, 0x00, 0xE2, 0x83, 0x01, 0x00, 0x9C, 0x8F, 0x01, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 197 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0xBC, 0x01, 0x00, 0x00, 0x8F, 0x01, 0x00, 0xC0, 0x83, 0x01, 0x00, 0xE0, 0x80, 0x01, 0x00, 0x60, 0x80, 0x01, 0x00, 0x60, 0x80, 0x01, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 198 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, 0x60, 0x00, 0x30, 0x02, 0x60, 0x00, 0x30, 0x02, 0x60, 0x00, 0xF0, 0x02, 0x60, 0x00, 0xB0, 0x03, 0x60, 0x00, 0x30, 0x01, 0x60, 0x00, 0x30, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x03, 0x0F, 0x00, 0x00, 0x02, 0x03, // 199 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x62, 0x30, 0x30, 0x00, 0x66, 0x30, 0x30, 0x00, 0x6E, 0x30, 0x30, 0x00, 0x68, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 200 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x68, 0x30, 0x30, 0x00, 0x6E, 0x30, 0x30, 0x00, 0x66, 0x30, 0x30, 0x00, 0x62, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 201 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x68, 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x66, 0x30, 0x30, 0x00, 0x66, 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x68, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 202 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 203 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0xE6, 0xFF, 0x3F, 0x00, 0xEE, 0xFF, 0x3F, 0x00, 0x08, // 204 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0xEE, 0xFF, 0x3F, 0x00, 0xE6, 0xFF, 0x3F, 0x00, 0x02, // 205 0x08, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0xE6, 0xFF, 0x3F, 0x00, 0xE6, 0xFF, 0x3F, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x08, // 206 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, // 207 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x03, 0x0E, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 208 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x8C, 0x03, 0x00, 0x00, 0x0E, 0x0E, 0x00, 0x00, 0x06, 0x3C, 0x00, 0x00, 0x06, 0x70, 0x00, 0x00, 0x0C, 0xE0, 0x01, 0x00, 0x0C, 0x80, 0x03, 0x00, 0x0E, 0x00, 0x0F, 0x00, 0x06, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 209 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x62, 0x00, 0x30, 0x00, 0x66, 0x00, 0x30, 0x00, 0x6E, 0x00, 0x30, 0x00, 0x68, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x0F, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 210 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x60, 0x00, 0x30, 0x00, 0x68, 0x00, 0x30, 0x00, 0x6E, 0x00, 0x30, 0x00, 0x66, 0x00, 0x30, 0x00, 0x62, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x0F, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 211 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x68, 0x00, 0x30, 0x00, 0x6C, 0x00, 0x30, 0x00, 0x66, 0x00, 0x30, 0x00, 0x66, 0x00, 0x30, 0x00, 0x6C, 0x00, 0x30, 0x00, 0xE8, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x0F, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 212 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xCC, 0x00, 0x18, 0x00, 0xEE, 0x00, 0x38, 0x00, 0x66, 0x00, 0x30, 0x00, 0x66, 0x00, 0x30, 0x00, 0x6C, 0x00, 0x30, 0x00, 0x6C, 0x00, 0x30, 0x00, 0x6E, 0x00, 0x30, 0x00, 0xE6, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x0F, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 213 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x6C, 0x00, 0x30, 0x00, 0x6C, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x6C, 0x00, 0x30, 0x00, 0xEC, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x0F, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 214 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x03, 0x00, 0x00, 0x8E, 0x03, 0x00, 0x00, 0xDC, 0x01, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0xDC, 0x01, 0x00, 0x00, 0x8E, 0x03, 0x00, 0x00, 0x06, 0x03, // 215 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x21, 0x00, 0x00, 0xFF, 0x77, 0x00, 0x80, 0x07, 0x3F, 0x00, 0xC0, 0x01, 0x1E, 0x00, 0xC0, 0x00, 0x1F, 0x00, 0xE0, 0x80, 0x3B, 0x00, 0x60, 0xC0, 0x31, 0x00, 0x60, 0xE0, 0x30, 0x00, 0x60, 0x70, 0x30, 0x00, 0x60, 0x38, 0x30, 0x00, 0x60, 0x1C, 0x30, 0x00, 0xE0, 0x0E, 0x38, 0x00, 0xC0, 0x07, 0x18, 0x00, 0xC0, 0x03, 0x1C, 0x00, 0xE0, 0x07, 0x0F, 0x00, 0x70, 0xFF, 0x07, 0x00, 0x20, 0xFC, 0x01, // 216 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x03, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x38, 0x00, 0x02, 0x00, 0x30, 0x00, 0x06, 0x00, 0x30, 0x00, 0x0E, 0x00, 0x30, 0x00, 0x08, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0x03, // 217 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x03, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x30, 0x00, 0x08, 0x00, 0x30, 0x00, 0x0E, 0x00, 0x30, 0x00, 0x06, 0x00, 0x30, 0x00, 0x02, 0x00, 0x30, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0x03, // 218 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x03, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x38, 0x00, 0x08, 0x00, 0x30, 0x00, 0x0C, 0x00, 0x30, 0x00, 0x06, 0x00, 0x30, 0x00, 0x06, 0x00, 0x30, 0x00, 0x0C, 0x00, 0x30, 0x00, 0x08, 0x00, 0x38, 0x00, 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0x03, // 219 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x03, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x38, 0x00, 0x0C, 0x00, 0x30, 0x00, 0x0C, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x0C, 0x00, 0x30, 0x00, 0x0C, 0x00, 0x38, 0x00, 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0x03, // 220 0x20, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x08, 0xF0, 0x3F, 0x00, 0x0E, 0xF0, 0x3F, 0x00, 0x06, 0x3C, 0x00, 0x00, 0x02, 0x1E, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x20, // 221 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x03, 0x06, 0x00, 0x00, 0x03, 0x06, 0x00, 0x00, 0x03, 0x06, 0x00, 0x00, 0x03, 0x06, 0x00, 0x00, 0x03, 0x06, 0x00, 0x00, 0x03, 0x06, 0x00, 0x00, 0x03, 0x06, 0x00, 0x00, 0x03, 0x07, 0x00, 0x00, 0x86, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x00, 0xF8, // 222 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0x3F, 0x00, 0xC0, 0xFF, 0x3F, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x60, 0x00, 0x08, 0x00, 0x60, 0x00, 0x1C, 0x00, 0x60, 0x00, 0x38, 0x00, 0xE0, 0x78, 0x30, 0x00, 0xC0, 0x7F, 0x30, 0x00, 0x80, 0xC7, 0x30, 0x00, 0x00, 0x80, 0x39, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0x0F, // 223 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0x00, 0x1C, 0x1F, 0x00, 0x00, 0x8C, 0x39, 0x00, 0x20, 0x86, 0x31, 0x00, 0x60, 0x86, 0x31, 0x00, 0xE0, 0xC6, 0x30, 0x00, 0x80, 0xC6, 0x18, 0x00, 0x00, 0xCE, 0x0C, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x00, 0x20, // 224 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0x00, 0x1C, 0x1F, 0x00, 0x00, 0x8C, 0x39, 0x00, 0x00, 0x86, 0x31, 0x00, 0x80, 0x86, 0x31, 0x00, 0xE0, 0xC6, 0x30, 0x00, 0x60, 0xC6, 0x18, 0x00, 0x20, 0xCE, 0x0C, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x00, 0x20, // 225 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0x00, 0x1C, 0x1F, 0x00, 0x80, 0x8C, 0x39, 0x00, 0xC0, 0x86, 0x31, 0x00, 0x60, 0x86, 0x31, 0x00, 0x60, 0xC6, 0x30, 0x00, 0xC0, 0xC6, 0x18, 0x00, 0x80, 0xCE, 0x0C, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x00, 0x20, // 226 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0xC0, 0x1C, 0x1F, 0x00, 0xE0, 0x8C, 0x39, 0x00, 0x60, 0x86, 0x31, 0x00, 0x60, 0x86, 0x31, 0x00, 0xC0, 0xC6, 0x30, 0x00, 0xC0, 0xC6, 0x18, 0x00, 0xE0, 0xCE, 0x0C, 0x00, 0x60, 0xFC, 0x1F, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x00, 0x20, // 227 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0x00, 0x1C, 0x1F, 0x00, 0xC0, 0x8C, 0x39, 0x00, 0xC0, 0x86, 0x31, 0x00, 0x00, 0x86, 0x31, 0x00, 0x00, 0xC6, 0x30, 0x00, 0xC0, 0xC6, 0x18, 0x00, 0xC0, 0xCE, 0x0C, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x00, 0x20, // 228 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0x00, 0x1C, 0x1F, 0x00, 0x00, 0x8C, 0x39, 0x00, 0x70, 0x86, 0x31, 0x00, 0x88, 0x86, 0x31, 0x00, 0x88, 0xC6, 0x30, 0x00, 0x88, 0xC6, 0x18, 0x00, 0x70, 0xCE, 0x0C, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x00, 0x20, // 229 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x0F, 0x00, 0x00, 0x9C, 0x1F, 0x00, 0x00, 0xCC, 0x39, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0x66, 0x18, 0x00, 0x00, 0x6E, 0x1C, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xCC, 0x1C, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xCC, 0x18, 0x00, 0x00, 0xF8, 0x0C, 0x00, 0x00, 0xE0, 0x04, // 230 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x02, 0x00, 0x06, 0x30, 0x02, 0x00, 0x06, 0xF0, 0x02, 0x00, 0x06, 0xB0, 0x03, 0x00, 0x0E, 0x38, 0x01, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x18, 0x0C, // 231 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xDC, 0x1C, 0x00, 0x20, 0xCE, 0x38, 0x00, 0x60, 0xC6, 0x30, 0x00, 0xE0, 0xC6, 0x30, 0x00, 0x80, 0xC6, 0x30, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x00, 0xDC, 0x18, 0x00, 0x00, 0xF8, 0x0C, 0x00, 0x00, 0xF0, 0x04, // 232 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xDC, 0x1C, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x80, 0xC6, 0x30, 0x00, 0xE0, 0xC6, 0x30, 0x00, 0x60, 0xC6, 0x30, 0x00, 0x20, 0xCE, 0x38, 0x00, 0x00, 0xDC, 0x18, 0x00, 0x00, 0xF8, 0x0C, 0x00, 0x00, 0xF0, 0x04, // 233 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xDC, 0x1C, 0x00, 0x80, 0xCE, 0x38, 0x00, 0xC0, 0xC6, 0x30, 0x00, 0x60, 0xC6, 0x30, 0x00, 0x60, 0xC6, 0x30, 0x00, 0xC0, 0xCE, 0x38, 0x00, 0x80, 0xDC, 0x18, 0x00, 0x00, 0xF8, 0x0C, 0x00, 0x00, 0xF0, 0x04, // 234 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xDC, 0x1C, 0x00, 0xC0, 0xCE, 0x38, 0x00, 0xC0, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0xC0, 0xCE, 0x38, 0x00, 0xC0, 0xDC, 0x18, 0x00, 0x00, 0xF8, 0x0C, 0x00, 0x00, 0xF0, 0x04, // 235 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x60, 0xFE, 0x3F, 0x00, 0xE0, 0xFE, 0x3F, 0x00, 0x80, // 236 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xE0, 0xFE, 0x3F, 0x00, 0x60, 0xFE, 0x3F, 0x00, 0x20, // 237 0x80, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x60, 0xFE, 0x3F, 0x00, 0x60, 0xFE, 0x3F, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x80, // 238 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, // 239 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1D, 0x1C, 0x00, 0xA0, 0x0F, 0x38, 0x00, 0xA0, 0x06, 0x30, 0x00, 0xE0, 0x06, 0x30, 0x00, 0xC0, 0x06, 0x30, 0x00, 0xC0, 0x0F, 0x38, 0x00, 0x20, 0x1F, 0x1C, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x00, 0xE0, 0x07, // 240 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0xC0, 0xFE, 0x3F, 0x00, 0xE0, 0x18, 0x00, 0x00, 0x60, 0x0C, 0x00, 0x00, 0x60, 0x06, 0x00, 0x00, 0xC0, 0x06, 0x00, 0x00, 0xC0, 0x06, 0x00, 0x00, 0xE0, 0x0E, 0x00, 0x00, 0x60, 0xFC, 0x3F, 0x00, 0x00, 0xF8, 0x3F, // 241 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x20, 0x0E, 0x38, 0x00, 0x60, 0x06, 0x30, 0x00, 0xE0, 0x06, 0x30, 0x00, 0x80, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x07, // 242 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x80, 0x06, 0x30, 0x00, 0xE0, 0x06, 0x30, 0x00, 0x60, 0x06, 0x30, 0x00, 0x20, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x07, // 243 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x80, 0x0E, 0x38, 0x00, 0xC0, 0x06, 0x30, 0x00, 0x60, 0x06, 0x30, 0x00, 0x60, 0x06, 0x30, 0x00, 0xC0, 0x0E, 0x38, 0x00, 0x80, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x07, // 244 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0xC0, 0x1C, 0x1C, 0x00, 0xE0, 0x0E, 0x38, 0x00, 0x60, 0x06, 0x30, 0x00, 0x60, 0x06, 0x30, 0x00, 0xC0, 0x06, 0x30, 0x00, 0xC0, 0x0E, 0x38, 0x00, 0xE0, 0x1C, 0x1C, 0x00, 0x60, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x07, // 245 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0xC0, 0x0E, 0x38, 0x00, 0xC0, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0xC0, 0x0E, 0x38, 0x00, 0xC0, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x07, // 246 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0xB6, 0x01, 0x00, 0x00, 0xB6, 0x01, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, // 247 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x67, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x3F, 0x00, 0x00, 0x86, 0x33, 0x00, 0x00, 0xE6, 0x31, 0x00, 0x00, 0x76, 0x30, 0x00, 0x00, 0x3E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xFF, 0x0F, 0x00, 0x00, 0xF3, 0x07, // 248 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x0F, 0x00, 0x00, 0xFE, 0x1F, 0x00, 0x20, 0x00, 0x38, 0x00, 0x60, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x30, 0x00, 0x80, 0x00, 0x30, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, // 249 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x0F, 0x00, 0x00, 0xFE, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x30, 0x00, 0x80, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x30, 0x00, 0x60, 0x00, 0x18, 0x00, 0x20, 0x00, 0x0C, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, // 250 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x0F, 0x00, 0x00, 0xFE, 0x1F, 0x00, 0x80, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xC0, 0x00, 0x18, 0x00, 0x80, 0x00, 0x0C, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, // 251 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x0F, 0x00, 0x00, 0xFE, 0x1F, 0x00, 0xC0, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x00, 0x0C, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, // 252 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x06, 0x00, 0xF0, 0x01, 0x06, 0x00, 0x80, 0x0F, 0x07, 0x80, 0x00, 0xFE, 0x03, 0xE0, 0x00, 0xFC, 0x00, 0x60, 0xC0, 0x1F, 0x00, 0x20, 0xF8, 0x03, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x06, // 253 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x07, 0xE0, 0xFF, 0xFF, 0x07, 0x00, 0x1C, 0x18, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x03, // 254 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x06, 0xC0, 0xF0, 0x01, 0x06, 0xC0, 0x80, 0x0F, 0x07, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0xFC, 0x00, 0xC0, 0xC0, 0x1F, 0x00, 0xC0, 0xF8, 0x03, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x06, // 255 }; #endif // OLED_CS ================================================ FILE: src/graphics/fonts/OLEDDisplayFontsCS.h ================================================ #ifndef OLEDDISPLAYFONTSCS_h #define OLEDDISPLAYFONTSCS_h #ifdef ARDUINO #include #elif __MBED__ #define PROGMEM #endif /** * Localization for Czech and Slovak language containing glyphs with diacritic. */ extern const uint8_t ArialMT_Plain_10_CS[] PROGMEM; extern const uint8_t ArialMT_Plain_16_CS[] PROGMEM; extern const uint8_t ArialMT_Plain_24_CS[] PROGMEM; #endif ================================================ FILE: src/graphics/fonts/OLEDDisplayFontsGR.cpp ================================================ #ifdef OLED_GR #include "OLEDDisplayFontsGR.h" /** * Greek font for OLED displays - ArialMT Plain 10pt * Contains ASCII 32-127 + Greek characters mapped to CP-1253 positions (192-254) * * Generated using ThingPulse OLED font converter * Font: Arial, Size: 10px * Character set: Basic Latin + Greek (Α-Ω, α-ω, accented) * * CP-1253 Greek character mapping: * 193-209: Α Β Γ Δ Ε Ζ Η Θ Ι Κ Λ Μ Ν Ξ Ο Π Ρ * 211-217: Σ Τ Υ Φ Χ Ψ Ω * 225-241: α β γ δ ε ζ η θ ι κ λ μ ν ξ ο π ρ * 242-249: ς σ τ υ φ χ ψ ω */ const uint8_t ArialMT_Plain_10_GR[] PROGMEM = { 0x0A, // Width: 10 0x0D, // Height: 13 0x20, // First char: 32 0xE0, // Number of chars: 224 // Jump Table (4 bytes per character: offset high, offset low, size, width) // Characters 32-127: Standard ASCII 0xFF, 0xFF, 0x00, 0x03, // 32 space 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 0 0x00, 0x6F, 0x08, 0x05, // 49 1 0x00, 0x77, 0x0A, 0x06, // 50 2 0x00, 0x81, 0x0A, 0x06, // 51 3 0x00, 0x8B, 0x0B, 0x07, // 52 4 0x00, 0x96, 0x0A, 0x06, // 53 5 0x00, 0xA0, 0x0A, 0x06, // 54 6 0x00, 0xAA, 0x09, 0x06, // 55 7 0x00, 0xB3, 0x0A, 0x06, // 56 8 0x00, 0xBD, 0x0A, 0x06, // 57 9 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 A 0x01, 0x18, 0x0C, 0x07, // 66 B 0x01, 0x24, 0x0C, 0x07, // 67 C 0x01, 0x30, 0x0B, 0x07, // 68 D 0x01, 0x3B, 0x0C, 0x07, // 69 E 0x01, 0x47, 0x09, 0x06, // 70 F 0x01, 0x50, 0x0D, 0x08, // 71 G 0x01, 0x5D, 0x0C, 0x07, // 72 H 0x01, 0x69, 0x04, 0x03, // 73 I 0x01, 0x6D, 0x08, 0x05, // 74 J 0x01, 0x75, 0x0E, 0x08, // 75 K 0x01, 0x83, 0x0C, 0x07, // 76 L 0x01, 0x8F, 0x10, 0x09, // 77 M 0x01, 0x9F, 0x0C, 0x07, // 78 N 0x01, 0xAB, 0x0E, 0x08, // 79 O 0x01, 0xB9, 0x0B, 0x07, // 80 P 0x01, 0xC4, 0x0E, 0x08, // 81 Q 0x01, 0xD2, 0x0C, 0x07, // 82 R 0x01, 0xDE, 0x0C, 0x07, // 83 S 0x01, 0xEA, 0x0B, 0x07, // 84 T 0x01, 0xF5, 0x0C, 0x07, // 85 U 0x02, 0x01, 0x0D, 0x08, // 86 V 0x02, 0x0E, 0x11, 0x0A, // 87 W 0x02, 0x1F, 0x0E, 0x08, // 88 X 0x02, 0x2D, 0x0D, 0x08, // 89 Y 0x02, 0x3A, 0x0C, 0x07, // 90 Z 0x02, 0x46, 0x06, 0x04, // 91 [ 0x02, 0x4C, 0x06, 0x04, // 92 backslash 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 a 0x02, 0x78, 0x0A, 0x06, // 98 b 0x02, 0x82, 0x0A, 0x06, // 99 c 0x02, 0x8C, 0x0A, 0x06, // 100 d 0x02, 0x96, 0x0A, 0x06, // 101 e 0x02, 0xA0, 0x05, 0x04, // 102 f 0x02, 0xA5, 0x0A, 0x06, // 103 g 0x02, 0xAF, 0x0A, 0x06, // 104 h 0x02, 0xB9, 0x04, 0x03, // 105 i 0x02, 0xBD, 0x04, 0x03, // 106 j 0x02, 0xC1, 0x08, 0x05, // 107 k 0x02, 0xC9, 0x04, 0x03, // 108 l 0x02, 0xCD, 0x10, 0x09, // 109 m 0x02, 0xDD, 0x0A, 0x06, // 110 n 0x02, 0xE7, 0x0A, 0x06, // 111 o 0x02, 0xF1, 0x0A, 0x06, // 112 p 0x02, 0xFB, 0x0A, 0x06, // 113 q 0x03, 0x05, 0x05, 0x04, // 114 r 0x03, 0x0A, 0x08, 0x05, // 115 s 0x03, 0x12, 0x06, 0x04, // 116 t 0x03, 0x18, 0x0A, 0x06, // 117 u 0x03, 0x22, 0x09, 0x06, // 118 v 0x03, 0x2B, 0x0E, 0x08, // 119 w 0x03, 0x39, 0x0A, 0x06, // 120 x 0x03, 0x43, 0x09, 0x06, // 121 y 0x03, 0x4C, 0x0A, 0x06, // 122 z 0x03, 0x56, 0x06, 0x04, // 123 { 0x03, 0x5C, 0x04, 0x03, // 124 | 0x03, 0x60, 0x05, 0x04, // 125 } 0x03, 0x65, 0x09, 0x06, // 126 ~ 0xFF, 0xFF, 0x00, 0x03, // 127 // Characters 128-191: Placeholders (extended ASCII) 0xFF, 0xFF, 0x00, 0x03, // 128 0xFF, 0xFF, 0x00, 0x03, // 129 0xFF, 0xFF, 0x00, 0x03, // 130 0xFF, 0xFF, 0x00, 0x03, // 131 0xFF, 0xFF, 0x00, 0x03, // 132 0xFF, 0xFF, 0x00, 0x03, // 133 0xFF, 0xFF, 0x00, 0x03, // 134 0xFF, 0xFF, 0x00, 0x03, // 135 0xFF, 0xFF, 0x00, 0x03, // 136 0xFF, 0xFF, 0x00, 0x03, // 137 0xFF, 0xFF, 0x00, 0x03, // 138 0xFF, 0xFF, 0x00, 0x03, // 139 0xFF, 0xFF, 0x00, 0x03, // 140 0xFF, 0xFF, 0x00, 0x03, // 141 0xFF, 0xFF, 0x00, 0x03, // 142 0xFF, 0xFF, 0x00, 0x03, // 143 0xFF, 0xFF, 0x00, 0x03, // 144 0xFF, 0xFF, 0x00, 0x03, // 145 0xFF, 0xFF, 0x00, 0x03, // 146 0xFF, 0xFF, 0x00, 0x03, // 147 0xFF, 0xFF, 0x00, 0x03, // 148 0xFF, 0xFF, 0x00, 0x03, // 149 0xFF, 0xFF, 0x00, 0x03, // 150 0xFF, 0xFF, 0x00, 0x03, // 151 0xFF, 0xFF, 0x00, 0x03, // 152 0xFF, 0xFF, 0x00, 0x03, // 153 0xFF, 0xFF, 0x00, 0x03, // 154 0xFF, 0xFF, 0x00, 0x03, // 155 0xFF, 0xFF, 0x00, 0x03, // 156 0xFF, 0xFF, 0x00, 0x03, // 157 0xFF, 0xFF, 0x00, 0x03, // 158 0xFF, 0xFF, 0x00, 0x03, // 159 0xFF, 0xFF, 0x00, 0x03, // 160 0xFF, 0xFF, 0x00, 0x03, // 161 0xFF, 0xFF, 0x00, 0x03, // 162 0xFF, 0xFF, 0x00, 0x03, // 163 0xFF, 0xFF, 0x00, 0x03, // 164 0xFF, 0xFF, 0x00, 0x03, // 165 0xFF, 0xFF, 0x00, 0x03, // 166 0xFF, 0xFF, 0x00, 0x03, // 167 0xFF, 0xFF, 0x00, 0x03, // 168 0xFF, 0xFF, 0x00, 0x03, // 169 0xFF, 0xFF, 0x00, 0x03, // 170 0xFF, 0xFF, 0x00, 0x03, // 171 0xFF, 0xFF, 0x00, 0x03, // 172 0xFF, 0xFF, 0x00, 0x03, // 173 0xFF, 0xFF, 0x00, 0x03, // 174 0xFF, 0xFF, 0x00, 0x03, // 175 0xFF, 0xFF, 0x00, 0x03, // 176 0xFF, 0xFF, 0x00, 0x03, // 177 0xFF, 0xFF, 0x00, 0x03, // 178 0xFF, 0xFF, 0x00, 0x03, // 179 0xFF, 0xFF, 0x00, 0x03, // 180 0xFF, 0xFF, 0x00, 0x03, // 181 0xFF, 0xFF, 0x00, 0x03, // 182 0xFF, 0xFF, 0x00, 0x03, // 183 0xFF, 0xFF, 0x00, 0x03, // 184 0xFF, 0xFF, 0x00, 0x03, // 185 0xFF, 0xFF, 0x00, 0x03, // 186 0xFF, 0xFF, 0x00, 0x03, // 187 0xFF, 0xFF, 0x00, 0x03, // 188 0xFF, 0xFF, 0x00, 0x03, // 189 0xFF, 0xFF, 0x00, 0x03, // 190 0xFF, 0xFF, 0x00, 0x03, // 191 // Characters 192-255: Greek letters (CP-1253 positions) 0xFF, 0xFF, 0x00, 0x03, // 192 (unused) 0x03, 0x6E, 0x0E, 0x08, // 193 Α Alpha 0x03, 0x7C, 0x0C, 0x07, // 194 Β Beta 0x03, 0x88, 0x09, 0x06, // 195 Γ Gamma 0x03, 0x91, 0x0C, 0x07, // 196 Δ Delta 0x03, 0x9D, 0x0C, 0x07, // 197 Ε Epsilon 0x03, 0xA9, 0x0A, 0x06, // 198 Ζ Zeta 0x03, 0xB3, 0x0C, 0x07, // 199 Η Eta 0x03, 0xBF, 0x0E, 0x08, // 200 Θ Theta 0x03, 0xCD, 0x04, 0x03, // 201 Ι Iota 0x03, 0xD1, 0x0E, 0x08, // 202 Κ Kappa 0x03, 0xDF, 0x0E, 0x08, // 203 Λ Lambda 0x03, 0xED, 0x10, 0x09, // 204 Μ Mu 0x03, 0xFD, 0x0C, 0x07, // 205 Ν Nu 0x04, 0x09, 0x0C, 0x07, // 206 Ξ Xi 0x04, 0x15, 0x0E, 0x08, // 207 Ο Omicron 0x04, 0x23, 0x0C, 0x07, // 208 Π Pi 0x04, 0x2F, 0x0B, 0x07, // 209 Ρ Rho 0xFF, 0xFF, 0x00, 0x03, // 210 (unused) 0x04, 0x3A, 0x0C, 0x07, // 211 Σ Sigma 0x04, 0x46, 0x0B, 0x07, // 212 Τ Tau 0x04, 0x51, 0x0D, 0x08, // 213 Υ Upsilon 0x04, 0x5E, 0x0E, 0x08, // 214 Φ Phi 0x04, 0x6C, 0x0E, 0x08, // 215 Χ Chi 0x04, 0x7A, 0x0E, 0x08, // 216 Ψ Psi 0x04, 0x88, 0x0E, 0x08, // 217 Ω Omega 0xFF, 0xFF, 0x00, 0x03, // 218 0xFF, 0xFF, 0x00, 0x03, // 219 0xFF, 0xFF, 0x00, 0x03, // 220 0xFF, 0xFF, 0x00, 0x03, // 221 0xFF, 0xFF, 0x00, 0x03, // 222 0xFF, 0xFF, 0x00, 0x03, // 223 0xFF, 0xFF, 0x00, 0x03, // 224 0x04, 0x96, 0x0A, 0x06, // 225 α alpha 0x04, 0xA0, 0x0A, 0x06, // 226 β beta 0x04, 0xAA, 0x09, 0x06, // 227 γ gamma 0x04, 0xB3, 0x0A, 0x06, // 228 δ delta 0x04, 0xBD, 0x08, 0x05, // 229 ε epsilon 0x04, 0xC5, 0x08, 0x05, // 230 ζ zeta 0x04, 0xCD, 0x0A, 0x06, // 231 η eta 0x04, 0xD7, 0x0A, 0x06, // 232 θ theta 0x04, 0xE1, 0x04, 0x03, // 233 ι iota 0x04, 0xE5, 0x08, 0x05, // 234 κ kappa 0x04, 0xED, 0x0A, 0x06, // 235 λ lambda 0x04, 0xF7, 0x0A, 0x06, // 236 μ mu 0x05, 0x01, 0x08, 0x05, // 237 ν nu 0x05, 0x09, 0x0A, 0x06, // 238 ξ xi 0x05, 0x13, 0x0A, 0x06, // 239 ο omicron 0x05, 0x1D, 0x0A, 0x06, // 240 π pi 0x05, 0x27, 0x0A, 0x06, // 241 ρ rho 0x05, 0x31, 0x08, 0x05, // 242 ς final sigma 0x05, 0x39, 0x0A, 0x06, // 243 σ sigma 0x05, 0x43, 0x06, 0x04, // 244 τ tau 0x05, 0x49, 0x0A, 0x06, // 245 υ upsilon 0x05, 0x53, 0x0C, 0x07, // 246 φ phi 0x05, 0x5F, 0x0A, 0x06, // 247 χ chi 0x05, 0x69, 0x0C, 0x07, // 248 ψ psi 0x05, 0x75, 0x0C, 0x07, // 249 ω omega 0xFF, 0xFF, 0x00, 0x03, // 250 0xFF, 0xFF, 0x00, 0x03, // 251 0xFF, 0xFF, 0x00, 0x03, // 252 0xFF, 0xFF, 0x00, 0x03, // 253 0xFF, 0xFF, 0x00, 0x03, // 254 0xFF, 0xFF, 0x00, 0x03, // 255 // Font Data - Basic ASCII (32-127) 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 0 0x00, 0x00, 0x20, 0x00, 0x10, 0x00, 0xF8, 0x03, // 49 1 0x10, 0x02, 0x08, 0x03, 0x88, 0x02, 0x48, 0x02, 0x30, 0x02, // 50 2 0x10, 0x01, 0x08, 0x02, 0x48, 0x02, 0x48, 0x02, 0xB0, 0x01, // 51 3 0xC0, 0x00, 0xA0, 0x00, 0x90, 0x00, 0x88, 0x00, 0xF8, 0x03, 0x80, // 52 4 0x60, 0x01, 0x38, 0x02, 0x28, 0x02, 0x28, 0x02, 0xC8, 0x01, // 53 5 0xF0, 0x01, 0x28, 0x02, 0x28, 0x02, 0x28, 0x02, 0xD0, 0x01, // 54 6 0x08, 0x00, 0x08, 0x03, 0xC8, 0x00, 0x38, 0x00, 0x08, // 55 7 0xB0, 0x01, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0xB0, 0x01, // 56 8 0x70, 0x01, 0x88, 0x02, 0x88, 0x02, 0x88, 0x02, 0xF0, 0x01, // 57 9 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 A 0x00, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0xF0, 0x01, // 66 B 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0x10, 0x01, // 67 C 0x00, 0x00, 0xF8, 0x03, 0x08, 0x02, 0x08, 0x02, 0x10, 0x01, 0xE0, // 68 D 0x00, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, // 69 E 0x00, 0x00, 0xF8, 0x03, 0x48, 0x00, 0x48, 0x00, 0x08, // 70 F 0x00, 0x00, 0xE0, 0x00, 0x10, 0x01, 0x08, 0x02, 0x48, 0x02, 0x50, 0x01, 0xC0, // 71 G 0x00, 0x00, 0xF8, 0x03, 0x40, 0x00, 0x40, 0x00, 0x40, 0x00, 0xF8, 0x03, // 72 H 0x00, 0x00, 0xF8, 0x03, // 73 I 0x00, 0x03, 0x00, 0x02, 0x00, 0x02, 0xF8, 0x01, // 74 J 0x00, 0x00, 0xF8, 0x03, 0x80, 0x00, 0x60, 0x00, 0x90, 0x00, 0x08, 0x01, 0x00, 0x02, // 75 K 0x00, 0x00, 0xF8, 0x03, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, // 76 L 0x00, 0x00, 0xF8, 0x03, 0x30, 0x00, 0xC0, 0x01, 0x00, 0x02, 0xC0, 0x01, 0x30, 0x00, 0xF8, 0x03, // 77 M 0x00, 0x00, 0xF8, 0x03, 0x30, 0x00, 0x40, 0x00, 0x80, 0x01, 0xF8, 0x03, // 78 N 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0xF0, 0x01, // 79 O 0x00, 0x00, 0xF8, 0x03, 0x48, 0x00, 0x48, 0x00, 0x48, 0x00, 0x30, // 80 P 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x03, 0x08, 0x03, 0xF0, 0x02, // 81 Q 0x00, 0x00, 0xF8, 0x03, 0x48, 0x00, 0x48, 0x00, 0xC8, 0x00, 0x30, 0x03, // 82 R 0x00, 0x00, 0x30, 0x01, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0x90, 0x01, // 83 S 0x00, 0x00, 0x08, 0x00, 0x08, 0x00, 0xF8, 0x03, 0x08, 0x00, 0x08, // 84 T 0x00, 0x00, 0xF8, 0x01, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0xF8, 0x01, // 85 U 0x08, 0x00, 0x70, 0x00, 0x80, 0x01, 0x00, 0x02, 0x80, 0x01, 0x70, 0x00, 0x08, // 86 V 0x18, 0x00, 0xE0, 0x01, 0x00, 0x02, 0xF0, 0x01, 0x08, 0x00, 0xF0, 0x01, 0x00, 0x02, 0xE0, 0x01, 0x18, // 87 W 0x00, 0x02, 0x08, 0x01, 0x90, 0x00, 0x60, 0x00, 0x90, 0x00, 0x08, 0x01, 0x00, 0x02, // 88 X 0x08, 0x00, 0x10, 0x00, 0x20, 0x00, 0xC0, 0x03, 0x20, 0x00, 0x10, 0x00, 0x08, // 89 Y 0x08, 0x03, 0x88, 0x02, 0xC8, 0x02, 0x68, 0x02, 0x38, 0x02, 0x18, 0x02, // 90 Z 0x00, 0x00, 0xF8, 0x0F, 0x08, 0x08, // 91 [ 0x18, 0x00, 0xE0, 0x00, 0x00, 0x03, // 92 backslash 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 a 0x00, 0x00, 0xF8, 0x03, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // 98 b 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0x40, 0x01, // 99 c 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0xF8, 0x03, // 100 d 0x00, 0x00, 0xC0, 0x01, 0xA0, 0x02, 0xA0, 0x02, 0xC0, 0x02, // 101 e 0x20, 0x00, 0xF0, 0x03, 0x28, // 102 f 0x00, 0x00, 0xC0, 0x05, 0x20, 0x0A, 0x20, 0x0A, 0xE0, 0x07, // 103 g 0x00, 0x00, 0xF8, 0x03, 0x20, 0x00, 0x20, 0x00, 0xC0, 0x03, // 104 h 0x00, 0x00, 0xE8, 0x03, // 105 i 0x00, 0x08, 0xE8, 0x07, // 106 j 0xF8, 0x03, 0x80, 0x00, 0xC0, 0x01, 0x20, 0x02, // 107 k 0x00, 0x00, 0xF8, 0x03, // 108 l 0x00, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0xC0, 0x03, // 109 m 0x00, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0xC0, 0x03, // 110 n 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // 111 o 0x00, 0x00, 0xE0, 0x0F, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // 112 p 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0xE0, 0x0F, // 113 q 0x00, 0x00, 0xE0, 0x03, 0x20, // 114 r 0x40, 0x02, 0xA0, 0x02, 0xA0, 0x02, 0x20, 0x01, // 115 s 0x20, 0x00, 0xF8, 0x03, 0x20, 0x02, // 116 t 0x00, 0x00, 0xE0, 0x01, 0x00, 0x02, 0x00, 0x02, 0xE0, 0x03, // 117 u 0x20, 0x00, 0xC0, 0x01, 0x00, 0x02, 0xC0, 0x01, 0x20, // 118 v 0xE0, 0x01, 0x00, 0x02, 0xC0, 0x01, 0x20, 0x00, 0xC0, 0x01, 0x00, 0x02, 0xE0, 0x01, // 119 w 0x20, 0x02, 0x40, 0x01, 0x80, 0x00, 0x40, 0x01, 0x20, 0x02, // 120 x 0x20, 0x00, 0xC0, 0x09, 0x00, 0x06, 0xC0, 0x01, 0x20, // 121 y 0x20, 0x02, 0x20, 0x03, 0xA0, 0x02, 0x60, 0x02, 0x20, 0x02, // 122 z 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 ~ // Greek uppercase letters (193-217 in CP-1253) 0x00, 0x02, 0xC0, 0x01, 0xB0, 0x00, 0x88, 0x00, 0xB0, 0x00, 0xC0, 0x01, 0x00, 0x02, // Α Alpha (same as A) 0x00, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0xF0, 0x01, // Β Beta (same as B) 0x00, 0x00, 0xF8, 0x03, 0x08, 0x00, 0x08, 0x00, 0x18, // Γ Gamma 0x00, 0x02, 0x80, 0x01, 0x60, 0x00, 0x10, 0x00, 0x60, 0x00, 0x80, 0x01, 0x00, 0x02, // Δ Delta 0x00, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, // Ε Epsilon (same as E) 0x08, 0x03, 0x88, 0x02, 0xC8, 0x02, 0x68, 0x02, 0x38, 0x02, // Ζ Zeta (same as Z) 0x00, 0x00, 0xF8, 0x03, 0x40, 0x00, 0x40, 0x00, 0x40, 0x00, 0xF8, 0x03, // Η Eta (same as H) 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x48, 0x02, 0x48, 0x02, 0x08, 0x02, 0xF0, 0x01, // Θ Theta 0x00, 0x00, 0xF8, 0x03, // Ι Iota (same as I) 0x00, 0x00, 0xF8, 0x03, 0x80, 0x00, 0x60, 0x00, 0x90, 0x00, 0x08, 0x01, 0x00, 0x02, // Κ Kappa (same as K) 0x00, 0x02, 0x80, 0x01, 0x70, 0x00, 0x08, 0x00, 0x70, 0x00, 0x80, 0x01, 0x00, 0x02, // Λ Lambda 0x00, 0x00, 0xF8, 0x03, 0x30, 0x00, 0xC0, 0x01, 0x00, 0x02, 0xC0, 0x01, 0x30, 0x00, 0xF8, 0x03, // Μ Mu (same as M) 0x00, 0x00, 0xF8, 0x03, 0x30, 0x00, 0x40, 0x00, 0x80, 0x01, 0xF8, 0x03, // Ν Nu (same as N) 0x00, 0x00, 0x48, 0x02, 0x48, 0x02, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, // Ξ Xi 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0xF0, 0x01, // Ο Omicron (same as O) 0x00, 0x00, 0xF8, 0x03, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0xF8, 0x03, // Π Pi 0x00, 0x00, 0xF8, 0x03, 0x48, 0x00, 0x48, 0x00, 0x48, 0x00, 0x30, // Ρ Rho (same as P) 0x00, 0x00, 0x30, 0x01, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0x90, 0x01, // Σ Sigma 0x00, 0x00, 0x08, 0x00, 0x08, 0x00, 0xF8, 0x03, 0x08, 0x00, 0x08, // Τ Tau (same as T) 0x08, 0x00, 0x10, 0x00, 0x20, 0x00, 0xC0, 0x03, 0x20, 0x00, 0x10, 0x00, 0x08, // Υ Upsilon (same as Y) 0x00, 0x00, 0x70, 0x00, 0x88, 0x00, 0xF8, 0x03, 0x88, 0x00, 0x70, 0x00, 0x00, // Φ Phi 0x00, 0x02, 0x08, 0x01, 0x90, 0x00, 0x60, 0x00, 0x90, 0x00, 0x08, 0x01, 0x00, 0x02, // Χ Chi (same as X) 0x00, 0x00, 0x08, 0x00, 0xF0, 0x01, 0x08, 0x02, 0xF8, 0x03, 0x08, 0x02, 0xF0, 0x01, // Ψ Psi 0x00, 0x00, 0x08, 0x02, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0xF0, 0x01, 0x08, 0x02, // Ω Omega // Greek lowercase letters (225-249 in CP-1253) 0x00, 0x00, 0x00, 0x03, 0xA0, 0x02, 0xA0, 0x02, 0xE0, 0x03, // α alpha 0x00, 0x00, 0xF8, 0x07, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // β beta 0x00, 0x04, 0x20, 0x02, 0xC0, 0x01, 0x20, 0x00, 0x20, // γ gamma 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0x50, 0x01, // δ delta 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0x40, // ε epsilon 0x00, 0x04, 0x00, 0x03, 0xE0, 0x00, 0x18, // ζ zeta 0x00, 0x00, 0xE0, 0x05, 0x20, 0x0A, 0x20, 0x02, 0xC0, 0x01, // η eta 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0xA0, 0x02, 0xC0, 0x01, // θ theta 0x00, 0x00, 0xE0, 0x03, // ι iota 0xE0, 0x03, 0x80, 0x00, 0x40, 0x01, 0x20, 0x02, // κ kappa 0x00, 0x02, 0x80, 0x01, 0x40, 0x00, 0x20, 0x00, 0xE0, 0x03, // λ lambda 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x02, 0x00, 0x02, 0xE0, 0x03, // μ mu 0x20, 0x00, 0xC0, 0x01, 0x00, 0x02, 0xE0, 0x03, // ν nu 0x00, 0x04, 0xC0, 0x03, 0xA0, 0x02, 0xA0, 0x02, 0xC0, 0x01, // ξ xi 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // ο omicron 0x00, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0xE0, 0x03, // π pi 0x00, 0x00, 0xE0, 0x0F, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // ρ rho 0x00, 0x04, 0x00, 0x03, 0xA0, 0x02, 0x40, 0x01, // ς final sigma 0x00, 0x00, 0x40, 0x02, 0xA0, 0x02, 0xA0, 0x02, 0xE0, 0x03, // σ sigma 0x20, 0x00, 0xE0, 0x03, 0x20, // τ tau 0x00, 0x00, 0xE0, 0x01, 0x00, 0x02, 0x00, 0x02, 0xE0, 0x03, // υ upsilon 0x00, 0x00, 0xC0, 0x00, 0x20, 0x01, 0xE0, 0x03, 0x20, 0x01, 0xC0, // φ phi 0x20, 0x02, 0x40, 0x01, 0x80, 0x00, 0x40, 0x01, 0x20, 0x02, // χ chi 0x00, 0x00, 0x20, 0x00, 0xC0, 0x05, 0x20, 0x02, 0xE0, 0x03, 0x20, // ψ psi 0x00, 0x00, 0x20, 0x02, 0xC0, 0x01, 0x20, 0x02, 0xC0, 0x01, 0x20, 0x02, // ω omega }; // Placeholder for 16pt font - needs to be generated with font converter tool const uint8_t ArialMT_Plain_16_GR[] PROGMEM = { 0x10, // Width: 16 0x13, // Height: 19 0x20, // First Char: 32 0x01, // Number of chars: 1 (placeholder) // Minimal placeholder - replace with full font data 0xFF, 0xFF, 0x00, 0x04, // 32 space // Font Data: // (empty placeholder) }; // Placeholder for 24pt font - needs to be generated with font converter tool const uint8_t ArialMT_Plain_24_GR[] PROGMEM = { 0x18, // Width: 24 0x1C, // Height: 28 0x20, // First Char: 32 0x01, // Number of chars: 1 (placeholder) // Minimal placeholder - replace with full font data 0xFF, 0xFF, 0x00, 0x06, // 32 space // Font Data: // (empty placeholder) }; #endif // OLED_GR ================================================ FILE: src/graphics/fonts/OLEDDisplayFontsGR.h ================================================ #ifndef OLEDDISPLAYFONTSGR_h #define OLEDDISPLAYFONTSGR_h #ifdef ARDUINO #include #elif __MBED__ #define PROGMEM #endif /** * Localization for Greek language containing glyphs for the Greek alphabet. * Uses Windows-1253 (CP-1253) encoding for Greek characters. * * Supported characters: * - Uppercase Greek: Α-Ω (U+0391 to U+03A9) * - Lowercase Greek: α-ω (U+03B1 to U+03C9) * - Accented Greek: ά, έ, ή, ί, ό, ύ, ώ, etc. */ extern const uint8_t ArialMT_Plain_10_GR[] PROGMEM; extern const uint8_t ArialMT_Plain_16_GR[] PROGMEM; extern const uint8_t ArialMT_Plain_24_GR[] PROGMEM; #endif ================================================ FILE: src/graphics/fonts/OLEDDisplayFontsPL.cpp ================================================ // trunk-ignore-all(clang-format): Preserve long lines #ifdef OLED_PL #include "OLEDDisplayFontsPL.h" 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 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, 0x0A, 0x06, // 133 0x03, 0xA3, 0x0C, 0x07, // 134 0x03, 0xAF, 0x0A, 0x06, // 135 0x03, 0xB9, 0x0A, 0x06, // 136 0x03, 0xC3, 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, 0xCD, 0x0E, 0x08, // 147 0x03, 0xDB, 0x0A, 0x06, // 148 0xFF, 0xFF, 0x00, 0x0A, // 149 0xFF, 0xFF, 0x00, 0x0A, // 150 0xFF, 0xFF, 0x00, 0x0A, // 151 0x03, 0xE5, 0x0C, 0x07, // 152 0x03, 0xF1, 0x0A, 0x06, // 153 0x03, 0xFB, 0x0C, 0x07, // 154 0x04, 0x07, 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, 0x0F, 0x04, 0x03, // 161 0x04, 0x13, 0x0A, 0x06, // 162 0x04, 0x1D, 0x0C, 0x07, // 163 0x04, 0x29, 0x0A, 0x06, // 164 0x04, 0x33, 0x0A, 0x06, // 165 0x04, 0x3D, 0x04, 0x03, // 166 0x04, 0x41, 0x0A, 0x06, // 167 0x04, 0x4B, 0x05, 0x04, // 168 0x04, 0x50, 0x0D, 0x08, // 169 0x04, 0x5D, 0x07, 0x05, // 170 0x04, 0x64, 0x0A, 0x06, // 171 0x04, 0x6E, 0x09, 0x06, // 172 0x04, 0x77, 0x03, 0x03, // 173 0x04, 0x7A, 0x0D, 0x08, // 174 0x04, 0x87, 0x0B, 0x07, // 175 0x04, 0x92, 0x07, 0x05, // 176 0x04, 0x99, 0x0A, 0x06, // 177 0x04, 0xA3, 0x05, 0x04, // 178 0x04, 0xA8, 0x05, 0x04, // 179 0x04, 0xAD, 0x05, 0x04, // 180 0x04, 0xB2, 0x0A, 0x06, // 181 0x04, 0xBC, 0x09, 0x06, // 182 0x04, 0xC5, 0x03, 0x03, // 183 0x04, 0xC8, 0x06, 0x04, // 184 0x04, 0xCE, 0x0C, 0x07, // 185 0x04, 0xDA, 0x07, 0x05, // 186 0x04, 0xE1, 0x0C, 0x07, // 187 0x04, 0xED, 0x0A, 0x06, // 188 0x04, 0xF7, 0x10, 0x09, // 189 0x05, 0x07, 0x10, 0x09, // 190 0x05, 0x17, 0x0A, 0x06, // 191 0x05, 0x21, 0x0E, 0x08, // 192 0x05, 0x2F, 0x0E, 0x08, // 193 0x05, 0x3D, 0x0E, 0x08, // 194 0x05, 0x4B, 0x0E, 0x08, // 195 0x05, 0x59, 0x0E, 0x08, // 196 0x05, 0x67, 0x0E, 0x08, // 197 0x05, 0x75, 0x12, 0x0A, // 198 0x05, 0x87, 0x0C, 0x07, // 199 0x05, 0x93, 0x0C, 0x07, // 200 0x05, 0x9F, 0x0C, 0x07, // 201 0x05, 0xAB, 0x0C, 0x07, // 202 0x05, 0xB7, 0x0C, 0x07, // 203 0x05, 0xC3, 0x05, 0x04, // 204 0x05, 0xC8, 0x04, 0x03, // 205 0x05, 0xCC, 0x04, 0x03, // 206 0x05, 0xD0, 0x05, 0x04, // 207 0x05, 0xD5, 0x0B, 0x07, // 208 0x05, 0xE0, 0x0C, 0x07, // 209 0x05, 0xEC, 0x0E, 0x08, // 210 0x05, 0xFA, 0x0E, 0x08, // 211 0x06, 0x08, 0x0E, 0x08, // 212 0x06, 0x16, 0x0E, 0x08, // 213 0x06, 0x24, 0x0E, 0x08, // 214 0x06, 0x32, 0x0A, 0x06, // 215 0x06, 0x3C, 0x0D, 0x08, // 216 0x06, 0x49, 0x0C, 0x07, // 217 0x06, 0x55, 0x0C, 0x07, // 218 0x06, 0x61, 0x0C, 0x07, // 219 0x06, 0x6D, 0x0C, 0x07, // 220 0x06, 0x79, 0x0D, 0x08, // 221 0x06, 0x86, 0x0B, 0x07, // 222 0x06, 0x91, 0x0C, 0x07, // 223 0x06, 0x9D, 0x0A, 0x06, // 224 0x06, 0xA7, 0x0A, 0x06, // 225 0x06, 0xB1, 0x0A, 0x06, // 226 0x06, 0xBB, 0x0A, 0x06, // 227 0x06, 0xC5, 0x0A, 0x06, // 228 0x06, 0xCF, 0x0A, 0x06, // 229 0x06, 0xD9, 0x10, 0x09, // 230 0x06, 0xE9, 0x0A, 0x06, // 231 0x06, 0xF3, 0x0A, 0x06, // 232 0x06, 0xFD, 0x0A, 0x06, // 233 0x07, 0x07, 0x0A, 0x06, // 234 0x07, 0x11, 0x0A, 0x06, // 235 0x07, 0x1B, 0x05, 0x04, // 236 0x07, 0x20, 0x04, 0x03, // 237 0x07, 0x24, 0x05, 0x04, // 238 0x07, 0x29, 0x05, 0x04, // 239 0x07, 0x2E, 0x0A, 0x06, // 240 0x07, 0x38, 0x0A, 0x06, // 241 0x07, 0x42, 0x0A, 0x06, // 242 0x07, 0x4C, 0x0A, 0x06, // 243 0x07, 0x56, 0x0A, 0x06, // 244 0x07, 0x60, 0x0A, 0x06, // 245 0x07, 0x6A, 0x0A, 0x06, // 246 0x07, 0x74, 0x09, 0x06, // 247 0x07, 0x7D, 0x0A, 0x06, // 248 0x07, 0x87, 0x0A, 0x06, // 249 0x07, 0x91, 0x0A, 0x06, // 250 0x07, 0x9B, 0x0A, 0x06, // 251 0x07, 0xA5, 0x0A, 0x06, // 252 0x07, 0xAF, 0x09, 0x06, // 253 0x07, 0xB8, 0x0A, 0x06, // 254 0x07, 0xC2, 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, 0x0D, 0x00, 0x0A, // 132 0x00, 0x00, 0x00, 0x03, 0xA0, 0x02, 0xA0, 0x0E, 0xE0, 0x0B, // 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, 0x0E, 0x48, 0x0A, 0x48, 0x02, // 152 0x00, 0x00, 0xC0, 0x01, 0xA0, 0x02, 0xA0, 0x0E, 0xC0, 0x0A, // 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 }; const uint8_t ArialMT_Plain_16_PL[] PROGMEM = { 0x10, // Width: 16 0x13, // Height: 19 0x20, // First char: 32 0xE0, // Number of chars: 224 // Jump Table: 0xFF, 0xFF, 0x00, 0x04, // 32 0x00, 0x00, 0x08, 0x04, // 33 0x00, 0x08, 0x0D, 0x06, // 34 0x00, 0x15, 0x1A, 0x0A, // 35 0x00, 0x2F, 0x17, 0x09, // 36 0x00, 0x46, 0x26, 0x0E, // 37 0x00, 0x6C, 0x1D, 0x0B, // 38 0x00, 0x89, 0x04, 0x03, // 39 0x00, 0x8D, 0x0C, 0x05, // 40 0x00, 0x99, 0x0B, 0x05, // 41 0x00, 0xA4, 0x0D, 0x06, // 42 0x00, 0xB1, 0x17, 0x09, // 43 0x00, 0xC8, 0x09, 0x04, // 44 0x00, 0xD1, 0x0B, 0x05, // 45 0x00, 0xDC, 0x08, 0x04, // 46 0x00, 0xE4, 0x0A, 0x05, // 47 0x00, 0xEE, 0x17, 0x09, // 48 0x01, 0x05, 0x11, 0x07, // 49 0x01, 0x16, 0x17, 0x09, // 50 0x01, 0x2D, 0x17, 0x09, // 51 0x01, 0x44, 0x17, 0x09, // 52 0x01, 0x5B, 0x17, 0x09, // 53 0x01, 0x72, 0x17, 0x09, // 54 0x01, 0x89, 0x16, 0x09, // 55 0x01, 0x9F, 0x17, 0x09, // 56 0x01, 0xB6, 0x17, 0x09, // 57 0x01, 0xCD, 0x05, 0x03, // 58 0x01, 0xD2, 0x06, 0x03, // 59 0x01, 0xD8, 0x17, 0x09, // 60 0x01, 0xEF, 0x17, 0x09, // 61 0x02, 0x06, 0x17, 0x09, // 62 0x02, 0x1D, 0x16, 0x09, // 63 0x02, 0x33, 0x2F, 0x11, // 64 0x02, 0x62, 0x1D, 0x0B, // 65 0x02, 0x7F, 0x1D, 0x0B, // 66 0x02, 0x9C, 0x20, 0x0C, // 67 0x02, 0xBC, 0x20, 0x0C, // 68 0x02, 0xDC, 0x1D, 0x0B, // 69 0x02, 0xF9, 0x19, 0x0A, // 70 0x03, 0x12, 0x20, 0x0C, // 71 0x03, 0x32, 0x1D, 0x0B, // 72 0x03, 0x4F, 0x05, 0x03, // 73 0x03, 0x54, 0x14, 0x08, // 74 0x03, 0x68, 0x1D, 0x0B, // 75 0x03, 0x85, 0x17, 0x09, // 76 0x03, 0x9C, 0x23, 0x0D, // 77 0x03, 0xBF, 0x1D, 0x0B, // 78 0x03, 0xDC, 0x20, 0x0C, // 79 0x03, 0xFC, 0x1C, 0x0B, // 80 0x04, 0x18, 0x20, 0x0C, // 81 0x04, 0x38, 0x1D, 0x0B, // 82 0x04, 0x55, 0x1D, 0x0B, // 83 0x04, 0x72, 0x19, 0x0A, // 84 0x04, 0x8B, 0x1D, 0x0B, // 85 0x04, 0xA8, 0x1C, 0x0B, // 86 0x04, 0xC4, 0x2B, 0x10, // 87 0x04, 0xEF, 0x20, 0x0C, // 88 0x05, 0x0F, 0x19, 0x0A, // 89 0x05, 0x28, 0x1A, 0x0A, // 90 0x05, 0x42, 0x0C, 0x05, // 91 0x05, 0x4E, 0x0B, 0x05, // 92 0x05, 0x59, 0x09, 0x04, // 93 0x05, 0x62, 0x14, 0x08, // 94 0x05, 0x76, 0x1B, 0x0A, // 95 0x05, 0x91, 0x07, 0x04, // 96 0x05, 0x98, 0x17, 0x09, // 97 0x05, 0xAF, 0x17, 0x09, // 98 0x05, 0xC6, 0x14, 0x08, // 99 0x05, 0xDA, 0x17, 0x09, // 100 0x05, 0xF1, 0x17, 0x09, // 101 0x06, 0x08, 0x0A, 0x05, // 102 0x06, 0x12, 0x17, 0x09, // 103 0x06, 0x29, 0x14, 0x08, // 104 0x06, 0x3D, 0x05, 0x03, // 105 0x06, 0x42, 0x06, 0x03, // 106 0x06, 0x48, 0x17, 0x09, // 107 0x06, 0x5F, 0x05, 0x03, // 108 0x06, 0x64, 0x23, 0x0D, // 109 0x06, 0x87, 0x14, 0x08, // 110 0x06, 0x9B, 0x17, 0x09, // 111 0x06, 0xB2, 0x17, 0x09, // 112 0x06, 0xC9, 0x18, 0x09, // 113 0x06, 0xE1, 0x0D, 0x06, // 114 0x06, 0xEE, 0x14, 0x08, // 115 0x07, 0x02, 0x0B, 0x05, // 116 0x07, 0x0D, 0x14, 0x08, // 117 0x07, 0x21, 0x13, 0x08, // 118 0x07, 0x34, 0x1F, 0x0C, // 119 0x07, 0x53, 0x14, 0x08, // 120 0x07, 0x67, 0x13, 0x08, // 121 0x07, 0x7A, 0x14, 0x08, // 122 0x07, 0x8E, 0x0F, 0x06, // 123 0x07, 0x9D, 0x06, 0x03, // 124 0x07, 0xA3, 0x0E, 0x06, // 125 0x07, 0xB1, 0x17, 0x09, // 126 0xFF, 0xFF, 0x00, 0x10, // 127 0xFF, 0xFF, 0x00, 0x10, // 128 0x07, 0xC8, 0x17, 0x09, // 129 0x07, 0xDF, 0x07, 0x04, // 130 0x07, 0xE6, 0x1D, 0x0B, // 131 0x08, 0x03, 0x1E, 0x0B, // 132 0x08, 0x21, 0x1B, 0x0A, // 133 0x08, 0x3C, 0x20, 0x0C, // 134 0x08, 0x5C, 0x14, 0x08, // 135 0x08, 0x70, 0x14, 0x08, // 136 0x08, 0x84, 0x14, 0x08, // 137 0xFF, 0xFF, 0x00, 0x10, // 138 0xFF, 0xFF, 0x00, 0x10, // 139 0xFF, 0xFF, 0x00, 0x10, // 140 0xFF, 0xFF, 0x00, 0x10, // 141 0xFF, 0xFF, 0x00, 0x10, // 142 0xFF, 0xFF, 0x00, 0x10, // 143 0xFF, 0xFF, 0x00, 0x10, // 144 0xFF, 0xFF, 0x00, 0x10, // 145 0xFF, 0xFF, 0x00, 0x10, // 146 0x08, 0x98, 0x20, 0x0C, // 147 0x08, 0xB8, 0x17, 0x09, // 148 0xFF, 0xFF, 0x00, 0x10, // 149 0xFF, 0xFF, 0x00, 0x10, // 150 0xFF, 0xFF, 0x00, 0x10, // 151 0x08, 0xCF, 0x1D, 0x0B, // 152 0x08, 0xEC, 0x17, 0x09, // 153 0x09, 0x03, 0x1D, 0x0B, // 154 0x09, 0x20, 0x14, 0x08, // 155 0xFF, 0xFF, 0x00, 0x10, // 156 0xFF, 0xFF, 0x00, 0x10, // 157 0xFF, 0xFF, 0x00, 0x10, // 158 0xFF, 0xFF, 0x00, 0x10, // 159 0xFF, 0xFF, 0x00, 0x10, // 160 0x09, 0x34, 0x09, 0x04, // 161 0x09, 0x3D, 0x17, 0x09, // 162 0x09, 0x54, 0x17, 0x09, // 163 0x09, 0x6B, 0x14, 0x08, // 164 0x09, 0x7F, 0x1A, 0x0A, // 165 0x09, 0x99, 0x06, 0x03, // 166 0x09, 0x9F, 0x17, 0x09, // 167 0x09, 0xB6, 0x07, 0x04, // 168 0x09, 0xBD, 0x23, 0x0D, // 169 0x09, 0xE0, 0x0E, 0x06, // 170 0x09, 0xEE, 0x14, 0x08, // 171 0x0A, 0x02, 0x17, 0x09, // 172 0x0A, 0x19, 0x0B, 0x05, // 173 0x0A, 0x24, 0x23, 0x0D, // 174 0x0A, 0x47, 0x19, 0x0A, // 175 0x0A, 0x60, 0x0D, 0x06, // 176 0x0A, 0x6D, 0x17, 0x09, // 177 0x0A, 0x84, 0x0E, 0x06, // 178 0x0A, 0x92, 0x0D, 0x06, // 179 0x0A, 0x9F, 0x0A, 0x05, // 180 0x0A, 0xA9, 0x17, 0x09, // 181 0x0A, 0xC0, 0x19, 0x0A, // 182 0x0A, 0xD9, 0x08, 0x04, // 183 0x0A, 0xE1, 0x0C, 0x05, // 184 0x0A, 0xED, 0x1A, 0x0A, // 185 0x0B, 0x07, 0x0D, 0x06, // 186 0x0B, 0x14, 0x1A, 0x0A, // 187 0x0B, 0x2E, 0x14, 0x08, // 188 0x0B, 0x42, 0x26, 0x0E, // 189 0x0B, 0x68, 0x26, 0x0E, // 190 0x0B, 0x8E, 0x1A, 0x0A, // 191 0x0B, 0xA8, 0x1D, 0x0B, // 192 0x0B, 0xC5, 0x1D, 0x0B, // 193 0x0B, 0xE2, 0x1D, 0x0B, // 194 0x0B, 0xFF, 0x1D, 0x0B, // 195 0x0C, 0x1C, 0x1D, 0x0B, // 196 0x0C, 0x39, 0x1D, 0x0B, // 197 0x0C, 0x56, 0x2C, 0x10, // 198 0x0C, 0x82, 0x20, 0x0C, // 199 0x0C, 0xA2, 0x1D, 0x0B, // 200 0x0C, 0xBF, 0x1D, 0x0B, // 201 0x0C, 0xDC, 0x1D, 0x0B, // 202 0x0C, 0xF9, 0x1D, 0x0B, // 203 0x0D, 0x16, 0x05, 0x03, // 204 0x0D, 0x1B, 0x07, 0x04, // 205 0x0D, 0x22, 0x0A, 0x05, // 206 0x0D, 0x2C, 0x07, 0x04, // 207 0x0D, 0x33, 0x20, 0x0C, // 208 0x0D, 0x53, 0x1D, 0x0B, // 209 0x0D, 0x70, 0x20, 0x0C, // 210 0x0D, 0x90, 0x20, 0x0C, // 211 0x0D, 0xB0, 0x20, 0x0C, // 212 0x0D, 0xD0, 0x20, 0x0C, // 213 0x0D, 0xF0, 0x20, 0x0C, // 214 0x0E, 0x10, 0x17, 0x09, // 215 0x0E, 0x27, 0x20, 0x0C, // 216 0x0E, 0x47, 0x1D, 0x0B, // 217 0x0E, 0x64, 0x1D, 0x0B, // 218 0x0E, 0x81, 0x1D, 0x0B, // 219 0x0E, 0x9E, 0x1D, 0x0B, // 220 0x0E, 0xBB, 0x19, 0x0A, // 221 0x0E, 0xD4, 0x1D, 0x0B, // 222 0x0E, 0xF1, 0x17, 0x09, // 223 0x0F, 0x08, 0x17, 0x09, // 224 0x0F, 0x1F, 0x17, 0x09, // 225 0x0F, 0x36, 0x17, 0x09, // 226 0x0F, 0x4D, 0x17, 0x09, // 227 0x0F, 0x64, 0x17, 0x09, // 228 0x0F, 0x7B, 0x17, 0x09, // 229 0x0F, 0x92, 0x29, 0x0F, // 230 0x0F, 0xBB, 0x14, 0x08, // 231 0x0F, 0xCF, 0x17, 0x09, // 232 0x0F, 0xE6, 0x17, 0x09, // 233 0x0F, 0xFD, 0x17, 0x09, // 234 0x10, 0x14, 0x17, 0x09, // 235 0x10, 0x2B, 0x05, 0x03, // 236 0x10, 0x30, 0x07, 0x04, // 237 0x10, 0x37, 0x0A, 0x05, // 238 0x10, 0x41, 0x07, 0x04, // 239 0x10, 0x48, 0x17, 0x09, // 240 0x10, 0x5F, 0x14, 0x08, // 241 0x10, 0x73, 0x17, 0x09, // 242 0x10, 0x8A, 0x17, 0x09, // 243 0x10, 0xA1, 0x17, 0x09, // 244 0x10, 0xB8, 0x17, 0x09, // 245 0x10, 0xCF, 0x17, 0x09, // 246 0x10, 0xE6, 0x17, 0x09, // 247 0x10, 0xFD, 0x17, 0x09, // 248 0x11, 0x14, 0x14, 0x08, // 249 0x11, 0x28, 0x14, 0x08, // 250 0x11, 0x3C, 0x14, 0x08, // 251 0x11, 0x50, 0x14, 0x08, // 252 0x11, 0x64, 0x13, 0x08, // 253 0x11, 0x77, 0x17, 0x09, // 254 0x11, 0x8E, 0x13, 0x08, // 255 // Font Data: 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x5F, // 33 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, // 34 0x80, 0x08, 0x00, 0x80, 0x78, 0x00, 0xC0, 0x0F, 0x00, 0xB8, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x78, 0x00, 0xC0, 0x0F, 0x00, 0xB8, 0x08, 0x00, 0x80, 0x08, // 35 0x00, 0x00, 0x00, 0xE0, 0x10, 0x00, 0x10, 0x21, 0x00, 0x08, 0x41, 0x00, 0xFC, 0xFF, 0x00, 0x08, 0x42, 0x00, 0x10, 0x22, 0x00, 0x20, 0x1C, // 36 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0x08, 0x61, 0x00, 0xF0, 0x18, 0x00, 0x00, 0x06, 0x00, 0xC0, 0x01, 0x00, 0x30, 0x3C, 0x00, 0x08, 0x42, 0x00, 0x00, 0x42, 0x00, 0x00, 0x42, 0x00, 0x00, 0x3C, // 37 0x00, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x70, 0x22, 0x00, 0x88, 0x41, 0x00, 0x08, 0x43, 0x00, 0x88, 0x44, 0x00, 0x70, 0x28, 0x00, 0x00, 0x10, 0x00, 0x00, 0x28, 0x00, 0x00, 0x44, // 38 0x00, 0x00, 0x00, 0x78, // 39 0x00, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x70, 0xC0, 0x01, 0x08, 0x00, 0x02, // 40 0x00, 0x00, 0x00, 0x08, 0x00, 0x02, 0x70, 0xC0, 0x01, 0x80, 0x3F, // 41 0x10, 0x00, 0x00, 0xD0, 0x00, 0x00, 0x38, 0x00, 0x00, 0xD0, 0x00, 0x00, 0x10, // 42 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, // 43 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, // 44 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, // 45 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, // 46 0x00, 0x60, 0x00, 0x00, 0x1E, 0x00, 0xE0, 0x01, 0x00, 0x18, // 47 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0xE0, 0x1F, // 48 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0x00, 0x10, 0x00, 0x00, 0xF8, 0x7F, // 49 0x00, 0x00, 0x00, 0x20, 0x40, 0x00, 0x10, 0x60, 0x00, 0x08, 0x50, 0x00, 0x08, 0x48, 0x00, 0x08, 0x44, 0x00, 0x10, 0x43, 0x00, 0xE0, 0x40, // 50 0x00, 0x00, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x88, 0x41, 0x00, 0xF0, 0x22, 0x00, 0x00, 0x1C, // 51 0x00, 0x0C, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x09, 0x00, 0xC0, 0x08, 0x00, 0x20, 0x08, 0x00, 0x10, 0x08, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x08, // 52 0x00, 0x00, 0x00, 0xC0, 0x11, 0x00, 0xB8, 0x20, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x08, 0x21, 0x00, 0x08, 0x1E, // 53 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x10, 0x21, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x10, 0x21, 0x00, 0x20, 0x1E, // 54 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x78, 0x00, 0x08, 0x07, 0x00, 0xC8, 0x00, 0x00, 0x28, 0x00, 0x00, 0x18, // 55 0x00, 0x00, 0x00, 0x60, 0x1C, 0x00, 0x90, 0x22, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x90, 0x22, 0x00, 0x60, 0x1C, // 56 0x00, 0x00, 0x00, 0xE0, 0x11, 0x00, 0x10, 0x22, 0x00, 0x08, 0x44, 0x00, 0x08, 0x44, 0x00, 0x08, 0x44, 0x00, 0x10, 0x22, 0x00, 0xE0, 0x1F, // 57 0x00, 0x00, 0x00, 0x40, 0x40, // 58 0x00, 0x00, 0x00, 0x40, 0xC0, 0x01, // 59 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x05, 0x00, 0x00, 0x05, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x40, 0x10, // 60 0x00, 0x00, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, // 61 0x00, 0x00, 0x00, 0x40, 0x10, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x00, 0x05, 0x00, 0x00, 0x05, 0x00, 0x00, 0x02, // 62 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x5C, 0x00, 0x08, 0x02, 0x00, 0x10, 0x01, 0x00, 0xE0, // 63 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0xC0, 0x40, 0x00, 0x20, 0x80, 0x00, 0x10, 0x1E, 0x01, 0x10, 0x21, 0x01, 0x88, 0x40, 0x02, 0x48, 0x40, 0x02, 0x48, 0x40, 0x02, 0x48, 0x20, 0x02, 0x88, 0x7C, 0x02, 0xC8, 0x43, 0x02, 0x10, 0x40, 0x02, 0x10, 0x20, 0x01, 0x60, 0x10, 0x01, 0x80, 0x8F, // 64 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x70, 0x04, 0x00, 0x08, 0x04, 0x00, 0x70, 0x04, 0x00, 0x80, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, // 65 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x90, 0x22, 0x00, 0x60, 0x1C, // 66 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, // 67 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 68 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x40, // 69 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, // 70 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, 0x10, 0x22, 0x00, 0x20, 0x12, 0x00, 0x00, 0x0E, // 71 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0xF8, 0x7F, // 72 0x00, 0x00, 0x00, 0xF8, 0x7F, // 73 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x3F, // 74 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x02, 0x00, 0x00, 0x01, 0x00, 0x80, 0x03, 0x00, 0x40, 0x04, 0x00, 0x20, 0x18, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, // 75 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, // 76 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x30, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x03, 0x00, 0xC0, 0x00, 0x00, 0x30, 0x00, 0x00, 0xF8, 0x7F, // 77 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x10, 0x00, 0x00, 0x60, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x04, 0x00, 0x00, 0x18, 0x00, 0x00, 0x20, 0x00, 0xF8, 0x7F, // 78 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 79 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x10, 0x01, 0x00, 0xE0, // 80 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x50, 0x00, 0x08, 0x50, 0x00, 0x10, 0x20, 0x00, 0x20, 0x70, 0x00, 0xC0, 0x4F, // 81 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x06, 0x00, 0x08, 0x1A, 0x00, 0x10, 0x21, 0x00, 0xE0, 0x40, // 82 0x00, 0x00, 0x00, 0x60, 0x10, 0x00, 0x90, 0x20, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, 0x10, 0x22, 0x00, 0x20, 0x1C, // 83 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, // 84 0x00, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0xF8, 0x1F, // 85 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x18, 0x00, 0x00, 0x60, 0x00, 0x00, 0x18, 0x00, 0x00, 0x07, 0x00, 0xE0, 0x00, 0x00, 0x18, // 86 0x18, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x03, 0x00, 0x70, 0x00, 0x00, 0x08, 0x00, 0x00, 0x70, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1E, 0x00, 0xE0, 0x01, 0x00, 0x18, // 87 0x00, 0x40, 0x00, 0x08, 0x20, 0x00, 0x10, 0x10, 0x00, 0x60, 0x0C, 0x00, 0x80, 0x02, 0x00, 0x00, 0x01, 0x00, 0x80, 0x02, 0x00, 0x60, 0x0C, 0x00, 0x10, 0x10, 0x00, 0x08, 0x20, 0x00, 0x00, 0x40, // 88 0x08, 0x00, 0x00, 0x30, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x7E, 0x00, 0x80, 0x01, 0x00, 0x40, 0x00, 0x00, 0x30, 0x00, 0x00, 0x08, // 89 0x00, 0x40, 0x00, 0x08, 0x60, 0x00, 0x08, 0x58, 0x00, 0x08, 0x44, 0x00, 0x08, 0x43, 0x00, 0x88, 0x40, 0x00, 0x68, 0x40, 0x00, 0x18, 0x40, 0x00, 0x08, 0x40, // 90 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x03, 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, // 91 0x18, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x60, // 92 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, 0xF8, 0xFF, 0x03, // 93 0x00, 0x01, 0x00, 0xC0, 0x00, 0x00, 0x30, 0x00, 0x00, 0x08, 0x00, 0x00, 0x30, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x01, // 94 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, // 95 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x10, // 96 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x42, 0x00, 0x40, 0x22, 0x00, 0x80, 0x7F, // 97 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 98 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, // 99 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0xF8, 0x7F, // 100 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x24, 0x00, 0x00, 0x17, // 101 0x40, 0x00, 0x00, 0xF0, 0x7F, 0x00, 0x48, 0x00, 0x00, 0x48, // 102 0x00, 0x00, 0x00, 0x00, 0x1F, 0x01, 0x80, 0x20, 0x02, 0x40, 0x40, 0x02, 0x40, 0x40, 0x02, 0x40, 0x40, 0x02, 0x80, 0x20, 0x01, 0xC0, 0xFF, // 103 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x7F, // 104 0x00, 0x00, 0x00, 0xC8, 0x7F, // 105 0x00, 0x00, 0x02, 0xC8, 0xFF, 0x01, // 106 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x06, 0x00, 0x00, 0x19, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, // 107 0x00, 0x00, 0x00, 0xF8, 0x7F, // 108 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x7F, // 109 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x7F, // 110 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 111 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 112 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0xC0, 0xFF, 0x03, // 113 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, // 114 0x00, 0x00, 0x00, 0x80, 0x23, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x38, // 115 0x40, 0x00, 0x00, 0xF0, 0x7F, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, // 116 0x00, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0xC0, 0x7F, // 117 0xC0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x03, 0x00, 0xC0, // 118 0xC0, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x03, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1F, 0x00, 0xC0, // 119 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, // 120 0xC0, 0x01, 0x00, 0x00, 0x06, 0x02, 0x00, 0x38, 0x02, 0x00, 0xE0, 0x01, 0x00, 0x38, 0x00, 0x00, 0x07, 0x00, 0xC0, // 121 0x40, 0x40, 0x00, 0x40, 0x60, 0x00, 0x40, 0x58, 0x00, 0x40, 0x44, 0x00, 0x40, 0x43, 0x00, 0xC0, 0x40, 0x00, 0x40, 0x40, // 122 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0xF0, 0xFB, 0x01, 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, // 123 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x03, // 124 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, 0xF0, 0xFB, 0x01, 0x00, 0x04, 0x00, 0x00, 0x04, // 125 0x00, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x01, // 126 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x42, 0x00, 0x00, 0x41, 0x00, 0x80, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, // 129 0x00, 0x01, 0x00, 0xF8, 0x7F, 0x00, 0x80, // 130 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x10, 0x00, 0x00, 0x60, 0x00, 0x00, 0x80, 0x00, 0x00, 0x08, 0x03, 0x00, 0x04, 0x04, 0x00, 0x02, 0x18, 0x00, 0x00, 0x20, 0x00, 0xF8, 0x7F, // 131 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x70, 0x04, 0x00, 0x08, 0x04, 0x00, 0x70, 0x04, 0x00, 0x80, 0x07, 0x00, 0x00, 0x9C, 0x01, 0x00, 0x60, 0x02, // 132 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x42, 0x00, 0x40, 0x22, 0x00, 0x80, 0x7F, 0x03, 0x00, 0x80, 0x04, // 133 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x0A, 0x40, 0x00, 0x09, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, // 134 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x50, 0x40, 0x00, 0x48, 0x40, 0x00, 0x80, 0x20, // 135 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x50, 0x00, 0x00, 0x48, 0x00, 0x00, 0x80, 0x7F, // 136 0x40, 0x40, 0x00, 0x40, 0x60, 0x00, 0x40, 0x58, 0x00, 0x50, 0x44, 0x00, 0x48, 0x43, 0x00, 0xC0, 0x40, 0x00, 0x40, 0x40, // 137 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x0A, 0x40, 0x00, 0x09, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 147 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x50, 0x40, 0x00, 0x48, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 148 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0xC1, 0x01, 0x08, 0x41, 0x02, 0x08, 0x41, 0x00, 0x08, 0x40, // 152 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x03, 0x40, 0xC4, 0x04, 0x80, 0x24, 0x00, 0x00, 0x17, // 153 0x00, 0x00, 0x00, 0x60, 0x10, 0x00, 0x90, 0x20, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x09, 0x42, 0x00, 0x08, 0x42, 0x00, 0x10, 0x22, 0x00, 0x20, 0x1C, // 154 0x00, 0x00, 0x00, 0x80, 0x23, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x50, 0x44, 0x00, 0x48, 0x44, 0x00, 0x80, 0x38, // 155 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0xFF, 0x03, // 161 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x03, 0x40, 0xF0, 0x00, 0x40, 0x4E, 0x00, 0xC0, 0x41, 0x00, 0xB8, 0x20, 0x00, 0x00, 0x11, // 162 0x00, 0x41, 0x00, 0xE0, 0x31, 0x00, 0x10, 0x2F, 0x00, 0x08, 0x21, 0x00, 0x08, 0x21, 0x00, 0x08, 0x40, 0x00, 0x10, 0x40, 0x00, 0x20, 0x20, // 163 0x00, 0x00, 0x00, 0x40, 0x0B, 0x00, 0x80, 0x04, 0x00, 0x40, 0x08, 0x00, 0x40, 0x08, 0x00, 0x80, 0x04, 0x00, 0x40, 0x0B, // 164 0x08, 0x0A, 0x00, 0x10, 0x0A, 0x00, 0x60, 0x0A, 0x00, 0x80, 0x0B, 0x00, 0x00, 0x7E, 0x00, 0x80, 0x0B, 0x00, 0x60, 0x0A, 0x00, 0x10, 0x0A, 0x00, 0x08, 0x0A, // 165 0x00, 0x00, 0x00, 0xF8, 0xF1, 0x03, // 166 0x00, 0x86, 0x00, 0x70, 0x09, 0x01, 0xC8, 0x10, 0x02, 0x88, 0x10, 0x02, 0x08, 0x21, 0x02, 0x08, 0x61, 0x02, 0x30, 0xD2, 0x01, 0x00, 0x0C, // 167 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, // 168 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0xC8, 0x47, 0x00, 0x28, 0x48, 0x00, 0x28, 0x48, 0x00, 0x28, 0x48, 0x00, 0x28, 0x48, 0x00, 0x48, 0x44, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 169 0xD0, 0x00, 0x00, 0x48, 0x01, 0x00, 0x28, 0x01, 0x00, 0x28, 0x01, 0x00, 0xF0, 0x01, // 170 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, 0x20, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, 0x20, // 171 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x0F, // 172 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, // 173 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0xE8, 0x4F, 0x00, 0x28, 0x41, 0x00, 0x28, 0x41, 0x00, 0x28, 0x43, 0x00, 0x28, 0x45, 0x00, 0xC8, 0x48, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 174 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, // 175 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x48, 0x00, 0x00, 0x48, 0x00, 0x00, 0x30, // 176 0x00, 0x00, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0xE0, 0x4F, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, // 177 0x10, 0x01, 0x00, 0x88, 0x01, 0x00, 0x48, 0x01, 0x00, 0x48, 0x01, 0x00, 0x30, 0x01, // 178 0x90, 0x00, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0x28, 0x01, 0x00, 0xD8, // 179 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, // 180 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0xC0, 0x7F, // 181 0xF0, 0x00, 0x00, 0xF8, 0x00, 0x00, 0xF8, 0x01, 0x00, 0xF8, 0x01, 0x00, 0xF8, 0xFF, 0x03, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0xFF, 0x03, 0x08, // 182 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, // 183 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x80, 0x02, 0x00, 0x00, 0x03, // 184 0x00, 0x40, 0x00, 0x08, 0x60, 0x00, 0x08, 0x58, 0x00, 0x08, 0x44, 0x00, 0x0A, 0x43, 0x00, 0x89, 0x40, 0x00, 0x68, 0x40, 0x00, 0x18, 0x40, 0x00, 0x08, 0x40, // 185 0xF0, 0x00, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0xF0, // 186 0x00, 0x40, 0x00, 0x08, 0x60, 0x00, 0x08, 0x58, 0x00, 0x08, 0x44, 0x00, 0x0A, 0x43, 0x00, 0x88, 0x40, 0x00, 0x68, 0x40, 0x00, 0x18, 0x40, 0x00, 0x08, 0x40, // 187 0x40, 0x40, 0x00, 0x40, 0x60, 0x00, 0x40, 0x58, 0x00, 0x50, 0x44, 0x00, 0x40, 0x43, 0x00, 0xC0, 0x40, 0x00, 0x40, 0x40, // 188 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x40, 0x00, 0xF8, 0x31, 0x00, 0x00, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x03, 0x00, 0x80, 0x00, 0x00, 0x60, 0x44, 0x00, 0x10, 0x62, 0x00, 0x08, 0x52, 0x00, 0x00, 0x52, 0x00, 0x00, 0x4C, // 189 0x90, 0x00, 0x00, 0x08, 0x01, 0x00, 0x08, 0x41, 0x00, 0x28, 0x21, 0x00, 0xD8, 0x18, 0x00, 0x00, 0x04, 0x00, 0x00, 0x03, 0x00, 0x80, 0x00, 0x00, 0x40, 0x30, 0x00, 0x30, 0x28, 0x00, 0x08, 0x24, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x20, // 190 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x10, 0x01, 0x00, 0x08, 0x02, 0x40, 0x07, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x01, 0x00, 0xC0, // 191 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x71, 0x04, 0x00, 0x0A, 0x04, 0x00, 0x70, 0x04, 0x00, 0x80, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, // 192 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x70, 0x04, 0x00, 0x0A, 0x04, 0x00, 0x71, 0x04, 0x00, 0x80, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, // 193 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x72, 0x04, 0x00, 0x09, 0x04, 0x00, 0x71, 0x04, 0x00, 0x82, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, // 194 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x72, 0x04, 0x00, 0x09, 0x04, 0x00, 0x72, 0x04, 0x00, 0x81, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, // 195 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x72, 0x04, 0x00, 0x08, 0x04, 0x00, 0x72, 0x04, 0x00, 0x80, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, // 196 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x7E, 0x04, 0x00, 0x0A, 0x04, 0x00, 0x7E, 0x04, 0x00, 0x80, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, // 197 0x00, 0x60, 0x00, 0x00, 0x18, 0x00, 0x00, 0x06, 0x00, 0x80, 0x05, 0x00, 0x60, 0x04, 0x00, 0x18, 0x04, 0x00, 0x08, 0x04, 0x00, 0x08, 0x04, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, // 198 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x02, 0x08, 0xC0, 0x02, 0x08, 0x40, 0x03, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, // 199 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x09, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x40, // 200 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x09, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x40, // 201 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x09, 0x41, 0x00, 0x09, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x40, // 202 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x08, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x40, // 203 0x01, 0x00, 0x00, 0xFA, 0x7F, // 204 0x00, 0x00, 0x00, 0xFA, 0x7F, 0x00, 0x01, // 205 0x02, 0x00, 0x00, 0xF9, 0x7F, 0x00, 0x01, 0x00, 0x00, 0x02, // 206 0x02, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x02, // 207 0x00, 0x02, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 208 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x10, 0x00, 0x00, 0x60, 0x00, 0x00, 0x82, 0x00, 0x00, 0x01, 0x03, 0x00, 0x02, 0x04, 0x00, 0x01, 0x18, 0x00, 0x00, 0x20, 0x00, 0xF8, 0x7F, // 209 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x09, 0x40, 0x00, 0x0A, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 210 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x0A, 0x40, 0x00, 0x09, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 211 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x0A, 0x40, 0x00, 0x09, 0x40, 0x00, 0x09, 0x40, 0x00, 0x0A, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 212 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x0A, 0x40, 0x00, 0x09, 0x40, 0x00, 0x0A, 0x40, 0x00, 0x09, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 213 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x0A, 0x40, 0x00, 0x08, 0x40, 0x00, 0x0A, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 214 0x00, 0x00, 0x00, 0x40, 0x10, 0x00, 0x80, 0x08, 0x00, 0x00, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x05, 0x00, 0x80, 0x08, 0x00, 0x40, 0x10, // 215 0x00, 0x00, 0x00, 0xC0, 0x4F, 0x00, 0x20, 0x30, 0x00, 0x10, 0x30, 0x00, 0x08, 0x4C, 0x00, 0x08, 0x42, 0x00, 0x08, 0x41, 0x00, 0xC8, 0x40, 0x00, 0x30, 0x20, 0x00, 0x30, 0x10, 0x00, 0xC8, 0x0F, // 216 0x00, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x01, 0x40, 0x00, 0x02, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0xF8, 0x1F, // 217 0x00, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x02, 0x40, 0x00, 0x01, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0xF8, 0x1F, // 218 0x00, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x02, 0x40, 0x00, 0x01, 0x40, 0x00, 0x01, 0x40, 0x00, 0x02, 0x40, 0x00, 0x00, 0x20, 0x00, 0xF8, 0x1F, // 219 0x00, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x02, 0x40, 0x00, 0x00, 0x40, 0x00, 0x02, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0xF8, 0x1F, // 220 0x08, 0x00, 0x00, 0x30, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x01, 0x00, 0x02, 0x7E, 0x00, 0x81, 0x01, 0x00, 0x40, 0x00, 0x00, 0x30, 0x00, 0x00, 0x08, // 221 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x20, 0x10, 0x00, 0x20, 0x10, 0x00, 0x20, 0x10, 0x00, 0x20, 0x10, 0x00, 0x20, 0x10, 0x00, 0x20, 0x10, 0x00, 0x40, 0x08, 0x00, 0x80, 0x07, // 222 0x00, 0x00, 0x00, 0xE0, 0x7F, 0x00, 0x10, 0x00, 0x00, 0x08, 0x20, 0x00, 0x88, 0x43, 0x00, 0x70, 0x42, 0x00, 0x00, 0x44, 0x00, 0x00, 0x38, // 223 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x40, 0x44, 0x00, 0x48, 0x44, 0x00, 0x50, 0x42, 0x00, 0x40, 0x22, 0x00, 0x80, 0x7F, // 224 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x40, 0x44, 0x00, 0x50, 0x44, 0x00, 0x48, 0x42, 0x00, 0x40, 0x22, 0x00, 0x80, 0x7F, // 225 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x50, 0x44, 0x00, 0x48, 0x44, 0x00, 0x48, 0x42, 0x00, 0x50, 0x22, 0x00, 0x80, 0x7F, // 226 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x50, 0x44, 0x00, 0x48, 0x44, 0x00, 0x50, 0x42, 0x00, 0x48, 0x22, 0x00, 0x80, 0x7F, // 227 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x50, 0x44, 0x00, 0x40, 0x44, 0x00, 0x50, 0x42, 0x00, 0x40, 0x22, 0x00, 0x80, 0x7F, // 228 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x5C, 0x44, 0x00, 0x54, 0x44, 0x00, 0x5C, 0x42, 0x00, 0x40, 0x22, 0x00, 0x80, 0x7F, // 229 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x42, 0x00, 0x40, 0x22, 0x00, 0x80, 0x3F, 0x00, 0x80, 0x24, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x24, 0x00, 0x00, 0x17, // 230 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x02, 0x40, 0xC0, 0x02, 0x40, 0x40, 0x03, 0x80, 0x20, // 231 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x48, 0x44, 0x00, 0x50, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x24, 0x00, 0x00, 0x17, // 232 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x40, 0x44, 0x00, 0x50, 0x44, 0x00, 0x48, 0x44, 0x00, 0x80, 0x24, 0x00, 0x00, 0x17, // 233 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x50, 0x44, 0x00, 0x48, 0x44, 0x00, 0x48, 0x44, 0x00, 0x90, 0x24, 0x00, 0x00, 0x17, // 234 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x50, 0x44, 0x00, 0x40, 0x44, 0x00, 0x50, 0x44, 0x00, 0x80, 0x24, 0x00, 0x00, 0x17, // 235 0x08, 0x00, 0x00, 0xD0, 0x7F, // 236 0x00, 0x00, 0x00, 0xD0, 0x7F, 0x00, 0x08, // 237 0x10, 0x00, 0x00, 0xC8, 0x7F, 0x00, 0x08, 0x00, 0x00, 0x10, // 238 0x10, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x10, // 239 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0xA0, 0x20, 0x00, 0x68, 0x40, 0x00, 0x58, 0x40, 0x00, 0x70, 0x40, 0x00, 0xE8, 0x20, 0x00, 0x00, 0x1F, // 240 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x90, 0x00, 0x00, 0x48, 0x00, 0x00, 0x50, 0x00, 0x00, 0x48, 0x00, 0x00, 0x80, 0x7F, // 241 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x48, 0x40, 0x00, 0x50, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 242 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x50, 0x40, 0x00, 0x48, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 243 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x50, 0x40, 0x00, 0x48, 0x40, 0x00, 0x48, 0x40, 0x00, 0x90, 0x20, 0x00, 0x00, 0x1F, // 244 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x50, 0x40, 0x00, 0x48, 0x40, 0x00, 0x50, 0x40, 0x00, 0x88, 0x20, 0x00, 0x00, 0x1F, // 245 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x50, 0x40, 0x00, 0x40, 0x40, 0x00, 0x50, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 246 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x80, 0x0A, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, // 247 0x00, 0x00, 0x00, 0x00, 0x5F, 0x00, 0x80, 0x30, 0x00, 0x40, 0x48, 0x00, 0x40, 0x44, 0x00, 0x40, 0x42, 0x00, 0x80, 0x21, 0x00, 0x40, 0x1F, // 248 0x00, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x40, 0x00, 0x00, 0x20, 0x00, 0xC0, 0x7F, // 249 0x00, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x10, 0x40, 0x00, 0x08, 0x20, 0x00, 0xC0, 0x7F, // 250 0x00, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x10, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0xC0, 0x7F, // 251 0x00, 0x00, 0x00, 0xD0, 0x3F, 0x00, 0x00, 0x40, 0x00, 0x10, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0xC0, 0x7F, // 252 0xC0, 0x01, 0x00, 0x00, 0x06, 0x02, 0x00, 0x38, 0x02, 0x10, 0xE0, 0x01, 0x08, 0x38, 0x00, 0x00, 0x07, 0x00, 0xC0, // 253 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x03, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 254 0xC0, 0x01, 0x00, 0x00, 0x06, 0x02, 0x10, 0x38, 0x02, 0x00, 0xE0, 0x01, 0x10, 0x38, 0x00, 0x00, 0x07, 0x00, 0xC0, // 255 }; const uint8_t ArialMT_Plain_24_PL[] PROGMEM = { 0x18, // Width: 24 0x1C, // Height: 28 0x20, // First char: 32 0xE0, // Number of chars: 224 // Jump Table: 0xFF, 0xFF, 0x00, 0x06, // 32 0x00, 0x00, 0x13, 0x06, // 33 0x00, 0x13, 0x1A, 0x08, // 34 0x00, 0x2D, 0x33, 0x0E, // 35 0x00, 0x60, 0x2F, 0x0D, // 36 0x00, 0x8F, 0x4F, 0x15, // 37 0x00, 0xDE, 0x3B, 0x10, // 38 0x01, 0x19, 0x0A, 0x04, // 39 0x01, 0x23, 0x1C, 0x08, // 40 0x01, 0x3F, 0x1B, 0x08, // 41 0x01, 0x5A, 0x21, 0x0A, // 42 0x01, 0x7B, 0x32, 0x0E, // 43 0x01, 0xAD, 0x10, 0x05, // 44 0x01, 0xBD, 0x1B, 0x08, // 45 0x01, 0xD8, 0x0F, 0x05, // 46 0x01, 0xE7, 0x19, 0x08, // 47 0x02, 0x00, 0x2F, 0x0D, // 48 0x02, 0x2F, 0x23, 0x0A, // 49 0x02, 0x52, 0x2F, 0x0D, // 50 0x02, 0x81, 0x2F, 0x0D, // 51 0x02, 0xB0, 0x2F, 0x0D, // 52 0x02, 0xDF, 0x2F, 0x0D, // 53 0x03, 0x0E, 0x2F, 0x0D, // 54 0x03, 0x3D, 0x2D, 0x0D, // 55 0x03, 0x6A, 0x2F, 0x0D, // 56 0x03, 0x99, 0x2F, 0x0D, // 57 0x03, 0xC8, 0x0F, 0x05, // 58 0x03, 0xD7, 0x10, 0x05, // 59 0x03, 0xE7, 0x2F, 0x0D, // 60 0x04, 0x16, 0x2F, 0x0D, // 61 0x04, 0x45, 0x2E, 0x0D, // 62 0x04, 0x73, 0x2E, 0x0D, // 63 0x04, 0xA1, 0x5B, 0x18, // 64 0x04, 0xFC, 0x3B, 0x10, // 65 0x05, 0x37, 0x3B, 0x10, // 66 0x05, 0x72, 0x3F, 0x11, // 67 0x05, 0xB1, 0x3F, 0x11, // 68 0x05, 0xF0, 0x3B, 0x10, // 69 0x06, 0x2B, 0x35, 0x0F, // 70 0x06, 0x60, 0x43, 0x12, // 71 0x06, 0xA3, 0x3B, 0x10, // 72 0x06, 0xDE, 0x0F, 0x05, // 73 0x06, 0xED, 0x27, 0x0B, // 74 0x07, 0x14, 0x3F, 0x11, // 75 0x07, 0x53, 0x2F, 0x0D, // 76 0x07, 0x82, 0x43, 0x12, // 77 0x07, 0xC5, 0x3B, 0x10, // 78 0x08, 0x00, 0x47, 0x13, // 79 0x08, 0x47, 0x3A, 0x10, // 80 0x08, 0x81, 0x47, 0x13, // 81 0x08, 0xC8, 0x3F, 0x11, // 82 0x09, 0x07, 0x3B, 0x10, // 83 0x09, 0x42, 0x35, 0x0F, // 84 0x09, 0x77, 0x3B, 0x10, // 85 0x09, 0xB2, 0x39, 0x10, // 86 0x09, 0xEB, 0x59, 0x18, // 87 0x0A, 0x44, 0x3B, 0x10, // 88 0x0A, 0x7F, 0x3D, 0x11, // 89 0x0A, 0xBC, 0x37, 0x0F, // 90 0x0A, 0xF3, 0x14, 0x06, // 91 0x0B, 0x07, 0x1B, 0x08, // 92 0x0B, 0x22, 0x18, 0x07, // 93 0x0B, 0x3A, 0x2A, 0x0C, // 94 0x0B, 0x64, 0x34, 0x0E, // 95 0x0B, 0x98, 0x11, 0x06, // 96 0x0B, 0xA9, 0x2F, 0x0D, // 97 0x0B, 0xD8, 0x33, 0x0E, // 98 0x0C, 0x0B, 0x2B, 0x0C, // 99 0x0C, 0x36, 0x2F, 0x0D, // 100 0x0C, 0x65, 0x2F, 0x0D, // 101 0x0C, 0x94, 0x1A, 0x08, // 102 0x0C, 0xAE, 0x2F, 0x0D, // 103 0x0C, 0xDD, 0x2F, 0x0D, // 104 0x0D, 0x0C, 0x0F, 0x05, // 105 0x0D, 0x1B, 0x10, 0x05, // 106 0x0D, 0x2B, 0x2F, 0x0D, // 107 0x0D, 0x5A, 0x0F, 0x05, // 108 0x0D, 0x69, 0x47, 0x13, // 109 0x0D, 0xB0, 0x2F, 0x0D, // 110 0x0D, 0xDF, 0x2F, 0x0D, // 111 0x0E, 0x0E, 0x33, 0x0E, // 112 0x0E, 0x41, 0x30, 0x0D, // 113 0x0E, 0x71, 0x1E, 0x09, // 114 0x0E, 0x8F, 0x2B, 0x0C, // 115 0x0E, 0xBA, 0x1B, 0x08, // 116 0x0E, 0xD5, 0x2F, 0x0D, // 117 0x0F, 0x04, 0x2A, 0x0C, // 118 0x0F, 0x2E, 0x42, 0x12, // 119 0x0F, 0x70, 0x2B, 0x0C, // 120 0x0F, 0x9B, 0x2A, 0x0C, // 121 0x0F, 0xC5, 0x2B, 0x0C, // 122 0x0F, 0xF0, 0x1C, 0x08, // 123 0x10, 0x0C, 0x10, 0x05, // 124 0x10, 0x1C, 0x1B, 0x08, // 125 0x10, 0x37, 0x32, 0x0E, // 126 0xFF, 0xFF, 0x00, 0x18, // 127 0xFF, 0xFF, 0x00, 0x18, // 128 0x10, 0x69, 0x2F, 0x0D, // 129 0x10, 0x98, 0x16, 0x07, // 130 0x10, 0xAE, 0x3B, 0x10, // 131 0x10, 0xE9, 0x40, 0x11, // 132 0x11, 0x29, 0x34, 0x0E, // 133 0x11, 0x5D, 0x3F, 0x11, // 134 0x11, 0x9C, 0x2B, 0x0C, // 135 0x11, 0xC7, 0x2F, 0x0D, // 136 0x11, 0xF6, 0x2B, 0x0C, // 137 0xFF, 0xFF, 0x00, 0x18, // 138 0xFF, 0xFF, 0x00, 0x18, // 139 0xFF, 0xFF, 0x00, 0x18, // 140 0xFF, 0xFF, 0x00, 0x18, // 141 0xFF, 0xFF, 0x00, 0x18, // 142 0xFF, 0xFF, 0x00, 0x18, // 143 0xFF, 0xFF, 0x00, 0x18, // 144 0xFF, 0xFF, 0x00, 0x18, // 145 0xFF, 0xFF, 0x00, 0x18, // 146 0x12, 0x21, 0x47, 0x13, // 147 0x12, 0x68, 0x2F, 0x0D, // 148 0xFF, 0xFF, 0x00, 0x18, // 149 0xFF, 0xFF, 0x00, 0x18, // 150 0xFF, 0xFF, 0x00, 0x18, // 151 0x12, 0x97, 0x3B, 0x10, // 152 0x12, 0xD2, 0x2F, 0x0D, // 153 0x13, 0x01, 0x3B, 0x10, // 154 0x13, 0x3C, 0x2B, 0x0C, // 155 0xFF, 0xFF, 0x00, 0x18, // 156 0xFF, 0xFF, 0x00, 0x18, // 157 0xFF, 0xFF, 0x00, 0x18, // 158 0xFF, 0xFF, 0x00, 0x18, // 159 0xFF, 0xFF, 0x00, 0x18, // 160 0x13, 0x67, 0x14, 0x06, // 161 0x13, 0x7B, 0x2B, 0x0C, // 162 0x13, 0xA6, 0x2F, 0x0D, // 163 0x13, 0xD5, 0x33, 0x0E, // 164 0x14, 0x08, 0x31, 0x0E, // 165 0x14, 0x39, 0x10, 0x05, // 166 0x14, 0x49, 0x2F, 0x0D, // 167 0x14, 0x78, 0x19, 0x08, // 168 0x14, 0x91, 0x46, 0x13, // 169 0x14, 0xD7, 0x1A, 0x08, // 170 0x14, 0xF1, 0x27, 0x0B, // 171 0x15, 0x18, 0x2F, 0x0D, // 172 0x15, 0x47, 0x1B, 0x08, // 173 0x15, 0x62, 0x46, 0x13, // 174 0x15, 0xA8, 0x31, 0x0E, // 175 0x15, 0xD9, 0x1E, 0x09, // 176 0x15, 0xF7, 0x33, 0x0E, // 177 0x16, 0x2A, 0x1A, 0x08, // 178 0x16, 0x44, 0x1A, 0x08, // 179 0x16, 0x5E, 0x19, 0x08, // 180 0x16, 0x77, 0x2F, 0x0D, // 181 0x16, 0xA6, 0x31, 0x0E, // 182 0x16, 0xD7, 0x12, 0x06, // 183 0x16, 0xE9, 0x18, 0x07, // 184 0x17, 0x01, 0x37, 0x0F, // 185 0x17, 0x38, 0x1E, 0x09, // 186 0x17, 0x56, 0x37, 0x0F, // 187 0x17, 0x8D, 0x2B, 0x0C, // 188 0x17, 0xB8, 0x4B, 0x14, // 189 0x18, 0x03, 0x4B, 0x14, // 190 0x18, 0x4E, 0x33, 0x0E, // 191 0x18, 0x81, 0x3B, 0x10, // 192 0x18, 0xBC, 0x3B, 0x10, // 193 0x18, 0xF7, 0x3B, 0x10, // 194 0x19, 0x32, 0x3B, 0x10, // 195 0x19, 0x6D, 0x3B, 0x10, // 196 0x19, 0xA8, 0x3B, 0x10, // 197 0x19, 0xE3, 0x5B, 0x18, // 198 0x1A, 0x3E, 0x3F, 0x11, // 199 0x1A, 0x7D, 0x3B, 0x10, // 200 0x1A, 0xB8, 0x3B, 0x10, // 201 0x1A, 0xF3, 0x3B, 0x10, // 202 0x1B, 0x2E, 0x3B, 0x10, // 203 0x1B, 0x69, 0x11, 0x06, // 204 0x1B, 0x7A, 0x11, 0x06, // 205 0x1B, 0x8B, 0x15, 0x07, // 206 0x1B, 0xA0, 0x15, 0x07, // 207 0x1B, 0xB5, 0x3F, 0x11, // 208 0x1B, 0xF4, 0x3B, 0x10, // 209 0x1C, 0x2F, 0x47, 0x13, // 210 0x1C, 0x76, 0x47, 0x13, // 211 0x1C, 0xBD, 0x47, 0x13, // 212 0x1D, 0x04, 0x47, 0x13, // 213 0x1D, 0x4B, 0x47, 0x13, // 214 0x1D, 0x92, 0x2B, 0x0C, // 215 0x1D, 0xBD, 0x47, 0x13, // 216 0x1E, 0x04, 0x3B, 0x10, // 217 0x1E, 0x3F, 0x3B, 0x10, // 218 0x1E, 0x7A, 0x3B, 0x10, // 219 0x1E, 0xB5, 0x3B, 0x10, // 220 0x1E, 0xF0, 0x3D, 0x11, // 221 0x1F, 0x2D, 0x3A, 0x10, // 222 0x1F, 0x67, 0x37, 0x0F, // 223 0x1F, 0x9E, 0x2F, 0x0D, // 224 0x1F, 0xCD, 0x2F, 0x0D, // 225 0x1F, 0xFC, 0x2F, 0x0D, // 226 0x20, 0x2B, 0x2F, 0x0D, // 227 0x20, 0x5A, 0x2F, 0x0D, // 228 0x20, 0x89, 0x2F, 0x0D, // 229 0x20, 0xB8, 0x53, 0x16, // 230 0x21, 0x0B, 0x2B, 0x0C, // 231 0x21, 0x36, 0x2F, 0x0D, // 232 0x21, 0x65, 0x2F, 0x0D, // 233 0x21, 0x94, 0x2F, 0x0D, // 234 0x21, 0xC3, 0x2F, 0x0D, // 235 0x21, 0xF2, 0x11, 0x06, // 236 0x22, 0x03, 0x11, 0x06, // 237 0x22, 0x14, 0x15, 0x07, // 238 0x22, 0x29, 0x15, 0x07, // 239 0x22, 0x3E, 0x2F, 0x0D, // 240 0x22, 0x6D, 0x2F, 0x0D, // 241 0x22, 0x9C, 0x2F, 0x0D, // 242 0x22, 0xCB, 0x2F, 0x0D, // 243 0x22, 0xFA, 0x2F, 0x0D, // 244 0x23, 0x29, 0x2F, 0x0D, // 245 0x23, 0x58, 0x2F, 0x0D, // 246 0x23, 0x87, 0x32, 0x0E, // 247 0x23, 0xB9, 0x33, 0x0E, // 248 0x23, 0xEC, 0x2F, 0x0D, // 249 0x24, 0x1B, 0x2F, 0x0D, // 250 0x24, 0x4A, 0x2F, 0x0D, // 251 0x24, 0x79, 0x2F, 0x0D, // 252 0x24, 0xA8, 0x2A, 0x0C, // 253 0x24, 0xD2, 0x2F, 0x0D, // 254 0x25, 0x01, 0x2A, 0x0C, // 255 // Font Data: 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x33, 0x00, 0xE0, 0xFF, 0x33, // 33 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xE0, 0x07, // 34 0x00, 0x0C, 0x03, 0x00, 0x00, 0x0C, 0x33, 0x00, 0x00, 0x0C, 0x3F, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x80, 0xFF, 0x03, 0x00, 0xE0, 0x0F, 0x03, 0x00, 0x60, 0x0C, 0x33, 0x00, 0x00, 0x0C, 0x3F, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x80, 0xFF, 0x03, 0x00, 0xE0, 0x0F, 0x03, 0x00, 0x60, 0x0C, 0x03, 0x00, 0x00, 0x0C, 0x03, // 35 0x00, 0x00, 0x00, 0x00, 0x80, 0x07, 0x06, 0x00, 0xC0, 0x0F, 0x1E, 0x00, 0xC0, 0x18, 0x1C, 0x00, 0x60, 0x18, 0x38, 0x00, 0x60, 0x30, 0x30, 0x00, 0xF0, 0xFF, 0xFF, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x60, 0x38, 0x00, 0xC0, 0x60, 0x18, 0x00, 0xC0, 0xC1, 0x1F, 0x00, 0x00, 0x81, 0x07, // 36 0x00, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x20, 0x20, 0x00, 0x00, 0x20, 0x20, 0x20, 0x00, 0x60, 0x30, 0x38, 0x00, 0xC0, 0x1F, 0x1E, 0x00, 0x80, 0x8F, 0x0F, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x8F, 0x0F, 0x00, 0xC0, 0xC3, 0x1F, 0x00, 0xE0, 0x60, 0x30, 0x00, 0x20, 0x20, 0x20, 0x00, 0x00, 0x20, 0x20, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x80, 0x0F, // 37 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x80, 0xE3, 0x1C, 0x00, 0xC0, 0x77, 0x38, 0x00, 0xE0, 0x3C, 0x30, 0x00, 0x60, 0x38, 0x30, 0x00, 0x60, 0x78, 0x30, 0x00, 0xE0, 0xEC, 0x38, 0x00, 0xC0, 0x8F, 0x1B, 0x00, 0x80, 0x03, 0x1F, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xC0, 0x38, 0x00, 0x00, 0x00, 0x10, // 38 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xE0, 0x07, // 39 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0xFE, 0x7F, 0x00, 0x80, 0x0F, 0xF0, 0x01, 0xC0, 0x01, 0x80, 0x03, 0x60, 0x00, 0x00, 0x06, 0x20, 0x00, 0x00, 0x04, // 40 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x04, 0x60, 0x00, 0x00, 0x06, 0xC0, 0x01, 0x80, 0x03, 0x80, 0x0F, 0xF0, 0x01, 0x00, 0xFE, 0x7F, 0x00, 0x00, 0xF0, 0x0F, // 41 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x04, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x80, 0x04, 0x00, 0x00, 0x80, // 42 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xFF, 0x0F, 0x00, 0x00, 0xFF, 0x0F, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, // 43 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x03, 0x00, 0x00, 0xF0, 0x01, // 44 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, // 45 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, // 46 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x80, 0x3F, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x60, // 47 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x38, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0x00, 0xFE, 0x03, // 48 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 49 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x30, 0x00, 0xC0, 0x03, 0x38, 0x00, 0xC0, 0x00, 0x3C, 0x00, 0x60, 0x00, 0x36, 0x00, 0x60, 0x00, 0x33, 0x00, 0x60, 0x80, 0x31, 0x00, 0x60, 0xC0, 0x30, 0x00, 0x60, 0x60, 0x30, 0x00, 0xC0, 0x30, 0x30, 0x00, 0xC0, 0x1F, 0x30, 0x00, 0x00, 0x0F, 0x30, // 50 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x06, 0x00, 0xC0, 0x01, 0x0E, 0x00, 0xC0, 0x00, 0x1C, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0xC0, 0x38, 0x30, 0x00, 0xC0, 0x6F, 0x18, 0x00, 0x80, 0xC7, 0x0F, 0x00, 0x00, 0x80, 0x07, // 51 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x3C, 0x03, 0x00, 0x00, 0x0E, 0x03, 0x00, 0x80, 0x07, 0x03, 0x00, 0xC0, 0x01, 0x03, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, // 52 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x06, 0x00, 0x80, 0x3F, 0x0E, 0x00, 0xE0, 0x1F, 0x18, 0x00, 0x60, 0x08, 0x30, 0x00, 0x60, 0x0C, 0x30, 0x00, 0x60, 0x0C, 0x30, 0x00, 0x60, 0x0C, 0x30, 0x00, 0x60, 0x0C, 0x30, 0x00, 0x60, 0x18, 0x1C, 0x00, 0x60, 0xF0, 0x0F, 0x00, 0x00, 0xE0, 0x03, // 53 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x03, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0xC0, 0x63, 0x1C, 0x00, 0xC0, 0x30, 0x38, 0x00, 0x60, 0x18, 0x30, 0x00, 0x60, 0x18, 0x30, 0x00, 0x60, 0x18, 0x30, 0x00, 0x60, 0x18, 0x30, 0x00, 0xE0, 0x30, 0x18, 0x00, 0xC0, 0xF1, 0x0F, 0x00, 0x80, 0xC1, 0x07, // 54 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x3C, 0x00, 0x60, 0x80, 0x3F, 0x00, 0x60, 0xE0, 0x03, 0x00, 0x60, 0x78, 0x00, 0x00, 0x60, 0x0E, 0x00, 0x00, 0x60, 0x03, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x60, // 55 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x80, 0xC7, 0x1F, 0x00, 0xC0, 0x6F, 0x18, 0x00, 0xE0, 0x38, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0xE0, 0x38, 0x30, 0x00, 0xC0, 0x6F, 0x18, 0x00, 0x80, 0xC7, 0x1F, 0x00, 0x00, 0x80, 0x07, // 56 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x0C, 0x00, 0x80, 0x7F, 0x1C, 0x00, 0xC0, 0x61, 0x38, 0x00, 0x60, 0xC0, 0x30, 0x00, 0x60, 0xC0, 0x30, 0x00, 0x60, 0xC0, 0x30, 0x00, 0x60, 0xC0, 0x30, 0x00, 0x60, 0x60, 0x18, 0x00, 0xC0, 0x31, 0x1E, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0x00, 0xFE, 0x01, // 57 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, // 58 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x30, 0x03, 0x00, 0x06, 0xF0, 0x01, // 59 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0xD8, 0x00, 0x00, 0x00, 0xD8, 0x00, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x04, 0x01, 0x00, 0x00, 0x06, 0x03, 0x00, 0x00, 0x06, 0x03, 0x00, 0x00, 0x03, 0x06, // 60 0x00, 0x00, 0x00, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, // 61 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x06, 0x00, 0x00, 0x06, 0x03, 0x00, 0x00, 0x06, 0x03, 0x00, 0x00, 0x04, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0xD8, 0x00, 0x00, 0x00, 0xD8, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x20, // 62 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x60, 0x80, 0x33, 0x00, 0x60, 0xC0, 0x33, 0x00, 0x60, 0xE0, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0xC0, 0x38, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x00, 0x07, // 63 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x1E, 0xF0, 0x00, 0x00, 0x07, 0xC0, 0x01, 0x80, 0xC3, 0x87, 0x01, 0xC0, 0xF1, 0x9F, 0x03, 0xC0, 0x38, 0x18, 0x03, 0xC0, 0x0C, 0x30, 0x03, 0x60, 0x0E, 0x30, 0x06, 0x60, 0x06, 0x30, 0x06, 0x60, 0x06, 0x18, 0x06, 0x60, 0x06, 0x0C, 0x06, 0x60, 0x0C, 0x1E, 0x06, 0x60, 0xF8, 0x3F, 0x06, 0xE0, 0xFE, 0x31, 0x06, 0xC0, 0x0E, 0x30, 0x06, 0xC0, 0x01, 0x18, 0x03, 0x80, 0x03, 0x1C, 0x03, 0x00, 0x07, 0x8F, 0x01, 0x00, 0xFE, 0x87, 0x01, 0x00, 0xF8, 0xC1, 0x00, 0x00, 0x00, 0x40, // 64 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x80, 0x8F, 0x01, 0x00, 0xE0, 0x83, 0x01, 0x00, 0x60, 0x80, 0x01, 0x00, 0xE0, 0x83, 0x01, 0x00, 0x80, 0x8F, 0x01, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 65 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0xC0, 0x78, 0x30, 0x00, 0xC0, 0xFF, 0x18, 0x00, 0x80, 0xC7, 0x1F, 0x00, 0x00, 0x80, 0x07, // 66 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x03, 0x0F, 0x00, 0x00, 0x02, 0x03, // 67 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x03, 0x0E, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 68 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 69 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, // 70 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xE0, 0x00, 0x18, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x60, 0x30, 0x00, 0x60, 0x60, 0x30, 0x00, 0xE0, 0x60, 0x38, 0x00, 0xC0, 0x60, 0x18, 0x00, 0xC0, 0x61, 0x18, 0x00, 0x80, 0xE3, 0x0F, 0x00, 0x00, 0xE2, 0x0F, // 71 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 72 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 73 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x38, 0x00, 0xE0, 0xFF, 0x1F, 0x00, 0xE0, 0xFF, 0x0F, // 74 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0xE7, 0x01, 0x00, 0x80, 0x83, 0x07, 0x00, 0xC0, 0x01, 0x0F, 0x00, 0xE0, 0x00, 0x1E, 0x00, 0x60, 0x00, 0x38, 0x00, 0x20, 0x00, 0x30, 0x00, 0x00, 0x00, 0x20, // 75 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, // 76 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xFE, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 77 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 78 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x0F, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 79 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x00, 0x0F, // 80 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x0C, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xE0, 0x00, 0x18, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x36, 0x00, 0x60, 0x00, 0x36, 0x00, 0xE0, 0x00, 0x3C, 0x00, 0xC0, 0x00, 0x1C, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x3F, 0x00, 0x00, 0xFF, 0x77, 0x00, 0x00, 0xFC, 0x61, // 81 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x70, 0x00, 0x00, 0x60, 0xF0, 0x00, 0x00, 0x60, 0xF0, 0x03, 0x00, 0x60, 0xB0, 0x07, 0x00, 0xE0, 0x18, 0x1F, 0x00, 0xC0, 0x1F, 0x3C, 0x00, 0x80, 0x0F, 0x30, 0x00, 0x00, 0x00, 0x20, // 82 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x07, 0x0F, 0x00, 0xC0, 0x1F, 0x1C, 0x00, 0xC0, 0x18, 0x18, 0x00, 0x60, 0x38, 0x38, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x70, 0x30, 0x00, 0xC0, 0x60, 0x18, 0x00, 0xC0, 0xE1, 0x18, 0x00, 0x80, 0xC3, 0x0F, 0x00, 0x00, 0x83, 0x07, // 83 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, // 84 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x03, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0x03, // 85 0x20, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x20, // 86 0x60, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x80, 0xFF, 0x00, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x80, 0x1F, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x80, 0xFF, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x60, // 87 0x00, 0x00, 0x20, 0x00, 0x20, 0x00, 0x30, 0x00, 0x60, 0x00, 0x3C, 0x00, 0xE0, 0x01, 0x1E, 0x00, 0xC0, 0x83, 0x07, 0x00, 0x00, 0xCF, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x00, 0xCF, 0x03, 0x00, 0xC0, 0x03, 0x07, 0x00, 0xE0, 0x01, 0x1E, 0x00, 0x60, 0x00, 0x3C, 0x00, 0x20, 0x00, 0x30, 0x00, 0x00, 0x00, 0x20, // 88 0x20, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x20, // 89 0x00, 0x00, 0x30, 0x00, 0x60, 0x00, 0x38, 0x00, 0x60, 0x00, 0x3C, 0x00, 0x60, 0x00, 0x37, 0x00, 0x60, 0x80, 0x33, 0x00, 0x60, 0xC0, 0x31, 0x00, 0x60, 0xE0, 0x30, 0x00, 0x60, 0x38, 0x30, 0x00, 0x60, 0x1C, 0x30, 0x00, 0x60, 0x0E, 0x30, 0x00, 0x60, 0x07, 0x30, 0x00, 0xE0, 0x01, 0x30, 0x00, 0xE0, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, // 90 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x07, 0xE0, 0xFF, 0xFF, 0x07, 0x60, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0x06, // 91 0x60, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 92 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0x06, 0xE0, 0xFF, 0xFF, 0x07, 0xE0, 0xFF, 0xFF, 0x07, // 93 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x20, // 94 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, // 95 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x80, // 96 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0x00, 0x1C, 0x1F, 0x00, 0x00, 0x8C, 0x39, 0x00, 0x00, 0x86, 0x31, 0x00, 0x00, 0x86, 0x31, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x18, 0x00, 0x00, 0xCE, 0x0C, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x00, 0x20, // 97 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x18, 0x0C, 0x00, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xE0, 0x03, // 98 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x18, 0x0C, // 99 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x18, 0x0C, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 100 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xDC, 0x1C, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x00, 0xDC, 0x18, 0x00, 0x00, 0xF8, 0x0C, 0x00, 0x00, 0xF0, 0x04, // 101 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0xC0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x06, 0x00, 0x00, 0x60, 0x06, 0x00, 0x00, 0x60, 0x06, // 102 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x83, 0x01, 0x00, 0xF8, 0x8F, 0x03, 0x00, 0x1C, 0x1C, 0x07, 0x00, 0x0E, 0x38, 0x06, 0x00, 0x06, 0x30, 0x06, 0x00, 0x06, 0x30, 0x06, 0x00, 0x06, 0x30, 0x06, 0x00, 0x0C, 0x18, 0x07, 0x00, 0x18, 0x8C, 0x03, 0x00, 0xFE, 0xFF, 0x01, 0x00, 0xFE, 0xFF, // 103 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xF8, 0x3F, // 104 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0xFE, 0x3F, 0x00, 0x60, 0xFE, 0x3F, // 105 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x60, 0xFE, 0xFF, 0x07, 0x60, 0xFE, 0xFF, 0x03, // 106 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0x98, 0x07, 0x00, 0x00, 0x0C, 0x0E, 0x00, 0x00, 0x06, 0x3C, 0x00, 0x00, 0x02, 0x30, 0x00, 0x00, 0x00, 0x20, // 107 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 108 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xF8, 0x3F, // 109 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xF8, 0x3F, // 110 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x07, // 111 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0x18, 0x0C, 0x00, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xE0, 0x03, // 112 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x18, 0x0C, 0x00, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0xFE, 0xFF, 0x07, // 113 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, // 114 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x0C, 0x00, 0x00, 0x7C, 0x1C, 0x00, 0x00, 0xEE, 0x38, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x31, 0x00, 0x00, 0xC6, 0x31, 0x00, 0x00, 0x8E, 0x39, 0x00, 0x00, 0x9C, 0x1F, 0x00, 0x00, 0x18, 0x0F, // 115 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0xC0, 0xFF, 0x1F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, // 116 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x0F, 0x00, 0x00, 0xFE, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, // 117 0x00, 0x06, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x06, // 118 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x0E, // 119 0x00, 0x02, 0x20, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x38, 0x0E, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x38, 0x0E, 0x00, 0x00, 0x1C, 0x3C, 0x00, 0x00, 0x0E, 0x30, 0x00, 0x00, 0x02, 0x20, // 120 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x06, 0x00, 0xF0, 0x01, 0x06, 0x00, 0x80, 0x0F, 0x07, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0xFC, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x06, // 121 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x06, 0x3C, 0x00, 0x00, 0x06, 0x3E, 0x00, 0x00, 0x06, 0x37, 0x00, 0x00, 0xC6, 0x33, 0x00, 0x00, 0xE6, 0x30, 0x00, 0x00, 0x76, 0x30, 0x00, 0x00, 0x3E, 0x30, 0x00, 0x00, 0x1E, 0x30, 0x00, 0x00, 0x06, 0x30, // 122 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xC0, 0x03, 0x00, 0xC0, 0x7F, 0xFE, 0x03, 0xE0, 0x3F, 0xFC, 0x07, 0x60, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0x06, // 123 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x0F, 0xE0, 0xFF, 0xFF, 0x0F, // 124 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0x06, 0xE0, 0x3F, 0xFC, 0x07, 0xC0, 0x7F, 0xFF, 0x03, 0x00, 0xC0, 0x03, 0x00, 0x00, 0x80, 0x01, // 125 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x60, // 126 0x00, 0x60, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x0C, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x03, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, // 129 0x00, 0x60, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x06, // 130 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x10, 0x3C, 0x00, 0x00, 0x1C, 0x70, 0x00, 0x00, 0x0C, 0xE0, 0x01, 0x00, 0x04, 0x80, 0x03, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 131 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x80, 0x8F, 0x01, 0x00, 0xE0, 0x83, 0x01, 0x00, 0x60, 0x80, 0x01, 0x00, 0xE0, 0x83, 0x01, 0x00, 0x80, 0x8F, 0x01, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x00, 0xB0, 0x03, 0x00, 0x00, 0x00, 0x03, // 132 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0x00, 0x1C, 0x1F, 0x00, 0x00, 0x8C, 0x39, 0x00, 0x00, 0x86, 0x31, 0x00, 0x00, 0x86, 0x31, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x18, 0x00, 0x00, 0xCE, 0x0C, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF8, 0xFF, 0x01, 0x00, 0x00, 0xA0, 0x03, 0x00, 0x00, 0x00, 0x03, // 133 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x68, 0x00, 0x30, 0x00, 0x6E, 0x00, 0x30, 0x00, 0x66, 0x00, 0x30, 0x00, 0x62, 0x00, 0x30, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x03, 0x0F, 0x00, 0x00, 0x02, 0x03, // 134 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x80, 0x06, 0x30, 0x00, 0xE0, 0x06, 0x30, 0x00, 0x60, 0x06, 0x30, 0x00, 0x20, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x18, 0x0C, // 135 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x80, 0x06, 0x00, 0x00, 0xE0, 0x06, 0x00, 0x00, 0x60, 0x06, 0x00, 0x00, 0x20, 0x0E, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xF8, 0x3F, // 136 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x06, 0x3C, 0x00, 0x00, 0x06, 0x3E, 0x00, 0x00, 0x06, 0x37, 0x00, 0x80, 0xC6, 0x33, 0x00, 0xE0, 0xE6, 0x30, 0x00, 0x60, 0x76, 0x30, 0x00, 0x20, 0x3E, 0x30, 0x00, 0x00, 0x1E, 0x30, 0x00, 0x00, 0x06, 0x30, // 137 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x68, 0x00, 0x30, 0x00, 0x6E, 0x00, 0x30, 0x00, 0x66, 0x00, 0x30, 0x00, 0xE2, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x0F, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 147 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x06, 0x30, 0x00, 0x80, 0x06, 0x30, 0x00, 0xE0, 0x06, 0x30, 0x00, 0x60, 0x0E, 0x38, 0x00, 0x20, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x07, // 148 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0xF0, 0x01, 0x60, 0x30, 0xB0, 0x03, 0x60, 0x30, 0x30, 0x03, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 152 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xDC, 0x1C, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0xF0, 0x01, 0x00, 0xC6, 0xB0, 0x03, 0x00, 0xCE, 0x38, 0x03, 0x00, 0xDC, 0x18, 0x00, 0x00, 0xF8, 0x0C, 0x00, 0x00, 0xF0, 0x04, // 153 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x07, 0x0F, 0x00, 0xC0, 0x1F, 0x1C, 0x00, 0xC0, 0x18, 0x18, 0x00, 0x60, 0x38, 0x38, 0x00, 0x60, 0x30, 0x30, 0x00, 0x68, 0x30, 0x30, 0x00, 0x6E, 0x30, 0x30, 0x00, 0x66, 0x30, 0x30, 0x00, 0x62, 0x70, 0x30, 0x00, 0xC0, 0x60, 0x18, 0x00, 0xC0, 0xE1, 0x18, 0x00, 0x80, 0xC3, 0x0F, 0x00, 0x00, 0x83, 0x07, // 154 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x0C, 0x00, 0x00, 0x7C, 0x1C, 0x00, 0x00, 0xEE, 0x38, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x80, 0xC6, 0x30, 0x00, 0xE0, 0xC6, 0x31, 0x00, 0x60, 0xC6, 0x31, 0x00, 0x20, 0x8E, 0x39, 0x00, 0x00, 0x9C, 0x1F, 0x00, 0x00, 0x18, 0x0F, // 155 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE6, 0xFF, 0x07, 0x00, 0xE6, 0xFF, 0x07, // 161 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x9C, 0x07, 0x00, 0x0E, 0x78, 0x00, 0x00, 0x06, 0x3F, 0x00, 0x00, 0xF6, 0x30, 0x00, 0x00, 0x0E, 0x30, 0x00, 0xE0, 0x0D, 0x1C, 0x00, 0x00, 0x1C, 0x0E, 0x00, 0x00, 0x10, 0x06, // 162 0x00, 0x60, 0x10, 0x00, 0x00, 0x60, 0x38, 0x00, 0x00, 0x7F, 0x1C, 0x00, 0xC0, 0xFF, 0x1F, 0x00, 0xE0, 0xE0, 0x19, 0x00, 0x60, 0x60, 0x18, 0x00, 0x60, 0x60, 0x18, 0x00, 0x60, 0x60, 0x30, 0x00, 0xE0, 0x00, 0x30, 0x00, 0xC0, 0x01, 0x30, 0x00, 0x80, 0x01, 0x38, 0x00, 0x00, 0x00, 0x10, // 163 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x04, 0x00, 0x00, 0xF7, 0x0E, 0x00, 0x00, 0xFE, 0x07, 0x00, 0x00, 0x0C, 0x03, 0x00, 0x00, 0x06, 0x06, 0x00, 0x00, 0x06, 0x06, 0x00, 0x00, 0x06, 0x06, 0x00, 0x00, 0x06, 0x06, 0x00, 0x00, 0x0C, 0x03, 0x00, 0x00, 0xFE, 0x07, 0x00, 0x00, 0xF7, 0x0E, 0x00, 0x00, 0x02, 0x04, // 164 0xE0, 0x60, 0x06, 0x00, 0xC0, 0x61, 0x06, 0x00, 0x80, 0x67, 0x06, 0x00, 0x00, 0x7E, 0x06, 0x00, 0x00, 0x7C, 0x06, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x7C, 0x06, 0x00, 0x00, 0x7E, 0x06, 0x00, 0x80, 0x67, 0x06, 0x00, 0xC0, 0x61, 0x06, 0x00, 0xE0, 0x60, 0x06, 0x00, 0x20, // 165 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x7F, 0xF8, 0x0F, 0xE0, 0x7F, 0xF8, 0x0F, // 166 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x80, 0xF3, 0xC1, 0x00, 0xC0, 0x1F, 0xC3, 0x03, 0xE0, 0x0C, 0x07, 0x03, 0x60, 0x1C, 0x06, 0x06, 0x60, 0x18, 0x0C, 0x06, 0x60, 0x30, 0x1C, 0x06, 0xE0, 0x70, 0x38, 0x07, 0xC0, 0xE1, 0xF4, 0x03, 0x80, 0xC1, 0xE7, 0x01, 0x00, 0x80, 0x03, // 167 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, // 168 0x00, 0xF8, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0x07, 0x07, 0x00, 0x80, 0x01, 0x0C, 0x00, 0xC0, 0x79, 0x1C, 0x00, 0xC0, 0xFE, 0x19, 0x00, 0x60, 0x86, 0x31, 0x00, 0x60, 0x03, 0x33, 0x00, 0x60, 0x03, 0x33, 0x00, 0x60, 0x03, 0x33, 0x00, 0x60, 0x03, 0x33, 0x00, 0x60, 0x87, 0x33, 0x00, 0xC0, 0x86, 0x19, 0x00, 0xC0, 0x85, 0x1C, 0x00, 0x80, 0x01, 0x0C, 0x00, 0x00, 0x07, 0x07, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0xF8, // 169 0x00, 0x00, 0x00, 0x00, 0xC0, 0x1C, 0x00, 0x00, 0xE0, 0x3E, 0x00, 0x00, 0x60, 0x32, 0x00, 0x00, 0x60, 0x32, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0xC0, 0x3F, // 170 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x78, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x84, 0x10, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x78, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x04, 0x10, // 171 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFC, 0x01, // 172 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, // 173 0x00, 0xF8, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0x07, 0x07, 0x00, 0x80, 0x01, 0x0C, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0xFE, 0x1B, 0x00, 0x60, 0xFE, 0x33, 0x00, 0x60, 0x66, 0x30, 0x00, 0x60, 0x66, 0x30, 0x00, 0x60, 0xE6, 0x30, 0x00, 0x60, 0xFE, 0x31, 0x00, 0x60, 0x3C, 0x33, 0x00, 0xC0, 0x00, 0x1A, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x01, 0x0C, 0x00, 0x00, 0x07, 0x07, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0xF8, // 174 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, // 175 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x40, 0x04, 0x00, 0x00, 0x20, 0x08, 0x00, 0x00, 0x20, 0x08, 0x00, 0x00, 0x20, 0x08, 0x00, 0x00, 0x40, 0x04, 0x00, 0x00, 0x80, 0x03, // 176 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0xFF, 0x3F, 0x00, 0x00, 0xFF, 0x3F, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, // 177 0x40, 0x20, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x20, 0x38, 0x00, 0x00, 0x20, 0x2C, 0x00, 0x00, 0x20, 0x26, 0x00, 0x00, 0xE0, 0x23, 0x00, 0x00, 0xC0, 0x21, // 178 0x40, 0x10, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x20, 0x20, 0x00, 0x00, 0x20, 0x22, 0x00, 0x00, 0x20, 0x22, 0x00, 0x00, 0xE0, 0x3D, 0x00, 0x00, 0xC0, 0x1D, // 179 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x20, // 180 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, // 181 0x00, 0x0F, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0xE0, 0x7F, 0x00, 0x00, 0xE0, 0x7F, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x07, 0xE0, 0xFF, 0xFF, 0x07, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x07, 0xE0, 0xFF, 0xFF, 0x07, 0x60, 0x00, 0x00, 0x00, 0x60, // 182 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, // 183 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0xC0, 0x02, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x01, // 184 0x00, 0x00, 0x30, 0x00, 0x60, 0x00, 0x38, 0x00, 0x60, 0x00, 0x3C, 0x00, 0x60, 0x00, 0x37, 0x00, 0x60, 0x80, 0x33, 0x00, 0x60, 0xC0, 0x31, 0x00, 0x68, 0xE0, 0x30, 0x00, 0x6E, 0x38, 0x30, 0x00, 0x66, 0x1C, 0x30, 0x00, 0x62, 0x0E, 0x30, 0x00, 0x60, 0x07, 0x30, 0x00, 0xE0, 0x01, 0x30, 0x00, 0xE0, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, // 185 0x00, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xE0, 0x38, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0xE0, 0x38, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x80, 0x0F, // 186 0x00, 0x00, 0x30, 0x00, 0x60, 0x00, 0x38, 0x00, 0x60, 0x00, 0x3C, 0x00, 0x60, 0x00, 0x37, 0x00, 0x60, 0x80, 0x33, 0x00, 0x60, 0xC0, 0x31, 0x00, 0x6C, 0xE0, 0x30, 0x00, 0x6C, 0x38, 0x30, 0x00, 0x60, 0x1C, 0x30, 0x00, 0x60, 0x0E, 0x30, 0x00, 0x60, 0x07, 0x30, 0x00, 0xE0, 0x01, 0x30, 0x00, 0xE0, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, // 187 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x06, 0x3C, 0x00, 0x00, 0x06, 0x3E, 0x00, 0x00, 0x06, 0x37, 0x00, 0xC0, 0xC6, 0x33, 0x00, 0xC0, 0xE6, 0x30, 0x00, 0x00, 0x76, 0x30, 0x00, 0x00, 0x3E, 0x30, 0x00, 0x00, 0x1E, 0x30, 0x00, 0x00, 0x06, 0x30, // 188 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x20, 0x00, 0xE0, 0x3F, 0x30, 0x00, 0xE0, 0x3F, 0x1C, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x4E, 0x20, 0x00, 0x00, 0x67, 0x30, 0x00, 0xC0, 0x21, 0x38, 0x00, 0xE0, 0x20, 0x2C, 0x00, 0x60, 0x20, 0x26, 0x00, 0x00, 0xE0, 0x27, 0x00, 0x00, 0xC0, 0x21, // 189 0x40, 0x10, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x20, 0x20, 0x00, 0x00, 0x20, 0x22, 0x20, 0x00, 0x20, 0x22, 0x30, 0x00, 0xE0, 0x3D, 0x38, 0x00, 0xC0, 0x1D, 0x0E, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x0E, 0x0C, 0x00, 0x00, 0x07, 0x0E, 0x00, 0x80, 0x83, 0x0B, 0x00, 0xE0, 0xC0, 0x08, 0x00, 0x60, 0xE0, 0x3F, 0x00, 0x20, 0xE0, 0x3F, 0x00, 0x00, 0x00, 0x08, // 190 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x1E, 0x03, 0x00, 0x00, 0x07, 0x07, 0x00, 0xE6, 0x03, 0x06, 0x00, 0xE6, 0x01, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xC0, // 191 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x82, 0x8F, 0x01, 0x00, 0xE6, 0x83, 0x01, 0x00, 0x6E, 0x80, 0x01, 0x00, 0xE8, 0x83, 0x01, 0x00, 0x80, 0x8F, 0x01, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 192 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x80, 0x8F, 0x01, 0x00, 0xE8, 0x83, 0x01, 0x00, 0x6E, 0x80, 0x01, 0x00, 0xE6, 0x83, 0x01, 0x00, 0x82, 0x8F, 0x01, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 193 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x88, 0x8F, 0x01, 0x00, 0xEC, 0x83, 0x01, 0x00, 0x66, 0x80, 0x01, 0x00, 0xE6, 0x83, 0x01, 0x00, 0x8C, 0x8F, 0x01, 0x00, 0x08, 0xFE, 0x01, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 194 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x0C, 0xFE, 0x01, 0x00, 0x8E, 0x8F, 0x01, 0x00, 0xE6, 0x83, 0x01, 0x00, 0x66, 0x80, 0x01, 0x00, 0xEC, 0x83, 0x01, 0x00, 0x8C, 0x8F, 0x01, 0x00, 0x0E, 0xFE, 0x01, 0x00, 0x06, 0xF0, 0x03, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 195 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x8C, 0x8F, 0x01, 0x00, 0xEC, 0x83, 0x01, 0x00, 0x60, 0x80, 0x01, 0x00, 0xE0, 0x83, 0x01, 0x00, 0x8C, 0x8F, 0x01, 0x00, 0x0C, 0xFE, 0x01, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 196 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x9C, 0x8F, 0x01, 0x00, 0xE2, 0x83, 0x01, 0x00, 0x62, 0x80, 0x01, 0x00, 0xE2, 0x83, 0x01, 0x00, 0x9C, 0x8F, 0x01, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 197 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0xBC, 0x01, 0x00, 0x00, 0x8F, 0x01, 0x00, 0xC0, 0x83, 0x01, 0x00, 0xE0, 0x80, 0x01, 0x00, 0x60, 0x80, 0x01, 0x00, 0x60, 0x80, 0x01, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 198 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, 0x60, 0x00, 0x30, 0x02, 0x60, 0x00, 0x30, 0x02, 0x60, 0x00, 0xF0, 0x02, 0x60, 0x00, 0xB0, 0x03, 0x60, 0x00, 0x30, 0x01, 0x60, 0x00, 0x30, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x03, 0x0F, 0x00, 0x00, 0x02, 0x03, // 199 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x62, 0x30, 0x30, 0x00, 0x66, 0x30, 0x30, 0x00, 0x6E, 0x30, 0x30, 0x00, 0x68, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 200 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x68, 0x30, 0x30, 0x00, 0x6E, 0x30, 0x30, 0x00, 0x66, 0x30, 0x30, 0x00, 0x62, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 201 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x68, 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x66, 0x30, 0x30, 0x00, 0x66, 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x68, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 202 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 203 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0xE6, 0xFF, 0x3F, 0x00, 0xEE, 0xFF, 0x3F, 0x00, 0x08, // 204 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0xEE, 0xFF, 0x3F, 0x00, 0xE6, 0xFF, 0x3F, 0x00, 0x02, // 205 0x08, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0xE6, 0xFF, 0x3F, 0x00, 0xE6, 0xFF, 0x3F, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x08, // 206 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, // 207 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x03, 0x0E, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 208 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x8C, 0x03, 0x00, 0x00, 0x0E, 0x0E, 0x00, 0x00, 0x06, 0x3C, 0x00, 0x00, 0x06, 0x70, 0x00, 0x00, 0x0C, 0xE0, 0x01, 0x00, 0x0C, 0x80, 0x03, 0x00, 0x0E, 0x00, 0x0F, 0x00, 0x06, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 209 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x62, 0x00, 0x30, 0x00, 0x66, 0x00, 0x30, 0x00, 0x6E, 0x00, 0x30, 0x00, 0x68, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x0F, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 210 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x60, 0x00, 0x30, 0x00, 0x68, 0x00, 0x30, 0x00, 0x6E, 0x00, 0x30, 0x00, 0x66, 0x00, 0x30, 0x00, 0x62, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x0F, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 211 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x68, 0x00, 0x30, 0x00, 0x6C, 0x00, 0x30, 0x00, 0x66, 0x00, 0x30, 0x00, 0x66, 0x00, 0x30, 0x00, 0x6C, 0x00, 0x30, 0x00, 0xE8, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x0F, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 212 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xCC, 0x00, 0x18, 0x00, 0xEE, 0x00, 0x38, 0x00, 0x66, 0x00, 0x30, 0x00, 0x66, 0x00, 0x30, 0x00, 0x6C, 0x00, 0x30, 0x00, 0x6C, 0x00, 0x30, 0x00, 0x6E, 0x00, 0x30, 0x00, 0xE6, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x0F, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 213 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x6C, 0x00, 0x30, 0x00, 0x6C, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x6C, 0x00, 0x30, 0x00, 0xEC, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x0F, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 214 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x03, 0x00, 0x00, 0x8E, 0x03, 0x00, 0x00, 0xDC, 0x01, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0xDC, 0x01, 0x00, 0x00, 0x8E, 0x03, 0x00, 0x00, 0x06, 0x03, // 215 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x21, 0x00, 0x00, 0xFF, 0x77, 0x00, 0x80, 0x07, 0x3F, 0x00, 0xC0, 0x01, 0x1E, 0x00, 0xC0, 0x00, 0x1F, 0x00, 0xE0, 0x80, 0x3B, 0x00, 0x60, 0xC0, 0x31, 0x00, 0x60, 0xE0, 0x30, 0x00, 0x60, 0x70, 0x30, 0x00, 0x60, 0x38, 0x30, 0x00, 0x60, 0x1C, 0x30, 0x00, 0xE0, 0x0E, 0x38, 0x00, 0xC0, 0x07, 0x18, 0x00, 0xC0, 0x03, 0x1C, 0x00, 0xE0, 0x07, 0x0F, 0x00, 0x70, 0xFF, 0x07, 0x00, 0x20, 0xFC, 0x01, // 216 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x03, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x38, 0x00, 0x02, 0x00, 0x30, 0x00, 0x06, 0x00, 0x30, 0x00, 0x0E, 0x00, 0x30, 0x00, 0x08, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0x03, // 217 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x03, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x30, 0x00, 0x08, 0x00, 0x30, 0x00, 0x0E, 0x00, 0x30, 0x00, 0x06, 0x00, 0x30, 0x00, 0x02, 0x00, 0x30, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0x03, // 218 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x03, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x38, 0x00, 0x08, 0x00, 0x30, 0x00, 0x0C, 0x00, 0x30, 0x00, 0x06, 0x00, 0x30, 0x00, 0x06, 0x00, 0x30, 0x00, 0x0C, 0x00, 0x30, 0x00, 0x08, 0x00, 0x38, 0x00, 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0x03, // 219 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x03, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x38, 0x00, 0x0C, 0x00, 0x30, 0x00, 0x0C, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x0C, 0x00, 0x30, 0x00, 0x0C, 0x00, 0x38, 0x00, 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0x03, // 220 0x20, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x08, 0xF0, 0x3F, 0x00, 0x0E, 0xF0, 0x3F, 0x00, 0x06, 0x3C, 0x00, 0x00, 0x02, 0x1E, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x20, // 221 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x03, 0x06, 0x00, 0x00, 0x03, 0x06, 0x00, 0x00, 0x03, 0x06, 0x00, 0x00, 0x03, 0x06, 0x00, 0x00, 0x03, 0x06, 0x00, 0x00, 0x03, 0x06, 0x00, 0x00, 0x03, 0x06, 0x00, 0x00, 0x03, 0x07, 0x00, 0x00, 0x86, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x00, 0xF8, // 222 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0x3F, 0x00, 0xC0, 0xFF, 0x3F, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x60, 0x00, 0x08, 0x00, 0x60, 0x00, 0x1C, 0x00, 0x60, 0x00, 0x38, 0x00, 0xE0, 0x78, 0x30, 0x00, 0xC0, 0x7F, 0x30, 0x00, 0x80, 0xC7, 0x30, 0x00, 0x00, 0x80, 0x39, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0x0F, // 223 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0x00, 0x1C, 0x1F, 0x00, 0x00, 0x8C, 0x39, 0x00, 0x20, 0x86, 0x31, 0x00, 0x60, 0x86, 0x31, 0x00, 0xE0, 0xC6, 0x30, 0x00, 0x80, 0xC6, 0x18, 0x00, 0x00, 0xCE, 0x0C, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x00, 0x20, // 224 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0x00, 0x1C, 0x1F, 0x00, 0x00, 0x8C, 0x39, 0x00, 0x00, 0x86, 0x31, 0x00, 0x80, 0x86, 0x31, 0x00, 0xE0, 0xC6, 0x30, 0x00, 0x60, 0xC6, 0x18, 0x00, 0x20, 0xCE, 0x0C, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x00, 0x20, // 225 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0x00, 0x1C, 0x1F, 0x00, 0x80, 0x8C, 0x39, 0x00, 0xC0, 0x86, 0x31, 0x00, 0x60, 0x86, 0x31, 0x00, 0x60, 0xC6, 0x30, 0x00, 0xC0, 0xC6, 0x18, 0x00, 0x80, 0xCE, 0x0C, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x00, 0x20, // 226 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0xC0, 0x1C, 0x1F, 0x00, 0xE0, 0x8C, 0x39, 0x00, 0x60, 0x86, 0x31, 0x00, 0x60, 0x86, 0x31, 0x00, 0xC0, 0xC6, 0x30, 0x00, 0xC0, 0xC6, 0x18, 0x00, 0xE0, 0xCE, 0x0C, 0x00, 0x60, 0xFC, 0x1F, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x00, 0x20, // 227 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0x00, 0x1C, 0x1F, 0x00, 0xC0, 0x8C, 0x39, 0x00, 0xC0, 0x86, 0x31, 0x00, 0x00, 0x86, 0x31, 0x00, 0x00, 0xC6, 0x30, 0x00, 0xC0, 0xC6, 0x18, 0x00, 0xC0, 0xCE, 0x0C, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x00, 0x20, // 228 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0x00, 0x1C, 0x1F, 0x00, 0x00, 0x8C, 0x39, 0x00, 0x70, 0x86, 0x31, 0x00, 0x88, 0x86, 0x31, 0x00, 0x88, 0xC6, 0x30, 0x00, 0x88, 0xC6, 0x18, 0x00, 0x70, 0xCE, 0x0C, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x00, 0x20, // 229 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x0F, 0x00, 0x00, 0x9C, 0x1F, 0x00, 0x00, 0xCC, 0x39, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0x66, 0x18, 0x00, 0x00, 0x6E, 0x1C, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xCC, 0x1C, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xCC, 0x18, 0x00, 0x00, 0xF8, 0x0C, 0x00, 0x00, 0xE0, 0x04, // 230 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x02, 0x00, 0x06, 0x30, 0x02, 0x00, 0x06, 0xF0, 0x02, 0x00, 0x06, 0xB0, 0x03, 0x00, 0x0E, 0x38, 0x01, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x18, 0x0C, // 231 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xDC, 0x1C, 0x00, 0x20, 0xCE, 0x38, 0x00, 0x60, 0xC6, 0x30, 0x00, 0xE0, 0xC6, 0x30, 0x00, 0x80, 0xC6, 0x30, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x00, 0xDC, 0x18, 0x00, 0x00, 0xF8, 0x0C, 0x00, 0x00, 0xF0, 0x04, // 232 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xDC, 0x1C, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x80, 0xC6, 0x30, 0x00, 0xE0, 0xC6, 0x30, 0x00, 0x60, 0xC6, 0x30, 0x00, 0x20, 0xCE, 0x38, 0x00, 0x00, 0xDC, 0x18, 0x00, 0x00, 0xF8, 0x0C, 0x00, 0x00, 0xF0, 0x04, // 233 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xDC, 0x1C, 0x00, 0x80, 0xCE, 0x38, 0x00, 0xC0, 0xC6, 0x30, 0x00, 0x60, 0xC6, 0x30, 0x00, 0x60, 0xC6, 0x30, 0x00, 0xC0, 0xCE, 0x38, 0x00, 0x80, 0xDC, 0x18, 0x00, 0x00, 0xF8, 0x0C, 0x00, 0x00, 0xF0, 0x04, // 234 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xDC, 0x1C, 0x00, 0xC0, 0xCE, 0x38, 0x00, 0xC0, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0xC0, 0xCE, 0x38, 0x00, 0xC0, 0xDC, 0x18, 0x00, 0x00, 0xF8, 0x0C, 0x00, 0x00, 0xF0, 0x04, // 235 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x60, 0xFE, 0x3F, 0x00, 0xE0, 0xFE, 0x3F, 0x00, 0x80, // 236 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xE0, 0xFE, 0x3F, 0x00, 0x60, 0xFE, 0x3F, 0x00, 0x20, // 237 0x80, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x60, 0xFE, 0x3F, 0x00, 0x60, 0xFE, 0x3F, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x80, // 238 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, // 239 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1D, 0x1C, 0x00, 0xA0, 0x0F, 0x38, 0x00, 0xA0, 0x06, 0x30, 0x00, 0xE0, 0x06, 0x30, 0x00, 0xC0, 0x06, 0x30, 0x00, 0xC0, 0x0F, 0x38, 0x00, 0x20, 0x1F, 0x1C, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x00, 0xE0, 0x07, // 240 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0xC0, 0xFE, 0x3F, 0x00, 0xE0, 0x18, 0x00, 0x00, 0x60, 0x0C, 0x00, 0x00, 0x60, 0x06, 0x00, 0x00, 0xC0, 0x06, 0x00, 0x00, 0xC0, 0x06, 0x00, 0x00, 0xE0, 0x0E, 0x00, 0x00, 0x60, 0xFC, 0x3F, 0x00, 0x00, 0xF8, 0x3F, // 241 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x20, 0x0E, 0x38, 0x00, 0x60, 0x06, 0x30, 0x00, 0xE0, 0x06, 0x30, 0x00, 0x80, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x07, // 242 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x80, 0x06, 0x30, 0x00, 0xE0, 0x06, 0x30, 0x00, 0x60, 0x06, 0x30, 0x00, 0x20, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x07, // 243 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x80, 0x0E, 0x38, 0x00, 0xC0, 0x06, 0x30, 0x00, 0x60, 0x06, 0x30, 0x00, 0x60, 0x06, 0x30, 0x00, 0xC0, 0x0E, 0x38, 0x00, 0x80, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x07, // 244 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0xC0, 0x1C, 0x1C, 0x00, 0xE0, 0x0E, 0x38, 0x00, 0x60, 0x06, 0x30, 0x00, 0x60, 0x06, 0x30, 0x00, 0xC0, 0x06, 0x30, 0x00, 0xC0, 0x0E, 0x38, 0x00, 0xE0, 0x1C, 0x1C, 0x00, 0x60, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x07, // 245 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0xC0, 0x0E, 0x38, 0x00, 0xC0, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0xC0, 0x0E, 0x38, 0x00, 0xC0, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x07, // 246 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0xB6, 0x01, 0x00, 0x00, 0xB6, 0x01, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, // 247 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x67, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x3F, 0x00, 0x00, 0x86, 0x33, 0x00, 0x00, 0xE6, 0x31, 0x00, 0x00, 0x76, 0x30, 0x00, 0x00, 0x3E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xFF, 0x0F, 0x00, 0x00, 0xF3, 0x07, // 248 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x0F, 0x00, 0x00, 0xFE, 0x1F, 0x00, 0x20, 0x00, 0x38, 0x00, 0x60, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x30, 0x00, 0x80, 0x00, 0x30, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, // 249 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x0F, 0x00, 0x00, 0xFE, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x30, 0x00, 0x80, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x30, 0x00, 0x60, 0x00, 0x18, 0x00, 0x20, 0x00, 0x0C, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, // 250 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x0F, 0x00, 0x00, 0xFE, 0x1F, 0x00, 0x80, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xC0, 0x00, 0x18, 0x00, 0x80, 0x00, 0x0C, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, // 251 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x0F, 0x00, 0x00, 0xFE, 0x1F, 0x00, 0xC0, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x00, 0x0C, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, // 252 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x06, 0x00, 0xF0, 0x01, 0x06, 0x00, 0x80, 0x0F, 0x07, 0x80, 0x00, 0xFE, 0x03, 0xE0, 0x00, 0xFC, 0x00, 0x60, 0xC0, 0x1F, 0x00, 0x20, 0xF8, 0x03, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x06, // 253 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x07, 0xE0, 0xFF, 0xFF, 0x07, 0x00, 0x1C, 0x18, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x03, // 254 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x06, 0xC0, 0xF0, 0x01, 0x06, 0xC0, 0x80, 0x0F, 0x07, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0xFC, 0x00, 0xC0, 0xC0, 0x1F, 0x00, 0xC0, 0xF8, 0x03, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x06, // 255 }; #endif // OLED_PL ================================================ FILE: src/graphics/fonts/OLEDDisplayFontsPL.h ================================================ #ifndef OLEDDISPLAYFONTSPL_h #define OLEDDISPLAYFONTSPL_h #ifdef ARDUINO #include #elif __MBED__ #define PROGMEM #endif extern const uint8_t ArialMT_Plain_10_PL[] PROGMEM; extern const uint8_t ArialMT_Plain_16_PL[] PROGMEM; extern const uint8_t ArialMT_Plain_24_PL[] PROGMEM; #endif ================================================ FILE: src/graphics/fonts/OLEDDisplayFontsRU.cpp ================================================ #ifdef OLED_RU #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 }; // Font generated or edited with the glyphEditor (@mrekin) const uint8_t ArialMT_Plain_16_RU[] PROGMEM = { 0x10, // Width: 16 0x13, // Height: 19 0x20, // First char: 32 0xE0, // Number of chars: 224 // Jump Table: 0xFF, 0xFF, 0x00, 0x04, // 32 0x00, 0x00, 0x08, 0x05, // 33 0x00, 0x08, 0x0D, 0x06, // 34 0x00, 0x15, 0x1A, 0x09, // 35 0x00, 0x2F, 0x17, 0x09, // 36 0x00, 0x46, 0x26, 0x0E, // 37 0x00, 0x6C, 0x1D, 0x0B, // 38 0x00, 0x89, 0x04, 0x03, // 39 0x00, 0x8D, 0x0C, 0x05, // 40 0x00, 0x99, 0x0B, 0x05, // 41 0x00, 0xA4, 0x0D, 0x06, // 42 0x00, 0xB1, 0x17, 0x09, // 43 0x00, 0xC8, 0x09, 0x04, // 44 0x00, 0xD1, 0x0B, 0x05, // 45 0x00, 0xDC, 0x08, 0x04, // 46 0x00, 0xE4, 0x0A, 0x04, // 47 0x00, 0xEE, 0x17, 0x09, // 48 0x01, 0x05, 0x11, 0x09, // 49 0x01, 0x16, 0x17, 0x09, // 50 0x01, 0x2D, 0x17, 0x09, // 51 0x01, 0x44, 0x17, 0x09, // 52 0x01, 0x5B, 0x17, 0x09, // 53 0x01, 0x72, 0x17, 0x09, // 54 0x01, 0x89, 0x16, 0x09, // 55 0x01, 0x9F, 0x17, 0x09, // 56 0x01, 0xB6, 0x17, 0x09, // 57 0x01, 0xCD, 0x05, 0x04, // 58 0x01, 0xD2, 0x06, 0x04, // 59 0x01, 0xD8, 0x17, 0x09, // 60 0x01, 0xEF, 0x17, 0x09, // 61 0x02, 0x06, 0x17, 0x09, // 62 0x02, 0x1D, 0x16, 0x09, // 63 0x02, 0x33, 0x2F, 0x10, // 64 0x02, 0x62, 0x1D, 0x0B, // 65 0x02, 0x7F, 0x1D, 0x0B, // 66 0x02, 0x9C, 0x20, 0x0C, // 67 0x02, 0xBC, 0x20, 0x0C, // 68 0x02, 0xDC, 0x1D, 0x0B, // 69 0x02, 0xF9, 0x19, 0x0A, // 70 0x03, 0x12, 0x20, 0x0C, // 71 0x03, 0x32, 0x1D, 0x0B, // 72 0x03, 0x4F, 0x05, 0x03, // 73 0x03, 0x54, 0x14, 0x08, // 74 0x03, 0x68, 0x1D, 0x0B, // 75 0x03, 0x85, 0x17, 0x09, // 76 0x03, 0x9C, 0x23, 0x0D, // 77 0x03, 0xBF, 0x1D, 0x0B, // 78 0x03, 0xDC, 0x20, 0x0C, // 79 0x03, 0xFC, 0x1C, 0x0B, // 80 0x04, 0x18, 0x20, 0x0C, // 81 0x04, 0x38, 0x1D, 0x0B, // 82 0x04, 0x55, 0x1D, 0x0B, // 83 0x04, 0x72, 0x19, 0x09, // 84 0x04, 0x8B, 0x1D, 0x0B, // 85 0x04, 0xA8, 0x1C, 0x0B, // 86 0x04, 0xC4, 0x2B, 0x0F, // 87 0x04, 0xEF, 0x20, 0x0B, // 88 0x05, 0x0F, 0x19, 0x09, // 89 0x05, 0x28, 0x1A, 0x09, // 90 0x05, 0x42, 0x0C, 0x04, // 91 0x05, 0x4E, 0x0B, 0x04, // 92 0x05, 0x59, 0x09, 0x04, // 93 0x05, 0x62, 0x14, 0x07, // 94 0x05, 0x76, 0x1B, 0x09, // 95 0x05, 0x91, 0x07, 0x05, // 96 0x05, 0x98, 0x17, 0x09, // 97 0x05, 0xAF, 0x17, 0x09, // 98 0x05, 0xC6, 0x14, 0x08, // 99 0x05, 0xDA, 0x17, 0x09, // 100 0x05, 0xF1, 0x17, 0x09, // 101 0x06, 0x08, 0x0A, 0x04, // 102 0x06, 0x12, 0x17, 0x09, // 103 0x06, 0x29, 0x14, 0x08, // 104 0x06, 0x3D, 0x05, 0x04, // 105 0x06, 0x42, 0x06, 0x03, // 106 0x06, 0x48, 0x17, 0x08, // 107 0x06, 0x5F, 0x05, 0x03, // 108 0x06, 0x64, 0x23, 0x0D, // 109 0x06, 0x87, 0x14, 0x08, // 110 0x06, 0x9B, 0x17, 0x09, // 111 0x06, 0xB2, 0x17, 0x09, // 112 0x06, 0xC9, 0x18, 0x09, // 113 0x06, 0xE1, 0x0D, 0x05, // 114 0x06, 0xEE, 0x14, 0x08, // 115 0x07, 0x02, 0x0B, 0x04, // 116 0x07, 0x0D, 0x14, 0x08, // 117 0x07, 0x21, 0x13, 0x07, // 118 0x07, 0x34, 0x1F, 0x0B, // 119 0x07, 0x53, 0x14, 0x07, // 120 0x07, 0x67, 0x13, 0x07, // 121 0x07, 0x7A, 0x14, 0x07, // 122 0x07, 0x8E, 0x0F, 0x05, // 123 0x07, 0x9D, 0x06, 0x03, // 124 0x07, 0xA3, 0x0E, 0x05, // 125 0x07, 0xB1, 0x17, 0x09, // 126 0xFF, 0xFF, 0x00, 0x10, // 127 0xFF, 0xFF, 0x00, 0x10, // 128 0xFF, 0xFF, 0x00, 0x10, // 129 0xFF, 0xFF, 0x00, 0x10, // 130 0xFF, 0xFF, 0x00, 0x10, // 131 0xFF, 0xFF, 0x00, 0x10, // 132 0xFF, 0xFF, 0x00, 0x10, // 133 0xFF, 0xFF, 0x00, 0x10, // 134 0xFF, 0xFF, 0x00, 0x10, // 135 0xFF, 0xFF, 0x00, 0x10, // 136 0xFF, 0xFF, 0x00, 0x10, // 137 0xFF, 0xFF, 0x00, 0x10, // 138 0xFF, 0xFF, 0x00, 0x10, // 139 0xFF, 0xFF, 0x00, 0x10, // 140 0xFF, 0xFF, 0x00, 0x10, // 141 0xFF, 0xFF, 0x00, 0x10, // 142 0xFF, 0xFF, 0x00, 0x10, // 143 0xFF, 0xFF, 0x00, 0x10, // 144 0xFF, 0xFF, 0x00, 0x10, // 145 0xFF, 0xFF, 0x00, 0x10, // 146 0xFF, 0xFF, 0x00, 0x10, // 147 0xFF, 0xFF, 0x00, 0x10, // 148 0xFF, 0xFF, 0x00, 0x10, // 149 0xFF, 0xFF, 0x00, 0x10, // 150 0xFF, 0xFF, 0x00, 0x10, // 151 0xFF, 0xFF, 0x00, 0x10, // 152 0xFF, 0xFF, 0x00, 0x10, // 153 0xFF, 0xFF, 0x00, 0x10, // 154 0xFF, 0xFF, 0x00, 0x10, // 155 0xFF, 0xFF, 0x00, 0x10, // 156 0xFF, 0xFF, 0x00, 0x10, // 157 0xFF, 0xFF, 0x00, 0x10, // 158 0xFF, 0xFF, 0x00, 0x10, // 159 0xFF, 0xFF, 0x00, 0x10, // 160 0x07, 0xC8, 0x09, 0x05, // 161 0x07, 0xD1, 0x17, 0x09, // 162 0x07, 0xE8, 0x17, 0x09, // 163 0x07, 0xFF, 0x14, 0x09, // 164 0x08, 0x13, 0x1A, 0x09, // 165 0x08, 0x2D, 0x06, 0x03, // 166 0x08, 0x33, 0x17, 0x09, // 167 0x08, 0x4A, 0x1D, 0x0B, // 168 0x08, 0x67, 0x23, 0x0C, // 169 0x08, 0x8A, 0x0E, 0x05, // 170 0x08, 0x98, 0x14, 0x09, // 171 0x08, 0xAC, 0x17, 0x09, // 172 0x08, 0xC3, 0x0B, 0x05, // 173 0x08, 0xCE, 0x23, 0x0C, // 174 0x08, 0xF1, 0x19, 0x09, // 175 0x09, 0x0A, 0x0D, 0x06, // 176 0x09, 0x17, 0x17, 0x09, // 177 0x09, 0x2E, 0x0E, 0x05, // 178 0x09, 0x3C, 0x0D, 0x05, // 179 0x09, 0x49, 0x0A, 0x05, // 180 0x09, 0x53, 0x17, 0x09, // 181 0x09, 0x6A, 0x19, 0x09, // 182 0x09, 0x83, 0x08, 0x05, // 183 0x09, 0x8B, 0x17, 0x09, // 184 0x09, 0xA2, 0x0B, 0x05, // 185 0x09, 0xAD, 0x0D, 0x05, // 186 0x09, 0xBA, 0x17, 0x09, // 187 0x09, 0xD1, 0x26, 0x0D, // 188 0x09, 0xF7, 0x26, 0x0D, // 189 0x0A, 0x1D, 0x26, 0x0D, // 190 0x0A, 0x43, 0x1B, 0x0C, // 191 0x0A, 0x5E, 0x1D, 0x0B, // 192 0x0A, 0x7B, 0x1A, 0x0B, // 193 0x0A, 0x95, 0x1D, 0x0B, // 194 0x0A, 0xB2, 0x19, 0x09, // 195 0x0A, 0xCB, 0x1E, 0x0B, // 196 0x0A, 0xE9, 0x1D, 0x0B, // 197 0x0B, 0x06, 0x2C, 0x0F, // 198 0x0B, 0x32, 0x1A, 0x0A, // 199 0x0B, 0x4C, 0x20, 0x0C, // 200 0x0B, 0x6C, 0x20, 0x0C, // 201 0x0B, 0x8C, 0x1A, 0x09, // 202 0x0B, 0xA6, 0x1A, 0x0B, // 203 0x0B, 0xC0, 0x23, 0x0D, // 204 0x0B, 0xE3, 0x1D, 0x0B, // 205 0x0C, 0x00, 0x20, 0x0C, // 206 0x0C, 0x20, 0x1D, 0x0C, // 207 0x0C, 0x3D, 0x1C, 0x0B, // 208 0x0C, 0x59, 0x20, 0x0C, // 209 0x0C, 0x79, 0x19, 0x09, // 210 0x0C, 0x92, 0x1C, 0x0A, // 211 0x0C, 0xAE, 0x23, 0x0D, // 212 0x0C, 0xD1, 0x20, 0x0B, // 213 0x0C, 0xF1, 0x21, 0x0C, // 214 0x0D, 0x12, 0x1A, 0x0B, // 215 0x0D, 0x2C, 0x26, 0x0F, // 216 0x0D, 0x52, 0x2A, 0x0F, // 217 0x0D, 0x7C, 0x23, 0x0D, // 218 0x0D, 0x9F, 0x23, 0x0E, // 219 0x0D, 0xC2, 0x1A, 0x0A, // 220 0x0D, 0xDC, 0x1D, 0x0C, // 221 0x0D, 0xF9, 0x2C, 0x10, // 222 0x0E, 0x25, 0x20, 0x0C, // 223 0x0E, 0x45, 0x17, 0x09, // 224 0x0E, 0x5C, 0x1A, 0x09, // 225 0x0E, 0x76, 0x14, 0x09, // 226 0x0E, 0x8A, 0x10, 0x06, // 227 0x0E, 0x9A, 0x1B, 0x09, // 228 0x0E, 0xB5, 0x17, 0x09, // 229 0x0E, 0xCC, 0x20, 0x0B, // 230 0x0E, 0xEC, 0x11, 0x07, // 231 0x0E, 0xFD, 0x17, 0x09, // 232 0x0F, 0x14, 0x17, 0x09, // 233 0x0F, 0x2B, 0x14, 0x07, // 234 0x0F, 0x3F, 0x17, 0x09, // 235 0x0F, 0x56, 0x1D, 0x0B, // 236 0x0F, 0x73, 0x17, 0x09, // 237 0x0F, 0x8A, 0x17, 0x09, // 238 0x0F, 0xA1, 0x17, 0x09, // 239 0x0F, 0xB8, 0x17, 0x09, // 240 0x0F, 0xCF, 0x14, 0x08, // 241 0x0F, 0xE3, 0x13, 0x07, // 242 0x0F, 0xF6, 0x13, 0x07, // 243 0x10, 0x09, 0x23, 0x0D, // 244 0x10, 0x2C, 0x17, 0x09, // 245 0x10, 0x43, 0x1A, 0x0A, // 246 0x10, 0x5D, 0x14, 0x08, // 247 0x10, 0x71, 0x23, 0x0D, // 248 0x10, 0x94, 0x23, 0x0D, // 249 0x10, 0xB7, 0x1A, 0x0A, // 250 0x10, 0xD1, 0x1D, 0x0C, // 251 0x10, 0xEE, 0x17, 0x09, // 252 0x11, 0x05, 0x14, 0x08, // 253 0x11, 0x19, 0x20, 0x0C, // 254 0x11, 0x39, 0x17, 0x09, // 255 // Font Data: 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x5F, // 33 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, // 34 0x80, 0x08, 0x00, 0x80, 0x78, 0x00, 0xC0, 0x0F, 0x00, 0xB8, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x78, 0x00, 0xC0, 0x0F, 0x00, 0xB8, 0x08, 0x00, 0x80, 0x08, // 35 0x00, 0x00, 0x00, 0xE0, 0x10, 0x00, 0x10, 0x21, 0x00, 0x08, 0x41, 0x00, 0xFC, 0xFF, 0x00, 0x08, 0x42, 0x00, 0x08, 0x22, 0x00, 0x30, 0x1C, // 36 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0x08, 0x61, 0x00, 0xF0, 0x18, 0x00, 0x00, 0x06, 0x00, 0xC0, 0x01, 0x00, 0x30, 0x3C, 0x00, 0x08, 0x42, 0x00, 0x00, 0x42, 0x00, 0x00, 0x42, 0x00, 0x00, 0x3C, // 37 0x00, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x70, 0x22, 0x00, 0x88, 0x41, 0x00, 0x08, 0x43, 0x00, 0x88, 0x44, 0x00, 0x70, 0x28, 0x00, 0x00, 0x10, 0x00, 0x00, 0x28, 0x00, 0x00, 0x44, // 38 0x00, 0x00, 0x00, 0x78, // 39 0x00, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x70, 0xC0, 0x01, 0x08, 0x00, 0x02, // 40 0x00, 0x00, 0x00, 0x08, 0x00, 0x02, 0x70, 0xC0, 0x01, 0x80, 0x3F, // 41 0x10, 0x00, 0x00, 0xD0, 0x00, 0x00, 0x38, 0x00, 0x00, 0xD0, 0x00, 0x00, 0x10, // 42 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, // 43 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, // 44 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, // 45 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, // 46 0x00, 0x60, 0x00, 0x00, 0x1E, 0x00, 0xE0, 0x01, 0x00, 0x18, // 47 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0xE0, 0x1F, // 48 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0x00, 0x10, 0x00, 0x00, 0xF8, 0x7F, // 49 0x00, 0x00, 0x00, 0x20, 0x40, 0x00, 0x10, 0x60, 0x00, 0x08, 0x50, 0x00, 0x08, 0x48, 0x00, 0x08, 0x44, 0x00, 0x18, 0x43, 0x00, 0xE0, 0x40, // 50 0x00, 0x00, 0x00, 0x20, 0x30, 0x00, 0x10, 0x20, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x88, 0x41, 0x00, 0xF0, 0x22, 0x00, 0x00, 0x1C, // 51 0x00, 0x0C, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x09, 0x00, 0xC0, 0x08, 0x00, 0x20, 0x08, 0x00, 0x10, 0x08, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x08, // 52 0x00, 0x00, 0x00, 0xC0, 0x11, 0x00, 0xB8, 0x20, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x08, 0x21, 0x00, 0x08, 0x1E, // 53 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x10, 0x21, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x10, 0x21, 0x00, 0x20, 0x1E, // 54 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x78, 0x00, 0x88, 0x07, 0x00, 0x68, 0x00, 0x00, 0x18, // 55 0x00, 0x00, 0x00, 0x60, 0x1C, 0x00, 0x90, 0x22, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x90, 0x22, 0x00, 0x60, 0x1C, // 56 0x00, 0x00, 0x00, 0xE0, 0x11, 0x00, 0x10, 0x22, 0x00, 0x08, 0x44, 0x00, 0x08, 0x44, 0x00, 0x08, 0x44, 0x00, 0x10, 0x22, 0x00, 0xE0, 0x1F, // 57 0x00, 0x00, 0x00, 0x40, 0x40, // 58 0x00, 0x00, 0x00, 0x40, 0xC0, 0x01, // 59 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x05, 0x00, 0x00, 0x05, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x40, 0x10, // 60 0x00, 0x00, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, // 61 0x00, 0x00, 0x00, 0x40, 0x10, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x00, 0x05, 0x00, 0x00, 0x05, 0x00, 0x00, 0x02, // 62 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x5C, 0x00, 0x08, 0x02, 0x00, 0x10, 0x01, 0x00, 0xE0, // 63 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0xC0, 0x40, 0x00, 0x20, 0x80, 0x00, 0x10, 0x1E, 0x01, 0x10, 0x21, 0x01, 0x88, 0x40, 0x02, 0x48, 0x40, 0x02, 0x48, 0x40, 0x02, 0x48, 0x20, 0x02, 0x88, 0x7C, 0x02, 0xC8, 0x43, 0x02, 0x10, 0x40, 0x02, 0x10, 0x20, 0x01, 0x60, 0x10, 0x01, 0x80, 0x8F, // 64 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x70, 0x04, 0x00, 0x08, 0x04, 0x00, 0x70, 0x04, 0x00, 0x80, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, // 65 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x90, 0x22, 0x00, 0x60, 0x1C, // 66 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, // 67 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 68 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x40, // 69 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, // 70 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, 0x10, 0x22, 0x00, 0x20, 0x12, 0x00, 0x00, 0x0E, // 71 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0xF8, 0x7F, // 72 0x00, 0x00, 0x00, 0xF8, 0x7F, // 73 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x3F, // 74 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x02, 0x00, 0x00, 0x01, 0x00, 0x80, 0x03, 0x00, 0x40, 0x04, 0x00, 0x20, 0x18, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, // 75 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, // 76 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x30, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x03, 0x00, 0xC0, 0x00, 0x00, 0x30, 0x00, 0x00, 0xF8, 0x7F, // 77 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x10, 0x00, 0x00, 0x60, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x04, 0x00, 0x00, 0x18, 0x00, 0x00, 0x20, 0x00, 0xF8, 0x7F, // 78 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 79 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x10, 0x01, 0x00, 0xE0, // 80 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x50, 0x00, 0x08, 0x50, 0x00, 0x10, 0x20, 0x00, 0x20, 0x70, 0x00, 0xC0, 0x5F, // 81 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x06, 0x00, 0x08, 0x1A, 0x00, 0x10, 0x21, 0x00, 0xE0, 0x40, // 82 0x00, 0x00, 0x00, 0x60, 0x10, 0x00, 0x90, 0x20, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, 0x10, 0x22, 0x00, 0x20, 0x1C, // 83 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, // 84 0x00, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0xF8, 0x1F, // 85 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x18, 0x00, 0x00, 0x60, 0x00, 0x00, 0x18, 0x00, 0x00, 0x07, 0x00, 0xE0, 0x00, 0x00, 0x18, // 86 0x18, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x03, 0x00, 0x70, 0x00, 0x00, 0x08, 0x00, 0x00, 0x70, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1E, 0x00, 0xE0, 0x01, 0x00, 0x18, // 87 0x00, 0x40, 0x00, 0x08, 0x20, 0x00, 0x10, 0x10, 0x00, 0x60, 0x0C, 0x00, 0x80, 0x02, 0x00, 0x00, 0x01, 0x00, 0x80, 0x02, 0x00, 0x60, 0x0C, 0x00, 0x10, 0x10, 0x00, 0x08, 0x20, 0x00, 0x00, 0x40, // 88 0x08, 0x00, 0x00, 0x30, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x7E, 0x00, 0x80, 0x01, 0x00, 0x40, 0x00, 0x00, 0x30, 0x00, 0x00, 0x08, // 89 0x00, 0x40, 0x00, 0x08, 0x60, 0x00, 0x08, 0x58, 0x00, 0x08, 0x44, 0x00, 0x08, 0x43, 0x00, 0x88, 0x40, 0x00, 0x68, 0x40, 0x00, 0x18, 0x40, 0x00, 0x08, 0x40, // 90 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x03, 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, // 91 0x18, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x60, // 92 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, 0xF8, 0xFF, 0x03, // 93 0x00, 0x01, 0x00, 0xC0, 0x00, 0x00, 0x30, 0x00, 0x00, 0x08, 0x00, 0x00, 0x30, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x01, // 94 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, // 95 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x10, // 96 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x42, 0x00, 0x40, 0x22, 0x00, 0x80, 0x7F, // 97 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 98 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, // 99 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0xF8, 0x7F, // 100 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x24, 0x00, 0x00, 0x17, // 101 0x40, 0x00, 0x00, 0xF0, 0x7F, 0x00, 0x48, 0x00, 0x00, 0x48, // 102 0x00, 0x00, 0x00, 0x00, 0x1F, 0x01, 0x80, 0x20, 0x02, 0x40, 0x40, 0x02, 0x40, 0x40, 0x02, 0x40, 0x40, 0x02, 0x80, 0x20, 0x01, 0xC0, 0xFF, // 103 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x7F, // 104 0x00, 0x00, 0x00, 0xC8, 0x7F, // 105 0x00, 0x00, 0x02, 0xC8, 0xFF, 0x01, // 106 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x06, 0x00, 0x00, 0x19, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, // 107 0x00, 0x00, 0x00, 0xF8, 0x7F, // 108 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x7F, // 109 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x7F, // 110 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 111 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x80, 0x60, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 112 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0xC0, 0xFF, 0x03, // 113 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, // 114 0x00, 0x00, 0x00, 0x80, 0x23, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x38, // 115 0x40, 0x00, 0x00, 0xF0, 0x7F, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, // 116 0x00, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0xC0, 0x7F, // 117 0xC0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x03, 0x00, 0xC0, // 118 0xC0, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x03, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1F, 0x00, 0xC0, // 119 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, // 120 0xC0, 0x01, 0x00, 0x00, 0x06, 0x02, 0x00, 0x38, 0x02, 0x00, 0xC0, 0x01, 0x00, 0x78, 0x00, 0x00, 0x07, 0x00, 0xC0, // 121 0x40, 0x40, 0x00, 0x40, 0x60, 0x00, 0x40, 0x58, 0x00, 0x40, 0x44, 0x00, 0x40, 0x43, 0x00, 0xC0, 0x40, 0x00, 0x40, 0x40, // 122 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0xF0, 0xFB, 0x01, 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, // 123 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x03, // 124 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, 0xF0, 0xFB, 0x01, 0x00, 0x04, 0x00, 0x00, 0x04, // 125 0x00, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x01, // 126 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0xFF, 0x03, // 161 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x03, 0x40, 0xF0, 0x00, 0x40, 0x4E, 0x00, 0xC0, 0x41, 0x00, 0xB8, 0x20, 0x00, 0x00, 0x11, // 162 0x00, 0x41, 0x00, 0xF0, 0x31, 0x00, 0x18, 0x2F, 0x00, 0x08, 0x21, 0x00, 0x08, 0x61, 0x00, 0x08, 0x40, 0x00, 0x10, 0x40, 0x00, 0x20, 0x20, // 163 0x00, 0x00, 0x00, 0x40, 0x0B, 0x00, 0x80, 0x04, 0x00, 0x40, 0x08, 0x00, 0x40, 0x08, 0x00, 0x80, 0x04, 0x00, 0x40, 0x0B, // 164 0x08, 0x0A, 0x00, 0x10, 0x0A, 0x00, 0x60, 0x0A, 0x00, 0x80, 0x0B, 0x00, 0x00, 0x7E, 0x00, 0x80, 0x0B, 0x00, 0x60, 0x0A, 0x00, 0x10, 0x0A, 0x00, 0x08, 0x0A, // 165 0x00, 0x00, 0x00, 0xF8, 0xF1, 0x03, // 166 0x00, 0x86, 0x00, 0x70, 0x09, 0x01, 0xC8, 0x10, 0x02, 0x88, 0x10, 0x02, 0x08, 0x21, 0x02, 0x08, 0x61, 0x02, 0x30, 0xD2, 0x01, 0x00, 0x0C, // 167 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x09, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x08, 0x41, 0x00, 0x09, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x40, // 168 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0xC8, 0x47, 0x00, 0x28, 0x48, 0x00, 0x28, 0x48, 0x00, 0x28, 0x48, 0x00, 0x28, 0x48, 0x00, 0x48, 0x44, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 169 0xD0, 0x00, 0x00, 0x48, 0x01, 0x00, 0x28, 0x01, 0x00, 0x28, 0x01, 0x00, 0xF0, 0x01, // 170 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, 0x20, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, 0x20, // 171 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x0F, // 172 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, // 173 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0xE8, 0x4F, 0x00, 0x28, 0x41, 0x00, 0x28, 0x41, 0x00, 0x28, 0x43, 0x00, 0x28, 0x45, 0x00, 0xC8, 0x48, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 174 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, // 175 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x48, 0x00, 0x00, 0x48, 0x00, 0x00, 0x30, // 176 0x00, 0x00, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0xE0, 0x4F, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, // 177 0x10, 0x01, 0x00, 0x88, 0x01, 0x00, 0x48, 0x01, 0x00, 0x48, 0x01, 0x00, 0x30, 0x01, // 178 0x90, 0x00, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0x28, 0x01, 0x00, 0xD8, // 179 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, // 180 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0xC0, 0x7F, // 181 0xF0, 0x00, 0x00, 0xF8, 0x00, 0x00, 0xF8, 0x01, 0x00, 0xF8, 0x01, 0x00, 0xF8, 0xFF, 0x03, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0xFF, 0x03, 0x08, // 182 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, // 183 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x90, 0x24, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x90, 0x24, 0x00, 0x00, 0x17, // 184 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x01, // 185 0xF0, 0x00, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0xF0, // 186 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x04, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x04, // 187 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x40, 0x00, 0xF8, 0x21, 0x00, 0x00, 0x10, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x02, 0x00, 0x80, 0x01, 0x00, 0x40, 0x30, 0x00, 0x30, 0x28, 0x00, 0x08, 0x24, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x20, // 188 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x40, 0x00, 0xF8, 0x31, 0x00, 0x00, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x03, 0x00, 0x80, 0x00, 0x00, 0x60, 0x44, 0x00, 0x10, 0x62, 0x00, 0x08, 0x52, 0x00, 0x00, 0x52, 0x00, 0x00, 0x4C, // 189 0x90, 0x00, 0x00, 0x08, 0x01, 0x00, 0x08, 0x41, 0x00, 0x28, 0x21, 0x00, 0xD8, 0x18, 0x00, 0x00, 0x04, 0x00, 0x00, 0x03, 0x00, 0x80, 0x00, 0x00, 0x40, 0x30, 0x00, 0x30, 0x28, 0x00, 0x08, 0x24, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x20, // 190 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x60, 0x03, 0x00, 0x3B, 0x02, 0x00, 0x3B, 0x02, 0x00, 0x00, 0x03, 0x00, 0x80, 0x01, // 191 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x70, 0x04, 0x00, 0x08, 0x04, 0x00, 0x70, 0x04, 0x00, 0x80, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, // 192 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, 0x08, 0x66, 0x00, 0x08, 0x3C, // 193 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x90, 0x22, 0x00, 0x60, 0x1C, // 194 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x18, // 195 0x00, 0xC0, 0x01, 0x00, 0x60, 0x00, 0xF0, 0x5F, 0x00, 0x18, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0xC0, 0x01, // 196 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x40, // 197 0x08, 0x40, 0x00, 0x08, 0x60, 0x00, 0x38, 0x38, 0x00, 0xE0, 0x0C, 0x00, 0x80, 0x07, 0x00, 0x00, 0x03, 0x00, 0x00, 0x02, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x02, 0x00, 0x00, 0x03, 0x00, 0x80, 0x07, 0x00, 0xE0, 0x0C, 0x00, 0x38, 0x38, 0x00, 0x08, 0x60, 0x00, 0x08, 0x40, // 198 0x00, 0x00, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, 0x10, 0x22, 0x00, 0xE0, 0x1D, // 199 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x20, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x02, 0x00, 0x00, 0x01, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0xF8, 0x7F, // 200 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x20, 0x00, 0x01, 0x10, 0x00, 0x02, 0x08, 0x00, 0x02, 0x04, 0x00, 0x02, 0x02, 0x00, 0x01, 0x01, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0xF8, 0x7F, // 201 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x01, 0x00, 0x80, 0x02, 0x00, 0x40, 0x04, 0x00, 0x20, 0x08, 0x00, 0x10, 0x10, 0x00, 0x08, 0x20, 0x00, 0x08, 0x40, // 202 0x00, 0x40, 0x00, 0x00, 0x60, 0x00, 0xF0, 0x3F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x7F, // 203 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x30, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x10, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x03, 0x00, 0xC0, 0x00, 0x00, 0x30, 0x00, 0x00, 0xF8, 0x7F, // 204 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0xF8, 0x7F, // 205 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 206 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x7F, // 207 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x10, 0x01, 0x00, 0xE0, // 208 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, // 209 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, // 210 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0xE0, 0x80, 0x00, 0x80, 0x83, 0x00, 0x00, 0xE6, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x07, 0x00, 0xC0, 0x01, 0x00, 0x60, 0x00, 0x00, 0x18, // 211 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x60, 0x18, 0x00, 0x20, 0x10, 0x00, 0x20, 0x10, 0x00, 0x20, 0x10, 0x00, 0xF8, 0x7F, 0x00, 0x20, 0x10, 0x00, 0x20, 0x10, 0x00, 0x20, 0x10, 0x00, 0x60, 0x18, 0x00, 0xC0, 0x0F, // 212 0x08, 0x40, 0x00, 0x08, 0x20, 0x00, 0x10, 0x10, 0x00, 0x60, 0x0C, 0x00, 0x80, 0x02, 0x00, 0x00, 0x01, 0x00, 0x80, 0x02, 0x00, 0x60, 0x0C, 0x00, 0x10, 0x10, 0x00, 0x08, 0x20, 0x00, 0x08, 0x40, // 213 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0xC0, 0x01, // 214 0x00, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0xF8, 0x7F, // 215 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x7F, // 216 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0xC0, 0x01, // 217 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x63, 0x00, 0x00, 0x3E, // 218 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x63, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x7F, // 219 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x63, 0x00, 0x00, 0x3E, // 220 0x00, 0x00, 0x00, 0x30, 0x30, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x10, 0x61, 0x00, 0x10, 0x31, 0x00, 0xE0, 0x1F, // 221 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x80, 0x0F, 0x00, 0xE0, 0x18, 0x00, 0x30, 0x30, 0x00, 0x18, 0x60, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x18, 0x60, 0x00, 0x30, 0x30, 0x00, 0xE0, 0x18, 0x00, 0x80, 0x0F, // 222 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x61, 0x00, 0x18, 0x13, 0x00, 0x08, 0x0A, 0x00, 0x08, 0x06, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0xF8, 0x7F, // 223 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x42, 0x00, 0x40, 0x22, 0x00, 0x80, 0x7F, // 224 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x30, 0x73, 0x00, 0x90, 0x40, 0x00, 0x90, 0x40, 0x00, 0x90, 0x40, 0x00, 0x98, 0x61, 0x00, 0x18, 0x3F, 0x00, 0x00, 0x0C, // 225 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0xC0, 0x6E, 0x00, 0x80, 0x3B, // 226 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, // 227 0x00, 0xC0, 0x01, 0x00, 0x70, 0x00, 0x80, 0x5F, 0x00, 0xC0, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0xC0, 0x01, // 228 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x24, 0x00, 0x00, 0x17, // 229 0x40, 0x40, 0x00, 0xC0, 0x70, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x04, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x3B, 0x00, 0xC0, 0x60, 0x00, 0x40, 0x40, // 230 0x00, 0x00, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x3B, // 231 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x20, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x02, 0x00, 0xC0, 0x7F, // 232 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0x20, 0x08, 0x00, 0x20, 0x04, 0x00, 0x10, 0x02, 0x00, 0xC0, 0x7F, // 233 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x1B, 0x00, 0xC0, 0x70, 0x00, 0x40, 0x40, // 234 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x80, 0x7F, 0x00, 0xC0, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0xC0, 0x7F, // 235 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x40, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x38, 0x00, 0xC0, 0x07, 0x00, 0x40, 0x00, 0x00, 0xC0, 0x7F, // 236 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0xC0, 0x7F, // 237 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 238 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0xC0, 0x7F, // 239 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x80, 0x60, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 240 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, // 241 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, // 242 0xC0, 0x01, 0x00, 0x00, 0x06, 0x02, 0x00, 0x38, 0x02, 0x00, 0xC0, 0x01, 0x00, 0x78, 0x00, 0x00, 0x07, 0x00, 0xC0, // 243 0x00, 0x00, 0x00, 0x80, 0x3F, 0x00, 0xC0, 0x60, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0xC0, 0x60, 0x00, 0xE0, 0xFF, 0x03, 0xC0, 0x60, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0xC0, 0x60, 0x00, 0x80, 0x3F, // 244 0x00, 0x00, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, // 245 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0xC0, // 246 0x00, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0xC0, 0x7F, // 247 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xC0, 0x7F, // 248 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0xC0, // 249 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x64, 0x00, 0x00, 0x38, // 250 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x6C, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x7F, // 251 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x6C, 0x00, 0x00, 0x38, // 252 0x00, 0x00, 0x00, 0x00, 0x11, 0x00, 0x80, 0x20, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0xC0, 0x24, 0x00, 0x80, 0x1F, // 253 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1E, 0x00, 0x80, 0x33, 0x00, 0xC0, 0x60, 0x00, 0x40, 0x40, 0x00, 0xC0, 0x60, 0x00, 0x80, 0x31, 0x00, 0x00, 0x1F, // 254 0x00, 0x00, 0x00, 0x80, 0x43, 0x00, 0xC0, 0x7A, 0x00, 0x40, 0x0C, 0x00, 0x40, 0x04, 0x00, 0x40, 0x04, 0x00, 0xC0, 0x06, 0x00, 0xC0, 0x7F, // 255 }; // Font generated or edited with the glyphEditor (@mrekin) const uint8_t ArialMT_Plain_24_RU[] PROGMEM = { 0x18, // Width: 24 0x1C, // Height: 28 0x20, // First char: 32 0xE0, // Number of chars: 224 // Jump Table: 0xFF, 0xFF, 0x00, 0x07, // 32 0x00, 0x00, 0x13, 0x08, // 33 0x00, 0x13, 0x1A, 0x09, // 34 0x00, 0x2D, 0x33, 0x0D, // 35 0x00, 0x60, 0x2F, 0x0D, // 36 0x00, 0x8F, 0x4F, 0x15, // 37 0x00, 0xDE, 0x3B, 0x10, // 38 0x01, 0x19, 0x0A, 0x05, // 39 0x01, 0x23, 0x1C, 0x08, // 40 0x01, 0x3F, 0x1B, 0x08, // 41 0x01, 0x5A, 0x21, 0x09, // 42 0x01, 0x7B, 0x32, 0x0E, // 43 0x01, 0xAD, 0x10, 0x07, // 44 0x01, 0xBD, 0x1B, 0x08, // 45 0x01, 0xD8, 0x0F, 0x07, // 46 0x01, 0xE7, 0x19, 0x07, // 47 0x02, 0x00, 0x2F, 0x0D, // 48 0x02, 0x2F, 0x23, 0x0D, // 49 0x02, 0x52, 0x2F, 0x0D, // 50 0x02, 0x81, 0x2F, 0x0D, // 51 0x02, 0xB0, 0x2F, 0x0D, // 52 0x02, 0xDF, 0x2F, 0x0D, // 53 0x03, 0x0E, 0x2F, 0x0D, // 54 0x03, 0x3D, 0x2D, 0x0D, // 55 0x03, 0x6A, 0x2F, 0x0D, // 56 0x03, 0x99, 0x2F, 0x0D, // 57 0x03, 0xC8, 0x0F, 0x07, // 58 0x03, 0xD7, 0x10, 0x07, // 59 0x03, 0xE7, 0x2F, 0x0E, // 60 0x04, 0x16, 0x2F, 0x0E, // 61 0x04, 0x45, 0x2E, 0x0E, // 62 0x04, 0x73, 0x2E, 0x0D, // 63 0x04, 0xA1, 0x5B, 0x18, // 64 0x04, 0xFC, 0x3B, 0x0F, // 65 0x05, 0x37, 0x3B, 0x10, // 66 0x05, 0x72, 0x3F, 0x11, // 67 0x05, 0xB1, 0x3F, 0x11, // 68 0x05, 0xF0, 0x3B, 0x10, // 69 0x06, 0x2B, 0x35, 0x0F, // 70 0x06, 0x60, 0x43, 0x13, // 71 0x06, 0xA3, 0x3B, 0x11, // 72 0x06, 0xDE, 0x0F, 0x06, // 73 0x06, 0xED, 0x27, 0x0C, // 74 0x07, 0x14, 0x3F, 0x10, // 75 0x07, 0x53, 0x2F, 0x0D, // 76 0x07, 0x82, 0x43, 0x13, // 77 0x07, 0xC5, 0x3B, 0x11, // 78 0x08, 0x00, 0x47, 0x13, // 79 0x08, 0x47, 0x3A, 0x10, // 80 0x08, 0x81, 0x47, 0x13, // 81 0x08, 0xC8, 0x3F, 0x11, // 82 0x09, 0x07, 0x3B, 0x10, // 83 0x09, 0x42, 0x35, 0x0E, // 84 0x09, 0x77, 0x3B, 0x11, // 85 0x09, 0xB2, 0x39, 0x0F, // 86 0x09, 0xEB, 0x59, 0x17, // 87 0x0A, 0x44, 0x3B, 0x0F, // 88 0x0A, 0x7F, 0x3D, 0x10, // 89 0x0A, 0xBC, 0x37, 0x0F, // 90 0x0A, 0xF3, 0x14, 0x07, // 91 0x0B, 0x07, 0x1B, 0x07, // 92 0x0B, 0x22, 0x18, 0x07, // 93 0x0B, 0x3A, 0x2A, 0x0C, // 94 0x0B, 0x64, 0x34, 0x0D, // 95 0x0B, 0x98, 0x11, 0x08, // 96 0x0B, 0xA9, 0x2F, 0x0D, // 97 0x0B, 0xD8, 0x33, 0x0E, // 98 0x0C, 0x0B, 0x2B, 0x0C, // 99 0x0C, 0x36, 0x2F, 0x0E, // 100 0x0C, 0x65, 0x2F, 0x0D, // 101 0x0C, 0x94, 0x1A, 0x07, // 102 0x0C, 0xAE, 0x2F, 0x0E, // 103 0x0C, 0xDD, 0x2F, 0x0E, // 104 0x0D, 0x0C, 0x0F, 0x05, // 105 0x0D, 0x1B, 0x10, 0x06, // 106 0x0D, 0x2B, 0x2F, 0x0C, // 107 0x0D, 0x5A, 0x0F, 0x06, // 108 0x0D, 0x69, 0x47, 0x14, // 109 0x0D, 0xB0, 0x2F, 0x0E, // 110 0x0D, 0xDF, 0x2F, 0x0D, // 111 0x0E, 0x0E, 0x33, 0x0E, // 112 0x0E, 0x41, 0x30, 0x0E, // 113 0x0E, 0x71, 0x1E, 0x08, // 114 0x0E, 0x8F, 0x2B, 0x0C, // 115 0x0E, 0xBA, 0x1B, 0x07, // 116 0x0E, 0xD5, 0x2F, 0x0E, // 117 0x0F, 0x04, 0x2A, 0x0B, // 118 0x0F, 0x2E, 0x42, 0x11, // 119 0x0F, 0x70, 0x2B, 0x0B, // 120 0x0F, 0x9B, 0x2A, 0x0C, // 121 0x0F, 0xC5, 0x2B, 0x0C, // 122 0x0F, 0xF0, 0x1C, 0x08, // 123 0x10, 0x0C, 0x10, 0x06, // 124 0x10, 0x1C, 0x1B, 0x08, // 125 0x10, 0x37, 0x32, 0x0E, // 126 0xFF, 0xFF, 0x00, 0x18, // 127 0xFF, 0xFF, 0x00, 0x18, // 128 0xFF, 0xFF, 0x00, 0x18, // 129 0xFF, 0xFF, 0x00, 0x18, // 130 0xFF, 0xFF, 0x00, 0x18, // 131 0xFF, 0xFF, 0x00, 0x18, // 132 0xFF, 0xFF, 0x00, 0x18, // 133 0xFF, 0xFF, 0x00, 0x18, // 134 0xFF, 0xFF, 0x00, 0x18, // 135 0xFF, 0xFF, 0x00, 0x18, // 136 0xFF, 0xFF, 0x00, 0x18, // 137 0xFF, 0xFF, 0x00, 0x18, // 138 0xFF, 0xFF, 0x00, 0x18, // 139 0xFF, 0xFF, 0x00, 0x18, // 140 0xFF, 0xFF, 0x00, 0x18, // 141 0xFF, 0xFF, 0x00, 0x18, // 142 0xFF, 0xFF, 0x00, 0x18, // 143 0xFF, 0xFF, 0x00, 0x18, // 144 0xFF, 0xFF, 0x00, 0x18, // 145 0xFF, 0xFF, 0x00, 0x18, // 146 0xFF, 0xFF, 0x00, 0x18, // 147 0xFF, 0xFF, 0x00, 0x18, // 148 0xFF, 0xFF, 0x00, 0x18, // 149 0xFF, 0xFF, 0x00, 0x18, // 150 0xFF, 0xFF, 0x00, 0x18, // 151 0xFF, 0xFF, 0x00, 0x18, // 152 0xFF, 0xFF, 0x00, 0x18, // 153 0xFF, 0xFF, 0x00, 0x18, // 154 0xFF, 0xFF, 0x00, 0x18, // 155 0xFF, 0xFF, 0x00, 0x18, // 156 0xFF, 0xFF, 0x00, 0x18, // 157 0xFF, 0xFF, 0x00, 0x18, // 158 0xFF, 0xFF, 0x00, 0x18, // 159 0xFF, 0xFF, 0x00, 0x07, // 160 0x10, 0x69, 0x14, 0x08, // 161 0x10, 0x7D, 0x2B, 0x0D, // 162 0x10, 0xA8, 0x2F, 0x0D, // 163 0x10, 0xD7, 0x33, 0x0D, // 164 0x11, 0x0A, 0x31, 0x0D, // 165 0x11, 0x3B, 0x10, 0x06, // 166 0x11, 0x4B, 0x2F, 0x0D, // 167 0x11, 0x7A, 0x3B, 0x10, // 168 0x11, 0xB5, 0x46, 0x12, // 169 0x11, 0xFB, 0x1A, 0x09, // 170 0x12, 0x15, 0x27, 0x0D, // 171 0x12, 0x3C, 0x2F, 0x0E, // 172 0x12, 0x6B, 0x1B, 0x08, // 173 0x12, 0x86, 0x46, 0x12, // 174 0x12, 0xCC, 0x31, 0x0D, // 175 0x12, 0xFD, 0x1E, 0x0A, // 176 0x13, 0x1B, 0x33, 0x0D, // 177 0x13, 0x4E, 0x1A, 0x08, // 178 0x13, 0x68, 0x1A, 0x08, // 179 0x13, 0x82, 0x19, 0x08, // 180 0x13, 0x9B, 0x2F, 0x0E, // 181 0x13, 0xCA, 0x31, 0x0D, // 182 0x13, 0xFB, 0x12, 0x08, // 183 0x14, 0x0D, 0x2F, 0x0D, // 184 0x14, 0x3C, 0x16, 0x08, // 185 0x14, 0x52, 0x1E, 0x09, // 186 0x14, 0x70, 0x2E, 0x0D, // 187 0x14, 0x9E, 0x4F, 0x14, // 188 0x14, 0xED, 0x4B, 0x14, // 189 0x15, 0x38, 0x4B, 0x14, // 190 0x15, 0x83, 0x3B, 0x12, // 191 0x15, 0xBE, 0x3B, 0x0F, // 192 0x15, 0xF9, 0x3B, 0x10, // 193 0x16, 0x34, 0x3B, 0x10, // 194 0x16, 0x6F, 0x31, 0x0D, // 195 0x16, 0xA0, 0x3C, 0x10, // 196 0x16, 0xDC, 0x3B, 0x10, // 197 0x17, 0x17, 0x57, 0x16, // 198 0x17, 0x6E, 0x33, 0x0F, // 199 0x17, 0xA1, 0x3B, 0x11, // 200 0x17, 0xDC, 0x3B, 0x11, // 201 0x18, 0x17, 0x37, 0x0E, // 202 0x18, 0x4E, 0x37, 0x10, // 203 0x18, 0x85, 0x43, 0x13, // 204 0x18, 0xC8, 0x3B, 0x11, // 205 0x19, 0x03, 0x47, 0x13, // 206 0x19, 0x4A, 0x3B, 0x11, // 207 0x19, 0x85, 0x3A, 0x10, // 208 0x19, 0xBF, 0x3F, 0x11, // 209 0x19, 0xFE, 0x35, 0x0E, // 210 0x1A, 0x33, 0x39, 0x0F, // 211 0x1A, 0x6C, 0x42, 0x12, // 212 0x1A, 0xAE, 0x3B, 0x0F, // 213 0x1A, 0xE9, 0x43, 0x12, // 214 0x1B, 0x2C, 0x37, 0x10, // 215 0x1B, 0x63, 0x4F, 0x16, // 216 0x1B, 0xB2, 0x58, 0x17, // 217 0x1C, 0x0A, 0x47, 0x13, // 218 0x1C, 0x51, 0x4B, 0x15, // 219 0x1C, 0x9C, 0x3B, 0x10, // 220 0x1C, 0xD7, 0x3F, 0x11, // 221 0x1D, 0x16, 0x5B, 0x18, // 222 0x1D, 0x71, 0x3B, 0x11, // 223 0x1D, 0xAC, 0x2F, 0x0D, // 224 0x1D, 0xDB, 0x33, 0x0E, // 225 0x1E, 0x0E, 0x2F, 0x0D, // 226 0x1E, 0x3D, 0x22, 0x09, // 227 0x1E, 0x5F, 0x33, 0x0E, // 228 0x1E, 0x92, 0x2F, 0x0D, // 229 0x1E, 0xC1, 0x3F, 0x10, // 230 0x1F, 0x00, 0x27, 0x0B, // 231 0x1F, 0x27, 0x2F, 0x0D, // 232 0x1F, 0x56, 0x2F, 0x0D, // 233 0x1F, 0x85, 0x27, 0x0B, // 234 0x1F, 0xAC, 0x2F, 0x0E, // 235 0x1F, 0xDB, 0x3B, 0x11, // 236 0x20, 0x16, 0x2F, 0x0D, // 237 0x20, 0x45, 0x2F, 0x0D, // 238 0x20, 0x74, 0x2B, 0x0D, // 239 0x20, 0x9F, 0x33, 0x0E, // 240 0x20, 0xD2, 0x2B, 0x0C, // 241 0x20, 0xFD, 0x2A, 0x0B, // 242 0x21, 0x27, 0x2A, 0x0C, // 243 0x21, 0x51, 0x4B, 0x14, // 244 0x21, 0x9C, 0x2B, 0x0B, // 245 0x21, 0xC7, 0x33, 0x0E, // 246 0x21, 0xFA, 0x2B, 0x0D, // 247 0x22, 0x25, 0x47, 0x13, // 248 0x22, 0x6C, 0x4B, 0x14, // 249 0x22, 0xB7, 0x37, 0x0F, // 250 0x22, 0xEE, 0x3B, 0x11, // 251 0x23, 0x29, 0x2F, 0x0D, // 252 0x23, 0x58, 0x2B, 0x0C, // 253 0x23, 0x83, 0x43, 0x12, // 254 0x23, 0xC6, 0x2B, 0x0D, // 255 // Font Data: 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x33, 0x00, 0xE0, 0xFF, 0x33, // 33 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xE0, 0x07, // 34 0x00, 0x0C, 0x03, 0x00, 0x00, 0x0C, 0x33, 0x00, 0x00, 0x0C, 0x3F, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x80, 0xFF, 0x03, 0x00, 0xE0, 0x0F, 0x03, 0x00, 0x60, 0x0C, 0x33, 0x00, 0x00, 0x0C, 0x3F, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x80, 0xFF, 0x03, 0x00, 0xE0, 0x0F, 0x03, 0x00, 0x60, 0x0C, 0x03, 0x00, 0x00, 0x0C, 0x03, // 35 0x00, 0x00, 0x00, 0x00, 0x80, 0x07, 0x06, 0x00, 0xC0, 0x0F, 0x1E, 0x00, 0xC0, 0x18, 0x1C, 0x00, 0x60, 0x18, 0x38, 0x00, 0x60, 0x30, 0x30, 0x00, 0xF0, 0xFF, 0xFF, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x60, 0x30, 0x00, 0xC0, 0x60, 0x18, 0x00, 0xC0, 0xC1, 0x1F, 0x00, 0x80, 0x81, 0x07, // 36 0x00, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x20, 0x20, 0x00, 0x00, 0x20, 0x20, 0x20, 0x00, 0x60, 0x30, 0x38, 0x00, 0xC0, 0x1F, 0x1E, 0x00, 0x80, 0x8F, 0x0F, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x8F, 0x0F, 0x00, 0xC0, 0xC3, 0x1F, 0x00, 0xE0, 0x60, 0x30, 0x00, 0x60, 0x20, 0x20, 0x00, 0x00, 0x20, 0x20, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x80, 0x0F, // 37 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x80, 0xE3, 0x1C, 0x00, 0xC0, 0x7F, 0x30, 0x00, 0xE0, 0x3C, 0x30, 0x00, 0x60, 0x38, 0x30, 0x00, 0x60, 0x78, 0x30, 0x00, 0xE0, 0xEC, 0x38, 0x00, 0xC0, 0x8F, 0x1B, 0x00, 0x80, 0x03, 0x1F, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x80, 0x38, 0x00, 0x00, 0x00, 0x10, // 38 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xE0, 0x07, // 39 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0xFE, 0x7F, 0x00, 0x80, 0x0F, 0xF0, 0x01, 0xC0, 0x01, 0x80, 0x03, 0x60, 0x00, 0x00, 0x06, 0x20, 0x00, 0x00, 0x04, // 40 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x04, 0x60, 0x00, 0x00, 0x06, 0xC0, 0x01, 0x80, 0x03, 0x80, 0x0F, 0xF0, 0x01, 0x00, 0xFE, 0x7F, 0x00, 0x00, 0xF0, 0x0F, // 41 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x04, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x80, 0x04, 0x00, 0x00, 0x80, // 42 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xFF, 0x0F, 0x00, 0x00, 0xFF, 0x0F, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, // 43 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x03, 0x00, 0x00, 0xF0, 0x01, // 44 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, // 45 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, // 46 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x80, 0x3F, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x60, // 47 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x80, 0xFF, 0x1F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x38, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0x00, 0xFE, 0x03, // 48 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 49 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x30, 0x00, 0xC0, 0x03, 0x38, 0x00, 0xC0, 0x00, 0x3C, 0x00, 0x60, 0x00, 0x36, 0x00, 0x60, 0x00, 0x33, 0x00, 0x60, 0x80, 0x33, 0x00, 0x60, 0xC0, 0x31, 0x00, 0x60, 0x60, 0x30, 0x00, 0xC0, 0x30, 0x30, 0x00, 0xC0, 0x1F, 0x30, 0x00, 0x00, 0x0F, 0x30, // 50 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x06, 0x00, 0xC0, 0x01, 0x0E, 0x00, 0xC0, 0x00, 0x1C, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0xC0, 0x38, 0x30, 0x00, 0xC0, 0x6F, 0x18, 0x00, 0x80, 0xC7, 0x0F, 0x00, 0x00, 0x80, 0x07, // 51 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x3C, 0x03, 0x00, 0x00, 0x0E, 0x03, 0x00, 0x80, 0x07, 0x03, 0x00, 0xC0, 0x01, 0x03, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, // 52 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x06, 0x00, 0x80, 0x3F, 0x1E, 0x00, 0xE0, 0x1F, 0x18, 0x00, 0x60, 0x08, 0x30, 0x00, 0x60, 0x0C, 0x30, 0x00, 0x60, 0x0C, 0x30, 0x00, 0x60, 0x0C, 0x30, 0x00, 0x60, 0x0C, 0x30, 0x00, 0x60, 0x18, 0x1C, 0x00, 0x60, 0xF0, 0x0F, 0x00, 0x00, 0xE0, 0x03, // 53 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x03, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0xC0, 0x63, 0x1C, 0x00, 0xC0, 0x30, 0x38, 0x00, 0x60, 0x18, 0x30, 0x00, 0x60, 0x18, 0x30, 0x00, 0x60, 0x18, 0x30, 0x00, 0x60, 0x18, 0x30, 0x00, 0xE0, 0x30, 0x18, 0x00, 0xC0, 0xF1, 0x0F, 0x00, 0x80, 0xC1, 0x07, // 54 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x3C, 0x00, 0x60, 0x80, 0x3F, 0x00, 0x60, 0xF0, 0x03, 0x00, 0x60, 0x78, 0x00, 0x00, 0x60, 0x0E, 0x00, 0x00, 0x60, 0x03, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x60, // 55 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x80, 0xC7, 0x1F, 0x00, 0xC0, 0x6F, 0x18, 0x00, 0xE0, 0x38, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0xE0, 0x38, 0x30, 0x00, 0xC0, 0x7F, 0x18, 0x00, 0x80, 0xC7, 0x1F, 0x00, 0x00, 0x80, 0x07, // 56 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x0C, 0x00, 0x80, 0x7F, 0x1C, 0x00, 0xC0, 0x61, 0x38, 0x00, 0x60, 0xC0, 0x30, 0x00, 0x60, 0xC0, 0x30, 0x00, 0x60, 0xC0, 0x30, 0x00, 0x60, 0xC0, 0x30, 0x00, 0x60, 0x60, 0x18, 0x00, 0xC0, 0x31, 0x1E, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0x00, 0xFE, 0x01, // 57 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, // 58 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x30, 0x03, 0x00, 0x06, 0xF0, 0x01, // 59 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0xD8, 0x00, 0x00, 0x00, 0xD8, 0x00, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x04, 0x01, 0x00, 0x00, 0x06, 0x03, 0x00, 0x00, 0x06, 0x03, 0x00, 0x00, 0x03, 0x06, // 60 0x00, 0x00, 0x00, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, // 61 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x06, 0x00, 0x00, 0x06, 0x03, 0x00, 0x00, 0x06, 0x03, 0x00, 0x00, 0x04, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0x8C, 0x01, 0x00, 0x00, 0xD8, 0x00, 0x00, 0x00, 0xD8, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x20, // 62 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x60, 0x80, 0x33, 0x00, 0x60, 0xC0, 0x33, 0x00, 0x60, 0xE0, 0x00, 0x00, 0xE0, 0x30, 0x00, 0x00, 0xC0, 0x38, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x00, 0x07, // 63 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x1E, 0xF0, 0x00, 0x00, 0x07, 0xC0, 0x01, 0x80, 0xC3, 0x87, 0x01, 0xC0, 0xF1, 0x9F, 0x03, 0xC0, 0x38, 0x18, 0x03, 0xC0, 0x0C, 0x30, 0x03, 0x60, 0x0E, 0x30, 0x06, 0x60, 0x06, 0x30, 0x06, 0x60, 0x06, 0x18, 0x06, 0x60, 0x06, 0x1C, 0x06, 0x60, 0x0C, 0x1E, 0x06, 0x60, 0xF8, 0x3F, 0x06, 0xE0, 0xFE, 0x31, 0x06, 0xC0, 0x0E, 0x30, 0x06, 0xC0, 0x01, 0x18, 0x03, 0x80, 0x03, 0x1C, 0x03, 0x00, 0x07, 0x8F, 0x01, 0x00, 0xFE, 0x87, 0x01, 0x00, 0xF8, 0xC1, 0x00, 0x00, 0x00, 0x40, // 64 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x80, 0x8F, 0x01, 0x00, 0xE0, 0x83, 0x01, 0x00, 0x60, 0x80, 0x01, 0x00, 0xE0, 0x83, 0x01, 0x00, 0x80, 0x8F, 0x01, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 65 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0xC0, 0x78, 0x30, 0x00, 0xC0, 0xFF, 0x18, 0x00, 0x80, 0xC7, 0x1F, 0x00, 0x00, 0x80, 0x07, // 66 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0E, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x03, 0x0F, 0x00, 0x00, 0x02, 0x03, // 67 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x03, 0x0E, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 68 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 69 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, // 70 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xE0, 0x00, 0x18, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x60, 0x30, 0x00, 0x60, 0x60, 0x30, 0x00, 0xE0, 0x60, 0x38, 0x00, 0xC0, 0x60, 0x18, 0x00, 0xC0, 0x61, 0x18, 0x00, 0x80, 0xE3, 0x0F, 0x00, 0x00, 0xE2, 0x0F, // 71 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 72 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 73 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x38, 0x00, 0xE0, 0xFF, 0x1F, 0x00, 0xE0, 0xFF, 0x0F, // 74 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0xE7, 0x01, 0x00, 0x80, 0x83, 0x07, 0x00, 0xC0, 0x01, 0x0F, 0x00, 0xE0, 0x00, 0x1E, 0x00, 0x60, 0x00, 0x38, 0x00, 0x20, 0x00, 0x30, 0x00, 0x00, 0x00, 0x20, // 75 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, // 76 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xFE, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 77 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 78 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x0F, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 79 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x00, 0x0F, // 80 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xE0, 0x00, 0x18, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x36, 0x00, 0x60, 0x00, 0x36, 0x00, 0xE0, 0x00, 0x3C, 0x00, 0xC0, 0x00, 0x1C, 0x00, 0xC0, 0x01, 0x3C, 0x00, 0x80, 0x07, 0x3F, 0x00, 0x00, 0xFF, 0x77, 0x00, 0x00, 0xFC, 0x61, // 81 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x70, 0x00, 0x00, 0x60, 0xF0, 0x00, 0x00, 0x60, 0xF0, 0x03, 0x00, 0x60, 0xB0, 0x07, 0x00, 0xE0, 0x18, 0x1F, 0x00, 0xC0, 0x1F, 0x3C, 0x00, 0x80, 0x0F, 0x30, 0x00, 0x00, 0x00, 0x20, // 82 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x07, 0x0F, 0x00, 0xC0, 0x1F, 0x1C, 0x00, 0xC0, 0x18, 0x18, 0x00, 0x60, 0x38, 0x38, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x70, 0x30, 0x00, 0x60, 0x70, 0x30, 0x00, 0xC0, 0x60, 0x18, 0x00, 0xC0, 0xE1, 0x1C, 0x00, 0x80, 0xC3, 0x0F, 0x00, 0x00, 0x83, 0x07, // 83 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, // 84 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x07, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x1C, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0x07, // 85 0x20, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x20, // 86 0x60, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x80, 0xFF, 0x00, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x80, 0x3F, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x80, 0xFF, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x60, // 87 0x00, 0x00, 0x20, 0x00, 0x20, 0x00, 0x30, 0x00, 0x60, 0x00, 0x3C, 0x00, 0xE0, 0x01, 0x1E, 0x00, 0xC0, 0x83, 0x0F, 0x00, 0x00, 0xCF, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x80, 0xCF, 0x03, 0x00, 0xC0, 0x83, 0x07, 0x00, 0xE0, 0x01, 0x1E, 0x00, 0x60, 0x00, 0x3C, 0x00, 0x20, 0x00, 0x30, 0x00, 0x00, 0x00, 0x20, // 88 0x20, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x20, // 89 0x00, 0x00, 0x30, 0x00, 0x60, 0x00, 0x38, 0x00, 0x60, 0x00, 0x3C, 0x00, 0x60, 0x00, 0x37, 0x00, 0x60, 0x80, 0x33, 0x00, 0x60, 0xC0, 0x31, 0x00, 0x60, 0xE0, 0x30, 0x00, 0x60, 0x38, 0x30, 0x00, 0x60, 0x1C, 0x30, 0x00, 0x60, 0x0E, 0x30, 0x00, 0x60, 0x07, 0x30, 0x00, 0xE0, 0x01, 0x30, 0x00, 0xE0, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, // 90 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x07, 0xE0, 0xFF, 0xFF, 0x07, 0x60, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0x06, // 91 0x60, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 92 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0x06, 0xE0, 0xFF, 0xFF, 0x07, 0xE0, 0xFF, 0xFF, 0x07, // 93 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x20, // 94 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, // 95 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x80, // 96 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0x00, 0x1C, 0x1F, 0x00, 0x00, 0x8C, 0x39, 0x00, 0x00, 0x86, 0x31, 0x00, 0x00, 0x86, 0x31, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x18, 0x00, 0x00, 0xCE, 0x1C, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x00, 0x20, // 97 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x18, 0x0C, 0x00, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x03, // 98 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x18, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x18, 0x0C, // 99 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x18, 0x0C, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 100 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xCC, 0x1C, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x00, 0xDC, 0x18, 0x00, 0x00, 0xF8, 0x0C, 0x00, 0x00, 0xF0, 0x04, // 101 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0xC0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x06, 0x00, 0x00, 0x60, 0x06, 0x00, 0x00, 0x60, 0x06, // 102 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x83, 0x01, 0x00, 0xF8, 0x8F, 0x03, 0x00, 0x1C, 0x1C, 0x07, 0x00, 0x0E, 0x38, 0x06, 0x00, 0x06, 0x30, 0x06, 0x00, 0x06, 0x30, 0x06, 0x00, 0x06, 0x30, 0x06, 0x00, 0x0C, 0x18, 0x07, 0x00, 0x18, 0x8C, 0x03, 0x00, 0xFE, 0xFF, 0x03, 0x00, 0xFE, 0xFF, // 103 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xF8, 0x3F, // 104 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0xFE, 0x3F, 0x00, 0x60, 0xFE, 0x3F, // 105 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x60, 0xFE, 0xFF, 0x07, 0x60, 0xFE, 0xFF, 0x03, // 106 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0x98, 0x07, 0x00, 0x00, 0x0C, 0x0F, 0x00, 0x00, 0x06, 0x3C, 0x00, 0x00, 0x02, 0x30, 0x00, 0x00, 0x00, 0x20, // 107 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 108 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xF8, 0x3F, // 109 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xF8, 0x3F, // 110 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x07, // 111 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0x18, 0x0C, 0x00, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x03, // 112 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x18, 0x0C, 0x00, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0xFE, 0xFF, 0x07, // 113 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, // 114 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x0C, 0x00, 0x00, 0x7C, 0x1C, 0x00, 0x00, 0xEE, 0x38, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x31, 0x00, 0x00, 0xC6, 0x31, 0x00, 0x00, 0x8E, 0x39, 0x00, 0x00, 0x9C, 0x1F, 0x00, 0x00, 0x18, 0x0F, // 115 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0xC0, 0xFF, 0x1F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, // 116 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x0F, 0x00, 0x00, 0xFE, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, // 117 0x00, 0x06, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x06, // 118 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x0E, // 119 0x00, 0x02, 0x20, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x38, 0x0E, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x38, 0x0E, 0x00, 0x00, 0x1C, 0x3C, 0x00, 0x00, 0x0E, 0x30, 0x00, 0x00, 0x02, 0x20, // 120 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x06, 0x00, 0xF0, 0x01, 0x06, 0x00, 0x80, 0x0F, 0x07, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0xFC, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x06, // 121 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x06, 0x3C, 0x00, 0x00, 0x06, 0x3E, 0x00, 0x00, 0x06, 0x37, 0x00, 0x00, 0xC6, 0x33, 0x00, 0x00, 0xE6, 0x30, 0x00, 0x00, 0x76, 0x30, 0x00, 0x00, 0x3E, 0x30, 0x00, 0x00, 0x1E, 0x30, 0x00, 0x00, 0x06, 0x30, // 122 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xC0, 0x03, 0x00, 0xC0, 0xFF, 0xFF, 0x03, 0xE0, 0x3F, 0xFC, 0x07, 0x60, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0x06, // 123 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x0F, 0xE0, 0xFF, 0xFF, 0x0F, // 124 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0x06, 0xE0, 0x3F, 0xFC, 0x07, 0xC0, 0xFF, 0xFF, 0x03, 0x00, 0xC0, 0x03, 0x00, 0x00, 0x80, 0x01, // 125 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x60, // 126 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE6, 0xFF, 0x07, 0x00, 0xE6, 0xFF, 0x07, // 161 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x9C, 0x07, 0x00, 0x0E, 0x78, 0x00, 0x00, 0x06, 0x3F, 0x00, 0x00, 0xE6, 0x30, 0x00, 0x00, 0x1E, 0x30, 0x00, 0xE0, 0x0D, 0x18, 0x00, 0x00, 0x1C, 0x0E, 0x00, 0x00, 0x10, 0x06, // 162 0x00, 0x60, 0x10, 0x00, 0x00, 0x60, 0x38, 0x00, 0x80, 0x7F, 0x1C, 0x00, 0xC0, 0xFF, 0x1F, 0x00, 0xE0, 0xE0, 0x19, 0x00, 0x60, 0x60, 0x18, 0x00, 0x60, 0x60, 0x18, 0x00, 0x60, 0x60, 0x30, 0x00, 0xE0, 0x00, 0x30, 0x00, 0xC0, 0x01, 0x30, 0x00, 0x80, 0x01, 0x38, 0x00, 0x00, 0x00, 0x10, // 163 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x04, 0x00, 0x00, 0xF7, 0x0E, 0x00, 0x00, 0xFE, 0x07, 0x00, 0x00, 0x0C, 0x03, 0x00, 0x00, 0x06, 0x06, 0x00, 0x00, 0x06, 0x06, 0x00, 0x00, 0x06, 0x06, 0x00, 0x00, 0x06, 0x06, 0x00, 0x00, 0x0C, 0x03, 0x00, 0x00, 0xFE, 0x07, 0x00, 0x00, 0xF7, 0x0E, 0x00, 0x00, 0x02, 0x04, // 164 0xE0, 0x60, 0x06, 0x00, 0xC0, 0x61, 0x06, 0x00, 0x80, 0x67, 0x06, 0x00, 0x00, 0x6E, 0x06, 0x00, 0x00, 0x7C, 0x06, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x7C, 0x06, 0x00, 0x00, 0x7E, 0x06, 0x00, 0x80, 0x67, 0x06, 0x00, 0xC0, 0x61, 0x06, 0x00, 0xE0, 0x60, 0x06, 0x00, 0x20, // 165 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x7F, 0xF8, 0x0F, 0xE0, 0x7F, 0xF8, 0x0F, // 166 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x80, 0xF3, 0xC1, 0x00, 0xC0, 0x1F, 0xC3, 0x03, 0xE0, 0x0C, 0x07, 0x03, 0x60, 0x1C, 0x06, 0x06, 0x60, 0x18, 0x0C, 0x06, 0x60, 0x30, 0x1C, 0x06, 0xE0, 0x70, 0x38, 0x07, 0xC0, 0xE1, 0xF4, 0x03, 0x80, 0xC1, 0xE7, 0x01, 0x00, 0x80, 0x03, // 167 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x6C, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 168 0x00, 0xF8, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0x07, 0x07, 0x00, 0x80, 0x01, 0x0C, 0x00, 0xC0, 0x78, 0x1C, 0x00, 0xC0, 0xFE, 0x19, 0x00, 0x60, 0x86, 0x31, 0x00, 0x60, 0x03, 0x33, 0x00, 0x60, 0x03, 0x33, 0x00, 0x60, 0x03, 0x33, 0x00, 0x60, 0x03, 0x33, 0x00, 0x60, 0x87, 0x33, 0x00, 0xC0, 0x86, 0x19, 0x00, 0xC0, 0x85, 0x1C, 0x00, 0x80, 0x01, 0x0C, 0x00, 0x00, 0x07, 0x07, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0xF8, // 169 0x00, 0x00, 0x00, 0x00, 0xC0, 0x1C, 0x00, 0x00, 0xE0, 0x3E, 0x00, 0x00, 0x60, 0x32, 0x00, 0x00, 0x60, 0x32, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0xC0, 0x3F, // 170 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x78, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x84, 0x10, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x78, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x04, 0x10, // 171 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFC, 0x01, // 172 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, // 173 0x00, 0xF8, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0x07, 0x07, 0x00, 0x80, 0x01, 0x0C, 0x00, 0xC0, 0x00, 0x1C, 0x00, 0xC0, 0xFE, 0x1B, 0x00, 0x60, 0xFE, 0x33, 0x00, 0x60, 0x66, 0x30, 0x00, 0x60, 0x66, 0x30, 0x00, 0x60, 0xE6, 0x30, 0x00, 0x60, 0xFE, 0x31, 0x00, 0x60, 0x3C, 0x33, 0x00, 0xC0, 0x00, 0x1A, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x01, 0x0C, 0x00, 0x00, 0x07, 0x07, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0xF8, // 174 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, // 175 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x40, 0x04, 0x00, 0x00, 0x20, 0x08, 0x00, 0x00, 0x20, 0x08, 0x00, 0x00, 0x20, 0x08, 0x00, 0x00, 0x40, 0x04, 0x00, 0x00, 0x80, 0x03, // 176 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0xFF, 0x3F, 0x00, 0x00, 0xFF, 0x3F, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, // 177 0x40, 0x20, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x20, 0x38, 0x00, 0x00, 0x20, 0x2C, 0x00, 0x00, 0x20, 0x26, 0x00, 0x00, 0xE0, 0x23, 0x00, 0x00, 0xC0, 0x21, // 178 0x40, 0x10, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x20, 0x20, 0x00, 0x00, 0x20, 0x22, 0x00, 0x00, 0x20, 0x22, 0x00, 0x00, 0xE0, 0x3D, 0x00, 0x00, 0xC0, 0x1D, // 179 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x20, // 180 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xFE, 0x3F, // 181 0x00, 0x0F, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0xE0, 0x7F, 0x00, 0x00, 0xE0, 0x7F, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x07, 0xE0, 0xFF, 0xFF, 0x07, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x07, 0xE0, 0xFF, 0xFF, 0x07, 0x60, 0x00, 0x00, 0x00, 0x60, // 182 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, // 183 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x80, 0x9C, 0x1C, 0x00, 0x00, 0x8C, 0x30, 0x00, 0x00, 0x86, 0x30, 0x00, 0x00, 0x86, 0x30, 0x00, 0x00, 0x86, 0x30, 0x00, 0x00, 0x8C, 0x30, 0x00, 0x80, 0x9C, 0x18, 0x00, 0x00, 0xF8, 0x1C, 0x00, 0x00, 0xF0, 0x0C, // 184 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0xE0, 0x3F, // 185 0x00, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xE0, 0x38, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0xE0, 0x38, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x80, 0x0F, // 186 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x10, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x78, 0x0F, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x84, 0x10, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x78, 0x0F, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x80, // 187 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x20, 0x00, 0xE0, 0x3F, 0x38, 0x00, 0xE0, 0x3F, 0x1C, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x80, 0x07, 0x0C, 0x00, 0xC0, 0x01, 0x0E, 0x00, 0xE0, 0x80, 0x0B, 0x00, 0x60, 0xC0, 0x08, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x00, 0x08, // 188 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x20, 0x00, 0xE0, 0x3F, 0x30, 0x00, 0xE0, 0x3F, 0x1C, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x4E, 0x20, 0x00, 0x00, 0x67, 0x30, 0x00, 0xC0, 0x21, 0x38, 0x00, 0xE0, 0x20, 0x2C, 0x00, 0x60, 0x20, 0x26, 0x00, 0x00, 0xE0, 0x27, 0x00, 0x00, 0xC0, 0x21, // 189 0x40, 0x10, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x20, 0x20, 0x00, 0x00, 0x20, 0x22, 0x20, 0x00, 0x20, 0x22, 0x30, 0x00, 0xE0, 0x3D, 0x38, 0x00, 0xC0, 0x1D, 0x0E, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x0E, 0x0C, 0x00, 0x00, 0x07, 0x0E, 0x00, 0x80, 0x83, 0x0B, 0x00, 0xE0, 0xC0, 0x09, 0x00, 0x60, 0xE0, 0x3F, 0x00, 0x20, 0xE0, 0x3F, 0x00, 0x00, 0x00, 0x08, // 190 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x0C, 0x03, 0x00, 0xF6, 0x07, 0x03, 0x00, 0xF6, 0x07, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x90, 0x03, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0xE0, // 191 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x80, 0x8F, 0x01, 0x00, 0xE0, 0x83, 0x01, 0x00, 0x60, 0x80, 0x01, 0x00, 0xE0, 0x83, 0x01, 0x00, 0x80, 0x8F, 0x01, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x30, // 192 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x60, 0x38, 0x00, 0x60, 0xE0, 0x1F, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x80, 0x07, // 193 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0xC0, 0x78, 0x30, 0x00, 0xC0, 0xFF, 0x18, 0x00, 0x80, 0xC7, 0x1F, 0x00, 0x00, 0x80, 0x07, // 194 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, // 195 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0x3E, 0x00, 0x80, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x33, 0x00, 0xE0, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0xFF, 0x01, 0x00, 0x00, 0xF0, 0x01, // 196 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x00, 0x30, // 197 0x60, 0x00, 0x20, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x3C, 0x00, 0xE0, 0x00, 0x1F, 0x00, 0xC0, 0x87, 0x07, 0x00, 0x00, 0xCF, 0x03, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0xCF, 0x03, 0x00, 0xC0, 0x87, 0x07, 0x00, 0xE0, 0x01, 0x1F, 0x00, 0x60, 0x00, 0x3C, 0x00, 0x60, 0x00, 0x38, 0x00, 0x60, 0x00, 0x20, // 198 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x06, 0x00, 0x80, 0x03, 0x0E, 0x00, 0xC0, 0x03, 0x1C, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x78, 0x30, 0x00, 0xE0, 0x7C, 0x38, 0x00, 0xC0, 0xEF, 0x1F, 0x00, 0x00, 0xC7, 0x0F, // 199 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 200 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x06, 0x00, 0x0F, 0x00, 0x0C, 0x80, 0x03, 0x00, 0x08, 0xE0, 0x01, 0x00, 0x08, 0x70, 0x00, 0x00, 0x08, 0x3C, 0x00, 0x00, 0x0C, 0x0E, 0x00, 0x00, 0x86, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 201 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0xCF, 0x03, 0x00, 0xC0, 0x87, 0x07, 0x00, 0xE0, 0x00, 0x1F, 0x00, 0x60, 0x00, 0x3C, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x20, // 202 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0xC0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x1F, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 203 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xFE, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 204 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 205 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0F, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x38, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x0F, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 206 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 207 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x00, 0x0F, // 208 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x80, 0x07, 0x0E, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xC0, 0x00, 0x18, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x03, 0x0F, 0x00, 0x00, 0x02, 0x03, // 209 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, // 210 0x20, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x30, 0x00, 0x80, 0x07, 0x30, 0x00, 0x00, 0x1E, 0x30, 0x00, 0x00, 0x78, 0x30, 0x00, 0x00, 0xE0, 0x38, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x20, // 211 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x00, 0x8F, 0x03, 0x00, 0x00, 0x03, 0x07, 0x00, 0x80, 0x01, 0x06, 0x00, 0x80, 0x01, 0x0C, 0x00, 0x80, 0x01, 0x0C, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x80, 0x01, 0x0C, 0x00, 0x80, 0x01, 0x0C, 0x00, 0x80, 0x01, 0x06, 0x00, 0x00, 0x03, 0x07, 0x00, 0x00, 0x87, 0x03, 0x00, 0x00, 0xFE, 0x01, 0x00, 0x00, 0xF8, // 212 0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0x30, 0x00, 0x60, 0x00, 0x38, 0x00, 0xE0, 0x01, 0x1E, 0x00, 0xC0, 0x83, 0x07, 0x00, 0x00, 0xCF, 0x03, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x80, 0xCF, 0x03, 0x00, 0xC0, 0x83, 0x07, 0x00, 0xE0, 0x01, 0x1E, 0x00, 0x60, 0x00, 0x38, 0x00, 0x20, 0x00, 0x30, 0x00, 0x20, 0x00, 0x20, // 213 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0xF0, // 214 0x00, 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 215 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 216 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0xF0, 0x01, // 217 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x70, 0x38, 0x00, 0x00, 0xE0, 0x1C, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x80, 0x07, // 218 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x70, 0x38, 0x00, 0x00, 0xE0, 0x1C, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 219 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x70, 0x38, 0x00, 0x00, 0xE0, 0x1C, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x80, 0x07, // 220 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x02, 0x00, 0x80, 0x03, 0x0E, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0xC0, 0x00, 0x18, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0x60, 0x30, 0x30, 0x00, 0xC0, 0x30, 0x18, 0x00, 0xC0, 0x30, 0x1C, 0x00, 0x80, 0x33, 0x0E, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x03, // 221 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0xC0, 0x03, 0x1E, 0x00, 0xE0, 0x00, 0x18, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0x60, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x38, 0x00, 0xC0, 0x01, 0x1C, 0x00, 0x80, 0x07, 0x0F, 0x00, 0x00, 0xFF, 0x07, 0x00, 0x00, 0xFC, 0x01, // 222 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x30, 0x00, 0x80, 0x0F, 0x38, 0x00, 0xC0, 0x1D, 0x1E, 0x00, 0xE0, 0xB8, 0x0F, 0x00, 0x60, 0xF0, 0x03, 0x00, 0x60, 0xF0, 0x01, 0x00, 0x60, 0x70, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0xE0, 0xFF, 0x3F, 0x00, 0xE0, 0xFF, 0x3F, // 223 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0x00, 0x1C, 0x1F, 0x00, 0x00, 0x8C, 0x39, 0x00, 0x00, 0x86, 0x31, 0x00, 0x00, 0x86, 0x31, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x10, 0x00, 0x00, 0xCE, 0x18, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0x00, 0x20, // 224 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0xC0, 0x19, 0x1E, 0x00, 0xE0, 0x0C, 0x38, 0x00, 0x60, 0x0C, 0x30, 0x00, 0x60, 0x04, 0x30, 0x00, 0x60, 0x04, 0x30, 0x00, 0x60, 0x0C, 0x30, 0x00, 0x60, 0x0C, 0x38, 0x00, 0x60, 0x78, 0x1E, 0x00, 0x70, 0xF0, 0x0F, 0x00, 0x10, 0xC0, 0x03, // 225 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x84, 0x31, 0x00, 0x00, 0x84, 0x31, 0x00, 0x00, 0x84, 0x31, 0x00, 0x00, 0x84, 0x31, 0x00, 0x00, 0xCC, 0x31, 0x00, 0x00, 0xFC, 0x31, 0x00, 0x00, 0x78, 0x1B, 0x00, 0x00, 0x00, 0x0E, // 226 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, // 227 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x30, 0x00, 0x00, 0x04, 0x30, 0x00, 0x00, 0x04, 0x30, 0x00, 0x00, 0x04, 0x30, 0x00, 0x00, 0x04, 0x30, 0x00, 0x00, 0x04, 0x30, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x00, 0xF0, // 228 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xCC, 0x1C, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x00, 0xDC, 0x18, 0x00, 0x00, 0xF8, 0x0C, 0x00, 0x00, 0xF0, 0x04, // 229 0x00, 0x04, 0x20, 0x00, 0x00, 0x04, 0x38, 0x00, 0x00, 0x0C, 0x3C, 0x00, 0x00, 0x38, 0x0F, 0x00, 0x00, 0x60, 0x03, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x60, 0x03, 0x00, 0x00, 0x38, 0x0F, 0x00, 0x00, 0x0C, 0x1C, 0x00, 0x00, 0x04, 0x38, 0x00, 0x00, 0x04, 0x20, // 230 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x1C, 0x00, 0x00, 0x1C, 0x3C, 0x00, 0x00, 0x0C, 0x30, 0x00, 0x00, 0x86, 0x20, 0x00, 0x00, 0x86, 0x20, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xFC, 0x31, 0x00, 0x00, 0x7C, 0x3F, 0x00, 0x00, 0x30, 0x1F, // 231 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, // 232 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x60, 0xFC, 0x3F, 0x00, 0xC0, 0x00, 0x3C, 0x00, 0x80, 0x00, 0x0F, 0x00, 0x80, 0xC0, 0x03, 0x00, 0x80, 0xE0, 0x01, 0x00, 0xC0, 0x70, 0x00, 0x00, 0x60, 0x18, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, // 233 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x70, 0x07, 0x00, 0x00, 0x3C, 0x1E, 0x00, 0x00, 0x0C, 0x3C, 0x00, 0x00, 0x04, 0x30, // 234 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, // 235 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, // 236 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, // 237 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x07, // 238 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, // 239 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0x18, 0x0C, 0x00, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x03, // 240 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x18, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x18, 0x0C, // 241 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, // 242 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x06, 0x00, 0xF0, 0x01, 0x06, 0x00, 0x80, 0x0F, 0x07, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0xFC, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x06, // 243 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x18, 0x00, 0x00, 0x0C, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x20, 0x00, 0x00, 0x04, 0x30, 0x00, 0x00, 0x0C, 0x18, 0x00, 0x80, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0x01, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x04, 0x30, 0x00, 0x00, 0x06, 0x20, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0C, 0x30, 0x00, 0x00, 0x1C, 0x18, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x07, // 244 0x00, 0x02, 0x20, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x38, 0x0E, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x38, 0x0E, 0x00, 0x00, 0x1C, 0x3C, 0x00, 0x00, 0x0E, 0x30, 0x00, 0x00, 0x02, 0x20, // 245 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0xFF, 0x00, 0x00, 0x00, 0xF0, // 246 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, // 247 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, // 248 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0xFF, 0x00, 0x00, 0x00, 0xF0, // 249 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0x80, 0x39, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0x0F, // 250 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x31, 0x00, 0x00, 0x80, 0x39, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, // 251 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0xC0, 0x30, 0x00, 0x00, 0x80, 0x31, 0x00, 0x00, 0x80, 0x3B, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x0E, // 252 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x02, 0x00, 0x00, 0x1C, 0x0E, 0x00, 0x00, 0x0C, 0x1C, 0x00, 0x00, 0x06, 0x38, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xC6, 0x30, 0x00, 0x00, 0xCC, 0x38, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF0, 0x07, // 253 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x3C, 0x1E, 0x00, 0x00, 0x0C, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x20, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0C, 0x30, 0x00, 0x00, 0x1C, 0x3C, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0xE0, 0x07, // 254 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0x30, 0x00, 0x00, 0xFC, 0x38, 0x00, 0x00, 0xCC, 0x1D, 0x00, 0x00, 0x8C, 0x0F, 0x00, 0x00, 0x84, 0x03, 0x00, 0x00, 0x84, 0x01, 0x00, 0x00, 0x84, 0x01, 0x00, 0x00, 0x84, 0x01, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, // 255 }; #endif // OLED_RU ================================================ FILE: src/graphics/fonts/OLEDDisplayFontsRU.h ================================================ #ifndef OLEDDISPLAYFONTSRU_h #define OLEDDISPLAYFONTSRU_h #ifdef ARDUINO #include #elif __MBED__ #define PROGMEM #endif extern const uint8_t ArialMT_Plain_10_RU[] PROGMEM; extern const uint8_t ArialMT_Plain_16_RU[] PROGMEM; extern const uint8_t ArialMT_Plain_24_RU[] PROGMEM; #endif ================================================ FILE: src/graphics/fonts/OLEDDisplayFontsUA.cpp ================================================ #ifdef OLED_UA #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, 0x06, 0x04, // 191 0x04, 0xA5, 0x0A, 0x06, // 192 0x04, 0xAF, 0x0A, 0x06, // 193 0x04, 0xB9, 0x0A, 0x06, // 194 0x04, 0xC3, 0x09, 0x06, // 195 0x04, 0xCC, 0x0A, 0x06, // 196 0x04, 0xD6, 0x0A, 0x06, // 197 0x04, 0xE0, 0x0A, 0x06, // 198 0x04, 0xEA, 0x08, 0x05, // 199 0x04, 0xF2, 0x0A, 0x06, // 200 0x04, 0xFC, 0x0A, 0x06, // 201 0x05, 0x06, 0x0A, 0x06, // 202 0x05, 0x10, 0x0A, 0x06, // 203 0x05, 0x1A, 0x0A, 0x06, // 204 0x05, 0x24, 0x0A, 0x06, // 205 0x05, 0x2E, 0x0A, 0x06, // 206 0x05, 0x38, 0x0A, 0x06, // 207 0x05, 0x42, 0x09, 0x06, // 208 0x05, 0x4B, 0x0A, 0x06, // 209 0x05, 0x55, 0x09, 0x06, // 210 0x05, 0x5E, 0x0A, 0x06, // 211 0x05, 0x68, 0x09, 0x06, // 212 0x05, 0x71, 0x0A, 0x06, // 213 0x05, 0x7B, 0x0A, 0x06, // 214 0x05, 0x85, 0x08, 0x05, // 215 0x05, 0x8D, 0x0A, 0x06, // 216 0x05, 0x97, 0x0C, 0x07, // 217 0x05, 0xA3, 0x0A, 0x06, // 218 0x05, 0xAD, 0x0A, 0x06, // 219 0x05, 0xB7, 0x08, 0x05, // 220 0x05, 0xBF, 0x0A, 0x06, // 221 0x05, 0xC9, 0x0A, 0x06, // 222 0x05, 0xD3, 0x0A, 0x06, // 223 0x05, 0xDD, 0x08, 0x05, // 224 0x05, 0xE5, 0x08, 0x05, // 225 0x05, 0xED, 0x08, 0x05, // 226 0x05, 0xF5, 0x07, 0x05, // 227 0x05, 0xFC, 0x0A, 0x06, // 228 0x06, 0x06, 0x09, 0x06, // 229 0x06, 0x0F, 0x0A, 0x06, // 230 0x06, 0x19, 0x08, 0x05, // 231 0x06, 0x21, 0x0A, 0x06, // 232 0x06, 0x2B, 0x0A, 0x06, // 233 0x06, 0x35, 0x06, 0x04, // 234 0x06, 0x3B, 0x08, 0x05, // 235 0x06, 0x43, 0x0A, 0x06, // 236 0x06, 0x4D, 0x08, 0x05, // 237 0x06, 0x55, 0x08, 0x05, // 238 0x06, 0x5D, 0x08, 0x05, // 239 0x06, 0x65, 0x05, 0x04, // 240 0x06, 0x6A, 0x08, 0x05, // 241 0x06, 0x72, 0x05, 0x04, // 242 0x06, 0x77, 0x08, 0x05, // 243 0x06, 0x7F, 0x09, 0x06, // 244 0x06, 0x88, 0x0A, 0x06, // 245 0x06, 0x92, 0x08, 0x05, // 246 0x06, 0x9A, 0x08, 0x05, // 247 0x06, 0xA2, 0x0A, 0x06, // 248 0x06, 0xAC, 0x0C, 0x07, // 249 0x06, 0xB8, 0x08, 0x05, // 250 0x06, 0xC0, 0x08, 0x05, // 251 0x06, 0xC8, 0x08, 0x05, // 252 0x06, 0xD0, 0x08, 0x05, // 253 0x06, 0xD8, 0x0A, 0x06, // 254 0x06, 0xE2, 0x08, 0x05, // 255 // Font Data: 0x00, 0x00, 0xF8, 0x02, // 33 0x38, 0x00, 0x00, 0x00, 0x38, // 34 0xA0, 0x03, 0xE0, 0x00, 0xB8, 0x03, 0xE0, 0x00, 0xB8, // 35 0x30, 0x01, 0x28, 0x02, 0xF8, 0x07, 0x48, 0x02, 0x90, 0x01, // 36 0x00, 0x00, 0x30, 0x00, 0x48, 0x00, 0x30, 0x03, 0xC0, 0x00, 0xB0, 0x01, 0x48, 0x02, 0x80, 0x01, // 37 0x80, 0x01, 0x50, 0x02, 0x68, 0x02, 0xA8, 0x02, 0x18, 0x01, 0x80, 0x03, 0x80, 0x02, // 38 0x38, // 39 0xE0, 0x03, 0x10, 0x04, 0x08, 0x08, // 40 0x08, 0x08, 0x10, 0x04, 0xE0, 0x03, // 41 0x28, 0x00, 0x18, 0x00, 0x28, // 42 0x40, 0x00, 0x40, 0x00, 0xF0, 0x01, 0x40, 0x00, 0x40, // 43 0x00, 0x00, 0x00, 0x06, // 44 0x80, 0x00, 0x80, // 45 0x00, 0x00, 0x00, 0x02, // 46 0x00, 0x03, 0xE0, 0x00, 0x18, // 47 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0xF0, 0x01, // 48 0x00, 0x00, 0x20, 0x00, 0x10, 0x00, 0xF8, 0x03, // 49 0x10, 0x02, 0x08, 0x03, 0x88, 0x02, 0x48, 0x02, 0x30, 0x02, // 50 0x10, 0x01, 0x08, 0x02, 0x48, 0x02, 0x48, 0x02, 0xB0, 0x01, // 51 0xC0, 0x00, 0xA0, 0x00, 0x90, 0x00, 0x88, 0x00, 0xF8, 0x03, 0x80, // 52 0x60, 0x01, 0x38, 0x02, 0x28, 0x02, 0x28, 0x02, 0xC8, 0x01, // 53 0xF0, 0x01, 0x28, 0x02, 0x28, 0x02, 0x28, 0x02, 0xD0, 0x01, // 54 0x08, 0x00, 0x08, 0x03, 0xC8, 0x00, 0x38, 0x00, 0x08, // 55 0xB0, 0x01, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0xB0, 0x01, // 56 0x70, 0x01, 0x88, 0x02, 0x88, 0x02, 0x88, 0x02, 0xF0, 0x01, // 57 0x00, 0x00, 0x20, 0x02, // 58 0x00, 0x00, 0x20, 0x06, // 59 0x00, 0x00, 0x40, 0x00, 0xA0, 0x00, 0xA0, 0x00, 0x10, 0x01, // 60 0xA0, 0x00, 0xA0, 0x00, 0xA0, 0x00, 0xA0, 0x00, 0xA0, // 61 0x00, 0x00, 0x10, 0x01, 0xA0, 0x00, 0xA0, 0x00, 0x40, // 62 0x10, 0x00, 0x08, 0x00, 0x08, 0x00, 0xC8, 0x02, 0x48, 0x00, 0x30, // 63 0x00, 0x00, 0xC0, 0x03, 0x30, 0x04, 0xD0, 0x09, 0x28, 0x0A, 0x28, 0x0A, 0xC8, 0x0B, 0x68, 0x0A, 0x10, 0x05, 0xE0, 0x04, // 64 0x00, 0x02, 0xC0, 0x01, 0xB0, 0x00, 0x88, 0x00, 0xB0, 0x00, 0xC0, 0x01, 0x00, 0x02, // 65 0x00, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0xF0, 0x01, // 66 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0x10, 0x01, // 67 0x00, 0x00, 0xF8, 0x03, 0x08, 0x02, 0x08, 0x02, 0x10, 0x01, 0xE0, // 68 0x00, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, // 69 0x00, 0x00, 0xF8, 0x03, 0x48, 0x00, 0x48, 0x00, 0x08, // 70 0x00, 0x00, 0xE0, 0x00, 0x10, 0x01, 0x08, 0x02, 0x48, 0x02, 0x50, 0x01, 0xC0, // 71 0x00, 0x00, 0xF8, 0x03, 0x40, 0x00, 0x40, 0x00, 0x40, 0x00, 0xF8, 0x03, // 72 0x00, 0x00, 0xF8, 0x03, // 73 0x00, 0x03, 0x00, 0x02, 0x00, 0x02, 0xF8, 0x01, // 74 0x00, 0x00, 0xF8, 0x03, 0x80, 0x00, 0x60, 0x00, 0x90, 0x00, 0x08, 0x01, 0x00, 0x02, // 75 0x00, 0x00, 0xF8, 0x03, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, // 76 0x00, 0x00, 0xF8, 0x03, 0x30, 0x00, 0xC0, 0x01, 0x00, 0x02, 0xC0, 0x01, 0x30, 0x00, 0xF8, 0x03, // 77 0x00, 0x00, 0xF8, 0x03, 0x30, 0x00, 0x40, 0x00, 0x80, 0x01, 0xF8, 0x03, // 78 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0xF0, 0x01, // 79 0x00, 0x00, 0xF8, 0x03, 0x48, 0x00, 0x48, 0x00, 0x48, 0x00, 0x30, // 80 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x03, 0x08, 0x03, 0xF0, 0x02, // 81 0x00, 0x00, 0xF8, 0x03, 0x48, 0x00, 0x48, 0x00, 0xC8, 0x00, 0x30, 0x03, // 82 0x00, 0x00, 0x30, 0x01, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0x90, 0x01, // 83 0x00, 0x00, 0x08, 0x00, 0x08, 0x00, 0xF8, 0x03, 0x08, 0x00, 0x08, // 84 0x00, 0x00, 0xF8, 0x01, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0xF8, 0x01, // 85 0x08, 0x00, 0x70, 0x00, 0x80, 0x01, 0x00, 0x02, 0x80, 0x01, 0x70, 0x00, 0x08, // 86 0x18, 0x00, 0xE0, 0x01, 0x00, 0x02, 0xF0, 0x01, 0x08, 0x00, 0xF0, 0x01, 0x00, 0x02, 0xE0, 0x01, 0x18, // 87 0x00, 0x02, 0x08, 0x01, 0x90, 0x00, 0x60, 0x00, 0x90, 0x00, 0x08, 0x01, 0x00, 0x02, // 88 0x08, 0x00, 0x10, 0x00, 0x20, 0x00, 0xC0, 0x03, 0x20, 0x00, 0x10, 0x00, 0x08, // 89 0x08, 0x03, 0x88, 0x02, 0xC8, 0x02, 0x68, 0x02, 0x38, 0x02, 0x18, 0x02, // 90 0x00, 0x00, 0xF8, 0x0F, 0x08, 0x08, // 91 0x18, 0x00, 0xE0, 0x00, 0x00, 0x03, // 92 0x08, 0x08, 0xF8, 0x0F, // 93 0x40, 0x00, 0x30, 0x00, 0x08, 0x00, 0x30, 0x00, 0x40, // 94 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, // 95 0x08, 0x00, 0x10, // 96 0x00, 0x00, 0x00, 0x03, 0xA0, 0x02, 0xA0, 0x02, 0xE0, 0x03, // 97 0x00, 0x00, 0xF8, 0x03, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // 98 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0x40, 0x01, // 99 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0xF8, 0x03, // 100 0x00, 0x00, 0xC0, 0x01, 0xA0, 0x02, 0xA0, 0x02, 0xC0, 0x02, // 101 0x20, 0x00, 0xF0, 0x03, 0x28, // 102 0x00, 0x00, 0xC0, 0x05, 0x20, 0x0A, 0x20, 0x0A, 0xE0, 0x07, // 103 0x00, 0x00, 0xF8, 0x03, 0x20, 0x00, 0x20, 0x00, 0xC0, 0x03, // 104 0x00, 0x00, 0xE8, 0x03, // 105 0x00, 0x08, 0xE8, 0x07, // 106 0xF8, 0x03, 0x80, 0x00, 0xC0, 0x01, 0x20, 0x02, // 107 0x00, 0x00, 0xF8, 0x03, // 108 0x00, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0xC0, 0x03, // 109 0x00, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0xC0, 0x03, // 110 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // 111 0x00, 0x00, 0xE0, 0x0F, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // 112 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0xE0, 0x0F, // 113 0x00, 0x00, 0xE0, 0x03, 0x20, // 114 0x40, 0x02, 0xA0, 0x02, 0xA0, 0x02, 0x20, 0x01, // 115 0x20, 0x00, 0xF8, 0x03, 0x20, 0x02, // 116 0x00, 0x00, 0xE0, 0x01, 0x00, 0x02, 0x00, 0x02, 0xE0, 0x03, // 117 0x20, 0x00, 0xC0, 0x01, 0x00, 0x02, 0xC0, 0x01, 0x20, // 118 0xE0, 0x01, 0x00, 0x02, 0xC0, 0x01, 0x20, 0x00, 0xC0, 0x01, 0x00, 0x02, 0xE0, 0x01, // 119 0x20, 0x02, 0x40, 0x01, 0x80, 0x00, 0x40, 0x01, 0x20, 0x02, // 120 0x20, 0x00, 0xC0, 0x09, 0x00, 0x06, 0xC0, 0x01, 0x20, // 121 0x20, 0x02, 0x20, 0x03, 0xA0, 0x02, 0x60, 0x02, 0x20, 0x02, // 122 0x80, 0x00, 0x78, 0x0F, 0x08, 0x08, // 123 0x00, 0x00, 0xF8, 0x0F, // 124 0x08, 0x08, 0x78, 0x0F, 0x80, // 125 0xC0, 0x00, 0x40, 0x00, 0xC0, 0x00, 0x80, 0x00, 0xC0, // 126 0x00, 0x00, 0xA0, 0x0F, // 161 0x00, 0x00, 0xC0, 0x01, 0xA0, 0x0F, 0x78, 0x02, 0x40, 0x01, // 162 0x40, 0x02, 0x70, 0x03, 0xC8, 0x02, 0x48, 0x02, 0x08, 0x02, 0x10, 0x02, // 163 0x00, 0x00, 0xE0, 0x01, 0x20, 0x01, 0x20, 0x01, 0xE0, 0x01, // 164 0x00, 0x00, 0xF8, 0x03, 0x08, 0x00, 0x08, 0x00, 0x0C, // 165 0x00, 0x00, 0x38, 0x0F, // 166 0xD0, 0x04, 0x28, 0x09, 0x48, 0x09, 0x48, 0x0A, 0x90, 0x05, // 167 0x00, 0x00, 0xE0, 0x03, 0xA8, 0x02, 0xA0, 0x02, 0xA8, 0x02, 0x20, 0x02, // 168 0xE0, 0x00, 0x10, 0x01, 0x48, 0x02, 0xA8, 0x02, 0xA8, 0x02, 0x10, 0x01, 0xE0, // 169 0x00, 0x00, 0xF0, 0x01, 0x58, 0x03, 0x48, 0x02, 0x08, 0x02, 0x10, 0x01, // 170 0x00, 0x00, 0x80, 0x01, 0x40, 0x02, 0x80, 0x01, 0x40, 0x02, // 171 0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0xE0, // 172 0x80, 0x00, 0x80, // 173 0xE0, 0x00, 0x10, 0x01, 0xE8, 0x02, 0x68, 0x02, 0xC8, 0x02, 0x10, 0x01, 0xE0, // 174 0x00, 0x00, 0x08, 0x02, 0x0A, 0x02, 0xF8, 0x03, 0x0A, 0x02, 0x08, 0x02, // 175 0x00, 0x00, 0x38, 0x00, 0x28, 0x00, 0x38, // 176 0x40, 0x02, 0x40, 0x02, 0xF0, 0x03, 0x40, 0x02, 0x40, 0x02, // 177 0x00, 0x00, 0x08, 0x02, 0x08, 0x02, 0xF8, 0x03, 0x08, 0x02, 0x08, 0x02, // 178 0x00, 0x00, 0x20, 0x02, 0x20, 0x02, 0xE8, 0x03, 0x20, 0x02, 0x20, 0x02, // 179 0x00, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x30, // 180 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x02, 0x00, 0x02, 0xE0, 0x03, // 181 0x70, 0x00, 0xF8, 0x0F, 0x08, 0x00, 0xF8, 0x0F, 0x08, // 182 0x00, 0x00, 0x40, // 183 0x00, 0x00, 0xC0, 0x01, 0xA8, 0x02, 0xA0, 0x02, 0xA8, 0x02, 0xC0, // 184 0x00, 0x00, 0xF0, 0x03, 0x40, 0x00, 0x80, 0x00, 0xF8, 0x03, 0x08, // 185 0x00, 0x00, 0xE0, 0x01, 0x50, 0x02, 0x50, 0x02, 0x10, 0x02, 0x20, 0x01, // 186 0x00, 0x00, 0x40, 0x02, 0x80, 0x01, 0x40, 0x02, 0x80, 0x01, // 187 0x00, 0x00, 0x10, 0x02, 0x78, 0x01, 0xC0, 0x00, 0x20, 0x01, 0x90, 0x01, 0xC8, 0x03, 0x00, 0x01, // 188 0x00, 0x00, 0x10, 0x02, 0x78, 0x01, 0x80, 0x00, 0x60, 0x00, 0x50, 0x02, 0x48, 0x03, 0xC0, 0x02, // 189 0x48, 0x00, 0x58, 0x00, 0x68, 0x03, 0x80, 0x00, 0x60, 0x01, 0x90, 0x01, 0xC8, 0x03, 0x00, 0x01, // 190 0x28, 0x02, 0xE0, 0x03, 0x28, 0x02, // 191 0xF0, 0x03, 0x88, 0x00, 0x88, 0x00, 0x88, 0x00, 0xF0, 0x03, // 192 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0x88, 0x01, // 193 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0xB0, 0x01, // 194 0xF8, 0x03, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x18, // 195 0x00, 0x02, 0xFC, 0x03, 0x04, 0x02, 0xFC, 0x03, 0x00, 0x02, // 196 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0x08, 0x02, // 197 0xB8, 0x03, 0x40, 0x00, 0xF8, 0x03, 0x40, 0x00, 0xB8, 0x03, // 198 0x08, 0x02, 0x48, 0x02, 0x48, 0x02, 0xB0, 0x01, // 199 0xF8, 0x03, 0x80, 0x00, 0x40, 0x00, 0x20, 0x00, 0xF8, 0x03, // 200 0xE0, 0x03, 0x08, 0x01, 0x90, 0x00, 0x48, 0x00, 0xE0, 0x03, // 201 0xF8, 0x03, 0x40, 0x00, 0xA0, 0x00, 0x10, 0x01, 0x08, 0x02, // 202 0x00, 0x02, 0xF0, 0x01, 0x08, 0x00, 0x08, 0x00, 0xF8, 0x03, // 203 0xF8, 0x03, 0x10, 0x00, 0x60, 0x00, 0x10, 0x00, 0xF8, 0x03, // 204 0xF8, 0x03, 0x40, 0x00, 0x40, 0x00, 0x40, 0x00, 0xF8, 0x03, // 205 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0xF0, 0x01, // 206 0xF8, 0x03, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0xF8, 0x03, // 207 0xF8, 0x03, 0x48, 0x00, 0x48, 0x00, 0x48, 0x00, 0x30, // 208 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0x10, 0x01, // 209 0x08, 0x00, 0x08, 0x00, 0xF8, 0x03, 0x08, 0x00, 0x08, // 210 0x38, 0x00, 0x40, 0x02, 0x40, 0x02, 0x40, 0x02, 0xF8, 0x01, // 211 0x70, 0x00, 0x88, 0x00, 0xF8, 0x03, 0x88, 0x00, 0x70, // 212 0x18, 0x03, 0xA0, 0x00, 0x40, 0x00, 0xA0, 0x00, 0x18, 0x03, // 213 0xF8, 0x03, 0x00, 0x02, 0x00, 0x02, 0xF8, 0x03, 0x00, 0x02, // 214 0x38, 0x00, 0x40, 0x00, 0x40, 0x00, 0xF8, 0x03, // 215 0xF8, 0x03, 0x00, 0x02, 0xF8, 0x03, 0x00, 0x02, 0xF8, 0x03, // 216 0xF8, 0x03, 0x00, 0x02, 0xF8, 0x03, 0x00, 0x02, 0xF8, 0x03, 0x00, 0x06, // 217 0x08, 0x00, 0xF8, 0x03, 0x40, 0x02, 0x40, 0x02, 0x80, 0x01, // 218 0xF8, 0x03, 0x40, 0x02, 0x40, 0x02, 0x80, 0x01, 0xF8, 0x03, // 219 0xF8, 0x03, 0x40, 0x02, 0x40, 0x02, 0x80, 0x01, // 220 0x10, 0x01, 0x08, 0x02, 0x48, 0x02, 0x48, 0x02, 0xF0, 0x01, // 221 0xF8, 0x03, 0x40, 0x00, 0xF0, 0x01, 0x08, 0x02, 0xF0, 0x01, // 222 0x30, 0x02, 0x48, 0x01, 0xC8, 0x00, 0x48, 0x00, 0xF8, 0x03, // 223 0x00, 0x01, 0xA0, 0x02, 0xA0, 0x02, 0xE0, 0x01, // 224 0xE0, 0x01, 0x50, 0x02, 0x48, 0x02, 0x88, 0x01, // 225 0xE0, 0x03, 0xA0, 0x02, 0xA0, 0x02, 0x40, 0x01, // 226 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0x60, // 227 0x00, 0x02, 0xC0, 0x03, 0x20, 0x02, 0xE0, 0x03, 0x00, 0x02, // 228 0xC0, 0x01, 0xA0, 0x02, 0xA0, 0x02, 0xA0, 0x02, 0xC0, // 229 0x60, 0x03, 0x80, 0x00, 0xE0, 0x03, 0x80, 0x00, 0x60, 0x03, // 230 0x20, 0x02, 0xA0, 0x02, 0xA0, 0x02, 0x40, 0x01, // 231 0xE0, 0x03, 0x00, 0x01, 0x80, 0x00, 0x40, 0x00, 0xE0, 0x03, // 232 0xE0, 0x03, 0x08, 0x01, 0x88, 0x00, 0x48, 0x00, 0xE0, 0x03, // 233 0xE0, 0x03, 0x80, 0x00, 0x60, 0x03, // 234 0x00, 0x02, 0xC0, 0x01, 0x20, 0x00, 0xE0, 0x03, // 235 0xE0, 0x03, 0x40, 0x00, 0x80, 0x00, 0x40, 0x00, 0xE0, 0x03, // 236 0xE0, 0x03, 0x80, 0x00, 0x80, 0x00, 0xE0, 0x03, // 237 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // 238 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0xE0, 0x03, // 239 0xE0, 0x03, 0xA0, 0x00, 0x40, // 240 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0x40, 0x01, // 241 0x20, 0x00, 0xE0, 0x03, 0x20, // 242 0x60, 0x00, 0x80, 0x02, 0x80, 0x02, 0xE0, 0x01, // 243 0xC0, 0x00, 0x20, 0x01, 0xE0, 0x03, 0x20, 0x01, 0xC0, // 244 0x20, 0x02, 0x40, 0x01, 0x80, 0x00, 0x40, 0x01, 0x20, 0x02, // 245 0xE0, 0x03, 0x00, 0x02, 0xE0, 0x03, 0x00, 0x06, // 246 0x60, 0x00, 0x80, 0x00, 0x80, 0x00, 0xE0, 0x03, // 247 0xE0, 0x03, 0x00, 0x02, 0xE0, 0x03, 0x00, 0x02, 0xE0, 0x03, // 248 0xE0, 0x03, 0x00, 0x02, 0xE0, 0x03, 0x00, 0x02, 0xE0, 0x03, 0x00, 0x06, // 249 0x20, 0x00, 0xE0, 0x03, 0x80, 0x02, 0x00, 0x01, // 250 0xE0, 0x03, 0x80, 0x02, 0x00, 0x01, 0xE0, 0x03, // 251 0xE0, 0x03, 0x80, 0x02, 0x80, 0x02, 0x00, 0x01, // 252 0x40, 0x01, 0x20, 0x02, 0xA0, 0x02, 0xC0, 0x01, // 253 0xE0, 0x03, 0x80, 0x00, 0xC0, 0x01, 0x20, 0x02, 0xC0, 0x01, // 254 0x40, 0x02, 0xA0, 0x01, 0xA0, 0x00, 0xE0, 0x03, // 255 }; const uint8_t ArialMT_Plain_16_UA[] PROGMEM = { 0x10, // Width: 16 0x13, // Height: 19 0x20, // First Char: 32 0xE0, // Numbers of Chars: 224 // Jump Table: 0xFF, 0xFF, 0x00, 0x04, // 32= :65535 0x00, 0x00, 0x08, 0x05, // 33=!:0 0x00, 0x08, 0x0D, 0x06, // 34=":8 0x00, 0x15, 0x1A, 0x09, // 35=#:21 0x00, 0x2F, 0x17, 0x09, // 36=$:47 0x00, 0x46, 0x26, 0x0E, // 37=%:70 0x00, 0x6C, 0x1D, 0x0B, // 38=&:108 0x00, 0x89, 0x04, 0x03, // 39=':137 0x00, 0x8D, 0x0C, 0x05, // 40=(:141 0x00, 0x99, 0x0B, 0x05, // 41=):153 0x00, 0xA4, 0x0D, 0x06, // 42=*:164 0x00, 0xB1, 0x17, 0x09, // 43=+:177 0x00, 0xC8, 0x09, 0x04, // 44=,:200 0x00, 0xD1, 0x0B, 0x05, // 45=-:209 0x00, 0xDC, 0x08, 0x04, // 46=.:220 0x00, 0xE4, 0x0A, 0x04, // 47=/:228 0x00, 0xEE, 0x17, 0x09, // 48=0:238 0x01, 0x05, 0x11, 0x09, // 49=1:261 0x01, 0x16, 0x17, 0x09, // 50=2:278 0x01, 0x2D, 0x17, 0x09, // 51=3:301 0x01, 0x44, 0x17, 0x09, // 52=4:324 0x01, 0x5B, 0x17, 0x09, // 53=5:347 0x01, 0x72, 0x17, 0x09, // 54=6:370 0x01, 0x89, 0x16, 0x09, // 55=7:393 0x01, 0x9F, 0x17, 0x09, // 56=8:415 0x01, 0xB6, 0x17, 0x09, // 57=9:438 0x01, 0xCD, 0x05, 0x04, // 58=::461 0x01, 0xD2, 0x06, 0x04, // 59=;:466 0x01, 0xD8, 0x17, 0x09, // 60=<:472 0x01, 0xEF, 0x17, 0x09, // 61==:495 0x02, 0x06, 0x17, 0x09, // 62=>:518 0x02, 0x1D, 0x16, 0x09, // 63=?:541 0x02, 0x33, 0x2F, 0x10, // 64=@:563 0x02, 0x62, 0x1D, 0x0B, // 65=A:610 0x02, 0x7F, 0x1D, 0x0B, // 66=B:639 0x02, 0x9C, 0x20, 0x0C, // 67=C:668 0x02, 0xBC, 0x20, 0x0C, // 68=D:700 0x02, 0xDC, 0x1D, 0x0B, // 69=E:732 0x02, 0xF9, 0x19, 0x0A, // 70=F:761 0x03, 0x12, 0x20, 0x0C, // 71=G:786 0x03, 0x32, 0x1D, 0x0B, // 72=H:818 0x03, 0x4F, 0x05, 0x03, // 73=I:847 0x03, 0x54, 0x14, 0x08, // 74=J:852 0x03, 0x68, 0x1D, 0x0B, // 75=K:872 0x03, 0x85, 0x17, 0x09, // 76=L:901 0x03, 0x9C, 0x23, 0x0D, // 77=M:924 0x03, 0xBF, 0x1D, 0x0B, // 78=N:959 0x03, 0xDC, 0x20, 0x0C, // 79=O:988 0x03, 0xFC, 0x1C, 0x0B, // 80=P:1020 0x04, 0x18, 0x20, 0x0C, // 81=Q:1048 0x04, 0x38, 0x1D, 0x0B, // 82=R:1080 0x04, 0x55, 0x1D, 0x0B, // 83=S:1109 0x04, 0x72, 0x19, 0x09, // 84=T:1138 0x04, 0x8B, 0x1D, 0x0B, // 85=U:1163 0x04, 0xA8, 0x1C, 0x0B, // 86=V:1192 0x04, 0xC4, 0x2B, 0x0F, // 87=W:1220 0x04, 0xEF, 0x20, 0x0B, // 88=X:1263 0x05, 0x0F, 0x19, 0x09, // 89=Y:1295 0x05, 0x28, 0x1A, 0x09, // 90=Z:1320 0x05, 0x42, 0x0C, 0x04, // 91=[:1346 0x05, 0x4E, 0x0B, 0x04, // 92=\:1358 0x05, 0x59, 0x09, 0x04, // 93=]:1369 0x05, 0x62, 0x14, 0x07, // 94=^:1378 0x05, 0x76, 0x1B, 0x09, // 95=_:1398 0x05, 0x91, 0x07, 0x05, // 96=`:1425 0x05, 0x98, 0x17, 0x09, // 97=a:1432 0x05, 0xAF, 0x17, 0x09, // 98=b:1455 0x05, 0xC6, 0x14, 0x08, // 99=c:1478 0x05, 0xDA, 0x17, 0x09, // 100=d:1498 0x05, 0xF1, 0x17, 0x09, // 101=e:1521 0x06, 0x08, 0x0A, 0x04, // 102=f:1544 0x06, 0x12, 0x17, 0x09, // 103=g:1554 0x06, 0x29, 0x14, 0x08, // 104=h:1577 0x06, 0x3D, 0x05, 0x04, // 105=i:1597 0x06, 0x42, 0x06, 0x03, // 106=j:1602 0x06, 0x48, 0x17, 0x08, // 107=k:1608 0x06, 0x5F, 0x05, 0x03, // 108=l:1631 0x06, 0x64, 0x23, 0x0D, // 109=m:1636 0x06, 0x87, 0x14, 0x08, // 110=n:1671 0x06, 0x9B, 0x17, 0x09, // 111=o:1691 0x06, 0xB2, 0x17, 0x09, // 112=p:1714 0x06, 0xC9, 0x18, 0x09, // 113=q:1737 0x06, 0xE1, 0x0D, 0x05, // 114=r:1761 0x06, 0xEE, 0x14, 0x08, // 115=s:1774 0x07, 0x02, 0x0B, 0x04, // 116=t:1794 0x07, 0x0D, 0x14, 0x08, // 117=u:1805 0x07, 0x21, 0x13, 0x07, // 118=v:1825 0x07, 0x34, 0x1F, 0x0B, // 119=w:1844 0x07, 0x53, 0x14, 0x07, // 120=x:1875 0x07, 0x67, 0x13, 0x07, // 121=y:1895 0x07, 0x7A, 0x14, 0x07, // 122=z:1914 0x07, 0x8E, 0x0F, 0x05, // 123={:1934 0x07, 0x9D, 0x06, 0x03, // 124=|:1949 0x07, 0xA3, 0x0E, 0x05, // 125=}:1955 0x07, 0xB1, 0x17, 0x09, // 126=~:1969 0x07, 0xC8, 0x1D, 0x0C, // 127=:1992 0x07, 0xE5, 0x26, 0x0E, // 1026=Ђ:2021 0x08, 0x0B, 0x19, 0x09, // 1027=Ѓ:2059 0x08, 0x24, 0x06, 0x04, // 8218=‚:2084 0x08, 0x2A, 0x10, 0x06, // 1107=ѓ:2090 0x08, 0x3A, 0x09, 0x05, // 8222=„:2106 0x08, 0x43, 0x26, 0x10, // 8230=…:2115 0x08, 0x69, 0x16, 0x09, // 8224=†:2153 0x08, 0x7F, 0x17, 0x09, // 8225=‡:2175 0x08, 0x96, 0x17, 0x09, // 8364=€:2198 0x08, 0xAD, 0x32, 0x11, // 8240=‰:2221 0x08, 0xDF, 0x2F, 0x11, // 1033=Љ:2271 0x09, 0x0E, 0x0B, 0x05, // 8249=‹:2318 0x09, 0x19, 0x2C, 0x10, // 1034=Њ:2329 0x09, 0x45, 0x1A, 0x09, // 1036=Ќ:2373 0x09, 0x5F, 0x23, 0x0D, // 1035=Ћ:2399 0x09, 0x82, 0x1D, 0x0C, // 1039=Џ:2434 0x09, 0x9F, 0x15, 0x08, // 1106=ђ:2463 0x09, 0xB4, 0x04, 0x04, // 8216=‘:2484 0x09, 0xB8, 0x04, 0x04, // 8217=’:2488 0x09, 0xBC, 0x07, 0x05, // 8220=“:2492 0x09, 0xC3, 0x0A, 0x05, // 8221=”:2499 0x09, 0xCD, 0x0E, 0x06, // 8226=•:2509 0x09, 0xDB, 0x1A, 0x09, // 8211=–:2523 0x09, 0xF5, 0x2F, 0x10, // 8212=—:2549 0x0A, 0x24, 0x1D, 0x0C, // 65533=�:2596 0x0A, 0x41, 0x2C, 0x10, // 8482=™:2625 0x0A, 0x6D, 0x29, 0x0F, // 1113=љ:2669 0x0A, 0x96, 0x0B, 0x05, // 8250=›:2710 0x0A, 0xA1, 0x23, 0x0D, // 1114=њ:2721 0x0A, 0xC4, 0x14, 0x07, // 1116=ќ:2756 0x0A, 0xD8, 0x14, 0x08, // 1115=ћ:2776 0x0A, 0xEC, 0x14, 0x08, // 1119=џ:2796 0xFF, 0xFF, 0x00, 0x04, // 160= :65535 0x0B, 0x00, 0x1C, 0x0A, // 1038=Ў:2816 0x0B, 0x1C, 0x13, 0x07, // 1118=ў:2844 0x0B, 0x2F, 0x14, 0x08, // 1032=Ј:2863 0x0B, 0x43, 0x14, 0x09, // 164=¤:2883 0x0B, 0x57, 0x16, 0x08, // 1168=Ґ:2903 0x0B, 0x6D, 0x06, 0x03, // 166=¦:2925 0x0B, 0x73, 0x17, 0x09, // 167=§:2931 0x0B, 0x8A, 0x1D, 0x0B, // 1025=Ё:2954 0x0B, 0xA7, 0x23, 0x0C, // 169=©:2983 0x0B, 0xCA, 0x20, 0x0C, // 1028=Є:3018 0x0B, 0xEA, 0x14, 0x09, // 171=«:3050 0x0B, 0xFE, 0x17, 0x09, // 172=¬:3070 0x0C, 0x15, 0x0B, 0x05, // 173=­:3093 0x0C, 0x20, 0x23, 0x0C, // 174=®:3104 0x0C, 0x43, 0x07, 0x03, // 1031=Ї:3139 0x0C, 0x4A, 0x0D, 0x06, // 176=°:3146 0x0C, 0x57, 0x17, 0x09, // 177=±:3159 0x0C, 0x6E, 0x05, 0x03, // 1030=І:3182 0x0C, 0x73, 0x05, 0x04, // 1110=і:3187 0x0C, 0x78, 0x10, 0x07, // 1169=ґ:3192 0x0C, 0x88, 0x17, 0x09, // 181=µ:3208 0x0C, 0x9F, 0x19, 0x09, // 182=¶:3231 0x0C, 0xB8, 0x08, 0x05, // 183=·:3256 0x0C, 0xC0, 0x17, 0x09, // 1105=ё:3264 0x0C, 0xD7, 0x2F, 0x11, // 8470=№:3287 0x0D, 0x06, 0x14, 0x08, // 1108=є:3334 0x0D, 0x1A, 0x17, 0x09, // 187=»:3354 0x0D, 0x31, 0x06, 0x03, // 1112=ј:3377 0x0D, 0x37, 0x1D, 0x0B, // 1029=Ѕ:3383 0x0D, 0x54, 0x14, 0x08, // 1109=ѕ:3412 0x0D, 0x68, 0x07, 0x03, // 1111=ї:3432 0x0D, 0x6F, 0x1D, 0x0B, // 1040=А:3439 0x0D, 0x8C, 0x1D, 0x0B, // 1041=Б:3468 0x0D, 0xA9, 0x1D, 0x0B, // 1042=В:3497 0x0D, 0xC6, 0x19, 0x09, // 1043=Г:3526 0x0D, 0xDF, 0x1E, 0x0B, // 1044=Д:3551 0x0D, 0xFD, 0x1D, 0x0B, // 1045=Е:3581 0x0E, 0x1A, 0x29, 0x0E, // 1046=Ж:3610 0x0E, 0x43, 0x1A, 0x0A, // 1047=З:3651 0x0E, 0x5D, 0x20, 0x0C, // 1048=И:3677 0x0E, 0x7D, 0x20, 0x0C, // 1049=Й:3709 0x0E, 0x9D, 0x1A, 0x09, // 1050=К:3741 0x0E, 0xB7, 0x1D, 0x0B, // 1051=Л:3767 0x0E, 0xD4, 0x23, 0x0D, // 1052=М:3796 0x0E, 0xF7, 0x1D, 0x0B, // 1053=Н:3831 0x0F, 0x14, 0x20, 0x0C, // 1054=О:3860 0x0F, 0x34, 0x1D, 0x0B, // 1055=П:3892 0x0F, 0x51, 0x1C, 0x0B, // 1056=Р:3921 0x0F, 0x6D, 0x20, 0x0C, // 1057=С:3949 0x0F, 0x8D, 0x19, 0x09, // 1058=Т:3981 0x0F, 0xA6, 0x1C, 0x0A, // 1059=У:4006 0x0F, 0xC2, 0x1D, 0x0B, // 1060=Ф:4034 0x0F, 0xDF, 0x20, 0x0B, // 1061=Х:4063 0x0F, 0xFF, 0x21, 0x0C, // 1062=Ц:4095 0x10, 0x20, 0x1A, 0x0A, // 1063=Ч:4128 0x10, 0x3A, 0x23, 0x0D, // 1064=Ш:4154 0x10, 0x5D, 0x27, 0x0E, // 1065=Щ:4189 0x10, 0x84, 0x23, 0x0D, // 1066=Ъ:4228 0x10, 0xA7, 0x26, 0x0E, // 1067=Ы:4263 0x10, 0xCD, 0x1D, 0x0B, // 1068=Ь:4301 0x10, 0xEA, 0x20, 0x0C, // 1069=Э:4330 0x11, 0x0A, 0x2C, 0x10, // 1070=Ю:4362 0x11, 0x36, 0x20, 0x0C, // 1071=Я:4406 0x11, 0x56, 0x17, 0x09, // 1072=а:4438 0x11, 0x6D, 0x17, 0x09, // 1073=б:4461 0x11, 0x84, 0x17, 0x09, // 1074=в:4484 0x11, 0x9B, 0x10, 0x06, // 1075=г:4507 0x11, 0xAB, 0x18, 0x09, // 1076=д:4523 0x11, 0xC3, 0x17, 0x09, // 1077=е:4547 0x11, 0xDA, 0x1D, 0x0A, // 1078=ж:4570 0x11, 0xF7, 0x11, 0x07, // 1079=з:4599 0x12, 0x08, 0x14, 0x08, // 1080=и:4616 0x12, 0x1C, 0x14, 0x08, // 1081=й:4636 0x12, 0x30, 0x14, 0x07, // 1082=к:4656 0x12, 0x44, 0x14, 0x08, // 1083=л:4676 0x12, 0x58, 0x1D, 0x0B, // 1084=м:4696 0x12, 0x75, 0x14, 0x08, // 1085=н:4725 0x12, 0x89, 0x17, 0x09, // 1086=о:4745 0x12, 0xA0, 0x14, 0x08, // 1087=п:4768 0x12, 0xB4, 0x17, 0x09, // 1088=р:4788 0x12, 0xCB, 0x14, 0x08, // 1089=с:4811 0x12, 0xDF, 0x13, 0x07, // 1090=т:4831 0x12, 0xF2, 0x13, 0x07, // 1091=у:4850 0x13, 0x05, 0x23, 0x0D, // 1092=ф:4869 0x13, 0x28, 0x14, 0x07, // 1093=х:4904 0x13, 0x3C, 0x1B, 0x09, // 1094=ц:4924 0x13, 0x57, 0x14, 0x08, // 1095=ч:4951 0x13, 0x6B, 0x1D, 0x0B, // 1096=ш:4971 0x13, 0x88, 0x21, 0x0B, // 1097=щ:5000 0x13, 0xA9, 0x1A, 0x0A, // 1098=ъ:5033 0x13, 0xC3, 0x20, 0x0C, // 1099=ы:5059 0x13, 0xE3, 0x17, 0x09, // 1100=ь:5091 0x13, 0xFA, 0x14, 0x08, // 1101=э:5114 0x14, 0x0E, 0x20, 0x0C, // 1102=ю:5134 0x14, 0x2E, 0x17, 0x09, // 1103=я:5166 // Font Data: 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x5F, // 33 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, // 34 0x80, 0x08, 0x00, 0x80, 0x78, 0x00, 0xC0, 0x0F, 0x00, 0xB8, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x78, 0x00, 0xC0, 0x0F, 0x00, 0xB8, 0x08, 0x00, 0x80, 0x08, // 35 0x00, 0x00, 0x00, 0xE0, 0x10, 0x00, 0x10, 0x21, 0x00, 0x08, 0x43, 0x00, 0xFC, 0xFF, 0x00, 0x08, 0x42, 0x00, 0x18, 0x22, 0x00, 0x20, 0x1C, // 36 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0x08, 0x61, 0x00, 0xF0, 0x18, 0x00, 0x00, 0x06, 0x00, 0xC0, 0x01, 0x00, 0x30, 0x3C, 0x00, 0x08, 0x42, 0x00, 0x00, 0x42, 0x00, 0x00, 0x42, 0x00, 0x00, 0x3C, // 37 0x00, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x70, 0x22, 0x00, 0x88, 0x41, 0x00, 0x08, 0x43, 0x00, 0x88, 0x44, 0x00, 0x70, 0x28, 0x00, 0x00, 0x10, 0x00, 0x00, 0x28, 0x00, 0x00, 0x44, // 38 0x00, 0x00, 0x00, 0x78, // 39 0x00, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x70, 0xC0, 0x01, 0x08, 0x00, 0x02, // 40 0x00, 0x00, 0x00, 0x08, 0x00, 0x02, 0x70, 0xC0, 0x01, 0x80, 0x3F, // 41 0x10, 0x00, 0x00, 0xD0, 0x00, 0x00, 0x38, 0x00, 0x00, 0xD0, 0x00, 0x00, 0x10, // 42 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, // 43 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, // 44 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, // 45 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, // 46 0x00, 0x60, 0x00, 0x00, 0x1E, 0x00, 0xE0, 0x01, 0x00, 0x18, // 47 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0xE0, 0x1F, // 48 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0x00, 0x10, 0x00, 0x00, 0xF8, 0x7F, // 49 0x00, 0x00, 0x00, 0x20, 0x40, 0x00, 0x10, 0x60, 0x00, 0x08, 0x50, 0x00, 0x08, 0x48, 0x00, 0x08, 0x44, 0x00, 0x10, 0x43, 0x00, 0xE0, 0x40, // 50 0x00, 0x00, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x88, 0x41, 0x00, 0xF0, 0x22, 0x00, 0x00, 0x1C, // 51 0x00, 0x0C, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x09, 0x00, 0xC0, 0x08, 0x00, 0x20, 0x08, 0x00, 0x10, 0x08, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x08, // 52 0x00, 0x00, 0x00, 0xC0, 0x11, 0x00, 0xB8, 0x20, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x08, 0x21, 0x00, 0x08, 0x1E, // 53 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x10, 0x21, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x88, 0x40, 0x00, 0x10, 0x21, 0x00, 0x20, 0x1E, // 54 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x78, 0x00, 0x88, 0x07, 0x00, 0x68, 0x00, 0x00, 0x18, // 55 0x00, 0x00, 0x00, 0x60, 0x1C, 0x00, 0x90, 0x22, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x90, 0x22, 0x00, 0x60, 0x1C, // 56 0x00, 0x00, 0x00, 0xE0, 0x11, 0x00, 0x10, 0x22, 0x00, 0x08, 0x44, 0x00, 0x08, 0x44, 0x00, 0x08, 0x44, 0x00, 0x10, 0x22, 0x00, 0xE0, 0x1F, // 57 0x00, 0x00, 0x00, 0x40, 0x40, // 58 0x00, 0x00, 0x00, 0x40, 0xC0, 0x01, // 59 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x05, 0x00, 0x00, 0x05, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x40, 0x10, // 60 0x00, 0x00, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, // 61 0x00, 0x00, 0x00, 0x40, 0x10, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x80, 0x08, 0x00, 0x00, 0x05, 0x00, 0x00, 0x05, 0x00, 0x00, 0x02, // 62 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x5C, 0x00, 0x08, 0x02, 0x00, 0x10, 0x01, 0x00, 0xE0, // 63 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0xC0, 0x40, 0x00, 0x20, 0x80, 0x00, 0x10, 0x1E, 0x01, 0x10, 0x21, 0x01, 0x88, 0x40, 0x02, 0x48, 0x40, 0x02, 0x48, 0x40, 0x02, 0x48, 0x20, 0x02, 0x88, 0x7C, 0x02, 0xC8, 0x43, 0x02, 0x10, 0x40, 0x02, 0x10, 0x20, 0x01, 0x60, 0x10, 0x01, 0x80, 0x8F, // 64 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x70, 0x04, 0x00, 0x08, 0x04, 0x00, 0x70, 0x04, 0x00, 0x80, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, // 65 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x90, 0x22, 0x00, 0x60, 0x1C, // 66 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, // 67 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 68 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x40, // 69 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, // 70 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, 0x10, 0x22, 0x00, 0x20, 0x12, 0x00, 0x00, 0x0E, // 71 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0xF8, 0x7F, // 72 0x00, 0x00, 0x00, 0xF8, 0x7F, // 73 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x3F, // 74 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x02, 0x00, 0x00, 0x01, 0x00, 0x80, 0x03, 0x00, 0x40, 0x04, 0x00, 0x20, 0x18, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, // 75 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, // 76 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x30, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x03, 0x00, 0xC0, 0x00, 0x00, 0x30, 0x00, 0x00, 0xF8, 0x7F, // 77 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x10, 0x00, 0x00, 0x60, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x04, 0x00, 0x00, 0x18, 0x00, 0x00, 0x20, 0x00, 0xF8, 0x7F, // 78 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 79 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x10, 0x01, 0x00, 0xE0, // 80 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x50, 0x00, 0x08, 0x50, 0x00, 0x10, 0x20, 0x00, 0x20, 0x70, 0x00, 0xC0, 0x4F, // 81 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x0E, 0x00, 0x08, 0x1A, 0x00, 0x10, 0x21, 0x00, 0xE0, 0x40, // 82 0x00, 0x00, 0x00, 0x60, 0x10, 0x00, 0x90, 0x20, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, 0x10, 0x22, 0x00, 0x20, 0x1C, // 83 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, // 84 0x00, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0xF8, 0x1F, // 85 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x18, 0x00, 0x00, 0x60, 0x00, 0x00, 0x18, 0x00, 0x00, 0x07, 0x00, 0xE0, 0x00, 0x00, 0x18, // 86 0x18, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x03, 0x00, 0x70, 0x00, 0x00, 0x08, 0x00, 0x00, 0x70, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1E, 0x00, 0xE0, 0x01, 0x00, 0x18, // 87 0x00, 0x40, 0x00, 0x08, 0x20, 0x00, 0x10, 0x18, 0x00, 0x60, 0x04, 0x00, 0x80, 0x02, 0x00, 0x00, 0x01, 0x00, 0x80, 0x02, 0x00, 0x60, 0x0C, 0x00, 0x10, 0x10, 0x00, 0x08, 0x20, 0x00, 0x00, 0x40, // 88 0x08, 0x00, 0x00, 0x30, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x7E, 0x00, 0x80, 0x01, 0x00, 0x40, 0x00, 0x00, 0x30, 0x00, 0x00, 0x08, // 89 0x00, 0x40, 0x00, 0x08, 0x60, 0x00, 0x08, 0x58, 0x00, 0x08, 0x44, 0x00, 0x08, 0x43, 0x00, 0x88, 0x40, 0x00, 0x68, 0x40, 0x00, 0x18, 0x40, 0x00, 0x08, 0x40, // 90 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x03, 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, // 91 0x18, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x60, // 92 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, 0xF8, 0xFF, 0x03, // 93 0x00, 0x01, 0x00, 0xC0, 0x00, 0x00, 0x30, 0x00, 0x00, 0x08, 0x00, 0x00, 0x30, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x01, // 94 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, // 95 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x10, // 96 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x42, 0x00, 0x40, 0x22, 0x00, 0x80, 0x7F, // 97 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 98 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, // 99 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0xF8, 0x7F, // 100 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x24, 0x00, 0x00, 0x17, // 101 0x40, 0x00, 0x00, 0xF0, 0x7F, 0x00, 0x48, 0x00, 0x00, 0x48, // 102 0x00, 0x00, 0x00, 0x00, 0x1F, 0x01, 0x80, 0x20, 0x02, 0x40, 0x40, 0x02, 0x40, 0x40, 0x02, 0x40, 0x40, 0x02, 0x80, 0x20, 0x01, 0xC0, 0xFF, // 103 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x7F, // 104 0x00, 0x00, 0x00, 0xC8, 0x7F, // 105 0x00, 0x00, 0x02, 0xC8, 0xFF, 0x01, // 106 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x06, 0x00, 0x00, 0x19, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, // 107 0x00, 0x00, 0x00, 0xF8, 0x7F, // 108 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x7F, // 109 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x7F, // 110 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 111 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 112 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0xC0, 0xFF, 0x03, // 113 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, // 114 0x00, 0x00, 0x00, 0x80, 0x23, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x38, // 115 0x40, 0x00, 0x00, 0xF0, 0x7F, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, // 116 0x00, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0xC0, 0x7F, // 117 0xC0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x03, 0x00, 0xC0, // 118 0xC0, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x03, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1F, 0x00, 0xC0, // 119 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, // 120 0xC0, 0x01, 0x00, 0x00, 0x06, 0x02, 0x00, 0x38, 0x02, 0x00, 0xC0, 0x01, 0x00, 0x38, 0x00, 0x00, 0x07, 0x00, 0xC0, // 121 0x40, 0x40, 0x00, 0x40, 0x60, 0x00, 0x40, 0x58, 0x00, 0x40, 0x44, 0x00, 0x40, 0x43, 0x00, 0xC0, 0x40, 0x00, 0x40, 0x40, // 122 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0xF0, 0xFB, 0x01, 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, // 123 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x03, // 124 0x08, 0x00, 0x02, 0x08, 0x00, 0x02, 0xF0, 0xFB, 0x01, 0x00, 0x04, 0x00, 0x00, 0x04, // 125 0x00, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x01, // 126 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x7F, 0x00, 0x20, 0x40, 0x00, 0x20, 0x40, 0x00, 0x20, 0x40, 0x00, 0x20, 0x40, 0x00, 0x20, 0x40, 0x00, 0x20, 0x40, 0x00, 0xE0, 0x7F, // 127 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x01, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x3E, // 1026 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x09, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, // 1027 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, // 8218 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x40, 0x00, 0x00, 0x50, 0x00, 0x00, 0x48, 0x00, 0x00, 0x40, // 1107 0x00, 0xC0, 0x01, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x01, // 8222 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, // 8230 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0xF8, 0xFF, 0x03, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, // 8224 0x00, 0x00, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0xF8, 0xFF, 0x03, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, // 8225 0x00, 0x05, 0x00, 0xC0, 0x1F, 0x00, 0x20, 0x25, 0x00, 0x10, 0x45, 0x00, 0x08, 0x45, 0x00, 0x08, 0x45, 0x00, 0x08, 0x45, 0x00, 0x10, 0x20, // 8364 0xF0, 0x00, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0x08, 0x71, 0x00, 0xF0, 0x0C, 0x00, 0x00, 0x03, 0x00, 0xE0, 0x3C, 0x00, 0x18, 0x42, 0x00, 0x00, 0x42, 0x00, 0x00, 0x42, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x42, 0x00, 0x00, 0x42, 0x00, 0x00, 0x42, 0x00, 0x00, 0x3C, // 8240 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x3F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x3E, // 1033 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, 0x20, // 8249 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x3E, // 1034 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x82, 0x02, 0x00, 0x61, 0x04, 0x00, 0x10, 0x08, 0x00, 0x08, 0x30, 0x00, 0x08, 0x40, // 1036 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0x08, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x7E, // 1035 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x7F, // 1039 0x10, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x90, 0x00, 0x00, 0x50, 0x00, 0x00, 0x50, 0x00, 0x00, 0x40, 0x00, 0x02, 0x80, 0xFF, 0x03, // 1106 0x00, 0x00, 0x00, 0x38, // 8216 0x00, 0x00, 0x00, 0x38, // 8217 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, // 8220 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, // 8221 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x80, 0x07, 0x00, 0x80, 0x07, 0x00, 0x00, 0x03, // 8226 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, // 8211 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, // 8212 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x7F, 0x00, 0x20, 0x40, 0x00, 0x20, 0x40, 0x00, 0x20, 0x40, 0x00, 0x20, 0x40, 0x00, 0x20, 0x40, 0x00, 0x20, 0x40, 0x00, 0xE0, 0x7F, // 65533 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x18, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x01, 0x00, 0xE0, 0x00, 0x00, 0x18, 0x00, 0x00, 0xF8, 0x01, // 8482 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xC0, 0x3F, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x64, 0x00, 0x00, 0x38, // 1113 0x00, 0x00, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x04, // 8250 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x38, // 1114 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x04, 0x00, 0x10, 0x04, 0x00, 0x08, 0x1B, 0x00, 0xC0, 0x20, 0x00, 0x40, 0x40, // 1116 0x10, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x90, 0x00, 0x00, 0x50, 0x00, 0x00, 0x50, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0x7F, // 1115 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x40, 0x00, 0xC0, 0x7F, // 1119 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x60, 0x40, 0x00, 0x81, 0x41, 0x00, 0x02, 0x66, 0x00, 0x02, 0x18, 0x00, 0x02, 0x06, 0x00, 0x81, 0x01, 0x00, 0x60, 0x00, 0x00, 0x18, // 1038 0xC0, 0x01, 0x00, 0x08, 0x06, 0x02, 0x10, 0x38, 0x02, 0x10, 0xC0, 0x01, 0x10, 0x38, 0x00, 0x08, 0x07, 0x00, 0xC0, // 1118 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x3F, // 1032 0x00, 0x00, 0x00, 0x40, 0x0B, 0x00, 0x80, 0x04, 0x00, 0x40, 0x08, 0x00, 0x40, 0x08, 0x00, 0x80, 0x04, 0x00, 0x40, 0x0B, // 164 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x0F, // 1168 0x00, 0x00, 0x00, 0xF8, 0xF1, 0x03, // 166 0x00, 0x86, 0x00, 0x70, 0x09, 0x01, 0xC8, 0x10, 0x02, 0x88, 0x10, 0x02, 0x08, 0x21, 0x02, 0x08, 0x61, 0x02, 0x30, 0xD2, 0x01, 0x00, 0x0C, // 167 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x08, 0x41, 0x00, 0x0A, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x40, // 1025 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0xC8, 0x47, 0x00, 0x28, 0x48, 0x00, 0x28, 0x48, 0x00, 0x28, 0x48, 0x00, 0x28, 0x48, 0x00, 0x48, 0x44, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 169 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x11, 0x00, 0x10, 0x21, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, // 1028 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, 0x20, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, 0x20, // 171 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x0F, // 172 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, // 173 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0xE8, 0x4F, 0x00, 0x28, 0x41, 0x00, 0x28, 0x41, 0x00, 0x28, 0x43, 0x00, 0x28, 0x45, 0x00, 0xC8, 0x48, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 174 0x02, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x02, // 1031 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x48, 0x00, 0x00, 0x48, 0x00, 0x00, 0x30, // 176 0x00, 0x00, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0xE0, 0x4F, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, // 177 0x00, 0x00, 0x00, 0xF8, 0x7F, // 1030 0x00, 0x00, 0x00, 0xC8, 0x7F, // 1110 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x78, // 1169 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0xC0, 0x7F, // 181 0xF0, 0x00, 0x00, 0xF8, 0x00, 0x00, 0xF8, 0x01, 0x00, 0xF8, 0x01, 0x00, 0xF8, 0xFF, 0x03, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0xFF, 0x03, 0x08, // 182 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, // 183 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x50, 0x44, 0x00, 0x40, 0x44, 0x00, 0x50, 0x44, 0x00, 0x80, 0x24, 0x00, 0x00, 0x17, // 1105 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x20, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x06, 0x00, 0x00, 0x08, 0x00, 0x00, 0x10, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x80, 0x27, 0x00, 0x40, 0x28, 0x00, 0x40, 0x28, 0x00, 0x40, 0x28, 0x00, 0x40, 0x28, 0x00, 0x80, 0x27, // 8470 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x20, 0x00, 0x00, 0x11, // 1108 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x04, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x04, // 187 0x00, 0x00, 0x02, 0xC8, 0xFF, 0x01, // 1112 0x00, 0x00, 0x00, 0x60, 0x10, 0x00, 0x90, 0x20, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x42, 0x00, 0x08, 0x42, 0x00, 0x10, 0x22, 0x00, 0x20, 0x1C, // 1029 0x00, 0x00, 0x00, 0x80, 0x23, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x38, // 1109 0x10, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x10, // 1111 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x80, 0x07, 0x00, 0x70, 0x04, 0x00, 0x08, 0x04, 0x00, 0x70, 0x04, 0x00, 0x80, 0x07, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, // 1040 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x00, 0x3E, // 1041 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x90, 0x22, 0x00, 0x60, 0x1C, // 1042 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, // 1043 0x00, 0xC0, 0x03, 0x00, 0x60, 0x00, 0x00, 0x5C, 0x00, 0xF8, 0x43, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0xC0, 0x03, // 1044 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x40, // 1045 0x08, 0x30, 0x00, 0x10, 0x08, 0x00, 0x60, 0x04, 0x00, 0x80, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x80, 0x02, 0x00, 0x60, 0x04, 0x00, 0x10, 0x08, 0x00, 0x08, 0x30, 0x00, 0x08, 0x40, // 1046 0x00, 0x00, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x88, 0x42, 0x00, 0x70, 0x42, 0x00, 0x00, 0x3C, // 1047 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x30, 0x00, 0x00, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x02, 0x00, 0x00, 0x01, 0x00, 0x80, 0x00, 0x00, 0x40, 0x00, 0x00, 0x30, 0x00, 0x00, 0xF8, 0x7F, // 1048 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x30, 0x00, 0x00, 0x08, 0x00, 0x01, 0x04, 0x00, 0x02, 0x02, 0x00, 0x02, 0x01, 0x00, 0x82, 0x00, 0x00, 0x41, 0x00, 0x00, 0x30, 0x00, 0x00, 0xF8, 0x7F, // 1049 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x80, 0x02, 0x00, 0x60, 0x04, 0x00, 0x10, 0x08, 0x00, 0x08, 0x30, 0x00, 0x08, 0x40, // 1050 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x3F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x7F, // 1051 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x30, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x60, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x03, 0x00, 0xC0, 0x00, 0x00, 0x30, 0x00, 0x00, 0xF8, 0x7F, // 1052 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0xF8, 0x7F, // 1053 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 1054 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x7F, // 1055 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x10, 0x01, 0x00, 0xE0, // 1056 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, // 1057 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, // 1058 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x60, 0x40, 0x00, 0x80, 0x41, 0x00, 0x00, 0x66, 0x00, 0x00, 0x18, 0x00, 0x00, 0x06, 0x00, 0x80, 0x01, 0x00, 0x60, 0x00, 0x00, 0x18, // 1059 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x40, 0x08, 0x00, 0x20, 0x10, 0x00, 0x20, 0x10, 0x00, 0xF8, 0x7F, 0x00, 0x20, 0x10, 0x00, 0x20, 0x10, 0x00, 0x40, 0x08, 0x00, 0x80, 0x07, // 1060 0x00, 0x40, 0x00, 0x08, 0x20, 0x00, 0x10, 0x18, 0x00, 0x60, 0x04, 0x00, 0x80, 0x02, 0x00, 0x00, 0x01, 0x00, 0x80, 0x02, 0x00, 0x60, 0x0C, 0x00, 0x10, 0x10, 0x00, 0x08, 0x20, 0x00, 0x00, 0x40, // 1061 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0xC0, 0x03, // 1062 0x00, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x02, 0x00, 0xF8, 0x7F, // 1063 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x7F, // 1064 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0xC0, 0x03, // 1065 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x3E, // 1066 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x7F, // 1067 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x41, 0x00, 0x00, 0x3E, // 1068 0x00, 0x00, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x08, 0x41, 0x00, 0x10, 0x21, 0x00, 0x20, 0x11, 0x00, 0xC0, 0x0F, // 1069 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0xC0, 0x0F, 0x00, 0x20, 0x10, 0x00, 0x10, 0x20, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x10, 0x20, 0x00, 0x20, 0x10, 0x00, 0xC0, 0x0F, // 1070 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x40, 0x00, 0x10, 0x21, 0x00, 0x08, 0x1A, 0x00, 0x08, 0x0E, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0xF8, 0x7F, // 1071 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x80, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x42, 0x00, 0x40, 0x22, 0x00, 0x80, 0x7F, // 1072 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x90, 0x20, 0x00, 0x48, 0x40, 0x00, 0x48, 0x40, 0x00, 0x48, 0x40, 0x00, 0x88, 0x20, 0x00, 0x08, 0x1F, // 1073 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x3B, // 1074 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, // 1075 0x00, 0xC0, 0x01, 0x00, 0x70, 0x00, 0xC0, 0x4F, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0xC0, 0x01, // 1076 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x24, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x24, 0x00, 0x00, 0x17, // 1077 0xC0, 0x20, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0xC0, 0x20, 0x00, 0x40, 0x40, // 1078 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x3B, // 1079 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x30, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x02, 0x00, 0x80, 0x01, 0x00, 0xC0, 0x7F, // 1080 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x08, 0x30, 0x00, 0x10, 0x0C, 0x00, 0x10, 0x02, 0x00, 0x90, 0x01, 0x00, 0xC8, 0x7F, // 1081 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0xC0, 0x20, 0x00, 0x40, 0x40, // 1082 0x00, 0x40, 0x00, 0xC0, 0x3F, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0xC0, 0x7F, // 1083 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x06, 0x00, 0x00, 0x18, 0x00, 0x00, 0x60, 0x00, 0x00, 0x18, 0x00, 0x00, 0x06, 0x00, 0xC0, 0x01, 0x00, 0xC0, 0x7F, // 1084 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0xC0, 0x7F, // 1085 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 1086 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0xC0, 0x7F, // 1087 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 1088 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, // 1089 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, // 1090 0xC0, 0x01, 0x00, 0x00, 0x06, 0x02, 0x00, 0x38, 0x02, 0x00, 0xC0, 0x01, 0x00, 0x38, 0x00, 0x00, 0x07, 0x00, 0xC0, // 1091 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0xF8, 0xFF, 0x03, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 1092 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1B, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, // 1093 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0xC0, 0x03, // 1094 0x00, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0x04, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x08, 0x00, 0xC0, 0x7F, // 1095 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xC0, 0x7F, // 1096 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0xC0, 0x03, // 1097 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x38, // 1098 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x7F, // 1099 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x44, 0x00, 0x00, 0x38, // 1100 0x00, 0x00, 0x00, 0x00, 0x11, 0x00, 0x80, 0x20, 0x00, 0x40, 0x44, 0x00, 0x40, 0x44, 0x00, 0x80, 0x24, 0x00, 0x00, 0x1F, // 1101 0x00, 0x00, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x20, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x40, 0x40, 0x00, 0x80, 0x20, 0x00, 0x00, 0x1F, // 1102 0x00, 0x00, 0x00, 0x80, 0x63, 0x00, 0x40, 0x14, 0x00, 0x40, 0x0C, 0x00, 0x40, 0x04, 0x00, 0x40, 0x04, 0x00, 0x40, 0x04, 0x00, 0xC0, 0x7F, // 1103 }; const uint8_t ArialMT_Plain_24_UA[] PROGMEM = { 0x18, // Width: 24 0x1C, // Height: 28 0x20, // First Char: 32 0xE0, // Numbers of Chars: 224 // Jump Table: 0xFF, 0xFF, 0x00, 0x07, // 32= :65535 0x00, 0x00, 0x13, 0x08, // 33=!:0 0x00, 0x13, 0x1A, 0x09, // 34=":19 0x00, 0x2D, 0x33, 0x0D, // 35=#:45 0x00, 0x60, 0x2F, 0x0D, // 36=$:96 0x00, 0x8F, 0x4F, 0x15, // 37=%:143 0x00, 0xDE, 0x3B, 0x10, // 38=&:222 0x01, 0x19, 0x0A, 0x05, // 39=':281 0x01, 0x23, 0x1C, 0x08, // 40=(:291 0x01, 0x3F, 0x1B, 0x08, // 41=):319 0x01, 0x5A, 0x22, 0x09, // 42=*:346 0x01, 0x7C, 0x33, 0x0E, // 43=+:380 0x01, 0xAF, 0x10, 0x07, // 44=,:431 0x01, 0xBF, 0x1B, 0x08, // 45=-:447 0x01, 0xDA, 0x0F, 0x07, // 46=.:474 0x01, 0xE9, 0x1A, 0x07, // 47=/:489 0x02, 0x03, 0x2F, 0x0D, // 48=0:515 0x02, 0x32, 0x23, 0x0D, // 49=1:562 0x02, 0x55, 0x2F, 0x0D, // 50=2:597 0x02, 0x84, 0x2F, 0x0D, // 51=3:644 0x02, 0xB3, 0x2F, 0x0D, // 52=4:691 0x02, 0xE2, 0x2F, 0x0D, // 53=5:738 0x03, 0x11, 0x2F, 0x0D, // 54=6:785 0x03, 0x40, 0x2E, 0x0D, // 55=7:832 0x03, 0x6E, 0x2F, 0x0D, // 56=8:878 0x03, 0x9D, 0x2F, 0x0D, // 57=9:925 0x03, 0xCC, 0x0F, 0x07, // 58=::972 0x03, 0xDB, 0x10, 0x07, // 59=;:987 0x03, 0xEB, 0x2F, 0x0E, // 60=<:1003 0x04, 0x1A, 0x2F, 0x0E, // 61==:1050 0x04, 0x49, 0x2E, 0x0E, // 62=>:1097 0x04, 0x77, 0x2E, 0x0D, // 63=?:1143 0x04, 0xA5, 0x5C, 0x18, // 64=@:1189 0x05, 0x01, 0x3B, 0x0F, // 65=A:1281 0x05, 0x3C, 0x3B, 0x10, // 66=B:1340 0x05, 0x77, 0x3F, 0x11, // 67=C:1399 0x05, 0xB6, 0x3F, 0x11, // 68=D:1462 0x05, 0xF5, 0x3B, 0x10, // 69=E:1525 0x06, 0x30, 0x36, 0x0F, // 70=F:1584 0x06, 0x66, 0x43, 0x13, // 71=G:1638 0x06, 0xA9, 0x3B, 0x11, // 72=H:1705 0x06, 0xE4, 0x0F, 0x06, // 73=I:1764 0x06, 0xF3, 0x27, 0x0C, // 74=J:1779 0x07, 0x1A, 0x3F, 0x10, // 75=K:1818 0x07, 0x59, 0x2F, 0x0D, // 76=L:1881 0x07, 0x88, 0x43, 0x13, // 77=M:1928 0x07, 0xCB, 0x3B, 0x11, // 78=N:1995 0x08, 0x06, 0x47, 0x13, // 79=O:2054 0x08, 0x4D, 0x3A, 0x10, // 80=P:2125 0x08, 0x87, 0x48, 0x13, // 81=Q:2183 0x08, 0xCF, 0x3F, 0x11, // 82=R:2255 0x09, 0x0E, 0x3B, 0x10, // 83=S:2318 0x09, 0x49, 0x36, 0x0E, // 84=T:2377 0x09, 0x7F, 0x3B, 0x11, // 85=U:2431 0x09, 0xBA, 0x39, 0x0F, // 86=V:2490 0x09, 0xF3, 0x5A, 0x17, // 87=W:2547 0x0A, 0x4D, 0x3B, 0x0F, // 88=X:2637 0x0A, 0x88, 0x3D, 0x10, // 89=Y:2696 0x0A, 0xC5, 0x37, 0x0F, // 90=Z:2757 0x0A, 0xFC, 0x14, 0x07, // 91=[:2812 0x0B, 0x10, 0x1B, 0x07, // 92=\:2832 0x0B, 0x2B, 0x18, 0x07, // 93=]:2859 0x0B, 0x43, 0x2A, 0x0C, // 94=^:2883 0x0B, 0x6D, 0x34, 0x0D, // 95=_:2925 0x0B, 0xA1, 0x12, 0x08, // 96=`:2977 0x0B, 0xB3, 0x2F, 0x0D, // 97=a:2995 0x0B, 0xE2, 0x33, 0x0E, // 98=b:3042 0x0C, 0x15, 0x2B, 0x0C, // 99=c:3093 0x0C, 0x40, 0x2F, 0x0E, // 100=d:3136 0x0C, 0x6F, 0x2F, 0x0D, // 101=e:3183 0x0C, 0x9E, 0x1A, 0x07, // 102=f:3230 0x0C, 0xB8, 0x30, 0x0E, // 103=g:3256 0x0C, 0xE8, 0x2F, 0x0E, // 104=h:3304 0x0D, 0x17, 0x0F, 0x05, // 105=i:3351 0x0D, 0x26, 0x10, 0x06, // 106=j:3366 0x0D, 0x36, 0x2F, 0x0C, // 107=k:3382 0x0D, 0x65, 0x0F, 0x06, // 108=l:3429 0x0D, 0x74, 0x47, 0x14, // 109=m:3444 0x0D, 0xBB, 0x2F, 0x0E, // 110=n:3515 0x0D, 0xEA, 0x2F, 0x0D, // 111=o:3562 0x0E, 0x19, 0x33, 0x0E, // 112=p:3609 0x0E, 0x4C, 0x30, 0x0E, // 113=q:3660 0x0E, 0x7C, 0x1E, 0x08, // 114=r:3708 0x0E, 0x9A, 0x2B, 0x0C, // 115=s:3738 0x0E, 0xC5, 0x1B, 0x07, // 116=t:3781 0x0E, 0xE0, 0x2F, 0x0E, // 117=u:3808 0x0F, 0x0F, 0x2A, 0x0B, // 118=v:3855 0x0F, 0x39, 0x42, 0x11, // 119=w:3897 0x0F, 0x7B, 0x2B, 0x0B, // 120=x:3963 0x0F, 0xA6, 0x2A, 0x0C, // 121=y:4006 0x0F, 0xD0, 0x2B, 0x0C, // 122=z:4048 0x0F, 0xFB, 0x1C, 0x08, // 123={:4091 0x10, 0x17, 0x10, 0x06, // 124=|:4119 0x10, 0x27, 0x1B, 0x08, // 125=}:4135 0x10, 0x42, 0x33, 0x0E, // 126=~:4162 0xFF, 0xFF, 0x00, 0x12, // 127:65535 0x10, 0x75, 0x4F, 0x15, // 1026=� �..:4213 0x10, 0xC4, 0x32, 0x0D, // 1027=� �.:4292 0x10, 0xF6, 0x0C, 0x05, // 8218=��.�.:4342 0x11, 0x02, 0x22, 0x09, // 1107=�.�..:4354 0x11, 0x24, 0x1C, 0x08, // 8222=��.�.:4388 0x11, 0x40, 0x4B, 0x18, // 8230=��.�.:4416 0x11, 0x8B, 0x32, 0x0D, // 8224=��.� :4491 0x11, 0xBD, 0x33, 0x0D, // 8225=��.�.:4541 0x11, 0xF0, 0x2F, 0x0D, // 8364=��..�.:4592 0x12, 0x1F, 0x63, 0x1A, // 8240=��.��:4639 0x12, 0x82, 0x5F, 0x19, // 1033=� �.�:4738 0x12, 0xE1, 0x17, 0x08, // 8249=��.�..:4833 0x12, 0xF8, 0x5B, 0x18, // 1034=� �.:4856 0x13, 0x53, 0x37, 0x0E, // 1036=� �.:4947 0x13, 0x8A, 0x4F, 0x16, // 1035=� �..:5002 0x13, 0xD9, 0x3B, 0x11, // 1039=� �.:5081 0x14, 0x14, 0x30, 0x0E, // 1106=�.�..:5140 0x14, 0x44, 0x0A, 0x05, // 8216=��.Ч.:5188 0x14, 0x4E, 0x0A, 0x05, // 8217=��.�..:5198 0x14, 0x58, 0x1A, 0x08, // 8220=��.�.:5208 0x14, 0x72, 0x1A, 0x08, // 8221=��.�.:5234 0x14, 0x8C, 0x1B, 0x08, // 8226=��.�.:5260 0x14, 0xA7, 0x33, 0x0D, // 8211=��.�..:5287 0x14, 0xDA, 0x5F, 0x18, // 8212=��.�..:5338 0xFF, 0xFF, 0x00, 0x12, // 65533=��.�.:65535 0x15, 0x39, 0x5B, 0x18, // 8482=��..�.:5433 0x15, 0x94, 0x53, 0x16, // 1113=�.�..:5524 0x15, 0xE7, 0x1B, 0x08, // 8250=��.�.:5607 0x16, 0x02, 0x4B, 0x14, // 1114=�.�.:5634 0x16, 0x4D, 0x2B, 0x0B, // 1116=�.�.:5709 0x16, 0x78, 0x2F, 0x0E, // 1115=�.�.�:5752 0x16, 0xA7, 0x2F, 0x0D, // 1119=�.�.:5799 0xFF, 0xFF, 0x00, 0x07, // 160=�.� :65535 0x16, 0xD6, 0x36, 0x0F, // 1038=� �.:5846 0x17, 0x0C, 0x2A, 0x0C, // 1118=�.�.:5900 0x17, 0x36, 0x27, 0x0C, // 1032=� �..:5942 0x17, 0x5D, 0x33, 0x0D, // 164=�.�.:5981 0x17, 0x90, 0x2A, 0x0C, // 1168=�.�.:6032 0x17, 0xBA, 0x10, 0x06, // 166=�.�.:6074 0x17, 0xCA, 0x2F, 0x0D, // 167=�.�.:6090 0x17, 0xF9, 0x3B, 0x10, // 1025=� �.:6137 0x18, 0x34, 0x47, 0x12, // 169=�.��:6196 0x18, 0x7B, 0x3F, 0x11, // 1028=� �..:6267 0x18, 0xBA, 0x27, 0x0D, // 171=�.�.:6330 0x18, 0xE1, 0x2F, 0x0E, // 172=�.�.:6369 0x19, 0x10, 0x1B, 0x08, // 173=�.�:6416 0x19, 0x2B, 0x47, 0x12, // 174=�.�.:6443 0x19, 0x72, 0x15, 0x06, // 1031=� �..:6514 0x19, 0x87, 0x1E, 0x0A, // 176=�.��:6535 0x19, 0xA5, 0x33, 0x0D, // 177=�.�.:6565 0x19, 0xD8, 0x0F, 0x06, // 1030=� �. :6616 0x19, 0xE7, 0x0F, 0x05, // 1110=�.�..:6631 0x19, 0xF6, 0x22, 0x0A, // 1169=�.�.:6646 0x1A, 0x18, 0x2F, 0x0E, // 181=�.�.:6680 0x1A, 0x47, 0x32, 0x0D, // 182=�.�.:6727 0x1A, 0x79, 0x13, 0x08, // 183=�.��:6777 0x1A, 0x8C, 0x2F, 0x0D, // 1105=�.�.:6796 0x1A, 0xBB, 0x63, 0x1A, // 8470=��..�..:6843 0x1B, 0x1E, 0x2B, 0x0C, // 1108=�.�..:6942 0x1B, 0x49, 0x2F, 0x0D, // 187=�.�.:6985 0x1B, 0x78, 0x10, 0x06, // 1112=�.Ч.:7032 0x1B, 0x88, 0x3B, 0x10, // 1029=� �..:7048 0x1B, 0xC3, 0x2B, 0x0C, // 1109=�.�..:7107 0x1B, 0xEE, 0x16, 0x06, // 1111=�.�..:7150 0x1C, 0x04, 0x3B, 0x0F, // 1040=� �.:7172 0x1C, 0x3F, 0x3B, 0x10, // 1041=� �.:7231 0x1C, 0x7A, 0x3B, 0x10, // 1042=� �..:7290 0x1C, 0xB5, 0x32, 0x0D, // 1043=� �..:7349 0x1C, 0xE7, 0x40, 0x10, // 1044=� �..:7399 0x1D, 0x27, 0x3B, 0x10, // 1045=� �..:7463 0x1D, 0x62, 0x57, 0x16, // 1046=� �..:7522 0x1D, 0xB9, 0x37, 0x0F, // 1047=� �..:7609 0x1D, 0xF0, 0x3B, 0x11, // 1048=� Ч.:7664 0x1E, 0x2B, 0x3B, 0x11, // 1049=� �..:7723 0x1E, 0x66, 0x37, 0x0E, // 1050=� �.:7782 0x1E, 0x9D, 0x37, 0x10, // 1051=� �.�:7837 0x1E, 0xD4, 0x43, 0x13, // 1052=� �.:7892 0x1F, 0x17, 0x3B, 0x11, // 1053=� �.:7959 0x1F, 0x52, 0x47, 0x13, // 1054=� �.:8018 0x1F, 0x99, 0x3B, 0x11, // 1055=� �.:8089 0x1F, 0xD4, 0x3A, 0x10, // 1056=� � :8148 0x20, 0x0E, 0x3F, 0x11, // 1057=� �.:8206 0x20, 0x4D, 0x36, 0x0E, // 1058=� �.:8269 0x20, 0x83, 0x36, 0x0F, // 1059=� �.:8323 0x20, 0xB9, 0x43, 0x12, // 1060=� �.:8377 0x20, 0xFC, 0x3B, 0x0F, // 1061=� �.:8444 0x21, 0x37, 0x44, 0x12, // 1062=� �.:8503 0x21, 0x7B, 0x37, 0x10, // 1063=� �.:8571 0x21, 0xB2, 0x53, 0x16, // 1064=� �.:8626 0x22, 0x05, 0x5C, 0x17, // 1065=� ��:8709 0x22, 0x61, 0x47, 0x13, // 1066=� �.:8801 0x22, 0xA8, 0x4B, 0x15, // 1067=� �.:8872 0x22, 0xF3, 0x3B, 0x10, // 1068=� �.:8947 0x23, 0x2E, 0x3F, 0x11, // 1069=� �:9006 0x23, 0x6D, 0x5B, 0x18, // 1070=� �.:9069 0x23, 0xC8, 0x3B, 0x11, // 1071=� �.:9160 0x24, 0x03, 0x2F, 0x0D, // 1072=� ��:9219 0x24, 0x32, 0x33, 0x0E, // 1073=� �.:9266 0x24, 0x65, 0x2F, 0x0D, // 1074=� �.:9317 0x24, 0x94, 0x22, 0x09, // 1075=� �.:9364 0x24, 0xB6, 0x34, 0x0E, // 1076=� �.:9398 0x24, 0xEA, 0x2F, 0x0D, // 1077=� �.:9450 0x25, 0x19, 0x3B, 0x10, // 1078=� �.:9497 0x25, 0x54, 0x27, 0x0B, // 1079=� ��:9556 0x25, 0x7B, 0x2F, 0x0D, // 1080=� �.:9595 0x25, 0xAA, 0x2F, 0x0D, // 1081=� �..:9642 0x25, 0xD9, 0x2B, 0x0B, // 1082=� �.:9689 0x26, 0x04, 0x2F, 0x0E, // 1083=� �.:9732 0x26, 0x33, 0x3B, 0x11, // 1084=� �:9779 0x26, 0x6E, 0x2F, 0x0D, // 1085=� �.:9838 0x26, 0x9D, 0x2F, 0x0D, // 1086=� �.:9885 0x26, 0xCC, 0x2F, 0x0D, // 1087=� �.:9932 0x26, 0xFB, 0x33, 0x0E, // 1088=�.�.:9979 0x27, 0x2E, 0x2B, 0x0C, // 1089=�.�.:10030 0x27, 0x59, 0x26, 0x0B, // 1090=�.�..:10073 0x27, 0x7F, 0x2A, 0x0C, // 1091=�.�.:10111 0x27, 0xA9, 0x4B, 0x14, // 1092=�.�..:10153 0x27, 0xF4, 0x2B, 0x0B, // 1093=�.�..:10228 0x28, 0x1F, 0x34, 0x0E, // 1094=�.�. :10271 0x28, 0x53, 0x2B, 0x0D, // 1095=�.�..:10323 0x28, 0x7E, 0x47, 0x13, // 1096=�.�..:10366 0x28, 0xC5, 0x4C, 0x14, // 1097=�.�.�:10437 0x29, 0x11, 0x37, 0x0F, // 1098=�.�.:10513 0x29, 0x48, 0x3B, 0x11, // 1099=�.�..:10568 0x29, 0x83, 0x2F, 0x0D, // 1100=�.�.:10627 0x29, 0xB2, 0x2B, 0x0C, // 1101=�.�.:10674 0x29, 0xDD, 0x43, 0x12, // 1102=�.�.:10717 0x2A, 0x20, 0x2B, 0x0D, // 1103=�.�.:10784 // Font Data: 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xCF, 0x00, 0x80, 0xFF, 0xCF, // 33 0x00, 0x00, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x80, 0x1F, // 34 0x00, 0x30, 0x0C, 0x00, 0x00, 0x30, 0xCC, 0x00, 0x00, 0x30, 0xFE, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0xFE, 0x0F, 0x00, 0x80, 0x3F, 0x0C, 0x00, 0x80, 0x31, 0xCC, 0x00, 0x00, 0x30, 0xFE, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0xFE, 0x0D, 0x00, 0x80, 0x3F, 0x0C, 0x00, 0x80, 0x31, 0x0C, 0x00, 0x00, 0x30, 0x0C, // 35 0x00, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x18, 0x00, 0x00, 0x3F, 0x78, 0x00, 0x00, 0x63, 0x70, 0x00, 0x80, 0x61, 0xE0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0xC0, 0xFF, 0xFF, 0x03, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0x81, 0xC1, 0x00, 0x00, 0x83, 0x61, 0x00, 0x00, 0x07, 0x7F, 0x00, 0x00, 0x04, 0x1E, // 36 0x00, 0x00, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x7F, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x80, 0x80, 0x80, 0x00, 0x80, 0xC1, 0xE0, 0x00, 0x00, 0x7F, 0x78, 0x00, 0x00, 0x3E, 0x3E, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x3C, 0x3E, 0x00, 0x00, 0x0F, 0x7F, 0x00, 0x80, 0x83, 0xC1, 0x00, 0x80, 0x80, 0x80, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x00, 0x7F, 0x00, 0x00, 0x00, 0x3E, // 37 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x7F, 0x00, 0x00, 0x8E, 0x73, 0x00, 0x00, 0xDF, 0xE1, 0x00, 0x80, 0xF3, 0xC0, 0x00, 0x80, 0xE1, 0xC0, 0x00, 0x80, 0xE1, 0xC1, 0x00, 0x80, 0xB3, 0xE3, 0x00, 0x00, 0x3F, 0x6E, 0x00, 0x00, 0x0E, 0x7C, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x7F, 0x00, 0x00, 0x00, 0xE2, 0x00, 0x00, 0x00, 0x40, // 38 0x00, 0x00, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x80, 0x1F, // 39 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0xF8, 0xFF, 0x01, 0x00, 0x3E, 0xC0, 0x07, 0x00, 0x07, 0x00, 0x0E, 0x80, 0x01, 0x00, 0x18, 0x80, 0x00, 0x00, 0x10, // 40 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x10, 0x80, 0x01, 0x00, 0x18, 0x00, 0x07, 0x00, 0x0E, 0x00, 0x3E, 0xC0, 0x07, 0x00, 0xF8, 0xFF, 0x01, 0x00, 0xC0, 0x3F, // 41 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x02, // 42 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, // 43 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x0C, 0x00, 0x00, 0xC0, 0x07, // 44 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, // 45 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, // 46 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x80, 0x01, // 47 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0x07, 0x70, 0x00, 0x80, 0x03, 0xE0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x03, 0xE0, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xF8, 0x0F, // 48 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 49 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0xC0, 0x00, 0x00, 0x0F, 0xE0, 0x00, 0x00, 0x03, 0xF0, 0x00, 0x80, 0x01, 0xD8, 0x00, 0x80, 0x01, 0xCC, 0x00, 0x80, 0x01, 0xC6, 0x00, 0x80, 0x01, 0xC3, 0x00, 0x80, 0x81, 0xC1, 0x00, 0x00, 0xC3, 0xC0, 0x00, 0x00, 0x7F, 0xC0, 0x00, 0x00, 0x3C, 0xC0, // 50 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x18, 0x00, 0x00, 0x07, 0x38, 0x00, 0x00, 0x03, 0x70, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x00, 0xE3, 0xC0, 0x00, 0x00, 0xBF, 0x61, 0x00, 0x00, 0x1E, 0x3F, 0x00, 0x00, 0x00, 0x1E, // 51 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xF0, 0x0C, 0x00, 0x00, 0x38, 0x0C, 0x00, 0x00, 0x1E, 0x0C, 0x00, 0x00, 0x07, 0x0C, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, // 52 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x18, 0x00, 0x00, 0xFE, 0x38, 0x00, 0x80, 0x7F, 0x60, 0x00, 0x80, 0x21, 0xC0, 0x00, 0x80, 0x31, 0xC0, 0x00, 0x80, 0x31, 0xC0, 0x00, 0x80, 0x31, 0xC0, 0x00, 0x80, 0x31, 0xC0, 0x00, 0x80, 0x61, 0x70, 0x00, 0x80, 0xC1, 0x3F, 0x00, 0x00, 0x80, 0x0F, // 53 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0x8F, 0x71, 0x00, 0x00, 0xC3, 0xE0, 0x00, 0x80, 0x61, 0xC0, 0x00, 0x80, 0x61, 0xC0, 0x00, 0x80, 0x61, 0xC0, 0x00, 0x80, 0x61, 0xC0, 0x00, 0x80, 0xC3, 0x60, 0x00, 0x00, 0xC7, 0x3F, 0x00, 0x00, 0x06, 0x1F, // 54 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0xF0, 0x00, 0x80, 0x01, 0xFE, 0x00, 0x80, 0xC1, 0x0F, 0x00, 0x80, 0xE1, 0x01, 0x00, 0x80, 0x39, 0x00, 0x00, 0x80, 0x0D, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x80, 0x01, // 55 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x1E, 0x7F, 0x00, 0x00, 0xBF, 0x61, 0x00, 0x80, 0xE3, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xE3, 0xC0, 0x00, 0x00, 0xBF, 0x61, 0x00, 0x00, 0x1E, 0x7F, 0x00, 0x00, 0x00, 0x1E, // 56 0x00, 0x00, 0x00, 0x00, 0x00, 0x7C, 0x30, 0x00, 0x00, 0xFE, 0x71, 0x00, 0x00, 0x87, 0xE1, 0x00, 0x80, 0x01, 0xC3, 0x00, 0x80, 0x01, 0xC3, 0x00, 0x80, 0x01, 0xC3, 0x00, 0x80, 0x01, 0xC3, 0x00, 0x80, 0x81, 0x61, 0x00, 0x00, 0xC7, 0x78, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0xF8, 0x07, // 57 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, // 58 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0xC0, 0x0C, 0x00, 0x18, 0xC0, 0x07, // 59 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x40, 0x01, 0x00, 0x00, 0x60, 0x03, 0x00, 0x00, 0x60, 0x03, 0x00, 0x00, 0x30, 0x06, 0x00, 0x00, 0x30, 0x06, 0x00, 0x00, 0x10, 0x04, 0x00, 0x00, 0x18, 0x0C, 0x00, 0x00, 0x18, 0x0C, 0x00, 0x00, 0x0C, 0x18, // 60 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x06, 0x00, 0x00, 0x30, 0x06, 0x00, 0x00, 0x30, 0x06, 0x00, 0x00, 0x30, 0x06, 0x00, 0x00, 0x30, 0x06, 0x00, 0x00, 0x30, 0x06, 0x00, 0x00, 0x30, 0x06, 0x00, 0x00, 0x30, 0x06, 0x00, 0x00, 0x30, 0x06, 0x00, 0x00, 0x30, 0x06, 0x00, 0x00, 0x30, 0x06, // 61 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x18, 0x0C, 0x00, 0x00, 0x18, 0x0C, 0x00, 0x00, 0x10, 0x04, 0x00, 0x00, 0x30, 0x06, 0x00, 0x00, 0x30, 0x06, 0x00, 0x00, 0x60, 0x03, 0x00, 0x00, 0x60, 0x03, 0x00, 0x00, 0x40, 0x01, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x80, // 62 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x80, 0x01, 0xCE, 0x00, 0x80, 0x01, 0xCF, 0x00, 0x80, 0x81, 0x01, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x00, 0xE3, 0x00, 0x00, 0x00, 0x7F, 0x00, 0x00, 0x00, 0x1C, // 63 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0xE0, 0xFF, 0x00, 0x00, 0x78, 0xC0, 0x03, 0x00, 0x1C, 0x00, 0x07, 0x00, 0x0E, 0x1F, 0x06, 0x00, 0xC7, 0x7F, 0x0E, 0x00, 0xE3, 0x60, 0x0C, 0x00, 0x33, 0xC0, 0x0C, 0x80, 0x39, 0xC0, 0x18, 0x80, 0x19, 0xC0, 0x18, 0x80, 0x19, 0x60, 0x18, 0x80, 0x19, 0x30, 0x18, 0x80, 0x31, 0x78, 0x18, 0x80, 0xE1, 0xFF, 0x18, 0x80, 0xFB, 0xC7, 0x18, 0x00, 0x3B, 0xC0, 0x18, 0x00, 0x07, 0x60, 0x0C, 0x00, 0x0E, 0x70, 0x0C, 0x00, 0x1C, 0x3C, 0x06, 0x00, 0xF8, 0x1F, 0x06, 0x00, 0xE0, 0x07, 0x03, 0x00, 0x00, 0x00, 0x01, // 64 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xF8, 0x07, 0x00, 0x00, 0x3E, 0x06, 0x00, 0x80, 0x0F, 0x06, 0x00, 0x80, 0x01, 0x06, 0x00, 0x80, 0x0F, 0x06, 0x00, 0x00, 0x3E, 0x06, 0x00, 0x00, 0xF8, 0x07, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0xC0, // 65 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x00, 0xE3, 0xC1, 0x00, 0x00, 0xFF, 0x61, 0x00, 0x00, 0x1E, 0x7F, 0x00, 0x00, 0x00, 0x1E, // 66 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, 0x03, 0x60, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x00, 0x03, 0x60, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, 0x0E, 0x3C, 0x00, 0x00, 0x08, 0x0C, // 67 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x03, 0x60, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF0, 0x07, // 68 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0x01, 0xC0, // 69 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0x01, // 70 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, 0x03, 0x60, 0x00, 0x80, 0x03, 0x60, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x81, 0xC1, 0x00, 0x80, 0x81, 0xC1, 0x00, 0x80, 0x83, 0xC1, 0x00, 0x00, 0x83, 0x61, 0x00, 0x00, 0x87, 0x61, 0x00, 0x00, 0x8E, 0x3F, 0x00, 0x00, 0x88, 0x3F, // 71 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 72 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 73 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x80, 0xFF, 0x3F, // 74 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x9C, 0x07, 0x00, 0x00, 0x0E, 0x1E, 0x00, 0x00, 0x07, 0x3C, 0x00, 0x80, 0x03, 0x78, 0x00, 0x80, 0x01, 0xE0, 0x00, 0x80, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x80, // 75 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, // 76 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 77 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x70, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 78 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, 0x03, 0x60, 0x00, 0x80, 0x03, 0xE0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x03, 0xE0, 0x00, 0x00, 0x03, 0x60, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF0, 0x07, // 79 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0x81, 0x01, 0x00, 0x80, 0x81, 0x01, 0x00, 0x80, 0x81, 0x01, 0x00, 0x80, 0x81, 0x01, 0x00, 0x80, 0x81, 0x01, 0x00, 0x80, 0x81, 0x01, 0x00, 0x80, 0x81, 0x01, 0x00, 0x80, 0x81, 0x01, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x3C, // 80 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x07, 0x30, 0x00, 0x00, 0x03, 0x60, 0x00, 0x80, 0x03, 0x60, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xD8, 0x00, 0x80, 0x01, 0xD8, 0x00, 0x80, 0x03, 0xF0, 0x00, 0x00, 0x03, 0x70, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, 0x1E, 0xFC, 0x00, 0x00, 0xFC, 0xDF, 0x01, 0x00, 0xF0, 0x87, 0x01, // 81 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x01, 0x00, 0x80, 0xC1, 0x03, 0x00, 0x80, 0xC1, 0x0F, 0x00, 0x80, 0xC1, 0x1E, 0x00, 0x80, 0x63, 0x7C, 0x00, 0x00, 0x7F, 0xF0, 0x00, 0x00, 0x3E, 0xC0, 0x00, 0x00, 0x00, 0x80, // 82 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x1C, 0x3C, 0x00, 0x00, 0x7F, 0x70, 0x00, 0x00, 0x63, 0x60, 0x00, 0x80, 0xE1, 0xE0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC1, 0x00, 0x80, 0x83, 0xE1, 0x00, 0x00, 0x87, 0x63, 0x00, 0x00, 0x0E, 0x3F, 0x00, 0x00, 0x0C, 0x1E, // 83 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, // 84 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0x80, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x70, 0x00, 0x80, 0xFF, 0x3F, 0x00, 0x80, 0xFF, 0x0F, // 85 0x80, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x80, // 86 0x80, 0x01, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0xFE, 0x03, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x80, 0x01, // 87 0x00, 0x00, 0x80, 0x00, 0x80, 0x00, 0xC0, 0x00, 0x80, 0x01, 0xF0, 0x00, 0x80, 0x07, 0x78, 0x00, 0x00, 0x0F, 0x1E, 0x00, 0x00, 0x3C, 0x0F, 0x00, 0x00, 0xF8, 0x07, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xF8, 0x07, 0x00, 0x00, 0x1E, 0x0F, 0x00, 0x00, 0x0F, 0x1C, 0x00, 0x80, 0x07, 0x78, 0x00, 0x80, 0x01, 0xF0, 0x00, 0x80, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x80, // 88 0x80, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x00, 0x00, 0xC0, 0xFF, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, // 89 0x00, 0x00, 0xC0, 0x00, 0x80, 0x01, 0xE0, 0x00, 0x80, 0x01, 0xF0, 0x00, 0x80, 0x01, 0xDC, 0x00, 0x80, 0x01, 0xCE, 0x00, 0x80, 0x01, 0xC7, 0x00, 0x80, 0x81, 0xC3, 0x00, 0x80, 0xE1, 0xC0, 0x00, 0x80, 0x71, 0xC0, 0x00, 0x80, 0x39, 0xC0, 0x00, 0x80, 0x1D, 0xC0, 0x00, 0x80, 0x07, 0xC0, 0x00, 0x80, 0x03, 0xC0, 0x00, 0x80, 0x01, 0xC0, // 90 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x1F, 0x80, 0xFF, 0xFF, 0x1F, 0x80, 0x01, 0x00, 0x18, 0x80, 0x01, 0x00, 0x18, // 91 0x80, 0x01, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0xC0, // 92 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x18, 0x80, 0x01, 0x00, 0x18, 0x80, 0xFF, 0xFF, 0x1F, 0x80, 0xFF, 0xFF, 0x1F, // 93 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x80, // 94 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, // 95 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x02, // 96 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x38, 0x00, 0x00, 0x70, 0x7C, 0x00, 0x00, 0x30, 0xE6, 0x00, 0x00, 0x18, 0xC6, 0x00, 0x00, 0x18, 0xC6, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0x18, 0x63, 0x00, 0x00, 0x38, 0x63, 0x00, 0x00, 0xF0, 0x7F, 0x00, 0x00, 0xE0, 0xFF, 0x00, 0x00, 0x00, 0x80, // 97 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0x30, 0x60, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x80, 0x0F, // 98 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0x60, 0x30, // 99 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x30, 0x60, 0x00, 0x00, 0x70, 0x30, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 100 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x70, 0x73, 0x00, 0x00, 0x38, 0xE3, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0x38, 0xE3, 0x00, 0x00, 0x70, 0x63, 0x00, 0x00, 0xE0, 0x33, 0x00, 0x00, 0xC0, 0x13, // 101 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0x19, 0x00, 0x00, 0x80, 0x19, 0x00, 0x00, 0x80, 0x19, // 102 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x06, 0x00, 0xE0, 0x3F, 0x0E, 0x00, 0x70, 0x70, 0x1C, 0x00, 0x38, 0xE0, 0x18, 0x00, 0x18, 0xC0, 0x18, 0x00, 0x18, 0xC0, 0x18, 0x00, 0x18, 0xC0, 0x18, 0x00, 0x30, 0x60, 0x1C, 0x00, 0x60, 0x30, 0x0E, 0x00, 0xF8, 0xFF, 0x07, 0x00, 0xF8, 0xFF, 0x03, // 103 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0x00, 0x00, 0xE0, 0xFF, // 104 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xF9, 0xFF, 0x00, 0x80, 0xF9, 0xFF, // 105 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x80, 0xF9, 0xFF, 0x1F, 0x80, 0xF9, 0xFF, 0x0F, // 106 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x60, 0x1E, 0x00, 0x00, 0x30, 0x38, 0x00, 0x00, 0x18, 0xF0, 0x00, 0x00, 0x08, 0xC0, 0x00, 0x00, 0x00, 0x80, // 107 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 108 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0x00, 0x00, 0xE0, 0xFF, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0x00, 0x00, 0xE0, 0xFF, // 109 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0x00, 0x00, 0xE0, 0xFF, // 110 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0xC0, 0x1F, // 111 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x1F, 0x00, 0xF8, 0xFF, 0x1F, 0x00, 0x60, 0x30, 0x00, 0x00, 0x30, 0x60, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0xC0, 0x0F, // 112 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x30, 0x60, 0x00, 0x00, 0x60, 0x30, 0x00, 0x00, 0xF8, 0xFF, 0x1F, 0x00, 0xF8, 0xFF, 0x1F, // 113 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, // 114 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x30, 0x00, 0x00, 0xF0, 0x71, 0x00, 0x00, 0xB8, 0xE3, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0x18, 0xC7, 0x00, 0x00, 0x18, 0xC7, 0x00, 0x00, 0x38, 0xE6, 0x00, 0x00, 0x70, 0x7E, 0x00, 0x00, 0x60, 0x3C, // 115 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0xFF, 0x7F, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, // 116 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x3F, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, // 117 0x00, 0x18, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0x18, // 118 0x00, 0x38, 0x00, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x00, 0x38, // 119 0x00, 0x08, 0x80, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x78, 0xF0, 0x00, 0x00, 0xE0, 0x38, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0xE0, 0x38, 0x00, 0x00, 0x70, 0xF0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x08, 0x80, // 120 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0xF8, 0x01, 0x18, 0x00, 0xC0, 0x07, 0x18, 0x00, 0x00, 0x3E, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x7F, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0x18, // 121 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x18, 0xF0, 0x00, 0x00, 0x18, 0xF8, 0x00, 0x00, 0x18, 0xDC, 0x00, 0x00, 0x18, 0xCF, 0x00, 0x00, 0x98, 0xC3, 0x00, 0x00, 0xD8, 0xC1, 0x00, 0x00, 0xF8, 0xC0, 0x00, 0x00, 0x78, 0xC0, 0x00, 0x00, 0x18, 0xC0, // 122 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0xFF, 0xF9, 0x0F, 0x80, 0xFF, 0xF0, 0x1F, 0x80, 0x01, 0x00, 0x18, 0x80, 0x01, 0x00, 0x18, // 123 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x3F, 0x80, 0xFF, 0xFF, 0x3F, // 124 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x18, 0x80, 0x01, 0x00, 0x18, 0x80, 0xFF, 0xF0, 0x1F, 0x00, 0xFF, 0xFD, 0x0F, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x06, // 125 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x80, 0x01, // 126 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0x61, 0x00, 0x00, 0x80, 0x61, 0x60, 0x00, 0x80, 0x61, 0xC0, 0x00, 0x80, 0x61, 0xC0, 0x00, 0x00, 0x60, 0xC0, 0x00, 0x00, 0xE0, 0xE0, 0x00, 0x00, 0xC0, 0x71, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0x00, 0x1F, // 1026 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xA0, 0x01, 0x00, 0x00, 0xB8, 0x01, 0x00, 0x00, 0x98, 0x01, 0x00, 0x00, 0x88, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, // 1027 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x0C, 0x00, 0x00, 0xC0, 0x07, // 8218 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x1A, 0x00, 0x00, 0x80, 0x1B, 0x00, 0x00, 0x80, 0x19, 0x00, 0x00, 0x80, 0x18, 0x00, 0x00, 0x00, 0x18, // 1107 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x0C, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x0C, 0x00, 0x00, 0xC0, 0x07, // 8222 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, // 8230 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x0F, 0x80, 0xFF, 0xFF, 0x0F, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, // 8224 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x80, 0xFF, 0xFF, 0x0F, 0x80, 0xFF, 0xFF, 0x0F, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, // 8225 0x00, 0x30, 0x03, 0x00, 0x00, 0xF8, 0x07, 0x00, 0x00, 0xFE, 0x1F, 0x00, 0x00, 0x36, 0x3B, 0x00, 0x00, 0x33, 0x73, 0x00, 0x00, 0x33, 0x63, 0x00, 0x80, 0x31, 0xC3, 0x00, 0x80, 0x31, 0xC3, 0x00, 0x80, 0x31, 0xC3, 0x00, 0x80, 0x31, 0xC3, 0x00, 0x80, 0x31, 0xC0, 0x00, 0x00, 0x03, 0x60, // 8364 0x00, 0x00, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x7F, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x80, 0xC1, 0xE0, 0x00, 0x00, 0x7F, 0xF8, 0x00, 0x00, 0x3E, 0x3E, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0x3E, 0x3E, 0x00, 0x80, 0x0F, 0x7F, 0x00, 0x80, 0x81, 0xC1, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x00, 0x7F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x7F, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x00, 0x7F, 0x00, 0x00, 0x00, 0x3E, // 8240 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x80, 0xFF, 0x3F, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0x80, 0x61, 0x00, 0x00, 0x80, 0x7F, 0x00, 0x00, 0x00, 0x1E, // 1033 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xE0, 0x3D, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0x10, 0x40, // 8249 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0x80, 0x61, 0x00, 0x00, 0x80, 0x7F, 0x00, 0x00, 0x00, 0x1E, // 1034 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x20, 0x60, 0x03, 0x00, 0x38, 0x78, 0x07, 0x00, 0x18, 0x1E, 0x0E, 0x00, 0x08, 0x07, 0x38, 0x00, 0x80, 0x01, 0xF0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0x80, // 1036 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0x61, 0x00, 0x00, 0x80, 0x61, 0x00, 0x00, 0x80, 0x61, 0x00, 0x00, 0x80, 0x61, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xC0, 0xFF, 0x00, 0x00, 0x00, 0xFF, // 1035 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 1039 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x73, 0x00, 0x00, 0x00, 0x33, 0x00, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x00, 0x1B, 0x00, 0x18, 0x00, 0x18, 0x00, 0x18, 0x00, 0x38, 0x00, 0x18, 0x00, 0xF0, 0xFF, 0x1F, 0x00, 0xE0, 0xFF, 0x0F, // 1106 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x80, 0x19, // 8216 0x00, 0x00, 0x00, 0x00, 0x80, 0x19, 0x00, 0x00, 0x80, 0x0F, // 8217 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x80, 0x19, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x80, 0x19, // 8220 0x00, 0x00, 0x00, 0x00, 0x80, 0x19, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x19, 0x00, 0x00, 0x80, 0x0F, // 8221 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xC0, 0x03, // 8226 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, // 8211 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, // 8212 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0xFF, 0x01, 0x00, 0x80, 0xFF, 0x01, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0x01, 0x00, 0x80, 0xFF, 0x01, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x80, 0xFF, 0x01, 0x00, 0x80, 0xFF, 0x01, // 8482 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xE7, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x3C, // 1113 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x40, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0xE0, 0x3D, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x02, // 8250 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xE7, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x3C, // 1114 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x02, 0x02, 0x00, 0x80, 0x03, 0x07, 0x00, 0x80, 0xC1, 0x0D, 0x00, 0x80, 0xF0, 0x38, 0x00, 0x00, 0x18, 0x70, 0x00, 0x00, 0x18, 0xE0, 0x00, 0x00, 0x00, 0x80, // 1116 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x73, 0x00, 0x00, 0x00, 0x33, 0x00, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0x00, 0x00, 0xE0, 0xFF, // 1115 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, // 1119 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, 0x1E, 0xC0, 0x00, 0x18, 0x78, 0xC0, 0x00, 0x30, 0xE0, 0xC1, 0x00, 0x20, 0x80, 0x77, 0x00, 0x20, 0x00, 0x3E, 0x00, 0x20, 0x80, 0x07, 0x00, 0x30, 0xE0, 0x01, 0x00, 0x18, 0x78, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x80, 0x01, // 1038 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0xF8, 0x01, 0x18, 0x80, 0xC1, 0x07, 0x18, 0x00, 0x03, 0x3E, 0x1C, 0x00, 0x02, 0xF8, 0x0F, 0x00, 0x02, 0xF0, 0x03, 0x00, 0x02, 0x7F, 0x00, 0x00, 0xE3, 0x0F, 0x00, 0x80, 0xF9, 0x00, 0x00, 0x00, 0x18, // 1118 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x80, 0xFF, 0x3F, // 1032 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x10, 0x00, 0x00, 0xDC, 0x3B, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0x30, 0x0C, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x30, 0x0C, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0xDC, 0x3B, 0x00, 0x00, 0x08, 0x10, // 164 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xFC, 0x01, 0x00, 0x00, 0xFC, 0x01, // 1168 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xE1, 0x3F, 0x80, 0xFF, 0xE1, 0x3F, // 166 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xCE, 0x07, 0x03, 0x00, 0x7F, 0x0C, 0x0F, 0x80, 0x33, 0x1C, 0x0C, 0x80, 0x71, 0x18, 0x18, 0x80, 0x61, 0x30, 0x18, 0x80, 0xC1, 0x70, 0x18, 0x80, 0xC3, 0xE1, 0x1C, 0x00, 0x87, 0xD3, 0x0F, 0x00, 0x06, 0x9F, 0x07, 0x00, 0x00, 0x0E, // 167 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0xB0, 0xC1, 0xC0, 0x00, 0xB0, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0xB0, 0xC1, 0xC0, 0x00, 0xB0, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0x01, 0xC0, // 1025 0x00, 0xE0, 0x03, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0xE7, 0x71, 0x00, 0x00, 0xFB, 0x67, 0x00, 0x80, 0x19, 0xC6, 0x00, 0x80, 0x0D, 0xCC, 0x00, 0x80, 0x0D, 0xCC, 0x00, 0x80, 0x0D, 0xCC, 0x00, 0x80, 0x0D, 0xCC, 0x00, 0x80, 0x1D, 0xCE, 0x00, 0x00, 0x1B, 0x66, 0x00, 0x00, 0x17, 0x72, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xE0, 0x03, // 169 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xCE, 0x3C, 0x00, 0x00, 0xC7, 0x70, 0x00, 0x00, 0xC3, 0x60, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x03, 0xE0, 0x00, 0x00, 0x03, 0x60, 0x00, 0x00, 0x0F, 0x78, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x08, 0x08, // 1028 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xE0, 0x3D, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0x10, 0x42, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0xE0, 0x3D, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0x10, 0x40, // 171 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xF0, 0x07, // 172 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, // 173 0x00, 0xE0, 0x03, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, 0xFB, 0x6F, 0x00, 0x80, 0xF9, 0xCF, 0x00, 0x80, 0x99, 0xC1, 0x00, 0x80, 0x99, 0xC1, 0x00, 0x80, 0x99, 0xC3, 0x00, 0x80, 0xF9, 0xC7, 0x00, 0x80, 0xF1, 0xCC, 0x00, 0x00, 0x03, 0x68, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xE0, 0x03, // 174 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, // 1031 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x80, 0x20, 0x00, 0x00, 0x80, 0x20, 0x00, 0x00, 0x80, 0x20, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x0E, // 176 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0xFC, 0xFF, 0x00, 0x00, 0xFC, 0xFF, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, // 177 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 1030 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xF9, 0xFF, 0x00, 0x80, 0xF9, 0xFF, // 1110 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x80, 0x1F, // 1169 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x1F, 0x00, 0xF8, 0xFF, 0x1F, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, // 181 0x00, 0x3C, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x80, 0xFF, 0x01, 0x00, 0x80, 0xFF, 0x01, 0x00, 0x80, 0xFF, 0xFF, 0x1F, 0x80, 0xFF, 0xFF, 0x1F, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x1F, 0x80, 0xFF, 0xFF, 0x1F, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, // 182 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, // 183 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x70, 0x73, 0x00, 0x00, 0x3B, 0xE3, 0x00, 0x00, 0x1B, 0xC3, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0x3B, 0xE3, 0x00, 0x00, 0x73, 0x63, 0x00, 0x00, 0xE0, 0x33, 0x00, 0x00, 0xC0, 0x13, // 1105 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x78, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x63, 0x00, 0x00, 0xF0, 0x67, 0x00, 0x00, 0x38, 0x6E, 0x00, 0x00, 0x18, 0x6C, 0x00, 0x00, 0x18, 0x6C, 0x00, 0x00, 0x38, 0x6E, 0x00, 0x00, 0xF0, 0x67, 0x00, 0x00, 0xE0, 0x63, // 8470 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xE0, 0x7F, 0x00, 0x00, 0x70, 0x73, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x38, 0x60, 0x00, 0x00, 0x70, 0x78, 0x00, 0x00, 0x60, 0x18, // 1108 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x40, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0xE0, 0x3D, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x10, 0x42, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0xE0, 0x3D, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x02, // 187 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x80, 0xF9, 0xFF, 0x1F, 0x80, 0xF9, 0xFF, 0x0F, // 1112 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x1C, 0x3C, 0x00, 0x00, 0x7F, 0x70, 0x00, 0x00, 0x63, 0x60, 0x00, 0x80, 0xE1, 0xE0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC1, 0x00, 0x80, 0x83, 0xE1, 0x00, 0x00, 0x87, 0x63, 0x00, 0x00, 0x0E, 0x3F, 0x00, 0x00, 0x0C, 0x1E, // 1029 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x30, 0x00, 0x00, 0xF0, 0x71, 0x00, 0x00, 0xB8, 0xE3, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0x18, 0xC7, 0x00, 0x00, 0x18, 0xC7, 0x00, 0x00, 0x38, 0xE6, 0x00, 0x00, 0x70, 0x7E, 0x00, 0x00, 0x60, 0x3C, // 1109 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, // 1111 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xF8, 0x07, 0x00, 0x00, 0x3E, 0x06, 0x00, 0x80, 0x0F, 0x06, 0x00, 0x80, 0x01, 0x06, 0x00, 0x80, 0x0F, 0x06, 0x00, 0x00, 0x3E, 0x06, 0x00, 0x00, 0xF8, 0x07, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0xC0, // 1040 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0x81, 0x61, 0x00, 0x00, 0x80, 0x7F, 0x00, 0x00, 0x00, 0x1E, // 1041 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x00, 0xE3, 0xC1, 0x00, 0x00, 0xFF, 0x61, 0x00, 0x00, 0x1E, 0x7F, 0x00, 0x00, 0x00, 0x1E, // 1042 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, // 1043 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0xDF, 0x00, 0x80, 0xFF, 0xC7, 0x00, 0x80, 0x7F, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xC0, 0x0F, // 1044 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0x01, 0xC0, // 1045 0x80, 0x01, 0x80, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xF0, 0x00, 0x00, 0x07, 0x3C, 0x00, 0x00, 0x1E, 0x0E, 0x00, 0x00, 0x38, 0x07, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x38, 0x07, 0x00, 0x00, 0x1E, 0x0E, 0x00, 0x00, 0x07, 0x3C, 0x00, 0x80, 0x01, 0xF0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0x80, // 1046 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x08, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x07, 0x78, 0x00, 0x80, 0x03, 0x60, 0x00, 0x80, 0x01, 0xE0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x00, 0xE3, 0xE1, 0x00, 0x00, 0xBF, 0x73, 0x00, 0x00, 0x1C, 0x3F, 0x00, 0x00, 0x00, 0x1E, // 1047 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 1048 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x70, 0x00, 0x18, 0x00, 0x3C, 0x00, 0x30, 0x00, 0x0E, 0x00, 0x20, 0x80, 0x07, 0x00, 0x20, 0xC0, 0x01, 0x00, 0x20, 0xF0, 0x00, 0x00, 0x30, 0x38, 0x00, 0x00, 0x18, 0x1E, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 1049 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x01, 0x00, 0x00, 0x60, 0x03, 0x00, 0x00, 0x78, 0x07, 0x00, 0x00, 0x1E, 0x0E, 0x00, 0x00, 0x07, 0x38, 0x00, 0x80, 0x01, 0xF0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0x80, // 1050 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x80, 0xFF, 0x3F, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 1051 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 1052 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 1053 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, 0x03, 0x60, 0x00, 0x80, 0x03, 0xE0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x03, 0xE0, 0x00, 0x00, 0x03, 0x60, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF0, 0x07, // 1054 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 1055 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0x81, 0x01, 0x00, 0x80, 0x81, 0x01, 0x00, 0x80, 0x81, 0x01, 0x00, 0x80, 0x81, 0x01, 0x00, 0x80, 0x81, 0x01, 0x00, 0x80, 0x81, 0x01, 0x00, 0x80, 0x81, 0x01, 0x00, 0x80, 0x81, 0x01, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x3C, // 1056 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, 0x03, 0x60, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x00, 0x03, 0x60, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, 0x0E, 0x3C, 0x00, 0x00, 0x08, 0x0C, // 1057 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, // 1058 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, 0x1E, 0xC0, 0x00, 0x00, 0x78, 0xC0, 0x00, 0x00, 0xE0, 0xC1, 0x00, 0x00, 0x80, 0x77, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x80, 0x01, // 1059 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x06, 0x30, 0x00, 0x00, 0x0E, 0x38, 0x00, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x1C, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xE0, 0x03, // 1060 0x00, 0x00, 0x80, 0x00, 0x80, 0x00, 0xC0, 0x00, 0x80, 0x01, 0xF0, 0x00, 0x80, 0x07, 0x78, 0x00, 0x00, 0x0F, 0x1E, 0x00, 0x00, 0x3C, 0x0F, 0x00, 0x00, 0xF8, 0x07, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xF8, 0x07, 0x00, 0x00, 0x1E, 0x0F, 0x00, 0x00, 0x0F, 0x1C, 0x00, 0x80, 0x07, 0x78, 0x00, 0x80, 0x01, 0xF0, 0x00, 0x80, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x80, // 1061 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xC0, 0x1F, // 1062 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x7F, 0x00, 0x00, 0x80, 0xFF, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 1063 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 1064 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xC0, 0x1F, // 1065 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0x80, 0x61, 0x00, 0x00, 0x80, 0x7F, 0x00, 0x00, 0x00, 0x1E, // 1066 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0x80, 0x61, 0x00, 0x00, 0x80, 0x7F, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 1067 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0x80, 0x61, 0x00, 0x00, 0x80, 0x7F, 0x00, 0x00, 0x00, 0x1E, // 1068 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x08, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x0F, 0x78, 0x00, 0x00, 0x03, 0x60, 0x00, 0x80, 0x03, 0xE0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x80, 0xC1, 0xC0, 0x00, 0x00, 0xC3, 0x60, 0x00, 0x00, 0xC7, 0x70, 0x00, 0x00, 0xCE, 0x38, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF0, 0x07, // 1069 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xF0, 0x07, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, 0x03, 0x60, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x80, 0x01, 0xC0, 0x00, 0x00, 0x03, 0x60, 0x00, 0x00, 0x07, 0x70, 0x00, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0xFC, 0x1F, 0x00, 0x00, 0xF0, 0x07, // 1070 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x3E, 0xC0, 0x00, 0x00, 0x7F, 0xF0, 0x00, 0x80, 0x63, 0x7C, 0x00, 0x80, 0xC1, 0x1E, 0x00, 0x80, 0xC1, 0x0F, 0x00, 0x80, 0xC1, 0x03, 0x00, 0x80, 0xC1, 0x01, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xC1, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, // 1071 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x38, 0x00, 0x00, 0x70, 0x7C, 0x00, 0x00, 0x30, 0xE6, 0x00, 0x00, 0x18, 0xC6, 0x00, 0x00, 0x18, 0xC6, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0x18, 0x63, 0x00, 0x00, 0x38, 0x63, 0x00, 0x00, 0xF0, 0x7F, 0x00, 0x00, 0xE0, 0xFF, 0x00, 0x00, 0x00, 0x80, // 1072 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0x00, 0x73, 0x70, 0x00, 0x00, 0x31, 0xE0, 0x00, 0x80, 0x19, 0xC0, 0x00, 0x80, 0x19, 0xC0, 0x00, 0x80, 0x19, 0xC0, 0x00, 0x80, 0x19, 0xC0, 0x00, 0x80, 0x39, 0xE0, 0x00, 0x80, 0x71, 0x70, 0x00, 0x80, 0xE1, 0x3F, 0x00, 0x80, 0x80, 0x0F, // 1073 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0xF0, 0xE7, 0x00, 0x00, 0xF0, 0x7E, 0x00, 0x00, 0x00, 0x3C, // 1074 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, // 1075 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0xFC, 0x00, 0x00, 0xF8, 0xDF, 0x00, 0x00, 0xF8, 0xC3, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xC0, 0x0F, // 1076 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x70, 0x73, 0x00, 0x00, 0x38, 0xE3, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0x38, 0xE3, 0x00, 0x00, 0x70, 0x63, 0x00, 0x00, 0xE0, 0x33, 0x00, 0x00, 0xC0, 0x13, // 1077 0x00, 0x18, 0xE0, 0x00, 0x00, 0x18, 0x70, 0x00, 0x00, 0xF8, 0x38, 0x00, 0x00, 0xE0, 0x0D, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0xE0, 0x0D, 0x00, 0x00, 0xF8, 0x38, 0x00, 0x00, 0x18, 0x70, 0x00, 0x00, 0x18, 0xE0, 0x00, 0x00, 0x00, 0x80, // 1078 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x30, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0x38, 0xE7, 0x00, 0x00, 0xF0, 0x7F, 0x00, 0x00, 0xE0, 0x3C, // 1079 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, // 1080 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x80, 0x01, 0xF0, 0x00, 0x00, 0x03, 0x3C, 0x00, 0x00, 0x02, 0x0F, 0x00, 0x00, 0x82, 0x07, 0x00, 0x00, 0xE2, 0x01, 0x00, 0x00, 0x7B, 0x00, 0x00, 0x80, 0xF9, 0xFF, 0x00, 0x00, 0xF8, 0xFF, // 1081 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0xC0, 0x0D, 0x00, 0x00, 0xF0, 0x38, 0x00, 0x00, 0x18, 0x70, 0x00, 0x00, 0x18, 0xE0, 0x00, 0x00, 0x00, 0x80, // 1082 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, // 1083 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, // 1084 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, // 1085 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0xC0, 0x1F, // 1086 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, // 1087 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x1F, 0x00, 0xF8, 0xFF, 0x1F, 0x00, 0x60, 0x30, 0x00, 0x00, 0x30, 0x60, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0xC0, 0x0F, // 1088 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0x60, 0x30, // 1089 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, // 1090 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0xF8, 0x01, 0x18, 0x00, 0xC0, 0x07, 0x18, 0x00, 0x00, 0x3E, 0x1C, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x7F, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0x18, // 1091 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x70, 0x60, 0x00, 0x80, 0xFF, 0xFF, 0x1F, 0x80, 0xFF, 0xFF, 0x1F, 0x00, 0x70, 0x60, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xE0, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0xC0, 0x1F, // 1092 0x00, 0x08, 0x80, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x78, 0xF0, 0x00, 0x00, 0xE0, 0x38, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x80, 0x1F, 0x00, 0x00, 0xE0, 0x38, 0x00, 0x00, 0x70, 0xF0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x08, 0x80, // 1093 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x1F, 0x00, 0x00, 0xC0, 0x1F, // 1094 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x00, 0xF8, 0x07, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, // 1095 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, // 1096 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x1F, 0x00, 0x00, 0xC0, 0x1F, // 1097 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xE7, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x3C, // 1098 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xE7, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, // 1099 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0xE7, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x3C, // 1100 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x18, 0x00, 0x00, 0x70, 0x78, 0x00, 0x00, 0x38, 0x60, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0x18, 0xC3, 0x00, 0x00, 0x70, 0x73, 0x00, 0x00, 0xE0, 0x7F, 0x00, 0x00, 0xC0, 0x1F, // 1101 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0xC0, 0x1F, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x00, 0x38, 0xE0, 0x00, 0x00, 0x70, 0x70, 0x00, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0xC0, 0x1F, // 1102 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xC1, 0x00, 0x00, 0xF0, 0xE3, 0x00, 0x00, 0x38, 0x7B, 0x00, 0x00, 0x18, 0x1A, 0x00, 0x00, 0x18, 0x0E, 0x00, 0x00, 0x18, 0x06, 0x00, 0x00, 0x18, 0x06, 0x00, 0x00, 0x18, 0x06, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xF8, 0xFF, // 1103 }; #endif // OLED_UA ================================================ FILE: src/graphics/fonts/OLEDDisplayFontsUA.h ================================================ #ifndef OLEDDISPLAYFONTSUA_h #define OLEDDISPLAYFONTSUA_h #ifdef ARDUINO #include #elif __MBED__ #define PROGMEM #endif extern const uint8_t ArialMT_Plain_10_UA[] PROGMEM; extern const uint8_t ArialMT_Plain_16_UA[] PROGMEM; extern const uint8_t ArialMT_Plain_24_UA[] PROGMEM; #endif ================================================ FILE: src/graphics/images.h ================================================ #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}; #define imgSatellite_width 8 #define imgSatellite_height 8 const uint8_t imgSatellite[] PROGMEM = { 0b00000000, 0b00000000, 0b00000000, 0b00011000, 0b11011011, 0b11111111, 0b11011011, 0b00011000, }; const uint8_t imgUSB[] PROGMEM = {0x00, 0xfc, 0xf0, 0xfc, 0x88, 0xff, 0x86, 0xfe, 0x85, 0xfe, 0x89, 0xff, 0xf1, 0xfc, 0x00, 0xfc}; const uint8_t imgUSB_HighResolution[] PROGMEM = {0x00, 0x3e, 0xf8, 0x80, 0x43, 0xf8, 0xc0, 0xc2, 0xff, 0x60, 0x42, 0xfc, 0x3c, 0xc2, 0xff, 0x22, 0x42, 0xf8, 0x3d, 0x42, 0xf8, 0x22, 0xc2, 0xff, 0x61, 0x42, 0xfc, 0xc0, 0xc2, 0xff, 0x80, 0x43, 0xf8, 0x00, 0x3e, 0xf8}; const uint8_t imgPower[] PROGMEM = {0x40, 0x40, 0x40, 0x58, 0x48, 0x08, 0x08, 0x08, 0x1C, 0x22, 0x22, 0x41, 0x7F, 0x22, 0x22, 0x22}; const uint8_t 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}; 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}; #if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) || \ defined(USE_ST7796) || defined(HACKADAY_COMMUNICATOR) || ARCH_PORTDUINO) && \ !defined(DISPLAY_FORCE_SMALL_FONTS) const uint8_t imgQuestionL1[] PROGMEM = {0xff, 0x01, 0x01, 0x32, 0x7b, 0x49, 0x49, 0x6f, 0x26, 0x01, 0x01, 0xff}; const uint8_t imgQuestionL2[] PROGMEM = {0x0f, 0x08, 0x08, 0x08, 0x06, 0x0f, 0x0f, 0x06, 0x08, 0x08, 0x08, 0x0f}; const uint8_t imgInfoL1[] PROGMEM = {0xff, 0x01, 0x01, 0x01, 0x1e, 0x7f, 0x1e, 0x01, 0x01, 0x01, 0x01, 0xff}; const uint8_t imgInfoL2[] PROGMEM = {0x0f, 0x08, 0x08, 0x08, 0x06, 0x0f, 0x0f, 0x06, 0x08, 0x08, 0x08, 0x0f}; const uint8_t imgSFL1[] PROGMEM = {0xb6, 0x8f, 0x19, 0x11, 0x31, 0xe3, 0xc2, 0x01, 0x01, 0xf9, 0xf9, 0x89, 0x89, 0x89, 0x09, 0xeb}; const uint8_t imgSFL2[] PROGMEM = {0x0e, 0x09, 0x09, 0x09, 0x09, 0x09, 0x08, 0x08, 0x00, 0x0f, 0x0f, 0x00, 0x08, 0x08, 0x08, 0x0f}; #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 // === Horizontal battery === // Basic battery design and all related pieces const unsigned char batteryBitmap_h_bottom[] PROGMEM = { 0b00011110, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00011110, 0b00000000}; const unsigned char batteryBitmap_h_top[] PROGMEM = { 0b00111100, 0b00000000, 0b01000000, 0b00000000, 0b01000000, 0b00000000, 0b01000000, 0b00000000, 0b01000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b01000000, 0b00000000, 0b01000000, 0b00000000, 0b01000000, 0b00000000, 0b01000000, 0b00000000, 0b00111100, 0b00000000}; // Lightning Bolt const unsigned char lightning_bolt_h[] PROGMEM = { 0b00000000, 0b00000000, 0b00100000, 0b00000000, 0b00110000, 0b00000000, 0b00111000, 0b00000000, 0b00111100, 0b00000000, 0b00011110, 0b00000000, 0b11111111, 0b00000000, 0b01111000, 0b00000000, 0b00111100, 0b00000000, 0b00011100, 0b00000000, 0b00001100, 0b00000000, 0b00000100, 0b00000000, 0b00000000, 0b00000000}; // === Vertical battery === // Basic battery design and all related pieces const unsigned char batteryBitmap_v[] PROGMEM = {0b00011100, 0b00111110, 0b01000001, 0b01000001, 0b00000000, 0b00000000, 0b00000000, 0b01000001, 0b01000001, 0b01000001, 0b00111110}; // This is the left and right bars for the fill in const unsigned char batteryBitmap_sidegaps_v[] PROGMEM = {0b10000010, 0b10000010, 0b10000010}; // Lightning Bolt const unsigned char lightning_bolt_v[] PROGMEM = {0b00000100, 0b00000110, 0b00011111, 0b00001100, 0b00000100}; #define mail_width 10 #define mail_height 7 static const unsigned char mail[] PROGMEM = { 0b11111111, 0b00, // Top line 0b10000001, 0b00, // Edges 0b11000011, 0b00, // Diagonals start 0b10100101, 0b00, // Inner M part 0b10011001, 0b00, // Inner M part 0b10000001, 0b00, // Edges 0b11111111, 0b00 // Bottom line }; // Hop icon (9x10) #define hop_width 9 #define hop_height 10 const uint8_t hop[] PROGMEM = {0x05, 0x00, 0x07, 0x00, 0x05, 0x00, 0x38, 0x00, 0x28, 0x00, 0x38, 0x00, 0xC0, 0x01, 0x40, 0x01, 0xC0, 0x01, 0x40, 0x00}; // 📬 Mail / Message const uint8_t icon_mail[] PROGMEM = { 0b11111111, // ████████ top border 0b10000001, // █ █ sides 0b11000011, // ██ ██ diagonal 0b10100101, // █ █ █ █ inner M 0b10011001, // █ ██ █ inner M 0b10000001, // █ █ sides 0b10000001, // █ █ sides 0b11111111 // ████████ bottom }; // 📍 GPS Screen / Location Pin const unsigned char icon_compass[] PROGMEM = { 0x3C, // Row 0: ..####.. 0x52, // Row 1: .#..#.#. 0x91, // Row 2: #...#..# 0x91, // Row 3: #...#..# 0x91, // Row 4: #...#..# 0x81, // Row 5: #......# 0x42, // Row 6: .#....#. 0x3C // Row 7: ..####.. }; const uint8_t icon_radio[] PROGMEM = { 0x0F, // Row 0: ####.... 0x10, // Row 1: ....#... 0x27, // Row 2: ###..#.. 0x48, // Row 3: ...#..#. 0x93, // Row 4: ##..#..# 0xA4, // Row 5: ..#..#.# 0xA8, // Row 6: ...#.#.# 0xA9 // Row 7: #..#.#.# }; // 🪙 System Icon const uint8_t icon_system[] PROGMEM = { 0x24, // Row 0: ..#..#.. 0x3C, // Row 1: ..####.. 0xC3, // Row 2: ##....## 0x5A, // Row 3: .#.##.#. 0x5A, // Row 4: .#.##.#. 0xC3, // Row 5: ##....## 0x3C, // Row 6: ..####.. 0x24 // Row 7: ..#..#.. }; // 🌐 Wi-Fi const uint8_t icon_wifi[] PROGMEM = {0b00000000, 0b00011000, 0b00111100, 0b01111110, 0b11011011, 0b00011000, 0b00011000, 0b00000000}; const uint8_t icon_nodes[] PROGMEM = { 0xF9, // Row 0 #..####### 0x00, // Row 1 0xF9, // Row 2 #..####### 0x00, // Row 3 0xF9, // Row 4 #..####### 0x00, // Row 5 0xF9, // Row 6 #..####### 0x00 // Row 7 }; // ➤ Chevron Triangle Arrow Icon (8x8) const uint8_t icon_list[] PROGMEM = { 0x10, // Row 0: ...#.... 0x10, // Row 1: ...#.... 0x38, // Row 2: ..###... 0x38, // Row 3: ..###... 0x7C, // Row 4: .#####.. 0x6C, // Row 5: .##.##.. 0xC6, // Row 6: ##...##. 0x82 // Row 7: #.....#. }; // 📶 Signal Bars Icon (left to right, small to large with spacing) const uint8_t icon_signal[] PROGMEM = { 0b00000000, // ░░░░░░░ 0b10000000, // ░░░░░░░ 0b10100000, // ░░░░█░█ 0b10100000, // ░░░░█░█ 0b10101000, // ░░█░█░█ 0b10101000, // ░░█░█░█ 0b10101010, // █░█░█░█ 0b11111111 // ███████ }; // ↔️ Distance / Measurement Icon (double-ended arrow) const uint8_t icon_distance[] PROGMEM = { 0b00000000, // ░░░░░░░░ 0b10000001, // █░░░░░█ arrowheads 0b01000010, // ░█░░░█░ 0b00100100, // ░░█░█░░ 0b00011000, // ░░░██░░ center 0b00100100, // ░░█░█░░ 0b01000010, // ░█░░░█░ 0b10000001 // █░░░░░█ }; // ⚠️ Error / Fault const uint8_t icon_error[] PROGMEM = { 0b00011000, // ░░░██░░░ 0b00011000, // ░░░██░░░ 0b00011000, // ░░░██░░░ 0b00011000, // ░░░██░░░ 0b00000000, // ░░░░░░░░ 0b00011000, // ░░░██░░░ 0b00000000, // ░░░░░░░░ 0b00000000 // ░░░░░░░░ }; // 🏠 Optimized Home Icon (8x8) const uint8_t icon_home[] PROGMEM = { 0b00011000, // ██ 0b00111100, // ████ 0b01111110, // ██████ 0b11111111, // ███████ 0b11000011, // ██ ██ 0b11011011, // ██ ██ ██ 0b11011011, // ██ ██ ██ 0b11111111 // ███████ }; // 🔧 Generic module (gear-like shape) const uint8_t icon_module[] PROGMEM = { 0b00011000, // ░░░██░░░ 0b00111100, // ░░████░░ 0b01111110, // ░██████░ 0b11011011, // ██░██░██ 0b11011011, // ██░██░██ 0b01111110, // ░██████░ 0b00111100, // ░░████░░ 0b00011000 // ░░░██░░░ }; #define mute_symbol_width 8 #define mute_symbol_height 8 const uint8_t mute_symbol[] PROGMEM = { 0b00011001, // █ 0b00100110, // █ 0b00100100, // ████ 0b01001010, // █ █ █ 0b01010010, // █ █ █ 0b01100010, // ████████ 0b11111111, // █ █ 0b10011000, // █ }; #define mute_symbol_big_width 16 #define mute_symbol_big_height 16 const uint8_t mute_symbol_big[] PROGMEM = {0b00000001, 0b00000000, 0b11000010, 0b00000011, 0b00110100, 0b00001100, 0b00011000, 0b00001000, 0b00011000, 0b00010000, 0b00101000, 0b00010000, 0b01001000, 0b00010000, 0b10001000, 0b00010000, 0b00001000, 0b00010001, 0b00001000, 0b00010010, 0b00001000, 0b00010100, 0b00000100, 0b00101000, 0b11111100, 0b00111111, 0b01000000, 0b00100010, 0b10000000, 0b01000001, 0b00000000, 0b10000000}; // Bell icon for Alert Message #define bell_alert_width 8 #define bell_alert_height 8 const unsigned char bell_alert[] PROGMEM = {0b00011000, 0b00100100, 0b00100100, 0b01000010, 0b01000010, 0b01000010, 0b11111111, 0b00011000}; #define key_symbol_width 8 #define key_symbol_height 8 const uint8_t key_symbol[] PROGMEM = {0b00000000, 0b00000000, 0b00000110, 0b11111001, 0b10101001, 0b10000110, 0b00000000, 0b00000000}; #define placeholder_width 8 #define placeholder_height 8 const uint8_t placeholder[] PROGMEM = {0b11111111, 0b11111111, 0b11111111, 0b11111111, 0b11111111, 0b11111111, 0b11111111, 0b11111111}; #define icon_node_width 8 #define icon_node_height 8 static const uint8_t icon_node[] PROGMEM = { 0x10, // # 0x10, // # ← antenna 0x10, // # 0xFE, // ####### ← device top 0x82, // # # 0xAA, // # # # # ← body with pattern 0x92, // # # # 0xFE // ####### ← device base }; #define bluetoothdisabled_width 8 #define bluetoothdisabled_height 8 const uint8_t bluetoothdisabled[] PROGMEM = {0b11101100, 0b01010100, 0b01001100, 0b01010100, 0b01001100, 0b00000000, 0b00000000, 0b00000000}; #define smallbulletpoint_width 8 #define smallbulletpoint_height 8 const uint8_t smallbulletpoint[] PROGMEM = {0b00000011, 0b00000011, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000}; // Digital Clock #define digital_icon_clock_width 8 #define digital_icon_clock_height 8 const uint8_t digital_icon_clock[] PROGMEM = {0b00111100, 0b01000010, 0b10000101, 0b10101001, 0b10010001, 0b10000001, 0b01000010, 0b00111100}; // Analog Clock #define analog_icon_clock_width 8 #define analog_icon_clock_height 8 const uint8_t analog_icon_clock[] PROGMEM = {0b11111111, 0b01000010, 0b00100100, 0b00011000, 0b00100100, 0b01000010, 0b01000010, 0b11111111}; #define chirpy_width 38 #define chirpy_height 50 const uint8_t chirpy[] = { 0xfe, 0xff, 0xff, 0xff, 0xdf, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x80, 0xe3, 0x01, 0x00, 0x00, 0xc0, 0xe7, 0x01, 0x00, 0x00, 0xc0, 0xe7, 0x01, 0x00, 0x00, 0xc0, 0xe7, 0x01, 0x00, 0x00, 0x80, 0xe3, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x81, 0xff, 0xff, 0x7f, 0xe0, 0xc1, 0xff, 0xff, 0xff, 0xe0, 0xc1, 0xff, 0xff, 0xff, 0xe0, 0xc1, 0xcf, 0x7f, 0xfe, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0xcf, 0x7f, 0xfe, 0xe0, 0xc1, 0xff, 0xff, 0xff, 0xe0, 0xc1, 0xff, 0xff, 0xff, 0xe0, 0x81, 0xff, 0xff, 0x7f, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0xc3, 0x00, 0xe0, 0x01, 0x00, 0xc3, 0x00, 0xe0, 0x01, 0x80, 0xe1, 0x01, 0xe0, 0x01, 0x80, 0xe1, 0x01, 0xe0, 0x01, 0xc0, 0x30, 0x03, 0xe0, 0x01, 0xc0, 0x30, 0x03, 0xe0, 0x01, 0x60, 0x18, 0x06, 0xe0, 0x01, 0x60, 0x18, 0x06, 0xe0, 0x01, 0x30, 0x0c, 0x0c, 0xe0, 0x01, 0x30, 0x0c, 0x0c, 0xe0, 0x01, 0x18, 0x06, 0x18, 0xe0, 0x01, 0x18, 0x06, 0x18, 0xe0, 0x01, 0x0c, 0x03, 0x30, 0xe0, 0x01, 0x0c, 0x03, 0x30, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0xfe, 0xff, 0xff, 0xff, 0xdf}; #define chirpy_small_image_width 8 #define chirpy_small_image_height 8 const uint8_t chirpy_small[] = {0x7f, 0x41, 0x55, 0x55, 0x55, 0x55, 0x41, 0x7f}; #define connection_icon_width 7 #define connection_icon_height 5 const uint8_t connection_icon[] = {0x36, 0x41, 0x5D, 0x41, 0x36}; #ifdef M5STACK_UNITC6L #include "img/icon_small.xbm" #else #include "img/icon.xbm" #endif static_assert(sizeof(icon_bits) >= 0, "Silence unused variable warning"); ================================================ FILE: src/graphics/img/icon.xbm ================================================ #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 ================================================ FILE: src/graphics/img/icon_small.xbm ================================================ #ifndef USERPREFS_HAS_SPLASH #define icon_width 50 #define icon_height 20 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, 0x0f, 0xc0, 0x0f, 0x00, 0x00, 0x00, 0xe0, 0x07, 0xe0, 0x0f, 0x00, 0x00, 0x00, 0xe0, 0x07, 0xf0, 0x1f, 0x00, 0x00, 0x00, 0xf0, 0x03, 0xf0, 0x3f, 0x00, 0x00, 0x00, 0xf8, 0x03, 0xf8, 0x7f, 0x00, 0x00, 0x00, 0xf8, 0x01, 0xfc, 0x7e, 0x00, 0x00, 0x00, 0xfc, 0x00, 0xfc, 0xfc, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x7e, 0xf8, 0x00, 0x00, 0x00, 0x7e, 0x00, 0x3f, 0xf8, 0x01, 0x00, 0x00, 0x3f, 0x00, 0x1f, 0xf0, 0x01, 0x00, 0x00, 0x1f, 0x80, 0x1f, 0xe0, 0x03, 0x00, 0x80, 0x1f, 0xc0, 0x0f, 0xe0, 0x03, 0x00, 0x80, 0x0f, 0xc0, 0x07, 0xc0, 0x07, 0x00, 0xc0, 0x0f, 0xe0, 0x07, 0x80, 0x0f, 0x00, 0xe0, 0x07, 0xf0, 0x03, 0x80, 0x1f, 0x00, 0xe0, 0x03, 0xf8, 0x03, 0x00, 0x1f, 0x00, 0xf0, 0x03, 0xf8, 0x01, 0x00, 0x3f, 0x00, 0xf8, 0x01, 0xfc, 0x00, 0x00, 0x7e, 0x00, 0xfc, 0x00, 0xfe, 0x00, 0x00, 0x7e, 0x00, 0xfc, 0x00, 0x7e, 0x00, 0x00, 0xfc, 0x00, 0x7e, 0x00, 0x3f, 0x00, 0x00, 0xf8, 0x00, 0x7e, 0x00, 0x3e, 0x00, 0x00, 0xf8, 0x00, 0x38, 0x00, 0x1c, 0x00, 0x00, 0x70, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; #endif ================================================ FILE: src/graphics/niche/Drivers/Backlight/LatchingBacklight.cpp ================================================ #ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS #include "./LatchingBacklight.h" #include "assert.h" #include "sleep.h" using namespace NicheGraphics::Drivers; // Private constructor // Called by getInstance LatchingBacklight::LatchingBacklight() { // Attach the deep sleep callback deepSleepObserver.observe(¬ifyDeepSleep); } // Get access to (or create) the singleton instance of this class LatchingBacklight *LatchingBacklight::getInstance() { // Instantiate the class the first time this method is called static LatchingBacklight *const singletonInstance = new LatchingBacklight; return singletonInstance; } // Which pin controls the backlight? // Is the light active HIGH (default) or active LOW? void LatchingBacklight::setPin(uint8_t pin, bool activeWhen) { this->pin = pin; this->logicActive = activeWhen; pinMode(pin, OUTPUT); off(); // Explicit off seem required by T-Echo? } // Called when device is shutting down // Ensures the backlight is off int LatchingBacklight::beforeDeepSleep(void *unused) { // Contingency only // - pin wasn't set if (pin != static_cast(-1)) { off(); pinMode(pin, INPUT); // High impedance - unnecessary? } else LOG_WARN("LatchingBacklight instantiated, but pin not set"); return 0; // Continue with deep sleep } // Turn the backlight on *temporarily* // This should be used for momentary illumination, such as while a button is held // The effect on the backlight is the same; peek and latch are separated to simplify short vs long press button handling void LatchingBacklight::peek() { assert(pin != static_cast(-1)); digitalWrite(pin, logicActive); // On on = true; latched = false; } // Turn the backlight on, and keep it on // This should be used when the backlight should remain active, even after user input ends // e.g. when enabled via the menu // The effect on the backlight is the same; peek and latch are separated to simplify short vs long press button handling void LatchingBacklight::latch() { assert(pin != static_cast(-1)); // Blink if moving from peek to latch // Indicates to user that the transition has taken place if (on && !latched) { digitalWrite(pin, !logicActive); // Off delay(25); digitalWrite(pin, logicActive); // On delay(25); digitalWrite(pin, !logicActive); // Off delay(25); } digitalWrite(pin, logicActive); // On on = true; latched = true; } // Turn the backlight off // Suitable for ending both peek and latch void LatchingBacklight::off() { assert(pin != static_cast(-1)); digitalWrite(pin, !logicActive); // Off on = false; latched = false; } bool LatchingBacklight::isOn() { return on; } bool LatchingBacklight::isLatched() { return latched; } #endif ================================================ FILE: src/graphics/niche/Drivers/Backlight/LatchingBacklight.h ================================================ /* Singleton class On-demand control of a display's backlight, connected to a GPIO Initial use case is control of T-Echo's frontlight, via the capacitive touch button - momentary on - latched on */ #pragma once #include "configuration.h" #include "Observer.h" namespace NicheGraphics::Drivers { class LatchingBacklight { public: static LatchingBacklight *getInstance(); // Create or get the singleton instance void setPin(uint8_t pin, bool activeWhen = HIGH); int beforeDeepSleep(void *unused); // Callback for auto-shutoff void peek(); // Backlight on temporarily, e.g. while button held void latch(); // Backlight on permanently, e.g. toggled via menu void off(); // Backlight off. Suitable for both peek and latch bool isOn(); // Either peek or latch bool isLatched(); private: LatchingBacklight(); // Constructor made private: force use of getInstance // Get notified when the system is shutting down CallbackObserver deepSleepObserver = CallbackObserver(this, &LatchingBacklight::beforeDeepSleep); uint8_t pin = static_cast(-1); bool logicActive = HIGH; // Is light active HIGH or active LOW bool on = false; // Is light on (either peek or latched) bool latched = false; // Is light latched on }; } // namespace NicheGraphics::Drivers ================================================ FILE: src/graphics/niche/Drivers/EInk/DEPG0213BNS800.cpp ================================================ #include "./DEPG0213BNS800.h" #ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS using namespace NicheGraphics::Drivers; // Describes the operation performed when a "fast refresh" is performed // Source: Modified from GxEPD2 (GxEPD2_213_BN) static const uint8_t LUT_FAST[] = { // 1 2 3 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // B2B (Existing black pixels) 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // B2W (New white pixels) 0x40, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // W2B (New black pixels) 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // W2W (Existing white pixels) 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // VCOM 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, // 1. Any pixels changing W2B or B2W. Two medium taps. 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 2. All pixels. One short tap. 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 3. Cooldown 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x00, 0x00, 0x00, // }; // How strongly the pixels are pulled and pushed void DEPG0213BNS800::configVoltages() { switch (updateType) { case FAST: // Reference: display datasheet, GxEPD1 sendCommand(0x03); // Gate voltage sendData(0x17); // VGH: 20V // Reference: display datasheet, GxEPD1 sendCommand(0x04); // Source voltage sendData(0x41); // VSH1: 15V sendData(0x00); // VSH2: NA sendData(0x32); // VSL: -15V // GxEPD1 sets this at -1.2V, but that seems to be drive the pixels very hard sendCommand(0x2C); // VCOM voltage sendData(0x08); // VCOM: -0.2V break; case FULL: default: // From OTP memory break; } } // Load settings about how the pixels are moved from old state to new state during a refresh // - manually specified, // - or with stored values from displays OTP memory void DEPG0213BNS800::configWaveform() { switch (updateType) { case FAST: sendCommand(0x3C); // Border waveform: sendData(0x80); // VSS sendCommand(0x32); // Write LUT register from MCU: sendData(LUT_FAST, sizeof(LUT_FAST)); // (describes operation for a FAST refresh) break; case FULL: default: // From OTP memory break; } } // Describes the sequence of events performed by the displays controller IC during a refresh // Includes "power up", "load settings from memory", "update the pixels", etc void DEPG0213BNS800::configUpdateSequence() { switch (updateType) { case FAST: sendCommand(0x22); // Set "update sequence" sendData(0xCF); // Differential, use manually loaded waveform break; case FULL: default: sendCommand(0x22); // Set "update sequence" sendData(0xF7); // Non-differential, load waveform from OTP break; } } // Once the refresh operation has been started, // begin periodically polling the display to check for completion, using the normal Meshtastic threading code // Only used when refresh is "async" void DEPG0213BNS800::detachFromUpdate() { switch (updateType) { case FAST: return beginPolling(50, 500); // At least 500ms, then poll every 50ms case FULL: default: return beginPolling(100, 3500); // At least 3500ms, then poll every 100ms } } // For this display, we do not need to re-write the new image. // We're overriding SSD16XX::finalizeUpdate to make this small optimization. // The display does also work just fine with the generic SSD16XX method, though. void DEPG0213BNS800::finalizeUpdate() { // Put a copy of the image into the "old memory". // Used with differential refreshes (e.g. FAST update), to determine which px need to move, and which can remain in place // We need to keep the "old memory" up to date, because don't know whether next refresh will be FULL or FAST etc. if (updateType != FULL) { // writeNewImage(); // Not required for this display writeOldImage(); sendCommand(0x7F); // Terminate image write without update wait(); } // Enter deep-sleep to save a few µA // Waking from this requires that display's reset pin is broken out if (pin_rst != 0xFF) deepSleep(); } #endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS ================================================ FILE: src/graphics/niche/Drivers/EInk/DEPG0213BNS800.h ================================================ /* E-Ink display driver - DEPG0213BNS800 - Manufacturer: DKE - Size: 2.13 inch - Resolution: 122px x 250px - Flex connector marking (not a unique identifier): FPC-7528B Note: this is from an older generation of DKE panels, which still used Solomon Systech controller ICs. DKE's website suggests that the latest DEPG0213BN displays may use Fitipower controllers instead. */ #pragma once #ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS #include "configuration.h" #include "./SSD16XX.h" namespace NicheGraphics::Drivers { class DEPG0213BNS800 : public SSD16XX { // Display properties private: static constexpr uint32_t width = 122; static constexpr uint32_t height = 250; static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST); public: DEPG0213BNS800() : SSD16XX(width, height, supported, 1) {} // Note: left edge of this display is offset by 1 byte protected: void configVoltages() override; void configWaveform() override; void configUpdateSequence() override; void detachFromUpdate() override; void finalizeUpdate() override; // Only overridden for a slight optimization }; } // namespace NicheGraphics::Drivers #endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS ================================================ FILE: src/graphics/niche/Drivers/EInk/DEPG0290BNS800.cpp ================================================ #include "./DEPG0290BNS800.h" #ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS using namespace NicheGraphics::Drivers; // Describes the operation performed when a "fast refresh" is performed // Source: custom, with DEPG0150BNS810 as a reference static const uint8_t LUT_FAST[] = { // 1 2 3 4 0x40, 0x00, 0x40, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // B2B (Existing black pixels) 0x00, 0x80, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // B2W (New white pixels) 0x00, 0x40, 0x40, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // W2B (New black pixels) 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // W2W (Existing white pixels) 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // VCOM 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 1. Tap existing black pixels back into place 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 2. Move new pixels 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 3. New pixels, and also existing black pixels 0x02, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, // 4. All pixels, then cooldown 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x00, 0x00, 0x00, }; // How strongly the pixels are pulled and pushed void DEPG0290BNS800::configVoltages() { switch (updateType) { case FAST: // Listed as "typical" in datasheet sendCommand(0x04); sendData(0x41); // VSH1 15V sendData(0x00); // VSH2 NA sendData(0x32); // VSL -15V break; case FULL: default: // From OTP memory break; } } // Load settings about how the pixels are moved from old state to new state during a refresh // - manually specified, // - or with stored values from displays OTP memory void DEPG0290BNS800::configWaveform() { switch (updateType) { case FAST: sendCommand(0x3C); // Border waveform: sendData(0x60); // Actively hold screen border during update sendCommand(0x32); // Write LUT register from MCU: sendData(LUT_FAST, sizeof(LUT_FAST)); // (describes operation for a FAST refresh) break; case FULL: default: // From OTP memory break; } } // Describes the sequence of events performed by the displays controller IC during a refresh // Includes "power up", "load settings from memory", "update the pixels", etc void DEPG0290BNS800::configUpdateSequence() { switch (updateType) { case FAST: sendCommand(0x22); // Set "update sequence" sendData(0xCF); // Differential, use manually loaded waveform break; case FULL: default: sendCommand(0x22); // Set "update sequence" sendData(0xF7); // Non-differential, load waveform from OTP break; } } // Once the refresh operation has been started, // begin periodically polling the display to check for completion, using the normal Meshtastic threading code // Only used when refresh is "async" void DEPG0290BNS800::detachFromUpdate() { switch (updateType) { case FAST: return beginPolling(50, 450); // At least 450ms for fast refresh case FULL: default: return beginPolling(100, 3000); // At least 3 seconds for full refresh } } // For this display, we do not need to re-write the new image. // We're overriding SSD16XX::finalizeUpdate to make this small optimization. // The display does also work just fine with the generic SSD16XX method, though. void DEPG0290BNS800::finalizeUpdate() { // Put a copy of the image into the "old memory". // Used with differential refreshes (e.g. FAST update), to determine which px need to move, and which can remain in place // We need to keep the "old memory" up to date, because don't know whether next refresh will be FULL or FAST etc. if (updateType != FULL) { // writeNewImage(); // Not required for this display writeOldImage(); sendCommand(0x7F); // Terminate image write without update wait(); } // Enter deep-sleep to save a few µA // Waking from this requires that display's reset pin is broken out if (pin_rst != 0xFF) deepSleep(); } #endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS ================================================ FILE: src/graphics/niche/Drivers/EInk/DEPG0290BNS800.h ================================================ /* E-Ink display driver - DEPG0290BNS800 - Manufacturer: DKE - Size: 2.9 inch - Resolution: 128px x 296px - Flex connector marking (not a unique identifier): FPC-7519 rev.b */ #pragma once #ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS #include "configuration.h" #include "./SSD16XX.h" namespace NicheGraphics::Drivers { class DEPG0290BNS800 : public SSD16XX { // Display properties private: static constexpr uint32_t width = 128; static constexpr uint32_t height = 296; static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST); public: DEPG0290BNS800() : SSD16XX(width, height, supported, 1) {} // Note: left edge of this display is offset by 1 byte protected: void configVoltages() override; void configWaveform() override; void configUpdateSequence() override; void detachFromUpdate() override; void finalizeUpdate() override; // Only overridden for a slight optimization }; } // namespace NicheGraphics::Drivers #endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS ================================================ FILE: src/graphics/niche/Drivers/EInk/E0213A367.cpp ================================================ #include "./E0213A367.h" #ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS using namespace NicheGraphics::Drivers; // Map the display controller IC's output to the connected panel void E0213A367::configScanning() { // "Driver output control" // Scan gates from 0 to 249 (vertical resolution 250px) sendCommand(0x01); sendData(0xF9); sendData(0x00); } // Specify which information is used to control the sequence of voltages applied to move the pixels void E0213A367::configWaveform() { // This command (0x37) is poorly documented // As of July 2025, the datasheet for this display's controller IC is unavailable // The values are supplied by Heltec, who presumably have privileged access to information from the display manufacturer // Datasheet for the similar SSD1680 IC hints at the function of this command: // "Spare VCOM OTP selection": // Unclear why 0x40 is set. Sane values for related SSD1680 seem to be 0x80 or 0x00. // Maybe value is redundant? No noticeable impact when set to 0x00. // We'll leave it set to 0x40, following Heltec's lead, just in case. // "Display Mode" // Seems to specify whether a waveform stored in OTP should use display mode 1 or 2 (full refresh or differential refresh) // Unusual that waveforms are programmed to OTP, but this meta information is not ..? sendCommand(0x37); // "Write Register for Display Option" ? sendData(0x40); // "Spare VCOM OTP selection" ? sendData(0x80); // "Display Mode for WS[7:0]" ? sendData(0x03); // "Display Mode for WS[15:8]" ? sendData(0x0E); // "Display Mode [23:16]" ? switch (updateType) { case FAST: sendCommand(0x3C); // Border waveform: sendData(0x81); // As specified by Heltec. Actually VCOM (0x80)?. Bit 0 seems redundant here. break; case FULL: default: sendCommand(0x3C); // Border waveform: sendData(0x01); // Follow LUT 1 (blink same as white pixels) break; } } // Tell controller IC which operations to run void E0213A367::configUpdateSequence() { switch (updateType) { case FAST: sendCommand(0x22); // Set "update sequence" sendData(0xFF); // Will load LUT from OTP memory, Display mode 2 "differential refresh" break; case FULL: default: sendCommand(0x22); // Set "update sequence" sendData(0xF7); // Will load LUT from OTP memory, Display mode 1 "full refresh" break; } } // Once the refresh operation has been started, // begin periodically polling the display to check for completion, using the normal Meshtastic threading code // Only used when refresh is "async" void E0213A367::detachFromUpdate() { switch (updateType) { case FAST: return beginPolling(50, 500); // At least 500ms for fast refresh case FULL: default: return beginPolling(100, 1500); // At least 1.5 seconds for full refresh } } #endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS ================================================ FILE: src/graphics/niche/Drivers/EInk/E0213A367.h ================================================ /* E-Ink display driver - SSD1682 - Manufacturer: SEEKINK - Size: 2.13 inch - Resolution: 122px x 255px - Flex connector marking: HINK-E0213A162-A1 (hidden, printed on reverse) */ #pragma once #ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS #include "configuration.h" #include "./SSD1682.h" namespace NicheGraphics::Drivers { class E0213A367 : public SSD1682 { // Display properties private: static constexpr uint32_t width = 122; static constexpr uint32_t height = 250; static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST); public: E0213A367() : SSD1682(width, height, supported, 0) {} protected: void configScanning() override; void configWaveform() override; void configUpdateSequence() override; void detachFromUpdate() override; }; } // namespace NicheGraphics::Drivers #endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS ================================================ FILE: src/graphics/niche/Drivers/EInk/EInk.cpp ================================================ #include "./EInk.h" #ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS using namespace NicheGraphics::Drivers; // Separate from EInk::begin method, as derived class constructors can probably supply these parameters as constants EInk::EInk(uint16_t width, uint16_t height, UpdateTypes supported) : concurrency::OSThread("EInkDriver"), width(width), height(height), supportedUpdateTypes(supported) { OSThread::disable(); } // Used by NicheGraphics implementations to check if a display supports a specific refresh operation. // Whether or not the update type is supported is specified in the constructor bool EInk::supports(UpdateTypes type) { // The EInkUpdateTypes enum assigns each type a unique bit. We are checking if that bit is set. if (supportedUpdateTypes & type) return true; else return false; } // Begins using the OSThread to detect when a display update is complete // This allows the refresh operation to run "asynchronously". // Rather than blocking execution waiting for the update to complete, we are periodically checking the hardware's BUSY pin // The expectedDuration argument allows us to delay the start of this checking, if we know "roughly" how long an update takes. // Potentially, a display without hardware BUSY could rely entirely on "expectedDuration", // provided its isUpdateDone() override always returns true. void EInk::beginPolling(uint32_t interval, uint32_t expectedDuration) { updateRunning = true; pollingInterval = interval; pollingBegunAt = millis(); // To minimize load, we can choose to delay polling for a few seconds, if we know roughly how long the update will take // By default, expectedDuration is 0, and we'll start polling immediately OSThread::setIntervalFromNow(expectedDuration); OSThread::enabled = true; } // Meshtastic's pseudo-threading layer // We're using this as a timer, to periodically check if an update is complete // This is what allows us to update the display asynchronously int32_t EInk::runOnce() { // Check for polling timeout // Manually set at 10 seconds, in case some big task holds up the firmware's cooperative multitasking if (millis() - pollingBegunAt > 10000) failed = true; // Handle failure // - polling timeout // - other error (derived classes) if (failed) { LOG_WARN("Display update failed. Check wiring & power supply."); updateRunning = false; failed = false; return disable(); } // If update not yet done if (!isUpdateDone()) return pollingInterval; // Poll again in a few ms // If update done finalizeUpdate(); // Any post-update code: power down panel hardware, hibernate, etc updateRunning = false; // Change what we report via EInk::busy() return disable(); // Stop polling } // Wait for an in progress update to complete before continuing // Run a normal (async) update first, *then* call await void EInk::await() { // Stop our concurrency thread OSThread::disable(); // Sit and block until the update is complete while (updateRunning) { runOnce(); yield(); } } #endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS ================================================ FILE: src/graphics/niche/Drivers/EInk/EInk.h ================================================ /* Base class for E-Ink display drivers */ #pragma once #ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS #include "configuration.h" #include "concurrency/OSThread.h" #include namespace NicheGraphics::Drivers { class EInk : private concurrency::OSThread { public: // Different possible operations used to update an E-Ink display // Some displays will not support all operations // Each value needs a unique bit. In some cases, we might set more than one bit (e.g. EInk::supportedUpdateType) enum UpdateTypes : uint8_t { UNSPECIFIED = 0, FULL = 1 << 0, FAST = 1 << 1, // "Partial Refresh" }; EInk(uint16_t width, uint16_t height, UpdateTypes supported); virtual void begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst = -1) = 0; virtual void update(uint8_t *imageData, UpdateTypes type) = 0; // Change the display image void await(); // Wait for an in-progress update to complete before proceeding bool supports(UpdateTypes type); // Can display perform a certain update type bool busy() { return updateRunning; } // Display able to update right now? const uint16_t width; // Public so that NicheGraphics implementations can access. Safe because const. const uint16_t height; protected: void beginPolling(uint32_t interval, uint32_t expectedDuration); // Begin checking repeatedly if update finished virtual bool isUpdateDone() = 0; // Check once if update finished virtual void finalizeUpdate() {} // Run any post-update code bool failed = false; // If an error occurred during update private: int32_t runOnce() override; // Repeated checking if update finished const UpdateTypes supportedUpdateTypes; // Capabilities of a derived display class bool updateRunning = false; // see EInk::busy() uint32_t pollingInterval = 0; // How often to check if update complete (ms) uint32_t pollingBegunAt = 0; // To timeout during polling }; } // namespace NicheGraphics::Drivers #endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS ================================================ FILE: src/graphics/niche/Drivers/EInk/GDEW0102T4.cpp ================================================ #ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS #include "./GDEW0102T4.h" #include using namespace NicheGraphics::Drivers; // LUTs from GxEPD2_102.cpp (GDEW0102T4 / UC8175). static const uint8_t LUT_W_FULL[] = { 0x60, 0x5A, 0x5A, 0x00, 0x00, 0x01, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // }; static const uint8_t LUT_B_FULL[] = { 0x90, 0x5A, 0x5A, 0x00, 0x00, 0x01, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // }; static const uint8_t LUT_W_FAST[] = { 0x60, 0x01, 0x01, 0x00, 0x00, 0x01, // 0x80, 0x12, 0x00, 0x00, 0x00, 0x01, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // }; static const uint8_t LUT_B_FAST[] = { 0x90, 0x01, 0x01, 0x00, 0x00, 0x01, // 0x40, 0x14, 0x00, 0x00, 0x00, 0x01, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // }; GDEW0102T4::GDEW0102T4() : UC8175(width, height, supported) {} void GDEW0102T4::setFastConfig(FastConfig cfg) { // Clamp out only clearly invalid PLL settings. if (cfg.reg30 < 0x05) cfg.reg30 = 0x05; fastConfig = cfg; } GDEW0102T4::FastConfig GDEW0102T4::getFastConfig() const { return fastConfig; } void GDEW0102T4::configCommon() { // Init path aligned with GxEPD2_GDEW0102T4 (UC8175 family). sendCommand(0xD2); sendData(0x3F); sendCommand(0x00); sendData(0x6F); sendCommand(0x01); sendData(0x03); sendData(0x00); sendData(0x2B); sendData(0x2B); sendCommand(0x06); sendData(0x3F); sendCommand(0x2A); sendData(0x00); sendData(0x00); sendCommand(0x30); // PLL / drive clock sendData(0x13); sendCommand(0x50); // Last border/data interval; subtle but can affect artifacts sendData(0x57); sendCommand(0x60); sendData(0x22); sendCommand(0x61); sendData(width); sendData(height); sendCommand(0x82); // VCOM DC setting sendData(0x12); sendCommand(0xE3); sendData(0x33); } void GDEW0102T4::configFull() { sendCommand(0x23); sendData(LUT_W_FULL, sizeof(LUT_W_FULL)); sendCommand(0x24); sendData(LUT_B_FULL, sizeof(LUT_B_FULL)); powerOn(); } void GDEW0102T4::configFast() { uint8_t lutW[sizeof(LUT_W_FAST)]; uint8_t lutB[sizeof(LUT_B_FAST)]; memcpy(lutW, LUT_W_FAST, sizeof(LUT_W_FAST)); memcpy(lutB, LUT_B_FAST, sizeof(LUT_B_FAST)); // Second stage duration bytes are the main "darkness vs ghosting" control for this panel. lutW[7] = fastConfig.lutW2; lutB[7] = fastConfig.lutB2; sendCommand(0x30); sendData(fastConfig.reg30); sendCommand(0x50); sendData(fastConfig.reg50); sendCommand(0x82); sendData(fastConfig.reg82); sendCommand(0x23); sendData(lutW, sizeof(lutW)); sendCommand(0x24); sendData(lutB, sizeof(lutB)); powerOn(); } void GDEW0102T4::writeOldImage() { // On this panel, FULL refresh is most reliable when "old image" is all white. if (updateType == FULL) { sendCommand(0x10); // Use buffered writes of 0xFF to avoid per-byte SPI transactions. const uint16_t chunkSize = 64; uint8_t ffBuf[chunkSize]; memset(ffBuf, 0xFF, sizeof(ffBuf)); uint32_t remaining = bufferSize; while (remaining > 0) { uint16_t toSend = remaining > chunkSize ? chunkSize : static_cast(remaining); sendData(ffBuf, toSend); remaining -= toSend; } return; } // FAST refresh uses differential data (previous frame as old image). if (previousBuffer) { writeImage(0x10, previousBuffer); } else { writeImage(0x10, buffer); } } void GDEW0102T4::finalizeUpdate() { // Keep panel out of deep-sleep between updates for better reliability of repeated FAST refresh. powerOff(); } #endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS ================================================ FILE: src/graphics/niche/Drivers/EInk/GDEW0102T4.h ================================================ /* E-Ink display driver - GDEW0102T4 - Controller: UC8175 - Size: 1.02 inch - Resolution: 80px x 128px */ #pragma once #ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS #include "configuration.h" #include "./UC8175.h" namespace NicheGraphics::Drivers { class GDEW0102T4 : public UC8175 { private: static constexpr uint16_t width = 80; static constexpr uint16_t height = 128; static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST); public: struct FastConfig { uint8_t reg30; uint8_t reg50; uint8_t reg82; uint8_t lutW2; uint8_t lutB2; }; GDEW0102T4(); void setFastConfig(FastConfig cfg); FastConfig getFastConfig() const; protected: void configCommon() override; void configFull() override; void configFast() override; void writeOldImage() override; void finalizeUpdate() override; private: FastConfig fastConfig = {0x13, 0xF2, 0x12, 0x0E, 0x14}; }; } // namespace NicheGraphics::Drivers #endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS ================================================ FILE: src/graphics/niche/Drivers/EInk/GDEY0154D67.cpp ================================================ #include "./GDEY0154D67.h" #ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS using namespace NicheGraphics::Drivers; // Map the display controller IC's output to the connected panel void GDEY0154D67::configScanning() { // "Driver output control" sendCommand(0x01); sendData(0xC7); // Scan until gate 199 (200px vertical res.) sendData(0x00); sendData(0x00); } // Specify which information is used to control the sequence of voltages applied to move the pixels // - For this display, configUpdateSequence() specifies that a suitable LUT will be loaded from // the controller IC's OTP memory, when the update procedure begins. void GDEY0154D67::configWaveform() { sendCommand(0x3C); // Border waveform: sendData(0x05); // Screen border should follow LUT1 waveform (actively drive pixels white) sendCommand(0x18); // Temperature sensor: sendData(0x80); // Use internal temperature sensor to select an appropriate refresh waveform } void GDEY0154D67::configUpdateSequence() { switch (updateType) { case FAST: sendCommand(0x22); // Set "update sequence" sendData(0xFF); // Will load LUT from OTP memory, Display mode 2 "differential refresh" break; case FULL: default: sendCommand(0x22); // Set "update sequence" sendData(0xF7); // Will load LUT from OTP memory break; } } // Once the refresh operation has been started, // begin periodically polling the display to check for completion, using the normal Meshtastic threading code // Only used when refresh is "async" void GDEY0154D67::detachFromUpdate() { switch (updateType) { case FAST: return beginPolling(50, 300); // At least 300ms for fast refresh case FULL: default: return beginPolling(100, 1500); // At least 1.5 seconds for full refresh } } #endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS ================================================ FILE: src/graphics/niche/Drivers/EInk/GDEY0154D67.h ================================================ /* E-Ink display driver - GDEY0154D67 - Manufacturer: Goodisplay - Size: 1.54 inch - Resolution: 200px x 200px - Flex connector marking (not a unique identifier): FPC-B001 */ #pragma once #ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS #include "configuration.h" #include "./SSD16XX.h" namespace NicheGraphics::Drivers { class GDEY0154D67 : public SSD16XX { // Display properties private: static constexpr uint32_t width = 200; static constexpr uint32_t height = 200; static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST); public: GDEY0154D67() : SSD16XX(width, height, supported) {} protected: void configScanning() override; void configWaveform() override; void configUpdateSequence() override; void detachFromUpdate() override; }; } // namespace NicheGraphics::Drivers #endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS ================================================ FILE: src/graphics/niche/Drivers/EInk/GDEY0213B74.cpp ================================================ #include "./GDEY0213B74.h" #ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS using namespace NicheGraphics::Drivers; // Map the display controller IC's output to the connected panel void GDEY0213B74::configScanning() { // "Driver output control" sendCommand(0x01); sendData(0xF9); sendData(0x00); sendData(0x00); } // Specify which information is used to control the sequence of voltages applied to move the pixels // - For this display, configUpdateSequence() specifies that a suitable LUT will be loaded from // the controller IC's OTP memory, when the update procedure begins. void GDEY0213B74::configWaveform() { sendCommand(0x3C); // Border waveform: sendData(0x05); // Screen border should follow LUT1 waveform (actively drive pixels white) sendCommand(0x18); // Temperature sensor: sendData(0x80); // Use internal temperature sensor to select an appropriate refresh waveform } void GDEY0213B74::configUpdateSequence() { switch (updateType) { case FAST: sendCommand(0x22); // Set "update sequence" sendData(0xFF); // Will load LUT from OTP memory, Display mode 2 "differential refresh" break; case FULL: default: sendCommand(0x22); // Set "update sequence" sendData(0xF7); // Will load LUT from OTP memory break; } } // Once the refresh operation has been started, // begin periodically polling the display to check for completion, using the normal Meshtastic threading code // Only used when refresh is "async" void GDEY0213B74::detachFromUpdate() { switch (updateType) { case FAST: return beginPolling(50, 500); // At least 500ms for fast refresh case FULL: default: return beginPolling(100, 2000); // At least 2 seconds for full refresh } } #endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS ================================================ FILE: src/graphics/niche/Drivers/EInk/GDEY0213B74.h ================================================ /* E-Ink display driver - GDEY0213B74 - Manufacturer: Goodisplay - Size: 2.13 inch - Resolution: 250px x 122px - Flex connector marking (not a unique identifier): - FPC-A002 - FPC-A005 20.06.15 TRX */ #pragma once #ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS #include "configuration.h" #include "./SSD16XX.h" namespace NicheGraphics::Drivers { class GDEY0213B74 : public SSD16XX { // Display properties private: static constexpr uint32_t width = 122; static constexpr uint32_t height = 250; static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST); public: GDEY0213B74() : SSD16XX(width, height, supported) {} protected: virtual void configScanning() override; virtual void configWaveform() override; virtual void configUpdateSequence() override; void detachFromUpdate() override; }; } // namespace NicheGraphics::Drivers #endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS ================================================ FILE: src/graphics/niche/Drivers/EInk/HINK_E0213A289.cpp ================================================ #include "./HINK_E0213A289.h" #ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS using namespace NicheGraphics::Drivers; // Map the display controller IC's output to the connected panel void HINK_E0213A289::configScanning() { // "Driver output control" // Scan gates from 0 to 249 (vertical resolution 250px) sendCommand(0x01); sendData(0xF9); // Maximum gate # (249, bits 0-7) sendData(0x00); // Maximum gate # (bit 8) sendData(0x00); // (Do not invert scanning order) } // Specify which information is used to control the sequence of voltages applied to move the pixels // - For this display, configUpdateSequence() specifies that a suitable LUT will be loaded from // the controller IC's OTP memory, when the update procedure begins. void HINK_E0213A289::configWaveform() { sendCommand(0x3C); // Border waveform: sendData(0x05); // Screen border should follow LUT1 waveform (actively drive pixels white) sendCommand(0x18); // Temperature sensor: sendData(0x80); // Use internal temperature sensor to select an appropriate refresh waveform } // Describes the sequence of events performed by the displays controller IC during a refresh // Includes "power up", "load settings from memory", "update the pixels", etc void HINK_E0213A289::configUpdateSequence() { switch (updateType) { case FAST: sendCommand(0x22); // Set "update sequence" sendData(0xFF); // Will load LUT from OTP memory, Display mode 2 "differential refresh" break; case FULL: default: sendCommand(0x22); // Set "update sequence" sendData(0xF7); // Will load LUT from OTP memory break; } } // Once the refresh operation has been started, // begin periodically polling the display to check for completion, using the normal Meshtastic threading code // Only used when refresh is "async" void HINK_E0213A289::detachFromUpdate() { switch (updateType) { case FAST: return beginPolling(50, 500); // At least 500ms for fast refresh case FULL: default: return beginPolling(100, 1000); // At least 1 second for full refresh (quick; display only blinks pixels once) } } #endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS ================================================ FILE: src/graphics/niche/Drivers/EInk/HINK_E0213A289.h ================================================ /* E-Ink display driver - HINK_E0213A289 - Manufacturer: Holitech - Size: 2.13 inch - Resolution: 122px x 250px - Flex connector label (not a unique identifier): FPC-7528B Note: as of Feb. 2025, these panels are used for "WeActStudio 2.13in B&W" display modules */ #pragma once #ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS #include "configuration.h" #include "./SSD16XX.h" namespace NicheGraphics::Drivers { class HINK_E0213A289 : public SSD16XX { // Display properties private: static constexpr uint32_t width = 122; static constexpr uint32_t height = 250; static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST); public: HINK_E0213A289() : SSD16XX(width, height, supported, 1) {} protected: void configScanning() override; void configWaveform() override; void configUpdateSequence() override; void detachFromUpdate() override; }; } // namespace NicheGraphics::Drivers #endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS ================================================ FILE: src/graphics/niche/Drivers/EInk/HINK_E042A87.cpp ================================================ #include "./HINK_E042A87.h" #ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS using namespace NicheGraphics::Drivers; // Load settings about how the pixels are moved from old state to new state during a refresh // - manually specified, // - or with stored values from displays OTP memory void HINK_E042A87::configWaveform() { sendCommand(0x3C); // Border waveform: sendData(0x01); // Follow LUT for VSH1 sendCommand(0x18); // Temperature sensor: sendData(0x80); // Use internal temperature sensor to select an appropriate refresh waveform } // Describes the sequence of events performed by the displays controller IC during a refresh // Includes "power up", "load settings from memory", "update the pixels", etc void HINK_E042A87::configUpdateSequence() { switch (updateType) { case FAST: sendCommand(0x21); // Use both "old" and "new" image memory (differential) sendData(0x00); sendData(0x00); sendCommand(0x22); // Set "update sequence" sendData(0xFF); // Differential, load waveform from OTP break; case FULL: default: sendCommand(0x21); // Bypass "old" image memory (non-differential) sendData(0x40); sendData(0x00); sendCommand(0x22); // Set "update sequence": sendData(0xF7); // Non-differential, load waveform from OTP break; } } // Once the refresh operation has been started, // begin periodically polling the display to check for completion, using the normal Meshtastic threading code // Only used when refresh is "async" void HINK_E042A87::detachFromUpdate() { switch (updateType) { case FAST: return beginPolling(50, 1000); // At least 1 second, then check every 50ms case FULL: default: return beginPolling(100, 3500); // At least 3.5 seconds, then check every 100ms } } #endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS ================================================ FILE: src/graphics/niche/Drivers/EInk/HINK_E042A87.h ================================================ /* E-Ink display driver - HINK-E042A87 - Manufacturer: Holitech - Size: 4.2 inch - Resolution: 400px x 300px - Flex connector marking (not a unique identifier): HINK-E042A07-FPC-A1 - Silver sticker with QR code, marked: HE042A87 Note: as of Feb. 2025, these panels are used for "WeActStudio 4.2in B&W" display modules */ #pragma once #ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS #include "configuration.h" #include "./SSD16XX.h" namespace NicheGraphics::Drivers { class HINK_E042A87 : public SSD16XX { // Display properties private: static constexpr uint32_t width = 400; static constexpr uint32_t height = 300; static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST); public: HINK_E042A87() : SSD16XX(width, height, supported) {} protected: void configWaveform() override; void configUpdateSequence() override; void detachFromUpdate() override; }; } // namespace NicheGraphics::Drivers #endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS ================================================ FILE: src/graphics/niche/Drivers/EInk/LCMEN2R13ECC1.cpp ================================================ #include "./LCMEN2R13ECC1.h" #ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS using namespace NicheGraphics::Drivers; // Map the display controller IC's output to the connected panel void LCMEN2R13ECC1::configScanning() { // "Driver output control" sendCommand(0x01); sendData(0xF9); sendData(0x00); sendData(0x00); // To-do: delete this method? // Values set here might be redundant: F9, 00, 00 seems to be default } // Specify which information is used to control the sequence of voltages applied to move the pixels // - For this display, configUpdateSequence() specifies that a suitable LUT will be loaded from // the controller IC's OTP memory, when the update procedure begins. void LCMEN2R13ECC1::configWaveform() { switch (updateType) { case FAST: sendCommand(0x3C); // Border waveform: sendData(0x85); break; case FULL: default: // From OTP memory break; } } void LCMEN2R13ECC1::configUpdateSequence() { switch (updateType) { case FAST: sendCommand(0x22); // Set "update sequence" sendData(0xFF); // Will load LUT from OTP memory, Display mode 2 "differential refresh" break; case FULL: default: sendCommand(0x22); // Set "update sequence" sendData(0xF7); // Will load LUT from OTP memory break; } } // Once the refresh operation has been started, // begin periodically polling the display to check for completion, using the normal Meshtastic threading code // Only used when refresh is "async" void LCMEN2R13ECC1::detachFromUpdate() { switch (updateType) { case FAST: return beginPolling(50, 800); // At least 500ms for fast refresh case FULL: default: return beginPolling(100, 2500); // At least 2 seconds for full refresh } } #endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS ================================================ FILE: src/graphics/niche/Drivers/EInk/LCMEN2R13ECC1.h ================================================ /* E-Ink display driver - SSD1680 - Manufacturer: WISEVAST - Size: 2.13 inch - Resolution: 122px x 255px */ #pragma once #ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS #include "configuration.h" #include "./SSD16XX.h" namespace NicheGraphics::Drivers { class LCMEN2R13ECC1 : public SSD16XX { // Display properties private: static constexpr uint32_t width = 122; static constexpr uint32_t height = 250; static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST); public: LCMEN2R13ECC1() : SSD16XX(width, height, supported, 1) {} // Note: left edge of this display is offset by 1 byte protected: virtual void configScanning() override; virtual void configWaveform() override; virtual void configUpdateSequence() override; void detachFromUpdate() override; }; } // namespace NicheGraphics::Drivers #endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS ================================================ FILE: src/graphics/niche/Drivers/EInk/LCMEN2R13EFC1.cpp ================================================ #ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS #include "./LCMEN2R13EFC1.h" #include #include "SPILock.h" using namespace NicheGraphics::Drivers; // Look up table: fast refresh, common electrode static const uint8_t LUT_FAST_VCOMDC[] = { 0x01, 0x06, 0x03, 0x02, 0x01, 0x01, 0x01, // 0x01, 0x06, 0x02, 0x01, 0x01, 0x01, 0x01, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // }; // Look up table: fast refresh, pixels which remain white static const uint8_t LUT_FAST_WW[] = { 0x01, 0x06, 0x03, 0x02, 0x81, 0x01, 0x01, // 0x01, 0x06, 0x02, 0x01, 0x01, 0x01, 0x01, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // }; // Look up table: fast refresh, pixel which change from black to white static const uint8_t LUT_FAST_BW[] = { 0x01, 0x86, 0x83, 0x82, 0x81, 0x01, 0x01, // 0x01, 0x86, 0x82, 0x01, 0x01, 0x01, 0x01, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // }; // Look up table: fast refresh, pixels which change from white to black static const uint8_t LUT_FAST_WB[] = { 0x01, 0x46, 0x43, 0x02, 0x01, 0x01, 0x01, // 0x01, 0x46, 0x42, 0x01, 0x01, 0x01, 0x01, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // }; // Look up table: fast refresh, pixels which remain black static const uint8_t LUT_FAST_BB[] = { 0x01, 0x06, 0x03, 0x42, 0x41, 0x01, 0x01, // 0x01, 0x06, 0x02, 0x01, 0x01, 0x01, 0x01, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // }; LCMEN213EFC1::LCMEN213EFC1() : EInk(width, height, supported) { // Pre-calculate size of the image buffer, for convenience // Determine the X dimension of the image buffer, in bytes. // Along rows, pixels are stored 8 per byte. // Not all display widths are divisible by 8. Need to make sure bytecount accommodates padding for these. bufferRowSize = ((width - 1) / 8) + 1; // Total size of image buffer, in bytes. bufferSize = bufferRowSize * height; } void LCMEN213EFC1::begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst) { this->spi = spi; this->pin_dc = pin_dc; this->pin_cs = pin_cs; this->pin_busy = pin_busy; this->pin_rst = pin_rst; pinMode(pin_dc, OUTPUT); pinMode(pin_cs, OUTPUT); pinMode(pin_busy, INPUT); // Reset is active low, hold high pinMode(pin_rst, INPUT_PULLUP); reset(); } // Display an image on the display void LCMEN213EFC1::update(uint8_t *imageData, UpdateTypes type) { this->updateType = type; this->buffer = imageData; reset(); // Config if (updateType == FULL) configFull(); else configFast(); // Transfer image data if (updateType == FULL) { writeNewImage(); writeOldImage(); } else { writeNewImage(); } sendCommand(0x04); // Power on the panel voltage wait(); sendCommand(0x12); // Begin executing the update // Let the update run async, on display hardware. Base class will poll completion, then finalize. // For a blocking update, call await after update detachFromUpdate(); } void LCMEN213EFC1::wait() { // Busy when LOW while (digitalRead(pin_busy) == LOW) yield(); } void LCMEN213EFC1::reset() { pinMode(pin_rst, OUTPUT); digitalWrite(pin_rst, LOW); delay(10); pinMode(pin_rst, INPUT_PULLUP); wait(); sendCommand(0x12); wait(); } void LCMEN213EFC1::sendCommand(const uint8_t command) { // Take firmware's SPI lock spiLock->lock(); spi->beginTransaction(spiSettings); digitalWrite(pin_dc, LOW); // DC pin low indicates command digitalWrite(pin_cs, LOW); spi->transfer(command); digitalWrite(pin_cs, HIGH); digitalWrite(pin_dc, HIGH); spi->endTransaction(); spiLock->unlock(); } void LCMEN213EFC1::sendData(uint8_t data) { sendData(&data, 1); } void LCMEN213EFC1::sendData(const uint8_t *data, uint32_t size) { // Take firmware's SPI lock spiLock->lock(); spi->beginTransaction(spiSettings); digitalWrite(pin_dc, HIGH); // DC pin HIGH indicates data, instead of command digitalWrite(pin_cs, LOW); // Platform-specific SPI command // Mothballing. This display model is only used by Heltec Wireless Paper (ESP32) #if defined(ARCH_ESP32) spi->transferBytes(data, NULL, size); // NULL for a "write only" transfer #elif defined(ARCH_NRF52) spi->transfer(data, NULL, size); // NULL for a "write only" transfer #else #error Not implemented yet? Feel free to add other platforms here. #endif digitalWrite(pin_cs, HIGH); digitalWrite(pin_dc, HIGH); spi->endTransaction(); spiLock->unlock(); } void LCMEN213EFC1::configFull() { sendCommand(0x00); // Panel setting register sendData(0b11 << 6 // Display resolution | 1 << 4 // B&W only | 1 << 3 // Vertical scan direction | 1 << 2 // Horizontal scan direction | 1 << 1 // Shutdown: no | 1 << 0 // Reset: no ); sendCommand(0x50); // VCOM and data interval setting register sendData(0b10 << 6 // Border driven white | 0b11 << 4 // Invert image colors: no | 0b0111 << 0 // Interval between VCOM on and image data (default) ); } void LCMEN213EFC1::configFast() { sendCommand(0x00); // Panel setting register sendData(0b11 << 6 // Display resolution | 1 << 5 // LUT from registers (set below) | 1 << 4 // B&W only | 1 << 3 // Vertical scan direction | 1 << 2 // Horizontal scan direction | 1 << 1 // Shutdown: no | 1 << 0 // Reset: no ); sendCommand(0x50); // VCOM and data interval setting register sendData(0b11 << 6 // Border floating | 0b01 << 4 // Invert image colors: no | 0b0111 << 0 // Interval between VCOM on and image data (default) ); // Load the various LUTs sendCommand(0x20); // VCOM sendData(LUT_FAST_VCOMDC, sizeof(LUT_FAST_VCOMDC)); sendCommand(0x21); // White -> White sendData(LUT_FAST_WW, sizeof(LUT_FAST_WW)); sendCommand(0x22); // Black -> White sendData(LUT_FAST_BW, sizeof(LUT_FAST_BW)); sendCommand(0x23); // White -> Black sendData(LUT_FAST_WB, sizeof(LUT_FAST_WB)); sendCommand(0x24); // Black -> Black sendData(LUT_FAST_BB, sizeof(LUT_FAST_BB)); } void LCMEN213EFC1::writeNewImage() { sendCommand(0x13); sendData(buffer, bufferSize); } void LCMEN213EFC1::writeOldImage() { sendCommand(0x10); sendData(buffer, bufferSize); } void LCMEN213EFC1::detachFromUpdate() { // To save power / cycles, displays can choose to specify an "expected duration" for various refresh types // If we know a full-refresh takes at least 4 seconds, we can delay polling until 3 seconds have passed // If not implemented, we'll just poll right from the get-go switch (updateType) { case FULL: EInk::beginPolling(10, 3650); break; case FAST: EInk::beginPolling(10, 720); break; default: assert(false); } } bool LCMEN213EFC1::isUpdateDone() { // Busy when LOW if (digitalRead(pin_busy) == LOW) return false; else return true; } void LCMEN213EFC1::finalizeUpdate() { // Power off the panel voltages sendCommand(0x02); wait(); // Put a copy of the image into the "old memory". // Used with differential refreshes (e.g. FAST update), to determine which px need to move, and which can remain in place // We need to keep the "old memory" up to date, because don't know whether next refresh will be FULL or FAST etc. if (updateType != FULL) { writeOldImage(); wait(); } } #endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS ================================================ FILE: src/graphics/niche/Drivers/EInk/LCMEN2R13EFC1.h ================================================ /* E-Ink display driver - LCMEN213EFC1 - Manufacturer: Wisevast - Size: 2.13 inch - Resolution: 122px x 250px - Flex connector marking (not a unique identifier): HINK-E0213A162-FPC-A0 (Hidden, printed on back-side) Note: this display uses an uncommon controller IC, Fitipower JD79656. It is implemented as a "one-off", directly inheriting the EInk base class, unlike SSD16XX displays. */ #pragma once #ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS #include "configuration.h" #include "./EInk.h" namespace NicheGraphics::Drivers { class LCMEN213EFC1 : public EInk { // Display properties private: static constexpr uint32_t width = 122; static constexpr uint32_t height = 250; static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST); public: LCMEN213EFC1(); void begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst); void update(uint8_t *imageData, UpdateTypes type) override; protected: void wait(); void reset(); void sendCommand(const uint8_t command); void sendData(const uint8_t data); void sendData(const uint8_t *data, uint32_t size); void configFull(); // Configure display for FULL refresh void configFast(); // Configure display for FAST refresh void writeNewImage(); void writeOldImage(); // Used for "differential update", aka FAST refresh void detachFromUpdate(); bool isUpdateDone(); void finalizeUpdate(); protected: uint8_t bufferOffsetX = 0; // In bytes. Panel x=0 does not always align with controller x=0. Quirky internal wiring? uint8_t bufferRowSize = 0; // In bytes. Rows store 8 pixels per byte. Rounded up to fit (e.g. 122px would require 16 bytes) uint32_t bufferSize = 0; // In bytes. Rows * Columns uint8_t *buffer = nullptr; UpdateTypes updateType = UpdateTypes::UNSPECIFIED; uint8_t pin_dc = -1; uint8_t pin_cs = -1; uint8_t pin_busy = -1; uint8_t pin_rst = -1; SPIClass *spi = nullptr; SPISettings spiSettings = SPISettings(6000000, MSBFIRST, SPI_MODE0); }; } // namespace NicheGraphics::Drivers #endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS ================================================ FILE: src/graphics/niche/Drivers/EInk/README.md ================================================ # NicheGraphics - E-Ink Driver A driver for E-Ink SPI displays. Suitable for re-use by various NicheGraphics UIs. Your UI should use the class `NicheGraphics::Drivers::EInk` . When you set up a hardware variant, you will use one of the specific display model classes, which extend the EInk class. An example setup might look like this: ```cpp void setupNicheGraphics() { using namespace NicheGraphics; // An imaginary UI YourCustomUI *yourUI = new YourCustomUI(); // Setup SPI SPIClass *hspi = new SPIClass(HSPI); hspi->begin(PIN_EINK_SCLK, -1, PIN_EINK_MOSI, PIN_EINK_CS); // Setup EInk driver Drivers::EInk *driver = new Drivers::DEPG0290BNS800; driver->begin(hspi, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY); // Pass the driver to your UI YourUI::driver = driver; } ``` - [Methods](#methods) - [`update(uint8_t *imageData, UpdateTypes type)`](#updateuint8_t-imagedata-updatetypes-type) - [`await()`](#await) - [`supports(UpdateTypes type)`](#supportsupdatetypes-type) - [`busy()`](#busy) - [`width()`](#width) - [`height()`](#height) - [Supporting New Displays](#supporting-new-displays) - [Controller IC](#controller-ic) - [Finding Information](#finding-information) ## Methods ### `update(uint8_t *imageData, UpdateTypes type)` Update the image on the display - _`imageData`_ to draw to the display. - _`type`_ which type of update to perform. - `FULL` - `FAST` (partial refresh) - (Other custom types may be possible) The imageData is a 1-bit image. X-Pixels are 8-per byte, with the MSB being the leftmost pixel. This was not an InkHUD design decision; it is the raw format accepted by the E-Ink display controllers ICs. _To-do: add a helper method to `InkHUD::Drivers::EInk` to do this arithmetic for you._ ```cpp uint16_t w = driver::width(); uint16_t h = driver::height(); uint8_t image[ (w/8) * h ]; // X pixels are 8-per-byte image[0] |= (1 << 7); // Set pixel x=0, y=0 image[0] |= (1 << 0); // Set pixel x=7, y=0 image[1] |= (1 << 7); // Set pixel x=8, y=0 uint8_t x = 12; uint8_t y = 2; uint8_t yBytes = y * (w/8); uint8_t xBytes = x / 8; uint8_t xBits = (7-x) % 8; image[yByte + xByte] |= (1 << xBits); // Set pixel x=12, y=2 ``` ### `await()` Wait for an in-progress update to complete before continuing ### `supports(UpdateTypes type)` Check if display supports a specific update type. `true` if supported. - _`type`_ type to check ### `busy()` Check if display is already performing an `update()`. `true` if already updating. ### `width()` Width of the display, in pixels. Note: most displays are portrait. Your UI will need to implement rotation in software. ### `height()` Height of the display, in pixels. Note: most displays are portrait. Your UI will need to implement rotation in software. ## Supporting New Displays _This topic is not covered in depth, but these notes may be helpful._ The `InkHUD::Drivers::EInk` class contains only the mechanism for implementing an E-Ink driver on-top of Meshtastic's `OSThread`. A driver for a specific display needs to extend this class. ### Controller IC If your display uses a controller IC from Solomon Systech, you can probably extend the existing `Drivers::SSD16XX` class, making only minor modifications. At this stage, displays using controller ICS from other manufacturers (UltraChip, Fitipower, etc) need to manually implemented. See `Drivers::LCMEN2R13EFC1` for an example. Generic base classes for manufacturers other than Solomon Systech might be added here in future. ### Finding Information #### Flex-Connector Labels The orange flex-connector attached to E-Ink displays is often printed with an identifying label. This is not a _totally_ unique identifier, but does give a very strong clue as to the true model of the display, which can be used to search out further information. #### Datasheets The manufacturer of a DIY display module may publish a datasheet. These are often incomplete, but might reveal the true model of the display, or the controller IC. If you can determine the true model name of the display, you can likely find a more complete datasheet on the display manufacturer's website. This will often provide a "typical operating sequence"; a general overview of the code used to drive the display #### Example Code The manufacturer of a DIY module may publish example code. You may have more luck finding example code published by the display manufacturer themselves, if you can determine the true model of the panel. These examples are a very valuable reference. #### Other E-Ink drivers Libraries like ZinggJM's GxEPD2 can be valuable sources of information, although your panel may not be _specifically_ supported, and only _compatible_ with a driver there, so some caution is advised. The display selection file in GxEPD2's Hello World example is also a useful resource for matching "flex connector labels" with display models, but the flex connector label is _not_ a unique identifier, so this is only another clue. ================================================ FILE: src/graphics/niche/Drivers/EInk/SSD1682.cpp ================================================ #include "./SSD1682.h" #ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS using namespace NicheGraphics::Drivers; SSD1682::SSD1682(uint16_t width, uint16_t height, EInk::UpdateTypes supported, uint8_t bufferOffsetX) : SSD16XX(width, height, supported, bufferOffsetX) { } // SSD1682 only accepts single-byte x and y values // This causes an incompatibility with the default SSD16XX::configFullscreen void SSD1682::configFullscreen() { // Define the boundaries of the "fullscreen" region, for the controller IC static const uint8_t sx = bufferOffsetX; // Notice the offset static const uint8_t sy = 0; static const uint8_t ex = bufferRowSize + bufferOffsetX - 1; // End is "max index", not "count". Minus 1 handles this static const uint8_t ey = height; // Data entry mode - Left to Right, Top to Bottom sendCommand(0x11); sendData(0x03); // Select controller IC memory region to display a fullscreen image sendCommand(0x44); // Memory X start - end sendData(sx); sendData(ex); sendCommand(0x45); // Memory Y start - end sendData(sy); sendData(ey); // Place the cursor at the start of this memory region, ready to send image data x=0 y=0 sendCommand(0x4E); // Memory cursor X sendData(sx); sendCommand(0x4F); // Memory cursor y sendData(sy); } #endif ================================================ FILE: src/graphics/niche/Drivers/EInk/SSD1682.h ================================================ /* E-Ink base class for displays based on SSD1682 SSD1682 has a few quirks. We're implementing them here in a new base class, to avoid re-implementing them every time we need to add a new SSD1682-based display. */ #pragma once #ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS #include "configuration.h" #include "./SSD16XX.h" namespace NicheGraphics::Drivers { class SSD1682 : public SSD16XX { public: SSD1682(uint16_t width, uint16_t height, EInk::UpdateTypes supported, uint8_t bufferOffsetX = 0); virtual void configFullscreen(); // Select memory region on controller IC virtual void deepSleep() {} // Not usable (image memory not retained) }; } // namespace NicheGraphics::Drivers #endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS ================================================ FILE: src/graphics/niche/Drivers/EInk/SSD16XX.cpp ================================================ #ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS #include "./SSD16XX.h" #include "SPILock.h" using namespace NicheGraphics::Drivers; SSD16XX::SSD16XX(uint16_t width, uint16_t height, UpdateTypes supported, uint8_t bufferOffsetX) : EInk(width, height, supported), bufferOffsetX(bufferOffsetX) { // Pre-calculate size of the image buffer, for convenience // Determine the X dimension of the image buffer, in bytes. // Along rows, pixels are stored 8 per byte. // Not all display widths are divisible by 8. Need to make sure bytecount accommodates padding for these. bufferRowSize = ((width - 1) / 8) + 1; // Total size of image buffer, in bytes. bufferSize = bufferRowSize * height; } void SSD16XX::begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst) { this->spi = spi; this->pin_dc = pin_dc; this->pin_cs = pin_cs; this->pin_busy = pin_busy; this->pin_rst = pin_rst; pinMode(pin_dc, OUTPUT); pinMode(pin_cs, OUTPUT); pinMode(pin_busy, INPUT); // If using a reset pin, hold high // Reset is active low for Solomon Systech ICs if (pin_rst != 0xFF) pinMode(pin_rst, INPUT_PULLUP); reset(); } // Poll the displays busy pin until an operation is complete // Timeout and set fail flag if something went wrong and the display got stuck void SSD16XX::wait(uint32_t timeout) { // Don't bother waiting if part of the update sequence failed // In that situation, we're now just failing-through the process, until we can try again with next update. if (failed) return; uint32_t startMs = millis(); // Busy when HIGH while (digitalRead(pin_busy) == HIGH) { // Check for timeout if (millis() - startMs > timeout) { failed = true; break; } yield(); } } void SSD16XX::reset() { // Check if reset pin is defined if (pin_rst != 0xFF) { pinMode(pin_rst, OUTPUT); digitalWrite(pin_rst, LOW); delay(10); digitalWrite(pin_rst, HIGH); delay(10); wait(); } sendCommand(0x12); wait(); } void SSD16XX::sendCommand(const uint8_t command) { // Abort if part of the update sequence failed // This will unlock again once we have failed-through the entire process if (failed) return; // Take firmware's SPI lock spiLock->lock(); spi->beginTransaction(spiSettings); digitalWrite(pin_dc, LOW); // DC pin low indicates command digitalWrite(pin_cs, LOW); spi->transfer(command); digitalWrite(pin_cs, HIGH); digitalWrite(pin_dc, HIGH); spi->endTransaction(); spiLock->unlock(); } void SSD16XX::sendData(uint8_t data) { sendData(&data, 1); } void SSD16XX::sendData(const uint8_t *data, uint32_t size) { // Abort if part of the update sequence failed // This will unlock again once we have failed-through the entire process if (failed) return; // Take firmware's SPI lock spiLock->lock(); spi->beginTransaction(spiSettings); digitalWrite(pin_dc, HIGH); // DC pin HIGH indicates data, instead of command digitalWrite(pin_cs, LOW); // Platform-specific SPI command #if defined(ARCH_ESP32) spi->transferBytes(data, NULL, size); // NULL for a "write only" transfer #elif defined(ARCH_NRF52) spi->transfer(data, NULL, size); // NULL for a "write only" transfer #else #error Not implemented yet? Feel free to add other platforms here. #endif digitalWrite(pin_cs, HIGH); digitalWrite(pin_dc, HIGH); spi->endTransaction(); spiLock->unlock(); } void SSD16XX::configFullscreen() { // Placing this code in a separate method because it's probably pretty consistent between displays // Should make it tidier to override SSD16XX::configure // Define the boundaries of the "fullscreen" region, for the controller IC static const uint16_t sx = bufferOffsetX; // Notice the offset static const uint16_t sy = 0; static const uint16_t ex = bufferRowSize + bufferOffsetX - 1; // End is "max index", not "count". Minus 1 handles this static const uint16_t ey = height; // Split into bytes static const uint8_t sy1 = sy & 0xFF; static const uint8_t sy2 = (sy >> 8) & 0xFF; static const uint8_t ey1 = ey & 0xFF; static const uint8_t ey2 = (ey >> 8) & 0xFF; // Data entry mode - Left to Right, Top to Bottom sendCommand(0x11); sendData(0x03); // Select controller IC memory region to display a fullscreen image sendCommand(0x44); // Memory X start - end sendData(sx); sendData(ex); sendCommand(0x45); // Memory Y start - end sendData(sy1); sendData(sy2); sendData(ey1); sendData(ey2); // Place the cursor at the start of this memory region, ready to send image data x=0 y=0 sendCommand(0x4E); // Memory cursor X sendData(sx); sendCommand(0x4F); // Memory cursor y sendData(sy1); sendData(sy2); } void SSD16XX::update(uint8_t *imageData, UpdateTypes type) { this->updateType = type; this->buffer = imageData; reset(); configFullscreen(); configScanning(); // Virtual, unused by base class configVoltages(); // Virtual, unused by base class configWaveform(); // Virtual, unused by base class wait(); if (updateType == FULL) { writeNewImage(); writeOldImage(); } else { writeNewImage(); } configUpdateSequence(); sendCommand(0x20); // Begin executing the update // Let the update run async, on display hardware. Base class will poll completion, then finalize. // For a blocking update, call await after update detachFromUpdate(); } // Send SPI commands for controller IC to begin executing the refresh operation void SSD16XX::configUpdateSequence() { switch (updateType) { default: sendCommand(0x22); // Set "update sequence" sendData(0xF7); // Non-differential, load waveform from OTP break; } } void SSD16XX::writeNewImage() { sendCommand(0x24); sendData(buffer, bufferSize); } void SSD16XX::writeOldImage() { sendCommand(0x26); sendData(buffer, bufferSize); } void SSD16XX::detachFromUpdate() { // To save power / cycles, displays can choose to specify an "expected duration" for various refresh types // If we know a full-refresh takes at least 4 seconds, we can delay polling until 3 seconds have passed // If not implemented, we'll just poll right from the get-go switch (updateType) { default: EInk::beginPolling(100, 0); } } bool SSD16XX::isUpdateDone() { // Busy when HIGH if (digitalRead(pin_busy) == HIGH) return false; else return true; } void SSD16XX::finalizeUpdate() { // Put a copy of the image into the "old memory". // Used with differential refreshes (e.g. FAST update), to determine which px need to move, and which can remain in place // We need to keep the "old memory" up to date, because don't know whether next refresh will be FULL or FAST etc. if (updateType != FULL) { writeNewImage(); // Only required by some controller variants. Todo: Override just for GDEY0154D678? writeOldImage(); sendCommand(0x7F); // Terminate image write without update wait(); } // Enter deep-sleep to save a few µA // Waking from this requires that display's reset pin is broken out if (pin_rst != 0xFF) deepSleep(); } // Enter a lower-power state // May only save a few µA.. void SSD16XX::deepSleep() { sendCommand(0x10); // Enter deep sleep sendData(0x01); // Mode 1: preserve image RAM } #endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS ================================================ FILE: src/graphics/niche/Drivers/EInk/SSD16XX.h ================================================ /* E-Ink base class for displays based on SSD16XX Most (but not all) SPI E-Ink displays use this family of controller IC. Implementing new SSD16XX displays should be fairly painless. See DEPG0154BNS800 and DEPG0290BNS800 for examples. */ #pragma once #ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS #include "configuration.h" #include "./EInk.h" namespace NicheGraphics::Drivers { class SSD16XX : public EInk { public: SSD16XX(uint16_t width, uint16_t height, UpdateTypes supported, uint8_t bufferOffsetX = 0); virtual void begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst = -1); virtual void update(uint8_t *imageData, UpdateTypes type) override; protected: virtual void wait(uint32_t timeout = 1000); virtual void reset(); virtual void sendCommand(const uint8_t command); virtual void sendData(const uint8_t data); virtual void sendData(const uint8_t *data, uint32_t size); virtual void configFullscreen(); // Select memory region on controller IC virtual void configScanning() {} // Optional. First & last gates, scan direction, etc virtual void configVoltages() {} // Optional. Manual panel voltages, soft-start, etc virtual void configWaveform() {} // Optional. LUT, panel border, temperature sensor, etc virtual void configUpdateSequence(); // Tell controller IC which operations to run virtual void writeNewImage(); virtual void writeOldImage(); // Image which can be used at *next* update for "differential refresh" virtual void detachFromUpdate(); virtual bool isUpdateDone() override; virtual void finalizeUpdate() override; virtual void deepSleep(); protected: uint8_t bufferOffsetX = 0; // In bytes. Panel x=0 does not always align with controller x=0. Quirky internal wiring? uint8_t bufferRowSize = 0; // In bytes. Rows store 8 pixels per byte. Rounded up to fit (e.g. 122px would require 16 bytes) uint32_t bufferSize = 0; // In bytes. Rows * Columns uint8_t *buffer = nullptr; UpdateTypes updateType = UpdateTypes::UNSPECIFIED; uint8_t pin_dc = -1; uint8_t pin_cs = -1; uint8_t pin_busy = -1; uint8_t pin_rst = -1; SPIClass *spi = nullptr; SPISettings spiSettings = SPISettings(4000000, MSBFIRST, SPI_MODE0); }; } // namespace NicheGraphics::Drivers #endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS ================================================ FILE: src/graphics/niche/Drivers/EInk/UC8175.cpp ================================================ #ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS #include "./UC8175.h" #include #include "SPILock.h" using namespace NicheGraphics::Drivers; UC8175::UC8175(uint16_t width, uint16_t height, UpdateTypes supported) : EInk(width, height, supported) { bufferRowSize = ((width - 1) / 8) + 1; bufferSize = bufferRowSize * height; } void UC8175::begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst) { this->spi = spi; this->pin_dc = pin_dc; this->pin_cs = pin_cs; this->pin_busy = pin_busy; this->pin_rst = pin_rst; pinMode(pin_dc, OUTPUT); pinMode(pin_cs, OUTPUT); pinMode(pin_busy, INPUT); // Reset is active LOW, hold HIGH when idle. if (pin_rst != (uint8_t)-1) { pinMode(pin_rst, OUTPUT); digitalWrite(pin_rst, HIGH); } if (!previousBuffer) { previousBuffer = new uint8_t[bufferSize]; if (previousBuffer) memset(previousBuffer, 0xFF, bufferSize); } } void UC8175::update(uint8_t *imageData, UpdateTypes type) { buffer = imageData; updateType = (type == UpdateTypes::UNSPECIFIED) ? UpdateTypes::FULL : type; if (updateType == FAST && hasPreviousBuffer && previousBuffer && memcmp(previousBuffer, buffer, bufferSize) == 0) return; reset(); configCommon(); if (updateType == FAST) configFast(); else configFull(); writeOldImage(); writeNewImage(); sendCommand(0x12); // Display refresh. if (previousBuffer) { memcpy(previousBuffer, buffer, bufferSize); hasPreviousBuffer = true; } detachFromUpdate(); } void UC8175::wait(uint32_t timeoutMs) { if (failed) return; uint32_t started = millis(); while (digitalRead(pin_busy) == BUSY_ACTIVE) { if ((millis() - started) > timeoutMs) { failed = true; break; } yield(); } } void UC8175::reset() { if (pin_rst != (uint8_t)-1) { digitalWrite(pin_rst, LOW); delay(20); digitalWrite(pin_rst, HIGH); delay(20); } else { sendCommand(0x12); // Software reset. delay(10); } wait(3000); } void UC8175::sendCommand(uint8_t command) { if (failed) return; spiLock->lock(); spi->beginTransaction(spiSettings); digitalWrite(pin_dc, LOW); digitalWrite(pin_cs, LOW); spi->transfer(command); digitalWrite(pin_cs, HIGH); digitalWrite(pin_dc, HIGH); spi->endTransaction(); spiLock->unlock(); } void UC8175::sendData(uint8_t data) { sendData(&data, 1); } void UC8175::sendData(const uint8_t *data, uint32_t size) { if (failed) return; spiLock->lock(); spi->beginTransaction(spiSettings); digitalWrite(pin_dc, HIGH); digitalWrite(pin_cs, LOW); #if defined(ARCH_ESP32) spi->transferBytes(data, NULL, size); #elif defined(ARCH_NRF52) spi->transfer(data, NULL, size); #else for (uint32_t i = 0; i < size; ++i) spi->transfer(data[i]); #endif digitalWrite(pin_cs, HIGH); digitalWrite(pin_dc, HIGH); spi->endTransaction(); spiLock->unlock(); } void UC8175::powerOn() { sendCommand(0x04); wait(2000); } void UC8175::powerOff() { sendCommand(0x02); // Power off. wait(1500); } void UC8175::writeImage(uint8_t command, const uint8_t *image) { sendCommand(command); sendData(image, bufferSize); } void UC8175::writeOldImage() { if (updateType == FAST && previousBuffer) writeImage(0x10, previousBuffer); else writeImage(0x10, buffer); } void UC8175::writeNewImage() { writeImage(0x13, buffer); } void UC8175::detachFromUpdate() { switch (updateType) { case FAST: return beginPolling(50, 400); case FULL: default: return beginPolling(100, 2000); } } bool UC8175::isUpdateDone() { return digitalRead(pin_busy) != BUSY_ACTIVE; } void UC8175::finalizeUpdate() { powerOff(); if (pin_rst != (uint8_t)-1) { sendCommand(0x07); // Deep sleep. sendData(0xA5); } } #endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS ================================================ FILE: src/graphics/niche/Drivers/EInk/UC8175.h ================================================ // E-Ink base class for displays based on UC8175 / UC8176 style controller ICs. #pragma once #ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS #include "configuration.h" #include "./EInk.h" namespace NicheGraphics::Drivers { class UC8175 : public EInk { public: UC8175(uint16_t width, uint16_t height, UpdateTypes supported); void begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst = -1) override; void update(uint8_t *imageData, UpdateTypes type) override; protected: virtual void wait(uint32_t timeoutMs = 1000); virtual void reset(); virtual void sendCommand(uint8_t command); virtual void sendData(uint8_t data); virtual void sendData(const uint8_t *data, uint32_t size); virtual void configCommon() = 0; // Always run virtual void configFull() = 0; // Run when updateType == FULL virtual void configFast() = 0; // Run when updateType == FAST virtual void powerOn(); virtual void powerOff(); virtual void writeOldImage(); virtual void writeNewImage(); virtual void writeImage(uint8_t command, const uint8_t *image); virtual void detachFromUpdate(); virtual bool isUpdateDone() override; virtual void finalizeUpdate() override; protected: static constexpr uint8_t BUSY_ACTIVE = LOW; uint16_t bufferRowSize = 0; uint32_t bufferSize = 0; uint8_t *buffer = nullptr; uint8_t *previousBuffer = nullptr; bool hasPreviousBuffer = false; UpdateTypes updateType = UpdateTypes::UNSPECIFIED; uint8_t pin_dc = (uint8_t)-1; uint8_t pin_cs = (uint8_t)-1; uint8_t pin_busy = (uint8_t)-1; uint8_t pin_rst = (uint8_t)-1; SPIClass *spi = nullptr; SPISettings spiSettings = SPISettings(8000000, MSBFIRST, SPI_MODE0); }; } // namespace NicheGraphics::Drivers #endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS ================================================ FILE: src/graphics/niche/Drivers/EInk/ZJY122250_0213BAAMFGN.cpp ================================================ #include "./ZJY122250_0213BAAMFGN.h" #ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS using namespace NicheGraphics::Drivers; // Map the display controller IC's output to the connected panel void ZJY122250_0213BAAMFGN::configScanning() { // "Driver output control" // Scan gates from 0 to 249 (vertical resolution 250px) sendCommand(0x01); sendData(0xF9); sendData(0x00); sendData(0x00); } // Specify which information is used to control the sequence of voltages applied to move the pixels // - For this display, configUpdateSequence() specifies that a suitable LUT will be loaded from // the controller IC's OTP memory, when the update procedure begins. void ZJY122250_0213BAAMFGN::configWaveform() { switch (updateType) { case FAST: sendCommand(0x3C); // Border waveform: sendData(0x80); // VCOM break; case FULL: default: sendCommand(0x3C); // Border waveform: sendData(0x01); // Follow LUT 1 (blink same as white pixels) break; } sendCommand(0x18); // Temperature sensor: sendData(0x80); // Use internal temperature sensor to select an appropriate refresh waveform } void ZJY122250_0213BAAMFGN::configUpdateSequence() { switch (updateType) { case FAST: sendCommand(0x22); // Set "update sequence" sendData(0xFF); // Will load LUT from OTP memory, Display mode 2 "differential refresh" break; case FULL: default: sendCommand(0x22); // Set "update sequence" sendData(0xF7); // Will load LUT from OTP memory break; } } // Once the refresh operation has been started, // begin periodically polling the display to check for completion, using the normal Meshtastic threading code // Only used when refresh is "async" void ZJY122250_0213BAAMFGN::detachFromUpdate() { switch (updateType) { case FAST: return beginPolling(50, 500); // At least 500ms for fast refresh case FULL: default: return beginPolling(100, 2000); // At least 2 seconds for full refresh } } #endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS ================================================ FILE: src/graphics/niche/Drivers/EInk/ZJY122250_0213BAAMFGN.h ================================================ /* E-Ink display driver - ZJY122250_0213BAAMFGN - Manufacturer: Zhongjingyuan - Size: 2.13 inch - Resolution: 250px x 122px - Flex connector marking (not a unique identifier): FPC-A002 */ #pragma once #ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS #include "configuration.h" #include "./SSD16XX.h" namespace NicheGraphics::Drivers { class ZJY122250_0213BAAMFGN : public SSD16XX { // Display properties private: static constexpr uint32_t width = 122; static constexpr uint32_t height = 250; static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST); public: ZJY122250_0213BAAMFGN() : SSD16XX(width, height, supported) {} protected: virtual void configScanning() override; virtual void configWaveform() override; virtual void configUpdateSequence() override; void detachFromUpdate() override; }; } // namespace NicheGraphics::Drivers #endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS ================================================ FILE: src/graphics/niche/Drivers/EInk/ZJY128296_029EAAMFGN.cpp ================================================ #include "./ZJY128296_029EAAMFGN.h" #ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS using namespace NicheGraphics::Drivers; // Map the display controller IC's output to the connected panel void ZJY128296_029EAAMFGN::configScanning() { // "Driver output control" // Scan gates from 0 to 295 (vertical resolution 296px) sendCommand(0x01); sendData(0x27); // Number of gates (295, bits 0-7) sendData(0x01); // Number of gates (295, bit 8) sendData(0x00); // (Do not invert scanning order) } // Specify which information is used to control the sequence of voltages applied to move the pixels // - For this display, configUpdateSequence() specifies that a suitable LUT will be loaded from // the controller IC's OTP memory, when the update procedure begins. void ZJY128296_029EAAMFGN::configWaveform() { sendCommand(0x3C); // Border waveform: sendData(0x05); // Screen border should follow LUT1 waveform (actively drive pixels white) sendCommand(0x18); // Temperature sensor: sendData(0x80); // Use internal temperature sensor to select an appropriate refresh waveform } void ZJY128296_029EAAMFGN::configUpdateSequence() { switch (updateType) { case FAST: sendCommand(0x22); // Set "update sequence" sendData(0xFF); // Will load LUT from OTP memory, Display mode 2 "differential refresh" break; case FULL: default: sendCommand(0x22); // Set "update sequence" sendData(0xF7); // Will load LUT from OTP memory break; } } // Once the refresh operation has been started, // begin periodically polling the display to check for completion, using the normal Meshtastic threading code // Only used when refresh is "async" void ZJY128296_029EAAMFGN::detachFromUpdate() { switch (updateType) { case FAST: return beginPolling(50, 300); // At least 300ms for fast refresh case FULL: default: return beginPolling(100, 2000); // At least 2 seconds for full refresh } } #endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS ================================================ FILE: src/graphics/niche/Drivers/EInk/ZJY128296_029EAAMFGN.h ================================================ /* E-Ink display driver - ZJY128296-029EAAMFGN - Manufacturer: Zhongjingyuan - Size: 2.9 inch - Resolution: 128px x 296px - Flex connector label (not a unique identifier): FPC-A005 20.06.15 TRX Note: as of Feb. 2025, these panels are used for "WeActStudio 2.9in B&W" display modules */ #pragma once #ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS #include "configuration.h" #include "./SSD16XX.h" namespace NicheGraphics::Drivers { class ZJY128296_029EAAMFGN : public SSD16XX { // Display properties private: static constexpr uint32_t width = 128; static constexpr uint32_t height = 296; static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST); public: ZJY128296_029EAAMFGN() : SSD16XX(width, height, supported) {} protected: void configScanning() override; void configWaveform() override; void configUpdateSequence() override; void detachFromUpdate() override; }; } // namespace NicheGraphics::Drivers #endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS ================================================ FILE: src/graphics/niche/Drivers/EInk/ZJY200200_0154DAAMFGN.h ================================================ /* E-Ink display driver - ZJY200200-0154DAAMFGN - Manufacturer: Zhongjingyuan - Size: 1.54 inch - Resolution: 200px x 200px - Flex connector marking: FPC-B001 Note: as of Feb. 2025, these panels are used for "WeActStudio 1.54in B&W" display modules This *is* a distinct panel, however the driver is currently identical to GDEY0154D67 We recognize it as separate now, to avoid breaking any custom builds if the drivers do need to diverge in future. */ #pragma once #ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS #include "configuration.h" #include "./GDEY0154D67.h" namespace NicheGraphics::Drivers { typedef GDEY0154D67 ZJY200200_0154DAAMFGN; } // namespace NicheGraphics::Drivers #endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS ================================================ FILE: src/graphics/niche/Drivers/README.md ================================================ # NicheGraphics - Drivers Common drivers which can be used by various NicheGraphics UIs ================================================ FILE: src/graphics/niche/Fonts/FreeSans12pt_Win1250.h ================================================ // trunk-ignore-all(clang-format) #pragma once /* PROPERTIES FONT_NAME FreeSans12pt_Win1250 */ const uint8_t FreeSans12pt_Win1250Bitmaps[] PROGMEM = { /* 0x01 */ 0x00, 0x30, 0x00, 0x09, 0x00, 0x01, 0x20, 0x00, 0x24, 0x00, 0x04, 0x80, 0x01, 0x90, 0x00, 0x62, 0x00, 0x30, 0xFE, 0x04, 0x10, 0x5F, 0x02, 0x0B, 0x00, 0x7F, 0xE0, 0x0C, 0x1C, 0x02, 0x83, 0x81, 0x9F, 0xF0, 0x02, 0x1E, 0x00, 0x41, 0xC0, 0x0E, 0x7F, 0x81, 0x78, 0x18, 0x62, 0x00, 0xFF, 0xC0, /* 0x02 */ 0x00, 0xFF, 0x80, 0x61, 0x13, 0xF0, 0x62, 0x60, 0x07, 0xFC, 0x00, 0x83, 0x80, 0x10, 0xF0, 0x33, 0xF6, 0x01, 0x41, 0xC0, 0x18, 0x38, 0x03, 0xFF, 0xE0, 0x47, 0x02, 0x08, 0x20, 0x61, 0xC4, 0x06, 0x17, 0x00, 0x22, 0x00, 0x02, 0x40, 0x00, 0x48, 0x00, 0x09, 0x00, 0x01, 0x20, 0x00, 0x3C, 0x00, /* 0x03 */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x00, 0x02, 0x30, 0x00, 0x09, 0x04, 0x08, 0x48, 0x70, 0xE1, 0xC3, 0x87, 0x0E, 0x08, 0x10, 0x70, 0x00, 0x03, 0x80, 0x00, 0x14, 0x00, 0x00, 0xA1, 0x81, 0x8D, 0x87, 0xF0, 0x44, 0x00, 0x06, 0x30, 0x00, 0x60, 0xC0, 0x06, 0x03, 0x80, 0x60, 0x07, 0xFC, 0x00, /* 0x04 */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x00, 0x02, 0x30, 0x00, 0x09, 0x10, 0x02, 0x48, 0xE0, 0x61, 0xC1, 0xCC, 0x0E, 0x78, 0x1C, 0x70, 0x00, 0x03, 0x80, 0x00, 0x14, 0xFF, 0xFC, 0xA6, 0x00, 0xCD, 0x9F, 0xFE, 0x44, 0x71, 0xE6, 0x30, 0xFC, 0x60, 0xC0, 0x06, 0x03, 0x80, 0x60, 0x07, 0xFC, 0x00, /* 0x05 */ 0x00, 0x18, 0x00, 0x00, 0x40, 0x01, 0x90, 0x01, 0xF4, 0x08, 0x12, 0x23, 0xC1, 0x91, 0x2C, 0x1C, 0x8A, 0xC3, 0x64, 0x64, 0x13, 0x22, 0x41, 0x98, 0x26, 0x2C, 0xC4, 0x22, 0x60, 0x42, 0x13, 0x04, 0x30, 0x80, 0x61, 0xA4, 0x02, 0x18, 0x20, 0x03, 0x41, 0x00, 0x20, 0x08, 0x02, 0x00, 0x60, 0x40, 0x03, 0xF8, /* 0x06 */ 0x00, 0x10, 0x00, 0x03, 0x00, 0x1C, 0x48, 0x00, 0xB4, 0x80, 0x09, 0xF9, 0xC0, 0xE0, 0xE4, 0x0C, 0x02, 0x8F, 0x80, 0x38, 0x88, 0x01, 0x0D, 0x00, 0x18, 0x30, 0x01, 0x60, 0x80, 0x13, 0x18, 0x03, 0xF2, 0xC0, 0x20, 0x26, 0x06, 0x07, 0xFF, 0xA0, 0x02, 0x39, 0x00, 0x14, 0x70, 0x01, 0xC3, 0x00, 0x18, 0x00, /* 0x07 */ /* 0x08 */ 0x00, 0x1F, 0x80, 0x00, 0x60, 0x80, 0x01, 0x00, 0x80, 0x06, 0x00, 0x80, 0x3C, 0x01, 0x01, 0x8C, 0x02, 0x02, 0x08, 0x04, 0x04, 0x08, 0x0C, 0x38, 0x00, 0x04, 0x80, 0x00, 0x06, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x2E, 0xC0, 0x01, 0x83, 0x7E, 0x0C, 0x10, 0x37, 0xE2, 0x61, 0x00, 0x0C, 0xC6, 0x10, 0x98, 0x0C, 0x63, 0x00, 0x00, 0xC6, 0x00, /* 0x09 */ 0x00, 0x1F, 0x80, 0x00, 0x60, 0x80, 0x01, 0x00, 0x80, 0x06, 0x00, 0x80, 0x3C, 0x01, 0x01, 0x8C, 0x02, 0x02, 0x08, 0x04, 0x04, 0x08, 0x0C, 0x38, 0x00, 0x04, 0x80, 0x00, 0x06, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x2E, 0xC0, 0x01, 0x83, 0x7E, 0x0C, 0x00, 0x37, 0xE0, /* 0x0A */ /* 0x0B */ 0x1F, 0x07, 0xC1, 0x86, 0x41, 0x10, 0x0C, 0x04, 0x80, 0x40, 0x18, 0x00, 0x00, 0xC0, 0x00, 0x06, 0x00, 0x00, 0x30, 0x00, 0x01, 0x40, 0x00, 0x0A, 0x00, 0x00, 0x88, 0x00, 0x04, 0x40, 0x00, 0x41, 0x00, 0x02, 0x04, 0x00, 0x20, 0x20, 0x02, 0x00, 0x80, 0x20, 0x02, 0x02, 0x00, 0x08, 0x20, 0x00, 0x22, 0x00, 0x00, 0xE0, 0x00, /* 0x0C */ 0x01, 0x00, 0x00, 0x38, 0x00, 0x04, 0xC0, 0x01, 0x08, 0x00, 0x18, 0x80, 0x1C, 0x10, 0x02, 0x07, 0x80, 0x81, 0x10, 0x1F, 0xC2, 0x02, 0x00, 0x60, 0x80, 0x1A, 0x20, 0x1C, 0x42, 0x1C, 0x08, 0xFE, 0x03, 0xA0, 0x01, 0x8C, 0x01, 0xC1, 0x43, 0xD0, 0x27, 0x81, 0xF8, /* 0x0D */ /* 0x0E */ 0x00, 0xE0, 0x00, 0x11, 0x00, 0x01, 0x10, 0x00, 0x0B, 0x00, 0x03, 0xF8, 0x00, 0x60, 0x60, 0x09, 0x02, 0x00, 0xA0, 0x10, 0x16, 0x01, 0x01, 0x40, 0x10, 0x10, 0x01, 0x01, 0x00, 0x08, 0x10, 0x00, 0x82, 0x1F, 0x08, 0x3F, 0x90, 0x44, 0x00, 0x06, 0xBF, 0xFF, 0xAF, 0xF0, 0xFF, 0xFF, 0x0F, 0xE3, 0xFB, 0xFC, /* 0x0F */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x40, 0x12, 0x34, 0x00, 0x69, 0x40, 0x01, 0x49, 0xE0, 0xF1, 0xCD, 0x06, 0x8E, 0x28, 0x14, 0x71, 0x40, 0xA3, 0x8B, 0xFD, 0x14, 0x50, 0x68, 0xA2, 0x81, 0x4D, 0x97, 0xFA, 0x44, 0xBF, 0xD6, 0x31, 0x02, 0xE0, 0xC8, 0x16, 0x08, 0x61, 0x08, 0x21, 0xF0, 0x80, 0xF8, 0x78, 0x00, /* 0x10 */ 0x00, 0xF0, 0x00, 0x3A, 0x00, 0x07, 0xC0, 0x00, 0xA8, 0x00, 0x1F, 0x00, 0x02, 0xB0, 0x00, 0x52, 0x00, 0x0A, 0x40, 0x02, 0x48, 0x00, 0x49, 0x00, 0x09, 0x30, 0x01, 0x22, 0x01, 0xC4, 0x70, 0xF0, 0x85, 0xE1, 0x10, 0x88, 0x37, 0x20, 0x03, 0x9C, 0x00, 0x37, 0x00, 0x06, 0x40, 0x01, 0x86, 0x00, /* 0x11 */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x60, 0x02, 0x36, 0x00, 0x09, 0x04, 0x0C, 0x48, 0x60, 0xC1, 0xC3, 0x0F, 0x0E, 0x00, 0x08, 0x70, 0x00, 0x23, 0x80, 0x63, 0x84, 0x01, 0x9F, 0x20, 0x0C, 0xFD, 0x80, 0x27, 0xE4, 0x03, 0x3F, 0x30, 0x33, 0xE0, 0xC0, 0x00, 0x03, 0x80, 0x60, 0x07, 0xFC, 0x00, /* 0x12 */ 0x00, 0xC2, 0x00, 0x1C, 0x24, 0x02, 0x18, 0x60, 0x64, 0x02, 0x02, 0x40, 0x20, 0x00, 0xF2, 0x03, 0x89, 0xE0, 0x7C, 0x80, 0x0E, 0x25, 0x80, 0xE1, 0x00, 0x1A, 0x08, 0x71, 0xB0, 0xC4, 0x39, 0x84, 0xC2, 0xCC, 0x40, 0x76, 0x7C, 0x05, 0xBB, 0x80, 0x4C, 0xE0, 0x0A, 0x78, 0x00, 0x9C, 0x00, 0x0F, 0x00, 0x00, /* 0x13 */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x00, 0x02, 0x30, 0x00, 0x09, 0x00, 0x00, 0x48, 0x60, 0xC1, 0xC6, 0xC9, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x03, 0x80, 0x00, 0x14, 0xFF, 0xF8, 0xA6, 0x00, 0xCD, 0x9F, 0xFE, 0x44, 0x71, 0xE6, 0x30, 0xFC, 0x60, 0xC0, 0x06, 0x03, 0x80, 0x60, 0x07, 0xFC, 0x00, /* 0x14 */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x20, 0x22, 0x33, 0x01, 0x89, 0x20, 0x02, 0x48, 0x60, 0xE1, 0xC8, 0x80, 0x8E, 0x46, 0x46, 0x72, 0x32, 0x33, 0x9F, 0x9F, 0x94, 0x78, 0x78, 0xA0, 0x00, 0x0D, 0x80, 0x00, 0x44, 0x0E, 0x06, 0x30, 0x00, 0x60, 0xC0, 0x06, 0x03, 0x80, 0x60, 0x07, 0xFC, 0x00, /* 0x15 */ 0x03, 0xFC, 0x20, 0x38, 0x1C, 0x81, 0x80, 0x1D, 0x08, 0x00, 0x32, 0x60, 0x00, 0x89, 0x00, 0x02, 0x18, 0x00, 0x08, 0x61, 0xC3, 0x22, 0x8D, 0x93, 0x72, 0x00, 0x00, 0x48, 0x00, 0x01, 0x20, 0x00, 0x04, 0x9F, 0xFF, 0x92, 0x60, 0x0E, 0x44, 0xFF, 0xF2, 0x11, 0xC3, 0x88, 0x21, 0xF8, 0x40, 0x40, 0x02, 0x00, 0xC0, 0x30, 0x00, 0xFF, 0x00, /* 0x16 */ 0x03, 0x80, 0x03, 0xC0, 0x01, 0xE0, 0x01, 0xE0, 0x03, 0xF0, 0x03, 0xF0, 0x27, 0xF0, 0x6F, 0x70, 0x6E, 0x60, 0xFC, 0x60, 0xFC, 0x7E, 0xFC, 0x7E, 0xFC, 0x3F, 0xF4, 0x1F, 0xF4, 0x1F, 0xF0, 0x0E, 0x70, 0x0E, 0x30, 0x1C, 0x38, 0x38, 0x0F, 0xF0, /* 0x17 */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x00, 0x02, 0x30, 0x00, 0x09, 0x00, 0x00, 0x48, 0x00, 0x21, 0xC0, 0x02, 0x8E, 0x20, 0xF4, 0x70, 0x84, 0x11, 0x82, 0x40, 0x84, 0x01, 0x03, 0x20, 0x0F, 0x85, 0x80, 0x03, 0x04, 0x00, 0x04, 0x30, 0x78, 0x10, 0xC0, 0x06, 0x03, 0x80, 0x60, 0x07, 0xFC, 0x00, /* 0x18 */ 0x00, 0xFC, 0x00, 0x02, 0x06, 0x00, 0x08, 0x24, 0x00, 0x21, 0xA4, 0x00, 0x4C, 0x48, 0x00, 0xA0, 0x50, 0x01, 0x92, 0x60, 0x03, 0x24, 0xC0, 0x06, 0x01, 0x81, 0x28, 0x03, 0x49, 0x6C, 0xC4, 0xAD, 0xD8, 0x16, 0xA4, 0xCC, 0xC4, 0x44, 0x86, 0x13, 0x05, 0x00, 0x28, 0x0A, 0x00, 0x50, 0x14, 0x00, 0x90, 0x48, 0x01, 0x20, 0x90, 0x02, 0x41, 0x20, 0x00, 0x00, /* 0x19 */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x00, 0x02, 0x30, 0x00, 0x09, 0x00, 0x00, 0x49, 0xC3, 0x81, 0xC0, 0x00, 0x0E, 0x78, 0xF0, 0x77, 0xEF, 0xC3, 0xA7, 0x4E, 0x15, 0x0A, 0x10, 0xA7, 0x8F, 0x0D, 0x80, 0x00, 0x44, 0x00, 0x06, 0x33, 0xF0, 0x60, 0xC0, 0x06, 0x03, 0x80, 0x60, 0x07, 0xFC, 0x00, /* 0x1A */ 0xFF, 0xFF, 0x00, 0x06, 0x00, 0x0C, 0x3E, 0x18, 0x82, 0x32, 0x02, 0x64, 0x04, 0xC8, 0x09, 0x80, 0x23, 0x00, 0x86, 0x02, 0x0C, 0x08, 0x18, 0x10, 0x30, 0x00, 0x60, 0x00, 0xC0, 0x81, 0x80, 0x03, 0x00, 0x07, 0xFF, 0xF8, /* 0x1B */ 0x00, 0xFE, 0x00, 0x03, 0x81, 0x80, 0x04, 0x00, 0x60, 0x08, 0x00, 0x30, 0x10, 0x00, 0x10, 0x30, 0x07, 0x88, 0x23, 0xC8, 0x08, 0x22, 0x00, 0x04, 0x60, 0x00, 0x44, 0x60, 0x00, 0x84, 0x63, 0x03, 0x04, 0x61, 0xFC, 0x04, 0x6B, 0x00, 0x9E, 0xA5, 0x01, 0x6A, 0xD5, 0x01, 0x43, 0xA8, 0x81, 0x05, 0xD0, 0x82, 0x0A, 0xA0, 0x82, 0x05, 0xC0, 0x82, 0x02, 0x61, 0xFF, 0x0C, 0x1E, 0x00, 0xF0, /* 0x1C */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x30, 0x02, 0x32, 0x00, 0x09, 0x00, 0x00, 0x48, 0x20, 0x61, 0xC3, 0x84, 0x0E, 0x1C, 0x78, 0x70, 0x40, 0x03, 0x80, 0x00, 0x14, 0x00, 0x00, 0xA0, 0x03, 0x0D, 0x83, 0xF0, 0x44, 0x00, 0x06, 0x30, 0x00, 0x60, 0xC0, 0x06, 0x03, 0x80, 0x60, 0x07, 0xFC, 0x00, /* 0x1D */ 0x01, 0xFE, 0x00, 0x3A, 0x1C, 0x03, 0x00, 0x30, 0x23, 0x1E, 0xC3, 0x38, 0x03, 0x10, 0xC3, 0x09, 0x00, 0x18, 0x68, 0x00, 0xC1, 0x40, 0x00, 0x0A, 0x07, 0x80, 0x50, 0x46, 0x02, 0x80, 0x00, 0x1A, 0x1E, 0x00, 0xCB, 0x10, 0x0D, 0x03, 0x00, 0x48, 0x60, 0x06, 0x40, 0x00, 0x22, 0x0C, 0x02, 0x10, 0x60, 0x60, 0x43, 0xFC, 0x01, 0xE0, 0x00, 0x00, /* 0x1E */ 0x01, 0xF0, 0x00, 0xEA, 0xC0, 0x31, 0x5F, 0x04, 0x5F, 0x88, 0x80, 0xA0, 0x48, 0x0E, 0x02, 0x8F, 0x40, 0x3C, 0x10, 0x21, 0x66, 0x87, 0x15, 0x98, 0x71, 0x41, 0x02, 0x14, 0x00, 0x01, 0x40, 0x00, 0x14, 0x00, 0x01, 0x21, 0xFE, 0x12, 0x00, 0x02, 0x10, 0x00, 0x60, 0x80, 0x0C, 0x06, 0x01, 0x80, 0x3F, 0xE0, /* 0x1F */ 0x0E, 0x00, 0x13, 0x00, 0x23, 0x00, 0xF3, 0x01, 0x31, 0x01, 0x11, 0x03, 0xD3, 0x06, 0xF2, 0x30, 0x34, 0xC7, 0x25, 0x33, 0x2B, 0xC2, 0x57, 0x04, 0x3A, 0x08, 0x72, 0x30, 0xA3, 0xC3, 0x40, 0x04, 0x40, 0x18, 0x40, 0x60, 0x7F, 0x80, /* ' ' 0x20 */ /* '!' 0x21 */ 0xFF, 0xFF, 0xFF, 0xF0, 0xF0, /* '"' 0x22 */ 0xCF, 0x3C, 0xF3, 0x8A, 0x20, /* '#' 0x23 */ 0x06, 0x30, 0x31, 0x03, 0x18, 0x18, 0xC7, 0xFF, 0xBF, 0xFC, 0x31, 0x01, 0x18, 0x18, 0xC7, 0xFF, 0xBF, 0xFC, 0x31, 0x01, 0x18, 0x18, 0xC0, 0xC6, 0x06, 0x30, /* '$' 0x24 */ 0x04, 0x03, 0xE1, 0xFF, 0x72, 0x7C, 0x47, 0x88, 0xF1, 0x07, 0xA0, 0x7E, 0x03, 0xF0, 0x17, 0x02, 0x7C, 0x47, 0x88, 0xF1, 0x1B, 0x26, 0x7F, 0xC3, 0xE0, 0x10, 0x02, 0x00, /* '%' 0x25 */ 0x00, 0x06, 0x03, 0xC0, 0x40, 0x7E, 0x0C, 0x0E, 0x70, 0x80, 0xC3, 0x18, 0x0C, 0x31, 0x00, 0xE7, 0x30, 0x07, 0xE6, 0x00, 0x3C, 0x40, 0x00, 0x0C, 0x7C, 0x00, 0x8F, 0xE0, 0x19, 0xC7, 0x01, 0x18, 0x30, 0x31, 0x83, 0x02, 0x1C, 0x70, 0x40, 0xFE, 0x04, 0x07, 0xC0, /* '&' 0x26 */ 0x0F, 0x00, 0x7E, 0x03, 0x9C, 0x0C, 0x30, 0x30, 0xC0, 0xE7, 0x01, 0xF8, 0x03, 0x80, 0x3E, 0x01, 0xCC, 0x6E, 0x39, 0xB0, 0x7C, 0xC0, 0xF3, 0x03, 0xCE, 0x1F, 0x9F, 0xE6, 0x3E, 0x1C, /* ''' 0x27 */ 0xFF, 0xA0, /* '(' 0x28 */ 0x08, 0x8C, 0x46, 0x31, 0x98, 0xC6, 0x31, 0x8C, 0x63, 0x08, 0x63, 0x08, 0x61, 0x0C, 0x20, /* ')' 0x29 */ 0x82, 0x18, 0xC3, 0x18, 0xC3, 0x18, 0xC6, 0x31, 0x8C, 0x62, 0x31, 0x88, 0xC4, 0x62, 0x00, /* '*' 0x2A */ 0x10, 0x23, 0x5B, 0xE3, 0x8D, 0x91, 0x00, /* '+' 0x2B */ 0x0C, 0x03, 0x00, 0xC0, 0x30, 0xFF, 0xFF, 0xF0, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, /* ',' 0x2C */ 0xF5, 0x60, /* '-' 0x2D */ 0xFF, 0xF0, /* '.' 0x2E */ 0xF0, /* '/' 0x2F */ 0x02, 0x0C, 0x10, 0x20, 0xC1, 0x02, 0x0C, 0x10, 0x20, 0xC1, 0x02, 0x0C, 0x10, 0x20, 0xC1, 0x00, /* '0' 0x30 */ 0x1F, 0x07, 0xF1, 0xC7, 0x30, 0x6C, 0x0F, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3E, 0x0E, 0xC1, 0x9C, 0x71, 0xFC, 0x1F, 0x00, /* '1' 0x31 */ 0x08, 0xCF, 0xFF, 0x8C, 0x63, 0x18, 0xC6, 0x31, 0x8C, 0x63, 0x18, /* '2' 0x32 */ 0x1F, 0x0F, 0xF9, 0x87, 0x60, 0x7C, 0x06, 0x00, 0xC0, 0x18, 0x07, 0x01, 0xC0, 0xF0, 0x78, 0x1C, 0x06, 0x00, 0xC0, 0x30, 0x07, 0xFF, 0xFF, 0xE0, /* '3' 0x33 */ 0x3F, 0x0F, 0xF3, 0x87, 0x60, 0x6C, 0x0C, 0x01, 0x80, 0x60, 0x78, 0x0F, 0x80, 0x18, 0x01, 0x80, 0x3C, 0x07, 0x80, 0xD8, 0x73, 0xFC, 0x3F, 0x00, /* '4' 0x34 */ 0x01, 0x80, 0x70, 0x0E, 0x03, 0xC0, 0xD8, 0x1B, 0x06, 0x61, 0x8C, 0x21, 0x8C, 0x33, 0x06, 0x7F, 0xFF, 0xFE, 0x03, 0x00, 0x60, 0x0C, 0x01, 0x80, /* '5' 0x35 */ 0x3F, 0xCF, 0xF9, 0x80, 0x30, 0x06, 0x00, 0xDE, 0x1F, 0xE7, 0x0E, 0x00, 0xE0, 0x0C, 0x01, 0x80, 0x30, 0x07, 0x81, 0xB8, 0x73, 0xFC, 0x1F, 0x00, /* '6' 0x36 */ 0x0F, 0x07, 0xF9, 0xC3, 0x30, 0x74, 0x01, 0x80, 0x33, 0xC7, 0xFE, 0xF1, 0xDC, 0x1F, 0x01, 0xE0, 0x3C, 0x06, 0xC1, 0xDC, 0x71, 0xFC, 0x1F, 0x00, /* '7' 0x37 */ 0xFF, 0xFF, 0xFC, 0x01, 0x00, 0x60, 0x18, 0x02, 0x00, 0xC0, 0x30, 0x06, 0x01, 0x80, 0x30, 0x04, 0x01, 0x80, 0x30, 0x06, 0x01, 0x80, 0x30, 0x00, /* '8' 0x38 */ 0x1F, 0x07, 0xF1, 0xC7, 0x30, 0x66, 0x0C, 0xC1, 0x8C, 0x61, 0xF8, 0x3F, 0x8E, 0x3B, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xD8, 0x31, 0xFC, 0x1F, 0x00, /* '9' 0x39 */ 0x1F, 0x07, 0xF1, 0xC7, 0x70, 0x6C, 0x07, 0x80, 0xF0, 0x1E, 0x07, 0x61, 0xEF, 0xFC, 0x79, 0x80, 0x30, 0x05, 0xC1, 0x98, 0x73, 0xFC, 0x1E, 0x00, /* ':' 0x3A */ 0xF0, 0x00, 0x03, 0xC0, /* ';' 0x3B */ 0xF0, 0x00, 0x0F, 0x56, /* '<' 0x3C */ 0x00, 0x70, 0x1E, 0x0F, 0x83, 0xC0, 0xF0, 0x0E, 0x00, 0x7C, 0x00, 0xF0, 0x03, 0xC0, 0x0F, 0x00, 0x10, /* '=' 0x3D */ 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, /* '>' 0x3E */ 0xE0, 0x07, 0x80, 0x1F, 0x00, 0x7C, 0x00, 0xF0, 0x07, 0x01, 0xE0, 0xF0, 0x3C, 0x0F, 0x00, 0x80, 0x00, /* '?' 0x3F */ 0x3F, 0x1F, 0xEE, 0x1F, 0x03, 0xC0, 0xC0, 0x30, 0x0C, 0x06, 0x03, 0x81, 0xC0, 0xE0, 0x30, 0x0C, 0x03, 0x00, 0x00, 0x00, 0x0C, 0x03, 0x00, /* '@' 0x40 */ 0x00, 0xFE, 0x00, 0x0F, 0xFE, 0x00, 0xF0, 0x3E, 0x07, 0x00, 0x3C, 0x38, 0x00, 0x38, 0xC1, 0xE0, 0x66, 0x0F, 0xD9, 0xD8, 0x61, 0xC3, 0xC3, 0x07, 0x0F, 0x1C, 0x1C, 0x3C, 0x60, 0x60, 0xF1, 0x81, 0x83, 0xC6, 0x06, 0x1B, 0x18, 0x38, 0xEE, 0x71, 0xE7, 0x18, 0xFD, 0xF8, 0x71, 0xE7, 0xC0, 0xE0, 0x00, 0x01, 0xE0, 0x00, 0x01, 0xFF, 0xC0, 0x01, 0xFC, 0x00, /* 'A' 0x41 */ 0x07, 0x80, 0x1E, 0x00, 0x78, 0x03, 0xF0, 0x0C, 0xC0, 0x33, 0x01, 0xCE, 0x06, 0x18, 0x18, 0x60, 0xE1, 0xC3, 0x03, 0x0F, 0xFC, 0x7F, 0xF9, 0x80, 0x66, 0x01, 0xB8, 0x07, 0xC0, 0x0F, 0x00, 0x30, /* 'B' 0x42 */ 0xFF, 0xC7, 0xFF, 0x30, 0x1D, 0x80, 0x6C, 0x03, 0x60, 0x1B, 0x00, 0xD8, 0x0C, 0xFF, 0xC7, 0xFF, 0x30, 0x0D, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x06, 0xFF, 0xF7, 0xFE, 0x00, /* 'C' 0x43 */ 0x07, 0xE0, 0x3F, 0xF0, 0xE0, 0x73, 0x80, 0x76, 0x00, 0x6C, 0x00, 0x30, 0x00, 0x60, 0x00, 0xC0, 0x01, 0x80, 0x03, 0x00, 0x06, 0x00, 0x0E, 0x00, 0x6C, 0x00, 0xDC, 0x03, 0x1E, 0x0E, 0x1F, 0xF8, 0x0F, 0xC0, /* 'D' 0x44 */ 0xFF, 0xC3, 0xFF, 0x8C, 0x07, 0x30, 0x0E, 0xC0, 0x1B, 0x00, 0x7C, 0x00, 0xF0, 0x03, 0xC0, 0x0F, 0x00, 0x3C, 0x00, 0xF0, 0x03, 0xC0, 0x1F, 0x00, 0x6C, 0x03, 0xB0, 0x1C, 0xFF, 0xE3, 0xFE, 0x00, /* 'E' 0x45 */ 0xFF, 0xFF, 0xFF, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xFF, 0xEF, 0xFE, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xFF, 0xFF, 0xFF, /* 'F' 0x46 */ 0xFF, 0xFF, 0xFF, 0x00, 0x60, 0x0C, 0x01, 0x80, 0x30, 0x06, 0x00, 0xFF, 0xDF, 0xFB, 0x00, 0x60, 0x0C, 0x01, 0x80, 0x30, 0x06, 0x00, 0xC0, 0x18, 0x00, /* 'G' 0x47 */ 0x07, 0xF0, 0x1F, 0xFC, 0x3C, 0x1E, 0x70, 0x07, 0x60, 0x03, 0xE0, 0x00, 0xC0, 0x00, 0xC0, 0x00, 0xC0, 0x7F, 0xC0, 0x7F, 0xC0, 0x03, 0xC0, 0x03, 0x60, 0x03, 0x60, 0x07, 0x30, 0x0F, 0x3C, 0x1F, 0x1F, 0xFB, 0x07, 0xE1, /* 'H' 0x48 */ 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xFF, 0xFF, 0xFF, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xC0, /* 'I' 0x49 */ 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, /* 'J' 0x4A */ 0x01, 0x80, 0xC0, 0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x01, 0x80, 0xC0, 0x60, 0x3C, 0x1E, 0x0F, 0x07, 0xC7, 0x7F, 0x1F, 0x00, /* 'K' 0x4B */ 0xC0, 0x3E, 0x03, 0xB0, 0x39, 0x83, 0x8C, 0x38, 0x63, 0x83, 0x38, 0x19, 0xC0, 0xDE, 0x07, 0xB8, 0x38, 0xE1, 0x83, 0x0C, 0x1C, 0x60, 0x73, 0x01, 0x98, 0x0E, 0xC0, 0x3E, 0x00, 0xC0, /* 'L' 0x4C */ 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xFF, 0xFF, 0xF0, /* 'M' 0x4D */ 0xE0, 0x07, 0xE0, 0x07, 0xF0, 0x0F, 0xF0, 0x0F, 0xD0, 0x0F, 0xD8, 0x1B, 0xD8, 0x1B, 0xD8, 0x1B, 0xCC, 0x33, 0xCC, 0x33, 0xCC, 0x33, 0xC6, 0x63, 0xC6, 0x63, 0xC6, 0x63, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC1, 0x83, /* 'N' 0x4E */ 0xE0, 0x1F, 0x00, 0xFC, 0x07, 0xE0, 0x3D, 0x81, 0xEE, 0x0F, 0x30, 0x79, 0xC3, 0xC6, 0x1E, 0x18, 0xF0, 0xE7, 0x83, 0x3C, 0x1D, 0xE0, 0x6F, 0x01, 0xF8, 0x0F, 0xC0, 0x3E, 0x01, 0xC0, /* 'O' 0x4F */ 0x07, 0xF0, 0x0F, 0xFE, 0x0F, 0x07, 0x86, 0x00, 0xC6, 0x00, 0x33, 0x00, 0x1B, 0x00, 0x07, 0x80, 0x03, 0xC0, 0x01, 0xE0, 0x00, 0xF0, 0x00, 0x78, 0x00, 0x36, 0x00, 0x33, 0x00, 0x18, 0xC0, 0x18, 0x78, 0x3C, 0x1F, 0xFC, 0x03, 0xF8, 0x00, /* 'P' 0x50 */ 0xFF, 0x8F, 0xFE, 0xC0, 0x6C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x06, 0xFF, 0xEF, 0xFC, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, /* 'Q' 0x51 */ 0x07, 0xF0, 0x0F, 0xFE, 0x0F, 0x07, 0x86, 0x00, 0xC6, 0x00, 0x33, 0x00, 0x1B, 0x00, 0x07, 0x80, 0x03, 0xC0, 0x01, 0xE0, 0x00, 0xF0, 0x00, 0x78, 0x00, 0x36, 0x00, 0x33, 0x01, 0x98, 0xC0, 0xFC, 0x78, 0x3C, 0x1F, 0xFF, 0x03, 0xF9, 0x80, 0x00, 0x40, /* 'R' 0x52 */ 0xFF, 0xE3, 0xFF, 0xCC, 0x03, 0xB0, 0x06, 0xC0, 0x1B, 0x00, 0x6C, 0x01, 0xB0, 0x0C, 0xFF, 0xE3, 0xFF, 0xCC, 0x03, 0xB0, 0x06, 0xC0, 0x1B, 0x00, 0x6C, 0x01, 0xB0, 0x06, 0xC0, 0x1B, 0x00, 0x70, /* 'S' 0x53 */ 0x0F, 0xE0, 0x7F, 0xC3, 0x83, 0x98, 0x07, 0x60, 0x0D, 0x80, 0x07, 0x00, 0x1E, 0x00, 0x3F, 0x80, 0x3F, 0xC0, 0x0F, 0x80, 0x07, 0xC0, 0x0F, 0x00, 0x3E, 0x00, 0xDE, 0x0E, 0x3F, 0xF0, 0x3F, 0x80, /* 'T' 0x54 */ 0xFF, 0xFF, 0xFF, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, /* 'U' 0x55 */ 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x80, 0xEE, 0x0E, 0x3F, 0xE0, 0xFC, 0x00, /* 'V' 0x56 */ 0xC0, 0x0F, 0x00, 0x7E, 0x01, 0x98, 0x06, 0x60, 0x39, 0xC0, 0xC3, 0x03, 0x0C, 0x1C, 0x38, 0x60, 0x61, 0x81, 0x8E, 0x07, 0x30, 0x0C, 0xC0, 0x37, 0x00, 0xF8, 0x01, 0xE0, 0x07, 0x80, 0x1C, 0x00, /* 'W' 0x57 */ 0xE0, 0x30, 0x1D, 0x80, 0xE0, 0x76, 0x07, 0x81, 0xDC, 0x1E, 0x06, 0x70, 0x7C, 0x18, 0xC1, 0xB0, 0xE3, 0x0C, 0xC3, 0x8C, 0x33, 0x0C, 0x38, 0xC6, 0x30, 0x67, 0x18, 0xC1, 0x98, 0x67, 0x06, 0x61, 0xD8, 0x1D, 0x83, 0x60, 0x3C, 0x0D, 0x80, 0xF0, 0x3E, 0x03, 0xC0, 0x70, 0x0F, 0x01, 0xC0, 0x18, 0x07, 0x00, /* 'X' 0x58 */ 0xE0, 0x1D, 0x80, 0xE7, 0x03, 0x0E, 0x1C, 0x18, 0x60, 0x73, 0x00, 0xFC, 0x01, 0xE0, 0x07, 0x00, 0x1E, 0x00, 0xF8, 0x03, 0x30, 0x1C, 0xE0, 0xE1, 0x83, 0x07, 0x1C, 0x0E, 0xE0, 0x1B, 0x00, 0x70, /* 'Y' 0x59 */ 0xC0, 0x0F, 0x80, 0x76, 0x01, 0x9C, 0x0C, 0x38, 0x70, 0x61, 0x81, 0xCE, 0x03, 0x30, 0x0F, 0x80, 0x1E, 0x00, 0x30, 0x00, 0xC0, 0x03, 0x00, 0x0C, 0x00, 0x30, 0x00, 0xC0, 0x03, 0x00, 0x0C, 0x00, /* 'Z' 0x5A */ 0xFF, 0xFF, 0xFF, 0xC0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0x60, 0x07, 0x00, 0x70, 0x07, 0x00, 0x30, 0x03, 0x80, 0x38, 0x03, 0x80, 0x18, 0x01, 0xC0, 0x1C, 0x00, 0xFF, 0xFF, 0xFF, 0xC0, /* '[' 0x5B */ 0xFF, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCF, 0xF0, /* '\' 0x5C */ 0x81, 0x81, 0x02, 0x06, 0x04, 0x08, 0x18, 0x10, 0x20, 0x60, 0x40, 0x81, 0x81, 0x02, 0x06, 0x04, /* ']' 0x5D */ 0xFF, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x3F, 0xF0, /* '^' 0x5E */ 0x0C, 0x0E, 0x05, 0x86, 0xC3, 0x21, 0x19, 0x8C, 0x83, 0xC1, 0x80, /* '_' 0x5F */ 0xFF, 0xFE, /* '`' 0x60 */ 0xE3, 0x8C, 0x30, /* 'a' 0x61 */ 0x3F, 0x07, 0xF8, 0xE1, 0xCC, 0x0C, 0x00, 0xC0, 0x1C, 0x3F, 0xCF, 0x8C, 0xC0, 0xCC, 0x0C, 0xE3, 0xC7, 0xEF, 0x3C, 0x70, /* 'b' 0x62 */ 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0xF8, 0xDF, 0xCF, 0x0E, 0xE0, 0x7C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x03, 0xE0, 0x6F, 0x0E, 0xDF, 0xCC, 0xF8, /* 'c' 0x63 */ 0x1F, 0x0F, 0xE6, 0x1F, 0x83, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x38, 0x37, 0x1C, 0xFE, 0x1F, 0x00, /* 'd' 0x64 */ 0x00, 0x60, 0x0C, 0x01, 0x80, 0x30, 0x06, 0x3C, 0xCF, 0xFB, 0x8F, 0xE0, 0xF8, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF8, 0x3B, 0x8F, 0x3F, 0x63, 0xCC, /* 'e' 0x65 */ 0x1F, 0x07, 0xF1, 0xC7, 0x70, 0x3C, 0x07, 0xFF, 0xFF, 0xFE, 0x00, 0xC0, 0x1C, 0x0D, 0xC3, 0x1F, 0xC1, 0xF0, /* 'f' 0x66 */ 0x3B, 0xD8, 0xC6, 0x7F, 0xEC, 0x63, 0x18, 0xC6, 0x31, 0x8C, 0x63, 0x00, /* 'g' 0x67 */ 0x1E, 0x67, 0xFD, 0xC7, 0xF0, 0x7C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x7C, 0x1D, 0xC7, 0x9F, 0xB1, 0xE6, 0x00, 0xC0, 0x3E, 0x0E, 0x7F, 0xC7, 0xE0, /* 'h' 0x68 */ 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x33, 0xCD, 0xFB, 0xC7, 0xE0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x30, /* 'i' 0x69 */ 0xF0, 0x3F, 0xFF, 0xFF, 0xF0, /* 'j' 0x6A */ 0x33, 0x00, 0x03, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x3F, 0xE0, /* 'k' 0x6B */ 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x6C, 0x33, 0x18, 0xCC, 0x37, 0x0F, 0xC3, 0xB8, 0xC6, 0x31, 0xCC, 0x3B, 0x06, 0xC1, 0xF0, 0x30, /* 'l' 0x6C */ 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, /* 'm' 0x6D */ 0xCF, 0x1F, 0x6F, 0xDF, 0xFC, 0x78, 0xFC, 0x18, 0x3C, 0x0C, 0x1E, 0x06, 0x0F, 0x03, 0x07, 0x81, 0x83, 0xC0, 0xC1, 0xE0, 0x60, 0xF0, 0x30, 0x78, 0x18, 0x3C, 0x0C, 0x18, /* 'n' 0x6E */ 0xCF, 0x37, 0xEF, 0x1F, 0x83, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xC0, /* 'o' 0x6F */ 0x1F, 0x07, 0xF1, 0xC7, 0x70, 0x7C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x7C, 0x1D, 0xC7, 0x1F, 0xC1, 0xF0, /* 'p' 0x70 */ 0xCF, 0x8D, 0xFC, 0xF0, 0xEE, 0x06, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x3E, 0x06, 0xF0, 0xEF, 0xFC, 0xCF, 0x8C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x00, /* 'q' 0x71 */ 0x1E, 0x67, 0xFD, 0xC7, 0xF0, 0x7C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x7C, 0x1D, 0xC7, 0x9F, 0xF1, 0xE6, 0x00, 0xC0, 0x18, 0x03, 0x00, 0x60, /* 'r' 0x72 */ 0xCF, 0x7F, 0x38, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC0, /* 's' 0x73 */ 0x3E, 0x1F, 0xEE, 0x1B, 0x00, 0xC0, 0x3C, 0x07, 0xF0, 0x3F, 0x01, 0xF0, 0x3E, 0x1D, 0xFE, 0x3F, 0x00, /* 't' 0x74 */ 0x63, 0x19, 0xFF, 0xB1, 0x8C, 0x63, 0x18, 0xC6, 0x31, 0xE7, /* 'u' 0x75 */ 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x7E, 0x3D, 0xFB, 0x3C, 0xC0, /* 'v' 0x76 */ 0xE0, 0x6C, 0x0D, 0x81, 0xB8, 0x63, 0x0C, 0x61, 0x8E, 0x60, 0xCC, 0x19, 0x83, 0xE0, 0x3C, 0x07, 0x00, 0xE0, /* 'w' 0x77 */ 0xC1, 0xC1, 0xB0, 0xE1, 0xD8, 0x70, 0xCC, 0x2C, 0x66, 0x36, 0x31, 0x9B, 0x18, 0xCD, 0x98, 0x64, 0x6C, 0x16, 0x36, 0x0F, 0x1A, 0x07, 0x8F, 0x03, 0x83, 0x80, 0xC1, 0xC0, /* 'x' 0x78 */ 0xC1, 0xF8, 0x66, 0x30, 0xCC, 0x3E, 0x07, 0x00, 0xC0, 0x78, 0x36, 0x0C, 0xC6, 0x3B, 0x06, 0xC0, 0xC0, /* 'y' 0x79 */ 0xE0, 0x6C, 0x0D, 0x83, 0x38, 0x63, 0x0C, 0x63, 0x0C, 0x60, 0xCC, 0x1B, 0x03, 0x60, 0x3C, 0x07, 0x00, 0xE0, 0x18, 0x03, 0x00, 0xE0, 0x78, 0x0E, 0x00, /* 'z' 0x7A */ 0xFF, 0xFF, 0xF0, 0x18, 0x0C, 0x07, 0x03, 0x81, 0xC0, 0x60, 0x30, 0x18, 0x0E, 0x03, 0xFF, 0xFF, 0xC0, /* '{' 0x7B */ 0x19, 0xCC, 0x63, 0x18, 0xC6, 0x31, 0x99, 0x86, 0x18, 0xC6, 0x31, 0x8C, 0x63, 0x1C, 0x60, /* '|' 0x7C */ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, /* '}' 0x7D */ 0xC7, 0x18, 0xC6, 0x31, 0x8C, 0x63, 0x0C, 0x33, 0x31, 0x8C, 0x63, 0x18, 0xC6, 0x73, 0x00, /* '~' 0x7E */ 0x70, 0x3E, 0x09, 0xE4, 0x1F, 0x03, 0x80, /* 0x7F */ /* 0x80 */ 0x01, 0xF0, 0x1F, 0xF0, 0xE0, 0xC7, 0x00, 0x18, 0x00, 0xC0, 0x07, 0xFF, 0x3F, 0xFC, 0x30, 0x01, 0xFF, 0x8F, 0xFC, 0x0C, 0x00, 0x18, 0x00, 0x70, 0x00, 0xE0, 0x81, 0xFE, 0x03, 0xF0, /* 0x81 */ /* 0x82 */ 0xF5, 0x80, /* 0x83 */ /* 0x84 */ 0xCF, 0x34, 0x51, 0x88, /* 0x85 */ 0xC6, 0x3C, 0x63, /* 0x86 */ 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x3F, 0xFF, 0xFC, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x00, /* 0x87 */ 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x3F, 0xFF, 0xFC, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x0F, 0xFF, 0xFF, 0x0C, 0x03, 0x00, 0xC0, 0x30, /* 0x88 */ /* 0x89 */ 0x38, 0x18, 0x00, 0xF8, 0x30, 0x03, 0x18, 0xC0, 0x04, 0x11, 0x80, 0x0C, 0x66, 0x00, 0x0F, 0x8C, 0x00, 0x0E, 0x30, 0x00, 0x00, 0x40, 0x00, 0x01, 0x80, 0x00, 0x06, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x31, 0xC0, 0xE0, 0x67, 0xC3, 0xC1, 0x98, 0xCC, 0xC3, 0x20, 0x90, 0x8C, 0x63, 0x33, 0x10, 0x7C, 0x3C, 0x60, 0x70, 0x38, /* 0x8A */ 0x0C, 0x40, 0x1F, 0x00, 0x38, 0x03, 0xF8, 0x1F, 0xF0, 0xE0, 0xE6, 0x01, 0xD8, 0x03, 0x60, 0x01, 0xC0, 0x07, 0x80, 0x0F, 0xE0, 0x0F, 0xF0, 0x03, 0xE0, 0x01, 0xF0, 0x03, 0xC0, 0x0F, 0x80, 0x37, 0x83, 0x8F, 0xFC, 0x0F, 0xE0, /* 0x8B */ 0x2F, 0x49, 0x99, /* 0x8C */ 0x01, 0x80, 0x0C, 0x00, 0x60, 0x00, 0x00, 0x0F, 0xE0, 0x7F, 0xC3, 0x83, 0x98, 0x07, 0x60, 0x0D, 0x80, 0x07, 0x00, 0x1E, 0x00, 0x3F, 0x80, 0x3F, 0xC0, 0x0F, 0x80, 0x07, 0xC0, 0x0F, 0x00, 0x3E, 0x00, 0xDE, 0x0E, 0x3F, 0xF0, 0x3F, 0x80, /* 0x8D */ 0x0C, 0xC0, 0xF8, 0x07, 0x0F, 0xFF, 0xFF, 0xF0, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, /* 0x8E */ 0x0C, 0xC0, 0x3C, 0x00, 0xE1, 0xFF, 0xFF, 0xFF, 0x80, 0x1C, 0x01, 0xC0, 0x1C, 0x00, 0xC0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0x60, 0x07, 0x00, 0x70, 0x07, 0x00, 0x30, 0x03, 0x80, 0x38, 0x01, 0xFF, 0xFF, 0xFF, 0x80, /* 0x8F */ 0x01, 0x80, 0x18, 0x01, 0x80, 0x00, 0x0F, 0xFF, 0xFF, 0xFC, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x06, 0x00, 0x70, 0x07, 0x00, 0x70, 0x03, 0x00, 0x38, 0x03, 0x80, 0x38, 0x01, 0x80, 0x1C, 0x01, 0xC0, 0x0F, 0xFF, 0xFF, 0xFC, /* 0x90 */ /* 0x91 */ 0x6A, 0xF0, /* 0x92 */ 0xF5, 0x60, /* 0x93 */ 0x4E, 0x28, 0xA2, 0xCF, 0x30, /* 0x94 */ 0xCF, 0x34, 0x51, 0x4E, 0x20, /* 0x95 */ 0x7B, 0xFF, 0xFF, 0xFD, 0xE0, /* 0x96 */ 0xFF, 0xFF, 0xF0, /* 0x97 */ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, /* 0x98 */ /* 0x99 */ 0xFF, 0x70, 0x1F, 0xFD, 0xC0, 0x71, 0x87, 0x83, 0xC6, 0x1E, 0x0F, 0x18, 0x68, 0x3C, 0x61, 0xB1, 0xB1, 0x86, 0xC6, 0xC6, 0x19, 0x1B, 0x18, 0x66, 0xCC, 0x61, 0x9B, 0x31, 0x86, 0x3C, 0xC6, 0x18, 0xE3, 0x18, 0x63, 0x8C, /* 0x9A */ 0x63, 0x0D, 0x83, 0x60, 0x70, 0x00, 0x0F, 0x87, 0xFB, 0x86, 0xC0, 0x30, 0x0F, 0x01, 0xFC, 0x0F, 0xC0, 0x7C, 0x0F, 0x87, 0x7F, 0x8F, 0xC0, /* 0x9B */ 0x99, 0x92, 0xF4, /* 0x9C */ 0x07, 0x03, 0x80, 0xC0, 0x60, 0x00, 0x0F, 0x87, 0xFB, 0x86, 0xC0, 0x30, 0x0F, 0x01, 0xFC, 0x0F, 0xC0, 0x7C, 0x0F, 0x87, 0x7F, 0x8F, 0xC0, /* 0x9D */ 0x03, 0x06, 0x66, 0x64, 0x60, 0xF8, 0xF8, 0x60, 0x60, 0x60, 0x60, 0x60, 0x60, 0x60, 0x60, 0x60, 0x78, 0x38, /* 0x9E */ 0x63, 0x0C, 0x83, 0x60, 0x70, 0x00, 0x3F, 0xFF, 0xFC, 0x06, 0x03, 0x01, 0xC0, 0xE0, 0x70, 0x18, 0x0C, 0x06, 0x03, 0x80, 0xFF, 0xFF, 0xF0, /* 0x9F */ 0x07, 0x01, 0x80, 0xC0, 0x20, 0x00, 0x3F, 0xFF, 0xFC, 0x06, 0x03, 0x01, 0xC0, 0xE0, 0x70, 0x18, 0x0C, 0x06, 0x03, 0x80, 0xFF, 0xFF, 0xF0, /* 0xA0 */ /* 0xA1 */ 0xC6, 0xD9, 0xB1, 0xC0, /* 0xA2 */ 0x83, 0x8D, 0xF1, 0xC0, /* 0xA3 */ 0x30, 0x01, 0x80, 0x0C, 0x00, 0x60, 0x03, 0x00, 0x18, 0x80, 0xCC, 0x06, 0xC0, 0x3C, 0x01, 0xC0, 0x3C, 0x01, 0x60, 0x03, 0x00, 0x18, 0x00, 0xC0, 0x06, 0x00, 0x3F, 0xF9, 0xFF, 0xC0, /* 0xA4 */ 0xDD, 0xFF, 0xD8, 0xD8, 0x3C, 0x1E, 0x0F, 0x8D, 0xFF, 0xDD, 0x80, /* 0xA5 */ 0x03, 0x80, 0x03, 0xC0, 0x07, 0xC0, 0x07, 0xC0, 0x04, 0xE0, 0x0C, 0xE0, 0x0C, 0xE0, 0x08, 0x70, 0x18, 0x70, 0x18, 0x70, 0x10, 0x38, 0x3F, 0xF8, 0x3F, 0xF8, 0x30, 0x1C, 0x70, 0x0C, 0x60, 0x0C, 0x60, 0x0E, 0xE0, 0x06, 0x00, 0x0E, 0x00, 0x18, 0x00, 0x18, 0x00, 0x18, 0x00, 0x0F, /* 0xA6 */ 0xFF, 0xFF, 0xF0, 0x3F, 0xFF, 0xFC, /* 0xA7 */ 0x0F, 0x03, 0xF0, 0xE7, 0x18, 0x63, 0x0C, 0x70, 0x07, 0x03, 0xF8, 0xC3, 0x98, 0x3B, 0x03, 0xF0, 0x37, 0x06, 0x78, 0xC7, 0xB0, 0x7C, 0x03, 0x80, 0x39, 0x83, 0x30, 0x67, 0x1C, 0x7F, 0x07, 0xC0, /* 0xA8 */ 0xCF, 0x30, /* 0xA9 */ 0x03, 0xF0, 0x03, 0xFF, 0x01, 0xE0, 0xE0, 0xE3, 0x1C, 0x73, 0xF3, 0x99, 0x86, 0x6C, 0xC1, 0x8F, 0x30, 0x03, 0xCC, 0x00, 0xF3, 0x00, 0x3C, 0xC1, 0x8D, 0x98, 0x66, 0x77, 0xF3, 0x8E, 0x79, 0xC1, 0xC0, 0xE0, 0x3F, 0xF0, 0x03, 0xF0, 0x00, /* 0xAA */ 0x0F, 0xC0, 0xFF, 0xC3, 0x03, 0x98, 0x07, 0x60, 0x0D, 0x80, 0x07, 0x00, 0x1E, 0x00, 0x3F, 0x80, 0x3F, 0xC0, 0x0F, 0x80, 0x07, 0xC0, 0x0F, 0x00, 0x3E, 0x00, 0xDC, 0x0E, 0x3F, 0xF0, 0x3F, 0x00, 0x20, 0x01, 0xE0, 0x01, 0x80, 0x06, 0x00, 0xF0, 0x00, /* 0xAB */ 0x21, 0x63, 0xE7, 0x84, 0x84, 0xE7, 0x63, 0x21, /* 0xAC */ 0xFF, 0xFF, 0xFF, 0x00, 0x30, 0x03, 0x00, 0x30, 0x03, /* 0xAD */ 0xFF, 0xF0, /* 0xAE */ 0x03, 0xF0, 0x03, 0xFF, 0x01, 0xE0, 0xE0, 0xFF, 0x1C, 0x7F, 0xF3, 0x9B, 0x04, 0x6C, 0xC1, 0x8F, 0x30, 0x43, 0xCF, 0xF0, 0xF3, 0xFC, 0x3C, 0xC1, 0x0D, 0xB0, 0x66, 0x7C, 0x1B, 0x8F, 0x07, 0xC1, 0xC0, 0xE0, 0x3F, 0xF0, 0x03, 0xF0, 0x00, /* 0xAF */ 0x03, 0x00, 0x18, 0x00, 0x01, 0xFF, 0xFF, 0xFF, 0x80, 0x1C, 0x01, 0xC0, 0x1C, 0x00, 0xC0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0x60, 0x07, 0x00, 0x70, 0x07, 0x00, 0x30, 0x03, 0x80, 0x38, 0x01, 0xFF, 0xFF, 0xFF, 0x80, /* 0xB0 */ 0x38, 0xFB, 0x1C, 0x18, 0x38, 0xDF, 0x1C, /* 0xB1 */ 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x7F, 0xE7, 0xFE, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x00, 0x00, 0x0F, 0xFF, 0xFF, 0xF0, /* 0xB2 */ 0x76, 0x31, 0x87, 0x80, /* 0xB3 */ 0x66, 0x66, 0x66, 0x67, 0x7E, 0xE6, 0x66, 0x66, 0x66, /* 0xB4 */ 0x3B, 0x99, 0x80, /* 0xB5 */ 0xC0, 0xCC, 0x0C, 0xC0, 0xCC, 0x0C, 0xC0, 0xCC, 0x0C, 0xC0, 0xCC, 0x0C, 0xC0, 0xCC, 0x1C, 0xE3, 0xCF, 0xEF, 0xFC, 0x7C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x00, /* 0xB6 */ 0x1F, 0xE7, 0xFD, 0xF3, 0x7E, 0x6F, 0xCD, 0xF9, 0xBF, 0x37, 0xE6, 0x7C, 0xCF, 0x98, 0xF3, 0x06, 0x60, 0xCC, 0x19, 0x83, 0x30, 0x66, 0x0C, 0xC1, 0x98, 0x33, 0x06, 0x60, 0xCC, /* 0xB7 */ 0xF0, /* 0xB8 */ 0x10, 0xF0, 0xE3, 0x78, /* 0xB9 */ 0x1F, 0x01, 0xFC, 0x1C, 0x70, 0xC1, 0x80, 0x0C, 0x00, 0xE0, 0xFF, 0x1F, 0x18, 0xC0, 0xC6, 0x06, 0x38, 0x70, 0xFF, 0xE3, 0xC7, 0x00, 0x30, 0x03, 0x00, 0x18, 0x00, 0xC0, 0x03, 0xC0, /* 0xBA */ 0x3F, 0x1F, 0xEE, 0x1B, 0x00, 0xC0, 0x3C, 0x07, 0xF0, 0x3E, 0x01, 0xF0, 0x3C, 0x0D, 0xDE, 0x7F, 0x02, 0x01, 0xE0, 0x18, 0x46, 0x0F, 0x00, /* 0xBB */ 0x88, 0xC6, 0xE7, 0x21, 0x21, 0xE7, 0xC6, 0x88, /* 0xBC */ 0xC3, 0x31, 0x8C, 0x63, 0x10, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xFF, 0xFF, 0xF0, /* 0xBD */ 0x77, 0x66, 0xCC, 0xC8, /* 0xBE */ 0xC7, 0x9B, 0x36, 0xCC, 0x18, 0x30, 0x60, 0xC1, 0x83, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x80, /* 0xBF */ 0x0C, 0x03, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xF0, 0x18, 0x0C, 0x07, 0x03, 0x81, 0xC0, 0x60, 0x30, 0x18, 0x0E, 0x03, 0xFF, 0xFF, 0xC0, /* 0xC0 */ 0x03, 0x80, 0x18, 0x00, 0x40, 0x00, 0x00, 0xFF, 0xE3, 0xFF, 0xCC, 0x03, 0xB0, 0x06, 0xC0, 0x1B, 0x00, 0x6C, 0x01, 0xB0, 0x0C, 0xFF, 0xE3, 0xFF, 0xCC, 0x03, 0xB0, 0x06, 0xC0, 0x1B, 0x00, 0x6C, 0x01, 0xB0, 0x06, 0xC0, 0x1B, 0x00, 0x70, /* 0xC1 */ 0x01, 0xC0, 0x0C, 0x00, 0x20, 0x00, 0x00, 0x07, 0x80, 0x1E, 0x00, 0x78, 0x03, 0xF0, 0x0C, 0xC0, 0x33, 0x01, 0xCE, 0x06, 0x18, 0x18, 0x60, 0xE1, 0xC3, 0x03, 0x0F, 0xFC, 0x7F, 0xF9, 0x80, 0x66, 0x01, 0xB8, 0x07, 0xC0, 0x0F, 0x00, 0x30, /* 0xC2 */ 0x07, 0x00, 0x3E, 0x01, 0x8C, 0x00, 0x00, 0x07, 0x80, 0x1E, 0x00, 0x78, 0x03, 0xF0, 0x0C, 0xC0, 0x33, 0x01, 0xCE, 0x06, 0x18, 0x18, 0x60, 0xE1, 0xC3, 0x03, 0x0F, 0xFC, 0x7F, 0xF9, 0x80, 0x66, 0x01, 0xB8, 0x07, 0xC0, 0x0F, 0x00, 0x30, /* 0xC3 */ 0x10, 0x40, 0x63, 0x00, 0xF8, 0x01, 0xE0, 0x07, 0x80, 0x1E, 0x00, 0x78, 0x03, 0xF0, 0x0C, 0xC0, 0x33, 0x01, 0xCE, 0x06, 0x18, 0x18, 0x60, 0xE1, 0xC3, 0x03, 0x0F, 0xFC, 0x7F, 0xF9, 0x80, 0x66, 0x01, 0xB8, 0x07, 0xC0, 0x0F, 0x00, 0x30, /* 0xC4 */ 0x0C, 0xC0, 0x33, 0x00, 0x00, 0x01, 0xE0, 0x07, 0x80, 0x1E, 0x00, 0xFC, 0x03, 0x30, 0x0C, 0xC0, 0x73, 0x81, 0x86, 0x06, 0x18, 0x38, 0x70, 0xC0, 0xC3, 0xFF, 0x1F, 0xFE, 0x60, 0x19, 0x80, 0x6E, 0x01, 0xF0, 0x03, 0xC0, 0x0C, /* 0xC5 */ 0x18, 0x0C, 0x06, 0x00, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xFF, 0xFF, 0xF0, /* 0xC6 */ 0x00, 0xC0, 0x03, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x7E, 0x03, 0xFF, 0x0E, 0x07, 0x38, 0x07, 0x60, 0x06, 0xC0, 0x03, 0x00, 0x06, 0x00, 0x0C, 0x00, 0x18, 0x00, 0x30, 0x00, 0x60, 0x00, 0xE0, 0x06, 0xC0, 0x0D, 0xC0, 0x31, 0xE0, 0xE1, 0xFF, 0x80, 0xFC, 0x00, /* 0xC7 */ 0x07, 0xE0, 0x3F, 0xF0, 0xE0, 0x73, 0x80, 0x66, 0x00, 0x7C, 0x00, 0x30, 0x00, 0x60, 0x00, 0xC0, 0x01, 0x80, 0x03, 0x00, 0x06, 0x00, 0x0E, 0x00, 0x6C, 0x01, 0xDC, 0x03, 0x1C, 0x1E, 0x1F, 0xF8, 0x0F, 0xC0, 0x08, 0x00, 0x1E, 0x00, 0x0C, 0x01, 0x18, 0x01, 0xE0, 0x00, /* 0xC8 */ 0x06, 0x30, 0x07, 0xC0, 0x07, 0x00, 0x3F, 0x01, 0xFF, 0x87, 0x03, 0x9C, 0x03, 0xB0, 0x03, 0x60, 0x01, 0x80, 0x03, 0x00, 0x06, 0x00, 0x0C, 0x00, 0x18, 0x00, 0x30, 0x00, 0x70, 0x03, 0x60, 0x06, 0xE0, 0x18, 0xF0, 0x70, 0xFF, 0xC0, 0x7E, 0x00, /* 0xC9 */ 0x07, 0x00, 0x60, 0x0C, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xFF, 0xEF, 0xFE, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xFF, 0xFF, 0xFF, /* 0xCA */ 0xFF, 0xE7, 0xFF, 0x30, 0x01, 0x80, 0x0C, 0x00, 0x60, 0x03, 0x00, 0x18, 0x00, 0xFF, 0xE7, 0xFF, 0x30, 0x01, 0x80, 0x0C, 0x00, 0x60, 0x03, 0x00, 0x18, 0x00, 0xFF, 0xF7, 0xFF, 0x80, 0x18, 0x01, 0x80, 0x0C, 0x00, 0x60, 0x01, 0xE0, /* 0xCB */ 0x19, 0x81, 0x98, 0x00, 0x0F, 0xFF, 0xFF, 0xFC, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0F, 0xFE, 0xFF, 0xEC, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0F, 0xFF, 0xFF, 0xF0, /* 0xCC */ 0x08, 0xC0, 0xF8, 0x07, 0x0F, 0xFF, 0xFF, 0xFC, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0F, 0xFE, 0xFF, 0xEC, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0F, 0xFF, 0xFF, 0xF0, /* 0xCD */ 0x36, 0xC0, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, /* 0xCE */ 0x39, 0xFC, 0x40, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC0, /* 0xCF */ 0x18, 0xC0, 0x3E, 0x00, 0x70, 0x3F, 0xF0, 0xFF, 0xE3, 0x01, 0xCC, 0x03, 0xB0, 0x06, 0xC0, 0x1F, 0x00, 0x3C, 0x00, 0xF0, 0x03, 0xC0, 0x0F, 0x00, 0x3C, 0x00, 0xF0, 0x07, 0xC0, 0x1B, 0x00, 0xEC, 0x07, 0x3F, 0xF8, 0xFF, 0x80, /* 0xD0 */ 0x7F, 0xE0, 0xFF, 0xE1, 0x80, 0xE3, 0x00, 0xE6, 0x00, 0xCC, 0x01, 0xD8, 0x01, 0xB0, 0x03, 0xFE, 0x07, 0xFC, 0x0D, 0x80, 0x1B, 0x00, 0x36, 0x00, 0x6C, 0x01, 0x98, 0x07, 0x30, 0x1C, 0x7F, 0xF0, 0xFF, 0xC0, /* 0xD1 */ 0x01, 0x80, 0x18, 0x01, 0x80, 0x00, 0x0E, 0x01, 0xF0, 0x0F, 0xC0, 0x7E, 0x03, 0xD8, 0x1E, 0xE0, 0xF3, 0x07, 0x9C, 0x3C, 0x61, 0xE1, 0x8F, 0x0E, 0x78, 0x33, 0xC1, 0xDE, 0x06, 0xF0, 0x1F, 0x80, 0xFC, 0x03, 0xE0, 0x1C, /* 0xD2 */ 0x0C, 0xC0, 0x3C, 0x00, 0xE1, 0xC0, 0x3E, 0x01, 0xF8, 0x0F, 0xC0, 0x7B, 0x03, 0xDC, 0x1E, 0x60, 0xF3, 0x87, 0x8C, 0x3C, 0x31, 0xE1, 0xCF, 0x06, 0x78, 0x3B, 0xC0, 0xDE, 0x03, 0xF0, 0x1F, 0x80, 0x7C, 0x03, 0x80, /* 0xD3 */ 0x00, 0xE0, 0x00, 0x60, 0x00, 0x40, 0x00, 0x00, 0x00, 0x7F, 0x00, 0xFF, 0xE0, 0xF0, 0x78, 0x60, 0x0C, 0x60, 0x03, 0x30, 0x01, 0xB0, 0x00, 0x78, 0x00, 0x3C, 0x00, 0x1E, 0x00, 0x0F, 0x00, 0x07, 0x80, 0x03, 0x60, 0x03, 0x30, 0x01, 0x8C, 0x01, 0x87, 0x83, 0xC1, 0xFF, 0xC0, 0x3F, 0x80, /* 0xD4 */ 0x03, 0xC0, 0x01, 0xE0, 0x01, 0x98, 0x00, 0x00, 0x00, 0x7F, 0x00, 0xFF, 0xE0, 0xF0, 0x78, 0x60, 0x0C, 0x60, 0x03, 0x30, 0x01, 0xB0, 0x00, 0x78, 0x00, 0x3C, 0x00, 0x1E, 0x00, 0x0F, 0x00, 0x07, 0x80, 0x03, 0x60, 0x03, 0x30, 0x01, 0x8C, 0x01, 0x87, 0x83, 0xC1, 0xFF, 0xC0, 0x3F, 0x80, /* 0xD5 */ 0x03, 0xB8, 0x01, 0x98, 0x00, 0x98, 0x00, 0x00, 0x00, 0x7F, 0x00, 0xFF, 0xE0, 0xF0, 0x78, 0x60, 0x0C, 0x60, 0x03, 0x30, 0x01, 0xB0, 0x00, 0x78, 0x00, 0x3C, 0x00, 0x1E, 0x00, 0x0F, 0x00, 0x07, 0x80, 0x03, 0x60, 0x03, 0x30, 0x01, 0x8C, 0x01, 0x87, 0x83, 0xC1, 0xFF, 0xC0, 0x3F, 0x80, /* 0xD6 */ 0x06, 0x30, 0x03, 0x18, 0x00, 0x00, 0x00, 0xFE, 0x01, 0xFF, 0xC1, 0xE0, 0xF0, 0xC0, 0x18, 0xC0, 0x06, 0x60, 0x03, 0x60, 0x00, 0xF0, 0x00, 0x78, 0x00, 0x3C, 0x00, 0x1E, 0x00, 0x0F, 0x00, 0x06, 0xC0, 0x06, 0x60, 0x03, 0x18, 0x03, 0x0F, 0x07, 0x83, 0xFF, 0x80, 0x7F, 0x00, /* 0xD7 */ 0x81, 0xC3, 0x66, 0x3C, 0x18, 0x3C, 0x66, 0xC3, 0x81, /* 0xD8 */ 0x0C, 0xC0, 0x1E, 0x00, 0x78, 0x3F, 0xF8, 0xFF, 0xF3, 0x00, 0xEC, 0x01, 0xB0, 0x06, 0xC0, 0x1B, 0x00, 0x6C, 0x03, 0x3F, 0xF8, 0xFF, 0xF3, 0x00, 0xEC, 0x01, 0xB0, 0x06, 0xC0, 0x1B, 0x00, 0x6C, 0x01, 0xB0, 0x06, 0xC0, 0x1C, /* 0xD9 */ 0x03, 0x00, 0x7C, 0x03, 0x70, 0x19, 0x80, 0xF8, 0x03, 0xC3, 0x00, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3E, 0x03, 0xB8, 0x38, 0xFF, 0x83, 0xF0, /* 0xDA */ 0x03, 0x80, 0x18, 0x01, 0x80, 0x00, 0x0C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xF8, 0x0E, 0xE0, 0xE3, 0xFE, 0x0F, 0xC0, /* 0xDB */ 0x0E, 0xE0, 0x66, 0x03, 0x60, 0x00, 0x0C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xF8, 0x0E, 0xE0, 0xE3, 0xFE, 0x0F, 0xC0, /* 0xDC */ 0x0C, 0xC0, 0x66, 0x00, 0x01, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1F, 0x01, 0xDC, 0x1C, 0x7F, 0xC1, 0xF8, 0x00, /* 0xDD */ 0x01, 0x80, 0x0C, 0x00, 0x60, 0x00, 0x00, 0xC0, 0x0F, 0x80, 0x76, 0x01, 0x9C, 0x0C, 0x38, 0x70, 0x61, 0x81, 0xCE, 0x03, 0x30, 0x0F, 0x80, 0x1E, 0x00, 0x30, 0x00, 0xC0, 0x03, 0x00, 0x0C, 0x00, 0x30, 0x00, 0xC0, 0x03, 0x00, 0x0C, 0x00, /* 0xDE */ 0xFF, 0xFF, 0xFF, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x04, 0x00, 0x78, 0x01, 0x81, 0x18, 0x0F, 0x00, /* 0xDF */ 0x1F, 0x0F, 0xF3, 0x87, 0x60, 0x6C, 0x0D, 0x81, 0xB0, 0x66, 0x38, 0xC7, 0xD8, 0x1B, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x3E, 0x0E, 0xCF, 0x99, 0xE0, /* 0xE0 */ 0x0C, 0x61, 0x8C, 0x03, 0x3D, 0xFC, 0xE3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x00, /* 0xE1 */ 0x07, 0x00, 0x60, 0x0C, 0x01, 0x80, 0x00, 0x03, 0xF0, 0x7F, 0x8E, 0x1C, 0xC0, 0xC0, 0x0C, 0x01, 0xC3, 0xFC, 0xF8, 0xCC, 0x0C, 0xC0, 0xCE, 0x3C, 0x7E, 0xF3, 0xC7, /* 0xE2 */ 0x0C, 0x01, 0xE0, 0x1B, 0x03, 0x30, 0x00, 0x03, 0xF0, 0x7F, 0x8E, 0x1C, 0xC0, 0xC0, 0x0C, 0x01, 0xC3, 0xFC, 0xF8, 0xCC, 0x0C, 0xC0, 0xCE, 0x3C, 0x7E, 0xF3, 0xC7, /* 0xE3 */ 0x20, 0x82, 0x10, 0x3F, 0x01, 0xE0, 0x00, 0x03, 0xF0, 0x7F, 0x8E, 0x1C, 0xC0, 0xC0, 0x0C, 0x01, 0xC3, 0xFC, 0xF8, 0xCC, 0x0C, 0xC0, 0xCE, 0x3C, 0x7E, 0xF3, 0xC7, /* 0xE4 */ 0x19, 0x81, 0x98, 0x00, 0x00, 0x00, 0x3F, 0x07, 0xF8, 0xE1, 0xCC, 0x0C, 0x00, 0xC0, 0x1C, 0x3F, 0xCF, 0x8C, 0xC0, 0xCC, 0x0C, 0xE3, 0xC7, 0xEF, 0x3C, 0x70, /* 0xE5 */ 0x3B, 0x30, 0x06, 0x31, 0x8C, 0x63, 0x18, 0xC6, 0x31, 0x8C, 0x63, 0x18, 0xC6, 0x30, /* 0xE6 */ 0x07, 0x01, 0x80, 0xC0, 0x20, 0x00, 0x07, 0xC3, 0xF9, 0x87, 0xE0, 0xF0, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0E, 0x0D, 0xC7, 0x3F, 0x87, 0xC0, /* 0xE7 */ 0x1F, 0x0F, 0xE7, 0x1D, 0x83, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x38, 0x37, 0x1C, 0xFE, 0x1F, 0x02, 0x00, 0xE0, 0x0C, 0x23, 0x07, 0x80, /* 0xE8 */ 0x21, 0x0C, 0xC1, 0xE0, 0x78, 0x00, 0x07, 0xC3, 0xF9, 0x87, 0xE0, 0xF0, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0E, 0x0D, 0xC7, 0x3F, 0x87, 0xC0, /* 0xE9 */ 0x03, 0x00, 0xC0, 0x30, 0x04, 0x00, 0x00, 0x3E, 0x0F, 0xE3, 0x8E, 0xE0, 0x78, 0x0F, 0xFF, 0xFF, 0xFC, 0x01, 0x80, 0x38, 0x1B, 0x86, 0x3F, 0x83, 0xE0, /* 0xEA */ 0x1F, 0x07, 0xF1, 0x87, 0x60, 0x6C, 0x07, 0xFF, 0xFF, 0xFE, 0x00, 0xC0, 0x1C, 0x1D, 0x87, 0x1F, 0xC1, 0xF8, 0x06, 0x01, 0x80, 0x30, 0x06, 0x00, 0x78, /* 0xEB */ 0x31, 0x86, 0x30, 0x00, 0x00, 0x01, 0xF0, 0x7F, 0x1C, 0x77, 0x03, 0xC0, 0x7F, 0xFF, 0xFF, 0xE0, 0x0C, 0x01, 0xC0, 0xDC, 0x31, 0xFC, 0x1F, 0x00, /* 0xEC */ 0x31, 0x82, 0x60, 0x6C, 0x07, 0x00, 0x00, 0x3E, 0x0F, 0xE3, 0x8E, 0xE0, 0x78, 0x0F, 0xFF, 0xFF, 0xFC, 0x01, 0x80, 0x38, 0x1B, 0x86, 0x3F, 0x83, 0xE0, /* 0xED */ 0x39, 0x99, 0x80, 0x18, 0xC6, 0x31, 0x8C, 0x63, 0x18, 0xC6, 0x31, 0x80, /* 0xEE */ 0x71, 0xED, 0xA3, 0x00, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC0, /* 0xEF */ 0x00, 0x67, 0x00, 0x66, 0x00, 0x64, 0x00, 0x6C, 0x00, 0x60, 0x1E, 0x60, 0x3F, 0xE0, 0x71, 0xE0, 0xE0, 0xE0, 0xC0, 0x60, 0xC0, 0x60, 0xC0, 0x60, 0xC0, 0x60, 0xC0, 0x60, 0xE0, 0xE0, 0x71, 0xE0, 0x3F, 0x60, 0x1E, 0x60, /* 0xF0 */ 0x00, 0x60, 0x06, 0x03, 0xF0, 0x06, 0x00, 0x61, 0xE6, 0x3F, 0xE7, 0x1E, 0xE0, 0xEC, 0x06, 0xC0, 0x6C, 0x06, 0xC0, 0x6C, 0x06, 0xE0, 0xE7, 0x1E, 0x3F, 0xE1, 0xE6, /* 0xF1 */ 0x03, 0x81, 0xC0, 0x60, 0x30, 0x00, 0x33, 0xCD, 0xFB, 0xC7, 0xE0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x30, /* 0xF2 */ 0x31, 0x84, 0xC1, 0xA0, 0x38, 0x00, 0x33, 0xCD, 0xFB, 0xC7, 0xE0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x30, /* 0xF3 */ 0x07, 0x00, 0xC0, 0x30, 0x04, 0x00, 0x00, 0x3E, 0x0F, 0xE3, 0x8E, 0xE0, 0xF8, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF8, 0x3B, 0x8E, 0x3F, 0x83, 0xE0, /* 0xF4 */ 0x0C, 0x03, 0xC0, 0xD8, 0x19, 0x80, 0x00, 0x3E, 0x0F, 0xE3, 0x8E, 0xE0, 0xF8, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF8, 0x3B, 0x8E, 0x3F, 0x83, 0xE0, /* 0xF5 */ 0x0C, 0xC3, 0xB8, 0x66, 0x0D, 0x80, 0x00, 0x3E, 0x0F, 0xE3, 0x8E, 0xE0, 0xF8, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF8, 0x3B, 0x8E, 0x3F, 0x83, 0xE0, /* 0xF6 */ 0x31, 0x86, 0x30, 0x00, 0x00, 0x01, 0xF0, 0x7F, 0x1C, 0x77, 0x07, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0xC1, 0xDC, 0x71, 0xFC, 0x1F, 0x00, /* 0xF7 */ 0x06, 0x00, 0x60, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x06, 0x00, /* 0xF8 */ 0xC7, 0x37, 0x8E, 0x03, 0x3D, 0xFC, 0xE3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x00, /* 0xF9 */ 0x0E, 0x07, 0xC1, 0xB8, 0x6C, 0x1F, 0x03, 0x8C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x07, 0xE3, 0xDF, 0xB3, 0xCC, /* 0xFA */ 0x03, 0x01, 0x80, 0x60, 0x30, 0x00, 0x30, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x1F, 0x8F, 0x7E, 0xCF, 0x30, /* 0xFB */ 0x1D, 0xC6, 0x61, 0xB0, 0xCC, 0x00, 0x30, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x1F, 0x8F, 0x7E, 0xCF, 0x30, /* 0xFC */ 0x31, 0x8C, 0x60, 0x00, 0x00, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x7E, 0x3D, 0xFB, 0x3C, 0xC0, /* 0xFD */ 0x03, 0x80, 0x60, 0x18, 0x06, 0x00, 0x01, 0xC0, 0xD8, 0x1B, 0x06, 0x70, 0xC6, 0x18, 0xC6, 0x18, 0xC1, 0x98, 0x36, 0x06, 0xC0, 0x78, 0x0E, 0x01, 0xC0, 0x30, 0x06, 0x01, 0xC0, 0xF0, 0x1C, 0x00, /* 0xFE */ 0x61, 0x86, 0x3E, 0xF9, 0x86, 0x18, 0x61, 0x86, 0x18, 0x61, 0x87, 0x8E, 0x10, 0x83, 0xC3, 0x78, /* 0xFF */ 0xF0, }; const GFXglyph FreeSans12pt_Win1250Glyphs[] PROGMEM = { /* 0x01 */ { 0, 19, 20, 21, 1, -17 }, /* 0x02 */ { 48, 19, 20, 21, 1, -17 }, /* 0x03 */ { 96, 21, 20, 23, 1, -17 }, /* 0x04 */ { 149, 21, 20, 23, 1, -17 }, /* 0x05 */ { 202, 20, 20, 22, 1, -17 }, /* 0x06 */ { 252, 20, 20, 22, 1, -17 }, /* 0x07 */ { 302, 0, 0, 8, 0, 0 }, /* 0x08 */ { 302, 23, 20, 25, 1, -17 }, /* 0x09 */ { 360, 23, 16, 25, 1, -16 }, /* 0x0A */ { 406, 0, 0, 8, 0, 0 }, /* 0x0B */ { 406, 21, 20, 23, 1, -17 }, /* 0x0C */ { 459, 19, 18, 21, 1, -15 }, /* 0x0D */ { 502, 0, 0, 8, 0, 0 }, /* 0x0E */ { 502, 20, 20, 22, 1, -17 }, /* 0x0F */ { 552, 21, 21, 23, 1, -18 }, /* 0x10 */ { 608, 19, 20, 21, 1, -17 }, /* 0x11 */ { 656, 21, 20, 23, 1, -17 }, /* 0x12 */ { 709, 20, 20, 22, 1, -17 }, /* 0x13 */ { 759, 21, 20, 23, 1, -17 }, /* 0x14 */ { 812, 21, 20, 23, 1, -17 }, /* 0x15 */ { 865, 22, 20, 24, 1, -17 }, /* 0x16 */ { 920, 16, 20, 18, 1, -17 }, /* 0x17 */ { 960, 21, 20, 23, 1, -17 }, /* 0x18 */ { 1013, 23, 20, 25, 1, -17 }, /* 0x19 */ { 1071, 21, 20, 23, 1, -17 }, /* 0x1A */ { 1124, 15, 19, 17, 1, -16 }, /* 0x1B */ { 1160, 24, 21, 26, 1, -18 }, /* 0x1C */ { 1223, 21, 20, 23, 1, -17 }, /* 0x1D */ { 1276, 21, 21, 23, 1, -18 }, /* 0x1E */ { 1332, 20, 20, 22, 1, -17 }, /* 0x1F */ { 1382, 15, 20, 17, 1, -17 }, /* ' ' 0x20 */ { 1420, 0, 0, 6, 0, 0 }, /* '!' 0x21 */ { 1420, 2, 18, 8, 3, -16 }, /* '"' 0x22 */ { 1425, 6, 6, 8, 1, -15 }, /* '#' 0x23 */ { 1430, 13, 16, 13, 0, -14 }, /* '$' 0x24 */ { 1456, 11, 20, 13, 1, -16 }, /* '%' 0x25 */ { 1484, 20, 17, 21, 1, -15 }, /* '&' 0x26 */ { 1527, 14, 17, 16, 1, -15 }, /* ''' 0x27 */ { 1557, 2, 6, 5, 1, -15 }, /* '(' 0x28 */ { 1559, 5, 23, 8, 2, -16 }, /* ')' 0x29 */ { 1574, 5, 23, 8, 1, -16 }, /* '*' 0x2A */ { 1589, 7, 7, 9, 1, -16 }, /* '+' 0x2B */ { 1596, 10, 11, 14, 2, -9 }, /* ',' 0x2C */ { 1610, 2, 6, 7, 2, 0 }, /* '-' 0x2D */ { 1612, 6, 2, 8, 1, -6 }, /* '.' 0x2E */ { 1614, 2, 2, 6, 2, 0 }, /* '/' 0x2F */ { 1615, 7, 18, 7, 0, -16 }, /* '0' 0x30 */ { 1631, 11, 17, 13, 1, -15 }, /* '1' 0x31 */ { 1655, 5, 17, 13, 3, -15 }, /* '2' 0x32 */ { 1666, 11, 17, 13, 1, -15 }, /* '3' 0x33 */ { 1690, 11, 17, 13, 1, -15 }, /* '4' 0x34 */ { 1714, 11, 17, 13, 1, -15 }, /* '5' 0x35 */ { 1738, 11, 17, 13, 1, -15 }, /* '6' 0x36 */ { 1762, 11, 17, 13, 1, -15 }, /* '7' 0x37 */ { 1786, 11, 17, 13, 1, -15 }, /* '8' 0x38 */ { 1810, 11, 17, 13, 1, -15 }, /* '9' 0x39 */ { 1834, 11, 17, 13, 1, -15 }, /* ':' 0x3A */ { 1858, 2, 13, 6, 2, -11 }, /* ';' 0x3B */ { 1862, 2, 16, 6, 2, -10 }, /* '<' 0x3C */ { 1866, 12, 11, 14, 1, -9 }, /* '=' 0x3D */ { 1883, 12, 6, 14, 1, -7 }, /* '>' 0x3E */ { 1892, 12, 11, 14, 1, -9 }, /* '?' 0x3F */ { 1909, 10, 18, 13, 2, -16 }, /* '@' 0x40 */ { 1932, 22, 21, 24, 1, -16 }, /* 'A' 0x41 */ { 1990, 14, 18, 16, 1, -16 }, /* 'B' 0x42 */ { 2022, 13, 18, 16, 2, -16 }, /* 'C' 0x43 */ { 2052, 15, 18, 17, 1, -16 }, /* 'D' 0x44 */ { 2086, 14, 18, 17, 2, -16 }, /* 'E' 0x45 */ { 2118, 12, 18, 15, 2, -16 }, /* 'F' 0x46 */ { 2145, 11, 18, 14, 2, -16 }, /* 'G' 0x47 */ { 2170, 16, 18, 18, 1, -16 }, /* 'H' 0x48 */ { 2206, 13, 18, 17, 2, -16 }, /* 'I' 0x49 */ { 2236, 2, 18, 7, 2, -16 }, /* 'J' 0x4A */ { 2241, 9, 18, 13, 1, -16 }, /* 'K' 0x4B */ { 2262, 13, 18, 16, 2, -16 }, /* 'L' 0x4C */ { 2292, 10, 18, 14, 2, -16 }, /* 'M' 0x4D */ { 2315, 16, 18, 20, 2, -16 }, /* 'N' 0x4E */ { 2351, 13, 18, 18, 2, -16 }, /* 'O' 0x4F */ { 2381, 17, 18, 19, 1, -16 }, /* 'P' 0x50 */ { 2420, 12, 18, 16, 2, -16 }, /* 'Q' 0x51 */ { 2447, 17, 19, 19, 1, -16 }, /* 'R' 0x52 */ { 2488, 14, 18, 17, 2, -16 }, /* 'S' 0x53 */ { 2520, 14, 18, 16, 1, -16 }, /* 'T' 0x54 */ { 2552, 12, 18, 15, 1, -16 }, /* 'U' 0x55 */ { 2579, 13, 18, 17, 2, -16 }, /* 'V' 0x56 */ { 2609, 14, 18, 15, 1, -16 }, /* 'W' 0x57 */ { 2641, 22, 18, 22, 0, -16 }, /* 'X' 0x58 */ { 2691, 14, 18, 16, 1, -16 }, /* 'Y' 0x59 */ { 2723, 14, 18, 16, 1, -16 }, /* 'Z' 0x5A */ { 2755, 13, 18, 15, 1, -16 }, /* '[' 0x5B */ { 2785, 4, 23, 7, 2, -16 }, /* '\' 0x5C */ { 2797, 7, 18, 7, 0, -16 }, /* ']' 0x5D */ { 2813, 4, 23, 7, 1, -16 }, /* '^' 0x5E */ { 2825, 9, 9, 11, 1, -15 }, /* '_' 0x5F */ { 2836, 15, 1, 13, -1, 5 }, /* '`' 0x60 */ { 2838, 5, 4, 6, 1, -16 }, /* 'a' 0x61 */ { 2841, 12, 13, 13, 1, -11 }, /* 'b' 0x62 */ { 2861, 12, 18, 13, 1, -16 }, /* 'c' 0x63 */ { 2888, 10, 13, 12, 1, -11 }, /* 'd' 0x64 */ { 2905, 11, 18, 13, 1, -16 }, /* 'e' 0x65 */ { 2930, 11, 13, 13, 1, -11 }, /* 'f' 0x66 */ { 2948, 5, 18, 7, 1, -16 }, /* 'g' 0x67 */ { 2960, 11, 18, 13, 1, -11 }, /* 'h' 0x68 */ { 2985, 10, 18, 13, 1, -16 }, /* 'i' 0x69 */ { 3008, 2, 18, 5, 2, -16 }, /* 'j' 0x6A */ { 3013, 4, 23, 6, 0, -16 }, /* 'k' 0x6B */ { 3025, 10, 18, 12, 1, -16 }, /* 'l' 0x6C */ { 3048, 2, 18, 5, 1, -16 }, /* 'm' 0x6D */ { 3053, 17, 13, 19, 1, -11 }, /* 'n' 0x6E */ { 3081, 10, 13, 13, 1, -11 }, /* 'o' 0x6F */ { 3098, 11, 13, 13, 1, -11 }, /* 'p' 0x70 */ { 3116, 12, 17, 13, 1, -11 }, /* 'q' 0x71 */ { 3142, 11, 17, 13, 1, -11 }, /* 'r' 0x72 */ { 3166, 6, 13, 8, 1, -11 }, /* 's' 0x73 */ { 3176, 10, 13, 12, 1, -11 }, /* 't' 0x74 */ { 3193, 5, 16, 7, 1, -14 }, /* 'u' 0x75 */ { 3203, 10, 13, 13, 1, -11 }, /* 'v' 0x76 */ { 3220, 11, 13, 12, 0, -11 }, /* 'w' 0x77 */ { 3238, 17, 13, 17, 0, -11 }, /* 'x' 0x78 */ { 3266, 10, 13, 11, 1, -11 }, /* 'y' 0x79 */ { 3283, 11, 18, 11, 0, -11 }, /* 'z' 0x7A */ { 3308, 10, 13, 12, 1, -11 }, /* '{' 0x7B */ { 3325, 5, 23, 8, 1, -16 }, /* '|' 0x7C */ { 3340, 2, 23, 6, 2, -16 }, /* '}' 0x7D */ { 3346, 5, 23, 8, 2, -16 }, /* '~' 0x7E */ { 3361, 10, 5, 12, 1, -9 }, /* 0x7F */ { 3368, 0, 0, 0, 0, 0 }, /* 0x80 */ { 3368, 14, 17, 16, 1, -17 }, /* 0x81 */ { 3398, 0, 0, 8, 0, 0 }, /* 0x82 */ { 3398, 2, 5, 6, 2, -2 }, /* 0x83 */ { 3400, 0, 0, 8, 0, 0 }, /* 0x84 */ { 3400, 6, 5, 10, 2, -2 }, /* 0x85 */ { 3404, 12, 2, 16, 2, -2 }, /* 0x86 */ { 3407, 10, 21, 13, 2, -17 }, /* 0x87 */ { 3434, 10, 20, 13, 2, -17 }, /* 0x88 */ { 3459, 0, 0, 8, 0, 0 }, /* 0x89 */ { 3459, 23, 18, 24, 0, -18 }, /* 0x8A */ { 3511, 14, 21, 16, 1, -21 }, /* 0x8B */ { 3548, 3, 8, 6, 1, -11 }, /* 0x8C */ { 3551, 14, 22, 16, 1, -22 }, /* 0x8D */ { 3590, 12, 21, 15, 1, -21 }, /* 0x8E */ { 3622, 13, 21, 15, 1, -21 }, /* 0x8F */ { 3657, 13, 22, 15, 1, -22 }, /* 0x90 */ { 3693, 0, 0, 8, 0, 0 }, /* 0x91 */ { 3693, 2, 6, 6, 2, -18 }, /* 0x92 */ { 3695, 2, 6, 6, 2, -18 }, /* 0x93 */ { 3697, 6, 6, 10, 2, -18 }, /* 0x94 */ { 3702, 6, 6, 10, 2, -18 }, /* 0x95 */ { 3707, 6, 6, 10, 2, -11 }, /* 0x96 */ { 3712, 10, 2, 12, 1, -8 }, /* 0x97 */ { 3715, 22, 2, 24, 1, -8 }, /* 0x98 */ { 3721, 0, 0, 8, 0, 0 }, /* 0x99 */ { 3721, 22, 13, 24, 2, -18 }, /* 0x9A */ { 3757, 10, 18, 12, 1, -18 }, /* 0x9B */ { 3780, 3, 8, 6, 2, -10 }, /* 0x9C */ { 3783, 10, 18, 12, 1, -18 }, /* 0x9D */ { 3806, 8, 18, 11, 1, -18 }, /* 0x9E */ { 3824, 10, 18, 12, 1, -18 }, /* 0x9F */ { 3847, 10, 18, 12, 1, -18 }, /* 0xA0 */ { 3870, 0, 0, 7, 0, 0 }, /* 0xA1 */ { 3870, 7, 4, 8, 0, -18 }, /* 0xA2 */ { 3874, 7, 4, 8, 0, -18 }, /* 0xA3 */ { 3878, 13, 18, 15, 1, -18 }, /* 0xA4 */ { 3908, 9, 9, 13, 2, -13 }, /* 0xA5 */ { 3919, 16, 23, 16, 1, -18 }, /* 0xA6 */ { 3965, 2, 23, 6, 2, -18 }, /* 0xA7 */ { 3971, 11, 23, 13, 1, -18 }, /* 0xA8 */ { 4003, 6, 2, 8, 1, -17 }, /* 0xA9 */ { 4005, 18, 17, 19, 1, -17 }, /* 0xAA */ { 4044, 14, 23, 16, 1, -18 }, /* 0xAB */ { 4085, 8, 8, 12, 2, -11 }, /* 0xAC */ { 4093, 12, 6, 14, 1, -9 }, /* 0xAD */ { 4102, 6, 2, 8, 1, -8 }, /* 0xAE */ { 4104, 18, 17, 19, 1, -17 }, /* 0xAF */ { 4143, 13, 21, 15, 1, -21 }, /* 0xB0 */ { 4178, 7, 8, 15, 4, -17 }, /* 0xB1 */ { 4185, 12, 15, 14, 1, -15 }, /* 0xB2 */ { 4208, 5, 5, 8, 1, 0 }, /* 0xB3 */ { 4212, 4, 18, 6, 1, -18 }, /* 0xB4 */ { 4221, 5, 4, 8, 2, -18 }, /* 0xB5 */ { 4224, 12, 17, 13, 2, -13 }, /* 0xB6 */ { 4250, 11, 21, 13, 2, -18 }, /* 0xB7 */ { 4279, 2, 2, 6, 2, -8 }, /* 0xB8 */ { 4280, 6, 5, 8, 1, 0 }, /* 0xB9 */ { 4284, 13, 18, 13, 1, -13 }, /* 0xBA */ { 4314, 10, 18, 12, 1, -13 }, /* 0xBB */ { 4337, 8, 8, 12, 2, -10 }, /* 0xBC */ { 4345, 10, 18, 14, 2, -18 }, /* 0xBD */ { 4368, 8, 4, 8, 0, -18 }, /* 0xBE */ { 4372, 7, 18, 9, 1, -18 }, /* 0xBF */ { 4388, 10, 17, 12, 1, -17 }, /* 0xC0 */ { 4410, 14, 22, 17, 2, -22 }, /* 0xC1 */ { 4449, 14, 22, 16, 1, -22 }, /* 0xC2 */ { 4488, 14, 22, 16, 1, -22 }, /* 0xC3 */ { 4527, 14, 22, 16, 1, -22 }, /* 0xC4 */ { 4566, 14, 21, 16, 1, -21 }, /* 0xC5 */ { 4603, 10, 22, 14, 2, -22 }, /* 0xC6 */ { 4631, 15, 22, 17, 1, -22 }, /* 0xC7 */ { 4673, 15, 23, 17, 1, -18 }, /* 0xC8 */ { 4717, 15, 21, 17, 1, -21 }, /* 0xC9 */ { 4757, 12, 22, 15, 2, -22 }, /* 0xCA */ { 4790, 13, 23, 15, 2, -18 }, /* 0xCB */ { 4828, 12, 21, 15, 2, -21 }, /* 0xCC */ { 4860, 12, 21, 15, 2, -21 }, /* 0xCD */ { 4892, 4, 22, 7, 1, -22 }, /* 0xCE */ { 4903, 6, 22, 7, 0, -22 }, /* 0xCF */ { 4920, 14, 21, 17, 2, -21 }, /* 0xD0 */ { 4957, 15, 18, 17, 1, -18 }, /* 0xD1 */ { 4991, 13, 22, 18, 2, -22 }, /* 0xD2 */ { 5027, 13, 21, 18, 2, -21 }, /* 0xD3 */ { 5062, 17, 22, 19, 1, -22 }, /* 0xD4 */ { 5109, 17, 22, 19, 1, -22 }, /* 0xD5 */ { 5156, 17, 22, 19, 1, -22 }, /* 0xD6 */ { 5203, 17, 21, 19, 1, -21 }, /* 0xD7 */ { 5248, 8, 9, 14, 3, -10 }, /* 0xD8 */ { 5257, 14, 21, 17, 2, -21 }, /* 0xD9 */ { 5294, 13, 24, 17, 2, -24 }, /* 0xDA */ { 5333, 13, 22, 17, 2, -22 }, /* 0xDB */ { 5369, 13, 22, 17, 2, -22 }, /* 0xDC */ { 5405, 13, 21, 17, 2, -21 }, /* 0xDD */ { 5440, 14, 22, 16, 1, -22 }, /* 0xDE */ { 5479, 12, 23, 15, 1, -18 }, /* 0xDF */ { 5514, 11, 18, 14, 2, -18 }, /* 0xE0 */ { 5539, 6, 18, 8, 1, -18 }, /* 0xE1 */ { 5553, 12, 18, 13, 1, -18 }, /* 0xE2 */ { 5580, 12, 18, 13, 1, -18 }, /* 0xE3 */ { 5607, 12, 18, 13, 1, -18 }, /* 0xE4 */ { 5634, 12, 17, 13, 1, -17 }, /* 0xE5 */ { 5660, 5, 22, 6, 0, -22 }, /* 0xE6 */ { 5674, 10, 18, 12, 1, -18 }, /* 0xE7 */ { 5697, 10, 18, 12, 1, -13 }, /* 0xE8 */ { 5720, 10, 18, 12, 1, -18 }, /* 0xE9 */ { 5743, 11, 18, 13, 1, -18 }, /* 0xEA */ { 5768, 11, 18, 13, 1, -13 }, /* 0xEB */ { 5793, 11, 17, 13, 1, -17 }, /* 0xEC */ { 5817, 11, 18, 13, 1, -18 }, /* 0xED */ { 5842, 5, 18, 5, 0, -18 }, /* 0xEE */ { 5854, 6, 18, 6, 0, -18 }, /* 0xEF */ { 5868, 16, 18, 18, 1, -18 }, /* 0xF0 */ { 5904, 12, 18, 14, 1, -18 }, /* 0xF1 */ { 5931, 10, 18, 13, 1, -18 }, /* 0xF2 */ { 5954, 10, 18, 13, 1, -18 }, /* 0xF3 */ { 5977, 11, 18, 13, 1, -18 }, /* 0xF4 */ { 6002, 11, 18, 13, 1, -18 }, /* 0xF5 */ { 6027, 11, 18, 13, 1, -18 }, /* 0xF6 */ { 6052, 11, 17, 13, 1, -17 }, /* 0xF7 */ { 6076, 12, 11, 14, 1, -11 }, /* 0xF8 */ { 6093, 6, 18, 8, 1, -18 }, /* 0xF9 */ { 6107, 10, 19, 13, 1, -19 }, /* 0xFA */ { 6131, 10, 18, 13, 1, -18 }, /* 0xFB */ { 6154, 10, 18, 13, 1, -18 }, /* 0xFC */ { 6177, 10, 17, 13, 1, -17 }, /* 0xFD */ { 6199, 11, 23, 11, 0, -18 }, /* 0xFE */ { 6231, 6, 21, 7, 1, -16 }, /* 0xFF */ { 6247, 2, 2, 8, 3, -17 }, }; const GFXfont FreeSans12pt_Win1250 PROGMEM = { (uint8_t*)FreeSans12pt_Win1250Bitmaps, (GFXglyph*)FreeSans12pt_Win1250Glyphs, 0x01, 0xFF, 19 }; ================================================ FILE: src/graphics/niche/Fonts/FreeSans12pt_Win1251.h ================================================ // trunk-ignore-all(clang-format) #pragma once /* PROPERTIES FONT_NAME FreeSans12pt_Win1251 */ const uint8_t FreeSans12pt_Win1251Bitmaps[] PROGMEM = { /* 0x01 */ 0x00, 0x30, 0x00, 0x09, 0x00, 0x01, 0x20, 0x00, 0x24, 0x00, 0x04, 0x80, 0x01, 0x90, 0x00, 0x62, 0x00, 0x30, 0xFE, 0x04, 0x10, 0x5F, 0x02, 0x0B, 0x00, 0x7F, 0xE0, 0x0C, 0x1C, 0x02, 0x83, 0x81, 0x9F, 0xF0, 0x02, 0x1E, 0x00, 0x41, 0xC0, 0x0E, 0x7F, 0x81, 0x78, 0x18, 0x62, 0x00, 0xFF, 0xC0, /* 0x02 */ 0x00, 0xFF, 0x80, 0x61, 0x13, 0xF0, 0x62, 0x60, 0x07, 0xFC, 0x00, 0x83, 0x80, 0x10, 0xF0, 0x33, 0xF6, 0x01, 0x41, 0xC0, 0x18, 0x38, 0x03, 0xFF, 0xE0, 0x47, 0x02, 0x08, 0x20, 0x61, 0xC4, 0x06, 0x17, 0x00, 0x22, 0x00, 0x02, 0x40, 0x00, 0x48, 0x00, 0x09, 0x00, 0x01, 0x20, 0x00, 0x3C, 0x00, /* 0x03 */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x00, 0x02, 0x30, 0x00, 0x09, 0x04, 0x08, 0x48, 0x70, 0xE1, 0xC3, 0x87, 0x0E, 0x08, 0x10, 0x70, 0x00, 0x03, 0x80, 0x00, 0x14, 0x00, 0x00, 0xA1, 0x81, 0x8D, 0x87, 0xF0, 0x44, 0x00, 0x06, 0x30, 0x00, 0x60, 0xC0, 0x06, 0x03, 0x80, 0x60, 0x07, 0xFC, 0x00, /* 0x04 */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x00, 0x02, 0x30, 0x00, 0x09, 0x10, 0x02, 0x48, 0xE0, 0x61, 0xC1, 0xCC, 0x0E, 0x78, 0x1C, 0x70, 0x00, 0x03, 0x80, 0x00, 0x14, 0xFF, 0xFC, 0xA6, 0x00, 0xCD, 0x9F, 0xFE, 0x44, 0x71, 0xE6, 0x30, 0xFC, 0x60, 0xC0, 0x06, 0x03, 0x80, 0x60, 0x07, 0xFC, 0x00, /* 0x05 */ 0x00, 0x18, 0x00, 0x00, 0x40, 0x01, 0x90, 0x01, 0xF4, 0x08, 0x12, 0x23, 0xC1, 0x91, 0x2C, 0x1C, 0x8A, 0xC3, 0x64, 0x64, 0x13, 0x22, 0x41, 0x98, 0x26, 0x2C, 0xC4, 0x22, 0x60, 0x42, 0x13, 0x04, 0x30, 0x80, 0x61, 0xA4, 0x02, 0x18, 0x20, 0x03, 0x41, 0x00, 0x20, 0x08, 0x02, 0x00, 0x60, 0x40, 0x03, 0xF8, /* 0x06 */ 0x00, 0x10, 0x00, 0x03, 0x00, 0x1C, 0x48, 0x00, 0xB4, 0x80, 0x09, 0xF9, 0xC0, 0xE0, 0xE4, 0x0C, 0x02, 0x8F, 0x80, 0x38, 0x88, 0x01, 0x0D, 0x00, 0x18, 0x30, 0x01, 0x60, 0x80, 0x13, 0x18, 0x03, 0xF2, 0xC0, 0x20, 0x26, 0x06, 0x07, 0xFF, 0xA0, 0x02, 0x39, 0x00, 0x14, 0x70, 0x01, 0xC3, 0x00, 0x18, 0x00, /* 0x07 */ /* 0x08 */ 0x00, 0x1F, 0x80, 0x00, 0x60, 0x80, 0x01, 0x00, 0x80, 0x06, 0x00, 0x80, 0x3C, 0x01, 0x01, 0x8C, 0x02, 0x02, 0x08, 0x04, 0x04, 0x08, 0x0C, 0x38, 0x00, 0x04, 0x80, 0x00, 0x06, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x2E, 0xC0, 0x01, 0x83, 0x7E, 0x0C, 0x10, 0x37, 0xE2, 0x61, 0x00, 0x0C, 0xC6, 0x10, 0x98, 0x0C, 0x63, 0x00, 0x00, 0xC6, 0x00, /* 0x09 */ 0x00, 0x1F, 0x80, 0x00, 0x60, 0x80, 0x01, 0x00, 0x80, 0x06, 0x00, 0x80, 0x3C, 0x01, 0x01, 0x8C, 0x02, 0x02, 0x08, 0x04, 0x04, 0x08, 0x0C, 0x38, 0x00, 0x04, 0x80, 0x00, 0x06, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x2E, 0xC0, 0x01, 0x83, 0x7E, 0x0C, 0x00, 0x37, 0xE0, /* 0x0A */ /* 0x0B */ 0x1F, 0x07, 0xC1, 0x86, 0x41, 0x10, 0x0C, 0x04, 0x80, 0x40, 0x18, 0x00, 0x00, 0xC0, 0x00, 0x06, 0x00, 0x00, 0x30, 0x00, 0x01, 0x40, 0x00, 0x0A, 0x00, 0x00, 0x88, 0x00, 0x04, 0x40, 0x00, 0x41, 0x00, 0x02, 0x04, 0x00, 0x20, 0x20, 0x02, 0x00, 0x80, 0x20, 0x02, 0x02, 0x00, 0x08, 0x20, 0x00, 0x22, 0x00, 0x00, 0xE0, 0x00, /* 0x0C */ 0x01, 0x00, 0x00, 0x38, 0x00, 0x04, 0xC0, 0x01, 0x08, 0x00, 0x18, 0x80, 0x1C, 0x10, 0x02, 0x07, 0x80, 0x81, 0x10, 0x1F, 0xC2, 0x02, 0x00, 0x60, 0x80, 0x1A, 0x20, 0x1C, 0x42, 0x1C, 0x08, 0xFE, 0x03, 0xA0, 0x01, 0x8C, 0x01, 0xC1, 0x43, 0xD0, 0x27, 0x81, 0xF8, /* 0x0D */ /* 0x0E */ 0x00, 0xE0, 0x00, 0x11, 0x00, 0x01, 0x10, 0x00, 0x0B, 0x00, 0x03, 0xF8, 0x00, 0x60, 0x60, 0x09, 0x02, 0x00, 0xA0, 0x10, 0x16, 0x01, 0x01, 0x40, 0x10, 0x10, 0x01, 0x01, 0x00, 0x08, 0x10, 0x00, 0x82, 0x1F, 0x08, 0x3F, 0x90, 0x44, 0x00, 0x06, 0xBF, 0xFF, 0xAF, 0xF0, 0xFF, 0xFF, 0x0F, 0xE3, 0xFB, 0xFC, /* 0x0F */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x40, 0x12, 0x34, 0x00, 0x69, 0x40, 0x01, 0x49, 0xE0, 0xF1, 0xCD, 0x06, 0x8E, 0x28, 0x14, 0x71, 0x40, 0xA3, 0x8B, 0xFD, 0x14, 0x50, 0x68, 0xA2, 0x81, 0x4D, 0x97, 0xFA, 0x44, 0xBF, 0xD6, 0x31, 0x02, 0xE0, 0xC8, 0x16, 0x08, 0x61, 0x08, 0x21, 0xF0, 0x80, 0xF8, 0x78, 0x00, /* 0x10 */ 0x00, 0xF0, 0x00, 0x3A, 0x00, 0x07, 0xC0, 0x00, 0xA8, 0x00, 0x1F, 0x00, 0x02, 0xB0, 0x00, 0x52, 0x00, 0x0A, 0x40, 0x02, 0x48, 0x00, 0x49, 0x00, 0x09, 0x30, 0x01, 0x22, 0x01, 0xC4, 0x70, 0xF0, 0x85, 0xE1, 0x10, 0x88, 0x37, 0x20, 0x03, 0x9C, 0x00, 0x37, 0x00, 0x06, 0x40, 0x01, 0x86, 0x00, /* 0x11 */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x60, 0x02, 0x36, 0x00, 0x09, 0x04, 0x0C, 0x48, 0x60, 0xC1, 0xC3, 0x0F, 0x0E, 0x00, 0x08, 0x70, 0x00, 0x23, 0x80, 0x63, 0x84, 0x01, 0x9F, 0x20, 0x0C, 0xFD, 0x80, 0x27, 0xE4, 0x03, 0x3F, 0x30, 0x33, 0xE0, 0xC0, 0x00, 0x03, 0x80, 0x60, 0x07, 0xFC, 0x00, /* 0x12 */ 0x00, 0xC2, 0x00, 0x1C, 0x24, 0x02, 0x18, 0x60, 0x64, 0x02, 0x02, 0x40, 0x20, 0x00, 0xF2, 0x03, 0x89, 0xE0, 0x7C, 0x80, 0x0E, 0x25, 0x80, 0xE1, 0x00, 0x1A, 0x08, 0x71, 0xB0, 0xC4, 0x39, 0x84, 0xC2, 0xCC, 0x40, 0x76, 0x7C, 0x05, 0xBB, 0x80, 0x4C, 0xE0, 0x0A, 0x78, 0x00, 0x9C, 0x00, 0x0F, 0x00, 0x00, /* 0x13 */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x00, 0x02, 0x30, 0x00, 0x09, 0x00, 0x00, 0x48, 0x60, 0xC1, 0xC6, 0xC9, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x03, 0x80, 0x00, 0x14, 0xFF, 0xF8, 0xA6, 0x00, 0xCD, 0x9F, 0xFE, 0x44, 0x71, 0xE6, 0x30, 0xFC, 0x60, 0xC0, 0x06, 0x03, 0x80, 0x60, 0x07, 0xFC, 0x00, /* 0x14 */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x20, 0x22, 0x33, 0x01, 0x89, 0x20, 0x02, 0x48, 0x60, 0xE1, 0xC8, 0x80, 0x8E, 0x46, 0x46, 0x72, 0x32, 0x33, 0x9F, 0x9F, 0x94, 0x78, 0x78, 0xA0, 0x00, 0x0D, 0x80, 0x00, 0x44, 0x0E, 0x06, 0x30, 0x00, 0x60, 0xC0, 0x06, 0x03, 0x80, 0x60, 0x07, 0xFC, 0x00, /* 0x15 */ 0x03, 0xFC, 0x20, 0x38, 0x1C, 0x81, 0x80, 0x1D, 0x08, 0x00, 0x32, 0x60, 0x00, 0x89, 0x00, 0x02, 0x18, 0x00, 0x08, 0x61, 0xC3, 0x22, 0x8D, 0x93, 0x72, 0x00, 0x00, 0x48, 0x00, 0x01, 0x20, 0x00, 0x04, 0x9F, 0xFF, 0x92, 0x60, 0x0E, 0x44, 0xFF, 0xF2, 0x11, 0xC3, 0x88, 0x21, 0xF8, 0x40, 0x40, 0x02, 0x00, 0xC0, 0x30, 0x00, 0xFF, 0x00, /* 0x16 */ 0x03, 0x80, 0x03, 0xC0, 0x01, 0xE0, 0x01, 0xE0, 0x03, 0xF0, 0x03, 0xF0, 0x27, 0xF0, 0x6F, 0x70, 0x6E, 0x60, 0xFC, 0x60, 0xFC, 0x7E, 0xFC, 0x7E, 0xFC, 0x3F, 0xF4, 0x1F, 0xF4, 0x1F, 0xF0, 0x0E, 0x70, 0x0E, 0x30, 0x1C, 0x38, 0x38, 0x0F, 0xF0, /* 0x17 */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x00, 0x02, 0x30, 0x00, 0x09, 0x00, 0x00, 0x48, 0x00, 0x21, 0xC0, 0x02, 0x8E, 0x20, 0xF4, 0x70, 0x84, 0x11, 0x82, 0x40, 0x84, 0x01, 0x03, 0x20, 0x0F, 0x85, 0x80, 0x03, 0x04, 0x00, 0x04, 0x30, 0x78, 0x10, 0xC0, 0x06, 0x03, 0x80, 0x60, 0x07, 0xFC, 0x00, /* 0x18 */ 0x00, 0xFC, 0x00, 0x02, 0x06, 0x00, 0x08, 0x24, 0x00, 0x21, 0xA4, 0x00, 0x4C, 0x48, 0x00, 0xA0, 0x50, 0x01, 0x92, 0x60, 0x03, 0x24, 0xC0, 0x06, 0x01, 0x81, 0x28, 0x03, 0x49, 0x6C, 0xC4, 0xAD, 0xD8, 0x16, 0xA4, 0xCC, 0xC4, 0x44, 0x86, 0x13, 0x05, 0x00, 0x28, 0x0A, 0x00, 0x50, 0x14, 0x00, 0x90, 0x48, 0x01, 0x20, 0x90, 0x02, 0x41, 0x20, 0x00, 0x00, /* 0x19 */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x00, 0x02, 0x30, 0x00, 0x09, 0x00, 0x00, 0x49, 0xC3, 0x81, 0xC0, 0x00, 0x0E, 0x78, 0xF0, 0x77, 0xEF, 0xC3, 0xA7, 0x4E, 0x15, 0x0A, 0x10, 0xA7, 0x8F, 0x0D, 0x80, 0x00, 0x44, 0x00, 0x06, 0x33, 0xF0, 0x60, 0xC0, 0x06, 0x03, 0x80, 0x60, 0x07, 0xFC, 0x00, /* 0x1A */ 0xFF, 0xFF, 0x00, 0x06, 0x00, 0x0C, 0x3E, 0x18, 0x82, 0x32, 0x02, 0x64, 0x04, 0xC8, 0x09, 0x80, 0x23, 0x00, 0x86, 0x02, 0x0C, 0x08, 0x18, 0x10, 0x30, 0x00, 0x60, 0x00, 0xC0, 0x81, 0x80, 0x03, 0x00, 0x07, 0xFF, 0xF8, /* 0x1B */ 0x00, 0xFE, 0x00, 0x03, 0x81, 0x80, 0x04, 0x00, 0x60, 0x08, 0x00, 0x30, 0x10, 0x00, 0x10, 0x30, 0x07, 0x88, 0x23, 0xC8, 0x08, 0x22, 0x00, 0x04, 0x60, 0x00, 0x44, 0x60, 0x00, 0x84, 0x63, 0x03, 0x04, 0x61, 0xFC, 0x04, 0x6B, 0x00, 0x9E, 0xA5, 0x01, 0x6A, 0xD5, 0x01, 0x43, 0xA8, 0x81, 0x05, 0xD0, 0x82, 0x0A, 0xA0, 0x82, 0x05, 0xC0, 0x82, 0x02, 0x61, 0xFF, 0x0C, 0x1E, 0x00, 0xF0, /* 0x1C */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x30, 0x02, 0x32, 0x00, 0x09, 0x00, 0x00, 0x48, 0x20, 0x61, 0xC3, 0x84, 0x0E, 0x1C, 0x78, 0x70, 0x40, 0x03, 0x80, 0x00, 0x14, 0x00, 0x00, 0xA0, 0x03, 0x0D, 0x83, 0xF0, 0x44, 0x00, 0x06, 0x30, 0x00, 0x60, 0xC0, 0x06, 0x03, 0x80, 0x60, 0x07, 0xFC, 0x00, /* 0x1D */ 0x01, 0xFE, 0x00, 0x3A, 0x1C, 0x03, 0x00, 0x30, 0x23, 0x1E, 0xC3, 0x38, 0x03, 0x10, 0xC3, 0x09, 0x00, 0x18, 0x68, 0x00, 0xC1, 0x40, 0x00, 0x0A, 0x07, 0x80, 0x50, 0x46, 0x02, 0x80, 0x00, 0x1A, 0x1E, 0x00, 0xCB, 0x10, 0x0D, 0x03, 0x00, 0x48, 0x60, 0x06, 0x40, 0x00, 0x22, 0x0C, 0x02, 0x10, 0x60, 0x60, 0x43, 0xFC, 0x01, 0xE0, 0x00, 0x00, /* 0x1E */ 0x01, 0xF0, 0x00, 0xEA, 0xC0, 0x31, 0x5F, 0x04, 0x5F, 0x88, 0x80, 0xA0, 0x48, 0x0E, 0x02, 0x8F, 0x40, 0x3C, 0x10, 0x21, 0x66, 0x87, 0x15, 0x98, 0x71, 0x41, 0x02, 0x14, 0x00, 0x01, 0x40, 0x00, 0x14, 0x00, 0x01, 0x21, 0xFE, 0x12, 0x00, 0x02, 0x10, 0x00, 0x60, 0x80, 0x0C, 0x06, 0x01, 0x80, 0x3F, 0xE0, /* 0x1F */ 0x0E, 0x00, 0x13, 0x00, 0x23, 0x00, 0xF3, 0x01, 0x31, 0x01, 0x11, 0x03, 0xD3, 0x06, 0xF2, 0x30, 0x34, 0xC7, 0x25, 0x33, 0x2B, 0xC2, 0x57, 0x04, 0x3A, 0x08, 0x72, 0x30, 0xA3, 0xC3, 0x40, 0x04, 0x40, 0x18, 0x40, 0x60, 0x7F, 0x80, /* ' ' 0x20 */ /* '!' 0x21 */ 0xFF, 0xFF, 0xFF, 0xF0, 0xF0, /* '"' 0x22 */ 0xCF, 0x3C, 0xF3, 0x8A, 0x20, /* '#' 0x23 */ 0x06, 0x30, 0x31, 0x03, 0x18, 0x18, 0xC7, 0xFF, 0xBF, 0xFC, 0x31, 0x01, 0x18, 0x18, 0xC7, 0xFF, 0xBF, 0xFC, 0x31, 0x01, 0x18, 0x18, 0xC0, 0xC6, 0x06, 0x30, /* '$' 0x24 */ 0x04, 0x03, 0xE1, 0xFF, 0x72, 0x7C, 0x47, 0x88, 0xF1, 0x07, 0xA0, 0x7E, 0x03, 0xF0, 0x17, 0x02, 0x7C, 0x47, 0x88, 0xF1, 0x1B, 0x26, 0x7F, 0xC3, 0xE0, 0x10, 0x02, 0x00, /* '%' 0x25 */ 0x00, 0x06, 0x03, 0xC0, 0x40, 0x7E, 0x0C, 0x0E, 0x70, 0x80, 0xC3, 0x18, 0x0C, 0x31, 0x00, 0xE7, 0x30, 0x07, 0xE6, 0x00, 0x3C, 0x40, 0x00, 0x0C, 0x7C, 0x00, 0x8F, 0xE0, 0x19, 0xC7, 0x01, 0x18, 0x30, 0x31, 0x83, 0x02, 0x1C, 0x70, 0x40, 0xFE, 0x04, 0x07, 0xC0, /* '&' 0x26 */ 0x0F, 0x00, 0x7E, 0x03, 0x9C, 0x0C, 0x30, 0x30, 0xC0, 0xE7, 0x01, 0xF8, 0x03, 0x80, 0x3E, 0x01, 0xCC, 0x6E, 0x39, 0xB0, 0x7C, 0xC0, 0xF3, 0x03, 0xCE, 0x1F, 0x9F, 0xE6, 0x3E, 0x1C, /* ''' 0x27 */ 0xFF, 0xA0, /* '(' 0x28 */ 0x08, 0x8C, 0x46, 0x31, 0x98, 0xC6, 0x31, 0x8C, 0x63, 0x08, 0x63, 0x08, 0x61, 0x0C, 0x20, /* ')' 0x29 */ 0x82, 0x18, 0xC3, 0x18, 0xC3, 0x18, 0xC6, 0x31, 0x8C, 0x62, 0x31, 0x88, 0xC4, 0x62, 0x00, /* '*' 0x2A */ 0x10, 0x23, 0x5B, 0xE3, 0x8D, 0x91, 0x00, /* '+' 0x2B */ 0x0C, 0x03, 0x00, 0xC0, 0x30, 0xFF, 0xFF, 0xF0, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, /* ',' 0x2C */ 0xF5, 0x60, /* '-' 0x2D */ 0xFF, 0xF0, /* '.' 0x2E */ 0xF0, /* '/' 0x2F */ 0x02, 0x0C, 0x10, 0x20, 0xC1, 0x02, 0x0C, 0x10, 0x20, 0xC1, 0x02, 0x0C, 0x10, 0x20, 0xC1, 0x00, /* '0' 0x30 */ 0x1F, 0x07, 0xF1, 0xC7, 0x30, 0x6C, 0x0F, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3E, 0x0E, 0xC1, 0x9C, 0x71, 0xFC, 0x1F, 0x00, /* '1' 0x31 */ 0x08, 0xCF, 0xFF, 0x8C, 0x63, 0x18, 0xC6, 0x31, 0x8C, 0x63, 0x18, /* '2' 0x32 */ 0x1F, 0x0F, 0xF9, 0x87, 0x60, 0x7C, 0x06, 0x00, 0xC0, 0x18, 0x07, 0x01, 0xC0, 0xF0, 0x78, 0x1C, 0x06, 0x00, 0xC0, 0x30, 0x07, 0xFF, 0xFF, 0xE0, /* '3' 0x33 */ 0x3F, 0x0F, 0xF3, 0x87, 0x60, 0x6C, 0x0C, 0x01, 0x80, 0x60, 0x78, 0x0F, 0x80, 0x18, 0x01, 0x80, 0x3C, 0x07, 0x80, 0xD8, 0x73, 0xFC, 0x3F, 0x00, /* '4' 0x34 */ 0x01, 0x80, 0x70, 0x0E, 0x03, 0xC0, 0xD8, 0x1B, 0x06, 0x61, 0x8C, 0x21, 0x8C, 0x33, 0x06, 0x7F, 0xFF, 0xFE, 0x03, 0x00, 0x60, 0x0C, 0x01, 0x80, /* '5' 0x35 */ 0x3F, 0xCF, 0xF9, 0x80, 0x30, 0x06, 0x00, 0xDE, 0x1F, 0xE7, 0x0E, 0x00, 0xE0, 0x0C, 0x01, 0x80, 0x30, 0x07, 0x81, 0xB8, 0x73, 0xFC, 0x1F, 0x00, /* '6' 0x36 */ 0x0F, 0x07, 0xF9, 0xC3, 0x30, 0x74, 0x01, 0x80, 0x33, 0xC7, 0xFE, 0xF1, 0xDC, 0x1F, 0x01, 0xE0, 0x3C, 0x06, 0xC1, 0xDC, 0x71, 0xFC, 0x1F, 0x00, /* '7' 0x37 */ 0xFF, 0xFF, 0xFC, 0x01, 0x00, 0x60, 0x18, 0x02, 0x00, 0xC0, 0x30, 0x06, 0x01, 0x80, 0x30, 0x04, 0x01, 0x80, 0x30, 0x06, 0x01, 0x80, 0x30, 0x00, /* '8' 0x38 */ 0x1F, 0x07, 0xF1, 0xC7, 0x30, 0x66, 0x0C, 0xC1, 0x8C, 0x61, 0xF8, 0x3F, 0x8E, 0x3B, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xD8, 0x31, 0xFC, 0x1F, 0x00, /* '9' 0x39 */ 0x1F, 0x07, 0xF1, 0xC7, 0x70, 0x6C, 0x07, 0x80, 0xF0, 0x1E, 0x07, 0x61, 0xEF, 0xFC, 0x79, 0x80, 0x30, 0x05, 0xC1, 0x98, 0x73, 0xFC, 0x1E, 0x00, /* ':' 0x3A */ 0xF0, 0x00, 0x03, 0xC0, /* ';' 0x3B */ 0xF0, 0x00, 0x0F, 0x56, /* '<' 0x3C */ 0x00, 0x70, 0x1E, 0x0F, 0x83, 0xC0, 0xF0, 0x0E, 0x00, 0x7C, 0x00, 0xF0, 0x03, 0xC0, 0x0F, 0x00, 0x10, /* '=' 0x3D */ 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, /* '>' 0x3E */ 0xE0, 0x07, 0x80, 0x1F, 0x00, 0x7C, 0x00, 0xF0, 0x07, 0x01, 0xE0, 0xF0, 0x3C, 0x0F, 0x00, 0x80, 0x00, /* '?' 0x3F */ 0x3F, 0x1F, 0xEE, 0x1F, 0x03, 0xC0, 0xC0, 0x30, 0x0C, 0x06, 0x03, 0x81, 0xC0, 0xE0, 0x30, 0x0C, 0x03, 0x00, 0x00, 0x00, 0x0C, 0x03, 0x00, /* '@' 0x40 */ 0x00, 0xFE, 0x00, 0x0F, 0xFE, 0x00, 0xF0, 0x3E, 0x07, 0x00, 0x3C, 0x38, 0x00, 0x38, 0xC1, 0xE0, 0x66, 0x0F, 0xD9, 0xD8, 0x61, 0xC3, 0xC3, 0x07, 0x0F, 0x1C, 0x1C, 0x3C, 0x60, 0x60, 0xF1, 0x81, 0x83, 0xC6, 0x06, 0x1B, 0x18, 0x38, 0xEE, 0x71, 0xE7, 0x18, 0xFD, 0xF8, 0x71, 0xE7, 0xC0, 0xE0, 0x00, 0x01, 0xE0, 0x00, 0x01, 0xFF, 0xC0, 0x01, 0xFC, 0x00, /* 'A' 0x41 */ 0x07, 0x80, 0x1E, 0x00, 0x78, 0x03, 0xF0, 0x0C, 0xC0, 0x33, 0x01, 0xCE, 0x06, 0x18, 0x18, 0x60, 0xE1, 0xC3, 0x03, 0x0F, 0xFC, 0x7F, 0xF9, 0x80, 0x66, 0x01, 0xB8, 0x07, 0xC0, 0x0F, 0x00, 0x30, /* 'B' 0x42 */ 0xFF, 0xC7, 0xFF, 0x30, 0x1D, 0x80, 0x6C, 0x03, 0x60, 0x1B, 0x00, 0xD8, 0x0C, 0xFF, 0xC7, 0xFF, 0x30, 0x0D, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x06, 0xFF, 0xF7, 0xFE, 0x00, /* 'C' 0x43 */ 0x07, 0xE0, 0x3F, 0xF0, 0xE0, 0x73, 0x80, 0x76, 0x00, 0x6C, 0x00, 0x30, 0x00, 0x60, 0x00, 0xC0, 0x01, 0x80, 0x03, 0x00, 0x06, 0x00, 0x0E, 0x00, 0x6C, 0x00, 0xDC, 0x03, 0x1E, 0x0E, 0x1F, 0xF8, 0x0F, 0xC0, /* 'D' 0x44 */ 0xFF, 0xC3, 0xFF, 0x8C, 0x07, 0x30, 0x0E, 0xC0, 0x1B, 0x00, 0x7C, 0x00, 0xF0, 0x03, 0xC0, 0x0F, 0x00, 0x3C, 0x00, 0xF0, 0x03, 0xC0, 0x1F, 0x00, 0x6C, 0x03, 0xB0, 0x1C, 0xFF, 0xE3, 0xFE, 0x00, /* 'E' 0x45 */ 0xFF, 0xFF, 0xFF, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xFF, 0xEF, 0xFE, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xFF, 0xFF, 0xFF, /* 'F' 0x46 */ 0xFF, 0xFF, 0xFF, 0x00, 0x60, 0x0C, 0x01, 0x80, 0x30, 0x06, 0x00, 0xFF, 0xDF, 0xFB, 0x00, 0x60, 0x0C, 0x01, 0x80, 0x30, 0x06, 0x00, 0xC0, 0x18, 0x00, /* 'G' 0x47 */ 0x07, 0xF0, 0x1F, 0xFC, 0x3C, 0x1E, 0x70, 0x07, 0x60, 0x03, 0xE0, 0x00, 0xC0, 0x00, 0xC0, 0x00, 0xC0, 0x7F, 0xC0, 0x7F, 0xC0, 0x03, 0xC0, 0x03, 0x60, 0x03, 0x60, 0x07, 0x30, 0x0F, 0x3C, 0x1F, 0x1F, 0xFB, 0x07, 0xE1, /* 'H' 0x48 */ 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xFF, 0xFF, 0xFF, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xC0, /* 'I' 0x49 */ 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, /* 'J' 0x4A */ 0x01, 0x80, 0xC0, 0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x01, 0x80, 0xC0, 0x60, 0x3C, 0x1E, 0x0F, 0x07, 0xC7, 0x7F, 0x1F, 0x00, /* 'K' 0x4B */ 0xC0, 0x3E, 0x03, 0xB0, 0x39, 0x83, 0x8C, 0x38, 0x63, 0x83, 0x38, 0x19, 0xC0, 0xDE, 0x07, 0xB8, 0x38, 0xE1, 0x83, 0x0C, 0x1C, 0x60, 0x73, 0x01, 0x98, 0x0E, 0xC0, 0x3E, 0x00, 0xC0, /* 'L' 0x4C */ 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xFF, 0xFF, 0xF0, /* 'M' 0x4D */ 0xE0, 0x07, 0xE0, 0x07, 0xF0, 0x0F, 0xF0, 0x0F, 0xD0, 0x0F, 0xD8, 0x1B, 0xD8, 0x1B, 0xD8, 0x1B, 0xCC, 0x33, 0xCC, 0x33, 0xCC, 0x33, 0xC6, 0x63, 0xC6, 0x63, 0xC6, 0x63, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC1, 0x83, /* 'N' 0x4E */ 0xE0, 0x1F, 0x00, 0xFC, 0x07, 0xE0, 0x3D, 0x81, 0xEE, 0x0F, 0x30, 0x79, 0xC3, 0xC6, 0x1E, 0x18, 0xF0, 0xE7, 0x83, 0x3C, 0x1D, 0xE0, 0x6F, 0x01, 0xF8, 0x0F, 0xC0, 0x3E, 0x01, 0xC0, /* 'O' 0x4F */ 0x07, 0xF0, 0x0F, 0xFE, 0x0F, 0x07, 0x86, 0x00, 0xC6, 0x00, 0x33, 0x00, 0x1B, 0x00, 0x07, 0x80, 0x03, 0xC0, 0x01, 0xE0, 0x00, 0xF0, 0x00, 0x78, 0x00, 0x36, 0x00, 0x33, 0x00, 0x18, 0xC0, 0x18, 0x78, 0x3C, 0x1F, 0xFC, 0x03, 0xF8, 0x00, /* 'P' 0x50 */ 0xFF, 0x8F, 0xFE, 0xC0, 0x6C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x06, 0xFF, 0xEF, 0xFC, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, /* 'Q' 0x51 */ 0x07, 0xF0, 0x0F, 0xFE, 0x0F, 0x07, 0x86, 0x00, 0xC6, 0x00, 0x33, 0x00, 0x1B, 0x00, 0x07, 0x80, 0x03, 0xC0, 0x01, 0xE0, 0x00, 0xF0, 0x00, 0x78, 0x00, 0x36, 0x00, 0x33, 0x01, 0x98, 0xC0, 0xFC, 0x78, 0x3C, 0x1F, 0xFF, 0x03, 0xF9, 0x80, 0x00, 0x40, /* 'R' 0x52 */ 0xFF, 0xE3, 0xFF, 0xCC, 0x03, 0xB0, 0x06, 0xC0, 0x1B, 0x00, 0x6C, 0x01, 0xB0, 0x0C, 0xFF, 0xE3, 0xFF, 0xCC, 0x03, 0xB0, 0x06, 0xC0, 0x1B, 0x00, 0x6C, 0x01, 0xB0, 0x06, 0xC0, 0x1B, 0x00, 0x70, /* 'S' 0x53 */ 0x0F, 0xE0, 0x7F, 0xC3, 0x83, 0x98, 0x07, 0x60, 0x0D, 0x80, 0x07, 0x00, 0x1E, 0x00, 0x3F, 0x80, 0x3F, 0xC0, 0x0F, 0x80, 0x07, 0xC0, 0x0F, 0x00, 0x3E, 0x00, 0xDE, 0x0E, 0x3F, 0xF0, 0x3F, 0x80, /* 'T' 0x54 */ 0xFF, 0xFF, 0xFF, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, /* 'U' 0x55 */ 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x80, 0xEE, 0x0E, 0x3F, 0xE0, 0xFC, 0x00, /* 'V' 0x56 */ 0xC0, 0x0F, 0x00, 0x7E, 0x01, 0x98, 0x06, 0x60, 0x39, 0xC0, 0xC3, 0x03, 0x0C, 0x1C, 0x38, 0x60, 0x61, 0x81, 0x8E, 0x07, 0x30, 0x0C, 0xC0, 0x37, 0x00, 0xF8, 0x01, 0xE0, 0x07, 0x80, 0x1C, 0x00, /* 'W' 0x57 */ 0xE0, 0x30, 0x1D, 0x80, 0xE0, 0x76, 0x07, 0x81, 0xDC, 0x1E, 0x06, 0x70, 0x7C, 0x18, 0xC1, 0xB0, 0xE3, 0x0C, 0xC3, 0x8C, 0x33, 0x0C, 0x38, 0xC6, 0x30, 0x67, 0x18, 0xC1, 0x98, 0x67, 0x06, 0x61, 0xD8, 0x1D, 0x83, 0x60, 0x3C, 0x0D, 0x80, 0xF0, 0x3E, 0x03, 0xC0, 0x70, 0x0F, 0x01, 0xC0, 0x18, 0x07, 0x00, /* 'X' 0x58 */ 0xE0, 0x1D, 0x80, 0xE7, 0x03, 0x0E, 0x1C, 0x18, 0x60, 0x73, 0x00, 0xFC, 0x01, 0xE0, 0x07, 0x00, 0x1E, 0x00, 0xF8, 0x03, 0x30, 0x1C, 0xE0, 0xE1, 0x83, 0x07, 0x1C, 0x0E, 0xE0, 0x1B, 0x00, 0x70, /* 'Y' 0x59 */ 0xC0, 0x0F, 0x80, 0x76, 0x01, 0x9C, 0x0C, 0x38, 0x70, 0x61, 0x81, 0xCE, 0x03, 0x30, 0x0F, 0x80, 0x1E, 0x00, 0x30, 0x00, 0xC0, 0x03, 0x00, 0x0C, 0x00, 0x30, 0x00, 0xC0, 0x03, 0x00, 0x0C, 0x00, /* 'Z' 0x5A */ 0xFF, 0xFF, 0xFF, 0xC0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0x60, 0x07, 0x00, 0x70, 0x07, 0x00, 0x30, 0x03, 0x80, 0x38, 0x03, 0x80, 0x18, 0x01, 0xC0, 0x1C, 0x00, 0xFF, 0xFF, 0xFF, 0xC0, /* '[' 0x5B */ 0xFF, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCF, 0xF0, /* '\' 0x5C */ 0x81, 0x81, 0x02, 0x06, 0x04, 0x08, 0x18, 0x10, 0x20, 0x60, 0x40, 0x81, 0x81, 0x02, 0x06, 0x04, /* ']' 0x5D */ 0xFF, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x3F, 0xF0, /* '^' 0x5E */ 0x0C, 0x0E, 0x05, 0x86, 0xC3, 0x21, 0x19, 0x8C, 0x83, 0xC1, 0x80, /* '_' 0x5F */ 0xFF, 0xFE, /* '`' 0x60 */ 0xE3, 0x8C, 0x30, /* 'a' 0x61 */ 0x3F, 0x07, 0xF8, 0xE1, 0xCC, 0x0C, 0x00, 0xC0, 0x1C, 0x3F, 0xCF, 0x8C, 0xC0, 0xCC, 0x0C, 0xE3, 0xC7, 0xEF, 0x3C, 0x70, /* 'b' 0x62 */ 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0xF8, 0xDF, 0xCF, 0x0E, 0xE0, 0x7C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x03, 0xE0, 0x6F, 0x0E, 0xDF, 0xCC, 0xF8, /* 'c' 0x63 */ 0x1F, 0x0F, 0xE6, 0x1F, 0x83, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x38, 0x37, 0x1C, 0xFE, 0x1F, 0x00, /* 'd' 0x64 */ 0x00, 0x60, 0x0C, 0x01, 0x80, 0x30, 0x06, 0x3C, 0xCF, 0xFB, 0x8F, 0xE0, 0xF8, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF8, 0x3B, 0x8F, 0x3F, 0x63, 0xCC, /* 'e' 0x65 */ 0x1F, 0x07, 0xF1, 0xC7, 0x70, 0x3C, 0x07, 0xFF, 0xFF, 0xFE, 0x00, 0xC0, 0x1C, 0x0D, 0xC3, 0x1F, 0xC1, 0xF0, /* 'f' 0x66 */ 0x3B, 0xD8, 0xC6, 0x7F, 0xEC, 0x63, 0x18, 0xC6, 0x31, 0x8C, 0x63, 0x00, /* 'g' 0x67 */ 0x1E, 0x67, 0xFD, 0xC7, 0xF0, 0x7C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x7C, 0x1D, 0xC7, 0x9F, 0xB1, 0xE6, 0x00, 0xC0, 0x3E, 0x0E, 0x7F, 0xC7, 0xE0, /* 'h' 0x68 */ 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x33, 0xCD, 0xFB, 0xC7, 0xE0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x30, /* 'i' 0x69 */ 0xF0, 0x3F, 0xFF, 0xFF, 0xF0, /* 'j' 0x6A */ 0x33, 0x00, 0x03, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x3F, 0xE0, /* 'k' 0x6B */ 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x6C, 0x33, 0x18, 0xCC, 0x37, 0x0F, 0xC3, 0xB8, 0xC6, 0x31, 0xCC, 0x3B, 0x06, 0xC1, 0xF0, 0x30, /* 'l' 0x6C */ 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, /* 'm' 0x6D */ 0xCF, 0x1F, 0x6F, 0xDF, 0xFC, 0x78, 0xFC, 0x18, 0x3C, 0x0C, 0x1E, 0x06, 0x0F, 0x03, 0x07, 0x81, 0x83, 0xC0, 0xC1, 0xE0, 0x60, 0xF0, 0x30, 0x78, 0x18, 0x3C, 0x0C, 0x18, /* 'n' 0x6E */ 0xCF, 0x37, 0xEF, 0x1F, 0x83, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xC0, /* 'o' 0x6F */ 0x1F, 0x07, 0xF1, 0xC7, 0x70, 0x7C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x7C, 0x1D, 0xC7, 0x1F, 0xC1, 0xF0, /* 'p' 0x70 */ 0xCF, 0x8D, 0xFC, 0xF0, 0xEE, 0x06, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x3E, 0x06, 0xF0, 0xEF, 0xFC, 0xCF, 0x8C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x00, /* 'q' 0x71 */ 0x1E, 0x67, 0xFD, 0xC7, 0xF0, 0x7C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x7C, 0x1D, 0xC7, 0x9F, 0xF1, 0xE6, 0x00, 0xC0, 0x18, 0x03, 0x00, 0x60, /* 'r' 0x72 */ 0xCF, 0x7F, 0x38, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC0, /* 's' 0x73 */ 0x3E, 0x1F, 0xEE, 0x1B, 0x00, 0xC0, 0x3C, 0x07, 0xF0, 0x3F, 0x01, 0xF0, 0x3E, 0x1D, 0xFE, 0x3F, 0x00, /* 't' 0x74 */ 0x63, 0x19, 0xFF, 0xB1, 0x8C, 0x63, 0x18, 0xC6, 0x31, 0xE7, /* 'u' 0x75 */ 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x7E, 0x3D, 0xFB, 0x3C, 0xC0, /* 'v' 0x76 */ 0xE0, 0x6C, 0x0D, 0x81, 0xB8, 0x63, 0x0C, 0x61, 0x8E, 0x60, 0xCC, 0x19, 0x83, 0xE0, 0x3C, 0x07, 0x00, 0xE0, /* 'w' 0x77 */ 0xC1, 0xC1, 0xB0, 0xE1, 0xD8, 0x70, 0xCC, 0x2C, 0x66, 0x36, 0x31, 0x9B, 0x18, 0xCD, 0x98, 0x64, 0x6C, 0x16, 0x36, 0x0F, 0x1A, 0x07, 0x8F, 0x03, 0x83, 0x80, 0xC1, 0xC0, /* 'x' 0x78 */ 0xC1, 0xF8, 0x66, 0x30, 0xCC, 0x3E, 0x07, 0x00, 0xC0, 0x78, 0x36, 0x0C, 0xC6, 0x3B, 0x06, 0xC0, 0xC0, /* 'y' 0x79 */ 0xE0, 0x6C, 0x0D, 0x83, 0x38, 0x63, 0x0C, 0x63, 0x0C, 0x60, 0xCC, 0x1B, 0x03, 0x60, 0x3C, 0x07, 0x00, 0xE0, 0x18, 0x03, 0x00, 0xE0, 0x78, 0x0E, 0x00, /* 'z' 0x7A */ 0xFF, 0xFF, 0xF0, 0x18, 0x0C, 0x07, 0x03, 0x81, 0xC0, 0x60, 0x30, 0x18, 0x0E, 0x03, 0xFF, 0xFF, 0xC0, /* '{' 0x7B */ 0x19, 0xCC, 0x63, 0x18, 0xC6, 0x31, 0x99, 0x86, 0x18, 0xC6, 0x31, 0x8C, 0x63, 0x1C, 0x60, /* '|' 0x7C */ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, /* '}' 0x7D */ 0xC7, 0x18, 0xC6, 0x31, 0x8C, 0x63, 0x0C, 0x33, 0x31, 0x8C, 0x63, 0x18, 0xC6, 0x73, 0x00, /* '~' 0x7E */ 0x70, 0x3E, 0x09, 0xE4, 0x1F, 0x03, 0x80, /* 0x7F */ /* 0x80 */ 0xFF, 0xE0, 0xFF, 0xE0, 0x06, 0x00, 0x06, 0x00, 0x06, 0x00, 0x06, 0x00, 0x06, 0x00, 0x07, 0xFC, 0x07, 0xFE, 0x06, 0x03, 0x06, 0x03, 0x06, 0x03, 0x06, 0x03, 0x06, 0x03, 0x06, 0x03, 0x06, 0x03, 0x06, 0x03, 0x06, 0x03, 0x00, 0x03, 0x00, 0x03, 0x00, 0x1E, 0x00, 0x1C, /* 0x81 */ 0x07, 0x01, 0xC0, 0x20, 0x00, 0x0F, 0xFF, 0xFF, 0xF0, 0x06, 0x00, 0xC0, 0x18, 0x03, 0x00, 0x60, 0x0C, 0x01, 0x80, 0x30, 0x06, 0x00, 0xC0, 0x18, 0x03, 0x00, 0x60, 0x0C, 0x01, 0x80, 0x00, /* 0x82 */ 0xF5, 0x80, /* 0x83 */ 0x0C, 0x38, 0x61, 0x80, 0x1F, 0xFF, 0xE0, 0xC1, 0x83, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x80, /* 0x84 */ 0xCF, 0x34, 0x51, 0x88, /* 0x85 */ 0xC6, 0x3C, 0x63, /* 0x86 */ 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x3F, 0xFF, 0xFC, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x00, /* 0x87 */ 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x3F, 0xFF, 0xFC, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x0F, 0xFF, 0xFF, 0x0C, 0x03, 0x00, 0xC0, 0x30, /* 0x88 */ 0x01, 0xF0, 0x1F, 0xF0, 0xE0, 0xC7, 0x00, 0x18, 0x00, 0xC0, 0x07, 0xFF, 0x3F, 0xFC, 0x30, 0x01, 0xFF, 0x8F, 0xFC, 0x0C, 0x00, 0x18, 0x00, 0x70, 0x00, 0xE0, 0x81, 0xFE, 0x03, 0xF0, /* 0x89 */ 0x38, 0x18, 0x00, 0xF8, 0x30, 0x03, 0x18, 0xC0, 0x04, 0x11, 0x80, 0x0C, 0x66, 0x00, 0x0F, 0x8C, 0x00, 0x0E, 0x30, 0x00, 0x00, 0x40, 0x00, 0x01, 0x80, 0x00, 0x06, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x31, 0xC0, 0xE0, 0x67, 0xC3, 0xC1, 0x98, 0xCC, 0xC3, 0x20, 0x90, 0x8C, 0x63, 0x33, 0x10, 0x7C, 0x3C, 0x60, 0x70, 0x38, /* 0x8A */ 0x1F, 0xF8, 0x00, 0x3F, 0xF0, 0x00, 0x60, 0x60, 0x00, 0xC0, 0xC0, 0x01, 0x81, 0x80, 0x03, 0x03, 0x00, 0x06, 0x06, 0x00, 0x0C, 0x0C, 0x00, 0x18, 0x1F, 0xF0, 0x30, 0x3F, 0xF0, 0x60, 0x60, 0x30, 0xC0, 0xC0, 0x31, 0x81, 0x80, 0x66, 0x03, 0x00, 0xCC, 0x06, 0x01, 0xB8, 0x0C, 0x06, 0xE0, 0x1F, 0xFD, 0x80, 0x3F, 0xE0, /* 0x8B */ 0x2F, 0x49, 0x99, /* 0x8C */ 0xC0, 0x60, 0x06, 0x03, 0x00, 0x30, 0x18, 0x01, 0x80, 0xC0, 0x0C, 0x06, 0x00, 0x60, 0x30, 0x03, 0x01, 0x80, 0x18, 0x0C, 0x00, 0xFF, 0xFF, 0xC7, 0xFF, 0xFF, 0x30, 0x18, 0x1D, 0x80, 0xC0, 0x3C, 0x06, 0x01, 0xE0, 0x30, 0x0F, 0x01, 0x80, 0x78, 0x0C, 0x06, 0xC0, 0x7F, 0xF6, 0x03, 0xFE, 0x00, /* 0x8D */ 0x03, 0x00, 0x60, 0x0C, 0x00, 0x00, 0xC0, 0x7C, 0x0E, 0xC1, 0xCC, 0x38, 0xC7, 0x0C, 0xE0, 0xDC, 0x0F, 0x80, 0xF0, 0x0F, 0x80, 0xDC, 0x0C, 0xE0, 0xC7, 0x0C, 0x30, 0xC3, 0x8C, 0x1C, 0xC0, 0xEC, 0x07, /* 0x8E */ 0xFF, 0xE0, 0xFF, 0xE0, 0x06, 0x00, 0x06, 0x00, 0x06, 0x00, 0x06, 0x00, 0x06, 0x00, 0x07, 0xFC, 0x07, 0xFE, 0x06, 0x03, 0x06, 0x03, 0x06, 0x03, 0x06, 0x03, 0x06, 0x03, 0x06, 0x03, 0x06, 0x03, 0x06, 0x03, 0x06, 0x03, /* 0x8F */ 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xFF, 0xFF, 0xFF, 0xC0, 0xC0, 0x06, 0x00, 0x30, 0x00, /* 0x90 */ 0x30, 0x0F, 0xF0, 0xFF, 0x03, 0x00, 0x30, 0x03, 0x3C, 0x37, 0xE3, 0xC7, 0x38, 0x33, 0x03, 0x30, 0x33, 0x03, 0x30, 0x33, 0x03, 0x30, 0x33, 0x03, 0x30, 0x33, 0x03, 0x00, 0x60, 0x06, 0x01, 0x80, 0x30, /* 0x91 */ 0x6A, 0xF0, /* 0x92 */ 0xF5, 0x60, /* 0x93 */ 0x4E, 0x28, 0xA2, 0xCF, 0x30, /* 0x94 */ 0xCF, 0x34, 0x51, 0x4E, 0x20, /* 0x95 */ 0x7B, 0xFF, 0xFF, 0xFD, 0xE0, /* 0x96 */ 0xFF, 0xFF, 0xF0, /* 0x97 */ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, /* 0x98 */ /* 0x99 */ 0xFF, 0x70, 0x1F, 0xFD, 0xC0, 0x71, 0x87, 0x83, 0xC6, 0x1E, 0x0F, 0x18, 0x68, 0x3C, 0x61, 0xB1, 0xB1, 0x86, 0xC6, 0xC6, 0x19, 0x1B, 0x18, 0x66, 0xCC, 0x61, 0x9B, 0x31, 0x86, 0x3C, 0xC6, 0x18, 0xE3, 0x18, 0x63, 0x8C, /* 0x9A */ 0x7F, 0x80, 0x3F, 0xC0, 0x18, 0x60, 0x0C, 0x30, 0x06, 0x18, 0x03, 0x0F, 0xF1, 0x87, 0xFC, 0xC3, 0x07, 0x61, 0x81, 0xB0, 0xC0, 0xD0, 0x60, 0xF8, 0x3F, 0xEC, 0x1F, 0xE0, /* 0x9B */ 0x99, 0x92, 0xF4, /* 0x9C */ 0xC0, 0xC0, 0x30, 0x30, 0x0C, 0x0C, 0x03, 0x03, 0x00, 0xC0, 0xC0, 0x3F, 0xFF, 0xCF, 0xFF, 0xFB, 0x03, 0x07, 0xC0, 0xC0, 0xF0, 0x30, 0x3C, 0x0C, 0x1F, 0x03, 0xFE, 0xC0, 0xFF, 0x00, /* 0x9D */ 0x07, 0x07, 0x03, 0x03, 0x00, 0x06, 0x0F, 0x0D, 0x8C, 0xCC, 0x6C, 0x3C, 0x1E, 0x0F, 0x86, 0xE3, 0x39, 0x8E, 0xC3, 0xE0, 0xC0, /* 0x9E */ 0x30, 0x1F, 0xE3, 0xFC, 0x18, 0x03, 0x00, 0x67, 0x0D, 0xF9, 0xC7, 0x38, 0x66, 0x0C, 0xC1, 0x98, 0x33, 0x06, 0x60, 0xCC, 0x19, 0x83, 0x30, 0x66, 0x0C, /* 0x9F */ 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0xFF, 0xFF, 0xC1, 0x80, 0x60, /* 0xA0 */ /* 0xA1 */ 0x10, 0x40, 0xC4, 0x07, 0xE0, 0x1E, 0x0C, 0x01, 0xF0, 0x1D, 0x80, 0xCE, 0x0E, 0x30, 0x61, 0xC7, 0x06, 0x30, 0x3B, 0x81, 0xD8, 0x07, 0xC0, 0x3C, 0x00, 0xE0, 0x06, 0x00, 0x70, 0x03, 0x80, 0x38, 0x01, 0xC0, 0x1C, 0x00, /* 0xA2 */ 0x21, 0x86, 0x20, 0xFC, 0x07, 0x0E, 0x06, 0xC0, 0xD8, 0x33, 0x86, 0x30, 0xC6, 0x30, 0xC6, 0x0C, 0xC1, 0xB0, 0x36, 0x03, 0xC0, 0x70, 0x0E, 0x01, 0x80, 0x30, 0x0E, 0x07, 0x80, 0xE0, 0x00, /* 0xA3 */ 0x01, 0x80, 0xC0, 0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x01, 0x80, 0xC0, 0x60, 0x3C, 0x1E, 0x0F, 0x07, 0xC7, 0x7F, 0x1F, 0x00, /* 0xA4 */ 0xDD, 0xFF, 0xD8, 0xD8, 0x3C, 0x1E, 0x0F, 0x8D, 0xFF, 0xDD, 0x80, /* 0xA5 */ 0x00, 0x60, 0x0F, 0xFF, 0xFF, 0xFC, 0x01, 0x80, 0x30, 0x06, 0x00, 0xC0, 0x18, 0x03, 0x00, 0x60, 0x0C, 0x01, 0x80, 0x30, 0x06, 0x00, 0xC0, 0x18, 0x03, 0x00, 0x60, 0x00, /* 0xA6 */ 0xFF, 0xFF, 0xF0, 0x3F, 0xFF, 0xFC, /* 0xA7 */ 0x0F, 0x03, 0xF0, 0xE7, 0x18, 0x63, 0x0C, 0x70, 0x07, 0x03, 0xF8, 0xC3, 0x98, 0x3B, 0x03, 0xF0, 0x37, 0x06, 0x78, 0xC7, 0xB0, 0x7C, 0x03, 0x80, 0x39, 0x83, 0x30, 0x67, 0x1C, 0x7F, 0x07, 0xC0, /* 0xA8 */ 0x19, 0x81, 0x98, 0x00, 0x0F, 0xFF, 0xFF, 0xFC, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0F, 0xFE, 0xFF, 0xEC, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0F, 0xFF, 0xFF, 0xF0, /* 0xA9 */ 0x03, 0xF0, 0x03, 0xFF, 0x01, 0xE0, 0xE0, 0xE3, 0x1C, 0x73, 0xF3, 0x99, 0x86, 0x6C, 0xC1, 0x8F, 0x30, 0x03, 0xCC, 0x00, 0xF3, 0x00, 0x3C, 0xC1, 0x8D, 0x98, 0x66, 0x77, 0xF3, 0x8E, 0x79, 0xC1, 0xC0, 0xE0, 0x3F, 0xF0, 0x03, 0xF0, 0x00, /* 0xAA */ 0x07, 0xE0, 0x3F, 0xF0, 0xF0, 0x71, 0x80, 0x76, 0x00, 0x6C, 0x00, 0x30, 0x00, 0x60, 0x00, 0xFF, 0xE1, 0xFF, 0xC3, 0x00, 0x06, 0x00, 0x0E, 0x00, 0x6C, 0x00, 0xDC, 0x03, 0x1C, 0x0E, 0x1F, 0xF8, 0x0F, 0xC0, /* 0xAB */ 0x21, 0x63, 0xE7, 0x84, 0x84, 0xE7, 0x63, 0x21, /* 0xAC */ 0xFF, 0xFF, 0xFF, 0x00, 0x30, 0x03, 0x00, 0x30, 0x03, /* 0xAD */ 0xFF, 0xF0, /* 0xAE */ 0x03, 0xF0, 0x03, 0xFF, 0x01, 0xE0, 0xE0, 0xFF, 0x1C, 0x7F, 0xF3, 0x9B, 0x04, 0x6C, 0xC1, 0x8F, 0x30, 0x43, 0xCF, 0xF0, 0xF3, 0xFC, 0x3C, 0xC1, 0x0D, 0xB0, 0x66, 0x7C, 0x1B, 0x8F, 0x07, 0xC1, 0xC0, 0xE0, 0x3F, 0xF0, 0x03, 0xF0, 0x00, /* 0xAF */ 0xC7, 0x8C, 0x01, 0x83, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x83, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x83, 0x00, /* 0xB0 */ 0x38, 0xFB, 0x1C, 0x18, 0x38, 0xDF, 0x1C, /* 0xB1 */ 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x7F, 0xE7, 0xFE, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x00, 0x00, 0x0F, 0xFF, 0xFF, 0xF0, /* 0xB2 */ 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, /* 0xB3 */ 0xF0, 0x3F, 0xFF, 0xFF, 0xF0, /* 0xB4 */ 0x03, 0x03, 0xFF, 0xFF, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, /* 0xB5 */ 0xC0, 0xCC, 0x0C, 0xC0, 0xCC, 0x0C, 0xC0, 0xCC, 0x0C, 0xC0, 0xCC, 0x0C, 0xC0, 0xCC, 0x1C, 0xE3, 0xCF, 0xEF, 0xFC, 0x7C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x00, /* 0xB6 */ 0x1F, 0xE7, 0xFD, 0xF3, 0x7E, 0x6F, 0xCD, 0xF9, 0xBF, 0x37, 0xE6, 0x7C, 0xCF, 0x98, 0xF3, 0x06, 0x60, 0xCC, 0x19, 0x83, 0x30, 0x66, 0x0C, 0xC1, 0x98, 0x33, 0x06, 0x60, 0xCC, /* 0xB7 */ 0xF0, /* 0xB8 */ 0x19, 0x83, 0x30, 0x00, 0x00, 0x01, 0xF0, 0x7F, 0x1C, 0x77, 0x03, 0xC0, 0x7F, 0xFF, 0xFF, 0xE0, 0x0C, 0x01, 0xC0, 0xDC, 0x31, 0xFC, 0x1F, 0x00, /* 0xB9 */ 0xC0, 0xC0, 0x18, 0x18, 0x03, 0x83, 0x00, 0x70, 0x60, 0x0B, 0x0C, 0x01, 0x61, 0x8F, 0xA6, 0x33, 0x1C, 0xC6, 0x41, 0x88, 0xC8, 0x31, 0x99, 0x06, 0x13, 0x20, 0xC3, 0x66, 0x38, 0x6C, 0xEF, 0x07, 0x8F, 0xA0, 0xF0, 0x04, 0x0E, 0x00, 0x81, 0xC7, 0xF0, 0x18, 0xFC, /* 0xBA */ 0x1F, 0x87, 0xF9, 0xC3, 0x30, 0x3E, 0x01, 0xFE, 0x3F, 0xC6, 0x00, 0xE0, 0x0C, 0x0D, 0xC3, 0x9F, 0xE1, 0xF8, /* 0xBB */ 0x88, 0xC6, 0xE7, 0x21, 0x21, 0xE7, 0xC6, 0x88, /* 0xBC */ 0x33, 0x00, 0x03, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x3F, 0xE0, /* 0xBD */ 0x0F, 0xE0, 0x7F, 0xC3, 0x83, 0x98, 0x07, 0x60, 0x0D, 0x80, 0x07, 0x00, 0x1E, 0x00, 0x3F, 0x80, 0x3F, 0xC0, 0x0F, 0x80, 0x07, 0xC0, 0x0F, 0x00, 0x3E, 0x00, 0xDE, 0x0E, 0x3F, 0xF0, 0x3F, 0x80, /* 0xBE */ 0x3E, 0x1F, 0xEE, 0x1B, 0x00, 0xC0, 0x3C, 0x07, 0xF0, 0x3F, 0x00, 0xF0, 0x3E, 0x1D, 0xFE, 0x3E, 0x00, /* 0xBF */ 0xCF, 0x30, 0x00, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, /* 0xC0 */ 0x07, 0x80, 0x1E, 0x00, 0x78, 0x03, 0xF0, 0x0C, 0xC0, 0x33, 0x01, 0xCE, 0x06, 0x18, 0x18, 0x60, 0xE1, 0xC3, 0x03, 0x0F, 0xFC, 0x7F, 0xF9, 0x80, 0x66, 0x01, 0xB8, 0x07, 0xC0, 0x0F, 0x00, 0x30, /* 0xC1 */ 0xFF, 0xE7, 0xFF, 0x30, 0x01, 0x80, 0x0C, 0x00, 0x60, 0x03, 0x00, 0x1F, 0xF8, 0xFF, 0xF6, 0x01, 0xB0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x06, 0xFF, 0xF7, 0xFE, 0x00, /* 0xC2 */ 0xFF, 0xC7, 0xFF, 0x30, 0x1D, 0x80, 0x6C, 0x03, 0x60, 0x1B, 0x00, 0xD8, 0x0C, 0xFF, 0xC7, 0xFF, 0x30, 0x0D, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x06, 0xFF, 0xF7, 0xFE, 0x00, /* 0xC3 */ 0xFF, 0xFF, 0xFF, 0x00, 0x60, 0x0C, 0x01, 0x80, 0x30, 0x06, 0x00, 0xC0, 0x18, 0x03, 0x00, 0x60, 0x0C, 0x01, 0x80, 0x30, 0x06, 0x00, 0xC0, 0x18, 0x00, /* 0xC4 */ 0x07, 0xFE, 0x01, 0xFF, 0x80, 0x60, 0x60, 0x18, 0x18, 0x06, 0x06, 0x01, 0x81, 0x80, 0x60, 0x60, 0x18, 0x18, 0x06, 0x06, 0x01, 0x81, 0x80, 0x60, 0x60, 0x18, 0x18, 0x0E, 0x06, 0x03, 0x01, 0x80, 0xC0, 0x60, 0x70, 0x18, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x00, 0x0F, 0x00, 0x03, 0xC0, 0x00, 0xC0, /* 0xC5 */ 0xFF, 0xFF, 0xFF, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xFF, 0xEF, 0xFE, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xFF, 0xFF, 0xFF, /* 0xC6 */ 0x70, 0x60, 0xE3, 0x06, 0x0C, 0x38, 0x61, 0xC1, 0xC6, 0x38, 0x0E, 0x67, 0x00, 0x66, 0x60, 0x03, 0x6C, 0x00, 0x3F, 0xC0, 0x01, 0xF8, 0x00, 0x1F, 0x80, 0x03, 0xFC, 0x00, 0x76, 0xE0, 0x0E, 0x67, 0x01, 0xC6, 0x38, 0x38, 0x61, 0xC3, 0x06, 0x0C, 0x60, 0x60, 0xEE, 0x06, 0x07, /* 0xC7 */ 0x0F, 0x81, 0xFF, 0x0C, 0x18, 0xC0, 0x66, 0x03, 0x00, 0x18, 0x01, 0xC0, 0x1C, 0x07, 0xC0, 0x3F, 0x00, 0x1C, 0x00, 0x7C, 0x01, 0xE0, 0x0F, 0x80, 0x6E, 0x0E, 0x3F, 0xE0, 0x7E, 0x00, /* 0xC8 */ 0xC0, 0x3E, 0x01, 0xF0, 0x1F, 0x80, 0xFC, 0x0D, 0xE0, 0xEF, 0x06, 0x78, 0x73, 0xC3, 0x1E, 0x30, 0xF1, 0x87, 0x98, 0x3D, 0xC1, 0xEC, 0x0F, 0xC0, 0x7E, 0x03, 0xE0, 0x1F, 0x00, 0xC0, /* 0xC9 */ 0x10, 0x40, 0xC4, 0x07, 0xE0, 0x1E, 0x0C, 0x03, 0xE0, 0x1F, 0x01, 0xF8, 0x0F, 0xC0, 0xDE, 0x0E, 0xF0, 0x67, 0x87, 0x3C, 0x31, 0xE3, 0x0F, 0x18, 0x79, 0x83, 0xDC, 0x1E, 0xC0, 0xFC, 0x07, 0xE0, 0x3E, 0x01, 0xF0, 0x0C, /* 0xCA */ 0xC0, 0x7C, 0x0E, 0xC1, 0xCC, 0x38, 0xC7, 0x0C, 0xE0, 0xDC, 0x0F, 0x80, 0xF0, 0x0F, 0x80, 0xDC, 0x0C, 0xE0, 0xC7, 0x0C, 0x30, 0xC3, 0x8C, 0x1C, 0xC0, 0xEC, 0x07, /* 0xCB */ 0x1F, 0xFC, 0x7F, 0xF1, 0x80, 0xC6, 0x03, 0x18, 0x0C, 0x60, 0x31, 0x80, 0xC6, 0x03, 0x18, 0x0C, 0x60, 0x31, 0x80, 0xC6, 0x03, 0x18, 0x0C, 0xC0, 0x33, 0x00, 0xDC, 0x03, 0xE0, 0x0F, 0x00, 0x30, /* 0xCC */ 0xE0, 0x07, 0xE0, 0x07, 0xF0, 0x0F, 0xF0, 0x0F, 0xD0, 0x0F, 0xD8, 0x1B, 0xD8, 0x1B, 0xD8, 0x1B, 0xCC, 0x33, 0xCC, 0x33, 0xCC, 0x33, 0xC6, 0x63, 0xC6, 0x63, 0xC6, 0x63, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC1, 0x83, /* 0xCD */ 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xFF, 0xFF, 0xFF, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xC0, /* 0xCE */ 0x07, 0xF0, 0x0F, 0xFE, 0x0F, 0x07, 0x86, 0x00, 0xC6, 0x00, 0x33, 0x00, 0x1B, 0x00, 0x07, 0x80, 0x03, 0xC0, 0x01, 0xE0, 0x00, 0xF0, 0x00, 0x78, 0x00, 0x36, 0x00, 0x33, 0x00, 0x18, 0xC0, 0x18, 0x78, 0x3C, 0x1F, 0xFC, 0x03, 0xF8, 0x00, /* 0xCF */ 0xFF, 0xFF, 0xFF, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xC0, /* 0xD0 */ 0xFF, 0x8F, 0xFE, 0xC0, 0x6C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x06, 0xFF, 0xEF, 0xFC, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, /* 0xD1 */ 0x07, 0xE0, 0x3F, 0xF0, 0xE0, 0x73, 0x80, 0x76, 0x00, 0x6C, 0x00, 0x30, 0x00, 0x60, 0x00, 0xC0, 0x01, 0x80, 0x03, 0x00, 0x06, 0x00, 0x0E, 0x00, 0x6C, 0x00, 0xDC, 0x03, 0x1E, 0x0E, 0x1F, 0xF8, 0x0F, 0xC0, /* 0xD2 */ 0xFF, 0xFF, 0xFF, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, /* 0xD3 */ 0xC0, 0x1F, 0x01, 0xD8, 0x0C, 0xE0, 0xE3, 0x06, 0x1C, 0x70, 0x63, 0x03, 0xB8, 0x1D, 0x80, 0x7C, 0x03, 0xC0, 0x0E, 0x00, 0x60, 0x07, 0x00, 0x38, 0x03, 0x80, 0x1C, 0x01, 0xC0, 0x00, /* 0xD4 */ 0x00, 0xC0, 0x00, 0x30, 0x00, 0xFF, 0xC0, 0xFF, 0xFC, 0x78, 0xC7, 0x98, 0x30, 0x6E, 0x0C, 0x1F, 0x03, 0x03, 0xC0, 0xC0, 0xF0, 0x30, 0x3C, 0x0C, 0x0F, 0x83, 0x07, 0x60, 0xC1, 0x9E, 0x31, 0xE3, 0xFF, 0xF0, 0x3F, 0xF0, 0x00, 0xC0, 0x00, 0x30, 0x00, /* 0xD5 */ 0xE0, 0x1D, 0x80, 0xE7, 0x03, 0x0E, 0x1C, 0x18, 0x60, 0x73, 0x00, 0xFC, 0x01, 0xE0, 0x07, 0x00, 0x1E, 0x00, 0xF8, 0x03, 0x30, 0x1C, 0xE0, 0xE1, 0x83, 0x07, 0x1C, 0x0E, 0xE0, 0x1B, 0x00, 0x70, /* 0xD6 */ 0xC0, 0x19, 0x80, 0x33, 0x00, 0x66, 0x00, 0xCC, 0x01, 0x98, 0x03, 0x30, 0x06, 0x60, 0x0C, 0xC0, 0x19, 0x80, 0x33, 0x00, 0x66, 0x00, 0xCC, 0x01, 0x98, 0x03, 0x30, 0x06, 0x60, 0x0C, 0xFF, 0xFF, 0xFF, 0xFC, 0x00, 0x18, 0x00, 0x30, 0x00, 0x60, /* 0xD7 */ 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x37, 0xFF, 0x3F, 0xF0, 0x03, 0x00, 0x30, 0x03, 0x00, 0x30, 0x03, 0x00, 0x30, 0x03, /* 0xD8 */ 0xC1, 0x83, 0xC1, 0x83, 0xC1, 0x83, 0xC1, 0x83, 0xC1, 0x83, 0xC1, 0x83, 0xC1, 0x83, 0xC1, 0x83, 0xC1, 0x83, 0xC1, 0x83, 0xC1, 0x83, 0xC1, 0x83, 0xC1, 0x83, 0xC1, 0x83, 0xC1, 0x83, 0xC1, 0x83, 0xFF, 0xFF, 0xFF, 0xFF, /* 0xD9 */ 0xC1, 0x83, 0x30, 0x60, 0xCC, 0x18, 0x33, 0x06, 0x0C, 0xC1, 0x83, 0x30, 0x60, 0xCC, 0x18, 0x33, 0x06, 0x0C, 0xC1, 0x83, 0x30, 0x60, 0xCC, 0x18, 0x33, 0x06, 0x0C, 0xC1, 0x83, 0x30, 0x60, 0xCC, 0x18, 0x33, 0x06, 0x0C, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0x0C, 0x00, 0x03, 0x00, 0x00, 0xC0, /* 0xDA */ 0xFE, 0x00, 0x3F, 0x80, 0x00, 0x60, 0x00, 0x18, 0x00, 0x06, 0x00, 0x01, 0x80, 0x00, 0x60, 0x00, 0x1F, 0xF8, 0x07, 0xFF, 0x01, 0x80, 0xE0, 0x60, 0x1C, 0x18, 0x03, 0x06, 0x00, 0xC1, 0x80, 0x30, 0x60, 0x1C, 0x18, 0x0E, 0x07, 0xFF, 0x01, 0xFF, 0x80, /* 0xDB */ 0xC0, 0x01, 0xE0, 0x00, 0xF0, 0x00, 0x78, 0x00, 0x3C, 0x00, 0x1E, 0x00, 0x0F, 0x00, 0x07, 0xFF, 0x83, 0xFF, 0xE1, 0xE0, 0x38, 0xF0, 0x0E, 0x78, 0x03, 0x3C, 0x01, 0x9E, 0x00, 0xCF, 0x00, 0xE7, 0x80, 0xE3, 0xFF, 0xE1, 0xFF, 0xE0, 0xC0, /* 0xDC */ 0xC0, 0x06, 0x00, 0x30, 0x01, 0x80, 0x0C, 0x00, 0x60, 0x03, 0x00, 0x1F, 0xF8, 0xFF, 0xE6, 0x03, 0xB0, 0x0F, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0xF8, 0x0E, 0xFF, 0xE7, 0xFE, 0x00, /* 0xDD */ 0x0F, 0xC0, 0x7F, 0xE1, 0xC1, 0xE7, 0x01, 0xCC, 0x01, 0xC0, 0x01, 0x80, 0x03, 0x00, 0x07, 0x1F, 0xFE, 0x3F, 0xFC, 0x00, 0x38, 0x00, 0x7C, 0x00, 0xD8, 0x03, 0x98, 0x0E, 0x38, 0x3C, 0x3F, 0xF0, 0x1F, 0x80, /* 0xDE */ 0xC0, 0x3F, 0x06, 0x07, 0xFE, 0x30, 0x70, 0x39, 0x87, 0x00, 0xEC, 0x30, 0x03, 0x61, 0x80, 0x1B, 0x18, 0x00, 0x78, 0xC0, 0x03, 0xFE, 0x00, 0x1F, 0xF0, 0x00, 0xF1, 0x80, 0x07, 0x8C, 0x00, 0x3C, 0x70, 0x03, 0xE1, 0x80, 0x1B, 0x0E, 0x01, 0xD8, 0x38, 0x1C, 0xC0, 0xFF, 0xC6, 0x01, 0xF8, 0x00, /* 0xDF */ 0x0F, 0xFC, 0xFF, 0xF3, 0x00, 0xD8, 0x03, 0x60, 0x0D, 0x80, 0x36, 0x00, 0xCC, 0x03, 0x3F, 0xFC, 0x3F, 0xF0, 0x38, 0xC1, 0xC3, 0x0E, 0x0C, 0x70, 0x31, 0x80, 0xCE, 0x03, 0x70, 0x0F, 0x80, 0x30, /* 0xE0 */ 0x3F, 0x07, 0xF8, 0xE1, 0xCC, 0x0C, 0x00, 0xC0, 0x1C, 0x3F, 0xCF, 0x8C, 0xC0, 0xCC, 0x0C, 0xE3, 0xC7, 0xEF, 0x3C, 0x70, /* 0xE1 */ 0x00, 0xC0, 0x38, 0x3F, 0x1F, 0x87, 0x00, 0xC0, 0x17, 0xC7, 0xFC, 0xF1, 0xDC, 0x1F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1F, 0x07, 0x71, 0xC7, 0xF0, 0x7C, 0x00, /* 0xE2 */ 0xFE, 0x3F, 0xEC, 0x3B, 0x06, 0xC1, 0xB0, 0xEF, 0xF3, 0x0E, 0xC0, 0xF0, 0x3C, 0x1F, 0xFE, 0xFF, 0x00, /* 0xE3 */ 0xFF, 0xFF, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x83, 0x06, 0x0C, 0x00, /* 0xE4 */ 0x0F, 0xF0, 0x3F, 0xC0, 0xC3, 0x03, 0x0C, 0x0C, 0x30, 0x30, 0xC0, 0xC3, 0x03, 0x0C, 0x1C, 0x30, 0x60, 0xC1, 0x83, 0x3F, 0xFF, 0xFF, 0xFF, 0x00, 0x3C, 0x00, 0xC0, /* 0xE5 */ 0x1F, 0x07, 0xF1, 0xC7, 0x70, 0x3C, 0x07, 0xFF, 0xFF, 0xFE, 0x00, 0xC0, 0x1C, 0x0D, 0xC3, 0x1F, 0xC1, 0xF0, /* 0xE6 */ 0xE1, 0x87, 0x71, 0x8E, 0x39, 0x9C, 0x1D, 0xB8, 0x0F, 0xF0, 0x07, 0xE0, 0x07, 0xE0, 0x0F, 0xF0, 0x1D, 0xB8, 0x39, 0x9C, 0x71, 0x8E, 0xE1, 0x87, 0xC1, 0x83, /* 0xE7 */ 0x3E, 0x7F, 0xB0, 0xE0, 0x30, 0x18, 0x78, 0x3C, 0x07, 0x01, 0xE0, 0xF8, 0xEF, 0xE3, 0xE0, /* 0xE8 */ 0xC0, 0xF8, 0x3F, 0x07, 0xE1, 0xFC, 0x37, 0x8C, 0xF3, 0x9E, 0x63, 0xD8, 0x7F, 0x0F, 0xC1, 0xF8, 0x3E, 0x06, /* 0xE9 */ 0x21, 0x86, 0x20, 0xFC, 0x0F, 0x0C, 0x0F, 0x83, 0xF0, 0x7E, 0x1F, 0xC3, 0x78, 0xCF, 0x39, 0xE6, 0x3D, 0x87, 0xF0, 0xFC, 0x1F, 0x83, 0xE0, 0x60, /* 0xEA */ 0xC1, 0xE1, 0xB1, 0x99, 0x8D, 0x87, 0x83, 0xC1, 0xF0, 0xDC, 0x67, 0x31, 0xD8, 0x7C, 0x18, /* 0xEB */ 0x3F, 0xCF, 0xF3, 0x0C, 0xC3, 0x30, 0xCC, 0x33, 0x0C, 0xC3, 0x30, 0xDC, 0x36, 0x0F, 0x83, 0xC0, 0xC0, /* 0xEC */ 0xE0, 0x7E, 0x07, 0xF0, 0xFF, 0x0F, 0xF0, 0xFD, 0x9B, 0xD9, 0xBD, 0xFB, 0xCF, 0x3C, 0xF3, 0xC6, 0x3C, 0x63, 0xC0, 0x30, /* 0xED */ 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xFF, 0xFF, 0xFF, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xC0, /* 0xEE */ 0x1F, 0x07, 0xF1, 0xC7, 0x70, 0x7C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x7C, 0x1D, 0xC7, 0x1F, 0xC1, 0xF0, /* 0xEF */ 0xFF, 0xFF, 0xFC, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xC0, /* 0xF0 */ 0xCF, 0x8D, 0xFC, 0xF0, 0xEE, 0x06, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x3E, 0x06, 0xF0, 0xEF, 0xFC, 0xCF, 0x8C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x00, /* 0xF1 */ 0x1F, 0x0F, 0xE6, 0x1F, 0x83, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x38, 0x37, 0x1C, 0xFE, 0x1F, 0x00, /* 0xF2 */ 0xFF, 0xFF, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, /* 0xF3 */ 0xE0, 0x6C, 0x0D, 0x83, 0x38, 0x63, 0x0C, 0x63, 0x0C, 0x60, 0xCC, 0x1B, 0x03, 0x60, 0x3C, 0x07, 0x00, 0xE0, 0x18, 0x03, 0x00, 0xE0, 0x78, 0x0E, 0x00, /* 0xF4 */ 0x00, 0xC0, 0x00, 0x18, 0x00, 0x03, 0x00, 0x0F, 0x67, 0x87, 0xFD, 0xF8, 0xC3, 0xE3, 0xB8, 0x78, 0x3E, 0x06, 0x03, 0xC0, 0xC0, 0x78, 0x18, 0x0F, 0x03, 0x01, 0xE0, 0x60, 0x3E, 0x1E, 0x0E, 0xC3, 0xE3, 0x9F, 0xFF, 0xE1, 0xF6, 0x78, 0x00, 0xC0, 0x00, 0x18, 0x00, 0x03, 0x00, 0x00, 0x60, 0x00, /* 0xF5 */ 0xC1, 0xF8, 0x66, 0x30, 0xCC, 0x3E, 0x07, 0x00, 0xC0, 0x78, 0x36, 0x0C, 0xC6, 0x3B, 0x06, 0xC0, 0xC0, /* 0xF6 */ 0xC0, 0xCC, 0x0C, 0xC0, 0xCC, 0x0C, 0xC0, 0xCC, 0x0C, 0xC0, 0xCC, 0x0C, 0xC0, 0xCC, 0x0C, 0xC0, 0xCF, 0xFF, 0xFF, 0xF0, 0x03, 0x00, 0x30, /* 0xF7 */ 0xC1, 0xE0, 0xF0, 0x78, 0x3C, 0x1E, 0x0F, 0xFE, 0xFF, 0x01, 0x80, 0xC0, 0x60, 0x30, 0x18, /* 0xF8 */ 0xC3, 0x0F, 0x0C, 0x3C, 0x30, 0xF0, 0xC3, 0xC3, 0x0F, 0x0C, 0x3C, 0x30, 0xF0, 0xC3, 0xC3, 0x0F, 0x0C, 0x3C, 0x30, 0xFF, 0xFF, 0xFF, 0xFC, /* 0xF9 */ 0xC3, 0x0C, 0xC3, 0x0C, 0xC3, 0x0C, 0xC3, 0x0C, 0xC3, 0x0C, 0xC3, 0x0C, 0xC3, 0x0C, 0xC3, 0x0C, 0xC3, 0x0C, 0xC3, 0x0C, 0xC3, 0x0C, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x03, 0x00, 0x03, /* 0xFA */ 0xFC, 0x03, 0xF0, 0x00, 0xC0, 0x03, 0x00, 0x0F, 0xF0, 0x3F, 0xE0, 0xC1, 0xC3, 0x03, 0x0C, 0x0C, 0x30, 0x30, 0xC1, 0xC3, 0xFE, 0x0F, 0xF0, /* 0xFB */ 0xC0, 0x0F, 0x00, 0x3C, 0x00, 0xF0, 0x03, 0xFF, 0x0F, 0xFE, 0x3C, 0x1C, 0xF0, 0x33, 0xC0, 0xCF, 0x03, 0x3C, 0x1C, 0xFF, 0xE3, 0xFF, 0x0C, /* 0xFC */ 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xFF, 0x3F, 0xEC, 0x1F, 0x03, 0xC0, 0xF0, 0x3C, 0x1F, 0xFE, 0xFF, 0x00, /* 0xFD */ 0x3F, 0x0F, 0xF1, 0x87, 0x60, 0x60, 0x0E, 0x3F, 0xC7, 0xF8, 0x03, 0x00, 0xD8, 0x1B, 0x87, 0x3F, 0xC3, 0xF0, /* 0xFE */ 0xC0, 0xF8, 0xC1, 0xFC, 0xC3, 0x8E, 0xC7, 0x07, 0xC6, 0x03, 0xFE, 0x03, 0xFE, 0x03, 0xC6, 0x03, 0xC6, 0x03, 0xC7, 0x06, 0xC3, 0x8E, 0xC1, 0xFC, 0xC0, 0xF8, /* 0xFF */ 0x1F, 0xCF, 0xF7, 0x0D, 0x83, 0x60, 0xD8, 0x33, 0xFC, 0x7F, 0x0C, 0xC6, 0x33, 0x0D, 0x83, 0xC0, 0xC0, }; const GFXglyph FreeSans12pt_Win1251Glyphs[] PROGMEM = { /* 0x01 */ { 0, 19, 20, 21, 1, -17 }, /* 0x02 */ { 48, 19, 20, 21, 1, -17 }, /* 0x03 */ { 96, 21, 20, 23, 1, -17 }, /* 0x04 */ { 149, 21, 20, 23, 1, -17 }, /* 0x05 */ { 202, 20, 20, 22, 1, -17 }, /* 0x06 */ { 252, 20, 20, 22, 1, -17 }, /* 0x07 */ { 302, 0, 0, 8, 0, 0 }, /* 0x08 */ { 302, 23, 20, 25, 1, -17 }, /* 0x09 */ { 360, 23, 16, 25, 1, -16 }, /* 0x0A */ { 406, 0, 0, 8, 0, 0 }, /* 0x0B */ { 406, 21, 20, 23, 1, -17 }, /* 0x0C */ { 459, 19, 18, 21, 1, -15 }, /* 0x0D */ { 502, 0, 0, 8, 0, 0 }, /* 0x0E */ { 502, 20, 20, 22, 1, -17 }, /* 0x0F */ { 552, 21, 21, 23, 1, -18 }, /* 0x10 */ { 608, 19, 20, 21, 1, -17 }, /* 0x11 */ { 656, 21, 20, 23, 1, -17 }, /* 0x12 */ { 709, 20, 20, 22, 1, -17 }, /* 0x13 */ { 759, 21, 20, 23, 1, -17 }, /* 0x14 */ { 812, 21, 20, 23, 1, -17 }, /* 0x15 */ { 865, 22, 20, 24, 1, -17 }, /* 0x16 */ { 920, 16, 20, 18, 1, -17 }, /* 0x17 */ { 960, 21, 20, 23, 1, -17 }, /* 0x18 */ { 1013, 23, 20, 25, 1, -17 }, /* 0x19 */ { 1071, 21, 20, 23, 1, -17 }, /* 0x1A */ { 1124, 15, 19, 17, 1, -16 }, /* 0x1B */ { 1160, 24, 21, 26, 1, -18 }, /* 0x1C */ { 1223, 21, 20, 23, 1, -17 }, /* 0x1D */ { 1276, 21, 21, 23, 1, -18 }, /* 0x1E */ { 1332, 20, 20, 22, 1, -17 }, /* 0x1F */ { 1382, 15, 20, 17, 1, -17 }, /* ' ' 0x20 */ { 1420, 0, 0, 6, 0, 0 }, /* '!' 0x21 */ { 1420, 2, 18, 8, 3, -16 }, /* '"' 0x22 */ { 1425, 6, 6, 8, 1, -15 }, /* '#' 0x23 */ { 1430, 13, 16, 13, 0, -14 }, /* '$' 0x24 */ { 1456, 11, 20, 13, 1, -16 }, /* '%' 0x25 */ { 1484, 20, 17, 21, 1, -15 }, /* '&' 0x26 */ { 1527, 14, 17, 16, 1, -15 }, /* ''' 0x27 */ { 1557, 2, 6, 5, 1, -15 }, /* '(' 0x28 */ { 1559, 5, 23, 8, 2, -16 }, /* ')' 0x29 */ { 1574, 5, 23, 8, 1, -16 }, /* '*' 0x2A */ { 1589, 7, 7, 9, 1, -16 }, /* '+' 0x2B */ { 1596, 10, 11, 14, 2, -9 }, /* ',' 0x2C */ { 1610, 2, 6, 7, 2, 0 }, /* '-' 0x2D */ { 1612, 6, 2, 8, 1, -6 }, /* '.' 0x2E */ { 1614, 2, 2, 6, 2, 0 }, /* '/' 0x2F */ { 1615, 7, 18, 7, 0, -16 }, /* '0' 0x30 */ { 1631, 11, 17, 13, 1, -15 }, /* '1' 0x31 */ { 1655, 5, 17, 13, 3, -15 }, /* '2' 0x32 */ { 1666, 11, 17, 13, 1, -15 }, /* '3' 0x33 */ { 1690, 11, 17, 13, 1, -15 }, /* '4' 0x34 */ { 1714, 11, 17, 13, 1, -15 }, /* '5' 0x35 */ { 1738, 11, 17, 13, 1, -15 }, /* '6' 0x36 */ { 1762, 11, 17, 13, 1, -15 }, /* '7' 0x37 */ { 1786, 11, 17, 13, 1, -15 }, /* '8' 0x38 */ { 1810, 11, 17, 13, 1, -15 }, /* '9' 0x39 */ { 1834, 11, 17, 13, 1, -15 }, /* ':' 0x3A */ { 1858, 2, 13, 6, 2, -11 }, /* ';' 0x3B */ { 1862, 2, 16, 6, 2, -10 }, /* '<' 0x3C */ { 1866, 12, 11, 14, 1, -9 }, /* '=' 0x3D */ { 1883, 12, 6, 14, 1, -7 }, /* '>' 0x3E */ { 1892, 12, 11, 14, 1, -9 }, /* '?' 0x3F */ { 1909, 10, 18, 13, 2, -16 }, /* '@' 0x40 */ { 1932, 22, 21, 24, 1, -16 }, /* 'A' 0x41 */ { 1990, 14, 18, 16, 1, -16 }, /* 'B' 0x42 */ { 2022, 13, 18, 16, 2, -16 }, /* 'C' 0x43 */ { 2052, 15, 18, 17, 1, -16 }, /* 'D' 0x44 */ { 2086, 14, 18, 17, 2, -16 }, /* 'E' 0x45 */ { 2118, 12, 18, 15, 2, -16 }, /* 'F' 0x46 */ { 2145, 11, 18, 14, 2, -16 }, /* 'G' 0x47 */ { 2170, 16, 18, 18, 1, -16 }, /* 'H' 0x48 */ { 2206, 13, 18, 17, 2, -16 }, /* 'I' 0x49 */ { 2236, 2, 18, 7, 2, -16 }, /* 'J' 0x4A */ { 2241, 9, 18, 13, 1, -16 }, /* 'K' 0x4B */ { 2262, 13, 18, 16, 2, -16 }, /* 'L' 0x4C */ { 2292, 10, 18, 14, 2, -16 }, /* 'M' 0x4D */ { 2315, 16, 18, 20, 2, -16 }, /* 'N' 0x4E */ { 2351, 13, 18, 18, 2, -16 }, /* 'O' 0x4F */ { 2381, 17, 18, 19, 1, -16 }, /* 'P' 0x50 */ { 2420, 12, 18, 16, 2, -16 }, /* 'Q' 0x51 */ { 2447, 17, 19, 19, 1, -16 }, /* 'R' 0x52 */ { 2488, 14, 18, 17, 2, -16 }, /* 'S' 0x53 */ { 2520, 14, 18, 16, 1, -16 }, /* 'T' 0x54 */ { 2552, 12, 18, 15, 1, -16 }, /* 'U' 0x55 */ { 2579, 13, 18, 17, 2, -16 }, /* 'V' 0x56 */ { 2609, 14, 18, 15, 1, -16 }, /* 'W' 0x57 */ { 2641, 22, 18, 22, 0, -16 }, /* 'X' 0x58 */ { 2691, 14, 18, 16, 1, -16 }, /* 'Y' 0x59 */ { 2723, 14, 18, 16, 1, -16 }, /* 'Z' 0x5A */ { 2755, 13, 18, 15, 1, -16 }, /* '[' 0x5B */ { 2785, 4, 23, 7, 2, -16 }, /* '\' 0x5C */ { 2797, 7, 18, 7, 0, -16 }, /* ']' 0x5D */ { 2813, 4, 23, 7, 1, -16 }, /* '^' 0x5E */ { 2825, 9, 9, 11, 1, -15 }, /* '_' 0x5F */ { 2836, 15, 1, 13, -1, 5 }, /* '`' 0x60 */ { 2838, 5, 4, 6, 1, -16 }, /* 'a' 0x61 */ { 2841, 12, 13, 13, 1, -11 }, /* 'b' 0x62 */ { 2861, 12, 18, 13, 1, -16 }, /* 'c' 0x63 */ { 2888, 10, 13, 12, 1, -11 }, /* 'd' 0x64 */ { 2905, 11, 18, 13, 1, -16 }, /* 'e' 0x65 */ { 2930, 11, 13, 13, 1, -11 }, /* 'f' 0x66 */ { 2948, 5, 18, 7, 1, -16 }, /* 'g' 0x67 */ { 2960, 11, 18, 13, 1, -11 }, /* 'h' 0x68 */ { 2985, 10, 18, 13, 1, -16 }, /* 'i' 0x69 */ { 3008, 2, 18, 5, 2, -16 }, /* 'j' 0x6A */ { 3013, 4, 23, 6, 0, -16 }, /* 'k' 0x6B */ { 3025, 10, 18, 12, 1, -16 }, /* 'l' 0x6C */ { 3048, 2, 18, 5, 1, -16 }, /* 'm' 0x6D */ { 3053, 17, 13, 19, 1, -11 }, /* 'n' 0x6E */ { 3081, 10, 13, 13, 1, -11 }, /* 'o' 0x6F */ { 3098, 11, 13, 13, 1, -11 }, /* 'p' 0x70 */ { 3116, 12, 17, 13, 1, -11 }, /* 'q' 0x71 */ { 3142, 11, 17, 13, 1, -11 }, /* 'r' 0x72 */ { 3166, 6, 13, 8, 1, -11 }, /* 's' 0x73 */ { 3176, 10, 13, 12, 1, -11 }, /* 't' 0x74 */ { 3193, 5, 16, 7, 1, -14 }, /* 'u' 0x75 */ { 3203, 10, 13, 13, 1, -11 }, /* 'v' 0x76 */ { 3220, 11, 13, 12, 0, -11 }, /* 'w' 0x77 */ { 3238, 17, 13, 17, 0, -11 }, /* 'x' 0x78 */ { 3266, 10, 13, 11, 1, -11 }, /* 'y' 0x79 */ { 3283, 11, 18, 11, 0, -11 }, /* 'z' 0x7A */ { 3308, 10, 13, 12, 1, -11 }, /* '{' 0x7B */ { 3325, 5, 23, 8, 1, -16 }, /* '|' 0x7C */ { 3340, 2, 23, 6, 2, -16 }, /* '}' 0x7D */ { 3346, 5, 23, 8, 2, -16 }, /* '~' 0x7E */ { 3361, 10, 5, 12, 1, -9 }, /* 0x7F */ { 3368, 0, 0, 0, 0, 0 }, /* 0x80 */ { 3368, 16, 22, 18, 1, -18 }, /* 0x81 */ { 3412, 11, 22, 14, 2, -22 }, /* 0x82 */ { 3443, 2, 5, 6, 2, -2 }, /* 0x83 */ { 3445, 7, 18, 9, 1, -18 }, /* 0x84 */ { 3461, 6, 5, 10, 2, -2 }, /* 0x85 */ { 3465, 12, 2, 16, 2, -2 }, /* 0x86 */ { 3468, 10, 21, 13, 2, -17 }, /* 0x87 */ { 3495, 10, 20, 13, 2, -17 }, /* 0x88 */ { 3520, 14, 17, 16, 1, -17 }, /* 0x89 */ { 3550, 23, 18, 24, 0, -18 }, /* 0x8A */ { 3602, 23, 18, 24, 0, -18 }, /* 0x8B */ { 3654, 3, 8, 6, 1, -11 }, /* 0x8C */ { 3657, 21, 18, 24, 2, -18 }, /* 0x8D */ { 3705, 12, 22, 15, 2, -22 }, /* 0x8E */ { 3738, 16, 18, 18, 1, -18 }, /* 0x8F */ { 3774, 13, 21, 17, 2, -18 }, /* 0x90 */ { 3809, 12, 22, 14, 0, -18 }, /* 0x91 */ { 3842, 2, 6, 6, 2, -18 }, /* 0x92 */ { 3844, 2, 6, 6, 2, -18 }, /* 0x93 */ { 3846, 6, 6, 10, 2, -18 }, /* 0x94 */ { 3851, 6, 6, 10, 2, -18 }, /* 0x95 */ { 3856, 6, 6, 10, 2, -11 }, /* 0x96 */ { 3861, 10, 2, 12, 1, -8 }, /* 0x97 */ { 3864, 22, 2, 24, 1, -8 }, /* 0x98 */ { 3870, 0, 0, 8, 0, 0 }, /* 0x99 */ { 3870, 22, 13, 24, 2, -18 }, /* 0x9A */ { 3906, 17, 13, 19, 1, -13 }, /* 0x9B */ { 3934, 3, 8, 6, 2, -10 }, /* 0x9C */ { 3937, 18, 13, 20, 1, -13 }, /* 0x9D */ { 3967, 9, 18, 12, 1, -18 }, /* 0x9E */ { 3988, 11, 18, 14, 1, -18 }, /* 0x9F */ { 4013, 10, 15, 13, 1, -13 }, /* 0xA0 */ { 4032, 0, 0, 7, 0, 0 }, /* 0xA1 */ { 4032, 13, 22, 15, 1, -22 }, /* 0xA2 */ { 4068, 11, 22, 11, 0, -17 }, /* 0xA3 */ { 4099, 9, 18, 13, 1, -18 }, /* 0xA4 */ { 4120, 9, 9, 13, 2, -13 }, /* 0xA5 */ { 4131, 11, 20, 14, 2, -20 }, /* 0xA6 */ { 4159, 2, 23, 6, 2, -18 }, /* 0xA7 */ { 4165, 11, 23, 13, 1, -18 }, /* 0xA8 */ { 4197, 12, 21, 15, 2, -21 }, /* 0xA9 */ { 4229, 18, 17, 19, 1, -17 }, /* 0xAA */ { 4268, 15, 18, 17, 1, -18 }, /* 0xAB */ { 4302, 8, 8, 12, 2, -11 }, /* 0xAC */ { 4310, 12, 6, 14, 1, -9 }, /* 0xAD */ { 4319, 6, 2, 8, 1, -8 }, /* 0xAE */ { 4321, 18, 17, 19, 1, -17 }, /* 0xAF */ { 4360, 7, 21, 7, 0, -21 }, /* 0xB0 */ { 4379, 7, 8, 15, 4, -17 }, /* 0xB1 */ { 4386, 12, 15, 14, 1, -15 }, /* 0xB2 */ { 4409, 2, 18, 7, 2, -18 }, /* 0xB3 */ { 4414, 2, 18, 5, 2, -18 }, /* 0xB4 */ { 4419, 8, 15, 9, 1, -15 }, /* 0xB5 */ { 4434, 12, 17, 13, 2, -13 }, /* 0xB6 */ { 4460, 11, 21, 13, 2, -18 }, /* 0xB7 */ { 4489, 2, 2, 6, 2, -8 }, /* 0xB8 */ { 4490, 11, 17, 13, 1, -17 }, /* 0xB9 */ { 4514, 19, 18, 22, 2, -18 }, /* 0xBA */ { 4557, 11, 13, 12, 0, -13 }, /* 0xBB */ { 4575, 8, 8, 12, 2, -10 }, /* 0xBC */ { 4583, 4, 23, 6, 0, -18 }, /* 0xBD */ { 4595, 14, 18, 16, 1, -18 }, /* 0xBE */ { 4627, 10, 13, 12, 1, -13 }, /* 0xBF */ { 4644, 6, 17, 6, 0, -17 }, /* 0xC0 */ { 4657, 14, 18, 16, 1, -18 }, /* 0xC1 */ { 4689, 13, 18, 16, 2, -18 }, /* 0xC2 */ { 4719, 13, 18, 16, 2, -18 }, /* 0xC3 */ { 4749, 11, 18, 14, 2, -18 }, /* 0xC4 */ { 4774, 18, 21, 19, 1, -18 }, /* 0xC5 */ { 4822, 12, 18, 15, 2, -18 }, /* 0xC6 */ { 4849, 20, 18, 22, 1, -18 }, /* 0xC7 */ { 4894, 13, 18, 16, 1, -18 }, /* 0xC8 */ { 4924, 13, 18, 18, 2, -18 }, /* 0xC9 */ { 4954, 13, 22, 18, 2, -22 }, /* 0xCA */ { 4990, 12, 18, 15, 2, -18 }, /* 0xCB */ { 5017, 14, 18, 16, 0, -18 }, /* 0xCC */ { 5049, 16, 18, 20, 2, -18 }, /* 0xCD */ { 5085, 13, 18, 17, 2, -18 }, /* 0xCE */ { 5115, 17, 18, 19, 1, -18 }, /* 0xCF */ { 5154, 13, 18, 17, 2, -18 }, /* 0xD0 */ { 5184, 12, 18, 16, 2, -18 }, /* 0xD1 */ { 5211, 15, 18, 17, 1, -18 }, /* 0xD2 */ { 5245, 12, 18, 15, 1, -18 }, /* 0xD3 */ { 5272, 13, 18, 15, 1, -18 }, /* 0xD4 */ { 5302, 18, 18, 20, 1, -18 }, /* 0xD5 */ { 5343, 14, 18, 16, 1, -18 }, /* 0xD6 */ { 5375, 15, 21, 18, 2, -18 }, /* 0xD7 */ { 5415, 12, 18, 15, 1, -18 }, /* 0xD8 */ { 5442, 16, 18, 20, 2, -18 }, /* 0xD9 */ { 5478, 18, 21, 20, 2, -18 }, /* 0xDA */ { 5526, 18, 18, 20, 1, -18 }, /* 0xDB */ { 5567, 17, 18, 21, 2, -18 }, /* 0xDC */ { 5606, 13, 18, 16, 2, -18 }, /* 0xDD */ { 5636, 15, 18, 17, 1, -18 }, /* 0xDE */ { 5670, 21, 18, 24, 2, -18 }, /* 0xDF */ { 5718, 14, 18, 16, 0, -18 }, /* 0xE0 */ { 5750, 12, 13, 13, 1, -13 }, /* 0xE1 */ { 5770, 11, 19, 13, 1, -19 }, /* 0xE2 */ { 5797, 10, 13, 12, 1, -13 }, /* 0xE3 */ { 5814, 7, 13, 9, 1, -13 }, /* 0xE4 */ { 5826, 14, 15, 14, 0, -13 }, /* 0xE5 */ { 5853, 11, 13, 13, 1, -13 }, /* 0xE6 */ { 5871, 16, 13, 18, 1, -13 }, /* 0xE7 */ { 5897, 9, 13, 12, 1, -13 }, /* 0xE8 */ { 5912, 11, 13, 13, 1, -13 }, /* 0xE9 */ { 5930, 11, 17, 13, 1, -17 }, /* 0xEA */ { 5954, 9, 13, 12, 1, -13 }, /* 0xEB */ { 5969, 10, 13, 12, 0, -13 }, /* 0xEC */ { 5986, 12, 13, 14, 1, -13 }, /* 0xED */ { 6006, 10, 13, 13, 1, -13 }, /* 0xEE */ { 6023, 11, 13, 13, 1, -13 }, /* 0xEF */ { 6041, 10, 13, 13, 1, -13 }, /* 0xF0 */ { 6058, 12, 17, 13, 1, -13 }, /* 0xF1 */ { 6084, 10, 13, 12, 1, -13 }, /* 0xF2 */ { 6101, 8, 13, 10, 1, -13 }, /* 0xF3 */ { 6114, 11, 18, 11, 0, -13 }, /* 0xF4 */ { 6139, 19, 20, 20, 1, -16 }, /* 0xF5 */ { 6187, 10, 13, 11, 1, -13 }, /* 0xF6 */ { 6204, 12, 15, 13, 1, -13 }, /* 0xF7 */ { 6227, 9, 13, 12, 1, -13 }, /* 0xF8 */ { 6242, 14, 13, 16, 1, -13 }, /* 0xF9 */ { 6265, 16, 15, 17, 1, -13 }, /* 0xFA */ { 6295, 14, 13, 15, 1, -13 }, /* 0xFB */ { 6318, 14, 13, 16, 1, -13 }, /* 0xFC */ { 6341, 10, 13, 12, 1, -13 }, /* 0xFD */ { 6358, 11, 13, 12, 1, -13 }, /* 0xFE */ { 6376, 16, 13, 18, 1, -13 }, /* 0xFF */ { 6402, 10, 13, 13, 1, -13 }, }; const GFXfont FreeSans12pt_Win1251 PROGMEM = { (uint8_t*)FreeSans12pt_Win1251Bitmaps, (GFXglyph*)FreeSans12pt_Win1251Glyphs, 0x01, 0xFF, 19 }; ================================================ FILE: src/graphics/niche/Fonts/FreeSans12pt_Win1252.h ================================================ // trunk-ignore-all(clang-format) #pragma once /* PROPERTIES FONT_NAME FreeSans12pt_Win1252 */ const uint8_t FreeSans12pt_Win1252Bitmaps[] PROGMEM = { /* 0x01 */ 0x00, 0x30, 0x00, 0x09, 0x00, 0x01, 0x20, 0x00, 0x24, 0x00, 0x04, 0x80, 0x01, 0x90, 0x00, 0x62, 0x00, 0x30, 0xFE, 0x04, 0x10, 0x5F, 0x02, 0x0B, 0x00, 0x7F, 0xE0, 0x0C, 0x1C, 0x02, 0x83, 0x81, 0x9F, 0xF0, 0x02, 0x1E, 0x00, 0x41, 0xC0, 0x0E, 0x7F, 0x81, 0x78, 0x18, 0x62, 0x00, 0xFF, 0xC0, /* 0x02 */ 0x00, 0xFF, 0x80, 0x61, 0x13, 0xF0, 0x62, 0x60, 0x07, 0xFC, 0x00, 0x83, 0x80, 0x10, 0xF0, 0x33, 0xF6, 0x01, 0x41, 0xC0, 0x18, 0x38, 0x03, 0xFF, 0xE0, 0x47, 0x02, 0x08, 0x20, 0x61, 0xC4, 0x06, 0x17, 0x00, 0x22, 0x00, 0x02, 0x40, 0x00, 0x48, 0x00, 0x09, 0x00, 0x01, 0x20, 0x00, 0x3C, 0x00, /* 0x03 */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x00, 0x02, 0x30, 0x00, 0x09, 0x04, 0x08, 0x48, 0x70, 0xE1, 0xC3, 0x87, 0x0E, 0x08, 0x10, 0x70, 0x00, 0x03, 0x80, 0x00, 0x14, 0x00, 0x00, 0xA1, 0x81, 0x8D, 0x87, 0xF0, 0x44, 0x00, 0x06, 0x30, 0x00, 0x60, 0xC0, 0x06, 0x03, 0x80, 0x60, 0x07, 0xFC, 0x00, /* 0x04 */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x00, 0x02, 0x30, 0x00, 0x09, 0x10, 0x02, 0x48, 0xE0, 0x61, 0xC1, 0xCC, 0x0E, 0x78, 0x1C, 0x70, 0x00, 0x03, 0x80, 0x00, 0x14, 0xFF, 0xFC, 0xA6, 0x00, 0xCD, 0x9F, 0xFE, 0x44, 0x71, 0xE6, 0x30, 0xFC, 0x60, 0xC0, 0x06, 0x03, 0x80, 0x60, 0x07, 0xFC, 0x00, /* 0x05 */ 0x00, 0x18, 0x00, 0x00, 0x40, 0x01, 0x90, 0x01, 0xF4, 0x08, 0x12, 0x23, 0xC1, 0x91, 0x2C, 0x1C, 0x8A, 0xC3, 0x64, 0x64, 0x13, 0x22, 0x41, 0x98, 0x26, 0x2C, 0xC4, 0x22, 0x60, 0x42, 0x13, 0x04, 0x30, 0x80, 0x61, 0xA4, 0x02, 0x18, 0x20, 0x03, 0x41, 0x00, 0x20, 0x08, 0x02, 0x00, 0x60, 0x40, 0x03, 0xF8, /* 0x06 */ 0x00, 0x10, 0x00, 0x03, 0x00, 0x1C, 0x48, 0x00, 0xB4, 0x80, 0x09, 0xF9, 0xC0, 0xE0, 0xE4, 0x0C, 0x02, 0x8F, 0x80, 0x38, 0x88, 0x01, 0x0D, 0x00, 0x18, 0x30, 0x01, 0x60, 0x80, 0x13, 0x18, 0x03, 0xF2, 0xC0, 0x20, 0x26, 0x06, 0x07, 0xFF, 0xA0, 0x02, 0x39, 0x00, 0x14, 0x70, 0x01, 0xC3, 0x00, 0x18, 0x00, /* 0x07 */ /* 0x08 */ 0x00, 0x1F, 0x80, 0x00, 0x60, 0x80, 0x01, 0x00, 0x80, 0x06, 0x00, 0x80, 0x3C, 0x01, 0x01, 0x8C, 0x02, 0x02, 0x08, 0x04, 0x04, 0x08, 0x0C, 0x38, 0x00, 0x04, 0x80, 0x00, 0x06, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x2E, 0xC0, 0x01, 0x83, 0x7E, 0x0C, 0x10, 0x37, 0xE2, 0x61, 0x00, 0x0C, 0xC6, 0x10, 0x98, 0x0C, 0x63, 0x00, 0x00, 0xC6, 0x00, /* 0x09 */ 0x00, 0x1F, 0x80, 0x00, 0x60, 0x80, 0x01, 0x00, 0x80, 0x06, 0x00, 0x80, 0x3C, 0x01, 0x01, 0x8C, 0x02, 0x02, 0x08, 0x04, 0x04, 0x08, 0x0C, 0x38, 0x00, 0x04, 0x80, 0x00, 0x06, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x2E, 0xC0, 0x01, 0x83, 0x7E, 0x0C, 0x00, 0x37, 0xE0, /* 0x0A */ /* 0x0B */ 0x1F, 0x07, 0xC1, 0x86, 0x41, 0x10, 0x0C, 0x04, 0x80, 0x40, 0x18, 0x00, 0x00, 0xC0, 0x00, 0x06, 0x00, 0x00, 0x30, 0x00, 0x01, 0x40, 0x00, 0x0A, 0x00, 0x00, 0x88, 0x00, 0x04, 0x40, 0x00, 0x41, 0x00, 0x02, 0x04, 0x00, 0x20, 0x20, 0x02, 0x00, 0x80, 0x20, 0x02, 0x02, 0x00, 0x08, 0x20, 0x00, 0x22, 0x00, 0x00, 0xE0, 0x00, /* 0x0C */ 0x01, 0x00, 0x00, 0x38, 0x00, 0x04, 0xC0, 0x01, 0x08, 0x00, 0x18, 0x80, 0x1C, 0x10, 0x02, 0x07, 0x80, 0x81, 0x10, 0x1F, 0xC2, 0x02, 0x00, 0x60, 0x80, 0x1A, 0x20, 0x1C, 0x42, 0x1C, 0x08, 0xFE, 0x03, 0xA0, 0x01, 0x8C, 0x01, 0xC1, 0x43, 0xD0, 0x27, 0x81, 0xF8, /* 0x0D */ /* 0x0E */ 0x00, 0xE0, 0x00, 0x11, 0x00, 0x01, 0x10, 0x00, 0x0B, 0x00, 0x03, 0xF8, 0x00, 0x60, 0x60, 0x09, 0x02, 0x00, 0xA0, 0x10, 0x16, 0x01, 0x01, 0x40, 0x10, 0x10, 0x01, 0x01, 0x00, 0x08, 0x10, 0x00, 0x82, 0x1F, 0x08, 0x3F, 0x90, 0x44, 0x00, 0x06, 0xBF, 0xFF, 0xAF, 0xF0, 0xFF, 0xFF, 0x0F, 0xE3, 0xFB, 0xFC, /* 0x0F */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x40, 0x12, 0x34, 0x00, 0x69, 0x40, 0x01, 0x49, 0xE0, 0xF1, 0xCD, 0x06, 0x8E, 0x28, 0x14, 0x71, 0x40, 0xA3, 0x8B, 0xFD, 0x14, 0x50, 0x68, 0xA2, 0x81, 0x4D, 0x97, 0xFA, 0x44, 0xBF, 0xD6, 0x31, 0x02, 0xE0, 0xC8, 0x16, 0x08, 0x61, 0x08, 0x21, 0xF0, 0x80, 0xF8, 0x78, 0x00, /* 0x10 */ 0x00, 0xF0, 0x00, 0x3A, 0x00, 0x07, 0xC0, 0x00, 0xA8, 0x00, 0x1F, 0x00, 0x02, 0xB0, 0x00, 0x52, 0x00, 0x0A, 0x40, 0x02, 0x48, 0x00, 0x49, 0x00, 0x09, 0x30, 0x01, 0x22, 0x01, 0xC4, 0x70, 0xF0, 0x85, 0xE1, 0x10, 0x88, 0x37, 0x20, 0x03, 0x9C, 0x00, 0x37, 0x00, 0x06, 0x40, 0x01, 0x86, 0x00, /* 0x11 */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x60, 0x02, 0x36, 0x00, 0x09, 0x04, 0x0C, 0x48, 0x60, 0xC1, 0xC3, 0x0F, 0x0E, 0x00, 0x08, 0x70, 0x00, 0x23, 0x80, 0x63, 0x84, 0x01, 0x9F, 0x20, 0x0C, 0xFD, 0x80, 0x27, 0xE4, 0x03, 0x3F, 0x30, 0x33, 0xE0, 0xC0, 0x00, 0x03, 0x80, 0x60, 0x07, 0xFC, 0x00, /* 0x12 */ 0x00, 0xC2, 0x00, 0x1C, 0x24, 0x02, 0x18, 0x60, 0x64, 0x02, 0x02, 0x40, 0x20, 0x00, 0xF2, 0x03, 0x89, 0xE0, 0x7C, 0x80, 0x0E, 0x25, 0x80, 0xE1, 0x00, 0x1A, 0x08, 0x71, 0xB0, 0xC4, 0x39, 0x84, 0xC2, 0xCC, 0x40, 0x76, 0x7C, 0x05, 0xBB, 0x80, 0x4C, 0xE0, 0x0A, 0x78, 0x00, 0x9C, 0x00, 0x0F, 0x00, 0x00, /* 0x13 */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x00, 0x02, 0x30, 0x00, 0x09, 0x00, 0x00, 0x48, 0x60, 0xC1, 0xC6, 0xC9, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x03, 0x80, 0x00, 0x14, 0xFF, 0xF8, 0xA6, 0x00, 0xCD, 0x9F, 0xFE, 0x44, 0x71, 0xE6, 0x30, 0xFC, 0x60, 0xC0, 0x06, 0x03, 0x80, 0x60, 0x07, 0xFC, 0x00, /* 0x14 */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x20, 0x22, 0x33, 0x01, 0x89, 0x20, 0x02, 0x48, 0x60, 0xE1, 0xC8, 0x80, 0x8E, 0x46, 0x46, 0x72, 0x32, 0x33, 0x9F, 0x9F, 0x94, 0x78, 0x78, 0xA0, 0x00, 0x0D, 0x80, 0x00, 0x44, 0x0E, 0x06, 0x30, 0x00, 0x60, 0xC0, 0x06, 0x03, 0x80, 0x60, 0x07, 0xFC, 0x00, /* 0x15 */ 0x03, 0xFC, 0x20, 0x38, 0x1C, 0x81, 0x80, 0x1D, 0x08, 0x00, 0x32, 0x60, 0x00, 0x89, 0x00, 0x02, 0x18, 0x00, 0x08, 0x61, 0xC3, 0x22, 0x8D, 0x93, 0x72, 0x00, 0x00, 0x48, 0x00, 0x01, 0x20, 0x00, 0x04, 0x9F, 0xFF, 0x92, 0x60, 0x0E, 0x44, 0xFF, 0xF2, 0x11, 0xC3, 0x88, 0x21, 0xF8, 0x40, 0x40, 0x02, 0x00, 0xC0, 0x30, 0x00, 0xFF, 0x00, /* 0x16 */ 0x03, 0x80, 0x03, 0xC0, 0x01, 0xE0, 0x01, 0xE0, 0x03, 0xF0, 0x03, 0xF0, 0x27, 0xF0, 0x6F, 0x70, 0x6E, 0x60, 0xFC, 0x60, 0xFC, 0x7E, 0xFC, 0x7E, 0xFC, 0x3F, 0xF4, 0x1F, 0xF4, 0x1F, 0xF0, 0x0E, 0x70, 0x0E, 0x30, 0x1C, 0x38, 0x38, 0x0F, 0xF0, /* 0x17 */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x00, 0x02, 0x30, 0x00, 0x09, 0x00, 0x00, 0x48, 0x00, 0x21, 0xC0, 0x02, 0x8E, 0x20, 0xF4, 0x70, 0x84, 0x11, 0x82, 0x40, 0x84, 0x01, 0x03, 0x20, 0x0F, 0x85, 0x80, 0x03, 0x04, 0x00, 0x04, 0x30, 0x78, 0x10, 0xC0, 0x06, 0x03, 0x80, 0x60, 0x07, 0xFC, 0x00, /* 0x18 */ 0x00, 0xFC, 0x00, 0x02, 0x06, 0x00, 0x08, 0x24, 0x00, 0x21, 0xA4, 0x00, 0x4C, 0x48, 0x00, 0xA0, 0x50, 0x01, 0x92, 0x60, 0x03, 0x24, 0xC0, 0x06, 0x01, 0x81, 0x28, 0x03, 0x49, 0x6C, 0xC4, 0xAD, 0xD8, 0x16, 0xA4, 0xCC, 0xC4, 0x44, 0x86, 0x13, 0x05, 0x00, 0x28, 0x0A, 0x00, 0x50, 0x14, 0x00, 0x90, 0x48, 0x01, 0x20, 0x90, 0x02, 0x41, 0x20, 0x00, 0x00, /* 0x19 */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x00, 0x02, 0x30, 0x00, 0x09, 0x00, 0x00, 0x49, 0xC3, 0x81, 0xC0, 0x00, 0x0E, 0x78, 0xF0, 0x77, 0xEF, 0xC3, 0xA7, 0x4E, 0x15, 0x0A, 0x10, 0xA7, 0x8F, 0x0D, 0x80, 0x00, 0x44, 0x00, 0x06, 0x33, 0xF0, 0x60, 0xC0, 0x06, 0x03, 0x80, 0x60, 0x07, 0xFC, 0x00, /* 0x1A */ 0xFF, 0xFF, 0x00, 0x06, 0x00, 0x0C, 0x3E, 0x18, 0x82, 0x32, 0x02, 0x64, 0x04, 0xC8, 0x09, 0x80, 0x23, 0x00, 0x86, 0x02, 0x0C, 0x08, 0x18, 0x10, 0x30, 0x00, 0x60, 0x00, 0xC0, 0x81, 0x80, 0x03, 0x00, 0x07, 0xFF, 0xF8, /* 0x1B */ 0x00, 0xFE, 0x00, 0x03, 0x81, 0x80, 0x04, 0x00, 0x60, 0x08, 0x00, 0x30, 0x10, 0x00, 0x10, 0x30, 0x07, 0x88, 0x23, 0xC8, 0x08, 0x22, 0x00, 0x04, 0x60, 0x00, 0x44, 0x60, 0x00, 0x84, 0x63, 0x03, 0x04, 0x61, 0xFC, 0x04, 0x6B, 0x00, 0x9E, 0xA5, 0x01, 0x6A, 0xD5, 0x01, 0x43, 0xA8, 0x81, 0x05, 0xD0, 0x82, 0x0A, 0xA0, 0x82, 0x05, 0xC0, 0x82, 0x02, 0x61, 0xFF, 0x0C, 0x1E, 0x00, 0xF0, /* 0x1C */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x30, 0x02, 0x32, 0x00, 0x09, 0x00, 0x00, 0x48, 0x20, 0x61, 0xC3, 0x84, 0x0E, 0x1C, 0x78, 0x70, 0x40, 0x03, 0x80, 0x00, 0x14, 0x00, 0x00, 0xA0, 0x03, 0x0D, 0x83, 0xF0, 0x44, 0x00, 0x06, 0x30, 0x00, 0x60, 0xC0, 0x06, 0x03, 0x80, 0x60, 0x07, 0xFC, 0x00, /* 0x1D */ 0x01, 0xFE, 0x00, 0x3A, 0x1C, 0x03, 0x00, 0x30, 0x23, 0x1E, 0xC3, 0x38, 0x03, 0x10, 0xC3, 0x09, 0x00, 0x18, 0x68, 0x00, 0xC1, 0x40, 0x00, 0x0A, 0x07, 0x80, 0x50, 0x46, 0x02, 0x80, 0x00, 0x1A, 0x1E, 0x00, 0xCB, 0x10, 0x0D, 0x03, 0x00, 0x48, 0x60, 0x06, 0x40, 0x00, 0x22, 0x0C, 0x02, 0x10, 0x60, 0x60, 0x43, 0xFC, 0x01, 0xE0, 0x00, 0x00, /* 0x1E */ 0x01, 0xF0, 0x00, 0xEA, 0xC0, 0x31, 0x5F, 0x04, 0x5F, 0x88, 0x80, 0xA0, 0x48, 0x0E, 0x02, 0x8F, 0x40, 0x3C, 0x10, 0x21, 0x66, 0x87, 0x15, 0x98, 0x71, 0x41, 0x02, 0x14, 0x00, 0x01, 0x40, 0x00, 0x14, 0x00, 0x01, 0x21, 0xFE, 0x12, 0x00, 0x02, 0x10, 0x00, 0x60, 0x80, 0x0C, 0x06, 0x01, 0x80, 0x3F, 0xE0, /* 0x1F */ 0x0E, 0x00, 0x13, 0x00, 0x23, 0x00, 0xF3, 0x01, 0x31, 0x01, 0x11, 0x03, 0xD3, 0x06, 0xF2, 0x30, 0x34, 0xC7, 0x25, 0x33, 0x2B, 0xC2, 0x57, 0x04, 0x3A, 0x08, 0x72, 0x30, 0xA3, 0xC3, 0x40, 0x04, 0x40, 0x18, 0x40, 0x60, 0x7F, 0x80, /* ' ' 0x20 */ /* '!' 0x21 */ 0xFF, 0xFF, 0xFF, 0xF0, 0xF0, /* '"' 0x22 */ 0xCF, 0x3C, 0xF3, 0x8A, 0x20, /* '#' 0x23 */ 0x06, 0x30, 0x31, 0x03, 0x18, 0x18, 0xC7, 0xFF, 0xBF, 0xFC, 0x31, 0x01, 0x18, 0x18, 0xC7, 0xFF, 0xBF, 0xFC, 0x31, 0x01, 0x18, 0x18, 0xC0, 0xC6, 0x06, 0x30, /* '$' 0x24 */ 0x04, 0x03, 0xE1, 0xFF, 0x72, 0x7C, 0x47, 0x88, 0xF1, 0x07, 0xA0, 0x7E, 0x03, 0xF0, 0x17, 0x02, 0x7C, 0x47, 0x88, 0xF1, 0x1B, 0x26, 0x7F, 0xC3, 0xE0, 0x10, 0x02, 0x00, /* '%' 0x25 */ 0x00, 0x06, 0x03, 0xC0, 0x40, 0x7E, 0x0C, 0x0E, 0x70, 0x80, 0xC3, 0x18, 0x0C, 0x31, 0x00, 0xE7, 0x30, 0x07, 0xE6, 0x00, 0x3C, 0x40, 0x00, 0x0C, 0x7C, 0x00, 0x8F, 0xE0, 0x19, 0xC7, 0x01, 0x18, 0x30, 0x31, 0x83, 0x02, 0x1C, 0x70, 0x40, 0xFE, 0x04, 0x07, 0xC0, /* '&' 0x26 */ 0x0F, 0x00, 0x7E, 0x03, 0x9C, 0x0C, 0x30, 0x30, 0xC0, 0xE7, 0x01, 0xF8, 0x03, 0x80, 0x3E, 0x01, 0xCC, 0x6E, 0x39, 0xB0, 0x7C, 0xC0, 0xF3, 0x03, 0xCE, 0x1F, 0x9F, 0xE6, 0x3E, 0x1C, /* ''' 0x27 */ 0xFF, 0xA0, /* '(' 0x28 */ 0x08, 0x8C, 0x46, 0x31, 0x98, 0xC6, 0x31, 0x8C, 0x63, 0x08, 0x63, 0x08, 0x61, 0x0C, 0x20, /* ')' 0x29 */ 0x82, 0x18, 0xC3, 0x18, 0xC3, 0x18, 0xC6, 0x31, 0x8C, 0x62, 0x31, 0x88, 0xC4, 0x62, 0x00, /* '*' 0x2A */ 0x10, 0x23, 0x5B, 0xE3, 0x8D, 0x91, 0x00, /* '+' 0x2B */ 0x0C, 0x03, 0x00, 0xC0, 0x30, 0xFF, 0xFF, 0xF0, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, /* ',' 0x2C */ 0xF5, 0x60, /* '-' 0x2D */ 0xFF, 0xF0, /* '.' 0x2E */ 0xF0, /* '/' 0x2F */ 0x02, 0x0C, 0x10, 0x20, 0xC1, 0x02, 0x0C, 0x10, 0x20, 0xC1, 0x02, 0x0C, 0x10, 0x20, 0xC1, 0x00, /* '0' 0x30 */ 0x1F, 0x07, 0xF1, 0xC7, 0x30, 0x6C, 0x0F, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3E, 0x0E, 0xC1, 0x9C, 0x71, 0xFC, 0x1F, 0x00, /* '1' 0x31 */ 0x08, 0xCF, 0xFF, 0x8C, 0x63, 0x18, 0xC6, 0x31, 0x8C, 0x63, 0x18, /* '2' 0x32 */ 0x1F, 0x0F, 0xF9, 0x87, 0x60, 0x7C, 0x06, 0x00, 0xC0, 0x18, 0x07, 0x01, 0xC0, 0xF0, 0x78, 0x1C, 0x06, 0x00, 0xC0, 0x30, 0x07, 0xFF, 0xFF, 0xE0, /* '3' 0x33 */ 0x3F, 0x0F, 0xF3, 0x87, 0x60, 0x6C, 0x0C, 0x01, 0x80, 0x60, 0x78, 0x0F, 0x80, 0x18, 0x01, 0x80, 0x3C, 0x07, 0x80, 0xD8, 0x73, 0xFC, 0x3F, 0x00, /* '4' 0x34 */ 0x01, 0x80, 0x70, 0x0E, 0x03, 0xC0, 0xD8, 0x1B, 0x06, 0x61, 0x8C, 0x21, 0x8C, 0x33, 0x06, 0x7F, 0xFF, 0xFE, 0x03, 0x00, 0x60, 0x0C, 0x01, 0x80, /* '5' 0x35 */ 0x3F, 0xCF, 0xF9, 0x80, 0x30, 0x06, 0x00, 0xDE, 0x1F, 0xE7, 0x0E, 0x00, 0xE0, 0x0C, 0x01, 0x80, 0x30, 0x07, 0x81, 0xB8, 0x73, 0xFC, 0x1F, 0x00, /* '6' 0x36 */ 0x0F, 0x07, 0xF9, 0xC3, 0x30, 0x74, 0x01, 0x80, 0x33, 0xC7, 0xFE, 0xF1, 0xDC, 0x1F, 0x01, 0xE0, 0x3C, 0x06, 0xC1, 0xDC, 0x71, 0xFC, 0x1F, 0x00, /* '7' 0x37 */ 0xFF, 0xFF, 0xFC, 0x01, 0x00, 0x60, 0x18, 0x02, 0x00, 0xC0, 0x30, 0x06, 0x01, 0x80, 0x30, 0x04, 0x01, 0x80, 0x30, 0x06, 0x01, 0x80, 0x30, 0x00, /* '8' 0x38 */ 0x1F, 0x07, 0xF1, 0xC7, 0x30, 0x66, 0x0C, 0xC1, 0x8C, 0x61, 0xF8, 0x3F, 0x8E, 0x3B, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xD8, 0x31, 0xFC, 0x1F, 0x00, /* '9' 0x39 */ 0x1F, 0x07, 0xF1, 0xC7, 0x70, 0x6C, 0x07, 0x80, 0xF0, 0x1E, 0x07, 0x61, 0xEF, 0xFC, 0x79, 0x80, 0x30, 0x05, 0xC1, 0x98, 0x73, 0xFC, 0x1E, 0x00, /* ':' 0x3A */ 0xF0, 0x00, 0x03, 0xC0, /* ';' 0x3B */ 0xF0, 0x00, 0x0F, 0x56, /* '<' 0x3C */ 0x00, 0x70, 0x1E, 0x0F, 0x83, 0xC0, 0xF0, 0x0E, 0x00, 0x7C, 0x00, 0xF0, 0x03, 0xC0, 0x0F, 0x00, 0x10, /* '=' 0x3D */ 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, /* '>' 0x3E */ 0xE0, 0x07, 0x80, 0x1F, 0x00, 0x7C, 0x00, 0xF0, 0x07, 0x01, 0xE0, 0xF0, 0x3C, 0x0F, 0x00, 0x80, 0x00, /* '?' 0x3F */ 0x3F, 0x1F, 0xEE, 0x1F, 0x03, 0xC0, 0xC0, 0x30, 0x0C, 0x06, 0x03, 0x81, 0xC0, 0xE0, 0x30, 0x0C, 0x03, 0x00, 0x00, 0x00, 0x0C, 0x03, 0x00, /* '@' 0x40 */ 0x00, 0xFE, 0x00, 0x0F, 0xFE, 0x00, 0xF0, 0x3E, 0x07, 0x00, 0x3C, 0x38, 0x00, 0x38, 0xC1, 0xE0, 0x66, 0x0F, 0xD9, 0xD8, 0x61, 0xC3, 0xC3, 0x07, 0x0F, 0x1C, 0x1C, 0x3C, 0x60, 0x60, 0xF1, 0x81, 0x83, 0xC6, 0x06, 0x1B, 0x18, 0x38, 0xEE, 0x71, 0xE7, 0x18, 0xFD, 0xF8, 0x71, 0xE7, 0xC0, 0xE0, 0x00, 0x01, 0xE0, 0x00, 0x01, 0xFF, 0xC0, 0x01, 0xFC, 0x00, /* 'A' 0x41 */ 0x07, 0x80, 0x1E, 0x00, 0x78, 0x03, 0xF0, 0x0C, 0xC0, 0x33, 0x01, 0xCE, 0x06, 0x18, 0x18, 0x60, 0xE1, 0xC3, 0x03, 0x0F, 0xFC, 0x7F, 0xF9, 0x80, 0x66, 0x01, 0xB8, 0x07, 0xC0, 0x0F, 0x00, 0x30, /* 'B' 0x42 */ 0xFF, 0xC7, 0xFF, 0x30, 0x1D, 0x80, 0x6C, 0x03, 0x60, 0x1B, 0x00, 0xD8, 0x0C, 0xFF, 0xC7, 0xFF, 0x30, 0x0D, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x06, 0xFF, 0xF7, 0xFE, 0x00, /* 'C' 0x43 */ 0x07, 0xE0, 0x3F, 0xF0, 0xE0, 0x73, 0x80, 0x76, 0x00, 0x6C, 0x00, 0x30, 0x00, 0x60, 0x00, 0xC0, 0x01, 0x80, 0x03, 0x00, 0x06, 0x00, 0x0E, 0x00, 0x6C, 0x00, 0xDC, 0x03, 0x1E, 0x0E, 0x1F, 0xF8, 0x0F, 0xC0, /* 'D' 0x44 */ 0xFF, 0xC3, 0xFF, 0x8C, 0x07, 0x30, 0x0E, 0xC0, 0x1B, 0x00, 0x7C, 0x00, 0xF0, 0x03, 0xC0, 0x0F, 0x00, 0x3C, 0x00, 0xF0, 0x03, 0xC0, 0x1F, 0x00, 0x6C, 0x03, 0xB0, 0x1C, 0xFF, 0xE3, 0xFE, 0x00, /* 'E' 0x45 */ 0xFF, 0xFF, 0xFF, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xFF, 0xEF, 0xFE, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xFF, 0xFF, 0xFF, /* 'F' 0x46 */ 0xFF, 0xFF, 0xFF, 0x00, 0x60, 0x0C, 0x01, 0x80, 0x30, 0x06, 0x00, 0xFF, 0xDF, 0xFB, 0x00, 0x60, 0x0C, 0x01, 0x80, 0x30, 0x06, 0x00, 0xC0, 0x18, 0x00, /* 'G' 0x47 */ 0x07, 0xF0, 0x1F, 0xFC, 0x3C, 0x1E, 0x70, 0x07, 0x60, 0x03, 0xE0, 0x00, 0xC0, 0x00, 0xC0, 0x00, 0xC0, 0x7F, 0xC0, 0x7F, 0xC0, 0x03, 0xC0, 0x03, 0x60, 0x03, 0x60, 0x07, 0x30, 0x0F, 0x3C, 0x1F, 0x1F, 0xFB, 0x07, 0xE1, /* 'H' 0x48 */ 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xFF, 0xFF, 0xFF, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xC0, /* 'I' 0x49 */ 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, /* 'J' 0x4A */ 0x01, 0x80, 0xC0, 0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x01, 0x80, 0xC0, 0x60, 0x3C, 0x1E, 0x0F, 0x07, 0xC7, 0x7F, 0x1F, 0x00, /* 'K' 0x4B */ 0xC0, 0x3E, 0x03, 0xB0, 0x39, 0x83, 0x8C, 0x38, 0x63, 0x83, 0x38, 0x19, 0xC0, 0xDE, 0x07, 0xB8, 0x38, 0xE1, 0x83, 0x0C, 0x1C, 0x60, 0x73, 0x01, 0x98, 0x0E, 0xC0, 0x3E, 0x00, 0xC0, /* 'L' 0x4C */ 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xFF, 0xFF, 0xF0, /* 'M' 0x4D */ 0xE0, 0x07, 0xE0, 0x07, 0xF0, 0x0F, 0xF0, 0x0F, 0xD0, 0x0F, 0xD8, 0x1B, 0xD8, 0x1B, 0xD8, 0x1B, 0xCC, 0x33, 0xCC, 0x33, 0xCC, 0x33, 0xC6, 0x63, 0xC6, 0x63, 0xC6, 0x63, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC1, 0x83, /* 'N' 0x4E */ 0xE0, 0x1F, 0x00, 0xFC, 0x07, 0xE0, 0x3D, 0x81, 0xEE, 0x0F, 0x30, 0x79, 0xC3, 0xC6, 0x1E, 0x18, 0xF0, 0xE7, 0x83, 0x3C, 0x1D, 0xE0, 0x6F, 0x01, 0xF8, 0x0F, 0xC0, 0x3E, 0x01, 0xC0, /* 'O' 0x4F */ 0x07, 0xF0, 0x0F, 0xFE, 0x0F, 0x07, 0x86, 0x00, 0xC6, 0x00, 0x33, 0x00, 0x1B, 0x00, 0x07, 0x80, 0x03, 0xC0, 0x01, 0xE0, 0x00, 0xF0, 0x00, 0x78, 0x00, 0x36, 0x00, 0x33, 0x00, 0x18, 0xC0, 0x18, 0x78, 0x3C, 0x1F, 0xFC, 0x03, 0xF8, 0x00, /* 'P' 0x50 */ 0xFF, 0x8F, 0xFE, 0xC0, 0x6C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x06, 0xFF, 0xEF, 0xFC, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, /* 'Q' 0x51 */ 0x07, 0xF0, 0x0F, 0xFE, 0x0F, 0x07, 0x86, 0x00, 0xC6, 0x00, 0x33, 0x00, 0x1B, 0x00, 0x07, 0x80, 0x03, 0xC0, 0x01, 0xE0, 0x00, 0xF0, 0x00, 0x78, 0x00, 0x36, 0x00, 0x33, 0x01, 0x98, 0xC0, 0xFC, 0x78, 0x3C, 0x1F, 0xFF, 0x03, 0xF9, 0x80, 0x00, 0x40, /* 'R' 0x52 */ 0xFF, 0xE3, 0xFF, 0xCC, 0x03, 0xB0, 0x06, 0xC0, 0x1B, 0x00, 0x6C, 0x01, 0xB0, 0x0C, 0xFF, 0xE3, 0xFF, 0xCC, 0x03, 0xB0, 0x06, 0xC0, 0x1B, 0x00, 0x6C, 0x01, 0xB0, 0x06, 0xC0, 0x1B, 0x00, 0x70, /* 'S' 0x53 */ 0x0F, 0xE0, 0x7F, 0xC3, 0x83, 0x98, 0x07, 0x60, 0x0D, 0x80, 0x07, 0x00, 0x1E, 0x00, 0x3F, 0x80, 0x3F, 0xC0, 0x0F, 0x80, 0x07, 0xC0, 0x0F, 0x00, 0x3E, 0x00, 0xDE, 0x0E, 0x3F, 0xF0, 0x3F, 0x80, /* 'T' 0x54 */ 0xFF, 0xFF, 0xFF, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, /* 'U' 0x55 */ 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x80, 0xEE, 0x0E, 0x3F, 0xE0, 0xFC, 0x00, /* 'V' 0x56 */ 0xC0, 0x0F, 0x00, 0x7E, 0x01, 0x98, 0x06, 0x60, 0x39, 0xC0, 0xC3, 0x03, 0x0C, 0x1C, 0x38, 0x60, 0x61, 0x81, 0x8E, 0x07, 0x30, 0x0C, 0xC0, 0x37, 0x00, 0xF8, 0x01, 0xE0, 0x07, 0x80, 0x1C, 0x00, /* 'W' 0x57 */ 0xE0, 0x30, 0x1D, 0x80, 0xE0, 0x76, 0x07, 0x81, 0xDC, 0x1E, 0x06, 0x70, 0x7C, 0x18, 0xC1, 0xB0, 0xE3, 0x0C, 0xC3, 0x8C, 0x33, 0x0C, 0x38, 0xC6, 0x30, 0x67, 0x18, 0xC1, 0x98, 0x67, 0x06, 0x61, 0xD8, 0x1D, 0x83, 0x60, 0x3C, 0x0D, 0x80, 0xF0, 0x3E, 0x03, 0xC0, 0x70, 0x0F, 0x01, 0xC0, 0x18, 0x07, 0x00, /* 'X' 0x58 */ 0xE0, 0x1D, 0x80, 0xE7, 0x03, 0x0E, 0x1C, 0x18, 0x60, 0x73, 0x00, 0xFC, 0x01, 0xE0, 0x07, 0x00, 0x1E, 0x00, 0xF8, 0x03, 0x30, 0x1C, 0xE0, 0xE1, 0x83, 0x07, 0x1C, 0x0E, 0xE0, 0x1B, 0x00, 0x70, /* 'Y' 0x59 */ 0xC0, 0x0F, 0x80, 0x76, 0x01, 0x9C, 0x0C, 0x38, 0x70, 0x61, 0x81, 0xCE, 0x03, 0x30, 0x0F, 0x80, 0x1E, 0x00, 0x30, 0x00, 0xC0, 0x03, 0x00, 0x0C, 0x00, 0x30, 0x00, 0xC0, 0x03, 0x00, 0x0C, 0x00, /* 'Z' 0x5A */ 0xFF, 0xFF, 0xFF, 0xC0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0x60, 0x07, 0x00, 0x70, 0x07, 0x00, 0x30, 0x03, 0x80, 0x38, 0x03, 0x80, 0x18, 0x01, 0xC0, 0x1C, 0x00, 0xFF, 0xFF, 0xFF, 0xC0, /* '[' 0x5B */ 0xFF, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCF, 0xF0, /* '\' 0x5C */ 0x81, 0x81, 0x02, 0x06, 0x04, 0x08, 0x18, 0x10, 0x20, 0x60, 0x40, 0x81, 0x81, 0x02, 0x06, 0x04, /* ']' 0x5D */ 0xFF, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x3F, 0xF0, /* '^' 0x5E */ 0x0C, 0x0E, 0x05, 0x86, 0xC3, 0x21, 0x19, 0x8C, 0x83, 0xC1, 0x80, /* '_' 0x5F */ 0xFF, 0xFE, /* '`' 0x60 */ 0xE3, 0x8C, 0x30, /* 'a' 0x61 */ 0x3F, 0x07, 0xF8, 0xE1, 0xCC, 0x0C, 0x00, 0xC0, 0x1C, 0x3F, 0xCF, 0x8C, 0xC0, 0xCC, 0x0C, 0xE3, 0xC7, 0xEF, 0x3C, 0x70, /* 'b' 0x62 */ 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0xF8, 0xDF, 0xCF, 0x0E, 0xE0, 0x7C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x03, 0xE0, 0x6F, 0x0E, 0xDF, 0xCC, 0xF8, /* 'c' 0x63 */ 0x1F, 0x0F, 0xE6, 0x1F, 0x83, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x38, 0x37, 0x1C, 0xFE, 0x1F, 0x00, /* 'd' 0x64 */ 0x00, 0x60, 0x0C, 0x01, 0x80, 0x30, 0x06, 0x3C, 0xCF, 0xFB, 0x8F, 0xE0, 0xF8, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF8, 0x3B, 0x8F, 0x3F, 0x63, 0xCC, /* 'e' 0x65 */ 0x1F, 0x07, 0xF1, 0xC7, 0x70, 0x3C, 0x07, 0xFF, 0xFF, 0xFE, 0x00, 0xC0, 0x1C, 0x0D, 0xC3, 0x1F, 0xC1, 0xF0, /* 'f' 0x66 */ 0x3B, 0xD8, 0xC6, 0x7F, 0xEC, 0x63, 0x18, 0xC6, 0x31, 0x8C, 0x63, 0x00, /* 'g' 0x67 */ 0x1E, 0x67, 0xFD, 0xC7, 0xF0, 0x7C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x7C, 0x1D, 0xC7, 0x9F, 0xB1, 0xE6, 0x00, 0xC0, 0x3E, 0x0E, 0x7F, 0xC7, 0xE0, /* 'h' 0x68 */ 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x33, 0xCD, 0xFB, 0xC7, 0xE0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x30, /* 'i' 0x69 */ 0xF0, 0x3F, 0xFF, 0xFF, 0xF0, /* 'j' 0x6A */ 0x33, 0x00, 0x03, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x3F, 0xE0, /* 'k' 0x6B */ 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x6C, 0x33, 0x18, 0xCC, 0x37, 0x0F, 0xC3, 0xB8, 0xC6, 0x31, 0xCC, 0x3B, 0x06, 0xC1, 0xF0, 0x30, /* 'l' 0x6C */ 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, /* 'm' 0x6D */ 0xCF, 0x1F, 0x6F, 0xDF, 0xFC, 0x78, 0xFC, 0x18, 0x3C, 0x0C, 0x1E, 0x06, 0x0F, 0x03, 0x07, 0x81, 0x83, 0xC0, 0xC1, 0xE0, 0x60, 0xF0, 0x30, 0x78, 0x18, 0x3C, 0x0C, 0x18, /* 'n' 0x6E */ 0xCF, 0x37, 0xEF, 0x1F, 0x83, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xC0, /* 'o' 0x6F */ 0x1F, 0x07, 0xF1, 0xC7, 0x70, 0x7C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x7C, 0x1D, 0xC7, 0x1F, 0xC1, 0xF0, /* 'p' 0x70 */ 0xCF, 0x8D, 0xFC, 0xF0, 0xEE, 0x06, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x3E, 0x06, 0xF0, 0xEF, 0xFC, 0xCF, 0x8C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x00, /* 'q' 0x71 */ 0x1E, 0x67, 0xFD, 0xC7, 0xF0, 0x7C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x7C, 0x1D, 0xC7, 0x9F, 0xF1, 0xE6, 0x00, 0xC0, 0x18, 0x03, 0x00, 0x60, /* 'r' 0x72 */ 0xCF, 0x7F, 0x38, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC0, /* 's' 0x73 */ 0x3E, 0x1F, 0xEE, 0x1B, 0x00, 0xC0, 0x3C, 0x07, 0xF0, 0x3F, 0x01, 0xF0, 0x3E, 0x1D, 0xFE, 0x3F, 0x00, /* 't' 0x74 */ 0x63, 0x19, 0xFF, 0xB1, 0x8C, 0x63, 0x18, 0xC6, 0x31, 0xE7, /* 'u' 0x75 */ 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x7E, 0x3D, 0xFB, 0x3C, 0xC0, /* 'v' 0x76 */ 0xE0, 0x6C, 0x0D, 0x81, 0xB8, 0x63, 0x0C, 0x61, 0x8E, 0x60, 0xCC, 0x19, 0x83, 0xE0, 0x3C, 0x07, 0x00, 0xE0, /* 'w' 0x77 */ 0xC1, 0xC1, 0xB0, 0xE1, 0xD8, 0x70, 0xCC, 0x2C, 0x66, 0x36, 0x31, 0x9B, 0x18, 0xCD, 0x98, 0x64, 0x6C, 0x16, 0x36, 0x0F, 0x1A, 0x07, 0x8F, 0x03, 0x83, 0x80, 0xC1, 0xC0, /* 'x' 0x78 */ 0xC1, 0xF8, 0x66, 0x30, 0xCC, 0x3E, 0x07, 0x00, 0xC0, 0x78, 0x36, 0x0C, 0xC6, 0x3B, 0x06, 0xC0, 0xC0, /* 'y' 0x79 */ 0xE0, 0x6C, 0x0D, 0x83, 0x38, 0x63, 0x0C, 0x63, 0x0C, 0x60, 0xCC, 0x1B, 0x03, 0x60, 0x3C, 0x07, 0x00, 0xE0, 0x18, 0x03, 0x00, 0xE0, 0x78, 0x0E, 0x00, /* 'z' 0x7A */ 0xFF, 0xFF, 0xF0, 0x18, 0x0C, 0x07, 0x03, 0x81, 0xC0, 0x60, 0x30, 0x18, 0x0E, 0x03, 0xFF, 0xFF, 0xC0, /* '{' 0x7B */ 0x19, 0xCC, 0x63, 0x18, 0xC6, 0x31, 0x99, 0x86, 0x18, 0xC6, 0x31, 0x8C, 0x63, 0x1C, 0x60, /* '|' 0x7C */ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, /* '}' 0x7D */ 0xC7, 0x18, 0xC6, 0x31, 0x8C, 0x63, 0x0C, 0x33, 0x31, 0x8C, 0x63, 0x18, 0xC6, 0x73, 0x00, /* '~' 0x7E */ 0x70, 0x3E, 0x09, 0xE4, 0x1F, 0x03, 0x80, /* 0x7F */ /* 0x80 */ 0x01, 0xF0, 0x1F, 0xF0, 0xE0, 0xC7, 0x00, 0x18, 0x00, 0xC0, 0x07, 0xFF, 0x3F, 0xFC, 0x30, 0x01, 0xFF, 0x8F, 0xFC, 0x0C, 0x00, 0x18, 0x00, 0x70, 0x00, 0xE0, 0x81, 0xFE, 0x03, 0xF0, /* 0x81 */ /* 0x82 */ 0xF5, 0x80, /* 0x83 */ 0x1C, 0xF3, 0x0C, 0x31, 0xF7, 0xCC, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x33, 0xCE, 0x00, /* 0x84 */ 0xCF, 0x34, 0x51, 0x88, /* 0x85 */ 0xC6, 0x3C, 0x63, /* 0x86 */ 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x3F, 0xFF, 0xFC, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x00, /* 0x87 */ 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x3F, 0xFF, 0xFC, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x0F, 0xFF, 0xFF, 0x0C, 0x03, 0x00, 0xC0, 0x30, /* 0x88 */ 0x38, 0xD9, 0xB6, 0x30, /* 0x89 */ 0x38, 0x18, 0x00, 0xF8, 0x30, 0x03, 0x18, 0xC0, 0x04, 0x11, 0x80, 0x0C, 0x66, 0x00, 0x0F, 0x8C, 0x00, 0x0E, 0x30, 0x00, 0x00, 0x40, 0x00, 0x01, 0x80, 0x00, 0x06, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x31, 0xC0, 0xE0, 0x67, 0xC3, 0xC1, 0x98, 0xCC, 0xC3, 0x20, 0x90, 0x8C, 0x63, 0x33, 0x10, 0x7C, 0x3C, 0x60, 0x70, 0x38, /* 0x8A */ 0x0C, 0x40, 0x1F, 0x00, 0x38, 0x03, 0xF8, 0x1F, 0xF0, 0xE0, 0xE6, 0x01, 0xD8, 0x03, 0x60, 0x01, 0xC0, 0x07, 0x80, 0x0F, 0xE0, 0x0F, 0xF0, 0x03, 0xE0, 0x01, 0xF0, 0x03, 0xC0, 0x0F, 0x80, 0x37, 0x83, 0x8F, 0xFC, 0x0F, 0xE0, /* 0x8B */ 0x2F, 0x49, 0x99, /* 0x8C */ 0x07, 0xCF, 0xFC, 0x7F, 0xFF, 0xF3, 0x83, 0xC0, 0x18, 0x07, 0x00, 0x60, 0x0C, 0x03, 0x00, 0x30, 0x0C, 0x00, 0xC0, 0x30, 0x03, 0x00, 0xC0, 0x0F, 0xFF, 0x00, 0x3F, 0xFC, 0x00, 0xC0, 0x30, 0x03, 0x00, 0xC0, 0x0C, 0x01, 0x80, 0x30, 0x07, 0x01, 0xC0, 0x0E, 0x0F, 0x00, 0x1F, 0xEF, 0xFC, 0x1F, 0x3F, 0xF0, /* 0x8D */ /* 0x8E */ 0x0C, 0xC0, 0x3C, 0x00, 0xE1, 0xFF, 0xFF, 0xFF, 0x80, 0x1C, 0x01, 0xC0, 0x1C, 0x00, 0xC0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0x60, 0x07, 0x00, 0x70, 0x07, 0x00, 0x30, 0x03, 0x80, 0x38, 0x01, 0xFF, 0xFF, 0xFF, 0x80, /* 0x8F */ /* 0x90 */ /* 0x91 */ 0x6A, 0xF0, /* 0x92 */ 0xF5, 0x60, /* 0x93 */ 0x4E, 0x28, 0xA2, 0xCF, 0x30, /* 0x94 */ 0xCF, 0x34, 0x51, 0x4E, 0x20, /* 0x95 */ 0x7B, 0xFF, 0xFF, 0xFD, 0xE0, /* 0x96 */ 0xFF, 0xFF, 0xF0, /* 0x97 */ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, /* 0x98 */ 0x63, 0xFE, 0x70, /* 0x99 */ 0xFF, 0x70, 0x1F, 0xFD, 0xC0, 0x71, 0x87, 0x83, 0xC6, 0x1E, 0x0F, 0x18, 0x68, 0x3C, 0x61, 0xB1, 0xB1, 0x86, 0xC6, 0xC6, 0x19, 0x1B, 0x18, 0x66, 0xCC, 0x61, 0x9B, 0x31, 0x86, 0x3C, 0xC6, 0x18, 0xE3, 0x18, 0x63, 0x8C, /* 0x9A */ 0x63, 0x0D, 0x83, 0x60, 0x70, 0x00, 0x0F, 0x87, 0xFB, 0x86, 0xC0, 0x30, 0x0F, 0x01, 0xFC, 0x0F, 0xC0, 0x7C, 0x0F, 0x87, 0x7F, 0x8F, 0xC0, /* 0x9B */ 0x99, 0x92, 0xF4, /* 0x9C */ 0x1F, 0x0F, 0x83, 0xF9, 0xFC, 0x71, 0xF8, 0x6E, 0x0F, 0x03, 0xC0, 0x60, 0x3C, 0x07, 0xFF, 0xC0, 0x7F, 0xFC, 0x06, 0x00, 0xC0, 0x60, 0x0E, 0x0F, 0x03, 0x71, 0xF8, 0x63, 0xF9, 0xFC, 0x1F, 0x0F, 0x80, /* 0x9D */ /* 0x9E */ 0x63, 0x0C, 0x83, 0x60, 0x70, 0x00, 0x3F, 0xFF, 0xFC, 0x06, 0x03, 0x01, 0xC0, 0xE0, 0x70, 0x18, 0x0C, 0x06, 0x03, 0x80, 0xFF, 0xFF, 0xF0, /* 0x9F */ 0x0C, 0xC0, 0x33, 0x00, 0x00, 0x30, 0x03, 0xE0, 0x1D, 0x80, 0x67, 0x03, 0x0E, 0x1C, 0x18, 0x60, 0x73, 0x80, 0xCC, 0x03, 0xE0, 0x07, 0x80, 0x0C, 0x00, 0x30, 0x00, 0xC0, 0x03, 0x00, 0x0C, 0x00, 0x30, 0x00, 0xC0, 0x03, 0x00, /* 0xA0 */ /* 0xA1 */ 0xF0, 0xBF, 0xFF, 0xFF, 0xF0, /* 0xA2 */ 0x04, 0x00, 0x80, 0x7C, 0x1F, 0xE7, 0x4C, 0xC8, 0xF1, 0x1E, 0x20, 0xC4, 0x18, 0x83, 0x10, 0x72, 0x37, 0x4E, 0x7F, 0x87, 0xC0, 0x20, 0x04, 0x00, /* 0xA3 */ 0x0F, 0xC1, 0xFE, 0x38, 0x76, 0x03, 0x60, 0x36, 0x00, 0x70, 0x03, 0x80, 0xFF, 0x0F, 0xF0, 0x1C, 0x00, 0xC0, 0x0C, 0x01, 0x80, 0x10, 0x02, 0xF1, 0x7F, 0xF6, 0x1F, /* 0xA4 */ 0xDD, 0xFF, 0xD8, 0xD8, 0x3C, 0x1E, 0x0F, 0x8D, 0xFF, 0xDD, 0x80, /* 0xA5 */ 0xC0, 0x3E, 0x06, 0x60, 0x63, 0x0C, 0x30, 0xC1, 0x98, 0x19, 0x80, 0xF0, 0x0F, 0x07, 0xFE, 0x06, 0x00, 0x60, 0x7F, 0xE0, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, /* 0xA6 */ 0xFF, 0xFF, 0xF0, 0x3F, 0xFF, 0xFC, /* 0xA7 */ 0x0F, 0x03, 0xF0, 0xE7, 0x18, 0x63, 0x0C, 0x70, 0x07, 0x03, 0xF8, 0xC3, 0x98, 0x3B, 0x03, 0xF0, 0x37, 0x06, 0x78, 0xC7, 0xB0, 0x7C, 0x03, 0x80, 0x39, 0x83, 0x30, 0x67, 0x1C, 0x7F, 0x07, 0xC0, /* 0xA8 */ 0xCF, 0x30, /* 0xA9 */ 0x03, 0xF0, 0x03, 0xFF, 0x01, 0xE0, 0xE0, 0xE3, 0x1C, 0x73, 0xF3, 0x99, 0x86, 0x6C, 0xC1, 0x8F, 0x30, 0x03, 0xCC, 0x00, 0xF3, 0x00, 0x3C, 0xC1, 0x8D, 0x98, 0x66, 0x77, 0xF3, 0x8E, 0x79, 0xC1, 0xC0, 0xE0, 0x3F, 0xF0, 0x03, 0xF0, 0x00, /* 0xAA */ 0x79, 0x08, 0x11, 0xEE, 0x50, 0xA3, 0x3B, 0x00, 0x03, 0xF8, /* 0xAB */ 0x21, 0x63, 0xE7, 0x84, 0x84, 0xE7, 0x63, 0x21, /* 0xAC */ 0xFF, 0xFF, 0xFF, 0x00, 0x30, 0x03, 0x00, 0x30, 0x03, /* 0xAD */ 0xFF, 0xF0, /* 0xAE */ 0x03, 0xF0, 0x03, 0xFF, 0x01, 0xE0, 0xE0, 0xFF, 0x1C, 0x7F, 0xF3, 0x9B, 0x04, 0x6C, 0xC1, 0x8F, 0x30, 0x43, 0xCF, 0xF0, 0xF3, 0xFC, 0x3C, 0xC1, 0x0D, 0xB0, 0x66, 0x7C, 0x1B, 0x8F, 0x07, 0xC1, 0xC0, 0xE0, 0x3F, 0xF0, 0x03, 0xF0, 0x00, /* 0xAF */ 0xFF, 0xF0, /* 0xB0 */ 0x38, 0xFB, 0x1C, 0x18, 0x38, 0xDF, 0x1C, /* 0xB1 */ 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x7F, 0xE7, 0xFE, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x00, 0x00, 0x0F, 0xFF, 0xFF, 0xF0, /* 0xB2 */ 0x7D, 0x8F, 0x18, 0x30, 0xC6, 0x18, 0x60, 0xFF, 0xFC, /* 0xB3 */ 0x7D, 0x8F, 0x18, 0x31, 0x80, 0xC1, 0xE3, 0xC6, 0xF8, /* 0xB4 */ 0x3B, 0x99, 0x80, /* 0xB5 */ 0xC0, 0xCC, 0x0C, 0xC0, 0xCC, 0x0C, 0xC0, 0xCC, 0x0C, 0xC0, 0xCC, 0x0C, 0xC0, 0xCC, 0x1C, 0xE3, 0xCF, 0xEF, 0xFC, 0x7C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x00, /* 0xB6 */ 0x1F, 0xE7, 0xFD, 0xF3, 0x7E, 0x6F, 0xCD, 0xF9, 0xBF, 0x37, 0xE6, 0x7C, 0xCF, 0x98, 0xF3, 0x06, 0x60, 0xCC, 0x19, 0x83, 0x30, 0x66, 0x0C, 0xC1, 0x98, 0x33, 0x06, 0x60, 0xCC, /* 0xB7 */ 0xF0, /* 0xB8 */ 0x10, 0xF0, 0xE3, 0x78, /* 0xB9 */ 0x2F, 0xB6, 0xDB, 0x6C, /* 0xBA */ 0x79, 0x38, 0x61, 0x86, 0x1C, 0xDE, 0x00, 0x0F, 0xC0, /* 0xBB */ 0x88, 0xC6, 0xE7, 0x21, 0x21, 0xE7, 0xC6, 0x88, /* 0xBC */ 0x20, 0x08, 0x30, 0x0C, 0x38, 0x04, 0x0C, 0x06, 0x06, 0x02, 0x03, 0x02, 0x01, 0x81, 0x00, 0xC1, 0x06, 0x61, 0x87, 0x30, 0x83, 0x80, 0xC2, 0xC0, 0x42, 0x60, 0x43, 0x30, 0x21, 0xFC, 0x20, 0x0C, 0x30, 0x06, 0x10, 0x03, 0x00, /* 0xBD */ 0x20, 0x00, 0x08, 0x02, 0x06, 0x01, 0x83, 0x80, 0x40, 0x60, 0x20, 0x18, 0x18, 0x06, 0x04, 0x01, 0x83, 0x00, 0x61, 0x9F, 0x98, 0x4E, 0x76, 0x33, 0x0C, 0x08, 0x03, 0x04, 0x03, 0x83, 0x01, 0x80, 0x81, 0x80, 0x60, 0xC0, 0x30, 0x3F, 0xC8, 0x0F, 0xF0, /* 0xBE */ 0x7C, 0x00, 0x18, 0xC0, 0x43, 0x18, 0x18, 0x03, 0x02, 0x00, 0x60, 0xC0, 0x30, 0x10, 0x01, 0x84, 0x00, 0x31, 0x80, 0xC6, 0x20, 0xD8, 0xC8, 0x39, 0xF1, 0x07, 0x00, 0x41, 0x60, 0x18, 0x4C, 0x02, 0x11, 0x80, 0x83, 0xF8, 0x10, 0x06, 0x04, 0x00, 0xC1, 0x00, 0x18, /* 0xBF */ 0x0C, 0x06, 0x00, 0x00, 0x00, 0xC0, 0x60, 0x60, 0x30, 0x30, 0x38, 0x38, 0x18, 0x0C, 0x06, 0x0F, 0x07, 0xC7, 0x7F, 0x1F, 0x00, /* 0xC0 */ 0x0C, 0x00, 0x18, 0x00, 0x30, 0x00, 0x00, 0x07, 0x80, 0x1E, 0x00, 0x78, 0x03, 0xF0, 0x0C, 0xC0, 0x33, 0x01, 0xCE, 0x06, 0x18, 0x18, 0x60, 0xE1, 0xC3, 0x03, 0x0F, 0xFC, 0x7F, 0xF9, 0x80, 0x66, 0x01, 0xB8, 0x07, 0xC0, 0x0F, 0x00, 0x30, /* 0xC1 */ 0x01, 0xC0, 0x0C, 0x00, 0x20, 0x00, 0x00, 0x07, 0x80, 0x1E, 0x00, 0x78, 0x03, 0xF0, 0x0C, 0xC0, 0x33, 0x01, 0xCE, 0x06, 0x18, 0x18, 0x60, 0xE1, 0xC3, 0x03, 0x0F, 0xFC, 0x7F, 0xF9, 0x80, 0x66, 0x01, 0xB8, 0x07, 0xC0, 0x0F, 0x00, 0x30, /* 0xC2 */ 0x07, 0x00, 0x3E, 0x01, 0x8C, 0x00, 0x00, 0x07, 0x80, 0x1E, 0x00, 0x78, 0x03, 0xF0, 0x0C, 0xC0, 0x33, 0x01, 0xCE, 0x06, 0x18, 0x18, 0x60, 0xE1, 0xC3, 0x03, 0x0F, 0xFC, 0x7F, 0xF9, 0x80, 0x66, 0x01, 0xB8, 0x07, 0xC0, 0x0F, 0x00, 0x30, /* 0xC3 */ 0x0E, 0x40, 0x7F, 0x01, 0x98, 0x00, 0x00, 0x07, 0x80, 0x1E, 0x00, 0x78, 0x03, 0xF0, 0x0C, 0xC0, 0x33, 0x01, 0xCE, 0x06, 0x18, 0x18, 0x60, 0xE1, 0xC3, 0x03, 0x0F, 0xFC, 0x7F, 0xF9, 0x80, 0x66, 0x01, 0xB8, 0x07, 0xC0, 0x0F, 0x00, 0x30, /* 0xC4 */ 0x0C, 0xC0, 0x33, 0x00, 0x00, 0x01, 0xE0, 0x07, 0x80, 0x1E, 0x00, 0xFC, 0x03, 0x30, 0x0C, 0xC0, 0x73, 0x81, 0x86, 0x06, 0x18, 0x38, 0x70, 0xC0, 0xC3, 0xFF, 0x1F, 0xFE, 0x60, 0x19, 0x80, 0x6E, 0x01, 0xF0, 0x03, 0xC0, 0x0C, /* 0xC5 */ 0x03, 0x00, 0x1E, 0x00, 0xEC, 0x03, 0x30, 0x0F, 0xC0, 0x1E, 0x00, 0x78, 0x01, 0xE0, 0x07, 0x80, 0x3F, 0x00, 0xCC, 0x03, 0x30, 0x1C, 0xE0, 0x61, 0x81, 0x86, 0x0E, 0x1C, 0x30, 0x30, 0xFF, 0xC7, 0xFF, 0x98, 0x06, 0x60, 0x1B, 0x80, 0x7C, 0x00, 0xF0, 0x03, /* 0xC6 */ 0x01, 0xFF, 0xFC, 0x07, 0xFF, 0xF0, 0x31, 0x80, 0x00, 0xC6, 0x00, 0x07, 0x18, 0x00, 0x18, 0x60, 0x00, 0x61, 0x80, 0x03, 0x86, 0x00, 0x0C, 0x1F, 0xF8, 0x70, 0x7F, 0xE1, 0x81, 0x80, 0x07, 0xFE, 0x00, 0x3F, 0xF8, 0x00, 0xC0, 0x60, 0x07, 0x01, 0x80, 0x1C, 0x06, 0x00, 0x60, 0x1F, 0xFF, 0x80, 0x7F, 0xF0, /* 0xC7 */ 0x07, 0xE0, 0x3F, 0xF0, 0xE0, 0x73, 0x80, 0x66, 0x00, 0x7C, 0x00, 0x30, 0x00, 0x60, 0x00, 0xC0, 0x01, 0x80, 0x03, 0x00, 0x06, 0x00, 0x0E, 0x00, 0x6C, 0x01, 0xDC, 0x03, 0x1C, 0x1E, 0x1F, 0xF8, 0x0F, 0xC0, 0x08, 0x00, 0x1E, 0x00, 0x0C, 0x01, 0x18, 0x01, 0xE0, 0x00, /* 0xC8 */ 0x1C, 0x00, 0xC0, 0x02, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xFF, 0xEF, 0xFE, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xFF, 0xFF, 0xFF, /* 0xC9 */ 0x07, 0x00, 0x60, 0x0C, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xFF, 0xEF, 0xFE, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xFF, 0xFF, 0xFF, /* 0xCA */ 0x0E, 0x01, 0xF0, 0x31, 0x80, 0x00, 0xFF, 0xFF, 0xFF, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xFF, 0xEF, 0xFE, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xFF, 0xFF, 0xFF, /* 0xCB */ 0x19, 0x81, 0x98, 0x00, 0x0F, 0xFF, 0xFF, 0xFC, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0F, 0xFE, 0xFF, 0xEC, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0F, 0xFF, 0xFF, 0xF0, /* 0xCC */ 0xE7, 0x10, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, /* 0xCD */ 0x36, 0xC0, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, /* 0xCE */ 0x39, 0xFC, 0x40, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC0, /* 0xCF */ 0xC7, 0x8C, 0x01, 0x83, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x83, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x83, 0x00, /* 0xD0 */ 0x7F, 0xE0, 0xFF, 0xE1, 0x80, 0xE3, 0x00, 0xE6, 0x00, 0xCC, 0x01, 0xD8, 0x01, 0xB0, 0x03, 0xFE, 0x07, 0xFC, 0x0D, 0x80, 0x1B, 0x00, 0x36, 0x00, 0x6C, 0x01, 0x98, 0x07, 0x30, 0x1C, 0x7F, 0xF0, 0xFF, 0xC0, /* 0xD1 */ 0x08, 0xC0, 0xFE, 0x05, 0xE0, 0x00, 0x0E, 0x01, 0xF0, 0x0F, 0xC0, 0x7E, 0x03, 0xD8, 0x1E, 0xE0, 0xF3, 0x07, 0x9C, 0x3C, 0x61, 0xE1, 0x8F, 0x0E, 0x78, 0x33, 0xC1, 0xDE, 0x06, 0xF0, 0x1F, 0x80, 0xFC, 0x03, 0xE0, 0x1C, /* 0xD2 */ 0x07, 0x00, 0x01, 0x80, 0x00, 0x60, 0x00, 0x00, 0x00, 0x7F, 0x00, 0xFF, 0xE0, 0xF0, 0x78, 0x60, 0x0C, 0x60, 0x03, 0x30, 0x01, 0xB0, 0x00, 0x78, 0x00, 0x3C, 0x00, 0x1E, 0x00, 0x0F, 0x00, 0x07, 0x80, 0x03, 0x60, 0x03, 0x30, 0x01, 0x8C, 0x01, 0x87, 0x83, 0xC1, 0xFF, 0xC0, 0x3F, 0x80, /* 0xD3 */ 0x00, 0xE0, 0x00, 0x60, 0x00, 0x40, 0x00, 0x00, 0x00, 0x7F, 0x00, 0xFF, 0xE0, 0xF0, 0x78, 0x60, 0x0C, 0x60, 0x03, 0x30, 0x01, 0xB0, 0x00, 0x78, 0x00, 0x3C, 0x00, 0x1E, 0x00, 0x0F, 0x00, 0x07, 0x80, 0x03, 0x60, 0x03, 0x30, 0x01, 0x8C, 0x01, 0x87, 0x83, 0xC1, 0xFF, 0xC0, 0x3F, 0x80, /* 0xD4 */ 0x03, 0xC0, 0x01, 0xE0, 0x01, 0x98, 0x00, 0x00, 0x00, 0x7F, 0x00, 0xFF, 0xE0, 0xF0, 0x78, 0x60, 0x0C, 0x60, 0x03, 0x30, 0x01, 0xB0, 0x00, 0x78, 0x00, 0x3C, 0x00, 0x1E, 0x00, 0x0F, 0x00, 0x07, 0x80, 0x03, 0x60, 0x03, 0x30, 0x01, 0x8C, 0x01, 0x87, 0x83, 0xC1, 0xFF, 0xC0, 0x3F, 0x80, /* 0xD5 */ 0x07, 0x20, 0x03, 0xF0, 0x01, 0x38, 0x00, 0x00, 0x00, 0x7F, 0x00, 0xFF, 0xE0, 0xF0, 0x78, 0x60, 0x0C, 0x60, 0x03, 0x30, 0x01, 0xB0, 0x00, 0x78, 0x00, 0x3C, 0x00, 0x1E, 0x00, 0x0F, 0x00, 0x07, 0x80, 0x03, 0x60, 0x03, 0x30, 0x01, 0x8C, 0x01, 0x87, 0x83, 0xC1, 0xFF, 0xC0, 0x3F, 0x80, /* 0xD6 */ 0x06, 0x30, 0x03, 0x18, 0x00, 0x00, 0x00, 0xFE, 0x01, 0xFF, 0xC1, 0xE0, 0xF0, 0xC0, 0x18, 0xC0, 0x06, 0x60, 0x03, 0x60, 0x00, 0xF0, 0x00, 0x78, 0x00, 0x3C, 0x00, 0x1E, 0x00, 0x0F, 0x00, 0x06, 0xC0, 0x06, 0x60, 0x03, 0x18, 0x03, 0x0F, 0x07, 0x83, 0xFF, 0x80, 0x7F, 0x00, /* 0xD7 */ 0x81, 0xC3, 0x66, 0x3C, 0x18, 0x3C, 0x66, 0xC3, 0x81, /* 0xD8 */ 0x07, 0xF0, 0x8F, 0xFE, 0x8F, 0x07, 0xC6, 0x00, 0xE6, 0x00, 0xF3, 0x00, 0xDF, 0x00, 0xC7, 0x80, 0xC3, 0xC0, 0xC1, 0xE0, 0xC0, 0xF0, 0xC0, 0x78, 0xC0, 0x3E, 0xC0, 0x33, 0xC0, 0x19, 0xC0, 0x1C, 0xF8, 0x3C, 0xDF, 0xF8, 0x43, 0xF8, 0x00, /* 0xD9 */ 0x0C, 0x00, 0x30, 0x00, 0xC0, 0x00, 0x0C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xF8, 0x0E, 0xE0, 0xE3, 0xFE, 0x0F, 0xC0, /* 0xDA */ 0x03, 0x80, 0x18, 0x01, 0x80, 0x00, 0x0C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xF8, 0x0E, 0xE0, 0xE3, 0xFE, 0x0F, 0xC0, /* 0xDB */ 0x07, 0x00, 0x7C, 0x06, 0x20, 0x00, 0x0C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xF8, 0x0E, 0xE0, 0xE3, 0xFE, 0x0F, 0xC0, /* 0xDC */ 0x0C, 0xC0, 0x66, 0x00, 0x01, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1F, 0x01, 0xDC, 0x1C, 0x7F, 0xC1, 0xF8, 0x00, /* 0xDD */ 0x01, 0x80, 0x0C, 0x00, 0x60, 0x00, 0x00, 0xC0, 0x0F, 0x80, 0x76, 0x01, 0x9C, 0x0C, 0x38, 0x70, 0x61, 0x81, 0xCE, 0x03, 0x30, 0x0F, 0x80, 0x1E, 0x00, 0x30, 0x00, 0xC0, 0x03, 0x00, 0x0C, 0x00, 0x30, 0x00, 0xC0, 0x03, 0x00, 0x0C, 0x00, /* 0xDE */ 0xC0, 0x0C, 0x00, 0xC0, 0x0F, 0xF8, 0xFF, 0xEC, 0x06, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x6F, 0xFE, 0xFF, 0x8C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, /* 0xDF */ 0x1F, 0x0F, 0xF3, 0x87, 0x60, 0x6C, 0x0D, 0x81, 0xB0, 0x66, 0x38, 0xC7, 0xD8, 0x1B, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x3E, 0x0E, 0xCF, 0x99, 0xE0, /* 0xE0 */ 0x1C, 0x00, 0xC0, 0x06, 0x00, 0x20, 0x00, 0x03, 0xF0, 0x7F, 0x8E, 0x1C, 0xC0, 0xC0, 0x0C, 0x01, 0xC3, 0xFC, 0xF8, 0xCC, 0x0C, 0xC0, 0xCE, 0x3C, 0x7E, 0xF3, 0xC7, /* 0xE1 */ 0x07, 0x00, 0x60, 0x0C, 0x01, 0x80, 0x00, 0x03, 0xF0, 0x7F, 0x8E, 0x1C, 0xC0, 0xC0, 0x0C, 0x01, 0xC3, 0xFC, 0xF8, 0xCC, 0x0C, 0xC0, 0xCE, 0x3C, 0x7E, 0xF3, 0xC7, /* 0xE2 */ 0x0C, 0x01, 0xE0, 0x1B, 0x03, 0x30, 0x00, 0x03, 0xF0, 0x7F, 0x8E, 0x1C, 0xC0, 0xC0, 0x0C, 0x01, 0xC3, 0xFC, 0xF8, 0xCC, 0x0C, 0xC0, 0xCE, 0x3C, 0x7E, 0xF3, 0xC7, /* 0xE3 */ 0x19, 0x83, 0xF0, 0x27, 0x00, 0x00, 0x00, 0x03, 0xF0, 0x7F, 0x8E, 0x1C, 0xC0, 0xC0, 0x0C, 0x01, 0xC3, 0xFC, 0xF8, 0xCC, 0x0C, 0xC0, 0xCE, 0x3C, 0x7E, 0xF3, 0xC7, /* 0xE4 */ 0x19, 0x81, 0x98, 0x00, 0x00, 0x00, 0x3F, 0x07, 0xF8, 0xE1, 0xCC, 0x0C, 0x00, 0xC0, 0x1C, 0x3F, 0xCF, 0x8C, 0xC0, 0xCC, 0x0C, 0xE3, 0xC7, 0xEF, 0x3C, 0x70, /* 0xE5 */ 0x0E, 0x01, 0xF0, 0x1B, 0x81, 0xB8, 0x1F, 0x00, 0xE0, 0x3F, 0x07, 0xF8, 0xE1, 0xCC, 0x0C, 0x00, 0xC0, 0x1C, 0x3F, 0xCF, 0x8C, 0xC0, 0xCC, 0x0C, 0xE3, 0xC7, 0xEF, 0x3C, 0x70, /* 0xE6 */ 0x3F, 0x1F, 0x0F, 0xF7, 0xF3, 0x87, 0xC3, 0x60, 0x70, 0x30, 0x0C, 0x06, 0x3F, 0xFF, 0xDF, 0xFF, 0xFF, 0x06, 0x00, 0xC0, 0xC0, 0x18, 0x3C, 0x0F, 0x8F, 0xC7, 0x3F, 0x9F, 0xE3, 0xC1, 0xF0, /* 0xE7 */ 0x1F, 0x0F, 0xE7, 0x1D, 0x83, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x38, 0x37, 0x1C, 0xFE, 0x1F, 0x02, 0x00, 0xE0, 0x0C, 0x23, 0x07, 0x80, /* 0xE8 */ 0x1C, 0x01, 0x80, 0x18, 0x01, 0x00, 0x00, 0x3E, 0x0F, 0xE3, 0x8E, 0xE0, 0x78, 0x0F, 0xFF, 0xFF, 0xFC, 0x01, 0x80, 0x38, 0x1B, 0x86, 0x3F, 0x83, 0xE0, /* 0xE9 */ 0x03, 0x00, 0xC0, 0x30, 0x04, 0x00, 0x00, 0x3E, 0x0F, 0xE3, 0x8E, 0xE0, 0x78, 0x0F, 0xFF, 0xFF, 0xFC, 0x01, 0x80, 0x38, 0x1B, 0x86, 0x3F, 0x83, 0xE0, /* 0xEA */ 0x0C, 0x03, 0xC0, 0x6C, 0x18, 0x80, 0x00, 0x3E, 0x0F, 0xE3, 0x8E, 0xE0, 0x78, 0x0F, 0xFF, 0xFF, 0xFC, 0x01, 0x80, 0x38, 0x1B, 0x86, 0x3F, 0x83, 0xE0, /* 0xEB */ 0x31, 0x86, 0x30, 0x00, 0x00, 0x01, 0xF0, 0x7F, 0x1C, 0x77, 0x03, 0xC0, 0x7F, 0xFF, 0xFF, 0xE0, 0x0C, 0x01, 0xC0, 0xDC, 0x31, 0xFC, 0x1F, 0x00, /* 0xEC */ 0xC6, 0x31, 0x06, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, /* 0xED */ 0x39, 0x99, 0x80, 0x18, 0xC6, 0x31, 0x8C, 0x63, 0x18, 0xC6, 0x31, 0x80, /* 0xEE */ 0x71, 0xED, 0xA3, 0x00, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC0, /* 0xEF */ 0xCF, 0x30, 0x00, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, /* 0xF0 */ 0x20, 0x07, 0xE0, 0x70, 0x3B, 0x00, 0x30, 0x3F, 0x0F, 0xF3, 0x8E, 0xE0, 0xF8, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF8, 0x3B, 0x8E, 0x3F, 0x83, 0xE0, /* 0xF1 */ 0x19, 0x8F, 0xE2, 0x70, 0x00, 0x00, 0x33, 0xCD, 0xFB, 0xC7, 0xE0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x30, /* 0xF2 */ 0x1C, 0x01, 0x80, 0x18, 0x01, 0x00, 0x00, 0x3E, 0x0F, 0xE3, 0x8E, 0xE0, 0xF8, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF8, 0x3B, 0x8E, 0x3F, 0x83, 0xE0, /* 0xF3 */ 0x07, 0x00, 0xC0, 0x30, 0x04, 0x00, 0x00, 0x3E, 0x0F, 0xE3, 0x8E, 0xE0, 0xF8, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF8, 0x3B, 0x8E, 0x3F, 0x83, 0xE0, /* 0xF4 */ 0x0C, 0x03, 0xC0, 0xD8, 0x19, 0x80, 0x00, 0x3E, 0x0F, 0xE3, 0x8E, 0xE0, 0xF8, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF8, 0x3B, 0x8E, 0x3F, 0x83, 0xE0, /* 0xF5 */ 0x19, 0x87, 0xE0, 0x9C, 0x00, 0x00, 0x00, 0x3E, 0x0F, 0xE3, 0x8E, 0xE0, 0xF8, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF8, 0x3B, 0x8E, 0x3F, 0x83, 0xE0, /* 0xF6 */ 0x31, 0x86, 0x30, 0x00, 0x00, 0x01, 0xF0, 0x7F, 0x1C, 0x77, 0x07, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0xC1, 0xDC, 0x71, 0xFC, 0x1F, 0x00, /* 0xF7 */ 0x06, 0x00, 0x60, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x06, 0x00, /* 0xF8 */ 0x1F, 0x27, 0xF5, 0xC7, 0x70, 0x7C, 0x17, 0x84, 0xF1, 0x1E, 0x43, 0xD0, 0x7C, 0x1D, 0xC7, 0x3F, 0xC9, 0xF0, /* 0xF9 */ 0x18, 0x03, 0x00, 0x60, 0x08, 0x00, 0x30, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x1F, 0x8F, 0x7E, 0xCF, 0x30, /* 0xFA */ 0x03, 0x01, 0x80, 0x60, 0x30, 0x00, 0x30, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x1F, 0x8F, 0x7E, 0xCF, 0x30, /* 0xFB */ 0x0C, 0x07, 0x81, 0x20, 0xCC, 0x00, 0x30, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x1F, 0x8F, 0x7E, 0xCF, 0x30, /* 0xFC */ 0x31, 0x8C, 0x60, 0x00, 0x00, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x7E, 0x3D, 0xFB, 0x3C, 0xC0, /* 0xFD */ 0x03, 0x80, 0x60, 0x18, 0x06, 0x00, 0x01, 0xC0, 0xD8, 0x1B, 0x06, 0x70, 0xC6, 0x18, 0xC6, 0x18, 0xC1, 0x98, 0x36, 0x06, 0xC0, 0x78, 0x0E, 0x01, 0xC0, 0x30, 0x06, 0x01, 0xC0, 0xF0, 0x1C, 0x00, /* 0xFE */ 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xCF, 0x8F, 0xFC, 0xF0, 0xEE, 0x06, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x3E, 0x06, 0xF0, 0xEF, 0xFC, 0xCF, 0x8C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x00, /* 0xFF */ 0x19, 0x83, 0x30, 0x00, 0x00, 0x0E, 0x06, 0xC0, 0xD8, 0x33, 0x86, 0x30, 0xC6, 0x30, 0xC6, 0x0C, 0xC1, 0xB0, 0x36, 0x03, 0xC0, 0x70, 0x0E, 0x01, 0x80, 0x30, 0x0E, 0x07, 0x80, 0xE0, 0x00, }; const GFXglyph FreeSans12pt_Win1252Glyphs[] PROGMEM = { /* 0x01 */ { 0, 19, 20, 21, 1, -17 }, /* 0x02 */ { 48, 19, 20, 21, 1, -17 }, /* 0x03 */ { 96, 21, 20, 23, 1, -17 }, /* 0x04 */ { 149, 21, 20, 23, 1, -17 }, /* 0x05 */ { 202, 20, 20, 22, 1, -17 }, /* 0x06 */ { 252, 20, 20, 22, 1, -17 }, /* 0x07 */ { 302, 0, 0, 8, 0, 0 }, /* 0x08 */ { 302, 23, 20, 25, 1, -17 }, /* 0x09 */ { 360, 23, 16, 25, 1, -16 }, /* 0x0A */ { 406, 0, 0, 8, 0, 0 }, /* 0x0B */ { 406, 21, 20, 23, 1, -17 }, /* 0x0C */ { 459, 19, 18, 21, 1, -15 }, /* 0x0D */ { 502, 0, 0, 8, 0, 0 }, /* 0x0E */ { 502, 20, 20, 22, 1, -17 }, /* 0x0F */ { 552, 21, 21, 23, 1, -18 }, /* 0x10 */ { 608, 19, 20, 21, 1, -17 }, /* 0x11 */ { 656, 21, 20, 23, 1, -17 }, /* 0x12 */ { 709, 20, 20, 22, 1, -17 }, /* 0x13 */ { 759, 21, 20, 23, 1, -17 }, /* 0x14 */ { 812, 21, 20, 23, 1, -17 }, /* 0x15 */ { 865, 22, 20, 24, 1, -17 }, /* 0x16 */ { 920, 16, 20, 18, 1, -17 }, /* 0x17 */ { 960, 21, 20, 23, 1, -17 }, /* 0x18 */ { 1013, 23, 20, 25, 1, -17 }, /* 0x19 */ { 1071, 21, 20, 23, 1, -17 }, /* 0x1A */ { 1124, 15, 19, 17, 1, -16 }, /* 0x1B */ { 1160, 24, 21, 26, 1, -18 }, /* 0x1C */ { 1223, 21, 20, 23, 1, -17 }, /* 0x1D */ { 1276, 21, 21, 23, 1, -18 }, /* 0x1E */ { 1332, 20, 20, 22, 1, -17 }, /* 0x1F */ { 1382, 15, 20, 17, 1, -17 }, /* ' ' 0x20 */ { 1420, 0, 0, 6, 0, 0 }, /* '!' 0x21 */ { 1420, 2, 18, 8, 3, -16 }, /* '"' 0x22 */ { 1425, 6, 6, 8, 1, -15 }, /* '#' 0x23 */ { 1430, 13, 16, 13, 0, -14 }, /* '$' 0x24 */ { 1456, 11, 20, 13, 1, -16 }, /* '%' 0x25 */ { 1484, 20, 17, 21, 1, -15 }, /* '&' 0x26 */ { 1527, 14, 17, 16, 1, -15 }, /* ''' 0x27 */ { 1557, 2, 6, 5, 1, -15 }, /* '(' 0x28 */ { 1559, 5, 23, 8, 2, -16 }, /* ')' 0x29 */ { 1574, 5, 23, 8, 1, -16 }, /* '*' 0x2A */ { 1589, 7, 7, 9, 1, -16 }, /* '+' 0x2B */ { 1596, 10, 11, 14, 2, -9 }, /* ',' 0x2C */ { 1610, 2, 6, 7, 2, 0 }, /* '-' 0x2D */ { 1612, 6, 2, 8, 1, -6 }, /* '.' 0x2E */ { 1614, 2, 2, 6, 2, 0 }, /* '/' 0x2F */ { 1615, 7, 18, 7, 0, -16 }, /* '0' 0x30 */ { 1631, 11, 17, 13, 1, -15 }, /* '1' 0x31 */ { 1655, 5, 17, 13, 3, -15 }, /* '2' 0x32 */ { 1666, 11, 17, 13, 1, -15 }, /* '3' 0x33 */ { 1690, 11, 17, 13, 1, -15 }, /* '4' 0x34 */ { 1714, 11, 17, 13, 1, -15 }, /* '5' 0x35 */ { 1738, 11, 17, 13, 1, -15 }, /* '6' 0x36 */ { 1762, 11, 17, 13, 1, -15 }, /* '7' 0x37 */ { 1786, 11, 17, 13, 1, -15 }, /* '8' 0x38 */ { 1810, 11, 17, 13, 1, -15 }, /* '9' 0x39 */ { 1834, 11, 17, 13, 1, -15 }, /* ':' 0x3A */ { 1858, 2, 13, 6, 2, -11 }, /* ';' 0x3B */ { 1862, 2, 16, 6, 2, -10 }, /* '<' 0x3C */ { 1866, 12, 11, 14, 1, -9 }, /* '=' 0x3D */ { 1883, 12, 6, 14, 1, -7 }, /* '>' 0x3E */ { 1892, 12, 11, 14, 1, -9 }, /* '?' 0x3F */ { 1909, 10, 18, 13, 2, -16 }, /* '@' 0x40 */ { 1932, 22, 21, 24, 1, -16 }, /* 'A' 0x41 */ { 1990, 14, 18, 16, 1, -16 }, /* 'B' 0x42 */ { 2022, 13, 18, 16, 2, -16 }, /* 'C' 0x43 */ { 2052, 15, 18, 17, 1, -16 }, /* 'D' 0x44 */ { 2086, 14, 18, 17, 2, -16 }, /* 'E' 0x45 */ { 2118, 12, 18, 15, 2, -16 }, /* 'F' 0x46 */ { 2145, 11, 18, 14, 2, -16 }, /* 'G' 0x47 */ { 2170, 16, 18, 18, 1, -16 }, /* 'H' 0x48 */ { 2206, 13, 18, 17, 2, -16 }, /* 'I' 0x49 */ { 2236, 2, 18, 7, 2, -16 }, /* 'J' 0x4A */ { 2241, 9, 18, 13, 1, -16 }, /* 'K' 0x4B */ { 2262, 13, 18, 16, 2, -16 }, /* 'L' 0x4C */ { 2292, 10, 18, 14, 2, -16 }, /* 'M' 0x4D */ { 2315, 16, 18, 20, 2, -16 }, /* 'N' 0x4E */ { 2351, 13, 18, 18, 2, -16 }, /* 'O' 0x4F */ { 2381, 17, 18, 19, 1, -16 }, /* 'P' 0x50 */ { 2420, 12, 18, 16, 2, -16 }, /* 'Q' 0x51 */ { 2447, 17, 19, 19, 1, -16 }, /* 'R' 0x52 */ { 2488, 14, 18, 17, 2, -16 }, /* 'S' 0x53 */ { 2520, 14, 18, 16, 1, -16 }, /* 'T' 0x54 */ { 2552, 12, 18, 15, 1, -16 }, /* 'U' 0x55 */ { 2579, 13, 18, 17, 2, -16 }, /* 'V' 0x56 */ { 2609, 14, 18, 15, 1, -16 }, /* 'W' 0x57 */ { 2641, 22, 18, 22, 0, -16 }, /* 'X' 0x58 */ { 2691, 14, 18, 16, 1, -16 }, /* 'Y' 0x59 */ { 2723, 14, 18, 16, 1, -16 }, /* 'Z' 0x5A */ { 2755, 13, 18, 15, 1, -16 }, /* '[' 0x5B */ { 2785, 4, 23, 7, 2, -16 }, /* '\' 0x5C */ { 2797, 7, 18, 7, 0, -16 }, /* ']' 0x5D */ { 2813, 4, 23, 7, 1, -16 }, /* '^' 0x5E */ { 2825, 9, 9, 11, 1, -15 }, /* '_' 0x5F */ { 2836, 15, 1, 13, -1, 5 }, /* '`' 0x60 */ { 2838, 5, 4, 6, 1, -16 }, /* 'a' 0x61 */ { 2841, 12, 13, 13, 1, -11 }, /* 'b' 0x62 */ { 2861, 12, 18, 13, 1, -16 }, /* 'c' 0x63 */ { 2888, 10, 13, 12, 1, -11 }, /* 'd' 0x64 */ { 2905, 11, 18, 13, 1, -16 }, /* 'e' 0x65 */ { 2930, 11, 13, 13, 1, -11 }, /* 'f' 0x66 */ { 2948, 5, 18, 7, 1, -16 }, /* 'g' 0x67 */ { 2960, 11, 18, 13, 1, -11 }, /* 'h' 0x68 */ { 2985, 10, 18, 13, 1, -16 }, /* 'i' 0x69 */ { 3008, 2, 18, 5, 2, -16 }, /* 'j' 0x6A */ { 3013, 4, 23, 6, 0, -16 }, /* 'k' 0x6B */ { 3025, 10, 18, 12, 1, -16 }, /* 'l' 0x6C */ { 3048, 2, 18, 5, 1, -16 }, /* 'm' 0x6D */ { 3053, 17, 13, 19, 1, -11 }, /* 'n' 0x6E */ { 3081, 10, 13, 13, 1, -11 }, /* 'o' 0x6F */ { 3098, 11, 13, 13, 1, -11 }, /* 'p' 0x70 */ { 3116, 12, 17, 13, 1, -11 }, /* 'q' 0x71 */ { 3142, 11, 17, 13, 1, -11 }, /* 'r' 0x72 */ { 3166, 6, 13, 8, 1, -11 }, /* 's' 0x73 */ { 3176, 10, 13, 12, 1, -11 }, /* 't' 0x74 */ { 3193, 5, 16, 7, 1, -14 }, /* 'u' 0x75 */ { 3203, 10, 13, 13, 1, -11 }, /* 'v' 0x76 */ { 3220, 11, 13, 12, 0, -11 }, /* 'w' 0x77 */ { 3238, 17, 13, 17, 0, -11 }, /* 'x' 0x78 */ { 3266, 10, 13, 11, 1, -11 }, /* 'y' 0x79 */ { 3283, 11, 18, 11, 0, -11 }, /* 'z' 0x7A */ { 3308, 10, 13, 12, 1, -11 }, /* '{' 0x7B */ { 3325, 5, 23, 8, 1, -16 }, /* '|' 0x7C */ { 3340, 2, 23, 6, 2, -16 }, /* '}' 0x7D */ { 3346, 5, 23, 8, 2, -16 }, /* '~' 0x7E */ { 3361, 10, 5, 12, 1, -9 }, /* 0x7F */ { 3368, 0, 0, 0, 0, 0 }, /* 0x80 */ { 3368, 14, 17, 16, 1, -15 }, /* 0x81 */ { 3398, 0, 0, 8, 0, 0 }, /* 0x82 */ { 3398, 2, 5, 6, 2, 0 }, /* 0x83 */ { 3400, 6, 23, 7, 0, -16 }, /* 0x84 */ { 3418, 6, 5, 10, 2, 0 }, /* 0x85 */ { 3422, 12, 2, 16, 2, 0 }, /* 0x86 */ { 3425, 10, 21, 13, 2, -15 }, /* 0x87 */ { 3452, 10, 20, 13, 2, -15 }, /* 0x88 */ { 3477, 7, 4, 8, 0, -16 }, /* 0x89 */ { 3481, 23, 18, 24, 0, -16 }, /* 0x8A */ { 3533, 14, 21, 16, 1, -19 }, /* 0x8B */ { 3570, 3, 8, 6, 1, -9 }, /* 0x8C */ { 3573, 22, 18, 24, 1, -16 }, /* 0x8D */ { 3623, 0, 0, 8, 0, 0 }, /* 0x8E */ { 3623, 13, 21, 15, 1, -19 }, /* 0x8F */ { 3658, 0, 0, 8, 0, 0 }, /* 0x90 */ { 3658, 0, 0, 8, 0, 0 }, /* 0x91 */ { 3658, 2, 6, 6, 2, -16 }, /* 0x92 */ { 3660, 2, 6, 6, 2, -16 }, /* 0x93 */ { 3662, 6, 6, 10, 2, -16 }, /* 0x94 */ { 3667, 6, 6, 10, 2, -16 }, /* 0x95 */ { 3672, 6, 6, 10, 2, -9 }, /* 0x96 */ { 3677, 10, 2, 12, 1, -6 }, /* 0x97 */ { 3680, 22, 2, 24, 1, -6 }, /* 0x98 */ { 3686, 7, 3, 8, 0, -16 }, /* 0x99 */ { 3689, 22, 13, 24, 2, -16 }, /* 0x9A */ { 3725, 10, 18, 12, 1, -16 }, /* 0x9B */ { 3748, 3, 8, 6, 2, -8 }, /* 0x9C */ { 3751, 20, 13, 22, 1, -11 }, /* 0x9D */ { 3784, 0, 0, 8, 0, 0 }, /* 0x9E */ { 3784, 10, 18, 12, 1, -16 }, /* 0x9F */ { 3807, 14, 21, 16, 1, -19 }, /* 0xA0 */ { 3844, 0, 0, 7, 0, 0 }, /* 0xA1 */ { 3844, 2, 18, 8, 3, -11 }, /* 0xA2 */ { 3849, 11, 17, 13, 1, -13 }, /* 0xA3 */ { 3873, 12, 18, 13, 0, -16 }, /* 0xA4 */ { 3900, 9, 9, 13, 2, -11 }, /* 0xA5 */ { 3911, 12, 17, 13, 1, -15 }, /* 0xA6 */ { 3937, 2, 23, 6, 2, -16 }, /* 0xA7 */ { 3943, 11, 23, 13, 1, -16 }, /* 0xA8 */ { 3975, 6, 2, 8, 1, -15 }, /* 0xA9 */ { 3977, 18, 17, 19, 1, -15 }, /* 0xAA */ { 4016, 7, 11, 9, 1, -16 }, /* 0xAB */ { 4026, 8, 8, 12, 2, -9 }, /* 0xAC */ { 4034, 12, 6, 14, 1, -7 }, /* 0xAD */ { 4043, 6, 2, 8, 1, -6 }, /* 0xAE */ { 4045, 18, 17, 19, 1, -15 }, /* 0xAF */ { 4084, 6, 2, 8, 1, -15 }, /* 0xB0 */ { 4086, 7, 8, 15, 4, -15 }, /* 0xB1 */ { 4093, 12, 15, 14, 1, -13 }, /* 0xB2 */ { 4116, 7, 10, 8, 1, -17 }, /* 0xB3 */ { 4125, 7, 10, 8, 1, -17 }, /* 0xB4 */ { 4134, 5, 4, 8, 2, -16 }, /* 0xB5 */ { 4137, 12, 17, 13, 2, -11 }, /* 0xB6 */ { 4163, 11, 21, 13, 2, -16 }, /* 0xB7 */ { 4192, 2, 2, 6, 2, -6 }, /* 0xB8 */ { 4193, 6, 5, 8, 1, 2 }, /* 0xB9 */ { 4197, 3, 10, 8, 3, -18 }, /* 0xBA */ { 4201, 6, 11, 9, 1, -16 }, /* 0xBB */ { 4210, 8, 8, 12, 2, -8 }, /* 0xBC */ { 4218, 17, 17, 21, 3, -15 }, /* 0xBD */ { 4255, 18, 18, 21, 3, -16 }, /* 0xBE */ { 4296, 19, 18, 21, 1, -16 }, /* 0xBF */ { 4339, 9, 18, 13, 3, -11 }, /* 0xC0 */ { 4360, 14, 22, 16, 1, -20 }, /* 0xC1 */ { 4399, 14, 22, 16, 1, -20 }, /* 0xC2 */ { 4438, 14, 22, 16, 1, -20 }, /* 0xC3 */ { 4477, 14, 22, 16, 1, -20 }, /* 0xC4 */ { 4516, 14, 21, 16, 1, -19 }, /* 0xC5 */ { 4553, 14, 24, 16, 1, -22 }, /* 0xC6 */ { 4595, 22, 18, 24, 0, -16 }, /* 0xC7 */ { 4645, 15, 23, 17, 1, -16 }, /* 0xC8 */ { 4689, 12, 22, 15, 2, -20 }, /* 0xC9 */ { 4722, 12, 22, 15, 2, -20 }, /* 0xCA */ { 4755, 12, 22, 15, 2, -20 }, /* 0xCB */ { 4788, 12, 21, 15, 2, -19 }, /* 0xCC */ { 4820, 4, 22, 7, 0, -20 }, /* 0xCD */ { 4831, 4, 22, 7, 1, -20 }, /* 0xCE */ { 4842, 6, 22, 7, 0, -20 }, /* 0xCF */ { 4859, 7, 21, 7, 0, -19 }, /* 0xD0 */ { 4878, 15, 18, 17, 1, -16 }, /* 0xD1 */ { 4912, 13, 22, 18, 2, -20 }, /* 0xD2 */ { 4948, 17, 22, 19, 1, -20 }, /* 0xD3 */ { 4995, 17, 22, 19, 1, -20 }, /* 0xD4 */ { 5042, 17, 22, 19, 1, -20 }, /* 0xD5 */ { 5089, 17, 22, 19, 1, -20 }, /* 0xD6 */ { 5136, 17, 21, 19, 1, -19 }, /* 0xD7 */ { 5181, 8, 9, 14, 3, -8 }, /* 0xD8 */ { 5190, 17, 18, 19, 1, -16 }, /* 0xD9 */ { 5229, 13, 22, 17, 2, -20 }, /* 0xDA */ { 5265, 13, 22, 17, 2, -20 }, /* 0xDB */ { 5301, 13, 22, 17, 2, -20 }, /* 0xDC */ { 5337, 13, 21, 17, 2, -19 }, /* 0xDD */ { 5372, 14, 22, 16, 1, -20 }, /* 0xDE */ { 5411, 12, 18, 15, 2, -16 }, /* 0xDF */ { 5438, 11, 18, 14, 2, -16 }, /* 0xE0 */ { 5463, 12, 18, 13, 1, -16 }, /* 0xE1 */ { 5490, 12, 18, 13, 1, -16 }, /* 0xE2 */ { 5517, 12, 18, 13, 1, -16 }, /* 0xE3 */ { 5544, 12, 18, 13, 1, -16 }, /* 0xE4 */ { 5571, 12, 17, 13, 1, -15 }, /* 0xE5 */ { 5597, 12, 19, 13, 1, -17 }, /* 0xE6 */ { 5626, 19, 13, 21, 1, -11 }, /* 0xE7 */ { 5657, 10, 18, 12, 1, -11 }, /* 0xE8 */ { 5680, 11, 18, 13, 1, -16 }, /* 0xE9 */ { 5705, 11, 18, 13, 1, -16 }, /* 0xEA */ { 5730, 11, 18, 13, 1, -16 }, /* 0xEB */ { 5755, 11, 17, 13, 1, -15 }, /* 0xEC */ { 5779, 4, 18, 5, 1, -16 }, /* 0xED */ { 5788, 5, 18, 5, 0, -16 }, /* 0xEE */ { 5800, 6, 18, 6, 0, -16 }, /* 0xEF */ { 5814, 6, 17, 6, 0, -15 }, /* 0xF0 */ { 5827, 11, 18, 13, 1, -16 }, /* 0xF1 */ { 5852, 10, 18, 13, 1, -16 }, /* 0xF2 */ { 5875, 11, 18, 13, 1, -16 }, /* 0xF3 */ { 5900, 11, 18, 13, 1, -16 }, /* 0xF4 */ { 5925, 11, 18, 13, 1, -16 }, /* 0xF5 */ { 5950, 11, 18, 13, 1, -16 }, /* 0xF6 */ { 5975, 11, 17, 13, 1, -15 }, /* 0xF7 */ { 5999, 12, 11, 14, 1, -9 }, /* 0xF8 */ { 6016, 11, 13, 13, 1, -11 }, /* 0xF9 */ { 6034, 10, 18, 13, 1, -16 }, /* 0xFA */ { 6057, 10, 18, 13, 1, -16 }, /* 0xFB */ { 6080, 10, 18, 13, 1, -16 }, /* 0xFC */ { 6103, 10, 17, 13, 1, -15 }, /* 0xFD */ { 6125, 11, 23, 11, 0, -16 }, /* 0xFE */ { 6157, 12, 21, 13, 1, -15 }, /* 0xFF */ { 6189, 11, 22, 11, 0, -15 }, }; const GFXfont FreeSans12pt_Win1252 PROGMEM = { (uint8_t*)FreeSans12pt_Win1252Bitmaps, (GFXglyph*)FreeSans12pt_Win1252Glyphs, 0x01, 0xFF, 19 }; ================================================ FILE: src/graphics/niche/Fonts/FreeSans12pt_Win1253.h ================================================ // trunk-ignore-all(clang-format) #pragma once /* PROPERTIES FONT_NAME FreeSans12pt_Win1253 */ const uint8_t FreeSans12pt_Win1253Bitmaps[] PROGMEM = { /* 0x01 */ 0x00, 0x30, 0x00, 0x09, 0x00, 0x01, 0x20, 0x00, 0x24, 0x00, 0x04, 0x80, 0x01, 0x90, 0x00, 0x62, 0x00, 0x30, 0xFE, 0x04, 0x10, 0x5F, 0x02, 0x0B, 0x00, 0x7F, 0xE0, 0x0C, 0x1C, 0x02, 0x83, 0x81, 0x9F, 0xF0, 0x02, 0x1E, 0x00, 0x41, 0xC0, 0x0E, 0x7F, 0x81, 0x78, 0x18, 0x62, 0x00, 0xFF, 0xC0, /* 0x02 */ 0x00, 0xFF, 0x80, 0x61, 0x13, 0xF0, 0x62, 0x60, 0x07, 0xFC, 0x00, 0x83, 0x80, 0x10, 0xF0, 0x33, 0xF6, 0x01, 0x41, 0xC0, 0x18, 0x38, 0x03, 0xFF, 0xE0, 0x47, 0x02, 0x08, 0x20, 0x61, 0xC4, 0x06, 0x17, 0x00, 0x22, 0x00, 0x02, 0x40, 0x00, 0x48, 0x00, 0x09, 0x00, 0x01, 0x20, 0x00, 0x3C, 0x00, /* 0x03 */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x00, 0x02, 0x30, 0x00, 0x09, 0x04, 0x08, 0x48, 0x70, 0xE1, 0xC3, 0x87, 0x0E, 0x08, 0x10, 0x70, 0x00, 0x03, 0x80, 0x00, 0x14, 0x00, 0x00, 0xA1, 0x81, 0x8D, 0x87, 0xF0, 0x44, 0x00, 0x06, 0x30, 0x00, 0x60, 0xC0, 0x06, 0x03, 0x80, 0x60, 0x07, 0xFC, 0x00, /* 0x04 */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x00, 0x02, 0x30, 0x00, 0x09, 0x10, 0x02, 0x48, 0xE0, 0x61, 0xC1, 0xCC, 0x0E, 0x78, 0x1C, 0x70, 0x00, 0x03, 0x80, 0x00, 0x14, 0xFF, 0xFC, 0xA6, 0x00, 0xCD, 0x9F, 0xFE, 0x44, 0x71, 0xE6, 0x30, 0xFC, 0x60, 0xC0, 0x06, 0x03, 0x80, 0x60, 0x07, 0xFC, 0x00, /* 0x05 */ 0x00, 0x18, 0x00, 0x00, 0x40, 0x01, 0x90, 0x01, 0xF4, 0x08, 0x12, 0x23, 0xC1, 0x91, 0x2C, 0x1C, 0x8A, 0xC3, 0x64, 0x64, 0x13, 0x22, 0x41, 0x98, 0x26, 0x2C, 0xC4, 0x22, 0x60, 0x42, 0x13, 0x04, 0x30, 0x80, 0x61, 0xA4, 0x02, 0x18, 0x20, 0x03, 0x41, 0x00, 0x20, 0x08, 0x02, 0x00, 0x60, 0x40, 0x03, 0xF8, /* 0x06 */ 0x00, 0x10, 0x00, 0x03, 0x00, 0x1C, 0x48, 0x00, 0xB4, 0x80, 0x09, 0xF9, 0xC0, 0xE0, 0xE4, 0x0C, 0x02, 0x8F, 0x80, 0x38, 0x88, 0x01, 0x0D, 0x00, 0x18, 0x30, 0x01, 0x60, 0x80, 0x13, 0x18, 0x03, 0xF2, 0xC0, 0x20, 0x26, 0x06, 0x07, 0xFF, 0xA0, 0x02, 0x39, 0x00, 0x14, 0x70, 0x01, 0xC3, 0x00, 0x18, 0x00, /* 0x07 */ /* 0x08 */ 0x00, 0x1F, 0x80, 0x00, 0x60, 0x80, 0x01, 0x00, 0x80, 0x06, 0x00, 0x80, 0x3C, 0x01, 0x01, 0x8C, 0x02, 0x02, 0x08, 0x04, 0x04, 0x08, 0x0C, 0x38, 0x00, 0x04, 0x80, 0x00, 0x06, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x2E, 0xC0, 0x01, 0x83, 0x7E, 0x0C, 0x10, 0x37, 0xE2, 0x61, 0x00, 0x0C, 0xC6, 0x10, 0x98, 0x0C, 0x63, 0x00, 0x00, 0xC6, 0x00, /* 0x09 */ 0x00, 0x1F, 0x80, 0x00, 0x60, 0x80, 0x01, 0x00, 0x80, 0x06, 0x00, 0x80, 0x3C, 0x01, 0x01, 0x8C, 0x02, 0x02, 0x08, 0x04, 0x04, 0x08, 0x0C, 0x38, 0x00, 0x04, 0x80, 0x00, 0x06, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x2E, 0xC0, 0x01, 0x83, 0x7E, 0x0C, 0x00, 0x37, 0xE0, /* 0x0A */ /* 0x0B */ 0x1F, 0x07, 0xC1, 0x86, 0x41, 0x10, 0x0C, 0x04, 0x80, 0x40, 0x18, 0x00, 0x00, 0xC0, 0x00, 0x06, 0x00, 0x00, 0x30, 0x00, 0x01, 0x40, 0x00, 0x0A, 0x00, 0x00, 0x88, 0x00, 0x04, 0x40, 0x00, 0x41, 0x00, 0x02, 0x04, 0x00, 0x20, 0x20, 0x02, 0x00, 0x80, 0x20, 0x02, 0x02, 0x00, 0x08, 0x20, 0x00, 0x22, 0x00, 0x00, 0xE0, 0x00, /* 0x0C */ 0x01, 0x00, 0x00, 0x38, 0x00, 0x04, 0xC0, 0x01, 0x08, 0x00, 0x18, 0x80, 0x1C, 0x10, 0x02, 0x07, 0x80, 0x81, 0x10, 0x1F, 0xC2, 0x02, 0x00, 0x60, 0x80, 0x1A, 0x20, 0x1C, 0x42, 0x1C, 0x08, 0xFE, 0x03, 0xA0, 0x01, 0x8C, 0x01, 0xC1, 0x43, 0xD0, 0x27, 0x81, 0xF8, /* 0x0D */ /* 0x0E */ 0x00, 0xE0, 0x00, 0x11, 0x00, 0x01, 0x10, 0x00, 0x0B, 0x00, 0x03, 0xF8, 0x00, 0x60, 0x60, 0x09, 0x02, 0x00, 0xA0, 0x10, 0x16, 0x01, 0x01, 0x40, 0x10, 0x10, 0x01, 0x01, 0x00, 0x08, 0x10, 0x00, 0x82, 0x1F, 0x08, 0x3F, 0x90, 0x44, 0x00, 0x06, 0xBF, 0xFF, 0xAF, 0xF0, 0xFF, 0xFF, 0x0F, 0xE3, 0xFB, 0xFC, /* 0x0F */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x40, 0x12, 0x34, 0x00, 0x69, 0x40, 0x01, 0x49, 0xE0, 0xF1, 0xCD, 0x06, 0x8E, 0x28, 0x14, 0x71, 0x40, 0xA3, 0x8B, 0xFD, 0x14, 0x50, 0x68, 0xA2, 0x81, 0x4D, 0x97, 0xFA, 0x44, 0xBF, 0xD6, 0x31, 0x02, 0xE0, 0xC8, 0x16, 0x08, 0x61, 0x08, 0x21, 0xF0, 0x80, 0xF8, 0x78, 0x00, /* 0x10 */ 0x00, 0xF0, 0x00, 0x3A, 0x00, 0x07, 0xC0, 0x00, 0xA8, 0x00, 0x1F, 0x00, 0x02, 0xB0, 0x00, 0x52, 0x00, 0x0A, 0x40, 0x02, 0x48, 0x00, 0x49, 0x00, 0x09, 0x30, 0x01, 0x22, 0x01, 0xC4, 0x70, 0xF0, 0x85, 0xE1, 0x10, 0x88, 0x37, 0x20, 0x03, 0x9C, 0x00, 0x37, 0x00, 0x06, 0x40, 0x01, 0x86, 0x00, /* 0x11 */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x60, 0x02, 0x36, 0x00, 0x09, 0x04, 0x0C, 0x48, 0x60, 0xC1, 0xC3, 0x0F, 0x0E, 0x00, 0x08, 0x70, 0x00, 0x23, 0x80, 0x63, 0x84, 0x01, 0x9F, 0x20, 0x0C, 0xFD, 0x80, 0x27, 0xE4, 0x03, 0x3F, 0x30, 0x33, 0xE0, 0xC0, 0x00, 0x03, 0x80, 0x60, 0x07, 0xFC, 0x00, /* 0x12 */ 0x00, 0xC2, 0x00, 0x1C, 0x24, 0x02, 0x18, 0x60, 0x64, 0x02, 0x02, 0x40, 0x20, 0x00, 0xF2, 0x03, 0x89, 0xE0, 0x7C, 0x80, 0x0E, 0x25, 0x80, 0xE1, 0x00, 0x1A, 0x08, 0x71, 0xB0, 0xC4, 0x39, 0x84, 0xC2, 0xCC, 0x40, 0x76, 0x7C, 0x05, 0xBB, 0x80, 0x4C, 0xE0, 0x0A, 0x78, 0x00, 0x9C, 0x00, 0x0F, 0x00, 0x00, /* 0x13 */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x00, 0x02, 0x30, 0x00, 0x09, 0x00, 0x00, 0x48, 0x60, 0xC1, 0xC6, 0xC9, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x03, 0x80, 0x00, 0x14, 0xFF, 0xF8, 0xA6, 0x00, 0xCD, 0x9F, 0xFE, 0x44, 0x71, 0xE6, 0x30, 0xFC, 0x60, 0xC0, 0x06, 0x03, 0x80, 0x60, 0x07, 0xFC, 0x00, /* 0x14 */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x20, 0x22, 0x33, 0x01, 0x89, 0x20, 0x02, 0x48, 0x60, 0xE1, 0xC8, 0x80, 0x8E, 0x46, 0x46, 0x72, 0x32, 0x33, 0x9F, 0x9F, 0x94, 0x78, 0x78, 0xA0, 0x00, 0x0D, 0x80, 0x00, 0x44, 0x0E, 0x06, 0x30, 0x00, 0x60, 0xC0, 0x06, 0x03, 0x80, 0x60, 0x07, 0xFC, 0x00, /* 0x15 */ 0x03, 0xFC, 0x20, 0x38, 0x1C, 0x81, 0x80, 0x1D, 0x08, 0x00, 0x32, 0x60, 0x00, 0x89, 0x00, 0x02, 0x18, 0x00, 0x08, 0x61, 0xC3, 0x22, 0x8D, 0x93, 0x72, 0x00, 0x00, 0x48, 0x00, 0x01, 0x20, 0x00, 0x04, 0x9F, 0xFF, 0x92, 0x60, 0x0E, 0x44, 0xFF, 0xF2, 0x11, 0xC3, 0x88, 0x21, 0xF8, 0x40, 0x40, 0x02, 0x00, 0xC0, 0x30, 0x00, 0xFF, 0x00, /* 0x16 */ 0x03, 0x80, 0x03, 0xC0, 0x01, 0xE0, 0x01, 0xE0, 0x03, 0xF0, 0x03, 0xF0, 0x27, 0xF0, 0x6F, 0x70, 0x6E, 0x60, 0xFC, 0x60, 0xFC, 0x7E, 0xFC, 0x7E, 0xFC, 0x3F, 0xF4, 0x1F, 0xF4, 0x1F, 0xF0, 0x0E, 0x70, 0x0E, 0x30, 0x1C, 0x38, 0x38, 0x0F, 0xF0, /* 0x17 */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x00, 0x02, 0x30, 0x00, 0x09, 0x00, 0x00, 0x48, 0x00, 0x21, 0xC0, 0x02, 0x8E, 0x20, 0xF4, 0x70, 0x84, 0x11, 0x82, 0x40, 0x84, 0x01, 0x03, 0x20, 0x0F, 0x85, 0x80, 0x03, 0x04, 0x00, 0x04, 0x30, 0x78, 0x10, 0xC0, 0x06, 0x03, 0x80, 0x60, 0x07, 0xFC, 0x00, /* 0x18 */ 0x00, 0xFC, 0x00, 0x02, 0x06, 0x00, 0x08, 0x24, 0x00, 0x21, 0xA4, 0x00, 0x4C, 0x48, 0x00, 0xA0, 0x50, 0x01, 0x92, 0x60, 0x03, 0x24, 0xC0, 0x06, 0x01, 0x81, 0x28, 0x03, 0x49, 0x6C, 0xC4, 0xAD, 0xD8, 0x16, 0xA4, 0xCC, 0xC4, 0x44, 0x86, 0x13, 0x05, 0x00, 0x28, 0x0A, 0x00, 0x50, 0x14, 0x00, 0x90, 0x48, 0x01, 0x20, 0x90, 0x02, 0x41, 0x20, 0x00, 0x00, /* 0x19 */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x00, 0x02, 0x30, 0x00, 0x09, 0x00, 0x00, 0x49, 0xC3, 0x81, 0xC0, 0x00, 0x0E, 0x78, 0xF0, 0x77, 0xEF, 0xC3, 0xA7, 0x4E, 0x15, 0x0A, 0x10, 0xA7, 0x8F, 0x0D, 0x80, 0x00, 0x44, 0x00, 0x06, 0x33, 0xF0, 0x60, 0xC0, 0x06, 0x03, 0x80, 0x60, 0x07, 0xFC, 0x00, /* 0x1A */ 0xFF, 0xFF, 0x00, 0x06, 0x00, 0x0C, 0x3E, 0x18, 0x82, 0x32, 0x02, 0x64, 0x04, 0xC8, 0x09, 0x80, 0x23, 0x00, 0x86, 0x02, 0x0C, 0x08, 0x18, 0x10, 0x30, 0x00, 0x60, 0x00, 0xC0, 0x81, 0x80, 0x03, 0x00, 0x07, 0xFF, 0xF8, /* 0x1B */ 0x00, 0xFE, 0x00, 0x03, 0x81, 0x80, 0x04, 0x00, 0x60, 0x08, 0x00, 0x30, 0x10, 0x00, 0x10, 0x30, 0x07, 0x88, 0x23, 0xC8, 0x08, 0x22, 0x00, 0x04, 0x60, 0x00, 0x44, 0x60, 0x00, 0x84, 0x63, 0x03, 0x04, 0x61, 0xFC, 0x04, 0x6B, 0x00, 0x9E, 0xA5, 0x01, 0x6A, 0xD5, 0x01, 0x43, 0xA8, 0x81, 0x05, 0xD0, 0x82, 0x0A, 0xA0, 0x82, 0x05, 0xC0, 0x82, 0x02, 0x61, 0xFF, 0x0C, 0x1E, 0x00, 0xF0, /* 0x1C */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x30, 0x02, 0x32, 0x00, 0x09, 0x00, 0x00, 0x48, 0x20, 0x61, 0xC3, 0x84, 0x0E, 0x1C, 0x78, 0x70, 0x40, 0x03, 0x80, 0x00, 0x14, 0x00, 0x00, 0xA0, 0x03, 0x0D, 0x83, 0xF0, 0x44, 0x00, 0x06, 0x30, 0x00, 0x60, 0xC0, 0x06, 0x03, 0x80, 0x60, 0x07, 0xFC, 0x00, /* 0x1D */ 0x01, 0xFE, 0x00, 0x3A, 0x1C, 0x03, 0x00, 0x30, 0x23, 0x1E, 0xC3, 0x38, 0x03, 0x10, 0xC3, 0x09, 0x00, 0x18, 0x68, 0x00, 0xC1, 0x40, 0x00, 0x0A, 0x07, 0x80, 0x50, 0x46, 0x02, 0x80, 0x00, 0x1A, 0x1E, 0x00, 0xCB, 0x10, 0x0D, 0x03, 0x00, 0x48, 0x60, 0x06, 0x40, 0x00, 0x22, 0x0C, 0x02, 0x10, 0x60, 0x60, 0x43, 0xFC, 0x01, 0xE0, 0x00, 0x00, /* 0x1E */ 0x01, 0xF0, 0x00, 0xEA, 0xC0, 0x31, 0x5F, 0x04, 0x5F, 0x88, 0x80, 0xA0, 0x48, 0x0E, 0x02, 0x8F, 0x40, 0x3C, 0x10, 0x21, 0x66, 0x87, 0x15, 0x98, 0x71, 0x41, 0x02, 0x14, 0x00, 0x01, 0x40, 0x00, 0x14, 0x00, 0x01, 0x21, 0xFE, 0x12, 0x00, 0x02, 0x10, 0x00, 0x60, 0x80, 0x0C, 0x06, 0x01, 0x80, 0x3F, 0xE0, /* 0x1F */ 0x0E, 0x00, 0x13, 0x00, 0x23, 0x00, 0xF3, 0x01, 0x31, 0x01, 0x11, 0x03, 0xD3, 0x06, 0xF2, 0x30, 0x34, 0xC7, 0x25, 0x33, 0x2B, 0xC2, 0x57, 0x04, 0x3A, 0x08, 0x72, 0x30, 0xA3, 0xC3, 0x40, 0x04, 0x40, 0x18, 0x40, 0x60, 0x7F, 0x80, /* ' ' 0x20 */ /* '!' 0x21 */ 0xFF, 0xFF, 0xFF, 0xF0, 0xF0, /* '"' 0x22 */ 0xCF, 0x3C, 0xF3, 0x8A, 0x20, /* '#' 0x23 */ 0x06, 0x30, 0x31, 0x03, 0x18, 0x18, 0xC7, 0xFF, 0xBF, 0xFC, 0x31, 0x01, 0x18, 0x18, 0xC7, 0xFF, 0xBF, 0xFC, 0x31, 0x01, 0x18, 0x18, 0xC0, 0xC6, 0x06, 0x30, /* '$' 0x24 */ 0x04, 0x03, 0xE1, 0xFF, 0x72, 0x7C, 0x47, 0x88, 0xF1, 0x07, 0xA0, 0x7E, 0x03, 0xF0, 0x17, 0x02, 0x7C, 0x47, 0x88, 0xF1, 0x1B, 0x26, 0x7F, 0xC3, 0xE0, 0x10, 0x02, 0x00, /* '%' 0x25 */ 0x00, 0x06, 0x03, 0xC0, 0x40, 0x7E, 0x0C, 0x0E, 0x70, 0x80, 0xC3, 0x18, 0x0C, 0x31, 0x00, 0xE7, 0x30, 0x07, 0xE6, 0x00, 0x3C, 0x40, 0x00, 0x0C, 0x7C, 0x00, 0x8F, 0xE0, 0x19, 0xC7, 0x01, 0x18, 0x30, 0x31, 0x83, 0x02, 0x1C, 0x70, 0x40, 0xFE, 0x04, 0x07, 0xC0, /* '&' 0x26 */ 0x0F, 0x00, 0x7E, 0x03, 0x9C, 0x0C, 0x30, 0x30, 0xC0, 0xE7, 0x01, 0xF8, 0x03, 0x80, 0x3E, 0x01, 0xCC, 0x6E, 0x39, 0xB0, 0x7C, 0xC0, 0xF3, 0x03, 0xCE, 0x1F, 0x9F, 0xE6, 0x3E, 0x1C, /* ''' 0x27 */ 0xFF, 0xA0, /* '(' 0x28 */ 0x08, 0x8C, 0x46, 0x31, 0x98, 0xC6, 0x31, 0x8C, 0x63, 0x08, 0x63, 0x08, 0x61, 0x0C, 0x20, /* ')' 0x29 */ 0x82, 0x18, 0xC3, 0x18, 0xC3, 0x18, 0xC6, 0x31, 0x8C, 0x62, 0x31, 0x88, 0xC4, 0x62, 0x00, /* '*' 0x2A */ 0x10, 0x23, 0x5B, 0xE3, 0x8D, 0x91, 0x00, /* '+' 0x2B */ 0x0C, 0x03, 0x00, 0xC0, 0x30, 0xFF, 0xFF, 0xF0, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, /* ',' 0x2C */ 0xF5, 0x60, /* '-' 0x2D */ 0xFF, 0xF0, /* '.' 0x2E */ 0xF0, /* '/' 0x2F */ 0x02, 0x0C, 0x10, 0x20, 0xC1, 0x02, 0x0C, 0x10, 0x20, 0xC1, 0x02, 0x0C, 0x10, 0x20, 0xC1, 0x00, /* '0' 0x30 */ 0x1F, 0x07, 0xF1, 0xC7, 0x30, 0x6C, 0x0F, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3E, 0x0E, 0xC1, 0x9C, 0x71, 0xFC, 0x1F, 0x00, /* '1' 0x31 */ 0x08, 0xCF, 0xFF, 0x8C, 0x63, 0x18, 0xC6, 0x31, 0x8C, 0x63, 0x18, /* '2' 0x32 */ 0x1F, 0x0F, 0xF9, 0x87, 0x60, 0x7C, 0x06, 0x00, 0xC0, 0x18, 0x07, 0x01, 0xC0, 0xF0, 0x78, 0x1C, 0x06, 0x00, 0xC0, 0x30, 0x07, 0xFF, 0xFF, 0xE0, /* '3' 0x33 */ 0x3F, 0x0F, 0xF3, 0x87, 0x60, 0x6C, 0x0C, 0x01, 0x80, 0x60, 0x78, 0x0F, 0x80, 0x18, 0x01, 0x80, 0x3C, 0x07, 0x80, 0xD8, 0x73, 0xFC, 0x3F, 0x00, /* '4' 0x34 */ 0x01, 0x80, 0x70, 0x0E, 0x03, 0xC0, 0xD8, 0x1B, 0x06, 0x61, 0x8C, 0x21, 0x8C, 0x33, 0x06, 0x7F, 0xFF, 0xFE, 0x03, 0x00, 0x60, 0x0C, 0x01, 0x80, /* '5' 0x35 */ 0x3F, 0xCF, 0xF9, 0x80, 0x30, 0x06, 0x00, 0xDE, 0x1F, 0xE7, 0x0E, 0x00, 0xE0, 0x0C, 0x01, 0x80, 0x30, 0x07, 0x81, 0xB8, 0x73, 0xFC, 0x1F, 0x00, /* '6' 0x36 */ 0x0F, 0x07, 0xF9, 0xC3, 0x30, 0x74, 0x01, 0x80, 0x33, 0xC7, 0xFE, 0xF1, 0xDC, 0x1F, 0x01, 0xE0, 0x3C, 0x06, 0xC1, 0xDC, 0x71, 0xFC, 0x1F, 0x00, /* '7' 0x37 */ 0xFF, 0xFF, 0xFC, 0x01, 0x00, 0x60, 0x18, 0x02, 0x00, 0xC0, 0x30, 0x06, 0x01, 0x80, 0x30, 0x04, 0x01, 0x80, 0x30, 0x06, 0x01, 0x80, 0x30, 0x00, /* '8' 0x38 */ 0x1F, 0x07, 0xF1, 0xC7, 0x30, 0x66, 0x0C, 0xC1, 0x8C, 0x61, 0xF8, 0x3F, 0x8E, 0x3B, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xD8, 0x31, 0xFC, 0x1F, 0x00, /* '9' 0x39 */ 0x1F, 0x07, 0xF1, 0xC7, 0x70, 0x6C, 0x07, 0x80, 0xF0, 0x1E, 0x07, 0x61, 0xEF, 0xFC, 0x79, 0x80, 0x30, 0x05, 0xC1, 0x98, 0x73, 0xFC, 0x1E, 0x00, /* ':' 0x3A */ 0xF0, 0x00, 0x03, 0xC0, /* ';' 0x3B */ 0xF0, 0x00, 0x0F, 0x56, /* '<' 0x3C */ 0x00, 0x70, 0x1E, 0x0F, 0x83, 0xC0, 0xF0, 0x0E, 0x00, 0x7C, 0x00, 0xF0, 0x03, 0xC0, 0x0F, 0x00, 0x10, /* '=' 0x3D */ 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, /* '>' 0x3E */ 0xE0, 0x07, 0x80, 0x1F, 0x00, 0x7C, 0x00, 0xF0, 0x07, 0x01, 0xE0, 0xF0, 0x3C, 0x0F, 0x00, 0x80, 0x00, /* '?' 0x3F */ 0x3F, 0x1F, 0xEE, 0x1F, 0x03, 0xC0, 0xC0, 0x30, 0x0C, 0x06, 0x03, 0x81, 0xC0, 0xE0, 0x30, 0x0C, 0x03, 0x00, 0x00, 0x00, 0x0C, 0x03, 0x00, /* '@' 0x40 */ 0x00, 0xFE, 0x00, 0x0F, 0xFE, 0x00, 0xF0, 0x3E, 0x07, 0x00, 0x3C, 0x38, 0x00, 0x38, 0xC1, 0xE0, 0x66, 0x0F, 0xD9, 0xD8, 0x61, 0xC3, 0xC3, 0x07, 0x0F, 0x1C, 0x1C, 0x3C, 0x60, 0x60, 0xF1, 0x81, 0x83, 0xC6, 0x06, 0x1B, 0x18, 0x38, 0xEE, 0x71, 0xE7, 0x18, 0xFD, 0xF8, 0x71, 0xE7, 0xC0, 0xE0, 0x00, 0x01, 0xE0, 0x00, 0x01, 0xFF, 0xC0, 0x01, 0xFC, 0x00, /* 'A' 0x41 */ 0x07, 0x80, 0x1E, 0x00, 0x78, 0x03, 0xF0, 0x0C, 0xC0, 0x33, 0x01, 0xCE, 0x06, 0x18, 0x18, 0x60, 0xE1, 0xC3, 0x03, 0x0F, 0xFC, 0x7F, 0xF9, 0x80, 0x66, 0x01, 0xB8, 0x07, 0xC0, 0x0F, 0x00, 0x30, /* 'B' 0x42 */ 0xFF, 0xC7, 0xFF, 0x30, 0x1D, 0x80, 0x6C, 0x03, 0x60, 0x1B, 0x00, 0xD8, 0x0C, 0xFF, 0xC7, 0xFF, 0x30, 0x0D, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x06, 0xFF, 0xF7, 0xFE, 0x00, /* 'C' 0x43 */ 0x07, 0xE0, 0x3F, 0xF0, 0xE0, 0x73, 0x80, 0x76, 0x00, 0x6C, 0x00, 0x30, 0x00, 0x60, 0x00, 0xC0, 0x01, 0x80, 0x03, 0x00, 0x06, 0x00, 0x0E, 0x00, 0x6C, 0x00, 0xDC, 0x03, 0x1E, 0x0E, 0x1F, 0xF8, 0x0F, 0xC0, /* 'D' 0x44 */ 0xFF, 0xC3, 0xFF, 0x8C, 0x07, 0x30, 0x0E, 0xC0, 0x1B, 0x00, 0x7C, 0x00, 0xF0, 0x03, 0xC0, 0x0F, 0x00, 0x3C, 0x00, 0xF0, 0x03, 0xC0, 0x1F, 0x00, 0x6C, 0x03, 0xB0, 0x1C, 0xFF, 0xE3, 0xFE, 0x00, /* 'E' 0x45 */ 0xFF, 0xFF, 0xFF, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xFF, 0xEF, 0xFE, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xFF, 0xFF, 0xFF, /* 'F' 0x46 */ 0xFF, 0xFF, 0xFF, 0x00, 0x60, 0x0C, 0x01, 0x80, 0x30, 0x06, 0x00, 0xFF, 0xDF, 0xFB, 0x00, 0x60, 0x0C, 0x01, 0x80, 0x30, 0x06, 0x00, 0xC0, 0x18, 0x00, /* 'G' 0x47 */ 0x07, 0xF0, 0x1F, 0xFC, 0x3C, 0x1E, 0x70, 0x07, 0x60, 0x03, 0xE0, 0x00, 0xC0, 0x00, 0xC0, 0x00, 0xC0, 0x7F, 0xC0, 0x7F, 0xC0, 0x03, 0xC0, 0x03, 0x60, 0x03, 0x60, 0x07, 0x30, 0x0F, 0x3C, 0x1F, 0x1F, 0xFB, 0x07, 0xE1, /* 'H' 0x48 */ 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xFF, 0xFF, 0xFF, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xC0, /* 'I' 0x49 */ 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, /* 'J' 0x4A */ 0x01, 0x80, 0xC0, 0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x01, 0x80, 0xC0, 0x60, 0x3C, 0x1E, 0x0F, 0x07, 0xC7, 0x7F, 0x1F, 0x00, /* 'K' 0x4B */ 0xC0, 0x3E, 0x03, 0xB0, 0x39, 0x83, 0x8C, 0x38, 0x63, 0x83, 0x38, 0x19, 0xC0, 0xDE, 0x07, 0xB8, 0x38, 0xE1, 0x83, 0x0C, 0x1C, 0x60, 0x73, 0x01, 0x98, 0x0E, 0xC0, 0x3E, 0x00, 0xC0, /* 'L' 0x4C */ 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xFF, 0xFF, 0xF0, /* 'M' 0x4D */ 0xE0, 0x07, 0xE0, 0x07, 0xF0, 0x0F, 0xF0, 0x0F, 0xD0, 0x0F, 0xD8, 0x1B, 0xD8, 0x1B, 0xD8, 0x1B, 0xCC, 0x33, 0xCC, 0x33, 0xCC, 0x33, 0xC6, 0x63, 0xC6, 0x63, 0xC6, 0x63, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC1, 0x83, /* 'N' 0x4E */ 0xE0, 0x1F, 0x00, 0xFC, 0x07, 0xE0, 0x3D, 0x81, 0xEE, 0x0F, 0x30, 0x79, 0xC3, 0xC6, 0x1E, 0x18, 0xF0, 0xE7, 0x83, 0x3C, 0x1D, 0xE0, 0x6F, 0x01, 0xF8, 0x0F, 0xC0, 0x3E, 0x01, 0xC0, /* 'O' 0x4F */ 0x07, 0xF0, 0x0F, 0xFE, 0x0F, 0x07, 0x86, 0x00, 0xC6, 0x00, 0x33, 0x00, 0x1B, 0x00, 0x07, 0x80, 0x03, 0xC0, 0x01, 0xE0, 0x00, 0xF0, 0x00, 0x78, 0x00, 0x36, 0x00, 0x33, 0x00, 0x18, 0xC0, 0x18, 0x78, 0x3C, 0x1F, 0xFC, 0x03, 0xF8, 0x00, /* 'P' 0x50 */ 0xFF, 0x8F, 0xFE, 0xC0, 0x6C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x06, 0xFF, 0xEF, 0xFC, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, /* 'Q' 0x51 */ 0x07, 0xF0, 0x0F, 0xFE, 0x0F, 0x07, 0x86, 0x00, 0xC6, 0x00, 0x33, 0x00, 0x1B, 0x00, 0x07, 0x80, 0x03, 0xC0, 0x01, 0xE0, 0x00, 0xF0, 0x00, 0x78, 0x00, 0x36, 0x00, 0x33, 0x01, 0x98, 0xC0, 0xFC, 0x78, 0x3C, 0x1F, 0xFF, 0x03, 0xF9, 0x80, 0x00, 0x40, /* 'R' 0x52 */ 0xFF, 0xE3, 0xFF, 0xCC, 0x03, 0xB0, 0x06, 0xC0, 0x1B, 0x00, 0x6C, 0x01, 0xB0, 0x0C, 0xFF, 0xE3, 0xFF, 0xCC, 0x03, 0xB0, 0x06, 0xC0, 0x1B, 0x00, 0x6C, 0x01, 0xB0, 0x06, 0xC0, 0x1B, 0x00, 0x70, /* 'S' 0x53 */ 0x0F, 0xE0, 0x7F, 0xC3, 0x83, 0x98, 0x07, 0x60, 0x0D, 0x80, 0x07, 0x00, 0x1E, 0x00, 0x3F, 0x80, 0x3F, 0xC0, 0x0F, 0x80, 0x07, 0xC0, 0x0F, 0x00, 0x3E, 0x00, 0xDE, 0x0E, 0x3F, 0xF0, 0x3F, 0x80, /* 'T' 0x54 */ 0xFF, 0xFF, 0xFF, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, /* 'U' 0x55 */ 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x80, 0xEE, 0x0E, 0x3F, 0xE0, 0xFC, 0x00, /* 'V' 0x56 */ 0xC0, 0x0F, 0x00, 0x7E, 0x01, 0x98, 0x06, 0x60, 0x39, 0xC0, 0xC3, 0x03, 0x0C, 0x1C, 0x38, 0x60, 0x61, 0x81, 0x8E, 0x07, 0x30, 0x0C, 0xC0, 0x37, 0x00, 0xF8, 0x01, 0xE0, 0x07, 0x80, 0x1C, 0x00, /* 'W' 0x57 */ 0xE0, 0x30, 0x1D, 0x80, 0xE0, 0x76, 0x07, 0x81, 0xDC, 0x1E, 0x06, 0x70, 0x7C, 0x18, 0xC1, 0xB0, 0xE3, 0x0C, 0xC3, 0x8C, 0x33, 0x0C, 0x38, 0xC6, 0x30, 0x67, 0x18, 0xC1, 0x98, 0x67, 0x06, 0x61, 0xD8, 0x1D, 0x83, 0x60, 0x3C, 0x0D, 0x80, 0xF0, 0x3E, 0x03, 0xC0, 0x70, 0x0F, 0x01, 0xC0, 0x18, 0x07, 0x00, /* 'X' 0x58 */ 0xE0, 0x1D, 0x80, 0xE7, 0x03, 0x0E, 0x1C, 0x18, 0x60, 0x73, 0x00, 0xFC, 0x01, 0xE0, 0x07, 0x00, 0x1E, 0x00, 0xF8, 0x03, 0x30, 0x1C, 0xE0, 0xE1, 0x83, 0x07, 0x1C, 0x0E, 0xE0, 0x1B, 0x00, 0x70, /* 'Y' 0x59 */ 0xC0, 0x0F, 0x80, 0x76, 0x01, 0x9C, 0x0C, 0x38, 0x70, 0x61, 0x81, 0xCE, 0x03, 0x30, 0x0F, 0x80, 0x1E, 0x00, 0x30, 0x00, 0xC0, 0x03, 0x00, 0x0C, 0x00, 0x30, 0x00, 0xC0, 0x03, 0x00, 0x0C, 0x00, /* 'Z' 0x5A */ 0xFF, 0xFF, 0xFF, 0xC0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0x60, 0x07, 0x00, 0x70, 0x07, 0x00, 0x30, 0x03, 0x80, 0x38, 0x03, 0x80, 0x18, 0x01, 0xC0, 0x1C, 0x00, 0xFF, 0xFF, 0xFF, 0xC0, /* '[' 0x5B */ 0xFF, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCF, 0xF0, /* '\' 0x5C */ 0x81, 0x81, 0x02, 0x06, 0x04, 0x08, 0x18, 0x10, 0x20, 0x60, 0x40, 0x81, 0x81, 0x02, 0x06, 0x04, /* ']' 0x5D */ 0xFF, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x3F, 0xF0, /* '^' 0x5E */ 0x0C, 0x0E, 0x05, 0x86, 0xC3, 0x21, 0x19, 0x8C, 0x83, 0xC1, 0x80, /* '_' 0x5F */ 0xFF, 0xFE, /* '`' 0x60 */ 0xE3, 0x8C, 0x30, /* 'a' 0x61 */ 0x3F, 0x07, 0xF8, 0xE1, 0xCC, 0x0C, 0x00, 0xC0, 0x1C, 0x3F, 0xCF, 0x8C, 0xC0, 0xCC, 0x0C, 0xE3, 0xC7, 0xEF, 0x3C, 0x70, /* 'b' 0x62 */ 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0xF8, 0xDF, 0xCF, 0x0E, 0xE0, 0x7C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x03, 0xE0, 0x6F, 0x0E, 0xDF, 0xCC, 0xF8, /* 'c' 0x63 */ 0x1F, 0x0F, 0xE6, 0x1F, 0x83, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x38, 0x37, 0x1C, 0xFE, 0x1F, 0x00, /* 'd' 0x64 */ 0x00, 0x60, 0x0C, 0x01, 0x80, 0x30, 0x06, 0x3C, 0xCF, 0xFB, 0x8F, 0xE0, 0xF8, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF8, 0x3B, 0x8F, 0x3F, 0x63, 0xCC, /* 'e' 0x65 */ 0x1F, 0x07, 0xF1, 0xC7, 0x70, 0x3C, 0x07, 0xFF, 0xFF, 0xFE, 0x00, 0xC0, 0x1C, 0x0D, 0xC3, 0x1F, 0xC1, 0xF0, /* 'f' 0x66 */ 0x3B, 0xD8, 0xC6, 0x7F, 0xEC, 0x63, 0x18, 0xC6, 0x31, 0x8C, 0x63, 0x00, /* 'g' 0x67 */ 0x1E, 0x67, 0xFD, 0xC7, 0xF0, 0x7C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x7C, 0x1D, 0xC7, 0x9F, 0xB1, 0xE6, 0x00, 0xC0, 0x3E, 0x0E, 0x7F, 0xC7, 0xE0, /* 'h' 0x68 */ 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x33, 0xCD, 0xFB, 0xC7, 0xE0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x30, /* 'i' 0x69 */ 0xF0, 0x3F, 0xFF, 0xFF, 0xF0, /* 'j' 0x6A */ 0x33, 0x00, 0x03, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x3F, 0xE0, /* 'k' 0x6B */ 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x6C, 0x33, 0x18, 0xCC, 0x37, 0x0F, 0xC3, 0xB8, 0xC6, 0x31, 0xCC, 0x3B, 0x06, 0xC1, 0xF0, 0x30, /* 'l' 0x6C */ 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, /* 'm' 0x6D */ 0xCF, 0x1F, 0x6F, 0xDF, 0xFC, 0x78, 0xFC, 0x18, 0x3C, 0x0C, 0x1E, 0x06, 0x0F, 0x03, 0x07, 0x81, 0x83, 0xC0, 0xC1, 0xE0, 0x60, 0xF0, 0x30, 0x78, 0x18, 0x3C, 0x0C, 0x18, /* 'n' 0x6E */ 0xCF, 0x37, 0xEF, 0x1F, 0x83, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xC0, /* 'o' 0x6F */ 0x1F, 0x07, 0xF1, 0xC7, 0x70, 0x7C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x7C, 0x1D, 0xC7, 0x1F, 0xC1, 0xF0, /* 'p' 0x70 */ 0xCF, 0x8D, 0xFC, 0xF0, 0xEE, 0x06, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x3E, 0x06, 0xF0, 0xEF, 0xFC, 0xCF, 0x8C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x00, /* 'q' 0x71 */ 0x1E, 0x67, 0xFD, 0xC7, 0xF0, 0x7C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x7C, 0x1D, 0xC7, 0x9F, 0xF1, 0xE6, 0x00, 0xC0, 0x18, 0x03, 0x00, 0x60, /* 'r' 0x72 */ 0xCF, 0x7F, 0x38, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC0, /* 's' 0x73 */ 0x3E, 0x1F, 0xEE, 0x1B, 0x00, 0xC0, 0x3C, 0x07, 0xF0, 0x3F, 0x01, 0xF0, 0x3E, 0x1D, 0xFE, 0x3F, 0x00, /* 't' 0x74 */ 0x63, 0x19, 0xFF, 0xB1, 0x8C, 0x63, 0x18, 0xC6, 0x31, 0xE7, /* 'u' 0x75 */ 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x7E, 0x3D, 0xFB, 0x3C, 0xC0, /* 'v' 0x76 */ 0xE0, 0x6C, 0x0D, 0x81, 0xB8, 0x63, 0x0C, 0x61, 0x8E, 0x60, 0xCC, 0x19, 0x83, 0xE0, 0x3C, 0x07, 0x00, 0xE0, /* 'w' 0x77 */ 0xC1, 0xC1, 0xB0, 0xE1, 0xD8, 0x70, 0xCC, 0x2C, 0x66, 0x36, 0x31, 0x9B, 0x18, 0xCD, 0x98, 0x64, 0x6C, 0x16, 0x36, 0x0F, 0x1A, 0x07, 0x8F, 0x03, 0x83, 0x80, 0xC1, 0xC0, /* 'x' 0x78 */ 0xC1, 0xF8, 0x66, 0x30, 0xCC, 0x3E, 0x07, 0x00, 0xC0, 0x78, 0x36, 0x0C, 0xC6, 0x3B, 0x06, 0xC0, 0xC0, /* 'y' 0x79 */ 0xE0, 0x6C, 0x0D, 0x83, 0x38, 0x63, 0x0C, 0x63, 0x0C, 0x60, 0xCC, 0x1B, 0x03, 0x60, 0x3C, 0x07, 0x00, 0xE0, 0x18, 0x03, 0x00, 0xE0, 0x78, 0x0E, 0x00, /* 'z' 0x7A */ 0xFF, 0xFF, 0xF0, 0x18, 0x0C, 0x07, 0x03, 0x81, 0xC0, 0x60, 0x30, 0x18, 0x0E, 0x03, 0xFF, 0xFF, 0xC0, /* '{' 0x7B */ 0x19, 0xCC, 0x63, 0x18, 0xC6, 0x31, 0x99, 0x86, 0x18, 0xC6, 0x31, 0x8C, 0x63, 0x1C, 0x60, /* '|' 0x7C */ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, /* '}' 0x7D */ 0xC7, 0x18, 0xC6, 0x31, 0x8C, 0x63, 0x0C, 0x33, 0x31, 0x8C, 0x63, 0x18, 0xC6, 0x73, 0x00, /* '~' 0x7E */ 0x70, 0x3E, 0x09, 0xE4, 0x1F, 0x03, 0x80, /* 0x7F */ /* 0x80 */ 0x01, 0xF0, 0x1F, 0xF0, 0xE0, 0xC7, 0x00, 0x18, 0x00, 0xC0, 0x07, 0xFF, 0x3F, 0xFC, 0x30, 0x01, 0xFF, 0x8F, 0xFC, 0x0C, 0x00, 0x18, 0x00, 0x70, 0x00, 0xE0, 0x81, 0xFE, 0x03, 0xF0, /* 0x81 */ /* 0x82 */ 0xF5, 0x80, /* 0x83 */ 0x1C, 0xF3, 0x0C, 0x31, 0xF7, 0xCC, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x33, 0xCE, 0x00, /* 0x84 */ 0xCF, 0x34, 0x51, 0x88, /* 0x85 */ 0xC6, 0x3C, 0x63, /* 0x86 */ 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x3F, 0xFF, 0xFC, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x00, /* 0x87 */ 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x3F, 0xFF, 0xFC, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x0F, 0xFF, 0xFF, 0x0C, 0x03, 0x00, 0xC0, 0x30, /* 0x88 */ 0x38, 0xD9, 0xB6, 0x30, /* 0x89 */ 0x38, 0x18, 0x00, 0xF8, 0x30, 0x03, 0x18, 0xC0, 0x04, 0x11, 0x80, 0x0C, 0x66, 0x00, 0x0F, 0x8C, 0x00, 0x0E, 0x30, 0x00, 0x00, 0x40, 0x00, 0x01, 0x80, 0x00, 0x06, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x31, 0xC0, 0xE0, 0x67, 0xC3, 0xC1, 0x98, 0xCC, 0xC3, 0x20, 0x90, 0x8C, 0x63, 0x33, 0x10, 0x7C, 0x3C, 0x60, 0x70, 0x38, /* 0x8A */ 0x0C, 0x40, 0x1F, 0x00, 0x38, 0x03, 0xF8, 0x1F, 0xF0, 0xE0, 0xE6, 0x01, 0xD8, 0x03, 0x60, 0x01, 0xC0, 0x07, 0x80, 0x0F, 0xE0, 0x0F, 0xF0, 0x03, 0xE0, 0x01, 0xF0, 0x03, 0xC0, 0x0F, 0x80, 0x37, 0x83, 0x8F, 0xFC, 0x0F, 0xE0, /* 0x8B */ 0x2F, 0x49, 0x99, /* 0x8C */ 0x07, 0xCF, 0xFC, 0x7F, 0xFF, 0xF3, 0x83, 0xC0, 0x18, 0x07, 0x00, 0x60, 0x0C, 0x03, 0x00, 0x30, 0x0C, 0x00, 0xC0, 0x30, 0x03, 0x00, 0xC0, 0x0F, 0xFF, 0x00, 0x3F, 0xFC, 0x00, 0xC0, 0x30, 0x03, 0x00, 0xC0, 0x0C, 0x01, 0x80, 0x30, 0x07, 0x01, 0xC0, 0x0E, 0x0F, 0x00, 0x1F, 0xEF, 0xFC, 0x1F, 0x3F, 0xF0, /* 0x8D */ /* 0x8E */ 0x0C, 0xC0, 0x3C, 0x00, 0xE1, 0xFF, 0xFF, 0xFF, 0x80, 0x1C, 0x01, 0xC0, 0x1C, 0x00, 0xC0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0x60, 0x07, 0x00, 0x70, 0x07, 0x00, 0x30, 0x03, 0x80, 0x38, 0x01, 0xFF, 0xFF, 0xFF, 0x80, /* 0x8F */ /* 0x90 */ /* 0x91 */ 0x6A, 0xF0, /* 0x92 */ 0xF5, 0x60, /* 0x93 */ 0x4E, 0x28, 0xA2, 0xCF, 0x30, /* 0x94 */ 0xCF, 0x34, 0x51, 0x4E, 0x20, /* 0x95 */ 0x7B, 0xFF, 0xFF, 0xFD, 0xE0, /* 0x96 */ 0xFF, 0xFF, 0xF0, /* 0x97 */ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, /* 0x98 */ 0x63, 0xFE, 0x70, /* 0x99 */ 0xFF, 0x70, 0x1F, 0xFD, 0xC0, 0x71, 0x87, 0x83, 0xC6, 0x1E, 0x0F, 0x18, 0x68, 0x3C, 0x61, 0xB1, 0xB1, 0x86, 0xC6, 0xC6, 0x19, 0x1B, 0x18, 0x66, 0xCC, 0x61, 0x9B, 0x31, 0x86, 0x3C, 0xC6, 0x18, 0xE3, 0x18, 0x63, 0x8C, /* 0x9A */ 0x63, 0x0D, 0x83, 0x60, 0x70, 0x00, 0x0F, 0x87, 0xFB, 0x86, 0xC0, 0x30, 0x0F, 0x01, 0xFC, 0x0F, 0xC0, 0x7C, 0x0F, 0x87, 0x7F, 0x8F, 0xC0, /* 0x9B */ 0x99, 0x92, 0xF4, /* 0x9C */ 0x1F, 0x0F, 0x83, 0xF9, 0xFC, 0x71, 0xF8, 0x6E, 0x0F, 0x03, 0xC0, 0x60, 0x3C, 0x07, 0xFF, 0xC0, 0x7F, 0xFC, 0x06, 0x00, 0xC0, 0x60, 0x0E, 0x0F, 0x03, 0x71, 0xF8, 0x63, 0xF9, 0xFC, 0x1F, 0x0F, 0x80, /* 0x9D */ /* 0x9E */ 0x63, 0x0C, 0x83, 0x60, 0x70, 0x00, 0x3F, 0xFF, 0xFC, 0x06, 0x03, 0x01, 0xC0, 0xE0, 0x70, 0x18, 0x0C, 0x06, 0x03, 0x80, 0xFF, 0xFF, 0xF0, /* 0x9F */ 0x0C, 0xC0, 0x33, 0x00, 0x00, 0x30, 0x03, 0xE0, 0x1D, 0x80, 0x67, 0x03, 0x0E, 0x1C, 0x18, 0x60, 0x73, 0x80, 0xCC, 0x03, 0xE0, 0x07, 0x80, 0x0C, 0x00, 0x30, 0x00, 0xC0, 0x03, 0x00, 0x0C, 0x00, 0x30, 0x00, 0xC0, 0x03, 0x00, /* 0xA0 */ /* 0xA1 */ 0xF0, 0xBF, 0xFF, 0xFF, 0xF0, /* 0xA2 */ 0x04, 0x00, 0x80, 0x7C, 0x1F, 0xE7, 0x4C, 0xC8, 0xF1, 0x1E, 0x20, 0xC4, 0x18, 0x83, 0x10, 0x72, 0x37, 0x4E, 0x7F, 0x87, 0xC0, 0x20, 0x04, 0x00, /* 0xA3 */ 0x0F, 0xC1, 0xFE, 0x38, 0x76, 0x03, 0x60, 0x36, 0x00, 0x70, 0x03, 0x80, 0xFF, 0x0F, 0xF0, 0x1C, 0x00, 0xC0, 0x0C, 0x01, 0x80, 0x10, 0x02, 0xF1, 0x7F, 0xF6, 0x1F, /* 0xA4 */ 0xDD, 0xFF, 0xD8, 0xD8, 0x3C, 0x1E, 0x0F, 0x8D, 0xFF, 0xDD, 0x80, /* 0xA5 */ 0xC0, 0x3E, 0x06, 0x60, 0x63, 0x0C, 0x30, 0xC1, 0x98, 0x19, 0x80, 0xF0, 0x0F, 0x07, 0xFE, 0x06, 0x00, 0x60, 0x7F, 0xE0, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, /* 0xA6 */ 0xFF, 0xFF, 0xF0, 0x3F, 0xFF, 0xFC, /* 0xA7 */ 0x0F, 0x03, 0xF0, 0xE7, 0x18, 0x63, 0x0C, 0x70, 0x07, 0x03, 0xF8, 0xC3, 0x98, 0x3B, 0x03, 0xF0, 0x37, 0x06, 0x78, 0xC7, 0xB0, 0x7C, 0x03, 0x80, 0x39, 0x83, 0x30, 0x67, 0x1C, 0x7F, 0x07, 0xC0, /* 0xA8 */ 0xCF, 0x30, /* 0xA9 */ 0x03, 0xF0, 0x03, 0xFF, 0x01, 0xE0, 0xE0, 0xE3, 0x1C, 0x73, 0xF3, 0x99, 0x86, 0x6C, 0xC1, 0x8F, 0x30, 0x03, 0xCC, 0x00, 0xF3, 0x00, 0x3C, 0xC1, 0x8D, 0x98, 0x66, 0x77, 0xF3, 0x8E, 0x79, 0xC1, 0xC0, 0xE0, 0x3F, 0xF0, 0x03, 0xF0, 0x00, /* 0xAA */ 0x79, 0x08, 0x11, 0xEE, 0x50, 0xA3, 0x3B, 0x00, 0x03, 0xF8, /* 0xAB */ 0x21, 0x63, 0xE7, 0x84, 0x84, 0xE7, 0x63, 0x21, /* 0xAC */ 0xFF, 0xFF, 0xFF, 0x00, 0x30, 0x03, 0x00, 0x30, 0x03, /* 0xAD */ 0xFF, 0xF0, /* 0xAE */ 0x03, 0xF0, 0x03, 0xFF, 0x01, 0xE0, 0xE0, 0xFF, 0x1C, 0x7F, 0xF3, 0x9B, 0x04, 0x6C, 0xC1, 0x8F, 0x30, 0x43, 0xCF, 0xF0, 0xF3, 0xFC, 0x3C, 0xC1, 0x0D, 0xB0, 0x66, 0x7C, 0x1B, 0x8F, 0x07, 0xC1, 0xC0, 0xE0, 0x3F, 0xF0, 0x03, 0xF0, 0x00, /* 0xAF */ 0xFF, 0xF0, /* 0xB0 */ 0x38, 0xFB, 0x1C, 0x18, 0x38, 0xDF, 0x1C, /* 0xB1 */ 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x7F, 0xE7, 0xFE, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x00, 0x00, 0x0F, 0xFF, 0xFF, 0xF0, /* 0xB2 */ 0x7D, 0x8F, 0x18, 0x30, 0xC6, 0x18, 0x60, 0xFF, 0xFC, /* 0xB3 */ 0x7D, 0x8F, 0x18, 0x31, 0x80, 0xC1, 0xE3, 0xC6, 0xF8, /* 0xB4 */ 0x3B, 0x99, 0x80, /* 0xB5 */ 0xC0, 0xCC, 0x0C, 0xC0, 0xCC, 0x0C, 0xC0, 0xCC, 0x0C, 0xC0, 0xCC, 0x0C, 0xC0, 0xCC, 0x1C, 0xE3, 0xCF, 0xEF, 0xFC, 0x7C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x00, /* 0xB6 */ 0x1F, 0xE7, 0xFD, 0xF3, 0x7E, 0x6F, 0xCD, 0xF9, 0xBF, 0x37, 0xE6, 0x7C, 0xCF, 0x98, 0xF3, 0x06, 0x60, 0xCC, 0x19, 0x83, 0x30, 0x66, 0x0C, 0xC1, 0x98, 0x33, 0x06, 0x60, 0xCC, /* 0xB7 */ 0xF0, /* 0xB8 */ 0x10, 0xF0, 0xE3, 0x78, /* 0xB9 */ 0x2F, 0xB6, 0xDB, 0x6C, /* 0xBA */ 0x79, 0x38, 0x61, 0x86, 0x1C, 0xDE, 0x00, 0x0F, 0xC0, /* 0xBB */ 0x88, 0xC6, 0xE7, 0x21, 0x21, 0xE7, 0xC6, 0x88, /* 0xBC */ 0x20, 0x08, 0x30, 0x0C, 0x38, 0x04, 0x0C, 0x06, 0x06, 0x02, 0x03, 0x02, 0x01, 0x81, 0x00, 0xC1, 0x06, 0x61, 0x87, 0x30, 0x83, 0x80, 0xC2, 0xC0, 0x42, 0x60, 0x43, 0x30, 0x21, 0xFC, 0x20, 0x0C, 0x30, 0x06, 0x10, 0x03, 0x00, /* 0xBD */ 0x20, 0x00, 0x08, 0x02, 0x06, 0x01, 0x83, 0x80, 0x40, 0x60, 0x20, 0x18, 0x18, 0x06, 0x04, 0x01, 0x83, 0x00, 0x61, 0x9F, 0x98, 0x4E, 0x76, 0x33, 0x0C, 0x08, 0x03, 0x04, 0x03, 0x83, 0x01, 0x80, 0x81, 0x80, 0x60, 0xC0, 0x30, 0x3F, 0xC8, 0x0F, 0xF0, /* 0xBE */ 0x7C, 0x00, 0x18, 0xC0, 0x43, 0x18, 0x18, 0x03, 0x02, 0x00, 0x60, 0xC0, 0x30, 0x10, 0x01, 0x84, 0x00, 0x31, 0x80, 0xC6, 0x20, 0xD8, 0xC8, 0x39, 0xF1, 0x07, 0x00, 0x41, 0x60, 0x18, 0x4C, 0x02, 0x11, 0x80, 0x83, 0xF8, 0x10, 0x06, 0x04, 0x00, 0xC1, 0x00, 0x18, /* 0xBF */ 0x0C, 0x06, 0x00, 0x00, 0x00, 0xC0, 0x60, 0x60, 0x30, 0x30, 0x38, 0x38, 0x18, 0x0C, 0x06, 0x0F, 0x07, 0xC7, 0x7F, 0x1F, 0x00, /* 0xC0 */ 0x0C, 0xDB, 0xD3, 0x00, 0x00, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, /* 0xC1 */ 0x03, 0x80, 0x07, 0x00, 0x1B, 0x00, 0x36, 0x00, 0xEE, 0x01, 0x8C, 0x03, 0x18, 0x0C, 0x18, 0x18, 0x30, 0x30, 0x60, 0xFF, 0xE1, 0xFF, 0xC7, 0x01, 0xCC, 0x01, 0x98, 0x03, 0x60, 0x03, 0xC0, 0x06, /* 0xC2 */ 0xFF, 0x87, 0xFF, 0x30, 0x1D, 0x80, 0x6C, 0x03, 0x60, 0x1B, 0x01, 0x9F, 0xFC, 0xFF, 0xE6, 0x03, 0xB0, 0x0F, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0xDF, 0xFE, 0xFF, 0xC0, /* 0xC3 */ 0xFF, 0xFF, 0xFF, 0x00, 0x60, 0x0C, 0x01, 0x80, 0x30, 0x06, 0x00, 0xC0, 0x18, 0x03, 0x00, 0x60, 0x0C, 0x01, 0x80, 0x30, 0x06, 0x00, 0xC0, 0x00, /* 0xC4 */ 0x01, 0xC0, 0x01, 0xC0, 0x03, 0x60, 0x03, 0x60, 0x07, 0x60, 0x06, 0x30, 0x06, 0x30, 0x0C, 0x18, 0x0C, 0x18, 0x1C, 0x18, 0x18, 0x0C, 0x18, 0x0C, 0x30, 0x06, 0x30, 0x06, 0x70, 0x06, 0x7F, 0xFF, 0x7F, 0xFF, /* 0xC5 */ 0xFF, 0xFF, 0xFF, 0xF0, 0x01, 0x80, 0x0C, 0x00, 0x60, 0x03, 0x00, 0x1F, 0xFE, 0xFF, 0xF6, 0x00, 0x30, 0x01, 0x80, 0x0C, 0x00, 0x60, 0x03, 0x00, 0x1F, 0xFF, 0xFF, 0xF8, /* 0xC6 */ 0x7F, 0xFD, 0xFF, 0xF0, 0x01, 0x80, 0x0C, 0x00, 0x60, 0x03, 0x80, 0x1C, 0x00, 0x60, 0x03, 0x00, 0x18, 0x00, 0xE0, 0x07, 0x00, 0x18, 0x00, 0xC0, 0x06, 0x00, 0x3F, 0xFF, 0xFF, 0xFC, /* 0xC7 */ 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x7F, 0xFF, 0xFF, 0xFE, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x18, /* 0xC8 */ 0x07, 0xF0, 0x0F, 0xFE, 0x0F, 0x07, 0x8E, 0x00, 0xE6, 0x00, 0x37, 0x00, 0x1F, 0x00, 0x07, 0x9F, 0xF3, 0xCF, 0xF9, 0xE0, 0x00, 0xF0, 0x00, 0x7C, 0x00, 0x76, 0x00, 0x33, 0x80, 0x38, 0xF0, 0x78, 0x3F, 0xF8, 0x07, 0xF0, 0x00, /* 0xC9 */ 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, /* 0xCA */ 0xC0, 0x3B, 0x01, 0xCC, 0x0E, 0x30, 0x70, 0xC3, 0x83, 0x1C, 0x0C, 0xE0, 0x37, 0x80, 0xFF, 0x03, 0xDC, 0x0E, 0x38, 0x30, 0x70, 0xC0, 0xE3, 0x03, 0x8C, 0x07, 0x30, 0x0E, 0xC0, 0x1C, /* 0xCB */ 0x01, 0xC0, 0x00, 0xE0, 0x00, 0xD8, 0x00, 0x6C, 0x00, 0x37, 0x00, 0x31, 0x80, 0x18, 0xC0, 0x18, 0x30, 0x0C, 0x18, 0x0E, 0x0E, 0x06, 0x03, 0x03, 0x01, 0x83, 0x00, 0x61, 0x80, 0x31, 0xC0, 0x1C, 0xC0, 0x06, 0x60, 0x03, 0x00, /* 0xCC */ 0xE0, 0x0F, 0xE0, 0x3F, 0xC0, 0x7F, 0x80, 0xFD, 0x83, 0x7B, 0x06, 0xF6, 0x0D, 0xE4, 0x13, 0xCC, 0x67, 0x98, 0xCF, 0x31, 0x9E, 0x36, 0x3C, 0x6C, 0x78, 0xD8, 0xF0, 0xA1, 0xE1, 0xC3, 0xC3, 0x86, /* 0xCD */ 0xC0, 0x1F, 0x00, 0xFC, 0x07, 0xE0, 0x3D, 0x81, 0xEE, 0x0F, 0x30, 0x78, 0xC3, 0xC7, 0x1E, 0x18, 0xF0, 0x67, 0x83, 0xBC, 0x0D, 0xE0, 0x3F, 0x01, 0xF8, 0x07, 0xC0, 0x18, /* 0xCE */ 0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xFE, 0x7F, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xFF, 0xFC, /* 0xCF */ 0x07, 0xF0, 0x0F, 0xFE, 0x0F, 0x07, 0x8E, 0x00, 0xE6, 0x00, 0x37, 0x00, 0x1F, 0x00, 0x07, 0x80, 0x03, 0xC0, 0x01, 0xE0, 0x00, 0xF0, 0x00, 0x7C, 0x00, 0x76, 0x00, 0x33, 0x80, 0x38, 0xF0, 0x78, 0x3F, 0xF8, 0x07, 0xF0, 0x00, /* 0xD0 */ 0xFF, 0xFF, 0xFF, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x18, /* 0xD1 */ 0xFF, 0xC7, 0xFF, 0xB0, 0x0D, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x06, 0xFF, 0xF7, 0xFE, 0x30, 0x01, 0x80, 0x0C, 0x00, 0x60, 0x03, 0x00, 0x18, 0x00, 0xC0, 0x00, /* 0xD2 */ /* 0xD3 */ 0xFF, 0xEF, 0xFE, 0xC0, 0x06, 0x00, 0x30, 0x01, 0x80, 0x0C, 0x00, 0x60, 0x03, 0x00, 0x60, 0x0C, 0x01, 0x80, 0x30, 0x06, 0x00, 0xC0, 0x0F, 0xFF, 0xFF, 0xF0, /* 0xD4 */ 0xFF, 0xFF, 0xFF, 0xF0, 0x30, 0x00, 0xC0, 0x03, 0x00, 0x0C, 0x00, 0x30, 0x00, 0xC0, 0x03, 0x00, 0x0C, 0x00, 0x30, 0x00, 0xC0, 0x03, 0x00, 0x0C, 0x00, 0x30, 0x00, 0xC0, 0x03, 0x00, /* 0xD5 */ 0xE0, 0x07, 0x60, 0x0E, 0x30, 0x1C, 0x38, 0x1C, 0x1C, 0x38, 0x0E, 0x70, 0x06, 0x60, 0x07, 0xE0, 0x03, 0xC0, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, /* 0xD6 */ 0x01, 0x80, 0x01, 0x80, 0x0F, 0xF0, 0x3F, 0xFC, 0x71, 0x8E, 0x61, 0x86, 0xC1, 0x83, 0xC1, 0x83, 0xC1, 0x83, 0xC1, 0x83, 0x61, 0x86, 0x71, 0x8E, 0x3F, 0xFC, 0x0F, 0xF0, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, /* 0xD7 */ 0x70, 0x1C, 0x70, 0x70, 0x61, 0xC0, 0xE3, 0x80, 0xEE, 0x00, 0xD8, 0x01, 0xF0, 0x01, 0xC0, 0x03, 0x80, 0x0D, 0x80, 0x3B, 0x80, 0x77, 0x01, 0xC7, 0x07, 0x07, 0x0E, 0x06, 0x38, 0x0E, 0xE0, 0x0E, /* 0xD8 */ 0xC1, 0x83, 0xC1, 0x83, 0xC1, 0x83, 0xC1, 0x83, 0xC1, 0x83, 0xC1, 0x83, 0xC1, 0x83, 0xC1, 0x83, 0x61, 0x86, 0x79, 0x9E, 0x3F, 0xFC, 0x0F, 0xF0, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, /* 0xD9 */ 0x07, 0xE0, 0x1F, 0xF8, 0x38, 0x3C, 0x70, 0x0E, 0x60, 0x06, 0xE0, 0x07, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0x60, 0x06, 0x60, 0x06, 0x30, 0x0C, 0x1C, 0x38, 0xFE, 0x7F, 0xFE, 0x7F, /* 0xDA */ 0xCF, 0x30, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, /* 0xDB */ 0x06, 0x60, 0x06, 0x60, 0x00, 0x00, 0xE0, 0x07, 0x60, 0x0E, 0x30, 0x1C, 0x38, 0x1C, 0x1C, 0x38, 0x0E, 0x70, 0x06, 0x60, 0x07, 0xE0, 0x03, 0xC0, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, /* 0xDC */ 0x03, 0x80, 0x30, 0x06, 0x00, 0x00, 0x1E, 0x33, 0xFB, 0x71, 0xFE, 0x0E, 0xC0, 0x6C, 0x06, 0xC0, 0x6C, 0x06, 0xC0, 0x6E, 0x0E, 0x71, 0xF3, 0xFF, 0x1F, 0x30, /* 0xDD */ 0x0C, 0x0C, 0x04, 0x00, 0x03, 0xE3, 0xFF, 0x8D, 0x80, 0xE0, 0x3E, 0x1F, 0x1C, 0x0C, 0x06, 0x0B, 0x8E, 0xFE, 0x3E, 0x00, /* 0xDE */ 0x07, 0x01, 0x80, 0xC0, 0x00, 0xCF, 0x3F, 0xEE, 0x1F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, /* 0xDF */ 0x76, 0xC0, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x60, /* 0xE0 */ 0x0C, 0x1B, 0x66, 0x98, 0x00, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x36, 0x19, 0xFE, 0x1E, 0x00, /* 0xE1 */ 0x1E, 0x33, 0xFB, 0x71, 0xFE, 0x0E, 0xC0, 0x6C, 0x06, 0xC0, 0x6C, 0x06, 0xC0, 0x6E, 0x0E, 0x71, 0xF3, 0xFF, 0x1F, 0x30, /* 0xE2 */ 0x1F, 0x0F, 0xF1, 0x87, 0x60, 0x6C, 0x0D, 0x83, 0x33, 0x86, 0x7C, 0xC1, 0xD8, 0x1F, 0x01, 0xE0, 0x3C, 0x07, 0xC0, 0xFC, 0x36, 0xFE, 0xCF, 0x18, 0x03, 0x00, 0x60, 0x0C, 0x01, 0x80, 0x00, /* 0xE3 */ 0x60, 0x66, 0x06, 0x60, 0x63, 0x0C, 0x30, 0xC3, 0x8C, 0x19, 0x81, 0x98, 0x1F, 0x80, 0xF0, 0x0F, 0x00, 0xF0, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, /* 0xE4 */ 0x7F, 0xCF, 0xF8, 0xE0, 0x07, 0x01, 0xF0, 0x7F, 0x1C, 0x77, 0x07, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0xC1, 0x9C, 0x71, 0xFC, 0x1F, 0x00, /* 0xE5 */ 0x3E, 0x3F, 0xF8, 0xD8, 0x0E, 0x03, 0xE1, 0xF1, 0xC0, 0xC0, 0x60, 0xB8, 0xEF, 0xE3, 0xE0, /* 0xE6 */ 0x3F, 0x9F, 0xC0, 0xC1, 0xC1, 0xC0, 0xC0, 0xC0, 0xC0, 0x60, 0x70, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x80, 0xFC, 0x3F, 0x80, 0xC0, 0x60, 0x70, 0xF0, 0x70, /* 0xE7 */ 0xCF, 0x3F, 0xEE, 0x1F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, /* 0xE8 */ 0x1F, 0x07, 0xF1, 0xC7, 0x30, 0x6C, 0x07, 0x80, 0xF0, 0x1F, 0xFF, 0xFF, 0xF8, 0x0F, 0x01, 0xE0, 0x3C, 0x0E, 0xC1, 0x9C, 0x71, 0xFC, 0x1F, 0x00, /* 0xE9 */ 0xFF, 0xFF, 0xFF, 0xC0, /* 0xEA */ 0xC1, 0xB0, 0xCC, 0x63, 0x30, 0xD8, 0x3C, 0x0F, 0x83, 0x60, 0xCC, 0x31, 0x8C, 0x73, 0x0C, 0xC1, 0x80, /* 0xEB */ 0x0C, 0x00, 0x60, 0x01, 0x80, 0x0C, 0x00, 0x60, 0x01, 0x80, 0x1C, 0x00, 0xF0, 0x0D, 0x80, 0x6C, 0x06, 0x30, 0x31, 0x83, 0x8E, 0x18, 0x30, 0xC1, 0x8C, 0x06, 0x60, 0x30, /* 0xEC */ 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF8, 0x7E, 0x1F, 0xFF, 0xDE, 0xF0, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x00, /* 0xED */ 0xC0, 0x78, 0x0D, 0x83, 0x30, 0x66, 0x0C, 0x63, 0x0C, 0x60, 0xD8, 0x1B, 0x03, 0x60, 0x38, 0x07, 0x00, 0x40, /* 0xEE */ 0x1F, 0x1F, 0x9C, 0x0C, 0x07, 0x01, 0xF8, 0x3C, 0x70, 0x70, 0x30, 0x30, 0x18, 0x0C, 0x06, 0x01, 0xC0, 0xFC, 0x1F, 0x00, 0xC0, 0x60, 0x70, 0xF0, 0x70, /* 0xEF */ 0x1F, 0x07, 0xF1, 0xC7, 0x70, 0x7C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x7C, 0x1D, 0xC7, 0x1F, 0xC1, 0xF0, /* 0xF0 */ 0xFF, 0xFF, 0xFF, 0xFF, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, /* 0xF1 */ 0x1F, 0x07, 0xF1, 0xC7, 0x70, 0x6C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x7C, 0x1F, 0xC7, 0x7F, 0xCD, 0xF1, 0x80, 0x30, 0x06, 0x00, 0xC0, 0x18, 0x00, /* 0xF2 */ 0x07, 0xE3, 0xFC, 0xE0, 0x30, 0x06, 0x01, 0x80, 0x30, 0x06, 0x00, 0xC0, 0x1C, 0x01, 0xC0, 0x1F, 0x80, 0xF8, 0x03, 0x80, 0x30, 0x0E, 0x0F, 0x81, 0xE0, /* 0xF3 */ 0x1F, 0xF9, 0xFF, 0xDC, 0x39, 0xC0, 0xCC, 0x03, 0x60, 0x1B, 0x00, 0xD8, 0x06, 0xC0, 0x37, 0x03, 0x9C, 0x38, 0x7F, 0x81, 0xF8, 0x00, /* 0xF4 */ 0xFF, 0xF3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, /* 0xF5 */ 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x36, 0x19, 0xFE, 0x1E, 0x00, /* 0xF6 */ 0x19, 0xE0, 0xEF, 0xC6, 0x31, 0xB8, 0xC3, 0xC3, 0x0F, 0x0C, 0x3C, 0x30, 0xF0, 0xC3, 0xE3, 0x1D, 0x8C, 0x67, 0xB7, 0x0F, 0xF8, 0x0F, 0xC0, 0x0C, 0x00, 0x30, 0x00, 0xC0, 0x03, 0x00, 0x0C, 0x00, /* 0xF7 */ 0x60, 0x33, 0x03, 0x8C, 0x18, 0x71, 0xC1, 0x8C, 0x0E, 0xC0, 0x36, 0x00, 0xE0, 0x07, 0x00, 0x38, 0x01, 0xC0, 0x1B, 0x00, 0xDC, 0x0C, 0x60, 0xE3, 0x86, 0x0C, 0x70, 0x33, 0x01, 0x80, /* 0xF8 */ 0xC3, 0x0F, 0x0C, 0x3C, 0x30, 0xF0, 0xC3, 0xC3, 0x0F, 0x0C, 0x3C, 0x30, 0xF0, 0xC3, 0xC3, 0x0F, 0x8C, 0x77, 0x33, 0x8F, 0xFC, 0x1F, 0xE0, 0x0C, 0x00, 0x30, 0x00, 0xC0, 0x03, 0x00, 0x0C, 0x00, /* 0xF9 */ 0x30, 0x0C, 0x60, 0x06, 0x60, 0x06, 0xE1, 0x87, 0xC1, 0x83, 0xC1, 0x83, 0xC1, 0x83, 0xC1, 0x83, 0xC1, 0x83, 0xE1, 0x87, 0x63, 0xC6, 0x7E, 0x7E, 0x3C, 0x38, /* 0xFA */ 0xCF, 0x30, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, /* 0xFB */ 0x33, 0x0C, 0xC0, 0x03, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xD8, 0x67, 0xF8, 0x78, /* 0xFC */ 0x07, 0x00, 0xC0, 0x30, 0x00, 0x01, 0xF0, 0x7F, 0x1C, 0x77, 0x07, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0xC1, 0xDC, 0x71, 0xFC, 0x1F, 0x00, /* 0xFD */ 0x06, 0x03, 0x00, 0x80, 0x00, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x36, 0x19, 0xFE, 0x1E, 0x00, /* 0xFE */ 0x00, 0xC0, 0x01, 0x80, 0x01, 0x00, 0x00, 0x00, 0x30, 0x0C, 0x60, 0x06, 0x60, 0x06, 0xE1, 0x87, 0xC1, 0x83, 0xC1, 0x83, 0xC1, 0x83, 0xC1, 0x83, 0xC1, 0x83, 0xE1, 0x87, 0x63, 0xC6, 0x7E, 0x7E, 0x3C, 0x38, /* 0xFF */ }; const GFXglyph FreeSans12pt_Win1253Glyphs[] PROGMEM = { /* 0x01 */ { 0, 19, 20, 21, 1, -17 }, /* 0x02 */ { 48, 19, 20, 21, 1, -17 }, /* 0x03 */ { 96, 21, 20, 23, 1, -17 }, /* 0x04 */ { 149, 21, 20, 23, 1, -17 }, /* 0x05 */ { 202, 20, 20, 22, 1, -17 }, /* 0x06 */ { 252, 20, 20, 22, 1, -17 }, /* 0x07 */ { 302, 0, 0, 8, 0, 0 }, /* 0x08 */ { 302, 23, 20, 25, 1, -17 }, /* 0x09 */ { 360, 23, 16, 25, 1, -16 }, /* 0x0A */ { 406, 0, 0, 8, 0, 0 }, /* 0x0B */ { 406, 21, 20, 23, 1, -17 }, /* 0x0C */ { 459, 19, 18, 21, 1, -15 }, /* 0x0D */ { 502, 0, 0, 8, 0, 0 }, /* 0x0E */ { 502, 20, 20, 22, 1, -17 }, /* 0x0F */ { 552, 21, 21, 23, 1, -18 }, /* 0x10 */ { 608, 19, 20, 21, 1, -17 }, /* 0x11 */ { 656, 21, 20, 23, 1, -17 }, /* 0x12 */ { 709, 20, 20, 22, 1, -17 }, /* 0x13 */ { 759, 21, 20, 23, 1, -17 }, /* 0x14 */ { 812, 21, 20, 23, 1, -17 }, /* 0x15 */ { 865, 22, 20, 24, 1, -17 }, /* 0x16 */ { 920, 16, 20, 18, 1, -17 }, /* 0x17 */ { 960, 21, 20, 23, 1, -17 }, /* 0x18 */ { 1013, 23, 20, 25, 1, -17 }, /* 0x19 */ { 1071, 21, 20, 23, 1, -17 }, /* 0x1A */ { 1124, 15, 19, 17, 1, -16 }, /* 0x1B */ { 1160, 24, 21, 26, 1, -18 }, /* 0x1C */ { 1223, 21, 20, 23, 1, -17 }, /* 0x1D */ { 1276, 21, 21, 23, 1, -18 }, /* 0x1E */ { 1332, 20, 20, 22, 1, -17 }, /* 0x1F */ { 1382, 15, 20, 17, 1, -17 }, /* ' ' 0x20 */ { 1420, 0, 0, 6, 0, 0 }, /* '!' 0x21 */ { 1420, 2, 18, 8, 3, -16 }, /* '"' 0x22 */ { 1425, 6, 6, 8, 1, -15 }, /* '#' 0x23 */ { 1430, 13, 16, 13, 0, -14 }, /* '$' 0x24 */ { 1456, 11, 20, 13, 1, -16 }, /* '%' 0x25 */ { 1484, 20, 17, 21, 1, -15 }, /* '&' 0x26 */ { 1527, 14, 17, 16, 1, -15 }, /* ''' 0x27 */ { 1557, 2, 6, 5, 1, -15 }, /* '(' 0x28 */ { 1559, 5, 23, 8, 2, -16 }, /* ')' 0x29 */ { 1574, 5, 23, 8, 1, -16 }, /* '*' 0x2A */ { 1589, 7, 7, 9, 1, -16 }, /* '+' 0x2B */ { 1596, 10, 11, 14, 2, -9 }, /* ',' 0x2C */ { 1610, 2, 6, 7, 2, 0 }, /* '-' 0x2D */ { 1612, 6, 2, 8, 1, -6 }, /* '.' 0x2E */ { 1614, 2, 2, 6, 2, 0 }, /* '/' 0x2F */ { 1615, 7, 18, 7, 0, -16 }, /* '0' 0x30 */ { 1631, 11, 17, 13, 1, -15 }, /* '1' 0x31 */ { 1655, 5, 17, 13, 3, -15 }, /* '2' 0x32 */ { 1666, 11, 17, 13, 1, -15 }, /* '3' 0x33 */ { 1690, 11, 17, 13, 1, -15 }, /* '4' 0x34 */ { 1714, 11, 17, 13, 1, -15 }, /* '5' 0x35 */ { 1738, 11, 17, 13, 1, -15 }, /* '6' 0x36 */ { 1762, 11, 17, 13, 1, -15 }, /* '7' 0x37 */ { 1786, 11, 17, 13, 1, -15 }, /* '8' 0x38 */ { 1810, 11, 17, 13, 1, -15 }, /* '9' 0x39 */ { 1834, 11, 17, 13, 1, -15 }, /* ':' 0x3A */ { 1858, 2, 13, 6, 2, -11 }, /* ';' 0x3B */ { 1862, 2, 16, 6, 2, -10 }, /* '<' 0x3C */ { 1866, 12, 11, 14, 1, -9 }, /* '=' 0x3D */ { 1883, 12, 6, 14, 1, -7 }, /* '>' 0x3E */ { 1892, 12, 11, 14, 1, -9 }, /* '?' 0x3F */ { 1909, 10, 18, 13, 2, -16 }, /* '@' 0x40 */ { 1932, 22, 21, 24, 1, -16 }, /* 'A' 0x41 */ { 1990, 14, 18, 16, 1, -16 }, /* 'B' 0x42 */ { 2022, 13, 18, 16, 2, -16 }, /* 'C' 0x43 */ { 2052, 15, 18, 17, 1, -16 }, /* 'D' 0x44 */ { 2086, 14, 18, 17, 2, -16 }, /* 'E' 0x45 */ { 2118, 12, 18, 15, 2, -16 }, /* 'F' 0x46 */ { 2145, 11, 18, 14, 2, -16 }, /* 'G' 0x47 */ { 2170, 16, 18, 18, 1, -16 }, /* 'H' 0x48 */ { 2206, 13, 18, 17, 2, -16 }, /* 'I' 0x49 */ { 2236, 2, 18, 7, 2, -16 }, /* 'J' 0x4A */ { 2241, 9, 18, 13, 1, -16 }, /* 'K' 0x4B */ { 2262, 13, 18, 16, 2, -16 }, /* 'L' 0x4C */ { 2292, 10, 18, 14, 2, -16 }, /* 'M' 0x4D */ { 2315, 16, 18, 20, 2, -16 }, /* 'N' 0x4E */ { 2351, 13, 18, 18, 2, -16 }, /* 'O' 0x4F */ { 2381, 17, 18, 19, 1, -16 }, /* 'P' 0x50 */ { 2420, 12, 18, 16, 2, -16 }, /* 'Q' 0x51 */ { 2447, 17, 19, 19, 1, -16 }, /* 'R' 0x52 */ { 2488, 14, 18, 17, 2, -16 }, /* 'S' 0x53 */ { 2520, 14, 18, 16, 1, -16 }, /* 'T' 0x54 */ { 2552, 12, 18, 15, 1, -16 }, /* 'U' 0x55 */ { 2579, 13, 18, 17, 2, -16 }, /* 'V' 0x56 */ { 2609, 14, 18, 15, 1, -16 }, /* 'W' 0x57 */ { 2641, 22, 18, 22, 0, -16 }, /* 'X' 0x58 */ { 2691, 14, 18, 16, 1, -16 }, /* 'Y' 0x59 */ { 2723, 14, 18, 16, 1, -16 }, /* 'Z' 0x5A */ { 2755, 13, 18, 15, 1, -16 }, /* '[' 0x5B */ { 2785, 4, 23, 7, 2, -16 }, /* '\' 0x5C */ { 2797, 7, 18, 7, 0, -16 }, /* ']' 0x5D */ { 2813, 4, 23, 7, 1, -16 }, /* '^' 0x5E */ { 2825, 9, 9, 11, 1, -15 }, /* '_' 0x5F */ { 2836, 15, 1, 13, -1, 5 }, /* '`' 0x60 */ { 2838, 5, 4, 6, 1, -16 }, /* 'a' 0x61 */ { 2841, 12, 13, 13, 1, -11 }, /* 'b' 0x62 */ { 2861, 12, 18, 13, 1, -16 }, /* 'c' 0x63 */ { 2888, 10, 13, 12, 1, -11 }, /* 'd' 0x64 */ { 2905, 11, 18, 13, 1, -16 }, /* 'e' 0x65 */ { 2930, 11, 13, 13, 1, -11 }, /* 'f' 0x66 */ { 2948, 5, 18, 7, 1, -16 }, /* 'g' 0x67 */ { 2960, 11, 18, 13, 1, -11 }, /* 'h' 0x68 */ { 2985, 10, 18, 13, 1, -16 }, /* 'i' 0x69 */ { 3008, 2, 18, 5, 2, -16 }, /* 'j' 0x6A */ { 3013, 4, 23, 6, 0, -16 }, /* 'k' 0x6B */ { 3025, 10, 18, 12, 1, -16 }, /* 'l' 0x6C */ { 3048, 2, 18, 5, 1, -16 }, /* 'm' 0x6D */ { 3053, 17, 13, 19, 1, -11 }, /* 'n' 0x6E */ { 3081, 10, 13, 13, 1, -11 }, /* 'o' 0x6F */ { 3098, 11, 13, 13, 1, -11 }, /* 'p' 0x70 */ { 3116, 12, 17, 13, 1, -11 }, /* 'q' 0x71 */ { 3142, 11, 17, 13, 1, -11 }, /* 'r' 0x72 */ { 3166, 6, 13, 8, 1, -11 }, /* 's' 0x73 */ { 3176, 10, 13, 12, 1, -11 }, /* 't' 0x74 */ { 3193, 5, 16, 7, 1, -14 }, /* 'u' 0x75 */ { 3203, 10, 13, 13, 1, -11 }, /* 'v' 0x76 */ { 3220, 11, 13, 12, 0, -11 }, /* 'w' 0x77 */ { 3238, 17, 13, 17, 0, -11 }, /* 'x' 0x78 */ { 3266, 10, 13, 11, 1, -11 }, /* 'y' 0x79 */ { 3283, 11, 18, 11, 0, -11 }, /* 'z' 0x7A */ { 3308, 10, 13, 12, 1, -11 }, /* '{' 0x7B */ { 3325, 5, 23, 8, 1, -16 }, /* '|' 0x7C */ { 3340, 2, 23, 6, 2, -16 }, /* '}' 0x7D */ { 3346, 5, 23, 8, 2, -16 }, /* '~' 0x7E */ { 3361, 10, 5, 12, 1, -9 }, /* 0x7F */ { 3368, 0, 0, 0, 0, 0 }, /* 0x80 */ { 3368, 14, 17, 16, 1, -15 }, /* 0x81 */ { 3398, 0, 0, 8, 0, 0 }, /* 0x82 */ { 3398, 2, 5, 6, 2, 0 }, /* 0x83 */ { 3400, 6, 23, 7, 0, -16 }, /* 0x84 */ { 3418, 6, 5, 10, 2, 0 }, /* 0x85 */ { 3422, 12, 2, 16, 2, 0 }, /* 0x86 */ { 3425, 10, 21, 13, 2, -15 }, /* 0x87 */ { 3452, 10, 20, 13, 2, -15 }, /* 0x88 */ { 3477, 7, 4, 8, 0, -16 }, /* 0x89 */ { 3481, 23, 18, 24, 0, -16 }, /* 0x8A */ { 3533, 14, 21, 16, 1, -19 }, /* 0x8B */ { 3570, 3, 8, 6, 1, -9 }, /* 0x8C */ { 3573, 22, 18, 24, 1, -16 }, /* 0x8D */ { 3623, 0, 0, 8, 0, 0 }, /* 0x8E */ { 3623, 13, 21, 15, 1, -19 }, /* 0x8F */ { 3658, 0, 0, 8, 0, 0 }, /* 0x90 */ { 3658, 0, 0, 8, 0, 0 }, /* 0x91 */ { 3658, 2, 6, 6, 2, -16 }, /* 0x92 */ { 3660, 2, 6, 6, 2, -16 }, /* 0x93 */ { 3662, 6, 6, 10, 2, -16 }, /* 0x94 */ { 3667, 6, 6, 10, 2, -16 }, /* 0x95 */ { 3672, 6, 6, 10, 2, -9 }, /* 0x96 */ { 3677, 10, 2, 12, 1, -6 }, /* 0x97 */ { 3680, 22, 2, 24, 1, -6 }, /* 0x98 */ { 3686, 7, 3, 8, 0, -16 }, /* 0x99 */ { 3689, 22, 13, 24, 2, -16 }, /* 0x9A */ { 3725, 10, 18, 12, 1, -16 }, /* 0x9B */ { 3748, 3, 8, 6, 2, -8 }, /* 0x9C */ { 3751, 20, 13, 22, 1, -11 }, /* 0x9D */ { 3784, 0, 0, 8, 0, 0 }, /* 0x9E */ { 3784, 10, 18, 12, 1, -16 }, /* 0x9F */ { 3807, 14, 21, 16, 1, -19 }, /* 0xA0 */ { 3844, 0, 0, 7, 0, 0 }, /* 0xA1 */ { 3844, 2, 18, 8, 3, -11 }, /* 0xA2 */ { 3849, 11, 17, 13, 1, -13 }, /* 0xA3 */ { 3873, 12, 18, 13, 0, -16 }, /* 0xA4 */ { 3900, 9, 9, 13, 2, -11 }, /* 0xA5 */ { 3911, 12, 17, 13, 1, -15 }, /* 0xA6 */ { 3937, 2, 23, 6, 2, -16 }, /* 0xA7 */ { 3943, 11, 23, 13, 1, -16 }, /* 0xA8 */ { 3975, 6, 2, 8, 1, -15 }, /* 0xA9 */ { 3977, 18, 17, 19, 1, -15 }, /* 0xAA */ { 4016, 7, 11, 9, 1, -16 }, /* 0xAB */ { 4026, 8, 8, 12, 2, -9 }, /* 0xAC */ { 4034, 12, 6, 14, 1, -7 }, /* 0xAD */ { 4043, 6, 2, 8, 1, -6 }, /* 0xAE */ { 4045, 18, 17, 19, 1, -15 }, /* 0xAF */ { 4084, 6, 2, 8, 1, -15 }, /* 0xB0 */ { 4086, 7, 8, 15, 4, -15 }, /* 0xB1 */ { 4093, 12, 15, 14, 1, -13 }, /* 0xB2 */ { 4116, 7, 10, 8, 1, -17 }, /* 0xB3 */ { 4125, 7, 10, 8, 1, -17 }, /* 0xB4 */ { 4134, 5, 4, 8, 2, -16 }, /* 0xB5 */ { 4137, 12, 17, 13, 2, -11 }, /* 0xB6 */ { 4163, 11, 21, 13, 2, -16 }, /* 0xB7 */ { 4192, 2, 2, 6, 2, -6 }, /* 0xB8 */ { 4193, 6, 5, 8, 1, 2 }, /* 0xB9 */ { 4197, 3, 10, 8, 3, -18 }, /* 0xBA */ { 4201, 6, 11, 9, 1, -16 }, /* 0xBB */ { 4210, 8, 8, 12, 2, -8 }, /* 0xBC */ { 4218, 17, 17, 21, 3, -15 }, /* 0xBD */ { 4255, 18, 18, 21, 3, -16 }, /* 0xBE */ { 4296, 19, 18, 21, 1, -16 }, /* 0xBF */ { 4339, 9, 18, 13, 3, -11 }, /* 0xC0 */ { 4360, 8, 18, 6, -1, -18 }, /* 0xC1 */ { 4378, 15, 17, 15, 0, -17 }, /* 0xC2 */ { 4410, 13, 17, 16, 2, -17 }, /* 0xC3 */ { 4438, 11, 17, 13, 2, -17 }, /* 0xC4 */ { 4462, 16, 17, 16, -1, -17 }, /* 0xC5 */ { 4496, 13, 17, 16, 2, -17 }, /* 0xC6 */ { 4524, 14, 17, 15, 0, -17 }, /* 0xC7 */ { 4554, 13, 17, 17, 2, -17 }, /* 0xC8 */ { 4582, 17, 17, 19, 1, -17 }, /* 0xC9 */ { 4619, 2, 17, 6, 2, -17 }, /* 0xCA */ { 4624, 14, 17, 16, 2, -17 }, /* 0xCB */ { 4654, 17, 17, 16, -1, -17 }, /* 0xCC */ { 4691, 15, 17, 19, 2, -17 }, /* 0xCD */ { 4723, 13, 17, 17, 2, -17 }, /* 0xCE */ { 4751, 14, 17, 16, 1, -17 }, /* 0xCF */ { 4781, 17, 17, 19, 1, -17 }, /* 0xD0 */ { 4818, 13, 17, 17, 2, -17 }, /* 0xD1 */ { 4846, 13, 17, 16, 2, -17 }, /* 0xD2 */ { 4874, 0, 0, 5, 0, 0 }, /* 0xD3 */ { 4874, 12, 17, 15, 2, -17 }, /* 0xD4 */ { 4900, 14, 17, 14, 0, -17 }, /* 0xD5 */ { 4930, 16, 17, 16, 0, -17 }, /* 0xD6 */ { 4964, 16, 17, 18, 1, -17 }, /* 0xD7 */ { 4998, 15, 17, 15, 0, -17 }, /* 0xD8 */ { 5030, 16, 17, 19, 2, -17 }, /* 0xD9 */ { 5064, 16, 17, 18, 1, -17 }, /* 0xDA */ { 5098, 6, 20, 6, 0, -20 }, /* 0xDB */ { 5113, 16, 20, 16, 0, -20 }, /* 0xDC */ { 5153, 12, 17, 14, 1, -17 }, /* 0xDD */ { 5179, 9, 17, 11, 1, -17 }, /* 0xDE */ { 5199, 10, 22, 14, 2, -17 }, /* 0xDF */ { 5227, 4, 17, 6, 1, -17 }, /* 0xE0 */ { 5236, 10, 17, 14, 2, -17 }, /* 0xE1 */ { 5258, 12, 13, 14, 1, -13 }, /* 0xE2 */ { 5278, 11, 22, 14, 2, -17 }, /* 0xE3 */ { 5309, 12, 18, 11, -1, -13 }, /* 0xE4 */ { 5336, 11, 17, 13, 1, -17 }, /* 0xE5 */ { 5360, 9, 13, 11, 1, -13 }, /* 0xE6 */ { 5375, 9, 22, 11, 1, -17 }, /* 0xE7 */ { 5400, 10, 18, 14, 2, -13 }, /* 0xE8 */ { 5423, 11, 17, 13, 1, -17 }, /* 0xE9 */ { 5447, 2, 13, 6, 2, -13 }, /* 0xEA */ { 5451, 10, 13, 12, 2, -13 }, /* 0xEB */ { 5468, 13, 17, 12, -1, -17 }, /* 0xEC */ { 5496, 10, 18, 14, 2, -13 }, /* 0xED */ { 5519, 11, 13, 11, 0, -13 }, /* 0xEE */ { 5537, 9, 22, 11, 1, -17 }, /* 0xEF */ { 5562, 11, 13, 13, 1, -13 }, /* 0xF0 */ { 5580, 16, 13, 17, 0, -13 }, /* 0xF1 */ { 5606, 11, 18, 14, 2, -13 }, /* 0xF2 */ { 5631, 11, 18, 12, 1, -13 }, /* 0xF3 */ { 5656, 13, 13, 15, 1, -13 }, /* 0xF4 */ { 5678, 6, 13, 9, 1, -13 }, /* 0xF5 */ { 5688, 10, 13, 14, 2, -13 }, /* 0xF6 */ { 5705, 14, 18, 16, 1, -13 }, /* 0xF7 */ { 5737, 13, 18, 13, 0, -13 }, /* 0xF8 */ { 5767, 14, 18, 18, 2, -13 }, /* 0xF9 */ { 5799, 16, 13, 18, 1, -13 }, /* 0xFA */ { 5825, 6, 16, 6, 0, -16 }, /* 0xFB */ { 5837, 10, 16, 14, 2, -16 }, /* 0xFC */ { 5857, 11, 17, 13, 1, -17 }, /* 0xFD */ { 5881, 10, 17, 14, 2, -17 }, /* 0xFE */ { 5903, 16, 17, 18, 1, -17 }, /* 0xFF */ { 5937, 0, 0, 5, 0, 0 }, }; const GFXfont FreeSans12pt_Win1253 PROGMEM = { (uint8_t*)FreeSans12pt_Win1253Bitmaps, (GFXglyph*)FreeSans12pt_Win1253Glyphs, 0x01, 0xFF, 19 }; ================================================ FILE: src/graphics/niche/Fonts/FreeSans6pt7b.h ================================================ #pragma once const uint8_t FreeSans6pt7bBitmaps[] PROGMEM = { /* ' ' 0x20 */ /* '!' 0x21 */ 0xFE, 0x80, /* '"' 0x22 */ 0xB6, 0x80, /* '#' 0x23 */ 0x24, 0x49, 0xF9, 0x42, 0x9F, 0x92, 0x28, /* '$' 0x24 */ 0x23, 0xAB, 0x5A, 0x38, 0xB5, 0xAB, 0x88, /* '%' 0x25 */ 0x71, 0x22, 0x88, 0xA2, 0x30, 0x74, 0x02, 0x60, 0xA4, 0x49, 0x11, 0x80, /* '&' 0x26 */ 0x31, 0x24, 0x8C, 0x72, 0x58, 0xA3, 0x74, /* ''' 0x27 */ 0xE0, /* '(' 0x28 */ 0x5A, 0xAA, 0x94, /* ')' 0x29 */ 0x89, 0x12, 0x49, 0x49, 0x00, /* '*' 0x2A */ 0x5E, 0x80, /* '+' 0x2B */ 0x21, 0x3E, 0x42, 0x00, /* ',' 0x2C */ 0xE0, /* '-' 0x2D */ 0xE0, /* '.' 0x2E */ 0x80, /* '/' 0x2F */ 0x25, 0x24, 0xA4, 0x80, /* '0' 0x30 */ 0x76, 0xE3, 0x18, 0xC6, 0x3B, 0x70, /* '1' 0x31 */ 0x5D, 0x55, 0x40, /* '2' 0x32 */ 0x74, 0x42, 0x11, 0x11, 0x10, 0xF8, /* '3' 0x33 */ 0x74, 0x42, 0x13, 0x04, 0x31, 0x70, /* '4' 0x34 */ 0x11, 0x8C, 0xA9, 0x4B, 0xE2, 0x10, /* '5' 0x35 */ 0x7D, 0x04, 0x1E, 0x4C, 0x10, 0x63, 0x78, /* '6' 0x36 */ 0x72, 0x61, 0xE8, 0xC6, 0x39, 0x70, /* '7' 0x37 */ 0xF8, 0x44, 0x22, 0x11, 0x08, 0x40, /* '8' 0x38 */ 0x7A, 0x18, 0x61, 0x7A, 0x18, 0x61, 0x78, /* '9' 0x39 */ 0x7B, 0x28, 0x61, 0xCD, 0xD0, 0x62, 0x70, /* ':' 0x3A */ 0x82, /* ';' 0x3B */ 0x87, /* '<' 0x3C */ 0x3E, 0x30, 0x60, 0x80, /* '=' 0x3D */ 0xF8, 0x3E, /* '>' 0x3E */ 0xE0, 0xC6, 0xC8, 0x00, /* '?' 0x3F */ 0x74, 0x42, 0x11, 0x10, 0x80, 0x20, /* '@' 0x40 */ 0x0F, 0x06, 0x19, 0x3B, 0xC4, 0x99, 0x13, 0x22, 0x64, 0x96, 0x6E, 0x40, 0x04, 0x00, 0x7C, 0x00, /* 'A' 0x41 */ 0x18, 0x18, 0x3C, 0x24, 0x24, 0x7E, 0x42, 0x42, 0xC3, /* 'B' 0x42 */ 0xFA, 0x18, 0x61, 0xFA, 0x18, 0x61, 0xF8, /* 'C' 0x43 */ 0x3E, 0x41, 0x80, 0x80, 0x80, 0x80, 0x81, 0x43, 0x3E, /* 'D' 0x44 */ 0xF9, 0x0A, 0x0C, 0x18, 0x30, 0x60, 0xC2, 0xF8, /* 'E' 0x45 */ 0xFE, 0x08, 0x20, 0xFA, 0x08, 0x20, 0xFC, /* 'F' 0x46 */ 0xFC, 0x21, 0x0F, 0xC2, 0x10, 0x80, /* 'G' 0x47 */ 0x3E, 0x41, 0x80, 0x80, 0x87, 0x81, 0xC1, 0x43, 0x3D, /* 'H' 0x48 */ 0x83, 0x06, 0x0C, 0x1F, 0xF0, 0x60, 0xC1, 0x82, /* 'I' 0x49 */ 0xFF, 0x80, /* 'J' 0x4A */ 0x08, 0x42, 0x10, 0x86, 0x31, 0x70, /* 'K' 0x4B */ 0x86, 0x29, 0x28, 0xD2, 0x48, 0xA1, 0x84, /* 'L' 0x4C */ 0x84, 0x21, 0x08, 0x42, 0x10, 0xF8, /* 'M' 0x4D */ 0xC3, 0xC3, 0xC3, 0xA5, 0xA5, 0xA5, 0x99, 0x99, 0x99, /* 'N' 0x4E */ 0xC3, 0x86, 0x8D, 0x19, 0x33, 0x62, 0xC3, 0x86, /* 'O' 0x4F */ 0x3E, 0x31, 0xB0, 0x70, 0x18, 0x0C, 0x07, 0x06, 0xC6, 0x3E, 0x00, /* 'P' 0x50 */ 0xFA, 0x18, 0x61, 0xFA, 0x08, 0x20, 0x80, /* 'Q' 0x51 */ 0x3E, 0x31, 0xB0, 0x70, 0x18, 0x0C, 0x07, 0x06, 0xC6, 0x3F, 0x00, 0x40, /* 'R' 0x52 */ 0xF9, 0x0A, 0x14, 0x2F, 0x90, 0xA1, 0x42, 0x86, /* 'S' 0x53 */ 0x7A, 0x18, 0x30, 0x78, 0x38, 0x71, 0x78, /* 'T' 0x54 */ 0xFC, 0x41, 0x04, 0x10, 0x41, 0x04, 0x10, /* 'U' 0x55 */ 0x83, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xE3, 0x7C, /* 'V' 0x56 */ 0xC2, 0x85, 0x0B, 0x22, 0x44, 0x8E, 0x0C, 0x18, /* 'W' 0x57 */ 0x84, 0x38, 0xCD, 0x29, 0x25, 0x24, 0xA4, 0xD2, 0x8C, 0x61, 0x8C, 0x31, 0x80, /* 'X' 0x58 */ 0x87, 0x34, 0x8C, 0x30, 0xC4, 0xA3, 0x84, /* 'Y' 0x59 */ 0x82, 0x89, 0x11, 0x43, 0x82, 0x04, 0x08, 0x10, /* 'Z' 0x5A */ 0x7E, 0x0C, 0x30, 0x41, 0x06, 0x18, 0x20, 0xFE, /* '[' 0x5B */ 0xEA, 0xAA, 0xAB, /* '\' 0x5C */ 0x92, 0x24, 0x91, 0x20, /* ']' 0x5D */ 0xD5, 0x55, 0x57, /* '^' 0x5E */ 0x46, 0xA9, 0x10, /* '_' 0x5F */ 0xFE, /* '`' 0x60 */ 0x80, /* 'a' 0x61 */ 0x79, 0x08, 0x11, 0xEC, 0x51, 0x9D, 0x80, /* 'b' 0x62 */ 0x84, 0x3D, 0xB8, 0xC6, 0x3B, 0xF0, /* 'c' 0x63 */ 0x7B, 0x18, 0x20, 0x83, 0x17, 0x80, /* 'd' 0x64 */ 0x04, 0x17, 0xF3, 0x86, 0x18, 0x73, 0x74, /* 'e' 0x65 */ 0x7B, 0x38, 0x7F, 0x83, 0x17, 0x80, /* 'f' 0x66 */ 0x6B, 0xA4, 0x92, 0x40, /* 'g' 0x67 */ 0x77, 0x38, 0x61, 0x87, 0x37, 0x41, 0x8D, 0xE0, /* 'h' 0x68 */ 0x84, 0x2D, 0x98, 0xC6, 0x31, 0x88, /* 'i' 0x69 */ 0xBF, 0x80, /* 'j' 0x6A */ 0x45, 0x55, 0x57, /* 'k' 0x6B */ 0x84, 0x25, 0x6E, 0x72, 0x52, 0x88, /* 'l' 0x6C */ 0xFF, 0x80, /* 'm' 0x6D */ 0xFF, 0x99, 0x91, 0x91, 0x91, 0x91, 0x91, /* 'n' 0x6E */ 0xB6, 0x63, 0x18, 0xC6, 0x20, /* 'o' 0x6F */ 0x7B, 0x38, 0x61, 0x87, 0x37, 0x80, /* 'p' 0x70 */ 0xF6, 0xE3, 0x18, 0xEF, 0xD0, 0x80, /* 'q' 0x71 */ 0x77, 0x38, 0x61, 0x87, 0x37, 0x41, 0x04, /* 'r' 0x72 */ 0xBA, 0x49, 0x20, /* 's' 0x73 */ 0x69, 0x8E, 0x19, 0x60, /* 't' 0x74 */ 0x5D, 0x24, 0x93, /* 'u' 0x75 */ 0x8C, 0x63, 0x18, 0xCD, 0xA0, /* 'v' 0x76 */ 0x85, 0x24, 0x92, 0x30, 0xC3, 0x00, /* 'w' 0x77 */ 0x89, 0x99, 0x59, 0x55, 0x56, 0x66, 0x26, /* 'x' 0x78 */ 0x4A, 0x4C, 0x43, 0x27, 0x20, /* 'y' 0x79 */ 0x85, 0x24, 0x92, 0x30, 0xC3, 0x08, 0x21, 0x80, /* 'z' 0x7A */ 0x78, 0x44, 0x46, 0x23, 0xE0, /* '{' 0x7B */ 0x69, 0x25, 0xB2, 0x49, 0x30, /* '|' 0x7C */ 0xFF, 0xE0, /* '}' 0x7D */ 0xC9, 0x24, 0xDA, 0x49, 0x60, /* '~' 0x7E */ 0x66, 0x70, }; const GFXglyph FreeSans6pt7bGlyphs[] PROGMEM = { /* ' ' 0x20 */ {0, 0, 0, 3, 0, 0}, /* '!' 0x21 */ {0, 1, 9, 4, 2, -8}, /* '"' 0x22 */ {2, 3, 3, 4, 0, -8}, /* '#' 0x23 */ {4, 7, 8, 7, 0, -7}, /* '$' 0x24 */ {11, 5, 11, 7, 1, -9}, /* '%' 0x25 */ {18, 10, 9, 11, 0, -8}, /* '&' 0x26 */ {30, 6, 9, 8, 1, -8}, /* ''' 0x27 */ {37, 1, 3, 2, 1, -8}, /* '(' 0x28 */ {38, 2, 11, 4, 1, -8}, /* ')' 0x29 */ {41, 3, 11, 4, 0, -8}, /* '*' 0x2A */ {46, 3, 3, 5, 1, -8}, /* '+' 0x2B */ {48, 5, 5, 7, 1, -4}, /* ',' 0x2C */ {52, 1, 3, 3, 1, 0}, /* '-' 0x2D */ {53, 3, 1, 4, 1, -3}, /* '.' 0x2E */ {54, 1, 1, 3, 1, 0}, /* '/' 0x2F */ {55, 3, 9, 3, 0, -8}, /* '0' 0x30 */ {59, 5, 9, 7, 1, -8}, /* '1' 0x31 */ {65, 2, 9, 7, 2, -8}, /* '2' 0x32 */ {68, 5, 9, 7, 1, -8}, /* '3' 0x33 */ {74, 5, 9, 7, 1, -8}, /* '4' 0x34 */ {80, 5, 9, 7, 1, -8}, /* '5' 0x35 */ {86, 6, 9, 7, 0, -8}, /* '6' 0x36 */ {93, 5, 9, 7, 1, -8}, /* '7' 0x37 */ {99, 5, 9, 7, 1, -8}, /* '8' 0x38 */ {105, 6, 9, 7, 0, -8}, /* '9' 0x39 */ {112, 6, 9, 7, 0, -8}, /* ':' 0x3A */ {119, 1, 7, 3, 1, -6}, /* ';' 0x3B */ {120, 1, 8, 3, 1, -5}, /* '<' 0x3C */ {121, 5, 5, 7, 1, -4}, /* '=' 0x3D */ {125, 5, 3, 7, 1, -3}, /* '>' 0x3E */ {127, 5, 5, 7, 1, -4}, /* '?' 0x3F */ {131, 5, 9, 7, 1, -8}, /* '@' 0x40 */ {137, 11, 11, 12, 0, -8}, /* 'A' 0x41 */ {153, 8, 9, 8, 0, -8}, /* 'B' 0x42 */ {162, 6, 9, 8, 1, -8}, /* 'C' 0x43 */ {169, 8, 9, 9, 0, -8}, /* 'D' 0x44 */ {178, 7, 9, 8, 1, -8}, /* 'E' 0x45 */ {186, 6, 9, 8, 1, -8}, /* 'F' 0x46 */ {193, 5, 9, 7, 1, -8}, /* 'G' 0x47 */ {199, 8, 9, 9, 0, -8}, /* 'H' 0x48 */ {208, 7, 9, 9, 1, -8}, /* 'I' 0x49 */ {216, 1, 9, 3, 1, -8}, /* 'J' 0x4A */ {218, 5, 9, 6, 0, -8}, /* 'K' 0x4B */ {224, 6, 9, 8, 1, -8}, /* 'L' 0x4C */ {231, 5, 9, 7, 1, -8}, /* 'M' 0x4D */ {237, 8, 9, 10, 1, -8}, /* 'N' 0x4E */ {246, 7, 9, 9, 1, -8}, /* 'O' 0x4F */ {254, 9, 9, 9, 0, -8}, /* 'P' 0x50 */ {265, 6, 9, 8, 1, -8}, /* 'Q' 0x51 */ {272, 9, 10, 9, 0, -8}, /* 'R' 0x52 */ {284, 7, 9, 9, 1, -8}, /* 'S' 0x53 */ {292, 6, 9, 8, 1, -8}, /* 'T' 0x54 */ {299, 6, 9, 8, 0, -8}, /* 'U' 0x55 */ {306, 7, 9, 9, 1, -8}, /* 'V' 0x56 */ {314, 7, 9, 8, 0, -8}, /* 'W' 0x57 */ {322, 11, 9, 11, 0, -8}, /* 'X' 0x58 */ {335, 6, 9, 8, 1, -8}, /* 'Y' 0x59 */ {342, 7, 9, 8, 1, -8}, /* 'Z' 0x5A */ {350, 7, 9, 7, 0, -8}, /* '[' 0x5B */ {358, 2, 12, 3, 1, -8}, /* '\' 0x5C */ {361, 3, 9, 3, 0, -8}, /* ']' 0x5D */ {365, 2, 12, 3, 0, -8}, /* '^' 0x5E */ {368, 4, 5, 6, 1, -8}, /* '_' 0x5F */ {371, 7, 1, 7, 0, 2}, /* '`' 0x60 */ {372, 1, 1, 3, 1, -8}, /* 'a' 0x61 */ {373, 7, 7, 7, 0, -6}, /* 'b' 0x62 */ {380, 5, 9, 7, 1, -8}, /* 'c' 0x63 */ {386, 6, 7, 6, 0, -6}, /* 'd' 0x64 */ {392, 6, 9, 7, 0, -8}, /* 'e' 0x65 */ {399, 6, 7, 6, 0, -6}, /* 'f' 0x66 */ {405, 3, 9, 3, 0, -8}, /* 'g' 0x67 */ {409, 6, 10, 7, 0, -6}, /* 'h' 0x68 */ {417, 5, 9, 6, 1, -8}, /* 'i' 0x69 */ {423, 1, 9, 3, 1, -8}, /* 'j' 0x6A */ {425, 2, 12, 3, 0, -8}, /* 'k' 0x6B */ {428, 5, 9, 6, 1, -8}, /* 'l' 0x6C */ {434, 1, 9, 3, 1, -8}, /* 'm' 0x6D */ {436, 8, 7, 10, 1, -6}, /* 'n' 0x6E */ {443, 5, 7, 6, 1, -6}, /* 'o' 0x6F */ {448, 6, 7, 6, 0, -6}, /* 'p' 0x70 */ {454, 5, 9, 7, 1, -6}, /* 'q' 0x71 */ {460, 6, 9, 7, 0, -6}, /* 'r' 0x72 */ {467, 3, 7, 4, 1, -6}, /* 's' 0x73 */ {470, 4, 7, 6, 1, -6}, /* 't' 0x74 */ {474, 3, 8, 3, 0, -7}, /* 'u' 0x75 */ {477, 5, 7, 6, 1, -6}, /* 'v' 0x76 */ {482, 6, 7, 6, 0, -6}, /* 'w' 0x77 */ {488, 8, 7, 9, 0, -6}, /* 'x' 0x78 */ {495, 5, 7, 6, 0, -6}, /* 'y' 0x79 */ {500, 6, 10, 6, 0, -6}, /* 'z' 0x7A */ {508, 5, 7, 6, 0, -6}, /* '{' 0x7B */ {513, 3, 12, 4, 0, -8}, /* '|' 0x7C */ {518, 1, 11, 3, 1, -8}, /* '}' 0x7D */ {520, 3, 12, 4, 1, -8}, /* '~' 0x7E */ {525, 6, 2, 6, 0, -4}, }; const GFXfont FreeSans6pt7b PROGMEM = {(uint8_t *)FreeSans6pt7bBitmaps, (GFXglyph *)FreeSans6pt7bGlyphs, 0x20, 0x7E, 14}; ================================================ FILE: src/graphics/niche/Fonts/FreeSans6pt_Win1250.h ================================================ // trunk-ignore-all(clang-format) #pragma once /* PROPERTIES FONT_NAME FreeSans6pt_Win1250 */ const uint8_t FreeSans6pt_Win1250Bitmaps[] PROGMEM = { /* 0x01 */ 0x1C, 0x0A, 0x05, 0x04, 0xFE, 0x08, 0x1C, 0x02, 0x07, 0xE0, 0x9F, 0xC0, /* 0x02 */ 0x3F, 0xF0, 0x40, 0xE0, 0x10, 0x3F, 0x04, 0x9E, 0x28, 0x14, 0x0E, 0x00, /* 0x03 */ 0x3F, 0x10, 0x28, 0x06, 0x49, 0x80, 0x60, 0x19, 0x26, 0x31, 0x40, 0x8F, 0xC0, /* 0x04 */ 0x3F, 0x10, 0x2A, 0x16, 0x49, 0xA1, 0x60, 0x19, 0xE6, 0x31, 0x40, 0x8F, 0xC0, /* 0x05 */ 0x28, 0x15, 0x2A, 0xB5, 0x55, 0xA8, 0x54, 0x12, 0x04, 0x41, 0x08, 0x81, 0xC0, /* 0x06 */ 0x04, 0x08, 0x88, 0x82, 0x07, 0x01, 0x11, 0xA2, 0xC4, 0x40, 0x70, 0x20, 0x88, 0x88, 0x10, 0x00, /* 0x07 */ /* 0x08 */ 0x03, 0x83, 0x44, 0x48, 0x28, 0x01, 0x80, 0x17, 0xFE, 0x08, 0x45, 0x28, 0x84, 0x00, /* 0x09 */ 0x01, 0xC0, 0x68, 0x82, 0x41, 0x10, 0x02, 0x80, 0x06, 0x00, 0x14, 0x00, 0x8F, 0xFC, /* 0x0A */ /* 0x0B */ 0x22, 0x2A, 0xA2, 0x30, 0x18, 0x0A, 0x09, 0x04, 0x44, 0x14, 0x04, 0x00, /* 0x0C */ 0x46, 0x00, 0x19, 0x03, 0x21, 0x20, 0x93, 0x04, 0x20, 0x11, 0x80, 0x50, 0x02, 0x7F, 0xE0, /* 0x0D */ /* 0x0E */ 0x08, 0x0E, 0x08, 0x88, 0x24, 0x12, 0x09, 0x05, 0x01, 0xFF, 0x8A, 0x02, 0x00, /* 0x0F */ 0x3F, 0x14, 0xAA, 0x16, 0x01, 0x92, 0x60, 0x18, 0xC6, 0x49, 0x40, 0x8F, 0xC0, /* 0x10 */ 0x1B, 0x02, 0xA0, 0x54, 0x12, 0x42, 0x48, 0x49, 0x31, 0x1E, 0x23, 0xEA, 0xFE, 0x3C, /* 0x11 */ 0x3F, 0x02, 0x00, 0x20, 0x6D, 0x27, 0xF8, 0x3F, 0xC1, 0xFE, 0x37, 0xD0, 0xBE, 0x40, 0xE1, 0xE2, 0x00, /* 0x12 */ 0x12, 0x42, 0x20, 0x24, 0xC0, 0x29, 0x99, 0x05, 0x23, 0x30, 0xB0, 0x30, 0x00, /* 0x13 */ 0x3F, 0x88, 0x0A, 0x44, 0xD5, 0x58, 0x03, 0x00, 0x67, 0xCC, 0x71, 0x40, 0x47, 0xF0, /* 0x14 */ 0x3F, 0x18, 0x69, 0x26, 0x85, 0xA1, 0x6C, 0xD8, 0x06, 0x31, 0x40, 0x8F, 0xC0, /* 0x15 */ 0x3F, 0x11, 0x00, 0xE8, 0x03, 0xA0, 0x1F, 0xB3, 0x7E, 0x00, 0xE9, 0xE0, 0x23, 0x00, 0x40, 0x40, 0xFE, 0x00, /* 0x16 */ 0x30, 0x38, 0x3A, 0x3E, 0x6E, 0xEB, 0xC3, 0xC3, 0x66, 0x3C, /* 0x17 */ 0x3F, 0x04, 0x00, 0x82, 0x88, 0x5C, 0xA4, 0x49, 0x22, 0x81, 0x98, 0xC4, 0x40, 0xA3, 0xF0, /* 0x18 */ 0x07, 0x80, 0x42, 0x04, 0x08, 0x21, 0x41, 0x42, 0x60, 0x0E, 0x8C, 0xB2, 0x89, 0x50, 0x52, 0x82, 0x80, /* 0x19 */ 0x3F, 0xC4, 0x02, 0x80, 0x18, 0x01, 0xB3, 0x1B, 0xB9, 0x80, 0x19, 0xE1, 0x40, 0x23, 0xFC, /* 0x1A */ 0xFF, 0xC0, 0x67, 0x34, 0x58, 0x4C, 0x46, 0x03, 0x11, 0x80, 0xFF, 0xC0, /* 0x1B */ 0x0F, 0xC0, 0x40, 0x82, 0x49, 0x08, 0x04, 0x00, 0x00, 0x12, 0x02, 0x31, 0x34, 0x0B, 0x88, 0x45, 0x00, 0x20, /* 0x1C */ 0x3F, 0x88, 0x0A, 0x44, 0xC9, 0x19, 0x3B, 0x00, 0x60, 0x4C, 0x71, 0x40, 0x47, 0xF0, /* 0x1D */ 0x3F, 0x8B, 0x0A, 0x00, 0xC8, 0x18, 0x13, 0x00, 0x48, 0xCA, 0xC1, 0x44, 0x53, 0x30, /* 0x1E */ 0x19, 0xC2, 0x02, 0x50, 0x1E, 0x49, 0x80, 0x12, 0x01, 0x27, 0x92, 0x01, 0x10, 0x20, 0xFC, /* 0x1F */ 0x30, 0x1C, 0x0C, 0x3E, 0x7E, 0xCF, 0x07, 0xC7, 0x7F, 0x3F, /* ' ' 0x20 */ /* '!' 0x21 */ 0xFC, 0x80, /* '"' 0x22 */ 0xB6, 0x80, /* '#' 0x23 */ 0x24, 0x51, 0xF9, 0x42, 0x9F, 0x92, 0x28, /* '$' 0x24 */ 0x10, 0xE5, 0x55, 0x50, 0xE1, 0x65, 0x55, 0xE1, 0x00, /* '%' 0x25 */ 0x71, 0x24, 0x89, 0x22, 0x50, 0x74, 0x02, 0x70, 0xA4, 0x49, 0x11, 0xC0, /* '&' 0x26 */ 0x71, 0x24, 0x9C, 0x62, 0x58, 0xA7, 0xF4, /* ''' 0x27 */ 0xE0, /* '(' 0x28 */ 0x5A, 0xAA, 0x94, /* ')' 0x29 */ 0x89, 0x12, 0x49, 0x29, 0x00, /* '*' 0x2A */ 0x5E, 0x80, /* '+' 0x2B */ 0x21, 0x3E, 0x42, 0x00, /* ',' 0x2C */ 0xE0, /* '-' 0x2D */ 0xC0, /* '.' 0x2E */ 0x80, /* '/' 0x2F */ 0x24, 0xA4, 0xA4, 0x80, /* '0' 0x30 */ 0x76, 0xE3, 0x18, 0xC6, 0x3B, 0x70, /* '1' 0x31 */ 0x27, 0x92, 0x49, 0x20, /* '2' 0x32 */ 0x79, 0x10, 0x41, 0x08, 0xC6, 0x10, 0xFC, /* '3' 0x33 */ 0x79, 0x30, 0x43, 0x18, 0x10, 0x71, 0x78, /* '4' 0x34 */ 0x08, 0x61, 0x8A, 0x49, 0x2F, 0xC2, 0x08, /* '5' 0x35 */ 0xFC, 0x21, 0xE8, 0x84, 0x31, 0xF0, /* '6' 0x36 */ 0x74, 0x61, 0xE8, 0xC6, 0x31, 0x70, /* '7' 0x37 */ 0xF8, 0x44, 0x22, 0x11, 0x08, 0x40, /* '8' 0x38 */ 0x39, 0x34, 0x53, 0x39, 0x1C, 0x51, 0x38, /* '9' 0x39 */ 0x39, 0x3C, 0x71, 0x4C, 0xF0, 0x53, 0x78, /* ':' 0x3A */ 0x82, /* ';' 0x3B */ 0x87, /* '<' 0x3C */ 0x3E, 0x30, 0x60, 0x80, /* '=' 0x3D */ 0xF8, 0x3E, /* '>' 0x3E */ 0xE0, 0xC6, 0xC8, 0x00, /* '?' 0x3F */ 0x74, 0x42, 0x11, 0x10, 0x80, 0x20, /* '@' 0x40 */ 0x0F, 0x86, 0x19, 0x9A, 0xA4, 0xD9, 0x13, 0x22, 0x56, 0xDA, 0x6E, 0x60, 0x06, 0x00, 0x3C, 0x00, /* 'A' 0x41 */ 0x18, 0x18, 0x24, 0x24, 0x24, 0x7E, 0x42, 0x42, 0xC3, /* 'B' 0x42 */ 0xFA, 0x18, 0x61, 0xFA, 0x18, 0x61, 0xFC, /* 'C' 0x43 */ 0x3E, 0x63, 0x40, 0x40, 0xC0, 0x40, 0x41, 0x63, 0x3E, /* 'D' 0x44 */ 0xF9, 0x0A, 0x1C, 0x18, 0x30, 0x61, 0xC2, 0xF8, /* 'E' 0x45 */ 0xFE, 0x08, 0x20, 0xFE, 0x08, 0x20, 0xFC, /* 'F' 0x46 */ 0xFE, 0x08, 0x20, 0xFA, 0x08, 0x20, 0x80, /* 'G' 0x47 */ 0x1E, 0x61, 0x40, 0x40, 0xC7, 0x41, 0x41, 0x63, 0x1D, /* 'H' 0x48 */ 0x83, 0x06, 0x0C, 0x1F, 0xF0, 0x60, 0xC1, 0x82, /* 'I' 0x49 */ 0xFF, 0x80, /* 'J' 0x4A */ 0x08, 0x42, 0x10, 0x87, 0x29, 0x70, /* 'K' 0x4B */ 0x85, 0x12, 0x45, 0x0D, 0x13, 0x22, 0x42, 0x86, /* 'L' 0x4C */ 0x84, 0x21, 0x08, 0x42, 0x10, 0xF8, /* 'M' 0x4D */ 0xC3, 0xC3, 0xC3, 0xA5, 0xA5, 0xA5, 0x99, 0x99, 0x99, /* 'N' 0x4E */ 0x83, 0x86, 0x8D, 0x19, 0x33, 0x62, 0xC3, 0x86, /* 'O' 0x4F */ 0x1E, 0x31, 0x90, 0x68, 0x1C, 0x0A, 0x05, 0x06, 0xC6, 0x1E, 0x00, /* 'P' 0x50 */ 0xFA, 0x18, 0x61, 0xFA, 0x08, 0x20, 0x80, /* 'Q' 0x51 */ 0x1E, 0x31, 0x90, 0x68, 0x1C, 0x0A, 0x05, 0x16, 0xC6, 0x1F, 0x00, 0x40, /* 'R' 0x52 */ 0xFD, 0x0E, 0x1C, 0x2F, 0x90, 0xA1, 0x42, 0x86, /* 'S' 0x53 */ 0x7A, 0x18, 0x30, 0x78, 0x38, 0x61, 0x78, /* 'T' 0x54 */ 0xFE, 0x20, 0x40, 0x81, 0x02, 0x04, 0x08, 0x10, /* 'U' 0x55 */ 0x83, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xE2, 0x78, /* 'V' 0x56 */ 0xC2, 0x85, 0x0B, 0x22, 0x44, 0x8E, 0x0C, 0x18, /* 'W' 0x57 */ 0xC4, 0x28, 0xCD, 0x29, 0x25, 0x24, 0xA4, 0x52, 0x8C, 0x61, 0x8C, 0x31, 0x80, /* 'X' 0x58 */ 0x87, 0x34, 0x8C, 0x30, 0xC4, 0xA3, 0x84, /* 'Y' 0x59 */ 0xC3, 0x42, 0x24, 0x34, 0x18, 0x08, 0x08, 0x08, 0x08, /* 'Z' 0x5A */ 0x7E, 0x0C, 0x30, 0x41, 0x06, 0x18, 0x20, 0xFE, /* '[' 0x5B */ 0xEA, 0xAA, 0xAB, /* '\' 0x5C */ 0x92, 0x24, 0x89, 0x20, /* ']' 0x5D */ 0xD5, 0x55, 0x57, /* '^' 0x5E */ 0x46, 0xA9, /* '_' 0x5F */ 0xFE, /* '`' 0x60 */ 0x80, /* 'a' 0x61 */ 0x79, 0x20, 0x4F, 0xC6, 0x37, 0x40, /* 'b' 0x62 */ 0x84, 0x3D, 0x18, 0xC6, 0x31, 0xF0, /* 'c' 0x63 */ 0x39, 0x3C, 0x20, 0xC1, 0x33, 0x80, /* 'd' 0x64 */ 0x04, 0x13, 0xD3, 0xC6, 0x1C, 0x53, 0x3C, /* 'e' 0x65 */ 0x39, 0x38, 0x7F, 0x81, 0x13, 0x80, /* 'f' 0x66 */ 0x6B, 0xA4, 0x92, 0x40, /* 'g' 0x67 */ 0x35, 0x3C, 0x61, 0xC5, 0x33, 0x41, 0x4D, 0xE0, /* 'h' 0x68 */ 0x84, 0x3D, 0x38, 0xC6, 0x31, 0x88, /* 'i' 0x69 */ 0xBF, 0x80, /* 'j' 0x6A */ 0x45, 0x55, 0x57, /* 'k' 0x6B */ 0x84, 0x25, 0x4E, 0x52, 0xD2, 0x88, /* 'l' 0x6C */ 0xFF, 0x80, /* 'm' 0x6D */ 0xF7, 0x99, 0x91, 0x91, 0x91, 0x91, 0x91, /* 'n' 0x6E */ 0xF4, 0x63, 0x18, 0xC6, 0x20, /* 'o' 0x6F */ 0x39, 0x3C, 0x61, 0xC5, 0x33, 0x80, /* 'p' 0x70 */ 0xF4, 0x63, 0x18, 0xC7, 0xD0, 0x80, /* 'q' 0x71 */ 0x3D, 0x3C, 0x61, 0xC5, 0x37, 0x41, 0x04, /* 'r' 0x72 */ 0xF2, 0x49, 0x20, /* 's' 0x73 */ 0x7A, 0x50, 0xE0, 0xE5, 0xE0, /* 't' 0x74 */ 0x5D, 0x24, 0x93, /* 'u' 0x75 */ 0x8C, 0x63, 0x18, 0xCF, 0xA0, /* 'v' 0x76 */ 0x85, 0x24, 0x92, 0x30, 0xC3, 0x00, /* 'w' 0x77 */ 0x89, 0x59, 0x59, 0x55, 0x56, 0x26, 0x26, /* 'x' 0x78 */ 0x4A, 0x4C, 0x43, 0x27, 0x20, /* 'y' 0x79 */ 0x8A, 0x52, 0xA5, 0x18, 0x84, 0x22, 0x00, /* 'z' 0x7A */ 0x78, 0x44, 0x46, 0x23, 0xE0, /* '{' 0x7B */ 0x6A, 0xAA, 0xA9, /* '|' 0x7C */ 0xFF, 0xE0, /* '}' 0x7D */ 0x95, 0x55, 0x56, /* '~' 0x7E */ 0x66, 0x60, /* 0x7F */ 0xFF, 0xC0, 0x67, 0x34, 0x58, 0x4C, 0x46, 0x03, 0x11, 0x80, 0xFF, 0xC0, /* 0x80 */ 0x1C, 0x45, 0x07, 0xE4, 0x1F, 0x10, 0x10, 0x1E, /* 0x81 */ /* 0x82 */ 0xE0, /* 0x83 */ /* 0x84 */ 0xB6, 0x80, /* 0x85 */ 0xA8, /* 0x86 */ 0x21, 0x09, 0xF2, 0x10, 0x84, 0x21, 0x08, /* 0x87 */ 0x21, 0x09, 0xF2, 0x10, 0x84, 0xF9, 0x08, /* 0x88 */ /* 0x89 */ 0x62, 0x09, 0x40, 0x98, 0x06, 0x80, 0x10, 0x01, 0x66, 0x29, 0x92, 0x99, 0x06, 0x60, /* 0x8A */ 0x28, 0x47, 0xA1, 0x83, 0x07, 0x83, 0x87, 0x17, 0x80, /* 0x8B */ 0x64, /* 0x8C */ 0x10, 0x87, 0xA1, 0x83, 0x07, 0x83, 0x87, 0x17, 0x80, /* 0x8D */ 0x28, 0x4F, 0xC4, 0x10, 0x41, 0x04, 0x10, 0x40, /* 0x8E */ 0x14, 0x11, 0xF8, 0x30, 0xC1, 0x04, 0x18, 0x61, 0xFC, /* 0x8F */ 0x08, 0x21, 0xF8, 0x30, 0xC1, 0x04, 0x18, 0x61, 0xFC, /* 0x90 */ /* 0x91 */ 0xE0, /* 0x92 */ 0xE0, /* 0x93 */ 0xB6, 0x80, /* 0x94 */ 0xB6, 0x80, /* 0x95 */ 0xFF, 0x80, /* 0x96 */ 0xFC, /* 0x97 */ 0xFF, 0xF0, /* 0x98 */ /* 0x99 */ 0xE6, 0x28, 0xCD, 0x19, 0xA3, 0x34, 0x6A, 0x8B, 0x51, 0x68, /* 0x9A */ 0x52, 0x69, 0x8E, 0x19, 0x60, /* 0x9B */ 0x98, /* 0x9C */ 0x24, 0x06, 0x98, 0xE1, 0x96, /* 0x9D */ 0x15, 0xE4, 0x44, 0x44, 0x60, /* 0x9E */ 0x51, 0x00, 0xF0, 0x88, 0x8C, 0x47, 0xC0, /* 0x9F */ 0x11, 0x00, 0xF0, 0x88, 0x8C, 0x47, 0xC0, /* 0xA0 */ /* 0xA1 */ 0xA8, /* 0xA2 */ 0x96, /* 0xA3 */ 0x41, 0x05, 0x18, 0x43, 0x04, 0x10, 0x7C, /* 0xA4 */ 0xFC, 0x63, 0xF0, /* 0xA5 */ 0x30, 0x38, 0x28, 0x48, 0x4C, 0x7C, 0x84, 0x86, 0x82, 0x04, 0x07, /* 0xA6 */ 0xF9, 0xF0, /* 0xA7 */ 0x32, 0x91, 0xC9, 0x47, 0x26, 0x14, 0xA4, 0xC0, /* 0xA8 */ 0xA0, /* 0xA9 */ 0x3E, 0x3F, 0xB8, 0xF4, 0x1A, 0x0D, 0x17, 0x76, 0xC6, 0x3E, 0x00, /* 0xAA */ 0x7A, 0x18, 0x30, 0x78, 0x38, 0x61, 0x78, 0xC1, 0x0C, /* 0xAB */ 0x5A, 0xA5, /* 0xAC */ 0xFC, 0x10, 0x40, /* 0xAD */ /* 0xAE */ 0x3E, 0x31, 0xB7, 0x72, 0x99, 0xCC, 0xC7, 0x56, 0xC6, 0x3E, 0x00, /* 0xAF */ 0x18, 0x31, 0xF8, 0x30, 0xC1, 0x04, 0x18, 0x61, 0xFC, /* 0xB0 */ 0x69, 0x96, /* 0xB1 */ 0x21, 0x3E, 0x42, 0x03, 0xE0, /* 0xB2 */ 0x9C, /* 0xB3 */ 0x49, 0x35, 0x92, 0x40, /* 0xB4 */ 0x80, /* 0xB5 */ 0x8A, 0x28, 0xA2, 0x8A, 0x6E, 0xE0, 0x80, /* 0xB6 */ 0x7F, 0xAE, 0xBA, 0x68, 0xA2, 0x8A, 0x28, 0xA0, /* 0xB7 */ 0x80, /* 0xB8 */ 0x67, 0x80, /* 0xB9 */ 0x78, 0x84, 0x04, 0x3C, 0xC4, 0x8C, 0x76, 0x04, 0x07, /* 0xBA */ 0x69, 0x8E, 0x19, 0x66, 0x26, /* 0xBB */ 0xA5, 0x5A, /* 0xBC */ 0xA5, 0x21, 0x08, 0x42, 0x10, 0xF8, /* 0xBD */ 0xA0, /* 0xBE */ 0xBA, 0x49, 0x24, 0x90, /* 0xBF */ 0x31, 0x9E, 0x11, 0x11, 0x88, 0xF8, /* 0xC0 */ 0x10, 0x43, 0xE4, 0x28, 0x50, 0xBE, 0x42, 0x85, 0x0C, /* 0xC1 */ 0x08, 0x10, 0x00, 0x18, 0x3C, 0x24, 0x24, 0x7E, 0x42, 0xC3, /* 0xC2 */ 0x18, 0x24, 0x00, 0x18, 0x3C, 0x24, 0x24, 0x7E, 0x42, 0xC3, /* 0xC3 */ 0x24, 0x18, 0x00, 0x18, 0x3C, 0x24, 0x24, 0x7E, 0x42, 0xC3, /* 0xC4 */ 0x24, 0x00, 0x18, 0x3C, 0x24, 0x24, 0x7E, 0x42, 0x42, 0xC3, /* 0xC5 */ 0x11, 0x21, 0x08, 0x42, 0x10, 0x87, 0xC0, /* 0xC6 */ 0x08, 0x20, 0x01, 0xE4, 0x30, 0x20, 0x40, 0x82, 0x8C, 0xF0, /* 0xC7 */ 0x3E, 0x61, 0xC0, 0x80, 0x80, 0x80, 0xC1, 0x63, 0x3E, 0x0C, 0x04, 0x1C, /* 0xC8 */ 0x28, 0x20, 0x01, 0xE4, 0x30, 0x20, 0x40, 0x82, 0x8C, 0xF0, /* 0xC9 */ 0x08, 0x40, 0x3F, 0x82, 0x0F, 0xA0, 0x83, 0xF0, /* 0xCA */ 0xFD, 0x02, 0x04, 0x0F, 0xD0, 0x20, 0x40, 0xFC, 0x10, 0x38, /* 0xCB */ 0x28, 0x0F, 0xE0, 0x83, 0xE8, 0x20, 0x83, 0xF0, /* 0xCC */ 0x28, 0x40, 0x3F, 0x82, 0x0F, 0xA0, 0x82, 0x0F, 0xC0, /* 0xCD */ 0x62, 0xAA, 0xA0, /* 0xCE */ 0x54, 0x24, 0x92, 0x48, /* 0xCF */ 0x50, 0x43, 0xE4, 0x28, 0x30, 0x60, 0xC1, 0x85, 0xF0, /* 0xD0 */ 0x7C, 0x42, 0x41, 0x41, 0xF1, 0x41, 0x41, 0x42, 0x7C, /* 0xD1 */ 0x08, 0x23, 0x0F, 0x1B, 0x32, 0x66, 0xC7, 0x87, 0x04, /* 0xD2 */ 0x28, 0x23, 0x0F, 0x1B, 0x32, 0x66, 0xC7, 0x87, 0x04, /* 0xD3 */ 0x04, 0x04, 0x0F, 0x8C, 0x6C, 0x1C, 0x06, 0x03, 0x83, 0x63, 0x1F, 0x00, /* 0xD4 */ 0x08, 0x0A, 0x00, 0x07, 0xC6, 0x36, 0x0E, 0x03, 0x01, 0xC1, 0xB1, 0x8F, 0x80, /* 0xD5 */ 0x0A, 0x0A, 0x00, 0x07, 0xC6, 0x36, 0x0E, 0x03, 0x01, 0xC1, 0xB1, 0x8F, 0x80, /* 0xD6 */ 0x14, 0x00, 0x00, 0x07, 0xC6, 0x36, 0x0E, 0x03, 0x01, 0xC1, 0xB1, 0x8F, 0x80, /* 0xD7 */ 0x8A, 0x88, 0xA8, 0x80, /* 0xD8 */ 0x50, 0x43, 0xE4, 0x28, 0x50, 0xBE, 0x42, 0x85, 0x0C, /* 0xD9 */ 0x10, 0x52, 0x4C, 0x18, 0x30, 0x60, 0xC1, 0xC6, 0xF8, /* 0xDA */ 0x08, 0x22, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0xC6, 0xF8, /* 0xDB */ 0x14, 0x52, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0xC6, 0xF8, /* 0xDC */ 0x29, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0xC6, 0xF8, /* 0xDD */ 0x09, 0x25, 0x12, 0x22, 0x87, 0x04, 0x08, 0x10, 0x20, /* 0xDE */ 0xFC, 0x41, 0x04, 0x10, 0x41, 0x04, 0x10, 0x60, 0x8E, /* 0xDF */ 0x7A, 0x18, 0x61, 0x8A, 0x18, 0x61, 0xB8, /* 0xE0 */ 0x42, 0xE9, 0x24, 0x80, /* 0xE1 */ 0x10, 0x40, 0x03, 0xC8, 0x40, 0x8F, 0x62, 0x8C, 0xEC, /* 0xE2 */ 0x10, 0x50, 0x03, 0xC8, 0x40, 0x8F, 0x62, 0x8C, 0xEC, /* 0xE3 */ 0x48, 0x60, 0x03, 0xC8, 0x40, 0x8F, 0x62, 0x8C, 0xEC, /* 0xE4 */ 0x28, 0x01, 0xE4, 0x20, 0x47, 0xB1, 0x46, 0x76, /* 0xE5 */ 0x62, 0xAA, 0xA0, /* 0xE6 */ 0x10, 0x80, 0x1E, 0xC6, 0x08, 0x20, 0xC5, 0xE0, /* 0xE7 */ 0x7B, 0x18, 0x20, 0x83, 0x17, 0x8C, 0x11, 0xC0, /* 0xE8 */ 0x28, 0x40, 0x1E, 0xC6, 0x08, 0x20, 0xC5, 0xE0, /* 0xE9 */ 0x10, 0x80, 0x1E, 0xCE, 0x1F, 0xE0, 0xC5, 0xE0, /* 0xEA */ 0x7B, 0x38, 0x7F, 0x83, 0x37, 0x84, 0x1C, /* 0xEB */ 0x28, 0x07, 0xB3, 0x87, 0xF8, 0x31, 0x78, /* 0xEC */ 0x28, 0x40, 0x1E, 0xCE, 0x1F, 0xE0, 0xC5, 0xE0, /* 0xED */ 0x62, 0xAA, 0xA0, /* 0xEE */ 0x54, 0x24, 0x92, 0x48, /* 0xEF */ 0x02, 0x0C, 0x13, 0xEC, 0xD0, 0xA1, 0x42, 0xCC, 0xE8, /* 0xF0 */ 0x04, 0x1D, 0xD6, 0x68, 0x50, 0xA1, 0x66, 0x74, /* 0xF1 */ 0x11, 0x01, 0x6C, 0xC6, 0x31, 0x8C, 0x40, /* 0xF2 */ 0x20, 0x81, 0x6C, 0xC6, 0x31, 0x8C, 0x40, /* 0xF3 */ 0x10, 0x80, 0x1E, 0xCE, 0x18, 0x61, 0xCD, 0xE0, /* 0xF4 */ 0x10, 0xA0, 0x1E, 0xCE, 0x18, 0x61, 0xCD, 0xE0, /* 0xF5 */ 0x29, 0x40, 0x1E, 0xCE, 0x18, 0x61, 0xCD, 0xE0, /* 0xF6 */ 0x28, 0x07, 0xB3, 0x86, 0x18, 0x73, 0x78, /* 0xF7 */ 0x20, 0x3E, 0x02, 0x00, /* 0xF8 */ 0xA8, 0x5D, 0x24, 0x90, /* 0xF9 */ 0x22, 0x89, 0x18, 0xC6, 0x31, 0x9B, 0x40, /* 0xFA */ 0x11, 0x23, 0x18, 0xC6, 0x33, 0x68, /* 0xFB */ 0x2A, 0x81, 0x18, 0xC6, 0x31, 0x9B, 0x40, /* 0xFC */ 0x50, 0x23, 0x18, 0xC6, 0x33, 0x68, /* 0xFD */ 0x10, 0x88, 0x52, 0x49, 0x23, 0x0C, 0x30, 0x82, 0x18, /* 0xFE */ 0x4E, 0x44, 0x44, 0x46, 0x31, 0x70, /* 0xFF */ 0x80, }; const GFXglyph FreeSans6pt_Win1250Glyphs[] PROGMEM = { /* 0x01 */ { 0, 9, 10, 11, 1, -9 }, /* 0x02 */ { 12, 9, 10, 11, 1, -8 }, /* 0x03 */ { 24, 10, 10, 12, 1, -8 }, /* 0x04 */ { 37, 10, 10, 12, 1, -8 }, /* 0x05 */ { 50, 10, 10, 12, 1, -9 }, /* 0x06 */ { 63, 11, 11, 13, 1, -9 }, /* 0x07 */ { 79, 0, 0, 8, 0, 0 }, /* 0x08 */ { 79, 12, 9, 14, 1, -8 }, /* 0x09 */ { 93, 14, 8, 16, 1, -7 }, /* 0x0A */ { 107, 0, 0, 8, 0, 0 }, /* 0x0B */ { 107, 9, 10, 11, 1, -9 }, /* 0x0C */ { 119, 13, 9, 15, 1, -8 }, /* 0x0D */ { 134, 0, 0, 8, 0, 0 }, /* 0x0E */ { 134, 9, 11, 11, 1, -9 }, /* 0x0F */ { 147, 10, 10, 12, 1, -9 }, /* 0x10 */ { 160, 11, 10, 13, 1, -9 }, /* 0x11 */ { 174, 13, 10, 15, 1, -9 }, /* 0x12 */ { 191, 10, 10, 12, 1, -9 }, /* 0x13 */ { 204, 11, 10, 13, 1, -9 }, /* 0x14 */ { 218, 10, 10, 12, 1, -9 }, /* 0x15 */ { 231, 14, 10, 16, 1, -9 }, /* 0x16 */ { 249, 8, 10, 10, 1, -9 }, /* 0x17 */ { 259, 12, 10, 14, 1, -9 }, /* 0x18 */ { 274, 13, 10, 15, 1, -9 }, /* 0x19 */ { 291, 12, 10, 14, 1, -9 }, /* 0x1A */ { 306, 9, 10, 11, 1, -8 }, /* 0x1B */ { 318, 14, 10, 16, 1, -9 }, /* 0x1C */ { 336, 11, 10, 13, 1, -9 }, /* 0x1D */ { 350, 11, 10, 13, 1, -9 }, /* 0x1E */ { 364, 12, 10, 14, 1, -9 }, /* 0x1F */ { 379, 8, 10, 11, 2, -9 }, /* ' ' 0x20 */ { 389, 0, 0, 3, 0, 0 }, /* '!' 0x21 */ { 389, 1, 9, 4, 2, -8 }, /* '"' 0x22 */ { 391, 3, 3, 4, 0, -8 }, /* '#' 0x23 */ { 393, 7, 8, 7, 0, -7 }, /* '$' 0x24 */ { 400, 6, 11, 7, 0, -9 }, /* '%' 0x25 */ { 409, 10, 9, 11, 0, -8 }, /* '&' 0x26 */ { 421, 6, 9, 8, 1, -8 }, /* ''' 0x27 */ { 428, 1, 3, 2, 1, -8 }, /* '(' 0x28 */ { 429, 2, 11, 4, 1, -8 }, /* ')' 0x29 */ { 432, 3, 11, 4, 0, -8 }, /* '*' 0x2A */ { 437, 3, 3, 5, 1, -8 }, /* '+' 0x2B */ { 439, 5, 5, 7, 1, -4 }, /* ',' 0x2C */ { 443, 1, 3, 3, 1, 0 }, /* '-' 0x2D */ { 444, 2, 1, 4, 1, -3 }, /* '.' 0x2E */ { 445, 1, 1, 3, 1, 0 }, /* '/' 0x2F */ { 446, 3, 9, 3, 0, -8 }, /* '0' 0x30 */ { 450, 5, 9, 7, 1, -8 }, /* '1' 0x31 */ { 456, 3, 9, 7, 1, -8 }, /* '2' 0x32 */ { 460, 6, 9, 7, 0, -8 }, /* '3' 0x33 */ { 467, 6, 9, 7, 0, -8 }, /* '4' 0x34 */ { 474, 6, 9, 7, 0, -8 }, /* '5' 0x35 */ { 481, 5, 9, 7, 1, -8 }, /* '6' 0x36 */ { 487, 5, 9, 7, 1, -8 }, /* '7' 0x37 */ { 493, 5, 9, 7, 1, -8 }, /* '8' 0x38 */ { 499, 6, 9, 7, 0, -8 }, /* '9' 0x39 */ { 506, 6, 9, 7, 0, -8 }, /* ':' 0x3A */ { 513, 1, 7, 3, 1, -6 }, /* ';' 0x3B */ { 514, 1, 8, 3, 1, -5 }, /* '<' 0x3C */ { 515, 5, 5, 7, 1, -4 }, /* '=' 0x3D */ { 519, 5, 3, 7, 1, -3 }, /* '>' 0x3E */ { 521, 5, 5, 7, 1, -4 }, /* '?' 0x3F */ { 525, 5, 9, 7, 1, -8 }, /* '@' 0x40 */ { 531, 11, 11, 12, 0, -8 }, /* 'A' 0x41 */ { 547, 8, 9, 8, 0, -8 }, /* 'B' 0x42 */ { 556, 6, 9, 8, 1, -8 }, /* 'C' 0x43 */ { 563, 8, 9, 9, 0, -8 }, /* 'D' 0x44 */ { 572, 7, 9, 8, 1, -8 }, /* 'E' 0x45 */ { 580, 6, 9, 8, 1, -8 }, /* 'F' 0x46 */ { 587, 6, 9, 7, 1, -8 }, /* 'G' 0x47 */ { 594, 8, 9, 9, 0, -8 }, /* 'H' 0x48 */ { 603, 7, 9, 9, 1, -8 }, /* 'I' 0x49 */ { 611, 1, 9, 3, 1, -8 }, /* 'J' 0x4A */ { 613, 5, 9, 6, 0, -8 }, /* 'K' 0x4B */ { 619, 7, 9, 8, 1, -8 }, /* 'L' 0x4C */ { 627, 5, 9, 7, 1, -8 }, /* 'M' 0x4D */ { 633, 8, 9, 10, 1, -8 }, /* 'N' 0x4E */ { 642, 7, 9, 9, 1, -8 }, /* 'O' 0x4F */ { 650, 9, 9, 9, 0, -8 }, /* 'P' 0x50 */ { 661, 6, 9, 8, 1, -8 }, /* 'Q' 0x51 */ { 668, 9, 10, 9, 0, -8 }, /* 'R' 0x52 */ { 680, 7, 9, 9, 1, -8 }, /* 'S' 0x53 */ { 688, 6, 9, 8, 1, -8 }, /* 'T' 0x54 */ { 695, 7, 9, 8, 0, -8 }, /* 'U' 0x55 */ { 703, 7, 9, 9, 1, -8 }, /* 'V' 0x56 */ { 711, 7, 9, 8, 0, -8 }, /* 'W' 0x57 */ { 719, 11, 9, 11, 0, -8 }, /* 'X' 0x58 */ { 732, 6, 9, 8, 1, -8 }, /* 'Y' 0x59 */ { 739, 8, 9, 8, 0, -8 }, /* 'Z' 0x5A */ { 748, 7, 9, 7, 0, -8 }, /* '[' 0x5B */ { 756, 2, 12, 3, 1, -8 }, /* '\' 0x5C */ { 759, 3, 9, 3, 0, -8 }, /* ']' 0x5D */ { 763, 2, 12, 3, 0, -8 }, /* '^' 0x5E */ { 766, 4, 4, 6, 1, -8 }, /* '_' 0x5F */ { 768, 7, 1, 7, 0, 2 }, /* '`' 0x60 */ { 769, 1, 1, 3, 1, -8 }, /* 'a' 0x61 */ { 770, 6, 7, 7, 0, -6 }, /* 'b' 0x62 */ { 776, 5, 9, 7, 1, -8 }, /* 'c' 0x63 */ { 782, 6, 7, 6, 0, -6 }, /* 'd' 0x64 */ { 788, 6, 9, 7, 0, -8 }, /* 'e' 0x65 */ { 795, 6, 7, 6, 0, -6 }, /* 'f' 0x66 */ { 801, 3, 9, 3, 0, -8 }, /* 'g' 0x67 */ { 805, 6, 10, 7, 0, -6 }, /* 'h' 0x68 */ { 813, 5, 9, 6, 1, -8 }, /* 'i' 0x69 */ { 819, 1, 9, 3, 1, -8 }, /* 'j' 0x6A */ { 821, 2, 12, 3, 0, -8 }, /* 'k' 0x6B */ { 824, 5, 9, 6, 1, -8 }, /* 'l' 0x6C */ { 830, 1, 9, 3, 1, -8 }, /* 'm' 0x6D */ { 832, 8, 7, 10, 1, -6 }, /* 'n' 0x6E */ { 839, 5, 7, 6, 1, -6 }, /* 'o' 0x6F */ { 844, 6, 7, 6, 0, -6 }, /* 'p' 0x70 */ { 850, 5, 9, 7, 1, -6 }, /* 'q' 0x71 */ { 856, 6, 9, 7, 0, -6 }, /* 'r' 0x72 */ { 863, 3, 7, 4, 1, -6 }, /* 's' 0x73 */ { 866, 5, 7, 6, 0, -6 }, /* 't' 0x74 */ { 871, 3, 8, 3, 0, -7 }, /* 'u' 0x75 */ { 874, 5, 7, 6, 1, -6 }, /* 'v' 0x76 */ { 879, 6, 7, 6, 0, -6 }, /* 'w' 0x77 */ { 885, 8, 7, 9, 0, -6 }, /* 'x' 0x78 */ { 892, 5, 7, 6, 0, -6 }, /* 'y' 0x79 */ { 897, 5, 10, 6, 0, -6 }, /* 'z' 0x7A */ { 904, 5, 7, 6, 0, -6 }, /* '{' 0x7B */ { 909, 2, 12, 4, 1, -8 }, /* '|' 0x7C */ { 912, 1, 11, 3, 1, -8 }, /* '}' 0x7D */ { 914, 2, 12, 4, 1, -8 }, /* '~' 0x7E */ { 917, 6, 2, 6, 0, -4 }, /* 0x7F */ { 919, 9, 10, 0, 1, -8 }, /* 0x80 */ { 931, 7, 9, 8, 0, -8 }, /* 0x81 */ { 939, 0, 0, 8, 0, 0 }, /* 0x82 */ { 939, 1, 3, 3, 1, 0 }, /* 0x83 */ { 940, 0, 0, 8, 0, 0 }, /* 0x84 */ { 940, 3, 3, 5, 1, 0 }, /* 0x85 */ { 942, 5, 1, 7, 1, 0 }, /* 0x86 */ { 943, 5, 11, 7, 1, -8 }, /* 0x87 */ { 950, 5, 11, 7, 1, -8 }, /* 0x88 */ { 957, 0, 0, 8, 0, 0 }, /* 0x89 */ { 957, 12, 9, 12, 0, -8 }, /* 0x8A */ { 971, 6, 11, 8, 1, -9 }, /* 0x8B */ { 980, 2, 3, 4, 1, -4 }, /* 0x8C */ { 981, 6, 11, 8, 1, -10 }, /* 0x8D */ { 990, 6, 10, 8, 0, -9 }, /* 0x8E */ { 998, 7, 10, 7, 0, -9 }, /* 0x8F */ { 1007, 7, 10, 7, 0, -9 }, /* 0x90 */ { 1016, 0, 0, 8, 0, 0 }, /* 0x91 */ { 1016, 1, 3, 3, 1, -8 }, /* 0x92 */ { 1017, 1, 3, 2, 1, -8 }, /* 0x93 */ { 1018, 3, 3, 5, 1, -8 }, /* 0x94 */ { 1020, 3, 3, 5, 1, -8 }, /* 0x95 */ { 1022, 3, 3, 5, 1, -5 }, /* 0x96 */ { 1024, 6, 1, 6, 0, -3 }, /* 0x97 */ { 1025, 12, 1, 12, 0, -3 }, /* 0x98 */ { 1027, 0, 0, 8, 0, 0 }, /* 0x99 */ { 1027, 11, 7, 12, 1, -8 }, /* 0x9A */ { 1037, 4, 9, 6, 1, -8 }, /* 0x9B */ { 1042, 2, 3, 3, 1, -4 }, /* 0x9C */ { 1043, 4, 10, 6, 1, -9 }, /* 0x9D */ { 1048, 4, 9, 5, 0, -8 }, /* 0x9E */ { 1053, 5, 10, 6, 0, -9 }, /* 0x9F */ { 1060, 5, 10, 6, 0, -9 }, /* 0xA0 */ { 1067, 0, 0, 3, 0, 0 }, /* 0xA1 */ { 1067, 3, 2, 4, 0, -8 }, /* 0xA2 */ { 1068, 4, 2, 4, 0, -8 }, /* 0xA3 */ { 1069, 6, 9, 7, 0, -8 }, /* 0xA4 */ { 1076, 5, 4, 7, 1, -5 }, /* 0xA5 */ { 1079, 8, 11, 8, 1, -8 }, /* 0xA6 */ { 1090, 1, 12, 3, 1, -8 }, /* 0xA7 */ { 1092, 5, 12, 7, 1, -8 }, /* 0xA8 */ { 1100, 3, 1, 4, 0, -7 }, /* 0xA9 */ { 1101, 9, 9, 10, 0, -8 }, /* 0xAA */ { 1112, 6, 12, 8, 1, -8 }, /* 0xAB */ { 1121, 4, 4, 6, 1, -4 }, /* 0xAC */ { 1123, 6, 3, 7, 1, -4 }, /* 0xAD */ { 1126, 0, 0, 0, 0, 0 }, /* 0xAE */ { 1126, 9, 9, 10, 0, -8 }, /* 0xAF */ { 1137, 7, 10, 7, 0, -9 }, /* 0xB0 */ { 1146, 4, 4, 7, 2, -8 }, /* 0xB1 */ { 1148, 5, 7, 7, 1, -6 }, /* 0xB2 */ { 1153, 3, 2, 4, 1, 1 }, /* 0xB3 */ { 1154, 3, 9, 3, 0, -8 }, /* 0xB4 */ { 1158, 1, 1, 4, 1, -8 }, /* 0xB5 */ { 1159, 6, 9, 7, 1, -6 }, /* 0xB6 */ { 1166, 6, 10, 6, 1, -8 }, /* 0xB7 */ { 1174, 1, 1, 3, 1, -2 }, /* 0xB8 */ { 1175, 3, 3, 4, 1, 1 }, /* 0xB9 */ { 1177, 8, 9, 7, 0, -6 }, /* 0xBA */ { 1186, 4, 10, 6, 1, -6 }, /* 0xBB */ { 1191, 4, 4, 6, 1, -5 }, /* 0xBC */ { 1193, 5, 9, 7, 1, -8 }, /* 0xBD */ { 1199, 3, 1, 4, 0, -8 }, /* 0xBE */ { 1200, 3, 10, 3, 1, -9 }, /* 0xBF */ { 1204, 5, 9, 6, 0, -8 }, /* 0xC0 */ { 1210, 7, 10, 9, 1, -9 }, /* 0xC1 */ { 1219, 8, 10, 8, 0, -9 }, /* 0xC2 */ { 1229, 8, 10, 8, 0, -9 }, /* 0xC3 */ { 1239, 8, 10, 8, 0, -9 }, /* 0xC4 */ { 1249, 8, 10, 8, 0, -9 }, /* 0xC5 */ { 1259, 5, 10, 7, 1, -9 }, /* 0xC6 */ { 1266, 7, 11, 9, 0, -10 }, /* 0xC7 */ { 1276, 8, 12, 9, 0, -8 }, /* 0xC8 */ { 1288, 7, 11, 9, 0, -10 }, /* 0xC9 */ { 1298, 6, 10, 8, 1, -9 }, /* 0xCA */ { 1306, 7, 11, 8, 1, -8 }, /* 0xCB */ { 1316, 6, 10, 8, 1, -9 }, /* 0xCC */ { 1324, 6, 11, 8, 1, -10 }, /* 0xCD */ { 1333, 2, 10, 3, 1, -9 }, /* 0xCE */ { 1336, 3, 10, 4, 0, -9 }, /* 0xCF */ { 1340, 7, 10, 8, 1, -9 }, /* 0xD0 */ { 1349, 8, 9, 8, 0, -8 }, /* 0xD1 */ { 1358, 7, 10, 9, 1, -9 }, /* 0xD2 */ { 1367, 7, 10, 9, 1, -9 }, /* 0xD3 */ { 1376, 9, 10, 9, 0, -9 }, /* 0xD4 */ { 1388, 9, 11, 9, 0, -10 }, /* 0xD5 */ { 1401, 9, 11, 9, 0, -10 }, /* 0xD6 */ { 1414, 9, 11, 9, 0, -10 }, /* 0xD7 */ { 1427, 5, 5, 7, 1, -5 }, /* 0xD8 */ { 1431, 7, 10, 9, 1, -9 }, /* 0xD9 */ { 1440, 7, 10, 9, 1, -9 }, /* 0xDA */ { 1449, 7, 10, 9, 1, -9 }, /* 0xDB */ { 1458, 7, 10, 9, 1, -9 }, /* 0xDC */ { 1467, 7, 10, 9, 1, -9 }, /* 0xDD */ { 1476, 7, 10, 8, 1, -9 }, /* 0xDE */ { 1485, 6, 12, 7, 0, -8 }, /* 0xDF */ { 1494, 6, 9, 7, 1, -8 }, /* 0xE0 */ { 1501, 3, 9, 4, 1, -8 }, /* 0xE1 */ { 1505, 7, 10, 7, 0, -9 }, /* 0xE2 */ { 1514, 7, 10, 7, 0, -9 }, /* 0xE3 */ { 1523, 7, 10, 7, 0, -9 }, /* 0xE4 */ { 1532, 7, 9, 7, 0, -8 }, /* 0xE5 */ { 1540, 2, 10, 3, 1, -9 }, /* 0xE6 */ { 1543, 6, 10, 6, 0, -9 }, /* 0xE7 */ { 1551, 6, 10, 6, 0, -6 }, /* 0xE8 */ { 1559, 6, 10, 6, 0, -9 }, /* 0xE9 */ { 1567, 6, 10, 6, 0, -9 }, /* 0xEA */ { 1575, 6, 9, 6, 0, -6 }, /* 0xEB */ { 1582, 6, 9, 6, 0, -8 }, /* 0xEC */ { 1589, 6, 10, 6, 0, -9 }, /* 0xED */ { 1597, 2, 10, 3, 1, -9 }, /* 0xEE */ { 1600, 3, 10, 3, 0, -9 }, /* 0xEF */ { 1604, 7, 10, 7, 0, -9 }, /* 0xF0 */ { 1613, 7, 9, 7, 0, -8 }, /* 0xF1 */ { 1621, 5, 10, 6, 1, -9 }, /* 0xF2 */ { 1628, 5, 10, 6, 1, -9 }, /* 0xF3 */ { 1635, 6, 10, 6, 0, -9 }, /* 0xF4 */ { 1643, 6, 10, 6, 0, -9 }, /* 0xF5 */ { 1651, 6, 10, 6, 0, -9 }, /* 0xF6 */ { 1659, 6, 9, 6, 0, -8 }, /* 0xF7 */ { 1666, 5, 5, 7, 1, -5 }, /* 0xF8 */ { 1670, 3, 10, 4, 1, -9 }, /* 0xF9 */ { 1674, 5, 10, 6, 1, -9 }, /* 0xFA */ { 1681, 5, 9, 6, 1, -8 }, /* 0xFB */ { 1687, 5, 10, 6, 1, -9 }, /* 0xFC */ { 1694, 5, 9, 6, 1, -8 }, /* 0xFD */ { 1700, 6, 12, 6, 0, -8 }, /* 0xFE */ { 1709, 4, 11, 3, 0, -7 }, /* 0xFF */ { 1715, 1, 1, 4, 1, -7 }, }; const GFXfont FreeSans6pt_Win1250 PROGMEM = { (uint8_t*)FreeSans6pt_Win1250Bitmaps, (GFXglyph*)FreeSans6pt_Win1250Glyphs, 0x01, 0xFF, 14 }; ================================================ FILE: src/graphics/niche/Fonts/FreeSans6pt_Win1251.h ================================================ // trunk-ignore-all(clang-format) #pragma once /* PROPERTIES FONT_NAME FreeSans6pt_Win1251 */ const uint8_t FreeSans6pt_Win1251Bitmaps[] PROGMEM = { /* 0x01 */ 0x1C, 0x0A, 0x05, 0x04, 0xFE, 0x08, 0x1C, 0x02, 0x07, 0xE0, 0x9F, 0xC0, /* 0x02 */ 0x3F, 0xF0, 0x40, 0xE0, 0x10, 0x3F, 0x04, 0x9E, 0x28, 0x14, 0x0E, 0x00, /* 0x03 */ 0x3F, 0x10, 0x28, 0x06, 0x49, 0x80, 0x60, 0x19, 0x26, 0x31, 0x40, 0x8F, 0xC0, /* 0x04 */ 0x3F, 0x10, 0x2A, 0x16, 0x49, 0xA1, 0x60, 0x19, 0xE6, 0x31, 0x40, 0x8F, 0xC0, /* 0x05 */ 0x28, 0x15, 0x2A, 0xB5, 0x55, 0xA8, 0x54, 0x12, 0x04, 0x41, 0x08, 0x81, 0xC0, /* 0x06 */ 0x04, 0x08, 0x88, 0x82, 0x07, 0x01, 0x11, 0xA2, 0xC4, 0x40, 0x70, 0x20, 0x88, 0x88, 0x10, 0x00, /* 0x07 */ /* 0x08 */ 0x03, 0x83, 0x44, 0x48, 0x28, 0x01, 0x80, 0x17, 0xFE, 0x08, 0x45, 0x28, 0x84, 0x00, /* 0x09 */ 0x01, 0xC0, 0x68, 0x82, 0x41, 0x10, 0x02, 0x80, 0x06, 0x00, 0x14, 0x00, 0x8F, 0xFC, /* 0x0A */ /* 0x0B */ 0x22, 0x2A, 0xA2, 0x30, 0x18, 0x0A, 0x09, 0x04, 0x44, 0x14, 0x04, 0x00, /* 0x0C */ 0x46, 0x00, 0x19, 0x03, 0x21, 0x20, 0x93, 0x04, 0x20, 0x11, 0x80, 0x50, 0x02, 0x7F, 0xE0, /* 0x0D */ /* 0x0E */ 0x08, 0x0E, 0x08, 0x88, 0x24, 0x12, 0x09, 0x05, 0x01, 0xFF, 0x8A, 0x02, 0x00, /* 0x0F */ 0x3F, 0x14, 0xAA, 0x16, 0x01, 0x92, 0x60, 0x18, 0xC6, 0x49, 0x40, 0x8F, 0xC0, /* 0x10 */ 0x1B, 0x02, 0xA0, 0x54, 0x12, 0x42, 0x48, 0x49, 0x31, 0x1E, 0x23, 0xEA, 0xFE, 0x3C, /* 0x11 */ 0x3F, 0x02, 0x00, 0x20, 0x6D, 0x27, 0xF8, 0x3F, 0xC1, 0xFE, 0x37, 0xD0, 0xBE, 0x40, 0xE1, 0xE2, 0x00, /* 0x12 */ 0x12, 0x42, 0x20, 0x24, 0xC0, 0x29, 0x99, 0x05, 0x23, 0x30, 0xB0, 0x30, 0x00, /* 0x13 */ 0x3F, 0x88, 0x0A, 0x44, 0xD5, 0x58, 0x03, 0x00, 0x67, 0xCC, 0x71, 0x40, 0x47, 0xF0, /* 0x14 */ 0x3F, 0x18, 0x69, 0x26, 0x85, 0xA1, 0x6C, 0xD8, 0x06, 0x31, 0x40, 0x8F, 0xC0, /* 0x15 */ 0x3F, 0x11, 0x00, 0xE8, 0x03, 0xA0, 0x1F, 0xB3, 0x7E, 0x00, 0xE9, 0xE0, 0x23, 0x00, 0x40, 0x40, 0xFE, 0x00, /* 0x16 */ 0x30, 0x38, 0x3A, 0x3E, 0x6E, 0xEB, 0xC3, 0xC3, 0x66, 0x3C, /* 0x17 */ 0x3F, 0x04, 0x00, 0x82, 0x88, 0x5C, 0xA4, 0x49, 0x22, 0x81, 0x98, 0xC4, 0x40, 0xA3, 0xF0, /* 0x18 */ 0x07, 0x80, 0x42, 0x04, 0x08, 0x21, 0x41, 0x42, 0x60, 0x0E, 0x8C, 0xB2, 0x89, 0x50, 0x52, 0x82, 0x80, /* 0x19 */ 0x3F, 0xC4, 0x02, 0x80, 0x18, 0x01, 0xB3, 0x1B, 0xB9, 0x80, 0x19, 0xE1, 0x40, 0x23, 0xFC, /* 0x1A */ 0xFF, 0xC0, 0x67, 0x34, 0x58, 0x4C, 0x46, 0x03, 0x11, 0x80, 0xFF, 0xC0, /* 0x1B */ 0x0F, 0xC0, 0x40, 0x82, 0x49, 0x08, 0x04, 0x00, 0x00, 0x12, 0x02, 0x31, 0x34, 0x0B, 0x88, 0x45, 0x00, 0x20, /* 0x1C */ 0x3F, 0x88, 0x0A, 0x44, 0xC9, 0x19, 0x3B, 0x00, 0x60, 0x4C, 0x71, 0x40, 0x47, 0xF0, /* 0x1D */ 0x3F, 0x8B, 0x0A, 0x00, 0xC8, 0x18, 0x13, 0x00, 0x48, 0xCA, 0xC1, 0x44, 0x53, 0x30, /* 0x1E */ 0x19, 0xC2, 0x02, 0x50, 0x1E, 0x49, 0x80, 0x12, 0x01, 0x27, 0x92, 0x01, 0x10, 0x20, 0xFC, /* 0x1F */ 0x30, 0x1C, 0x0C, 0x3E, 0x7E, 0xCF, 0x07, 0xC7, 0x7F, 0x3F, /* ' ' 0x20 */ /* '!' 0x21 */ 0xFC, 0x80, /* '"' 0x22 */ 0xB6, 0x80, /* '#' 0x23 */ 0x24, 0x51, 0xF9, 0x42, 0x9F, 0x92, 0x28, /* '$' 0x24 */ 0x10, 0xE5, 0x55, 0x50, 0xE1, 0x65, 0x55, 0xE1, 0x00, /* '%' 0x25 */ 0x71, 0x24, 0x89, 0x22, 0x50, 0x74, 0x02, 0x70, 0xA4, 0x49, 0x11, 0xC0, /* '&' 0x26 */ 0x71, 0x24, 0x9C, 0x62, 0x58, 0xA7, 0xF4, /* ''' 0x27 */ 0xE0, /* '(' 0x28 */ 0x5A, 0xAA, 0x94, /* ')' 0x29 */ 0x89, 0x12, 0x49, 0x29, 0x00, /* '*' 0x2A */ 0x5E, 0x80, /* '+' 0x2B */ 0x21, 0x3E, 0x42, 0x00, /* ',' 0x2C */ 0xE0, /* '-' 0x2D */ 0xC0, /* '.' 0x2E */ 0x80, /* '/' 0x2F */ 0x24, 0xA4, 0xA4, 0x80, /* '0' 0x30 */ 0x76, 0xE3, 0x18, 0xC6, 0x3B, 0x70, /* '1' 0x31 */ 0x27, 0x92, 0x49, 0x20, /* '2' 0x32 */ 0x79, 0x10, 0x41, 0x08, 0xC6, 0x10, 0xFC, /* '3' 0x33 */ 0x79, 0x30, 0x43, 0x18, 0x10, 0x71, 0x78, /* '4' 0x34 */ 0x08, 0x61, 0x8A, 0x49, 0x2F, 0xC2, 0x08, /* '5' 0x35 */ 0xFC, 0x21, 0xE8, 0x84, 0x31, 0xF0, /* '6' 0x36 */ 0x74, 0x61, 0xE8, 0xC6, 0x31, 0x70, /* '7' 0x37 */ 0xF8, 0x44, 0x22, 0x11, 0x08, 0x40, /* '8' 0x38 */ 0x39, 0x34, 0x53, 0x39, 0x1C, 0x51, 0x38, /* '9' 0x39 */ 0x39, 0x3C, 0x71, 0x4C, 0xF0, 0x53, 0x78, /* ':' 0x3A */ 0x82, /* ';' 0x3B */ 0x87, /* '<' 0x3C */ 0x3E, 0x30, 0x60, 0x80, /* '=' 0x3D */ 0xF8, 0x3E, /* '>' 0x3E */ 0xE0, 0xC6, 0xC8, 0x00, /* '?' 0x3F */ 0x74, 0x42, 0x11, 0x10, 0x80, 0x20, /* '@' 0x40 */ 0x0F, 0x86, 0x19, 0x9A, 0xA4, 0xD9, 0x13, 0x22, 0x56, 0xDA, 0x6E, 0x60, 0x06, 0x00, 0x3C, 0x00, /* 'A' 0x41 */ 0x18, 0x18, 0x24, 0x24, 0x24, 0x7E, 0x42, 0x42, 0xC3, /* 'B' 0x42 */ 0xFA, 0x18, 0x61, 0xFA, 0x18, 0x61, 0xFC, /* 'C' 0x43 */ 0x3E, 0x63, 0x40, 0x40, 0xC0, 0x40, 0x41, 0x63, 0x3E, /* 'D' 0x44 */ 0xF9, 0x0A, 0x1C, 0x18, 0x30, 0x61, 0xC2, 0xF8, /* 'E' 0x45 */ 0xFE, 0x08, 0x20, 0xFE, 0x08, 0x20, 0xFC, /* 'F' 0x46 */ 0xFE, 0x08, 0x20, 0xFA, 0x08, 0x20, 0x80, /* 'G' 0x47 */ 0x1E, 0x61, 0x40, 0x40, 0xC7, 0x41, 0x41, 0x63, 0x1D, /* 'H' 0x48 */ 0x83, 0x06, 0x0C, 0x1F, 0xF0, 0x60, 0xC1, 0x82, /* 'I' 0x49 */ 0xFF, 0x80, /* 'J' 0x4A */ 0x08, 0x42, 0x10, 0x87, 0x29, 0x70, /* 'K' 0x4B */ 0x85, 0x12, 0x45, 0x0D, 0x13, 0x22, 0x42, 0x86, /* 'L' 0x4C */ 0x84, 0x21, 0x08, 0x42, 0x10, 0xF8, /* 'M' 0x4D */ 0xC3, 0xC3, 0xC3, 0xA5, 0xA5, 0xA5, 0x99, 0x99, 0x99, /* 'N' 0x4E */ 0x83, 0x86, 0x8D, 0x19, 0x33, 0x62, 0xC3, 0x86, /* 'O' 0x4F */ 0x1E, 0x31, 0x90, 0x68, 0x1C, 0x0A, 0x05, 0x06, 0xC6, 0x1E, 0x00, /* 'P' 0x50 */ 0xFA, 0x18, 0x61, 0xFA, 0x08, 0x20, 0x80, /* 'Q' 0x51 */ 0x1E, 0x31, 0x90, 0x68, 0x1C, 0x0A, 0x05, 0x16, 0xC6, 0x1F, 0x00, 0x40, /* 'R' 0x52 */ 0xFD, 0x0E, 0x1C, 0x2F, 0x90, 0xA1, 0x42, 0x86, /* 'S' 0x53 */ 0x7A, 0x18, 0x30, 0x78, 0x38, 0x61, 0x78, /* 'T' 0x54 */ 0xFE, 0x20, 0x40, 0x81, 0x02, 0x04, 0x08, 0x10, /* 'U' 0x55 */ 0x83, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xE2, 0x78, /* 'V' 0x56 */ 0xC2, 0x85, 0x0B, 0x22, 0x44, 0x8E, 0x0C, 0x18, /* 'W' 0x57 */ 0xC4, 0x28, 0xCD, 0x29, 0x25, 0x24, 0xA4, 0x52, 0x8C, 0x61, 0x8C, 0x31, 0x80, /* 'X' 0x58 */ 0x87, 0x34, 0x8C, 0x30, 0xC4, 0xA3, 0x84, /* 'Y' 0x59 */ 0xC3, 0x42, 0x24, 0x34, 0x18, 0x08, 0x08, 0x08, 0x08, /* 'Z' 0x5A */ 0x7E, 0x0C, 0x30, 0x41, 0x06, 0x18, 0x20, 0xFE, /* '[' 0x5B */ 0xEA, 0xAA, 0xAB, /* '\' 0x5C */ 0x92, 0x24, 0x89, 0x20, /* ']' 0x5D */ 0xD5, 0x55, 0x57, /* '^' 0x5E */ 0x46, 0xA9, /* '_' 0x5F */ 0xFE, /* '`' 0x60 */ 0x80, /* 'a' 0x61 */ 0x79, 0x20, 0x4F, 0xC6, 0x37, 0x40, /* 'b' 0x62 */ 0x84, 0x3D, 0x18, 0xC6, 0x31, 0xF0, /* 'c' 0x63 */ 0x39, 0x3C, 0x20, 0xC1, 0x33, 0x80, /* 'd' 0x64 */ 0x04, 0x13, 0xD3, 0xC6, 0x1C, 0x53, 0x3C, /* 'e' 0x65 */ 0x39, 0x38, 0x7F, 0x81, 0x13, 0x80, /* 'f' 0x66 */ 0x6B, 0xA4, 0x92, 0x40, /* 'g' 0x67 */ 0x35, 0x3C, 0x61, 0xC5, 0x33, 0x41, 0x4D, 0xE0, /* 'h' 0x68 */ 0x84, 0x3D, 0x38, 0xC6, 0x31, 0x88, /* 'i' 0x69 */ 0xBF, 0x80, /* 'j' 0x6A */ 0x45, 0x55, 0x57, /* 'k' 0x6B */ 0x84, 0x25, 0x4E, 0x52, 0xD2, 0x88, /* 'l' 0x6C */ 0xFF, 0x80, /* 'm' 0x6D */ 0xF7, 0x99, 0x91, 0x91, 0x91, 0x91, 0x91, /* 'n' 0x6E */ 0xF4, 0x63, 0x18, 0xC6, 0x20, /* 'o' 0x6F */ 0x39, 0x3C, 0x61, 0xC5, 0x33, 0x80, /* 'p' 0x70 */ 0xF4, 0x63, 0x18, 0xC7, 0xD0, 0x80, /* 'q' 0x71 */ 0x3D, 0x3C, 0x61, 0xC5, 0x37, 0x41, 0x04, /* 'r' 0x72 */ 0xF2, 0x49, 0x20, /* 's' 0x73 */ 0x7A, 0x50, 0xE0, 0xE5, 0xE0, /* 't' 0x74 */ 0x5D, 0x24, 0x93, /* 'u' 0x75 */ 0x8C, 0x63, 0x18, 0xCF, 0xA0, /* 'v' 0x76 */ 0x85, 0x24, 0x92, 0x30, 0xC3, 0x00, /* 'w' 0x77 */ 0x89, 0x59, 0x59, 0x55, 0x56, 0x26, 0x26, /* 'x' 0x78 */ 0x4A, 0x4C, 0x43, 0x27, 0x20, /* 'y' 0x79 */ 0x8A, 0x52, 0xA5, 0x18, 0x84, 0x22, 0x00, /* 'z' 0x7A */ 0x78, 0x44, 0x46, 0x23, 0xE0, /* '{' 0x7B */ 0x6A, 0xAA, 0xA9, /* '|' 0x7C */ 0xFF, 0xE0, /* '}' 0x7D */ 0x95, 0x55, 0x56, /* '~' 0x7E */ 0x66, 0x60, /* 0x7F */ /* 0x80 */ 0xFC, 0x08, 0x04, 0x02, 0x01, 0xF0, 0x8C, 0x46, 0x23, 0x11, 0x80, 0xC0, 0xC0, /* 0x81 */ 0x10, 0x8F, 0xE0, 0x82, 0x08, 0x20, 0x82, 0x00, /* 0x82 */ 0xE0, /* 0x83 */ 0x24, 0x0F, 0x88, 0x88, 0x80, /* 0x84 */ 0xB6, 0x80, /* 0x85 */ 0xA8, /* 0x86 */ 0x21, 0x09, 0xF2, 0x10, 0x84, 0x21, 0x08, /* 0x87 */ 0x21, 0x09, 0xF2, 0x10, 0x84, 0xF9, 0x08, /* 0x88 */ 0x1C, 0x45, 0x07, 0xE4, 0x1F, 0x10, 0x10, 0x1E, /* 0x89 */ 0x62, 0x09, 0x40, 0x98, 0x06, 0x80, 0x10, 0x01, 0x66, 0x29, 0x92, 0x99, 0x06, 0x60, /* 0x8A */ 0x7C, 0x08, 0x81, 0x10, 0x22, 0x04, 0x7C, 0x88, 0x51, 0x0A, 0x21, 0x87, 0xC0, /* 0x8B */ 0x64, /* 0x8C */ 0x84, 0x10, 0x82, 0x10, 0x42, 0x0F, 0xFD, 0x08, 0xA1, 0x0C, 0x23, 0x87, 0xC0, /* 0x8D */ 0x10, 0x88, 0xE6, 0xB3, 0x8C, 0x28, 0x92, 0x28, 0xC0, /* 0x8E */ 0xFC, 0x08, 0x04, 0x02, 0x01, 0xF0, 0x8C, 0x46, 0x23, 0x11, 0x80, /* 0x8F */ 0x83, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0xFE, 0x20, 0x40, /* 0x90 */ 0x43, 0xC4, 0x1F, 0x45, 0x14, 0x51, 0x44, 0x11, 0x80, /* 0x91 */ 0xE0, /* 0x92 */ 0xE0, /* 0x93 */ 0xB6, 0x80, /* 0x94 */ 0xB6, 0x80, /* 0x95 */ 0xFF, 0x80, /* 0x96 */ 0xFC, /* 0x97 */ 0xFF, 0xF0, /* 0x98 */ /* 0x99 */ 0xE6, 0x28, 0xCD, 0x19, 0xA3, 0x34, 0x6A, 0x8B, 0x51, 0x68, /* 0x9A */ 0x78, 0x24, 0x13, 0xC9, 0x14, 0x8E, 0x7C, /* 0x9B */ 0x98, /* 0x9C */ 0x88, 0x44, 0x3F, 0xD1, 0x38, 0x8C, 0x78, /* 0x9D */ 0x24, 0x09, 0xAC, 0xCA, 0x90, /* 0x9E */ 0x43, 0xC4, 0x1F, 0x45, 0x14, 0x51, 0x44, /* 0x9F */ 0x8C, 0x63, 0x18, 0xFC, 0x80, /* 0xA0 */ /* 0xA1 */ 0x24, 0x33, 0x0A, 0x36, 0x45, 0x8E, 0x0C, 0x10, 0x60, 0x80, /* 0xA2 */ 0x51, 0x22, 0x95, 0xA8, 0xC4, 0x23, 0x10, /* 0xA3 */ 0x08, 0x42, 0x10, 0x86, 0x31, 0x78, /* 0xA4 */ 0xFC, 0x63, 0xF0, /* 0xA5 */ 0x07, 0xF8, 0x20, 0x82, 0x08, 0x20, 0x82, 0x00, /* 0xA6 */ 0xF9, 0xF0, /* 0xA7 */ 0x32, 0x91, 0xC9, 0x47, 0x26, 0x14, 0xA4, 0xC0, /* 0xA8 */ 0x28, 0x0F, 0xE0, 0x82, 0x0F, 0xE0, 0x82, 0x0F, 0xC0, /* 0xA9 */ 0x3E, 0x3F, 0xB8, 0xF4, 0x1A, 0x0D, 0x17, 0x76, 0xC6, 0x3E, 0x00, /* 0xAA */ 0x38, 0x8A, 0x0C, 0x0F, 0x90, 0x20, 0xE3, 0x7C, /* 0xAB */ 0x5A, 0xA5, /* 0xAC */ 0x51, 0x55, 0x56, /* 0xAD */ /* 0xAE */ 0x3E, 0x31, 0xB7, 0x72, 0x99, 0xCC, 0xC7, 0x56, 0xC6, 0x3E, 0x00, /* 0xAF */ 0xA1, 0x24, 0x92, 0x49, 0x00, /* 0xB0 */ 0x69, 0x96, /* 0xB1 */ 0x21, 0x3E, 0x42, 0x03, 0xE0, /* 0xB2 */ 0xFF, 0x80, /* 0xB3 */ 0xDF, 0x80, /* 0xB4 */ 0x27, 0xC9, 0x24, /* 0xB5 */ 0x8A, 0x28, 0xA2, 0x8A, 0x6E, 0xE0, 0x80, /* 0xB6 */ 0x7F, 0xAE, 0xBA, 0x68, 0xA2, 0x8A, 0x28, 0xA0, /* 0xB7 */ 0x80, /* 0xB8 */ 0x28, 0xA0, 0x1E, 0x47, 0xFC, 0x11, 0x78, /* 0xB9 */ 0x88, 0x44, 0x32, 0x59, 0xDA, 0xCD, 0x66, 0x6B, 0x32, 0x8B, 0x80, /* 0xBA */ 0x79, 0x1F, 0x30, 0x45, 0xE0, /* 0xBB */ 0xA5, 0x5A, /* 0xBC */ 0x45, 0x55, 0x57, /* 0xBD */ 0x7A, 0x18, 0x70, 0x78, 0x38, 0x61, 0x7C, /* 0xBE */ 0x7A, 0x1C, 0x1C, 0xBC, /* 0xBF */ 0xB4, 0x24, 0x92, 0x40, /* 0xC0 */ 0x18, 0x18, 0x3C, 0x24, 0x24, 0x7E, 0x42, 0x42, 0xC3, /* 0xC1 */ 0xFE, 0x08, 0x20, 0xFA, 0x18, 0x61, 0xF8, /* 0xC2 */ 0xFA, 0x18, 0x61, 0xFA, 0x18, 0x61, 0xFC, /* 0xC3 */ 0xFE, 0x08, 0x20, 0x82, 0x08, 0x20, 0x80, /* 0xC4 */ 0x1F, 0x08, 0x84, 0x42, 0x21, 0x10, 0x88, 0x44, 0x42, 0xFF, 0xC0, 0x60, 0x20, /* 0xC5 */ 0xFE, 0x08, 0x20, 0xFE, 0x08, 0x20, 0xFC, /* 0xC6 */ 0x88, 0xA4, 0x9A, 0x87, 0xC1, 0xC1, 0xF1, 0xAD, 0x92, 0x88, 0x80, /* 0xC7 */ 0x7A, 0x18, 0x41, 0x38, 0x18, 0x61, 0x7C, /* 0xC8 */ 0x87, 0x0E, 0x2C, 0x59, 0x34, 0x68, 0xE1, 0xC2, /* 0xC9 */ 0x28, 0x22, 0x1C, 0x38, 0xB1, 0x64, 0xD1, 0xA3, 0x87, 0x08, /* 0xCA */ 0x8E, 0x6B, 0x38, 0xC2, 0x89, 0x22, 0x8C, /* 0xCB */ 0x3E, 0x44, 0x89, 0x12, 0x24, 0x58, 0xA1, 0xC2, /* 0xCC */ 0xC3, 0xC3, 0xC3, 0xA5, 0xA5, 0xA5, 0x99, 0x99, 0x99, /* 0xCD */ 0x83, 0x06, 0x0C, 0x1F, 0xF0, 0x60, 0xC1, 0x82, /* 0xCE */ 0x3C, 0x42, 0x81, 0x81, 0x81, 0x81, 0x81, 0xC2, 0x7C, /* 0xCF */ 0xFF, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x82, /* 0xD0 */ 0xFA, 0x18, 0x61, 0xFE, 0x08, 0x20, 0x80, /* 0xD1 */ 0x38, 0x8A, 0x0C, 0x08, 0x10, 0x20, 0xE3, 0x7C, /* 0xD2 */ 0xFE, 0x20, 0x40, 0x81, 0x02, 0x04, 0x08, 0x10, /* 0xD3 */ 0xC2, 0x8D, 0x91, 0x63, 0x83, 0x04, 0x18, 0x20, /* 0xD4 */ 0x08, 0x1F, 0x32, 0x71, 0x18, 0x8C, 0x47, 0x26, 0xFE, 0x08, 0x00, /* 0xD5 */ 0x87, 0x34, 0x8C, 0x30, 0xC4, 0xB3, 0x84, /* 0xD6 */ 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0xFF, 0x01, 0x01, /* 0xD7 */ 0x8E, 0x38, 0xE3, 0x8D, 0xF0, 0xC3, 0x0C, /* 0xD8 */ 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0xFF, /* 0xD9 */ 0x99, 0x4C, 0xA6, 0x53, 0x29, 0x94, 0xCA, 0x65, 0x32, 0xFF, 0x80, 0x40, 0x20, /* 0xDA */ 0xF0, 0x04, 0x01, 0x00, 0x40, 0x1F, 0x84, 0x21, 0x0C, 0x42, 0x1F, 0x00, /* 0xDB */ 0x81, 0xC0, 0xE0, 0x70, 0x3F, 0xDC, 0x2E, 0x17, 0x0B, 0xF9, 0x80, /* 0xDC */ 0x82, 0x08, 0x20, 0xFE, 0x18, 0x61, 0xF8, /* 0xDD */ 0x79, 0x8A, 0x18, 0x13, 0xE0, 0x60, 0xC2, 0x7C, /* 0xDE */ 0x87, 0x26, 0x39, 0x06, 0x41, 0xF0, 0x64, 0x19, 0x06, 0x63, 0x8F, 0x80, /* 0xDF */ 0x7E, 0x18, 0x61, 0x7C, 0xD6, 0x71, 0x84, /* 0xE0 */ 0x79, 0x11, 0xD9, 0xCD, 0xD0, /* 0xE1 */ 0x0D, 0xC4, 0x1E, 0x47, 0x1C, 0x51, 0x78, /* 0xE2 */ 0xF4, 0xBD, 0x29, 0xF8, /* 0xE3 */ 0xF8, 0x88, 0x88, /* 0xE4 */ 0x3C, 0x48, 0x91, 0x22, 0x5F, 0xE0, 0x80, /* 0xE5 */ 0x79, 0x1F, 0xF0, 0x45, 0xE0, /* 0xE6 */ 0x92, 0x54, 0x38, 0x3C, 0x56, 0x93, /* 0xE7 */ 0x78, 0x23, 0x82, 0xCD, 0xE0, /* 0xE8 */ 0x9C, 0xEB, 0x5C, 0xC4, /* 0xE9 */ 0x70, 0x27, 0x3A, 0xD7, 0x31, /* 0xEA */ 0x9A, 0xCC, 0xA9, /* 0xEB */ 0x7A, 0x52, 0x94, 0xE4, /* 0xEC */ 0x8F, 0x3D, 0x6D, 0xA6, 0x90, /* 0xED */ 0x8C, 0x7F, 0x18, 0xC4, /* 0xEE */ 0x79, 0x1C, 0x71, 0x45, 0xE0, /* 0xEF */ 0xFC, 0x63, 0x18, 0xC4, /* 0xF0 */ 0xFC, 0x63, 0x18, 0xFA, 0x10, 0x80, /* 0xF1 */ 0x79, 0x1C, 0x30, 0x45, 0xE0, /* 0xF2 */ 0xF9, 0x08, 0x42, 0x10, /* 0xF3 */ 0x8A, 0x56, 0xA3, 0x10, 0x8C, 0x40, /* 0xF4 */ 0x04, 0x01, 0x07, 0xF9, 0x31, 0xC4, 0x71, 0x14, 0xC5, 0xFE, 0x04, 0x01, 0x00, 0x40, /* 0xF5 */ 0x4B, 0x8C, 0x65, 0xE4, /* 0xF6 */ 0x8A, 0x28, 0xA2, 0x8B, 0xF0, 0x40, /* 0xF7 */ 0x99, 0x97, 0x11, /* 0xF8 */ 0x96, 0x59, 0x65, 0x97, 0xF0, /* 0xF9 */ 0x95, 0x2A, 0x54, 0xA9, 0x5F, 0xC0, 0x80, /* 0xFA */ 0xF0, 0x20, 0x78, 0x91, 0x23, 0xC0, /* 0xFB */ 0x86, 0x1F, 0x63, 0x8F, 0xD0, /* 0xFC */ 0x84, 0x3D, 0x18, 0xF8, /* 0xFD */ 0xF4, 0xDE, 0x19, 0xF8, /* 0xFE */ 0x9E, 0xA2, 0xE1, 0xA1, 0xA2, 0x9E, /* 0xFF */ 0xFC, 0x7E, 0xD4, 0xC4, }; const GFXglyph FreeSans6pt_Win1251Glyphs[] PROGMEM = { /* 0x01 */ { 0, 9, 10, 11, 1, -9 }, /* 0x02 */ { 12, 9, 10, 11, 1, -8 }, /* 0x03 */ { 24, 10, 10, 12, 1, -8 }, /* 0x04 */ { 37, 10, 10, 12, 1, -8 }, /* 0x05 */ { 50, 10, 10, 12, 1, -9 }, /* 0x06 */ { 63, 11, 11, 13, 1, -9 }, /* 0x07 */ { 79, 0, 0, 8, 0, 0 }, /* 0x08 */ { 79, 12, 9, 14, 1, -8 }, /* 0x09 */ { 93, 14, 8, 16, 1, -7 }, /* 0x0A */ { 107, 0, 0, 8, 0, 0 }, /* 0x0B */ { 107, 9, 10, 11, 1, -9 }, /* 0x0C */ { 119, 13, 9, 15, 1, -8 }, /* 0x0D */ { 134, 0, 0, 8, 0, 0 }, /* 0x0E */ { 134, 9, 11, 11, 1, -9 }, /* 0x0F */ { 147, 10, 10, 12, 1, -9 }, /* 0x10 */ { 160, 11, 10, 13, 1, -9 }, /* 0x11 */ { 174, 13, 10, 15, 1, -9 }, /* 0x12 */ { 191, 10, 10, 12, 1, -9 }, /* 0x13 */ { 204, 11, 10, 13, 1, -9 }, /* 0x14 */ { 218, 10, 10, 12, 1, -9 }, /* 0x15 */ { 231, 14, 10, 16, 1, -9 }, /* 0x16 */ { 249, 8, 10, 10, 1, -9 }, /* 0x17 */ { 259, 12, 10, 14, 1, -9 }, /* 0x18 */ { 274, 13, 10, 15, 1, -9 }, /* 0x19 */ { 291, 12, 10, 14, 1, -9 }, /* 0x1A */ { 306, 9, 10, 11, 1, -8 }, /* 0x1B */ { 318, 14, 10, 16, 1, -9 }, /* 0x1C */ { 336, 11, 10, 13, 1, -9 }, /* 0x1D */ { 350, 11, 10, 13, 1, -9 }, /* 0x1E */ { 364, 12, 10, 14, 1, -9 }, /* 0x1F */ { 379, 8, 10, 11, 2, -9 }, /* ' ' 0x20 */ { 389, 0, 0, 3, 0, 0 }, /* '!' 0x21 */ { 389, 1, 9, 4, 2, -8 }, /* '"' 0x22 */ { 391, 3, 3, 4, 0, -8 }, /* '#' 0x23 */ { 393, 7, 8, 7, 0, -7 }, /* '$' 0x24 */ { 400, 6, 11, 7, 0, -9 }, /* '%' 0x25 */ { 409, 10, 9, 11, 0, -8 }, /* '&' 0x26 */ { 421, 6, 9, 8, 1, -8 }, /* ''' 0x27 */ { 428, 1, 3, 2, 1, -8 }, /* '(' 0x28 */ { 429, 2, 11, 4, 1, -8 }, /* ')' 0x29 */ { 432, 3, 11, 4, 0, -8 }, /* '*' 0x2A */ { 437, 3, 3, 5, 1, -8 }, /* '+' 0x2B */ { 439, 5, 5, 7, 1, -4 }, /* ',' 0x2C */ { 443, 1, 3, 3, 1, 0 }, /* '-' 0x2D */ { 444, 2, 1, 4, 1, -3 }, /* '.' 0x2E */ { 445, 1, 1, 3, 1, 0 }, /* '/' 0x2F */ { 446, 3, 9, 3, 0, -8 }, /* '0' 0x30 */ { 450, 5, 9, 7, 1, -8 }, /* '1' 0x31 */ { 456, 3, 9, 7, 1, -8 }, /* '2' 0x32 */ { 460, 6, 9, 7, 0, -8 }, /* '3' 0x33 */ { 467, 6, 9, 7, 0, -8 }, /* '4' 0x34 */ { 474, 6, 9, 7, 0, -8 }, /* '5' 0x35 */ { 481, 5, 9, 7, 1, -8 }, /* '6' 0x36 */ { 487, 5, 9, 7, 1, -8 }, /* '7' 0x37 */ { 493, 5, 9, 7, 1, -8 }, /* '8' 0x38 */ { 499, 6, 9, 7, 0, -8 }, /* '9' 0x39 */ { 506, 6, 9, 7, 0, -8 }, /* ':' 0x3A */ { 513, 1, 7, 3, 1, -6 }, /* ';' 0x3B */ { 514, 1, 8, 3, 1, -5 }, /* '<' 0x3C */ { 515, 5, 5, 7, 1, -4 }, /* '=' 0x3D */ { 519, 5, 3, 7, 1, -3 }, /* '>' 0x3E */ { 521, 5, 5, 7, 1, -4 }, /* '?' 0x3F */ { 525, 5, 9, 7, 1, -8 }, /* '@' 0x40 */ { 531, 11, 11, 12, 0, -8 }, /* 'A' 0x41 */ { 547, 8, 9, 8, 0, -8 }, /* 'B' 0x42 */ { 556, 6, 9, 8, 1, -8 }, /* 'C' 0x43 */ { 563, 8, 9, 9, 0, -8 }, /* 'D' 0x44 */ { 572, 7, 9, 8, 1, -8 }, /* 'E' 0x45 */ { 580, 6, 9, 8, 1, -8 }, /* 'F' 0x46 */ { 587, 6, 9, 7, 1, -8 }, /* 'G' 0x47 */ { 594, 8, 9, 9, 0, -8 }, /* 'H' 0x48 */ { 603, 7, 9, 9, 1, -8 }, /* 'I' 0x49 */ { 611, 1, 9, 3, 1, -8 }, /* 'J' 0x4A */ { 613, 5, 9, 6, 0, -8 }, /* 'K' 0x4B */ { 619, 7, 9, 8, 1, -8 }, /* 'L' 0x4C */ { 627, 5, 9, 7, 1, -8 }, /* 'M' 0x4D */ { 633, 8, 9, 10, 1, -8 }, /* 'N' 0x4E */ { 642, 7, 9, 9, 1, -8 }, /* 'O' 0x4F */ { 650, 9, 9, 9, 0, -8 }, /* 'P' 0x50 */ { 661, 6, 9, 8, 1, -8 }, /* 'Q' 0x51 */ { 668, 9, 10, 9, 0, -8 }, /* 'R' 0x52 */ { 680, 7, 9, 9, 1, -8 }, /* 'S' 0x53 */ { 688, 6, 9, 8, 1, -8 }, /* 'T' 0x54 */ { 695, 7, 9, 8, 0, -8 }, /* 'U' 0x55 */ { 703, 7, 9, 9, 1, -8 }, /* 'V' 0x56 */ { 711, 7, 9, 8, 0, -8 }, /* 'W' 0x57 */ { 719, 11, 9, 11, 0, -8 }, /* 'X' 0x58 */ { 732, 6, 9, 8, 1, -8 }, /* 'Y' 0x59 */ { 739, 8, 9, 8, 0, -8 }, /* 'Z' 0x5A */ { 748, 7, 9, 7, 0, -8 }, /* '[' 0x5B */ { 756, 2, 12, 3, 1, -8 }, /* '\' 0x5C */ { 759, 3, 9, 3, 0, -8 }, /* ']' 0x5D */ { 763, 2, 12, 3, 0, -8 }, /* '^' 0x5E */ { 766, 4, 4, 6, 1, -8 }, /* '_' 0x5F */ { 768, 7, 1, 7, 0, 2 }, /* '`' 0x60 */ { 769, 1, 1, 3, 1, -8 }, /* 'a' 0x61 */ { 770, 6, 7, 7, 0, -6 }, /* 'b' 0x62 */ { 776, 5, 9, 7, 1, -8 }, /* 'c' 0x63 */ { 782, 6, 7, 6, 0, -6 }, /* 'd' 0x64 */ { 788, 6, 9, 7, 0, -8 }, /* 'e' 0x65 */ { 795, 6, 7, 6, 0, -6 }, /* 'f' 0x66 */ { 801, 3, 9, 3, 0, -8 }, /* 'g' 0x67 */ { 805, 6, 10, 7, 0, -6 }, /* 'h' 0x68 */ { 813, 5, 9, 6, 1, -8 }, /* 'i' 0x69 */ { 819, 1, 9, 3, 1, -8 }, /* 'j' 0x6A */ { 821, 2, 12, 3, 0, -8 }, /* 'k' 0x6B */ { 824, 5, 9, 6, 1, -8 }, /* 'l' 0x6C */ { 830, 1, 9, 3, 1, -8 }, /* 'm' 0x6D */ { 832, 8, 7, 10, 1, -6 }, /* 'n' 0x6E */ { 839, 5, 7, 6, 1, -6 }, /* 'o' 0x6F */ { 844, 6, 7, 6, 0, -6 }, /* 'p' 0x70 */ { 850, 5, 9, 7, 1, -6 }, /* 'q' 0x71 */ { 856, 6, 9, 7, 0, -6 }, /* 'r' 0x72 */ { 863, 3, 7, 4, 1, -6 }, /* 's' 0x73 */ { 866, 5, 7, 6, 0, -6 }, /* 't' 0x74 */ { 871, 3, 8, 3, 0, -7 }, /* 'u' 0x75 */ { 874, 5, 7, 6, 1, -6 }, /* 'v' 0x76 */ { 879, 6, 7, 6, 0, -6 }, /* 'w' 0x77 */ { 885, 8, 7, 9, 0, -6 }, /* 'x' 0x78 */ { 892, 5, 7, 6, 0, -6 }, /* 'y' 0x79 */ { 897, 5, 10, 6, 0, -6 }, /* 'z' 0x7A */ { 904, 5, 7, 6, 0, -6 }, /* '{' 0x7B */ { 909, 2, 12, 4, 1, -8 }, /* '|' 0x7C */ { 912, 1, 11, 3, 1, -8 }, /* '}' 0x7D */ { 914, 2, 12, 4, 1, -8 }, /* '~' 0x7E */ { 917, 6, 2, 6, 0, -4 }, /* 0x7F */ { 919, 0, 0, 0, 0, 0 }, /* 0x80 */ { 919, 9, 11, 9, 0, -8 }, /* 0x81 */ { 932, 6, 10, 7, 1, -9 }, /* 0x82 */ { 940, 1, 3, 3, 1, 0 }, /* 0x83 */ { 941, 4, 9, 5, 1, -8 }, /* 0x84 */ { 946, 3, 3, 5, 1, 0 }, /* 0x85 */ { 948, 5, 1, 7, 1, 0 }, /* 0x86 */ { 949, 5, 11, 7, 1, -8 }, /* 0x87 */ { 956, 5, 11, 7, 1, -8 }, /* 0x88 */ { 963, 7, 9, 8, 0, -8 }, /* 0x89 */ { 971, 12, 9, 12, 0, -8 }, /* 0x8A */ { 985, 11, 9, 13, 1, -8 }, /* 0x8B */ { 998, 2, 3, 4, 1, -4 }, /* 0x8C */ { 999, 11, 9, 12, 1, -8 }, /* 0x8D */ { 1012, 6, 11, 8, 1, -10 }, /* 0x8E */ { 1021, 9, 9, 9, 0, -8 }, /* 0x8F */ { 1032, 7, 11, 9, 1, -8 }, /* 0x90 */ { 1042, 6, 11, 7, 0, -8 }, /* 0x91 */ { 1051, 1, 3, 3, 1, -8 }, /* 0x92 */ { 1052, 1, 3, 2, 1, -8 }, /* 0x93 */ { 1053, 3, 3, 5, 1, -8 }, /* 0x94 */ { 1055, 3, 3, 5, 1, -8 }, /* 0x95 */ { 1057, 3, 3, 5, 1, -5 }, /* 0x96 */ { 1059, 6, 1, 6, 0, -3 }, /* 0x97 */ { 1060, 12, 1, 12, 0, -3 }, /* 0x98 */ { 1062, 0, 0, 8, 0, 0 }, /* 0x99 */ { 1062, 11, 7, 12, 1, -8 }, /* 0x9A */ { 1072, 9, 6, 10, 0, -5 }, /* 0x9B */ { 1079, 2, 3, 3, 1, -4 }, /* 0x9C */ { 1080, 9, 6, 10, 1, -5 }, /* 0x9D */ { 1087, 4, 9, 6, 1, -8 }, /* 0x9E */ { 1092, 6, 9, 7, 0, -8 }, /* 0x9F */ { 1099, 5, 7, 7, 1, -5 }, /* 0xA0 */ { 1104, 0, 0, 3, 0, 0 }, /* 0xA1 */ { 1104, 7, 11, 7, 0, -10 }, /* 0xA2 */ { 1114, 5, 11, 6, 0, -7 }, /* 0xA3 */ { 1121, 5, 9, 6, 0, -8 }, /* 0xA4 */ { 1127, 5, 4, 7, 1, -5 }, /* 0xA5 */ { 1130, 6, 10, 7, 1, -9 }, /* 0xA6 */ { 1138, 1, 12, 3, 1, -8 }, /* 0xA7 */ { 1140, 5, 12, 7, 1, -8 }, /* 0xA8 */ { 1148, 6, 11, 8, 1, -10 }, /* 0xA9 */ { 1157, 9, 9, 10, 0, -8 }, /* 0xAA */ { 1168, 7, 9, 9, 1, -8 }, /* 0xAB */ { 1176, 4, 4, 6, 1, -4 }, /* 0xAC */ { 1178, 2, 12, 3, 0, -8 }, /* 0xAD */ { 1181, 0, 0, 0, 0, 0 }, /* 0xAE */ { 1181, 9, 9, 10, 0, -8 }, /* 0xAF */ { 1192, 3, 11, 3, 0, -10 }, /* 0xB0 */ { 1197, 4, 4, 7, 2, -8 }, /* 0xB1 */ { 1199, 5, 7, 7, 1, -6 }, /* 0xB2 */ { 1204, 1, 9, 3, 1, -8 }, /* 0xB3 */ { 1206, 1, 9, 3, 1, -8 }, /* 0xB4 */ { 1208, 3, 8, 5, 1, -7 }, /* 0xB5 */ { 1211, 6, 9, 7, 1, -6 }, /* 0xB6 */ { 1218, 6, 10, 6, 1, -8 }, /* 0xB7 */ { 1226, 1, 1, 3, 1, -2 }, /* 0xB8 */ { 1227, 6, 9, 7, 0, -8 }, /* 0xB9 */ { 1234, 9, 9, 11, 1, -8 }, /* 0xBA */ { 1245, 6, 6, 6, 0, -5 }, /* 0xBB */ { 1250, 4, 4, 6, 1, -5 }, /* 0xBC */ { 1252, 2, 12, 3, 0, -8 }, /* 0xBD */ { 1255, 6, 9, 8, 1, -8 }, /* 0xBE */ { 1262, 5, 6, 6, 0, -5 }, /* 0xBF */ { 1266, 3, 9, 3, 0, -8 }, /* 0xC0 */ { 1270, 8, 9, 8, 0, -8 }, /* 0xC1 */ { 1279, 6, 9, 8, 1, -8 }, /* 0xC2 */ { 1286, 6, 9, 8, 1, -8 }, /* 0xC3 */ { 1293, 6, 9, 7, 1, -8 }, /* 0xC4 */ { 1300, 9, 11, 10, 0, -8 }, /* 0xC5 */ { 1313, 6, 9, 8, 1, -8 }, /* 0xC6 */ { 1320, 9, 9, 11, 1, -8 }, /* 0xC7 */ { 1331, 6, 9, 8, 1, -8 }, /* 0xC8 */ { 1338, 7, 9, 9, 1, -8 }, /* 0xC9 */ { 1346, 7, 11, 9, 1, -10 }, /* 0xCA */ { 1356, 6, 9, 8, 1, -8 }, /* 0xCB */ { 1363, 7, 9, 8, 0, -8 }, /* 0xCC */ { 1371, 8, 9, 10, 1, -8 }, /* 0xCD */ { 1380, 7, 9, 9, 1, -8 }, /* 0xCE */ { 1388, 8, 9, 10, 1, -8 }, /* 0xCF */ { 1397, 7, 9, 9, 1, -8 }, /* 0xD0 */ { 1405, 6, 9, 8, 1, -8 }, /* 0xD1 */ { 1412, 7, 9, 9, 1, -8 }, /* 0xD2 */ { 1420, 7, 9, 7, 0, -8 }, /* 0xD3 */ { 1428, 7, 9, 7, 0, -8 }, /* 0xD4 */ { 1436, 9, 9, 10, 1, -8 }, /* 0xD5 */ { 1447, 6, 9, 8, 1, -8 }, /* 0xD6 */ { 1454, 8, 11, 9, 1, -8 }, /* 0xD7 */ { 1465, 6, 9, 8, 1, -8 }, /* 0xD8 */ { 1472, 8, 9, 10, 1, -8 }, /* 0xD9 */ { 1481, 9, 11, 10, 1, -8 }, /* 0xDA */ { 1494, 10, 9, 10, 0, -8 }, /* 0xDB */ { 1506, 9, 9, 10, 1, -8 }, /* 0xDC */ { 1517, 6, 9, 8, 1, -8 }, /* 0xDD */ { 1524, 7, 9, 9, 1, -8 }, /* 0xDE */ { 1532, 10, 9, 12, 1, -8 }, /* 0xDF */ { 1544, 6, 9, 8, 1, -8 }, /* 0xE0 */ { 1551, 6, 6, 7, 0, -5 }, /* 0xE1 */ { 1556, 6, 9, 7, 0, -8 }, /* 0xE2 */ { 1563, 5, 6, 6, 1, -5 }, /* 0xE3 */ { 1567, 4, 6, 5, 1, -5 }, /* 0xE4 */ { 1570, 7, 7, 7, 0, -5 }, /* 0xE5 */ { 1577, 6, 6, 7, 0, -5 }, /* 0xE6 */ { 1582, 8, 6, 9, 1, -5 }, /* 0xE7 */ { 1588, 6, 6, 6, 0, -5 }, /* 0xE8 */ { 1593, 5, 6, 7, 1, -5 }, /* 0xE9 */ { 1597, 5, 8, 7, 1, -7 }, /* 0xEA */ { 1602, 4, 6, 6, 1, -5 }, /* 0xEB */ { 1605, 5, 6, 6, 0, -5 }, /* 0xEC */ { 1609, 6, 6, 7, 1, -5 }, /* 0xED */ { 1614, 5, 6, 7, 1, -5 }, /* 0xEE */ { 1618, 6, 6, 7, 0, -5 }, /* 0xEF */ { 1623, 5, 6, 7, 1, -5 }, /* 0xF0 */ { 1627, 5, 9, 7, 1, -5 }, /* 0xF1 */ { 1633, 6, 6, 6, 0, -5 }, /* 0xF2 */ { 1638, 5, 6, 5, 0, -5 }, /* 0xF3 */ { 1642, 5, 9, 6, 0, -5 }, /* 0xF4 */ { 1648, 10, 11, 10, 0, -7 }, /* 0xF5 */ { 1662, 5, 6, 6, 0, -5 }, /* 0xF6 */ { 1666, 6, 7, 7, 1, -5 }, /* 0xF7 */ { 1672, 4, 6, 6, 1, -5 }, /* 0xF8 */ { 1675, 6, 6, 8, 1, -5 }, /* 0xF9 */ { 1680, 7, 7, 9, 1, -5 }, /* 0xFA */ { 1687, 7, 6, 8, 0, -5 }, /* 0xFB */ { 1693, 6, 6, 8, 1, -5 }, /* 0xFC */ { 1698, 5, 6, 6, 1, -5 }, /* 0xFD */ { 1702, 5, 6, 6, 1, -5 }, /* 0xFE */ { 1706, 8, 6, 9, 1, -5 }, /* 0xFF */ { 1712, 5, 6, 7, 1, -5 }, }; const GFXfont FreeSans6pt_Win1251 PROGMEM = { (uint8_t*)FreeSans6pt_Win1251Bitmaps, (GFXglyph*)FreeSans6pt_Win1251Glyphs, 0x01, 0xFF, 14 }; ================================================ FILE: src/graphics/niche/Fonts/FreeSans6pt_Win1252.h ================================================ // trunk-ignore-all(clang-format) #pragma once /* PROPERTIES FONT_NAME FreeSans6pt_Win1252 */ const uint8_t FreeSans6pt_Win1252Bitmaps[] PROGMEM = { /* 0x01 */ 0x1C, 0x0A, 0x05, 0x04, 0xFE, 0x08, 0x1C, 0x02, 0x07, 0xE0, 0x9F, 0xC0, /* 0x02 */ 0x3F, 0xF0, 0x40, 0xE0, 0x10, 0x3F, 0x04, 0x9E, 0x28, 0x14, 0x0E, 0x00, /* 0x03 */ 0x3F, 0x10, 0x28, 0x06, 0x49, 0x80, 0x60, 0x19, 0x26, 0x31, 0x40, 0x8F, 0xC0, /* 0x04 */ 0x3F, 0x10, 0x2A, 0x16, 0x49, 0xA1, 0x60, 0x19, 0xE6, 0x31, 0x40, 0x8F, 0xC0, /* 0x05 */ 0x28, 0x15, 0x2A, 0xB5, 0x55, 0xA8, 0x54, 0x12, 0x04, 0x41, 0x08, 0x81, 0xC0, /* 0x06 */ 0x04, 0x08, 0x88, 0x82, 0x07, 0x01, 0x11, 0xA2, 0xC4, 0x40, 0x70, 0x20, 0x88, 0x88, 0x10, 0x00, /* 0x07 */ /* 0x08 */ 0x03, 0x83, 0x44, 0x48, 0x28, 0x01, 0x80, 0x17, 0xFE, 0x08, 0x45, 0x28, 0x84, 0x00, /* 0x09 */ 0x01, 0xC0, 0x68, 0x82, 0x41, 0x10, 0x02, 0x80, 0x06, 0x00, 0x14, 0x00, 0x8F, 0xFC, /* 0x0A */ /* 0x0B */ 0x22, 0x2A, 0xA2, 0x30, 0x18, 0x0A, 0x09, 0x04, 0x44, 0x14, 0x04, 0x00, /* 0x0C */ 0x46, 0x00, 0x19, 0x03, 0x21, 0x20, 0x93, 0x04, 0x20, 0x11, 0x80, 0x50, 0x02, 0x7F, 0xE0, /* 0x0D */ /* 0x0E */ 0x08, 0x0E, 0x08, 0x88, 0x24, 0x12, 0x09, 0x05, 0x01, 0xFF, 0x8A, 0x02, 0x00, /* 0x0F */ 0x3F, 0x14, 0xAA, 0x16, 0x01, 0x92, 0x60, 0x18, 0xC6, 0x49, 0x40, 0x8F, 0xC0, /* 0x10 */ 0x1B, 0x02, 0xA0, 0x54, 0x12, 0x42, 0x48, 0x49, 0x31, 0x1E, 0x23, 0xEA, 0xFE, 0x3C, /* 0x11 */ 0x3F, 0x02, 0x00, 0x20, 0x6D, 0x27, 0xF8, 0x3F, 0xC1, 0xFE, 0x37, 0xD0, 0xBE, 0x40, 0xE1, 0xE2, 0x00, /* 0x12 */ 0x12, 0x42, 0x20, 0x24, 0xC0, 0x29, 0x99, 0x05, 0x23, 0x30, 0xB0, 0x30, 0x00, /* 0x13 */ 0x3F, 0x88, 0x0A, 0x44, 0xD5, 0x58, 0x03, 0x00, 0x67, 0xCC, 0x71, 0x40, 0x47, 0xF0, /* 0x14 */ 0x3F, 0x18, 0x69, 0x26, 0x85, 0xA1, 0x6C, 0xD8, 0x06, 0x31, 0x40, 0x8F, 0xC0, /* 0x15 */ 0x3F, 0x11, 0x00, 0xE8, 0x03, 0xA0, 0x1F, 0xB3, 0x7E, 0x00, 0xE9, 0xE0, 0x23, 0x00, 0x40, 0x40, 0xFE, 0x00, /* 0x16 */ 0x30, 0x38, 0x3A, 0x3E, 0x6E, 0xEB, 0xC3, 0xC3, 0x66, 0x3C, /* 0x17 */ 0x3F, 0x04, 0x00, 0x82, 0x88, 0x5C, 0xA4, 0x49, 0x22, 0x81, 0x98, 0xC4, 0x40, 0xA3, 0xF0, /* 0x18 */ 0x07, 0x80, 0x42, 0x04, 0x08, 0x21, 0x41, 0x42, 0x60, 0x0E, 0x8C, 0xB2, 0x89, 0x50, 0x52, 0x82, 0x80, /* 0x19 */ 0x3F, 0xC4, 0x02, 0x80, 0x18, 0x01, 0xB3, 0x1B, 0xB9, 0x80, 0x19, 0xE1, 0x40, 0x23, 0xFC, /* 0x1A */ 0xFF, 0xC0, 0x67, 0x34, 0x58, 0x4C, 0x46, 0x03, 0x11, 0x80, 0xFF, 0xC0, /* 0x1B */ 0x0F, 0xC0, 0x40, 0x82, 0x49, 0x08, 0x04, 0x00, 0x00, 0x12, 0x02, 0x31, 0x34, 0x0B, 0x88, 0x45, 0x00, 0x20, /* 0x1C */ 0x3F, 0x88, 0x0A, 0x44, 0xC9, 0x19, 0x3B, 0x00, 0x60, 0x4C, 0x71, 0x40, 0x47, 0xF0, /* 0x1D */ 0x3F, 0x8B, 0x0A, 0x00, 0xC8, 0x18, 0x13, 0x00, 0x48, 0xCA, 0xC1, 0x44, 0x53, 0x30, /* 0x1E */ 0x19, 0xC2, 0x02, 0x50, 0x1E, 0x49, 0x80, 0x12, 0x01, 0x27, 0x92, 0x01, 0x10, 0x20, 0xFC, /* 0x1F */ 0x30, 0x1C, 0x0C, 0x3E, 0x7E, 0xCF, 0x07, 0xC7, 0x7F, 0x3F, /* ' ' 0x20 */ /* '!' 0x21 */ 0xFC, 0x80, /* '"' 0x22 */ 0xB6, 0x80, /* '#' 0x23 */ 0x24, 0x51, 0xF9, 0x42, 0x9F, 0x92, 0x28, /* '$' 0x24 */ 0x10, 0xE5, 0x55, 0x50, 0xE1, 0x65, 0x55, 0xE1, 0x00, /* '%' 0x25 */ 0x71, 0x24, 0x89, 0x22, 0x50, 0x74, 0x02, 0x70, 0xA4, 0x49, 0x11, 0xC0, /* '&' 0x26 */ 0x71, 0x24, 0x9C, 0x62, 0x58, 0xA7, 0xF4, /* ''' 0x27 */ 0xE0, /* '(' 0x28 */ 0x5A, 0xAA, 0x94, /* ')' 0x29 */ 0x89, 0x12, 0x49, 0x29, 0x00, /* '*' 0x2A */ 0x5E, 0x80, /* '+' 0x2B */ 0x21, 0x3E, 0x42, 0x00, /* ',' 0x2C */ 0xE0, /* '-' 0x2D */ 0xC0, /* '.' 0x2E */ 0x80, /* '/' 0x2F */ 0x24, 0xA4, 0xA4, 0x80, /* '0' 0x30 */ 0x76, 0xE3, 0x18, 0xC6, 0x3B, 0x70, /* '1' 0x31 */ 0x27, 0x92, 0x49, 0x20, /* '2' 0x32 */ 0x79, 0x10, 0x41, 0x08, 0xC6, 0x10, 0xFC, /* '3' 0x33 */ 0x79, 0x30, 0x43, 0x18, 0x10, 0x71, 0x78, /* '4' 0x34 */ 0x08, 0x61, 0x8A, 0x49, 0x2F, 0xC2, 0x08, /* '5' 0x35 */ 0xFC, 0x21, 0xE8, 0x84, 0x31, 0xF0, /* '6' 0x36 */ 0x74, 0x61, 0xE8, 0xC6, 0x31, 0x70, /* '7' 0x37 */ 0xF8, 0x44, 0x22, 0x11, 0x08, 0x40, /* '8' 0x38 */ 0x39, 0x34, 0x53, 0x39, 0x1C, 0x51, 0x38, /* '9' 0x39 */ 0x39, 0x3C, 0x71, 0x4C, 0xF0, 0x53, 0x78, /* ':' 0x3A */ 0x82, /* ';' 0x3B */ 0x87, /* '<' 0x3C */ 0x3E, 0x30, 0x60, 0x80, /* '=' 0x3D */ 0xF8, 0x3E, /* '>' 0x3E */ 0xE0, 0xC6, 0xC8, 0x00, /* '?' 0x3F */ 0x74, 0x42, 0x11, 0x10, 0x80, 0x20, /* '@' 0x40 */ 0x0F, 0x86, 0x19, 0x9A, 0xA4, 0xD9, 0x13, 0x22, 0x56, 0xDA, 0x6E, 0x60, 0x06, 0x00, 0x3C, 0x00, /* 'A' 0x41 */ 0x18, 0x18, 0x24, 0x24, 0x24, 0x7E, 0x42, 0x42, 0xC3, /* 'B' 0x42 */ 0xFA, 0x18, 0x61, 0xFA, 0x18, 0x61, 0xFC, /* 'C' 0x43 */ 0x3E, 0x63, 0x40, 0x40, 0xC0, 0x40, 0x41, 0x63, 0x3E, /* 'D' 0x44 */ 0xF9, 0x0A, 0x1C, 0x18, 0x30, 0x61, 0xC2, 0xF8, /* 'E' 0x45 */ 0xFE, 0x08, 0x20, 0xFE, 0x08, 0x20, 0xFC, /* 'F' 0x46 */ 0xFE, 0x08, 0x20, 0xFA, 0x08, 0x20, 0x80, /* 'G' 0x47 */ 0x1E, 0x61, 0x40, 0x40, 0xC7, 0x41, 0x41, 0x63, 0x1D, /* 'H' 0x48 */ 0x83, 0x06, 0x0C, 0x1F, 0xF0, 0x60, 0xC1, 0x82, /* 'I' 0x49 */ 0xFF, 0x80, /* 'J' 0x4A */ 0x08, 0x42, 0x10, 0x87, 0x29, 0x70, /* 'K' 0x4B */ 0x85, 0x12, 0x45, 0x0D, 0x13, 0x22, 0x42, 0x86, /* 'L' 0x4C */ 0x84, 0x21, 0x08, 0x42, 0x10, 0xF8, /* 'M' 0x4D */ 0xC3, 0xC3, 0xC3, 0xA5, 0xA5, 0xA5, 0x99, 0x99, 0x99, /* 'N' 0x4E */ 0x83, 0x86, 0x8D, 0x19, 0x33, 0x62, 0xC3, 0x86, /* 'O' 0x4F */ 0x1E, 0x31, 0x90, 0x68, 0x1C, 0x0A, 0x05, 0x06, 0xC6, 0x1E, 0x00, /* 'P' 0x50 */ 0xFA, 0x18, 0x61, 0xFA, 0x08, 0x20, 0x80, /* 'Q' 0x51 */ 0x1E, 0x31, 0x90, 0x68, 0x1C, 0x0A, 0x05, 0x16, 0xC6, 0x1F, 0x00, 0x40, /* 'R' 0x52 */ 0xFD, 0x0E, 0x1C, 0x2F, 0x90, 0xA1, 0x42, 0x86, /* 'S' 0x53 */ 0x7A, 0x18, 0x30, 0x78, 0x38, 0x61, 0x78, /* 'T' 0x54 */ 0xFE, 0x20, 0x40, 0x81, 0x02, 0x04, 0x08, 0x10, /* 'U' 0x55 */ 0x83, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xE2, 0x78, /* 'V' 0x56 */ 0xC2, 0x85, 0x0B, 0x22, 0x44, 0x8E, 0x0C, 0x18, /* 'W' 0x57 */ 0xC4, 0x28, 0xCD, 0x29, 0x25, 0x24, 0xA4, 0x52, 0x8C, 0x61, 0x8C, 0x31, 0x80, /* 'X' 0x58 */ 0x87, 0x34, 0x8C, 0x30, 0xC4, 0xA3, 0x84, /* 'Y' 0x59 */ 0xC3, 0x42, 0x24, 0x34, 0x18, 0x08, 0x08, 0x08, 0x08, /* 'Z' 0x5A */ 0x7E, 0x0C, 0x30, 0x41, 0x06, 0x18, 0x20, 0xFE, /* '[' 0x5B */ 0xEA, 0xAA, 0xAB, /* '\' 0x5C */ 0x92, 0x24, 0x89, 0x20, /* ']' 0x5D */ 0xD5, 0x55, 0x57, /* '^' 0x5E */ 0x46, 0xA9, /* '_' 0x5F */ 0xFE, /* '`' 0x60 */ 0x80, /* 'a' 0x61 */ 0x79, 0x20, 0x4F, 0xC6, 0x37, 0x40, /* 'b' 0x62 */ 0x84, 0x3D, 0x18, 0xC6, 0x31, 0xF0, /* 'c' 0x63 */ 0x39, 0x3C, 0x20, 0xC1, 0x33, 0x80, /* 'd' 0x64 */ 0x04, 0x13, 0xD3, 0xC6, 0x1C, 0x53, 0x3C, /* 'e' 0x65 */ 0x39, 0x38, 0x7F, 0x81, 0x13, 0x80, /* 'f' 0x66 */ 0x6B, 0xA4, 0x92, 0x40, /* 'g' 0x67 */ 0x35, 0x3C, 0x61, 0xC5, 0x33, 0x41, 0x4D, 0xE0, /* 'h' 0x68 */ 0x84, 0x3D, 0x38, 0xC6, 0x31, 0x88, /* 'i' 0x69 */ 0xBF, 0x80, /* 'j' 0x6A */ 0x45, 0x55, 0x57, /* 'k' 0x6B */ 0x84, 0x25, 0x4E, 0x52, 0xD2, 0x88, /* 'l' 0x6C */ 0xFF, 0x80, /* 'm' 0x6D */ 0xF7, 0x99, 0x91, 0x91, 0x91, 0x91, 0x91, /* 'n' 0x6E */ 0xF4, 0x63, 0x18, 0xC6, 0x20, /* 'o' 0x6F */ 0x39, 0x3C, 0x61, 0xC5, 0x33, 0x80, /* 'p' 0x70 */ 0xF4, 0x63, 0x18, 0xC7, 0xD0, 0x80, /* 'q' 0x71 */ 0x3D, 0x3C, 0x61, 0xC5, 0x37, 0x41, 0x04, /* 'r' 0x72 */ 0xF2, 0x49, 0x20, /* 's' 0x73 */ 0x7A, 0x50, 0xE0, 0xE5, 0xE0, /* 't' 0x74 */ 0x5D, 0x24, 0x93, /* 'u' 0x75 */ 0x8C, 0x63, 0x18, 0xCF, 0xA0, /* 'v' 0x76 */ 0x85, 0x24, 0x92, 0x30, 0xC3, 0x00, /* 'w' 0x77 */ 0x89, 0x59, 0x59, 0x55, 0x56, 0x26, 0x26, /* 'x' 0x78 */ 0x4A, 0x4C, 0x43, 0x27, 0x20, /* 'y' 0x79 */ 0x8A, 0x52, 0xA5, 0x18, 0x84, 0x22, 0x00, /* 'z' 0x7A */ 0x78, 0x44, 0x46, 0x23, 0xE0, /* '{' 0x7B */ 0x6A, 0xAA, 0xA9, /* '|' 0x7C */ 0xFF, 0xE0, /* '}' 0x7D */ 0x95, 0x55, 0x56, /* '~' 0x7E */ 0x66, 0x60, /* 0x7F */ /* 0x80 */ 0x1C, 0x45, 0x07, 0xE4, 0x1F, 0x10, 0x10, 0x1E, /* 0x81 */ /* 0x82 */ 0xE0, /* 0x83 */ 0x6B, 0xA4, 0x92, 0x49, 0x60, /* 0x84 */ 0xB6, 0x80, /* 0x85 */ 0xA8, /* 0x86 */ 0x21, 0x09, 0xF2, 0x10, 0x84, 0x21, 0x08, /* 0x87 */ 0x21, 0x09, 0xF2, 0x10, 0x84, 0xF9, 0x08, /* 0x88 */ 0x54, /* 0x89 */ 0x62, 0x09, 0x40, 0x98, 0x06, 0x80, 0x10, 0x01, 0x66, 0x29, 0x92, 0x99, 0x06, 0x60, /* 0x8A */ 0x28, 0x47, 0xA1, 0x83, 0x07, 0x83, 0x87, 0x17, 0x80, /* 0x8B */ 0x64, /* 0x8C */ 0x3B, 0xE8, 0xC2, 0x08, 0x41, 0x08, 0x3F, 0x04, 0x20, 0x82, 0x30, 0x3B, 0xE0, /* 0x8D */ /* 0x8E */ 0x14, 0x11, 0xF8, 0x30, 0xC1, 0x04, 0x18, 0x61, 0xFC, /* 0x8F */ /* 0x90 */ /* 0x91 */ 0xE0, /* 0x92 */ 0xE0, /* 0x93 */ 0xB6, 0x80, /* 0x94 */ 0xB6, 0x80, /* 0x95 */ 0xFF, 0x80, /* 0x96 */ 0xFC, /* 0x97 */ 0xFF, 0xF0, /* 0x98 */ 0xDB, /* 0x99 */ 0xE6, 0x28, 0xCD, 0x19, 0xA3, 0x34, 0x6A, 0x8B, 0x51, 0x68, /* 0x9A */ 0x52, 0x69, 0x8E, 0x19, 0x60, /* 0x9B */ 0x98, /* 0x9C */ 0x7B, 0xD9, 0xCE, 0x10, 0xC3, 0xF8, 0x41, 0x9C, 0x5E, 0xF0, /* 0x9D */ /* 0x9E */ 0x51, 0x1E, 0x11, 0x11, 0x88, 0xF8, /* 0x9F */ 0x29, 0x05, 0x12, 0x22, 0x87, 0x04, 0x08, 0x10, 0x20, /* 0xA0 */ /* 0xA1 */ 0xBF, 0x80, /* 0xA2 */ 0x23, 0xAB, 0x4A, 0x52, 0xAE, 0x20, /* 0xA3 */ 0x39, 0x14, 0x10, 0xF0, 0x82, 0x1C, 0x4C, /* 0xA4 */ 0xFC, 0x63, 0xF0, /* 0xA5 */ 0x8C, 0x54, 0xAF, 0x93, 0xE4, 0x20, /* 0xA6 */ 0xF9, 0xF0, /* 0xA7 */ 0x32, 0x91, 0xC9, 0x47, 0x26, 0x14, 0xA4, 0xC0, /* 0xA8 */ 0xA0, /* 0xA9 */ 0x3E, 0x3F, 0xB8, 0xF4, 0x1A, 0x0D, 0x17, 0x76, 0xC6, 0x3E, 0x00, /* 0xAA */ 0x61, 0x79, 0x60, /* 0xAB */ 0x5A, 0xA5, /* 0xAC */ 0xFC, 0x10, 0x40, /* 0xAD */ /* 0xAE */ 0x3E, 0x31, 0xB7, 0x72, 0x99, 0xCC, 0xC7, 0x56, 0xC6, 0x3E, 0x00, /* 0xAF */ 0xE0, /* 0xB0 */ 0x69, 0x96, /* 0xB1 */ 0x21, 0x3E, 0x42, 0x03, 0xE0, /* 0xB2 */ 0x69, 0x3C, 0xF0, /* 0xB3 */ 0x79, 0x29, 0x70, /* 0xB4 */ 0x80, /* 0xB5 */ 0x8A, 0x28, 0xA2, 0x8A, 0x6E, 0xE0, 0x80, /* 0xB6 */ 0x7F, 0xAE, 0xBA, 0x68, 0xA2, 0x8A, 0x28, 0xA0, /* 0xB7 */ 0x80, /* 0xB8 */ 0x67, 0x80, /* 0xB9 */ 0x75, 0x50, /* 0xBA */ 0x69, 0x96, 0xF0, /* 0xBB */ 0xA5, 0x5A, /* 0xBC */ 0x42, 0x30, 0x84, 0x41, 0x10, 0x48, 0x82, 0x61, 0x28, 0x8F, 0x20, 0x80, /* 0xBD */ 0x40, 0x63, 0x11, 0x09, 0x74, 0xA8, 0x84, 0x44, 0x44, 0x43, 0x80, /* 0xBE */ 0x71, 0x24, 0x82, 0x20, 0x50, 0x98, 0x9A, 0x61, 0x28, 0x4F, 0x20, 0x80, /* 0xBF */ 0x20, 0x08, 0x44, 0x42, 0x11, 0x70, /* 0xC0 */ 0x10, 0x08, 0x00, 0x18, 0x3C, 0x24, 0x24, 0x7E, 0x42, 0xC3, /* 0xC1 */ 0x08, 0x10, 0x00, 0x18, 0x3C, 0x24, 0x24, 0x7E, 0x42, 0xC3, /* 0xC2 */ 0x18, 0x24, 0x00, 0x18, 0x3C, 0x24, 0x24, 0x7E, 0x42, 0xC3, /* 0xC3 */ 0x34, 0x2C, 0x00, 0x18, 0x3C, 0x24, 0x24, 0x7E, 0x42, 0xC3, /* 0xC4 */ 0x24, 0x00, 0x18, 0x3C, 0x24, 0x24, 0x7E, 0x42, 0x42, 0xC3, /* 0xC5 */ 0x18, 0x24, 0x18, 0x18, 0x3C, 0x24, 0x24, 0x7E, 0x42, 0xC3, /* 0xC6 */ 0x1F, 0xC5, 0x02, 0x40, 0x90, 0x47, 0xDF, 0x04, 0x42, 0x10, 0x87, 0xC0, /* 0xC7 */ 0x3E, 0x61, 0xC0, 0x80, 0x80, 0x80, 0xC1, 0x63, 0x3E, 0x0C, 0x04, 0x1C, /* 0xC8 */ 0x20, 0x40, 0x3F, 0x82, 0x0F, 0xA0, 0x83, 0xF0, /* 0xC9 */ 0x08, 0x40, 0x3F, 0x82, 0x0F, 0xA0, 0x83, 0xF0, /* 0xCA */ 0x10, 0xA0, 0x3F, 0x82, 0x0F, 0xA0, 0x83, 0xF0, /* 0xCB */ 0x28, 0x0F, 0xE0, 0x83, 0xE8, 0x20, 0x83, 0xF0, /* 0xCC */ 0x91, 0x55, 0x50, /* 0xCD */ 0x62, 0xAA, 0xA0, /* 0xCE */ 0x54, 0x24, 0x92, 0x48, /* 0xCF */ 0xA1, 0x24, 0x92, 0x48, /* 0xD0 */ 0x7C, 0x42, 0x41, 0x41, 0xF1, 0x41, 0x41, 0x42, 0x7C, /* 0xD1 */ 0x14, 0x53, 0x0F, 0x1B, 0x32, 0x66, 0xC7, 0x87, 0x04, /* 0xD2 */ 0x10, 0x04, 0x0F, 0x8C, 0x6C, 0x1C, 0x06, 0x03, 0x83, 0x63, 0x1F, 0x00, /* 0xD3 */ 0x04, 0x04, 0x0F, 0x8C, 0x6C, 0x1C, 0x06, 0x03, 0x83, 0x63, 0x1F, 0x00, /* 0xD4 */ 0x08, 0x0A, 0x00, 0x07, 0xC6, 0x36, 0x0E, 0x03, 0x01, 0xC1, 0xB1, 0x8F, 0x80, /* 0xD5 */ 0x1A, 0x0B, 0x00, 0x07, 0xC6, 0x36, 0x0E, 0x03, 0x01, 0xC1, 0xB1, 0x8F, 0x80, /* 0xD6 */ 0x14, 0x00, 0x00, 0x07, 0xC6, 0x36, 0x0E, 0x03, 0x01, 0xC1, 0xB1, 0x8F, 0x80, /* 0xD7 */ 0x8A, 0x88, 0xA8, 0x80, /* 0xD8 */ 0x3E, 0xB1, 0xB0, 0xF0, 0x98, 0x8C, 0x87, 0x86, 0xC6, 0xBE, 0x00, /* 0xD9 */ 0x20, 0x22, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0xC6, 0xF8, /* 0xDA */ 0x08, 0x22, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0xC6, 0xF8, /* 0xDB */ 0x10, 0x52, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0xC6, 0xF8, /* 0xDC */ 0x29, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0xC6, 0xF8, /* 0xDD */ 0x09, 0x25, 0x12, 0x22, 0x87, 0x04, 0x08, 0x10, 0x20, /* 0xDE */ 0x83, 0xE8, 0x61, 0x87, 0xE8, 0x20, 0x80, /* 0xDF */ 0x7A, 0x18, 0x61, 0x8A, 0x18, 0x61, 0xB8, /* 0xE0 */ 0x20, 0x20, 0x03, 0xC8, 0x40, 0x8F, 0x62, 0x8C, 0xEC, /* 0xE1 */ 0x10, 0x40, 0x03, 0xC8, 0x40, 0x8F, 0x62, 0x8C, 0xEC, /* 0xE2 */ 0x10, 0x50, 0x03, 0xC8, 0x40, 0x8F, 0x62, 0x8C, 0xEC, /* 0xE3 */ 0x68, 0xB0, 0x03, 0xC8, 0x40, 0x8F, 0x62, 0x8C, 0xEC, /* 0xE4 */ 0x28, 0x01, 0xE4, 0x20, 0x47, 0xB1, 0x46, 0x76, /* 0xE5 */ 0x10, 0x50, 0x43, 0xC8, 0x40, 0x8F, 0x62, 0x8C, 0xEC, /* 0xE6 */ 0x7B, 0xA1, 0x90, 0x45, 0xFF, 0x84, 0x23, 0x17, 0x38, /* 0xE7 */ 0x7B, 0x18, 0x20, 0x83, 0x17, 0x8C, 0x11, 0xC0, /* 0xE8 */ 0x20, 0x40, 0x1E, 0xCE, 0x1F, 0xE0, 0xC5, 0xE0, /* 0xE9 */ 0x10, 0x80, 0x1E, 0xCE, 0x1F, 0xE0, 0xC5, 0xE0, /* 0xEA */ 0x10, 0xA0, 0x1E, 0xCE, 0x1F, 0xE0, 0xC5, 0xE0, /* 0xEB */ 0x28, 0x07, 0xB3, 0x87, 0xF8, 0x31, 0x78, /* 0xEC */ 0x91, 0x55, 0x50, /* 0xED */ 0x62, 0xAA, 0xA0, /* 0xEE */ 0x54, 0x24, 0x92, 0x48, /* 0xEF */ 0xA1, 0x24, 0x92, 0x40, /* 0xF0 */ 0x28, 0x42, 0x8F, 0x46, 0x18, 0x52, 0x30, /* 0xF1 */ 0x6A, 0xC1, 0x6C, 0xC6, 0x31, 0x8C, 0x40, /* 0xF2 */ 0x20, 0x40, 0x1E, 0xCE, 0x18, 0x61, 0xCD, 0xE0, /* 0xF3 */ 0x10, 0x80, 0x1E, 0xCE, 0x18, 0x61, 0xCD, 0xE0, /* 0xF4 */ 0x10, 0xA0, 0x1E, 0xCE, 0x18, 0x61, 0xCD, 0xE0, /* 0xF5 */ 0x69, 0x60, 0x1E, 0xCE, 0x18, 0x61, 0xCD, 0xE0, /* 0xF6 */ 0x28, 0x07, 0xB3, 0x86, 0x18, 0x73, 0x78, /* 0xF7 */ 0x20, 0x3E, 0x02, 0x00, /* 0xF8 */ 0x7F, 0x39, 0x69, 0xC7, 0x3F, 0x80, /* 0xF9 */ 0x41, 0x23, 0x18, 0xC6, 0x33, 0x68, /* 0xFA */ 0x11, 0x23, 0x18, 0xC6, 0x33, 0x68, /* 0xFB */ 0x22, 0x81, 0x18, 0xC6, 0x31, 0x9B, 0x40, /* 0xFC */ 0x50, 0x23, 0x18, 0xC6, 0x33, 0x68, /* 0xFD */ 0x10, 0x88, 0x52, 0x49, 0x23, 0x0C, 0x30, 0x82, 0x18, /* 0xFE */ 0x84, 0x3D, 0xB8, 0xC6, 0x3B, 0xF4, 0x20, /* 0xFF */ 0x28, 0x08, 0x52, 0x49, 0x23, 0x0C, 0x30, 0x82, 0x18, }; const GFXglyph FreeSans6pt_Win1252Glyphs[] PROGMEM = { /* 0x01 */ { 0, 9, 10, 11, 1, -9 }, /* 0x02 */ { 12, 9, 10, 11, 1, -8 }, /* 0x03 */ { 24, 10, 10, 12, 1, -8 }, /* 0x04 */ { 37, 10, 10, 12, 1, -8 }, /* 0x05 */ { 50, 10, 10, 12, 1, -9 }, /* 0x06 */ { 63, 11, 11, 13, 1, -9 }, /* 0x07 */ { 79, 0, 0, 8, 0, 0 }, /* 0x08 */ { 79, 12, 9, 14, 1, -8 }, /* 0x09 */ { 93, 14, 8, 16, 1, -7 }, /* 0x0A */ { 107, 0, 0, 8, 0, 0 }, /* 0x0B */ { 107, 9, 10, 11, 1, -9 }, /* 0x0C */ { 119, 13, 9, 15, 1, -8 }, /* 0x0D */ { 134, 0, 0, 8, 0, 0 }, /* 0x0E */ { 134, 9, 11, 11, 1, -9 }, /* 0x0F */ { 147, 10, 10, 12, 1, -9 }, /* 0x10 */ { 160, 11, 10, 13, 1, -9 }, /* 0x11 */ { 174, 13, 10, 15, 1, -9 }, /* 0x12 */ { 191, 10, 10, 12, 1, -9 }, /* 0x13 */ { 204, 11, 10, 13, 1, -9 }, /* 0x14 */ { 218, 10, 10, 12, 1, -9 }, /* 0x15 */ { 231, 14, 10, 16, 1, -9 }, /* 0x16 */ { 249, 8, 10, 10, 1, -9 }, /* 0x17 */ { 259, 12, 10, 14, 1, -9 }, /* 0x18 */ { 274, 13, 10, 15, 1, -9 }, /* 0x19 */ { 291, 12, 10, 14, 1, -9 }, /* 0x1A */ { 306, 9, 10, 11, 1, -8 }, /* 0x1B */ { 318, 14, 10, 16, 1, -9 }, /* 0x1C */ { 336, 11, 10, 13, 1, -9 }, /* 0x1D */ { 350, 11, 10, 13, 1, -9 }, /* 0x1E */ { 364, 12, 10, 14, 1, -9 }, /* 0x1F */ { 379, 8, 10, 11, 2, -9 }, /* ' ' 0x20 */ { 389, 0, 0, 3, 0, 0 }, /* '!' 0x21 */ { 389, 1, 9, 4, 2, -8 }, /* '"' 0x22 */ { 391, 3, 3, 4, 0, -8 }, /* '#' 0x23 */ { 393, 7, 8, 7, 0, -7 }, /* '$' 0x24 */ { 400, 6, 11, 7, 0, -9 }, /* '%' 0x25 */ { 409, 10, 9, 11, 0, -8 }, /* '&' 0x26 */ { 421, 6, 9, 8, 1, -8 }, /* ''' 0x27 */ { 428, 1, 3, 2, 1, -8 }, /* '(' 0x28 */ { 429, 2, 11, 4, 1, -8 }, /* ')' 0x29 */ { 432, 3, 11, 4, 0, -8 }, /* '*' 0x2A */ { 437, 3, 3, 5, 1, -8 }, /* '+' 0x2B */ { 439, 5, 5, 7, 1, -4 }, /* ',' 0x2C */ { 443, 1, 3, 3, 1, 0 }, /* '-' 0x2D */ { 444, 2, 1, 4, 1, -3 }, /* '.' 0x2E */ { 445, 1, 1, 3, 1, 0 }, /* '/' 0x2F */ { 446, 3, 9, 3, 0, -8 }, /* '0' 0x30 */ { 450, 5, 9, 7, 1, -8 }, /* '1' 0x31 */ { 456, 3, 9, 7, 1, -8 }, /* '2' 0x32 */ { 460, 6, 9, 7, 0, -8 }, /* '3' 0x33 */ { 467, 6, 9, 7, 0, -8 }, /* '4' 0x34 */ { 474, 6, 9, 7, 0, -8 }, /* '5' 0x35 */ { 481, 5, 9, 7, 1, -8 }, /* '6' 0x36 */ { 487, 5, 9, 7, 1, -8 }, /* '7' 0x37 */ { 493, 5, 9, 7, 1, -8 }, /* '8' 0x38 */ { 499, 6, 9, 7, 0, -8 }, /* '9' 0x39 */ { 506, 6, 9, 7, 0, -8 }, /* ':' 0x3A */ { 513, 1, 7, 3, 1, -6 }, /* ';' 0x3B */ { 514, 1, 8, 3, 1, -5 }, /* '<' 0x3C */ { 515, 5, 5, 7, 1, -4 }, /* '=' 0x3D */ { 519, 5, 3, 7, 1, -3 }, /* '>' 0x3E */ { 521, 5, 5, 7, 1, -4 }, /* '?' 0x3F */ { 525, 5, 9, 7, 1, -8 }, /* '@' 0x40 */ { 531, 11, 11, 12, 0, -8 }, /* 'A' 0x41 */ { 547, 8, 9, 8, 0, -8 }, /* 'B' 0x42 */ { 556, 6, 9, 8, 1, -8 }, /* 'C' 0x43 */ { 563, 8, 9, 9, 0, -8 }, /* 'D' 0x44 */ { 572, 7, 9, 8, 1, -8 }, /* 'E' 0x45 */ { 580, 6, 9, 8, 1, -8 }, /* 'F' 0x46 */ { 587, 6, 9, 7, 1, -8 }, /* 'G' 0x47 */ { 594, 8, 9, 9, 0, -8 }, /* 'H' 0x48 */ { 603, 7, 9, 9, 1, -8 }, /* 'I' 0x49 */ { 611, 1, 9, 3, 1, -8 }, /* 'J' 0x4A */ { 613, 5, 9, 6, 0, -8 }, /* 'K' 0x4B */ { 619, 7, 9, 8, 1, -8 }, /* 'L' 0x4C */ { 627, 5, 9, 7, 1, -8 }, /* 'M' 0x4D */ { 633, 8, 9, 10, 1, -8 }, /* 'N' 0x4E */ { 642, 7, 9, 9, 1, -8 }, /* 'O' 0x4F */ { 650, 9, 9, 9, 0, -8 }, /* 'P' 0x50 */ { 661, 6, 9, 8, 1, -8 }, /* 'Q' 0x51 */ { 668, 9, 10, 9, 0, -8 }, /* 'R' 0x52 */ { 680, 7, 9, 9, 1, -8 }, /* 'S' 0x53 */ { 688, 6, 9, 8, 1, -8 }, /* 'T' 0x54 */ { 695, 7, 9, 8, 0, -8 }, /* 'U' 0x55 */ { 703, 7, 9, 9, 1, -8 }, /* 'V' 0x56 */ { 711, 7, 9, 8, 0, -8 }, /* 'W' 0x57 */ { 719, 11, 9, 11, 0, -8 }, /* 'X' 0x58 */ { 732, 6, 9, 8, 1, -8 }, /* 'Y' 0x59 */ { 739, 8, 9, 8, 0, -8 }, /* 'Z' 0x5A */ { 748, 7, 9, 7, 0, -8 }, /* '[' 0x5B */ { 756, 2, 12, 3, 1, -8 }, /* '\' 0x5C */ { 759, 3, 9, 3, 0, -8 }, /* ']' 0x5D */ { 763, 2, 12, 3, 0, -8 }, /* '^' 0x5E */ { 766, 4, 4, 6, 1, -8 }, /* '_' 0x5F */ { 768, 7, 1, 7, 0, 2 }, /* '`' 0x60 */ { 769, 1, 1, 3, 1, -8 }, /* 'a' 0x61 */ { 770, 6, 7, 7, 0, -6 }, /* 'b' 0x62 */ { 776, 5, 9, 7, 1, -8 }, /* 'c' 0x63 */ { 782, 6, 7, 6, 0, -6 }, /* 'd' 0x64 */ { 788, 6, 9, 7, 0, -8 }, /* 'e' 0x65 */ { 795, 6, 7, 6, 0, -6 }, /* 'f' 0x66 */ { 801, 3, 9, 3, 0, -8 }, /* 'g' 0x67 */ { 805, 6, 10, 7, 0, -6 }, /* 'h' 0x68 */ { 813, 5, 9, 6, 1, -8 }, /* 'i' 0x69 */ { 819, 1, 9, 3, 1, -8 }, /* 'j' 0x6A */ { 821, 2, 12, 3, 0, -8 }, /* 'k' 0x6B */ { 824, 5, 9, 6, 1, -8 }, /* 'l' 0x6C */ { 830, 1, 9, 3, 1, -8 }, /* 'm' 0x6D */ { 832, 8, 7, 10, 1, -6 }, /* 'n' 0x6E */ { 839, 5, 7, 6, 1, -6 }, /* 'o' 0x6F */ { 844, 6, 7, 6, 0, -6 }, /* 'p' 0x70 */ { 850, 5, 9, 7, 1, -6 }, /* 'q' 0x71 */ { 856, 6, 9, 7, 0, -6 }, /* 'r' 0x72 */ { 863, 3, 7, 4, 1, -6 }, /* 's' 0x73 */ { 866, 5, 7, 6, 0, -6 }, /* 't' 0x74 */ { 871, 3, 8, 3, 0, -7 }, /* 'u' 0x75 */ { 874, 5, 7, 6, 1, -6 }, /* 'v' 0x76 */ { 879, 6, 7, 6, 0, -6 }, /* 'w' 0x77 */ { 885, 8, 7, 9, 0, -6 }, /* 'x' 0x78 */ { 892, 5, 7, 6, 0, -6 }, /* 'y' 0x79 */ { 897, 5, 10, 6, 0, -6 }, /* 'z' 0x7A */ { 904, 5, 7, 6, 0, -6 }, /* '{' 0x7B */ { 909, 2, 12, 4, 1, -8 }, /* '|' 0x7C */ { 912, 1, 11, 3, 1, -8 }, /* '}' 0x7D */ { 914, 2, 12, 4, 1, -8 }, /* '~' 0x7E */ { 917, 6, 2, 6, 0, -4 }, /* 0x7F */ { 919, 0, 0, 0, 0, 0 }, /* 0x80 */ { 919, 7, 9, 8, 0, -8 }, /* 0x81 */ { 927, 0, 0, 8, 0, 0 }, /* 0x82 */ { 927, 1, 3, 3, 1, 0 }, /* 0x83 */ { 928, 3, 12, 3, 0, -8 }, /* 0x84 */ { 933, 3, 3, 5, 1, 0 }, /* 0x85 */ { 935, 5, 1, 7, 1, 0 }, /* 0x86 */ { 936, 5, 11, 7, 1, -8 }, /* 0x87 */ { 943, 5, 11, 7, 1, -8 }, /* 0x88 */ { 950, 3, 2, 4, 0, -9 }, /* 0x89 */ { 951, 12, 9, 12, 0, -8 }, /* 0x8A */ { 965, 6, 11, 8, 1, -9 }, /* 0x8B */ { 974, 2, 3, 4, 1, -4 }, /* 0x8C */ { 975, 11, 9, 12, 0, -8 }, /* 0x8D */ { 988, 0, 0, 8, 0, 0 }, /* 0x8E */ { 988, 7, 10, 7, 0, -9 }, /* 0x8F */ { 997, 0, 0, 8, 0, 0 }, /* 0x90 */ { 997, 0, 0, 8, 0, 0 }, /* 0x91 */ { 997, 1, 3, 3, 1, -8 }, /* 0x92 */ { 998, 1, 3, 2, 1, -8 }, /* 0x93 */ { 999, 3, 3, 5, 1, -8 }, /* 0x94 */ { 1001, 3, 3, 5, 1, -8 }, /* 0x95 */ { 1003, 3, 3, 5, 1, -5 }, /* 0x96 */ { 1005, 6, 1, 6, 0, -3 }, /* 0x97 */ { 1006, 12, 1, 12, 0, -3 }, /* 0x98 */ { 1008, 4, 2, 4, 0, -8 }, /* 0x99 */ { 1009, 11, 7, 12, 1, -8 }, /* 0x9A */ { 1019, 4, 9, 6, 1, -8 }, /* 0x9B */ { 1024, 2, 3, 3, 1, -4 }, /* 0x9C */ { 1025, 11, 7, 11, 0, -6 }, /* 0x9D */ { 1035, 0, 0, 8, 0, 0 }, /* 0x9E */ { 1035, 5, 9, 6, 0, -8 }, /* 0x9F */ { 1041, 7, 10, 8, 1, -9 }, /* 0xA0 */ { 1050, 0, 0, 3, 0, 0 }, /* 0xA1 */ { 1050, 1, 9, 4, 1, -5 }, /* 0xA2 */ { 1052, 5, 9, 7, 1, -7 }, /* 0xA3 */ { 1058, 6, 9, 7, 0, -8 }, /* 0xA4 */ { 1065, 5, 4, 7, 1, -5 }, /* 0xA5 */ { 1068, 5, 9, 7, 1, -8 }, /* 0xA6 */ { 1074, 1, 12, 3, 1, -8 }, /* 0xA7 */ { 1076, 5, 12, 7, 1, -8 }, /* 0xA8 */ { 1084, 3, 1, 4, 0, -7 }, /* 0xA9 */ { 1085, 9, 9, 10, 0, -8 }, /* 0xAA */ { 1096, 4, 5, 4, 0, -8 }, /* 0xAB */ { 1099, 4, 4, 6, 1, -4 }, /* 0xAC */ { 1101, 6, 3, 7, 1, -4 }, /* 0xAD */ { 1104, 0, 0, 0, 0, 0 }, /* 0xAE */ { 1104, 9, 9, 10, 0, -8 }, /* 0xAF */ { 1115, 3, 1, 4, 0, -8 }, /* 0xB0 */ { 1116, 4, 4, 7, 2, -8 }, /* 0xB1 */ { 1118, 5, 7, 7, 1, -6 }, /* 0xB2 */ { 1123, 4, 5, 4, 0, -9 }, /* 0xB3 */ { 1126, 4, 5, 4, 0, -9 }, /* 0xB4 */ { 1129, 1, 1, 4, 1, -8 }, /* 0xB5 */ { 1130, 6, 9, 7, 1, -6 }, /* 0xB6 */ { 1137, 6, 10, 6, 1, -8 }, /* 0xB7 */ { 1145, 1, 1, 3, 1, -2 }, /* 0xB8 */ { 1146, 3, 3, 4, 1, 1 }, /* 0xB9 */ { 1148, 2, 6, 4, 1, -9 }, /* 0xBA */ { 1150, 4, 5, 4, 0, -8 }, /* 0xBB */ { 1153, 4, 4, 6, 1, -5 }, /* 0xBC */ { 1155, 10, 9, 10, 1, -8 }, /* 0xBD */ { 1167, 9, 9, 10, 1, -8 }, /* 0xBE */ { 1178, 10, 9, 11, 0, -8 }, /* 0xBF */ { 1190, 5, 9, 7, 1, -5 }, /* 0xC0 */ { 1196, 8, 10, 8, 0, -9 }, /* 0xC1 */ { 1206, 8, 10, 8, 0, -9 }, /* 0xC2 */ { 1216, 8, 10, 8, 0, -9 }, /* 0xC3 */ { 1226, 8, 10, 8, 0, -9 }, /* 0xC4 */ { 1236, 8, 10, 8, 0, -9 }, /* 0xC5 */ { 1246, 8, 10, 8, 0, -9 }, /* 0xC6 */ { 1256, 10, 9, 12, 1, -8 }, /* 0xC7 */ { 1268, 8, 12, 9, 0, -8 }, /* 0xC8 */ { 1280, 6, 10, 8, 1, -9 }, /* 0xC9 */ { 1288, 6, 10, 8, 1, -9 }, /* 0xCA */ { 1296, 6, 10, 8, 1, -9 }, /* 0xCB */ { 1304, 6, 10, 8, 1, -9 }, /* 0xCC */ { 1312, 2, 10, 3, 0, -9 }, /* 0xCD */ { 1315, 2, 10, 3, 1, -9 }, /* 0xCE */ { 1318, 3, 10, 4, 0, -9 }, /* 0xCF */ { 1322, 3, 10, 4, 0, -9 }, /* 0xD0 */ { 1326, 8, 9, 8, 0, -8 }, /* 0xD1 */ { 1335, 7, 10, 9, 1, -9 }, /* 0xD2 */ { 1344, 9, 10, 9, 0, -9 }, /* 0xD3 */ { 1356, 9, 10, 9, 0, -9 }, /* 0xD4 */ { 1368, 9, 11, 9, 0, -10 }, /* 0xD5 */ { 1381, 9, 11, 9, 0, -10 }, /* 0xD6 */ { 1394, 9, 11, 9, 0, -10 }, /* 0xD7 */ { 1407, 5, 5, 7, 1, -5 }, /* 0xD8 */ { 1411, 9, 9, 9, 0, -8 }, /* 0xD9 */ { 1422, 7, 10, 9, 1, -9 }, /* 0xDA */ { 1431, 7, 10, 9, 1, -9 }, /* 0xDB */ { 1440, 7, 10, 9, 1, -9 }, /* 0xDC */ { 1449, 7, 10, 9, 1, -9 }, /* 0xDD */ { 1458, 7, 10, 8, 1, -9 }, /* 0xDE */ { 1467, 6, 9, 8, 1, -8 }, /* 0xDF */ { 1474, 6, 9, 7, 1, -8 }, /* 0xE0 */ { 1481, 7, 10, 7, 0, -9 }, /* 0xE1 */ { 1490, 7, 10, 7, 0, -9 }, /* 0xE2 */ { 1499, 7, 10, 7, 0, -9 }, /* 0xE3 */ { 1508, 7, 10, 7, 0, -9 }, /* 0xE4 */ { 1517, 7, 9, 7, 0, -8 }, /* 0xE5 */ { 1525, 7, 10, 7, 0, -9 }, /* 0xE6 */ { 1534, 10, 7, 10, 0, -6 }, /* 0xE7 */ { 1543, 6, 10, 6, 0, -6 }, /* 0xE8 */ { 1551, 6, 10, 6, 0, -9 }, /* 0xE9 */ { 1559, 6, 10, 6, 0, -9 }, /* 0xEA */ { 1567, 6, 10, 6, 0, -9 }, /* 0xEB */ { 1575, 6, 9, 6, 0, -8 }, /* 0xEC */ { 1582, 2, 10, 3, 0, -9 }, /* 0xED */ { 1585, 2, 10, 3, 1, -9 }, /* 0xEE */ { 1588, 3, 10, 3, 0, -9 }, /* 0xEF */ { 1592, 3, 9, 3, 0, -8 }, /* 0xF0 */ { 1596, 6, 9, 6, 0, -8 }, /* 0xF1 */ { 1603, 5, 10, 6, 1, -9 }, /* 0xF2 */ { 1610, 6, 10, 6, 0, -9 }, /* 0xF3 */ { 1618, 6, 10, 6, 0, -9 }, /* 0xF4 */ { 1626, 6, 10, 6, 0, -9 }, /* 0xF5 */ { 1634, 6, 10, 6, 0, -9 }, /* 0xF6 */ { 1642, 6, 9, 6, 0, -8 }, /* 0xF7 */ { 1649, 5, 5, 7, 1, -5 }, /* 0xF8 */ { 1653, 6, 7, 6, 0, -6 }, /* 0xF9 */ { 1659, 5, 9, 6, 1, -8 }, /* 0xFA */ { 1665, 5, 9, 6, 1, -8 }, /* 0xFB */ { 1671, 5, 10, 6, 1, -9 }, /* 0xFC */ { 1678, 5, 9, 6, 1, -8 }, /* 0xFD */ { 1684, 6, 12, 6, 0, -8 }, /* 0xFE */ { 1693, 5, 11, 7, 1, -8 }, /* 0xFF */ { 1700, 6, 12, 6, 0, -8 }, }; const GFXfont FreeSans6pt_Win1252 PROGMEM = { (uint8_t*)FreeSans6pt_Win1252Bitmaps, (GFXglyph*)FreeSans6pt_Win1252Glyphs, 0x01, 0xFF, 10 }; ================================================ FILE: src/graphics/niche/Fonts/FreeSans6pt_Win1253.h ================================================ // trunk-ignore-all(clang-format) #pragma once /* PROPERTIES FONT_NAME FreeSans6pt_Win1253 */ const uint8_t FreeSans6pt_Win1253Bitmaps[] PROGMEM = { /* 0x01 */ 0x1C, 0x0A, 0x05, 0x04, 0xFE, 0x08, 0x1C, 0x02, 0x07, 0xE0, 0x9F, 0xC0, /* 0x02 */ 0x3F, 0xF0, 0x40, 0xE0, 0x10, 0x3F, 0x04, 0x9E, 0x28, 0x14, 0x0E, 0x00, /* 0x03 */ 0x3F, 0x10, 0x28, 0x06, 0x49, 0x80, 0x60, 0x19, 0x26, 0x31, 0x40, 0x8F, 0xC0, /* 0x04 */ 0x3F, 0x10, 0x2A, 0x16, 0x49, 0xA1, 0x60, 0x19, 0xE6, 0x31, 0x40, 0x8F, 0xC0, /* 0x05 */ 0x28, 0x15, 0x2A, 0xB5, 0x55, 0xA8, 0x54, 0x12, 0x04, 0x41, 0x08, 0x81, 0xC0, /* 0x06 */ 0x04, 0x08, 0x88, 0x82, 0x07, 0x01, 0x11, 0xA2, 0xC4, 0x40, 0x70, 0x20, 0x88, 0x88, 0x10, 0x00, /* 0x07 */ /* 0x08 */ 0x03, 0x83, 0x44, 0x48, 0x28, 0x01, 0x80, 0x17, 0xFE, 0x08, 0x45, 0x28, 0x84, 0x00, /* 0x09 */ 0x01, 0xC0, 0x68, 0x82, 0x41, 0x10, 0x02, 0x80, 0x06, 0x00, 0x14, 0x00, 0x8F, 0xFC, /* 0x0A */ /* 0x0B */ 0x22, 0x2A, 0xA2, 0x30, 0x18, 0x0A, 0x09, 0x04, 0x44, 0x14, 0x04, 0x00, /* 0x0C */ 0x46, 0x00, 0x19, 0x03, 0x21, 0x20, 0x93, 0x04, 0x20, 0x11, 0x80, 0x50, 0x02, 0x7F, 0xE0, /* 0x0D */ /* 0x0E */ 0x08, 0x0E, 0x08, 0x88, 0x24, 0x12, 0x09, 0x05, 0x01, 0xFF, 0x8A, 0x02, 0x00, /* 0x0F */ 0x3F, 0x14, 0xAA, 0x16, 0x01, 0x92, 0x60, 0x18, 0xC6, 0x49, 0x40, 0x8F, 0xC0, /* 0x10 */ 0x1B, 0x02, 0xA0, 0x54, 0x12, 0x42, 0x48, 0x49, 0x31, 0x1E, 0x23, 0xEA, 0xFE, 0x3C, /* 0x11 */ 0x3F, 0x02, 0x00, 0x20, 0x6D, 0x27, 0xF8, 0x3F, 0xC1, 0xFE, 0x37, 0xD0, 0xBE, 0x40, 0xE1, 0xE2, 0x00, /* 0x12 */ 0x12, 0x42, 0x20, 0x24, 0xC0, 0x29, 0x99, 0x05, 0x23, 0x30, 0xB0, 0x30, 0x00, /* 0x13 */ 0x3F, 0x88, 0x0A, 0x44, 0xD5, 0x58, 0x03, 0x00, 0x67, 0xCC, 0x71, 0x40, 0x47, 0xF0, /* 0x14 */ 0x3F, 0x18, 0x69, 0x26, 0x85, 0xA1, 0x6C, 0xD8, 0x06, 0x31, 0x40, 0x8F, 0xC0, /* 0x15 */ 0x3F, 0x11, 0x00, 0xE8, 0x03, 0xA0, 0x1F, 0xB3, 0x7E, 0x00, 0xE9, 0xE0, 0x23, 0x00, 0x40, 0x40, 0xFE, 0x00, /* 0x16 */ 0x30, 0x38, 0x3A, 0x3E, 0x6E, 0xEB, 0xC3, 0xC3, 0x66, 0x3C, /* 0x17 */ 0x3F, 0x04, 0x00, 0x82, 0x88, 0x5C, 0xA4, 0x49, 0x22, 0x81, 0x98, 0xC4, 0x40, 0xA3, 0xF0, /* 0x18 */ 0x07, 0x80, 0x42, 0x04, 0x08, 0x21, 0x41, 0x42, 0x60, 0x0E, 0x8C, 0xB2, 0x89, 0x50, 0x52, 0x82, 0x80, /* 0x19 */ 0x3F, 0xC4, 0x02, 0x80, 0x18, 0x01, 0xB3, 0x1B, 0xB9, 0x80, 0x19, 0xE1, 0x40, 0x23, 0xFC, /* 0x1A */ 0xFF, 0xC0, 0x67, 0x34, 0x58, 0x4C, 0x46, 0x03, 0x11, 0x80, 0xFF, 0xC0, /* 0x1B */ 0x0F, 0xC0, 0x40, 0x82, 0x49, 0x08, 0x04, 0x00, 0x00, 0x12, 0x02, 0x31, 0x34, 0x0B, 0x88, 0x45, 0x00, 0x20, /* 0x1C */ 0x3F, 0x88, 0x0A, 0x44, 0xC9, 0x19, 0x3B, 0x00, 0x60, 0x4C, 0x71, 0x40, 0x47, 0xF0, /* 0x1D */ 0x3F, 0x8B, 0x0A, 0x00, 0xC8, 0x18, 0x13, 0x00, 0x48, 0xCA, 0xC1, 0x44, 0x53, 0x30, /* 0x1E */ 0x19, 0xC2, 0x02, 0x50, 0x1E, 0x49, 0x80, 0x12, 0x01, 0x27, 0x92, 0x01, 0x10, 0x20, 0xFC, /* 0x1F */ 0x30, 0x1C, 0x0C, 0x3E, 0x7E, 0xCF, 0x07, 0xC7, 0x7F, 0x3F, /* ' ' 0x20 */ /* '!' 0x21 */ 0xFC, 0x80, /* '"' 0x22 */ 0xB6, 0x80, /* '#' 0x23 */ 0x24, 0x51, 0xF9, 0x42, 0x9F, 0x92, 0x28, /* '$' 0x24 */ 0x10, 0xE5, 0x55, 0x50, 0xE1, 0x65, 0x55, 0xE1, 0x00, /* '%' 0x25 */ 0x71, 0x24, 0x89, 0x22, 0x50, 0x74, 0x02, 0x70, 0xA4, 0x49, 0x11, 0xC0, /* '&' 0x26 */ 0x71, 0x24, 0x9C, 0x62, 0x58, 0xA7, 0xF4, /* ''' 0x27 */ 0xE0, /* '(' 0x28 */ 0x5A, 0xAA, 0x94, /* ')' 0x29 */ 0x89, 0x12, 0x49, 0x29, 0x00, /* '*' 0x2A */ 0x5E, 0x80, /* '+' 0x2B */ 0x21, 0x3E, 0x42, 0x00, /* ',' 0x2C */ 0xE0, /* '-' 0x2D */ 0xC0, /* '.' 0x2E */ 0x80, /* '/' 0x2F */ 0x24, 0xA4, 0xA4, 0x80, /* '0' 0x30 */ 0x76, 0xE3, 0x18, 0xC6, 0x3B, 0x70, /* '1' 0x31 */ 0x27, 0x92, 0x49, 0x20, /* '2' 0x32 */ 0x79, 0x10, 0x41, 0x08, 0xC6, 0x10, 0xFC, /* '3' 0x33 */ 0x79, 0x30, 0x43, 0x18, 0x10, 0x71, 0x78, /* '4' 0x34 */ 0x08, 0x61, 0x8A, 0x49, 0x2F, 0xC2, 0x08, /* '5' 0x35 */ 0xFC, 0x21, 0xE8, 0x84, 0x31, 0xF0, /* '6' 0x36 */ 0x74, 0x61, 0xE8, 0xC6, 0x31, 0x70, /* '7' 0x37 */ 0xF8, 0x44, 0x22, 0x11, 0x08, 0x40, /* '8' 0x38 */ 0x39, 0x34, 0x53, 0x39, 0x1C, 0x51, 0x38, /* '9' 0x39 */ 0x39, 0x3C, 0x71, 0x4C, 0xF0, 0x53, 0x78, /* ':' 0x3A */ 0x82, /* ';' 0x3B */ 0x87, /* '<' 0x3C */ 0x3E, 0x30, 0x60, 0x80, /* '=' 0x3D */ 0xF8, 0x3E, /* '>' 0x3E */ 0xE0, 0xC6, 0xC8, 0x00, /* '?' 0x3F */ 0x74, 0x42, 0x11, 0x10, 0x80, 0x20, /* '@' 0x40 */ 0x0F, 0x86, 0x19, 0x9A, 0xA4, 0xD9, 0x13, 0x22, 0x56, 0xDA, 0x6E, 0x60, 0x06, 0x00, 0x3C, 0x00, /* 'A' 0x41 */ 0x18, 0x18, 0x24, 0x24, 0x24, 0x7E, 0x42, 0x42, 0xC3, /* 'B' 0x42 */ 0xFA, 0x18, 0x61, 0xFA, 0x18, 0x61, 0xFC, /* 'C' 0x43 */ 0x3E, 0x63, 0x40, 0x40, 0xC0, 0x40, 0x41, 0x63, 0x3E, /* 'D' 0x44 */ 0xF9, 0x0A, 0x1C, 0x18, 0x30, 0x61, 0xC2, 0xF8, /* 'E' 0x45 */ 0xFE, 0x08, 0x20, 0xFE, 0x08, 0x20, 0xFC, /* 'F' 0x46 */ 0xFE, 0x08, 0x20, 0xFA, 0x08, 0x20, 0x80, /* 'G' 0x47 */ 0x1E, 0x61, 0x40, 0x40, 0xC7, 0x41, 0x41, 0x63, 0x1D, /* 'H' 0x48 */ 0x83, 0x06, 0x0C, 0x1F, 0xF0, 0x60, 0xC1, 0x82, /* 'I' 0x49 */ 0xFF, 0x80, /* 'J' 0x4A */ 0x08, 0x42, 0x10, 0x87, 0x29, 0x70, /* 'K' 0x4B */ 0x85, 0x12, 0x45, 0x0D, 0x13, 0x22, 0x42, 0x86, /* 'L' 0x4C */ 0x84, 0x21, 0x08, 0x42, 0x10, 0xF8, /* 'M' 0x4D */ 0xC3, 0xC3, 0xC3, 0xA5, 0xA5, 0xA5, 0x99, 0x99, 0x99, /* 'N' 0x4E */ 0x83, 0x86, 0x8D, 0x19, 0x33, 0x62, 0xC3, 0x86, /* 'O' 0x4F */ 0x1E, 0x31, 0x90, 0x68, 0x1C, 0x0A, 0x05, 0x06, 0xC6, 0x1E, 0x00, /* 'P' 0x50 */ 0xFA, 0x18, 0x61, 0xFA, 0x08, 0x20, 0x80, /* 'Q' 0x51 */ 0x1E, 0x31, 0x90, 0x68, 0x1C, 0x0A, 0x05, 0x16, 0xC6, 0x1F, 0x00, 0x40, /* 'R' 0x52 */ 0xFD, 0x0E, 0x1C, 0x2F, 0x90, 0xA1, 0x42, 0x86, /* 'S' 0x53 */ 0x7A, 0x18, 0x30, 0x78, 0x38, 0x61, 0x78, /* 'T' 0x54 */ 0xFE, 0x20, 0x40, 0x81, 0x02, 0x04, 0x08, 0x10, /* 'U' 0x55 */ 0x83, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xE2, 0x78, /* 'V' 0x56 */ 0xC2, 0x85, 0x0B, 0x22, 0x44, 0x8E, 0x0C, 0x18, /* 'W' 0x57 */ 0xC4, 0x28, 0xCD, 0x29, 0x25, 0x24, 0xA4, 0x52, 0x8C, 0x61, 0x8C, 0x31, 0x80, /* 'X' 0x58 */ 0x87, 0x34, 0x8C, 0x30, 0xC4, 0xA3, 0x84, /* 'Y' 0x59 */ 0xC3, 0x42, 0x24, 0x34, 0x18, 0x08, 0x08, 0x08, 0x08, /* 'Z' 0x5A */ 0x7E, 0x0C, 0x30, 0x41, 0x06, 0x18, 0x20, 0xFE, /* '[' 0x5B */ 0xEA, 0xAA, 0xAB, /* '\' 0x5C */ 0x92, 0x24, 0x89, 0x20, /* ']' 0x5D */ 0xD5, 0x55, 0x57, /* '^' 0x5E */ 0x46, 0xA9, /* '_' 0x5F */ 0xFE, /* '`' 0x60 */ 0x80, /* 'a' 0x61 */ 0x79, 0x20, 0x4F, 0xC6, 0x37, 0x40, /* 'b' 0x62 */ 0x84, 0x3D, 0x18, 0xC6, 0x31, 0xF0, /* 'c' 0x63 */ 0x39, 0x3C, 0x20, 0xC1, 0x33, 0x80, /* 'd' 0x64 */ 0x04, 0x13, 0xD3, 0xC6, 0x1C, 0x53, 0x3C, /* 'e' 0x65 */ 0x39, 0x38, 0x7F, 0x81, 0x13, 0x80, /* 'f' 0x66 */ 0x6B, 0xA4, 0x92, 0x40, /* 'g' 0x67 */ 0x35, 0x3C, 0x61, 0xC5, 0x33, 0x41, 0x4D, 0xE0, /* 'h' 0x68 */ 0x84, 0x3D, 0x38, 0xC6, 0x31, 0x88, /* 'i' 0x69 */ 0xBF, 0x80, /* 'j' 0x6A */ 0x45, 0x55, 0x57, /* 'k' 0x6B */ 0x84, 0x25, 0x4E, 0x52, 0xD2, 0x88, /* 'l' 0x6C */ 0xFF, 0x80, /* 'm' 0x6D */ 0xF7, 0x99, 0x91, 0x91, 0x91, 0x91, 0x91, /* 'n' 0x6E */ 0xF4, 0x63, 0x18, 0xC6, 0x20, /* 'o' 0x6F */ 0x39, 0x3C, 0x61, 0xC5, 0x33, 0x80, /* 'p' 0x70 */ 0xF4, 0x63, 0x18, 0xC7, 0xD0, 0x80, /* 'q' 0x71 */ 0x3D, 0x3C, 0x61, 0xC5, 0x37, 0x41, 0x04, /* 'r' 0x72 */ 0xF2, 0x49, 0x20, /* 's' 0x73 */ 0x7A, 0x50, 0xE0, 0xE5, 0xE0, /* 't' 0x74 */ 0x5D, 0x24, 0x93, /* 'u' 0x75 */ 0x8C, 0x63, 0x18, 0xCF, 0xA0, /* 'v' 0x76 */ 0x85, 0x24, 0x92, 0x30, 0xC3, 0x00, /* 'w' 0x77 */ 0x89, 0x59, 0x59, 0x55, 0x56, 0x26, 0x26, /* 'x' 0x78 */ 0x4A, 0x4C, 0x43, 0x27, 0x20, /* 'y' 0x79 */ 0x8A, 0x52, 0xA5, 0x18, 0x84, 0x22, 0x00, /* 'z' 0x7A */ 0x78, 0x44, 0x46, 0x23, 0xE0, /* '{' 0x7B */ 0x6A, 0xAA, 0xA9, /* '|' 0x7C */ 0xFF, 0xE0, /* '}' 0x7D */ 0x95, 0x55, 0x56, /* '~' 0x7E */ 0x66, 0x60, /* 0x7F */ /* 0x80 */ 0x1C, 0x45, 0x07, 0xE4, 0x1F, 0x10, 0x10, 0x1E, /* 0x81 */ /* 0x82 */ 0xE0, /* 0x83 */ 0x6B, 0xA4, 0x92, 0x49, 0x60, /* 0x84 */ 0xB6, 0x80, /* 0x85 */ 0xA8, /* 0x86 */ 0x21, 0x09, 0xF2, 0x10, 0x84, 0x21, 0x08, /* 0x87 */ 0x21, 0x09, 0xF2, 0x10, 0x84, 0xF9, 0x08, /* 0x88 */ 0x54, /* 0x89 */ 0x62, 0x09, 0x40, 0x98, 0x06, 0x80, 0x10, 0x01, 0x66, 0x29, 0x92, 0x99, 0x06, 0x60, /* 0x8A */ 0x28, 0x47, 0xA1, 0x83, 0x07, 0x83, 0x87, 0x17, 0x80, /* 0x8B */ 0x64, /* 0x8C */ 0x3B, 0xE8, 0xC2, 0x08, 0x41, 0x08, 0x3F, 0x04, 0x20, 0x82, 0x30, 0x3B, 0xE0, /* 0x8D */ /* 0x8E */ 0x14, 0x11, 0xF8, 0x30, 0xC1, 0x04, 0x18, 0x61, 0xFC, /* 0x8F */ /* 0x90 */ /* 0x91 */ 0xE0, /* 0x92 */ 0xE0, /* 0x93 */ 0xB6, 0x80, /* 0x94 */ 0xB6, 0x80, /* 0x95 */ 0xFF, 0x80, /* 0x96 */ 0xFC, /* 0x97 */ 0xFF, 0xF0, /* 0x98 */ 0xDB, /* 0x99 */ 0xE6, 0x28, 0xCD, 0x19, 0xA3, 0x34, 0x6A, 0x8B, 0x51, 0x68, /* 0x9A */ 0x52, 0x69, 0x8E, 0x19, 0x60, /* 0x9B */ 0x98, /* 0x9C */ 0x7B, 0xD9, 0xCE, 0x10, 0xC3, 0xF8, 0x41, 0x9C, 0x5E, 0xF0, /* 0x9D */ /* 0x9E */ 0x51, 0x1E, 0x11, 0x11, 0x88, 0xF8, /* 0x9F */ 0x29, 0x05, 0x12, 0x22, 0x87, 0x04, 0x08, 0x10, 0x20, /* 0xA0 */ /* 0xA1 */ 0xBF, 0x80, /* 0xA2 */ 0x23, 0xAB, 0x4A, 0x52, 0xAE, 0x20, /* 0xA3 */ 0x39, 0x14, 0x10, 0xF0, 0x82, 0x1C, 0x4C, /* 0xA4 */ 0xFC, 0x63, 0xF0, /* 0xA5 */ 0x8C, 0x54, 0xAF, 0x93, 0xE4, 0x20, /* 0xA6 */ 0xF9, 0xF0, /* 0xA7 */ 0x32, 0x91, 0xC9, 0x47, 0x26, 0x14, 0xA4, 0xC0, /* 0xA8 */ 0xA0, /* 0xA9 */ 0x3E, 0x3F, 0xB8, 0xF4, 0x1A, 0x0D, 0x17, 0x76, 0xC6, 0x3E, 0x00, /* 0xAA */ 0x61, 0x79, 0x60, /* 0xAB */ 0x5A, 0xA5, /* 0xAC */ 0xFC, 0x10, 0x40, /* 0xAD */ /* 0xAE */ 0x3E, 0x31, 0xB7, 0x72, 0x99, 0xCC, 0xC7, 0x56, 0xC6, 0x3E, 0x00, /* 0xAF */ 0xE0, /* 0xB0 */ 0x69, 0x96, /* 0xB1 */ 0x21, 0x3E, 0x42, 0x03, 0xE0, /* 0xB2 */ 0x69, 0x3C, 0xF0, /* 0xB3 */ 0x79, 0x29, 0x70, /* 0xB4 */ 0x80, /* 0xB5 */ 0x8A, 0x28, 0xA2, 0x8A, 0x6E, 0xE0, 0x80, /* 0xB6 */ 0x7F, 0xAE, 0xBA, 0x68, 0xA2, 0x8A, 0x28, 0xA0, /* 0xB7 */ 0x80, /* 0xB8 */ 0x67, 0x80, /* 0xB9 */ 0x75, 0x50, /* 0xBA */ 0x69, 0x96, 0xF0, /* 0xBB */ 0xA5, 0x5A, /* 0xBC */ 0x42, 0x30, 0x84, 0x41, 0x10, 0x48, 0x82, 0x61, 0x28, 0x8F, 0x20, 0x80, /* 0xBD */ 0x40, 0x63, 0x11, 0x09, 0x74, 0xA8, 0x84, 0x44, 0x44, 0x43, 0x80, /* 0xBE */ 0x71, 0x24, 0x82, 0x20, 0x50, 0x98, 0x9A, 0x61, 0x28, 0x4F, 0x20, 0x80, /* 0xBF */ 0x20, 0x08, 0x44, 0x42, 0x11, 0x70, /* 0xC0 */ 0x2D, 0x02, 0x22, 0x22, 0x22, /* 0xC1 */ 0x10, 0x50, 0xA1, 0x44, 0x4F, 0x91, 0x41, 0x82, /* 0xC2 */ 0xFA, 0x18, 0x61, 0xFE, 0x18, 0x61, 0xF8, /* 0xC3 */ 0xFE, 0x08, 0x20, 0x82, 0x08, 0x20, 0x80, /* 0xC4 */ 0x08, 0x0A, 0x05, 0x02, 0x82, 0x21, 0x11, 0x04, 0x82, 0x7F, 0x00, /* 0xC5 */ 0xFE, 0x08, 0x20, 0xFE, 0x08, 0x20, 0xFC, /* 0xC6 */ 0x7E, 0x08, 0x20, 0x41, 0x04, 0x08, 0x20, 0xFE, /* 0xC7 */ 0x83, 0x06, 0x0C, 0x1F, 0xF0, 0x60, 0xC1, 0x82, /* 0xC8 */ 0x38, 0x8A, 0x0C, 0x1B, 0xB0, 0x60, 0xA2, 0x38, /* 0xC9 */ 0xFF, 0x80, /* 0xCA */ 0x83, 0x0A, 0x24, 0x8A, 0x1A, 0x22, 0x42, 0x82, /* 0xCB */ 0x08, 0x0A, 0x05, 0x02, 0x82, 0x21, 0x11, 0x04, 0x82, 0x41, 0x00, /* 0xCC */ 0x83, 0x8F, 0x1D, 0x5A, 0xB5, 0x6A, 0xC9, 0x92, /* 0xCD */ 0x83, 0x86, 0x8D, 0x19, 0x31, 0x62, 0xC3, 0x82, /* 0xCE */ 0xFC, 0x00, 0x00, 0x78, 0x00, 0x00, 0xFC, /* 0xCF */ 0x38, 0x8A, 0x0C, 0x18, 0x30, 0x60, 0xA2, 0x38, /* 0xD0 */ 0xFF, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x82, /* 0xD1 */ 0xFA, 0x18, 0x61, 0xFA, 0x08, 0x20, 0x80, /* 0xD2 */ /* 0xD3 */ 0xFE, 0x04, 0x08, 0x10, 0x84, 0x20, 0xFC, /* 0xD4 */ 0xFE, 0x20, 0x40, 0x81, 0x02, 0x04, 0x08, 0x10, /* 0xD5 */ 0x82, 0x89, 0x11, 0x41, 0x02, 0x04, 0x08, 0x10, /* 0xD6 */ 0x10, 0xFA, 0x4C, 0x99, 0x32, 0x64, 0xBE, 0x10, /* 0xD7 */ 0x82, 0x89, 0x11, 0x41, 0x05, 0x11, 0x22, 0x82, /* 0xD8 */ 0x93, 0x26, 0x4C, 0x99, 0x2F, 0x84, 0x08, 0x10, /* 0xD9 */ 0x38, 0x8A, 0x0C, 0x18, 0x30, 0x60, 0xA2, 0xEE, /* 0xDA */ 0xA1, 0x24, 0x92, 0x49, 0x00, /* 0xDB */ 0x28, 0x02, 0x0A, 0x24, 0x45, 0x04, 0x08, 0x10, 0x20, 0x40, /* 0xDC */ 0x11, 0x00, 0xD9, 0x4A, 0x52, 0x93, 0x40, /* 0xDD */ 0x11, 0x00, 0xF8, 0x41, 0x90, 0x83, 0xC0, /* 0xDE */ 0x11, 0x01, 0x6C, 0xC6, 0x31, 0x8C, 0x42, 0x10, /* 0xDF */ 0x62, 0xAA, 0xA0, /* 0xE0 */ 0x25, 0x81, 0x18, 0xC6, 0x31, 0x8B, 0x80, /* 0xE1 */ 0x6C, 0xA5, 0x29, 0x49, 0xA0, /* 0xE2 */ 0x74, 0x63, 0x1B, 0x46, 0x39, 0xB4, 0x20, /* 0xE3 */ 0x44, 0x89, 0x11, 0x42, 0x85, 0x04, 0x08, 0x10, /* 0xE4 */ 0x71, 0x1D, 0x18, 0xC6, 0x31, 0x70, /* 0xE5 */ 0x7C, 0x20, 0xC8, 0x41, 0xE0, /* 0xE6 */ 0x72, 0x44, 0x88, 0x88, 0x71, 0x20, /* 0xE7 */ 0xB6, 0x63, 0x18, 0xC6, 0x21, 0x08, /* 0xE8 */ 0x74, 0x63, 0x1F, 0xC6, 0x31, 0x70, /* 0xE9 */ 0xFE, /* 0xEA */ 0x8A, 0x4A, 0x38, 0x92, 0x48, 0x80, /* 0xEB */ 0x20, 0x41, 0x04, 0x28, 0xA2, 0x91, 0x44, /* 0xEC */ 0x8C, 0x63, 0x18, 0xC7, 0xF0, 0x80, /* 0xED */ 0x8C, 0x54, 0xA5, 0x10, 0x80, /* 0xEE */ 0x68, 0x86, 0x48, 0x88, 0x71, 0x20, /* 0xEF */ 0x74, 0x63, 0x18, 0xC5, 0xC0, /* 0xF0 */ 0xFF, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, /* 0xF1 */ 0x74, 0x63, 0x18, 0xC7, 0xD0, 0x80, /* 0xF2 */ 0x34, 0x88, 0x88, 0x71, 0x60, /* 0xF3 */ 0x7F, 0x12, 0x24, 0x48, 0x91, 0x1C, 0x00, /* 0xF4 */ 0xE9, 0x24, 0x90, /* 0xF5 */ 0x8C, 0x63, 0x18, 0xC5, 0xC0, /* 0xF6 */ 0x5A, 0x59, 0x65, 0x95, 0x53, 0x84, 0x10, /* 0xF7 */ 0x49, 0x24, 0x8C, 0x30, 0xC4, 0x92, 0x48, /* 0xF8 */ 0x93, 0x26, 0x4C, 0x99, 0x32, 0x5F, 0x08, 0x10, /* 0xF9 */ 0x45, 0x06, 0x4C, 0x99, 0x32, 0x5B, 0x00, /* 0xFA */ 0xA1, 0x24, 0x92, 0x40, /* 0xFB */ 0x50, 0x23, 0x18, 0xC6, 0x31, 0x70, /* 0xFC */ 0x11, 0x00, 0xE8, 0xC6, 0x31, 0x8B, 0x80, /* 0xFD */ 0x21, 0x01, 0x18, 0xC6, 0x31, 0x8B, 0x80, /* 0xFE */ 0x08, 0x20, 0x02, 0x28, 0x32, 0x64, 0xC9, 0x92, 0xD8, /* 0xFF */ }; const GFXglyph FreeSans6pt_Win1253Glyphs[] PROGMEM = { /* 0x01 */ { 0, 9, 10, 11, 1, -9 }, /* 0x02 */ { 12, 9, 10, 11, 1, -8 }, /* 0x03 */ { 24, 10, 10, 12, 1, -8 }, /* 0x04 */ { 37, 10, 10, 12, 1, -8 }, /* 0x05 */ { 50, 10, 10, 12, 1, -9 }, /* 0x06 */ { 63, 11, 11, 13, 1, -9 }, /* 0x07 */ { 79, 0, 0, 8, 0, 0 }, /* 0x08 */ { 79, 12, 9, 14, 1, -8 }, /* 0x09 */ { 93, 14, 8, 16, 1, -7 }, /* 0x0A */ { 107, 0, 0, 8, 0, 0 }, /* 0x0B */ { 107, 9, 10, 11, 1, -9 }, /* 0x0C */ { 119, 13, 9, 15, 1, -8 }, /* 0x0D */ { 134, 0, 0, 8, 0, 0 }, /* 0x0E */ { 134, 9, 11, 11, 1, -9 }, /* 0x0F */ { 147, 10, 10, 12, 1, -9 }, /* 0x10 */ { 160, 11, 10, 13, 1, -9 }, /* 0x11 */ { 174, 13, 10, 15, 1, -9 }, /* 0x12 */ { 191, 10, 10, 12, 1, -9 }, /* 0x13 */ { 204, 11, 10, 13, 1, -9 }, /* 0x14 */ { 218, 10, 10, 12, 1, -9 }, /* 0x15 */ { 231, 14, 10, 16, 1, -9 }, /* 0x16 */ { 249, 8, 10, 10, 1, -9 }, /* 0x17 */ { 259, 12, 10, 14, 1, -9 }, /* 0x18 */ { 274, 13, 10, 15, 1, -9 }, /* 0x19 */ { 291, 12, 10, 14, 1, -9 }, /* 0x1A */ { 306, 9, 10, 11, 1, -8 }, /* 0x1B */ { 318, 14, 10, 16, 1, -9 }, /* 0x1C */ { 336, 11, 10, 13, 1, -9 }, /* 0x1D */ { 350, 11, 10, 13, 1, -9 }, /* 0x1E */ { 364, 12, 10, 14, 1, -9 }, /* 0x1F */ { 379, 8, 10, 11, 2, -9 }, /* ' ' 0x20 */ { 389, 0, 0, 3, 0, 0 }, /* '!' 0x21 */ { 389, 1, 9, 4, 2, -8 }, /* '"' 0x22 */ { 391, 3, 3, 4, 0, -8 }, /* '#' 0x23 */ { 393, 7, 8, 7, 0, -7 }, /* '$' 0x24 */ { 400, 6, 11, 7, 0, -9 }, /* '%' 0x25 */ { 409, 10, 9, 11, 0, -8 }, /* '&' 0x26 */ { 421, 6, 9, 8, 1, -8 }, /* ''' 0x27 */ { 428, 1, 3, 2, 1, -8 }, /* '(' 0x28 */ { 429, 2, 11, 4, 1, -8 }, /* ')' 0x29 */ { 432, 3, 11, 4, 0, -8 }, /* '*' 0x2A */ { 437, 3, 3, 5, 1, -8 }, /* '+' 0x2B */ { 439, 5, 5, 7, 1, -4 }, /* ',' 0x2C */ { 443, 1, 3, 3, 1, 0 }, /* '-' 0x2D */ { 444, 2, 1, 4, 1, -3 }, /* '.' 0x2E */ { 445, 1, 1, 3, 1, 0 }, /* '/' 0x2F */ { 446, 3, 9, 3, 0, -8 }, /* '0' 0x30 */ { 450, 5, 9, 7, 1, -8 }, /* '1' 0x31 */ { 456, 3, 9, 7, 1, -8 }, /* '2' 0x32 */ { 460, 6, 9, 7, 0, -8 }, /* '3' 0x33 */ { 467, 6, 9, 7, 0, -8 }, /* '4' 0x34 */ { 474, 6, 9, 7, 0, -8 }, /* '5' 0x35 */ { 481, 5, 9, 7, 1, -8 }, /* '6' 0x36 */ { 487, 5, 9, 7, 1, -8 }, /* '7' 0x37 */ { 493, 5, 9, 7, 1, -8 }, /* '8' 0x38 */ { 499, 6, 9, 7, 0, -8 }, /* '9' 0x39 */ { 506, 6, 9, 7, 0, -8 }, /* ':' 0x3A */ { 513, 1, 7, 3, 1, -6 }, /* ';' 0x3B */ { 514, 1, 8, 3, 1, -5 }, /* '<' 0x3C */ { 515, 5, 5, 7, 1, -4 }, /* '=' 0x3D */ { 519, 5, 3, 7, 1, -3 }, /* '>' 0x3E */ { 521, 5, 5, 7, 1, -4 }, /* '?' 0x3F */ { 525, 5, 9, 7, 1, -8 }, /* '@' 0x40 */ { 531, 11, 11, 12, 0, -8 }, /* 'A' 0x41 */ { 547, 8, 9, 8, 0, -8 }, /* 'B' 0x42 */ { 556, 6, 9, 8, 1, -8 }, /* 'C' 0x43 */ { 563, 8, 9, 9, 0, -8 }, /* 'D' 0x44 */ { 572, 7, 9, 8, 1, -8 }, /* 'E' 0x45 */ { 580, 6, 9, 8, 1, -8 }, /* 'F' 0x46 */ { 587, 6, 9, 7, 1, -8 }, /* 'G' 0x47 */ { 594, 8, 9, 9, 0, -8 }, /* 'H' 0x48 */ { 603, 7, 9, 9, 1, -8 }, /* 'I' 0x49 */ { 611, 1, 9, 3, 1, -8 }, /* 'J' 0x4A */ { 613, 5, 9, 6, 0, -8 }, /* 'K' 0x4B */ { 619, 7, 9, 8, 1, -8 }, /* 'L' 0x4C */ { 627, 5, 9, 7, 1, -8 }, /* 'M' 0x4D */ { 633, 8, 9, 10, 1, -8 }, /* 'N' 0x4E */ { 642, 7, 9, 9, 1, -8 }, /* 'O' 0x4F */ { 650, 9, 9, 9, 0, -8 }, /* 'P' 0x50 */ { 661, 6, 9, 8, 1, -8 }, /* 'Q' 0x51 */ { 668, 9, 10, 9, 0, -8 }, /* 'R' 0x52 */ { 680, 7, 9, 9, 1, -8 }, /* 'S' 0x53 */ { 688, 6, 9, 8, 1, -8 }, /* 'T' 0x54 */ { 695, 7, 9, 8, 0, -8 }, /* 'U' 0x55 */ { 703, 7, 9, 9, 1, -8 }, /* 'V' 0x56 */ { 711, 7, 9, 8, 0, -8 }, /* 'W' 0x57 */ { 719, 11, 9, 11, 0, -8 }, /* 'X' 0x58 */ { 732, 6, 9, 8, 1, -8 }, /* 'Y' 0x59 */ { 739, 8, 9, 8, 0, -8 }, /* 'Z' 0x5A */ { 748, 7, 9, 7, 0, -8 }, /* '[' 0x5B */ { 756, 2, 12, 3, 1, -8 }, /* '\' 0x5C */ { 759, 3, 9, 3, 0, -8 }, /* ']' 0x5D */ { 763, 2, 12, 3, 0, -8 }, /* '^' 0x5E */ { 766, 4, 4, 6, 1, -8 }, /* '_' 0x5F */ { 768, 7, 1, 7, 0, 2 }, /* '`' 0x60 */ { 769, 1, 1, 3, 1, -8 }, /* 'a' 0x61 */ { 770, 6, 7, 7, 0, -6 }, /* 'b' 0x62 */ { 776, 5, 9, 7, 1, -8 }, /* 'c' 0x63 */ { 782, 6, 7, 6, 0, -6 }, /* 'd' 0x64 */ { 788, 6, 9, 7, 0, -8 }, /* 'e' 0x65 */ { 795, 6, 7, 6, 0, -6 }, /* 'f' 0x66 */ { 801, 3, 9, 3, 0, -8 }, /* 'g' 0x67 */ { 805, 6, 10, 7, 0, -6 }, /* 'h' 0x68 */ { 813, 5, 9, 6, 1, -8 }, /* 'i' 0x69 */ { 819, 1, 9, 3, 1, -8 }, /* 'j' 0x6A */ { 821, 2, 12, 3, 0, -8 }, /* 'k' 0x6B */ { 824, 5, 9, 6, 1, -8 }, /* 'l' 0x6C */ { 830, 1, 9, 3, 1, -8 }, /* 'm' 0x6D */ { 832, 8, 7, 10, 1, -6 }, /* 'n' 0x6E */ { 839, 5, 7, 6, 1, -6 }, /* 'o' 0x6F */ { 844, 6, 7, 6, 0, -6 }, /* 'p' 0x70 */ { 850, 5, 9, 7, 1, -6 }, /* 'q' 0x71 */ { 856, 6, 9, 7, 0, -6 }, /* 'r' 0x72 */ { 863, 3, 7, 4, 1, -6 }, /* 's' 0x73 */ { 866, 5, 7, 6, 0, -6 }, /* 't' 0x74 */ { 871, 3, 8, 3, 0, -7 }, /* 'u' 0x75 */ { 874, 5, 7, 6, 1, -6 }, /* 'v' 0x76 */ { 879, 6, 7, 6, 0, -6 }, /* 'w' 0x77 */ { 885, 8, 7, 9, 0, -6 }, /* 'x' 0x78 */ { 892, 5, 7, 6, 0, -6 }, /* 'y' 0x79 */ { 897, 5, 10, 6, 0, -6 }, /* 'z' 0x7A */ { 904, 5, 7, 6, 0, -6 }, /* '{' 0x7B */ { 909, 2, 12, 4, 1, -8 }, /* '|' 0x7C */ { 912, 1, 11, 3, 1, -8 }, /* '}' 0x7D */ { 914, 2, 12, 4, 1, -8 }, /* '~' 0x7E */ { 917, 6, 2, 6, 0, -4 }, /* 0x7F */ { 919, 0, 0, 0, 0, 0 }, /* 0x80 */ { 919, 7, 9, 8, 0, -8 }, /* 0x81 */ { 927, 0, 0, 8, 0, 0 }, /* 0x82 */ { 927, 1, 3, 3, 1, 0 }, /* 0x83 */ { 928, 3, 12, 3, 0, -8 }, /* 0x84 */ { 933, 3, 3, 5, 1, 0 }, /* 0x85 */ { 935, 5, 1, 7, 1, 0 }, /* 0x86 */ { 936, 5, 11, 7, 1, -8 }, /* 0x87 */ { 943, 5, 11, 7, 1, -8 }, /* 0x88 */ { 950, 3, 2, 4, 0, -9 }, /* 0x89 */ { 951, 12, 9, 12, 0, -8 }, /* 0x8A */ { 965, 6, 11, 8, 1, -9 }, /* 0x8B */ { 974, 2, 3, 4, 1, -4 }, /* 0x8C */ { 975, 11, 9, 12, 0, -8 }, /* 0x8D */ { 988, 0, 0, 8, 0, 0 }, /* 0x8E */ { 988, 7, 10, 7, 0, -9 }, /* 0x8F */ { 997, 0, 0, 8, 0, 0 }, /* 0x90 */ { 997, 0, 0, 8, 0, 0 }, /* 0x91 */ { 997, 1, 3, 3, 1, -8 }, /* 0x92 */ { 998, 1, 3, 2, 1, -8 }, /* 0x93 */ { 999, 3, 3, 5, 1, -8 }, /* 0x94 */ { 1001, 3, 3, 5, 1, -8 }, /* 0x95 */ { 1003, 3, 3, 5, 1, -5 }, /* 0x96 */ { 1005, 6, 1, 6, 0, -3 }, /* 0x97 */ { 1006, 12, 1, 12, 0, -3 }, /* 0x98 */ { 1008, 4, 2, 4, 0, -8 }, /* 0x99 */ { 1009, 11, 7, 12, 1, -8 }, /* 0x9A */ { 1019, 4, 9, 6, 1, -8 }, /* 0x9B */ { 1024, 2, 3, 3, 1, -4 }, /* 0x9C */ { 1025, 11, 7, 11, 0, -6 }, /* 0x9D */ { 1035, 0, 0, 8, 0, 0 }, /* 0x9E */ { 1035, 5, 9, 6, 0, -8 }, /* 0x9F */ { 1041, 7, 10, 8, 1, -9 }, /* 0xA0 */ { 1050, 0, 0, 3, 0, 0 }, /* 0xA1 */ { 1050, 1, 9, 4, 1, -5 }, /* 0xA2 */ { 1052, 5, 9, 7, 1, -7 }, /* 0xA3 */ { 1058, 6, 9, 7, 0, -8 }, /* 0xA4 */ { 1065, 5, 4, 7, 1, -5 }, /* 0xA5 */ { 1068, 5, 9, 7, 1, -8 }, /* 0xA6 */ { 1074, 1, 12, 3, 1, -8 }, /* 0xA7 */ { 1076, 5, 12, 7, 1, -8 }, /* 0xA8 */ { 1084, 3, 1, 4, 0, -7 }, /* 0xA9 */ { 1085, 9, 9, 10, 0, -8 }, /* 0xAA */ { 1096, 4, 5, 4, 0, -8 }, /* 0xAB */ { 1099, 4, 4, 6, 1, -4 }, /* 0xAC */ { 1101, 6, 3, 7, 1, -4 }, /* 0xAD */ { 1104, 0, 0, 0, 0, 0 }, /* 0xAE */ { 1104, 9, 9, 10, 0, -8 }, /* 0xAF */ { 1115, 3, 1, 4, 0, -8 }, /* 0xB0 */ { 1116, 4, 4, 7, 2, -8 }, /* 0xB1 */ { 1118, 5, 7, 7, 1, -6 }, /* 0xB2 */ { 1123, 4, 5, 4, 0, -9 }, /* 0xB3 */ { 1126, 4, 5, 4, 0, -9 }, /* 0xB4 */ { 1129, 1, 1, 4, 1, -8 }, /* 0xB5 */ { 1130, 6, 9, 7, 1, -6 }, /* 0xB6 */ { 1137, 6, 10, 6, 1, -8 }, /* 0xB7 */ { 1145, 1, 1, 3, 1, -2 }, /* 0xB8 */ { 1146, 3, 3, 4, 1, 1 }, /* 0xB9 */ { 1148, 2, 6, 4, 1, -9 }, /* 0xBA */ { 1150, 4, 5, 4, 0, -8 }, /* 0xBB */ { 1153, 4, 4, 6, 1, -5 }, /* 0xBC */ { 1155, 10, 9, 10, 1, -8 }, /* 0xBD */ { 1167, 9, 9, 10, 1, -8 }, /* 0xBE */ { 1178, 10, 9, 11, 0, -8 }, /* 0xBF */ { 1190, 5, 9, 7, 1, -5 }, /* 0xC0 */ { 1196, 4, 10, 3, -1, -10 }, /* 0xC1 */ { 1201, 7, 9, 7, 0, -9 }, /* 0xC2 */ { 1209, 6, 9, 8, 1, -9 }, /* 0xC3 */ { 1216, 6, 9, 7, 1, -9 }, /* 0xC4 */ { 1223, 9, 9, 7, -1, -9 }, /* 0xC5 */ { 1234, 6, 9, 8, 1, -9 }, /* 0xC6 */ { 1241, 7, 9, 7, 0, -9 }, /* 0xC7 */ { 1249, 7, 9, 9, 1, -9 }, /* 0xC8 */ { 1257, 7, 9, 9, 1, -9 }, /* 0xC9 */ { 1265, 1, 9, 3, 1, -9 }, /* 0xCA */ { 1267, 7, 9, 8, 1, -9 }, /* 0xCB */ { 1275, 9, 9, 7, -1, -9 }, /* 0xCC */ { 1286, 7, 9, 9, 1, -9 }, /* 0xCD */ { 1294, 7, 9, 9, 1, -9 }, /* 0xCE */ { 1302, 6, 9, 8, 1, -9 }, /* 0xCF */ { 1309, 7, 9, 9, 1, -9 }, /* 0xD0 */ { 1317, 7, 9, 9, 1, -9 }, /* 0xD1 */ { 1325, 6, 9, 8, 1, -9 }, /* 0xD2 */ { 1332, 0, 0, 5, 0, 0 }, /* 0xD3 */ { 1332, 6, 9, 7, 1, -9 }, /* 0xD4 */ { 1339, 7, 9, 7, 0, -9 }, /* 0xD5 */ { 1347, 7, 9, 7, 0, -9 }, /* 0xD6 */ { 1355, 7, 9, 9, 1, -9 }, /* 0xD7 */ { 1363, 7, 9, 7, 0, -9 }, /* 0xD8 */ { 1371, 7, 9, 9, 1, -9 }, /* 0xD9 */ { 1379, 7, 9, 9, 1, -9 }, /* 0xDA */ { 1387, 3, 11, 3, 0, -11 }, /* 0xDB */ { 1392, 7, 11, 7, 0, -11 }, /* 0xDC */ { 1402, 5, 10, 7, 1, -10 }, /* 0xDD */ { 1409, 5, 10, 5, 0, -10 }, /* 0xDE */ { 1416, 5, 12, 7, 1, -10 }, /* 0xDF */ { 1424, 2, 10, 3, 1, -10 }, /* 0xE0 */ { 1427, 5, 10, 7, 1, -10 }, /* 0xE1 */ { 1434, 5, 7, 7, 1, -7 }, /* 0xE2 */ { 1439, 5, 11, 7, 1, -9 }, /* 0xE3 */ { 1446, 7, 9, 5, -1, -7 }, /* 0xE4 */ { 1454, 5, 9, 7, 1, -9 }, /* 0xE5 */ { 1460, 5, 7, 5, 0, -7 }, /* 0xE6 */ { 1465, 4, 11, 5, 1, -9 }, /* 0xE7 */ { 1471, 5, 9, 7, 1, -7 }, /* 0xE8 */ { 1477, 5, 9, 7, 1, -9 }, /* 0xE9 */ { 1483, 1, 7, 3, 1, -7 }, /* 0xEA */ { 1484, 6, 7, 7, 1, -7 }, /* 0xEB */ { 1490, 6, 9, 5, -1, -9 }, /* 0xEC */ { 1497, 5, 9, 7, 1, -7 }, /* 0xED */ { 1503, 5, 7, 5, 0, -7 }, /* 0xEE */ { 1508, 4, 11, 5, 1, -9 }, /* 0xEF */ { 1514, 5, 7, 7, 1, -7 }, /* 0xF0 */ { 1519, 8, 7, 8, 0, -7 }, /* 0xF1 */ { 1526, 5, 9, 7, 1, -7 }, /* 0xF2 */ { 1532, 4, 9, 6, 1, -7 }, /* 0xF3 */ { 1537, 7, 7, 7, 1, -7 }, /* 0xF4 */ { 1544, 3, 7, 5, 1, -7 }, /* 0xF5 */ { 1547, 5, 7, 7, 1, -7 }, /* 0xF6 */ { 1552, 6, 9, 8, 1, -7 }, /* 0xF7 */ { 1559, 6, 9, 6, 0, -7 }, /* 0xF8 */ { 1566, 7, 9, 9, 1, -7 }, /* 0xF9 */ { 1574, 7, 7, 9, 1, -7 }, /* 0xFA */ { 1581, 3, 9, 3, 0, -9 }, /* 0xFB */ { 1585, 5, 9, 7, 1, -9 }, /* 0xFC */ { 1591, 5, 10, 7, 1, -10 }, /* 0xFD */ { 1598, 5, 10, 7, 1, -10 }, /* 0xFE */ { 1605, 7, 10, 9, 1, -10 }, /* 0xFF */ { 1614, 0, 0, 5, 0, 0 }, }; const GFXfont FreeSans6pt_Win1253 PROGMEM = { (uint8_t*)FreeSans6pt_Win1253Bitmaps, (GFXglyph*)FreeSans6pt_Win1253Glyphs, 0x01, 0xFF, 10 }; ================================================ FILE: src/graphics/niche/Fonts/FreeSans9pt_Win1250.h ================================================ // trunk-ignore-all(clang-format) #pragma once /* PROPERTIES FONT_NAME FreeSans9pt_Win1250 */ const uint8_t FreeSans9pt_Win1250Bitmaps[] PROGMEM = { /* 0x01 */ 0x07, 0x00, 0x0A, 0x00, 0x24, 0x00, 0x48, 0x01, 0x10, 0x04, 0x40, 0x10, 0xFF, 0x20, 0x02, 0x81, 0xFD, 0x00, 0x06, 0x07, 0xF4, 0x08, 0x24, 0x0F, 0x88, 0x11, 0x0F, 0xDC, 0x00, /* 0x02 */ 0x3F, 0x70, 0x81, 0x11, 0x03, 0xE4, 0x08, 0x28, 0x1F, 0xD0, 0x00, 0x60, 0x7F, 0x20, 0x02, 0x43, 0xFC, 0x44, 0x00, 0x44, 0x00, 0x48, 0x00, 0x90, 0x00, 0xA0, 0x01, 0xC0, 0x00, /* 0x03 */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x00, 0x28, 0x00, 0x31, 0x8C, 0x63, 0x18, 0xC0, 0x01, 0x80, 0x03, 0x00, 0x06, 0x20, 0x8C, 0x3E, 0x14, 0x00, 0x44, 0x01, 0x06, 0x0C, 0x03, 0xE0, /* 0x04 */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x00, 0x28, 0x82, 0x30, 0x88, 0x62, 0x08, 0xC0, 0x01, 0x80, 0x03, 0x00, 0x06, 0x3F, 0x8C, 0x3E, 0x14, 0x00, 0x44, 0x01, 0x06, 0x0C, 0x03, 0xE0, /* 0x05 */ 0x0B, 0x10, 0x14, 0xA8, 0x12, 0x50, 0x29, 0x42, 0x24, 0xA5, 0x32, 0x95, 0x5A, 0x09, 0x48, 0x09, 0x24, 0x01, 0x10, 0x01, 0x48, 0x02, 0xA4, 0x02, 0x42, 0x04, 0x01, 0x98, 0x00, 0x60, /* 0x06 */ 0x00, 0x80, 0x22, 0x80, 0x65, 0x00, 0xBE, 0xE1, 0x82, 0x4E, 0x03, 0x24, 0x04, 0x28, 0x06, 0x30, 0x12, 0x20, 0x3C, 0xA0, 0xC3, 0xFE, 0x80, 0x4D, 0x00, 0xA6, 0x01, 0x80, 0x00, /* 0x07 */ /* 0x08 */ 0x00, 0xF8, 0x00, 0x82, 0x00, 0x80, 0x83, 0xE0, 0x41, 0x10, 0x21, 0x04, 0x1B, 0x00, 0x03, 0x00, 0x01, 0x80, 0x00, 0xE0, 0x00, 0x4F, 0xE1, 0xC0, 0x0F, 0x02, 0x00, 0x03, 0x01, 0x00, 0x09, 0x88, 0x0C, 0x0C, /* 0x09 */ 0x00, 0xF8, 0x00, 0x82, 0x00, 0x80, 0x83, 0xE0, 0x41, 0x10, 0x21, 0x04, 0x1B, 0x00, 0x03, 0x00, 0x01, 0x80, 0x00, 0xE0, 0x00, 0x4F, 0xE1, 0xC0, 0x0F, 0x00, /* 0x0A */ /* 0x0B */ 0x1C, 0x1C, 0x31, 0xB1, 0x90, 0x50, 0x50, 0x10, 0x18, 0x00, 0x0C, 0x00, 0x06, 0x00, 0x02, 0x80, 0x02, 0x40, 0x01, 0x10, 0x01, 0x04, 0x01, 0x01, 0x01, 0x00, 0x41, 0x00, 0x11, 0x00, 0x07, 0x00, 0x01, 0x00, /* 0x0C */ 0x06, 0x00, 0x0A, 0x00, 0x12, 0x00, 0x32, 0x01, 0x84, 0x04, 0x10, 0x08, 0x98, 0x1C, 0x18, 0x40, 0x48, 0x82, 0x11, 0xF0, 0x74, 0x02, 0x18, 0x70, 0x2F, 0x9F, 0x80, /* 0x0D */ /* 0x0E */ 0x01, 0x00, 0x05, 0x00, 0x0A, 0x00, 0x3E, 0x00, 0x82, 0x02, 0x82, 0x06, 0x04, 0x10, 0x04, 0x20, 0x08, 0x40, 0x10, 0xFF, 0x22, 0x00, 0x29, 0xFF, 0x3F, 0x8F, 0xDF, 0x9F, 0x01, 0xC0, /* 0x0F */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x00, 0x28, 0x82, 0x36, 0x03, 0x60, 0x00, 0xCC, 0x19, 0xA4, 0x4B, 0x00, 0x06, 0x8E, 0x2B, 0x22, 0x66, 0x7C, 0xCC, 0x71, 0x98, 0x03, 0x00, /* 0x10 */ 0x03, 0x80, 0x07, 0x00, 0x0E, 0x00, 0x1E, 0x00, 0x54, 0x00, 0xA8, 0x01, 0x50, 0x02, 0xA0, 0x05, 0x20, 0x32, 0x61, 0xC4, 0x74, 0x49, 0x10, 0x6C, 0x00, 0xD8, 0x01, 0x10, 0x00, /* 0x11 */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x40, 0x29, 0x00, 0x31, 0x84, 0x63, 0x18, 0xC0, 0x00, 0x80, 0x15, 0x03, 0x7E, 0x02, 0xFA, 0x04, 0xE4, 0x18, 0x84, 0x00, 0x06, 0x0C, 0x03, 0xE0, /* 0x12 */ 0x02, 0x08, 0x01, 0x08, 0x40, 0x10, 0xC0, 0x08, 0xC0, 0x60, 0x80, 0x28, 0x04, 0x12, 0x4C, 0x10, 0x80, 0x08, 0x23, 0x0E, 0x08, 0xC4, 0x82, 0x04, 0x20, 0x83, 0x09, 0x82, 0x47, 0x01, 0x1C, 0x01, 0x30, 0x00, 0xE0, 0x00, 0x00, /* 0x13 */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x00, 0x28, 0x00, 0x31, 0x08, 0x65, 0x28, 0xC0, 0x01, 0x80, 0x03, 0x00, 0x06, 0x3F, 0x8C, 0x3E, 0x14, 0x00, 0x44, 0x01, 0x06, 0x0C, 0x03, 0xE0, /* 0x14 */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x22, 0x29, 0x83, 0x30, 0x00, 0x65, 0x14, 0xD3, 0x4D, 0xBA, 0xEB, 0x38, 0xE6, 0x00, 0x0A, 0x00, 0x24, 0x38, 0x44, 0x01, 0x07, 0x1C, 0x01, 0xC0, /* 0x15 */ 0x07, 0xC0, 0x30, 0x18, 0x80, 0x32, 0x00, 0xF8, 0x01, 0xF1, 0x09, 0xA5, 0x28, 0x40, 0x01, 0x80, 0x03, 0x00, 0x06, 0x3F, 0x8C, 0x3E, 0x14, 0x00, 0x44, 0x01, 0x06, 0x0C, 0x03, 0xE0, /* 0x16 */ 0x0C, 0x00, 0xC0, 0x1C, 0x03, 0x80, 0xF8, 0xBB, 0x36, 0xC7, 0x99, 0xF3, 0xFE, 0x3F, 0xC3, 0xF0, 0x7E, 0x0E, 0xC1, 0x8E, 0xE0, 0x20, /* 0x17 */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x00, 0x28, 0x00, 0x10, 0x01, 0x20, 0x1D, 0x44, 0x42, 0x84, 0x85, 0x00, 0x86, 0x00, 0xC4, 0x00, 0x44, 0x7C, 0x44, 0x00, 0x06, 0x0C, 0x03, 0xE0, /* 0x18 */ 0x01, 0xE0, 0x00, 0x84, 0x00, 0x40, 0x80, 0x20, 0x10, 0x08, 0x24, 0x02, 0x41, 0x00, 0x86, 0x03, 0x12, 0x03, 0xB4, 0x03, 0x52, 0x81, 0x23, 0x80, 0x70, 0xA0, 0x14, 0x28, 0x05, 0x0A, 0x01, 0x42, 0x80, 0x50, /* 0x19 */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x00, 0x28, 0x00, 0x33, 0x18, 0x60, 0x00, 0xDC, 0xE1, 0xB9, 0xC3, 0x7B, 0xC6, 0x63, 0x0A, 0x00, 0x24, 0xF0, 0x44, 0x01, 0x06, 0x0C, 0x03, 0xE0, /* 0x1A */ 0xFF, 0xFC, 0x00, 0x63, 0xE3, 0x31, 0x99, 0x04, 0xC8, 0x66, 0x06, 0x30, 0x61, 0x82, 0x0C, 0x10, 0x60, 0x03, 0x04, 0x18, 0x00, 0xFF, 0xFC, /* 0x1B */ 0x07, 0xF0, 0x06, 0x0C, 0x04, 0x01, 0x04, 0x00, 0x44, 0x22, 0x12, 0x2A, 0x89, 0x00, 0x04, 0x80, 0x02, 0x44, 0x11, 0x01, 0xF0, 0x04, 0x01, 0x0D, 0x01, 0x6A, 0x41, 0x2C, 0x00, 0x05, 0xC0, 0x0E, 0x18, 0x18, /* 0x1C */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0xC0, 0x2A, 0x00, 0x33, 0x00, 0x66, 0x00, 0xCC, 0x39, 0x80, 0x83, 0x00, 0x06, 0x00, 0x8C, 0x3E, 0x14, 0x00, 0x44, 0x01, 0x06, 0x0C, 0x03, 0xE0, /* 0x1D */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x70, 0x28, 0x00, 0x31, 0x80, 0x63, 0x18, 0xC0, 0x31, 0x80, 0x03, 0x00, 0x06, 0x60, 0x0D, 0x33, 0x12, 0x10, 0x48, 0x21, 0x23, 0x8C, 0x00, /* 0x1E */ 0x03, 0x00, 0x07, 0x9E, 0x07, 0x00, 0x86, 0x00, 0x27, 0xC0, 0x0F, 0xC0, 0x07, 0x8C, 0x62, 0x06, 0x31, 0x20, 0x00, 0x90, 0x00, 0x48, 0x00, 0x24, 0x3E, 0x11, 0x00, 0x10, 0x40, 0x10, 0x18, 0x30, 0x03, 0xE0, /* 0x1F */ 0x18, 0x02, 0x80, 0x4C, 0x16, 0x41, 0x24, 0x3C, 0x88, 0x6E, 0x65, 0xF2, 0x78, 0x46, 0x88, 0xCF, 0x18, 0x02, 0x80, 0x8C, 0x60, 0x70, /* ' ' 0x20 */ /* '!' 0x21 */ 0xFF, 0xFF, 0xF0, 0xC0, /* '"' 0x22 */ 0xDE, 0xF7, 0x20, /* '#' 0x23 */ 0x09, 0x86, 0x41, 0x91, 0xFF, 0x13, 0x04, 0xC3, 0x20, 0xC8, 0xFF, 0x89, 0x82, 0x61, 0x90, /* '$' 0x24 */ 0x10, 0x1F, 0x14, 0xDA, 0x3D, 0x1E, 0x83, 0x40, 0x78, 0x17, 0x08, 0xF4, 0x7A, 0x35, 0x33, 0xF0, 0x40, 0x20, /* '%' 0x25 */ 0x38, 0x10, 0xEC, 0x20, 0xC6, 0x20, 0xC6, 0x40, 0xC6, 0x40, 0x6C, 0x80, 0x39, 0x00, 0x01, 0x3C, 0x02, 0x77, 0x02, 0x63, 0x04, 0x63, 0x04, 0x77, 0x08, 0x3C, /* '&' 0x26 */ 0x0E, 0x0C, 0xC3, 0x30, 0xCC, 0x1E, 0x03, 0x03, 0xC1, 0x9B, 0xC2, 0xF0, 0xEC, 0x19, 0x8F, 0x3C, 0x40, /* ''' 0x27 */ 0xFE, /* '(' 0x28 */ 0x13, 0x26, 0x6C, 0xCC, 0xCC, 0xC4, 0x66, 0x23, 0x10, /* ')' 0x29 */ 0x8C, 0x46, 0x63, 0x33, 0x33, 0x32, 0x66, 0x4C, 0x80, /* '*' 0x2A */ 0x25, 0x7E, 0xA5, 0x00, /* '+' 0x2B */ 0x30, 0xC3, 0x3F, 0x30, 0xC3, 0x0C, /* ',' 0x2C */ 0xD6, /* '-' 0x2D */ 0xF0, /* '.' 0x2E */ 0xC0, /* '/' 0x2F */ 0x08, 0x44, 0x21, 0x10, 0x84, 0x42, 0x11, 0x08, 0x00, /* '0' 0x30 */ 0x3C, 0x66, 0x42, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x42, 0x66, 0x3C, /* '1' 0x31 */ 0x11, 0x3F, 0x33, 0x33, 0x33, 0x33, 0x30, /* '2' 0x32 */ 0x3E, 0x31, 0xB0, 0x78, 0x30, 0x18, 0x1C, 0x1C, 0x1C, 0x18, 0x18, 0x10, 0x08, 0x07, 0xF8, /* '3' 0x33 */ 0x3C, 0x66, 0xC3, 0xC3, 0x03, 0x06, 0x1C, 0x07, 0x03, 0xC3, 0xC3, 0x66, 0x3C, /* '4' 0x34 */ 0x0C, 0x18, 0x71, 0x62, 0xC9, 0xA3, 0x46, 0xFE, 0x18, 0x30, 0x60, 0xC0, /* '5' 0x35 */ 0x7F, 0x20, 0x10, 0x08, 0x08, 0x07, 0xF3, 0x8C, 0x03, 0x01, 0x80, 0xF0, 0x6C, 0x63, 0xE0, /* '6' 0x36 */ 0x1E, 0x31, 0x98, 0x78, 0x0C, 0x06, 0xF3, 0x8D, 0x83, 0xC1, 0xE0, 0xD0, 0x6C, 0x63, 0xE0, /* '7' 0x37 */ 0xFF, 0x03, 0x02, 0x06, 0x04, 0x0C, 0x08, 0x18, 0x18, 0x18, 0x10, 0x30, 0x30, /* '8' 0x38 */ 0x3E, 0x31, 0xB0, 0x78, 0x3C, 0x1B, 0x18, 0xF8, 0xC6, 0xC1, 0xE0, 0xF0, 0x6C, 0x63, 0xE0, /* '9' 0x39 */ 0x3C, 0x66, 0xC2, 0xC3, 0xC3, 0xC3, 0x67, 0x3B, 0x03, 0x03, 0xC2, 0x66, 0x3C, /* ':' 0x3A */ 0xC0, 0x00, 0x30, /* ';' 0x3B */ 0xC0, 0x00, 0x00, 0x64, 0xA0, /* '<' 0x3C */ 0x00, 0x81, 0xC7, 0x8E, 0x0C, 0x07, 0x80, 0x70, 0x0E, 0x01, 0x80, /* '=' 0x3D */ 0xFF, 0x80, 0x00, 0x1F, 0xF0, /* '>' 0x3E */ 0xE0, 0x1C, 0x03, 0x80, 0x30, 0x70, 0xE3, 0x81, 0x00, /* '?' 0x3F */ 0x3E, 0x31, 0xB0, 0x78, 0x30, 0x18, 0x18, 0x38, 0x18, 0x18, 0x0C, 0x00, 0x00, 0x01, 0x80, /* '@' 0x40 */ 0x03, 0xF0, 0x06, 0x0E, 0x06, 0x01, 0x86, 0x00, 0x66, 0x1D, 0xBB, 0x31, 0xCF, 0x18, 0xC7, 0x98, 0x63, 0xCC, 0x31, 0xE6, 0x11, 0xB3, 0x99, 0xCC, 0xF7, 0x86, 0x00, 0x01, 0x80, 0x00, 0x70, 0x40, 0x0F, 0xE0, /* 'A' 0x41 */ 0x06, 0x00, 0xF0, 0x0F, 0x00, 0x90, 0x19, 0x81, 0x98, 0x10, 0x83, 0x0C, 0x3F, 0xC2, 0x04, 0x60, 0x66, 0x06, 0xC0, 0x30, /* 'B' 0x42 */ 0xFF, 0x18, 0x33, 0x03, 0x60, 0x6C, 0x0D, 0x83, 0x3F, 0xC6, 0x06, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x6F, 0xF8, /* 'C' 0x43 */ 0x1F, 0x86, 0x19, 0x81, 0xA0, 0x3C, 0x01, 0x80, 0x30, 0x06, 0x00, 0xC0, 0x68, 0x0D, 0x83, 0x18, 0x61, 0xF0, /* 'D' 0x44 */ 0xFF, 0x18, 0x33, 0x03, 0x60, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x03, 0x60, 0xCF, 0xF0, /* 'E' 0x45 */ 0xFF, 0xE0, 0x30, 0x18, 0x0C, 0x06, 0x03, 0xFD, 0x80, 0xC0, 0x60, 0x30, 0x18, 0x0F, 0xF8, /* 'F' 0x46 */ 0xFF, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xFE, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, /* 'G' 0x47 */ 0x0F, 0x83, 0x0E, 0x60, 0x66, 0x03, 0xC0, 0x0C, 0x00, 0xC1, 0xFC, 0x03, 0xC0, 0x36, 0x03, 0x60, 0x73, 0x0F, 0x0F, 0x10, /* 'H' 0x48 */ 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xFF, 0xFE, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x06, /* 'I' 0x49 */ 0xFF, 0xFF, 0xFF, 0xC0, /* 'J' 0x4A */ 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x83, 0x07, 0x8F, 0x1E, 0x27, 0x80, /* 'K' 0x4B */ 0xC0, 0xF0, 0x6C, 0x33, 0x18, 0xCC, 0x37, 0x0F, 0xC3, 0x98, 0xC3, 0x30, 0xCC, 0x1B, 0x03, 0xC0, 0xC0, /* 'L' 0x4C */ 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xFF, /* 'M' 0x4D */ 0xE0, 0x3F, 0x01, 0xFC, 0x1F, 0xE0, 0xFD, 0x05, 0xEC, 0x6F, 0x63, 0x79, 0x13, 0xCD, 0x9E, 0x6C, 0xF1, 0x47, 0x8E, 0x3C, 0x71, 0x80, /* 'N' 0x4E */ 0xE0, 0x7C, 0x0F, 0xC1, 0xE8, 0x3D, 0x87, 0x98, 0xF1, 0x1E, 0x33, 0xC3, 0x78, 0x6F, 0x07, 0xE0, 0x7C, 0x0E, /* 'O' 0x4F */ 0x0F, 0x81, 0x83, 0x18, 0x0C, 0xC0, 0x6C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1B, 0x01, 0x98, 0x0C, 0x60, 0xC0, 0xF8, 0x00, /* 'P' 0x50 */ 0xFF, 0x30, 0x6C, 0x0F, 0x03, 0xC0, 0xF0, 0x6F, 0xF3, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x00, /* 'Q' 0x51 */ 0x0F, 0x81, 0x83, 0x18, 0x0C, 0xC0, 0x6C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1B, 0x01, 0x98, 0x6C, 0x60, 0xC0, 0xFB, 0x00, 0x08, /* 'R' 0x52 */ 0xFF, 0x8C, 0x0E, 0xC0, 0x6C, 0x06, 0xC0, 0x6C, 0x0C, 0xFF, 0x8C, 0x0E, 0xC0, 0x6C, 0x06, 0xC0, 0x6C, 0x06, 0xC0, 0x70, /* 'S' 0x53 */ 0x3F, 0x18, 0x6C, 0x0F, 0x03, 0xC0, 0x1E, 0x01, 0xF0, 0x0E, 0x00, 0xF0, 0x3C, 0x0D, 0x86, 0x3F, 0x00, /* 'T' 0x54 */ 0xFF, 0x86, 0x03, 0x01, 0x80, 0xC0, 0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x01, 0x80, 0xC0, /* 'U' 0x55 */ 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xB0, 0x61, 0xF0, /* 'V' 0x56 */ 0xC0, 0x6C, 0x0D, 0x81, 0x10, 0x63, 0x0C, 0x61, 0x04, 0x60, 0xCC, 0x19, 0x01, 0x60, 0x3C, 0x07, 0x00, 0x60, /* 'W' 0x57 */ 0xC1, 0x81, 0x61, 0xC3, 0x61, 0xC3, 0x61, 0x43, 0x62, 0x62, 0x22, 0x66, 0x32, 0x26, 0x36, 0x26, 0x14, 0x34, 0x14, 0x34, 0x1C, 0x1C, 0x18, 0x1C, 0x08, 0x18, /* 'X' 0x58 */ 0xC0, 0xD8, 0x66, 0x18, 0xCC, 0x1E, 0x07, 0x00, 0xC0, 0x78, 0x32, 0x0C, 0xC6, 0x1B, 0x07, 0xC0, 0xC0, /* 'Y' 0x59 */ 0xC0, 0x36, 0x06, 0x30, 0xC3, 0x0C, 0x19, 0x81, 0xD8, 0x0F, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, /* 'Z' 0x5A */ 0xFF, 0xC0, 0x60, 0x30, 0x0C, 0x06, 0x03, 0x01, 0xC0, 0x60, 0x30, 0x18, 0x06, 0x03, 0x00, 0xFF, 0xC0, /* '[' 0x5B */ 0xFB, 0x6D, 0xB6, 0xDB, 0x6D, 0xB6, 0xE0, /* '\' 0x5C */ 0x84, 0x10, 0x84, 0x10, 0x84, 0x10, 0x84, 0x10, 0x80, /* ']' 0x5D */ 0xED, 0xB6, 0xDB, 0x6D, 0xB6, 0xDB, 0xE0, /* '^' 0x5E */ 0x30, 0x60, 0xA2, 0x44, 0xD8, 0xA1, 0x80, /* '_' 0x5F */ 0xFF, 0xC0, /* '`' 0x60 */ 0xC6, 0x30, /* 'a' 0x61 */ 0x7E, 0x71, 0xB0, 0xC0, 0x60, 0xF3, 0xDB, 0x0D, 0x86, 0xC7, 0x3D, 0xC0, /* 'b' 0x62 */ 0xC0, 0x60, 0x30, 0x1B, 0xCE, 0x36, 0x0F, 0x07, 0x83, 0xC1, 0xE0, 0xF0, 0x7C, 0x6D, 0xE0, /* 'c' 0x63 */ 0x3C, 0x66, 0xC3, 0xC0, 0xC0, 0xC0, 0xC0, 0xC3, 0x66, 0x3C, /* 'd' 0x64 */ 0x03, 0x03, 0x03, 0x3B, 0x67, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x67, 0x3B, /* 'e' 0x65 */ 0x3C, 0x66, 0xC3, 0xC3, 0xFF, 0xC0, 0xC0, 0xC3, 0x66, 0x3C, /* 'f' 0x66 */ 0x36, 0x6F, 0x66, 0x66, 0x66, 0x66, 0x60, /* 'g' 0x67 */ 0x3B, 0x67, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x67, 0x3B, 0x03, 0x03, 0xC6, 0x7C, /* 'h' 0x68 */ 0xC0, 0xC0, 0xC0, 0xDE, 0xE3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, /* 'i' 0x69 */ 0xC3, 0xFF, 0xFF, 0xC0, /* 'j' 0x6A */ 0x30, 0x03, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0xE0, /* 'k' 0x6B */ 0xC0, 0xC0, 0xC0, 0xC2, 0xC4, 0xCC, 0xD8, 0xF8, 0xEC, 0xC4, 0xC6, 0xC3, 0xC3, /* 'l' 0x6C */ 0xFF, 0xFF, 0xFF, 0xC0, /* 'm' 0x6D */ 0xDE, 0xF7, 0x1C, 0xF0, 0xC7, 0x86, 0x3C, 0x31, 0xE1, 0x8F, 0x0C, 0x78, 0x63, 0xC3, 0x1E, 0x18, 0xC0, /* 'n' 0x6E */ 0xDE, 0xE3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, /* 'o' 0x6F */ 0x3C, 0x66, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x66, 0x3C, /* 'p' 0x70 */ 0xDE, 0x71, 0xB0, 0x78, 0x3C, 0x1E, 0x0F, 0x07, 0x83, 0xE3, 0x6F, 0x30, 0x18, 0x0C, 0x00, /* 'q' 0x71 */ 0x3B, 0x67, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x67, 0x3B, 0x03, 0x03, 0x03, /* 'r' 0x72 */ 0xDF, 0x31, 0x8C, 0x63, 0x18, 0xC6, 0x00, /* 's' 0x73 */ 0x3E, 0xE3, 0xC0, 0xC0, 0xE0, 0x3C, 0x07, 0xC3, 0xE3, 0x7E, /* 't' 0x74 */ 0x66, 0xF6, 0x66, 0x66, 0x66, 0x67, /* 'u' 0x75 */ 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC7, 0x7B, /* 'v' 0x76 */ 0xC1, 0xA0, 0x98, 0xCC, 0x42, 0x21, 0xB0, 0xD0, 0x28, 0x1C, 0x0C, 0x00, /* 'w' 0x77 */ 0xC6, 0x1E, 0x38, 0x91, 0xC4, 0xCA, 0x66, 0xD3, 0x16, 0xD0, 0xA6, 0x87, 0x1C, 0x38, 0xC0, 0xC6, 0x00, /* 'x' 0x78 */ 0x87, 0x89, 0xB1, 0xC3, 0x07, 0x1E, 0x26, 0xC5, 0x0C, /* 'y' 0x79 */ 0xC1, 0x43, 0x63, 0x62, 0x26, 0x36, 0x34, 0x1C, 0x1C, 0x18, 0x18, 0x18, 0x10, 0x60, /* 'z' 0x7A */ 0xFE, 0x0C, 0x30, 0xC1, 0x86, 0x18, 0x20, 0xC1, 0xFC, /* '{' 0x7B */ 0x36, 0x66, 0x66, 0x6E, 0xCE, 0x66, 0x66, 0x66, 0x30, /* '|' 0x7C */ 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, /* '}' 0x7D */ 0xC6, 0x66, 0x66, 0x67, 0x37, 0x66, 0x66, 0x66, 0xC0, /* '~' 0x7E */ 0x61, 0x24, 0x38, /* 0x7F */ 0xFF, 0xFC, 0x00, 0x63, 0xE3, 0x31, 0x99, 0x04, 0xC8, 0x66, 0x06, 0x30, 0x61, 0x83, 0x0C, 0x18, 0x60, 0x03, 0x06, 0x18, 0x00, 0xFF, 0xFC, /* 0x80 */ 0x07, 0xC6, 0x13, 0x00, 0xC0, 0x60, 0x3F, 0xE6, 0x03, 0xFC, 0x60, 0x0C, 0x03, 0x00, 0x61, 0x07, 0xC0, /* 0x81 */ /* 0x82 */ 0xDC, /* 0x83 */ /* 0x84 */ 0xDA, 0x76, /* 0x85 */ 0xCC, 0xC0, /* 0x86 */ 0x18, 0x18, 0x18, 0x18, 0xFF, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, /* 0x87 */ 0x18, 0x18, 0x18, 0x18, 0xFF, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0xFF, 0x18, 0x18, 0x18, 0x18, /* 0x88 */ /* 0x89 */ 0x70, 0x80, 0x22, 0x20, 0x08, 0x90, 0x02, 0x24, 0x00, 0x72, 0x00, 0x00, 0x80, 0x00, 0x40, 0x00, 0x10, 0x00, 0x09, 0xC7, 0x84, 0x8B, 0x31, 0x22, 0x84, 0x88, 0xB3, 0x21, 0xC7, 0x80, /* 0x8A */ 0x1B, 0x03, 0x83, 0xF1, 0x86, 0xC0, 0xF0, 0x3C, 0x01, 0xE0, 0x1F, 0x00, 0xE0, 0x0F, 0x03, 0xC0, 0xD8, 0x63, 0xF0, /* 0x8B */ 0x69, /* 0x8C */ 0x06, 0x03, 0x03, 0xF1, 0x86, 0xC0, 0xF0, 0x3C, 0x01, 0xE0, 0x1F, 0x00, 0xE0, 0x0F, 0x03, 0xC0, 0xD8, 0x63, 0xF0, /* 0x8D */ 0x33, 0x0F, 0x3F, 0xE1, 0x80, 0xC0, 0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x01, 0x80, 0xC0, 0x60, 0x30, /* 0x8E */ 0x1B, 0x03, 0x8F, 0xFC, 0x06, 0x03, 0x00, 0xC0, 0x60, 0x30, 0x1C, 0x06, 0x03, 0x01, 0x80, 0x60, 0x30, 0x0F, 0xFC, /* 0x8F */ 0x0C, 0x06, 0x0F, 0xFC, 0x06, 0x03, 0x00, 0xC0, 0x60, 0x30, 0x1C, 0x06, 0x03, 0x01, 0x80, 0x60, 0x30, 0x0F, 0xFC, /* 0x90 */ /* 0x91 */ 0x6B, /* 0x92 */ 0xD6, /* 0x93 */ 0x4C, 0xA5, 0xB0, /* 0x94 */ 0xDA, 0x53, 0x20, /* 0x95 */ 0x6F, 0xFF, 0x60, /* 0x96 */ 0xFE, /* 0x97 */ 0xFF, 0xFF, /* 0x98 */ /* 0x99 */ 0xFC, 0xE1, 0xCC, 0x38, 0x73, 0x0E, 0x1C, 0xC3, 0x8F, 0x30, 0xD2, 0xCC, 0x34, 0xB3, 0x0D, 0x6C, 0xC3, 0x53, 0x30, 0xCC, 0xCC, 0x33, 0x30, /* 0x9A */ 0x24, 0x3C, 0x18, 0x7E, 0xE3, 0xC0, 0xC0, 0x60, 0x3C, 0x07, 0xC3, 0xE3, 0x7E, /* 0x9B */ 0x96, /* 0x9C */ 0x0C, 0x18, 0x10, 0x3E, 0xE3, 0xC0, 0xC0, 0xE0, 0x3C, 0x07, 0xC3, 0xE3, 0x7E, /* 0x9D */ 0x0D, 0xA7, 0x3C, 0x61, 0x86, 0x18, 0x61, 0x86, 0x18, 0x70, /* 0x9E */ 0x48, 0xF0, 0xC7, 0xF0, 0x61, 0x86, 0x0C, 0x30, 0xC1, 0x06, 0x0F, 0xE0, /* 0x9F */ 0x0C, 0x10, 0x47, 0xF0, 0x61, 0x86, 0x0C, 0x30, 0xC1, 0x06, 0x0F, 0xE0, /* 0xA0 */ /* 0xA1 */ 0x8A, 0x9C, /* 0xA2 */ 0x85, 0xE0, /* 0xA3 */ 0x60, 0x30, 0x18, 0x0C, 0x86, 0xC3, 0xC1, 0xC1, 0xC0, 0xE0, 0x30, 0x18, 0x0C, 0x07, 0xF8, /* 0xA4 */ 0xFF, 0xDF, 0x1E, 0x3E, 0xFF, 0xC0, /* 0xA5 */ 0x06, 0x00, 0xF0, 0x0F, 0x01, 0x30, 0x13, 0x81, 0x38, 0x21, 0x82, 0x1C, 0x3F, 0xC6, 0x04, 0x60, 0x66, 0x06, 0xC0, 0x30, 0x06, 0x00, 0xC0, 0x0C, 0x00, 0x70, /* 0xA6 */ 0xFF, 0xFC, 0x0F, 0xFF, 0xC0, /* 0xA7 */ 0x0C, 0x09, 0x0C, 0xC6, 0x63, 0x81, 0xE3, 0x19, 0x87, 0xE1, 0xB8, 0xC6, 0x41, 0xC0, 0x73, 0x19, 0x8C, 0x66, 0x1E, 0x00, /* 0xA8 */ 0xCC, /* 0xA9 */ 0x0F, 0xC0, 0x61, 0x87, 0x03, 0x9B, 0xC6, 0xD9, 0x8F, 0x60, 0x3D, 0x00, 0xF4, 0x03, 0xD8, 0x0D, 0xE6, 0x67, 0xF3, 0x86, 0x18, 0x0F, 0xC0, /* 0xAA */ 0x3F, 0x18, 0x6C, 0x0F, 0x03, 0xC0, 0x1E, 0x01, 0xF0, 0x0E, 0x00, 0xF0, 0x3C, 0x0D, 0x86, 0x3F, 0x02, 0x00, 0xE0, 0x18, 0x1C, 0x00, /* 0xAB */ 0x22, 0xCF, 0x26, 0x46, 0x64, 0x40, /* 0xAC */ 0xFF, 0x80, 0xC0, 0x60, 0x30, 0x18, /* 0xAD */ /* 0xAE */ 0x0F, 0xC0, 0x61, 0x87, 0x03, 0x9F, 0xE6, 0xD0, 0x8F, 0x42, 0x3D, 0xF0, 0xF4, 0x23, 0xD0, 0x8D, 0xC2, 0x67, 0x0B, 0x86, 0x18, 0x0F, 0xC0, /* 0xAF */ 0x0C, 0x00, 0x0F, 0xFC, 0x06, 0x03, 0x00, 0xC0, 0x60, 0x30, 0x1C, 0x06, 0x03, 0x01, 0x80, 0x60, 0x30, 0x0F, 0xFC, /* 0xB0 */ 0x74, 0x63, 0x17, 0x00, /* 0xB1 */ 0x0C, 0x06, 0x03, 0x07, 0xE0, 0xC0, 0x60, 0x30, 0x18, 0x00, 0x00, 0x3F, 0xE0, /* 0xB2 */ 0x6C, 0xC7, /* 0xB3 */ 0x66, 0x66, 0x67, 0x6E, 0x66, 0x66, 0x60, /* 0xB4 */ 0x36, 0xC0, /* 0xB5 */ 0xC3, 0x61, 0xB0, 0xD8, 0x6C, 0x36, 0x1B, 0x0D, 0x86, 0xE7, 0x7D, 0xF0, 0x18, 0x0C, 0x00, /* 0xB6 */ 0x3F, 0x7E, 0xF2, 0xF2, 0xF2, 0xF2, 0xF2, 0x72, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, /* 0xB7 */ 0xE0, /* 0xB8 */ 0x21, 0xC7, 0xE0, /* 0xB9 */ 0x7E, 0x38, 0xCC, 0x30, 0x0C, 0x0F, 0x1E, 0xCC, 0x33, 0x0C, 0xC7, 0x1E, 0xE0, 0x10, 0x0C, 0x03, 0x00, 0x70, /* 0xBA */ 0x3E, 0xE3, 0xC0, 0xC0, 0xE0, 0x3C, 0x07, 0xC3, 0xC3, 0x7E, 0x10, 0x1C, 0x0C, 0x38, /* 0xBB */ 0x89, 0x98, 0x99, 0x3C, 0xD1, 0x00, /* 0xBC */ 0xC6, 0xC4, 0xC8, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xFF, /* 0xBD */ 0x6F, 0x69, 0x00, /* 0xBE */ 0xDE, 0xB9, 0x8C, 0x63, 0x18, 0xC6, 0x31, 0x8C, 0x00, /* 0xBF */ 0x30, 0x03, 0xF8, 0x30, 0xC3, 0x06, 0x18, 0x60, 0x83, 0x07, 0xF0, /* 0xC0 */ 0x06, 0x00, 0xC0, 0xFF, 0x8C, 0x0E, 0xC0, 0x6C, 0x06, 0xC0, 0x6C, 0x0C, 0xFF, 0x8C, 0x0E, 0xC0, 0x6C, 0x06, 0xC0, 0x6C, 0x06, 0xC0, 0x70, /* 0xC1 */ 0x06, 0x03, 0x00, 0x00, 0x30, 0x1E, 0x07, 0x81, 0x20, 0xCC, 0x33, 0x0F, 0xC6, 0x19, 0x86, 0x40, 0xB0, 0x30, /* 0xC2 */ 0x0C, 0x04, 0x80, 0x00, 0x30, 0x1E, 0x07, 0x81, 0x20, 0xCC, 0x33, 0x0F, 0xC6, 0x19, 0x86, 0x40, 0xB0, 0x30, /* 0xC3 */ 0x21, 0x07, 0x80, 0x00, 0x30, 0x1E, 0x07, 0x81, 0x20, 0xCC, 0x33, 0x0F, 0xC6, 0x19, 0x86, 0x40, 0xB0, 0x30, /* 0xC4 */ 0x33, 0x00, 0x00, 0xC0, 0x78, 0x1E, 0x04, 0x83, 0x30, 0xCC, 0x33, 0x1F, 0xE6, 0x19, 0x02, 0xC0, 0xF0, 0x30, /* 0xC5 */ 0x30, 0x60, 0x00, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xFF, /* 0xC6 */ 0x06, 0x01, 0x80, 0x00, 0x0F, 0xC3, 0x0C, 0xC0, 0xD0, 0x1E, 0x00, 0xC0, 0x18, 0x03, 0x01, 0xA0, 0x36, 0x0C, 0x61, 0x87, 0xC0, /* 0xC7 */ 0x1F, 0x06, 0x19, 0x83, 0xA0, 0x3C, 0x01, 0x80, 0x30, 0x06, 0x00, 0xC0, 0x68, 0x0D, 0x83, 0x18, 0xE1, 0xF0, 0x08, 0x01, 0xC0, 0x18, 0x0E, 0x00, /* 0xC8 */ 0x19, 0x81, 0xE0, 0x00, 0x0F, 0xC3, 0x0C, 0xC0, 0xF0, 0x1E, 0x00, 0xC0, 0x18, 0x03, 0x01, 0xA0, 0x36, 0x0C, 0x61, 0x87, 0xC0, /* 0xC9 */ 0x0C, 0x0C, 0x00, 0x1F, 0xFC, 0x06, 0x03, 0x01, 0x80, 0xFF, 0x60, 0x30, 0x18, 0x0C, 0x07, 0xFC, /* 0xCA */ 0xFF, 0xD8, 0x03, 0x00, 0x60, 0x0C, 0x01, 0x80, 0x3F, 0xF6, 0x00, 0xC0, 0x18, 0x03, 0x00, 0x60, 0x0F, 0xFC, 0x01, 0x80, 0x60, 0x0C, 0x00, 0xE0, /* 0xCB */ 0x33, 0x00, 0x3F, 0xF8, 0x0C, 0x06, 0x03, 0x01, 0xFE, 0xC0, 0x60, 0x30, 0x18, 0x0C, 0x07, 0xFC, /* 0xCC */ 0x33, 0x0F, 0x00, 0x1F, 0xFC, 0x06, 0x03, 0x01, 0x80, 0xFF, 0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0xFE, /* 0xCD */ 0x78, 0x36, 0xDB, 0x6D, 0xB6, 0xC0, /* 0xCE */ 0x76, 0xC0, 0x63, 0x18, 0xC6, 0x31, 0x8C, 0x63, 0x18, /* 0xCF */ 0x66, 0x0F, 0x00, 0x03, 0xF8, 0xC3, 0x30, 0x6C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC1, 0xB0, 0xEF, 0xE0, /* 0xD0 */ 0x7F, 0x0C, 0x31, 0x83, 0x30, 0x36, 0x06, 0xC0, 0xFE, 0x1B, 0x03, 0x60, 0x6C, 0x0D, 0x83, 0x30, 0xE7, 0xF0, /* 0xD1 */ 0x03, 0x01, 0x83, 0x81, 0xF0, 0x3F, 0x07, 0xA0, 0xF6, 0x1E, 0x63, 0xC4, 0x78, 0xCF, 0x0D, 0xE1, 0xBC, 0x1F, 0x81, 0xC0, /* 0xD2 */ 0x19, 0x81, 0xE3, 0x81, 0xF0, 0x3F, 0x07, 0xA0, 0xF6, 0x1E, 0x63, 0xC4, 0x78, 0xCF, 0x0D, 0xE1, 0xBC, 0x1F, 0x81, 0xC0, /* 0xD3 */ 0x03, 0x00, 0x60, 0x00, 0x00, 0xF0, 0x39, 0xC6, 0x06, 0x60, 0x6C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x36, 0x06, 0x60, 0x63, 0x9C, 0x0F, 0x00, /* 0xD4 */ 0x0F, 0x01, 0x98, 0x00, 0x00, 0xF0, 0x39, 0xC6, 0x06, 0x60, 0x6C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x36, 0x06, 0x60, 0x63, 0x9C, 0x0F, 0x00, /* 0xD5 */ 0x0D, 0x81, 0xB0, 0x00, 0x00, 0xF0, 0x39, 0xC6, 0x06, 0x60, 0x6C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x36, 0x06, 0x60, 0x63, 0x9C, 0x0F, 0x00, /* 0xD6 */ 0x19, 0x81, 0x98, 0x00, 0x00, 0xF0, 0x39, 0xC6, 0x06, 0x60, 0x6C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x36, 0x06, 0x60, 0x63, 0x9C, 0x0F, 0x00, /* 0xD7 */ 0x83, 0x89, 0xA1, 0x83, 0x89, 0xA1, 0x80, /* 0xD8 */ 0x33, 0x01, 0xE0, 0xFF, 0x8C, 0x0E, 0xC0, 0x6C, 0x06, 0xC0, 0x6C, 0x0C, 0xFF, 0x8C, 0x0E, 0xC0, 0x6C, 0x06, 0xC0, 0x6C, 0x06, 0xC0, 0x70, /* 0xD9 */ 0x04, 0x01, 0x43, 0x11, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x36, 0x0C, 0x3E, 0x00, /* 0xDA */ 0x06, 0x01, 0x83, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x36, 0x0C, 0x3E, 0x00, /* 0xDB */ 0x0D, 0x83, 0x63, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x36, 0x0C, 0x3E, 0x00, /* 0xDC */ 0x1B, 0x00, 0x03, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x36, 0x0C, 0x3E, 0x00, /* 0xDD */ 0x03, 0x0C, 0x63, 0x60, 0x63, 0x0C, 0x30, 0xC1, 0x98, 0x1D, 0x80, 0xF0, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, /* 0xDE */ 0xFF, 0x86, 0x03, 0x01, 0x80, 0xC0, 0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x01, 0x80, 0xC0, 0x40, 0x3C, 0x06, 0x1E, 0x00, /* 0xDF */ 0x3C, 0x33, 0x30, 0xD8, 0x6C, 0x36, 0x33, 0x39, 0x86, 0xC1, 0xE0, 0xF0, 0x78, 0x6D, 0xE0, /* 0xE0 */ 0x19, 0x89, 0xBE, 0x63, 0x18, 0xC6, 0x31, 0x8C, 0x00, /* 0xE1 */ 0x0C, 0x04, 0x04, 0x0F, 0xCE, 0x36, 0x18, 0x0C, 0x1E, 0x7B, 0x61, 0xB0, 0xD8, 0xE7, 0xB8, /* 0xE2 */ 0x10, 0x14, 0x1B, 0x0F, 0xCE, 0x36, 0x18, 0x0C, 0x1E, 0x7B, 0x61, 0xB0, 0xD8, 0xE7, 0xB8, /* 0xE3 */ 0x66, 0x1E, 0x00, 0x0F, 0xCE, 0x36, 0x18, 0x0C, 0x1E, 0x7B, 0x61, 0xB0, 0xD8, 0xE7, 0xB8, /* 0xE4 */ 0x66, 0x00, 0x1F, 0x9C, 0x6C, 0x30, 0x18, 0x3C, 0xF6, 0xC3, 0x61, 0xB1, 0xCF, 0x70, /* 0xE5 */ 0x78, 0x36, 0xDB, 0x6D, 0xB6, 0xD8, /* 0xE6 */ 0x0C, 0x08, 0x10, 0x3C, 0x66, 0xC3, 0xC0, 0xC0, 0xC0, 0xC0, 0xC3, 0x66, 0x3C, /* 0xE7 */ 0x3C, 0x66, 0xC3, 0xC0, 0xC0, 0xC0, 0xC0, 0xC3, 0x66, 0x3C, 0x10, 0x1C, 0x0C, 0x38, /* 0xE8 */ 0x44, 0x28, 0x38, 0x3C, 0x66, 0xC3, 0xC0, 0xC0, 0xC0, 0xC0, 0xC3, 0x66, 0x3C, /* 0xE9 */ 0x0C, 0x08, 0x18, 0x3C, 0x66, 0xC3, 0xC3, 0xFF, 0xC0, 0xC0, 0xC3, 0x66, 0x3C, /* 0xEA */ 0x3C, 0x62, 0xC3, 0xC3, 0xFF, 0xC0, 0xC0, 0xC3, 0x66, 0x3E, 0x04, 0x0C, 0x0C, 0x06, /* 0xEB */ 0x66, 0x00, 0x3C, 0x66, 0xC3, 0xC3, 0xFF, 0xC0, 0xC0, 0xC3, 0x66, 0x3C, /* 0xEC */ 0x64, 0x2C, 0x18, 0x3C, 0x66, 0xC3, 0xC3, 0xFF, 0xC0, 0xC0, 0xC3, 0x66, 0x3C, /* 0xED */ 0x7A, 0x6D, 0xB6, 0xDB, 0x6C, /* 0xEE */ 0x69, 0x06, 0x66, 0x66, 0x66, 0x66, 0x60, /* 0xEF */ 0x03, 0x30, 0x32, 0x03, 0x43, 0xB0, 0x67, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x06, 0x70, 0x3B, 0x00, /* 0xF0 */ 0x03, 0x07, 0xC0, 0xC7, 0x66, 0x76, 0x1B, 0x0D, 0x86, 0xC3, 0x61, 0xB0, 0xCC, 0xE3, 0xB0, /* 0xF1 */ 0x0C, 0x18, 0x00, 0xDE, 0xE3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, /* 0xF2 */ 0x66, 0x3C, 0x00, 0xDE, 0xE3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, /* 0xF3 */ 0x0C, 0x18, 0x00, 0x3C, 0x66, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x66, 0x3C, /* 0xF4 */ 0x18, 0x24, 0x00, 0x3C, 0x66, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x66, 0x3C, /* 0xF5 */ 0x36, 0x6C, 0x00, 0x3C, 0x66, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x66, 0x3C, /* 0xF6 */ 0x66, 0x00, 0x3C, 0x66, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x66, 0x3C, /* 0xF7 */ 0x18, 0x00, 0x00, 0x1F, 0xF0, 0x00, 0x00, 0x00, 0x30, /* 0xF8 */ 0xDB, 0x81, 0xBE, 0x63, 0x18, 0xC6, 0x31, 0x8C, 0x00, /* 0xF9 */ 0x10, 0x28, 0x10, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC7, 0x7B, /* 0xFA */ 0x06, 0x0C, 0x18, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC7, 0x7B, /* 0xFB */ 0x36, 0x6C, 0x00, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC7, 0x7B, /* 0xFC */ 0x66, 0x00, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC7, 0x7B, /* 0xFD */ 0x06, 0x04, 0x08, 0xC1, 0x43, 0x63, 0x62, 0x26, 0x36, 0x34, 0x1C, 0x1C, 0x18, 0x18, 0x18, 0x10, 0x60, /* 0xFE */ 0x63, 0x3C, 0xC6, 0x31, 0x8C, 0x63, 0x18, 0xE2, 0x1C, 0x6F, /* 0xFF */ 0xC0, }; const GFXglyph FreeSans9pt_Win1250Glyphs[] PROGMEM = { /* 0x01 */ { 0, 15, 15, 17, 1, -13 }, /* 0x02 */ { 29, 15, 15, 17, 1, -13 }, /* 0x03 */ { 58, 15, 16, 17, 1, -14 }, /* 0x04 */ { 88, 15, 16, 17, 1, -14 }, /* 0x05 */ { 118, 16, 15, 18, 1, -13 }, /* 0x06 */ { 148, 15, 15, 17, 1, -13 }, /* 0x07 */ { 177, 0, 0, 8, 0, 0 }, /* 0x08 */ { 177, 17, 16, 19, 1, -14 }, /* 0x09 */ { 211, 17, 12, 19, 1, -12 }, /* 0x0A */ { 237, 0, 0, 8, 0, 0 }, /* 0x0B */ { 237, 17, 16, 19, 1, -14 }, /* 0x0C */ { 271, 15, 14, 17, 1, -12 }, /* 0x0D */ { 298, 0, 0, 8, 0, 0 }, /* 0x0E */ { 298, 15, 16, 17, 1, -14 }, /* 0x0F */ { 328, 15, 15, 17, 1, -13 }, /* 0x10 */ { 357, 15, 15, 17, 1, -13 }, /* 0x11 */ { 386, 15, 16, 17, 1, -14 }, /* 0x12 */ { 416, 17, 17, 19, 1, -15 }, /* 0x13 */ { 453, 15, 16, 17, 1, -14 }, /* 0x14 */ { 483, 15, 16, 17, 1, -14 }, /* 0x15 */ { 513, 15, 16, 17, 1, -14 }, /* 0x16 */ { 543, 11, 16, 13, 1, -14 }, /* 0x17 */ { 565, 15, 16, 17, 1, -14 }, /* 0x18 */ { 595, 18, 15, 20, 1, -13 }, /* 0x19 */ { 629, 15, 16, 17, 1, -14 }, /* 0x1A */ { 659, 13, 14, 15, 1, -12 }, /* 0x1B */ { 682, 17, 16, 19, 1, -14 }, /* 0x1C */ { 716, 15, 16, 17, 1, -14 }, /* 0x1D */ { 746, 15, 15, 17, 1, -13 }, /* 0x1E */ { 775, 17, 16, 19, 1, -14 }, /* 0x1F */ { 809, 11, 16, 13, 1, -14 }, /* ' ' 0x20 */ { 831, 0, 0, 5, 0, 0 }, /* '!' 0x21 */ { 831, 2, 13, 6, 2, -12 }, /* '"' 0x22 */ { 835, 5, 4, 6, 1, -12 }, /* '#' 0x23 */ { 838, 10, 12, 10, 0, -11 }, /* '$' 0x24 */ { 853, 9, 16, 10, 1, -13 }, /* '%' 0x25 */ { 871, 16, 13, 16, 1, -12 }, /* '&' 0x26 */ { 897, 10, 13, 12, 1, -12 }, /* ''' 0x27 */ { 914, 2, 4, 4, 1, -12 }, /* '(' 0x28 */ { 915, 4, 17, 6, 1, -12 }, /* ')' 0x29 */ { 924, 4, 17, 6, 1, -12 }, /* '*' 0x2A */ { 933, 5, 5, 7, 1, -12 }, /* '+' 0x2B */ { 937, 6, 8, 11, 3, -7 }, /* ',' 0x2C */ { 943, 2, 4, 5, 2, 0 }, /* '-' 0x2D */ { 944, 4, 1, 6, 1, -4 }, /* '.' 0x2E */ { 945, 2, 1, 5, 1, 0 }, /* '/' 0x2F */ { 946, 5, 13, 5, 0, -12 }, /* '0' 0x30 */ { 955, 8, 13, 10, 1, -12 }, /* '1' 0x31 */ { 968, 4, 13, 10, 3, -12 }, /* '2' 0x32 */ { 975, 9, 13, 10, 1, -12 }, /* '3' 0x33 */ { 990, 8, 13, 10, 1, -12 }, /* '4' 0x34 */ { 1003, 7, 13, 10, 2, -12 }, /* '5' 0x35 */ { 1015, 9, 13, 10, 1, -12 }, /* '6' 0x36 */ { 1030, 9, 13, 10, 1, -12 }, /* '7' 0x37 */ { 1045, 8, 13, 10, 0, -12 }, /* '8' 0x38 */ { 1058, 9, 13, 10, 1, -12 }, /* '9' 0x39 */ { 1073, 8, 13, 10, 1, -12 }, /* ':' 0x3A */ { 1086, 2, 10, 5, 1, -9 }, /* ';' 0x3B */ { 1089, 3, 12, 5, 1, -8 }, /* '<' 0x3C */ { 1094, 9, 9, 11, 1, -8 }, /* '=' 0x3D */ { 1105, 9, 4, 11, 1, -5 }, /* '>' 0x3E */ { 1110, 9, 8, 11, 1, -7 }, /* '?' 0x3F */ { 1119, 9, 13, 10, 1, -12 }, /* '@' 0x40 */ { 1134, 17, 16, 18, 1, -12 }, /* 'A' 0x41 */ { 1168, 12, 13, 12, 0, -12 }, /* 'B' 0x42 */ { 1188, 11, 13, 12, 1, -12 }, /* 'C' 0x43 */ { 1206, 11, 13, 13, 1, -12 }, /* 'D' 0x44 */ { 1224, 11, 13, 13, 1, -12 }, /* 'E' 0x45 */ { 1242, 9, 13, 11, 1, -12 }, /* 'F' 0x46 */ { 1257, 8, 13, 11, 1, -12 }, /* 'G' 0x47 */ { 1270, 12, 13, 14, 1, -12 }, /* 'H' 0x48 */ { 1290, 11, 13, 13, 1, -12 }, /* 'I' 0x49 */ { 1308, 2, 13, 5, 2, -12 }, /* 'J' 0x4A */ { 1312, 7, 13, 10, 1, -12 }, /* 'K' 0x4B */ { 1324, 10, 13, 12, 1, -12 }, /* 'L' 0x4C */ { 1341, 8, 13, 10, 1, -12 }, /* 'M' 0x4D */ { 1354, 13, 13, 15, 1, -12 }, /* 'N' 0x4E */ { 1376, 11, 13, 13, 1, -12 }, /* 'O' 0x4F */ { 1394, 13, 13, 14, 1, -12 }, /* 'P' 0x50 */ { 1416, 10, 13, 12, 1, -12 }, /* 'Q' 0x51 */ { 1433, 13, 14, 14, 1, -12 }, /* 'R' 0x52 */ { 1456, 12, 13, 13, 1, -12 }, /* 'S' 0x53 */ { 1476, 10, 13, 12, 1, -12 }, /* 'T' 0x54 */ { 1493, 9, 13, 11, 1, -12 }, /* 'U' 0x55 */ { 1508, 11, 13, 13, 1, -12 }, /* 'V' 0x56 */ { 1526, 11, 13, 11, 0, -12 }, /* 'W' 0x57 */ { 1544, 16, 13, 17, 0, -12 }, /* 'X' 0x58 */ { 1570, 10, 13, 12, 1, -12 }, /* 'Y' 0x59 */ { 1587, 12, 13, 12, 0, -12 }, /* 'Z' 0x5A */ { 1607, 10, 13, 11, 1, -12 }, /* '[' 0x5B */ { 1624, 3, 17, 5, 1, -12 }, /* '\' 0x5C */ { 1631, 5, 13, 5, 0, -12 }, /* ']' 0x5D */ { 1640, 3, 17, 5, 0, -12 }, /* '^' 0x5E */ { 1647, 7, 7, 8, 1, -12 }, /* '_' 0x5F */ { 1654, 10, 1, 10, 0, 3 }, /* '`' 0x60 */ { 1656, 4, 3, 5, 0, -12 }, /* 'a' 0x61 */ { 1658, 9, 10, 10, 1, -9 }, /* 'b' 0x62 */ { 1670, 9, 13, 10, 1, -12 }, /* 'c' 0x63 */ { 1685, 8, 10, 9, 1, -9 }, /* 'd' 0x64 */ { 1695, 8, 13, 10, 1, -12 }, /* 'e' 0x65 */ { 1708, 8, 10, 10, 1, -9 }, /* 'f' 0x66 */ { 1718, 4, 13, 5, 1, -12 }, /* 'g' 0x67 */ { 1725, 8, 14, 10, 1, -9 }, /* 'h' 0x68 */ { 1739, 8, 13, 10, 1, -12 }, /* 'i' 0x69 */ { 1752, 2, 13, 4, 1, -12 }, /* 'j' 0x6A */ { 1756, 4, 17, 4, 0, -12 }, /* 'k' 0x6B */ { 1765, 8, 13, 9, 1, -12 }, /* 'l' 0x6C */ { 1778, 2, 13, 4, 1, -12 }, /* 'm' 0x6D */ { 1782, 13, 10, 15, 1, -9 }, /* 'n' 0x6E */ { 1799, 8, 10, 10, 1, -9 }, /* 'o' 0x6F */ { 1809, 8, 10, 10, 1, -9 }, /* 'p' 0x70 */ { 1819, 9, 13, 10, 1, -9 }, /* 'q' 0x71 */ { 1834, 8, 13, 10, 1, -9 }, /* 'r' 0x72 */ { 1847, 5, 10, 6, 1, -9 }, /* 's' 0x73 */ { 1854, 8, 10, 9, 1, -9 }, /* 't' 0x74 */ { 1864, 4, 12, 5, 1, -11 }, /* 'u' 0x75 */ { 1870, 8, 10, 10, 1, -9 }, /* 'v' 0x76 */ { 1880, 9, 10, 9, 0, -9 }, /* 'w' 0x77 */ { 1892, 13, 10, 13, 0, -9 }, /* 'x' 0x78 */ { 1909, 7, 10, 9, 1, -9 }, /* 'y' 0x79 */ { 1918, 8, 14, 9, 0, -9 }, /* 'z' 0x7A */ { 1932, 7, 10, 9, 1, -9 }, /* '{' 0x7B */ { 1941, 4, 17, 6, 1, -12 }, /* '|' 0x7C */ { 1950, 2, 17, 4, 2, -12 }, /* '}' 0x7D */ { 1955, 4, 17, 6, 1, -12 }, /* '~' 0x7E */ { 1964, 7, 3, 9, 1, -7 }, /* 0x7F */ { 1967, 13, 14, 15, 1, -12 }, /* 0x80 */ { 1990, 10, 13, 12, 1, -12 }, /* 0x81 */ { 2007, 0, 0, 0, 0, 0 }, /* 0x82 */ { 2007, 2, 3, 5, 1, 0 }, /* 0x83 */ { 2008, 0, 0, 0, 0, 0 }, /* 0x84 */ { 2008, 5, 3, 7, 1, 0 }, /* 0x85 */ { 2010, 10, 1, 12, 1, 0 }, /* 0x86 */ { 2012, 8, 16, 10, 1, -12 }, /* 0x87 */ { 2028, 8, 16, 10, 1, -12 }, /* 0x88 */ { 2044, 0, 0, 0, 0, 0 }, /* 0x89 */ { 2044, 18, 13, 18, 0, -12 }, /* 0x8A */ { 2074, 10, 15, 12, 1, -14 }, /* 0x8B */ { 2093, 2, 4, 4, 1, -6 }, /* 0x8C */ { 2094, 10, 15, 12, 1, -14 }, /* 0x8D */ { 2113, 9, 15, 11, 1, -14 }, /* 0x8E */ { 2130, 10, 15, 11, 1, -14 }, /* 0x8F */ { 2149, 10, 15, 11, 1, -14 }, /* 0x90 */ { 2168, 0, 0, 0, 0, 0 }, /* 0x91 */ { 2168, 2, 4, 4, 2, -12 }, /* 0x92 */ { 2169, 2, 4, 4, 1, -12 }, /* 0x93 */ { 2170, 5, 4, 7, 2, -12 }, /* 0x94 */ { 2173, 5, 4, 7, 1, -12 }, /* 0x95 */ { 2176, 4, 5, 7, 1, -8 }, /* 0x96 */ { 2179, 7, 1, 9, 1, -4 }, /* 0x97 */ { 2180, 16, 1, 18, 1, -4 }, /* 0x98 */ { 2182, 0, 0, 0, 0, 0 }, /* 0x99 */ { 2182, 18, 10, 18, 1, -13 }, /* 0x9A */ { 2205, 8, 13, 9, 1, -12 }, /* 0x9B */ { 2218, 2, 4, 5, 2, -6 }, /* 0x9C */ { 2219, 8, 13, 9, 1, -12 }, /* 0x9D */ { 2232, 6, 13, 8, 1, -12 }, /* 0x9E */ { 2242, 7, 13, 9, 1, -12 }, /* 0x9F */ { 2254, 7, 13, 9, 1, -12 }, /* 0xA0 */ { 2266, 0, 0, 5, 0, 0 }, /* 0xA1 */ { 2266, 5, 3, 6, 0, -12 }, /* 0xA2 */ { 2268, 6, 2, 6, 0, -12 }, /* 0xA3 */ { 2270, 9, 13, 11, 1, -12 }, /* 0xA4 */ { 2285, 7, 6, 10, 2, -8 }, /* 0xA5 */ { 2291, 12, 17, 12, 1, -12 }, /* 0xA6 */ { 2317, 2, 17, 5, 2, -12 }, /* 0xA7 */ { 2322, 9, 17, 10, 1, -12 }, /* 0xA8 */ { 2342, 6, 1, 6, 0, -11 }, /* 0xA9 */ { 2343, 14, 13, 14, 1, -12 }, /* 0xAA */ { 2366, 10, 17, 12, 1, -12 }, /* 0xAB */ { 2388, 7, 6, 9, 1, -7 }, /* 0xAC */ { 2394, 9, 5, 11, 2, -5 }, /* 0xAD */ { 2400, 0, 0, 0, 0, 0 }, /* 0xAE */ { 2400, 14, 13, 14, 1, -12 }, /* 0xAF */ { 2423, 10, 15, 11, 1, -14 }, /* 0xB0 */ { 2442, 5, 5, 11, 3, -11 }, /* 0xB1 */ { 2446, 9, 11, 11, 1, -10 }, /* 0xB2 */ { 2459, 4, 4, 6, 1, 1 }, /* 0xB3 */ { 2461, 4, 13, 5, 1, -12 }, /* 0xB4 */ { 2468, 4, 3, 6, 2, -12 }, /* 0xB5 */ { 2470, 9, 13, 10, 1, -9 }, /* 0xB6 */ { 2485, 8, 16, 10, 2, -12 }, /* 0xB7 */ { 2501, 3, 1, 5, 1, -4 }, /* 0xB8 */ { 2502, 5, 4, 6, 1, 1 }, /* 0xB9 */ { 2505, 10, 14, 10, 1, -9 }, /* 0xBA */ { 2523, 8, 14, 9, 1, -9 }, /* 0xBB */ { 2537, 7, 6, 9, 1, -7 }, /* 0xBC */ { 2543, 8, 13, 10, 1, -12 }, /* 0xBD */ { 2556, 6, 3, 6, 0, -12 }, /* 0xBE */ { 2559, 5, 13, 7, 1, -12 }, /* 0xBF */ { 2568, 7, 12, 9, 1, -11 }, /* 0xC0 */ { 2579, 12, 15, 13, 1, -14 }, /* 0xC1 */ { 2602, 10, 14, 12, 1, -13 }, /* 0xC2 */ { 2620, 10, 14, 12, 1, -13 }, /* 0xC3 */ { 2638, 10, 14, 12, 1, -13 }, /* 0xC4 */ { 2656, 10, 14, 12, 1, -13 }, /* 0xC5 */ { 2674, 8, 14, 10, 1, -13 }, /* 0xC6 */ { 2688, 11, 15, 13, 1, -14 }, /* 0xC7 */ { 2709, 11, 17, 13, 1, -12 }, /* 0xC8 */ { 2733, 11, 15, 13, 1, -14 }, /* 0xC9 */ { 2754, 9, 14, 11, 1, -13 }, /* 0xCA */ { 2770, 11, 17, 12, 1, -12 }, /* 0xCB */ { 2794, 9, 14, 11, 1, -13 }, /* 0xCC */ { 2810, 9, 15, 11, 1, -14 }, /* 0xCD */ { 2827, 3, 14, 5, 1, -13 }, /* 0xCE */ { 2833, 5, 14, 5, 0, -13 }, /* 0xCF */ { 2842, 10, 15, 13, 2, -14 }, /* 0xD0 */ { 2861, 11, 13, 13, 1, -12 }, /* 0xD1 */ { 2879, 11, 14, 13, 1, -13 }, /* 0xD2 */ { 2899, 11, 14, 13, 1, -13 }, /* 0xD3 */ { 2919, 12, 15, 13, 1, -14 }, /* 0xD4 */ { 2942, 12, 15, 13, 1, -14 }, /* 0xD5 */ { 2965, 12, 15, 13, 1, -14 }, /* 0xD6 */ { 2988, 12, 15, 13, 1, -14 }, /* 0xD7 */ { 3011, 7, 7, 11, 2, -7 }, /* 0xD8 */ { 3018, 12, 15, 13, 1, -14 }, /* 0xD9 */ { 3041, 11, 14, 13, 1, -13 }, /* 0xDA */ { 3061, 11, 14, 13, 1, -13 }, /* 0xDB */ { 3081, 11, 14, 13, 1, -13 }, /* 0xDC */ { 3101, 11, 14, 13, 1, -13 }, /* 0xDD */ { 3121, 12, 14, 12, 0, -13 }, /* 0xDE */ { 3142, 9, 17, 11, 1, -12 }, /* 0xDF */ { 3162, 9, 13, 11, 1, -12 }, /* 0xE0 */ { 3177, 5, 13, 6, 1, -12 }, /* 0xE1 */ { 3186, 9, 13, 10, 1, -12 }, /* 0xE2 */ { 3201, 9, 13, 10, 1, -12 }, /* 0xE3 */ { 3216, 9, 13, 10, 1, -12 }, /* 0xE4 */ { 3231, 9, 12, 10, 1, -11 }, /* 0xE5 */ { 3245, 3, 15, 4, 0, -14 }, /* 0xE6 */ { 3251, 8, 13, 9, 1, -12 }, /* 0xE7 */ { 3264, 8, 14, 9, 1, -9 }, /* 0xE8 */ { 3278, 8, 13, 9, 1, -12 }, /* 0xE9 */ { 3291, 8, 13, 10, 1, -12 }, /* 0xEA */ { 3304, 8, 14, 10, 1, -9 }, /* 0xEB */ { 3318, 8, 12, 10, 1, -11 }, /* 0xEC */ { 3330, 8, 13, 10, 1, -12 }, /* 0xED */ { 3343, 3, 13, 4, 1, -12 }, /* 0xEE */ { 3348, 4, 13, 5, 0, -12 }, /* 0xEF */ { 3355, 12, 13, 12, 1, -12 }, /* 0xF0 */ { 3375, 9, 13, 10, 1, -12 }, /* 0xF1 */ { 3390, 8, 13, 10, 1, -12 }, /* 0xF2 */ { 3403, 8, 13, 10, 1, -12 }, /* 0xF3 */ { 3416, 8, 13, 10, 1, -12 }, /* 0xF4 */ { 3429, 8, 13, 10, 1, -12 }, /* 0xF5 */ { 3442, 8, 13, 10, 1, -12 }, /* 0xF6 */ { 3455, 8, 12, 10, 1, -11 }, /* 0xF7 */ { 3467, 9, 8, 11, 1, -7 }, /* 0xF8 */ { 3476, 5, 13, 6, 1, -12 }, /* 0xF9 */ { 3485, 8, 13, 10, 1, -12 }, /* 0xFA */ { 3498, 8, 13, 10, 1, -12 }, /* 0xFB */ { 3511, 8, 13, 10, 1, -12 }, /* 0xFC */ { 3524, 8, 12, 10, 1, -11 }, /* 0xFD */ { 3536, 8, 17, 9, 0, -12 }, /* 0xFE */ { 3553, 5, 16, 5, 1, -11 }, /* 0xFF */ { 3563, 2, 1, 6, 2, -11 }, }; const GFXfont FreeSans9pt_Win1250 PROGMEM = { (uint8_t*)FreeSans9pt_Win1250Bitmaps, (GFXglyph*)FreeSans9pt_Win1250Glyphs, 0x01, 0xFF, 21 }; ================================================ FILE: src/graphics/niche/Fonts/FreeSans9pt_Win1251.h ================================================ // trunk-ignore-all(clang-format) #pragma once /* PROPERTIES FONT_NAME FreeSans9pt_Win1251 */ const uint8_t FreeSans9pt_Win1251Bitmaps[] PROGMEM = { /* 0x01 */ 0x07, 0x00, 0x0A, 0x00, 0x24, 0x00, 0x48, 0x01, 0x10, 0x04, 0x40, 0x10, 0xFF, 0x20, 0x02, 0x81, 0xFD, 0x00, 0x06, 0x07, 0xF4, 0x08, 0x24, 0x0F, 0x88, 0x11, 0x0F, 0xDC, 0x00, /* 0x02 */ 0x3F, 0x70, 0x81, 0x11, 0x03, 0xE4, 0x08, 0x28, 0x1F, 0xD0, 0x00, 0x60, 0x7F, 0x20, 0x02, 0x43, 0xFC, 0x44, 0x00, 0x44, 0x00, 0x48, 0x00, 0x90, 0x00, 0xA0, 0x01, 0xC0, 0x00, /* 0x03 */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x00, 0x28, 0x00, 0x31, 0x8C, 0x63, 0x18, 0xC0, 0x01, 0x80, 0x03, 0x00, 0x06, 0x20, 0x8C, 0x3E, 0x14, 0x00, 0x44, 0x01, 0x06, 0x0C, 0x03, 0xE0, /* 0x04 */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x00, 0x28, 0x82, 0x30, 0x88, 0x62, 0x08, 0xC0, 0x01, 0x80, 0x03, 0x00, 0x06, 0x3F, 0x8C, 0x3E, 0x14, 0x00, 0x44, 0x01, 0x06, 0x0C, 0x03, 0xE0, /* 0x05 */ 0x0B, 0x10, 0x14, 0xA8, 0x12, 0x50, 0x29, 0x42, 0x24, 0xA5, 0x32, 0x95, 0x5A, 0x09, 0x48, 0x09, 0x24, 0x01, 0x10, 0x01, 0x48, 0x02, 0xA4, 0x02, 0x42, 0x04, 0x01, 0x98, 0x00, 0x60, /* 0x06 */ 0x00, 0x80, 0x22, 0x80, 0x65, 0x00, 0xBE, 0xE1, 0x82, 0x4E, 0x03, 0x24, 0x04, 0x28, 0x06, 0x30, 0x12, 0x20, 0x3C, 0xA0, 0xC3, 0xFE, 0x80, 0x4D, 0x00, 0xA6, 0x01, 0x80, 0x00, /* 0x07 */ /* 0x08 */ 0x00, 0xF8, 0x00, 0x82, 0x00, 0x80, 0x83, 0xE0, 0x41, 0x10, 0x21, 0x04, 0x1B, 0x00, 0x03, 0x00, 0x01, 0x80, 0x00, 0xE0, 0x00, 0x4F, 0xE1, 0xC0, 0x0F, 0x02, 0x00, 0x03, 0x01, 0x00, 0x09, 0x88, 0x0C, 0x0C, /* 0x09 */ 0x00, 0xF8, 0x00, 0x82, 0x00, 0x80, 0x83, 0xE0, 0x41, 0x10, 0x21, 0x04, 0x1B, 0x00, 0x03, 0x00, 0x01, 0x80, 0x00, 0xE0, 0x00, 0x4F, 0xE1, 0xC0, 0x0F, 0x00, /* 0x0A */ /* 0x0B */ 0x1C, 0x1C, 0x31, 0xB1, 0x90, 0x50, 0x50, 0x10, 0x18, 0x00, 0x0C, 0x00, 0x06, 0x00, 0x02, 0x80, 0x02, 0x40, 0x01, 0x10, 0x01, 0x04, 0x01, 0x01, 0x01, 0x00, 0x41, 0x00, 0x11, 0x00, 0x07, 0x00, 0x01, 0x00, /* 0x0C */ 0x06, 0x00, 0x0A, 0x00, 0x12, 0x00, 0x32, 0x01, 0x84, 0x04, 0x10, 0x08, 0x98, 0x1C, 0x18, 0x40, 0x48, 0x82, 0x11, 0xF0, 0x74, 0x02, 0x18, 0x70, 0x2F, 0x9F, 0x80, /* 0x0D */ /* 0x0E */ 0x01, 0x00, 0x05, 0x00, 0x0A, 0x00, 0x3E, 0x00, 0x82, 0x02, 0x82, 0x06, 0x04, 0x10, 0x04, 0x20, 0x08, 0x40, 0x10, 0xFF, 0x22, 0x00, 0x29, 0xFF, 0x3F, 0x8F, 0xDF, 0x9F, 0x01, 0xC0, /* 0x0F */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x00, 0x28, 0x82, 0x36, 0x03, 0x60, 0x00, 0xCC, 0x19, 0xA4, 0x4B, 0x00, 0x06, 0x8E, 0x2B, 0x22, 0x66, 0x7C, 0xCC, 0x71, 0x98, 0x03, 0x00, /* 0x10 */ 0x03, 0x80, 0x07, 0x00, 0x0E, 0x00, 0x1E, 0x00, 0x54, 0x00, 0xA8, 0x01, 0x50, 0x02, 0xA0, 0x05, 0x20, 0x32, 0x61, 0xC4, 0x74, 0x49, 0x10, 0x6C, 0x00, 0xD8, 0x01, 0x10, 0x00, /* 0x11 */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x40, 0x29, 0x00, 0x31, 0x84, 0x63, 0x18, 0xC0, 0x00, 0x80, 0x15, 0x03, 0x7E, 0x02, 0xFA, 0x04, 0xE4, 0x18, 0x84, 0x00, 0x06, 0x0C, 0x03, 0xE0, /* 0x12 */ 0x02, 0x08, 0x01, 0x08, 0x40, 0x10, 0xC0, 0x08, 0xC0, 0x60, 0x80, 0x28, 0x04, 0x12, 0x4C, 0x10, 0x80, 0x08, 0x23, 0x0E, 0x08, 0xC4, 0x82, 0x04, 0x20, 0x83, 0x09, 0x82, 0x47, 0x01, 0x1C, 0x01, 0x30, 0x00, 0xE0, 0x00, 0x00, /* 0x13 */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x00, 0x28, 0x00, 0x31, 0x08, 0x65, 0x28, 0xC0, 0x01, 0x80, 0x03, 0x00, 0x06, 0x3F, 0x8C, 0x3E, 0x14, 0x00, 0x44, 0x01, 0x06, 0x0C, 0x03, 0xE0, /* 0x14 */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x22, 0x29, 0x83, 0x30, 0x00, 0x65, 0x14, 0xD3, 0x4D, 0xBA, 0xEB, 0x38, 0xE6, 0x00, 0x0A, 0x00, 0x24, 0x38, 0x44, 0x01, 0x07, 0x1C, 0x01, 0xC0, /* 0x15 */ 0x07, 0xC0, 0x30, 0x18, 0x80, 0x32, 0x00, 0xF8, 0x01, 0xF1, 0x09, 0xA5, 0x28, 0x40, 0x01, 0x80, 0x03, 0x00, 0x06, 0x3F, 0x8C, 0x3E, 0x14, 0x00, 0x44, 0x01, 0x06, 0x0C, 0x03, 0xE0, /* 0x16 */ 0x0C, 0x00, 0xC0, 0x1C, 0x03, 0x80, 0xF8, 0xBB, 0x36, 0xC7, 0x99, 0xF3, 0xFE, 0x3F, 0xC3, 0xF0, 0x7E, 0x0E, 0xC1, 0x8E, 0xE0, 0x20, /* 0x17 */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x00, 0x28, 0x00, 0x10, 0x01, 0x20, 0x1D, 0x44, 0x42, 0x84, 0x85, 0x00, 0x86, 0x00, 0xC4, 0x00, 0x44, 0x7C, 0x44, 0x00, 0x06, 0x0C, 0x03, 0xE0, /* 0x18 */ 0x01, 0xE0, 0x00, 0x84, 0x00, 0x40, 0x80, 0x20, 0x10, 0x08, 0x24, 0x02, 0x41, 0x00, 0x86, 0x03, 0x12, 0x03, 0xB4, 0x03, 0x52, 0x81, 0x23, 0x80, 0x70, 0xA0, 0x14, 0x28, 0x05, 0x0A, 0x01, 0x42, 0x80, 0x50, /* 0x19 */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x00, 0x28, 0x00, 0x33, 0x18, 0x60, 0x00, 0xDC, 0xE1, 0xB9, 0xC3, 0x7B, 0xC6, 0x63, 0x0A, 0x00, 0x24, 0xF0, 0x44, 0x01, 0x06, 0x0C, 0x03, 0xE0, /* 0x1A */ 0xFF, 0xFC, 0x00, 0x63, 0xE3, 0x31, 0x99, 0x04, 0xC8, 0x66, 0x06, 0x30, 0x61, 0x82, 0x0C, 0x10, 0x60, 0x03, 0x04, 0x18, 0x00, 0xFF, 0xFC, /* 0x1B */ 0x07, 0xF0, 0x06, 0x0C, 0x04, 0x01, 0x04, 0x00, 0x44, 0x22, 0x12, 0x2A, 0x89, 0x00, 0x04, 0x80, 0x02, 0x44, 0x11, 0x01, 0xF0, 0x04, 0x01, 0x0D, 0x01, 0x6A, 0x41, 0x2C, 0x00, 0x05, 0xC0, 0x0E, 0x18, 0x18, /* 0x1C */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0xC0, 0x2A, 0x00, 0x33, 0x00, 0x66, 0x00, 0xCC, 0x39, 0x80, 0x83, 0x00, 0x06, 0x00, 0x8C, 0x3E, 0x14, 0x00, 0x44, 0x01, 0x06, 0x0C, 0x03, 0xE0, /* 0x1D */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x70, 0x28, 0x00, 0x31, 0x80, 0x63, 0x18, 0xC0, 0x31, 0x80, 0x03, 0x00, 0x06, 0x60, 0x0D, 0x33, 0x12, 0x10, 0x48, 0x21, 0x23, 0x8C, 0x00, /* 0x1E */ 0x03, 0x00, 0x07, 0x9E, 0x07, 0x00, 0x86, 0x00, 0x27, 0xC0, 0x0F, 0xC0, 0x07, 0x8C, 0x62, 0x06, 0x31, 0x20, 0x00, 0x90, 0x00, 0x48, 0x00, 0x24, 0x3E, 0x11, 0x00, 0x10, 0x40, 0x10, 0x18, 0x30, 0x03, 0xE0, /* 0x1F */ 0x18, 0x02, 0x80, 0x4C, 0x16, 0x41, 0x24, 0x3C, 0x88, 0x6E, 0x65, 0xF2, 0x78, 0x46, 0x88, 0xCF, 0x18, 0x02, 0x80, 0x8C, 0x60, 0x70, /* ' ' 0x20 */ /* '!' 0x21 */ 0xFF, 0xFF, 0xF0, 0xC0, /* '"' 0x22 */ 0xDE, 0xF7, 0x20, /* '#' 0x23 */ 0x09, 0x86, 0x41, 0x91, 0xFF, 0x13, 0x04, 0xC3, 0x20, 0xC8, 0xFF, 0x89, 0x82, 0x61, 0x90, /* '$' 0x24 */ 0x10, 0x1F, 0x14, 0xDA, 0x3D, 0x1E, 0x83, 0x40, 0x78, 0x17, 0x08, 0xF4, 0x7A, 0x35, 0x33, 0xF0, 0x40, 0x20, /* '%' 0x25 */ 0x38, 0x10, 0xEC, 0x20, 0xC6, 0x20, 0xC6, 0x40, 0xC6, 0x40, 0x6C, 0x80, 0x39, 0x00, 0x01, 0x3C, 0x02, 0x77, 0x02, 0x63, 0x04, 0x63, 0x04, 0x77, 0x08, 0x3C, /* '&' 0x26 */ 0x0E, 0x0C, 0xC3, 0x30, 0xCC, 0x1E, 0x03, 0x03, 0xC1, 0x9B, 0xC2, 0xF0, 0xEC, 0x19, 0x8F, 0x3C, 0x40, /* ''' 0x27 */ 0xFE, /* '(' 0x28 */ 0x13, 0x26, 0x6C, 0xCC, 0xCC, 0xC4, 0x66, 0x23, 0x10, /* ')' 0x29 */ 0x8C, 0x46, 0x63, 0x33, 0x33, 0x32, 0x66, 0x4C, 0x80, /* '*' 0x2A */ 0x25, 0x7E, 0xA5, 0x00, /* '+' 0x2B */ 0x30, 0xC3, 0x3F, 0x30, 0xC3, 0x0C, /* ',' 0x2C */ 0xD6, /* '-' 0x2D */ 0xF0, /* '.' 0x2E */ 0xC0, /* '/' 0x2F */ 0x08, 0x44, 0x21, 0x10, 0x84, 0x42, 0x11, 0x08, 0x00, /* '0' 0x30 */ 0x3C, 0x66, 0x42, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x42, 0x66, 0x3C, /* '1' 0x31 */ 0x11, 0x3F, 0x33, 0x33, 0x33, 0x33, 0x30, /* '2' 0x32 */ 0x3E, 0x31, 0xB0, 0x78, 0x30, 0x18, 0x1C, 0x1C, 0x1C, 0x18, 0x18, 0x10, 0x08, 0x07, 0xF8, /* '3' 0x33 */ 0x3C, 0x66, 0xC3, 0xC3, 0x03, 0x06, 0x1C, 0x07, 0x03, 0xC3, 0xC3, 0x66, 0x3C, /* '4' 0x34 */ 0x0C, 0x18, 0x71, 0x62, 0xC9, 0xA3, 0x46, 0xFE, 0x18, 0x30, 0x60, 0xC0, /* '5' 0x35 */ 0x7F, 0x20, 0x10, 0x08, 0x08, 0x07, 0xF3, 0x8C, 0x03, 0x01, 0x80, 0xF0, 0x6C, 0x63, 0xE0, /* '6' 0x36 */ 0x1E, 0x31, 0x98, 0x78, 0x0C, 0x06, 0xF3, 0x8D, 0x83, 0xC1, 0xE0, 0xD0, 0x6C, 0x63, 0xE0, /* '7' 0x37 */ 0xFF, 0x03, 0x02, 0x06, 0x04, 0x0C, 0x08, 0x18, 0x18, 0x18, 0x10, 0x30, 0x30, /* '8' 0x38 */ 0x3E, 0x31, 0xB0, 0x78, 0x3C, 0x1B, 0x18, 0xF8, 0xC6, 0xC1, 0xE0, 0xF0, 0x6C, 0x63, 0xE0, /* '9' 0x39 */ 0x3C, 0x66, 0xC2, 0xC3, 0xC3, 0xC3, 0x67, 0x3B, 0x03, 0x03, 0xC2, 0x66, 0x3C, /* ':' 0x3A */ 0xC0, 0x00, 0x30, /* ';' 0x3B */ 0xC0, 0x00, 0x00, 0x64, 0xA0, /* '<' 0x3C */ 0x00, 0x81, 0xC7, 0x8E, 0x0C, 0x07, 0x80, 0x70, 0x0E, 0x01, 0x80, /* '=' 0x3D */ 0xFF, 0x80, 0x00, 0x1F, 0xF0, /* '>' 0x3E */ 0xE0, 0x1C, 0x03, 0x80, 0x30, 0x70, 0xE3, 0x81, 0x00, /* '?' 0x3F */ 0x3E, 0x31, 0xB0, 0x78, 0x30, 0x18, 0x18, 0x38, 0x18, 0x18, 0x0C, 0x00, 0x00, 0x01, 0x80, /* '@' 0x40 */ 0x03, 0xF0, 0x06, 0x0E, 0x06, 0x01, 0x86, 0x00, 0x66, 0x1D, 0xBB, 0x31, 0xCF, 0x18, 0xC7, 0x98, 0x63, 0xCC, 0x31, 0xE6, 0x11, 0xB3, 0x99, 0xCC, 0xF7, 0x86, 0x00, 0x01, 0x80, 0x00, 0x70, 0x40, 0x0F, 0xE0, /* 'A' 0x41 */ 0x06, 0x00, 0xF0, 0x0F, 0x00, 0x90, 0x19, 0x81, 0x98, 0x10, 0x83, 0x0C, 0x3F, 0xC2, 0x04, 0x60, 0x66, 0x06, 0xC0, 0x30, /* 'B' 0x42 */ 0xFF, 0x18, 0x33, 0x03, 0x60, 0x6C, 0x0D, 0x83, 0x3F, 0xC6, 0x06, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x6F, 0xF8, /* 'C' 0x43 */ 0x1F, 0x86, 0x19, 0x81, 0xA0, 0x3C, 0x01, 0x80, 0x30, 0x06, 0x00, 0xC0, 0x68, 0x0D, 0x83, 0x18, 0x61, 0xF0, /* 'D' 0x44 */ 0xFF, 0x18, 0x33, 0x03, 0x60, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x03, 0x60, 0xCF, 0xF0, /* 'E' 0x45 */ 0xFF, 0xE0, 0x30, 0x18, 0x0C, 0x06, 0x03, 0xFD, 0x80, 0xC0, 0x60, 0x30, 0x18, 0x0F, 0xF8, /* 'F' 0x46 */ 0xFF, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xFE, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, /* 'G' 0x47 */ 0x0F, 0x83, 0x0E, 0x60, 0x66, 0x03, 0xC0, 0x0C, 0x00, 0xC1, 0xFC, 0x03, 0xC0, 0x36, 0x03, 0x60, 0x73, 0x0F, 0x0F, 0x10, /* 'H' 0x48 */ 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xFF, 0xFE, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x06, /* 'I' 0x49 */ 0xFF, 0xFF, 0xFF, 0xC0, /* 'J' 0x4A */ 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x83, 0x07, 0x8F, 0x1E, 0x27, 0x80, /* 'K' 0x4B */ 0xC0, 0xF0, 0x6C, 0x33, 0x18, 0xCC, 0x37, 0x0F, 0xC3, 0x98, 0xC3, 0x30, 0xCC, 0x1B, 0x03, 0xC0, 0xC0, /* 'L' 0x4C */ 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xFF, /* 'M' 0x4D */ 0xE0, 0x3F, 0x01, 0xFC, 0x1F, 0xE0, 0xFD, 0x05, 0xEC, 0x6F, 0x63, 0x79, 0x13, 0xCD, 0x9E, 0x6C, 0xF1, 0x47, 0x8E, 0x3C, 0x71, 0x80, /* 'N' 0x4E */ 0xE0, 0x7C, 0x0F, 0xC1, 0xE8, 0x3D, 0x87, 0x98, 0xF1, 0x1E, 0x33, 0xC3, 0x78, 0x6F, 0x07, 0xE0, 0x7C, 0x0E, /* 'O' 0x4F */ 0x0F, 0x81, 0x83, 0x18, 0x0C, 0xC0, 0x6C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1B, 0x01, 0x98, 0x0C, 0x60, 0xC0, 0xF8, 0x00, /* 'P' 0x50 */ 0xFF, 0x30, 0x6C, 0x0F, 0x03, 0xC0, 0xF0, 0x6F, 0xF3, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x00, /* 'Q' 0x51 */ 0x0F, 0x81, 0x83, 0x18, 0x0C, 0xC0, 0x6C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1B, 0x01, 0x98, 0x6C, 0x60, 0xC0, 0xFB, 0x00, 0x08, /* 'R' 0x52 */ 0xFF, 0x8C, 0x0E, 0xC0, 0x6C, 0x06, 0xC0, 0x6C, 0x0C, 0xFF, 0x8C, 0x0E, 0xC0, 0x6C, 0x06, 0xC0, 0x6C, 0x06, 0xC0, 0x70, /* 'S' 0x53 */ 0x3F, 0x18, 0x6C, 0x0F, 0x03, 0xC0, 0x1E, 0x01, 0xF0, 0x0E, 0x00, 0xF0, 0x3C, 0x0D, 0x86, 0x3F, 0x00, /* 'T' 0x54 */ 0xFF, 0x86, 0x03, 0x01, 0x80, 0xC0, 0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x01, 0x80, 0xC0, /* 'U' 0x55 */ 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xB0, 0x61, 0xF0, /* 'V' 0x56 */ 0xC0, 0x6C, 0x0D, 0x81, 0x10, 0x63, 0x0C, 0x61, 0x04, 0x60, 0xCC, 0x19, 0x01, 0x60, 0x3C, 0x07, 0x00, 0x60, /* 'W' 0x57 */ 0xC1, 0x81, 0x61, 0xC3, 0x61, 0xC3, 0x61, 0x43, 0x62, 0x62, 0x22, 0x66, 0x32, 0x26, 0x36, 0x26, 0x14, 0x34, 0x14, 0x34, 0x1C, 0x1C, 0x18, 0x1C, 0x08, 0x18, /* 'X' 0x58 */ 0xC0, 0xD8, 0x66, 0x18, 0xCC, 0x1E, 0x07, 0x00, 0xC0, 0x78, 0x32, 0x0C, 0xC6, 0x1B, 0x07, 0xC0, 0xC0, /* 'Y' 0x59 */ 0xC0, 0x36, 0x06, 0x30, 0xC3, 0x0C, 0x19, 0x81, 0xD8, 0x0F, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, /* 'Z' 0x5A */ 0xFF, 0xC0, 0x60, 0x30, 0x0C, 0x06, 0x03, 0x01, 0xC0, 0x60, 0x30, 0x18, 0x06, 0x03, 0x00, 0xFF, 0xC0, /* '[' 0x5B */ 0xFB, 0x6D, 0xB6, 0xDB, 0x6D, 0xB6, 0xE0, /* '\' 0x5C */ 0x84, 0x10, 0x84, 0x10, 0x84, 0x10, 0x84, 0x10, 0x80, /* ']' 0x5D */ 0xED, 0xB6, 0xDB, 0x6D, 0xB6, 0xDB, 0xE0, /* '^' 0x5E */ 0x30, 0x60, 0xA2, 0x44, 0xD8, 0xA1, 0x80, /* '_' 0x5F */ 0xFF, 0xC0, /* '`' 0x60 */ 0xC6, 0x30, /* 'a' 0x61 */ 0x7E, 0x71, 0xB0, 0xC0, 0x60, 0xF3, 0xDB, 0x0D, 0x86, 0xC7, 0x3D, 0xC0, /* 'b' 0x62 */ 0xC0, 0x60, 0x30, 0x1B, 0xCE, 0x36, 0x0F, 0x07, 0x83, 0xC1, 0xE0, 0xF0, 0x7C, 0x6D, 0xE0, /* 'c' 0x63 */ 0x3C, 0x66, 0xC3, 0xC0, 0xC0, 0xC0, 0xC0, 0xC3, 0x66, 0x3C, /* 'd' 0x64 */ 0x03, 0x03, 0x03, 0x3B, 0x67, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x67, 0x3B, /* 'e' 0x65 */ 0x3C, 0x66, 0xC3, 0xC3, 0xFF, 0xC0, 0xC0, 0xC3, 0x66, 0x3C, /* 'f' 0x66 */ 0x36, 0x6F, 0x66, 0x66, 0x66, 0x66, 0x60, /* 'g' 0x67 */ 0x3B, 0x67, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x67, 0x3B, 0x03, 0x03, 0xC6, 0x7C, /* 'h' 0x68 */ 0xC0, 0xC0, 0xC0, 0xDE, 0xE3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, /* 'i' 0x69 */ 0xC3, 0xFF, 0xFF, 0xC0, /* 'j' 0x6A */ 0x30, 0x03, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0xE0, /* 'k' 0x6B */ 0xC0, 0xC0, 0xC0, 0xC2, 0xC4, 0xCC, 0xD8, 0xF8, 0xEC, 0xC4, 0xC6, 0xC3, 0xC3, /* 'l' 0x6C */ 0xFF, 0xFF, 0xFF, 0xC0, /* 'm' 0x6D */ 0xDE, 0xF7, 0x1C, 0xF0, 0xC7, 0x86, 0x3C, 0x31, 0xE1, 0x8F, 0x0C, 0x78, 0x63, 0xC3, 0x1E, 0x18, 0xC0, /* 'n' 0x6E */ 0xDE, 0xE3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, /* 'o' 0x6F */ 0x3C, 0x66, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x66, 0x3C, /* 'p' 0x70 */ 0xDE, 0x71, 0xB0, 0x78, 0x3C, 0x1E, 0x0F, 0x07, 0x83, 0xE3, 0x6F, 0x30, 0x18, 0x0C, 0x00, /* 'q' 0x71 */ 0x3B, 0x67, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x67, 0x3B, 0x03, 0x03, 0x03, /* 'r' 0x72 */ 0xDF, 0x31, 0x8C, 0x63, 0x18, 0xC6, 0x00, /* 's' 0x73 */ 0x3E, 0xE3, 0xC0, 0xC0, 0xE0, 0x3C, 0x07, 0xC3, 0xE3, 0x7E, /* 't' 0x74 */ 0x66, 0xF6, 0x66, 0x66, 0x66, 0x67, /* 'u' 0x75 */ 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC7, 0x7B, /* 'v' 0x76 */ 0xC1, 0xA0, 0x98, 0xCC, 0x42, 0x21, 0xB0, 0xD0, 0x28, 0x1C, 0x0C, 0x00, /* 'w' 0x77 */ 0xC6, 0x1E, 0x38, 0x91, 0xC4, 0xCA, 0x66, 0xD3, 0x16, 0xD0, 0xA6, 0x87, 0x1C, 0x38, 0xC0, 0xC6, 0x00, /* 'x' 0x78 */ 0x87, 0x89, 0xB1, 0xC3, 0x07, 0x1E, 0x26, 0xC5, 0x0C, /* 'y' 0x79 */ 0xC1, 0x43, 0x63, 0x62, 0x26, 0x36, 0x34, 0x1C, 0x1C, 0x18, 0x18, 0x18, 0x10, 0x60, /* 'z' 0x7A */ 0xFE, 0x0C, 0x30, 0xC1, 0x86, 0x18, 0x20, 0xC1, 0xFC, /* '{' 0x7B */ 0x36, 0x66, 0x66, 0x6E, 0xCE, 0x66, 0x66, 0x66, 0x30, /* '|' 0x7C */ 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, /* '}' 0x7D */ 0xC6, 0x66, 0x66, 0x67, 0x37, 0x66, 0x66, 0x66, 0xC0, /* '~' 0x7E */ 0x61, 0x24, 0x38, /* 0x7F */ 0xFF, 0xFC, 0x00, 0x63, 0xE3, 0x31, 0x99, 0x04, 0xC8, 0x66, 0x06, 0x30, 0x61, 0x83, 0x0C, 0x18, 0x60, 0x03, 0x06, 0x18, 0x00, 0xFF, 0xFC, /* 0x80 */ 0xFF, 0x01, 0x80, 0x18, 0x01, 0x80, 0x18, 0x01, 0xFE, 0x18, 0x31, 0x83, 0x18, 0x31, 0x83, 0x18, 0x31, 0x83, 0x18, 0x30, 0x03, 0x00, 0x30, 0x0E, /* 0x81 */ 0x0C, 0x18, 0x00, 0xFF, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, /* 0x82 */ 0xDC, /* 0x83 */ 0x18, 0x89, 0xFC, 0x63, 0x18, 0xC6, 0x31, 0x8C, 0x00, /* 0x84 */ 0xDA, 0x76, /* 0x85 */ 0xCC, 0xC0, /* 0x86 */ 0x18, 0x18, 0x18, 0x18, 0xFF, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, /* 0x87 */ 0x18, 0x18, 0x18, 0x18, 0xFF, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0xFF, 0x18, 0x18, 0x18, 0x18, /* 0x88 */ 0x07, 0xC6, 0x13, 0x00, 0xC0, 0x60, 0x3F, 0xE6, 0x03, 0xFC, 0x60, 0x0C, 0x03, 0x00, 0x61, 0x07, 0xC0, /* 0x89 */ 0x70, 0x80, 0x22, 0x20, 0x08, 0x90, 0x02, 0x24, 0x00, 0x72, 0x00, 0x00, 0x80, 0x00, 0x40, 0x00, 0x10, 0x00, 0x09, 0xC7, 0x84, 0x8B, 0x31, 0x22, 0x84, 0x88, 0xB3, 0x21, 0xC7, 0x80, /* 0x8A */ 0x3F, 0x80, 0x18, 0xC0, 0x0C, 0x60, 0x06, 0x30, 0x03, 0x18, 0x01, 0x8C, 0x00, 0xC7, 0xF8, 0x63, 0x06, 0x31, 0x81, 0x90, 0xC0, 0xD8, 0x60, 0x6C, 0x30, 0x6C, 0x1F, 0xE0, /* 0x8B */ 0x69, /* 0x8C */ 0xC0, 0xC0, 0x60, 0x60, 0x30, 0x30, 0x18, 0x18, 0x0C, 0x0C, 0x06, 0x06, 0x03, 0xFF, 0xF9, 0x81, 0x86, 0xC0, 0xC1, 0xE0, 0x60, 0xF0, 0x30, 0x78, 0x18, 0x6C, 0x0F, 0xE0, /* 0x8D */ 0x0C, 0x06, 0x0C, 0x1B, 0x0C, 0xC6, 0x33, 0x0D, 0x83, 0xC0, 0xF0, 0x3E, 0x0D, 0xC3, 0x38, 0xC7, 0x30, 0xEC, 0x1C, /* 0x8E */ 0xFF, 0x01, 0x80, 0x18, 0x01, 0x80, 0x18, 0x01, 0xFE, 0x18, 0x31, 0x83, 0x18, 0x31, 0x83, 0x18, 0x31, 0x83, 0x18, 0x30, /* 0x8F */ 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3F, 0xFE, 0x0C, 0x01, 0x80, /* 0x90 */ 0x60, 0x7C, 0x18, 0x0D, 0xE7, 0x1B, 0x0D, 0x86, 0xC3, 0x61, 0xB0, 0xD8, 0x6C, 0x36, 0x18, 0x18, 0x08, 0x08, /* 0x91 */ 0x6B, /* 0x92 */ 0xD6, /* 0x93 */ 0x4C, 0xA5, 0xB0, /* 0x94 */ 0xDA, 0x53, 0x20, /* 0x95 */ 0x6F, 0xFF, 0x60, /* 0x96 */ 0xFE, /* 0x97 */ 0xFF, 0xFF, /* 0x98 */ /* 0x99 */ 0xFC, 0xE1, 0xCC, 0x38, 0x73, 0x0E, 0x1C, 0xC3, 0x8F, 0x30, 0xD2, 0xCC, 0x34, 0xB3, 0x0D, 0x6C, 0xC3, 0x53, 0x30, 0xCC, 0xCC, 0x33, 0x30, /* 0x9A */ 0x7E, 0x03, 0x30, 0x19, 0x80, 0xCC, 0x06, 0x60, 0x33, 0xF9, 0x98, 0x6C, 0xC3, 0x46, 0x1E, 0x3F, 0x80, /* 0x9B */ 0x96, /* 0x9C */ 0xC3, 0x03, 0x0C, 0x0C, 0x30, 0x30, 0xC0, 0xC3, 0x03, 0xFF, 0xEC, 0x30, 0xF0, 0xC3, 0xC3, 0x0F, 0x0F, 0xE0, /* 0x9D */ 0x0C, 0x30, 0x46, 0x3C, 0xDB, 0x34, 0x70, 0xF1, 0xB3, 0x36, 0x3C, 0x20, /* 0x9E */ 0x60, 0x7C, 0x18, 0x0D, 0xE7, 0x3B, 0x0D, 0x86, 0xC3, 0x61, 0xB0, 0xD8, 0x6C, 0x36, 0x18, /* 0x9F */ 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xFF, 0x18, 0x18, /* 0xA0 */ /* 0xA1 */ 0x21, 0x07, 0x8C, 0x0F, 0x06, 0x61, 0x98, 0xC3, 0x30, 0xD8, 0x1E, 0x07, 0x00, 0xC0, 0x60, 0x18, 0x0C, 0x03, 0x00, /* 0xA2 */ 0x66, 0x18, 0xC1, 0x43, 0x63, 0x62, 0x26, 0x36, 0x34, 0x1C, 0x1C, 0x18, 0x18, 0x18, 0x10, 0x60, /* 0xA3 */ 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x83, 0x07, 0x8F, 0x1E, 0x27, 0x80, /* 0xA4 */ 0xFF, 0xDF, 0x1E, 0x3E, 0xFF, 0xC0, /* 0xA5 */ 0x00, 0xC0, 0x3F, 0xFF, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x00, /* 0xA6 */ 0xFF, 0xFC, 0x0F, 0xFF, 0xC0, /* 0xA7 */ 0x0C, 0x09, 0x0C, 0xC6, 0x63, 0x81, 0xE3, 0x19, 0x87, 0xE1, 0xB8, 0xC6, 0x41, 0xC0, 0x73, 0x19, 0x8C, 0x66, 0x1E, 0x00, /* 0xA8 */ 0x33, 0x00, 0x3F, 0xF8, 0x0C, 0x06, 0x03, 0x01, 0x80, 0xFF, 0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0xFE, /* 0xA9 */ 0x0F, 0xC0, 0x61, 0x87, 0x03, 0x9B, 0xC6, 0xD9, 0x8F, 0x60, 0x3D, 0x00, 0xF4, 0x03, 0xD8, 0x0D, 0xE6, 0x67, 0xF3, 0x86, 0x18, 0x0F, 0xC0, /* 0xAA */ 0x1F, 0x86, 0x19, 0x81, 0xB0, 0x3C, 0x01, 0x80, 0x3F, 0xC6, 0x00, 0xC0, 0x68, 0x0D, 0x83, 0x18, 0x61, 0xF0, /* 0xAB */ 0x22, 0xCF, 0x26, 0x46, 0x64, 0x40, /* 0xAC */ 0xFF, 0x80, 0xC0, 0x60, 0x30, 0x18, /* 0xAD */ /* 0xAE */ 0x0F, 0xC0, 0x61, 0x87, 0x03, 0x9F, 0xE6, 0xD0, 0x8F, 0x42, 0x3D, 0xF0, 0xF4, 0x23, 0xD0, 0x8D, 0xC2, 0x67, 0x0B, 0x86, 0x18, 0x0F, 0xC0, /* 0xAF */ 0xCC, 0x03, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x00, /* 0xB0 */ 0x74, 0x63, 0x17, 0x00, /* 0xB1 */ 0x0C, 0x06, 0x03, 0x07, 0xE0, 0xC0, 0x60, 0x30, 0x18, 0x00, 0x00, 0x3F, 0xE0, /* 0xB2 */ 0xFF, 0xFF, 0xFF, 0xC0, /* 0xB3 */ 0xC3, 0xFF, 0xFF, 0xC0, /* 0xB4 */ 0x0C, 0x3F, 0xF0, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, /* 0xB5 */ 0xC3, 0x61, 0xB0, 0xD8, 0x6C, 0x36, 0x1B, 0x0D, 0x86, 0xE7, 0x7D, 0xF0, 0x18, 0x0C, 0x00, /* 0xB6 */ 0x3F, 0x7E, 0xF2, 0xF2, 0xF2, 0xF2, 0xF2, 0x72, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, /* 0xB7 */ 0xE0, /* 0xB8 */ 0x66, 0x00, 0x3C, 0x66, 0xC3, 0xC3, 0xFF, 0xC0, 0xC0, 0xC3, 0x66, 0x3C, /* 0xB9 */ 0xC1, 0x81, 0x83, 0x03, 0x86, 0x05, 0x0C, 0xEB, 0x1A, 0x32, 0x34, 0x66, 0x68, 0xC4, 0xD1, 0x8D, 0xB3, 0x0B, 0x3A, 0x1E, 0x04, 0x1C, 0x08, 0x1B, 0xC0, /* 0xBA */ 0x3C, 0x46, 0xC3, 0x80, 0xF8, 0x80, 0x80, 0xC3, 0x46, 0x3C, /* 0xBB */ 0x89, 0x98, 0x99, 0x3C, 0xD1, 0x00, /* 0xBC */ 0x30, 0x03, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0xE0, /* 0xBD */ 0x3F, 0x18, 0x6C, 0x0F, 0x03, 0xC0, 0x1E, 0x01, 0xF0, 0x0E, 0x00, 0xF0, 0x3C, 0x0D, 0x86, 0x3F, 0x00, /* 0xBE */ 0x3E, 0xE3, 0xC0, 0xC0, 0x60, 0x3C, 0x07, 0xC3, 0xE3, 0x7E, /* 0xBF */ 0xCC, 0x03, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, /* 0xC0 */ 0x06, 0x00, 0xF0, 0x0F, 0x00, 0x90, 0x19, 0x81, 0x98, 0x10, 0x83, 0x0C, 0x3F, 0xC2, 0x04, 0x60, 0x66, 0x06, 0xC0, 0x30, /* 0xC1 */ 0xFF, 0x18, 0x03, 0x00, 0x60, 0x0C, 0x01, 0x80, 0x3F, 0xE6, 0x06, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x6F, 0xF8, /* 0xC2 */ 0xFF, 0x18, 0x33, 0x03, 0x60, 0x6C, 0x0D, 0x83, 0x3F, 0xC6, 0x06, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x6F, 0xF8, /* 0xC3 */ 0xFF, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, /* 0xC4 */ 0x1F, 0xF0, 0x60, 0xC1, 0x83, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x83, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x03, 0x0C, 0x0C, 0xFF, 0xFF, 0x00, 0x3C, 0x00, 0xF0, 0x03, /* 0xC5 */ 0xFF, 0xE0, 0x30, 0x18, 0x0C, 0x06, 0x03, 0xFD, 0x80, 0xC0, 0x60, 0x30, 0x18, 0x0F, 0xF8, /* 0xC6 */ 0x61, 0x86, 0x31, 0x8C, 0x19, 0x98, 0x19, 0x98, 0x0D, 0xB0, 0x07, 0xE0, 0x03, 0xC0, 0x07, 0xE0, 0x0D, 0xB0, 0x19, 0x98, 0x31, 0x8C, 0x61, 0x86, 0xC1, 0x83, /* 0xC7 */ 0x3F, 0x18, 0x6C, 0x0F, 0x03, 0x00, 0xC0, 0x60, 0xF0, 0x06, 0x00, 0xF0, 0x3C, 0x0D, 0x86, 0x3F, 0x00, /* 0xC8 */ 0xC0, 0xF8, 0x1F, 0x07, 0xE0, 0xBC, 0x37, 0x8C, 0xF1, 0x1E, 0x63, 0xD8, 0x7A, 0x0F, 0xC1, 0xF0, 0x3E, 0x06, /* 0xC9 */ 0x11, 0x03, 0xE0, 0x00, 0x60, 0x7C, 0x0F, 0x83, 0xF0, 0x5E, 0x1B, 0xC6, 0x78, 0x8F, 0x31, 0xEC, 0x3D, 0x07, 0xE0, 0xF8, 0x1F, 0x03, /* 0xCA */ 0xC1, 0xB0, 0xCC, 0x63, 0x30, 0xD8, 0x3C, 0x0F, 0x03, 0xE0, 0xDC, 0x33, 0x8C, 0x73, 0x0E, 0xC1, 0xC0, /* 0xCB */ 0x3F, 0xCC, 0x33, 0x0C, 0xC3, 0x30, 0xCC, 0x33, 0x0C, 0xC3, 0x30, 0xC8, 0x36, 0x0D, 0x83, 0xC0, 0xC0, /* 0xCC */ 0xE0, 0x3F, 0x01, 0xFC, 0x1F, 0xE0, 0xFD, 0x05, 0xEC, 0x6F, 0x63, 0x79, 0x13, 0xCD, 0x9E, 0x6C, 0xF1, 0x47, 0x8E, 0x3C, 0x71, 0x80, /* 0xCD */ 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xFF, 0xFE, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x06, /* 0xCE */ 0x0F, 0x81, 0x83, 0x18, 0x0C, 0xC0, 0x6C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1B, 0x01, 0x98, 0x0C, 0x60, 0xC0, 0xF8, 0x00, /* 0xCF */ 0xFF, 0xF8, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x06, /* 0xD0 */ 0xFF, 0x30, 0x6C, 0x0F, 0x03, 0xC0, 0xF0, 0x6F, 0xF3, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x00, /* 0xD1 */ 0x1F, 0x86, 0x19, 0x81, 0xA0, 0x3C, 0x01, 0x80, 0x30, 0x06, 0x00, 0xC0, 0x68, 0x0D, 0x83, 0x18, 0x61, 0xF0, /* 0xD2 */ 0xFF, 0x86, 0x03, 0x01, 0x80, 0xC0, 0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x01, 0x80, 0xC0, /* 0xD3 */ 0xC0, 0xF0, 0x66, 0x19, 0x8C, 0x33, 0x0D, 0x81, 0xE0, 0x70, 0x0C, 0x06, 0x01, 0x80, 0xC0, 0x30, 0x00, /* 0xD4 */ 0x03, 0x00, 0x0C, 0x01, 0xFE, 0x1C, 0xCE, 0xE3, 0x1F, 0x0C, 0x3C, 0x30, 0xF0, 0xC3, 0xE3, 0x1D, 0xCC, 0xE3, 0xFF, 0x00, 0xC0, 0x03, 0x00, /* 0xD5 */ 0xC0, 0xD8, 0x66, 0x18, 0xCC, 0x1E, 0x07, 0x00, 0xC0, 0x78, 0x32, 0x0C, 0xC6, 0x1B, 0x07, 0xC0, 0xC0, /* 0xD6 */ 0xC0, 0x66, 0x03, 0x30, 0x19, 0x80, 0xCC, 0x06, 0x60, 0x33, 0x01, 0x98, 0x0C, 0xC0, 0x66, 0x03, 0x30, 0x19, 0x80, 0xCF, 0xFF, 0x80, 0x0C, 0x00, 0x60, /* 0xD7 */ 0xC1, 0xE0, 0xF0, 0x78, 0x3C, 0x1E, 0x0F, 0x06, 0xFF, 0x01, 0x80, 0xC0, 0x60, 0x30, 0x18, /* 0xD8 */ 0xC3, 0x1E, 0x18, 0xF0, 0xC7, 0x86, 0x3C, 0x31, 0xE1, 0x8F, 0x0C, 0x78, 0x63, 0xC3, 0x1E, 0x18, 0xF0, 0xC7, 0x86, 0x3F, 0xFF, 0x80, /* 0xD9 */ 0xC3, 0x19, 0x86, 0x33, 0x0C, 0x66, 0x18, 0xCC, 0x31, 0x98, 0x63, 0x30, 0xC6, 0x61, 0x8C, 0xC3, 0x19, 0x86, 0x33, 0x0C, 0x66, 0x18, 0xCF, 0xFF, 0xE0, 0x00, 0xC0, 0x01, 0x80, /* 0xDA */ 0xF8, 0x00, 0xC0, 0x06, 0x00, 0x30, 0x01, 0x80, 0x0F, 0xF0, 0x60, 0xC3, 0x03, 0x18, 0x18, 0xC0, 0xC6, 0x06, 0x30, 0x61, 0xFE, 0x00, /* 0xDB */ 0xC0, 0x0F, 0x00, 0x3C, 0x00, 0xF0, 0x03, 0xC0, 0x0F, 0xFE, 0x3C, 0x0C, 0xF0, 0x1B, 0xC0, 0x6F, 0x01, 0xBC, 0x06, 0xF0, 0x33, 0xFF, 0x8C, /* 0xDC */ 0xC0, 0x18, 0x03, 0x00, 0x60, 0x0C, 0x01, 0xFF, 0x30, 0x36, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x6F, 0xF8, /* 0xDD */ 0x3F, 0x0C, 0x33, 0x83, 0x60, 0x20, 0x06, 0x00, 0x47, 0xF8, 0x01, 0xC0, 0x78, 0x0D, 0x81, 0x30, 0xC1, 0xF0, /* 0xDE */ 0xC0, 0xF8, 0x61, 0x83, 0x31, 0x80, 0xD8, 0xC0, 0x6C, 0xC0, 0x1E, 0x60, 0x0F, 0xF0, 0x07, 0x98, 0x03, 0xCC, 0x01, 0xE3, 0x01, 0xB1, 0x80, 0xD8, 0x60, 0xCC, 0x0F, 0x80, /* 0xDF */ 0x3F, 0xD8, 0x3C, 0x0F, 0x03, 0xC0, 0xD8, 0x33, 0xFC, 0x33, 0x18, 0xCC, 0x36, 0x0D, 0x83, 0xC0, 0xC0, /* 0xE0 */ 0x7E, 0x71, 0xB0, 0xC0, 0x60, 0xF3, 0xDB, 0x0D, 0x86, 0xC7, 0x3D, 0xC0, /* 0xE1 */ 0x03, 0x1F, 0x78, 0x40, 0xFC, 0xE6, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x66, 0x3C, /* 0xE2 */ 0xFD, 0x8F, 0x0E, 0x3F, 0xDF, 0xB1, 0xE1, 0xC7, 0xF8, /* 0xE3 */ 0xFE, 0x31, 0x8C, 0x63, 0x18, 0xC6, 0x00, /* 0xE4 */ 0x1F, 0x83, 0x30, 0x66, 0x0C, 0xC1, 0x98, 0x33, 0x06, 0x61, 0x8C, 0x31, 0x9F, 0xFF, 0x01, 0xE0, 0x30, /* 0xE5 */ 0x3C, 0x66, 0xC3, 0xC3, 0xFF, 0xC0, 0xC0, 0xC3, 0x66, 0x3C, /* 0xE6 */ 0xC6, 0x36, 0x66, 0x36, 0xC1, 0xF8, 0x0F, 0x01, 0xF8, 0x36, 0xC6, 0x66, 0xC6, 0x38, 0x61, /* 0xE7 */ 0x79, 0x8C, 0x18, 0x30, 0x43, 0x01, 0xE3, 0xC6, 0xF8, /* 0xE8 */ 0xC7, 0xC7, 0xCF, 0xCB, 0xCB, 0xD3, 0xD3, 0xF3, 0xE3, 0xE3, /* 0xE9 */ 0x66, 0x18, 0xC7, 0xC7, 0xCF, 0xCB, 0xCB, 0xD3, 0xD3, 0xF3, 0xE3, 0xE3, /* 0xEA */ 0xC7, 0x9B, 0x66, 0x8E, 0x1E, 0x36, 0x66, 0xC7, 0x84, /* 0xEB */ 0x7E, 0xCD, 0x9B, 0x36, 0x6C, 0xD9, 0xA3, 0xC7, 0x0C, /* 0xEC */ 0xE3, 0xF1, 0xF8, 0xFE, 0xFF, 0x7E, 0xAF, 0x77, 0x93, 0xC9, 0xE0, 0xC0, /* 0xED */ 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xFF, 0xC3, 0xC3, 0xC3, 0xC3, /* 0xEE */ 0x3C, 0x66, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x66, 0x3C, /* 0xEF */ 0xFF, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, /* 0xF0 */ 0xDE, 0x71, 0xB0, 0x78, 0x3C, 0x1E, 0x0F, 0x07, 0x83, 0xE3, 0x6F, 0x30, 0x18, 0x0C, 0x00, /* 0xF1 */ 0x3C, 0x66, 0xC3, 0xC0, 0xC0, 0xC0, 0xC0, 0xC3, 0x66, 0x3C, /* 0xF2 */ 0xFC, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC0, /* 0xF3 */ 0xC1, 0x43, 0x63, 0x62, 0x26, 0x36, 0x34, 0x1C, 0x1C, 0x18, 0x18, 0x18, 0x10, 0x60, /* 0xF4 */ 0x03, 0x00, 0x0C, 0x03, 0xB7, 0x19, 0xE6, 0xC3, 0x0F, 0x0C, 0x3C, 0x30, 0xF0, 0xC3, 0xC3, 0x0F, 0x0C, 0x36, 0x79, 0x8E, 0xDC, 0x03, 0x00, 0x0C, 0x00, 0x30, 0x00, /* 0xF5 */ 0x87, 0x89, 0xB1, 0xC3, 0x07, 0x1E, 0x26, 0xC5, 0x0C, /* 0xF6 */ 0xC3, 0x30, 0xCC, 0x33, 0x0C, 0xC3, 0x30, 0xCC, 0x33, 0x0C, 0xC3, 0x3F, 0xF0, 0x0C, 0x03, /* 0xF7 */ 0xC7, 0x8F, 0x1E, 0x3C, 0x6F, 0xC1, 0x83, 0x06, 0x0C, /* 0xF8 */ 0xCC, 0xF3, 0x3C, 0xCF, 0x33, 0xCC, 0xF3, 0x3C, 0xCF, 0x33, 0xCC, 0xFF, 0xF0, /* 0xF9 */ 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCF, 0xFF, 0x00, 0x30, 0x03, /* 0xFA */ 0xF0, 0x18, 0x0C, 0x06, 0x03, 0xF1, 0x8C, 0xC6, 0x63, 0x31, 0x9F, 0x80, /* 0xFB */ 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xFE, 0xF0, 0xFC, 0x3F, 0x0F, 0xC3, 0xFF, 0xB0, /* 0xFC */ 0xC0, 0xC0, 0xC0, 0xC0, 0xFE, 0xC3, 0xC3, 0xC3, 0xC3, 0xFE, /* 0xFD */ 0x3C, 0x62, 0xC3, 0x01, 0x1F, 0x01, 0x01, 0xC3, 0x62, 0x3C, /* 0xFE */ 0xC7, 0xCC, 0xC6, 0xD8, 0x3D, 0x83, 0xF8, 0x3D, 0x83, 0xD8, 0x3C, 0xC2, 0xCC, 0x6C, 0x7C, /* 0xFF */ 0x7F, 0xC3, 0xC3, 0xC3, 0x7F, 0x13, 0x33, 0x63, 0xC3, 0x83, }; const GFXglyph FreeSans9pt_Win1251Glyphs[] PROGMEM = { /* 0x01 */ { 0, 15, 15, 17, 1, -13 }, /* 0x02 */ { 29, 15, 15, 17, 1, -13 }, /* 0x03 */ { 58, 15, 16, 17, 1, -14 }, /* 0x04 */ { 88, 15, 16, 17, 1, -14 }, /* 0x05 */ { 118, 16, 15, 18, 1, -13 }, /* 0x06 */ { 148, 15, 15, 17, 1, -13 }, /* 0x07 */ { 177, 0, 0, 8, 0, 0 }, /* 0x08 */ { 177, 17, 16, 19, 1, -14 }, /* 0x09 */ { 211, 17, 12, 19, 1, -12 }, /* 0x0A */ { 237, 0, 0, 8, 0, 0 }, /* 0x0B */ { 237, 17, 16, 19, 1, -14 }, /* 0x0C */ { 271, 15, 14, 17, 1, -12 }, /* 0x0D */ { 298, 0, 0, 8, 0, 0 }, /* 0x0E */ { 298, 15, 16, 17, 1, -14 }, /* 0x0F */ { 328, 15, 15, 17, 1, -13 }, /* 0x10 */ { 357, 15, 15, 17, 1, -13 }, /* 0x11 */ { 386, 15, 16, 17, 1, -14 }, /* 0x12 */ { 416, 17, 17, 19, 1, -15 }, /* 0x13 */ { 453, 15, 16, 17, 1, -14 }, /* 0x14 */ { 483, 15, 16, 17, 1, -14 }, /* 0x15 */ { 513, 15, 16, 17, 1, -14 }, /* 0x16 */ { 543, 11, 16, 13, 1, -14 }, /* 0x17 */ { 565, 15, 16, 17, 1, -14 }, /* 0x18 */ { 595, 18, 15, 20, 1, -13 }, /* 0x19 */ { 629, 15, 16, 17, 1, -14 }, /* 0x1A */ { 659, 13, 14, 15, 1, -12 }, /* 0x1B */ { 682, 17, 16, 19, 1, -14 }, /* 0x1C */ { 716, 15, 16, 17, 1, -14 }, /* 0x1D */ { 746, 15, 15, 17, 1, -13 }, /* 0x1E */ { 775, 17, 16, 19, 1, -14 }, /* 0x1F */ { 809, 11, 16, 13, 1, -14 }, /* ' ' 0x20 */ { 831, 0, 0, 5, 0, 0 }, /* '!' 0x21 */ { 831, 2, 13, 6, 2, -12 }, /* '"' 0x22 */ { 835, 5, 4, 6, 1, -12 }, /* '#' 0x23 */ { 838, 10, 12, 10, 0, -11 }, /* '$' 0x24 */ { 853, 9, 16, 10, 1, -13 }, /* '%' 0x25 */ { 871, 16, 13, 16, 1, -12 }, /* '&' 0x26 */ { 897, 10, 13, 12, 1, -12 }, /* ''' 0x27 */ { 914, 2, 4, 4, 1, -12 }, /* '(' 0x28 */ { 915, 4, 17, 6, 1, -12 }, /* ')' 0x29 */ { 924, 4, 17, 6, 1, -12 }, /* '*' 0x2A */ { 933, 5, 5, 7, 1, -12 }, /* '+' 0x2B */ { 937, 6, 8, 11, 3, -7 }, /* ',' 0x2C */ { 943, 2, 4, 5, 2, 0 }, /* '-' 0x2D */ { 944, 4, 1, 6, 1, -4 }, /* '.' 0x2E */ { 945, 2, 1, 5, 1, 0 }, /* '/' 0x2F */ { 946, 5, 13, 5, 0, -12 }, /* '0' 0x30 */ { 955, 8, 13, 10, 1, -12 }, /* '1' 0x31 */ { 968, 4, 13, 10, 3, -12 }, /* '2' 0x32 */ { 975, 9, 13, 10, 1, -12 }, /* '3' 0x33 */ { 990, 8, 13, 10, 1, -12 }, /* '4' 0x34 */ { 1003, 7, 13, 10, 2, -12 }, /* '5' 0x35 */ { 1015, 9, 13, 10, 1, -12 }, /* '6' 0x36 */ { 1030, 9, 13, 10, 1, -12 }, /* '7' 0x37 */ { 1045, 8, 13, 10, 0, -12 }, /* '8' 0x38 */ { 1058, 9, 13, 10, 1, -12 }, /* '9' 0x39 */ { 1073, 8, 13, 10, 1, -12 }, /* ':' 0x3A */ { 1086, 2, 10, 5, 1, -9 }, /* ';' 0x3B */ { 1089, 3, 12, 5, 1, -8 }, /* '<' 0x3C */ { 1094, 9, 9, 11, 1, -8 }, /* '=' 0x3D */ { 1105, 9, 4, 11, 1, -5 }, /* '>' 0x3E */ { 1110, 9, 8, 11, 1, -7 }, /* '?' 0x3F */ { 1119, 9, 13, 10, 1, -12 }, /* '@' 0x40 */ { 1134, 17, 16, 18, 1, -12 }, /* 'A' 0x41 */ { 1168, 12, 13, 12, 0, -12 }, /* 'B' 0x42 */ { 1188, 11, 13, 12, 1, -12 }, /* 'C' 0x43 */ { 1206, 11, 13, 13, 1, -12 }, /* 'D' 0x44 */ { 1224, 11, 13, 13, 1, -12 }, /* 'E' 0x45 */ { 1242, 9, 13, 11, 1, -12 }, /* 'F' 0x46 */ { 1257, 8, 13, 11, 1, -12 }, /* 'G' 0x47 */ { 1270, 12, 13, 14, 1, -12 }, /* 'H' 0x48 */ { 1290, 11, 13, 13, 1, -12 }, /* 'I' 0x49 */ { 1308, 2, 13, 5, 2, -12 }, /* 'J' 0x4A */ { 1312, 7, 13, 10, 1, -12 }, /* 'K' 0x4B */ { 1324, 10, 13, 12, 1, -12 }, /* 'L' 0x4C */ { 1341, 8, 13, 10, 1, -12 }, /* 'M' 0x4D */ { 1354, 13, 13, 15, 1, -12 }, /* 'N' 0x4E */ { 1376, 11, 13, 13, 1, -12 }, /* 'O' 0x4F */ { 1394, 13, 13, 14, 1, -12 }, /* 'P' 0x50 */ { 1416, 10, 13, 12, 1, -12 }, /* 'Q' 0x51 */ { 1433, 13, 14, 14, 1, -12 }, /* 'R' 0x52 */ { 1456, 12, 13, 13, 1, -12 }, /* 'S' 0x53 */ { 1476, 10, 13, 12, 1, -12 }, /* 'T' 0x54 */ { 1493, 9, 13, 11, 1, -12 }, /* 'U' 0x55 */ { 1508, 11, 13, 13, 1, -12 }, /* 'V' 0x56 */ { 1526, 11, 13, 11, 0, -12 }, /* 'W' 0x57 */ { 1544, 16, 13, 17, 0, -12 }, /* 'X' 0x58 */ { 1570, 10, 13, 12, 1, -12 }, /* 'Y' 0x59 */ { 1587, 12, 13, 12, 0, -12 }, /* 'Z' 0x5A */ { 1607, 10, 13, 11, 1, -12 }, /* '[' 0x5B */ { 1624, 3, 17, 5, 1, -12 }, /* '\' 0x5C */ { 1631, 5, 13, 5, 0, -12 }, /* ']' 0x5D */ { 1640, 3, 17, 5, 0, -12 }, /* '^' 0x5E */ { 1647, 7, 7, 8, 1, -12 }, /* '_' 0x5F */ { 1654, 10, 1, 10, 0, 3 }, /* '`' 0x60 */ { 1656, 4, 3, 5, 0, -12 }, /* 'a' 0x61 */ { 1658, 9, 10, 10, 1, -9 }, /* 'b' 0x62 */ { 1670, 9, 13, 10, 1, -12 }, /* 'c' 0x63 */ { 1685, 8, 10, 9, 1, -9 }, /* 'd' 0x64 */ { 1695, 8, 13, 10, 1, -12 }, /* 'e' 0x65 */ { 1708, 8, 10, 10, 1, -9 }, /* 'f' 0x66 */ { 1718, 4, 13, 5, 1, -12 }, /* 'g' 0x67 */ { 1725, 8, 14, 10, 1, -9 }, /* 'h' 0x68 */ { 1739, 8, 13, 10, 1, -12 }, /* 'i' 0x69 */ { 1752, 2, 13, 4, 1, -12 }, /* 'j' 0x6A */ { 1756, 4, 17, 4, 0, -12 }, /* 'k' 0x6B */ { 1765, 8, 13, 9, 1, -12 }, /* 'l' 0x6C */ { 1778, 2, 13, 4, 1, -12 }, /* 'm' 0x6D */ { 1782, 13, 10, 15, 1, -9 }, /* 'n' 0x6E */ { 1799, 8, 10, 10, 1, -9 }, /* 'o' 0x6F */ { 1809, 8, 10, 10, 1, -9 }, /* 'p' 0x70 */ { 1819, 9, 13, 10, 1, -9 }, /* 'q' 0x71 */ { 1834, 8, 13, 10, 1, -9 }, /* 'r' 0x72 */ { 1847, 5, 10, 6, 1, -9 }, /* 's' 0x73 */ { 1854, 8, 10, 9, 1, -9 }, /* 't' 0x74 */ { 1864, 4, 12, 5, 1, -11 }, /* 'u' 0x75 */ { 1870, 8, 10, 10, 1, -9 }, /* 'v' 0x76 */ { 1880, 9, 10, 9, 0, -9 }, /* 'w' 0x77 */ { 1892, 13, 10, 13, 0, -9 }, /* 'x' 0x78 */ { 1909, 7, 10, 9, 1, -9 }, /* 'y' 0x79 */ { 1918, 8, 14, 9, 0, -9 }, /* 'z' 0x7A */ { 1932, 7, 10, 9, 1, -9 }, /* '{' 0x7B */ { 1941, 4, 17, 6, 1, -12 }, /* '|' 0x7C */ { 1950, 2, 17, 4, 2, -12 }, /* '}' 0x7D */ { 1955, 4, 17, 6, 1, -12 }, /* '~' 0x7E */ { 1964, 7, 3, 9, 1, -7 }, /* 0x7F */ { 1967, 13, 14, 15, 1, -12 }, /* 0x80 */ { 1990, 12, 16, 14, 1, -12 }, /* 0x81 */ { 2014, 8, 15, 11, 1, -14 }, /* 0x82 */ { 2029, 2, 3, 5, 1, 0 }, /* 0x83 */ { 2030, 5, 13, 7, 1, -12 }, /* 0x84 */ { 2039, 5, 3, 7, 1, 0 }, /* 0x85 */ { 2041, 10, 1, 12, 1, 0 }, /* 0x86 */ { 2043, 8, 16, 10, 1, -12 }, /* 0x87 */ { 2059, 8, 16, 10, 1, -12 }, /* 0x88 */ { 2075, 10, 13, 12, 1, -12 }, /* 0x89 */ { 2092, 18, 13, 18, 0, -12 }, /* 0x8A */ { 2122, 17, 13, 18, 1, -12 }, /* 0x8B */ { 2150, 2, 4, 4, 1, -6 }, /* 0x8C */ { 2151, 17, 13, 18, 1, -12 }, /* 0x8D */ { 2179, 10, 15, 11, 1, -14 }, /* 0x8E */ { 2198, 12, 13, 14, 1, -12 }, /* 0x8F */ { 2218, 11, 15, 13, 1, -12 }, /* 0x90 */ { 2239, 9, 16, 10, 1, -12 }, /* 0x91 */ { 2257, 2, 4, 4, 2, -12 }, /* 0x92 */ { 2258, 2, 4, 4, 1, -12 }, /* 0x93 */ { 2259, 5, 4, 7, 2, -12 }, /* 0x94 */ { 2262, 5, 4, 7, 1, -12 }, /* 0x95 */ { 2265, 4, 5, 7, 1, -8 }, /* 0x96 */ { 2268, 7, 1, 9, 1, -4 }, /* 0x97 */ { 2269, 16, 1, 18, 1, -4 }, /* 0x98 */ { 2271, 0, 0, 0, 0, 0 }, /* 0x99 */ { 2271, 18, 10, 18, 1, -13 }, /* 0x9A */ { 2294, 13, 10, 14, 1, -9 }, /* 0x9B */ { 2311, 2, 4, 5, 2, -6 }, /* 0x9C */ { 2312, 14, 10, 15, 1, -9 }, /* 0x9D */ { 2330, 7, 13, 9, 1, -12 }, /* 0x9E */ { 2342, 9, 13, 10, 1, -12 }, /* 0x9F */ { 2357, 8, 12, 10, 1, -9 }, /* 0xA0 */ { 2369, 0, 0, 5, 0, 0 }, /* 0xA1 */ { 2369, 10, 15, 11, 1, -14 }, /* 0xA2 */ { 2388, 8, 16, 9, 0, -11 }, /* 0xA3 */ { 2404, 7, 13, 10, 1, -12 }, /* 0xA4 */ { 2416, 7, 6, 10, 2, -8 }, /* 0xA5 */ { 2422, 10, 14, 11, 1, -13 }, /* 0xA6 */ { 2440, 2, 17, 5, 2, -12 }, /* 0xA7 */ { 2445, 9, 17, 10, 1, -12 }, /* 0xA8 */ { 2465, 9, 15, 12, 1, -14 }, /* 0xA9 */ { 2482, 14, 13, 14, 1, -12 }, /* 0xAA */ { 2505, 11, 13, 13, 1, -12 }, /* 0xAB */ { 2523, 7, 6, 9, 1, -7 }, /* 0xAC */ { 2529, 9, 5, 11, 2, -5 }, /* 0xAD */ { 2535, 0, 0, 0, 0, 0 }, /* 0xAE */ { 2535, 14, 13, 14, 1, -12 }, /* 0xAF */ { 2558, 6, 15, 5, 0, -14 }, /* 0xB0 */ { 2570, 5, 5, 11, 3, -11 }, /* 0xB1 */ { 2574, 9, 11, 11, 1, -10 }, /* 0xB2 */ { 2587, 2, 13, 4, 1, -12 }, /* 0xB3 */ { 2591, 2, 13, 4, 1, -12 }, /* 0xB4 */ { 2595, 6, 12, 7, 1, -11 }, /* 0xB5 */ { 2604, 9, 13, 10, 1, -9 }, /* 0xB6 */ { 2619, 8, 16, 10, 2, -12 }, /* 0xB7 */ { 2635, 3, 1, 5, 1, -4 }, /* 0xB8 */ { 2636, 8, 12, 10, 1, -11 }, /* 0xB9 */ { 2648, 15, 13, 17, 1, -12 }, /* 0xBA */ { 2673, 8, 10, 9, 1, -9 }, /* 0xBB */ { 2683, 7, 6, 9, 1, -7 }, /* 0xBC */ { 2689, 4, 17, 4, 0, -12 }, /* 0xBD */ { 2698, 10, 13, 12, 1, -12 }, /* 0xBE */ { 2715, 8, 10, 9, 1, -9 }, /* 0xBF */ { 2725, 6, 12, 5, -1, -11 }, /* 0xC0 */ { 2734, 12, 13, 12, 0, -12 }, /* 0xC1 */ { 2754, 11, 13, 12, 1, -12 }, /* 0xC2 */ { 2772, 11, 13, 12, 1, -12 }, /* 0xC3 */ { 2790, 8, 13, 8, 1, -12 }, /* 0xC4 */ { 2803, 14, 16, 15, 1, -12 }, /* 0xC5 */ { 2831, 9, 13, 12, 1, -12 }, /* 0xC6 */ { 2846, 16, 13, 16, 0, -12 }, /* 0xC7 */ { 2872, 10, 13, 12, 1, -12 }, /* 0xC8 */ { 2889, 11, 13, 13, 1, -12 }, /* 0xC9 */ { 2907, 11, 16, 13, 1, -15 }, /* 0xCA */ { 2929, 10, 13, 11, 1, -12 }, /* 0xCB */ { 2946, 10, 13, 12, 1, -12 }, /* 0xCC */ { 2963, 13, 13, 15, 1, -12 }, /* 0xCD */ { 2985, 11, 13, 13, 1, -12 }, /* 0xCE */ { 3003, 13, 13, 14, 1, -12 }, /* 0xCF */ { 3025, 11, 13, 13, 1, -12 }, /* 0xD0 */ { 3043, 10, 13, 12, 1, -12 }, /* 0xD1 */ { 3060, 11, 13, 13, 1, -12 }, /* 0xD2 */ { 3078, 9, 13, 11, 1, -12 }, /* 0xD3 */ { 3093, 10, 13, 11, 1, -12 }, /* 0xD4 */ { 3110, 14, 13, 15, 1, -12 }, /* 0xD5 */ { 3133, 10, 13, 12, 1, -12 }, /* 0xD6 */ { 3150, 13, 15, 13, 1, -12 }, /* 0xD7 */ { 3175, 9, 13, 11, 1, -12 }, /* 0xD8 */ { 3190, 13, 13, 15, 1, -12 }, /* 0xD9 */ { 3212, 15, 15, 15, 1, -12 }, /* 0xDA */ { 3241, 13, 13, 15, 2, -12 }, /* 0xDB */ { 3263, 14, 13, 16, 1, -12 }, /* 0xDC */ { 3286, 11, 13, 12, 1, -12 }, /* 0xDD */ { 3304, 11, 13, 13, 1, -12 }, /* 0xDE */ { 3322, 17, 13, 18, 1, -12 }, /* 0xDF */ { 3350, 10, 13, 12, 1, -12 }, /* 0xE0 */ { 3367, 9, 10, 10, 1, -9 }, /* 0xE1 */ { 3379, 8, 14, 10, 1, -13 }, /* 0xE2 */ { 3393, 7, 10, 9, 1, -9 }, /* 0xE3 */ { 3402, 5, 10, 7, 1, -9 }, /* 0xE4 */ { 3409, 11, 12, 10, 0, -9 }, /* 0xE5 */ { 3426, 8, 10, 10, 1, -9 }, /* 0xE6 */ { 3436, 12, 10, 14, 1, -9 }, /* 0xE7 */ { 3451, 7, 10, 9, 1, -9 }, /* 0xE8 */ { 3460, 8, 10, 10, 1, -9 }, /* 0xE9 */ { 3470, 8, 12, 10, 1, -11 }, /* 0xEA */ { 3482, 7, 10, 9, 1, -9 }, /* 0xEB */ { 3491, 7, 10, 8, 0, -9 }, /* 0xEC */ { 3500, 9, 10, 11, 1, -9 }, /* 0xED */ { 3512, 8, 10, 10, 1, -9 }, /* 0xEE */ { 3522, 8, 10, 10, 1, -9 }, /* 0xEF */ { 3532, 8, 10, 10, 1, -9 }, /* 0xF0 */ { 3542, 9, 13, 10, 1, -9 }, /* 0xF1 */ { 3557, 8, 10, 9, 1, -9 }, /* 0xF2 */ { 3567, 6, 10, 7, 1, -9 }, /* 0xF3 */ { 3575, 8, 14, 9, 0, -9 }, /* 0xF4 */ { 3589, 14, 15, 15, 1, -11 }, /* 0xF5 */ { 3616, 7, 10, 9, 1, -9 }, /* 0xF6 */ { 3625, 10, 12, 10, 1, -9 }, /* 0xF7 */ { 3640, 7, 10, 9, 1, -9 }, /* 0xF8 */ { 3649, 10, 10, 12, 1, -9 }, /* 0xF9 */ { 3662, 12, 12, 13, 1, -9 }, /* 0xFA */ { 3680, 9, 10, 12, 2, -9 }, /* 0xFB */ { 3692, 10, 10, 12, 1, -9 }, /* 0xFC */ { 3705, 8, 10, 9, 1, -9 }, /* 0xFD */ { 3715, 8, 10, 9, 1, -9 }, /* 0xFE */ { 3725, 12, 10, 13, 1, -9 }, /* 0xFF */ { 3740, 8, 10, 10, 1, -9 }, }; const GFXfont FreeSans9pt_Win1251 PROGMEM = { (uint8_t*)FreeSans9pt_Win1251Bitmaps, (GFXglyph*)FreeSans9pt_Win1251Glyphs, 0x01, 0xFF, 21 }; ================================================ FILE: src/graphics/niche/Fonts/FreeSans9pt_Win1252.h ================================================ // trunk-ignore-all(clang-format) #pragma once /* PROPERTIES FONT_NAME FreeSans9pt_Win1252 */ const uint8_t FreeSans9pt_Win1252Bitmaps[] PROGMEM = { /* 0x01 */ 0x07, 0x00, 0x0A, 0x00, 0x24, 0x00, 0x48, 0x01, 0x10, 0x04, 0x40, 0x10, 0xFF, 0x20, 0x02, 0x81, 0xFD, 0x00, 0x06, 0x07, 0xF4, 0x08, 0x24, 0x0F, 0x88, 0x11, 0x0F, 0xDC, 0x00, /* 0x02 */ 0x3F, 0x70, 0x81, 0x11, 0x03, 0xE4, 0x08, 0x28, 0x1F, 0xD0, 0x00, 0x60, 0x7F, 0x20, 0x02, 0x43, 0xFC, 0x44, 0x00, 0x44, 0x00, 0x48, 0x00, 0x90, 0x00, 0xA0, 0x01, 0xC0, 0x00, /* 0x03 */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x00, 0x28, 0x00, 0x31, 0x8C, 0x63, 0x18, 0xC0, 0x01, 0x80, 0x03, 0x00, 0x06, 0x20, 0x8C, 0x3E, 0x14, 0x00, 0x44, 0x01, 0x06, 0x0C, 0x03, 0xE0, /* 0x04 */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x00, 0x28, 0x82, 0x30, 0x88, 0x62, 0x08, 0xC0, 0x01, 0x80, 0x03, 0x00, 0x06, 0x3F, 0x8C, 0x3E, 0x14, 0x00, 0x44, 0x01, 0x06, 0x0C, 0x03, 0xE0, /* 0x05 */ 0x0B, 0x10, 0x14, 0xA8, 0x12, 0x50, 0x29, 0x42, 0x24, 0xA5, 0x32, 0x95, 0x5A, 0x09, 0x48, 0x09, 0x24, 0x01, 0x10, 0x01, 0x48, 0x02, 0xA4, 0x02, 0x42, 0x04, 0x01, 0x98, 0x00, 0x60, /* 0x06 */ 0x00, 0x80, 0x22, 0x80, 0x65, 0x00, 0xBE, 0xE1, 0x82, 0x4E, 0x03, 0x24, 0x04, 0x28, 0x06, 0x30, 0x12, 0x20, 0x3C, 0xA0, 0xC3, 0xFE, 0x80, 0x4D, 0x00, 0xA6, 0x01, 0x80, 0x00, /* 0x07 */ /* 0x08 */ 0x00, 0xF8, 0x00, 0x82, 0x00, 0x80, 0x83, 0xE0, 0x41, 0x10, 0x21, 0x04, 0x1B, 0x00, 0x03, 0x00, 0x01, 0x80, 0x00, 0xE0, 0x00, 0x4F, 0xE1, 0xC0, 0x0F, 0x02, 0x00, 0x03, 0x01, 0x00, 0x09, 0x88, 0x0C, 0x0C, /* 0x09 */ 0x00, 0xF8, 0x00, 0x82, 0x00, 0x80, 0x83, 0xE0, 0x41, 0x10, 0x21, 0x04, 0x1B, 0x00, 0x03, 0x00, 0x01, 0x80, 0x00, 0xE0, 0x00, 0x4F, 0xE1, 0xC0, 0x0F, 0x00, /* 0x0A */ /* 0x0B */ 0x1C, 0x1C, 0x31, 0xB1, 0x90, 0x50, 0x50, 0x10, 0x18, 0x00, 0x0C, 0x00, 0x06, 0x00, 0x02, 0x80, 0x02, 0x40, 0x01, 0x10, 0x01, 0x04, 0x01, 0x01, 0x01, 0x00, 0x41, 0x00, 0x11, 0x00, 0x07, 0x00, 0x01, 0x00, /* 0x0C */ 0x06, 0x00, 0x0A, 0x00, 0x12, 0x00, 0x32, 0x01, 0x84, 0x04, 0x10, 0x08, 0x98, 0x1C, 0x18, 0x40, 0x48, 0x82, 0x11, 0xF0, 0x74, 0x02, 0x18, 0x70, 0x2F, 0x9F, 0x80, /* 0x0D */ /* 0x0E */ 0x01, 0x00, 0x05, 0x00, 0x0A, 0x00, 0x3E, 0x00, 0x82, 0x02, 0x82, 0x06, 0x04, 0x10, 0x04, 0x20, 0x08, 0x40, 0x10, 0xFF, 0x22, 0x00, 0x29, 0xFF, 0x3F, 0x8F, 0xDF, 0x9F, 0x01, 0xC0, /* 0x0F */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x00, 0x28, 0x82, 0x36, 0x03, 0x60, 0x00, 0xCC, 0x19, 0xA4, 0x4B, 0x00, 0x06, 0x8E, 0x2B, 0x22, 0x66, 0x7C, 0xCC, 0x71, 0x98, 0x03, 0x00, /* 0x10 */ 0x03, 0x80, 0x07, 0x00, 0x0E, 0x00, 0x1E, 0x00, 0x54, 0x00, 0xA8, 0x01, 0x50, 0x02, 0xA0, 0x05, 0x20, 0x32, 0x61, 0xC4, 0x74, 0x49, 0x10, 0x6C, 0x00, 0xD8, 0x01, 0x10, 0x00, /* 0x11 */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x40, 0x29, 0x00, 0x31, 0x84, 0x63, 0x18, 0xC0, 0x00, 0x80, 0x15, 0x03, 0x7E, 0x02, 0xFA, 0x04, 0xE4, 0x18, 0x84, 0x00, 0x06, 0x0C, 0x03, 0xE0, /* 0x12 */ 0x02, 0x08, 0x01, 0x08, 0x40, 0x10, 0xC0, 0x08, 0xC0, 0x60, 0x80, 0x28, 0x04, 0x12, 0x4C, 0x10, 0x80, 0x08, 0x23, 0x0E, 0x08, 0xC4, 0x82, 0x04, 0x20, 0x83, 0x09, 0x82, 0x47, 0x01, 0x1C, 0x01, 0x30, 0x00, 0xE0, 0x00, 0x00, /* 0x13 */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x00, 0x28, 0x00, 0x31, 0x08, 0x65, 0x28, 0xC0, 0x01, 0x80, 0x03, 0x00, 0x06, 0x3F, 0x8C, 0x3E, 0x14, 0x00, 0x44, 0x01, 0x06, 0x0C, 0x03, 0xE0, /* 0x14 */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x22, 0x29, 0x83, 0x30, 0x00, 0x65, 0x14, 0xD3, 0x4D, 0xBA, 0xEB, 0x38, 0xE6, 0x00, 0x0A, 0x00, 0x24, 0x38, 0x44, 0x01, 0x07, 0x1C, 0x01, 0xC0, /* 0x15 */ 0x07, 0xC0, 0x30, 0x18, 0x80, 0x32, 0x00, 0xF8, 0x01, 0xF1, 0x09, 0xA5, 0x28, 0x40, 0x01, 0x80, 0x03, 0x00, 0x06, 0x3F, 0x8C, 0x3E, 0x14, 0x00, 0x44, 0x01, 0x06, 0x0C, 0x03, 0xE0, /* 0x16 */ 0x0C, 0x00, 0xC0, 0x1C, 0x03, 0x80, 0xF8, 0xBB, 0x36, 0xC7, 0x99, 0xF3, 0xFE, 0x3F, 0xC3, 0xF0, 0x7E, 0x0E, 0xC1, 0x8E, 0xE0, 0x20, /* 0x17 */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x00, 0x28, 0x00, 0x10, 0x01, 0x20, 0x1D, 0x44, 0x42, 0x84, 0x85, 0x00, 0x86, 0x00, 0xC4, 0x00, 0x44, 0x7C, 0x44, 0x00, 0x06, 0x0C, 0x03, 0xE0, /* 0x18 */ 0x01, 0xE0, 0x00, 0x84, 0x00, 0x40, 0x80, 0x20, 0x10, 0x08, 0x24, 0x02, 0x41, 0x00, 0x86, 0x03, 0x12, 0x03, 0xB4, 0x03, 0x52, 0x81, 0x23, 0x80, 0x70, 0xA0, 0x14, 0x28, 0x05, 0x0A, 0x01, 0x42, 0x80, 0x50, /* 0x19 */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x00, 0x28, 0x00, 0x33, 0x18, 0x60, 0x00, 0xDC, 0xE1, 0xB9, 0xC3, 0x7B, 0xC6, 0x63, 0x0A, 0x00, 0x24, 0xF0, 0x44, 0x01, 0x06, 0x0C, 0x03, 0xE0, /* 0x1A */ 0xFF, 0xFC, 0x00, 0x63, 0xE3, 0x31, 0x99, 0x04, 0xC8, 0x66, 0x06, 0x30, 0x61, 0x82, 0x0C, 0x10, 0x60, 0x03, 0x04, 0x18, 0x00, 0xFF, 0xFC, /* 0x1B */ 0x07, 0xF0, 0x06, 0x0C, 0x04, 0x01, 0x04, 0x00, 0x44, 0x22, 0x12, 0x2A, 0x89, 0x00, 0x04, 0x80, 0x02, 0x44, 0x11, 0x01, 0xF0, 0x04, 0x01, 0x0D, 0x01, 0x6A, 0x41, 0x2C, 0x00, 0x05, 0xC0, 0x0E, 0x18, 0x18, /* 0x1C */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0xC0, 0x2A, 0x00, 0x33, 0x00, 0x66, 0x00, 0xCC, 0x39, 0x80, 0x83, 0x00, 0x06, 0x00, 0x8C, 0x3E, 0x14, 0x00, 0x44, 0x01, 0x06, 0x0C, 0x03, 0xE0, /* 0x1D */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x70, 0x28, 0x00, 0x31, 0x80, 0x63, 0x18, 0xC0, 0x31, 0x80, 0x03, 0x00, 0x06, 0x60, 0x0D, 0x33, 0x12, 0x10, 0x48, 0x21, 0x23, 0x8C, 0x00, /* 0x1E */ 0x03, 0x00, 0x07, 0x9E, 0x07, 0x00, 0x86, 0x00, 0x27, 0xC0, 0x0F, 0xC0, 0x07, 0x8C, 0x62, 0x06, 0x31, 0x20, 0x00, 0x90, 0x00, 0x48, 0x00, 0x24, 0x3E, 0x11, 0x00, 0x10, 0x40, 0x10, 0x18, 0x30, 0x03, 0xE0, /* 0x1F */ 0x18, 0x02, 0x80, 0x4C, 0x16, 0x41, 0x24, 0x3C, 0x88, 0x6E, 0x65, 0xF2, 0x78, 0x46, 0x88, 0xCF, 0x18, 0x02, 0x80, 0x8C, 0x60, 0x70, /* ' ' 0x20 */ /* '!' 0x21 */ 0xFF, 0xFF, 0xF0, 0xC0, /* '"' 0x22 */ 0xDE, 0xF7, 0x20, /* '#' 0x23 */ 0x09, 0x86, 0x41, 0x91, 0xFF, 0x13, 0x04, 0xC3, 0x20, 0xC8, 0xFF, 0x89, 0x82, 0x61, 0x90, /* '$' 0x24 */ 0x10, 0x1F, 0x14, 0xDA, 0x3D, 0x1E, 0x83, 0x40, 0x78, 0x17, 0x08, 0xF4, 0x7A, 0x35, 0x33, 0xF0, 0x40, 0x20, /* '%' 0x25 */ 0x38, 0x10, 0xEC, 0x20, 0xC6, 0x20, 0xC6, 0x40, 0xC6, 0x40, 0x6C, 0x80, 0x39, 0x00, 0x01, 0x3C, 0x02, 0x77, 0x02, 0x63, 0x04, 0x63, 0x04, 0x77, 0x08, 0x3C, /* '&' 0x26 */ 0x0E, 0x0C, 0xC3, 0x30, 0xCC, 0x1E, 0x03, 0x03, 0xC1, 0x9B, 0xC2, 0xF0, 0xEC, 0x19, 0x8F, 0x3C, 0x40, /* ''' 0x27 */ 0xFE, /* '(' 0x28 */ 0x13, 0x26, 0x6C, 0xCC, 0xCC, 0xC4, 0x66, 0x23, 0x10, /* ')' 0x29 */ 0x8C, 0x46, 0x63, 0x33, 0x33, 0x32, 0x66, 0x4C, 0x80, /* '*' 0x2A */ 0x25, 0x7E, 0xA5, 0x00, /* '+' 0x2B */ 0x30, 0xC3, 0x3F, 0x30, 0xC3, 0x0C, /* ',' 0x2C */ 0xD6, /* '-' 0x2D */ 0xF0, /* '.' 0x2E */ 0xC0, /* '/' 0x2F */ 0x08, 0x44, 0x21, 0x10, 0x84, 0x42, 0x11, 0x08, 0x00, /* '0' 0x30 */ 0x3C, 0x66, 0x42, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x42, 0x66, 0x3C, /* '1' 0x31 */ 0x11, 0x3F, 0x33, 0x33, 0x33, 0x33, 0x30, /* '2' 0x32 */ 0x3E, 0x31, 0xB0, 0x78, 0x30, 0x18, 0x1C, 0x1C, 0x1C, 0x18, 0x18, 0x10, 0x08, 0x07, 0xF8, /* '3' 0x33 */ 0x3C, 0x66, 0xC3, 0xC3, 0x03, 0x06, 0x1C, 0x07, 0x03, 0xC3, 0xC3, 0x66, 0x3C, /* '4' 0x34 */ 0x0C, 0x18, 0x71, 0x62, 0xC9, 0xA3, 0x46, 0xFE, 0x18, 0x30, 0x60, 0xC0, /* '5' 0x35 */ 0x7F, 0x20, 0x10, 0x08, 0x08, 0x07, 0xF3, 0x8C, 0x03, 0x01, 0x80, 0xF0, 0x6C, 0x63, 0xE0, /* '6' 0x36 */ 0x1E, 0x31, 0x98, 0x78, 0x0C, 0x06, 0xF3, 0x8D, 0x83, 0xC1, 0xE0, 0xD0, 0x6C, 0x63, 0xE0, /* '7' 0x37 */ 0xFF, 0x03, 0x02, 0x06, 0x04, 0x0C, 0x08, 0x18, 0x18, 0x18, 0x10, 0x30, 0x30, /* '8' 0x38 */ 0x3E, 0x31, 0xB0, 0x78, 0x3C, 0x1B, 0x18, 0xF8, 0xC6, 0xC1, 0xE0, 0xF0, 0x6C, 0x63, 0xE0, /* '9' 0x39 */ 0x3C, 0x66, 0xC2, 0xC3, 0xC3, 0xC3, 0x67, 0x3B, 0x03, 0x03, 0xC2, 0x66, 0x3C, /* ':' 0x3A */ 0xC0, 0x00, 0x30, /* ';' 0x3B */ 0xC0, 0x00, 0x00, 0x64, 0xA0, /* '<' 0x3C */ 0x00, 0x81, 0xC7, 0x8E, 0x0C, 0x07, 0x80, 0x70, 0x0E, 0x01, 0x80, /* '=' 0x3D */ 0xFF, 0x80, 0x00, 0x1F, 0xF0, /* '>' 0x3E */ 0xE0, 0x1C, 0x03, 0x80, 0x30, 0x70, 0xE3, 0x81, 0x00, /* '?' 0x3F */ 0x3E, 0x31, 0xB0, 0x78, 0x30, 0x18, 0x18, 0x38, 0x18, 0x18, 0x0C, 0x00, 0x00, 0x01, 0x80, /* '@' 0x40 */ 0x03, 0xF0, 0x06, 0x0E, 0x06, 0x01, 0x86, 0x00, 0x66, 0x1D, 0xBB, 0x31, 0xCF, 0x18, 0xC7, 0x98, 0x63, 0xCC, 0x31, 0xE6, 0x11, 0xB3, 0x99, 0xCC, 0xF7, 0x86, 0x00, 0x01, 0x80, 0x00, 0x70, 0x40, 0x0F, 0xE0, /* 'A' 0x41 */ 0x06, 0x00, 0xF0, 0x0F, 0x00, 0x90, 0x19, 0x81, 0x98, 0x10, 0x83, 0x0C, 0x3F, 0xC2, 0x04, 0x60, 0x66, 0x06, 0xC0, 0x30, /* 'B' 0x42 */ 0xFF, 0x18, 0x33, 0x03, 0x60, 0x6C, 0x0D, 0x83, 0x3F, 0xC6, 0x06, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x6F, 0xF8, /* 'C' 0x43 */ 0x1F, 0x86, 0x19, 0x81, 0xA0, 0x3C, 0x01, 0x80, 0x30, 0x06, 0x00, 0xC0, 0x68, 0x0D, 0x83, 0x18, 0x61, 0xF0, /* 'D' 0x44 */ 0xFF, 0x18, 0x33, 0x03, 0x60, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x03, 0x60, 0xCF, 0xF0, /* 'E' 0x45 */ 0xFF, 0xE0, 0x30, 0x18, 0x0C, 0x06, 0x03, 0xFD, 0x80, 0xC0, 0x60, 0x30, 0x18, 0x0F, 0xF8, /* 'F' 0x46 */ 0xFF, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xFE, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, /* 'G' 0x47 */ 0x0F, 0x83, 0x0E, 0x60, 0x66, 0x03, 0xC0, 0x0C, 0x00, 0xC1, 0xFC, 0x03, 0xC0, 0x36, 0x03, 0x60, 0x73, 0x0F, 0x0F, 0x10, /* 'H' 0x48 */ 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xFF, 0xFE, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x06, /* 'I' 0x49 */ 0xFF, 0xFF, 0xFF, 0xC0, /* 'J' 0x4A */ 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x83, 0x07, 0x8F, 0x1E, 0x27, 0x80, /* 'K' 0x4B */ 0xC0, 0xF0, 0x6C, 0x33, 0x18, 0xCC, 0x37, 0x0F, 0xC3, 0x98, 0xC3, 0x30, 0xCC, 0x1B, 0x03, 0xC0, 0xC0, /* 'L' 0x4C */ 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xFF, /* 'M' 0x4D */ 0xE0, 0x3F, 0x01, 0xFC, 0x1F, 0xE0, 0xFD, 0x05, 0xEC, 0x6F, 0x63, 0x79, 0x13, 0xCD, 0x9E, 0x6C, 0xF1, 0x47, 0x8E, 0x3C, 0x71, 0x80, /* 'N' 0x4E */ 0xE0, 0x7C, 0x0F, 0xC1, 0xE8, 0x3D, 0x87, 0x98, 0xF1, 0x1E, 0x33, 0xC3, 0x78, 0x6F, 0x07, 0xE0, 0x7C, 0x0E, /* 'O' 0x4F */ 0x0F, 0x81, 0x83, 0x18, 0x0C, 0xC0, 0x6C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1B, 0x01, 0x98, 0x0C, 0x60, 0xC0, 0xF8, 0x00, /* 'P' 0x50 */ 0xFF, 0x30, 0x6C, 0x0F, 0x03, 0xC0, 0xF0, 0x6F, 0xF3, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x00, /* 'Q' 0x51 */ 0x0F, 0x81, 0x83, 0x18, 0x0C, 0xC0, 0x6C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1B, 0x01, 0x98, 0x6C, 0x60, 0xC0, 0xFB, 0x00, 0x08, /* 'R' 0x52 */ 0xFF, 0x8C, 0x0E, 0xC0, 0x6C, 0x06, 0xC0, 0x6C, 0x0C, 0xFF, 0x8C, 0x0E, 0xC0, 0x6C, 0x06, 0xC0, 0x6C, 0x06, 0xC0, 0x70, /* 'S' 0x53 */ 0x3F, 0x18, 0x6C, 0x0F, 0x03, 0xC0, 0x1E, 0x01, 0xF0, 0x0E, 0x00, 0xF0, 0x3C, 0x0D, 0x86, 0x3F, 0x00, /* 'T' 0x54 */ 0xFF, 0x86, 0x03, 0x01, 0x80, 0xC0, 0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x01, 0x80, 0xC0, /* 'U' 0x55 */ 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xB0, 0x61, 0xF0, /* 'V' 0x56 */ 0xC0, 0x6C, 0x0D, 0x81, 0x10, 0x63, 0x0C, 0x61, 0x04, 0x60, 0xCC, 0x19, 0x01, 0x60, 0x3C, 0x07, 0x00, 0x60, /* 'W' 0x57 */ 0xC1, 0x81, 0x61, 0xC3, 0x61, 0xC3, 0x61, 0x43, 0x62, 0x62, 0x22, 0x66, 0x32, 0x26, 0x36, 0x26, 0x14, 0x34, 0x14, 0x34, 0x1C, 0x1C, 0x18, 0x1C, 0x08, 0x18, /* 'X' 0x58 */ 0xC0, 0xD8, 0x66, 0x18, 0xCC, 0x1E, 0x07, 0x00, 0xC0, 0x78, 0x32, 0x0C, 0xC6, 0x1B, 0x07, 0xC0, 0xC0, /* 'Y' 0x59 */ 0xC0, 0x36, 0x06, 0x30, 0xC3, 0x0C, 0x19, 0x81, 0xD8, 0x0F, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, /* 'Z' 0x5A */ 0xFF, 0xC0, 0x60, 0x30, 0x0C, 0x06, 0x03, 0x01, 0xC0, 0x60, 0x30, 0x18, 0x06, 0x03, 0x00, 0xFF, 0xC0, /* '[' 0x5B */ 0xFB, 0x6D, 0xB6, 0xDB, 0x6D, 0xB6, 0xE0, /* '\' 0x5C */ 0x84, 0x10, 0x84, 0x10, 0x84, 0x10, 0x84, 0x10, 0x80, /* ']' 0x5D */ 0xED, 0xB6, 0xDB, 0x6D, 0xB6, 0xDB, 0xE0, /* '^' 0x5E */ 0x30, 0x60, 0xA2, 0x44, 0xD8, 0xA1, 0x80, /* '_' 0x5F */ 0xFF, 0xC0, /* '`' 0x60 */ 0xC6, 0x30, /* 'a' 0x61 */ 0x7E, 0x71, 0xB0, 0xC0, 0x60, 0xF3, 0xDB, 0x0D, 0x86, 0xC7, 0x3D, 0xC0, /* 'b' 0x62 */ 0xC0, 0x60, 0x30, 0x1B, 0xCE, 0x36, 0x0F, 0x07, 0x83, 0xC1, 0xE0, 0xF0, 0x7C, 0x6D, 0xE0, /* 'c' 0x63 */ 0x3C, 0x66, 0xC3, 0xC0, 0xC0, 0xC0, 0xC0, 0xC3, 0x66, 0x3C, /* 'd' 0x64 */ 0x03, 0x03, 0x03, 0x3B, 0x67, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x67, 0x3B, /* 'e' 0x65 */ 0x3C, 0x66, 0xC3, 0xC3, 0xFF, 0xC0, 0xC0, 0xC3, 0x66, 0x3C, /* 'f' 0x66 */ 0x36, 0x6F, 0x66, 0x66, 0x66, 0x66, 0x60, /* 'g' 0x67 */ 0x3B, 0x67, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x67, 0x3B, 0x03, 0x03, 0xC6, 0x7C, /* 'h' 0x68 */ 0xC0, 0xC0, 0xC0, 0xDE, 0xE3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, /* 'i' 0x69 */ 0xC3, 0xFF, 0xFF, 0xC0, /* 'j' 0x6A */ 0x30, 0x03, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0xE0, /* 'k' 0x6B */ 0xC0, 0xC0, 0xC0, 0xC2, 0xC4, 0xCC, 0xD8, 0xF8, 0xEC, 0xC4, 0xC6, 0xC3, 0xC3, /* 'l' 0x6C */ 0xFF, 0xFF, 0xFF, 0xC0, /* 'm' 0x6D */ 0xDE, 0xF7, 0x1C, 0xF0, 0xC7, 0x86, 0x3C, 0x31, 0xE1, 0x8F, 0x0C, 0x78, 0x63, 0xC3, 0x1E, 0x18, 0xC0, /* 'n' 0x6E */ 0xDE, 0xE3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, /* 'o' 0x6F */ 0x3C, 0x66, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x66, 0x3C, /* 'p' 0x70 */ 0xDE, 0x71, 0xB0, 0x78, 0x3C, 0x1E, 0x0F, 0x07, 0x83, 0xE3, 0x6F, 0x30, 0x18, 0x0C, 0x00, /* 'q' 0x71 */ 0x3B, 0x67, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x67, 0x3B, 0x03, 0x03, 0x03, /* 'r' 0x72 */ 0xDF, 0x31, 0x8C, 0x63, 0x18, 0xC6, 0x00, /* 's' 0x73 */ 0x3E, 0xE3, 0xC0, 0xC0, 0xE0, 0x3C, 0x07, 0xC3, 0xE3, 0x7E, /* 't' 0x74 */ 0x66, 0xF6, 0x66, 0x66, 0x66, 0x67, /* 'u' 0x75 */ 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC7, 0x7B, /* 'v' 0x76 */ 0xC1, 0xA0, 0x98, 0xCC, 0x42, 0x21, 0xB0, 0xD0, 0x28, 0x1C, 0x0C, 0x00, /* 'w' 0x77 */ 0xC6, 0x1E, 0x38, 0x91, 0xC4, 0xCA, 0x66, 0xD3, 0x16, 0xD0, 0xA6, 0x87, 0x1C, 0x38, 0xC0, 0xC6, 0x00, /* 'x' 0x78 */ 0x87, 0x89, 0xB1, 0xC3, 0x07, 0x1E, 0x26, 0xC5, 0x0C, /* 'y' 0x79 */ 0xC1, 0x43, 0x63, 0x62, 0x26, 0x36, 0x34, 0x1C, 0x1C, 0x18, 0x18, 0x18, 0x10, 0x60, /* 'z' 0x7A */ 0xFE, 0x0C, 0x30, 0xC1, 0x86, 0x18, 0x20, 0xC1, 0xFC, /* '{' 0x7B */ 0x36, 0x66, 0x66, 0x6E, 0xCE, 0x66, 0x66, 0x66, 0x30, /* '|' 0x7C */ 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, /* '}' 0x7D */ 0xC6, 0x66, 0x66, 0x67, 0x37, 0x66, 0x66, 0x66, 0xC0, /* '~' 0x7E */ 0x61, 0x24, 0x38, /* 0x7F */ /* 0x80 */ 0x07, 0xC6, 0x13, 0x00, 0xC0, 0x60, 0x3F, 0xE6, 0x03, 0xFC, 0x60, 0x0C, 0x03, 0x00, 0x61, 0x07, 0xC0, /* 0x81 */ /* 0x82 */ 0xDC, /* 0x83 */ 0x19, 0x8C, 0xF3, 0x18, 0xC6, 0x31, 0x8C, 0x63, 0x18, 0xC6, 0xE0, /* 0x84 */ 0xDA, 0x76, /* 0x85 */ 0xCC, 0xC0, /* 0x86 */ 0x18, 0x18, 0x18, 0x18, 0xFF, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, /* 0x87 */ 0x18, 0x18, 0x18, 0x18, 0xFF, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0xFF, 0x18, 0x18, 0x18, 0x18, /* 0x88 */ 0x72, 0xA2, /* 0x89 */ 0x70, 0x80, 0x22, 0x20, 0x08, 0x90, 0x02, 0x24, 0x00, 0x72, 0x00, 0x00, 0x80, 0x00, 0x40, 0x00, 0x10, 0x00, 0x09, 0xC7, 0x84, 0x8B, 0x31, 0x22, 0x84, 0x88, 0xB3, 0x21, 0xC7, 0x80, /* 0x8A */ 0x1B, 0x03, 0x80, 0x00, 0xFC, 0x61, 0xB0, 0x3C, 0x0F, 0x00, 0x78, 0x07, 0xC0, 0x38, 0x03, 0xC0, 0xF0, 0x36, 0x18, 0xFC, /* 0x8B */ 0x69, /* 0x8C */ 0x1E, 0xFE, 0x43, 0x81, 0x83, 0x06, 0x06, 0x0C, 0x0C, 0x18, 0x18, 0x30, 0x3F, 0xE0, 0x60, 0xC0, 0xC1, 0x81, 0x81, 0x83, 0x01, 0x8E, 0x01, 0xEF, 0xE0, /* 0x8D */ /* 0x8E */ 0x1B, 0x03, 0x80, 0x03, 0xFF, 0x01, 0x80, 0xC0, 0x30, 0x18, 0x0C, 0x07, 0x01, 0x80, 0xC0, 0x60, 0x18, 0x0C, 0x03, 0xFF, /* 0x8F */ /* 0x90 */ /* 0x91 */ 0x6B, /* 0x92 */ 0xD6, /* 0x93 */ 0x4C, 0xA5, 0xB0, /* 0x94 */ 0xDA, 0x53, 0x20, /* 0x95 */ 0x6F, 0xFF, 0x60, /* 0x96 */ 0xFE, /* 0x97 */ 0xFF, 0xFF, /* 0x98 */ 0x4D, 0xC0, /* 0x99 */ 0xFC, 0xE1, 0xCC, 0x38, 0x73, 0x0E, 0x1C, 0xC3, 0x8F, 0x30, 0xD2, 0xCC, 0x34, 0xB3, 0x0D, 0x6C, 0xC3, 0x53, 0x30, 0xCC, 0xCC, 0x33, 0x30, /* 0x9A */ 0x24, 0x3C, 0x18, 0x7E, 0xE3, 0xC0, 0xC0, 0x60, 0x3C, 0x07, 0xC3, 0xE3, 0x7E, /* 0x9B */ 0x96, /* 0x9C */ 0x3C, 0xF8, 0xCF, 0x1B, 0x0C, 0x1E, 0x18, 0x3C, 0x3F, 0xF8, 0x60, 0x30, 0xC0, 0x61, 0x83, 0x67, 0x8C, 0x79, 0xF0, /* 0x9D */ /* 0x9E */ 0x48, 0xF0, 0xC7, 0xF0, 0x61, 0x86, 0x0C, 0x30, 0xC1, 0x06, 0x0F, 0xE0, /* 0x9F */ 0x19, 0x80, 0x00, 0xC0, 0x36, 0x06, 0x30, 0xC3, 0x0C, 0x19, 0x81, 0xD8, 0x0F, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, /* 0xA0 */ /* 0xA1 */ 0xCF, 0xFF, 0xFF, 0xC0, /* 0xA2 */ 0x08, 0x04, 0x0F, 0x8D, 0x6C, 0x9E, 0x43, 0x21, 0x90, 0xC8, 0x64, 0xDA, 0xC7, 0xC0, 0x80, 0x40, /* 0xA3 */ 0x1F, 0x0C, 0x66, 0x0D, 0x83, 0x60, 0x0C, 0x0F, 0xC0, 0x60, 0x18, 0x06, 0x03, 0x01, 0xF1, 0x43, 0xC0, /* 0xA4 */ 0xFF, 0xDF, 0x1E, 0x3E, 0xFF, 0xC0, /* 0xA5 */ 0xC3, 0x42, 0x42, 0x24, 0x24, 0x3C, 0x18, 0x7E, 0x18, 0x7E, 0x18, 0x18, 0x18, /* 0xA6 */ 0xFF, 0xFC, 0x0F, 0xFF, 0xC0, /* 0xA7 */ 0x0C, 0x09, 0x0C, 0xC6, 0x63, 0x81, 0xE3, 0x19, 0x87, 0xE1, 0xB8, 0xC6, 0x41, 0xC0, 0x73, 0x19, 0x8C, 0x66, 0x1E, 0x00, /* 0xA8 */ 0xCC, /* 0xA9 */ 0x0F, 0xC0, 0x61, 0x87, 0x03, 0x9B, 0xC6, 0xD9, 0x8F, 0x60, 0x3D, 0x00, 0xF4, 0x03, 0xD8, 0x0D, 0xE6, 0x67, 0xF3, 0x86, 0x18, 0x0F, 0xC0, /* 0xAA */ 0x74, 0x8D, 0xA9, 0x7C, 0x1F, /* 0xAB */ 0x22, 0xCF, 0x26, 0x46, 0x64, 0x40, /* 0xAC */ 0xFF, 0x80, 0xC0, 0x60, 0x30, 0x18, /* 0xAD */ /* 0xAE */ 0x0F, 0xC0, 0x61, 0x87, 0x03, 0x9F, 0xE6, 0xD0, 0x8F, 0x42, 0x3D, 0xF0, 0xF4, 0x23, 0xD0, 0x8D, 0xC2, 0x67, 0x0B, 0x86, 0x18, 0x0F, 0xC0, /* 0xAF */ 0xF8, /* 0xB0 */ 0x74, 0x63, 0x17, 0x00, /* 0xB1 */ 0x0C, 0x06, 0x03, 0x07, 0xE0, 0xC0, 0x60, 0x30, 0x18, 0x00, 0x00, 0x3F, 0xE0, /* 0xB2 */ 0x7B, 0x30, 0xC3, 0x11, 0x84, 0x3F, /* 0xB3 */ 0x7D, 0x8C, 0x18, 0xC0, 0x60, 0xF1, 0xBE, /* 0xB4 */ 0x36, 0xC0, /* 0xB5 */ 0xC3, 0x61, 0xB0, 0xD8, 0x6C, 0x36, 0x1B, 0x0D, 0x86, 0xE7, 0x7D, 0xF0, 0x18, 0x0C, 0x00, /* 0xB6 */ 0x3F, 0x7E, 0xF2, 0xF2, 0xF2, 0xF2, 0xF2, 0x72, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, /* 0xB7 */ 0xE0, /* 0xB8 */ 0x21, 0xC7, 0xE0, /* 0xB9 */ 0x3D, 0xB6, 0xD8, /* 0xBA */ 0x74, 0x63, 0x18, 0xB8, 0x1F, /* 0xBB */ 0x89, 0x98, 0x99, 0x3C, 0xD1, 0x00, /* 0xBC */ 0x20, 0x43, 0x81, 0x06, 0x08, 0x18, 0x20, 0x61, 0x01, 0x84, 0x06, 0x21, 0x80, 0x86, 0x04, 0x78, 0x32, 0x60, 0x87, 0xC4, 0x06, 0x10, 0x18, /* 0xBD */ 0x20, 0x43, 0x81, 0x06, 0x08, 0x18, 0x20, 0x61, 0x01, 0x8D, 0xE6, 0x2C, 0xC1, 0x03, 0x0C, 0x0C, 0x20, 0x41, 0x86, 0x0C, 0x30, 0x20, 0xFC, /* 0xBE */ 0x78, 0x11, 0x98, 0x40, 0x31, 0x00, 0x82, 0x00, 0xC8, 0x01, 0x90, 0x33, 0x43, 0x3D, 0x06, 0x02, 0x3C, 0x08, 0x98, 0x10, 0xF8, 0x40, 0x61, 0x00, 0xC0, /* 0xBF */ 0x0C, 0x00, 0x00, 0x01, 0x80, 0xC0, 0xC0, 0xE0, 0xC0, 0xC0, 0x60, 0xF0, 0x6C, 0x63, 0xE0, /* 0xC0 */ 0x18, 0x03, 0x00, 0x00, 0x30, 0x1E, 0x07, 0x81, 0x20, 0xCC, 0x33, 0x0F, 0xC6, 0x19, 0x86, 0x40, 0xB0, 0x30, /* 0xC1 */ 0x06, 0x03, 0x00, 0x00, 0x30, 0x1E, 0x07, 0x81, 0x20, 0xCC, 0x33, 0x0F, 0xC6, 0x19, 0x86, 0x40, 0xB0, 0x30, /* 0xC2 */ 0x0C, 0x04, 0x80, 0x00, 0x30, 0x1E, 0x07, 0x81, 0x20, 0xCC, 0x33, 0x0F, 0xC6, 0x19, 0x86, 0x40, 0xB0, 0x30, /* 0xC3 */ 0x19, 0x09, 0x80, 0x00, 0x30, 0x1E, 0x07, 0x81, 0x20, 0xCC, 0x33, 0x0F, 0xC6, 0x19, 0x86, 0x40, 0xB0, 0x30, /* 0xC4 */ 0x33, 0x00, 0x00, 0xC0, 0x78, 0x1E, 0x04, 0x83, 0x30, 0xCC, 0x33, 0x1F, 0xE6, 0x19, 0x02, 0xC0, 0xF0, 0x30, /* 0xC5 */ 0x0C, 0x04, 0x81, 0x20, 0x30, 0x1E, 0x07, 0x81, 0x20, 0xCC, 0x33, 0x0F, 0xC6, 0x19, 0x86, 0x40, 0xB0, 0x30, /* 0xC6 */ 0x07, 0xFF, 0x04, 0xC0, 0x0C, 0xC0, 0x08, 0xC0, 0x18, 0xC0, 0x18, 0xC0, 0x30, 0xFF, 0x30, 0xC0, 0x3F, 0xC0, 0x60, 0xC0, 0x60, 0xC0, 0xC0, 0xC0, 0xC0, 0xFF, /* 0xC7 */ 0x1F, 0x06, 0x19, 0x83, 0xA0, 0x3C, 0x01, 0x80, 0x30, 0x06, 0x00, 0xC0, 0x68, 0x0D, 0x83, 0x18, 0xE1, 0xF0, 0x08, 0x01, 0xC0, 0x18, 0x0E, 0x00, /* 0xC8 */ 0x18, 0x06, 0x00, 0x1F, 0xFC, 0x06, 0x03, 0x01, 0x80, 0xFF, 0x60, 0x30, 0x18, 0x0C, 0x07, 0xFC, /* 0xC9 */ 0x0C, 0x0C, 0x00, 0x1F, 0xFC, 0x06, 0x03, 0x01, 0x80, 0xFF, 0x60, 0x30, 0x18, 0x0C, 0x07, 0xFC, /* 0xCA */ 0x1C, 0x1B, 0x00, 0x1F, 0xFC, 0x06, 0x03, 0x01, 0x80, 0xFF, 0x60, 0x30, 0x18, 0x0C, 0x07, 0xFC, /* 0xCB */ 0x33, 0x00, 0x3F, 0xF8, 0x0C, 0x06, 0x03, 0x01, 0xFE, 0xC0, 0x60, 0x30, 0x18, 0x0C, 0x07, 0xFC, /* 0xCC */ 0xCC, 0x36, 0xDB, 0x6D, 0xB6, 0xD8, /* 0xCD */ 0x78, 0x36, 0xDB, 0x6D, 0xB6, 0xC0, /* 0xCE */ 0x76, 0xC0, 0x63, 0x18, 0xC6, 0x31, 0x8C, 0x63, 0x18, /* 0xCF */ 0xCC, 0x03, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC0, /* 0xD0 */ 0x7F, 0x0C, 0x31, 0x83, 0x30, 0x36, 0x06, 0xC0, 0xFE, 0x1B, 0x03, 0x60, 0x6C, 0x0D, 0x83, 0x30, 0xE7, 0xF0, /* 0xD1 */ 0x19, 0x02, 0xC3, 0x81, 0xF0, 0x3F, 0x07, 0xA0, 0xF6, 0x1E, 0x63, 0xC4, 0x78, 0xCF, 0x0D, 0xE1, 0xBC, 0x1F, 0x81, 0xC0, /* 0xD2 */ 0x0C, 0x00, 0x60, 0x00, 0x00, 0xF0, 0x39, 0xC6, 0x06, 0x60, 0x6C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x36, 0x06, 0x60, 0x63, 0x9C, 0x0F, 0x00, /* 0xD3 */ 0x03, 0x00, 0x60, 0x00, 0x00, 0xF0, 0x39, 0xC6, 0x06, 0x60, 0x6C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x36, 0x06, 0x60, 0x63, 0x9C, 0x0F, 0x00, /* 0xD4 */ 0x0F, 0x01, 0x98, 0x00, 0x00, 0xF0, 0x39, 0xC6, 0x06, 0x60, 0x6C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x36, 0x06, 0x60, 0x63, 0x9C, 0x0F, 0x00, /* 0xD5 */ 0x1C, 0x81, 0x38, 0x00, 0x00, 0xF0, 0x39, 0xC6, 0x06, 0x60, 0x6C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x36, 0x06, 0x60, 0x63, 0x9C, 0x0F, 0x00, /* 0xD6 */ 0x19, 0x81, 0x98, 0x00, 0x00, 0xF0, 0x39, 0xC6, 0x06, 0x60, 0x6C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x36, 0x06, 0x60, 0x63, 0x9C, 0x0F, 0x00, /* 0xD7 */ 0x83, 0x89, 0xA1, 0x83, 0x89, 0xA1, 0x80, /* 0xD8 */ 0x0F, 0xD9, 0x83, 0x18, 0x1C, 0xC1, 0xEC, 0x19, 0xE0, 0x8F, 0x08, 0x78, 0x83, 0xC8, 0x1B, 0x81, 0x98, 0x0C, 0xE0, 0xC8, 0xF8, 0x00, /* 0xD9 */ 0x0C, 0x00, 0xC3, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x36, 0x0C, 0x3E, 0x00, /* 0xDA */ 0x06, 0x01, 0x83, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x36, 0x0C, 0x3E, 0x00, /* 0xDB */ 0x0E, 0x03, 0x63, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x36, 0x0C, 0x3E, 0x00, /* 0xDC */ 0x1B, 0x00, 0x03, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x36, 0x0C, 0x3E, 0x00, /* 0xDD */ 0x03, 0x0C, 0x63, 0x60, 0x63, 0x0C, 0x30, 0xC1, 0x98, 0x1D, 0x80, 0xF0, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, /* 0xDE */ 0xC0, 0x30, 0x0F, 0xF3, 0x06, 0xC0, 0xF0, 0x3C, 0x0F, 0x06, 0xFF, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x00, /* 0xDF */ 0x3C, 0x33, 0x30, 0xD8, 0x6C, 0x36, 0x33, 0x39, 0x86, 0xC1, 0xE0, 0xF0, 0x78, 0x6D, 0xE0, /* 0xE0 */ 0x60, 0x18, 0x06, 0x0F, 0xCE, 0x36, 0x18, 0x0C, 0x1E, 0x7B, 0x61, 0xB0, 0xD8, 0xE7, 0xB8, /* 0xE1 */ 0x0C, 0x04, 0x04, 0x0F, 0xCE, 0x36, 0x18, 0x0C, 0x1E, 0x7B, 0x61, 0xB0, 0xD8, 0xE7, 0xB8, /* 0xE2 */ 0x10, 0x14, 0x1B, 0x0F, 0xCE, 0x36, 0x18, 0x0C, 0x1E, 0x7B, 0x61, 0xB0, 0xD8, 0xE7, 0xB8, /* 0xE3 */ 0x24, 0x2E, 0x00, 0x0F, 0xCE, 0x36, 0x18, 0x0C, 0x1E, 0x7B, 0x61, 0xB0, 0xD8, 0xE7, 0xB8, /* 0xE4 */ 0x66, 0x00, 0x1F, 0x9C, 0x6C, 0x30, 0x18, 0x3C, 0xF6, 0xC3, 0x61, 0xB1, 0xCF, 0x70, /* 0xE5 */ 0x1C, 0x1B, 0x0D, 0x83, 0x87, 0xE7, 0x1B, 0x0C, 0x06, 0x0F, 0x3D, 0xB0, 0xD8, 0x6C, 0x73, 0xDC, /* 0xE6 */ 0x7E, 0xF9, 0xC7, 0x1B, 0x0C, 0x18, 0x18, 0x33, 0xFF, 0xFC, 0x60, 0x30, 0xC0, 0x61, 0x83, 0xC7, 0x8C, 0xF1, 0xF0, /* 0xE7 */ 0x3C, 0x66, 0xC3, 0xC0, 0xC0, 0xC0, 0xC0, 0xC3, 0x66, 0x3C, 0x10, 0x1C, 0x0C, 0x38, /* 0xE8 */ 0x60, 0x30, 0x18, 0x3C, 0x66, 0xC3, 0xC3, 0xFF, 0xC0, 0xC0, 0xC3, 0x66, 0x3C, /* 0xE9 */ 0x0C, 0x08, 0x18, 0x3C, 0x66, 0xC3, 0xC3, 0xFF, 0xC0, 0xC0, 0xC3, 0x66, 0x3C, /* 0xEA */ 0x10, 0x28, 0x6C, 0x3C, 0x66, 0xC3, 0xC3, 0xFF, 0xC0, 0xC0, 0xC3, 0x66, 0x3C, /* 0xEB */ 0x66, 0x00, 0x3C, 0x66, 0xC3, 0xC3, 0xFF, 0xC0, 0xC0, 0xC3, 0x66, 0x3C, /* 0xEC */ 0xCC, 0xB6, 0xDB, 0x6D, 0xB6, /* 0xED */ 0x7A, 0x6D, 0xB6, 0xDB, 0x6C, /* 0xEE */ 0x6E, 0x96, 0x66, 0x66, 0x66, 0x66, 0x60, /* 0xEF */ 0xCC, 0x03, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, /* 0xF0 */ 0x34, 0x0C, 0x16, 0x03, 0x3F, 0x67, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x66, 0x3C, /* 0xF1 */ 0x24, 0x5C, 0x00, 0xDE, 0xE3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, /* 0xF2 */ 0x30, 0x18, 0x00, 0x3C, 0x66, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x66, 0x3C, /* 0xF3 */ 0x0C, 0x18, 0x00, 0x3C, 0x66, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x66, 0x3C, /* 0xF4 */ 0x18, 0x24, 0x00, 0x3C, 0x66, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x66, 0x3C, /* 0xF5 */ 0x34, 0x2C, 0x00, 0x3C, 0x66, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x66, 0x3C, /* 0xF6 */ 0x66, 0x00, 0x3C, 0x66, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x66, 0x3C, /* 0xF7 */ 0x18, 0x00, 0x00, 0x1F, 0xF0, 0x00, 0x00, 0x00, 0x30, /* 0xF8 */ 0x3D, 0x66, 0xC7, 0xCB, 0xCB, 0xD3, 0xD3, 0xE3, 0x66, 0xBC, /* 0xF9 */ 0x60, 0x30, 0x18, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC7, 0x7B, /* 0xFA */ 0x06, 0x0C, 0x18, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC7, 0x7B, /* 0xFB */ 0x3C, 0x66, 0x00, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC7, 0x7B, /* 0xFC */ 0x66, 0x00, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC7, 0x7B, /* 0xFD */ 0x06, 0x04, 0x08, 0xC1, 0x43, 0x63, 0x62, 0x26, 0x36, 0x34, 0x1C, 0x1C, 0x18, 0x18, 0x18, 0x10, 0x60, /* 0xFE */ 0xC0, 0x60, 0x30, 0x1B, 0xCE, 0x36, 0x0F, 0x07, 0x83, 0xC1, 0xE0, 0xF0, 0x7C, 0x6D, 0xE6, 0x03, 0x01, 0x80, /* 0xFF */ 0x33, 0x00, 0xC1, 0x43, 0x63, 0x62, 0x26, 0x36, 0x34, 0x1C, 0x1C, 0x18, 0x18, 0x18, 0x10, 0x60, }; const GFXglyph FreeSans9pt_Win1252Glyphs[] PROGMEM = { /* 0x01 */ { 0, 15, 15, 17, 1, -13 }, /* 0x02 */ { 29, 15, 15, 17, 1, -13 }, /* 0x03 */ { 58, 15, 16, 17, 1, -14 }, /* 0x04 */ { 88, 15, 16, 17, 1, -14 }, /* 0x05 */ { 118, 16, 15, 18, 1, -13 }, /* 0x06 */ { 148, 15, 15, 17, 1, -13 }, /* 0x07 */ { 177, 0, 0, 8, 0, 0 }, /* 0x08 */ { 177, 17, 16, 19, 1, -14 }, /* 0x09 */ { 211, 17, 12, 19, 1, -12 }, /* 0x0A */ { 237, 0, 0, 8, 0, 0 }, /* 0x0B */ { 237, 17, 16, 19, 1, -14 }, /* 0x0C */ { 271, 15, 14, 17, 1, -12 }, /* 0x0D */ { 298, 0, 0, 8, 0, 0 }, /* 0x0E */ { 298, 15, 16, 17, 1, -14 }, /* 0x0F */ { 328, 15, 15, 17, 1, -13 }, /* 0x10 */ { 357, 15, 15, 17, 1, -13 }, /* 0x11 */ { 386, 15, 16, 17, 1, -14 }, /* 0x12 */ { 416, 17, 17, 19, 1, -15 }, /* 0x13 */ { 453, 15, 16, 17, 1, -14 }, /* 0x14 */ { 483, 15, 16, 17, 1, -14 }, /* 0x15 */ { 513, 15, 16, 17, 1, -14 }, /* 0x16 */ { 543, 11, 16, 13, 1, -14 }, /* 0x17 */ { 565, 15, 16, 17, 1, -14 }, /* 0x18 */ { 595, 18, 15, 20, 1, -13 }, /* 0x19 */ { 629, 15, 16, 17, 1, -14 }, /* 0x1A */ { 659, 13, 14, 15, 1, -12 }, /* 0x1B */ { 682, 17, 16, 19, 1, -14 }, /* 0x1C */ { 716, 15, 16, 17, 1, -14 }, /* 0x1D */ { 746, 15, 15, 17, 1, -13 }, /* 0x1E */ { 775, 17, 16, 19, 1, -14 }, /* 0x1F */ { 809, 11, 16, 13, 1, -14 }, /* ' ' 0x20 */ { 831, 0, 0, 5, 0, 0 }, /* '!' 0x21 */ { 831, 2, 13, 6, 2, -12 }, /* '"' 0x22 */ { 835, 5, 4, 6, 1, -12 }, /* '#' 0x23 */ { 838, 10, 12, 10, 0, -11 }, /* '$' 0x24 */ { 853, 9, 16, 10, 1, -13 }, /* '%' 0x25 */ { 871, 16, 13, 16, 1, -12 }, /* '&' 0x26 */ { 897, 10, 13, 12, 1, -12 }, /* ''' 0x27 */ { 914, 2, 4, 4, 1, -12 }, /* '(' 0x28 */ { 915, 4, 17, 6, 1, -12 }, /* ')' 0x29 */ { 924, 4, 17, 6, 1, -12 }, /* '*' 0x2A */ { 933, 5, 5, 7, 1, -12 }, /* '+' 0x2B */ { 937, 6, 8, 11, 3, -7 }, /* ',' 0x2C */ { 943, 2, 4, 5, 2, 0 }, /* '-' 0x2D */ { 944, 4, 1, 6, 1, -4 }, /* '.' 0x2E */ { 945, 2, 1, 5, 1, 0 }, /* '/' 0x2F */ { 946, 5, 13, 5, 0, -12 }, /* '0' 0x30 */ { 955, 8, 13, 10, 1, -12 }, /* '1' 0x31 */ { 968, 4, 13, 10, 3, -12 }, /* '2' 0x32 */ { 975, 9, 13, 10, 1, -12 }, /* '3' 0x33 */ { 990, 8, 13, 10, 1, -12 }, /* '4' 0x34 */ { 1003, 7, 13, 10, 2, -12 }, /* '5' 0x35 */ { 1015, 9, 13, 10, 1, -12 }, /* '6' 0x36 */ { 1030, 9, 13, 10, 1, -12 }, /* '7' 0x37 */ { 1045, 8, 13, 10, 0, -12 }, /* '8' 0x38 */ { 1058, 9, 13, 10, 1, -12 }, /* '9' 0x39 */ { 1073, 8, 13, 10, 1, -12 }, /* ':' 0x3A */ { 1086, 2, 10, 5, 1, -9 }, /* ';' 0x3B */ { 1089, 3, 12, 5, 1, -8 }, /* '<' 0x3C */ { 1094, 9, 9, 11, 1, -8 }, /* '=' 0x3D */ { 1105, 9, 4, 11, 1, -5 }, /* '>' 0x3E */ { 1110, 9, 8, 11, 1, -7 }, /* '?' 0x3F */ { 1119, 9, 13, 10, 1, -12 }, /* '@' 0x40 */ { 1134, 17, 16, 18, 1, -12 }, /* 'A' 0x41 */ { 1168, 12, 13, 12, 0, -12 }, /* 'B' 0x42 */ { 1188, 11, 13, 12, 1, -12 }, /* 'C' 0x43 */ { 1206, 11, 13, 13, 1, -12 }, /* 'D' 0x44 */ { 1224, 11, 13, 13, 1, -12 }, /* 'E' 0x45 */ { 1242, 9, 13, 11, 1, -12 }, /* 'F' 0x46 */ { 1257, 8, 13, 11, 1, -12 }, /* 'G' 0x47 */ { 1270, 12, 13, 14, 1, -12 }, /* 'H' 0x48 */ { 1290, 11, 13, 13, 1, -12 }, /* 'I' 0x49 */ { 1308, 2, 13, 5, 2, -12 }, /* 'J' 0x4A */ { 1312, 7, 13, 10, 1, -12 }, /* 'K' 0x4B */ { 1324, 10, 13, 12, 1, -12 }, /* 'L' 0x4C */ { 1341, 8, 13, 10, 1, -12 }, /* 'M' 0x4D */ { 1354, 13, 13, 15, 1, -12 }, /* 'N' 0x4E */ { 1376, 11, 13, 13, 1, -12 }, /* 'O' 0x4F */ { 1394, 13, 13, 14, 1, -12 }, /* 'P' 0x50 */ { 1416, 10, 13, 12, 1, -12 }, /* 'Q' 0x51 */ { 1433, 13, 14, 14, 1, -12 }, /* 'R' 0x52 */ { 1456, 12, 13, 13, 1, -12 }, /* 'S' 0x53 */ { 1476, 10, 13, 12, 1, -12 }, /* 'T' 0x54 */ { 1493, 9, 13, 11, 1, -12 }, /* 'U' 0x55 */ { 1508, 11, 13, 13, 1, -12 }, /* 'V' 0x56 */ { 1526, 11, 13, 11, 0, -12 }, /* 'W' 0x57 */ { 1544, 16, 13, 17, 0, -12 }, /* 'X' 0x58 */ { 1570, 10, 13, 12, 1, -12 }, /* 'Y' 0x59 */ { 1587, 12, 13, 12, 0, -12 }, /* 'Z' 0x5A */ { 1607, 10, 13, 11, 1, -12 }, /* '[' 0x5B */ { 1624, 3, 17, 5, 1, -12 }, /* '\' 0x5C */ { 1631, 5, 13, 5, 0, -12 }, /* ']' 0x5D */ { 1640, 3, 17, 5, 0, -12 }, /* '^' 0x5E */ { 1647, 7, 7, 8, 1, -12 }, /* '_' 0x5F */ { 1654, 10, 1, 10, 0, 3 }, /* '`' 0x60 */ { 1656, 4, 3, 5, 0, -12 }, /* 'a' 0x61 */ { 1658, 9, 10, 10, 1, -9 }, /* 'b' 0x62 */ { 1670, 9, 13, 10, 1, -12 }, /* 'c' 0x63 */ { 1685, 8, 10, 9, 1, -9 }, /* 'd' 0x64 */ { 1695, 8, 13, 10, 1, -12 }, /* 'e' 0x65 */ { 1708, 8, 10, 10, 1, -9 }, /* 'f' 0x66 */ { 1718, 4, 13, 5, 1, -12 }, /* 'g' 0x67 */ { 1725, 8, 14, 10, 1, -9 }, /* 'h' 0x68 */ { 1739, 8, 13, 10, 1, -12 }, /* 'i' 0x69 */ { 1752, 2, 13, 4, 1, -12 }, /* 'j' 0x6A */ { 1756, 4, 17, 4, 0, -12 }, /* 'k' 0x6B */ { 1765, 8, 13, 9, 1, -12 }, /* 'l' 0x6C */ { 1778, 2, 13, 4, 1, -12 }, /* 'm' 0x6D */ { 1782, 13, 10, 15, 1, -9 }, /* 'n' 0x6E */ { 1799, 8, 10, 10, 1, -9 }, /* 'o' 0x6F */ { 1809, 8, 10, 10, 1, -9 }, /* 'p' 0x70 */ { 1819, 9, 13, 10, 1, -9 }, /* 'q' 0x71 */ { 1834, 8, 13, 10, 1, -9 }, /* 'r' 0x72 */ { 1847, 5, 10, 6, 1, -9 }, /* 's' 0x73 */ { 1854, 8, 10, 9, 1, -9 }, /* 't' 0x74 */ { 1864, 4, 12, 5, 1, -11 }, /* 'u' 0x75 */ { 1870, 8, 10, 10, 1, -9 }, /* 'v' 0x76 */ { 1880, 9, 10, 9, 0, -9 }, /* 'w' 0x77 */ { 1892, 13, 10, 13, 0, -9 }, /* 'x' 0x78 */ { 1909, 7, 10, 9, 1, -9 }, /* 'y' 0x79 */ { 1918, 8, 14, 9, 0, -9 }, /* 'z' 0x7A */ { 1932, 7, 10, 9, 1, -9 }, /* '{' 0x7B */ { 1941, 4, 17, 6, 1, -12 }, /* '|' 0x7C */ { 1950, 2, 17, 4, 2, -12 }, /* '}' 0x7D */ { 1955, 4, 17, 6, 1, -12 }, /* '~' 0x7E */ { 1964, 7, 3, 9, 1, -7 }, /* 0x7F */ { 1967, 0, 0, 0, 0, 0 }, /* 0x80 */ { 1967, 10, 13, 12, 1, -12 }, /* 0x81 */ { 1984, 0, 0, 8, 0, 0 }, /* 0x82 */ { 1984, 2, 3, 5, 1, 0 }, /* 0x83 */ { 1985, 5, 17, 5, 0, -12 }, /* 0x84 */ { 1996, 5, 3, 7, 1, 0 }, /* 0x85 */ { 1998, 10, 1, 12, 1, 0 }, /* 0x86 */ { 2000, 8, 16, 10, 1, -12 }, /* 0x87 */ { 2016, 8, 16, 10, 1, -12 }, /* 0x88 */ { 2032, 5, 3, 6, 0, -12 }, /* 0x89 */ { 2034, 18, 13, 18, 0, -12 }, /* 0x8A */ { 2064, 10, 16, 12, 1, -15 }, /* 0x8B */ { 2084, 2, 4, 4, 1, -6 }, /* 0x8C */ { 2085, 15, 13, 18, 1, -12 }, /* 0x8D */ { 2110, 0, 0, 8, 0, 0 }, /* 0x8E */ { 2110, 10, 16, 11, 1, -15 }, /* 0x8F */ { 2130, 0, 0, 8, 0, 0 }, /* 0x90 */ { 2130, 0, 0, 8, 0, 0 }, /* 0x91 */ { 2130, 2, 4, 4, 2, -12 }, /* 0x92 */ { 2131, 2, 4, 4, 1, -12 }, /* 0x93 */ { 2132, 5, 4, 7, 2, -12 }, /* 0x94 */ { 2135, 5, 4, 7, 1, -12 }, /* 0x95 */ { 2138, 4, 5, 7, 1, -8 }, /* 0x96 */ { 2141, 7, 1, 9, 1, -4 }, /* 0x97 */ { 2142, 16, 1, 18, 1, -4 }, /* 0x98 */ { 2144, 5, 2, 6, 0, -12 }, /* 0x99 */ { 2146, 18, 10, 18, 1, -13 }, /* 0x9A */ { 2169, 8, 13, 9, 1, -12 }, /* 0x9B */ { 2182, 2, 4, 5, 2, -6 }, /* 0x9C */ { 2183, 15, 10, 17, 1, -9 }, /* 0x9D */ { 2202, 0, 0, 8, 0, 0 }, /* 0x9E */ { 2202, 7, 13, 9, 1, -12 }, /* 0x9F */ { 2214, 12, 14, 12, 0, -13 }, /* 0xA0 */ { 2235, 0, 0, 5, 0, 0 }, /* 0xA1 */ { 2235, 2, 13, 6, 2, -8 }, /* 0xA2 */ { 2239, 9, 14, 10, 1, -11 }, /* 0xA3 */ { 2255, 10, 13, 10, 0, -12 }, /* 0xA4 */ { 2272, 7, 6, 10, 2, -8 }, /* 0xA5 */ { 2278, 8, 13, 10, 1, -12 }, /* 0xA6 */ { 2291, 2, 17, 5, 2, -12 }, /* 0xA7 */ { 2296, 9, 17, 10, 1, -12 }, /* 0xA8 */ { 2316, 6, 1, 6, 0, -11 }, /* 0xA9 */ { 2317, 14, 13, 14, 1, -12 }, /* 0xAA */ { 2340, 5, 8, 7, 1, -12 }, /* 0xAB */ { 2345, 7, 6, 9, 1, -7 }, /* 0xAC */ { 2351, 9, 5, 11, 2, -5 }, /* 0xAD */ { 2357, 0, 0, 0, 0, 0 }, /* 0xAE */ { 2357, 14, 13, 14, 1, -12 }, /* 0xAF */ { 2380, 5, 1, 6, 0, -12 }, /* 0xB0 */ { 2381, 5, 5, 11, 3, -11 }, /* 0xB1 */ { 2385, 9, 11, 11, 1, -10 }, /* 0xB2 */ { 2398, 6, 8, 6, 1, -13 }, /* 0xB3 */ { 2404, 7, 8, 6, 0, -13 }, /* 0xB4 */ { 2411, 4, 3, 6, 2, -12 }, /* 0xB5 */ { 2413, 9, 13, 10, 1, -9 }, /* 0xB6 */ { 2428, 8, 16, 10, 2, -12 }, /* 0xB7 */ { 2444, 3, 1, 5, 1, -4 }, /* 0xB8 */ { 2445, 5, 4, 6, 1, 1 }, /* 0xB9 */ { 2448, 3, 7, 6, 2, -13 }, /* 0xBA */ { 2451, 5, 8, 7, 1, -12 }, /* 0xBB */ { 2456, 7, 6, 9, 1, -7 }, /* 0xBC */ { 2462, 14, 13, 16, 2, -12 }, /* 0xBD */ { 2485, 14, 13, 16, 2, -12 }, /* 0xBE */ { 2508, 15, 13, 16, 1, -12 }, /* 0xBF */ { 2533, 9, 13, 10, 1, -8 }, /* 0xC0 */ { 2548, 10, 14, 12, 1, -13 }, /* 0xC1 */ { 2566, 10, 14, 12, 1, -13 }, /* 0xC2 */ { 2584, 10, 14, 12, 1, -13 }, /* 0xC3 */ { 2602, 10, 14, 12, 1, -13 }, /* 0xC4 */ { 2620, 10, 14, 12, 1, -13 }, /* 0xC5 */ { 2638, 10, 14, 12, 1, -13 }, /* 0xC6 */ { 2656, 16, 13, 18, 1, -12 }, /* 0xC7 */ { 2682, 11, 17, 13, 1, -12 }, /* 0xC8 */ { 2706, 9, 14, 11, 1, -13 }, /* 0xC9 */ { 2722, 9, 14, 11, 1, -13 }, /* 0xCA */ { 2738, 9, 14, 11, 1, -13 }, /* 0xCB */ { 2754, 9, 14, 11, 1, -13 }, /* 0xCC */ { 2770, 3, 15, 5, 1, -13 }, /* 0xCD */ { 2776, 3, 14, 5, 1, -13 }, /* 0xCE */ { 2782, 5, 14, 5, 0, -13 }, /* 0xCF */ { 2791, 6, 14, 5, 0, -13 }, /* 0xD0 */ { 2802, 11, 13, 13, 1, -12 }, /* 0xD1 */ { 2820, 11, 14, 13, 1, -13 }, /* 0xD2 */ { 2840, 12, 15, 13, 1, -14 }, /* 0xD3 */ { 2863, 12, 15, 13, 1, -14 }, /* 0xD4 */ { 2886, 12, 15, 13, 1, -14 }, /* 0xD5 */ { 2909, 12, 15, 13, 1, -14 }, /* 0xD6 */ { 2932, 12, 15, 13, 1, -14 }, /* 0xD7 */ { 2955, 7, 7, 11, 2, -7 }, /* 0xD8 */ { 2962, 13, 13, 14, 1, -12 }, /* 0xD9 */ { 2984, 11, 14, 13, 1, -13 }, /* 0xDA */ { 3004, 11, 14, 13, 1, -13 }, /* 0xDB */ { 3024, 11, 14, 13, 1, -13 }, /* 0xDC */ { 3044, 11, 14, 13, 1, -13 }, /* 0xDD */ { 3064, 12, 14, 12, 0, -13 }, /* 0xDE */ { 3085, 10, 13, 12, 1, -12 }, /* 0xDF */ { 3102, 9, 13, 11, 1, -12 }, /* 0xE0 */ { 3117, 9, 13, 10, 1, -12 }, /* 0xE1 */ { 3132, 9, 13, 10, 1, -12 }, /* 0xE2 */ { 3147, 9, 13, 10, 1, -12 }, /* 0xE3 */ { 3162, 9, 13, 10, 1, -12 }, /* 0xE4 */ { 3177, 9, 12, 10, 1, -11 }, /* 0xE5 */ { 3191, 9, 14, 10, 1, -13 }, /* 0xE6 */ { 3207, 15, 10, 16, 1, -9 }, /* 0xE7 */ { 3226, 8, 14, 9, 1, -9 }, /* 0xE8 */ { 3240, 8, 13, 10, 1, -12 }, /* 0xE9 */ { 3253, 8, 13, 10, 1, -12 }, /* 0xEA */ { 3266, 8, 13, 10, 1, -12 }, /* 0xEB */ { 3279, 8, 12, 10, 1, -11 }, /* 0xEC */ { 3291, 3, 13, 4, 0, -12 }, /* 0xED */ { 3296, 3, 13, 4, 1, -12 }, /* 0xEE */ { 3301, 4, 13, 5, 0, -12 }, /* 0xEF */ { 3308, 6, 12, 5, -1, -11 }, /* 0xF0 */ { 3317, 8, 13, 10, 1, -12 }, /* 0xF1 */ { 3330, 8, 13, 10, 1, -12 }, /* 0xF2 */ { 3343, 8, 13, 10, 1, -12 }, /* 0xF3 */ { 3356, 8, 13, 10, 1, -12 }, /* 0xF4 */ { 3369, 8, 13, 10, 1, -12 }, /* 0xF5 */ { 3382, 8, 13, 10, 1, -12 }, /* 0xF6 */ { 3395, 8, 12, 10, 1, -11 }, /* 0xF7 */ { 3407, 9, 8, 11, 1, -7 }, /* 0xF8 */ { 3416, 8, 10, 10, 1, -9 }, /* 0xF9 */ { 3426, 8, 13, 10, 1, -12 }, /* 0xFA */ { 3439, 8, 13, 10, 1, -12 }, /* 0xFB */ { 3452, 8, 13, 10, 1, -12 }, /* 0xFC */ { 3465, 8, 12, 10, 1, -11 }, /* 0xFD */ { 3477, 8, 17, 9, 0, -12 }, /* 0xFE */ { 3494, 9, 16, 10, 1, -12 }, /* 0xFF */ { 3512, 8, 16, 9, 0, -11 }, }; const GFXfont FreeSans9pt_Win1252 PROGMEM = { (uint8_t*)FreeSans9pt_Win1252Bitmaps, (GFXglyph*)FreeSans9pt_Win1252Glyphs, 0x01, 0xFF, 16 }; ================================================ FILE: src/graphics/niche/Fonts/FreeSans9pt_Win1253.h ================================================ // trunk-ignore-all(clang-format) #pragma once /* PROPERTIES FONT_NAME FreeSans9pt_Win1253 */ const uint8_t FreeSans9pt_Win1253Bitmaps[] PROGMEM = { /* 0x01 */ 0x07, 0x00, 0x0A, 0x00, 0x24, 0x00, 0x48, 0x01, 0x10, 0x04, 0x40, 0x10, 0xFF, 0x20, 0x02, 0x81, 0xFD, 0x00, 0x06, 0x07, 0xF4, 0x08, 0x24, 0x0F, 0x88, 0x11, 0x0F, 0xDC, 0x00, /* 0x02 */ 0x3F, 0x70, 0x81, 0x11, 0x03, 0xE4, 0x08, 0x28, 0x1F, 0xD0, 0x00, 0x60, 0x7F, 0x20, 0x02, 0x43, 0xFC, 0x44, 0x00, 0x44, 0x00, 0x48, 0x00, 0x90, 0x00, 0xA0, 0x01, 0xC0, 0x00, /* 0x03 */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x00, 0x28, 0x00, 0x31, 0x8C, 0x63, 0x18, 0xC0, 0x01, 0x80, 0x03, 0x00, 0x06, 0x20, 0x8C, 0x3E, 0x14, 0x00, 0x44, 0x01, 0x06, 0x0C, 0x03, 0xE0, /* 0x04 */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x00, 0x28, 0x82, 0x30, 0x88, 0x62, 0x08, 0xC0, 0x01, 0x80, 0x03, 0x00, 0x06, 0x3F, 0x8C, 0x3E, 0x14, 0x00, 0x44, 0x01, 0x06, 0x0C, 0x03, 0xE0, /* 0x05 */ 0x0B, 0x10, 0x14, 0xA8, 0x12, 0x50, 0x29, 0x42, 0x24, 0xA5, 0x32, 0x95, 0x5A, 0x09, 0x48, 0x09, 0x24, 0x01, 0x10, 0x01, 0x48, 0x02, 0xA4, 0x02, 0x42, 0x04, 0x01, 0x98, 0x00, 0x60, /* 0x06 */ 0x00, 0x80, 0x22, 0x80, 0x65, 0x00, 0xBE, 0xE1, 0x82, 0x4E, 0x03, 0x24, 0x04, 0x28, 0x06, 0x30, 0x12, 0x20, 0x3C, 0xA0, 0xC3, 0xFE, 0x80, 0x4D, 0x00, 0xA6, 0x01, 0x80, 0x00, /* 0x07 */ /* 0x08 */ 0x00, 0xF8, 0x00, 0x82, 0x00, 0x80, 0x83, 0xE0, 0x41, 0x10, 0x21, 0x04, 0x1B, 0x00, 0x03, 0x00, 0x01, 0x80, 0x00, 0xE0, 0x00, 0x4F, 0xE1, 0xC0, 0x0F, 0x02, 0x00, 0x03, 0x01, 0x00, 0x09, 0x88, 0x0C, 0x0C, /* 0x09 */ 0x00, 0xF8, 0x00, 0x82, 0x00, 0x80, 0x83, 0xE0, 0x41, 0x10, 0x21, 0x04, 0x1B, 0x00, 0x03, 0x00, 0x01, 0x80, 0x00, 0xE0, 0x00, 0x4F, 0xE1, 0xC0, 0x0F, 0x00, /* 0x0A */ /* 0x0B */ 0x1C, 0x1C, 0x31, 0xB1, 0x90, 0x50, 0x50, 0x10, 0x18, 0x00, 0x0C, 0x00, 0x06, 0x00, 0x02, 0x80, 0x02, 0x40, 0x01, 0x10, 0x01, 0x04, 0x01, 0x01, 0x01, 0x00, 0x41, 0x00, 0x11, 0x00, 0x07, 0x00, 0x01, 0x00, /* 0x0C */ 0x06, 0x00, 0x0A, 0x00, 0x12, 0x00, 0x32, 0x01, 0x84, 0x04, 0x10, 0x08, 0x98, 0x1C, 0x18, 0x40, 0x48, 0x82, 0x11, 0xF0, 0x74, 0x02, 0x18, 0x70, 0x2F, 0x9F, 0x80, /* 0x0D */ /* 0x0E */ 0x01, 0x00, 0x05, 0x00, 0x0A, 0x00, 0x3E, 0x00, 0x82, 0x02, 0x82, 0x06, 0x04, 0x10, 0x04, 0x20, 0x08, 0x40, 0x10, 0xFF, 0x22, 0x00, 0x29, 0xFF, 0x3F, 0x8F, 0xDF, 0x9F, 0x01, 0xC0, /* 0x0F */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x00, 0x28, 0x82, 0x36, 0x03, 0x60, 0x00, 0xCC, 0x19, 0xA4, 0x4B, 0x00, 0x06, 0x8E, 0x2B, 0x22, 0x66, 0x7C, 0xCC, 0x71, 0x98, 0x03, 0x00, /* 0x10 */ 0x03, 0x80, 0x07, 0x00, 0x0E, 0x00, 0x1E, 0x00, 0x54, 0x00, 0xA8, 0x01, 0x50, 0x02, 0xA0, 0x05, 0x20, 0x32, 0x61, 0xC4, 0x74, 0x49, 0x10, 0x6C, 0x00, 0xD8, 0x01, 0x10, 0x00, /* 0x11 */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x40, 0x29, 0x00, 0x31, 0x84, 0x63, 0x18, 0xC0, 0x00, 0x80, 0x15, 0x03, 0x7E, 0x02, 0xFA, 0x04, 0xE4, 0x18, 0x84, 0x00, 0x06, 0x0C, 0x03, 0xE0, /* 0x12 */ 0x02, 0x08, 0x01, 0x08, 0x40, 0x10, 0xC0, 0x08, 0xC0, 0x60, 0x80, 0x28, 0x04, 0x12, 0x4C, 0x10, 0x80, 0x08, 0x23, 0x0E, 0x08, 0xC4, 0x82, 0x04, 0x20, 0x83, 0x09, 0x82, 0x47, 0x01, 0x1C, 0x01, 0x30, 0x00, 0xE0, 0x00, 0x00, /* 0x13 */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x00, 0x28, 0x00, 0x31, 0x08, 0x65, 0x28, 0xC0, 0x01, 0x80, 0x03, 0x00, 0x06, 0x3F, 0x8C, 0x3E, 0x14, 0x00, 0x44, 0x01, 0x06, 0x0C, 0x03, 0xE0, /* 0x14 */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x22, 0x29, 0x83, 0x30, 0x00, 0x65, 0x14, 0xD3, 0x4D, 0xBA, 0xEB, 0x38, 0xE6, 0x00, 0x0A, 0x00, 0x24, 0x38, 0x44, 0x01, 0x07, 0x1C, 0x01, 0xC0, /* 0x15 */ 0x07, 0xC0, 0x30, 0x18, 0x80, 0x32, 0x00, 0xF8, 0x01, 0xF1, 0x09, 0xA5, 0x28, 0x40, 0x01, 0x80, 0x03, 0x00, 0x06, 0x3F, 0x8C, 0x3E, 0x14, 0x00, 0x44, 0x01, 0x06, 0x0C, 0x03, 0xE0, /* 0x16 */ 0x0C, 0x00, 0xC0, 0x1C, 0x03, 0x80, 0xF8, 0xBB, 0x36, 0xC7, 0x99, 0xF3, 0xFE, 0x3F, 0xC3, 0xF0, 0x7E, 0x0E, 0xC1, 0x8E, 0xE0, 0x20, /* 0x17 */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x00, 0x28, 0x00, 0x10, 0x01, 0x20, 0x1D, 0x44, 0x42, 0x84, 0x85, 0x00, 0x86, 0x00, 0xC4, 0x00, 0x44, 0x7C, 0x44, 0x00, 0x06, 0x0C, 0x03, 0xE0, /* 0x18 */ 0x01, 0xE0, 0x00, 0x84, 0x00, 0x40, 0x80, 0x20, 0x10, 0x08, 0x24, 0x02, 0x41, 0x00, 0x86, 0x03, 0x12, 0x03, 0xB4, 0x03, 0x52, 0x81, 0x23, 0x80, 0x70, 0xA0, 0x14, 0x28, 0x05, 0x0A, 0x01, 0x42, 0x80, 0x50, /* 0x19 */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x00, 0x28, 0x00, 0x33, 0x18, 0x60, 0x00, 0xDC, 0xE1, 0xB9, 0xC3, 0x7B, 0xC6, 0x63, 0x0A, 0x00, 0x24, 0xF0, 0x44, 0x01, 0x06, 0x0C, 0x03, 0xE0, /* 0x1A */ 0xFF, 0xFC, 0x00, 0x63, 0xE3, 0x31, 0x99, 0x04, 0xC8, 0x66, 0x06, 0x30, 0x61, 0x82, 0x0C, 0x10, 0x60, 0x03, 0x04, 0x18, 0x00, 0xFF, 0xFC, /* 0x1B */ 0x07, 0xF0, 0x06, 0x0C, 0x04, 0x01, 0x04, 0x00, 0x44, 0x22, 0x12, 0x2A, 0x89, 0x00, 0x04, 0x80, 0x02, 0x44, 0x11, 0x01, 0xF0, 0x04, 0x01, 0x0D, 0x01, 0x6A, 0x41, 0x2C, 0x00, 0x05, 0xC0, 0x0E, 0x18, 0x18, /* 0x1C */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0xC0, 0x2A, 0x00, 0x33, 0x00, 0x66, 0x00, 0xCC, 0x39, 0x80, 0x83, 0x00, 0x06, 0x00, 0x8C, 0x3E, 0x14, 0x00, 0x44, 0x01, 0x06, 0x0C, 0x03, 0xE0, /* 0x1D */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x70, 0x28, 0x00, 0x31, 0x80, 0x63, 0x18, 0xC0, 0x31, 0x80, 0x03, 0x00, 0x06, 0x60, 0x0D, 0x33, 0x12, 0x10, 0x48, 0x21, 0x23, 0x8C, 0x00, /* 0x1E */ 0x03, 0x00, 0x07, 0x9E, 0x07, 0x00, 0x86, 0x00, 0x27, 0xC0, 0x0F, 0xC0, 0x07, 0x8C, 0x62, 0x06, 0x31, 0x20, 0x00, 0x90, 0x00, 0x48, 0x00, 0x24, 0x3E, 0x11, 0x00, 0x10, 0x40, 0x10, 0x18, 0x30, 0x03, 0xE0, /* 0x1F */ 0x18, 0x02, 0x80, 0x4C, 0x16, 0x41, 0x24, 0x3C, 0x88, 0x6E, 0x65, 0xF2, 0x78, 0x46, 0x88, 0xCF, 0x18, 0x02, 0x80, 0x8C, 0x60, 0x70, /* ' ' 0x20 */ /* '!' 0x21 */ 0xFF, 0xFF, 0xF0, 0xC0, /* '"' 0x22 */ 0xDE, 0xF7, 0x20, /* '#' 0x23 */ 0x09, 0x86, 0x41, 0x91, 0xFF, 0x13, 0x04, 0xC3, 0x20, 0xC8, 0xFF, 0x89, 0x82, 0x61, 0x90, /* '$' 0x24 */ 0x10, 0x1F, 0x14, 0xDA, 0x3D, 0x1E, 0x83, 0x40, 0x78, 0x17, 0x08, 0xF4, 0x7A, 0x35, 0x33, 0xF0, 0x40, 0x20, /* '%' 0x25 */ 0x38, 0x10, 0xEC, 0x20, 0xC6, 0x20, 0xC6, 0x40, 0xC6, 0x40, 0x6C, 0x80, 0x39, 0x00, 0x01, 0x3C, 0x02, 0x77, 0x02, 0x63, 0x04, 0x63, 0x04, 0x77, 0x08, 0x3C, /* '&' 0x26 */ 0x0E, 0x0C, 0xC3, 0x30, 0xCC, 0x1E, 0x03, 0x03, 0xC1, 0x9B, 0xC2, 0xF0, 0xEC, 0x19, 0x8F, 0x3C, 0x40, /* ''' 0x27 */ 0xFE, /* '(' 0x28 */ 0x13, 0x26, 0x6C, 0xCC, 0xCC, 0xC4, 0x66, 0x23, 0x10, /* ')' 0x29 */ 0x8C, 0x46, 0x63, 0x33, 0x33, 0x32, 0x66, 0x4C, 0x80, /* '*' 0x2A */ 0x25, 0x7E, 0xA5, 0x00, /* '+' 0x2B */ 0x30, 0xC3, 0x3F, 0x30, 0xC3, 0x0C, /* ',' 0x2C */ 0xD6, /* '-' 0x2D */ 0xF0, /* '.' 0x2E */ 0xC0, /* '/' 0x2F */ 0x08, 0x44, 0x21, 0x10, 0x84, 0x42, 0x11, 0x08, 0x00, /* '0' 0x30 */ 0x3C, 0x66, 0x42, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x42, 0x66, 0x3C, /* '1' 0x31 */ 0x11, 0x3F, 0x33, 0x33, 0x33, 0x33, 0x30, /* '2' 0x32 */ 0x3E, 0x31, 0xB0, 0x78, 0x30, 0x18, 0x1C, 0x1C, 0x1C, 0x18, 0x18, 0x10, 0x08, 0x07, 0xF8, /* '3' 0x33 */ 0x3C, 0x66, 0xC3, 0xC3, 0x03, 0x06, 0x1C, 0x07, 0x03, 0xC3, 0xC3, 0x66, 0x3C, /* '4' 0x34 */ 0x0C, 0x18, 0x71, 0x62, 0xC9, 0xA3, 0x46, 0xFE, 0x18, 0x30, 0x60, 0xC0, /* '5' 0x35 */ 0x7F, 0x20, 0x10, 0x08, 0x08, 0x07, 0xF3, 0x8C, 0x03, 0x01, 0x80, 0xF0, 0x6C, 0x63, 0xE0, /* '6' 0x36 */ 0x1E, 0x31, 0x98, 0x78, 0x0C, 0x06, 0xF3, 0x8D, 0x83, 0xC1, 0xE0, 0xD0, 0x6C, 0x63, 0xE0, /* '7' 0x37 */ 0xFF, 0x03, 0x02, 0x06, 0x04, 0x0C, 0x08, 0x18, 0x18, 0x18, 0x10, 0x30, 0x30, /* '8' 0x38 */ 0x3E, 0x31, 0xB0, 0x78, 0x3C, 0x1B, 0x18, 0xF8, 0xC6, 0xC1, 0xE0, 0xF0, 0x6C, 0x63, 0xE0, /* '9' 0x39 */ 0x3C, 0x66, 0xC2, 0xC3, 0xC3, 0xC3, 0x67, 0x3B, 0x03, 0x03, 0xC2, 0x66, 0x3C, /* ':' 0x3A */ 0xC0, 0x00, 0x30, /* ';' 0x3B */ 0xC0, 0x00, 0x00, 0x64, 0xA0, /* '<' 0x3C */ 0x00, 0x81, 0xC7, 0x8E, 0x0C, 0x07, 0x80, 0x70, 0x0E, 0x01, 0x80, /* '=' 0x3D */ 0xFF, 0x80, 0x00, 0x1F, 0xF0, /* '>' 0x3E */ 0xE0, 0x1C, 0x03, 0x80, 0x30, 0x70, 0xE3, 0x81, 0x00, /* '?' 0x3F */ 0x3E, 0x31, 0xB0, 0x78, 0x30, 0x18, 0x18, 0x38, 0x18, 0x18, 0x0C, 0x00, 0x00, 0x01, 0x80, /* '@' 0x40 */ 0x03, 0xF0, 0x06, 0x0E, 0x06, 0x01, 0x86, 0x00, 0x66, 0x1D, 0xBB, 0x31, 0xCF, 0x18, 0xC7, 0x98, 0x63, 0xCC, 0x31, 0xE6, 0x11, 0xB3, 0x99, 0xCC, 0xF7, 0x86, 0x00, 0x01, 0x80, 0x00, 0x70, 0x40, 0x0F, 0xE0, /* 'A' 0x41 */ 0x06, 0x00, 0xF0, 0x0F, 0x00, 0x90, 0x19, 0x81, 0x98, 0x10, 0x83, 0x0C, 0x3F, 0xC2, 0x04, 0x60, 0x66, 0x06, 0xC0, 0x30, /* 'B' 0x42 */ 0xFF, 0x18, 0x33, 0x03, 0x60, 0x6C, 0x0D, 0x83, 0x3F, 0xC6, 0x06, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x6F, 0xF8, /* 'C' 0x43 */ 0x1F, 0x86, 0x19, 0x81, 0xA0, 0x3C, 0x01, 0x80, 0x30, 0x06, 0x00, 0xC0, 0x68, 0x0D, 0x83, 0x18, 0x61, 0xF0, /* 'D' 0x44 */ 0xFF, 0x18, 0x33, 0x03, 0x60, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x03, 0x60, 0xCF, 0xF0, /* 'E' 0x45 */ 0xFF, 0xE0, 0x30, 0x18, 0x0C, 0x06, 0x03, 0xFD, 0x80, 0xC0, 0x60, 0x30, 0x18, 0x0F, 0xF8, /* 'F' 0x46 */ 0xFF, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xFE, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, /* 'G' 0x47 */ 0x0F, 0x83, 0x0E, 0x60, 0x66, 0x03, 0xC0, 0x0C, 0x00, 0xC1, 0xFC, 0x03, 0xC0, 0x36, 0x03, 0x60, 0x73, 0x0F, 0x0F, 0x10, /* 'H' 0x48 */ 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xFF, 0xFE, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x06, /* 'I' 0x49 */ 0xFF, 0xFF, 0xFF, 0xC0, /* 'J' 0x4A */ 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x83, 0x07, 0x8F, 0x1E, 0x27, 0x80, /* 'K' 0x4B */ 0xC0, 0xF0, 0x6C, 0x33, 0x18, 0xCC, 0x37, 0x0F, 0xC3, 0x98, 0xC3, 0x30, 0xCC, 0x1B, 0x03, 0xC0, 0xC0, /* 'L' 0x4C */ 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xFF, /* 'M' 0x4D */ 0xE0, 0x3F, 0x01, 0xFC, 0x1F, 0xE0, 0xFD, 0x05, 0xEC, 0x6F, 0x63, 0x79, 0x13, 0xCD, 0x9E, 0x6C, 0xF1, 0x47, 0x8E, 0x3C, 0x71, 0x80, /* 'N' 0x4E */ 0xE0, 0x7C, 0x0F, 0xC1, 0xE8, 0x3D, 0x87, 0x98, 0xF1, 0x1E, 0x33, 0xC3, 0x78, 0x6F, 0x07, 0xE0, 0x7C, 0x0E, /* 'O' 0x4F */ 0x0F, 0x81, 0x83, 0x18, 0x0C, 0xC0, 0x6C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1B, 0x01, 0x98, 0x0C, 0x60, 0xC0, 0xF8, 0x00, /* 'P' 0x50 */ 0xFF, 0x30, 0x6C, 0x0F, 0x03, 0xC0, 0xF0, 0x6F, 0xF3, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x00, /* 'Q' 0x51 */ 0x0F, 0x81, 0x83, 0x18, 0x0C, 0xC0, 0x6C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1B, 0x01, 0x98, 0x6C, 0x60, 0xC0, 0xFB, 0x00, 0x08, /* 'R' 0x52 */ 0xFF, 0x8C, 0x0E, 0xC0, 0x6C, 0x06, 0xC0, 0x6C, 0x0C, 0xFF, 0x8C, 0x0E, 0xC0, 0x6C, 0x06, 0xC0, 0x6C, 0x06, 0xC0, 0x70, /* 'S' 0x53 */ 0x3F, 0x18, 0x6C, 0x0F, 0x03, 0xC0, 0x1E, 0x01, 0xF0, 0x0E, 0x00, 0xF0, 0x3C, 0x0D, 0x86, 0x3F, 0x00, /* 'T' 0x54 */ 0xFF, 0x86, 0x03, 0x01, 0x80, 0xC0, 0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x01, 0x80, 0xC0, /* 'U' 0x55 */ 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xB0, 0x61, 0xF0, /* 'V' 0x56 */ 0xC0, 0x6C, 0x0D, 0x81, 0x10, 0x63, 0x0C, 0x61, 0x04, 0x60, 0xCC, 0x19, 0x01, 0x60, 0x3C, 0x07, 0x00, 0x60, /* 'W' 0x57 */ 0xC1, 0x81, 0x61, 0xC3, 0x61, 0xC3, 0x61, 0x43, 0x62, 0x62, 0x22, 0x66, 0x32, 0x26, 0x36, 0x26, 0x14, 0x34, 0x14, 0x34, 0x1C, 0x1C, 0x18, 0x1C, 0x08, 0x18, /* 'X' 0x58 */ 0xC0, 0xD8, 0x66, 0x18, 0xCC, 0x1E, 0x07, 0x00, 0xC0, 0x78, 0x32, 0x0C, 0xC6, 0x1B, 0x07, 0xC0, 0xC0, /* 'Y' 0x59 */ 0xC0, 0x36, 0x06, 0x30, 0xC3, 0x0C, 0x19, 0x81, 0xD8, 0x0F, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, /* 'Z' 0x5A */ 0xFF, 0xC0, 0x60, 0x30, 0x0C, 0x06, 0x03, 0x01, 0xC0, 0x60, 0x30, 0x18, 0x06, 0x03, 0x00, 0xFF, 0xC0, /* '[' 0x5B */ 0xFB, 0x6D, 0xB6, 0xDB, 0x6D, 0xB6, 0xE0, /* '\' 0x5C */ 0x84, 0x10, 0x84, 0x10, 0x84, 0x10, 0x84, 0x10, 0x80, /* ']' 0x5D */ 0xED, 0xB6, 0xDB, 0x6D, 0xB6, 0xDB, 0xE0, /* '^' 0x5E */ 0x30, 0x60, 0xA2, 0x44, 0xD8, 0xA1, 0x80, /* '_' 0x5F */ 0xFF, 0xC0, /* '`' 0x60 */ 0xC6, 0x30, /* 'a' 0x61 */ 0x7E, 0x71, 0xB0, 0xC0, 0x60, 0xF3, 0xDB, 0x0D, 0x86, 0xC7, 0x3D, 0xC0, /* 'b' 0x62 */ 0xC0, 0x60, 0x30, 0x1B, 0xCE, 0x36, 0x0F, 0x07, 0x83, 0xC1, 0xE0, 0xF0, 0x7C, 0x6D, 0xE0, /* 'c' 0x63 */ 0x3C, 0x66, 0xC3, 0xC0, 0xC0, 0xC0, 0xC0, 0xC3, 0x66, 0x3C, /* 'd' 0x64 */ 0x03, 0x03, 0x03, 0x3B, 0x67, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x67, 0x3B, /* 'e' 0x65 */ 0x3C, 0x66, 0xC3, 0xC3, 0xFF, 0xC0, 0xC0, 0xC3, 0x66, 0x3C, /* 'f' 0x66 */ 0x36, 0x6F, 0x66, 0x66, 0x66, 0x66, 0x60, /* 'g' 0x67 */ 0x3B, 0x67, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x67, 0x3B, 0x03, 0x03, 0xC6, 0x7C, /* 'h' 0x68 */ 0xC0, 0xC0, 0xC0, 0xDE, 0xE3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, /* 'i' 0x69 */ 0xC3, 0xFF, 0xFF, 0xC0, /* 'j' 0x6A */ 0x30, 0x03, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0xE0, /* 'k' 0x6B */ 0xC0, 0xC0, 0xC0, 0xC2, 0xC4, 0xCC, 0xD8, 0xF8, 0xEC, 0xC4, 0xC6, 0xC3, 0xC3, /* 'l' 0x6C */ 0xFF, 0xFF, 0xFF, 0xC0, /* 'm' 0x6D */ 0xDE, 0xF7, 0x1C, 0xF0, 0xC7, 0x86, 0x3C, 0x31, 0xE1, 0x8F, 0x0C, 0x78, 0x63, 0xC3, 0x1E, 0x18, 0xC0, /* 'n' 0x6E */ 0xDE, 0xE3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, /* 'o' 0x6F */ 0x3C, 0x66, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x66, 0x3C, /* 'p' 0x70 */ 0xDE, 0x71, 0xB0, 0x78, 0x3C, 0x1E, 0x0F, 0x07, 0x83, 0xE3, 0x6F, 0x30, 0x18, 0x0C, 0x00, /* 'q' 0x71 */ 0x3B, 0x67, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x67, 0x3B, 0x03, 0x03, 0x03, /* 'r' 0x72 */ 0xDF, 0x31, 0x8C, 0x63, 0x18, 0xC6, 0x00, /* 's' 0x73 */ 0x3E, 0xE3, 0xC0, 0xC0, 0xE0, 0x3C, 0x07, 0xC3, 0xE3, 0x7E, /* 't' 0x74 */ 0x66, 0xF6, 0x66, 0x66, 0x66, 0x67, /* 'u' 0x75 */ 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC7, 0x7B, /* 'v' 0x76 */ 0xC1, 0xA0, 0x98, 0xCC, 0x42, 0x21, 0xB0, 0xD0, 0x28, 0x1C, 0x0C, 0x00, /* 'w' 0x77 */ 0xC6, 0x1E, 0x38, 0x91, 0xC4, 0xCA, 0x66, 0xD3, 0x16, 0xD0, 0xA6, 0x87, 0x1C, 0x38, 0xC0, 0xC6, 0x00, /* 'x' 0x78 */ 0x87, 0x89, 0xB1, 0xC3, 0x07, 0x1E, 0x26, 0xC5, 0x0C, /* 'y' 0x79 */ 0xC1, 0x43, 0x63, 0x62, 0x26, 0x36, 0x34, 0x1C, 0x1C, 0x18, 0x18, 0x18, 0x10, 0x60, /* 'z' 0x7A */ 0xFE, 0x0C, 0x30, 0xC1, 0x86, 0x18, 0x20, 0xC1, 0xFC, /* '{' 0x7B */ 0x36, 0x66, 0x66, 0x6E, 0xCE, 0x66, 0x66, 0x66, 0x30, /* '|' 0x7C */ 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, /* '}' 0x7D */ 0xC6, 0x66, 0x66, 0x67, 0x37, 0x66, 0x66, 0x66, 0xC0, /* '~' 0x7E */ 0x61, 0x24, 0x38, /* 0x7F */ /* 0x80 */ 0x07, 0xC6, 0x13, 0x00, 0xC0, 0x60, 0x3F, 0xE6, 0x03, 0xFC, 0x60, 0x0C, 0x03, 0x00, 0x61, 0x07, 0xC0, /* 0x81 */ /* 0x82 */ 0xDC, /* 0x83 */ 0x19, 0x8C, 0xF3, 0x18, 0xC6, 0x31, 0x8C, 0x63, 0x18, 0xC6, 0xE0, /* 0x84 */ 0xDA, 0x76, /* 0x85 */ 0xCC, 0xC0, /* 0x86 */ 0x18, 0x18, 0x18, 0x18, 0xFF, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, /* 0x87 */ 0x18, 0x18, 0x18, 0x18, 0xFF, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0xFF, 0x18, 0x18, 0x18, 0x18, /* 0x88 */ 0x72, 0xA2, /* 0x89 */ 0x70, 0x80, 0x22, 0x20, 0x08, 0x90, 0x02, 0x24, 0x00, 0x72, 0x00, 0x00, 0x80, 0x00, 0x40, 0x00, 0x10, 0x00, 0x09, 0xC7, 0x84, 0x8B, 0x31, 0x22, 0x84, 0x88, 0xB3, 0x21, 0xC7, 0x80, /* 0x8A */ 0x1B, 0x03, 0x80, 0x00, 0xFC, 0x61, 0xB0, 0x3C, 0x0F, 0x00, 0x78, 0x07, 0xC0, 0x38, 0x03, 0xC0, 0xF0, 0x36, 0x18, 0xFC, /* 0x8B */ 0x69, /* 0x8C */ 0x1E, 0xFE, 0x43, 0x81, 0x83, 0x06, 0x06, 0x0C, 0x0C, 0x18, 0x18, 0x30, 0x3F, 0xE0, 0x60, 0xC0, 0xC1, 0x81, 0x81, 0x83, 0x01, 0x8E, 0x01, 0xEF, 0xE0, /* 0x8D */ /* 0x8E */ 0x1B, 0x03, 0x80, 0x03, 0xFF, 0x01, 0x80, 0xC0, 0x30, 0x18, 0x0C, 0x07, 0x01, 0x80, 0xC0, 0x60, 0x18, 0x0C, 0x03, 0xFF, /* 0x8F */ /* 0x90 */ /* 0x91 */ 0x6B, /* 0x92 */ 0xD6, /* 0x93 */ 0x4C, 0xA5, 0xB0, /* 0x94 */ 0xDA, 0x53, 0x20, /* 0x95 */ 0x6F, 0xFF, 0x60, /* 0x96 */ 0xFE, /* 0x97 */ 0xFF, 0xFF, /* 0x98 */ 0x4D, 0xC0, /* 0x99 */ 0xFC, 0xE1, 0xCC, 0x38, 0x73, 0x0E, 0x1C, 0xC3, 0x8F, 0x30, 0xD2, 0xCC, 0x34, 0xB3, 0x0D, 0x6C, 0xC3, 0x53, 0x30, 0xCC, 0xCC, 0x33, 0x30, /* 0x9A */ 0x24, 0x3C, 0x18, 0x7E, 0xE3, 0xC0, 0xC0, 0x60, 0x3C, 0x07, 0xC3, 0xE3, 0x7E, /* 0x9B */ 0x96, /* 0x9C */ 0x3C, 0xF8, 0xCF, 0x1B, 0x0C, 0x1E, 0x18, 0x3C, 0x3F, 0xF8, 0x60, 0x30, 0xC0, 0x61, 0x83, 0x67, 0x8C, 0x79, 0xF0, /* 0x9D */ /* 0x9E */ 0x48, 0xF0, 0xC7, 0xF0, 0x61, 0x86, 0x0C, 0x30, 0xC1, 0x06, 0x0F, 0xE0, /* 0x9F */ 0x19, 0x80, 0x00, 0xC0, 0x36, 0x06, 0x30, 0xC3, 0x0C, 0x19, 0x81, 0xD8, 0x0F, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, /* 0xA0 */ /* 0xA1 */ 0xCF, 0xFF, 0xFF, 0xC0, /* 0xA2 */ 0x08, 0x04, 0x0F, 0x8D, 0x6C, 0x9E, 0x43, 0x21, 0x90, 0xC8, 0x64, 0xDA, 0xC7, 0xC0, 0x80, 0x40, /* 0xA3 */ 0x1F, 0x0C, 0x66, 0x0D, 0x83, 0x60, 0x0C, 0x0F, 0xC0, 0x60, 0x18, 0x06, 0x03, 0x01, 0xF1, 0x43, 0xC0, /* 0xA4 */ 0xFF, 0xDF, 0x1E, 0x3E, 0xFF, 0xC0, /* 0xA5 */ 0xC3, 0x42, 0x42, 0x24, 0x24, 0x3C, 0x18, 0x7E, 0x18, 0x7E, 0x18, 0x18, 0x18, /* 0xA6 */ 0xFF, 0xFC, 0x0F, 0xFF, 0xC0, /* 0xA7 */ 0x0C, 0x09, 0x0C, 0xC6, 0x63, 0x81, 0xE3, 0x19, 0x87, 0xE1, 0xB8, 0xC6, 0x41, 0xC0, 0x73, 0x19, 0x8C, 0x66, 0x1E, 0x00, /* 0xA8 */ 0xCC, /* 0xA9 */ 0x0F, 0xC0, 0x61, 0x87, 0x03, 0x9B, 0xC6, 0xD9, 0x8F, 0x60, 0x3D, 0x00, 0xF4, 0x03, 0xD8, 0x0D, 0xE6, 0x67, 0xF3, 0x86, 0x18, 0x0F, 0xC0, /* 0xAA */ 0x74, 0x8D, 0xA9, 0x7C, 0x1F, /* 0xAB */ 0x22, 0xCF, 0x26, 0x46, 0x64, 0x40, /* 0xAC */ 0xFF, 0x80, 0xC0, 0x60, 0x30, 0x18, /* 0xAD */ /* 0xAE */ 0x0F, 0xC0, 0x61, 0x87, 0x03, 0x9F, 0xE6, 0xD0, 0x8F, 0x42, 0x3D, 0xF0, 0xF4, 0x23, 0xD0, 0x8D, 0xC2, 0x67, 0x0B, 0x86, 0x18, 0x0F, 0xC0, /* 0xAF */ 0xF8, /* 0xB0 */ 0x74, 0x63, 0x17, 0x00, /* 0xB1 */ 0x0C, 0x06, 0x03, 0x07, 0xE0, 0xC0, 0x60, 0x30, 0x18, 0x00, 0x00, 0x3F, 0xE0, /* 0xB2 */ 0x7B, 0x30, 0xC3, 0x11, 0x84, 0x3F, /* 0xB3 */ 0x7D, 0x8C, 0x18, 0xC0, 0x60, 0xF1, 0xBE, /* 0xB4 */ 0x36, 0xC0, /* 0xB5 */ 0xC3, 0x61, 0xB0, 0xD8, 0x6C, 0x36, 0x1B, 0x0D, 0x86, 0xE7, 0x7D, 0xF0, 0x18, 0x0C, 0x00, /* 0xB6 */ 0x3F, 0x7E, 0xF2, 0xF2, 0xF2, 0xF2, 0xF2, 0x72, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, /* 0xB7 */ 0xE0, /* 0xB8 */ 0x21, 0xC7, 0xE0, /* 0xB9 */ 0x3D, 0xB6, 0xD8, /* 0xBA */ 0x74, 0x63, 0x18, 0xB8, 0x1F, /* 0xBB */ 0x89, 0x98, 0x99, 0x3C, 0xD1, 0x00, /* 0xBC */ 0x20, 0x43, 0x81, 0x06, 0x08, 0x18, 0x20, 0x61, 0x01, 0x84, 0x06, 0x21, 0x80, 0x86, 0x04, 0x78, 0x32, 0x60, 0x87, 0xC4, 0x06, 0x10, 0x18, /* 0xBD */ 0x20, 0x43, 0x81, 0x06, 0x08, 0x18, 0x20, 0x61, 0x01, 0x8D, 0xE6, 0x2C, 0xC1, 0x03, 0x0C, 0x0C, 0x20, 0x41, 0x86, 0x0C, 0x30, 0x20, 0xFC, /* 0xBE */ 0x78, 0x11, 0x98, 0x40, 0x31, 0x00, 0x82, 0x00, 0xC8, 0x01, 0x90, 0x33, 0x43, 0x3D, 0x06, 0x02, 0x3C, 0x08, 0x98, 0x10, 0xF8, 0x40, 0x61, 0x00, 0xC0, /* 0xBF */ 0x0C, 0x00, 0x00, 0x01, 0x80, 0xC0, 0xC0, 0xE0, 0xC0, 0xC0, 0x60, 0xF0, 0x6C, 0x63, 0xE0, /* 0xC0 */ 0x0C, 0xDB, 0xD3, 0x00, 0x00, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, /* 0xC1 */ 0x0E, 0x01, 0xC0, 0x6C, 0x0D, 0x81, 0xB0, 0x63, 0x0C, 0x61, 0xFC, 0x7F, 0xCC, 0x19, 0x83, 0x60, 0x3C, 0x06, /* 0xC2 */ 0xFF, 0x3F, 0xEC, 0x0F, 0x03, 0xC0, 0xFF, 0xEF, 0xFB, 0x03, 0xC0, 0xF0, 0x3C, 0x1F, 0xFE, 0xFF, 0x00, /* 0xC3 */ 0xFF, 0xFF, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, /* 0xC4 */ 0x07, 0x00, 0x38, 0x01, 0xC0, 0x1B, 0x00, 0xD8, 0x0C, 0x60, 0x63, 0x03, 0x18, 0x30, 0x61, 0x83, 0x18, 0x0C, 0xFF, 0xE7, 0xFF, 0x00, /* 0xC5 */ 0xFF, 0xFF, 0xFC, 0x03, 0x00, 0xC0, 0x3F, 0xEF, 0xFB, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0xFF, 0xFF, 0xC0, /* 0xC6 */ 0x7F, 0xDF, 0xF0, 0x18, 0x0C, 0x07, 0x01, 0x80, 0xC0, 0x60, 0x38, 0x0C, 0x06, 0x03, 0xFF, 0xFF, 0xC0, /* 0xC7 */ 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0xFF, 0xFF, 0xFE, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x06, /* 0xC8 */ 0x0F, 0x03, 0xFC, 0x70, 0xE6, 0x06, 0xC0, 0x3C, 0xF3, 0xCF, 0x3C, 0x03, 0xC0, 0x36, 0x06, 0x70, 0xE3, 0xFC, 0x1F, 0x80, /* 0xC9 */ 0xFF, 0xFF, 0xFF, 0xC0, /* 0xCA */ 0xC1, 0xD8, 0x73, 0x1C, 0x67, 0x0D, 0xC1, 0xF0, 0x3F, 0x07, 0x70, 0xC7, 0x18, 0x63, 0x0E, 0x60, 0xEC, 0x0E, /* 0xCB */ 0x07, 0x00, 0x38, 0x01, 0xC0, 0x1B, 0x00, 0xD8, 0x0C, 0x60, 0x63, 0x03, 0x18, 0x30, 0x61, 0x83, 0x1C, 0x1C, 0xC0, 0x66, 0x03, 0x00, /* 0xCC */ 0xE0, 0x3F, 0x83, 0xFC, 0x1F, 0xE0, 0xFD, 0x8D, 0xEC, 0x6F, 0x63, 0x79, 0x13, 0xCD, 0x9E, 0x6C, 0xF3, 0x67, 0x8E, 0x3C, 0x71, 0x80, /* 0xCD */ 0xC0, 0x7C, 0x0F, 0xC1, 0xF8, 0x3D, 0x87, 0x98, 0xF3, 0x9E, 0x33, 0xC3, 0x78, 0x3F, 0x07, 0xE0, 0x7C, 0x06, /* 0xCE */ 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x1F, 0xE7, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x03, 0xFF, 0xFF, 0xC0, /* 0xCF */ 0x0F, 0x83, 0xFC, 0x70, 0xE6, 0x06, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x36, 0x06, 0x70, 0xE3, 0xFC, 0x0F, 0x00, /* 0xD0 */ 0xFF, 0xFF, 0xFF, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x06, /* 0xD1 */ 0xFF, 0x3F, 0xEC, 0x1F, 0x03, 0xC0, 0xF0, 0x7F, 0xFB, 0xFC, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x00, /* 0xD2 */ /* 0xD3 */ 0xFF, 0xFF, 0xC0, 0x60, 0x30, 0x18, 0x0C, 0x18, 0x30, 0x60, 0xC0, 0xFF, 0xFF, /* 0xD4 */ 0xFF, 0xFF, 0xF0, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x00, /* 0xD5 */ 0xE0, 0x76, 0x06, 0x30, 0xC3, 0x9C, 0x19, 0x80, 0xF0, 0x0F, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, /* 0xD6 */ 0x06, 0x00, 0x60, 0x1F, 0x87, 0xFE, 0xE6, 0x7C, 0x63, 0xC6, 0x3C, 0x63, 0xE6, 0x77, 0xFE, 0x1F, 0x80, 0x60, 0x06, 0x00, /* 0xD7 */ 0x71, 0xC6, 0x30, 0x6C, 0x0D, 0x80, 0xE0, 0x1C, 0x03, 0x80, 0xD8, 0x1B, 0x07, 0x70, 0xC6, 0x30, 0x6E, 0x0E, /* 0xD8 */ 0xC6, 0x3C, 0x63, 0xC6, 0x3C, 0x63, 0xC6, 0x3C, 0x63, 0x46, 0x66, 0x66, 0x3F, 0xC0, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, /* 0xD9 */ 0x1F, 0x07, 0xF1, 0xC7, 0x70, 0x7C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0x40, 0x4C, 0x18, 0xEE, 0x7D, 0xFF, 0xBE, /* 0xDA */ 0xCF, 0x30, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, /* 0xDB */ 0x19, 0x81, 0x98, 0x00, 0x0E, 0x07, 0x60, 0x63, 0x0C, 0x39, 0xC1, 0x98, 0x0F, 0x00, 0xF0, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, /* 0xDC */ 0x06, 0x0C, 0x00, 0x3B, 0x7B, 0xEE, 0xC6, 0xC6, 0xC6, 0xC6, 0xEE, 0x7B, 0x3B, /* 0xDD */ 0x18, 0x20, 0x03, 0xCF, 0xF8, 0xB0, 0x38, 0x71, 0x83, 0x17, 0xF7, 0x80, /* 0xDE */ 0x0C, 0x18, 0x00, 0xDE, 0xFF, 0xE3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x03, 0x03, 0x03, 0x03, /* 0xDF */ 0x78, 0x6D, 0xB6, 0xDB, 0x6C, /* 0xE0 */ 0x0C, 0xDB, 0xD3, 0x00, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xE7, 0x7E, 0x3C, /* 0xE1 */ 0x3B, 0x7B, 0xEE, 0xC6, 0xC6, 0xC6, 0xC6, 0xEE, 0x7B, 0x3B, /* 0xE2 */ 0x3C, 0x7E, 0xC6, 0xC6, 0xC4, 0xD8, 0xDE, 0xC7, 0xC3, 0xC3, 0xE7, 0xFE, 0xDC, 0xC0, 0xC0, 0xC0, 0xC0, /* 0xE3 */ 0x61, 0x98, 0x66, 0x18, 0xCC, 0x33, 0x0C, 0xC1, 0xE0, 0x78, 0x1E, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, /* 0xE4 */ 0x7E, 0x7E, 0x30, 0x3C, 0x7E, 0xE7, 0xC3, 0xC3, 0xC3, 0xC3, 0xE7, 0x7E, 0x3C, /* 0xE5 */ 0x79, 0xFF, 0x16, 0x07, 0x0E, 0x30, 0x62, 0xFE, 0xF0, /* 0xE6 */ 0x7E, 0xFC, 0x30, 0xC3, 0x0C, 0x18, 0x60, 0xC1, 0x83, 0x07, 0xE7, 0xE0, 0xC1, 0x83, 0x0C, /* 0xE7 */ 0xDE, 0xFF, 0xE3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x03, 0x03, 0x03, 0x03, /* 0xE8 */ 0x3C, 0x7E, 0x66, 0xC3, 0xC3, 0xFF, 0xFF, 0xC3, 0xC3, 0xC3, 0x66, 0x7E, 0x3C, /* 0xE9 */ 0xFF, 0xFF, 0xF0, /* 0xEA */ 0xC3, 0x63, 0x33, 0x1B, 0x0F, 0x06, 0xC3, 0x31, 0x8C, 0xC6, 0x61, 0x80, /* 0xEB */ 0x30, 0x0C, 0x06, 0x03, 0x01, 0xC1, 0xE0, 0xD0, 0x6C, 0x36, 0x33, 0x18, 0xCC, 0x66, 0x30, /* 0xEC */ 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xE7, 0xFF, 0xDB, 0xC0, 0xC0, 0xC0, 0xC0, /* 0xED */ 0xC1, 0xE0, 0xD8, 0xCC, 0x66, 0x31, 0xB0, 0xD8, 0x38, 0x1C, 0x04, 0x00, /* 0xEE */ 0x7D, 0xFB, 0x06, 0x07, 0xC7, 0x9C, 0x70, 0xC1, 0x83, 0x83, 0xE3, 0xE0, 0xC1, 0x8E, 0x18, /* 0xEF */ 0x3C, 0x7E, 0xE7, 0xC3, 0xC3, 0xC3, 0xC3, 0xE7, 0x7E, 0x3C, /* 0xF0 */ 0xFF, 0xFF, 0xFF, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, /* 0xF1 */ 0x3C, 0x7E, 0xE7, 0xC3, 0xC3, 0xC3, 0xC3, 0xE7, 0xFE, 0xDC, 0xC0, 0xC0, 0xC0, 0xC0, /* 0xF2 */ 0x1E, 0xFD, 0x86, 0x0C, 0x18, 0x30, 0x70, 0x7C, 0x7C, 0x18, 0x33, 0xE7, 0x00, /* 0xF3 */ 0x3F, 0xDF, 0xFE, 0x63, 0x0C, 0xC3, 0x30, 0xCC, 0x33, 0x9C, 0x7E, 0x0F, 0x00, /* 0xF4 */ 0xFF, 0xF3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC0, /* 0xF5 */ 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xE7, 0x7E, 0x3C, /* 0xF6 */ 0x2F, 0x1B, 0xEC, 0xDF, 0x33, 0xCC, 0xF3, 0x3C, 0xCD, 0xB6, 0x7F, 0x8F, 0x80, 0xC0, 0x30, 0x0C, 0x03, 0x00, /* 0xF7 */ 0x63, 0x31, 0x8D, 0x86, 0xC3, 0x60, 0xE0, 0x70, 0x38, 0x1C, 0x1B, 0x0D, 0x86, 0xC6, 0x33, 0x18, /* 0xF8 */ 0xCC, 0xF3, 0x3C, 0xCF, 0x33, 0xCC, 0xF3, 0x3C, 0xCF, 0x33, 0x6D, 0x8F, 0xC0, 0xC0, 0x30, 0x0C, 0x03, 0x00, /* 0xF9 */ 0x30, 0xC6, 0x06, 0x66, 0x6C, 0x63, 0xC6, 0x3C, 0x63, 0xC6, 0x3E, 0xF7, 0x79, 0xE3, 0x9C, /* 0xFA */ 0xCF, 0x30, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, /* 0xFB */ 0x66, 0x66, 0x00, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xE7, 0x7E, 0x3C, /* 0xFC */ 0x0C, 0x18, 0x00, 0x3C, 0x7E, 0xE7, 0xC3, 0xC3, 0xC3, 0xC3, 0xE7, 0x7E, 0x3C, /* 0xFD */ 0x08, 0x10, 0x00, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xE7, 0x7E, 0x3C, /* 0xFE */ 0x03, 0x00, 0x60, 0x00, 0x03, 0x0C, 0x60, 0x66, 0x66, 0xC6, 0x3C, 0x63, 0xC6, 0x3C, 0x63, 0xEF, 0x77, 0x9E, 0x39, 0xC0, /* 0xFF */ }; const GFXglyph FreeSans9pt_Win1253Glyphs[] PROGMEM = { /* 0x01 */ { 0, 15, 15, 17, 1, -13 }, /* 0x02 */ { 29, 15, 15, 17, 1, -13 }, /* 0x03 */ { 58, 15, 16, 17, 1, -14 }, /* 0x04 */ { 88, 15, 16, 17, 1, -14 }, /* 0x05 */ { 118, 16, 15, 18, 1, -13 }, /* 0x06 */ { 148, 15, 15, 17, 1, -13 }, /* 0x07 */ { 177, 0, 0, 8, 0, 0 }, /* 0x08 */ { 177, 17, 16, 19, 1, -14 }, /* 0x09 */ { 211, 17, 12, 19, 1, -12 }, /* 0x0A */ { 237, 0, 0, 8, 0, 0 }, /* 0x0B */ { 237, 17, 16, 19, 1, -14 }, /* 0x0C */ { 271, 15, 14, 17, 1, -12 }, /* 0x0D */ { 298, 0, 0, 8, 0, 0 }, /* 0x0E */ { 298, 15, 16, 17, 1, -14 }, /* 0x0F */ { 328, 15, 15, 17, 1, -13 }, /* 0x10 */ { 357, 15, 15, 17, 1, -13 }, /* 0x11 */ { 386, 15, 16, 17, 1, -14 }, /* 0x12 */ { 416, 17, 17, 19, 1, -15 }, /* 0x13 */ { 453, 15, 16, 17, 1, -14 }, /* 0x14 */ { 483, 15, 16, 17, 1, -14 }, /* 0x15 */ { 513, 15, 16, 17, 1, -14 }, /* 0x16 */ { 543, 11, 16, 13, 1, -14 }, /* 0x17 */ { 565, 15, 16, 17, 1, -14 }, /* 0x18 */ { 595, 18, 15, 20, 1, -13 }, /* 0x19 */ { 629, 15, 16, 17, 1, -14 }, /* 0x1A */ { 659, 13, 14, 15, 1, -12 }, /* 0x1B */ { 682, 17, 16, 19, 1, -14 }, /* 0x1C */ { 716, 15, 16, 17, 1, -14 }, /* 0x1D */ { 746, 15, 15, 17, 1, -13 }, /* 0x1E */ { 775, 17, 16, 19, 1, -14 }, /* 0x1F */ { 809, 11, 16, 13, 1, -14 }, /* ' ' 0x20 */ { 831, 0, 0, 5, 0, 0 }, /* '!' 0x21 */ { 831, 2, 13, 6, 2, -12 }, /* '"' 0x22 */ { 835, 5, 4, 6, 1, -12 }, /* '#' 0x23 */ { 838, 10, 12, 10, 0, -11 }, /* '$' 0x24 */ { 853, 9, 16, 10, 1, -13 }, /* '%' 0x25 */ { 871, 16, 13, 16, 1, -12 }, /* '&' 0x26 */ { 897, 10, 13, 12, 1, -12 }, /* ''' 0x27 */ { 914, 2, 4, 4, 1, -12 }, /* '(' 0x28 */ { 915, 4, 17, 6, 1, -12 }, /* ')' 0x29 */ { 924, 4, 17, 6, 1, -12 }, /* '*' 0x2A */ { 933, 5, 5, 7, 1, -12 }, /* '+' 0x2B */ { 937, 6, 8, 11, 3, -7 }, /* ',' 0x2C */ { 943, 2, 4, 5, 2, 0 }, /* '-' 0x2D */ { 944, 4, 1, 6, 1, -4 }, /* '.' 0x2E */ { 945, 2, 1, 5, 1, 0 }, /* '/' 0x2F */ { 946, 5, 13, 5, 0, -12 }, /* '0' 0x30 */ { 955, 8, 13, 10, 1, -12 }, /* '1' 0x31 */ { 968, 4, 13, 10, 3, -12 }, /* '2' 0x32 */ { 975, 9, 13, 10, 1, -12 }, /* '3' 0x33 */ { 990, 8, 13, 10, 1, -12 }, /* '4' 0x34 */ { 1003, 7, 13, 10, 2, -12 }, /* '5' 0x35 */ { 1015, 9, 13, 10, 1, -12 }, /* '6' 0x36 */ { 1030, 9, 13, 10, 1, -12 }, /* '7' 0x37 */ { 1045, 8, 13, 10, 0, -12 }, /* '8' 0x38 */ { 1058, 9, 13, 10, 1, -12 }, /* '9' 0x39 */ { 1073, 8, 13, 10, 1, -12 }, /* ':' 0x3A */ { 1086, 2, 10, 5, 1, -9 }, /* ';' 0x3B */ { 1089, 3, 12, 5, 1, -8 }, /* '<' 0x3C */ { 1094, 9, 9, 11, 1, -8 }, /* '=' 0x3D */ { 1105, 9, 4, 11, 1, -5 }, /* '>' 0x3E */ { 1110, 9, 8, 11, 1, -7 }, /* '?' 0x3F */ { 1119, 9, 13, 10, 1, -12 }, /* '@' 0x40 */ { 1134, 17, 16, 18, 1, -12 }, /* 'A' 0x41 */ { 1168, 12, 13, 12, 0, -12 }, /* 'B' 0x42 */ { 1188, 11, 13, 12, 1, -12 }, /* 'C' 0x43 */ { 1206, 11, 13, 13, 1, -12 }, /* 'D' 0x44 */ { 1224, 11, 13, 13, 1, -12 }, /* 'E' 0x45 */ { 1242, 9, 13, 11, 1, -12 }, /* 'F' 0x46 */ { 1257, 8, 13, 11, 1, -12 }, /* 'G' 0x47 */ { 1270, 12, 13, 14, 1, -12 }, /* 'H' 0x48 */ { 1290, 11, 13, 13, 1, -12 }, /* 'I' 0x49 */ { 1308, 2, 13, 5, 2, -12 }, /* 'J' 0x4A */ { 1312, 7, 13, 10, 1, -12 }, /* 'K' 0x4B */ { 1324, 10, 13, 12, 1, -12 }, /* 'L' 0x4C */ { 1341, 8, 13, 10, 1, -12 }, /* 'M' 0x4D */ { 1354, 13, 13, 15, 1, -12 }, /* 'N' 0x4E */ { 1376, 11, 13, 13, 1, -12 }, /* 'O' 0x4F */ { 1394, 13, 13, 14, 1, -12 }, /* 'P' 0x50 */ { 1416, 10, 13, 12, 1, -12 }, /* 'Q' 0x51 */ { 1433, 13, 14, 14, 1, -12 }, /* 'R' 0x52 */ { 1456, 12, 13, 13, 1, -12 }, /* 'S' 0x53 */ { 1476, 10, 13, 12, 1, -12 }, /* 'T' 0x54 */ { 1493, 9, 13, 11, 1, -12 }, /* 'U' 0x55 */ { 1508, 11, 13, 13, 1, -12 }, /* 'V' 0x56 */ { 1526, 11, 13, 11, 0, -12 }, /* 'W' 0x57 */ { 1544, 16, 13, 17, 0, -12 }, /* 'X' 0x58 */ { 1570, 10, 13, 12, 1, -12 }, /* 'Y' 0x59 */ { 1587, 12, 13, 12, 0, -12 }, /* 'Z' 0x5A */ { 1607, 10, 13, 11, 1, -12 }, /* '[' 0x5B */ { 1624, 3, 17, 5, 1, -12 }, /* '\' 0x5C */ { 1631, 5, 13, 5, 0, -12 }, /* ']' 0x5D */ { 1640, 3, 17, 5, 0, -12 }, /* '^' 0x5E */ { 1647, 7, 7, 8, 1, -12 }, /* '_' 0x5F */ { 1654, 10, 1, 10, 0, 3 }, /* '`' 0x60 */ { 1656, 4, 3, 5, 0, -12 }, /* 'a' 0x61 */ { 1658, 9, 10, 10, 1, -9 }, /* 'b' 0x62 */ { 1670, 9, 13, 10, 1, -12 }, /* 'c' 0x63 */ { 1685, 8, 10, 9, 1, -9 }, /* 'd' 0x64 */ { 1695, 8, 13, 10, 1, -12 }, /* 'e' 0x65 */ { 1708, 8, 10, 10, 1, -9 }, /* 'f' 0x66 */ { 1718, 4, 13, 5, 1, -12 }, /* 'g' 0x67 */ { 1725, 8, 14, 10, 1, -9 }, /* 'h' 0x68 */ { 1739, 8, 13, 10, 1, -12 }, /* 'i' 0x69 */ { 1752, 2, 13, 4, 1, -12 }, /* 'j' 0x6A */ { 1756, 4, 17, 4, 0, -12 }, /* 'k' 0x6B */ { 1765, 8, 13, 9, 1, -12 }, /* 'l' 0x6C */ { 1778, 2, 13, 4, 1, -12 }, /* 'm' 0x6D */ { 1782, 13, 10, 15, 1, -9 }, /* 'n' 0x6E */ { 1799, 8, 10, 10, 1, -9 }, /* 'o' 0x6F */ { 1809, 8, 10, 10, 1, -9 }, /* 'p' 0x70 */ { 1819, 9, 13, 10, 1, -9 }, /* 'q' 0x71 */ { 1834, 8, 13, 10, 1, -9 }, /* 'r' 0x72 */ { 1847, 5, 10, 6, 1, -9 }, /* 's' 0x73 */ { 1854, 8, 10, 9, 1, -9 }, /* 't' 0x74 */ { 1864, 4, 12, 5, 1, -11 }, /* 'u' 0x75 */ { 1870, 8, 10, 10, 1, -9 }, /* 'v' 0x76 */ { 1880, 9, 10, 9, 0, -9 }, /* 'w' 0x77 */ { 1892, 13, 10, 13, 0, -9 }, /* 'x' 0x78 */ { 1909, 7, 10, 9, 1, -9 }, /* 'y' 0x79 */ { 1918, 8, 14, 9, 0, -9 }, /* 'z' 0x7A */ { 1932, 7, 10, 9, 1, -9 }, /* '{' 0x7B */ { 1941, 4, 17, 6, 1, -12 }, /* '|' 0x7C */ { 1950, 2, 17, 4, 2, -12 }, /* '}' 0x7D */ { 1955, 4, 17, 6, 1, -12 }, /* '~' 0x7E */ { 1964, 7, 3, 9, 1, -7 }, /* 0x7F */ { 1967, 0, 0, 0, 0, 0 }, /* 0x80 */ { 1967, 10, 13, 12, 1, -12 }, /* 0x81 */ { 1984, 0, 0, 8, 0, 0 }, /* 0x82 */ { 1984, 2, 3, 5, 1, 0 }, /* 0x83 */ { 1985, 5, 17, 5, 0, -12 }, /* 0x84 */ { 1996, 5, 3, 7, 1, 0 }, /* 0x85 */ { 1998, 10, 1, 12, 1, 0 }, /* 0x86 */ { 2000, 8, 16, 10, 1, -12 }, /* 0x87 */ { 2016, 8, 16, 10, 1, -12 }, /* 0x88 */ { 2032, 5, 3, 6, 0, -12 }, /* 0x89 */ { 2034, 18, 13, 18, 0, -12 }, /* 0x8A */ { 2064, 10, 16, 12, 1, -15 }, /* 0x8B */ { 2084, 2, 4, 4, 1, -6 }, /* 0x8C */ { 2085, 15, 13, 18, 1, -12 }, /* 0x8D */ { 2110, 0, 0, 8, 0, 0 }, /* 0x8E */ { 2110, 10, 16, 11, 1, -15 }, /* 0x8F */ { 2130, 0, 0, 8, 0, 0 }, /* 0x90 */ { 2130, 0, 0, 8, 0, 0 }, /* 0x91 */ { 2130, 2, 4, 4, 2, -12 }, /* 0x92 */ { 2131, 2, 4, 4, 1, -12 }, /* 0x93 */ { 2132, 5, 4, 7, 2, -12 }, /* 0x94 */ { 2135, 5, 4, 7, 1, -12 }, /* 0x95 */ { 2138, 4, 5, 7, 1, -8 }, /* 0x96 */ { 2141, 7, 1, 9, 1, -4 }, /* 0x97 */ { 2142, 16, 1, 18, 1, -4 }, /* 0x98 */ { 2144, 5, 2, 6, 0, -12 }, /* 0x99 */ { 2146, 18, 10, 18, 1, -13 }, /* 0x9A */ { 2169, 8, 13, 9, 1, -12 }, /* 0x9B */ { 2182, 2, 4, 5, 2, -6 }, /* 0x9C */ { 2183, 15, 10, 17, 1, -9 }, /* 0x9D */ { 2202, 0, 0, 8, 0, 0 }, /* 0x9E */ { 2202, 7, 13, 9, 1, -12 }, /* 0x9F */ { 2214, 12, 14, 12, 0, -13 }, /* 0xA0 */ { 2235, 0, 0, 5, 0, 0 }, /* 0xA1 */ { 2235, 2, 13, 6, 2, -8 }, /* 0xA2 */ { 2239, 9, 14, 10, 1, -11 }, /* 0xA3 */ { 2255, 10, 13, 10, 0, -12 }, /* 0xA4 */ { 2272, 7, 6, 10, 2, -8 }, /* 0xA5 */ { 2278, 8, 13, 10, 1, -12 }, /* 0xA6 */ { 2291, 2, 17, 5, 2, -12 }, /* 0xA7 */ { 2296, 9, 17, 10, 1, -12 }, /* 0xA8 */ { 2316, 6, 1, 6, 0, -11 }, /* 0xA9 */ { 2317, 14, 13, 14, 1, -12 }, /* 0xAA */ { 2340, 5, 8, 7, 1, -12 }, /* 0xAB */ { 2345, 7, 6, 9, 1, -7 }, /* 0xAC */ { 2351, 9, 5, 11, 2, -5 }, /* 0xAD */ { 2357, 0, 0, 0, 0, 0 }, /* 0xAE */ { 2357, 14, 13, 14, 1, -12 }, /* 0xAF */ { 2380, 5, 1, 6, 0, -12 }, /* 0xB0 */ { 2381, 5, 5, 11, 3, -11 }, /* 0xB1 */ { 2385, 9, 11, 11, 1, -10 }, /* 0xB2 */ { 2398, 6, 8, 6, 1, -13 }, /* 0xB3 */ { 2404, 7, 8, 6, 0, -13 }, /* 0xB4 */ { 2411, 4, 3, 6, 2, -12 }, /* 0xB5 */ { 2413, 9, 13, 10, 1, -9 }, /* 0xB6 */ { 2428, 8, 16, 10, 2, -12 }, /* 0xB7 */ { 2444, 3, 1, 5, 1, -4 }, /* 0xB8 */ { 2445, 5, 4, 6, 1, 1 }, /* 0xB9 */ { 2448, 3, 7, 6, 2, -13 }, /* 0xBA */ { 2451, 5, 8, 7, 1, -12 }, /* 0xBB */ { 2456, 7, 6, 9, 1, -7 }, /* 0xBC */ { 2462, 14, 13, 16, 2, -12 }, /* 0xBD */ { 2485, 14, 13, 16, 2, -12 }, /* 0xBE */ { 2508, 15, 13, 16, 1, -12 }, /* 0xBF */ { 2533, 9, 13, 10, 1, -8 }, /* 0xC0 */ { 2548, 8, 15, 4, -2, -15 }, /* 0xC1 */ { 2563, 11, 13, 11, 0, -13 }, /* 0xC2 */ { 2581, 10, 13, 12, 1, -13 }, /* 0xC3 */ { 2598, 8, 13, 10, 2, -13 }, /* 0xC4 */ { 2611, 13, 13, 12, -1, -13 }, /* 0xC5 */ { 2633, 10, 13, 12, 1, -13 }, /* 0xC6 */ { 2650, 10, 13, 11, 0, -13 }, /* 0xC7 */ { 2667, 11, 13, 13, 1, -13 }, /* 0xC8 */ { 2685, 12, 13, 14, 1, -13 }, /* 0xC9 */ { 2705, 2, 13, 4, 1, -13 }, /* 0xCA */ { 2709, 11, 13, 12, 1, -13 }, /* 0xCB */ { 2727, 13, 13, 12, -1, -13 }, /* 0xCC */ { 2749, 13, 13, 15, 1, -13 }, /* 0xCD */ { 2771, 11, 13, 13, 1, -13 }, /* 0xCE */ { 2789, 10, 13, 12, 1, -13 }, /* 0xCF */ { 2806, 12, 13, 14, 1, -13 }, /* 0xD0 */ { 2826, 11, 13, 13, 1, -13 }, /* 0xD1 */ { 2844, 10, 13, 12, 1, -13 }, /* 0xD2 */ { 2861, 0, 0, 5, 0, 0 }, /* 0xD3 */ { 2861, 8, 13, 11, 2, -13 }, /* 0xD4 */ { 2874, 10, 13, 12, 1, -13 }, /* 0xD5 */ { 2891, 12, 13, 12, 0, -13 }, /* 0xD6 */ { 2911, 12, 13, 14, 1, -13 }, /* 0xD7 */ { 2931, 11, 13, 11, 0, -13 }, /* 0xD8 */ { 2949, 12, 13, 14, 1, -13 }, /* 0xD9 */ { 2969, 11, 13, 13, 1, -13 }, /* 0xDA */ { 2987, 6, 16, 4, -1, -16 }, /* 0xDB */ { 2999, 12, 16, 12, 0, -16 }, /* 0xDC */ { 3023, 8, 13, 10, 1, -13 }, /* 0xDD */ { 3036, 7, 13, 8, 1, -13 }, /* 0xDE */ { 3048, 8, 17, 10, 1, -13 }, /* 0xDF */ { 3065, 3, 13, 4, 1, -13 }, /* 0xE0 */ { 3070, 8, 14, 10, 1, -14 }, /* 0xE1 */ { 3084, 8, 10, 10, 1, -10 }, /* 0xE2 */ { 3094, 8, 17, 10, 1, -13 }, /* 0xE3 */ { 3111, 10, 14, 8, -1, -10 }, /* 0xE4 */ { 3129, 8, 13, 10, 1, -13 }, /* 0xE5 */ { 3142, 7, 10, 8, 1, -10 }, /* 0xE6 */ { 3151, 7, 17, 8, 1, -13 }, /* 0xE7 */ { 3166, 8, 14, 10, 1, -10 }, /* 0xE8 */ { 3180, 8, 13, 10, 1, -13 }, /* 0xE9 */ { 3193, 2, 10, 4, 1, -10 }, /* 0xEA */ { 3196, 9, 10, 9, 1, -10 }, /* 0xEB */ { 3208, 9, 13, 9, 0, -13 }, /* 0xEC */ { 3223, 8, 14, 10, 1, -10 }, /* 0xED */ { 3237, 9, 10, 9, 0, -10 }, /* 0xEE */ { 3249, 7, 17, 8, 1, -13 }, /* 0xEF */ { 3264, 8, 10, 10, 1, -10 }, /* 0xF0 */ { 3274, 12, 10, 12, 0, -10 }, /* 0xF1 */ { 3289, 8, 14, 10, 1, -10 }, /* 0xF2 */ { 3303, 7, 14, 9, 1, -10 }, /* 0xF3 */ { 3316, 10, 10, 11, 1, -10 }, /* 0xF4 */ { 3329, 6, 10, 8, 1, -10 }, /* 0xF5 */ { 3337, 8, 10, 10, 1, -10 }, /* 0xF6 */ { 3347, 10, 14, 12, 1, -10 }, /* 0xF7 */ { 3365, 9, 14, 9, 0, -10 }, /* 0xF8 */ { 3381, 10, 14, 12, 1, -10 }, /* 0xF9 */ { 3399, 12, 10, 14, 1, -10 }, /* 0xFA */ { 3414, 6, 13, 4, -1, -13 }, /* 0xFB */ { 3424, 8, 13, 10, 1, -13 }, /* 0xFC */ { 3437, 8, 13, 10, 1, -13 }, /* 0xFD */ { 3450, 8, 13, 10, 1, -13 }, /* 0xFE */ { 3463, 12, 13, 14, 1, -13 }, /* 0xFF */ { 3483, 0, 0, 5, 0, 0 }, }; const GFXfont FreeSans9pt_Win1253 PROGMEM = { (uint8_t*)FreeSans9pt_Win1253Bitmaps, (GFXglyph*)FreeSans9pt_Win1253Glyphs, 0x01, 0xFF, 16 }; ================================================ FILE: src/graphics/niche/Fonts/README.md ================================================ # NicheGraphics - Fonts A common area to store fonts which might be reused by different Niche Graphics UIs In future, we may want to separate these by library (AdafruitGFX, u8g2, etc) ================================================ FILE: src/graphics/niche/InkHUD/Applet.cpp ================================================ #include "graphics/niche/InkHUD/Tile.h" #include #ifdef MESHTASTIC_INCLUDE_INKHUD #include "./Applet.h" #include "main.h" #include "RTC.h" using namespace NicheGraphics; InkHUD::AppletFont InkHUD::Applet::fontLarge; // General purpose fonts. Set in nicheGraphics.h InkHUD::AppletFont InkHUD::Applet::fontMedium; InkHUD::AppletFont InkHUD::Applet::fontSmall; constexpr float InkHUD::Applet::LOGO_ASPECT_RATIO; // Ratio of the Meshtastic logo InkHUD::Applet::Applet() : GFX(0, 0) { // GFX is given initial dimensions of 0 // The width and height will change dynamically, depending on Applet tiling // If you're getting a "divide by zero error", consider it an assert: // WindowManager should be the only one controlling the rendering inkhud = InkHUD::getInstance(); settings = &inkhud->persistence->settings; latestMessage = &inkhud->persistence->latestMessage; } // Draw a single pixel // The raw pixel output generated by AdafruitGFX drawing all passes through here // Hand off to the applet's tile, which will in-turn pass to the renderer void InkHUD::Applet::drawPixel(int16_t x, int16_t y, uint16_t color) { // Only render pixels if they fall within user's cropped region if (x >= cropLeft && x < (cropLeft + cropWidth) && y >= cropTop && y < (cropTop + cropHeight)) assignedTile->handleAppletPixel(x, y, static_cast(color)); } // Link our applet to a tile // This can only be called by Tile::assignApplet // The tile determines the applets dimensions // Pixel output is passed to tile during render() void InkHUD::Applet::setTile(Tile *t) { // If we're setting (not clearing), make sure the link is "reciprocal" if (t) assert(t->getAssignedApplet() == this); assignedTile = t; } // The tile to which our applet is assigned InkHUD::Tile *InkHUD::Applet::getTile() { return assignedTile; } // Draw the applet void InkHUD::Applet::render(bool full) { assert(assignedTile); // Ensure that we have a tile assert(assignedTile->getAssignedApplet() == this); // Ensure that we have a reciprocal link with the tile // WindowManager::update has now consumed the info about our update request // Clear everything for future requests wantRender = false; // Flag set by requestUpdate wantAutoshow = false; // Flag set by requestAutoShow. May or may not have been honored. wantUpdateType = Drivers::EInk::UpdateTypes::UNSPECIFIED; // Update type we wanted. May on may not have been granted. wantFullRender = true; // Default to a full render updateDimensions(); resetDrawingSpace(); onRender(full); // Draw the applet // Handle "Tile Highlighting" // Some devices may use an auxiliary button to switch between tiles // When this happens, we temporarily highlight the newly focused tile with a border // If our tile is (or was) highlighted, to indicate a change in focus if (Tile::highlightTarget == assignedTile) { // Draw the highlight if (!Tile::highlightShown) { drawRect(0, 0, width(), height(), BLACK); Tile::startHighlightTimeout(); Tile::highlightShown = true; } // Clear the highlight else { Tile::cancelHighlightTimeout(); Tile::highlightShown = false; Tile::highlightTarget = nullptr; } } } // Does the applet want to render now? // Checks whether the applet called requestUpdate recently, in response to an event // Used by WindowManager::update bool InkHUD::Applet::wantsToRender() { return wantRender; } // Does the applet want to be moved to foreground before next render, to show new data? // User specifies whether an applet has permission for this, using the on-screen menu // Used by WindowManager::update bool InkHUD::Applet::wantsToAutoshow() { return wantAutoshow; } // Which technique would this applet prefer that the display use to change the image? // Used by WindowManager::update Drivers::EInk::UpdateTypes InkHUD::Applet::wantsUpdateType() { return wantUpdateType; } bool InkHUD::Applet::wantsFullRender() { return wantFullRender; } // Get size of the applet's drawing space from its tile // Performed immediately before derived applet's drawing code runs void InkHUD::Applet::updateDimensions() { assert(assignedTile); WIDTH = assignedTile->getWidth(); HEIGHT = assignedTile->getHeight(); _width = WIDTH; _height = HEIGHT; } // Ensure that render() always starts with the same initial drawing config void InkHUD::Applet::resetDrawingSpace() { resetCrop(); // Allow pixel from any region of the applet to draw setTextColor(BLACK); // Reset text params setCursor(0, 0); setTextWrap(false); setFont(fontSmall); } // Sets one or more inputs to enabled/disabled for this applet and if they should be sent to it void InkHUD::Applet::setInputsSubscribed(uint8_t input, bool captured) { if (captured) subscribedInputs |= input; else subscribedInputs &= ~input; } // Checks if a specific input is enabled for this applet and should be sent to it bool InkHUD::Applet::isInputSubscribed(InputMask input) { return (subscribedInputs & input) == input; } // Tell InkHUD::Renderer that we want to render now // Applets should internally listen for events they are interested in, via MeshModule, CallbackObserver etc // When an applet decides it has heard something important, and wants to redraw, it calls this method // Once the renderer has given other applets a chance to process whatever event we just detected, // it will run Applet::render(), which may draw our applet to screen, if it is shown (foreground) // We should requestUpdate even if our applet is currently background, because this might be changed by autoshow void InkHUD::Applet::requestUpdate(Drivers::EInk::UpdateTypes type, bool full) { wantRender = true; wantUpdateType = type; wantFullRender = full; inkhud->requestUpdate(); } // Ask window manager to move this applet to foreground at start of next render // Users select which applets have permission for this using the on-screen menu void InkHUD::Applet::requestAutoshow() { wantAutoshow = true; } // Called when an Applet begins running // Active applets are considered "enabled" // They should now listen for events, and request their own updates // They may also be unexpectedly renderer at any time by other InkHUD components // Applets can be activated at run-time through the on-screen menu void InkHUD::Applet::activate() { onActivate(); // Call derived class' handler active = true; } // Called when an Applet stops running // Inactive applets are considered "disabled" // They should not listen for events, process data // They will not be rendered // Applets can be deactivated at run-time through the on-screen menu void InkHUD::Applet::deactivate() { // If applet is still in foreground, run its onBackground code first if (isForeground()) sendToBackground(); // If applet is active, run its onDeactivate code first if (isActive()) onDeactivate(); // Derived class' handler active = false; } // Is the Applet running? // Note: active / inactive is not related to background / foreground // An inactive applet is *fully* disabled bool InkHUD::Applet::isActive() { return active; } // Begin showing the Applet // It will be rendered immediately to whichever tile it is assigned // The Renderer will also now honor requestUpdate() calls from this applet void InkHUD::Applet::bringToForeground() { if (!foreground) { foreground = true; onForeground(); // Run derived applet class' handler } requestUpdate(); } // Stop showing the Applet // Calls to requestUpdate() will no longer be honored // When one applet moves to background, another should move to foreground (exception: some system applets) void InkHUD::Applet::sendToBackground() { if (foreground) { foreground = false; onBackground(); // Run derived applet class' handler } } // Is the applet currently displayed on a tile // Note: in some uncommon situations, an applet may be "foreground", and still not visible. // This can occur when a system applet is covering the screen (e.g. during BLE pairing) // This is not our applets responsibility to handle, // as in those situations, the system applet will have "locked" rendering bool InkHUD::Applet::isForeground() { return foreground; } // Limit drawing to a certain region of the applet // Pixels outside this region will be discarded void InkHUD::Applet::setCrop(int16_t left, int16_t top, uint16_t width, uint16_t height) { cropLeft = left; cropTop = top; cropWidth = width; cropHeight = height; } // Allow drawing to any region of the Applet // Reverses Applet::setCrop void InkHUD::Applet::resetCrop() { setCrop(0, 0, width(), height()); } // Convert relative width to absolute width, in px // X(0) is 0 // X(0.5) is width() / 2 // X(1) is width() uint16_t InkHUD::Applet::X(float f) { return width() * f; } // Convert relative hight to absolute height, in px // Y(0) is 0 // Y(0.5) is height() / 2 // Y(1) is height() uint16_t InkHUD::Applet::Y(float f) { return height() * f; } // Print text, specifying the position of any edge / corner of the textbox void InkHUD::Applet::printAt(int16_t x, int16_t y, const char *text, HorizontalAlignment ha, VerticalAlignment va) { // We do still have to run getTextBounds to find the width int16_t textOffsetX, textOffsetY; uint16_t textWidth, textHeight; getTextBounds(text, 0, 0, &textOffsetX, &textOffsetY, &textWidth, &textHeight); int16_t cursorX = 0; int16_t cursorY = 0; switch (ha) { case LEFT: cursorX = x - textOffsetX; break; case CENTER: cursorX = (x - textOffsetX) - (textWidth / 2); break; case RIGHT: cursorX = (x - textOffsetX) - textWidth; break; } // We're using a fixed line height, rather than sizing to text (getTextBounds) switch (va) { case TOP: cursorY = y + currentFont.heightAboveCursor(); break; case MIDDLE: cursorY = (y + currentFont.heightAboveCursor()) - (currentFont.lineHeight() / 2); break; case BOTTOM: cursorY = (y + currentFont.heightAboveCursor()) - currentFont.lineHeight(); break; } setCursor(cursorX, cursorY); print(text); } // Print text, specifying the position of any edge / corner of the textbox void InkHUD::Applet::printAt(int16_t x, int16_t y, const std::string &text, HorizontalAlignment ha, VerticalAlignment va) { printAt(x, y, text.c_str(), ha, va); } // Set which font should be used for subsequent drawing // This is AppletFont type, which is a wrapper for AdafruitGFX font, with some precalculated dimension data void InkHUD::Applet::setFont(AppletFont f) { GFX::setFont(f.gfxFont); currentFont = f; } // Get which font is currently being used for drawing // This is AppletFont type, which is a wrapper for AdafruitGFX font, with some precalculated dimension data InkHUD::AppletFont InkHUD::Applet::getFont() { return currentFont; } // Parse any text which might have "special characters" // Re-encodes UTF-8 characters to match our 8-bit encoded fonts std::string InkHUD::Applet::parse(const std::string &text) { return getFont().decodeUTF8(text); } // Get the best version of a node's short name available to us // Parses any non-ascii chars // Swaps for last-four of node-id if the real short name is unknown or can't be rendered (emoji) std::string InkHUD::Applet::parseShortName(meshtastic_NodeInfoLite *node) { assert(node); // Use the true shortname if known, and doesn't contain any unprintable characters (emoji, etc.) if (node->has_user) { std::string parsed = parse(node->user.short_name); if (isPrintable(parsed)) return parsed; } // Otherwise, use the "last 4" of node id // - if short name unknown, or // - if short name is emoji (we can't render this) std::string nodeID = hexifyNodeNum(node->num); return nodeID.substr(nodeID.length() - 4); } // Determine if all characters of a string are printable using the current font bool InkHUD::Applet::isPrintable(const std::string &text) { // Scan for SUB (0x1A), which is the value assigned by AppletFont::applyEncoding if a unicode character is not handled for (const char &c : text) { if (c == '\x1A') return false; } // No unprintable characters found return true; } // Gets rendered width of a string // Wrapper for getTextBounds uint16_t InkHUD::Applet::getTextWidth(const char *text) { // We do still have to run getTextBounds to find the width int16_t textOffsetX, textOffsetY; uint16_t textWidth, textHeight; getTextBounds(text, 0, 0, &textOffsetX, &textOffsetY, &textWidth, &textHeight); return textWidth; } // Gets rendered width of a string // Wrapper for getTextBounds uint16_t InkHUD::Applet::getTextWidth(const std::string &text) { return getTextWidth(text.c_str()); } // Evaluate SNR and RSSI to qualify signal strength at one of four discrete levels // Roughly comparable to values used by the iOS app; // I didn't actually go look up the code, just fit to a sample graphic I have of the iOS signal indicator InkHUD::Applet::SignalStrength InkHUD::Applet::getSignalStrength(float snr, float rssi) { uint8_t score = 0; // Give a score for the SNR if (snr > -17.5) score += 2; else if (snr > -26.0) score += 1; // Give a score for the RSSI if (rssi > -115.0) score += 3; else if (rssi > -120.0) score += 2; else if (rssi > -126.0) score += 1; // Combine scores, then give a result if (score >= 5) return SIGNAL_GOOD; else if (score >= 4) return SIGNAL_FAIR; else if (score > 0) return SIGNAL_BAD; else return SIGNAL_NONE; } // Apply the standard "node id" formatting to a nodenum int: !0123abdc std::string InkHUD::Applet::hexifyNodeNum(NodeNum num) { // Not found in nodeDB, show a hex nodeid instead char nodeIdHex[10]; sprintf(nodeIdHex, "!%0x", num); // Convert to the typical "fixed width hex with !" format return std::string(nodeIdHex); } // Print text, with word wrapping // Avoids splitting words in half, instead moving the entire word to a new line wherever possible void InkHUD::Applet::printWrapped(int16_t left, int16_t top, uint16_t width, const std::string &text) { // Place the AdafruitGFX cursor to suit our "top" coord setCursor(left, top + getFont().heightAboveCursor()); // How wide a space character is // Used when simulating print, for dimensioning // Works around issues where getTextDimensions() doesn't account for whitespace const uint8_t wSp = getFont().widthBetweenWords(); // Move through our text, character by character uint16_t wordStart = 0; for (uint16_t i = 0; i < text.length(); i++) { // Found: end of word (split by spaces or newline) // Also handles end of string if (text[i] == ' ' || text[i] == '\n' || i == text.length() - 1) { // Isolate this word uint16_t wordLength = (i - wordStart) + 1; // Plus one. Imagine: "a". End - Start is 0, but length is 1 std::string word = text.substr(wordStart, wordLength); wordStart = i + 1; // Next word starts *after* the space // If word is terminated by a newline char, don't actually print it. // We'll manually add a new line later if (word.back() == '\n') word.pop_back(); // Measure the word, in px int16_t l, t; uint16_t w, h; getTextBounds(word.c_str(), getCursorX(), getCursorY(), &l, &t, &w, &h); // Word is short if (w < width) { // Word fits on current line if ((l + w + wSp) < left + width) print(word.c_str()); // Word doesn't fit on current line else { setCursor(left, getCursorY() + getFont().lineHeight()); // Newline print(word.c_str()); } } // Word is really long // (wider than applet) else { // Horribly inefficient: // Rather than working directly with the glyph sizes, // we're going to run everything through getTextBounds as a c-string of length 1 // This is because AdafruitGFX has special internal handling for their legacy 6x8 font, // which would be a pain to add manually here. // These super-long strings probably don't come up often so we can maybe tolerate this. // Todo: rewrite making use of AdafruitGFX native text wrapping char cstr[] = {0, 0}; int16_t bx, by; uint16_t bw, bh; for (uint16_t c = 0; c < word.length(); c++) { // Shove next char into a c string cstr[0] = word[c]; getTextBounds(cstr, getCursorX(), getCursorY(), &bx, &by, &bw, &bh); // Manual newline, if next character will spill beyond screen edge if ((bx + bw) > left + width) setCursor(left, getCursorY() + getFont().lineHeight()); // Print next character print(word[c]); } } } // If word was terminated by a newline char, manually add the new line now if (text[i] == '\n') { setCursor(left, getCursorY() + getFont().lineHeight()); // Manual newline wordStart = i + 1; // New word begins after the newline. Otherwise print will add an *extra* line } } } // Simulate running printWrapped, to determine how tall the block of text will be. // This is a wasteful way of handling things. Maybe some way to optimize in future? uint32_t InkHUD::Applet::getWrappedTextHeight(int16_t left, uint16_t width, const std::string &text) { // Cache the current crop region int16_t cL = cropLeft; int16_t cT = cropTop; uint16_t cW = cropWidth; uint16_t cH = cropHeight; setCrop(-1, -1, 0, 0); // Set crop to temporarily discard all pixels printWrapped(left, 0, width, text); // Simulate only - no pixels drawn // Restore previous crop region cropLeft = cL; cropTop = cT; cropWidth = cW; cropHeight = cH; // Note: printWrapped() offsets the initial cursor position by heightAboveCursor() val, // so we need to account for that when determining the height return (getCursorY() + getFont().heightBelowCursor()); } // Fill a region with sparse diagonal lines, to create a pseudo-translucent fill void InkHUD::Applet::hatchRegion(int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t spacing, Color color) { // Cache the currently cropped region int16_t oldCropL = cropLeft; int16_t oldCropT = cropTop; uint16_t oldCropW = cropWidth; uint16_t oldCropH = cropHeight; setCrop(x, y, w, h); // Draw lines starting along the top edge, every few px for (int16_t ix = x; ix < x + w; ix += spacing) { for (int16_t i = 0; i < w || i < h; i++) { drawPixel(ix + i, y + i, color); } } // Draw lines starting along the left edge, every few px for (int16_t iy = y; iy < y + h; iy += spacing) { for (int16_t i = 0; i < w || i < h; i++) { drawPixel(x + i, iy + i, color); } } // Restore any previous crop // If none was set, this will clear cropLeft = oldCropL; cropTop = oldCropT; cropWidth = oldCropW; cropHeight = oldCropH; } // Get a human readable time representation of an epoch time (seconds since 1970) // If time is invalid, this will be an empty string std::string InkHUD::Applet::getTimeString(uint32_t epochSeconds) { #ifdef BUILD_EPOCH constexpr uint32_t validAfterEpoch = BUILD_EPOCH - (SEC_PER_DAY * 30 * 6); // 6 Months prior to build #else constexpr uint32_t validAfterEpoch = 1738368000 - (SEC_PER_DAY * 30 * 6); // 6 Months prior to Feb 1, 2025 12:00:00 AM GMT #endif uint32_t epochNow = getValidTime(RTCQuality::RTCQualityDevice, true); int32_t daysAgo = (epochNow - epochSeconds) / SEC_PER_DAY; int32_t hoursAgo = (epochNow - epochSeconds) / SEC_PER_HOUR; // Times are invalid: rtc is much older than when code was built // Don't give any human readable string if (epochNow <= validAfterEpoch) return ""; // Times are invalid: argument time is significantly ahead of RTC // Don't give any human readable string if (daysAgo < -2) return ""; // Times are probably invalid: more than 6 months ago if (daysAgo > 6 * 30) return ""; if (daysAgo > 1) return to_string(daysAgo) + " days ago"; else if (hoursAgo > 18) return "Yesterday"; else { uint32_t hms = epochSeconds % SEC_PER_DAY; hms = (hms + SEC_PER_DAY) % SEC_PER_DAY; // Tear apart hms into h:m uint32_t hour = hms / SEC_PER_HOUR; uint32_t min = (hms % SEC_PER_HOUR) / SEC_PER_MIN; // Format the clock string, either 12 hour or 24 hour char clockStr[11]; if (config.display.use_12h_clock) sprintf(clockStr, "%u:%02u %s", (hour % 12 == 0 ? 12 : hour % 12), min, hour > 11 ? "PM" : "AM"); else sprintf(clockStr, "%02u:%02u", hour, min); return clockStr; } } // If no argument specified, get time string for the current RTC time std::string InkHUD::Applet::getTimeString() { return getTimeString(getValidTime(RTCQuality::RTCQualityDevice, true)); } // Calculate how many nodes have been seen within our preferred window of activity // This period is set by user, via the menu // Todo: optimize to calculate once only per WindowManager::render uint16_t InkHUD::Applet::getActiveNodeCount() { // Don't even try to count nodes if RTC isn't set // The last heard values in nodedb will be incomprehensible if (getRTCQuality() == RTCQualityNone) return 0; uint16_t count = 0; // For each node in db for (uint16_t i = 0; i < nodeDB->getNumMeshNodes(); i++) { const meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); // Check if heard recently, and not our own node if (sinceLastSeen(node) < settings->recentlyActiveSeconds && node->num != nodeDB->getNodeNum()) count++; } return count; } // Get an abbreviated, human readable, distance string // Honors config.display.units, to offer both metric and imperial std::string InkHUD::Applet::localizeDistance(uint32_t meters) { constexpr float FEET_PER_METER = 3.28084; constexpr uint16_t FEET_PER_MILE = 5280; // Resulting string std::string localized; // Imperial if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { uint32_t feet = meters * FEET_PER_METER; // Distant (miles, rounded) if (feet > FEET_PER_MILE / 2) { localized += to_string((uint32_t)roundf(feet / FEET_PER_MILE)); localized += "mi"; } // Nearby (feet) else { localized += to_string(feet); localized += "ft"; } } // Metric else { // Distant (kilometers, rounded) if (meters >= 500) { localized += to_string((uint32_t)roundf(meters / 1000.0)); localized += "km"; } // Nearby (meters) else { localized += to_string(meters); localized += "m"; } } return localized; } // Print text with a "faux bold" effect, by drawing it multiple times, offsetting slightly void InkHUD::Applet::printThick(int16_t xCenter, int16_t yCenter, const std::string &text, uint8_t thicknessX, uint8_t thicknessY) { // How many times to draw along x axis int16_t xStart; int16_t xEnd; switch (thicknessX) { case 0: assert(false); case 1: xStart = xCenter; xEnd = xCenter; break; case 2: xStart = xCenter; xEnd = xCenter + 1; break; default: xStart = xCenter - (thicknessX / 2); xEnd = xCenter + (thicknessX / 2); } // How many times to draw along Y axis int16_t yStart; int16_t yEnd; switch (thicknessY) { case 0: assert(false); case 1: yStart = yCenter; yEnd = yCenter; break; case 2: yStart = yCenter; yEnd = yCenter + 1; break; default: yStart = yCenter - (thicknessY / 2); yEnd = yCenter + (thicknessY / 2); } // Print multiple times, overlapping for (int16_t x = xStart; x <= xEnd; x++) { for (int16_t y = yStart; y <= yEnd; y++) { printAt(x, y, text, CENTER, MIDDLE); } } } // Allow this applet to suppress notifications // Asked before a notification is shown via the NotificationApplet // An applet might want to suppress a notification if the applet itself already displays this info // Example: AllMessageApplet should not approve notifications for messages, if it is in foreground bool InkHUD::Applet::approveNotification(NicheGraphics::InkHUD::Notification &n) { // By default, no objection return true; } // Draw the standard header, used by most Applets /* ┌───────────────────────────────┐ │ Applet::name here │ │ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ │ │ │ │ │ │ └───────────────────────────────┘ */ void InkHUD::Applet::drawHeader(const std::string &text) { // Y position for divider // - between header text and messages constexpr int16_t padDivH = 2; const int16_t headerDivY = padDivH + fontSmall.lineHeight() + padDivH - 1; // Print header printAt(0, padDivH, text); // Divider // - below header text: separates message // - above header text: separates other applets for (int16_t x = 0; x < width(); x += 2) { drawPixel(x, 0, BLACK); drawPixel(x, headerDivY, BLACK); // Dotted 50% } // Dither near battery if (settings->optionalFeatures.batteryIcon) { constexpr uint16_t ditherSizePx = 4; Tile *batteryTile = ((Applet *)inkhud->getSystemApplet("BatteryIcon"))->getTile(); const uint16_t batteryTileLeft = batteryTile->getLeft(); const uint16_t batteryTileTop = batteryTile->getTop(); const uint16_t batteryTileHeight = batteryTile->getHeight(); hatchRegion(batteryTileLeft - ditherSizePx, batteryTileTop, ditherSizePx, batteryTileHeight, 2, WHITE); } } // Get the height of the standard applet header // This will vary, depending on font // Applets use this value to avoid drawing overtop the header uint16_t InkHUD::Applet::getHeaderHeight() { // Y position for divider // - between header text and messages constexpr int16_t padDivH = 2; const int16_t headerDivY = padDivH + fontSmall.lineHeight() + padDivH - 1; return headerDivY + 1; // "Plus one": height is always one more than Y position } // "Scale to fit": width of Meshtastic logo to fit given region, maintaining aspect ratio uint16_t InkHUD::Applet::getLogoWidth(uint16_t limitWidth, uint16_t limitHeight) { // Determine whether we're limited by width or height // Makes sure we draw the logo as large as possible, within the specified region, // while still maintaining correct aspect ratio if (limitWidth > limitHeight * LOGO_ASPECT_RATIO) return limitHeight * LOGO_ASPECT_RATIO; else return limitWidth; } // "Scale to fit": height of Meshtastic logo to fit given region, maintaining aspect ratio uint16_t InkHUD::Applet::getLogoHeight(uint16_t limitWidth, uint16_t limitHeight) { // Determine whether we're limited by width or height // Makes sure we draw the logo as large as possible, within the specified region, // while still maintaining correct aspect ratio if (limitHeight > limitWidth / LOGO_ASPECT_RATIO) return limitWidth / LOGO_ASPECT_RATIO; else return limitHeight; } // Draw a scalable Meshtastic logo // Make sure to provide dimensions which have the correct aspect ratio (~2) // Three paths, drawn thick using quads, with one corner "radiused" /* - ^ /- /-\ // // \\ // // \\ // // \\ // // \\ */ void InkHUD::Applet::drawLogo(int16_t centerX, int16_t centerY, uint16_t width, uint16_t height, Color color) { struct Point { int x; int y; }; typedef Point Distance; int16_t logoTh = width * 0.068; // Thickness scales with width. Measured from logo at meshtastic.org. int16_t logoL = centerX - (width / 2) + (logoTh / 2); int16_t logoT = centerY - (height / 2) + (logoTh / 2); int16_t logoW = width - logoTh; int16_t logoH = height - logoTh; int16_t logoR = logoL + logoW - 1; int16_t logoB = logoT + logoH - 1; // Points for paths (a, b, and c) /* +-----------------------------+ --| a2 b2/c1 | | | | | | | --| a1 b1 c2 | +-----------------------------+ | | | | */ Point a1 = {map(0, 0, 3, logoL, logoR), logoB}; Point a2 = {map(1, 0, 3, logoL, logoR), logoT}; Point b1 = {map(1, 0, 3, logoL, logoR), logoB}; Point b2 = {map(2, 0, 3, logoL, logoR), logoT}; Point c1 = {map(2, 0, 3, logoL, logoR), logoT}; Point c2 = {map(3, 0, 3, logoL, logoR), logoB}; // Find angle of the path(s) // Used to thicken the single pixel paths /* +-------------------------------+ | a2 | | -| | | -/ | | | -/ | | | -/# | | | -/ # | | | / # | | | a1---------- | +-------------------------------+ */ Distance deltaA = {abs(a2.x - a1.x), abs(a2.y - a1.y)}; float angle = tanh((float)deltaA.y / deltaA.x); // Distance (at right angle to the paths), which will give corners for our "quads" // The distance is unsigned. We will vary the signedness of the x and y components to suit the path and corner /* | a2 | . | .. | aq1 .. | # .. | | # .. |fromPath.y | # .. | +----a1 | | fromPath.x +-------------------------------- */ Distance fromPath; fromPath.x = cos(radians(90) - angle) * logoTh * 0.5; fromPath.y = sin(radians(90) - angle) * logoTh * 0.5; // Make the paths thick // Corner points for the rectangles (quads): /* aq2 a2 / aq3 / / aq1 / a1 aq3 */ // Filled as two triangles per quad: /* aq2 # # ### ## # aq3 ## ### - ## #### -/ ## ### -/ ## #### -/ aq1 ## -/ --- -/ \---aq4 */ // Make the path thick: path a becomes quad a Point aq1{a1.x - fromPath.x, a1.y - fromPath.y}; Point aq2{a2.x - fromPath.x, a2.y - fromPath.y}; Point aq3{a2.x + fromPath.x, a2.y + fromPath.y}; Point aq4{a1.x + fromPath.x, a1.y + fromPath.y}; fillTriangle(aq1.x, aq1.y, aq2.x, aq2.y, aq3.x, aq3.y, color); fillTriangle(aq1.x, aq1.y, aq3.x, aq3.y, aq4.x, aq4.y, color); // Make the path thick: path b becomes quad b Point bq1{b1.x - fromPath.x, b1.y - fromPath.y}; Point bq2{b2.x - fromPath.x, b2.y - fromPath.y}; Point bq3{b2.x + fromPath.x, b2.y + fromPath.y}; Point bq4{b1.x + fromPath.x, b1.y + fromPath.y}; fillTriangle(bq1.x, bq1.y, bq2.x, bq2.y, bq3.x, bq3.y, color); fillTriangle(bq1.x, bq1.y, bq3.x, bq3.y, bq4.x, bq4.y, color); // Make the path thick: path c becomes quad c Point cq1{c1.x - fromPath.x, c1.y + fromPath.y}; Point cq2{c2.x - fromPath.x, c2.y + fromPath.y}; Point cq3{c2.x + fromPath.x, c2.y - fromPath.y}; Point cq4{c1.x + fromPath.x, c1.y - fromPath.y}; fillTriangle(cq1.x, cq1.y, cq2.x, cq2.y, cq3.x, cq3.y, color); fillTriangle(cq1.x, cq1.y, cq3.x, cq3.y, cq4.x, cq4.y, color); // Radius the intersection of quad b and quad c /* b2 / c1 #### ## ## / \ / \/ \ / /\ \ / / \ \ */ // Don't attempt if logo is tiny if (logoTh > 3) { // The radius for the cap *should* be the same as logoTh, but it's not, due to accumulated rounding // We get better results just re-deriving it int16_t capRad = sqrt(pow(fromPath.x, 2) + pow(fromPath.y, 2)); fillCircle(b2.x, b2.y, capRad, color); } } #endif ================================================ FILE: src/graphics/niche/InkHUD/Applet.h ================================================ #ifdef MESHTASTIC_INCLUDE_INKHUD /* Base class for InkHUD applets Must be overridden An applet is one "program" which may show info on the display. */ #pragma once #include "configuration.h" #include // GFXRoot drawing lib #include "mesh/MeshTypes.h" #include "./AppletFont.h" #include "./Applets/System/Notification/Notification.h" // The notification object, not the applet #include "./InkHUD.h" #include "./Persistence.h" #include "./Tile.h" #include "graphics/niche/Drivers/EInk/EInk.h" namespace NicheGraphics::InkHUD { using NicheGraphics::Drivers::EInk; using std::to_string; class Applet : public GFX { public: // Which edge Applet::printAt will place on the Y parameter enum VerticalAlignment : uint8_t { TOP, MIDDLE, BOTTOM, }; // Which edge Applet::printAt will place on the X parameter enum HorizontalAlignment : uint8_t { LEFT, RIGHT, CENTER, }; // An easy-to-understand interpretation of SNR and RSSI // Calculate with Applet::getSignalStrength enum SignalStrength : int8_t { SIGNAL_UNKNOWN = -1, SIGNAL_NONE, SIGNAL_BAD, SIGNAL_FAIR, SIGNAL_GOOD, }; Applet(); void setTile(Tile *t); // Should only be called via Tile::setApplet Tile *getTile(); // Tile with which this applet is linked // Rendering void render(bool full); // Draw the applet bool wantsToRender(); // Check whether applet wants to render bool wantsToAutoshow(); // Check whether applet wants to become foreground Drivers::EInk::UpdateTypes wantsUpdateType(); // Check which display update type the applet would prefer bool wantsFullRender(); // Check whether applet wants to render over its previous render void updateDimensions(); // Get current size from tile void resetDrawingSpace(); // Makes sure every render starts with same parameters // State of the applet void activate(); // Begin running void deactivate(); // Stop running void bringToForeground(); // Show void sendToBackground(); // Hide bool isActive(); bool isForeground(); // Event handlers virtual void onRender(bool full) = 0; // For drawing the applet virtual void onActivate() {} virtual void onDeactivate() {} virtual void onForeground() {} virtual void onBackground() {} virtual void onShutdown() {} // Input Events virtual void onButtonShortPress() {} virtual void onButtonLongPress() {} virtual void onExitShort() {} virtual void onExitLong() {} virtual void onNavUp() {} virtual void onNavDown() {} virtual void onNavLeft() {} virtual void onNavRight() {} virtual void onFreeText(char c) {} virtual void onFreeTextDone() {} virtual void onFreeTextCancel() {} // List of inputs which can be subscribed to enum InputMask { // | No Joystick | With Joystick | BUTTON_SHORT = 1, // | Button Click | Joystick Center Click | BUTTON_LONG = 2, // | Button Hold | Joystick Center Hold | EXIT_SHORT = 4, // | no-op | Back Button Click | EXIT_LONG = 8, // | no-op | Back Button Hold | NAV_UP = 16, // | no-op | Joystick Up | NAV_DOWN = 32, // | no-op | Joystick Down | NAV_LEFT = 64, // | no-op | Joystick Left | NAV_RIGHT = 128 // | no-op | Joystick Right | }; bool isInputSubscribed(InputMask input); // Check if input should be handled by applet, this should not be overloaded. virtual bool approveNotification(Notification &n); // Allow an applet to veto a notification static uint16_t getHeaderHeight(); // How tall the "standard" applet header is static AppletFont fontSmall, fontMedium, fontLarge; // The general purpose fonts, used by all applets const char *name = nullptr; // Shown in applet selection menu. Also used as an identifier by InkHUD::getSystemApplet protected: void drawPixel(int16_t x, int16_t y, uint16_t color) override; // Place a single pixel. All drawing output passes through here void requestUpdate(EInk::UpdateTypes type = EInk::UpdateTypes::UNSPECIFIED, bool full = true); // Ask WindowManager to schedule a display update void requestAutoshow(); // Ask for applet to be moved to foreground uint16_t X(float f); // Map applet width, mapped from 0 to 1.0 uint16_t Y(float f); // Map applet height, mapped from 0 to 1.0 void setCrop(int16_t left, int16_t top, uint16_t width, uint16_t height); // Ignore pixels drawn outside a certain region void resetCrop(); // Removes setCrop() // User Input Handling uint8_t subscribedInputs = 0b00000000; // Maybe uint16_t for futureproofing? other devices may need more inputs void setInputsSubscribed(uint8_t input, bool captured); // Set if an input should be handled by applet or not, this should not be // overloaded. Can take multiple inputs at once if you OR/`|` them together // Text void setFont(AppletFont f); AppletFont getFont(); uint16_t getTextWidth(const std::string &text); uint16_t getTextWidth(const char *text); uint32_t getWrappedTextHeight(int16_t left, uint16_t width, const std::string &text); // Result of printWrapped void printAt(int16_t x, int16_t y, const char *text, HorizontalAlignment ha = LEFT, VerticalAlignment va = TOP); void printAt(int16_t x, int16_t y, const std::string &text, HorizontalAlignment ha = LEFT, VerticalAlignment va = TOP); void printThick(int16_t xCenter, int16_t yCenter, const std::string &text, uint8_t thicknessX, uint8_t thicknessY); // Faux bold void printWrapped(int16_t left, int16_t top, uint16_t width, const std::string &text); // Per-word line wrapping void hatchRegion(int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t spacing, Color color); // Fill with sparse lines void drawHeader(const std::string &text); // Draw the standard applet header // Meshtastic Logo static constexpr float LOGO_ASPECT_RATIO = 1.9; // Width:Height for drawing the Meshtastic logo uint16_t getLogoWidth(uint16_t limitWidth, uint16_t limitHeight); // Size Meshtastic logo to fit within region uint16_t getLogoHeight(uint16_t limitWidth, uint16_t limitHeight); // Size Meshtastic logo to fit within region void drawLogo(int16_t centerX, int16_t centerY, uint16_t width, uint16_t height, Color color = BLACK); // Draw the Meshtastic logo std::string hexifyNodeNum(NodeNum num); // Style as !0123abdc SignalStrength getSignalStrength(float snr, float rssi); // Interpret SNR and RSSI, as an easy to understand value std::string getTimeString(uint32_t epochSeconds); // Human readable std::string getTimeString(); // Current time, human readable uint16_t getActiveNodeCount(); // Duration determined by user, in onscreen menu std::string localizeDistance(uint32_t meters); // Human readable distance, imperial or metric std::string parse(const std::string &text); // Handle text which might contain special chars std::string parseShortName(meshtastic_NodeInfoLite *node); // Get the shortname, or a substitute if has unprintable chars bool isPrintable(const std::string &text); // Check for characters which the font can't print // Convenient references InkHUD *inkhud = nullptr; Persistence::Settings *settings = nullptr; Persistence::LatestMessage *latestMessage = nullptr; private: Tile *assignedTile = nullptr; // Rendered pixels are fed into a Tile object, which translates them, then passes to WM bool active = false; // Has the user enabled this applet (at run-time)? bool foreground = false; // Is the applet currently drawn on a tile? bool wantRender = false; // In some situations, checked by WindowManager when updating, to skip unneeded redrawing. bool wantAutoshow = false; // Does the applet have new data it would like to display in foreground? NicheGraphics::Drivers::EInk::UpdateTypes wantUpdateType = NicheGraphics::Drivers::EInk::UpdateTypes::UNSPECIFIED; // Which update method we'd prefer when redrawing the display bool wantFullRender = true; // Render with a fresh canvas using GFX::setFont; // Make sure derived classes use AppletFont instead of AdafruitGFX fonts directly using GFX::setRotation; // Block setRotation calls. Rotation is handled globally by WindowManager. AppletFont currentFont; // As passed to setFont // As set by setCrop int16_t cropLeft = 0; int16_t cropTop = 0; uint16_t cropWidth = 0; uint16_t cropHeight = 0; }; }; // namespace NicheGraphics::InkHUD #endif ================================================ FILE: src/graphics/niche/InkHUD/AppletFont.cpp ================================================ #ifdef MESHTASTIC_INCLUDE_INKHUD #include "./AppletFont.h" #include using namespace NicheGraphics; InkHUD::AppletFont::AppletFont() { // Default constructor uses the in-built AdafruitGFX font (not recommended) } InkHUD::AppletFont::AppletFont(const GFXfont &adafruitGFXFont, Encoding encoding, int8_t paddingTop, int8_t paddingBottom) : gfxFont(&adafruitGFXFont), encoding(encoding) { // AdafruitGFX fonts are drawn relative to a "cursor line"; // they print as if the glyphs are resting on the line of piece of ruled paper. // The glyphs also each have a different height. // To simplify drawing, we will scan the entire font now, and determine an appropriate height for a line of text // We also need to know where that "cursor line" sits inside this "line height"; // we need this additional info in order to align text by top-left, bottom-right, etc // AdafruitGFX fonts do declare a line-height, but this seems to include a certain amount of padding, // which we'd rather not deal with. If we want padding, we'll add it manually. this->ascenderHeight = 0; this->descenderHeight = 0; this->height = 0; // Scan each glyph in the AdafruitGFX font for (uint16_t i = 0; i <= (gfxFont->last - gfxFont->first); i++) { uint8_t glyphHeight = gfxFont->glyph[i].height; // Height of glyph this->height = max(this->height, glyphHeight); // Store if it's a new max // Calculate how far the glyph rises the cursor line // Store if new max value // Caution: signed and unsigned types int8_t glyphAscender = 0 - gfxFont->glyph[i].yOffset; if (glyphAscender > 0) this->ascenderHeight = max(this->ascenderHeight, static_cast(glyphAscender)); int8_t glyphDescender = gfxFont->glyph[i].height + gfxFont->glyph[i].yOffset; if (glyphDescender > 0) this->descenderHeight = max(this->descenderHeight, static_cast(glyphDescender)); } // Apply any manual padding to grow or shrink the line size // Helpful if a font has one or two exceptionally large characters, which would make the lines ridiculously tall ascenderHeight += paddingTop; descenderHeight += paddingBottom; // Find how far the cursor advances when we "print" a space character spaceCharWidth = gfxFont->glyph[static_cast(' ') - gfxFont->first].xAdvance; } /* ▲ ##### # ▲ │ # # │ lineHeight │ ### # │ │ # # # # │ heightAboveCursor │ # # # # │ │ # # #### │ │ -----------------#---- │ # │ heightBelowCursor ▼ ### ▼ */ uint8_t InkHUD::AppletFont::lineHeight() { return this->height; } // AdafruitGFX fonts print characters so that they nicely on an imaginary line (think: ruled paper). // This value is the height of the font, above that imaginary line. // Used to calculate the true height of the font uint8_t InkHUD::AppletFont::heightAboveCursor() { return this->ascenderHeight; } // AdafruitGFX fonts print characters so that they nicely on an imaginary line (think: ruled paper). // This value is the height of the font, below that imaginary line. // Used to calculate the true height of the font uint8_t InkHUD::AppletFont::heightBelowCursor() { return this->descenderHeight; } // Width of the space character // Used with Applet::printWrapped uint8_t InkHUD::AppletFont::widthBetweenWords() { return this->spaceCharWidth; } // Convert a unicode char from set of UTF-8 bytes to UTF-32 // Used by AppletFont::applyEncoding, which remaps unicode chars for extended ASCII fonts, based on their UTF-32 value uint32_t InkHUD::AppletFont::toUtf32(const std::string &utf8) { uint32_t utf32 = 0; switch (utf8.length()) { case 2: // 5 bits + 6 bits utf32 |= (utf8.at(0) & 0b00011111) << 6; utf32 |= (utf8.at(1) & 0b00111111); break; case 3: // 4 bits + 6 bits + 6 bits utf32 |= (utf8.at(0) & 0b00001111) << (6 + 6); utf32 |= (utf8.at(1) & 0b00111111) << 6; utf32 |= (utf8.at(2) & 0b00111111); break; case 4: // 3 bits + 6 bits + 6 bits + 6 bits utf32 |= (utf8.at(0) & 0b00000111) << (6 + 6 + 6); utf32 |= (utf8.at(1) & 0b00111111) << (6 + 6); utf32 |= (utf8.at(2) & 0b00111111) << 6; utf32 |= (utf8.at(3) & 0b00111111); break; default: return 0; } return utf32; } // Process a string, collating UTF-8 bytes, and sending them off for re-encoding to extended ASCII // Not all InkHUD text is passed through here, only text which could potentially contain non-ASCII chars std::string InkHUD::AppletFont::decodeUTF8(const std::string &encoded) { // Final processed output std::string decoded; // Holds bytes for one UTF-8 char during parsing std::string utf8Char; uint8_t utf8CharSize = 0; for (const char &c : encoded) { // If first byte if (utf8Char.empty()) { // If MSB is unset, byte is an ASCII char // If MSB is set, byte is part of a UTF-8 char. Counting number of higher-order bits tells how many bytes in char if ((c & 0x80)) { char c1 = c; while (c1 & 0x80) { c1 <<= 1; utf8CharSize++; } } } // Append the byte to the UTF-8 char we're building utf8Char += c; // More bytes left to collect. Iterate. if (utf8Char.length() < utf8CharSize) continue; // Now collected all bytes for this char // Remap the value to match the encoding of our 8-bit AppletFont decoded += applyEncoding(utf8Char); // Reset, ready to build next UTF-8 char from the encoded bytes utf8Char.clear(); utf8CharSize = 0; } // For each char // All chars processed, return result return decoded; } // Re-encode a single UTF-8 character to extended ASCII // Target encoding depends on the font char InkHUD::AppletFont::applyEncoding(const std::string &utf8) { // ##################################################### Syntactic Sugar ##################################################### #define REMAP(in, out) \ case in: \ return out; // ########################################################################################################################### // Latin - Central Europe // https://www.unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP1250.TXT if (encoding == WINDOWS_1250) { // 1-Byte chars: no remapping if (utf8.length() == 1) return utf8.at(0); // Multi-byte chars: switch (toUtf32(utf8)) { REMAP(0x20AC, 0x80); // EURO SIGN REMAP(0x201A, 0x82); // SINGLE LOW-9 QUOTATION MARK REMAP(0x201E, 0x84); // DOUBLE LOW-9 QUOTATION MARK REMAP(0x2026, 0x85); // HORIZONTAL ELLIPSIS REMAP(0x2020, 0x86); // DAGGER REMAP(0x2021, 0x87); // DOUBLE DAGGER REMAP(0x2030, 0x89); // PER MILLE SIGN REMAP(0x0160, 0x8A); // LATIN CAPITAL LETTER S WITH CARON REMAP(0x2039, 0x8B); // SINGLE LEFT-POINTING ANGLE QUOTATION MARK REMAP(0x015A, 0x8C); // LATIN CAPITAL LETTER S WITH ACUTE REMAP(0x0164, 0x8D); // LATIN CAPITAL LETTER T WITH CARON REMAP(0x017D, 0x8E); // LATIN CAPITAL LETTER Z WITH CARON REMAP(0x0179, 0x8F); // LATIN CAPITAL LETTER Z WITH ACUTE REMAP(0x2018, 0x91); // LEFT SINGLE QUOTATION MARK REMAP(0x2019, 0x92); // RIGHT SINGLE QUOTATION MARK REMAP(0x201C, 0x93); // LEFT DOUBLE QUOTATION MARK REMAP(0x201D, 0x94); // RIGHT DOUBLE QUOTATION MARK REMAP(0x2022, 0x95); // BULLET REMAP(0x2013, 0x96); // EN DASH REMAP(0x2014, 0x97); // EM DASH REMAP(0x2122, 0x99); // TRADE MARK SIGN REMAP(0x0161, 0x9A); // LATIN SMALL LETTER S WITH CARON REMAP(0x203A, 0x9B); // SINGLE RIGHT-POINTING ANGLE QUOTATION MARK REMAP(0x015B, 0x9C); // LATIN SMALL LETTER S WITH ACUTE REMAP(0x0165, 0x9D); // LATIN SMALL LETTER T WITH CARON REMAP(0x017E, 0x9E); // LATIN SMALL LETTER Z WITH CARON REMAP(0x017A, 0x9F); // LATIN SMALL LETTER Z WITH ACUTE REMAP(0x00A0, 0xA0); // NO-BREAK SPACE REMAP(0x02C7, 0xA1); // CARON REMAP(0x02D8, 0xA2); // BREVE REMAP(0x0141, 0xA3); // LATIN CAPITAL LETTER L WITH STROKE REMAP(0x00A4, 0xA4); // CURRENCY SIGN REMAP(0x0104, 0xA5); // LATIN CAPITAL LETTER A WITH OGONEK REMAP(0x00A6, 0xA6); // BROKEN BAR REMAP(0x00A7, 0xA7); // SECTION SIGN REMAP(0x00A8, 0xA8); // DIAERESIS REMAP(0x00A9, 0xA9); // COPYRIGHT SIGN REMAP(0x015E, 0xAA); // LATIN CAPITAL LETTER S WITH CEDILLA REMAP(0x00AB, 0xAB); // LEFT-POINTING DOUBLE ANGLE QUOTATION MARK REMAP(0x00AC, 0xAC); // NOT SIGN REMAP(0x00AD, 0xAD); // SOFT HYPHEN REMAP(0x00AE, 0xAE); // REGISTERED SIGN REMAP(0x017B, 0xAF); // LATIN CAPITAL LETTER Z WITH DOT ABOVE REMAP(0x00B0, 0xB0); // DEGREE SIGN REMAP(0x00B1, 0xB1); // PLUS-MINUS SIGN REMAP(0x02DB, 0xB2); // OGONEK REMAP(0x0142, 0xB3); // LATIN SMALL LETTER L WITH STROKE REMAP(0x00B4, 0xB4); // ACUTE ACCENT REMAP(0x00B5, 0xB5); // MICRO SIGN REMAP(0x00B6, 0xB6); // PILCROW SIGN REMAP(0x00B7, 0xB7); // MIDDLE DOT REMAP(0x00B8, 0xB8); // CEDILLA REMAP(0x0105, 0xB9); // LATIN SMALL LETTER A WITH OGONEK REMAP(0x015F, 0xBA); // LATIN SMALL LETTER S WITH CEDILLA REMAP(0x00BB, 0xBB); // RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK REMAP(0x013D, 0xBC); // LATIN CAPITAL LETTER L WITH CARON REMAP(0x02DD, 0xBD); // DOUBLE ACUTE ACCENT REMAP(0x013E, 0xBE); // LATIN SMALL LETTER L WITH CARON REMAP(0x017C, 0xBF); // LATIN SMALL LETTER Z WITH DOT ABOVE REMAP(0x0154, 0xC0); // LATIN CAPITAL LETTER R WITH ACUTE REMAP(0x00C1, 0xC1); // LATIN CAPITAL LETTER A WITH ACUTE REMAP(0x00C2, 0xC2); // LATIN CAPITAL LETTER A WITH CIRCUMFLEX REMAP(0x0102, 0xC3); // LATIN CAPITAL LETTER A WITH BREVE REMAP(0x00C4, 0xC4); // LATIN CAPITAL LETTER A WITH DIAERESIS REMAP(0x0139, 0xC5); // LATIN CAPITAL LETTER L WITH ACUTE REMAP(0x0106, 0xC6); // LATIN CAPITAL LETTER C WITH ACUTE REMAP(0x00C7, 0xC7); // LATIN CAPITAL LETTER C WITH CEDILLA REMAP(0x010C, 0xC8); // LATIN CAPITAL LETTER C WITH CARON REMAP(0x00C9, 0xC9); // LATIN CAPITAL LETTER E WITH ACUTE REMAP(0x0118, 0xCA); // LATIN CAPITAL LETTER E WITH OGONEK REMAP(0x00CB, 0xCB); // LATIN CAPITAL LETTER E WITH DIAERESIS REMAP(0x011A, 0xCC); // LATIN CAPITAL LETTER E WITH CARON REMAP(0x00CD, 0xCD); // LATIN CAPITAL LETTER I WITH ACUTE REMAP(0x00CE, 0xCE); // LATIN CAPITAL LETTER I WITH CIRCUMFLEX REMAP(0x010E, 0xCF); // LATIN CAPITAL LETTER D WITH CARON REMAP(0x0110, 0xD0); // LATIN CAPITAL LETTER D WITH STROKE REMAP(0x0143, 0xD1); // LATIN CAPITAL LETTER N WITH ACUTE REMAP(0x0147, 0xD2); // LATIN CAPITAL LETTER N WITH CARON REMAP(0x00D3, 0xD3); // LATIN CAPITAL LETTER O WITH ACUTE REMAP(0x00D4, 0xD4); // LATIN CAPITAL LETTER O WITH CIRCUMFLEX REMAP(0x0150, 0xD5); // LATIN CAPITAL LETTER O WITH DOUBLE ACUTE REMAP(0x00D6, 0xD6); // LATIN CAPITAL LETTER O WITH DIAERESIS REMAP(0x00D7, 0xD7); // MULTIPLICATION SIGN REMAP(0x0158, 0xD8); // LATIN CAPITAL LETTER R WITH CARON REMAP(0x016E, 0xD9); // LATIN CAPITAL LETTER U WITH RING ABOVE REMAP(0x00DA, 0xDA); // LATIN CAPITAL LETTER U WITH ACUTE REMAP(0x0170, 0xDB); // LATIN CAPITAL LETTER U WITH DOUBLE ACUTE REMAP(0x00DC, 0xDC); // LATIN CAPITAL LETTER U WITH DIAERESIS REMAP(0x00DD, 0xDD); // LATIN CAPITAL LETTER Y WITH ACUTE REMAP(0x0162, 0xDE); // LATIN CAPITAL LETTER T WITH CEDILLA REMAP(0x00DF, 0xDF); // LATIN SMALL LETTER SHARP S REMAP(0x0155, 0xE0); // LATIN SMALL LETTER R WITH ACUTE REMAP(0x00E1, 0xE1); // LATIN SMALL LETTER A WITH ACUTE REMAP(0x00E2, 0xE2); // LATIN SMALL LETTER A WITH CIRCUMFLEX REMAP(0x0103, 0xE3); // LATIN SMALL LETTER A WITH BREVE REMAP(0x00E4, 0xE4); // LATIN SMALL LETTER A WITH DIAERESIS REMAP(0x013A, 0xE5); // LATIN SMALL LETTER L WITH ACUTE REMAP(0x0107, 0xE6); // LATIN SMALL LETTER C WITH ACUTE REMAP(0x00E7, 0xE7); // LATIN SMALL LETTER C WITH CEDILLA REMAP(0x010D, 0xE8); // LATIN SMALL LETTER C WITH CARON REMAP(0x00E9, 0xE9); // LATIN SMALL LETTER E WITH ACUTE REMAP(0x0119, 0xEA); // LATIN SMALL LETTER E WITH OGONEK REMAP(0x00EB, 0xEB); // LATIN SMALL LETTER E WITH DIAERESIS REMAP(0x011B, 0xEC); // LATIN SMALL LETTER E WITH CARON REMAP(0x00ED, 0xED); // LATIN SMALL LETTER I WITH ACUTE REMAP(0x00EE, 0xEE); // LATIN SMALL LETTER I WITH CIRCUMFLEX REMAP(0x010F, 0xEF); // LATIN SMALL LETTER D WITH CARON REMAP(0x0111, 0xF0); // LATIN SMALL LETTER D WITH STROKE REMAP(0x0144, 0xF1); // LATIN SMALL LETTER N WITH ACUTE REMAP(0x0148, 0xF2); // LATIN SMALL LETTER N WITH CARON REMAP(0x00F3, 0xF3); // LATIN SMALL LETTER O WITH ACUTE REMAP(0x00F4, 0xF4); // LATIN SMALL LETTER O WITH CIRCUMFLEX REMAP(0x0151, 0xF5); // LATIN SMALL LETTER O WITH DOUBLE ACUTE REMAP(0x00F6, 0xF6); // LATIN SMALL LETTER O WITH DIAERESIS REMAP(0x00F7, 0xF7); // DIVISION SIGN REMAP(0x0159, 0xF8); // LATIN SMALL LETTER R WITH CARON REMAP(0x016F, 0xF9); // LATIN SMALL LETTER U WITH RING ABOVE REMAP(0x00FA, 0xFA); // LATIN SMALL LETTER U WITH ACUTE REMAP(0x0171, 0xFB); // LATIN SMALL LETTER U WITH DOUBLE ACUTE REMAP(0x00FC, 0xFC); // LATIN SMALL LETTER U WITH DIAERESIS REMAP(0x00FD, 0xFD); // LATIN SMALL LETTER Y WITH ACUTE REMAP(0x0163, 0xFE); // LATIN SMALL LETTER T WITH CEDILLA REMAP(0x02D9, 0xFF); // DOT ABOVE } } // Latin - Cyrillic // https://www.unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP1251.TXT else if (encoding == WINDOWS_1251) { // 1-Byte chars: no remapping if (utf8.length() == 1) return utf8.at(0); // Multi-byte chars: switch (toUtf32(utf8)) { REMAP(0x0402, 0x80); // CYRILLIC CAPITAL LETTER DJE REMAP(0x0403, 0x81); // CYRILLIC CAPITAL LETTER GJE REMAP(0x201A, 0x82); // SINGLE LOW-9 QUOTATION MARK REMAP(0x0453, 0x83); // CYRILLIC SMALL LETTER GJE REMAP(0x201E, 0x84); // DOUBLE LOW-9 QUOTATION MARK REMAP(0x2026, 0x85); // HORIZONTAL ELLIPSIS REMAP(0x2020, 0x86); // DAGGER REMAP(0x2021, 0x87); // DOUBLE DAGGER REMAP(0x20AC, 0x88); // EURO SIGN REMAP(0x2030, 0x89); // PER MILLE SIGN REMAP(0x0409, 0x8A); // CYRILLIC CAPITAL LETTER LJE REMAP(0x2039, 0x8B); // SINGLE LEFT-POINTING ANGLE QUOTATION MARK REMAP(0x040A, 0x8C); // CYRILLIC CAPITAL LETTER NJE REMAP(0x040C, 0x8D); // CYRILLIC CAPITAL LETTER KJE REMAP(0x040B, 0x8E); // CYRILLIC CAPITAL LETTER TSHE REMAP(0x040F, 0x8F); // CYRILLIC CAPITAL LETTER DZHE REMAP(0x0452, 0x90); // CYRILLIC SMALL LETTER DJE REMAP(0x2018, 0x91); // LEFT SINGLE QUOTATION MARK REMAP(0x2019, 0x92); // RIGHT SINGLE QUOTATION MARK REMAP(0x201C, 0x93); // LEFT DOUBLE QUOTATION MARK REMAP(0x201D, 0x94); // RIGHT DOUBLE QUOTATION MARK REMAP(0x2022, 0x95); // BULLET REMAP(0x2013, 0x96); // EN DASH REMAP(0x2014, 0x97); // EM DASH REMAP(0x2122, 0x99); // TRADE MARK SIGN REMAP(0x0459, 0x9A); // CYRILLIC SMALL LETTER LJE REMAP(0x203A, 0x9B); // SINGLE RIGHT-POINTING ANGLE QUOTATION MARK REMAP(0x045A, 0x9C); // CYRILLIC SMALL LETTER NJE REMAP(0x045C, 0x9D); // CYRILLIC SMALL LETTER KJE REMAP(0x045B, 0x9E); // CYRILLIC SMALL LETTER TSHE REMAP(0x045F, 0x9F); // CYRILLIC SMALL LETTER DZHE REMAP(0x00A0, 0xA0); // NO-BREAK SPACE REMAP(0x040E, 0xA1); // CYRILLIC CAPITAL LETTER SHORT U REMAP(0x045E, 0xA2); // CYRILLIC SMALL LETTER SHORT U REMAP(0x0408, 0xA3); // CYRILLIC CAPITAL LETTER JE REMAP(0x00A4, 0xA4); // CURRENCY SIGN REMAP(0x0490, 0xA5); // CYRILLIC CAPITAL LETTER GHE WITH UPTURN REMAP(0x00A6, 0xA6); // BROKEN BAR REMAP(0x00A7, 0xA7); // SECTION SIGN REMAP(0x0401, 0xA8); // CYRILLIC CAPITAL LETTER IO REMAP(0x00A9, 0xA9); // COPYRIGHT SIGN REMAP(0x0404, 0xAA); // CYRILLIC CAPITAL LETTER UKRAINIAN IE REMAP(0x00AB, 0xAB); // LEFT-POINTING DOUBLE ANGLE QUOTATION MARK REMAP(0x00AC, 0xAC); // NOT SIGN REMAP(0x00AD, 0xAD); // SOFT HYPHEN REMAP(0x00AE, 0xAE); // REGISTERED SIGN REMAP(0x0407, 0xAF); // CYRILLIC CAPITAL LETTER YI REMAP(0x00B0, 0xB0); // DEGREE SIGN REMAP(0x00B1, 0xB1); // PLUS-MINUS SIGN REMAP(0x0406, 0xB2); // CYRILLIC CAPITAL LETTER BYELORUSSIAN-UKRAINIAN I REMAP(0x0456, 0xB3); // CYRILLIC SMALL LETTER BYELORUSSIAN-UKRAINIAN I REMAP(0x0491, 0xB4); // CYRILLIC SMALL LETTER GHE WITH UPTURN REMAP(0x00B5, 0xB5); // MICRO SIGN REMAP(0x00B6, 0xB6); // PILCROW SIGN REMAP(0x00B7, 0xB7); // MIDDLE DOT REMAP(0x0451, 0xB8); // CYRILLIC SMALL LETTER IO REMAP(0x2116, 0xB9); // NUMERO SIGN REMAP(0x0454, 0xBA); // CYRILLIC SMALL LETTER UKRAINIAN IE REMAP(0x00BB, 0xBB); // RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK REMAP(0x0458, 0xBC); // CYRILLIC SMALL LETTER JE REMAP(0x0405, 0xBD); // CYRILLIC CAPITAL LETTER DZE REMAP(0x0455, 0xBE); // CYRILLIC SMALL LETTER DZE REMAP(0x0457, 0xBF); // CYRILLIC SMALL LETTER YI REMAP(0x0410, 0xC0); // CYRILLIC CAPITAL LETTER A REMAP(0x0411, 0xC1); // CYRILLIC CAPITAL LETTER BE REMAP(0x0412, 0xC2); // CYRILLIC CAPITAL LETTER VE REMAP(0x0413, 0xC3); // CYRILLIC CAPITAL LETTER GHE REMAP(0x0414, 0xC4); // CYRILLIC CAPITAL LETTER DE REMAP(0x0415, 0xC5); // CYRILLIC CAPITAL LETTER IE REMAP(0x0416, 0xC6); // CYRILLIC CAPITAL LETTER ZHE REMAP(0x0417, 0xC7); // CYRILLIC CAPITAL LETTER ZE REMAP(0x0418, 0xC8); // CYRILLIC CAPITAL LETTER I REMAP(0x0419, 0xC9); // CYRILLIC CAPITAL LETTER SHORT I REMAP(0x041A, 0xCA); // CYRILLIC CAPITAL LETTER KA REMAP(0x041B, 0xCB); // CYRILLIC CAPITAL LETTER EL REMAP(0x041C, 0xCC); // CYRILLIC CAPITAL LETTER EM REMAP(0x041D, 0xCD); // CYRILLIC CAPITAL LETTER EN REMAP(0x041E, 0xCE); // CYRILLIC CAPITAL LETTER O REMAP(0x041F, 0xCF); // CYRILLIC CAPITAL LETTER PE REMAP(0x0420, 0xD0); // CYRILLIC CAPITAL LETTER ER REMAP(0x0421, 0xD1); // CYRILLIC CAPITAL LETTER ES REMAP(0x0422, 0xD2); // CYRILLIC CAPITAL LETTER TE REMAP(0x0423, 0xD3); // CYRILLIC CAPITAL LETTER U REMAP(0x0424, 0xD4); // CYRILLIC CAPITAL LETTER EF REMAP(0x0425, 0xD5); // CYRILLIC CAPITAL LETTER HA REMAP(0x0426, 0xD6); // CYRILLIC CAPITAL LETTER TSE REMAP(0x0427, 0xD7); // CYRILLIC CAPITAL LETTER CHE REMAP(0x0428, 0xD8); // CYRILLIC CAPITAL LETTER SHA REMAP(0x0429, 0xD9); // CYRILLIC CAPITAL LETTER SHCHA REMAP(0x042A, 0xDA); // CYRILLIC CAPITAL LETTER HARD SIGN REMAP(0x042B, 0xDB); // CYRILLIC CAPITAL LETTER YERU REMAP(0x042C, 0xDC); // CYRILLIC CAPITAL LETTER SOFT SIGN REMAP(0x042D, 0xDD); // CYRILLIC CAPITAL LETTER E REMAP(0x042E, 0xDE); // CYRILLIC CAPITAL LETTER YU REMAP(0x042F, 0xDF); // CYRILLIC CAPITAL LETTER YA REMAP(0x0430, 0xE0); // CYRILLIC SMALL LETTER A REMAP(0x0431, 0xE1); // CYRILLIC SMALL LETTER BE REMAP(0x0432, 0xE2); // CYRILLIC SMALL LETTER VE REMAP(0x0433, 0xE3); // CYRILLIC SMALL LETTER GHE REMAP(0x0434, 0xE4); // CYRILLIC SMALL LETTER DE REMAP(0x0435, 0xE5); // CYRILLIC SMALL LETTER IE REMAP(0x0436, 0xE6); // CYRILLIC SMALL LETTER ZHE REMAP(0x0437, 0xE7); // CYRILLIC SMALL LETTER ZE REMAP(0x0438, 0xE8); // CYRILLIC SMALL LETTER I REMAP(0x0439, 0xE9); // CYRILLIC SMALL LETTER SHORT I REMAP(0x043A, 0xEA); // CYRILLIC SMALL LETTER KA REMAP(0x043B, 0xEB); // CYRILLIC SMALL LETTER EL REMAP(0x043C, 0xEC); // CYRILLIC SMALL LETTER EM REMAP(0x043D, 0xED); // CYRILLIC SMALL LETTER EN REMAP(0x043E, 0xEE); // CYRILLIC SMALL LETTER O REMAP(0x043F, 0xEF); // CYRILLIC SMALL LETTER PE REMAP(0x0440, 0xF0); // CYRILLIC SMALL LETTER ER REMAP(0x0441, 0xF1); // CYRILLIC SMALL LETTER ES REMAP(0x0442, 0xF2); // CYRILLIC SMALL LETTER TE REMAP(0x0443, 0xF3); // CYRILLIC SMALL LETTER U REMAP(0x0444, 0xF4); // CYRILLIC SMALL LETTER EF REMAP(0x0445, 0xF5); // CYRILLIC SMALL LETTER HA REMAP(0x0446, 0xF6); // CYRILLIC SMALL LETTER TSE REMAP(0x0447, 0xF7); // CYRILLIC SMALL LETTER CHE REMAP(0x0448, 0xF8); // CYRILLIC SMALL LETTER SHA REMAP(0x0449, 0xF9); // CYRILLIC SMALL LETTER SHCHA REMAP(0x044A, 0xFA); // CYRILLIC SMALL LETTER HARD SIGN REMAP(0x044B, 0xFB); // CYRILLIC SMALL LETTER YERU REMAP(0x044C, 0xFC); // CYRILLIC SMALL LETTER SOFT SIGN REMAP(0x044D, 0xFD); // CYRILLIC SMALL LETTER E REMAP(0x044E, 0xFE); // CYRILLIC SMALL LETTER YU REMAP(0x044F, 0xFF); // CYRILLIC SMALL LETTER YA } } // Latin - Western Europe // https://www.unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP1252.TXT else if (encoding == WINDOWS_1252) { // 1-Byte chars: no remapping if (utf8.length() == 1) return utf8.at(0); // Multi-byte chars: switch (toUtf32(utf8)) { REMAP(0x20AC, 0x80) // EURO SIGN REMAP(0x201A, 0x82) // SINGLE LOW-9 QUOTATION MARK REMAP(0x0192, 0x83) // LATIN SMALL LETTER F WITH HOOK REMAP(0x201E, 0x84) // DOUBLE LOW-9 QUOTATION MARK REMAP(0x2026, 0x85) // HORIZONTAL ELLIPSIS REMAP(0x2020, 0x86) // DAGGER REMAP(0x2021, 0x87) // DOUBLE DAGGER REMAP(0x02C6, 0x88) // MODIFIER LETTER CIRCUMFLEX ACCENT REMAP(0x2030, 0x89) // PER MILLE SIGN REMAP(0x0160, 0x8A) // LATIN CAPITAL LETTER S WITH CARON REMAP(0x2039, 0x8B) // SINGLE LEFT-POINTING ANGLE QUOTATION MARK REMAP(0x0152, 0x8C) // LATIN CAPITAL LIGATURE OE REMAP(0x017D, 0x8E) // LATIN CAPITAL LETTER Z WITH CARON REMAP(0x2018, 0x91) // LEFT SINGLE QUOTATION MARK REMAP(0x2019, 0x92) // RIGHT SINGLE QUOTATION MARK REMAP(0x201C, 0x93) // LEFT DOUBLE QUOTATION MARK REMAP(0x201D, 0x94) // RIGHT DOUBLE QUOTATION MARK REMAP(0x2022, 0x95) // BULLET REMAP(0x2013, 0x96) // EN DASH REMAP(0x2014, 0x97) // EM DASH REMAP(0x02DC, 0x98) // SMALL TILDE REMAP(0x2122, 0x99) // TRADE MARK SIGN REMAP(0x0161, 0x9A) // LATIN SMALL LETTER S WITH CARON REMAP(0x203A, 0x9B) // SINGLE RIGHT-POINTING ANGLE QUOTATION MARK REMAP(0x0153, 0x9C) // LATIN SMALL LIGATURE OE REMAP(0x017E, 0x9E) // LATIN SMALL LETTER Z WITH CARON REMAP(0x0178, 0x9F) // LATIN CAPITAL LETTER Y WITH DIAERESIS REMAP(0x00A0, 0xA0) // NO-BREAK SPACE REMAP(0x00A1, 0xA1) // INVERTED EXCLAMATION MARK REMAP(0x00A2, 0xA2) // CENT SIGN REMAP(0x00A3, 0xA3) // POUND SIGN REMAP(0x00A4, 0xA4) // CURRENCY SIGN REMAP(0x00A5, 0xA5) // YEN SIGN REMAP(0x00A6, 0xA6) // BROKEN BAR REMAP(0x00A7, 0xA7) // SECTION SIGN REMAP(0x00A8, 0xA8) // DIAERESIS REMAP(0x00A9, 0xA9) // COPYRIGHT SIGN REMAP(0x00AA, 0xAA) // FEMININE ORDINAL INDICATOR REMAP(0x00AB, 0xAB) // LEFT-POINTING DOUBLE ANGLE QUOTATION MARK REMAP(0x00AC, 0xAC) // NOT SIGN REMAP(0x00AD, 0xAD) // SOFT HYPHEN REMAP(0x00AE, 0xAE) // REGISTERED SIGN REMAP(0x00AF, 0xAF) // MACRON REMAP(0x00B0, 0xB0) // DEGREE SIGN REMAP(0x00B1, 0xB1) // PLUS-MINUS SIGN REMAP(0x00B2, 0xB2) // SUPERSCRIPT TWO REMAP(0x00B3, 0xB3) // SUPERSCRIPT THREE REMAP(0x00B4, 0xB4) // ACUTE ACCENT REMAP(0x00B5, 0xB5) // MICRO SIGN REMAP(0x00B6, 0xB6) // PILCROW SIGN REMAP(0x00B7, 0xB7) // MIDDLE DOT REMAP(0x00B8, 0xB8) // CEDILLA REMAP(0x00B9, 0xB9) // SUPERSCRIPT ONE REMAP(0x00BA, 0xBA) // MASCULINE ORDINAL INDICATOR REMAP(0x00BB, 0xBB) // RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK REMAP(0x00BC, 0xBC) // VULGAR FRACTION ONE QUARTER REMAP(0x00BD, 0xBD) // VULGAR FRACTION ONE HALF REMAP(0x00BE, 0xBE) // VULGAR FRACTION THREE QUARTERS REMAP(0x00BF, 0xBF) // INVERTED QUESTION MARK REMAP(0x00C0, 0xC0) // LATIN CAPITAL LETTER A WITH GRAVE REMAP(0x00C1, 0xC1) // LATIN CAPITAL LETTER A WITH ACUTE REMAP(0x00C2, 0xC2) // LATIN CAPITAL LETTER A WITH CIRCUMFLEX REMAP(0x00C3, 0xC3) // LATIN CAPITAL LETTER A WITH TILDE REMAP(0x00C4, 0xC4) // LATIN CAPITAL LETTER A WITH DIAERESIS REMAP(0x00C5, 0xC5) // LATIN CAPITAL LETTER A WITH RING ABOVE REMAP(0x00C6, 0xC6) // LATIN CAPITAL LETTER AE REMAP(0x00C7, 0xC7) // LATIN CAPITAL LETTER C WITH CEDILLA REMAP(0x00C8, 0xC8) // LATIN CAPITAL LETTER E WITH GRAVE REMAP(0x00C9, 0xC9) // LATIN CAPITAL LETTER E WITH ACUTE REMAP(0x00CA, 0xCA) // LATIN CAPITAL LETTER E WITH CIRCUMFLEX REMAP(0x00CB, 0xCB) // LATIN CAPITAL LETTER E WITH DIAERESIS REMAP(0x00CC, 0xCC) // LATIN CAPITAL LETTER I WITH GRAVE REMAP(0x00CD, 0xCD) // LATIN CAPITAL LETTER I WITH ACUTE REMAP(0x00CE, 0xCE) // LATIN CAPITAL LETTER I WITH CIRCUMFLEX REMAP(0x00CF, 0xCF) // LATIN CAPITAL LETTER I WITH DIAERESIS REMAP(0x00D0, 0xD0) // LATIN CAPITAL LETTER ETH REMAP(0x00D1, 0xD1) // LATIN CAPITAL LETTER N WITH TILDE REMAP(0x00D2, 0xD2) // LATIN CAPITAL LETTER O WITH GRAVE REMAP(0x00D3, 0xD3) // LATIN CAPITAL LETTER O WITH ACUTE REMAP(0x00D4, 0xD4) // LATIN CAPITAL LETTER O WITH CIRCUMFLEX REMAP(0x00D5, 0xD5) // LATIN CAPITAL LETTER O WITH TILDE REMAP(0x00D6, 0xD6) // LATIN CAPITAL LETTER O WITH DIAERESIS REMAP(0x00D7, 0xD7) // MULTIPLICATION SIGN REMAP(0x00D8, 0xD8) // LATIN CAPITAL LETTER O WITH STROKE REMAP(0x00D9, 0xD9) // LATIN CAPITAL LETTER U WITH GRAVE REMAP(0x00DA, 0xDA) // LATIN CAPITAL LETTER U WITH ACUTE REMAP(0x00DB, 0xDB) // LATIN CAPITAL LETTER U WITH CIRCUMFLEX REMAP(0x00DC, 0xDC) // LATIN CAPITAL LETTER U WITH DIAERESIS REMAP(0x00DD, 0xDD) // LATIN CAPITAL LETTER Y WITH ACUTE REMAP(0x00DE, 0xDE) // LATIN CAPITAL LETTER THORN REMAP(0x00DF, 0xDF) // LATIN SMALL LETTER SHARP S REMAP(0x00E0, 0xE0) // LATIN SMALL LETTER A WITH GRAVE REMAP(0x00E1, 0xE1) // LATIN SMALL LETTER A WITH ACUTE REMAP(0x00E2, 0xE2) // LATIN SMALL LETTER A WITH CIRCUMFLEX REMAP(0x00E3, 0xE3) // LATIN SMALL LETTER A WITH TILDE REMAP(0x00E4, 0xE4) // LATIN SMALL LETTER A WITH DIAERESIS REMAP(0x00E5, 0xE5) // LATIN SMALL LETTER A WITH RING ABOVE REMAP(0x00E6, 0xE6) // LATIN SMALL LETTER AE REMAP(0x00E7, 0xE7) // LATIN SMALL LETTER C WITH CEDILLA REMAP(0x00E8, 0xE8) // LATIN SMALL LETTER E WITH GRAVE REMAP(0x00E9, 0xE9) // LATIN SMALL LETTER E WITH ACUTE REMAP(0x00EA, 0xEA) // LATIN SMALL LETTER E WITH CIRCUMFLEX REMAP(0x00EB, 0xEB) // LATIN SMALL LETTER E WITH DIAERESIS REMAP(0x00EC, 0xEC) // LATIN SMALL LETTER I WITH GRAVE REMAP(0x00ED, 0xED) // LATIN SMALL LETTER I WITH ACUTE REMAP(0x00EE, 0xEE) // LATIN SMALL LETTER I WITH CIRCUMFLEX REMAP(0x00EF, 0xEF) // LATIN SMALL LETTER I WITH DIAERESIS REMAP(0x00F0, 0xF0) // LATIN SMALL LETTER ETH REMAP(0x00F1, 0xF1) // LATIN SMALL LETTER N WITH TILDE REMAP(0x00F2, 0xF2) // LATIN SMALL LETTER O WITH GRAVE REMAP(0x00F3, 0xF3) // LATIN SMALL LETTER O WITH ACUTE REMAP(0x00F4, 0xF4) // LATIN SMALL LETTER O WITH CIRCUMFLEX REMAP(0x00F5, 0xF5) // LATIN SMALL LETTER O WITH TILDE REMAP(0x00F6, 0xF6) // LATIN SMALL LETTER O WITH DIAERESIS REMAP(0x00F7, 0xF7) // DIVISION SIGN REMAP(0x00F8, 0xF8) // LATIN SMALL LETTER O WITH STROKE REMAP(0x00F9, 0xF9) // LATIN SMALL LETTER U WITH GRAVE REMAP(0x00FA, 0xFA) // LATIN SMALL LETTER U WITH ACUTE REMAP(0x00FB, 0xFB) // LATIN SMALL LETTER U WITH CIRCUMFLEX REMAP(0x00FC, 0xFC) // LATIN SMALL LETTER U WITH DIAERESIS REMAP(0x00FD, 0xFD) // LATIN SMALL LETTER Y WITH ACUTE REMAP(0x00FE, 0xFE) // LATIN SMALL LETTER THORN REMAP(0x00FF, 0xFF) // LATIN SMALL LETTER Y WITH DIAERESIS } } else if (encoding == WINDOWS_1253) { // Greek // 1-Byte chars: no remapping if (utf8.length() == 1) return utf8.at(0); // Multi-byte chars: switch (toUtf32(utf8)) { // Windows-1253 special characters (0x80-0xBF range) REMAP(0x20AC, 0x80) // EURO SIGN REMAP(0x2018, 0x91) // LEFT SINGLE QUOTATION MARK REMAP(0x2019, 0x92) // RIGHT SINGLE QUOTATION MARK REMAP(0x201C, 0x93) // LEFT DOUBLE QUOTATION MARK REMAP(0x201D, 0x94) // RIGHT DOUBLE QUOTATION MARK REMAP(0x2022, 0x95) // BULLET REMAP(0x2013, 0x96) // EN DASH REMAP(0x2014, 0x97) // EM DASH // Greek accented capitals REMAP(0x0386, 0xA2) // GREEK CAPITAL LETTER ALPHA WITH TONOS REMAP(0x0388, 0xB8) // GREEK CAPITAL LETTER EPSILON WITH TONOS REMAP(0x0389, 0xB9) // GREEK CAPITAL LETTER ETA WITH TONOS REMAP(0x038A, 0xBA) // GREEK CAPITAL LETTER IOTA WITH TONOS REMAP(0x038C, 0xBC) // GREEK CAPITAL LETTER OMICRON WITH TONOS REMAP(0x038E, 0xBE) // GREEK CAPITAL LETTER UPSILON WITH TONOS REMAP(0x038F, 0xBF) // GREEK CAPITAL LETTER OMEGA WITH TONOS // Greek capital letters (U+0391-U+03A9 -> 0xC1-0xD1, with gaps) REMAP(0x0391, 0xC1) // GREEK CAPITAL LETTER ALPHA REMAP(0x0392, 0xC2) // GREEK CAPITAL LETTER BETA REMAP(0x0393, 0xC3) // GREEK CAPITAL LETTER GAMMA REMAP(0x0394, 0xC4) // GREEK CAPITAL LETTER DELTA REMAP(0x0395, 0xC5) // GREEK CAPITAL LETTER EPSILON REMAP(0x0396, 0xC6) // GREEK CAPITAL LETTER ZETA REMAP(0x0397, 0xC7) // GREEK CAPITAL LETTER ETA REMAP(0x0398, 0xC8) // GREEK CAPITAL LETTER THETA REMAP(0x0399, 0xC9) // GREEK CAPITAL LETTER IOTA REMAP(0x039A, 0xCA) // GREEK CAPITAL LETTER KAPPA REMAP(0x039B, 0xCB) // GREEK CAPITAL LETTER LAMDA REMAP(0x039C, 0xCC) // GREEK CAPITAL LETTER MU REMAP(0x039D, 0xCD) // GREEK CAPITAL LETTER NU REMAP(0x039E, 0xCE) // GREEK CAPITAL LETTER XI REMAP(0x039F, 0xCF) // GREEK CAPITAL LETTER OMICRON REMAP(0x03A0, 0xD0) // GREEK CAPITAL LETTER PI REMAP(0x03A1, 0xD1) // GREEK CAPITAL LETTER RHO REMAP(0x03A3, 0xD3) // GREEK CAPITAL LETTER SIGMA REMAP(0x03A4, 0xD4) // GREEK CAPITAL LETTER TAU REMAP(0x03A5, 0xD5) // GREEK CAPITAL LETTER UPSILON REMAP(0x03A6, 0xD6) // GREEK CAPITAL LETTER PHI REMAP(0x03A7, 0xD7) // GREEK CAPITAL LETTER CHI REMAP(0x03A8, 0xD8) // GREEK CAPITAL LETTER PSI REMAP(0x03A9, 0xD9) // GREEK CAPITAL LETTER OMEGA // Greek small letters with tonos (accented) REMAP(0x03AC, 0xDC) // GREEK SMALL LETTER ALPHA WITH TONOS REMAP(0x03AD, 0xDD) // GREEK SMALL LETTER EPSILON WITH TONOS REMAP(0x03AE, 0xDE) // GREEK SMALL LETTER ETA WITH TONOS REMAP(0x03AF, 0xDF) // GREEK SMALL LETTER IOTA WITH TONOS // Greek small letters (U+03B1-U+03C9 -> 0xE1-0xF9) REMAP(0x03B1, 0xE1) // GREEK SMALL LETTER ALPHA REMAP(0x03B2, 0xE2) // GREEK SMALL LETTER BETA REMAP(0x03B3, 0xE3) // GREEK SMALL LETTER GAMMA REMAP(0x03B4, 0xE4) // GREEK SMALL LETTER DELTA REMAP(0x03B5, 0xE5) // GREEK SMALL LETTER EPSILON REMAP(0x03B6, 0xE6) // GREEK SMALL LETTER ZETA REMAP(0x03B7, 0xE7) // GREEK SMALL LETTER ETA REMAP(0x03B8, 0xE8) // GREEK SMALL LETTER THETA REMAP(0x03B9, 0xE9) // GREEK SMALL LETTER IOTA REMAP(0x03BA, 0xEA) // GREEK SMALL LETTER KAPPA REMAP(0x03BB, 0xEB) // GREEK SMALL LETTER LAMDA REMAP(0x03BC, 0xEC) // GREEK SMALL LETTER MU REMAP(0x03BD, 0xED) // GREEK SMALL LETTER NU REMAP(0x03BE, 0xEE) // GREEK SMALL LETTER XI REMAP(0x03BF, 0xEF) // GREEK SMALL LETTER OMICRON REMAP(0x03C0, 0xF0) // GREEK SMALL LETTER PI REMAP(0x03C1, 0xF1) // GREEK SMALL LETTER RHO REMAP(0x03C2, 0xF2) // GREEK SMALL LETTER FINAL SIGMA REMAP(0x03C3, 0xF3) // GREEK SMALL LETTER SIGMA REMAP(0x03C4, 0xF4) // GREEK SMALL LETTER TAU REMAP(0x03C5, 0xF5) // GREEK SMALL LETTER UPSILON REMAP(0x03C6, 0xF6) // GREEK SMALL LETTER PHI REMAP(0x03C7, 0xF7) // GREEK SMALL LETTER CHI REMAP(0x03C8, 0xF8) // GREEK SMALL LETTER PSI REMAP(0x03C9, 0xF9) // GREEK SMALL LETTER OMEGA // More accented small letters REMAP(0x03CA, 0xFA) // GREEK SMALL LETTER IOTA WITH DIALYTIKA REMAP(0x03CB, 0xFB) // GREEK SMALL LETTER UPSILON WITH DIALYTIKA REMAP(0x03CC, 0xFC) // GREEK SMALL LETTER OMICRON WITH TONOS REMAP(0x03CD, 0xFD) // GREEK SMALL LETTER UPSILON WITH TONOS REMAP(0x03CE, 0xFE) // GREEK SMALL LETTER OMEGA WITH TONOS } } else /*ASCII or Unhandled*/ { if (utf8.length() == 1) return utf8.at(0); } // All single-byte (ASCII) characters should have been handled by now // Only unhandled multi-byte UTF8 characters should remain assert(utf8.length() > 1); // Parse emoji // Strip emoji modifiers switch (toUtf32(utf8)) { REMAP(0x1F44D, 0x01) // 👍 Thumbs Up REMAP(0x1F44E, 0x02) // 👎 Thumbs Down REMAP(0x1F60A, 0x03) // 😊 Smiling Face with Smiling Eyes REMAP(0x1F642, 0x03) // 🙂 Slightly Smiling Face REMAP(0x1F601, 0x03) // 😁 Grinning Face with Smiling Eye REMAP(0x1F602, 0x04) // 😂 Face with Tears of Joy REMAP(0x1F923, 0x04) // 🤣 Rolling on the Floor Laughing REMAP(0x1F606, 0x04) // 😆 Smiling with Open Mouth and Closed Eyes REMAP(0x1F44B, 0x05) // 👋 Waving Hand REMAP(0x02600, 0x06) // ☀ Sun REMAP(0x1F31E, 0x06) // 🌞 Sun with Face // 0x07 - Bell character (unused) REMAP(0x1F327, 0x08) // 🌧️ Cloud with Rain REMAP(0x02601, 0x09) // ☁️ Cloud REMAP(0x1F32B, 0x09) // Fog REMAP(0x1F9E1, 0x0B) // 🧡 Orange Heart REMAP(0x02763, 0x0B) // ❣ Heart Exclamation REMAP(0x02764, 0x0B) // ❤ Heart REMAP(0x1F495, 0x0B) // 💕 Two Hearts REMAP(0x1F496, 0x0B) // 💖 Sparkling Heart REMAP(0x1F497, 0x0B) // 💗 Growing Heart REMAP(0x1F498, 0x0B) // 💘 Heart with Arrow REMAP(0x1F4A9, 0x0C) // 💩 Pile of Poo // 0x0D - Carriage return (unused) REMAP(0x1F514, 0x0E) // 🔔 Bell REMAP(0x1F62D, 0x0F) // 😭 Loudly Crying Face REMAP(0x1F622, 0x0F) // 😢 Crying Face REMAP(0x1F64F, 0x10) // 🙏 Person with Folded Hands REMAP(0x1F618, 0x11) // 😘 Face Throwing a Kiss REMAP(0x1F389, 0x12) // 🎉 Party Popper REMAP(0x1F600, 0x13) // 😀 Grinning Face REMAP(0x1F603, 0x13) // 😃 Smiling Face with Open Mouth REMAP(0x1F604, 0x13) // 😄 Smiling Face with Open Mouth and Smiling Eyes REMAP(0x1F97A, 0x14) // 🥺 Face with Pleading Eyes REMAP(0x1F605, 0x15) // 😅 Smiling with Sweat REMAP(0x1F525, 0x16) // 🔥 Fire REMAP(0x1F926, 0x17) // 🤦 Face Palm REMAP(0x1F937, 0x18) // 🤷 Shrug REMAP(0x1F644, 0x19) // 🙄 Face with Rolling Eyes // 0x1A Substitution (unused) REMAP(0x1F917, 0x1B) // 🤗 Hugging Face REMAP(0x1F609, 0x1C) // 😉 Winking Face REMAP(0x1F61C, 0x1C) // 😜 Face with Stuck-Out Tongue and Winking Eye REMAP(0x1F60F, 0x1C) // 😏 Smirking Face REMAP(0x1F914, 0x1D) // 🤔 Thinking Face REMAP(0x1FAE1, 0x1E) // 🫡 Saluting Face REMAP(0x1F44C, 0x1F) // 👌 OK Hand Sign REMAP(0x02755, '!') // ❕ REMAP(0x02757, '!') // ❗ REMAP(0x0203C, '!') // ‼ REMAP(0x02753, '?') // ❓ REMAP(0x02754, '?') // ❔ REMAP(0x02049, '?') // ⁉ // Modifiers (deleted) REMAP(0x02640, 0x7F) // Gender REMAP(0x02642, 0x7F) REMAP(0x1F3FB, 0x7F) // Skin Tones REMAP(0x1F3FC, 0x7F) REMAP(0x1F3FD, 0x7F) REMAP(0x1F3FE, 0x7F) REMAP(0x1F3FF, 0x7F) REMAP(0x0FE00, 0x7F) // Variation Selectors REMAP(0x0FE01, 0x7F) REMAP(0x0FE02, 0x7F) REMAP(0x0FE03, 0x7F) REMAP(0x0FE04, 0x7F) REMAP(0x0FE05, 0x7F) REMAP(0x0FE06, 0x7F) REMAP(0x0FE07, 0x7F) REMAP(0x0FE08, 0x7F) REMAP(0x0FE09, 0x7F) REMAP(0x0FE0A, 0x7F) REMAP(0x0FE0B, 0x7F) REMAP(0x0FE0C, 0x7F) REMAP(0x0FE0D, 0x7F) REMAP(0x0FE0E, 0x7F) REMAP(0x0FE0F, 0x7F) REMAP(0x0200D, 0x7F) // Zero Width Joiner } // If not handled, return SUB return '\x1A'; // Sweep up the syntactic sugar // Don't want ants in the house #undef REMAP } #endif ================================================ FILE: src/graphics/niche/InkHUD/AppletFont.h ================================================ #ifdef MESHTASTIC_INCLUDE_INKHUD /* Wrapper class for an AdafruitGFX font Pre-calculates some font dimension info which InkHUD uses repeatedly Re-encodes UTF-8 characters to suit extended ASCII AdafruitGFX fonts */ #pragma once #include "configuration.h" #include // GFXRoot drawing lib namespace NicheGraphics::InkHUD { // An AdafruitGFX font, bundled with precalculated dimensions which are used frequently by InkHUD class AppletFont { public: enum Encoding { ASCII, WINDOWS_1250, WINDOWS_1251, WINDOWS_1252, WINDOWS_1253, }; AppletFont(); explicit AppletFont(const GFXfont &adafruitGFXFont, Encoding encoding = ASCII, int8_t paddingTop = 0, int8_t paddingBottom = 0); uint8_t lineHeight(); uint8_t heightAboveCursor(); uint8_t heightBelowCursor(); uint8_t widthBetweenWords(); // Width of the space character std::string decodeUTF8(const std::string &encoded); const GFXfont *gfxFont = nullptr; // Default value: in-built AdafruitGFX font private: uint32_t toUtf32(const std::string &utf8); char applyEncoding(const std::string &utf8); uint8_t height = 8; // Default value: in-built AdafruitGFX font uint8_t ascenderHeight = 0; // Default value: in-built AdafruitGFX font uint8_t descenderHeight = 8; // Default value: in-built AdafruitGFX font uint8_t spaceCharWidth = 8; // Default value: in-built AdafruitGFX font Encoding encoding = ASCII; }; } // namespace NicheGraphics::InkHUD // Macros for InkHUD's standard fonts // -------------------------------------- // Use these once only, passing them to InkHUD::Applet::fontLarge and InkHUD::Applet:fontSmall // Line padding has been adjusted manually, to compensate for a few *extra tall* diacritics // Central European #include "graphics/niche/Fonts/FreeSans12pt_Win1250.h" #include "graphics/niche/Fonts/FreeSans6pt_Win1250.h" #include "graphics/niche/Fonts/FreeSans9pt_Win1250.h" #define FREESANS_12PT_WIN1250 InkHUD::AppletFont(FreeSans12pt_Win1250, InkHUD::AppletFont::WINDOWS_1250, -3, 1) #define FREESANS_9PT_WIN1250 InkHUD::AppletFont(FreeSans9pt_Win1250, InkHUD::AppletFont::WINDOWS_1250, -1, -1) #define FREESANS_6PT_WIN1250 InkHUD::AppletFont(FreeSans6pt_Win1250, InkHUD::AppletFont::WINDOWS_1250, -1, -2) // Cyrillic #include "graphics/niche/Fonts/FreeSans12pt_Win1251.h" #include "graphics/niche/Fonts/FreeSans6pt_Win1251.h" #include "graphics/niche/Fonts/FreeSans9pt_Win1251.h" #define FREESANS_12PT_WIN1251 InkHUD::AppletFont(FreeSans12pt_Win1251, InkHUD::AppletFont::WINDOWS_1251, -3, 1) #define FREESANS_9PT_WIN1251 InkHUD::AppletFont(FreeSans9pt_Win1251, InkHUD::AppletFont::WINDOWS_1251, -2, -1) #define FREESANS_6PT_WIN1251 InkHUD::AppletFont(FreeSans6pt_Win1251, InkHUD::AppletFont::WINDOWS_1251, -1, -2) // Western European #include "graphics/niche/Fonts/FreeSans12pt_Win1252.h" #include "graphics/niche/Fonts/FreeSans6pt_Win1252.h" #include "graphics/niche/Fonts/FreeSans9pt_Win1252.h" #define FREESANS_12PT_WIN1252 InkHUD::AppletFont(FreeSans12pt_Win1252, InkHUD::AppletFont::WINDOWS_1252, -3, 1) #define FREESANS_9PT_WIN1252 InkHUD::AppletFont(FreeSans9pt_Win1252, InkHUD::AppletFont::WINDOWS_1252, -2, -1) #define FREESANS_6PT_WIN1252 InkHUD::AppletFont(FreeSans6pt_Win1252, InkHUD::AppletFont::WINDOWS_1252, -1, -2) // Greek #include "graphics/niche/Fonts/FreeSans12pt_Win1253.h" #include "graphics/niche/Fonts/FreeSans6pt_Win1253.h" #include "graphics/niche/Fonts/FreeSans9pt_Win1253.h" #define FREESANS_12PT_WIN1253 InkHUD::AppletFont(FreeSans12pt_Win1253, InkHUD::AppletFont::WINDOWS_1253, -3, 1) #define FREESANS_9PT_WIN1253 InkHUD::AppletFont(FreeSans9pt_Win1253, InkHUD::AppletFont::WINDOWS_1253, -2, -1) #define FREESANS_6PT_WIN1253 InkHUD::AppletFont(FreeSans6pt_Win1253, InkHUD::AppletFont::WINDOWS_1253, -1, -2) #endif ================================================ FILE: src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.cpp ================================================ #ifdef MESHTASTIC_INCLUDE_INKHUD #include "./MapApplet.h" using namespace NicheGraphics; void InkHUD::MapApplet::onRender(bool full) { // Abort if no markers to render if (!enoughMarkers()) { printAt(X(0.5), Y(0.5) - (getFont().lineHeight() / 2), "Node positions", CENTER, MIDDLE); printAt(X(0.5), Y(0.5) + (getFont().lineHeight() / 2), "will appear here", CENTER, MIDDLE); return; } // Helper: draw rounded rectangle centered at x,y auto fillRoundedRect = [&](int16_t cx, int16_t cy, int16_t w, int16_t h, int16_t r, uint16_t color) { int16_t x = cx - (w / 2); int16_t y = cy - (h / 2); // center rects fillRect(x + r, y, w - 2 * r, h, color); fillRect(x, y + r, r, h - 2 * r, color); fillRect(x + w - r, y + r, r, h - 2 * r, color); // corners fillCircle(x + r, y + r, r, color); fillCircle(x + w - r - 1, y + r, r, color); fillCircle(x + r, y + h - r - 1, r, color); fillCircle(x + w - r - 1, y + h - r - 1, r, color); }; // Find center of map getMapCenter(&latCenter, &lngCenter); calculateAllMarkers(); getMapSize(&widthMeters, &heightMeters); calculateMapScale(); // Draw all markers first for (Marker m : markers) { int16_t x = X(0.5) + (m.eastMeters * metersToPx); int16_t y = Y(0.5) - (m.northMeters * metersToPx); // Add white halo outline first constexpr int outlinePad = 1; int boxSize = 11; int radius = 2; // rounded corner radius // White halo background fillRoundedRect(x, y, boxSize + (outlinePad * 2), boxSize + (outlinePad * 2), radius + 1, WHITE); // Draw inner box fillRoundedRect(x, y, boxSize, boxSize, radius, BLACK); // Text inside setFont(fontSmall); setTextColor(WHITE); // Draw actual marker on top if (m.hasHopsAway && m.hopsAway > config.lora.hop_limit) { printAt(x + 1, y + 1, "X", CENTER, MIDDLE); } else if (!m.hasHopsAway) { printAt(x + 1, y + 1, "?", CENTER, MIDDLE); } else { char hopStr[4]; snprintf(hopStr, sizeof(hopStr), "%d", m.hopsAway); printAt(x, y + 1, hopStr, CENTER, MIDDLE); } // Restore default font and color setFont(fontSmall); setTextColor(BLACK); } // Dual map scale bars int16_t horizPx = width() * 0.25f; int16_t vertPx = height() * 0.25f; float horizMeters = horizPx / metersToPx; float vertMeters = vertPx / metersToPx; auto formatDistance = [&](float meters, char *out, size_t len) { if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { float feet = meters * 3.28084f; if (feet < 528) snprintf(out, len, "%.0f ft", feet); else { float miles = feet / 5280.0f; snprintf(out, len, miles < 10 ? "%.1f mi" : "%.0f mi", miles); } } else { if (meters >= 1000) snprintf(out, len, "%.1f km", meters / 1000.0f); else snprintf(out, len, "%.0f m", meters); } }; // Horizontal scale bar int16_t horizBarY = height() - 2; int16_t horizBarX = 1; drawLine(horizBarX, horizBarY, horizBarX + horizPx, horizBarY, BLACK); drawLine(horizBarX, horizBarY - 3, horizBarX, horizBarY + 3, BLACK); drawLine(horizBarX + horizPx, horizBarY - 3, horizBarX + horizPx, horizBarY + 3, BLACK); char horizLabel[32]; formatDistance(horizMeters, horizLabel, sizeof(horizLabel)); int16_t horizLabelW = getTextWidth(horizLabel); int16_t horizLabelH = getFont().lineHeight(); int16_t horizLabelX = horizBarX + horizPx + 4; int16_t horizLabelY = horizBarY - horizLabelH + 1; fillRect(horizLabelX - 2, horizLabelY - 1, horizLabelW + 4, horizLabelH + 2, WHITE); printAt(horizLabelX, horizBarY, horizLabel, LEFT, BOTTOM); // Vertical scale bar int16_t vertBarX = 1; int16_t vertBarBottom = horizBarY; int16_t vertBarTop = vertBarBottom - vertPx; drawLine(vertBarX, vertBarBottom, vertBarX, vertBarTop, BLACK); drawLine(vertBarX - 3, vertBarBottom, vertBarX + 3, vertBarBottom, BLACK); drawLine(vertBarX - 3, vertBarTop, vertBarX + 3, vertBarTop, BLACK); char vertTopLabel[32]; formatDistance(vertMeters, vertTopLabel, sizeof(vertTopLabel)); int16_t topLabelY = vertBarTop - getFont().lineHeight() - 2; int16_t topLabelW = getTextWidth(vertTopLabel); int16_t topLabelH = getFont().lineHeight(); fillRect(vertBarX - 2, topLabelY - 1, topLabelW + 6, topLabelH + 2, WHITE); printAt(vertBarX + (topLabelW / 2) + 1, topLabelY + (topLabelH / 2), vertTopLabel, CENTER, MIDDLE); char vertBottomLabel[32]; formatDistance(vertMeters, vertBottomLabel, sizeof(vertBottomLabel)); int16_t bottomLabelY = vertBarBottom + 4; int16_t bottomLabelW = getTextWidth(vertBottomLabel); int16_t bottomLabelH = getFont().lineHeight(); fillRect(vertBarX - 2, bottomLabelY - 1, bottomLabelW + 6, bottomLabelH + 2, WHITE); printAt(vertBarX + (bottomLabelW / 2) + 1, bottomLabelY + (bottomLabelH / 2), vertBottomLabel, CENTER, MIDDLE); // Draw our node LAST with full white fill + outline meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); if (ourNode && nodeDB->hasValidPosition(ourNode)) { Marker self = calculateMarker(ourNode->position.latitude_i * 1e-7, ourNode->position.longitude_i * 1e-7, false, 0); int16_t centerX = X(0.5) + (self.eastMeters * metersToPx); int16_t centerY = Y(0.5) - (self.northMeters * metersToPx); // White fill background + halo fillCircle(centerX, centerY, 8, WHITE); // big white base drawCircle(centerX, centerY, 8, WHITE); // crisp edge // Black bullseye on top drawCircle(centerX, centerY, 6, BLACK); fillCircle(centerX, centerY, 2, BLACK); // Crosshairs drawLine(centerX - 8, centerY, centerX + 8, centerY, BLACK); drawLine(centerX, centerY - 8, centerX, centerY + 8, BLACK); } } // Find the center point, in the middle of all node positions // Calculated values are written to the *lat and *long pointer args // - Finds the "mean lat long" // - Calculates furthest nodes from "mean lat long" // - Place map center directly between these furthest nodes void InkHUD::MapApplet::getMapCenter(float *lat, float *lng) { // If we have a valid position for our own node, use that as the anchor meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); if (ourNode && nodeDB->hasValidPosition(ourNode)) { *lat = ourNode->position.latitude_i * 1e-7; *lng = ourNode->position.longitude_i * 1e-7; } else { // Find mean lat long coords // ============================ // - assigning X, Y and Z values to position on Earth's surface in 3D space, relative to center of planet // - averages the x, y and z coords // - uses tan to find angles for lat / long degrees // - longitude: triangle formed by x and y (on plane of the equator) // - latitude: triangle formed by z (north south), // and the line along plane of equator which stretches from earth's axis to where point xyz intersects planet's // surface // Working totals, averaged after nodeDB processed uint32_t positionCount = 0; float xAvg = 0; float yAvg = 0; float zAvg = 0; // For each node in db for (uint32_t i = 0; i < nodeDB->getNumMeshNodes(); i++) { meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); // Skip if no position if (!nodeDB->hasValidPosition(node)) continue; // Skip if derived applet doesn't want to show this node on the map if (!shouldDrawNode(node)) continue; // Latitude and Longitude of node, in radians float latRad = node->position.latitude_i * (1e-7) * DEG_TO_RAD; float lngRad = node->position.longitude_i * (1e-7) * DEG_TO_RAD; // Convert to cartesian points, with center of earth at 0, 0, 0 // Exact distance from center is irrelevant, as we're only interested in the vector float x = cos(latRad) * cos(lngRad); float y = cos(latRad) * sin(lngRad); float z = sin(latRad); // To find mean values shortly xAvg += x; yAvg += y; zAvg += z; positionCount++; } // All NodeDB processed, find mean values xAvg /= positionCount; yAvg /= positionCount; zAvg /= positionCount; // Longitude from cartesian coords // (Angle from 3D coords describing a point of globe's surface) /* UK /-------\ (Top View) /- -\ /- (You) -\ /- . -\ /- . X -\ Asia - ... - USA \- Y -/ \- -/ \- -/ \- -/ \- -----/ Pacific */ *lng = atan2(yAvg, xAvg) * RAD_TO_DEG; // Latitude from cartesian coords // (Angle from 3D coords describing a point on the globe's surface) // As latitude increases, distance from the Earth's north-south axis out to our surface point decreases. // Means we need to first find the hypotenuse which becomes base of our triangle in the second step /* UK North /-------\ (Front View) /-------\ (Top View) /- -\ /- -\ /- (You) -\ /-(You) -\ /- /. -\ /- . -\ /- √X²+Y²/ . X -\ /- Z . -\ Asia - /... - USA - ..... - \- Y -/ \- √X²+Y² -/ \- -/ \- -/ \- -/ \- -/ \- -/ \- -/ \- -----/ \- -----/ Pacific South */ float hypotenuse = sqrt((xAvg * xAvg) + (yAvg * yAvg)); // Distance from globe's north-south axis to surface intersect *lat = atan2(zAvg, hypotenuse) * RAD_TO_DEG; } // Use either our node position, or the mean fallback as the center latCenter = *lat; lngCenter = *lng; // ---------------------------------------------- // This has given us either: // - our actual position (preferred), or // - a mean position (fallback if we had no fix) // // What we actually want is to place our center so that our outermost nodes // end up on the border of our map. The only real use of our "center" is to give // us a reference frame: which direction is east, and which is west. //------------------------------------------------ // Find furthest nodes from our center // ======================================== float northernmost = latCenter; float southernmost = latCenter; float easternmost = lngCenter; float westernmost = lngCenter; for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) { meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); // Skip if no position if (!nodeDB->hasValidPosition(node)) continue; // Skip if derived applet doesn't want to show this node on the map if (!shouldDrawNode(node)) continue; // Check for a new top or bottom latitude float latNode = node->position.latitude_i * 1e-7; northernmost = max(northernmost, latNode); southernmost = min(southernmost, latNode); // Longitude is trickier float lngNode = node->position.longitude_i * 1e-7; float degEastward = fmod(((lngNode - lngCenter) + 360), 360); // Degrees traveled east from lngCenter to reach node float degWestward = abs(fmod(((lngNode - lngCenter) - 360), 360)); // Degrees traveled west from lngCenter to reach node if (degEastward < degWestward) easternmost = max(easternmost, lngCenter + degEastward); else westernmost = min(westernmost, lngCenter - degWestward); } // Todo: check for issues with map spans >180 deg. MQTT only.. latCenter = (northernmost + southernmost) / 2; lngCenter = (westernmost + easternmost) / 2; // In case our new center is west of -180, or east of +180, for some reason lngCenter = fmod(lngCenter, 180); } // Size of map in meters // Grown to fit the nodes furthest from map center // Overridable if derived applet wants a custom map size (fixed size?) void InkHUD::MapApplet::getMapSize(uint32_t *widthMeters, uint32_t *heightMeters) { // Reset the value *widthMeters = 0; *heightMeters = 0; // Find the greatest distance horizontally and vertically from map center for (Marker m : markers) { *widthMeters = max(*widthMeters, (uint32_t)abs(m.eastMeters) * 2); *heightMeters = max(*heightMeters, (uint32_t)abs(m.northMeters) * 2); } // Add padding *widthMeters *= 1.1; *heightMeters *= 1.1; } // Convert and store info we need for drawing a marker // Lat / long to "meters relative to map center", for position on screen // Info about hopsAway, for marker size InkHUD::MapApplet::Marker InkHUD::MapApplet::calculateMarker(float lat, float lng, bool hasHopsAway, uint8_t hopsAway) { assert(lat != 0 || lng != 0); // Not null island. Applets should check this before calling. // Bearing and distance from map center to node float distanceFromCenter = GeoCoord::latLongToMeter(latCenter, lngCenter, lat, lng); float bearingFromCenter = GeoCoord::bearing(latCenter, lngCenter, lat, lng); // in radians // Split into meters north and meters east components (signed) // - signedness of cos / sin automatically sets negative if south or west float northMeters = cos(bearingFromCenter) * distanceFromCenter; float eastMeters = sin(bearingFromCenter) * distanceFromCenter; // Store this as a new marker Marker m; m.eastMeters = eastMeters; m.northMeters = northMeters; m.hasHopsAway = hasHopsAway; m.hopsAway = hopsAway; return m; } // Draw a marker on the map for a node, with a shortname label, and backing box void InkHUD::MapApplet::drawLabeledMarker(meshtastic_NodeInfoLite *node) { // Find x and y position based on node's position in nodeDB assert(nodeDB->hasValidPosition(node)); Marker m = calculateMarker(node->position.latitude_i * 1e-7, // Lat, converted from Meshtastic's internal int32 style node->position.longitude_i * 1e-7, // Long, converted from Meshtastic's internal int32 style node->has_hops_away, // Is the hopsAway number valid node->hops_away // Hops away ); // Convert to pixel coords int16_t markerX = X(0.5) + (m.eastMeters * metersToPx); int16_t markerY = Y(0.5) - (m.northMeters * metersToPx); constexpr uint16_t paddingH = 2; constexpr uint16_t paddingW = 4; uint16_t paddingInnerW = 2; // Zero'd out if no text constexpr uint16_t markerSizeMax = 12; // Size of cross (if marker uses a cross) constexpr uint16_t markerSizeMin = 5; int16_t textX; int16_t textY; uint16_t textW; uint16_t textH; int16_t labelX; int16_t labelY; uint16_t labelW; uint16_t labelH; uint8_t markerSize; bool tooManyHops = node->hops_away > config.lora.hop_limit; bool isOurNode = node->num == nodeDB->getNodeNum(); bool unknownHops = !node->has_hops_away && !isOurNode; // Parse any non-ascii chars in the short name, // and use last 4 instead if unknown / can't render std::string shortName = parseShortName(node); // We will draw a left or right hand variant, to place text towards screen center // Hopefully avoid text spilling off screen // Most values are the same, regardless of left-right handedness // Pick emblem style if (tooManyHops) markerSize = getTextWidth("!"); else if (unknownHops) markerSize = markerSizeMin; else markerSize = map(node->hops_away, 0, config.lora.hop_limit, markerSizeMax, markerSizeMin); // Common dimensions (left or right variant) textW = getTextWidth(shortName); if (textW == 0) paddingInnerW = 0; // If no text, no padding for text textH = fontSmall.lineHeight(); labelH = paddingH + max((int16_t)(textH), (int16_t)markerSize) + paddingH; labelY = markerY - (labelH / 2); textY = markerY; labelW = paddingW + markerSize + paddingInnerW + textW + paddingW; // Width is same whether right or left hand variant // Left-side variant if (markerX < width() / 2) { labelX = markerX - (markerSize / 2) - paddingW; textX = labelX + paddingW + markerSize + paddingInnerW; } // Right-side variant else { labelX = markerX - (markerSize / 2) - paddingInnerW - textW - paddingW; textX = labelX + paddingW; } // Prevent overlap with scale bars and their labels // Define a "safe zone" in the bottom-left where the scale bars and text are drawn constexpr int16_t safeZoneHeight = 28; // adjust based on your label font height constexpr int16_t safeZoneWidth = 60; // adjust based on horizontal label width zone bool overlapsScale = (labelY + labelH > height() - safeZoneHeight) && (labelX < safeZoneWidth); // If it overlaps, shift label upward slightly above the safe zone if (overlapsScale) { labelY = height() - safeZoneHeight - labelH - 2; textY = labelY + (labelH / 2); } // Backing box fillRect(labelX, labelY, labelW, labelH, WHITE); drawRect(labelX, labelY, labelW, labelH, BLACK); // Short name printAt(textX, textY, shortName, LEFT, MIDDLE); // If the label is for our own node, // fade it by overdrawing partially with white if (node == nodeDB->getMeshNode(nodeDB->getNodeNum())) hatchRegion(labelX, labelY, labelW, labelH, 2, WHITE); // Draw the marker emblem // - after the fading, because hatching (own node) can align with cross and make it look weird if (tooManyHops) printAt(markerX, markerY, "!", CENTER, MIDDLE); else drawCross(markerX, markerY, markerSize); // The fewer the hops, the larger the marker. Also handles unknownHops } // Check if we actually have enough nodes which would be shown on the map // Need at least two, to draw a sensible map bool InkHUD::MapApplet::enoughMarkers() { size_t count = 0; for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) { meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); // Count nodes if (nodeDB->hasValidPosition(node) && shouldDrawNode(node)) count++; // We need to find two if (count == 2) return true; // Two nodes is enough for a sensible map } return false; // No nodes would be drawn (or just the one, uselessly at 0,0) } // Calculate how far north and east of map center each node is // Derived applets can control which nodes to calculate (and later, draw) by overriding MapApplet::shouldDrawNode void InkHUD::MapApplet::calculateAllMarkers() { // Clear old markers markers.clear(); // For each node in db for (uint32_t i = 0; i < nodeDB->getNumMeshNodes(); i++) { meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); // Skip if no position if (!nodeDB->hasValidPosition(node)) continue; // Skip if derived applet doesn't want to show this node on the map if (!shouldDrawNode(node)) continue; // Skip if our own node // - special handling in render() if (node->num == nodeDB->getNodeNum()) continue; // Calculate marker and store it markers.push_back( calculateMarker(node->position.latitude_i * 1e-7, // Lat, converted from Meshtastic's internal int32 style node->position.longitude_i * 1e-7, // Long, converted from Meshtastic's internal int32 style node->has_hops_away, // Is the hopsAway number valid node->hops_away // Hops away )); } } // Determine the conversion factor between metres, and pixels on screen // May be overridden by derived applet, if custom scale required (fixed map size?) void InkHUD::MapApplet::calculateMapScale() { // Aspect ratio of map and screen // - larger = wide, smaller = tall // - used to set scale, so that widest map dimension fits in applet float mapAspectRatio = (float)widthMeters / heightMeters; float appletAspectRatio = (float)width() / height(); // "Shrink to fit" // Scale the map so that the largest dimension is fully displayed // Because aspect ratio will be maintained, the other dimension will appear "padded" if (mapAspectRatio > appletAspectRatio) metersToPx = (float)width() / widthMeters; // Too wide for applet. Constrain to fit width. else metersToPx = (float)height() / heightMeters; // Too tall for applet. Constrain to fit height. } // Draw an x, centered on a specific point // Most markers will draw with this method void InkHUD::MapApplet::drawCross(int16_t x, int16_t y, uint8_t size) { int16_t x0 = x - (size / 2); int16_t y0 = y - (size / 2); int16_t x1 = x0 + size - 1; int16_t y1 = y0 + size - 1; drawLine(x0, y0, x1, y1, BLACK); drawLine(x0, y1, x1, y0, BLACK); } #endif ================================================ FILE: src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.h ================================================ #ifdef MESHTASTIC_INCLUDE_INKHUD /* Base class for Applets which show nodes on a map Plots position of for a selection of nodes, with north facing up. Size of cross represents hops away. Our own node is identified with a faded label. The base applet doesn't handle any events; this is left to the derived applets. */ #pragma once #include "configuration.h" #include "graphics/niche/InkHUD/Applet.h" #include "MeshModule.h" #include "gps/GeoCoord.h" namespace NicheGraphics::InkHUD { class MapApplet : public Applet { public: void onRender(bool full) override; protected: virtual bool shouldDrawNode(meshtastic_NodeInfoLite *node) { return true; } // Allow derived applets to filter the nodes virtual void getMapCenter(float *lat, float *lng); virtual void getMapSize(uint32_t *widthMeters, uint32_t *heightMeters); bool enoughMarkers(); // Anything to draw? void drawLabeledMarker(meshtastic_NodeInfoLite *node); // Highlight a specific marker private: // Position and size of a marker to be drawn struct Marker { float eastMeters = 0; // Meters east of map center. Negative if west. float northMeters = 0; // Meters north of map center. Negative if south. bool hasHopsAway = false; uint8_t hopsAway = 0; // Determines marker size }; Marker calculateMarker(float lat, float lng, bool hasHopsAway, uint8_t hopsAway); void calculateAllMarkers(); void calculateMapScale(); // Conversion factor for meters to pixels void drawCross(int16_t x, int16_t y, uint8_t size); // Draw the X used for most markers float metersToPx = 0; // Conversion factor for meters to pixels float latCenter = 0; // Map center: latitude float lngCenter = 0; // Map center: longitude std::list markers; uint32_t widthMeters = 0; // Map width: meters uint32_t heightMeters = 0; // Map height: meters }; } // namespace NicheGraphics::InkHUD #endif ================================================ FILE: src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.cpp ================================================ #ifdef MESHTASTIC_INCLUDE_INKHUD #include "RTC.h" #include "GeoCoord.h" #include "NodeDB.h" #include "./NodeListApplet.h" using namespace NicheGraphics; InkHUD::NodeListApplet::NodeListApplet(const char *name) : MeshModule(name) { // We only need to be promiscuous in order to hear NodeInfo, apparently. See NodeInfoModule // For all other packets, we manually act as if isPromiscuous=false, in wantPacket MeshModule::isPromiscuous = true; } // Do we want to process this packet with handleReceived()? bool InkHUD::NodeListApplet::wantPacket(const meshtastic_MeshPacket *p) { // Only interested if: return isActive() // Applet is active && !isFromUs(p) // Packet is incoming (not outgoing) && (isToUs(p) || isBroadcast(p->to) || // Either: intended for us, p->decoded.portnum == meshtastic_PortNum_NODEINFO_APP); // or nodeinfo // To match the behavior seen in the client apps: // - NodeInfoModule's ProtoBufModule base is "promiscuous" // - All other activity is *not* promiscuous // To achieve this, our MeshModule *is* promiscuous, and we're manually reimplementing non-promiscuous behavior here, // to match the code in MeshModule::callModules } // MeshModule packets arrive here // Extract the info and pass it to the derived applet // Derived applet will store the CardInfo, and perform any required sorting of the CardInfo collection // Derived applet might also need to keep other tallies (active nodes count?) ProcessMessage InkHUD::NodeListApplet::handleReceived(const meshtastic_MeshPacket &mp) { // Abort if applet fully deactivated // Already handled by wantPacket in this case, but good practice for all applets, as some *do* require this early return if (!isActive()) return ProcessMessage::CONTINUE; // Assemble info: from this event CardInfo c; c.nodeNum = mp.from; c.signal = getSignalStrength(mp.rx_snr, mp.rx_rssi); // Assemble info: from nodeDB (needed to detect changes) meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(c.nodeNum); meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); if (node) { if (node->has_hops_away) c.hopsAway = node->hops_away; if (nodeDB->hasValidPosition(node) && nodeDB->hasValidPosition(ourNode)) { // Get lat and long as float // Meshtastic stores these as integers internally float ourLat = ourNode->position.latitude_i * 1e-7; float ourLong = ourNode->position.longitude_i * 1e-7; float theirLat = node->position.latitude_i * 1e-7; float theirLong = node->position.longitude_i * 1e-7; c.distanceMeters = (int32_t)GeoCoord::latLongToMeter(theirLat, theirLong, ourLat, ourLong); } } // Pass to the derived applet // Derived applet is responsible for requesting update, if justified // That request will eventually trigger our class' onRender method handleParsed(c); return ProcessMessage::CONTINUE; // Let others look at this message also if they want } // Calculate maximum number of cards we may ever need to render, in our tallest layout config // Number might be slightly in excess of the true value: applet header text not accounted for uint8_t InkHUD::NodeListApplet::maxCards() { // Cache result. Shouldn't change during execution static uint8_t maxCardCount = 0; if (!maxCardCount) { const uint16_t height = Tile::maxDisplayDimension(); // Use a loop instead of arithmetic, because it's easier for my brain to follow // Add cards one by one, until the latest card extends below screen uint16_t y = cardH; // First card: no margin above maxCardCount = 1; while (y < height) { y += cardMarginH; y += cardH; maxCardCount++; } } return maxCardCount; } // Draw, using info which derived applet placed into NodeListApplet::cards for us void InkHUD::NodeListApplet::onRender(bool full) { // ================================ // Draw the standard applet header // ================================ drawHeader(getHeaderText()); // Ask derived applet for the title // Dimensions of the header int16_t headerDivY = getHeaderHeight() - 1; constexpr uint16_t padDivH = 2; // ======================== // Draw the main node list // ======================== // Leave a small gutter between long-name text and right-side card content constexpr uint8_t rightContentGap = 2; // Truncate with trailing "...", sized using the current font. auto ellipsizeToWidth = [this](std::string text, uint16_t maxWidth) { constexpr const char *ellipsis = "..."; const uint16_t ellipsisW = getTextWidth(ellipsis); uint16_t textW = getTextWidth(text); if (maxWidth == 0) return std::string(); if (textW <= maxWidth) return text; if (ellipsisW > maxWidth) return std::string(); while (!text.empty() && (textW + ellipsisW > maxWidth)) { text.pop_back(); textW = getTextWidth(text); } return text + ellipsis; }; // Y value (top) of the current card. Increases as we draw. uint16_t cardTopY = headerDivY + padDivH; // Clean up deleted nodes before drawing cards.erase( std::remove_if(cards.begin(), cards.end(), [](const CardInfo &c) { return nodeDB->getMeshNode(c.nodeNum) == nullptr; }), cards.end()); // -- Each node in list -- for (auto card = cards.begin(); card != cards.end(); ++card) { // Gather info // ======================================== const NodeNum &nodeNum = card->nodeNum; SignalStrength &signal = card->signal; std::string longName; // handled below std::string shortName; // handled below std::string distance; // handled below const uint8_t &hopsAway = card->hopsAway; meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeNum); // Skip deleted nodes if (!node) { continue; } // -- Shortname -- // Parse special chars in the short name // Use "?" if unknown if (node) shortName = parseShortName(node); else shortName = "?"; // -- Longname -- // Parse special chars in long name // Use node id if unknown if (node && node->has_user) longName = parse(node->user.long_name); // Found in nodeDB else { // Not found in nodeDB, show a hex nodeid instead longName = hexifyNodeNum(nodeNum); } // -- Distance -- if (card->distanceMeters != CardInfo::DISTANCE_UNKNOWN) distance = localizeDistance(card->distanceMeters); // Draw the info // ==================================== // Define two lines of text for the card // We will center our text on these lines uint16_t lineAY = cardTopY + (fontMedium.lineHeight() / 2); uint16_t lineBY = cardTopY + fontMedium.lineHeight() + (fontSmall.lineHeight() / 2); // Print the short name setFont(fontMedium); printAt(0, lineAY, shortName, LEFT, MIDDLE); // Right-side labels and long name are rendered in small font. setFont(fontSmall); uint16_t rightContentW = 0; // Bottom row right: distance. if (!distance.empty()) { rightContentW = std::max(rightContentW, getTextWidth(distance)); printAt(width() - 1, lineBY, distance, RIGHT, MIDDLE); } // Top row right: direct-link signal only. if (hopsAway == 0 && signal != SIGNAL_UNKNOWN) { uint16_t signalW = getTextWidth("Xkm"); // Indicator width tuned to a short right-side label uint16_t signalH = fontMedium.lineHeight() * 0.75; int16_t signalY = lineAY + (fontMedium.lineHeight() / 2) - (fontMedium.lineHeight() * 0.75); int16_t signalX = width() - signalW; rightContentW = std::max(rightContentW, signalW); drawSignalIndicator(signalX, signalY, signalW, signalH, signal); } else if (hopsAway != CardInfo::HOPS_UNKNOWN) { std::string hopString = to_string(hopsAway) + (hopsAway == 1 ? " Hop" : " Hops"); rightContentW = std::max(rightContentW, getTextWidth(hopString)); printAt(width() - 1, lineAY, hopString, RIGHT, MIDDLE); } // Give long names as much room as possible while still avoiding right side signal and hop space const uint16_t longNameMaxW = (rightContentW + rightContentGap < width()) ? (width() - rightContentW - rightContentGap) : 0; const std::string longNameShown = ellipsizeToWidth(longName, longNameMaxW); // Safety crop setCrop(0, cardTopY, longNameMaxW, cardH); printAt(0, lineBY, longNameShown, LEFT, MIDDLE); resetCrop(); // Draw separator between cards const int16_t separatorY = cardTopY + cardH - 1; if (separatorY < height() - 1 && (card + 1) != cards.end()) { for (int16_t xSep = 0; xSep < width(); xSep += 2) drawPixel(xSep, separatorY, BLACK); } // Prepare to draw the next card cardTopY += cardH; // Once we've run out of screen, stop drawing cards // Depending on tiles / rotation, this may be before we hit maxCards if (cardTopY > height()) break; } } // Draw element: a "mobile phone" style signal indicator // We will calculate values as floats, then "rasterize" at the last moment, relative to x and w, etc // This prevents issues with premature rounding when rendering tiny elements void InkHUD::NodeListApplet::drawSignalIndicator(int16_t x, int16_t y, uint16_t w, uint16_t h, SignalStrength strength) { /* +-------------------------------------------+ | | | | | barHeightRelative=1.0 | +--+ ^ | | gutterW +--+ | | | | | <--> +--+ | | | | | | | +--+ | | | | | | | | | | | | | | | | | | | | <-> +--+ +--+ +--+ +--+ v | | paddingW ^ | | paddingH | | | v | +-------------------------------------------+ */ constexpr float paddingW = 0.1; // Either side constexpr float paddingH = 0.1; // Above and below constexpr float gutterW = 0.1; // Between bars constexpr float barHRel[] = {0.3, 0.5, 0.7, 1.0}; // Heights of the signal bars, relative to the tallest constexpr uint8_t barCount = 4; // How many bars we draw. Reference only: changing value won't change the count. // Dynamically calculate the width of the bars, and height of the rightmost, relative to other dimensions float barW = (1.0 - (paddingW + ((barCount - 1) * gutterW) + paddingW)) / barCount; float barHMax = 1.0 - (paddingH + paddingH); // Draw signal bar rectangles, then placeholder lines once strength reached for (uint8_t i = 0; i < barCount; i++) { // Coords for this specific bar float barH = barHMax * barHRel[i]; float barX = paddingW + (i * (gutterW + barW)); float barY = paddingH + (barHMax - barH); // Rasterize to px coords at the last moment int16_t rX = (x + (w * barX)) + 0.5; int16_t rY = (y + (h * barY)) + 0.5; uint16_t rW = (w * barW) + 0.5; uint16_t rH = (h * barH) + 0.5; // Draw signal bars, until we are displaying the correct "signal strength", then just draw placeholder lines if (i <= strength) drawRect(rX, rY, rW, rH, BLACK); else { // Just draw a placeholder line float lineY = barY + barH; uint16_t rLineY = (y + (h * lineY)) + 0.5; // Rasterize drawLine(rX, rLineY, rX + rW - 1, rLineY, BLACK); } } } #endif ================================================ FILE: src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.h ================================================ #ifdef MESHTASTIC_INCLUDE_INKHUD /* Base class for Applets which display a list of nodes Used by the "Recents" and "Heard" applets. Possibly more in future? +-------------------------------+ | | | | SHRT . | | | | Long name 50km | | | | ABCD 2 Hops | | abcdedfghijk 30km | | | +-------------------------------+ */ #pragma once #include "configuration.h" #include "graphics/niche/InkHUD/Applet.h" #include "main.h" namespace NicheGraphics::InkHUD { class NodeListApplet : public Applet, public MeshModule { protected: // Info needed to draw a node card to the list // - generated each time we hear a node struct CardInfo { static constexpr uint8_t HOPS_UNKNOWN = -1; static constexpr uint32_t DISTANCE_UNKNOWN = -1; NodeNum nodeNum = 0; SignalStrength signal = SignalStrength::SIGNAL_UNKNOWN; uint32_t distanceMeters = DISTANCE_UNKNOWN; uint8_t hopsAway = HOPS_UNKNOWN; }; public: NodeListApplet(const char *name); void onRender(bool full) override; bool wantPacket(const meshtastic_MeshPacket *p) override; ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; protected: virtual void handleParsed(CardInfo c) = 0; // Tell derived applet that we heard a node virtual std::string getHeaderText() = 0; // Ask derived class what the applet's title should be uint8_t maxCards(); // Max number of cards which could ever fit on screen std::deque cards; // Cards to be rendered. Derived applet fills this. private: void drawSignalIndicator(int16_t x, int16_t y, uint16_t w, uint16_t h, SignalStrength signal); // Draw a "mobile phone" style signal indicator // Card Dimensions // - for rendering and for maxCards calc uint8_t cardMarginH = 1; // Gap between cards (minimal to fit more rows) uint16_t cardH = fontMedium.lineHeight() + fontSmall.lineHeight() + cardMarginH; // Height of card }; } // namespace NicheGraphics::InkHUD #endif ================================================ FILE: src/graphics/niche/InkHUD/Applets/Examples/BasicExample/BasicExampleApplet.cpp ================================================ #ifdef MESHTASTIC_INCLUDE_INKHUD #include "./BasicExampleApplet.h" using namespace NicheGraphics; // All drawing happens here // Our basic example doesn't do anything useful. It just passively prints some text. void InkHUD::BasicExampleApplet::onRender(bool full) { printAt(0, 0, "Hello, World!"); // If text might contain "special characters", is needs parsing first // This applies to data such as text-messages and and node names // std::string greeting = parse("Grüezi mitenand!"); // printAt(0, 0, greeting); } #endif ================================================ FILE: src/graphics/niche/InkHUD/Applets/Examples/BasicExample/BasicExampleApplet.h ================================================ #ifdef MESHTASTIC_INCLUDE_INKHUD /* A bare-minimum example of an InkHUD applet. Only prints Hello World. In variants//nicheGraphics.h: - include this .h file - add the following line of code: windowManager->addApplet("Basic", new InkHUD::BasicExampleApplet); */ #pragma once #include "configuration.h" #include "graphics/niche/InkHUD/Applet.h" namespace NicheGraphics::InkHUD { class BasicExampleApplet : public Applet { public: // You must have an onRender() method // All drawing happens here void onRender(bool full) override; }; } // namespace NicheGraphics::InkHUD #endif ================================================ FILE: src/graphics/niche/InkHUD/Applets/Examples/NewMsgExample/NewMsgExampleApplet.cpp ================================================ #ifdef MESHTASTIC_INCLUDE_INKHUD #include "./NewMsgExampleApplet.h" using namespace NicheGraphics; // We configured the Module API to call this method when we receive a new text message ProcessMessage InkHUD::NewMsgExampleApplet::handleReceived(const meshtastic_MeshPacket &mp) { // Abort if applet fully deactivated // Don't waste time: we wouldn't be rendered anyway if (!isActive()) return ProcessMessage::CONTINUE; // Check that this is an incoming message // Outgoing messages (sent by us) will also call handleReceived if (!isFromUs(&mp)) { // Store the sender's nodenum // We need to keep this information, so we can re-use it anytime render() is called haveMessage = true; fromWho = mp.from; // Tell InkHUD that we have something new to show on the screen requestUpdate(); } // Tell Module API to continue informing other firmware components about this message // We're not the only component which is interested in new text messages return ProcessMessage::CONTINUE; } // All drawing happens here // We can trigger a render by calling requestUpdate() // Render might be called by some external source // We should always be ready to draw void InkHUD::NewMsgExampleApplet::onRender(bool full) { printAt(0, 0, "Example: NewMsg", LEFT, TOP); // Print top-left corner of text at (0,0) int16_t centerX = X(0.5); // Same as width() / 2 int16_t centerY = Y(0.5); // Same as height() / 2 if (haveMessage) { printAt(centerX, centerY, "New Message", CENTER, BOTTOM); printAt(centerX, centerY, "From: " + hexifyNodeNum(fromWho), CENTER, TOP); } else { printAt(centerX, centerY, "No Message", CENTER, MIDDLE); // Place center of string at (centerX, centerY) } } #endif ================================================ FILE: src/graphics/niche/InkHUD/Applets/Examples/NewMsgExample/NewMsgExampleApplet.h ================================================ #ifdef MESHTASTIC_INCLUDE_INKHUD /* An example of an InkHUD applet. Tells us when a new text message arrives. This applet makes use of the Module API to detect new messages, which is a general part of the Meshtastic firmware, and not part of InkHUD. In variants//nicheGraphics.h: - include this .h file - add the following line of code: windowManager->addApplet("New Msg", new InkHUD::NewMsgExampleApplet); */ #pragma once #include "configuration.h" #include "graphics/niche/InkHUD/Applet.h" #include "mesh/SinglePortModule.h" namespace NicheGraphics::InkHUD { class NewMsgExampleApplet : public Applet, public SinglePortModule { public: // The MeshModule API requires us to have a constructor, to specify that we're interested in Text Messages. NewMsgExampleApplet() : SinglePortModule("NewMsgExampleApplet", meshtastic_PortNum_TEXT_MESSAGE_APP) {} // All drawing happens here void onRender(bool full) override; // Your applet might also want to use some of these // Useful for setting up or tidying up /* void onActivate(); // When started void onDeactivate(); // When stopped void onForeground(); // When shown by short-press void onBackground(); // When hidden by short-press */ private: // Called when we receive new text messages // Part of the MeshModule API ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; // Store info from handleReceived bool haveMessage = false; NodeNum fromWho = 0; }; } // namespace NicheGraphics::InkHUD #endif ================================================ FILE: src/graphics/niche/InkHUD/Applets/Examples/UserAppletInputExample/UserAppletInputExample.cpp ================================================ #ifdef MESHTASTIC_INCLUDE_INKHUD #include "./UserAppletInputExample.h" using namespace NicheGraphics; void InkHUD::UserAppletInputExampleApplet::onActivate() { setGrabbed(false); } void InkHUD::UserAppletInputExampleApplet::onRender(bool full) { drawHeader("Input Example"); uint16_t headerHeight = getHeaderHeight(); std::string buttonName; if (settings->joystick.enabled) buttonName = "joystick center button"; else buttonName = "user button"; std::string additional = " | Control is grabbed, long press " + buttonName + " to release controls"; if (!isGrabbed) additional = " | Control is released, long press " + buttonName + " to grab controls"; printWrapped(0, headerHeight, width(), "Last button: " + lastInput + additional); } void InkHUD::UserAppletInputExampleApplet::setGrabbed(bool grabbed) { isGrabbed = grabbed; setInputsSubscribed(BUTTON_SHORT | EXIT_SHORT | EXIT_LONG | NAV_UP | NAV_DOWN | NAV_LEFT | NAV_RIGHT, grabbed); // Enables/disables grabbing all inputs setInputsSubscribed(BUTTON_LONG, true); // Always grab this input } void InkHUD::UserAppletInputExampleApplet::onButtonShortPress() { lastInput = "BUTTON_SHORT"; requestUpdate(); } void InkHUD::UserAppletInputExampleApplet::onButtonLongPress() { lastInput = "BUTTON_LONG"; setGrabbed(!isGrabbed); requestUpdate(); } void InkHUD::UserAppletInputExampleApplet::onExitShort() { lastInput = "EXIT_SHORT"; requestUpdate(); } void InkHUD::UserAppletInputExampleApplet::onExitLong() { lastInput = "EXIT_LONG"; requestUpdate(); } void InkHUD::UserAppletInputExampleApplet::onNavUp() { lastInput = "NAV_UP"; requestUpdate(); } void InkHUD::UserAppletInputExampleApplet::onNavDown() { lastInput = "NAV_DOWN"; requestUpdate(); } void InkHUD::UserAppletInputExampleApplet::onNavLeft() { lastInput = "NAV_LEFT"; requestUpdate(); } void InkHUD::UserAppletInputExampleApplet::onNavRight() { lastInput = "NAV_RIGHT"; requestUpdate(); } #endif ================================================ FILE: src/graphics/niche/InkHUD/Applets/Examples/UserAppletInputExample/UserAppletInputExample.h ================================================ #ifdef MESHTASTIC_INCLUDE_INKHUD #pragma once #include "configuration.h" #include "graphics/niche/InkHUD/Applet.h" namespace NicheGraphics::InkHUD { class UserAppletInputExampleApplet : public Applet { public: void onActivate() override; void onRender(bool full) override; void onButtonShortPress() override; void onButtonLongPress() override; void onExitShort() override; void onExitLong() override; void onNavUp() override; void onNavDown() override; void onNavLeft() override; void onNavRight() override; private: std::string lastInput = "None"; bool isGrabbed = false; void setGrabbed(bool grabbed); }; } // namespace NicheGraphics::InkHUD #endif ================================================ FILE: src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.cpp ================================================ #ifdef MESHTASTIC_INCLUDE_INKHUD #include "./AlignStickApplet.h" using namespace NicheGraphics; InkHUD::AlignStickApplet::AlignStickApplet() { if (!settings->joystick.aligned) bringToForeground(); } void InkHUD::AlignStickApplet::onRender(bool full) { setFont(fontMedium); printAt(0, 0, "Align Joystick:"); setFont(fontSmall); std::string instructions = "Move joystick in the direction indicated"; printWrapped(0, fontMedium.lineHeight() * 1.5, width(), instructions); // Size of the region in which the joystick graphic should fit uint16_t joyXLimit = X(0.8); uint16_t contentH = fontMedium.lineHeight() * 1.5 + fontSmall.lineHeight() * 1; if (getTextWidth(instructions) > width()) contentH += fontSmall.lineHeight(); uint16_t freeY = height() - contentH - fontSmall.lineHeight() * 1.2; uint16_t joyYLimit = freeY * 0.8; // Use the shorter of the two uint16_t joyWidth = joyXLimit < joyYLimit ? joyXLimit : joyYLimit; // Center the joystick graphic uint16_t centerX = X(0.5); uint16_t centerY = contentH + freeY * 0.5; // Draw joystick graphic drawStick(centerX, centerY, joyWidth); setFont(fontSmall); printAt(X(0.5), Y(1.0) - fontSmall.lineHeight() * 0.2, "Long press to skip", CENTER, BOTTOM); } // Draw a scalable joystick graphic void InkHUD::AlignStickApplet::drawStick(uint16_t centerX, uint16_t centerY, uint16_t width) { if (width < 9) // too small to draw return; else if (width < 40) { // only draw up arrow uint16_t chamfer = width < 20 ? 1 : 2; // Draw filled up arrow drawDirection(centerX, centerY - width / 4, Direction::UP, width, chamfer, BLACK); } else { // large enough to draw the full thing uint16_t chamfer = width < 80 ? 1 : 2; uint16_t stroke = 3; // pixels uint16_t arrowW = width * 0.22; uint16_t hollowW = arrowW - stroke * 2; // Draw center circle fillCircle((int16_t)centerX, (int16_t)centerY, (int16_t)(width * 0.2), BLACK); fillCircle((int16_t)centerX, (int16_t)centerY, (int16_t)(width * 0.2) - stroke, WHITE); // Draw filled up arrow drawDirection(centerX, centerY - width / 2, Direction::UP, arrowW, chamfer, BLACK); // Draw down arrow drawDirection(centerX, centerY + width / 2, Direction::DOWN, arrowW, chamfer, BLACK); drawDirection(centerX, centerY + width / 2 - stroke, Direction::DOWN, hollowW, 0, WHITE); // Draw left arrow drawDirection(centerX - width / 2, centerY, Direction::LEFT, arrowW, chamfer, BLACK); drawDirection(centerX - width / 2 + stroke, centerY, Direction::LEFT, hollowW, 0, WHITE); // Draw right arrow drawDirection(centerX + width / 2, centerY, Direction::RIGHT, arrowW, chamfer, BLACK); drawDirection(centerX + width / 2 - stroke, centerY, Direction::RIGHT, hollowW, 0, WHITE); } } // Draw a scalable joystick direction arrow // a right-triangle with blunted tips /* _ <--point ^ / \ | / \ size / \ | / \ v |_________| */ void InkHUD::AlignStickApplet::drawDirection(uint16_t pointX, uint16_t pointY, Direction direction, uint16_t size, uint16_t chamfer, Color color) { uint16_t chamferW = chamfer * 2 + 1; uint16_t triangleW = size - chamferW; // Draw arrow switch (direction) { case Direction::UP: fillRect(pointX - chamfer, pointY, chamferW, triangleW, color); fillRect(pointX - chamfer - triangleW, pointY + triangleW, chamferW + triangleW * 2, chamferW, color); fillTriangle(pointX - chamfer, pointY, pointX - chamfer - triangleW, pointY + triangleW, pointX - chamfer, pointY + triangleW, color); fillTriangle(pointX + chamfer, pointY, pointX + chamfer + triangleW, pointY + triangleW, pointX + chamfer, pointY + triangleW, color); break; case Direction::DOWN: fillRect(pointX - chamfer, pointY - triangleW + 1, chamferW, triangleW, color); fillRect(pointX - chamfer - triangleW, pointY - size + 1, chamferW + triangleW * 2, chamferW, color); fillTriangle(pointX - chamfer, pointY, pointX - chamfer - triangleW, pointY - triangleW, pointX - chamfer, pointY - triangleW, color); fillTriangle(pointX + chamfer, pointY, pointX + chamfer + triangleW, pointY - triangleW, pointX + chamfer, pointY - triangleW, color); break; case Direction::LEFT: fillRect(pointX, pointY - chamfer, triangleW, chamferW, color); fillRect(pointX + triangleW, pointY - chamfer - triangleW, chamferW, chamferW + triangleW * 2, color); fillTriangle(pointX, pointY - chamfer, pointX + triangleW, pointY - chamfer - triangleW, pointX + triangleW, pointY - chamfer, color); fillTriangle(pointX, pointY + chamfer, pointX + triangleW, pointY + chamfer + triangleW, pointX + triangleW, pointY + chamfer, color); break; case Direction::RIGHT: fillRect(pointX - triangleW + 1, pointY - chamfer, triangleW, chamferW, color); fillRect(pointX - size + 1, pointY - chamfer - triangleW, chamferW, chamferW + triangleW * 2, color); fillTriangle(pointX, pointY - chamfer, pointX - triangleW, pointY - chamfer - triangleW, pointX - triangleW, pointY - chamfer, color); fillTriangle(pointX, pointY + chamfer, pointX - triangleW, pointY + chamfer + triangleW, pointX - triangleW, pointY + chamfer, color); break; } } void InkHUD::AlignStickApplet::onForeground() { // Prevent most other applets from requesting update, and skip their rendering entirely // Another system applet with a higher precedence can potentially ignore this SystemApplet::lockRendering = true; SystemApplet::lockRequests = true; handleInput = true; // Intercept the button input for our applet } void InkHUD::AlignStickApplet::onBackground() { // Allow normal update behavior to resume SystemApplet::lockRendering = false; SystemApplet::lockRequests = false; SystemApplet::handleInput = false; // Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background // Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case inkhud->forceUpdate(EInk::UpdateTypes::FULL, true); } void InkHUD::AlignStickApplet::onButtonLongPress() { sendToBackground(); } void InkHUD::AlignStickApplet::onExitLong() { sendToBackground(); } void InkHUD::AlignStickApplet::onNavUp() { settings->joystick.aligned = true; sendToBackground(); } void InkHUD::AlignStickApplet::onNavDown() { inkhud->rotateJoystick(2); // 180 deg settings->joystick.aligned = true; sendToBackground(); } void InkHUD::AlignStickApplet::onNavLeft() { inkhud->rotateJoystick(3); // 270 deg settings->joystick.aligned = true; sendToBackground(); } void InkHUD::AlignStickApplet::onNavRight() { inkhud->rotateJoystick(1); // 90 deg settings->joystick.aligned = true; sendToBackground(); } #endif ================================================ FILE: src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.h ================================================ #ifdef MESHTASTIC_INCLUDE_INKHUD /* System Applet for manually aligning the joystick with the screen should be run at startup if the joystick is enabled and not aligned to the screen */ #pragma once #include "configuration.h" #include "graphics/niche/InkHUD/SystemApplet.h" namespace NicheGraphics::InkHUD { class AlignStickApplet : public SystemApplet { public: AlignStickApplet(); void onRender(bool full) override; void onForeground() override; void onBackground() override; void onButtonLongPress() override; void onExitLong() override; void onNavUp() override; void onNavDown() override; void onNavLeft() override; void onNavRight() override; protected: enum Direction { UP, DOWN, LEFT, RIGHT, }; void drawStick(uint16_t centerX, uint16_t centerY, uint16_t width); void drawDirection(uint16_t pointX, uint16_t pointY, Direction direction, uint16_t size, uint16_t chamfer, Color color); }; } // namespace NicheGraphics::InkHUD #endif ================================================ FILE: src/graphics/niche/InkHUD/Applets/System/BatteryIcon/BatteryIconApplet.cpp ================================================ #ifdef MESHTASTIC_INCLUDE_INKHUD #include "./BatteryIconApplet.h" using namespace NicheGraphics; InkHUD::BatteryIconApplet::BatteryIconApplet() { alwaysRender = true; // render every time the screen is updated // Show at boot, if user has previously enabled the feature if (settings->optionalFeatures.batteryIcon) bringToForeground(); // Register to our have BatteryIconApplet::onPowerStatusUpdate method called when new power info is available // This happens whether or not the battery icon feature is enabled powerStatusObserver.observe(&powerStatus->onNewStatus); } // We handle power status' even when the feature is disabled, // so that we have up to date data ready if the feature is enabled later. // Otherwise could be 30s before new status update, with weird battery value displayed int InkHUD::BatteryIconApplet::onPowerStatusUpdate(const meshtastic::Status *status) { // System applets are always active assert(isActive()); // This method should only receive power statuses // If we get a different type of status, something has gone weird elsewhere assert(status->getStatusType() == STATUS_TYPE_POWER); const meshtastic::PowerStatus *pwrStatus = (const meshtastic::PowerStatus *)status; // Get the new state of charge %, and round to the nearest 10% uint8_t newSocRounded = ((pwrStatus->getBatteryChargePercent() + 5) / 10) * 10; // If rounded value has changed, trigger a display update // It's okay to requestUpdate before we store the new value, as the update won't run until next loop() // Don't trigger an update if the feature is disabled if (this->socRounded != newSocRounded && settings->optionalFeatures.batteryIcon) requestUpdate(); // Store the new value this->socRounded = newSocRounded; return 0; // Tell Observable to continue informing other observers } void InkHUD::BatteryIconApplet::onRender(bool full) { // Clear the region beneath the tile, including the border // Most applets are drawing onto an empty frame buffer and don't need to do this // We do need to do this with the battery though, as it is an "overlay" fillRect(0, 0, width(), height(), WHITE); // ===================== // Draw battery outline // ===================== // Positive terminal "bump" constexpr uint16_t bumpW = 2; const int16_t &bumpL = 1; const uint16_t bumpH = (height() - 2) / 2; const int16_t bumpT = (1 + ((height() - 2) / 2)) - (bumpH / 2); fillRect(bumpL, bumpT, bumpW, bumpH, BLACK); // Main body of battery const int16_t bodyL = 1 + bumpW; const int16_t &bodyT = 1; const int16_t &bodyH = height() - 2; // Handle top/bottom padding const int16_t bodyW = (width() - 1) - bumpW; // Handle 1px left pad drawRect(bodyL, bodyT, bodyW, bodyH, BLACK); // Erase join between bump and body drawLine(bodyL, bumpT, bodyL, bumpT + bumpH - 1, WHITE); // =================== // Draw battery level // =================== constexpr int16_t slicePad = 2; int16_t sliceL = bodyL + slicePad; const int16_t sliceT = bodyT + slicePad; const uint16_t sliceH = bodyH - (slicePad * 2); uint16_t sliceW = bodyW - (slicePad * 2); sliceW = (sliceW * socRounded) / 100; // Apply percentage sliceL += ((bodyW - (slicePad * 2)) - sliceW); // Shift slice to the battery's negative terminal, correcting drain direction hatchRegion(sliceL, sliceT, sliceW, sliceH, 2, BLACK); drawRect(sliceL, sliceT, sliceW, sliceH, BLACK); } #endif ================================================ FILE: src/graphics/niche/InkHUD/Applets/System/BatteryIcon/BatteryIconApplet.h ================================================ #ifdef MESHTASTIC_INCLUDE_INKHUD /* This applet floats top-left, giving a graphical representation of battery remaining It should be optional, enabled by the on-screen menu */ #pragma once #include "configuration.h" #include "graphics/niche/InkHUD/SystemApplet.h" #include "PowerStatus.h" namespace NicheGraphics::InkHUD { class BatteryIconApplet : public SystemApplet { public: BatteryIconApplet(); void onRender(bool full) override; int onPowerStatusUpdate(const meshtastic::Status *status); // Called when new info about battery is available private: // Get informed when new information about the battery is available (via onPowerStatusUpdate method) CallbackObserver powerStatusObserver = CallbackObserver(this, &BatteryIconApplet::onPowerStatusUpdate); uint8_t socRounded = 0; // Battery state of charge, rounded to nearest 10% }; } // namespace NicheGraphics::InkHUD #endif ================================================ FILE: src/graphics/niche/InkHUD/Applets/System/Keyboard/KeyboardApplet.cpp ================================================ #ifdef MESHTASTIC_INCLUDE_INKHUD #include "./KeyboardApplet.h" using namespace NicheGraphics; InkHUD::KeyboardApplet::KeyboardApplet() { // Calculate row widths for (uint8_t row = 0; row < KBD_ROWS; row++) { rowWidths[row] = 0; for (uint8_t col = 0; col < KBD_COLS; col++) rowWidths[row] += keyWidths[row * KBD_COLS + col]; } } void InkHUD::KeyboardApplet::onRender(bool full) { uint16_t em = fontSmall.lineHeight(); // 16 pt uint16_t keyH = Y(1.0) / KBD_ROWS; int16_t keyTopPadding = (keyH - fontSmall.lineHeight()) / 2; if (full) { // Draw full keyboard for (uint8_t row = 0; row < KBD_ROWS; row++) { // Calculate the remaining space to be used as padding int16_t keyXPadding = X(1.0) - ((rowWidths[row] * em) >> 4); // Draw keys uint16_t xPos = 0; for (uint8_t col = 0; col < KBD_COLS; col++) { Color fgcolor = BLACK; uint8_t index = row * KBD_COLS + col; uint16_t keyX = ((xPos * em) >> 4) + ((col * keyXPadding) / (KBD_COLS - 1)); uint16_t keyY = row * keyH; uint16_t keyW = (keyWidths[index] * em) >> 4; if (index == selectedKey) { fgcolor = WHITE; fillRect(keyX, keyY, keyW, keyH, BLACK); } drawKeyLabel(keyX, keyY + keyTopPadding, keyW, keys[index], fgcolor); xPos += keyWidths[index]; } } } else { // Only draw the difference if (selectedKey != prevSelectedKey) { // Draw previously selected key uint8_t row = prevSelectedKey / KBD_COLS; int16_t keyXPadding = X(1.0) - ((rowWidths[row] * em) >> 4); uint16_t xPos = 0; for (uint8_t i = prevSelectedKey - (prevSelectedKey % KBD_COLS); i < prevSelectedKey; i++) xPos += keyWidths[i]; uint16_t keyX = ((xPos * em) >> 4) + (((prevSelectedKey % KBD_COLS) * keyXPadding) / (KBD_COLS - 1)); uint16_t keyY = row * keyH; uint16_t keyW = (keyWidths[prevSelectedKey] * em) >> 4; fillRect(keyX, keyY, keyW, keyH, WHITE); drawKeyLabel(keyX, keyY + keyTopPadding, keyW, keys[prevSelectedKey], BLACK); // Draw newly selected key row = selectedKey / KBD_COLS; keyXPadding = X(1.0) - ((rowWidths[row] * em) >> 4); xPos = 0; for (uint8_t i = selectedKey - (selectedKey % KBD_COLS); i < selectedKey; i++) xPos += keyWidths[i]; keyX = ((xPos * em) >> 4) + (((selectedKey % KBD_COLS) * keyXPadding) / (KBD_COLS - 1)); keyY = row * keyH; keyW = (keyWidths[selectedKey] * em) >> 4; fillRect(keyX, keyY, keyW, keyH, BLACK); drawKeyLabel(keyX, keyY + keyTopPadding, keyW, keys[selectedKey], WHITE); } } prevSelectedKey = selectedKey; } // Draw the key label corresponding to the char // for most keys it draws the character itself // for ['\b', '\n', ' ', '\x1b'] it draws special glyphs void InkHUD::KeyboardApplet::drawKeyLabel(uint16_t left, uint16_t top, uint16_t width, char key, Color color) { if (key == '\b') { // Draw backspace glyph: 13 x 9 px /** * [][][][][][][][][] * [][] [] * [][] [] [] [] * [][] [] [] [] * [][] [] [] * [][] [] [] [] * [][] [] [] [] * [][] [] * [][][][][][][][][] */ const uint8_t bsBitmap[] = {0x0f, 0xf8, 0x18, 0x08, 0x32, 0x28, 0x61, 0x48, 0xc0, 0x88, 0x61, 0x48, 0x32, 0x28, 0x18, 0x08, 0x0f, 0xf8}; uint16_t leftPadding = (width - 13) >> 1; drawBitmap(left + leftPadding, top + 1, bsBitmap, 13, 9, color); } else if (key == '\n') { // Draw done glyph: 12 x 9 px /** * [][] * [][] * [][] * [][] * [][] * [][] [][] * [][] [][] * [][][] * [] */ const uint8_t doneBitmap[] = {0x00, 0x30, 0x00, 0x60, 0x00, 0xc0, 0x01, 0x80, 0x03, 0x00, 0xc6, 0x00, 0x6c, 0x00, 0x38, 0x00, 0x10, 0x00}; uint16_t leftPadding = (width - 12) >> 1; drawBitmap(left + leftPadding, top + 1, doneBitmap, 12, 9, color); } else if (key == ' ') { // Draw space glyph: 13 x 9 px /** * * * * * [] [] * [] [] * [][][][][][][][][][][][][] * * */ const uint8_t spaceBitmap[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x08, 0x80, 0x08, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x00}; uint16_t leftPadding = (width - 13) >> 1; drawBitmap(left + leftPadding, top + 1, spaceBitmap, 13, 9, color); } else if (key == '\x1b') { setTextColor(color); std::string keyText = "ESC"; uint16_t leftPadding = (width - getTextWidth(keyText)) >> 1; printAt(left + leftPadding, top, keyText); } else { setTextColor(color); if (key >= 0x61) key -= 32; // capitalize std::string keyText = std::string(1, key); uint16_t leftPadding = (width - getTextWidth(keyText)) >> 1; printAt(left + leftPadding, top, keyText); } } void InkHUD::KeyboardApplet::onForeground() { handleInput = true; // Intercept the button input for our applet // Select the first key selectedKey = 0; prevSelectedKey = 0; } void InkHUD::KeyboardApplet::onBackground() { handleInput = false; } void InkHUD::KeyboardApplet::onButtonShortPress() { char key = keys[selectedKey]; if (key == '\n') { inkhud->freeTextDone(); inkhud->closeKeyboard(); } else if (key == '\x1b') { inkhud->freeTextCancel(); inkhud->closeKeyboard(); } else { inkhud->freeText(key); } } void InkHUD::KeyboardApplet::onButtonLongPress() { char key = keys[selectedKey]; if (key == '\n') { inkhud->freeTextDone(); inkhud->closeKeyboard(); } else if (key == '\x1b') { inkhud->freeTextCancel(); inkhud->closeKeyboard(); } else { if (key >= 0x61) key -= 32; // capitalize inkhud->freeText(key); } } void InkHUD::KeyboardApplet::onExitShort() { inkhud->freeTextCancel(); inkhud->closeKeyboard(); } void InkHUD::KeyboardApplet::onExitLong() { inkhud->freeTextCancel(); inkhud->closeKeyboard(); } void InkHUD::KeyboardApplet::onNavUp() { if (selectedKey < KBD_COLS) // wrap selectedKey += KBD_COLS * (KBD_ROWS - 1); else // move 1 row back selectedKey -= KBD_COLS; // Request rendering over the previously drawn render requestUpdate(EInk::UpdateTypes::FAST, false); // Force an update to bypass lockRequests inkhud->forceUpdate(EInk::UpdateTypes::FAST); } void InkHUD::KeyboardApplet::onNavDown() { selectedKey += KBD_COLS; selectedKey %= (KBD_COLS * KBD_ROWS); // Request rendering over the previously drawn render requestUpdate(EInk::UpdateTypes::FAST, false); // Force an update to bypass lockRequests inkhud->forceUpdate(EInk::UpdateTypes::FAST); } void InkHUD::KeyboardApplet::onNavLeft() { if (selectedKey % KBD_COLS == 0) // wrap selectedKey += KBD_COLS - 1; else // move 1 column back selectedKey--; // Request rendering over the previously drawn render requestUpdate(EInk::UpdateTypes::FAST, false); // Force an update to bypass lockRequests inkhud->forceUpdate(EInk::UpdateTypes::FAST); } void InkHUD::KeyboardApplet::onNavRight() { if (selectedKey % KBD_COLS == KBD_COLS - 1) // wrap selectedKey -= KBD_COLS - 1; else // move 1 column forward selectedKey++; // Request rendering over the previously drawn render requestUpdate(EInk::UpdateTypes::FAST, false); // Force an update to bypass lockRequests inkhud->forceUpdate(EInk::UpdateTypes::FAST); } uint16_t InkHUD::KeyboardApplet::getKeyboardHeight() { const uint16_t keyH = fontSmall.lineHeight() * 1.2; return keyH * KBD_ROWS; } #endif ================================================ FILE: src/graphics/niche/InkHUD/Applets/System/Keyboard/KeyboardApplet.h ================================================ #ifdef MESHTASTIC_INCLUDE_INKHUD /* System Applet to render an on-screen keyboard */ #pragma once #include "configuration.h" #include "graphics/niche/InkHUD/InkHUD.h" #include "graphics/niche/InkHUD/SystemApplet.h" #include namespace NicheGraphics::InkHUD { class KeyboardApplet : public SystemApplet { public: KeyboardApplet(); void onRender(bool full) override; void onForeground() override; void onBackground() override; void onButtonShortPress() override; void onButtonLongPress() override; void onExitShort() override; void onExitLong() override; void onNavUp() override; void onNavDown() override; void onNavLeft() override; void onNavRight() override; static uint16_t getKeyboardHeight(); // used to set the keyboard tile height private: void drawKeyLabel(uint16_t left, uint16_t top, uint16_t width, char key, Color color); static const uint8_t KBD_COLS = 11; static const uint8_t KBD_ROWS = 4; const char keys[KBD_COLS * KBD_ROWS] = { '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '\b', // row 0 'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '\n', // row 1 'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', '!', ' ', // row 2 'z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '.', '?', '\x1b' // row 3 }; // This array represents the widths of each key in points // 16 pt = line height of the text const uint16_t keyWidths[KBD_COLS * KBD_ROWS] = { 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 24, // row 0 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 24, // row 1 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 24, // row 2 16, 16, 16, 16, 16, 16, 16, 10, 10, 12, 40 // row 3 }; uint16_t rowWidths[KBD_ROWS]; uint8_t selectedKey = 0; // selected key index uint8_t prevSelectedKey = 0; }; } // namespace NicheGraphics::InkHUD #endif ================================================ FILE: src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.cpp ================================================ #ifdef MESHTASTIC_INCLUDE_INKHUD #include "./LogoApplet.h" #include "mesh/NodeDB.h" using namespace NicheGraphics; InkHUD::LogoApplet::LogoApplet() : concurrency::OSThread("LogoApplet") { OSThread::setIntervalFromNow(8 * 1000UL); OSThread::enabled = true; // During onboarding, show the default short name as well as the version string // This behavior assists manufacturers during mass production, and should not be modified without good reason if (!settings->tips.safeShutdownSeen) { meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); fontTitle = fontMedium; textLeft = xstr(APP_VERSION_SHORT); textRight = parseShortName(ourNode); textTitle = "Meshtastic"; } else { fontTitle = fontSmall; textLeft = ""; textRight = ""; textTitle = xstr(APP_VERSION_SHORT); } bringToForeground(); // This is then drawn with a FULL refresh by Renderer::begin } void InkHUD::LogoApplet::onRender(bool full) { // Size of the region which the logo should "scale to fit" uint16_t logoWLimit = X(0.8); uint16_t logoHLimit = Y(0.5); // Get the max width and height we can manage within the region, while still maintaining aspect ratio uint16_t logoW = getLogoWidth(logoWLimit, logoHLimit); uint16_t logoH = getLogoHeight(logoWLimit, logoHLimit); // Where to place the center of the logo int16_t logoCX = X(0.5); int16_t logoCY = Y(0.5 - 0.05); // Invert colors if black-on-white // Used during shutdown, to report display health // Todo: handle this in InkHUD::Renderer instead if (inverted) { fillScreen(BLACK); setTextColor(WHITE); } #ifdef USERPREFS_OEM_IMAGE_DATA // Custom boot screen, if defined in userPrefs.jsonc // Only show the custom screen at startup // This allows us to draw the usual Meshtastic logo at shutdown // The effect is similar to the two-stage userPrefs boot screen used by BaseUI if (millis() < 10 * 1000UL) { // Draw the custom logo const uint8_t logo[] = USERPREFS_OEM_IMAGE_DATA; drawXBitmap(logoCX - (USERPREFS_OEM_IMAGE_WIDTH / 2), // Left logoCY - (USERPREFS_OEM_IMAGE_HEIGHT / 2), // Top logo, // XBM data USERPREFS_OEM_IMAGE_WIDTH, // Width USERPREFS_OEM_IMAGE_HEIGHT, // Height inverted ? WHITE : BLACK // Color ); // Select the largest font which will still comfortably fit the custom text setFont(fontLarge); if (getTextWidth(USERPREFS_OEM_TEXT) > 0.8 * width()) setFont(fontMedium); if (getTextWidth(USERPREFS_OEM_TEXT) > 0.8 * width()) setFont(fontSmall); // Draw custom text below logo int16_t logoB = logoCY + (USERPREFS_OEM_IMAGE_HEIGHT / 2); // Bottom of the logo printAt(X(0.5), logoB + Y(0.1), USERPREFS_OEM_TEXT, CENTER, TOP); // Don't draw the normal boot screen, we've already drawn our custom version return; } #endif drawLogo(logoCX, logoCY, logoW, logoH, inverted ? WHITE : BLACK); if (!textLeft.empty()) { setFont(fontSmall); printAt(0, 0, textLeft, LEFT, TOP); } if (!textRight.empty()) { setFont(fontSmall); printAt(X(1), 0, textRight, RIGHT, TOP); } if (!textTitle.empty()) { int16_t logoB = logoCY + (logoH / 2); // Bottom of the logo setFont(fontTitle); printAt(X(0.5), logoB + Y(0.1), textTitle, CENTER, TOP); } } void InkHUD::LogoApplet::onForeground() { SystemApplet::lockRendering = true; SystemApplet::lockRequests = true; SystemApplet::handleInput = true; // We don't actually use this input. Just blocking other applets from using it. } void InkHUD::LogoApplet::onBackground() { SystemApplet::lockRendering = false; SystemApplet::lockRequests = false; SystemApplet::handleInput = false; // Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background // Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case inkhud->forceUpdate(EInk::UpdateTypes::FULL, true); } // Begin displaying the screen which is shown at shutdown void InkHUD::LogoApplet::onShutdown() { bringToForeground(); textLeft = ""; textRight = ""; textTitle = "Shutting Down..."; fontTitle = fontSmall; // Draw a shutting down screen, twice. // Once white on black, once black on white. // Intention is to restore display health. inverted = true; inkhud->forceUpdate(Drivers::EInk::FULL, true, false); delay(1000); // Cooldown. Back to back updates aren't great for health. inverted = false; inkhud->forceUpdate(Drivers::EInk::FULL, true, false); delay(1000); // Cooldown // Prepare for the powered-off screen now // We can change these values because the initial "shutting down" screen has already rendered at this point meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); textLeft = ""; textRight = ""; textTitle = parseShortName(ourNode); fontTitle = fontMedium; // This is then drawn by InkHUD::Events::onShutdown, with a blocking FULL update, after InkHUD's flash write is complete } void InkHUD::LogoApplet::onApplyingChanges() { bringToForeground(); textLeft = ""; textRight = ""; textTitle = "Applying changes"; fontTitle = fontSmall; inkhud->forceUpdate(Drivers::EInk::FAST, false); } void InkHUD::LogoApplet::onReboot() { bringToForeground(); textLeft = ""; textRight = ""; textTitle = "Rebooting..."; fontTitle = fontSmall; inkhud->forceUpdate(Drivers::EInk::FULL, true, false); // Perform the update right now, waiting here until complete } int32_t InkHUD::LogoApplet::runOnce() { sendToBackground(); return OSThread::disable(); } #endif ================================================ FILE: src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.h ================================================ #ifdef MESHTASTIC_INCLUDE_INKHUD /* Shows the Meshtastic logo fullscreen, with accompanying text Used for boot and shutdown */ #pragma once #include "configuration.h" #include "concurrency/OSThread.h" #include "graphics/niche/InkHUD/SystemApplet.h" namespace NicheGraphics::InkHUD { class LogoApplet : public SystemApplet, public concurrency::OSThread { public: LogoApplet(); void onRender(bool full) override; void onForeground() override; void onBackground() override; void onShutdown() override; void onReboot() override; void onApplyingChanges(); protected: int32_t runOnce() override; std::string textLeft; std::string textRight; std::string textTitle; AppletFont fontTitle; bool inverted = false; // Invert colors. Used during shutdown, to restore display health. }; } // namespace NicheGraphics::InkHUD #endif ================================================ FILE: src/graphics/niche/InkHUD/Applets/System/Menu/MenuAction.h ================================================ #ifdef MESHTASTIC_INCLUDE_INKHUD /* Set of end-point actions for the Menu Applet Added as menu entries in MenuApplet::showPage Behaviors assigned in MenuApplet::execute */ #pragma once #include "configuration.h" namespace NicheGraphics::InkHUD { enum MenuAction { NO_ACTION, SEND_PING, FREE_TEXT, STORE_CANNEDMESSAGE_SELECTION, SEND_CANNEDMESSAGE, SHUTDOWN, NEXT_TILE, TOGGLE_BACKLIGHT, TOGGLE_GPS, ENABLE_BLUETOOTH, TOGGLE_APPLET, TOGGLE_AUTOSHOW_APPLET, SET_RECENTS, ROTATE, ALIGN_JOYSTICK, LAYOUT, TOGGLE_BATTERY_ICON, TOGGLE_NOTIFICATIONS, TOGGLE_INVERT_COLOR, TOGGLE_12H_CLOCK, // Regions SET_REGION_US, SET_REGION_EU_868, SET_REGION_EU_433, SET_REGION_CN, SET_REGION_JP, SET_REGION_ANZ, SET_REGION_KR, SET_REGION_TW, SET_REGION_RU, SET_REGION_IN, SET_REGION_NZ_865, SET_REGION_TH, SET_REGION_LORA_24, SET_REGION_UA_433, SET_REGION_UA_868, SET_REGION_MY_433, SET_REGION_MY_919, SET_REGION_SG_923, SET_REGION_PH_433, SET_REGION_PH_868, SET_REGION_PH_915, SET_REGION_ANZ_433, SET_REGION_KZ_433, SET_REGION_KZ_863, SET_REGION_NP_865, SET_REGION_BR_902, // Device Roles SET_ROLE_CLIENT, SET_ROLE_CLIENT_MUTE, SET_ROLE_ROUTER, SET_ROLE_REPEATER, // Presets SET_PRESET_LONG_SLOW, SET_PRESET_LONG_MODERATE, SET_PRESET_LONG_FAST, SET_PRESET_MEDIUM_SLOW, SET_PRESET_MEDIUM_FAST, SET_PRESET_SHORT_SLOW, SET_PRESET_SHORT_FAST, SET_PRESET_SHORT_TURBO, // Timezones SET_TZ_US_HAWAII, SET_TZ_US_ALASKA, SET_TZ_US_PACIFIC, SET_TZ_US_ARIZONA, SET_TZ_US_MOUNTAIN, SET_TZ_US_CENTRAL, SET_TZ_US_EASTERN, SET_TZ_BR_BRAZILIA, SET_TZ_UTC, SET_TZ_EU_WESTERN, SET_TZ_EU_CENTRAL, SET_TZ_EU_EASTERN, SET_TZ_ASIA_KOLKATA, SET_TZ_ASIA_HONG_KONG, SET_TZ_AU_AWST, SET_TZ_AU_ACST, SET_TZ_AU_AEST, SET_TZ_PACIFIC_NZ, // Power TOGGLE_POWER_SAVE, CALIBRATE_ADC, // Bluetooth TOGGLE_BLUETOOTH, TOGGLE_BLUETOOTH_PAIR_MODE, // Channel TOGGLE_CHANNEL_UPLINK, TOGGLE_CHANNEL_DOWNLINK, TOGGLE_CHANNEL_POSITION, SET_CHANNEL_PRECISION, // Display TOGGLE_DISPLAY_UNITS, // Network TOGGLE_WIFI, // Administration RESET_NODEDB_ALL, RESET_NODEDB_KEEP_FAVORITES, }; } // namespace NicheGraphics::InkHUD #endif ================================================ FILE: src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp ================================================ #ifdef MESHTASTIC_INCLUDE_INKHUD #include "./MenuApplet.h" #include "DisplayFormatters.h" #include "GPS.h" #include "MeshService.h" #include "RTC.h" #include "Router.h" #include "airtime.h" #include "main.h" #include "mesh/generated/meshtastic/deviceonly.pb.h" #include "power.h" #include #include #if defined(ARCH_ESP32) && HAS_WIFI #include "mesh/wifi/WiFiAPClient.h" #include #include #endif using namespace NicheGraphics; static constexpr uint8_t MENU_TIMEOUT_SEC = 60; // How many seconds before menu auto-closes // Options for the "Recents" menu // These are offered to users as possible values for settings.recentlyActiveSeconds static constexpr uint8_t RECENTS_OPTIONS_MINUTES[] = {2, 5, 10, 30, 60, 120}; struct PositionPrecisionOption { uint8_t value; // proto value const char *metric; const char *imperial; }; static constexpr PositionPrecisionOption POSITION_PRECISION_OPTIONS[] = { {32, "Precise", "Precise"}, {19, "50 m", "150 ft"}, {18, "90 m", "300 ft"}, {17, "200 m", "600 ft"}, {16, "350 m", "0.2 mi"}, {15, "700 m", "0.5 mi"}, {14, "1.5 km", "0.9 mi"}, {13, "2.9 km", "1.8 mi"}, {12, "5.8 km", "3.6 mi"}, {11, "12 km", "7.3 mi"}, {10, "23 km", "15 mi"}, }; InkHUD::MenuApplet::MenuApplet() : concurrency::OSThread("MenuApplet") { // No timer tasks at boot OSThread::disable(); // Note: don't get instance if we're not actually using the backlight, // or else you will unintentionally instantiate it if (settings->optionalMenuItems.backlight) { backlight = Drivers::LatchingBacklight::getInstance(); } // Initialize the Canned Message store // This is a shared nicheGraphics component // - handles loading & parsing the canned messages // - handles setting / getting of canned messages via apps (Client API Admin Messages) cm.store = CannedMessageStore::getInstance(); } void InkHUD::MenuApplet::onForeground() { // We do need this before we render, but we can optimize by just calculating it once now systemInfoPanelHeight = getSystemInfoPanelHeight(); // Force Region page ONLY when explicitly requested (one-shot) if (inkhud->forceRegionMenu) { inkhud->forceRegionMenu = false; // consume one-shot flag showPage(MenuPage::REGION); } else { showPage(MenuPage::ROOT); } // If device has a backlight which isn't controlled by aux button: // backlight on always when menu opens. // Courtesy to T-Echo users who removed the capacitive touch button if (settings->optionalMenuItems.backlight) { assert(backlight); if (!backlight->isOn()) backlight->peek(); } // Prevent user applets requesting update while menu is open // Handle button input with this applet SystemApplet::lockRequests = true; SystemApplet::handleInput = true; // Begin the auto-close timeout OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); OSThread::enabled = true; freeTextMode = false; // Upgrade the refresh to FAST, for guaranteed responsiveness inkhud->forceUpdate(EInk::UpdateTypes::FAST); } void InkHUD::MenuApplet::onBackground() { // Discard any data we generated while selecting a canned message // Frees heap mem freeCannedMessageResources(); // If device has a backlight which isn't controlled by aux button: // Item in options submenu allows keeping backlight on after menu is closed // If this item is deselected we will turn backlight off again, now that menu is closing if (settings->optionalMenuItems.backlight) { assert(backlight); if (!backlight->isLatched()) backlight->off(); } // Stop the auto-timeout OSThread::disable(); // Resume normal rendering and button behavior of user applets SystemApplet::lockRequests = false; SystemApplet::handleInput = false; handleFreeText = false; // Restore the user applet whose tile we borrowed if (borrowedTileOwner) borrowedTileOwner->bringToForeground(); Tile *t = getTile(); t->assignApplet(borrowedTileOwner); // Break our link with the tile, (and relink it with real owner, if it had one) borrowedTileOwner = nullptr; // Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background // We're only updating here to upgrade from UNSPECIFIED to FAST, to ensure responsiveness when exiting menu inkhud->forceUpdate(EInk::UpdateTypes::FAST); } // Open the menu // Parameter specifies which user-tile the menu will use // The user applet originally on this tile will be restored when the menu closes void InkHUD::MenuApplet::show(Tile *t) { // Remember who *really* owns this tile borrowedTileOwner = t->getAssignedApplet(); // Hide the owner, if it is a valid applet if (borrowedTileOwner) borrowedTileOwner->sendToBackground(); // Break the owner's link with tile // Relink it to menu applet t->assignApplet(this); // Show menu bringToForeground(); } // Auto-exit the menu applet after a period of inactivity // The values shown on the root menu are only a snapshot: they are not re-rendered while the menu remains open. // By exiting the menu, we prevent users mistakenly believing that the data will update. int32_t InkHUD::MenuApplet::runOnce() { // runOnce's interval is pushed back when a button is pressed // If we do actually run, it means no button input occurred within MENU_TIMEOUT_SEC, // so we close the menu. showPage(EXIT); // Timer should disable after firing // This is redundant, as onBackground() will also disable return OSThread::disable(); } static void applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode region) { if (config.lora.region == region) return; config.lora.region = region; auto changes = SEGMENT_CONFIG; #if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN || MESHTASTIC_EXCLUDE_PKI) if (crypto) { crypto->ensurePkiKeys(config.security, owner); } #endif config.lora.tx_enabled = true; initRegion(); if (myRegion && myRegion->dutyCycle < 100) { config.lora.ignore_mqtt = true; } if (strncmp(moduleConfig.mqtt.root, default_mqtt_root, strlen(default_mqtt_root)) == 0) { sprintf(moduleConfig.mqtt.root, "%s/%s", default_mqtt_root, myRegion->name); changes |= SEGMENT_MODULECONFIG; } // Notify UI that changes are being applied InkHUD::InkHUD::getInstance()->notifyApplyingChanges(); service->reloadConfig(changes); rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; } static void applyDeviceRole(meshtastic_Config_DeviceConfig_Role role) { if (config.device.role == role) return; config.device.role = role; nodeDB->saveToDisk(SEGMENT_CONFIG); service->reloadConfig(SEGMENT_CONFIG); // Notify UI that changes are being applied InkHUD::InkHUD::getInstance()->notifyApplyingChanges(); rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; } static void applyLoRaPreset(meshtastic_Config_LoRaConfig_ModemPreset preset) { if (config.lora.modem_preset == preset) return; config.lora.use_preset = true; config.lora.modem_preset = preset; nodeDB->saveToDisk(SEGMENT_CONFIG); service->reloadConfig(SEGMENT_CONFIG); // Notify UI that changes are being applied InkHUD::InkHUD::getInstance()->notifyApplyingChanges(); rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; } static const char *getTimezoneLabelFromValue(const char *tzdef) { if (!tzdef || !*tzdef) return "Unset"; // Must match TIMEZONE menu entries if (strcmp(tzdef, "HST10") == 0) return "US/Hawaii"; if (strcmp(tzdef, "AKST9AKDT,M3.2.0,M11.1.0") == 0) return "US/Alaska"; if (strcmp(tzdef, "PST8PDT,M3.2.0,M11.1.0") == 0) return "US/Pacific"; if (strcmp(tzdef, "MST7") == 0) return "US/Arizona"; if (strcmp(tzdef, "MST7MDT,M3.2.0,M11.1.0") == 0) return "US/Mountain"; if (strcmp(tzdef, "CST6CDT,M3.2.0,M11.1.0") == 0) return "US/Central"; if (strcmp(tzdef, "EST5EDT,M3.2.0,M11.1.0") == 0) return "US/Eastern"; if (strcmp(tzdef, "BRT3") == 0) return "BR/Brasilia"; if (strcmp(tzdef, "UTC0") == 0) return "UTC"; if (strcmp(tzdef, "GMT0BST,M3.5.0/1,M10.5.0") == 0) return "EU/Western"; if (strcmp(tzdef, "CET-1CEST,M3.5.0,M10.5.0/3") == 0) return "EU/Central"; if (strcmp(tzdef, "EET-2EEST,M3.5.0/3,M10.5.0/4") == 0) return "EU/Eastern"; if (strcmp(tzdef, "IST-5:30") == 0) return "Asia/Kolkata"; if (strcmp(tzdef, "HKT-8") == 0) return "Asia/Hong Kong"; if (strcmp(tzdef, "AWST-8") == 0) return "AU/AWST"; if (strcmp(tzdef, "ACST-9:30ACDT,M10.1.0,M4.1.0/3") == 0) return "AU/ACST"; if (strcmp(tzdef, "AEST-10AEDT,M10.1.0,M4.1.0/3") == 0) return "AU/AEST"; if (strcmp(tzdef, "NZST-12NZDT,M9.5.0,M4.1.0/3") == 0) return "Pacific/NZ"; return tzdef; // fallback for unknown/custom values } static void applyTimezone(const char *tz) { if (!tz || strcmp(config.device.tzdef, tz) == 0) return; strncpy(config.device.tzdef, tz, sizeof(config.device.tzdef)); config.device.tzdef[sizeof(config.device.tzdef) - 1] = '\0'; setenv("TZ", config.device.tzdef, 1); nodeDB->saveToDisk(SEGMENT_CONFIG); service->reloadConfig(SEGMENT_CONFIG); } // Perform action for a menu item, then change page // Behaviors for MenuActions are defined here void InkHUD::MenuApplet::execute(MenuItem item) { // Perform an action // ------------------ switch (item.action) { // Open a submenu without performing any action // Also handles exit case NO_ACTION: if (currentPage == MenuPage::NODE_CONFIG_CHANNELS && item.nextPage == MenuPage::NODE_CONFIG_CHANNEL_DETAIL) { // cursor - 1 because index 0 is "Back" selectedChannelIndex = cursor - 1; } break; case NEXT_TILE: inkhud->nextTile(); // Unselect menu item after tile change cursorShown = false; cursor = 0; break; case SEND_PING: service->refreshLocalMeshNode(); service->trySendPosition(NODENUM_BROADCAST, true); // Force the next refresh to use FULL, to protect the display, as some users will probably spam this button inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FULL); break; case FREE_TEXT: OSThread::enabled = false; handleFreeText = true; cm.freeTextItem.rawText.erase(); // clear the previous freetext message freeTextMode = true; // render input field instead of normal menu // Open the on-screen keyboard only for full joystick devices if (settings->joystick.enabled && !inkhud->twoWayRocker) inkhud->openKeyboard(); break; case STORE_CANNEDMESSAGE_SELECTION: if (!settings->joystick.enabled || inkhud->twoWayRocker) cm.selectedMessageItem = &cm.messageItems.at(cursor - 1); // Minus one: offset for the initial "Send Ping" entry else cm.selectedMessageItem = &cm.messageItems.at(cursor - 2); // Minus two: offset for the "Send Ping" and free text entry break; case SEND_CANNEDMESSAGE: cm.selectedRecipientItem = &cm.recipientItems.at(cursor); // send selected message sendText(cm.selectedRecipientItem->dest, cm.selectedRecipientItem->channelIndex, cm.selectedMessageItem->rawText.c_str()); inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FULL); // Next refresh should be FULL. Lots of button pressing to get here break; case ROTATE: inkhud->rotate(); break; case ALIGN_JOYSTICK: inkhud->openAlignStick(); break; case LAYOUT: // Todo: smarter incrementing of tile count settings->userTiles.count++; if (settings->userTiles.count == 3) // Skip 3 tiles: not done yet settings->userTiles.count++; if (settings->userTiles.count > settings->userTiles.maxCount) // Loop around if tile count now too high settings->userTiles.count = 1; inkhud->updateLayout(); break; case TOGGLE_APPLET: if (item.checkState) { *item.checkState = !(*item.checkState); inkhud->updateAppletSelection(); } break; case TOGGLE_AUTOSHOW_APPLET: // Toggle settings.userApplets.autoshow[] value, via MenuItem::checkState pointer set in populateAutoshowPage() if (item.checkState) { *item.checkState = !(*item.checkState); } break; case TOGGLE_NOTIFICATIONS: if (item.checkState) { *item.checkState = !(*item.checkState); } break; case TOGGLE_INVERT_COLOR: if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) config.display.displaymode = meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT; else config.display.displaymode = meshtastic_Config_DisplayConfig_DisplayMode_INVERTED; nodeDB->saveToDisk(SEGMENT_CONFIG); break; case SET_RECENTS: { // cursor - 1 because index 0 is "Back" const uint8_t index = cursor - 1; constexpr uint8_t optionCount = sizeof(RECENTS_OPTIONS_MINUTES) / sizeof(RECENTS_OPTIONS_MINUTES[0]); assert(index < optionCount); settings->recentlyActiveSeconds = RECENTS_OPTIONS_MINUTES[index] * 60; break; } case SHUTDOWN: LOG_INFO("Shutting down from menu"); shutdownAtMsec = millis(); // Menu is then sent to background via onShutdown break; case TOGGLE_BATTERY_ICON: inkhud->toggleBatteryIcon(); break; case TOGGLE_BACKLIGHT: // Note: backlight is already on in this situation // We're marking that it should *remain* on once menu closes assert(backlight); if (backlight->isLatched()) backlight->off(); else backlight->latch(); break; case TOGGLE_12H_CLOCK: config.display.use_12h_clock = !config.display.use_12h_clock; nodeDB->saveToDisk(SEGMENT_CONFIG); break; case TOGGLE_GPS: #if !MESHTASTIC_EXCLUDE_GPS && HAS_GPS if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_DISABLED) { config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_ENABLED; } else if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) { config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_DISABLED; } else { // NOT_PRESENT do nothing break; } nodeDB->saveToDisk(SEGMENT_CONFIG); service->reloadConfig(SEGMENT_CONFIG); #endif break; case ENABLE_BLUETOOTH: // This helps users recover from a bad wifi config LOG_INFO("Enabling Bluetooth"); config.network.wifi_enabled = false; config.bluetooth.enabled = true; nodeDB->saveToDisk(SEGMENT_CONFIG); InkHUD::InkHUD::getInstance()->notifyApplyingChanges(); rebootAtMsec = millis() + 2000; break; // Power / Network (ESP32-only) #if defined(ARCH_ESP32) case TOGGLE_POWER_SAVE: config.power.is_power_saving = !config.power.is_power_saving; nodeDB->saveToDisk(SEGMENT_CONFIG); InkHUD::InkHUD::getInstance()->notifyApplyingChanges(); rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; break; case TOGGLE_WIFI: config.network.wifi_enabled = !config.network.wifi_enabled; if (config.network.wifi_enabled) { // Switch behavior: WiFi ON forces Bluetooth OFF config.bluetooth.enabled = false; } nodeDB->saveToDisk(SEGMENT_CONFIG); InkHUD::InkHUD::getInstance()->notifyApplyingChanges(); rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; break; #endif // ADC Calibration case CALIBRATE_ADC: { // Read current measured voltage float measuredV = powerStatus->getBatteryVoltageMv() / 1000.0f; // Sanity check if (measuredV < 3.0f || measuredV > 4.5f) { LOG_WARN("ADC calibration aborted, unreasonable voltage: %.2fV", measuredV); break; } // Determine the base multiplier currently in effect float baseMult = 0.0f; if (config.power.adc_multiplier_override > 0.0f) { baseMult = config.power.adc_multiplier_override; } #ifdef ADC_MULTIPLIER else { baseMult = ADC_MULTIPLIER; } #endif if (baseMult <= 0.0f) { LOG_WARN("ADC calibration failed: no base multiplier"); break; } // Target voltage considered 100% by UI constexpr float TARGET_VOLTAGE = 4.19f; // Calculate new multiplier float newMult = baseMult * (TARGET_VOLTAGE / measuredV); config.power.adc_multiplier_override = newMult; nodeDB->saveToDisk(SEGMENT_CONFIG); LOG_INFO("ADC calibrated: measured=%.3fV base=%.4f new=%.4f", measuredV, baseMult, newMult); break; } // Display case TOGGLE_DISPLAY_UNITS: if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) config.display.units = meshtastic_Config_DisplayConfig_DisplayUnits_METRIC; else config.display.units = meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL; nodeDB->saveToDisk(SEGMENT_CONFIG); break; // Bluetooth case TOGGLE_BLUETOOTH: config.bluetooth.enabled = !config.bluetooth.enabled; if (config.bluetooth.enabled) { // Switch behavior: Bluetooth ON forces WiFi OFF config.network.wifi_enabled = false; } nodeDB->saveToDisk(SEGMENT_CONFIG); InkHUD::InkHUD::getInstance()->notifyApplyingChanges(); rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; break; case TOGGLE_BLUETOOTH_PAIR_MODE: config.bluetooth.fixed_pin = !config.bluetooth.fixed_pin; nodeDB->saveToDisk(SEGMENT_CONFIG); break; // Regions case SET_REGION_US: applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_US); break; case SET_REGION_EU_868: applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868); break; case SET_REGION_EU_433: applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_433); break; case SET_REGION_CN: applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_CN); break; case SET_REGION_JP: applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_JP); break; case SET_REGION_ANZ: applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_ANZ); break; case SET_REGION_KR: applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_KR); break; case SET_REGION_TW: applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_TW); break; case SET_REGION_RU: applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_RU); break; case SET_REGION_IN: applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_IN); break; case SET_REGION_NZ_865: applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_NZ_865); break; case SET_REGION_TH: applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_TH); break; case SET_REGION_LORA_24: applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_LORA_24); break; case SET_REGION_UA_433: applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_UA_433); break; case SET_REGION_UA_868: applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_UA_868); break; case SET_REGION_MY_433: applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_MY_433); break; case SET_REGION_MY_919: applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_MY_919); break; case SET_REGION_SG_923: applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_SG_923); break; case SET_REGION_PH_433: applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_PH_433); break; case SET_REGION_PH_868: applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_PH_868); break; case SET_REGION_PH_915: applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_PH_915); break; case SET_REGION_ANZ_433: applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_ANZ_433); break; case SET_REGION_KZ_433: applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_KZ_433); break; case SET_REGION_KZ_863: applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_KZ_863); break; case SET_REGION_NP_865: applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_NP_865); break; case SET_REGION_BR_902: applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_BR_902); break; // Roles case SET_ROLE_CLIENT: applyDeviceRole(meshtastic_Config_DeviceConfig_Role_CLIENT); break; case SET_ROLE_CLIENT_MUTE: applyDeviceRole(meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE); break; case SET_ROLE_ROUTER: applyDeviceRole(meshtastic_Config_DeviceConfig_Role_ROUTER); break; case SET_ROLE_REPEATER: applyDeviceRole(meshtastic_Config_DeviceConfig_Role_REPEATER); break; // Presets case SET_PRESET_LONG_SLOW: applyLoRaPreset(meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW); break; case SET_PRESET_LONG_MODERATE: applyLoRaPreset(meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE); break; case SET_PRESET_LONG_FAST: applyLoRaPreset(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST); break; case SET_PRESET_MEDIUM_SLOW: applyLoRaPreset(meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW); break; case SET_PRESET_MEDIUM_FAST: applyLoRaPreset(meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST); break; case SET_PRESET_SHORT_SLOW: applyLoRaPreset(meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW); break; case SET_PRESET_SHORT_FAST: applyLoRaPreset(meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST); break; case SET_PRESET_SHORT_TURBO: applyLoRaPreset(meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO); break; // Timezones case SET_TZ_US_HAWAII: applyTimezone("HST10"); break; case SET_TZ_US_ALASKA: applyTimezone("AKST9AKDT,M3.2.0,M11.1.0"); break; case SET_TZ_US_PACIFIC: applyTimezone("PST8PDT,M3.2.0,M11.1.0"); break; case SET_TZ_US_ARIZONA: applyTimezone("MST7"); break; case SET_TZ_US_MOUNTAIN: applyTimezone("MST7MDT,M3.2.0,M11.1.0"); break; case SET_TZ_US_CENTRAL: applyTimezone("CST6CDT,M3.2.0,M11.1.0"); break; case SET_TZ_US_EASTERN: applyTimezone("EST5EDT,M3.2.0,M11.1.0"); break; case SET_TZ_BR_BRAZILIA: applyTimezone("BRT3"); break; case SET_TZ_UTC: applyTimezone("UTC0"); break; case SET_TZ_EU_WESTERN: applyTimezone("GMT0BST,M3.5.0/1,M10.5.0"); break; case SET_TZ_EU_CENTRAL: applyTimezone("CET-1CEST,M3.5.0,M10.5.0/3"); break; case SET_TZ_EU_EASTERN: applyTimezone("EET-2EEST,M3.5.0/3,M10.5.0/4"); break; case SET_TZ_ASIA_KOLKATA: applyTimezone("IST-5:30"); break; case SET_TZ_ASIA_HONG_KONG: applyTimezone("HKT-8"); break; case SET_TZ_AU_AWST: applyTimezone("AWST-8"); break; case SET_TZ_AU_ACST: applyTimezone("ACST-9:30ACDT,M10.1.0,M4.1.0/3"); break; case SET_TZ_AU_AEST: applyTimezone("AEST-10AEDT,M10.1.0,M4.1.0/3"); break; case SET_TZ_PACIFIC_NZ: applyTimezone("NZST-12NZDT,M9.5.0,M4.1.0/3"); break; // Channels case TOGGLE_CHANNEL_UPLINK: { auto &ch = channels.getByIndex(selectedChannelIndex); ch.settings.uplink_enabled = !ch.settings.uplink_enabled; nodeDB->saveToDisk(SEGMENT_CHANNELS); service->reloadConfig(SEGMENT_CHANNELS); break; } case TOGGLE_CHANNEL_DOWNLINK: { auto &ch = channels.getByIndex(selectedChannelIndex); ch.settings.downlink_enabled = !ch.settings.downlink_enabled; nodeDB->saveToDisk(SEGMENT_CHANNELS); service->reloadConfig(SEGMENT_CHANNELS); break; } case TOGGLE_CHANNEL_POSITION: { auto &ch = channels.getByIndex(selectedChannelIndex); if (!ch.settings.has_module_settings) ch.settings.has_module_settings = true; if (ch.settings.module_settings.position_precision > 0) ch.settings.module_settings.position_precision = 0; else ch.settings.module_settings.position_precision = 13; // default nodeDB->saveToDisk(SEGMENT_CHANNELS); service->reloadConfig(SEGMENT_CHANNELS); break; } case SET_CHANNEL_PRECISION: { auto &ch = channels.getByIndex(selectedChannelIndex); if (!ch.settings.has_module_settings) ch.settings.has_module_settings = true; // Cursor - 1 because of "Back" uint8_t index = cursor - 1; constexpr uint8_t optionCount = sizeof(POSITION_PRECISION_OPTIONS) / sizeof(POSITION_PRECISION_OPTIONS[0]); if (index < optionCount) { ch.settings.module_settings.position_precision = POSITION_PRECISION_OPTIONS[index].value; } nodeDB->saveToDisk(SEGMENT_CHANNELS); service->reloadConfig(SEGMENT_CHANNELS); break; } case RESET_NODEDB_ALL: InkHUD::getInstance()->notifyApplyingChanges(); nodeDB->resetNodes(); rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; break; case RESET_NODEDB_KEEP_FAVORITES: InkHUD::getInstance()->notifyApplyingChanges(); nodeDB->resetNodes(1); rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; break; default: LOG_WARN("Action not implemented"); } // Move to next page, as defined for the MenuItem showPage(item.nextPage); } // Display a new page of MenuItems // May reload same page, or exit menu applet entirely // Fills the MenuApplet::items vector void InkHUD::MenuApplet::showPage(MenuPage page) { items.clear(); items.shrink_to_fit(); nodeConfigLabels.clear(); switch (page) { case ROOT: previousPage = MenuPage::EXIT; // Optional: next applet if (settings->optionalMenuItems.nextTile && settings->userTiles.count > 1) items.push_back(MenuItem("Next Tile", MenuAction::NEXT_TILE, MenuPage::ROOT)); // Only if multiple applets shown items.push_back(MenuItem("Send", MenuPage::SEND)); items.push_back(MenuItem("Options", MenuPage::OPTIONS)); // items.push_back(MenuItem("Display Off", MenuPage::EXIT)); // TODO items.push_back(MenuItem("Node Config", MenuPage::NODE_CONFIG)); items.push_back(MenuItem("Save & Shut Down", MenuAction::SHUTDOWN)); items.push_back(MenuItem("Exit", MenuPage::EXIT)); break; case SEND: populateSendPage(); previousPage = MenuPage::ROOT; break; case CANNEDMESSAGE_RECIPIENT: populateRecipientPage(); previousPage = MenuPage::SEND; break; case OPTIONS: previousPage = MenuPage::ROOT; items.push_back(MenuItem("Back", previousPage)); // Optional: backlight if (settings->optionalMenuItems.backlight) items.push_back(MenuItem(backlight->isLatched() ? "Backlight Off" : "Keep Backlight On", // Label MenuAction::TOGGLE_BACKLIGHT, // Action MenuPage::EXIT // Exit once complete )); // Options Toggles items.push_back(MenuItem("Applets", MenuPage::APPLETS)); items.push_back(MenuItem("Auto-show", MenuPage::AUTOSHOW)); items.push_back(MenuItem("Recents Duration", MenuPage::RECENTS)); if (settings->userTiles.maxCount > 1) items.push_back(MenuItem("Layout", MenuAction::LAYOUT, MenuPage::OPTIONS)); items.push_back(MenuItem("Rotate", MenuAction::ROTATE, MenuPage::OPTIONS)); if (settings->joystick.enabled && !inkhud->twoWayRocker) items.push_back(MenuItem("Align Joystick", MenuAction::ALIGN_JOYSTICK, MenuPage::EXIT)); items.push_back(MenuItem("Notifications", MenuAction::TOGGLE_NOTIFICATIONS, MenuPage::OPTIONS, &settings->optionalFeatures.notifications)); items.push_back(MenuItem("Battery Icon", MenuAction::TOGGLE_BATTERY_ICON, MenuPage::OPTIONS, &settings->optionalFeatures.batteryIcon)); invertedColors = (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED); items.push_back(MenuItem("Invert Color", MenuAction::TOGGLE_INVERT_COLOR, MenuPage::OPTIONS, &invertedColors)); items.push_back(MenuItem("Exit", MenuPage::EXIT)); break; case APPLETS: previousPage = MenuPage::OPTIONS; populateAppletPage(); // must be first items.insert(items.begin(), MenuItem("Back", previousPage)); items.push_back(MenuItem("Exit", MenuPage::EXIT)); break; case AUTOSHOW: previousPage = MenuPage::OPTIONS; populateAutoshowPage(); // must be first items.insert(items.begin(), MenuItem("Back", previousPage)); items.push_back(MenuItem("Exit", MenuPage::EXIT)); break; case RECENTS: previousPage = MenuPage::OPTIONS; populateRecentsPage(); // builds only the options items.insert(items.begin(), MenuItem("Back", previousPage)); items.push_back(MenuItem("Exit", MenuPage::EXIT)); break; case NODE_CONFIG: previousPage = MenuPage::ROOT; items.push_back(MenuItem("Back", previousPage)); // Radio Config Section items.push_back(MenuItem::Header("Radio Config")); items.push_back(MenuItem("LoRa", MenuPage::NODE_CONFIG_LORA)); items.push_back(MenuItem("Channel", MenuPage::NODE_CONFIG_CHANNELS)); // Device Config Section items.push_back(MenuItem::Header("Device Config")); items.push_back(MenuItem("Device", MenuPage::NODE_CONFIG_DEVICE)); items.push_back(MenuItem("Position", MenuPage::NODE_CONFIG_POSITION)); items.push_back(MenuItem("Power", MenuPage::NODE_CONFIG_POWER)); #if defined(ARCH_ESP32) items.push_back(MenuItem("Network", MenuPage::NODE_CONFIG_NETWORK)); #endif items.push_back(MenuItem("Display", MenuPage::NODE_CONFIG_DISPLAY)); items.push_back(MenuItem("Bluetooth", MenuPage::NODE_CONFIG_BLUETOOTH)); // Administration Section items.push_back(MenuItem::Header("Administration")); items.push_back(MenuItem("Reset NodeDB", MenuPage::NODE_CONFIG_ADMIN_RESET)); // Exit items.push_back(MenuItem("Exit", MenuPage::EXIT)); break; case NODE_CONFIG_DEVICE: { previousPage = MenuPage::NODE_CONFIG; items.push_back(MenuItem("Back", previousPage)); const char *role = DisplayFormatters::getDeviceRole(config.device.role); nodeConfigLabels.emplace_back("Role: " + std::string(role)); items.push_back(MenuItem(nodeConfigLabels.back().c_str(), MenuAction::NO_ACTION, MenuPage::NODE_CONFIG_DEVICE_ROLE)); const char *tzLabel = getTimezoneLabelFromValue(config.device.tzdef); nodeConfigLabels.emplace_back("Timezone: " + std::string(tzLabel)); items.push_back(MenuItem(nodeConfigLabels.back().c_str(), MenuAction::NO_ACTION, MenuPage::TIMEZONE)); items.push_back(MenuItem("Exit", MenuPage::EXIT)); break; } case NODE_CONFIG_POSITION: { previousPage = MenuPage::NODE_CONFIG; items.push_back(MenuItem("Back", previousPage)); #if !MESHTASTIC_EXCLUDE_GPS && HAS_GPS const auto mode = config.position.gps_mode; if (mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT) { items.push_back(MenuItem("GPS None", MenuAction::NO_ACTION, MenuPage::NODE_CONFIG_POSITION)); } else { gpsEnabled = (mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED); items.push_back(MenuItem("GPS", MenuAction::TOGGLE_GPS, MenuPage::NODE_CONFIG_POSITION, &gpsEnabled)); } #endif items.push_back(MenuItem("Exit", MenuPage::EXIT)); break; } case NODE_CONFIG_POWER: { previousPage = MenuPage::NODE_CONFIG; items.push_back(MenuItem("Back", previousPage)); #if defined(ARCH_ESP32) items.push_back(MenuItem("Powersave", MenuAction::TOGGLE_POWER_SAVE, MenuPage::EXIT, &config.power.is_power_saving)); #endif // ADC Multiplier float effectiveMult = 0.0f; // User override always shows if it exists if (config.power.adc_multiplier_override > 0.0f) { effectiveMult = config.power.adc_multiplier_override; } #ifdef ADC_MULTIPLIER else { // Fallback to variant defined effectiveMult = ADC_MULTIPLIER; } #endif // Only show if we actually have a value if (effectiveMult > 0.0f) { char buf[32]; snprintf(buf, sizeof(buf), "ADC Mult: %.3f", effectiveMult); nodeConfigLabels.emplace_back(buf); items.push_back( MenuItem(nodeConfigLabels.back().c_str(), MenuAction::NO_ACTION, MenuPage::NODE_CONFIG_POWER_ADC_CAL)); } items.push_back(MenuItem("Exit", MenuPage::EXIT)); break; } case NODE_CONFIG_POWER_ADC_CAL: { previousPage = MenuPage::NODE_CONFIG_POWER; items.push_back(MenuItem("Back", previousPage)); // Instruction text (header-style, non-selectable) items.push_back(MenuItem::Header("Run on full charge Only")); // Action items.push_back(MenuItem("Calibrate ADC", MenuAction::CALIBRATE_ADC, MenuPage::NODE_CONFIG_POWER)); items.push_back(MenuItem("Exit", MenuPage::EXIT)); break; } case NODE_CONFIG_NETWORK: { previousPage = MenuPage::NODE_CONFIG; items.push_back(MenuItem("Back", previousPage)); const char *wifiLabel = config.network.wifi_enabled ? "WiFi: On" : "WiFi: Off"; items.push_back(MenuItem(wifiLabel, MenuAction::TOGGLE_WIFI, MenuPage::EXIT)); #if HAS_WIFI && defined(ARCH_ESP32) if (config.network.wifi_enabled) { // Status if (WiFi.status() == WL_CONNECTED) { nodeConfigLabels.emplace_back("Status: Connected"); } else { nodeConfigLabels.emplace_back("Status: Not Connected"); } items.push_back(MenuItem(nodeConfigLabels.back().c_str(), MenuAction::NO_ACTION, MenuPage::NODE_CONFIG_NETWORK)); // Signal if (WiFi.status() == WL_CONNECTED) { int rssi = WiFi.RSSI(); int quality = constrain(2 * (rssi + 100), 0, 100); char sigBuf[32]; snprintf(sigBuf, sizeof(sigBuf), "Signal: %d%%", quality); nodeConfigLabels.emplace_back(sigBuf); items.push_back(MenuItem(nodeConfigLabels.back().c_str(), MenuAction::NO_ACTION, MenuPage::NODE_CONFIG_NETWORK)); char ipBuf[64]; snprintf(ipBuf, sizeof(ipBuf), "IP: %s", WiFi.localIP().toString().c_str()); nodeConfigLabels.emplace_back(ipBuf); items.push_back(MenuItem(nodeConfigLabels.back().c_str(), MenuAction::NO_ACTION, MenuPage::NODE_CONFIG_NETWORK)); } // SSID if (config.network.wifi_ssid && strlen(config.network.wifi_ssid) > 0) { std::string ssidLabel = "SSID: "; ssidLabel += config.network.wifi_ssid; nodeConfigLabels.emplace_back(ssidLabel); items.push_back(MenuItem(nodeConfigLabels.back().c_str(), MenuAction::NO_ACTION, MenuPage::NODE_CONFIG_NETWORK)); } // Hostname const char *host = WiFi.getHostname(); if (host && strlen(host) > 0) { std::string hostLabel = "Host: "; hostLabel += host; nodeConfigLabels.emplace_back(hostLabel); items.push_back(MenuItem(nodeConfigLabels.back().c_str(), MenuAction::NO_ACTION, MenuPage::NODE_CONFIG_NETWORK)); } } #endif items.push_back(MenuItem("Exit", MenuPage::EXIT)); break; } case NODE_CONFIG_DISPLAY: { previousPage = MenuPage::NODE_CONFIG; items.push_back(MenuItem("Back", previousPage)); items.push_back(MenuItem("12-Hour Clock", MenuAction::TOGGLE_12H_CLOCK, MenuPage::NODE_CONFIG_DISPLAY, &config.display.use_12h_clock)); const char *unitsLabel = (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) ? "Units: Imperial" : "Units: Metric"; items.push_back(MenuItem(unitsLabel, MenuAction::TOGGLE_DISPLAY_UNITS, MenuPage::NODE_CONFIG_DISPLAY)); items.push_back(MenuItem("Exit", MenuPage::EXIT)); break; } case NODE_CONFIG_BLUETOOTH: { previousPage = MenuPage::NODE_CONFIG; items.push_back(MenuItem("Back", previousPage)); const char *btLabel = config.bluetooth.enabled ? "Bluetooth: On" : "Bluetooth: Off"; items.push_back(MenuItem(btLabel, MenuAction::TOGGLE_BLUETOOTH, MenuPage::EXIT)); const char *pairLabel = config.bluetooth.fixed_pin ? "Pair Mode: Fixed" : "Pair Mode: Random"; items.push_back(MenuItem(pairLabel, MenuAction::TOGGLE_BLUETOOTH_PAIR_MODE, MenuPage::NODE_CONFIG_BLUETOOTH)); items.push_back(MenuItem("Exit", MenuPage::EXIT)); break; } case NODE_CONFIG_LORA: { previousPage = MenuPage::NODE_CONFIG; items.push_back(MenuItem("Back", previousPage)); const char *region = myRegion ? myRegion->name : "Unset"; nodeConfigLabels.emplace_back("Region: " + std::string(region)); items.push_back(MenuItem(nodeConfigLabels.back().c_str(), MenuAction::NO_ACTION, MenuPage::REGION)); const char *preset = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false, config.lora.use_preset); nodeConfigLabels.emplace_back("Preset: " + std::string(preset)); items.push_back(MenuItem(nodeConfigLabels.back().c_str(), MenuAction::NO_ACTION, MenuPage::NODE_CONFIG_PRESET)); char freqBuf[32]; float freq = RadioLibInterface::instance->getFreq(); snprintf(freqBuf, sizeof(freqBuf), "Freq: %.3f MHz", freq); nodeConfigLabels.emplace_back(freqBuf); items.push_back(MenuItem(nodeConfigLabels.back().c_str(), MenuAction::NO_ACTION, MenuPage::NODE_CONFIG_LORA)); items.push_back(MenuItem("Exit", MenuPage::EXIT)); break; } case NODE_CONFIG_CHANNELS: { previousPage = MenuPage::NODE_CONFIG; items.push_back(MenuItem("Back", previousPage)); for (uint8_t i = 0; i < MAX_NUM_CHANNELS; i++) { const meshtastic_Channel &ch = channels.getByIndex(i); if (!ch.has_settings) continue; if (ch.role == meshtastic_Channel_Role_DISABLED) continue; std::string label = "#"; if (ch.role == meshtastic_Channel_Role_PRIMARY) { label += "Primary"; } else if (strlen(ch.settings.name) > 0) { label += parse(ch.settings.name); } else { label += "Channel" + to_string(i + 1); } nodeConfigLabels.push_back(label); items.push_back( MenuItem(nodeConfigLabels.back().c_str(), MenuAction::NO_ACTION, MenuPage::NODE_CONFIG_CHANNEL_DETAIL)); } items.push_back(MenuItem("Exit", MenuPage::EXIT)); break; } case NODE_CONFIG_CHANNEL_DETAIL: { previousPage = MenuPage::NODE_CONFIG_CHANNELS; items.push_back(MenuItem("Back", previousPage)); meshtastic_Channel &ch = channels.getByIndex(selectedChannelIndex); // Name (read-only) const char *name = strlen(ch.settings.name) > 0 ? ch.settings.name : "Unnamed"; nodeConfigLabels.emplace_back("Ch: " + parse(name)); items.push_back(MenuItem(nodeConfigLabels.back().c_str(), MenuAction::NO_ACTION, MenuPage::NODE_CONFIG_CHANNEL_DETAIL)); // Uplink items.push_back(MenuItem("Uplink", MenuAction::TOGGLE_CHANNEL_UPLINK, MenuPage::NODE_CONFIG_CHANNEL_DETAIL, &ch.settings.uplink_enabled)); items.push_back(MenuItem("Downlink", MenuAction::TOGGLE_CHANNEL_DOWNLINK, MenuPage::NODE_CONFIG_CHANNEL_DETAIL, &ch.settings.downlink_enabled)); // Position channelPositionEnabled = ch.settings.has_module_settings && ch.settings.module_settings.position_precision > 0; items.push_back(MenuItem("Position", MenuAction::TOGGLE_CHANNEL_POSITION, MenuPage::NODE_CONFIG_CHANNEL_DETAIL, &channelPositionEnabled)); // Precision if (channelPositionEnabled) { std::string precisionLabel = "Unknown"; for (const auto &opt : POSITION_PRECISION_OPTIONS) { if (opt.value == ch.settings.module_settings.position_precision) { precisionLabel = (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) ? opt.imperial : opt.metric; break; } } nodeConfigLabels.emplace_back("Precision: " + precisionLabel); items.push_back( MenuItem(nodeConfigLabels.back().c_str(), MenuAction::NO_ACTION, MenuPage::NODE_CONFIG_CHANNEL_PRECISION)); } items.push_back(MenuItem("Exit", MenuPage::EXIT)); break; } case NODE_CONFIG_CHANNEL_PRECISION: { previousPage = MenuPage::NODE_CONFIG_CHANNEL_DETAIL; items.push_back(MenuItem("Back", previousPage)); const meshtastic_Channel &ch = channels.getByIndex(selectedChannelIndex); if (!ch.settings.has_module_settings || ch.settings.module_settings.position_precision == 0) { items.push_back(MenuItem("Position is Off", MenuPage::NODE_CONFIG_CHANNEL_DETAIL)); break; } constexpr uint8_t optionCount = sizeof(POSITION_PRECISION_OPTIONS) / sizeof(POSITION_PRECISION_OPTIONS[0]); for (uint8_t i = 0; i < optionCount; i++) { const auto &opt = POSITION_PRECISION_OPTIONS[i]; const char *label = (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) ? opt.imperial : opt.metric; nodeConfigLabels.emplace_back(label); items.push_back(MenuItem(nodeConfigLabels.back().c_str(), MenuAction::SET_CHANNEL_PRECISION, MenuPage::NODE_CONFIG_CHANNEL_DETAIL)); } items.push_back(MenuItem("Exit", MenuPage::EXIT)); break; } case NODE_CONFIG_DEVICE_ROLE: { previousPage = MenuPage::NODE_CONFIG_DEVICE; items.push_back(MenuItem("Back", previousPage)); items.push_back(MenuItem("Client", MenuAction::SET_ROLE_CLIENT, MenuPage::EXIT)); items.push_back(MenuItem("Client Mute", MenuAction::SET_ROLE_CLIENT_MUTE, MenuPage::EXIT)); items.push_back(MenuItem("Router", MenuAction::SET_ROLE_ROUTER, MenuPage::EXIT)); items.push_back(MenuItem("Repeater", MenuAction::SET_ROLE_REPEATER, MenuPage::EXIT)); items.push_back(MenuItem("Exit", MenuPage::EXIT)); break; } case TIMEZONE: previousPage = MenuPage::NODE_CONFIG_DEVICE; items.push_back(MenuItem("Back", previousPage)); items.push_back(MenuItem("US/Hawaii", SET_TZ_US_HAWAII, MenuPage::NODE_CONFIG_DEVICE)); items.push_back(MenuItem("US/Alaska", SET_TZ_US_ALASKA, MenuPage::NODE_CONFIG_DEVICE)); items.push_back(MenuItem("US/Pacific", SET_TZ_US_PACIFIC, MenuPage::NODE_CONFIG_DEVICE)); items.push_back(MenuItem("US/Arizona", SET_TZ_US_ARIZONA, MenuPage::NODE_CONFIG_DEVICE)); items.push_back(MenuItem("US/Mountain", SET_TZ_US_MOUNTAIN, MenuPage::NODE_CONFIG_DEVICE)); items.push_back(MenuItem("US/Central", SET_TZ_US_CENTRAL, MenuPage::NODE_CONFIG_DEVICE)); items.push_back(MenuItem("US/Eastern", SET_TZ_US_EASTERN, MenuPage::NODE_CONFIG_DEVICE)); items.push_back(MenuItem("BR/Brasilia", SET_TZ_BR_BRAZILIA, MenuPage::NODE_CONFIG_DEVICE)); items.push_back(MenuItem("UTC", SET_TZ_UTC, MenuPage::NODE_CONFIG_DEVICE)); items.push_back(MenuItem("EU/Western", SET_TZ_EU_WESTERN, MenuPage::NODE_CONFIG_DEVICE)); items.push_back(MenuItem("EU/Central", SET_TZ_EU_CENTRAL, MenuPage::NODE_CONFIG_DEVICE)); items.push_back(MenuItem("EU/Eastern", SET_TZ_EU_EASTERN, MenuPage::NODE_CONFIG_DEVICE)); items.push_back(MenuItem("Asia/Kolkata", SET_TZ_ASIA_KOLKATA, MenuPage::NODE_CONFIG_DEVICE)); items.push_back(MenuItem("Asia/Hong Kong", SET_TZ_ASIA_HONG_KONG, MenuPage::NODE_CONFIG_DEVICE)); items.push_back(MenuItem("AU/AWST", SET_TZ_AU_AWST, MenuPage::NODE_CONFIG_DEVICE)); items.push_back(MenuItem("AU/ACST", SET_TZ_AU_ACST, MenuPage::NODE_CONFIG_DEVICE)); items.push_back(MenuItem("AU/AEST", SET_TZ_AU_AEST, MenuPage::NODE_CONFIG_DEVICE)); items.push_back(MenuItem("Exit", MenuPage::EXIT)); break; case REGION: previousPage = MenuPage::NODE_CONFIG_LORA; items.push_back(MenuItem("Back", previousPage)); items.push_back(MenuItem("US", MenuAction::SET_REGION_US, MenuPage::EXIT)); items.push_back(MenuItem("EU 868", MenuAction::SET_REGION_EU_868, MenuPage::EXIT)); items.push_back(MenuItem("EU 433", MenuAction::SET_REGION_EU_433, MenuPage::EXIT)); items.push_back(MenuItem("CN", MenuAction::SET_REGION_CN, MenuPage::EXIT)); items.push_back(MenuItem("JP", MenuAction::SET_REGION_JP, MenuPage::EXIT)); items.push_back(MenuItem("ANZ", MenuAction::SET_REGION_ANZ, MenuPage::EXIT)); items.push_back(MenuItem("KR", MenuAction::SET_REGION_KR, MenuPage::EXIT)); items.push_back(MenuItem("TW", MenuAction::SET_REGION_TW, MenuPage::EXIT)); items.push_back(MenuItem("RU", MenuAction::SET_REGION_RU, MenuPage::EXIT)); items.push_back(MenuItem("IN", MenuAction::SET_REGION_IN, MenuPage::EXIT)); items.push_back(MenuItem("NZ 865", MenuAction::SET_REGION_NZ_865, MenuPage::EXIT)); items.push_back(MenuItem("TH", MenuAction::SET_REGION_TH, MenuPage::EXIT)); items.push_back(MenuItem("LoRa 2.4", MenuAction::SET_REGION_LORA_24, MenuPage::EXIT)); items.push_back(MenuItem("UA 433", MenuAction::SET_REGION_UA_433, MenuPage::EXIT)); items.push_back(MenuItem("UA 868", MenuAction::SET_REGION_UA_868, MenuPage::EXIT)); items.push_back(MenuItem("MY 433", MenuAction::SET_REGION_MY_433, MenuPage::EXIT)); items.push_back(MenuItem("MY 919", MenuAction::SET_REGION_MY_919, MenuPage::EXIT)); items.push_back(MenuItem("SG 923", MenuAction::SET_REGION_SG_923, MenuPage::EXIT)); items.push_back(MenuItem("PH 433", MenuAction::SET_REGION_PH_433, MenuPage::EXIT)); items.push_back(MenuItem("PH 868", MenuAction::SET_REGION_PH_868, MenuPage::EXIT)); items.push_back(MenuItem("PH 915", MenuAction::SET_REGION_PH_915, MenuPage::EXIT)); items.push_back(MenuItem("ANZ 433", MenuAction::SET_REGION_ANZ_433, MenuPage::EXIT)); items.push_back(MenuItem("KZ 433", MenuAction::SET_REGION_KZ_433, MenuPage::EXIT)); items.push_back(MenuItem("KZ 863", MenuAction::SET_REGION_KZ_863, MenuPage::EXIT)); items.push_back(MenuItem("NP 865", MenuAction::SET_REGION_NP_865, MenuPage::EXIT)); items.push_back(MenuItem("BR 902", MenuAction::SET_REGION_BR_902, MenuPage::EXIT)); items.push_back(MenuItem("Exit", MenuPage::EXIT)); break; case NODE_CONFIG_PRESET: { previousPage = MenuPage::NODE_CONFIG_LORA; items.push_back(MenuItem("Back", previousPage)); items.push_back(MenuItem("Long Moderate", MenuAction::SET_PRESET_LONG_MODERATE, MenuPage::EXIT)); items.push_back(MenuItem("Long Fast", MenuAction::SET_PRESET_LONG_FAST, MenuPage::EXIT)); items.push_back(MenuItem("Medium Slow", MenuAction::SET_PRESET_MEDIUM_SLOW, MenuPage::EXIT)); items.push_back(MenuItem("Medium Fast", MenuAction::SET_PRESET_MEDIUM_FAST, MenuPage::EXIT)); items.push_back(MenuItem("Short Slow", MenuAction::SET_PRESET_SHORT_SLOW, MenuPage::EXIT)); items.push_back(MenuItem("Short Fast", MenuAction::SET_PRESET_SHORT_FAST, MenuPage::EXIT)); items.push_back(MenuItem("Short Turbo", MenuAction::SET_PRESET_SHORT_TURBO, MenuPage::EXIT)); items.push_back(MenuItem("Exit", MenuPage::EXIT)); break; } // Administration Section case NODE_CONFIG_ADMIN_RESET: previousPage = MenuPage::NODE_CONFIG; items.push_back(MenuItem("Back", previousPage)); items.push_back(MenuItem("Reset All", MenuAction::RESET_NODEDB_ALL, MenuPage::EXIT)); items.push_back(MenuItem("Keep Favorites Only", MenuAction::RESET_NODEDB_KEEP_FAVORITES, MenuPage::EXIT)); items.push_back(MenuItem("Exit", MenuPage::EXIT)); break; // Exit case EXIT: sendToBackground(); // Menu applet dismissed, allow normal behavior to resume break; default: LOG_WARN("Page not implemented"); } // Reset the cursor, unless reloading same page // (or now out-of-bounds) if (page != currentPage || cursor >= items.size()) { cursor = 0; // ROOT menu has special handling: unselected at first, to emphasise the system info panel if (page == ROOT) cursorShown = false; } // Ensure cursor never rests on a header if (cursorShown) { while (cursor < items.size() && items.at(cursor).isHeader) { cursor++; } if (cursor >= items.size()) cursor = 0; } // Remember which page we are on now currentPage = page; } void InkHUD::MenuApplet::onRender(bool full) { // Free text mode draws a text input field and skips the normal rendering if (freeTextMode) { drawInputField(0, fontSmall.lineHeight(), X(1.0), Y(1.0) - fontSmall.lineHeight() - 1, cm.freeTextItem.rawText); return; } if (items.size() == 0) LOG_ERROR("Empty Menu"); // Dimensions for the slots where we will draw menuItems const float padding = 0.05; const uint16_t itemH = fontSmall.lineHeight() * 1.6; const int16_t selectInsetY = 2; const int16_t itemW = width() - X(padding) - X(padding); const int16_t itemL = X(padding); const int16_t itemR = X(1 - padding); int16_t itemT = 0; // Top (y px of current slot). Incremented as we draw. Adjusted to fit system info panel on ROOT menu. // How many full menuItems will fit on screen uint8_t slotCount = (height() - itemT) / itemH; // System info panel at the top of the menu // ========================================= uint16_t &siH = systemInfoPanelHeight; // System info - height. Calculated at onForeground const uint8_t slotsObscured = ceilf(siH / (float)itemH); // How many slots are obscured by system info panel // System info - top // Remain at 0px, until cursor reaches bottom of screen, then begin to scroll off screen. // This is the same behavior we expect from the non-root menus. // Implementing this with the systemp panel is slightly annoying though, // and required adding the MenuApplet::getSystemInfoPanelHeight method int16_t siT; if (cursor < slotCount - slotsObscured - 1) // (Minus 1: comparing zero based index with a count) siT = 0; else siT = 0 - ((cursor - (slotCount - slotsObscured - 1)) * itemH); // If showing ROOT menu, // and the panel isn't yet scrolled off screen top if (currentPage == ROOT) { drawSystemInfoPanel(0, siT, width()); // Draw the panel. itemT = max(siT + siH, 0); // Offset the first menu entry, so menu starts below the system info panel } // Draw menu items // =================== // Which item will be drawn to the top-most slot? // Initially, this is the item 0, but may increase once we begin scrolling uint8_t firstItem; if (cursor < slotCount) firstItem = 0; else firstItem = cursor - (slotCount - 1); // Which item will be drawn to the bottom-most slot? // This may be beyond the slot-count, to draw a partially off-screen item below the bottom-most slow // This may be less than the slot-count, if we are reaching the end of the menuItems uint8_t lastItem = min((uint8_t)firstItem + slotCount, (uint8_t)items.size() - 1); // -- Loop: draw each (visible) menu item -- for (uint8_t i = firstItem; i <= lastItem; i++) { // Grab the menu item MenuItem &item = items.at(i); // Vertical center of this slot int16_t center = itemT + (itemH / 2); // Header (non-selectable section label) if (item.isHeader) { setFont(fontSmall); // Header text (flush left) printAt(itemL + X(padding), center, item.label, LEFT, MIDDLE); // Subtle underline int16_t underlineY = itemT + itemH - 2; drawLine(itemL + X(padding), underlineY, itemR - X(padding), underlineY, BLACK); } else { // Box, if currently selected if (cursorShown && i == cursor) drawRect(itemL, itemT + selectInsetY, itemW, itemH - (selectInsetY * 2), BLACK); // Indented normal item text printAt(itemL + X(padding * 2), center, item.label, LEFT, MIDDLE); } // Checkbox, if relevant if (item.checkState) { const uint16_t cbWH = fontSmall.lineHeight(); // Checkbox: width / height const int16_t cbL = itemR - X(padding) - cbWH; // Checkbox: left const int16_t cbT = center - (cbWH / 2); // Checkbox : top // Checkbox ticked if (*(item.checkState)) { drawRect(cbL, cbT, cbWH, cbWH, BLACK); // First point of tick: pen down const int16_t t1Y = center; const int16_t t1X = cbL + 3; // Second point of tick: base const int16_t t2Y = center + (cbWH / 2) - 2; const int16_t t2X = cbL + (cbWH / 2); // Third point of tick: end of tail const int16_t t3Y = center - (cbWH / 2) - 2; const int16_t t3X = cbL + cbWH + 2; // Draw twice: faux bold drawLine(t1X, t1Y, t2X, t2Y, BLACK); drawLine(t2X, t2Y, t3X, t3Y, BLACK); drawLine(t1X + 1, t1Y, t2X + 1, t2Y, BLACK); drawLine(t2X + 1, t2Y, t3X + 1, t3Y, BLACK); } // Checkbox ticked else drawRect(cbL, cbT, cbWH, cbWH, BLACK); } // Increment the y value (top) as we go itemT += itemH; } } void InkHUD::MenuApplet::onButtonShortPress() { if (!freeTextMode) { // Push the auto-close timer back OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); if (!settings->joystick.enabled) { if (!cursorShown) { cursorShown = true; // Select the first item that isn't a header cursor = 0; while (cursor < items.size() && items.at(cursor).isHeader) { cursor++; } if (cursor >= items.size()) { cursorShown = false; cursor = 0; } } else { do { cursor = (cursor + 1) % items.size(); } while (items.at(cursor).isHeader); } requestUpdate(Drivers::EInk::UpdateTypes::FAST); } else { if (cursorShown) execute(items.at(cursor)); else showPage(MenuPage::EXIT); if (!wantsToRender()) requestUpdate(Drivers::EInk::UpdateTypes::FAST); } } } void InkHUD::MenuApplet::onButtonLongPress() { if (!freeTextMode) { // Push the auto-close timer back OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); if (cursorShown) execute(items.at(cursor)); else showPage(MenuPage::EXIT); // Special case: Peek at root-menu; longpress again to close // If we didn't already request a specialized update, when handling a menu action, // then perform the usual fast update. // FAST keeps things responsive: important because we're dealing with user input if (!wantsToRender()) requestUpdate(Drivers::EInk::UpdateTypes::FAST); } } void InkHUD::MenuApplet::onExitShort() { // Exit the menu showPage(MenuPage::EXIT); requestUpdate(Drivers::EInk::UpdateTypes::FAST); } void InkHUD::MenuApplet::onNavUp() { if (!freeTextMode) { OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); if (!cursorShown) { cursorShown = true; // Select the last item that isn't a header cursor = items.size() - 1; while (items.at(cursor).isHeader) { if (cursor == 0) { cursorShown = false; break; } cursor--; } } else { do { if (cursor == 0) cursor = items.size() - 1; else cursor--; } while (items.at(cursor).isHeader); } requestUpdate(Drivers::EInk::UpdateTypes::FAST); } } void InkHUD::MenuApplet::onNavDown() { if (!freeTextMode) { OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); if (!cursorShown) { cursorShown = true; // Select the first item that isn't a header cursor = 0; while (cursor < items.size() && items.at(cursor).isHeader) { cursor++; } if (cursor >= items.size()) { cursorShown = false; cursor = 0; } } else { do { cursor = (cursor + 1) % items.size(); } while (items.at(cursor).isHeader); } requestUpdate(Drivers::EInk::UpdateTypes::FAST); } } void InkHUD::MenuApplet::onNavLeft() { if (!freeTextMode) { OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); // Go to the previous menu page showPage(previousPage); requestUpdate(Drivers::EInk::UpdateTypes::FAST); } } void InkHUD::MenuApplet::onNavRight() { if (!freeTextMode) { OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); if (cursorShown) execute(items.at(cursor)); if (!wantsToRender()) requestUpdate(Drivers::EInk::UpdateTypes::FAST); } } void InkHUD::MenuApplet::onFreeText(char c) { if (cm.freeTextItem.rawText.length() >= menuTextLimit && c != '\b') return; if (c == '\b') { if (!cm.freeTextItem.rawText.empty()) cm.freeTextItem.rawText.pop_back(); } else { cm.freeTextItem.rawText += c; } requestUpdate(Drivers::EInk::UpdateTypes::FAST); } void InkHUD::MenuApplet::onFreeTextDone() { // Restart the auto-close timeout OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); OSThread::enabled = true; handleFreeText = false; freeTextMode = false; if (!cm.freeTextItem.rawText.empty()) { cm.selectedMessageItem = &cm.freeTextItem; showPage(MenuPage::CANNEDMESSAGE_RECIPIENT); } requestUpdate(Drivers::EInk::UpdateTypes::FAST); } void InkHUD::MenuApplet::onFreeTextCancel() { // Restart the auto-close timeout OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); OSThread::enabled = true; handleFreeText = false; freeTextMode = false; // Clear the free text message cm.freeTextItem.rawText.erase(); requestUpdate(Drivers::EInk::UpdateTypes::FAST); } // Dynamically create MenuItem entries for activating / deactivating Applets, for the "Applet Selection" submenu void InkHUD::MenuApplet::populateAppletPage() { assert(items.size() == 0); for (uint8_t i = 0; i < inkhud->userApplets.size(); i++) { const char *name = inkhud->userApplets.at(i)->name; bool *isActive = &(settings->userApplets.active[i]); items.push_back(MenuItem(name, MenuAction::TOGGLE_APPLET, MenuPage::APPLETS, isActive)); } } // Dynamically create MenuItem entries for selecting which applets will automatically come to foreground when they have new data // We only populate this menu page with applets which are actually active // We use the MenuItem::checkState pointer to toggle the setting in MenuApplet::execute. Bit of a hack, but convenient. void InkHUD::MenuApplet::populateAutoshowPage() { assert(items.size() == 0); for (uint8_t i = 0; i < inkhud->userApplets.size(); i++) { // Only add a menu item if applet is active if (settings->userApplets.active[i]) { const char *name = inkhud->userApplets.at(i)->name; bool *isActive = &(settings->userApplets.autoshow[i]); items.push_back(MenuItem(name, MenuAction::TOGGLE_AUTOSHOW_APPLET, MenuPage::AUTOSHOW, isActive)); } } } // Create MenuItem entries to select our definition of "Recent" // Controls how long data will remain in any "Recents" flavored applets void InkHUD::MenuApplet::populateRecentsPage() { // How many values are shown for use to choose from constexpr uint8_t optionCount = sizeof(RECENTS_OPTIONS_MINUTES) / sizeof(RECENTS_OPTIONS_MINUTES[0]); // Create an entry for each item in RECENTS_OPTIONS_MINUTES array // (Defined at top of this file) for (uint8_t i = 0; i < optionCount; i++) { std::string label = to_string(RECENTS_OPTIONS_MINUTES[i]) + " mins"; recentsSelected[i] = (settings->recentlyActiveSeconds == RECENTS_OPTIONS_MINUTES[i] * 60); items.push_back(MenuItem(label.c_str(), MenuAction::SET_RECENTS, MenuPage::OPTIONS, &recentsSelected[i])); } } // MenuItem entries for the "send" page // Dynamically creates menu items based on available canned messages void InkHUD::MenuApplet::populateSendPage() { // Position / NodeInfo packet items.push_back(MenuItem("Ping", MenuAction::SEND_PING, MenuPage::EXIT)); // If joystick is available, include the Free Text option if (settings->joystick.enabled && !inkhud->twoWayRocker) items.push_back(MenuItem("Free Text", MenuAction::FREE_TEXT, MenuPage::SEND)); // One menu item for each canned message uint8_t count = cm.store->size(); for (uint8_t i = 0; i < count; i++) { // Gather the information for this item CannedMessages::MessageItem messageItem; messageItem.rawText = cm.store->at(i); messageItem.label = parse(messageItem.rawText); // Store the item (until the menu closes) cm.messageItems.push_back(messageItem); // Create a menu item const char *itemText = cm.messageItems.back().label.c_str(); items.push_back(MenuItem(itemText, MenuAction::STORE_CANNEDMESSAGE_SELECTION, MenuPage::CANNEDMESSAGE_RECIPIENT)); } items.push_back(MenuItem("Exit", MenuPage::EXIT)); } // Dynamically create MenuItem entries for possible canned message destinations // All available channels are shown // Favorite nodes are shown, provided we don't have an *excessive* amount void InkHUD::MenuApplet::populateRecipientPage() { // Create recipient data (and menu items) for any channels // -------------------------------------------------------- for (uint8_t i = 0; i < MAX_NUM_CHANNELS; i++) { // Get the channel, and check if it's enabled const meshtastic_Channel &channel = channels.getByIndex(i); if (!channel.has_settings || channel.role == meshtastic_Channel_Role_DISABLED) continue; CannedMessages::RecipientItem r; // Set index r.channelIndex = channel.index; // Set a label for the menu item r.label = "Ch " + to_string(i) + ": "; if (channel.role == meshtastic_Channel_Role_PRIMARY) r.label += "Primary"; else r.label += parse(channel.settings.name); // Add to the list of recipients cm.recipientItems.push_back(r); // Add a menu item for this recipient const char *itemText = cm.recipientItems.back().label.c_str(); items.push_back(MenuItem(itemText, SEND_CANNEDMESSAGE, MenuPage::EXIT)); } // Create recipient data (and menu items) for favorite nodes // --------------------------------------------------------- uint32_t nodeCount = nodeDB->getNumMeshNodes(); uint32_t favoriteCount = 0; // Count favorites for (uint32_t i = 0; i < nodeCount; i++) { if (nodeDB->getMeshNodeByIndex(i)->is_favorite) favoriteCount++; } // Only add favorites if the number is reasonable // Don't want some monstrous list that takes 100 clicks to reach exit if (favoriteCount < 20) { for (uint32_t i = 0; i < nodeCount; i++) { meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); // Skip node if not a favorite if (!node->is_favorite) continue; CannedMessages::RecipientItem r; r.dest = node->num; r.channelIndex = nodeDB->getMeshNodeChannel(node->num); // Channel index only relevant if encrypted DM not possible(?) // Set a label for the menu item r.label = "DM: "; if (node->has_user) r.label += parse(node->user.long_name); else r.label += hexifyNodeNum(node->num); // Unsure if it's possible to favorite a node without NodeInfo? // Add to the list of recipients cm.recipientItems.push_back(r); // Add a menu item for this recipient const char *itemText = cm.recipientItems.back().label.c_str(); items.push_back(MenuItem(itemText, SEND_CANNEDMESSAGE, MenuPage::EXIT)); } } items.push_back(MenuItem("Exit", MenuPage::EXIT)); } void InkHUD::MenuApplet::drawInputField(uint16_t left, uint16_t top, uint16_t width, uint16_t height, const std::string &text) { setFont(fontSmall); uint16_t wrapMaxH = 0; // Draw the text, input box, and cursor // Adjusting the box for screen height while (wrapMaxH < height - fontSmall.lineHeight()) { wrapMaxH += fontSmall.lineHeight(); } // If the text is so long that it goes outside of the input box, the text is actually rendered off screen. uint32_t textHeight = getWrappedTextHeight(0, width - 5, text); if (!text.empty()) { uint16_t textPadding = X(1.0) > Y(1.0) ? wrapMaxH - textHeight : wrapMaxH - textHeight + 1; if (textHeight > wrapMaxH) printWrapped(2, textPadding, width - 5, text); else printWrapped(2, top + 2, width - 5, text); } uint16_t textCursorX = text.empty() ? 1 : getCursorX(); uint16_t textCursorY = text.empty() ? fontSmall.lineHeight() + 2 : getCursorY() - fontSmall.lineHeight() + 3; if (textCursorX + 1 > width - 5) { textCursorX = getCursorX() - width + 5; textCursorY += fontSmall.lineHeight(); } fillRect(textCursorX + 1, textCursorY, 1, fontSmall.lineHeight(), BLACK); // A white rectangle clears the top part of the screen for any text that's printed beyond the input box fillRect(0, 0, X(1.0), top, WHITE); // Draw character limit std::string ftlen = std::to_string(text.length()) + "/" + to_string(menuTextLimit); uint16_t textLen = getTextWidth(ftlen); printAt(X(1.0) - textLen - 2, 0, ftlen); // Draw the border drawRect(0, top, width, wrapMaxH + 5, BLACK); } // Renders the panel shown at the top of the root menu. // Displays the clock, and several other pieces of instantaneous system info, // which we'd prefer not to have displayed in a normal applet, as they update too frequently. void InkHUD::MenuApplet::drawSystemInfoPanel(int16_t left, int16_t top, uint16_t width, uint16_t *renderedHeight) { // Reset the height // We'll add to this as we add elements uint16_t height = 0; // Clock (potentially) // ==================== std::string clockString = getTimeString(); if (clockString.length() > 0) { setFont(fontMedium); printAt(width / 2, top, clockString, CENTER, TOP); height += fontMedium.lineHeight(); height += fontMedium.lineHeight() * 0.1; // Padding below clock } // Stats // =================== setFont(fontSmall); // Position of the label row for the system info const int16_t labelT = top + height; height += fontSmall.lineHeight() * 1.1; // Slightly increased spacing // Position of the data row for the system info const int16_t valT = top + height; height += fontSmall.lineHeight() * 1.1; // Slightly increased spacing (between bottom line and divider) // Position of divider between the info panel and the menu entries const int16_t divY = top + height; height += fontSmall.lineHeight() * 0.2; // Padding *below* the divider. (Above first menu item) // Create a variable number of columns // Either 3 or 4, depending on whether we have GPS // Todo constexpr uint8_t N_COL = 3; int16_t colL[N_COL]; int16_t colC[N_COL]; int16_t colR[N_COL]; for (uint8_t i = 0; i < N_COL; i++) { colL[i] = left + ((width / N_COL) * i); colC[i] = colL[i] + ((width / N_COL) / 2); colR[i] = colL[i] + (width / N_COL); } // Info blocks, left to right // Voltage float voltage = powerStatus->getBatteryVoltageMv() / 1000.0; char voltageStr[6]; // "XX.XV" sprintf(voltageStr, "%.2fV", voltage); printAt(colC[0], labelT, "Bat", CENTER, TOP); printAt(colC[0], valT, voltageStr, CENTER, TOP); // Divider for (int16_t y = valT; y <= divY; y += 3) drawPixel(colR[0], y, BLACK); // Channel Util char chUtilStr[4]; // "XX%" sprintf(chUtilStr, "%2.f%%", airTime->channelUtilizationPercent()); printAt(colC[1], labelT, "Ch", CENTER, TOP); printAt(colC[1], valT, chUtilStr, CENTER, TOP); // Divider for (int16_t y = valT; y <= divY; y += 3) drawPixel(colR[1], y, BLACK); // Duty Cycle (AirTimeTx) char dutyUtilStr[4]; // "XX%" sprintf(dutyUtilStr, "%2.f%%", airTime->utilizationTXPercent()); printAt(colC[2], labelT, "Duty", CENTER, TOP); printAt(colC[2], valT, dutyUtilStr, CENTER, TOP); /* // Divider for (int16_t y = valT; y <= divY; y += 3) drawPixel(colR[2], y, BLACK); // GPS satellites - todo printAt(colC[3], labelT, "Sats", CENTER, TOP); printAt(colC[3], valT, "ToDo", CENTER, TOP); */ // Horizontal divider, at bottom of system info panel for (int16_t x = 0; x < width; x += 2) // Divider, centered in the padding between first system panel and first item drawPixel(x, divY, BLACK); if (renderedHeight != nullptr) *renderedHeight = height; } // Get the height of the the panel drawn at the top of the menu // This is inefficient, as we do actually have to render the panel to determine the height // It solves a catch-22 situation, where slotCount needs to know panel height, and panel height needs to know slotCount uint16_t InkHUD::MenuApplet::getSystemInfoPanelHeight() { // Render *far* off screen uint16_t height = 0; drawSystemInfoPanel(INT16_MIN, INT16_MIN, 1, &height); return height; } // Send a text message to the mesh // Used to send our canned messages void InkHUD::MenuApplet::sendText(NodeNum dest, ChannelIndex channel, const char *message) { meshtastic_MeshPacket *p = router->allocForSending(); p->decoded.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP; p->to = dest; p->channel = channel; p->want_ack = true; p->decoded.payload.size = strlen(message); memcpy(p->decoded.payload.bytes, message, p->decoded.payload.size); // Tack on a bell character if requested if (moduleConfig.canned_message.send_bell && p->decoded.payload.size < meshtastic_Constants_DATA_PAYLOAD_LEN) { p->decoded.payload.bytes[p->decoded.payload.size] = 7; // Bell character p->decoded.payload.bytes[p->decoded.payload.size + 1] = '\0'; // Append Null Terminator p->decoded.payload.size++; } LOG_INFO("Send message id=%d, dest=%x, msg=%.*s", p->id, p->to, p->decoded.payload.size, p->decoded.payload.bytes); service->sendToMesh(p, RX_SRC_LOCAL, true); // Send to mesh, cc to phone } // Free up any heap memory we'd used while selecting / sending canned messages void InkHUD::MenuApplet::freeCannedMessageResources() { cm.selectedMessageItem = nullptr; cm.selectedRecipientItem = nullptr; cm.messageItems.clear(); cm.recipientItems.clear(); } #endif // MESHTASTIC_INCLUDE_INKHUD ================================================ FILE: src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.h ================================================ #ifdef MESHTASTIC_INCLUDE_INKHUD #include "configuration.h" #include "graphics/niche/Drivers/Backlight/LatchingBacklight.h" #include "graphics/niche/InkHUD/InkHUD.h" #include "graphics/niche/InkHUD/Persistence.h" #include "graphics/niche/InkHUD/SystemApplet.h" #include "graphics/niche/Utils/CannedMessageStore.h" #include "./MenuItem.h" #include "./MenuPage.h" #include "Channels.h" #include "concurrency/OSThread.h" namespace NicheGraphics::InkHUD { class Applet; class MenuApplet : public SystemApplet, public concurrency::OSThread { public: MenuApplet(); void onForeground() override; void onBackground() override; void onButtonShortPress() override; void onButtonLongPress() override; void onExitShort() override; void onNavUp() override; void onNavDown() override; void onNavLeft() override; void onNavRight() override; void onFreeText(char c) override; void onFreeTextDone() override; void onFreeTextCancel() override; void onRender(bool full) override; void show(Tile *t); // Open the menu, onto a user tile void setStartPage(MenuPage page); protected: Drivers::LatchingBacklight *backlight = nullptr; // Convenient access to the backlight singleton int32_t runOnce() override; void execute(MenuItem item); // Perform the MenuAction associated with a MenuItem, if any void showPage(MenuPage page); // Load and display a MenuPage void populateSendPage(); // Dynamically create MenuItems including canned messages void populateRecipientPage(); // Dynamically create a page of possible destinations for a canned message void populateAppletPage(); // Dynamically create MenuItems for toggling loaded applets void populateAutoshowPage(); // Dynamically create MenuItems for selecting which applets can autoshow void populateRecentsPage(); // Create menu items: a choice of values for settings.recentlyActiveSeconds void drawInputField(uint16_t left, uint16_t top, uint16_t width, uint16_t height, const std::string &text); // Draw input field for free text uint16_t getSystemInfoPanelHeight(); void drawSystemInfoPanel(int16_t left, int16_t top, uint16_t width, uint16_t *height = nullptr); // Info panel at top of root menu void sendText(NodeNum dest, ChannelIndex channel, const char *message); // Send a text message to mesh void freeCannedMessageResources(); // Clear MenuApplet's canned message processing data MenuPage startPageOverride = MenuPage::ROOT; MenuPage currentPage = MenuPage::ROOT; MenuPage previousPage = MenuPage::EXIT; uint8_t cursor = 0; // Which menu item is currently highlighted bool cursorShown = false; // Is *any* item highlighted? (Root menu: no initial selection) bool freeTextMode = false; uint16_t systemInfoPanelHeight = 0; // Need to know before we render uint16_t menuTextLimit = 200; std::vector items; // MenuItems for the current page. Filled by ShowPage std::vector nodeConfigLabels; // Persistent labels for Node Config pages uint8_t selectedChannelIndex = 0; // Currently selected LoRa channel (Node Config → Radio → Channel) bool channelPositionEnabled = false; bool gpsEnabled = false; // Recents menu checkbox state (derived from settings.recentlyActiveSeconds) static constexpr uint8_t RECENTS_COUNT = 6; bool recentsSelected[RECENTS_COUNT] = {}; // Data for selecting and sending canned messages via the menu // Placed into a sub-class for organization only class CannedMessages { public: // Share NicheGraphics component // Handles loading, getting, setting CannedMessageStore *store; // One canned message // Links the menu item to the true message text struct MessageItem { std::string label; // Shown in menu. Prefixed, and UTF-8 chars parsed std::string rawText; // The message which will be sent, if this item is selected } *selectedMessageItem; // One possible destination for a canned message // Links the menu item to the intended recipient // May represent either broadcast or DM struct RecipientItem { std::string label; // Shown in menu NodeNum dest = NODENUM_BROADCAST; uint8_t channelIndex = 0; } *selectedRecipientItem; // These lists are generated when the menu page is populated // Cleared onBackground (when MenuApplet closes) std::vector messageItems; std::vector recipientItems; MessageItem freeTextItem; } cm; Applet *borrowedTileOwner = nullptr; // Which applet we have temporarily replaced while displaying menu bool invertedColors = false; // Helper to display current state of config.display.displaymode in InkHUD options }; } // namespace NicheGraphics::InkHUD #endif ================================================ FILE: src/graphics/niche/InkHUD/Applets/System/Menu/MenuItem.h ================================================ #ifdef MESHTASTIC_INCLUDE_INKHUD /* One item of a MenuPage, in InkHUD::MenuApplet Added to MenuPages in InkHUD::showPage - May open a submenu or exit - May perform an action - May toggle a bool value, shown by a checkbox */ #pragma once #include "configuration.h" #include "./MenuAction.h" #include "./MenuPage.h" namespace NicheGraphics::InkHUD { // One item of a MenuPage class MenuItem { public: std::string label; MenuAction action = NO_ACTION; MenuPage nextPage = EXIT; bool *checkState = nullptr; bool isHeader = false; // Non-selectable section label // Various constructors, depending on the intended function of the item MenuItem(const char *label, MenuPage nextPage) : label(label), nextPage(nextPage) {} MenuItem(const char *label, MenuAction action) : label(label), action(action) {} MenuItem(const char *label, MenuAction action, MenuPage nextPage) : label(label), action(action), nextPage(nextPage) {} MenuItem(const char *label, MenuAction action, MenuPage nextPage, bool *checkState) : label(label), action(action), nextPage(nextPage), checkState(checkState) { } static MenuItem Header(const char *label) { MenuItem item(label, NO_ACTION, EXIT); item.isHeader = true; return item; } }; } // namespace NicheGraphics::InkHUD #endif ================================================ FILE: src/graphics/niche/InkHUD/Applets/System/Menu/MenuPage.h ================================================ #ifdef MESHTASTIC_INCLUDE_INKHUD /* Sub-menu for InkHUD::MenuApplet Structure of the menu is defined in InkHUD::showPage */ #pragma once #include "configuration.h" namespace NicheGraphics::InkHUD { // Sub-menu for MenuApplet enum MenuPage : uint8_t { ROOT, // Initial menu page SEND, CANNEDMESSAGE_RECIPIENT, // Select destination for a canned message OPTIONS, NODE_CONFIG, NODE_CONFIG_LORA, NODE_CONFIG_CHANNELS, // List of channels NODE_CONFIG_CHANNEL_DETAIL, // Per-channel options NODE_CONFIG_CHANNEL_PRECISION, NODE_CONFIG_PRESET, NODE_CONFIG_DEVICE, NODE_CONFIG_DEVICE_ROLE, NODE_CONFIG_POWER, NODE_CONFIG_POWER_ADC_CAL, NODE_CONFIG_NETWORK, NODE_CONFIG_DISPLAY, NODE_CONFIG_BLUETOOTH, NODE_CONFIG_POSITION, NODE_CONFIG_ADMIN_RESET, TIMEZONE, APPLETS, AUTOSHOW, RECENTS, // Select length of "recentlyActiveSeconds" REGION, EXIT, // Dismiss the menu applet }; } // namespace NicheGraphics::InkHUD #endif ================================================ FILE: src/graphics/niche/InkHUD/Applets/System/Notification/Notification.h ================================================ #ifdef MESHTASTIC_INCLUDE_INKHUD /* A notification which might be displayed by the NotificationApplet An instance of this class is offered to Applets via Applet::approveNotification, in case they want to veto the notification. An Applet should veto a notification if it is already displaying the same info which the notification would convey. */ #pragma once #include "configuration.h" namespace NicheGraphics::InkHUD { class Notification { public: enum Type : uint8_t { NOTIFICATION_MESSAGE_BROADCAST, NOTIFICATION_MESSAGE_DIRECT, NOTIFICATION_BATTERY } type; uint32_t timestamp; uint8_t getChannel() { return channel; } uint32_t getSender() { return sender; } uint8_t getBatteryPercentage() { return batteryPercentage; } friend class NotificationApplet; protected: uint8_t channel; uint32_t sender; uint8_t batteryPercentage; }; } // namespace NicheGraphics::InkHUD #endif ================================================ FILE: src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.cpp ================================================ #ifdef MESHTASTIC_INCLUDE_INKHUD #include "./NotificationApplet.h" #include "./Notification.h" #include "graphics/niche/InkHUD/Persistence.h" #include "meshUtils.h" #include "modules/TextMessageModule.h" #include "RTC.h" using namespace NicheGraphics; InkHUD::NotificationApplet::NotificationApplet() { textMessageObserver.observe(textMessageModule); } // Collect meta-info about the text message, and ask for approval for the notification // No need to save the message itself; we can use the cached InkHUD::latestMessage data during render() int InkHUD::NotificationApplet::onReceiveTextMessage(const meshtastic_MeshPacket *p) { // System applets are always active assert(isActive()); // Abort if feature disabled // This is a bit clumsy, but avoids complicated handling when the feature is enabled / disabled if (!settings->optionalFeatures.notifications) return 0; // Abort if this is an outgoing message if (getFrom(p) == nodeDB->getNodeNum()) return 0; Notification n; n.timestamp = getValidTime(RTCQuality::RTCQualityDevice, true); // Current RTC time // Gather info: in-channel message if (isBroadcast(p->to)) { n.type = Notification::Type::NOTIFICATION_MESSAGE_BROADCAST; n.channel = p->channel; } // Gather info: DM else { n.type = Notification::Type::NOTIFICATION_MESSAGE_DIRECT; n.sender = p->from; } // Close an old notification, if shown dismiss(); // Check if we should display the notification // A foreground applet might already be displaying this info hasNotification = true; currentNotification = n; if (isApproved()) { bringToForeground(); inkhud->forceUpdate(); } else hasNotification = false; // Clear the pending notification: it was rejected // Return zero: no issues here, carry on notifying other observers! return 0; } void InkHUD::NotificationApplet::onRender(bool full) { // Clear the region beneath the tile // Most applets are drawing onto an empty frame buffer and don't need to do this // We do need to do this with the battery though, as it is an "overlay" fillRect(0, 0, width(), height(), WHITE); // Padding (horizontal) const uint16_t padW = 4; // Main border drawRect(0, 0, width(), height(), BLACK); // drawRect(1, 1, width() - 2, height() - 2, BLACK); // Timestamp (potentially) // ==================== std::string ts = getTimeString(currentNotification.timestamp); uint16_t tsW = 0; int16_t divX = 0; // Timestamp available if (ts.length() > 0) { tsW = getTextWidth(ts); divX = padW + tsW + padW; hatchRegion(0, 0, divX, height(), 2, BLACK); // Fill with a dark background drawLine(divX, 0, divX, height() - 1, BLACK); // Draw divider between timestamp and main text setCrop(1, 1, divX - 1, height() - 2); // Drop shadow setTextColor(WHITE); printThick(padW + (tsW / 2), height() / 2, ts, 4, 4); // Bold text setTextColor(BLACK); printThick(padW + (tsW / 2), height() / 2, ts, 2, 1); } // Main text // ===================== // Background fill // - medium dark (1/3) hatchRegion(divX, 0, width() - divX - 1, height(), 3, BLACK); uint16_t availableWidth = width() - divX - padW; std::string text = getNotificationText(availableWidth); int16_t textM = divX + padW + (getTextWidth(text) / 2); // Restrict area for printing // - don't overlap border, or divider setCrop(divX + 1, 1, (width() - (divX + 1) - 1), height() - 2); // Drop shadow // - thick white text setTextColor(WHITE); printThick(textM, height() / 2, text, 4, 4); // Main text // - faux bold: double width setTextColor(BLACK); printThick(textM, height() / 2, text, 2, 1); } void InkHUD::NotificationApplet::onForeground() { handleInput = true; // Intercept the button input for our applet, so we can dismiss the notification } void InkHUD::NotificationApplet::onBackground() { handleInput = false; inkhud->forceUpdate(EInk::UpdateTypes::FULL, true); } void InkHUD::NotificationApplet::onButtonShortPress() { dismiss(); } void InkHUD::NotificationApplet::onButtonLongPress() { dismiss(); } void InkHUD::NotificationApplet::onExitShort() { dismiss(); } void InkHUD::NotificationApplet::onExitLong() { dismiss(); } void InkHUD::NotificationApplet::onNavUp() { dismiss(); } void InkHUD::NotificationApplet::onNavDown() { dismiss(); } void InkHUD::NotificationApplet::onNavLeft() { dismiss(); } void InkHUD::NotificationApplet::onNavRight() { dismiss(); } // Ask the WindowManager to check whether any displayed applets are already displaying the info from this notification // Called internally when we first get a "notifiable event", and then again before render, // in case autoshow swapped which applet was displayed bool InkHUD::NotificationApplet::isApproved() { // Instead of an assert if (!hasNotification) { LOG_WARN("No notif to approve"); return false; } // Ask all visible user applets for approval for (Applet *ua : inkhud->userApplets) { if (ua->isForeground() && !ua->approveNotification(currentNotification)) return false; } return true; } // Mark that the notification should no-longer be rendered // In addition to calling thing method, code needs to request a re-render of all applets void InkHUD::NotificationApplet::dismiss() { sendToBackground(); hasNotification = false; // Not requesting update directly from this method, // as it is used to dismiss notifications which have been made redundant by autoshow settings, before they are ever drawn } // Get a string for the main body text of a notification // Formatted to suit screen width // Takes info from InkHUD::currentNotification std::string InkHUD::NotificationApplet::getNotificationText(uint16_t widthAvailable) { assert(hasNotification); std::string text; // Text message // ============== if (IS_ONE_OF(currentNotification.type, Notification::Type::NOTIFICATION_MESSAGE_DIRECT, Notification::Type::NOTIFICATION_MESSAGE_BROADCAST)) { // Although we are handling DM and broadcast notifications together, we do need to treat them slightly differently bool msgIsBroadcast = currentNotification.type == Notification::Type::NOTIFICATION_MESSAGE_BROADCAST; // Pick source of message const MessageStore::Message *message = msgIsBroadcast ? &inkhud->persistence->latestMessage.broadcast : &inkhud->persistence->latestMessage.dm; // Find info about the sender meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(message->sender); // Leading tag (channel vs. DM) text += msgIsBroadcast ? "From:" : "DM: "; // Sender id if (node && node->has_user) text += parseShortName(node); else text += hexifyNodeNum(message->sender); // Check if text fits // - use a longer string, if we have the space if (getTextWidth(text) < widthAvailable * 0.5) { text.clear(); // Leading tag (channel vs. DM) text += msgIsBroadcast ? "Msg from " : "DM from "; // Sender id if (node && node->has_user) text += parseShortName(node); else text += hexifyNodeNum(message->sender); text += ": "; text += message->text; } } // Parse any non-ascii characters and return return parse(text); } #endif ================================================ FILE: src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.h ================================================ #ifdef MESHTASTIC_INCLUDE_INKHUD /* Pop-up notification bar, on screen top edge Displays information we feel is important, but which is not shown on currently focused applet(s) E.g.: messages, while viewing map, etc Feature should be optional; enable disable via on-screen menu */ #pragma once #include "configuration.h" #include "concurrency/OSThread.h" #include "graphics/niche/InkHUD/SystemApplet.h" namespace NicheGraphics::InkHUD { class NotificationApplet : public SystemApplet { public: NotificationApplet(); void onRender(bool full) override; void onForeground() override; void onBackground() override; void onButtonShortPress() override; void onButtonLongPress() override; void onExitShort() override; void onExitLong() override; void onNavUp() override; void onNavDown() override; void onNavLeft() override; void onNavRight() override; int onReceiveTextMessage(const meshtastic_MeshPacket *p); bool isApproved(); // Does a foreground applet make notification redundant? void dismiss(); // Close the Notification Popup protected: // Get notified when a new text message arrives CallbackObserver textMessageObserver = CallbackObserver(this, &NotificationApplet::onReceiveTextMessage); std::string getNotificationText(uint16_t widthAvailable); // Get text for notification, to suit screen width bool hasNotification = false; // Only used for assert. Todo: remove? Notification currentNotification = Notification(); // Set when something notification-worthy happens. Used by render() }; } // namespace NicheGraphics::InkHUD #endif ================================================ FILE: src/graphics/niche/InkHUD/Applets/System/Pairing/PairingApplet.cpp ================================================ #ifdef MESHTASTIC_INCLUDE_INKHUD #include "./PairingApplet.h" using namespace NicheGraphics; InkHUD::PairingApplet::PairingApplet() { bluetoothStatusObserver.observe(&bluetoothStatus->onNewStatus); } void InkHUD::PairingApplet::onRender(bool full) { // Header setFont(fontMedium); printAt(X(0.5), Y(0.25), "Bluetooth", CENTER, BOTTOM); setFont(fontSmall); printAt(X(0.5), Y(0.25), "Enter this code", CENTER, TOP); // Passkey setFont(fontMedium); printThick(X(0.5), Y(0.5), passkey.substr(0, 3) + " " + passkey.substr(3), 3, 2); // Device's bluetooth name, if it will fit setFont(fontSmall); std::string name = "Name: " + parse(getDeviceName()); if (getTextWidth(name) > width()) // Too wide, try without the leading "Name: " name = parse(getDeviceName()); if (getTextWidth(name) < width()) // Does it fit? printAt(X(0.5), Y(0.75), name, CENTER, MIDDLE); } void InkHUD::PairingApplet::onForeground() { // Prevent most other applets from requesting update, and skip their rendering entirely // Another system applet with a higher precedence can potentially ignore this SystemApplet::lockRendering = true; SystemApplet::lockRequests = true; } void InkHUD::PairingApplet::onBackground() { // Allow normal update behavior to resume SystemApplet::lockRendering = false; SystemApplet::lockRequests = false; // Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background // Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case inkhud->forceUpdate(EInk::UpdateTypes::FULL, true); } int InkHUD::PairingApplet::onBluetoothStatusUpdate(const meshtastic::Status *status) { // The standard Meshtastic convention is to pass these "generic" Status objects, // check their type, and then cast them. // We'll mimic that behavior, just to keep in line with the other Statuses, // even though I'm not sure what the original reason for jumping through these extra hoops was. assert(status->getStatusType() == STATUS_TYPE_BLUETOOTH); const auto *btStatus = static_cast(status); // When pairing begins if (btStatus->getConnectionState() == meshtastic::BluetoothStatus::ConnectionState::PAIRING) { // Store the passkey for rendering passkey = btStatus->getPasskey(); // Show pairing screen bringToForeground(); } // When pairing ends // or rather, when something changes, and we shouldn't be showing the pairing screen else if (isForeground()) sendToBackground(); return 0; // No special result to report back to Observable } #endif ================================================ FILE: src/graphics/niche/InkHUD/Applets/System/Pairing/PairingApplet.h ================================================ #ifdef MESHTASTIC_INCLUDE_INKHUD /* Shows the Bluetooth passkey during pairing */ #pragma once #include "configuration.h" #include "graphics/niche/InkHUD/SystemApplet.h" #include "main.h" namespace NicheGraphics::InkHUD { class PairingApplet : public SystemApplet { public: PairingApplet(); void onRender(bool full) override; void onForeground() override; void onBackground() override; int onBluetoothStatusUpdate(const meshtastic::Status *status); protected: // Get notified when status of the Bluetooth connection changes CallbackObserver bluetoothStatusObserver = CallbackObserver(this, &PairingApplet::onBluetoothStatusUpdate); std::string passkey = ""; // Passkey. Six digits, possibly with leading zeros }; } // namespace NicheGraphics::InkHUD #endif ================================================ FILE: src/graphics/niche/InkHUD/Applets/System/Placeholder/PlaceholderApplet.cpp ================================================ #ifdef MESHTASTIC_INCLUDE_INKHUD #include "./PlaceholderApplet.h" using namespace NicheGraphics; void InkHUD::PlaceholderApplet::onRender(bool full) { // This placeholder applet fills its area with sparse diagonal lines hatchRegion(0, 0, width(), height(), 8, BLACK); } #endif ================================================ FILE: src/graphics/niche/InkHUD/Applets/System/Placeholder/PlaceholderApplet.h ================================================ #ifdef MESHTASTIC_INCLUDE_INKHUD /* Shown when a tile doesn't have any other valid Applets Fills the area with diagonal lines */ #include "configuration.h" #include "graphics/niche/InkHUD/SystemApplet.h" namespace NicheGraphics::InkHUD { class PlaceholderApplet : public SystemApplet { public: void onRender(bool full) override; // Note: onForeground, onBackground, and wantsToRender are not meaningful for this applet. // The window manager decides when and where it should be rendered // It may be drawn to several different tiles during an Renderer::render call }; } // namespace NicheGraphics::InkHUD #endif ================================================ FILE: src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.cpp ================================================ #ifdef MESHTASTIC_INCLUDE_INKHUD #include "./TipsApplet.h" #include "graphics/niche/InkHUD/Persistence.h" #include "main.h" using namespace NicheGraphics; InkHUD::TipsApplet::TipsApplet() { bool needsRegion = (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET); bool showTutorialTips = (settings->tips.firstBoot || needsRegion); // Welcome screen if (showTutorialTips) tipQueue.push_back(Tip::WELCOME); // Finish setup if (needsRegion) tipQueue.push_back(Tip::FINISH_SETUP); // Using the UI if (showTutorialTips) { tipQueue.push_back(Tip::CUSTOMIZATION); tipQueue.push_back(Tip::BUTTONS); } // Shutdown info // Shown until user performs one valid shutdown if (!settings->tips.safeShutdownSeen) tipQueue.push_back(Tip::SAFE_SHUTDOWN); // Catch an incorrect attempt at rotating display if (config.display.flip_screen) tipQueue.push_back(Tip::ROTATION); // Region picker if (needsRegion) tipQueue.push_back(Tip::PICK_REGION); if (!tipQueue.empty()) bringToForeground(); } void InkHUD::TipsApplet::onRender(bool full) { switch (tipQueue.front()) { case Tip::WELCOME: renderWelcome(); break; case Tip::FINISH_SETUP: { setFont(fontMedium); const char *title = "Tip: Finish Setup"; uint16_t h = getWrappedTextHeight(0, width(), title); printWrapped(0, 0, width(), title); setFont(fontSmall); int16_t cursorY = h + fontSmall.lineHeight(); auto drawBullet = [&](const char *text) { uint16_t bh = getWrappedTextHeight(0, width(), text); printWrapped(0, cursorY, width(), text); cursorY += bh + (fontSmall.lineHeight() / 3); }; drawBullet("- connect antenna"); drawBullet("- connect a client app"); if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET) drawBullet("- set region"); if (!(*config.device.tzdef && config.device.tzdef[0] != 0)) drawBullet("- set timezone"); cursorY += fontSmall.lineHeight() / 2; drawBullet("More info at meshtastic.org"); printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM); } break; case Tip::PICK_REGION: { setFont(fontMedium); printAt(0, 0, "Set Region"); setFont(fontSmall); printWrapped(0, fontMedium.lineHeight() * 1.5, width(), "Please select your LoRa region to complete setup."); printAt(0, Y(1.0), "Press button to choose", LEFT, BOTTOM); } break; case Tip::SAFE_SHUTDOWN: { setFont(fontMedium); const char *title = "Tip: Shutdown"; uint16_t h = getWrappedTextHeight(0, width(), title); printWrapped(0, 0, width(), title); setFont(fontSmall); int16_t cursorY = h + fontSmall.lineHeight(); const char *body = "Before removing power, please shut down from InkHUD menu, or a client app.\n\n" "This ensures data is saved."; uint16_t bodyH = getWrappedTextHeight(0, width(), body); printWrapped(0, cursorY, width(), body); cursorY += bodyH + (fontSmall.lineHeight() / 2); printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM); } break; case Tip::CUSTOMIZATION: { setFont(fontMedium); const char *title = "Tip: Customization"; uint16_t h = getWrappedTextHeight(0, width(), title); printWrapped(0, 0, width(), title); setFont(fontSmall); int16_t cursorY = h + fontSmall.lineHeight(); const char *body = "Configure & control display with the InkHUD menu. " "Optional features, layout, rotation, and more."; uint16_t bodyH = getWrappedTextHeight(0, width(), body); printWrapped(0, cursorY, width(), body); cursorY += bodyH + (fontSmall.lineHeight() / 2); printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM); } break; case Tip::BUTTONS: { setFont(fontMedium); const char *title = "Tip: Buttons"; uint16_t h = getWrappedTextHeight(0, width(), title); printWrapped(0, 0, width(), title); setFont(fontSmall); int16_t cursorY = h + fontSmall.lineHeight(); auto drawBullet = [&](const char *text) { uint16_t bh = getWrappedTextHeight(0, width(), text); printWrapped(0, cursorY, width(), text); cursorY += bh + (fontSmall.lineHeight() / 3); }; if (!settings->joystick.enabled) { drawBullet("User Button"); drawBullet("- short press: next"); drawBullet("- long press: select or open menu"); } else if (inkhud->twoWayRocker) { drawBullet("Rocker + Button"); drawBullet("- center press: open menu or select"); drawBullet("- left/right: applet nav"); drawBullet("- in menu: up/down"); } else { drawBullet("Joystick"); drawBullet("- press: open menu or select"); drawBullet("Exit Button"); drawBullet("- press: switch tile or close menu"); } printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM); } break; case Tip::ROTATION: { setFont(fontMedium); const char *title = "Tip: Rotation"; uint16_t h = getWrappedTextHeight(0, width(), title); printWrapped(0, 0, width(), title); setFont(fontSmall); if (!settings->joystick.enabled) { int16_t cursorY = h + fontSmall.lineHeight(); const char *body = "To rotate the display, use the InkHUD menu. " "Long-press the user button > Options > Rotate."; uint16_t bh = getWrappedTextHeight(0, width(), body); printWrapped(0, cursorY, width(), body); cursorY += bh + (fontSmall.lineHeight() / 2); } else { printWrapped(0, fontMedium.lineHeight() * 1.5, width(), "To rotate the display, use the InkHUD menu. Press the user button > Options > Rotate."); } printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM); // Revert the "flip screen" setting, preventing this message showing again config.display.flip_screen = false; nodeDB->saveToDisk(SEGMENT_DEVICESTATE); } break; } } // This tip has its own render method, only because it's a big block of code // Didn't want to clutter up the switch in onRender too much void InkHUD::TipsApplet::renderWelcome() { uint16_t padW = X(0.05); // Detect portrait orientation bool portrait = height() > width(); // Block 1 - logo & title // ======================== // Logo size uint16_t logoWLimit = portrait ? X(0.5) : X(0.3); uint16_t logoHLimit = portrait ? Y(0.25) : Y(0.3); uint16_t logoW = getLogoWidth(logoWLimit, logoHLimit); uint16_t logoH = getLogoHeight(logoWLimit, logoHLimit); // Title size setFont(fontMedium); std::string title; if (width() >= 200) // Future proofing: hide if *tiny* display title = "meshtastic.org"; uint16_t titleW = getTextWidth(title); // Center the block // Desired effect: equal margin from display edge for logo left and title right int16_t block1Y = portrait ? Y(0.2) : Y(0.3); int16_t block1CX = X(0.5) + (logoW / 2) - (titleW / 2); int16_t logoCX = block1CX - (logoW / 2) - (padW / 2); int16_t titleCX = block1CX + (titleW / 2) + (padW / 2); // Draw block drawLogo(logoCX, block1Y, logoW, logoH); printAt(titleCX, block1Y, title, CENTER, MIDDLE); // Block 2 - subtitle // ======================= setFont(fontSmall); std::string subtitle = "InkHUD"; if (width() >= 200) subtitle += " - A Heads-Up Display"; // Future proofing: narrower for tiny display printAt(X(0.5), portrait ? Y(0.45) : Y(0.6), subtitle, CENTER, MIDDLE); // Block 3 - press to continue // ============================ printAt(X(0.5), Y(1), "Press button to continue", CENTER, BOTTOM); } void InkHUD::TipsApplet::onForeground() { // Prevent most other applets from requesting update, and skip their rendering entirely // Another system applet with a higher precedence can potentially ignore this SystemApplet::lockRendering = true; SystemApplet::lockRequests = true; SystemApplet::handleInput = true; // Our applet should handle button input (unless another system applet grabs it first) } void InkHUD::TipsApplet::onBackground() { // Allow normal update behavior to resume SystemApplet::lockRendering = false; SystemApplet::lockRequests = false; SystemApplet::handleInput = false; // Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background // Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case inkhud->forceUpdate(EInk::UpdateTypes::FULL, true); } // While our SystemApplet::handleInput flag is true void InkHUD::TipsApplet::onButtonShortPress() { bool needsRegion = (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET); // If we're prompting the user to pick a region, hand off to the menu if (!tipQueue.empty() && tipQueue.front() == Tip::PICK_REGION) { tipQueue.pop_front(); // Signal InkHUD to open the menu on Region page inkhud->forceRegionMenu = true; // Close tips and open menu sendToBackground(); inkhud->openMenu(); return; } // Consume current tip tipQueue.pop_front(); // All tips done if (tipQueue.empty()) { // Record that user has now seen the "tutorial" set of tips // Don't show them on subsequent boots if (settings->tips.firstBoot && !needsRegion) { settings->tips.firstBoot = false; inkhud->persistence->saveSettings(); } // Close applet sendToBackground(); } else { requestUpdate(); } } // Functions the same as the user button in this instance void InkHUD::TipsApplet::onExitShort() { onButtonShortPress(); } #endif ================================================ FILE: src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.h ================================================ #ifdef MESHTASTIC_INCLUDE_INKHUD /* Shows info on how to use InkHUD - tutorial at first boot - additional tips in certain situation (e.g. bad shutdown, region unset) */ #pragma once #include "configuration.h" #include "graphics/niche/InkHUD/SystemApplet.h" namespace NicheGraphics::InkHUD { class TipsApplet : public SystemApplet { protected: enum class Tip { WELCOME, FINISH_SETUP, PICK_REGION, SAFE_SHUTDOWN, CUSTOMIZATION, BUTTONS, ROTATION, }; public: TipsApplet(); void onRender(bool full) override; void onForeground() override; void onBackground() override; void onButtonShortPress() override; void onExitShort() override; protected: void renderWelcome(); // Very first screen of tutorial std::deque tipQueue; // List of tips to show, one after another WindowManager *windowManager = nullptr; // For convenience. Set in constructor. }; } // namespace NicheGraphics::InkHUD #endif ================================================ FILE: src/graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.cpp ================================================ #ifdef MESHTASTIC_INCLUDE_INKHUD #include "./AllMessageApplet.h" using namespace NicheGraphics; void InkHUD::AllMessageApplet::onActivate() { textMessageObserver.observe(textMessageModule); } void InkHUD::AllMessageApplet::onDeactivate() { textMessageObserver.unobserve(textMessageModule); } // We're not consuming the data passed to this method; // we're just just using it to trigger a render int InkHUD::AllMessageApplet::onReceiveTextMessage(const meshtastic_MeshPacket *p) { // Abort if applet fully deactivated // Already handled by onActivate and onDeactivate, but good practice for all applets if (!isActive()) return 0; // Abort if this is an outgoing message if (getFrom(p) == nodeDB->getNodeNum()) return 0; requestAutoshow(); // Want to become foreground, if permitted requestUpdate(); // Want to update display, if applet is foreground // Return zero: no issues here, carry on notifying other observers! return 0; } void InkHUD::AllMessageApplet::onRender(bool full) { // Find newest message, regardless of whether DM or broadcast MessageStore::Message *message; if (latestMessage->wasBroadcast) message = &latestMessage->broadcast; else message = &latestMessage->dm; // Short circuit: no text message if (!message->sender) { printAt(X(0.5), Y(0.5), "No Message", CENTER, MIDDLE); return; } // =========================== // Header (sender, timestamp) // =========================== // Y position for divider // - between header text and messages std::string header; // RX Time // - if valid std::string timeString = getTimeString(message->timestamp); if (timeString.length() > 0) { header += timeString; header += ": "; } // Sender's id // - short name and long name, if available, or // - node id meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(message->sender); if (sender && sender->has_user) { header += parseShortName(sender); // May be last-four of node if unprintable (emoji, etc) header += " ("; header += parse(sender->user.long_name); header += ")"; } else header += hexifyNodeNum(message->sender); // Draw a "standard" applet header drawHeader(header); // Fade the right edge of the header, if text spills over edge uint8_t wF = getFont().lineHeight() / 2; // Width of fade effect uint8_t hF = getHeaderHeight(); // Height of fade effect if (getCursorX() > width()) hatchRegion(width() - wF - 1, 1, wF, hF, 2, WHITE); // Dimensions of the header constexpr int16_t padDivH = 2; const int16_t headerDivY = Applet::getHeaderHeight() - 1; // =================== // Print message text // =================== // Parse any non-ascii chars in the message std::string text = parse(message->text); // Extra gap below the header int16_t textTop = headerDivY + padDivH; // Attempt to print with fontLarge uint32_t textHeight; setFont(fontLarge); textHeight = getWrappedTextHeight(0, width(), text); if (textHeight <= (uint32_t)height()) { printWrapped(0, textTop, width(), text); return; } // Fallback (too large): attempt to print with fontMedium setFont(fontMedium); textHeight = getWrappedTextHeight(0, width(), text); if (textHeight <= (uint32_t)height()) { printWrapped(0, textTop, width(), text); return; } // Fallback (too large): print with fontSmall setFont(fontSmall); printWrapped(0, textTop, width(), text); } // Don't show notifications for text messages when our applet is displayed bool InkHUD::AllMessageApplet::approveNotification(Notification &n) { if (n.type == Notification::Type::NOTIFICATION_MESSAGE_BROADCAST) return false; else if (n.type == Notification::Type::NOTIFICATION_MESSAGE_DIRECT) return false; else return true; } #endif ================================================ FILE: src/graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h ================================================ #ifdef MESHTASTIC_INCLUDE_INKHUD /* Shows the latest incoming text message, as well as sender. Both broadcast and direct messages will be shown here, from all channels. This module doesn't doesn't use the devicestate.rx_text_message,' as this is overwritten to contain outgoing messages This module doesn't collect its own text message. Instead, the WindowManager stores the most recent incoming text message. This is available to any interested modules (SingeMessageApplet, NotificationApplet etc.) via InkHUD::latestMessage We do still receive notifications from the text message module though, to know when a new message has arrived, and trigger the update. */ #pragma once #include "configuration.h" #include "graphics/niche/InkHUD/Applet.h" #include "modules/TextMessageModule.h" namespace NicheGraphics::InkHUD { class Applet; class AllMessageApplet : public Applet { public: void onRender(bool full) override; void onActivate() override; void onDeactivate() override; int onReceiveTextMessage(const meshtastic_MeshPacket *p); bool approveNotification(Notification &n) override; // Which notifications to suppress protected: // Used to register our text message callback CallbackObserver textMessageObserver = CallbackObserver(this, &AllMessageApplet::onReceiveTextMessage); }; } // namespace NicheGraphics::InkHUD #endif ================================================ FILE: src/graphics/niche/InkHUD/Applets/User/DM/DMApplet.cpp ================================================ #ifdef MESHTASTIC_INCLUDE_INKHUD #include "./DMApplet.h" using namespace NicheGraphics; void InkHUD::DMApplet::onActivate() { textMessageObserver.observe(textMessageModule); } void InkHUD::DMApplet::onDeactivate() { textMessageObserver.unobserve(textMessageModule); } // We're not consuming the data passed to this method; // we're just just using it to trigger a render int InkHUD::DMApplet::onReceiveTextMessage(const meshtastic_MeshPacket *p) { // Abort if applet fully deactivated // Already handled by onActivate and onDeactivate, but good practice for all applets if (!isActive()) return 0; // If DM (not broadcast) if (!isBroadcast(p->to)) { // Want to update display, if applet is foreground requestUpdate(); // If this was an incoming message, suggest that our applet becomes foreground, if permitted if (getFrom(p) != nodeDB->getNodeNum()) requestAutoshow(); } // Return zero: no issues here, carry on notifying other observers! return 0; } void InkHUD::DMApplet::onRender(bool full) { // Abort if no text message if (!latestMessage->dm.sender) { printAt(X(0.5), Y(0.5), "No DMs", CENTER, MIDDLE); return; } // =========================== // Header (sender, timestamp) // =========================== // Y position for divider // - between header text and messages std::string header; // RX Time // - if valid std::string timeString = getTimeString(latestMessage->dm.timestamp); if (timeString.length() > 0) { header += timeString; header += ": "; } // Sender's id // - shortname and long name, if available, or // - node id meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(latestMessage->dm.sender); if (sender && sender->has_user) { header += parseShortName(sender); // May be last-four of node if unprintable (emoji, etc) header += " ("; header += parse(sender->user.long_name); header += ")"; } else header += hexifyNodeNum(latestMessage->dm.sender); // Draw a "standard" applet header drawHeader(header); // Fade the right edge of the header, if text spills over edge uint8_t wF = getFont().lineHeight() / 2; // Width of fade effect uint8_t hF = getHeaderHeight(); // Height of fade effect if (getCursorX() > width()) hatchRegion(width() - wF - 1, 1, wF, hF, 2, WHITE); // Dimensions of the header constexpr int16_t padDivH = 2; const int16_t headerDivY = Applet::getHeaderHeight() - 1; // =================== // Print message text // =================== // Parse any non-ascii chars in the message std::string text = parse(latestMessage->dm.text); // Extra gap below the header int16_t textTop = headerDivY + padDivH; // Attempt to print with fontLarge uint32_t textHeight; setFont(fontLarge); textHeight = getWrappedTextHeight(0, width(), text); if (textHeight <= (uint32_t)height()) { printWrapped(0, textTop, width(), text); return; } // Fallback (too large): attempt to print with fontMedium setFont(fontMedium); textHeight = getWrappedTextHeight(0, width(), text); if (textHeight <= (uint32_t)height()) { printWrapped(0, textTop, width(), text); return; } // Fallback (too large): print with fontSmall setFont(fontSmall); printWrapped(0, textTop, width(), text); } // Don't show notifications for direct messages when our applet is displayed bool InkHUD::DMApplet::approveNotification(Notification &n) { if (n.type == Notification::Type::NOTIFICATION_MESSAGE_DIRECT) return false; else return true; } #endif ================================================ FILE: src/graphics/niche/InkHUD/Applets/User/DM/DMApplet.h ================================================ #ifdef MESHTASTIC_INCLUDE_INKHUD /* Shows the latest incoming *Direct Message* (DM), as well as sender. This compliments the threaded message applets This module doesn't doesn't use the devicestate.rx_text_message,' as this is overwritten to contain outgoing messages This module doesn't collect its own text message. Instead, the WindowManager stores the most recent incoming text message. This is available to any interested modules (SingeMessageApplet, NotificationApplet etc.) via InkHUD::latestMessage We do still receive notifications from the text message module though, to know when a new message has arrived, and trigger the update. */ #pragma once #include "configuration.h" #include "graphics/niche/InkHUD/Applet.h" #include "modules/TextMessageModule.h" namespace NicheGraphics::InkHUD { class Applet; class DMApplet : public Applet { public: void onRender(bool full) override; void onActivate() override; void onDeactivate() override; int onReceiveTextMessage(const meshtastic_MeshPacket *p); bool approveNotification(Notification &n) override; // Which notifications to suppress protected: // Used to register our text message callback CallbackObserver textMessageObserver = CallbackObserver(this, &DMApplet::onReceiveTextMessage); }; } // namespace NicheGraphics::InkHUD #endif ================================================ FILE: src/graphics/niche/InkHUD/Applets/User/FavoritesMap/FavoritesMapApplet.cpp ================================================ #ifdef MESHTASTIC_INCLUDE_INKHUD #include "./FavoritesMapApplet.h" #include "NodeDB.h" using namespace NicheGraphics; bool InkHUD::FavoritesMapApplet::shouldDrawNode(meshtastic_NodeInfoLite *node) { // Keep our own node available as map anchor/center; all others must be favorited. return node && (node->num == nodeDB->getNodeNum() || node->is_favorite); } void InkHUD::FavoritesMapApplet::onRender(bool full) { // Custom empty state text for favorites-only map. if (!enoughMarkers()) { printAt(X(0.5), Y(0.5) - (getFont().lineHeight() / 2), "Favorite node position", CENTER, MIDDLE); printAt(X(0.5), Y(0.5) + (getFont().lineHeight() / 2), "will appear here", CENTER, MIDDLE); return; } // Draw the usual map applet first. MapApplet::onRender(full); // Draw our latest "node of interest" as a special marker. meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(lastFrom); if (node && node->is_favorite && nodeDB->hasValidPosition(node) && enoughMarkers()) drawLabeledMarker(node); } // Determine if we need to redraw the map, when we receive a new position packet. ProcessMessage InkHUD::FavoritesMapApplet::handleReceived(const meshtastic_MeshPacket &mp) { // If applet is not active, we shouldn't be handling any data. if (!isActive()) return ProcessMessage::CONTINUE; // Try decode a position from the packet. bool hasPosition = false; float lat; float lng; if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag && mp.decoded.portnum == meshtastic_PortNum_POSITION_APP) { meshtastic_Position position = meshtastic_Position_init_default; if (pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, &meshtastic_Position_msg, &position)) { if (position.has_latitude_i && position.has_longitude_i // Actually has position && (position.latitude_i != 0 || position.longitude_i != 0)) // Position isn't "null island" { hasPosition = true; lat = position.latitude_i * 1e-7; // Convert from Meshtastic's internal int32_t format lng = position.longitude_i * 1e-7; } } } // Skip if we didn't get a valid position. if (!hasPosition) return ProcessMessage::CONTINUE; const int8_t hopsAway = getHopsAway(mp); const bool hasHopsAway = hopsAway >= 0; // Determine if the position packet would change anything on-screen. bool somethingChanged = false; // If our own position. if (isFromUs(&mp)) { // Ignore tiny local movement to reduce update spam. if (GeoCoord::latLongToMeter(ourLastLat, ourLastLng, lat, lng) > 50) { somethingChanged = true; ourLastLat = lat; ourLastLng = lng; } } else { // For non-local packets, this applet only reacts to favorited nodes. const meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(mp.from); if (!sender || !sender->is_favorite) return ProcessMessage::CONTINUE; // Check if this position is from someone different than our previous position packet. if (mp.from != lastFrom) { somethingChanged = true; lastFrom = mp.from; lastLat = lat; lastLng = lng; lastHopsAway = hopsAway; } // Same sender: check if position changed. else if (GeoCoord::latLongToMeter(lastLat, lastLng, lat, lng) > 10) { somethingChanged = true; lastLat = lat; lastLng = lng; } // Same sender, same position: check if hops changed. else if (hasHopsAway && (hopsAway != lastHopsAway)) { somethingChanged = true; lastHopsAway = hopsAway; } } if (somethingChanged) { requestAutoshow(); requestUpdate(); } return ProcessMessage::CONTINUE; } #endif ================================================ FILE: src/graphics/niche/InkHUD/Applets/User/FavoritesMap/FavoritesMapApplet.h ================================================ #ifdef MESHTASTIC_INCLUDE_INKHUD /* Plots position of favorited nodes from DB, with North facing up. Scaled to fit the most distant node. Size of marker represents hops away. The favorite node which most recently sent a position will be labeled. */ #pragma once #include "configuration.h" #include "graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.h" #include "SinglePortModule.h" namespace NicheGraphics::InkHUD { class FavoritesMapApplet : public MapApplet, public SinglePortModule { public: FavoritesMapApplet() : SinglePortModule("FavoritesMapApplet", meshtastic_PortNum_POSITION_APP) {} void onRender(bool full) override; protected: bool shouldDrawNode(meshtastic_NodeInfoLite *node) override; ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; NodeNum lastFrom = 0; // Sender of most recent favorited (non-local) position packet float lastLat = 0.0; float lastLng = 0.0; float lastHopsAway = 0; float ourLastLat = 0.0; // Info about most recent local position float ourLastLng = 0.0; }; } // namespace NicheGraphics::InkHUD #endif ================================================ FILE: src/graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.cpp ================================================ #ifdef MESHTASTIC_INCLUDE_INKHUD #include "RTC.h" #include "gps/GeoCoord.h" #include "./HeardApplet.h" using namespace NicheGraphics; void InkHUD::HeardApplet::onActivate() { // When applet begins, pre-fill with stale info from NodeDB populateFromNodeDB(); } void InkHUD::HeardApplet::onDeactivate() { // Avoid an unlikely situation where frequent activation / deactivation populates duplicate info from node DB cards.clear(); } // When base applet hears a new packet, it extracts the info and passes it to us as CardInfo // We need to store it (at front to sort recent), and request display update if our list has visibly changed as a result void InkHUD::HeardApplet::handleParsed(CardInfo c) { // Grab the previous entry. // To check if the new data is different enough to justify re-render // Need to cache now, before we manipulate the deque CardInfo previous; if (!cards.empty()) previous = cards.at(0); // If we're updating an existing entry, remove the old one. Will reinsert at front for (auto it = cards.begin(); it != cards.end(); ++it) { if (it->nodeNum == c.nodeNum) { cards.erase(it); break; } } cards.push_front(c); // Insert into base class' card collection cards.resize(min(maxCards(), (uint8_t)cards.size())); // Don't keep more cards than we could *ever* fit on screen cards.shrink_to_fit(); // Our rendered image needs to change if: if (previous.nodeNum != c.nodeNum // Different node || previous.signal != c.signal // or different signal strength || previous.distanceMeters != c.distanceMeters // or different position || previous.hopsAway != c.hopsAway) // or different hops away { requestAutoshow(); requestUpdate(); } } // When applet is activated, pre-fill with stale data from NodeDB // We're sorting using the last_heard value. Susceptible to weirdness if node's RTC changes. // No SNR is available in node db, so we can't calculate signal either // These initial cards from node db will be gradually pushed out by new packets which originate from out base applet instead void InkHUD::HeardApplet::populateFromNodeDB() { // Fill a collection with pointers to each node in db std::vector ordered; for (auto mn = nodeDB->meshNodes->begin(); mn != nodeDB->meshNodes->end(); ++mn) { // Only copy if valid, and not our own node if (mn->num != 0 && mn->num != nodeDB->getNodeNum()) ordered.push_back(&*mn); } // Sort the collection by age std::sort(ordered.begin(), ordered.end(), [](const meshtastic_NodeInfoLite *top, const meshtastic_NodeInfoLite *bottom) -> bool { return (top->last_heard > bottom->last_heard); }); // Keep the most recent entries only // Just enough to fill the screen if (ordered.size() > maxCards()) ordered.resize(maxCards()); // Create card info for these (stale) node observations meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); for (meshtastic_NodeInfoLite *node : ordered) { CardInfo c; c.nodeNum = node->num; if (node->has_hops_away) c.hopsAway = node->hops_away; if (nodeDB->hasValidPosition(node) && nodeDB->hasValidPosition(ourNode)) { // Get lat and long as float // Meshtastic stores these as integers internally float ourLat = ourNode->position.latitude_i * 1e-7; float ourLong = ourNode->position.longitude_i * 1e-7; float theirLat = node->position.latitude_i * 1e-7; float theirLong = node->position.longitude_i * 1e-7; c.distanceMeters = (int32_t)GeoCoord::latLongToMeter(theirLat, theirLong, ourLat, ourLong); } // Insert into the card collection (member of base class) cards.push_back(c); } } // Text drawn in the usual applet header // Handled by base class: ChronoListApplet std::string InkHUD::HeardApplet::getHeaderText() { uint16_t nodeCount = nodeDB->getNumMeshNodes() - 1; // Don't count our own node std::string text = "Heard: "; // Print node count, if nodeDB not yet nearing full if (nodeCount < MAX_NUM_NODES) { text += to_string(nodeCount); // Max nodes text += " "; text += (nodeCount == 1) ? "node" : "nodes"; } return text; } #endif ================================================ FILE: src/graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.h ================================================ #ifdef MESHTASTIC_INCLUDE_INKHUD /* Shows a list of all nodes (recently heard or not), sorted by time last heard. Most of the work is done by the InkHUD::NodeListApplet base class */ #pragma once #include "configuration.h" #include "graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.h" namespace NicheGraphics::InkHUD { class HeardApplet : public NodeListApplet { public: HeardApplet() : NodeListApplet("HeardApplet") {} void onActivate() override; void onDeactivate() override; protected: void handleParsed(CardInfo c) override; // Store new info, and update display if needed std::string getHeaderText() override; // Set title for this applet void populateFromNodeDB(); // Pre-fill the CardInfo collection from NodeDB }; } // namespace NicheGraphics::InkHUD #endif ================================================ FILE: src/graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.cpp ================================================ #ifdef MESHTASTIC_INCLUDE_INKHUD #include "./PositionsApplet.h" #include "NodeDB.h" using namespace NicheGraphics; void InkHUD::PositionsApplet::onRender(bool full) { // Draw the usual map applet first MapApplet::onRender(full); // Draw our latest "node of interest" as a special marker // ------------------------------------------------------- // We might be rendering because we got a position packet from them // We might be rendering because our own position updated // Either way, we still highlight which node most recently sent us a position packet meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(lastFrom); if (node && nodeDB->hasValidPosition(node) && enoughMarkers()) drawLabeledMarker(node); } // Determine if we need to redraw the map, when we receive a new position packet ProcessMessage InkHUD::PositionsApplet::handleReceived(const meshtastic_MeshPacket &mp) { // If applet is not active, we shouldn't be handling any data // It's good practice for all applets to implement an early return like this // for PositionsApplet, this is **required** - it's where we're handling active vs deactive if (!isActive()) return ProcessMessage::CONTINUE; // Try decode a position from the packet bool hasPosition = false; float lat; float lng; if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag && mp.decoded.portnum == meshtastic_PortNum_POSITION_APP) { meshtastic_Position position = meshtastic_Position_init_default; if (pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, &meshtastic_Position_msg, &position)) { if (position.has_latitude_i && position.has_longitude_i // Actually has position && (position.latitude_i != 0 || position.longitude_i != 0)) // Position isn't "null island" { hasPosition = true; lat = position.latitude_i * 1e-7; // Convert from Meshtastic's internal int32_t format lng = position.longitude_i * 1e-7; } } } // Skip if we didn't get a valid position if (!hasPosition) return ProcessMessage::CONTINUE; const int8_t hopsAway = getHopsAway(mp); const bool hasHopsAway = hopsAway >= 0; // Determine if the position packet would change anything on-screen // ----------------------------------------------------------------- bool somethingChanged = false; // If our own position if (isFromUs(&mp)) { // We get frequent position updates from connected phone // Only update if we're travelled some distance, for rate limiting // Todo: smarter detection of position changes if (GeoCoord::latLongToMeter(ourLastLat, ourLastLng, lat, lng) > 50) { somethingChanged = true; ourLastLat = lat; ourLastLng = lng; } } // If someone else's position else { // Check if this position is from someone different than our previous position packet if (mp.from != lastFrom) { somethingChanged = true; lastFrom = mp.from; lastLat = lat; lastLng = lng; lastHopsAway = hopsAway; } // Same sender: check if position changed // Todo: smarter detection of position changes else if (GeoCoord::latLongToMeter(lastLat, lastLng, lat, lng) > 10) { somethingChanged = true; lastLat = lat; lastLng = lng; } // Same sender, same position: check if hops changed // Only pay attention if the hopsAway value is valid else if (hasHopsAway && (hopsAway != lastHopsAway)) { somethingChanged = true; lastHopsAway = hopsAway; } } // Decision reached // ----------------- if (somethingChanged) { requestAutoshow(); // Todo: only request this in some situations? requestUpdate(); } return ProcessMessage::CONTINUE; } #endif ================================================ FILE: src/graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h ================================================ #ifdef MESHTASTIC_INCLUDE_INKHUD /* Plots position of all nodes from DB, with North facing up. Scaled to fit the most distant node. Size of cross represents hops away. The node which has most recently sent a position will be labeled. */ #pragma once #include "configuration.h" #include "graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.h" #include "SinglePortModule.h" namespace NicheGraphics::InkHUD { class PositionsApplet : public MapApplet, public SinglePortModule { public: PositionsApplet() : SinglePortModule("PositionsApplet", meshtastic_PortNum_POSITION_APP) {} void onRender(bool full) override; protected: ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; NodeNum lastFrom = 0; // Sender of most recent (non-local) position packet float lastLat = 0.0; float lastLng = 0.0; float lastHopsAway = 0; float ourLastLat = 0.0; // Info about the most recent (non-local) position packet float ourLastLng = 0.0; // Info about most recent *local* position }; } // namespace NicheGraphics::InkHUD #endif ================================================ FILE: src/graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.cpp ================================================ #ifdef MESHTASTIC_INCLUDE_INKHUD #include "./RecentsListApplet.h" #include "RTC.h" using namespace NicheGraphics; InkHUD::RecentsListApplet::RecentsListApplet() : NodeListApplet("RecentsListApplet"), concurrency::OSThread("RecentsListApplet") { // No scheduled tasks initially OSThread::disable(); } void InkHUD::RecentsListApplet::onActivate() { // When the applet is activated, begin scheduled purging of any nodes which are no longer "active" OSThread::enabled = true; OSThread::setIntervalFromNow(60 * 1000UL); // Every minute } void InkHUD::RecentsListApplet::onDeactivate() { // Halt scheduled purging OSThread::disable(); } int32_t InkHUD::RecentsListApplet::runOnce() { prune(); // Remove CardInfo and Age record for nodes which we haven't heard recently return OSThread::interval; } // When base applet hears a new packet, it extracts the info and passes it to us as CardInfo // We need to store it (at front to sort recent), and request display update if our list has visibly changed as a result // We also need to record the current time against the nodenum, so we know when it becomes inactive void InkHUD::RecentsListApplet::handleParsed(CardInfo c) { // Grab the previous entry. // To check if the new data is different enough to justify re-render // Need to cache now, before we manipulate the deque CardInfo previous; if (!cards.empty()) previous = cards.at(0); // If we're updating an existing entry, remove the old one. Will reinsert at front for (auto it = cards.begin(); it != cards.end(); ++it) { if (it->nodeNum == c.nodeNum) { cards.erase(it); break; } } cards.push_front(c); // Store this CardInfo cards.resize(min(maxCards(), (uint8_t)cards.size())); // Don't keep more cards than we could *ever* fit on screen cards.shrink_to_fit(); // Record the time of this observation // Used to count active nodes, and to know when to prune inactive nodes seenNow(c.nodeNum); // Our rendered image needs to change if: if (previous.nodeNum != c.nodeNum // Different node || previous.signal != c.signal // or different signal strength || previous.distanceMeters != c.distanceMeters // or different position || previous.hopsAway != c.hopsAway) // or different hops away { prune(); // Take the opportunity now to remove inactive nodes requestAutoshow(); requestUpdate(); } } // Record the time (millis, right now) that we hear a node // If we do not hear from a node for a while, its card and age info will be removed by the purge method, which runs regularly void InkHUD::RecentsListApplet::seenNow(NodeNum nodeNum) { // If we're updating an existing entry, remove the old one. Will reinsert at front for (auto it = ages.begin(); it != ages.end(); ++it) { if (it->nodeNum == nodeNum) { ages.erase(it); break; } } Age a; a.nodeNum = nodeNum; a.seenAtMs = millis(); ages.push_front(a); } // Remove Card and Age info for any nodes which are now inactive // Determined by when a node was last heard, in our internal record (not from nodeDB) void InkHUD::RecentsListApplet::prune() { // Iterate age records from newest to oldest for (uint16_t i = 0; i < ages.size(); i++) { // Found the first record which is too old if (!isActive(ages.at(i).seenAtMs)) { // Drop this item, and all others behind it ages.resize(i); ages.shrink_to_fit(); cards.resize(i); cards.shrink_to_fit(); // Request an update, if pruning did modify our data // Required if pruning was scheduled. Redundant if pruning was prior to rendering. requestAutoshow(); requestUpdate(); break; } } // Push next scheduled pruning back // Pruning may be called from by handleParsed, immediately prior to rendering // In that case, we can slightly delay our scheduled pruning OSThread::setIntervalFromNow(60 * 1000UL); } // Is a timestamp old enough that it would make a node inactive, and in need of purging? bool InkHUD::RecentsListApplet::isActive(uint32_t seenAtMs) { uint32_t now = millis(); uint32_t secsAgo = (now - seenAtMs) / 1000UL; // millis() overflow safe return (secsAgo < settings->recentlyActiveSeconds); } // Text to be shown at top of applet // ChronoListApplet base class allows us to set this dynamically // Might want to adjust depending on node count, RTC status, etc std::string InkHUD::RecentsListApplet::getHeaderText() { std::string text; // Print the length of our "Recents" time-window text += "Last "; text += to_string(settings->recentlyActiveSeconds / 60); text += " mins"; // Print the node count const uint16_t nodeCount = ages.size(); text += ": "; text += to_string(nodeCount); text += " "; text += (nodeCount == 1) ? "node" : "nodes"; return text; } #endif ================================================ FILE: src/graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.h ================================================ #ifdef MESHTASTIC_INCLUDE_INKHUD /* Shows a list of nodes which have been recently active The length of this "recently active" window is configurable using the onscreen menu Most of the work is done by the shared InkHUD::NodeListApplet base class */ #pragma once #include "configuration.h" #include "graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.h" namespace NicheGraphics::InkHUD { class RecentsListApplet : public NodeListApplet, public concurrency::OSThread { protected: // Used internally to count the number of active nodes // We count for ourselves, instead of using the value provided by NodeDB, // as the values occasionally differ, due to the timing of our Applet's purge method struct Age { uint32_t nodeNum; uint32_t seenAtMs; }; public: RecentsListApplet(); void onActivate() override; void onDeactivate() override; protected: int32_t runOnce() override; void handleParsed(CardInfo c) override; // Store new info, update active count, update display if needed std::string getHeaderText() override; // Set title for this applet void seenNow(NodeNum nodeNum); // Record that we have just seen this node, for active node count void prune(); // Remove cards for nodes which we haven't seen recently bool isActive(uint32_t seenAtMillis); // Is a node still active, based on when we last heard it? std::deque ages; // Information about when we last heard nodes. Independent of NodeDB }; } // namespace NicheGraphics::InkHUD #endif ================================================ FILE: src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.cpp ================================================ #ifdef MESHTASTIC_INCLUDE_INKHUD #include "./ThreadedMessageApplet.h" #include "RTC.h" #include "mesh/NodeDB.h" using namespace NicheGraphics; // Hard limits on how much message data to write to flash // Avoid filling the storage if something goes wrong // Normal usage should be well below this size constexpr uint8_t MAX_MESSAGES_SAVED = 10; constexpr uint32_t MAX_MESSAGE_SIZE = 250; InkHUD::ThreadedMessageApplet::ThreadedMessageApplet(uint8_t channelIndex) : SinglePortModule("ThreadedMessageApplet", meshtastic_PortNum_TEXT_MESSAGE_APP), channelIndex(channelIndex) { // Create the message store // Will shortly attempt to load messages from RAM, if applet is active // Label (filename in flash) is set from channel index store = new MessageStore("ch" + to_string(channelIndex)); } void InkHUD::ThreadedMessageApplet::onRender(bool full) { // ============= // Draw a header // ============= // Header text std::string headerText; headerText += "Channel "; headerText += to_string(channelIndex); headerText += ": "; if (channels.isDefaultChannel(channelIndex)) headerText += "Public"; else headerText += channels.getByIndex(channelIndex).settings.name; // Draw a "standard" applet header drawHeader(headerText); // Y position for divider const int16_t dividerY = Applet::getHeaderHeight() - 1; // ================== // Draw each message // ================== // Restrict drawing area // - don't overdraw the header // - small gap below divider setCrop(0, dividerY + 2, width(), height() - (dividerY + 2)); // Set padding // - separates text from the vertical line which marks its edge constexpr uint16_t padW = 2; constexpr int16_t msgL = padW; const int16_t msgR = (width() - 1) - padW; const uint16_t msgW = (msgR - msgL) + 1; int16_t msgB = height() - 1; // Vertical cursor for drawing. Messages are bottom-aligned to this value. uint8_t i = 0; // Index of stored message // Loop over messages // - until no messages left, or // - until no part of message fits on screen while (msgB >= (0 - fontSmall.lineHeight()) && i < store->messages.size()) { // Grab data for message const MessageStore::Message &m = store->messages.at(i); bool outgoing = (m.sender == 0) || (m.sender == myNodeInfo.my_node_num); // Own NodeNum if canned message std::string bodyText = parse(m.text); // Parse any non-ascii chars in the message // Cache bottom Y of message text // - Used when drawing vertical line alongside const int16_t dotsB = msgB; // Get dimensions for message text uint16_t bodyH = getWrappedTextHeight(msgL, msgW, bodyText); int16_t bodyT = msgB - bodyH; // Print message // - if incoming if (!outgoing) printWrapped(msgL, bodyT, msgW, bodyText); // Print message // - if outgoing else { if (getTextWidth(bodyText) < width()) // If short, printAt(msgR, bodyT, bodyText, RIGHT); // print right align else // If long, printWrapped(msgL, bodyT, msgW, bodyText); // need printWrapped(), which doesn't support right align } // Move cursor up // - above message text msgB -= bodyH; msgB -= getFont().lineHeight() * 0.2; // Padding between message and header // Compose info string // - shortname, if possible, or "me" // - time received, if possible std::string info; if (outgoing) info += "Me"; else { // Check if sender is node db meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(m.sender); if (sender) info += parseShortName(sender); // Handle any unprintable chars in short name else info += hexifyNodeNum(m.sender); // No node info at all. Print the node num } std::string timeString = getTimeString(m.timestamp); if (timeString.length() > 0) { info += " - "; info += timeString; } // Print the info string // - Faux bold: printed twice, shifted horizontally by one px printAt(outgoing ? msgR : msgL, msgB, info, outgoing ? RIGHT : LEFT, BOTTOM); printAt(outgoing ? msgR - 1 : msgL + 1, msgB, info, outgoing ? RIGHT : LEFT, BOTTOM); // Underline the info string const int16_t divY = msgB; int16_t divL; int16_t divR; if (!outgoing) { // Left side - incoming divL = msgL; divR = getTextWidth(info) + getFont().lineHeight() / 2; } else { // Right side - outgoing divR = msgR; divL = divR - getTextWidth(info) - getFont().lineHeight() / 2; } for (int16_t x = divL; x <= divR; x += 2) drawPixel(x, divY, BLACK); // Move cursor up: above info string msgB -= fontSmall.lineHeight(); // Vertical line alongside message for (int16_t y = msgB; y < dotsB; y += 1) drawPixel(outgoing ? width() - 1 : 0, y, BLACK); // Move cursor up: padding before next message msgB -= fontSmall.lineHeight() * 0.5; i++; } // End of loop: drawing each message // Fade effect: // Area immediately below the divider. Overdraw with sparse white lines. // Make text appear to pass behind the header hatchRegion(0, dividerY + 1, width(), fontSmall.lineHeight() / 3, 2, WHITE); // If we've run out of screen to draw messages, we can drop any leftover data from the queue // Those messages have been pushed off the screen-top by newer ones while (i < store->messages.size()) store->messages.pop_back(); } // Code which runs when the applet begins running // This might happen at boot, or if user enables the applet at run-time, via the menu void InkHUD::ThreadedMessageApplet::onActivate() { loadMessagesFromFlash(); loopbackOk = true; // Allow us to handle messages generated on the node (canned messages) } // Code which runs when the applet stop running // This might be at shutdown, or if the user disables the applet at run-time, via the menu void InkHUD::ThreadedMessageApplet::onDeactivate() { loopbackOk = false; // Slightly reduce our impact if the applet is disabled } // Handle new text messages // These might be incoming, from the mesh, or outgoing from phone // Each instance of the ThreadMessageApplet will only listen on one specific channel ProcessMessage InkHUD::ThreadedMessageApplet::handleReceived(const meshtastic_MeshPacket &mp) { // Abort if applet fully deactivated if (!isActive()) return ProcessMessage::CONTINUE; // Abort if wrong channel if (mp.channel != this->channelIndex) return ProcessMessage::CONTINUE; // Abort if message was a DM if (mp.to != NODENUM_BROADCAST) return ProcessMessage::CONTINUE; // Extract info into our slimmed-down "StoredMessage" type MessageStore::Message newMessage; newMessage.timestamp = getValidTime(RTCQuality::RTCQualityDevice, true); // Current RTC time newMessage.sender = mp.from; newMessage.channelIndex = mp.channel; newMessage.text = std::string((const char *)mp.decoded.payload.bytes, mp.decoded.payload.size); // Store newest message at front // These records are used when rendering, and also stored in flash at shutdown store->messages.push_front(newMessage); // If this was an incoming message, suggest that our applet becomes foreground, if permitted if (getFrom(&mp) != nodeDB->getNodeNum()) requestAutoshow(); // Redraw the applet, perhaps. requestUpdate(); // Want to update display, if applet is foreground // Tell Module API to continue informing other firmware components about this message // We're not the only component which is interested in new text messages return ProcessMessage::CONTINUE; } // Don't show notifications for text messages broadcast to our channel, when the applet is displayed bool InkHUD::ThreadedMessageApplet::approveNotification(Notification &n) { if (n.type == Notification::Type::NOTIFICATION_MESSAGE_BROADCAST && n.getChannel() == channelIndex) return false; // None of our business. Allow the notification. else return true; } // Save several recent messages to flash // Stores the contents of ThreadedMessageApplet::messages // Just enough messages to fill the display // Messages are packed "back-to-back", to minimize blocks of flash used void InkHUD::ThreadedMessageApplet::saveMessagesToFlash() { // Create a label (will become the filename in flash) std::string label = "ch" + to_string(channelIndex); store->saveToFlash(); } // Load recent messages to flash // Fills ThreadedMessageApplet::messages with previous messages // Just enough messages have been stored to cover the display void InkHUD::ThreadedMessageApplet::loadMessagesFromFlash() { // Create a label (will become the filename in flash) std::string label = "ch" + to_string(channelIndex); store->loadFromFlash(); } // Code to run when device is shutting down // This is in addition to any onDeactivate() code, which will also run // Todo: implement before a reboot also void InkHUD::ThreadedMessageApplet::onShutdown() { // Save our current set of messages to flash, provided the applet isn't disabled if (isActive()) saveMessagesToFlash(); } #endif ================================================ FILE: src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h ================================================ #ifdef MESHTASTIC_INCLUDE_INKHUD /* Displays a thread-view of incoming and outgoing message for a specific channel The channel for this applet is set in the constructor, when the applet is added to WindowManager in the setupNicheGraphics method. Several messages are saved to flash at shutdown, to preserve applet between reboots. This class has its own internal method for saving and loading to fs, which interacts directly with the FSCommon layer. If the amount of flash usage is unacceptable, we could keep these in RAM only. Multiple instances of this channel may be used. This must be done at buildtime. Suggest a max of two channel, to minimize fs usage? */ #pragma once #include "configuration.h" #include "graphics/niche/InkHUD/Applet.h" #include "graphics/niche/InkHUD/MessageStore.h" #include "modules/TextMessageModule.h" namespace NicheGraphics::InkHUD { class Applet; class ThreadedMessageApplet : public Applet, public SinglePortModule { public: explicit ThreadedMessageApplet(uint8_t channelIndex); ThreadedMessageApplet() = delete; void onRender(bool full) override; void onActivate() override; void onDeactivate() override; void onShutdown() override; ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; bool approveNotification(Notification &n) override; // Which notifications to suppress protected: void saveMessagesToFlash(); void loadMessagesFromFlash(); MessageStore *store; // Messages, held in RAM for use, ready to save to flash on shutdown uint8_t channelIndex = 0; }; } // namespace NicheGraphics::InkHUD #endif ================================================ FILE: src/graphics/niche/InkHUD/DisplayHealth.cpp ================================================ #ifdef MESHTASTIC_INCLUDE_INKHUD #include "./DisplayHealth.h" #include "DisplayHealth.h" using namespace NicheGraphics; // Timing for "maintenance" // Paying off full-refresh debt with unprovoked updates, if the display is not very active static constexpr uint32_t MAINTENANCE_MS_INITIAL = 60 * 1000UL; static constexpr uint32_t MAINTENANCE_MS = 60 * 60 * 1000UL; InkHUD::DisplayHealth::DisplayHealth() : concurrency::OSThread("Mediator") { // Timer disabled by default OSThread::disable(); } // Request which update type we would prefer, when the display image next changes // DisplayHealth class will consider our suggestion, and weigh it against other requests void InkHUD::DisplayHealth::requestUpdateType(Drivers::EInk::UpdateTypes type) { // Update our "working decision", to decide if this request is important enough to change our plan if (!forced) workingDecision = prioritize(workingDecision, type); } // Demand that a specific update type be used, when the display image next changes // Note: multiple DisplayHealth::force calls should not be made, // but if they are, the importance of the type will be weighed the same as if both calls were to DisplayHealth::request void InkHUD::DisplayHealth::forceUpdateType(Drivers::EInk::UpdateTypes type) { if (!forced) workingDecision = type; else workingDecision = prioritize(workingDecision, type); forced = true; } // Find out which update type the DisplayHealth has chosen for us // Calling this method consumes the result, and resets for the next update Drivers::EInk::UpdateTypes InkHUD::DisplayHealth::decideUpdateType() { LOG_DEBUG("FULL-update debt:%f", debt); // For convenience typedef Drivers::EInk::UpdateTypes UpdateTypes; // Grab our final decision for the update type, so we can reset now, for the next update // We do this at top of the method, so we can return early UpdateTypes finalDecision = workingDecision; workingDecision = UpdateTypes::UNSPECIFIED; forced = false; // Check whether we've paid off enough debt to stop unprovoked refreshing (if in progress) // This maintenance behavior will also have opportunity to halt itself when the timer next fires, // but that could be an hour away, so we can stop it early here and free up resources if (OSThread::enabled && debt == 0.0) endMaintenance(); // Explicitly requested FULL if (finalDecision == UpdateTypes::FULL) { LOG_DEBUG("Explicit FULL"); debt = max(debt - 1.0, 0.0); // Record that we have paid back (some of) the FULL refresh debt return UpdateTypes::FULL; } // Explicitly requested FAST if (finalDecision == UpdateTypes::FAST) { LOG_DEBUG("Explicit FAST"); // Add to the FULL refresh debt if (debt < 1.0) debt += 1.0 / fastPerFull; else debt += stressMultiplier * (1.0 / fastPerFull); // More debt if too many consecutive FAST refreshes // If *significant debt*, begin occasionally refreshing *unprovoked* // This maintenance behavior is only triggered here, by periods of user interaction // Debt would otherwise not be able to climb above 1.0 if (debt >= 2.0) beginMaintenance(); return UpdateTypes::FAST; // Give them what the asked for } // Handling UpdateTypes::UNSPECIFIED // ----------------------------------- // In this case, the UI doesn't care which refresh we use // Not much debt: suggest FAST if (debt < 1.0) { LOG_DEBUG("UNSPECIFIED: using FAST"); debt += 1.0 / fastPerFull; return UpdateTypes::FAST; } // In debt: suggest FULL else { LOG_DEBUG("UNSPECIFIED: using FULL"); debt = max(debt - 1.0, 0.0); // Record that we have paid back (some of) the FULL refresh debt // When maintenance begins, the first refresh happens shortly after user interaction ceases (a minute or so) // If we *are* given an opportunity to refresh before that, we'll skip that initial maintenance refresh // We were intending to use that initial refresh to redraw the screen as FULL, but we're doing that now, organically if (OSThread::enabled && OSThread::interval == MAINTENANCE_MS_INITIAL) OSThread::setInterval(MAINTENANCE_MS); // Note: not intervalFromNow return UpdateTypes::FULL; } } // Determine which of two update types is more important to honor // Explicit FAST is more important than UNSPECIFIED - prioritize responsiveness // Explicit FULL is more important than explicit FAST - prioritize image quality: explicit FULL is rare // Used when multiple applets have all requested update simultaneously, each with their own preferred UpdateType Drivers::EInk::UpdateTypes InkHUD::DisplayHealth::prioritize(Drivers::EInk::UpdateTypes type1, Drivers::EInk::UpdateTypes type2) { switch (type1) { case Drivers::EInk::UpdateTypes::UNSPECIFIED: return type2; case Drivers::EInk::UpdateTypes::FAST: return (type2 == Drivers::EInk::UpdateTypes::FULL) ? Drivers::EInk::UpdateTypes::FULL : Drivers::EInk::UpdateTypes::FAST; case Drivers::EInk::UpdateTypes::FULL: return type1; } return Drivers::EInk::UpdateTypes::UNSPECIFIED; // Suppress compiler warning only } // We're using the timer to perform "maintenance" // If significant FULL-refresh debt has accumulated, we will occasionally run FULL refreshes unprovoked. // This prevents gradual build-up of debt, // in case we aren't doing enough UNSPECIFIED refreshes to pay the debt back organically. // The first refresh takes place shortly after user finishes interacting with the device; this does the bulk of the restoration // Subsequent refreshes take place *much* less frequently. // Hopefully an applet will want to render before this, meaning we can cancel the maintenance. int32_t InkHUD::DisplayHealth::runOnce() { if (debt > 0.0) { LOG_DEBUG("debt=%f: performing maintenance", debt); // Ask WindowManager to redraw everything, purely for the refresh // Todo: optimize? Could update without re-rendering InkHUD::getInstance()->forceUpdate(Drivers::EInk::UpdateTypes::FULL); // Record that we have paid back (some of) the FULL refresh debt debt = max(debt - 1.0, 0.0); // Next maintenance refresh scheduled - long wait (an hour?) return MAINTENANCE_MS; } else return endMaintenance(); } // Begin periodically refreshing the display, to repay FULL-refresh debt // We do this in case user doesn't have enough activity to repay it organically, with UpdateTypes::UNSPECIFIED // After an initial refresh, to redraw as FULL, we only perform these maintenance refreshes very infrequently // This gives the display a chance to heal by evaluating UNSPECIFIED as FULL, which is preferable void InkHUD::DisplayHealth::beginMaintenance() { OSThread::setIntervalFromNow(MAINTENANCE_MS_INITIAL); OSThread::enabled = true; } // FULL-refresh debt is low enough that we no longer need to pay it back with periodic updates int32_t InkHUD::DisplayHealth::endMaintenance() { return OSThread::disable(); } #endif ================================================ FILE: src/graphics/niche/InkHUD/DisplayHealth.h ================================================ #ifdef MESHTASTIC_INCLUDE_INKHUD /* Responsible for maintaining display health, by optimizing the ratio of FAST vs FULL refreshes - counts number of FULL vs FAST refresh - suggests whether to use FAST or FULL, when not explicitly specified - periodically requests update unprovoked, if required for display health */ #pragma once #include "configuration.h" #include "InkHUD.h" #include "graphics/niche/Drivers/EInk/EInk.h" namespace NicheGraphics::InkHUD { class DisplayHealth : protected concurrency::OSThread { public: DisplayHealth(); void requestUpdateType(Drivers::EInk::UpdateTypes type); void forceUpdateType(Drivers::EInk::UpdateTypes type); Drivers::EInk::UpdateTypes decideUpdateType(); uint8_t fastPerFull = 5; // Ideal number of fast refreshes between full refreshes float stressMultiplier = 2.0; // How bad for the display are extra fast refreshes beyond fastPerFull? private: int32_t runOnce() override; void beginMaintenance(); // Excessive debt: begin unprovoked refreshing of display, for health int32_t endMaintenance(); // End unprovoked refreshing: debt paid Drivers::EInk::UpdateTypes prioritize(Drivers::EInk::UpdateTypes type1, Drivers::EInk::UpdateTypes type2); // Determine which of two update types is more important to honor bool forced = false; Drivers::EInk::UpdateTypes workingDecision = Drivers::EInk::UpdateTypes::UNSPECIFIED; float debt = 0.0; // How many full refreshes are due }; } // namespace NicheGraphics::InkHUD #endif ================================================ FILE: src/graphics/niche/InkHUD/Events.cpp ================================================ #ifdef MESHTASTIC_INCLUDE_INKHUD #include "./Events.h" #include "RTC.h" #include "buzz.h" #include "modules/ExternalNotificationModule.h" #include "modules/TextMessageModule.h" #include "sleep.h" #include "./Applet.h" #include "./SystemApplet.h" #include "graphics/niche/Utils/FlashData.h" using namespace NicheGraphics; InkHUD::Events::Events() { // Get convenient references inkhud = InkHUD::getInstance(); settings = &inkhud->persistence->settings; } void InkHUD::Events::begin() { // Register our callbacks for the various events deepSleepObserver.observe(¬ifyDeepSleep); rebootObserver.observe(¬ifyReboot); textMessageObserver.observe(textMessageModule); #if !MESHTASTIC_EXCLUDE_ADMIN adminMessageObserver.observe((Observable *)adminModule); #endif #ifdef ARCH_ESP32 lightSleepObserver.observe(¬ifyLightSleep); #endif } void InkHUD::Events::onButtonShort() { // Audio feedback (via buzzer) // Short tone playChirp(); // Cancel any beeping, buzzing, blinking // Some button handling suppressed if we are dismissing an external notification (see below) bool dismissedExt = dismissExternalNotification(); // Check which system applet wants to handle the button press (if any) SystemApplet *consumer = nullptr; for (SystemApplet *sa : inkhud->systemApplets) { if (sa->handleInput) { consumer = sa; break; } } // If no system applet is handling input, default behavior instead is to cycle applets // or open menu if joystick is enabled if (consumer) { consumer->onButtonShortPress(); } else if (!dismissedExt) { // Don't change applet if this button press silenced the external notification module Applet *userConsumer = inkhud->getActiveApplet(); if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::BUTTON_SHORT)) userConsumer->onButtonShortPress(); else { if (!settings->joystick.enabled) inkhud->nextApplet(); else inkhud->openMenu(); } } } void InkHUD::Events::onButtonLong() { // Audio feedback (via buzzer) // Slightly longer than playChirp playBoop(); // Check which system applet wants to handle the button press (if any) SystemApplet *consumer = nullptr; for (SystemApplet *sa : inkhud->systemApplets) { if (sa->handleInput) { consumer = sa; break; } } // If no system applet is handling input, default behavior instead is to open the menu if (consumer) consumer->onButtonLongPress(); else { Applet *userConsumer = inkhud->getActiveApplet(); if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::BUTTON_LONG)) userConsumer->onButtonLongPress(); else inkhud->openMenu(); } } void InkHUD::Events::onExitShort() { if (settings->joystick.enabled) { // Audio feedback (via buzzer) // Short tone playChirp(); // Cancel any beeping, buzzing, blinking // Some button handling suppressed if we are dismissing an external notification (see below) bool dismissedExt = dismissExternalNotification(); // Check which system applet wants to handle the button press (if any) SystemApplet *consumer = nullptr; for (SystemApplet *sa : inkhud->systemApplets) { if (sa->handleInput) { consumer = sa; break; } } // If no system applet is handling input, default behavior instead is change tiles if (consumer) consumer->onExitShort(); else if (!dismissedExt) { // Don't change tile if this button press silenced the external notification module Applet *userConsumer = inkhud->getActiveApplet(); if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::EXIT_SHORT)) userConsumer->onExitShort(); else inkhud->nextTile(); } } } void InkHUD::Events::onExitLong() { if (settings->joystick.enabled) { // Audio feedback (via buzzer) // Slightly longer than playChirp playBoop(); // Check which system applet wants to handle the button press (if any) SystemApplet *consumer = nullptr; for (SystemApplet *sa : inkhud->systemApplets) { if (sa->handleInput) { consumer = sa; break; } } if (consumer) consumer->onExitLong(); else { Applet *userConsumer = inkhud->getActiveApplet(); if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::EXIT_LONG)) userConsumer->onExitLong(); // Nothing uses exit long yet } } } void InkHUD::Events::onNavUp() { if (settings->joystick.enabled) { // Audio feedback (via buzzer) // Short tone playChirp(); // Cancel any beeping, buzzing, blinking // Some button handling suppressed if we are dismissing an external notification (see below) bool dismissedExt = dismissExternalNotification(); // Check which system applet wants to handle the button press (if any) SystemApplet *consumer = nullptr; for (SystemApplet *sa : inkhud->systemApplets) { if (sa->handleInput) { consumer = sa; break; } } if (consumer) consumer->onNavUp(); else if (!dismissedExt) { Applet *userConsumer = inkhud->getActiveApplet(); if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::NAV_UP)) userConsumer->onNavUp(); } } } void InkHUD::Events::onNavDown() { if (settings->joystick.enabled) { // Audio feedback (via buzzer) // Short tone playChirp(); // Cancel any beeping, buzzing, blinking // Some button handling suppressed if we are dismissing an external notification (see below) bool dismissedExt = dismissExternalNotification(); // Check which system applet wants to handle the button press (if any) SystemApplet *consumer = nullptr; for (SystemApplet *sa : inkhud->systemApplets) { if (sa->handleInput) { consumer = sa; break; } } if (consumer) consumer->onNavDown(); else if (!dismissedExt) { Applet *userConsumer = inkhud->getActiveApplet(); if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::NAV_DOWN)) userConsumer->onNavDown(); } } } void InkHUD::Events::onNavLeft() { if (settings->joystick.enabled) { // Audio feedback (via buzzer) // Short tone playChirp(); // Cancel any beeping, buzzing, blinking // Some button handling suppressed if we are dismissing an external notification (see below) bool dismissedExt = dismissExternalNotification(); // Check which system applet wants to handle the button press (if any) SystemApplet *consumer = nullptr; for (SystemApplet *sa : inkhud->systemApplets) { if (sa->handleInput) { consumer = sa; break; } } // If no system applet is handling input, default behavior instead is to cycle applets if (consumer) consumer->onNavLeft(); else if (!dismissedExt) { // Don't change applet if this button press silenced the external notification module Applet *userConsumer = inkhud->getActiveApplet(); if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::NAV_LEFT)) userConsumer->onNavLeft(); else inkhud->prevApplet(); } } } void InkHUD::Events::onNavRight() { if (settings->joystick.enabled) { // Audio feedback (via buzzer) // Short tone playChirp(); // Cancel any beeping, buzzing, blinking // Some button handling suppressed if we are dismissing an external notification (see below) bool dismissedExt = dismissExternalNotification(); // Check which system applet wants to handle the button press (if any) SystemApplet *consumer = nullptr; for (SystemApplet *sa : inkhud->systemApplets) { if (sa->handleInput) { consumer = sa; break; } } // If no system applet is handling input, default behavior instead is to cycle applets if (consumer) consumer->onNavRight(); else if (!dismissedExt) { // Don't change applet if this button press silenced the external notification module Applet *userConsumer = inkhud->getActiveApplet(); if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::NAV_RIGHT)) userConsumer->onNavRight(); else inkhud->nextApplet(); } } } void InkHUD::Events::onFreeText(char c) { // Trigger the first system applet that wants to handle the new character for (SystemApplet *sa : inkhud->systemApplets) { if (sa->handleFreeText) { sa->onFreeText(c); break; } } } void InkHUD::Events::onFreeTextDone() { // Trigger the first system applet that wants to handle it for (SystemApplet *sa : inkhud->systemApplets) { if (sa->handleFreeText) { sa->onFreeTextDone(); break; } } } void InkHUD::Events::onFreeTextCancel() { // Trigger the first system applet that wants to handle it for (SystemApplet *sa : inkhud->systemApplets) { if (sa->handleFreeText) { sa->onFreeTextCancel(); break; } } } // Callback for deepSleepObserver // Returns 0 to signal that we agree to sleep now int InkHUD::Events::beforeDeepSleep(void *unused) { // If a previous display update is in progress, wait for it to complete. inkhud->awaitUpdate(); // Notify all applets that we're shutting down for (Applet *ua : inkhud->userApplets) { ua->onDeactivate(); ua->onShutdown(); } for (SystemApplet *sa : inkhud->systemApplets) { // Note: no onDeactivate. System applets are always active. sa->onShutdown(); } // User has successful executed a safe shutdown // We don't need to nag at boot anymore settings->tips.safeShutdownSeen = true; inkhud->persistence->saveSettings(); inkhud->persistence->saveLatestMessage(); // LogoApplet::onShutdown attempted to heal the display by drawing a "shutting down" screen twice, // then prepared a final powered-off screen for us, which shows device shortname. // We're updating to show that one now. inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FULL, true, false); delay(1000); // Cooldown, before potentially yanking display power // InkHUD shutdown complete // Firmware shutdown continues for several seconds more; flash write still pending playShutdownMelody(); return 0; // We agree: deep sleep now } // Display an intermediate screen while configuration changes are applied void InkHUD::Events::applyingChanges() { // Bring the logo applet forward with a temporary message for (SystemApplet *sa : inkhud->systemApplets) { sa->onApplyingChanges(); } } // Callback for rebootObserver // Same as shutdown, without drawing the logoApplet // Makes sure we don't lose message history / InkHUD config int InkHUD::Events::beforeReboot(void *unused) { // Notify all applets that we're "shutting down" // They don't need to know that it's really a reboot for (Applet *a : inkhud->userApplets) { a->onDeactivate(); a->onShutdown(); } for (SystemApplet *sa : inkhud->systemApplets) { // Note: no onDeactivate. System applets are always active. sa->onReboot(); } // Save settings to flash, or erase if factory reset in progress if (!eraseOnReboot) { inkhud->persistence->saveSettings(); inkhud->persistence->saveLatestMessage(); } else { NicheGraphics::clearFlashData(); } // Note: no forceUpdate call here // We don't have any final screen to draw, although LogoApplet::onReboot did already display a "rebooting" screen return 0; // No special status to report. Ignored anyway by this Observable } // Callback when a new text message is received // Caches the most recently received message, for use by applets // Rx does not trigger a save to flash, however the data *will* be saved alongside other during shutdown, etc. // Note: this is different from devicestate.rx_text_message, which may contain an *outgoing* message int InkHUD::Events::onReceiveTextMessage(const meshtastic_MeshPacket *packet) { // Short circuit: don't store outgoing messages if (getFrom(packet) == nodeDB->getNodeNum()) return 0; // Determine whether the message is broadcast or a DM // Store this info to prevent confusion after a reboot // Avoids need to compare timestamps, because of situation where "future" messages block newly received, if time not set inkhud->persistence->latestMessage.wasBroadcast = isBroadcast(packet->to); // Pick the appropriate variable to store the message in MessageStore::Message *storedMessage = inkhud->persistence->latestMessage.wasBroadcast ? &inkhud->persistence->latestMessage.broadcast : &inkhud->persistence->latestMessage.dm; // Store nodenum of the sender // Applets can use this to fetch user data from nodedb, if they want storedMessage->sender = packet->from; // Store the time (epoch seconds) when message received storedMessage->timestamp = getValidTime(RTCQuality::RTCQualityDevice, true); // Current RTC time // Store the channel // - (potentially) used to determine whether notification shows // - (potentially) used to determine which applet to focus storedMessage->channelIndex = packet->channel; // Store the text // Need to specify manually how many bytes, because source not null-terminated storedMessage->text = std::string(&packet->decoded.payload.bytes[0], &packet->decoded.payload.bytes[packet->decoded.payload.size]); return 0; // Tell caller to continue notifying other observers. (No reason to abort this event) } int InkHUD::Events::onAdminMessage(AdminModule_ObserverData *data) { switch (data->request->which_payload_variant) { // Factory reset // Two possible messages. One preserves BLE bonds, other wipes. Both should clear InkHUD data. case meshtastic_AdminMessage_factory_reset_device_tag: case meshtastic_AdminMessage_factory_reset_config_tag: eraseOnReboot = true; *data->result = AdminMessageHandleResult::HANDLED; break; default: break; } return 0; // Tell caller to continue notifying other observers. (No reason to abort this event) } #ifdef ARCH_ESP32 // Callback for lightSleepObserver // Make sure the display is not partway through an update when we begin light sleep // This is because some displays require active input from us to terminate the update process, and protect the panel hardware int InkHUD::Events::beforeLightSleep(void *unused) { inkhud->awaitUpdate(); return 0; // No special status to report. Ignored anyway by this Observable } #endif // Silence all ongoing beeping, blinking, buzzing, coming from the external notification module // Returns true if an external notification was active, and we dismissed it // Button handling changes depending on our result bool InkHUD::Events::dismissExternalNotification() { // Abort if not using external notifications if (!moduleConfig.external_notification.enabled) return false; // Abort if nothing to dismiss if (!externalNotificationModule->nagging()) return false; // Stop the beep buzz blink externalNotificationModule->stopNow(); // Inform that we did indeed dismiss an external notification return true; } #endif ================================================ FILE: src/graphics/niche/InkHUD/Events.h ================================================ #ifdef MESHTASTIC_INCLUDE_INKHUD #pragma once /* Handles non-specific events for InkHUD Individual applets are responsible for listening for their own events via the module api etc, however this class handles general events which concern InkHUD as a whole, e.g. shutdown */ #include "configuration.h" #include "modules/AdminModule.h" #include "./InkHUD.h" #include "./Persistence.h" namespace NicheGraphics::InkHUD { class Events { public: Events(); void begin(); void onButtonShort(); // User button: short press void onButtonLong(); // User button: long press void applyingChanges(); void onExitShort(); // Exit button: short press void onExitLong(); // Exit button: long press void onNavUp(); // Navigate up void onNavDown(); // Navigate down void onNavLeft(); // Navigate left void onNavRight(); // Navigate right // Free text typing events void onFreeText(char c); // New freetext character input void onFreeTextDone(); void onFreeTextCancel(); int beforeDeepSleep(void *unused); // Prepare for shutdown int beforeReboot(void *unused); // Prepare for reboot int onReceiveTextMessage(const meshtastic_MeshPacket *packet); // Store most recent text message int onAdminMessage(AdminModule_ObserverData *data); // Handle incoming admin messages #ifdef ARCH_ESP32 int beforeLightSleep(void *unused); // Prepare for light sleep #endif private: // For convenience InkHUD *inkhud = nullptr; Persistence::Settings *settings = nullptr; // Get notified when the system is shutting down CallbackObserver deepSleepObserver = CallbackObserver(this, &Events::beforeDeepSleep); // Get notified when the system is rebooting CallbackObserver rebootObserver = CallbackObserver(this, &Events::beforeReboot); // Cache *incoming* text messages, for use by applets CallbackObserver textMessageObserver = CallbackObserver(this, &Events::onReceiveTextMessage); // Get notified of incoming admin messages, and handle any which are relevant to InkHUD CallbackObserver adminMessageObserver = CallbackObserver(this, &Events::onAdminMessage); #ifdef ARCH_ESP32 // Get notified when the system is entering light sleep CallbackObserver lightSleepObserver = CallbackObserver(this, &Events::beforeLightSleep); #endif // End any externalNotification beeping, buzzing, blinking etc bool dismissExternalNotification(); // If set, InkHUD's data will be erased during onReboot bool eraseOnReboot = false; }; } // namespace NicheGraphics::InkHUD #endif ================================================ FILE: src/graphics/niche/InkHUD/InkHUD.cpp ================================================ #ifdef MESHTASTIC_INCLUDE_INKHUD #include "./InkHUD.h" #include "./Applet.h" #include "./Events.h" #include "./Persistence.h" #include "./Renderer.h" #include "./SystemApplet.h" #include "./Tile.h" #include "./WindowManager.h" using namespace NicheGraphics; // Get or create the singleton InkHUD::InkHUD *InkHUD::InkHUD::getInstance() { // Create the singleton instance of our class, if not yet done static InkHUD *instance = nullptr; if (!instance) { instance = new InkHUD; instance->persistence = new Persistence; instance->windowManager = new WindowManager; instance->renderer = new Renderer; instance->events = new Events; } return instance; } // Connect the (fully set-up) E-Ink driver to InkHUD // Should happen in your variant's nicheGraphics.h file, before InkHUD::begin is called void InkHUD::InkHUD::setDriver(Drivers::EInk *driver) { renderer->setDriver(driver); } // Set the target number of FAST display updates in a row, before a FULL update is used for display health // This value applies only to updates with an UNSPECIFIED update type // If explicitly requested FAST updates exceed this target, the stressMultiplier parameter determines how many // subsequent FULL updates will be performed, in an attempt to restore the display's health void InkHUD::InkHUD::setDisplayResilience(uint8_t fastPerFull, float stressMultiplier) { renderer->setDisplayResilience(fastPerFull, stressMultiplier); } // Register a user applet with InkHUD // A variant's nicheGraphics.h file should instantiate your chosen applets, then pass them to this method // Passing an applet to this method is all that is required to make it available to the user in your InkHUD build void InkHUD::InkHUD::addApplet(const char *name, Applet *a, bool defaultActive, bool defaultAutoshow, uint8_t onTile) { windowManager->addApplet(name, a, defaultActive, defaultAutoshow, onTile); } void InkHUD::InkHUD::notifyApplyingChanges() { if (events) { events->applyingChanges(); } } // Start InkHUD! // Call this only after you have configured InkHUD void InkHUD::InkHUD::begin() { persistence->loadSettings(); persistence->loadLatestMessage(); windowManager->begin(); events->begin(); renderer->begin(); // LogoApplet shows boot screen here } // Call this when your user button gets a short press // Should be connected to an input source in nicheGraphics.h (NicheGraphics::Inputs::TwoButton?) void InkHUD::InkHUD::shortpress() { events->onButtonShort(); } // Call this when your user button gets a long press // Should be connected to an input source in nicheGraphics.h (NicheGraphics::Inputs::TwoButton?) void InkHUD::InkHUD::longpress() { events->onButtonLong(); } // Call this when your exit button gets a short press void InkHUD::InkHUD::exitShort() { events->onExitShort(); } // Call this when your exit button gets a long press void InkHUD::InkHUD::exitLong() { events->onExitLong(); } // Call this when your joystick gets an up input void InkHUD::InkHUD::navUp() { switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) { case 1: // 90 deg events->onNavLeft(); break; case 2: // 180 deg events->onNavDown(); break; case 3: // 270 deg events->onNavRight(); break; default: // 0 deg events->onNavUp(); break; } } // Call this when your joystick gets a down input void InkHUD::InkHUD::navDown() { switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) { case 1: // 90 deg events->onNavRight(); break; case 2: // 180 deg events->onNavUp(); break; case 3: // 270 deg events->onNavLeft(); break; default: // 0 deg events->onNavDown(); break; } } // Call this when your joystick gets a left input void InkHUD::InkHUD::navLeft() { switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) { case 1: // 90 deg events->onNavDown(); break; case 2: // 180 deg events->onNavRight(); break; case 3: // 270 deg events->onNavUp(); break; default: // 0 deg events->onNavLeft(); break; } } // Call this when your joystick gets a right input void InkHUD::InkHUD::navRight() { switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) { case 1: // 90 deg events->onNavUp(); break; case 2: // 180 deg events->onNavLeft(); break; case 3: // 270 deg events->onNavDown(); break; default: // 0 deg events->onNavRight(); break; } } // Call this for keyboard input // The Keyboard Applet also calls this void InkHUD::InkHUD::freeText(char c) { events->onFreeText(c); } // Call this to complete a freetext input void InkHUD::InkHUD::freeTextDone() { events->onFreeTextDone(); } // Call this to cancel a freetext input void InkHUD::InkHUD::freeTextCancel() { events->onFreeTextCancel(); } // Cycle the next user applet to the foreground // Only activated applets are cycled // If user has a multi-applet layout, the applets will cycle on the "focused tile" void InkHUD::InkHUD::nextApplet() { windowManager->nextApplet(); } // Cycle the previous user applet to the foreground // Only activated applets are cycled // If user has a multi-applet layout, the applets will cycle on the "focused tile" void InkHUD::InkHUD::prevApplet() { windowManager->prevApplet(); } // Returns the currently active applet InkHUD::Applet *InkHUD::InkHUD::getActiveApplet() { return windowManager->getActiveApplet(); } // Show the menu (on the the focused tile) // The applet previously displayed there will be restored once the menu closes void InkHUD::InkHUD::openMenu() { windowManager->openMenu(); } // Bring AlignStick applet to the foreground void InkHUD::InkHUD::openAlignStick() { windowManager->openAlignStick(); } // Open the on-screen keyboard void InkHUD::InkHUD::openKeyboard() { windowManager->openKeyboard(); } // Close the on-screen keyboard void InkHUD::InkHUD::closeKeyboard() { windowManager->closeKeyboard(); } // In layouts where multiple applets are shown at once, change which tile is focused // The focused tile in the one which cycles applets on button short press, and displays menu on long press void InkHUD::InkHUD::nextTile() { windowManager->nextTile(); } // In layouts where multiple applets are shown at once, change which tile is focused // The focused tile in the one which cycles applets on button short press, and displays menu on long press void InkHUD::InkHUD::prevTile() { windowManager->prevTile(); } // Rotate the display image by 90 degrees void InkHUD::InkHUD::rotate() { windowManager->rotate(); } // rotate the joystick in 90 degree increments void InkHUD::InkHUD::rotateJoystick(uint8_t angle) { persistence->settings.joystick.alignment += angle; persistence->settings.joystick.alignment %= 4; } // Show / hide the battery indicator in top-right void InkHUD::InkHUD::toggleBatteryIcon() { windowManager->toggleBatteryIcon(); } // An applet asking for the display to be updated // This does not occur immediately // Instead, rendering is scheduled ASAP, for the next Renderer::runOnce call // This allows multiple applets to observe the same event, and then share the same opportunity to update // Applets should requestUpdate, whether or not they are currently displayed ("foreground") // This is because they *might* be automatically brought to foreground by WindowManager::autoshow void InkHUD::InkHUD::requestUpdate() { renderer->requestUpdate(); } // Demand that the display be updated // Ignores all diplomacy: // - the display *will* update // - the specified update type *will* be used // If the all parameter is true, the whole screen buffer is cleared and re-rendered // If the async parameter is false, code flow is blocked while the update takes place void InkHUD::InkHUD::forceUpdate(EInk::UpdateTypes type, bool all, bool async) { renderer->forceUpdate(type, all, async); } // Wait for any in-progress display update to complete before continuing void InkHUD::InkHUD::awaitUpdate() { renderer->awaitUpdate(); } // Ask the window manager to potentially bring a different user applet to foreground // An applet will be brought to foreground if it has just received new and relevant info // For Example: AllMessagesApplet has just received a new text message // Permission for this autoshow behavior is granted by the user, on an applet-by-applet basis // If autoshow brings an applet to foreground, an InkHUD notification will not be generated for the same event void InkHUD::InkHUD::autoshow() { windowManager->autoshow(); } // Tell the window manager that the Persistence::Settings value for applet activation has changed, // and that it should reconfigure accordingly. // This is triggered at boot, or when the user enables / disabled applets via the on-screen menu void InkHUD::InkHUD::updateAppletSelection() { windowManager->changeActivatedApplets(); } // Tell the window manager that the Persistence::Settings value for layout or rotation has changed, // and that it should reconfigure accordingly. // This is triggered at boot, or by rotate / layout options in the on-screen menu void InkHUD::InkHUD::updateLayout() { windowManager->changeLayout(); } // Width of the display, in the context of the current rotation uint16_t InkHUD::InkHUD::width() { return renderer->width(); } // Height of the display, in the context of the current rotation uint16_t InkHUD::InkHUD::height() { return renderer->height(); } // A collection of any user tiles which do not have a valid user applet // This can occur in various situations, such as when a user enables fewer applets than their layout has tiles // The tiles (and which regions the occupy) are private information of the window manager // The renderer needs to know which regions (if any) are empty, // in order to fill them with a "placeholder" pattern. // -- There may be a tidier way to accomplish this -- std::vector InkHUD::InkHUD::getEmptyTiles() { return windowManager->getEmptyTiles(); } // Get a system applet by its name // This isn't particularly elegant, but it does avoid: // - passing around a big set of references // - having two sets of references (systemApplet vector for iteration) InkHUD::SystemApplet *InkHUD::InkHUD::getSystemApplet(const char *name) { for (SystemApplet *sa : systemApplets) { if (strcmp(name, sa->name) == 0) return sa; } assert(false); // Invalid name } // Place a pixel into the image buffer // The x and y coordinates are in the context of the current display rotation // - Applets pass "relative" pixels to tiles // - Tiles pass translated pixels to this method // - this methods (Renderer) places rotated pixels into the image buffer // This method provides the final formatting step required. The image buffer is suitable for writing to display void InkHUD::InkHUD::drawPixel(int16_t x, int16_t y, Color c) { renderer->handlePixel(x, y, c); } #endif ================================================ FILE: src/graphics/niche/InkHUD/InkHUD.h ================================================ #ifdef MESHTASTIC_INCLUDE_INKHUD /* InkHUD's main class - singleton - mediator between the various components */ #pragma once #include "configuration.h" #include "graphics/niche/Drivers/EInk/EInk.h" #include "./AppletFont.h" #include namespace NicheGraphics::InkHUD { // Color, understood by display controller IC (as bit values) // Also suitable for use as AdafruitGFX colors enum Color : uint8_t { BLACK = 0, WHITE = 1, }; class Applet; class Events; class Persistence; class Renderer; class SystemApplet; class Tile; class WindowManager; class InkHUD { public: static InkHUD *getInstance(); // Access to this singleton class // Configuration // - before InkHUD::begin, in variant nicheGraphics.h, void setDriver(Drivers::EInk *driver); void setDisplayResilience(uint8_t fastPerFull = 5, float stressMultiplier = 2.0); void addApplet(const char *name, Applet *a, bool defaultActive = false, bool defaultAutoshow = false, uint8_t onTile = -1); void notifyApplyingChanges(); void begin(); // Handle user-button press // - connected to an input source, in variant nicheGraphics.h void shortpress(); void longpress(); void exitShort(); void exitLong(); void navUp(); void navDown(); void navLeft(); void navRight(); // Freetext handlers void freeText(char c); void freeTextDone(); void freeTextCancel(); // Trigger UI changes // - called by various InkHUD components // - suitable(?) for use by aux button, connected in variant nicheGraphics.h void nextApplet(); void prevApplet(); NicheGraphics::InkHUD::Applet *getActiveApplet(); void openMenu(); void openAlignStick(); void openKeyboard(); void closeKeyboard(); void nextTile(); void prevTile(); void rotate(); void rotateJoystick(uint8_t angle = 1); // rotate 90 deg by default void toggleBatteryIcon(); // Used by TipsApplet to force menu to start on Region selection bool forceRegionMenu = false; // Input mode hint for devices that use a left/right rocker plus center button bool twoWayRocker = false; // Updating the display // - called by various InkHUD components void requestUpdate(); void forceUpdate(Drivers::EInk::UpdateTypes type = Drivers::EInk::UpdateTypes::UNSPECIFIED, bool all = false, bool async = true); void awaitUpdate(); // (Re)configuring WindowManager void autoshow(); // Bring an applet to foreground void updateAppletSelection(); // Change which applets are active void updateLayout(); // Change multiplexing (count, rotation) // Information passed between components uint16_t width(); // From E-Ink driver uint16_t height(); // From E-Ink driver std::vector getEmptyTiles(); // From WindowManager // Applets SystemApplet *getSystemApplet(const char *name); std::vector userApplets; std::vector systemApplets; // Pass drawing output to Renderer void drawPixel(int16_t x, int16_t y, Color c); // Shared data which persists between boots Persistence *persistence = nullptr; private: InkHUD() {} // Constructor made private to force use of InkHUD::getInstance Events *events = nullptr; // Handle non-specific firmware events Renderer *renderer = nullptr; // Co-ordinate display updates WindowManager *windowManager = nullptr; // Multiplexing of applets }; } // namespace NicheGraphics::InkHUD #endif ================================================ FILE: src/graphics/niche/InkHUD/MessageStore.cpp ================================================ #ifdef MESHTASTIC_INCLUDE_INKHUD #include "./MessageStore.h" #include "SafeFile.h" using namespace NicheGraphics; // Hard limits on how much message data to write to flash // Avoid filling the storage if something goes wrong // Normal usage should be well below this size constexpr uint8_t MAX_MESSAGES_SAVED = 10; constexpr uint32_t MAX_MESSAGE_SIZE = 250; InkHUD::MessageStore::MessageStore(const std::string &label) { filename = ""; filename += "/NicheGraphics"; filename += "/"; filename += label; filename += ".msgs"; } // Write the contents of the MessageStore::messages object to flash // Takes the firmware's SPI lock during FS operations. Implemented for consistency, but only relevant when using SD card. // Need to lock and unlock around specific FS methods, as the SafeFile class takes the lock for itself internally void InkHUD::MessageStore::saveToFlash() { assert(!filename.empty()); #ifdef FSCom // Make the directory, if doesn't already exist // This is the same directory accessed by NicheGraphics::FlashData spiLock->lock(); FSCom.mkdir("/NicheGraphics"); spiLock->unlock(); // Open or create the file // No "full atomic": don't save then rename auto f = SafeFile(filename.c_str(), false); LOG_INFO("Saving messages in %s", filename.c_str()); // Take firmware's SPI Lock while writing spiLock->lock(); // 1st byte: how many messages will be written to store f.write(messages.size()); // For each message for (uint8_t i = 0; i < messages.size() && i < MAX_MESSAGES_SAVED; i++) { Message &m = messages.at(i); f.write(reinterpret_cast(&m.timestamp), sizeof(m.timestamp)); // Write timestamp. 4 bytes f.write(reinterpret_cast(&m.sender), sizeof(m.sender)); // Write sender NodeId. 4 Bytes f.write(reinterpret_cast(&m.channelIndex), sizeof(m.channelIndex)); // Write channel index. 1 Byte f.write(reinterpret_cast(m.text.c_str()), min(MAX_MESSAGE_SIZE, m.text.size())); // Write message text f.write('\0'); // Append null term LOG_DEBUG("Wrote message %u, length %u, text \"%s\"", static_cast(i), min(MAX_MESSAGE_SIZE, m.text.size()), m.text.c_str()); } // Release firmware's SPI lock, because SafeFile::close needs it spiLock->unlock(); bool writeSucceeded = f.close(); if (!writeSucceeded) { LOG_ERROR("Can't write data!"); } #else LOG_ERROR("ERROR: Filesystem not implemented\n"); #endif } // Attempt to load the previous contents of the MessageStore:message deque from flash. // Filename is controlled by the "label" parameter // Takes the firmware's SPI lock during FS operations. Implemented for consistency, but only relevant when using SD card. void InkHUD::MessageStore::loadFromFlash() { // Hopefully redundant. Initial intention is to only load / save once per boot. messages.clear(); #ifdef FSCom // Take the firmware's SPI Lock, in case filesystem is on SD card concurrency::LockGuard guard(spiLock); // Check that the file *does* actually exist if (!FSCom.exists(filename.c_str())) { LOG_WARN("'%s' not found. Using default values", filename.c_str()); return; } // Check that the file *does* actually exist if (!FSCom.exists(filename.c_str())) { LOG_INFO("'%s' not found.", filename.c_str()); return; } // Open the file auto f = FSCom.open(filename.c_str(), FILE_O_READ); if (f.size() == 0) { LOG_INFO("%s is empty", filename.c_str()); f.close(); return; } // If opened, start reading if (f) { LOG_INFO("Loading threaded messages '%s'", filename.c_str()); // First byte: how many messages are in the flash store uint8_t flashMessageCount = 0; f.readBytes(reinterpret_cast(&flashMessageCount), 1); LOG_DEBUG("Messages available: %u", static_cast(flashMessageCount)); // For each message for (uint8_t i = 0; i < flashMessageCount && i < MAX_MESSAGES_SAVED; i++) { Message m; // Read meta data (fixed width) f.readBytes(reinterpret_cast(&m.timestamp), sizeof(m.timestamp)); f.readBytes(reinterpret_cast(&m.sender), sizeof(m.sender)); f.readBytes(reinterpret_cast(&m.channelIndex), sizeof(m.channelIndex)); // Read characters until we find a null term char c; while (m.text.size() < MAX_MESSAGE_SIZE) { f.readBytes(&c, 1); if (c != '\0') m.text += c; else break; } // Store in RAM messages.push_back(m); LOG_DEBUG("#%u, timestamp=%u, sender(num)=%u, text=\"%s\"", static_cast(i), m.timestamp, m.sender, m.text.c_str()); } f.close(); } else { LOG_ERROR("Could not open / read %s", filename.c_str()); } #else LOG_ERROR("Filesystem not implemented"); state = LoadFileState::NO_FILESYSTEM; #endif return; } #endif ================================================ FILE: src/graphics/niche/InkHUD/MessageStore.h ================================================ #ifdef MESHTASTIC_INCLUDE_INKHUD /* We hold a few recent messages, for features like the threaded message applet. This class contains a struct for storing those messages, and methods for serializing them to flash. */ #pragma once #include "configuration.h" #include #include "mesh/MeshTypes.h" namespace NicheGraphics::InkHUD { class MessageStore { public: // A stored message struct Message { uint32_t timestamp; // Epoch seconds NodeNum sender = 0; uint8_t channelIndex; std::string text; }; MessageStore() = delete; explicit MessageStore(const std::string &label); // Label determines filename in flash void saveToFlash(); void loadFromFlash(); std::deque messages; // Interact with this object! private: std::string filename; }; } // namespace NicheGraphics::InkHUD #endif ================================================ FILE: src/graphics/niche/InkHUD/Persistence.cpp ================================================ #ifdef MESHTASTIC_INCLUDE_INKHUD #include "./Persistence.h" using namespace NicheGraphics; // Load settings and latestMessage data void InkHUD::Persistence::loadSettings() { // Load the InkHUD settings from flash, and check version number // We should only consider the version number if the InkHUD flashdata component reports that we *did* actually load flash data Settings loadedSettings; bool loadSucceeded = FlashData::load(&loadedSettings, "settings"); if (loadSucceeded && loadedSettings.meta.version == SETTINGS_VERSION && loadedSettings.meta.version != 0) settings = loadedSettings; // Version matched, replace the defaults with the loaded values else LOG_WARN("Settings version changed. Using defaults"); } // Load settings and latestMessage data void InkHUD::Persistence::loadLatestMessage() { // Load previous "latestMessages" data from flash MessageStore store("latest"); store.loadFromFlash(); // Place into latestMessage struct, for convenient access // Number of strings loaded determines whether last message was broadcast or dm if (store.messages.size() == 1) { latestMessage.dm = store.messages.at(0); latestMessage.wasBroadcast = false; } else if (store.messages.size() == 2) { latestMessage.dm = store.messages.at(0); latestMessage.broadcast = store.messages.at(1); latestMessage.wasBroadcast = true; } } // Save the InkHUD settings to flash void InkHUD::Persistence::saveSettings() { FlashData::save(&settings, "settings"); } // Save latestMessage data to flash void InkHUD::Persistence::saveLatestMessage() { // Number of strings saved determines whether last message was broadcast or dm MessageStore store("latest"); store.messages.push_back(latestMessage.dm); if (latestMessage.wasBroadcast) store.messages.push_back(latestMessage.broadcast); store.saveToFlash(); } /* void InkHUD::Persistence::printSettings(Settings *settings) { if (SETTINGS_VERSION != 2) LOG_WARN("Persistence::printSettings was written for SETTINGS_VERSION=2, current is %d", SETTINGS_VERSION); LOG_DEBUG("meta.version=%d", settings->meta.version); LOG_DEBUG("userTiles.count=%d", settings->userTiles.count); LOG_DEBUG("userTiles.maxCount=%d", settings->userTiles.maxCount); LOG_DEBUG("userTiles.focused=%d", settings->userTiles.focused); for (uint8_t i = 0; i < MAX_TILES_GLOBAL; i++) LOG_DEBUG("userTiles.displayedUserApplet[%d]=%d", i, settings->userTiles.displayedUserApplet[i]); for (uint8_t i = 0; i < MAX_USERAPPLETS_GLOBAL; i++) LOG_DEBUG("userApplets.active[%d]=%d", i, settings->userApplets.active[i]); for (uint8_t i = 0; i < MAX_USERAPPLETS_GLOBAL; i++) LOG_DEBUG("userApplets.autoshow[%d]=%d", i, settings->userApplets.autoshow[i]); LOG_DEBUG("optionalFeatures.notifications=%d", settings->optionalFeatures.notifications); LOG_DEBUG("optionalFeatures.batteryIcon=%d", settings->optionalFeatures.batteryIcon); LOG_DEBUG("optionalMenuItems.nextTile=%d", settings->optionalMenuItems.nextTile); LOG_DEBUG("optionalMenuItems.backlight=%d", settings->optionalMenuItems.backlight); LOG_DEBUG("tips.firstBoot=%d", settings->tips.firstBoot); LOG_DEBUG("tips.safeShutdownSeen=%d", settings->tips.safeShutdownSeen); LOG_DEBUG("rotation=%d", settings->rotation); LOG_DEBUG("recentlyActiveSeconds=%d", settings->recentlyActiveSeconds); } */ #endif ================================================ FILE: src/graphics/niche/InkHUD/Persistence.h ================================================ #ifdef MESHTASTIC_INCLUDE_INKHUD /* A quick and dirty alternative to storing "device only" settings using the protobufs Convenient during development. Potentially a polite option, to avoid polluting the generated code with values for obscure use cases like this. The save / load mechanism is a shared NicheGraphics feature. */ #pragma once #include "configuration.h" #include "./InkHUD.h" #include "graphics/niche/InkHUD/MessageStore.h" #include "graphics/niche/Utils/FlashData.h" namespace NicheGraphics::InkHUD { class Persistence { public: static constexpr uint8_t MAX_TILES_GLOBAL = 4; static constexpr uint8_t MAX_USERAPPLETS_GLOBAL = 16; // Used to invalidate old settings, if needed // Version 0 is reserved for testing, and will always load defaults static constexpr uint32_t SETTINGS_VERSION = 3; struct Settings { struct Meta { // Used to invalidate old savefiles, if we make breaking changes uint32_t version = SETTINGS_VERSION; } meta; struct UserTiles { // How many tiles are shown uint8_t count = 1; // Maximum amount of tiles for this display uint8_t maxCount = 4; // Which tile is focused (responding to user button input) uint8_t focused = 0; // Which applet is displayed on which tile // Index of array: which tile, as indexed in WindowManager::userTiles // Value of array: which applet, as indexed in InkHUD::userApplets uint8_t displayedUserApplet[MAX_TILES_GLOBAL] = {0, 1, 2, 3}; } userTiles; struct UserApplets { // Which applets are running (either displayed, or in the background) // Index of array: which applet, as indexed in InkHUD::userApplets // Initial value is set by the "activeByDefault" parameter of InkHUD::addApplet, in setupNicheGraphics method bool active[MAX_USERAPPLETS_GLOBAL]{false}; // Which user applets should be automatically shown when they have important data to show // If none set, foreground applets should remain foreground without manual user input // If multiple applets request this at once, // priority is the order which they were passed to InkHUD::addApplets, in setupNicheGraphics method bool autoshow[MAX_USERAPPLETS_GLOBAL]{false}; } userApplets; // Features which the user can enable / disable via the on-screen menu struct OptionalFeatures { bool notifications = true; bool batteryIcon = false; } optionalFeatures; // Some menu items may not be required, based on device / configuration // We can enable them only when needed, to de-clutter the menu struct OptionalMenuItems { // If aux button is used to swap between tiles, we have no need for this menu item bool nextTile = true; // Used if backlight present, and not controlled by AUX button // If this item is added to menu: backlight is always active when menu is open // The added menu items then allows the user to "Keep Backlight On", globally. bool backlight = false; } optionalMenuItems; // Allows tips to be run once only struct Tips { // Enables the longer "tutorial" shown only on first boot // Once tutorial has been completed, it is no longer shown bool firstBoot = true; // User is advised to shut down before removing device power // Once user executes a shutdown (either via menu or client app), // this tip is no longer shown bool safeShutdownSeen = false; } tips; // Joystick settings for enabling and aligning to the screen struct Joystick { // Modifies the UI for joystick use bool enabled = false; // gets set to true when AlignStick applet is completed bool aligned = false; // Rotation of the joystick // Multiples of 90 degrees clockwise uint8_t alignment = 0; } joystick; // Rotation of the display // Multiples of 90 degrees clockwise // Most commonly: rotation is 0 when flex connector is oriented below display uint8_t rotation = 0; // How long do we consider another node to be "active"? // Used when applets want to filter for "active nodes" only uint32_t recentlyActiveSeconds = 2 * 60; }; // Most recently received text message // Value is updated by InkHUD::WindowManager, as a courtesy to applets // Note: different from devicestate.rx_text_message, // which may contain an *outgoing message* to broadcast struct LatestMessage { MessageStore::Message broadcast; // Most recent message received broadcast MessageStore::Message dm; // Most recent received DM bool wasBroadcast; // True if most recent broadcast is newer than most recent dm }; void loadSettings(); void saveSettings(); void loadLatestMessage(); void saveLatestMessage(); // void printSettings(Settings *settings); // Debugging use only Settings settings; LatestMessage latestMessage; }; } // namespace NicheGraphics::InkHUD #endif ================================================ FILE: src/graphics/niche/InkHUD/PlatformioConfig.ini ================================================ [inkhud] build_src_filter = +; Include the nicheGraphics directory build_flags = -D MESHTASTIC_INCLUDE_NICHE_GRAPHICS ; Use NicheGraphics -D MESHTASTIC_INCLUDE_INKHUD ; Use InkHUD (a NicheGraphics UI) -D MESHTASTIC_EXCLUDE_SCREEN ; Suppress default Screen class -D MESHTASTIC_EXCLUDE_INPUTBROKER ; Suppress default input handling -D HAS_BUTTON=0 ; Suppress default ButtonThread lib_deps = # renovate: datasource=github-tags depName=GFX_Root packageName=ZinggJM/GFX_Root https://github.com/ZinggJM/GFX_Root/archive/3195764e352a0d2567c8d277ac408ca7293a99b0.zip ; Used by InkHUD as a "slimmer" version of AdafruitGFX ================================================ FILE: src/graphics/niche/InkHUD/Renderer.cpp ================================================ #ifdef MESHTASTIC_INCLUDE_INKHUD #include "./Renderer.h" #include "main.h" #include "./Applet.h" #include "./SystemApplet.h" #include "./Tile.h" using namespace NicheGraphics; InkHUD::Renderer::Renderer() : concurrency::OSThread("Renderer") { // Nothing for the timer to do just yet OSThread::disable(); // Convenient references inkhud = InkHUD::getInstance(); settings = &inkhud->persistence->settings; } // Connect the (fully set-up) E-Ink driver to InkHUD // Should happen in your variant's nicheGraphics.h file, before InkHUD::begin is called void InkHUD::Renderer::setDriver(Drivers::EInk *driver) { // Make sure not already set if (this->driver) { LOG_ERROR("Driver already set"); delay(2000); // Wait for native serial.. assert(false); } // Store the driver which was created in setupNicheGraphics() this->driver = driver; // Determine the dimensions of the image buffer, in bytes. // Along rows, pixels are stored 8 per byte. // Not all display widths are divisible by 8. Need to make sure bytecount accommodates padding for these. imageBufferWidth = ((driver->width - 1) / 8) + 1; imageBufferHeight = driver->height; // Allocate the image buffer imageBuffer = new uint8_t[imageBufferWidth * imageBufferHeight]; } // Set the target number of FAST display updates in a row, before a FULL update is used for display health // This value applies only to updates with an UNSPECIFIED update type // If explicitly requested FAST updates exceed this target, the stressMultiplier parameter determines how many // subsequent FULL updates will be performed, in an attempt to restore the display's health void InkHUD::Renderer::setDisplayResilience(uint8_t fastPerFull, float stressMultiplier) { displayHealth.fastPerFull = fastPerFull; displayHealth.stressMultiplier = stressMultiplier; } void InkHUD::Renderer::begin() { forceUpdate(Drivers::EInk::UpdateTypes::FULL, true, false); } // Set a flag, which will be picked up by runOnce, ASAP. // Quite likely, multiple applets will all want to respond to one event (Observable, etc) // Each affected applet can independently call requestUpdate(), and all share the one opportunity to render, at next runOnce void InkHUD::Renderer::requestUpdate(bool all) { requested = true; renderAll |= all; // We will run the thread as soon as we loop(), // after all Applets have had a chance to observe whatever event set this off OSThread::setIntervalFromNow(0); OSThread::enabled = true; runASAP = true; } // requestUpdate will not actually update if no requests were made by applets which are actually visible // This can occur, because applets requestUpdate even from the background, // in case the user's autoshow settings permit them to be moved to foreground. // Sometimes, however, we will want to trigger a display update manually, in the absence of any sort of applet event // Display health, for example. // In these situations, we use forceUpdate void InkHUD::Renderer::forceUpdate(Drivers::EInk::UpdateTypes type, bool all, bool async) { requested = true; forced = true; renderAll |= all; displayHealth.forceUpdateType(type); // Normally, we need to start the timer, in case the display is busy and we briefly defer the update if (async) { // We will run the thread as soon as we loop(), // after all Applets have had a chance to observe whatever event set this off OSThread::setIntervalFromNow(0); OSThread::enabled = true; runASAP = true; } // If the update is *not* asynchronous, we begin the render process directly here // so that it can block code flow while running else render(false); } // Wait for any in-progress display update to complete before continuing void InkHUD::Renderer::awaitUpdate() { if (driver->busy()) { LOG_INFO("Waiting for display"); driver->await(); // Wait here for update to complete } } // Set a ready-to-draw pixel into the image buffer // All rotations / translations have already taken place: this buffer data is formatted ready for the driver void InkHUD::Renderer::handlePixel(int16_t x, int16_t y, Color c) { rotatePixelCoords(&x, &y); uint32_t byteNum = (y * imageBufferWidth) + (x / 8); // X data is 8 pixels per byte uint8_t bitNum = 7 - (x % 8); // Invert order: leftmost bit (most significant) is leftmost pixel of byte. bitWrite(imageBuffer[byteNum], bitNum, c); } // Width of the display, relative to rotation uint16_t InkHUD::Renderer::width() { if (settings->rotation % 2) return driver->height; else return driver->width; } // Height of the display, relative to rotation uint16_t InkHUD::Renderer::height() { if (settings->rotation % 2) return driver->width; else return driver->height; } // Runs at regular intervals // - postponing render: until next loop(), allowing all applets to be notified of some Mesh event before render // - queuing another render: while one is already is progress int32_t InkHUD::Renderer::runOnce() { // If an applet asked to render, and hardware is able, lets try now if (requested && !driver->busy()) { render(); } // If our render() call failed, try again shortly // otherwise, stop our thread until next update due if (requested) return 250UL; else return OSThread::disable(); } // Applies the system-wide rotation to pixel positions // This step is applied to image data which has already been translated by a Tile object // This is the final step before the pixel is placed into the image buffer // No return: values of the *x and *y parameters are modified by the method void InkHUD::Renderer::rotatePixelCoords(int16_t *x, int16_t *y) { // Apply a global rotation to pixel locations int16_t x1 = 0; int16_t y1 = 0; switch (settings->rotation) { case 0: x1 = *x; y1 = *y; break; case 1: x1 = (driver->width - 1) - *y; y1 = *x; break; case 2: x1 = (driver->width - 1) - *x; y1 = (driver->height - 1) - *y; break; case 3: x1 = *y; y1 = (driver->height - 1) - *x; break; } *x = x1; *y = y1; } // Make an attempt to gather image data from some / all applets, and update the display // Might not be possible right now, if update already is progress. void InkHUD::Renderer::render(bool async) { // Make sure the display is ready for a new update if (async) { // Previous update still running, Will try again shortly, via runOnce() if (driver->busy()) return; } else { // Wait here for previous update to complete driver->await(); } // Determine if a system applet has requested exclusive rights to request an update, // or exclusive rights to render checkLocks(); // (Potentially) change applet to display new info, // then check if this newly displayed applet makes a pending notification redundant inkhud->autoshow(); // If an update is justified. // We don't know this until after autoshow has run, as new applets may now be in foreground if (shouldUpdate()) { // Decide which technique the display will use to change image // Done early, as rendering resets the Applets' requested types Drivers::EInk::UpdateTypes updateType = decideUpdateType(); // Render the new image if (renderAll) clearBuffer(); renderUserApplets(); renderPlaceholders(); renderSystemApplets(); // Invert Buffer if set by user if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { for (size_t i = 0; i < imageBufferWidth * imageBufferHeight; ++i) { imageBuffer[i] = ~imageBuffer[i]; } } // Tell display to begin process of drawing new image LOG_INFO("Updating display"); driver->update(imageBuffer, updateType); // If not async, wait here until the update is complete if (!async) driver->await(); } // Our part is done now. // If update is async, the display hardware is still performing the update process, // but that's all handled by NicheGraphics::Drivers::EInk // Tidy up, ready for a new request requested = false; forced = false; renderAll = false; } // Manually fill the image buffer with WHITE // Clears any old drawing // Note: benchmarking revealed that this is *much* faster than setting pixels individually // So much so that it's more efficient to re-render all applets, // rather than rendering selectively, and manually blanking a portion of the display void InkHUD::Renderer::clearBuffer() { memset(imageBuffer, 0xFF, imageBufferHeight * imageBufferWidth); } // Manually clear the pixels below a tile void InkHUD::Renderer::clearTile(Tile *t) { // Rotate the tile dimensions int16_t left = 0; int16_t top = 0; uint16_t tileW = 0; uint16_t tileH = 0; switch (settings->rotation) { case 0: left = t->getLeft(); top = t->getTop(); tileW = t->getWidth(); tileH = t->getHeight(); break; case 1: left = driver->width - (t->getTop() + t->getHeight()); top = t->getLeft(); tileW = t->getHeight(); tileH = t->getWidth(); break; case 2: left = driver->width - (t->getLeft() + t->getWidth()); top = driver->height - (t->getTop() + t->getHeight()); tileW = t->getWidth(); tileH = t->getHeight(); break; case 3: left = t->getTop(); top = driver->height - (t->getLeft() + t->getWidth()); tileW = t->getHeight(); tileH = t->getWidth(); break; } // Calculate the bounds to clear uint16_t xStart = (left < 0) ? 0 : left; uint16_t yStart = (top < 0) ? 0 : top; if (xStart >= driver->width || yStart >= driver->height || left + tileW < 0 || top + tileH < 0) return; // the box is completely off the screen uint16_t xEnd = left + tileW; uint16_t yEnd = top + tileH; if (xEnd > driver->width) xEnd = driver->width; if (yEnd > driver->height) yEnd = driver->height; // Clear the pixels if (xStart == 0 && xEnd == driver->width) { // full width box is easier to clear memset(imageBuffer + (yStart * imageBufferWidth), 0xFF, (yEnd - yStart) * imageBufferWidth); } else { const uint16_t byteStart = (xStart / 8) + 1; const uint16_t byteEnd = xEnd / 8; const uint8_t leadingByte = 0xFF >> (xStart - ((byteStart - 1) * 8)); const uint8_t trailingByte = (0xFF00 >> (xEnd - (byteEnd * 8))) & 0xFF; for (uint16_t i = yStart * imageBufferWidth; i < yEnd * imageBufferWidth; i += imageBufferWidth) { // Set the leading byte imageBuffer[i + byteStart - 1] |= leadingByte; // Set the continuous bytes if (byteStart < byteEnd) memset(imageBuffer + i + byteStart, 0xFF, byteEnd - byteStart); // Set the trailing byte if (byteEnd != imageBufferWidth) imageBuffer[i + byteEnd] |= trailingByte; } } } void InkHUD::Renderer::checkLocks() { lockRendering = nullptr; lockRequests = nullptr; for (SystemApplet *sa : inkhud->systemApplets) { if (!lockRendering && sa->lockRendering && sa->isForeground()) { lockRendering = sa; } if (!lockRequests && sa->lockRequests && sa->isForeground()) { lockRequests = sa; } } } bool InkHUD::Renderer::shouldUpdate() { bool should = false; // via forceUpdate should |= forced; // via a system applet (which has locked update requests) if (lockRequests) { should |= lockRequests->wantsToRender(); return should; // Early exit - no other requests considered } // via system applet (not locked) for (SystemApplet *sa : inkhud->systemApplets) { if (sa->wantsToRender() // This applet requested && sa->isForeground()) // This applet is currently shown { should = true; break; } } // via user applet for (Applet *ua : inkhud->userApplets) { if (ua // Tile has valid applet && ua->wantsToRender() // This applet requested display update && ua->isForeground()) // This applet is currently shown { should = true; break; } } return should; } // Determine which type of E-Ink update the display will perform, to change the image. // Considers the needs of the various applets, then weighs against display health. // An update type specified by forceUpdate will be granted with no further questioning. Drivers::EInk::UpdateTypes InkHUD::Renderer::decideUpdateType() { // Ask applets which update type they would prefer // Some update types take priority over others // No need to consider the "requests" if somebody already forced an update if (!forced) { // User applets for (Applet *ua : inkhud->userApplets) { if (ua && ua->isForeground() && (ua->wantsToRender() || renderAll)) displayHealth.requestUpdateType(ua->wantsUpdateType()); } // System Applets for (SystemApplet *sa : inkhud->systemApplets) { if (sa && sa->isForeground() && (sa->wantsToRender() || sa->alwaysRender || renderAll)) displayHealth.requestUpdateType(sa->wantsUpdateType()); } } return displayHealth.decideUpdateType(); } // Run the drawing operations of any user applets which are currently displayed // Pixel output is placed into the framebuffer, ready for handoff to the EInk driver void InkHUD::Renderer::renderUserApplets() { // Don't render user applets if a system applet has demanded the whole display to itself if (lockRendering) return; // Render any user applets which are currently visible for (Applet *ua : inkhud->userApplets) { if (ua && ua->isActive() && ua->isForeground() && (ua->wantsToRender() || renderAll)) { // Clear the tile unless the applet wants to draw over its previous render // or everything is getting re-rendered anyways if (ua->wantsFullRender() && !renderAll) clearTile(ua->getTile()); uint32_t start = millis(); bool full = ua->wantsFullRender() || renderAll; ua->render(full); // Draw! uint32_t stop = millis(); LOG_DEBUG("%s took %dms to render", ua->name, stop - start); } } } // Run the drawing operations of any system applets which are currently displayed // Pixel output is placed into the framebuffer, ready for handoff to the EInk driver void InkHUD::Renderer::renderSystemApplets() { SystemApplet *battery = inkhud->getSystemApplet("BatteryIcon"); SystemApplet *menu = inkhud->getSystemApplet("Menu"); SystemApplet *notifications = inkhud->getSystemApplet("Notification"); // Each system applet for (SystemApplet *sa : inkhud->systemApplets) { // Skip if not shown if (!sa->isForeground()) continue; if (!sa->wantsToRender() && !sa->alwaysRender && !renderAll) continue; // Skip if locked by another applet if (lockRendering && lockRendering != sa) continue; // Don't draw the battery or notifications overtop the menu // Todo: smarter way to handle this if (menu->isForeground() && (sa == battery || sa == notifications)) continue; assert(sa->getTile()); // Clear the tile unless the applet wants to draw over its previous render // or everything is getting re-rendered anyways if (sa->wantsFullRender() && !renderAll) clearTile(sa->getTile()); // uint32_t start = millis(); bool full = sa->wantsFullRender() || renderAll; sa->render(full); // Draw! // uint32_t stop = millis(); // LOG_DEBUG("%s took %dms to render", sa->name, stop - start); } } // In some situations (e.g. layout or applet selection changes), // a user tile can end up without an assigned applet. // In this case, we will fill the empty space with diagonal lines. void InkHUD::Renderer::renderPlaceholders() { // Don't fill empty space with placeholders if a system applet wants exclusive use of the display if (lockRendering) return; // Ask the window manager which tiles are empty std::vector emptyTiles = inkhud->getEmptyTiles(); // No empty tiles if (emptyTiles.size() == 0) return; SystemApplet *placeholder = inkhud->getSystemApplet("Placeholder"); // uint32_t start = millis(); for (Tile *t : emptyTiles) { t->assignApplet(placeholder); // Clear the tile unless everything is getting re-rendered if (!renderAll) clearTile(t); placeholder->render(true); // full render t->assignApplet(nullptr); } // uint32_t stop = millis(); // LOG_DEBUG("Placeholders took %dms to render", stop - start); } #endif ================================================ FILE: src/graphics/niche/InkHUD/Renderer.h ================================================ #ifdef MESHTASTIC_INCLUDE_INKHUD /* Orchestrates updating of the display image - takes requests (or demands) for display update - performs the various steps of the rendering operation - interfaces with the E-Ink driver */ #pragma once #include "configuration.h" #include "./DisplayHealth.h" #include "./InkHUD.h" #include "./Persistence.h" #include "graphics/niche/Drivers/EInk/EInk.h" namespace NicheGraphics::InkHUD { class Renderer : protected concurrency::OSThread { public: Renderer(); // Configuration, before begin void setDriver(Drivers::EInk *driver); void setDisplayResilience(uint8_t fastPerFull, float stressMultiplier); void begin(); // Call these to make the image change void requestUpdate(bool all = false); // Update display, if a foreground applet has info it wants to show void forceUpdate(Drivers::EInk::UpdateTypes type = Drivers::EInk::UpdateTypes::UNSPECIFIED, bool all = false, bool async = true); // Update display, regardless of whether any applets requested this // Wait for an update to complete void awaitUpdate(); // Receives pixel output from an applet (via a tile, which translates the coordinates) void handlePixel(int16_t x, int16_t y, Color c); // Size of display, in context of current rotation uint16_t width(); uint16_t height(); private: // Make attempts to render / update, once triggered by requestUpdate or forceUpdate int32_t runOnce() override; // Apply the display rotation to handled pixels void rotatePixelCoords(int16_t *x, int16_t *y); // Execute the render process now, then hand off to driver for display update void render(bool async = true); // Steps of the rendering process void clearBuffer(); void clearTile(Tile *t); void checkLocks(); bool shouldUpdate(); Drivers::EInk::UpdateTypes decideUpdateType(); void renderUserApplets(); void renderSystemApplets(); void renderPlaceholders(); Drivers::EInk *driver = nullptr; // Interacts with your variants display hardware DisplayHealth displayHealth; // Manages display health by controlling type of update uint8_t *imageBuffer = nullptr; // Fed into driver uint16_t imageBufferHeight = 0; uint16_t imageBufferWidth = 0; uint32_t imageBufferSize = 0; // Bytes SystemApplet *lockRendering = nullptr; // Render this applet *only* SystemApplet *lockRequests = nullptr; // Honor update requests from this applet *only* bool requested = false; bool forced = false; bool renderAll = false; // For convenience InkHUD *inkhud = nullptr; Persistence::Settings *settings = nullptr; }; } // namespace NicheGraphics::InkHUD #endif ================================================ FILE: src/graphics/niche/InkHUD/SystemApplet.h ================================================ #ifdef MESHTASTIC_INCLUDE_INKHUD /* An applet with nonstandard behavior, which will require special handling For features like the menu, and the battery icon. */ #pragma once #include "configuration.h" #include "./Applet.h" namespace NicheGraphics::InkHUD { class SystemApplet : public Applet { public: // System applets have the right to: bool handleInput = false; // - respond to input from the user button bool handleFreeText = false; // - respond to free text input bool lockRendering = false; // - prevent other applets from being rendered during an update bool lockRequests = false; // - prevent other applets from triggering display updates bool alwaysRender = false; // - render every time the screen is updated virtual void onReboot() { onShutdown(); } // - handle reboot specially virtual void onApplyingChanges() {} // Other system applets may take precedence over our own system applet though // The order an applet is passed to WindowManager::addSystemApplet determines this hierarchy (added earlier = higher rank) private: // System applets are always running (active), but may not be visible (foreground) void onActivate() override {} void onDeactivate() override {} }; }; // namespace NicheGraphics::InkHUD #endif ================================================ FILE: src/graphics/niche/InkHUD/Tile.cpp ================================================ #ifdef MESHTASTIC_INCLUDE_INKHUD #include "./Tile.h" #include "concurrency/Periodic.h" using namespace NicheGraphics; // Static members of Tile class (for linking) InkHUD::Tile *InkHUD::Tile::highlightTarget; bool InkHUD::Tile::highlightShown; // For dismissing the highlight indicator, after a few seconds // Highlighting is used to inform user of which tile is now focused static concurrency::Periodic *taskHighlight; static int32_t runtaskHighlight() { LOG_DEBUG("Dismissing Highlight"); InkHUD::Tile::highlightShown = false; InkHUD::Tile::highlightTarget = nullptr; InkHUD::InkHUD::getInstance()->forceUpdate(Drivers::EInk::UpdateTypes::FAST, true); // Re-render, clearing the highlighting return taskHighlight->disable(); } static void inittaskHighlight() { static bool doneOnce = false; if (!doneOnce) { taskHighlight = new concurrency::Periodic("Highlight", runtaskHighlight); taskHighlight->disable(); doneOnce = true; } } InkHUD::Tile::Tile() { inkhud = InkHUD::getInstance(); inittaskHighlight(); Tile::highlightTarget = nullptr; Tile::highlightShown = false; } InkHUD::Tile::Tile(int16_t left, int16_t top, uint16_t width, uint16_t height) { assert(width > 0 && height > 0); this->left = left; this->top = top; this->width = width; this->height = height; } // Set the region of the tile automatically, based on the user's chosen layout // This method places tiles which will host user applets // The WindowManager multiplexes the applets to these tiles automatically void InkHUD::Tile::setRegion(uint8_t userTileCount, uint8_t tileIndex) { uint16_t displayWidth = inkhud->width(); uint16_t displayHeight = inkhud->height(); bool landscape = displayWidth > displayHeight; // Check for any stray tiles if (tileIndex > (userTileCount - 1)) { // Dummy values to prevent rendering LOG_WARN("Tile index out of bounds"); left = -2; top = -2; width = 1; height = 1; return; } // Todo: special handling for 3 tile layout // Gutters between tiles const uint16_t spacing = 4; switch (userTileCount) { // One tile only case 1: left = 0; top = 0; width = displayWidth; height = displayHeight; break; // Two tiles case 2: if (landscape) { // Side by side left = ((displayWidth / 2) + (spacing / 2)) * tileIndex; top = 0; width = (displayWidth / 2) - (spacing / 2); height = displayHeight; } else { // Above and below left = 0; top = 0 + (((displayHeight / 2) + (spacing / 2)) * tileIndex); width = displayWidth; height = (displayHeight / 2) - (spacing / 2); } break; // Four tiles case 4: width = (displayWidth / 2) - (spacing / 2); height = (displayHeight / 2) - (spacing / 2); switch (tileIndex) { case 0: left = 0; top = 0; break; case 1: left = 0 + (width - 1) + spacing; top = 0; break; case 2: left = 0; top = 0 + (height - 1) + spacing; break; case 3: left = 0 + (width - 1) + spacing; top = 0 + (height - 1) + spacing; break; } break; default: LOG_ERROR("Unsupported tile layout"); assert(0); } assert(width > 0 && height > 0); } // Manually set the region for a tile // This is only done for tiles which will host certain "System Applets", which have unique position / sizes: // Things like the NotificationApplet, BatteryIconApplet, etc void InkHUD::Tile::setRegion(int16_t left, int16_t top, uint16_t width, uint16_t height) { assert(width > 0 && height > 0); this->left = left; this->top = top; this->width = width; this->height = height; } // Place an applet onto a tile // Creates a reciprocal link between applet and tile // The tile should always know which applet is displayed // The applet should always know which tile it is display on // This is enforced with asserts // Assigning a new applet will break a previous link // Link may also be broken by assigning a nullptr void InkHUD::Tile::assignApplet(Applet *a) { // Break the link between old applet and this tile if (assignedApplet) assignedApplet->setTile(nullptr); // Store the new applet assignedApplet = a; // Create the reciprocal link between the new applet and this tile if (a) a->setTile(this); } // Get pointer to whichever applet is displayed on this tile InkHUD::Applet *InkHUD::Tile::getAssignedApplet() { return assignedApplet; } // Receive drawing output from the assigned applet, // and translate it from "applet-space" coordinates, to it's true location. // The final "rotation" step is performed by the windowManager void InkHUD::Tile::handleAppletPixel(int16_t x, int16_t y, Color c) { // Move pixels from applet-space to tile-space x += left; y += top; // Crop to tile borders if (x >= left && x < (left + width) && y >= top && y < (top + height)) { // Pass to the renderer inkhud->drawPixel(x, y, c); } } // Used in Renderer for clearing the tile int16_t InkHUD::Tile::getLeft() { return left; } // Used in Renderer for clearing the tile int16_t InkHUD::Tile::getTop() { return top; } // Called by Applet base class, when setting applet dimensions, immediately before render uint16_t InkHUD::Tile::getWidth() { return width; } // Called by Applet base class, when setting applet dimensions, immediately before render uint16_t InkHUD::Tile::getHeight() { return height; } // Longest edge of the display, in pixels // A 296px x 250px display will return 296, for example // Maximum possible size of any tile's width / height // Used by some components to allocate resources for the "worst possible situation" // "Sizing the cathedral for christmas eve" uint16_t InkHUD::Tile::maxDisplayDimension() { InkHUD *inkhud = InkHUD::getInstance(); return max(inkhud->height(), inkhud->width()); } // Ask for this tile to be highlighted // Used to indicate which tile is now indicated after focus changes // Only used for aux button focus changes, not changes via menu void InkHUD::Tile::requestHighlight() { Tile::highlightTarget = this; Tile::highlightShown = false; inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FAST, true); } // Starts the timer which will automatically dismiss the highlighting, if the tile doesn't organically redraw first void InkHUD::Tile::startHighlightTimeout() { taskHighlight->setIntervalFromNow(5 * 1000UL); taskHighlight->enabled = true; } // Stop the timer which would automatically dismiss the highlighting // Called if the tile organically renders before the timer is up void InkHUD::Tile::cancelHighlightTimeout() { if (taskHighlight->enabled) taskHighlight->disable(); } #endif ================================================ FILE: src/graphics/niche/InkHUD/Tile.h ================================================ #ifdef MESHTASTIC_INCLUDE_INKHUD /* Class which represents a region of the display area Applets are assigned to a tile Tile controls the Applet's dimensions Tile receives pixel output from the applet, and translates it to the correct display region */ #pragma once #include "configuration.h" #include "./Applet.h" #include "./InkHUD.h" namespace NicheGraphics::InkHUD { class Tile { public: Tile(); Tile(int16_t left, int16_t top, uint16_t width, uint16_t height); void setRegion(uint8_t layoutSize, uint8_t tileIndex); // Assign region automatically, based on layout void setRegion(int16_t left, int16_t top, uint16_t width, uint16_t height); // Assign region manually void handleAppletPixel(int16_t x, int16_t y, Color c); // Receive px output from assigned applet int16_t getLeft(); int16_t getTop(); uint16_t getWidth(); uint16_t getHeight(); static uint16_t maxDisplayDimension(); // Largest possible width / height any tile may ever encounter void assignApplet(Applet *a); // Link an applet with this tile Applet *getAssignedApplet(); // Applet which is currently linked with this tile void requestHighlight(); // Ask for this tile to be highlighted static void startHighlightTimeout(); // Start the auto-dismissal timer static void cancelHighlightTimeout(); // Cancel the auto-dismissal timer early; already dismissed static Tile *highlightTarget; // Which tile are we highlighting? (Intending to highlight?) static bool highlightShown; // Is the tile highlighted yet? Controls highlight vs dismiss private: InkHUD *inkhud = nullptr; int16_t left = 0; int16_t top = 0; uint16_t width = 0; uint16_t height = 0; Applet *assignedApplet = nullptr; // Pointer to the applet which is currently linked with the tile }; } // namespace NicheGraphics::InkHUD #endif ================================================ FILE: src/graphics/niche/InkHUD/WindowManager.cpp ================================================ #ifdef MESHTASTIC_INCLUDE_INKHUD #include "./WindowManager.h" #include "./Applets/System/AlignStick/AlignStickApplet.h" #include "./Applets/System/BatteryIcon/BatteryIconApplet.h" #include "./Applets/System/Keyboard/KeyboardApplet.h" #include "./Applets/System/Logo/LogoApplet.h" #include "./Applets/System/Menu/MenuApplet.h" #include "./Applets/System/Notification/NotificationApplet.h" #include "./Applets/System/Pairing/PairingApplet.h" #include "./Applets/System/Placeholder/PlaceholderApplet.h" #include "./Applets/System/Tips/TipsApplet.h" #include "./SystemApplet.h" using namespace NicheGraphics; InkHUD::WindowManager::WindowManager() { // Convenient references inkhud = InkHUD::getInstance(); settings = &inkhud->persistence->settings; } // Register a user applet with InkHUD // This is called in setupNicheGraphics() // This should be the only time that specific user applets are mentioned in the code // If a user applet is not added with this method, its code should not be built // Call before begin void InkHUD::WindowManager::addApplet(const char *name, Applet *a, bool defaultActive, bool defaultAutoshow, uint8_t onTile) { inkhud->userApplets.push_back(a); // If requested, mark in settings that this applet should be active by default // This means that it will be available for the user to cycle to with short-press of the button // This is the default state only: user can activate or deactivate applets through the menu. // User's choice of active applets is stored in settings, and will be honored instead of these defaults, if present if (defaultActive) settings->userApplets.active[inkhud->userApplets.size() - 1] = true; // If requested, mark in settings that this applet should "autoshow" by default // This means that the applet will be automatically brought to foreground when it has new data to show // This is the default state only: user can select which applets have this behavior through the menu // User's selection is stored in settings, and will be honored instead of these defaults, if present if (defaultAutoshow) settings->userApplets.autoshow[inkhud->userApplets.size() - 1] = true; // If specified, mark this as the default applet for a given tile index // Used only to avoid placeholder applet "out of the box", when default settings have more than one tile if (onTile != (uint8_t)-1) settings->userTiles.displayedUserApplet[onTile] = inkhud->userApplets.size() - 1; // The label that will be show in the applet selection menu, on the device a->name = name; } // Initial configuration at startup void InkHUD::WindowManager::begin() { assert(inkhud); createSystemApplets(); placeSystemTiles(); createUserApplets(); createUserTiles(); placeUserTiles(); assignUserAppletsToTiles(); refocusTile(); } // Focus on a different tile // The "focused tile" is the one which cycles applets on user button press, // and the one where the menu will be displayed void InkHUD::WindowManager::nextTile() { // Close the menu applet if open // We don't *really* want to do this, but it simplifies handling *a lot* MenuApplet *menu = (MenuApplet *)inkhud->getSystemApplet("Menu"); bool menuWasOpen = false; if (menu->isForeground()) { menu->sendToBackground(); menuWasOpen = true; } // Swap to next tile settings->userTiles.focused = (settings->userTiles.focused + 1) % settings->userTiles.count; // Make sure that we don't get stuck on the placeholder tile refocusTile(); if (menuWasOpen) menu->show(userTiles.at(settings->userTiles.focused)); // Ask the tile to draw an indicator showing which tile is now focused // Requests a render // We only draw this indicator if the device uses an aux button to switch tiles. // Assume aux button is used to switch tiles if the "next tile" menu item is hidden if (!settings->optionalMenuItems.nextTile) userTiles.at(settings->userTiles.focused)->requestHighlight(); } // Focus on a different tile but decrement index void InkHUD::WindowManager::prevTile() { // Close the menu applet if open // We don't *really* want to do this, but it simplifies handling *a lot* MenuApplet *menu = (MenuApplet *)inkhud->getSystemApplet("Menu"); bool menuWasOpen = false; if (menu->isForeground()) { menu->sendToBackground(); menuWasOpen = true; } // Swap to next tile if (settings->userTiles.focused == 0) settings->userTiles.focused = settings->userTiles.count - 1; else settings->userTiles.focused--; // Make sure that we don't get stuck on the placeholder tile refocusTile(); if (menuWasOpen) menu->show(userTiles.at(settings->userTiles.focused)); // Ask the tile to draw an indicator showing which tile is now focused // Requests a render // We only draw this indicator if the device uses an aux button to switch tiles. // Assume aux button is used to switch tiles if the "next tile" menu item is hidden if (!settings->optionalMenuItems.nextTile) userTiles.at(settings->userTiles.focused)->requestHighlight(); } // Show the menu (on the the focused tile) // The applet previously displayed there will be restored once the menu closes void InkHUD::WindowManager::openMenu() { MenuApplet *menu = (MenuApplet *)inkhud->getSystemApplet("Menu"); menu->show(userTiles.at(settings->userTiles.focused)); } // Bring the AlignStick applet to the foreground void InkHUD::WindowManager::openAlignStick() { if (settings->joystick.enabled && !inkhud->twoWayRocker) { AlignStickApplet *alignStick = (AlignStickApplet *)inkhud->getSystemApplet("AlignStick"); alignStick->bringToForeground(); } } void InkHUD::WindowManager::openKeyboard() { if (!settings->joystick.enabled || inkhud->twoWayRocker) return; KeyboardApplet *keyboard = (KeyboardApplet *)inkhud->getSystemApplet("Keyboard"); if (keyboard) { keyboard->bringToForeground(); keyboardOpen = true; changeLayout(); } } void InkHUD::WindowManager::closeKeyboard() { if (!settings->joystick.enabled || inkhud->twoWayRocker) return; KeyboardApplet *keyboard = (KeyboardApplet *)inkhud->getSystemApplet("Keyboard"); if (keyboard) { keyboard->sendToBackground(); keyboardOpen = false; changeLayout(); } } // On the currently focussed tile: cycle to the next available user applet // Applets available for this must be activated, and not already displayed on another tile void InkHUD::WindowManager::nextApplet() { Tile *t = userTiles.at(settings->userTiles.focused); // Abort if zero applets available // nullptr means WindowManager::refocusTile determined that there were no available applets if (!t->getAssignedApplet()) return; // Find the index of the applet currently shown on the tile uint8_t appletIndex = -1; for (uint8_t i = 0; i < inkhud->userApplets.size(); i++) { if (inkhud->userApplets.at(i) == t->getAssignedApplet()) { appletIndex = i; break; } } // Confirm that we did find the applet assert(appletIndex != (uint8_t)-1); // Iterate forward through the WindowManager::applets, looking for the next valid applet Applet *nextValidApplet = nullptr; for (uint8_t i = 1; i < inkhud->userApplets.size(); i++) { uint8_t newAppletIndex = (appletIndex + i) % inkhud->userApplets.size(); Applet *a = inkhud->userApplets.at(newAppletIndex); // Looking for an applet which is active (enabled by user), but currently in background if (a->isActive() && !a->isForeground()) { nextValidApplet = a; settings->userTiles.displayedUserApplet[settings->userTiles.focused] = newAppletIndex; // Remember this setting between boots! break; } } // Confirm that we found another applet if (!nextValidApplet) return; // Hide old applet, show new applet t->getAssignedApplet()->sendToBackground(); t->assignApplet(nextValidApplet); nextValidApplet->bringToForeground(); inkhud->forceUpdate(EInk::UpdateTypes::FAST); // bringToForeground already requested, but we're manually forcing FAST } // On the currently focussed tile: cycle to the previous available user applet // Applets available for this must be activated, and not already displayed on another tile void InkHUD::WindowManager::prevApplet() { Tile *t = userTiles.at(settings->userTiles.focused); // Abort if zero applets available // nullptr means WindowManager::refocusTile determined that there were no available applets if (!t->getAssignedApplet()) return; // Find the index of the applet currently shown on the tile uint8_t appletIndex = -1; for (uint8_t i = 0; i < inkhud->userApplets.size(); i++) { if (inkhud->userApplets.at(i) == t->getAssignedApplet()) { appletIndex = i; break; } } // Confirm that we did find the applet assert(appletIndex != (uint8_t)-1); // Iterate forward through the WindowManager::applets, looking for the previous valid applet Applet *prevValidApplet = nullptr; for (uint8_t i = 1; i < inkhud->userApplets.size(); i++) { uint8_t newAppletIndex = 0; if (i > appletIndex) newAppletIndex = inkhud->userApplets.size() + appletIndex - i; else newAppletIndex = (appletIndex - i); Applet *a = inkhud->userApplets.at(newAppletIndex); // Looking for an applet which is active (enabled by user), but currently in background if (a->isActive() && !a->isForeground()) { prevValidApplet = a; settings->userTiles.displayedUserApplet[settings->userTiles.focused] = newAppletIndex; // Remember this setting between boots! break; } } // Confirm that we found another applet if (!prevValidApplet) return; // Hide old applet, show new applet t->getAssignedApplet()->sendToBackground(); t->assignApplet(prevValidApplet); prevValidApplet->bringToForeground(); inkhud->forceUpdate(EInk::UpdateTypes::FAST); // bringToForeground already requested, but we're manually forcing FAST } // Returns active applet NicheGraphics::InkHUD::Applet *InkHUD::WindowManager::getActiveApplet() { return userTiles.at(settings->userTiles.focused)->getAssignedApplet(); } // Rotate the display image by 90 degrees void InkHUD::WindowManager::rotate() { settings->rotation = (settings->rotation + 1) % 4; changeLayout(); } // Change whether the battery icon is displayed (top right corner) // Don't toggle the OptionalFeatures value before calling this, our method handles it internally void InkHUD::WindowManager::toggleBatteryIcon() { BatteryIconApplet *batteryIcon = (BatteryIconApplet *)inkhud->getSystemApplet("BatteryIcon"); settings->optionalFeatures.batteryIcon = !settings->optionalFeatures.batteryIcon; // Preserve the change between boots // Show or hide the applet if (settings->optionalFeatures.batteryIcon) batteryIcon->bringToForeground(); else batteryIcon->sendToBackground(); // Force-render inkhud->forceUpdate(EInk::UpdateTypes::FAST); } // Perform necessary reconfiguration when user changes number of tiles (or rotation) at run-time // Call after changing settings.tiles.count void InkHUD::WindowManager::changeLayout() { // Recreate tiles // - correct number created, from settings.userTiles.count // - set dimension and position of tiles, according to layout createUserTiles(); placeUserTiles(); placeSystemTiles(); // Handle fewer tiles // - background any applets which have lost their tile findOrphanApplets(); // Handle more tiles // - create extra applets // - assign them to the new extra tiles createUserApplets(); assignUserAppletsToTiles(); // Focus a valid tile // - info: focused tile is the one which cycles applets when user button pressed // - may now be out of bounds if tile count has decreased refocusTile(); // Restore menu // - its tile was just destroyed and recreated (createUserTiles) // - its assignment was cleared (assignUserAppletsToTiles) MenuApplet *menu = (MenuApplet *)inkhud->getSystemApplet("Menu"); if (menu->isForeground()) { Tile *ft = userTiles.at(settings->userTiles.focused); menu->show(ft); } // Resize for the on-screen keyboard if (keyboardOpen) { // Send all user applets to the background // User applets currently don't handle free text input for (uint8_t i = 0; i < inkhud->userApplets.size(); i++) inkhud->userApplets.at(i)->sendToBackground(); // Find the first system applet that can handle freetext and resize it for (SystemApplet *sa : inkhud->systemApplets) { if (sa->handleFreeText) { const uint16_t keyboardHeight = KeyboardApplet::getKeyboardHeight(); sa->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height() - keyboardHeight - 1); break; } } } // Force-render // - redraw all applets inkhud->forceUpdate(EInk::UpdateTypes::FAST, true); } // Perform necessary reconfiguration when user activates or deactivates applets at run-time // Call after changing settings.userApplets.active void InkHUD::WindowManager::changeActivatedApplets() { MenuApplet *menu = (MenuApplet *)inkhud->getSystemApplet("Menu"); assert(menu->isForeground()); // Activate or deactivate applets // - to match value of settings.userApplets.active createUserApplets(); // Assign the placeholder applet // - if applet was foreground on a tile when deactivated, swap it with a placeholder // - placeholder applet may be assigned to multiple tiles, if needed assignUserAppletsToTiles(); // Ensure focused tile has a valid applet // - if focused tile's old applet was deactivated, give it a real applet, instead of placeholder // - reason: nextApplet() won't cycle applets if placeholder is shown refocusTile(); // Restore menu // - its assignment was cleared (assignUserAppletsToTiles) if (menu->isForeground()) { Tile *ft = userTiles.at(settings->userTiles.focused); menu->show(ft); } // Force-render // - redraw all applets inkhud->forceUpdate(EInk::UpdateTypes::FAST, true); } // Some applets may be permitted to bring themselves to foreground, to show new data // User selects which applets have this permission via on-screen menu // Priority is determined by the order which applets were added to WindowManager in setupNicheGraphics // We will only autoshow one applet void InkHUD::WindowManager::autoshow() { // Don't perform autoshow if a system applet has exclusive use of the display right now // Note: lockRequests prevents autoshow attempting to hide menuApplet for (const SystemApplet *sa : inkhud->systemApplets) { if (sa->lockRendering || sa->lockRequests) return; } NotificationApplet *notificationApplet = (NotificationApplet *)inkhud->getSystemApplet("Notification"); for (uint8_t i = 0; i < inkhud->userApplets.size(); i++) { Applet *a = inkhud->userApplets.at(i); if (a->wantsToAutoshow() // Applet wants to become foreground && !a->isForeground() // Not yet foreground && settings->userApplets.autoshow[i]) // User permits this applet to autoshow { Tile *t = userTiles.at(settings->userTiles.focused); // Get focused tile t->getAssignedApplet()->sendToBackground(); // Background whichever applet is already on the tile t->assignApplet(a); // Assign our new applet to tile a->bringToForeground(); // Foreground our new applet // Check if autoshown applet shows the same information as notification intended to // In this case, we can dismiss the notification before it is shown // Note: we are re-running the approval process. This normally occurs when the notification is initially triggered. if (notificationApplet->isForeground() && !notificationApplet->isApproved()) notificationApplet->dismiss(); break; // One autoshow only! Avoid conflicts } } } // A collection of any user tiles which do not have a valid user applet // This can occur in various situations, such as when a user enables fewer applets than their layout has tiles // The tiles (and which regions the occupy) are private information of the window manager // The renderer needs to know which regions (if any) are empty, // in order to fill them with a "placeholder" pattern. // -- There may be a tidier way to accomplish this -- std::vector InkHUD::WindowManager::getEmptyTiles() { std::vector empty; for (Tile *t : userTiles) { Applet *a = t->getAssignedApplet(); if (!a || !a->isActive()) empty.push_back(t); } return empty; } // Complete the configuration of one newly instantiated system applet // - link it with its tile // Unlike user applets, most system applets have their own unique tile; // the only reference to this tile is held by the system applet itself. // - give it a name // A system applet's name is its unique identifier. // The name is our only reference to specific system applets, via InkHUD->getSystemApplet // - add it to the list of system applets void InkHUD::WindowManager::addSystemApplet(const char *name, SystemApplet *applet, Tile *tile) { // Some system applets might not have their own tile (e.g. menu, placeholder) if (tile) tile->assignApplet(applet); applet->name = name; inkhud->systemApplets.push_back(applet); } // Create the "system applets" // These handle things like bootscreen, pop-up notifications etc // They are processed separately from the user applets, because they might need to do "weird things" void InkHUD::WindowManager::createSystemApplets() { addSystemApplet("Logo", new LogoApplet, new Tile); addSystemApplet("Pairing", new PairingApplet, new Tile); addSystemApplet("Tips", new TipsApplet, new Tile); if (settings->joystick.enabled && !inkhud->twoWayRocker) { addSystemApplet("AlignStick", new AlignStickApplet, new Tile); addSystemApplet("Keyboard", new KeyboardApplet, new Tile); } addSystemApplet("Menu", new MenuApplet, nullptr); // Battery and notifications *behind* the menu addSystemApplet("Notification", new NotificationApplet, new Tile); addSystemApplet("BatteryIcon", new BatteryIconApplet, new Tile); // Special handling only, via Rendering::renderPlaceholders addSystemApplet("Placeholder", new PlaceholderApplet, nullptr); // System applets are always active for (SystemApplet *sa : inkhud->systemApplets) sa->activate(); } // Set the position and size of most system applets // Most system applets have their own tile. We manually set the region this tile occupies void InkHUD::WindowManager::placeSystemTiles() { inkhud->getSystemApplet("Logo")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height()); inkhud->getSystemApplet("Pairing")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height()); inkhud->getSystemApplet("Tips")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height()); if (settings->joystick.enabled && !inkhud->twoWayRocker) { inkhud->getSystemApplet("AlignStick")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height()); const uint16_t keyboardHeight = KeyboardApplet::getKeyboardHeight(); inkhud->getSystemApplet("Keyboard") ->getTile() ->setRegion(0, inkhud->height() - keyboardHeight, inkhud->width(), keyboardHeight); } inkhud->getSystemApplet("Notification")->getTile()->setRegion(0, 0, inkhud->width(), 20); const uint16_t batteryIconHeight = Applet::getHeaderHeight() - 2 - 2; const uint16_t batteryIconWidth = batteryIconHeight * 1.8; inkhud->getSystemApplet("BatteryIcon") ->getTile() ->setRegion(inkhud->width() - batteryIconWidth - 1, // x 1, // y batteryIconWidth + 1, // width batteryIconHeight + 2); // height // Note: the tiles of placeholder and menu applets are manipulated specially // - menuApplet borrows user tiles // - placeholder applet is temporarily assigned to each user tile of WindowManager::getEmptyTiles } // Activate or deactivate user applets, to match settings // Called at boot, or after run-time config changes via menu // Note: this method does not instantiate the applets; // this is done in setupNicheGraphics, when passing to InkHUD::addApplet void InkHUD::WindowManager::createUserApplets() { // Deactivate and remove any no-longer-needed applets for (uint8_t i = 0; i < inkhud->userApplets.size(); i++) { Applet *a = inkhud->userApplets.at(i); // If the applet is active, but settings say it shouldn't be: // - run applet's custom deactivation code // - mark applet as inactive (internally) if (a->isActive() && !settings->userApplets.active[i]) a->deactivate(); } // Activate and add any new applets for (uint8_t i = 0; i < inkhud->userApplets.size(); i++) { // If not activated, but it now should be: // - run applet's custom activation code // - mark applet as active (internally) if (!inkhud->userApplets.at(i)->isActive() && settings->userApplets.active[i]) inkhud->userApplets.at(i)->activate(); } } // Creates the tiles which will host user applets // The amount of these is controlled by the user, via "layout" option in the InkHUD menu void InkHUD::WindowManager::createUserTiles() { // Delete any tiles which currently exist for (Tile *t : userTiles) delete t; userTiles.clear(); // Create new tiles for (uint8_t i = 0; i < settings->userTiles.count; i++) { Tile *t = new Tile; userTiles.push_back(t); } } // Calculate the display region occupied by each tile // This determines how pixels are translated from "relative" applet-space to "absolute" windowmanager-space // The size and position depend on the amount of tiles the user prefers, set by the "layout" option void InkHUD::WindowManager::placeUserTiles() { for (uint8_t i = 0; i < userTiles.size(); i++) userTiles.at(i)->setRegion(settings->userTiles.count, i); } // Link "foreground" user applets with tiles // Which applet should be *initially* shown on a tile? // This initial state changes once WindowManager::nextApplet is called. // Performed at startup, or during certain run-time reconfigurations (e.g number of tiles) // This state of "which applets are foreground" is preserved between reboots, but the value needs validating at startup. void InkHUD::WindowManager::assignUserAppletsToTiles() { // Each user tile for (uint8_t i = 0; i < userTiles.size(); i++) { Tile *t = userTiles.at(i); // Check whether tile can display the previously shown applet again uint8_t oldIndex = settings->userTiles.displayedUserApplet[i]; // Previous index in WindowManager::userApplets bool canRestore = true; if (oldIndex > inkhud->userApplets.size() - 1) // Check if old index is now out of bounds canRestore = false; else if (!settings->userApplets.active[oldIndex]) // Check that old applet is still activated canRestore = false; else { // Check that the old applet isn't now shown already on a different tile for (uint8_t i2 = 0; i2 < i; i2++) { if (settings->userTiles.displayedUserApplet[i2] == oldIndex) { canRestore = false; break; } } } // Restore previously shown applet if possible, // otherwise assign nullptr, which will render specially using placeholderApplet if (canRestore) { Applet *a = inkhud->userApplets.at(oldIndex); t->assignApplet(a); a->bringToForeground(); } else { t->assignApplet(nullptr); settings->userTiles.displayedUserApplet[i] = -1; // Update settings: current tile has no valid applet } } } // During layout changes, our focused tile setting can become invalid // This method identifies that situation and corrects for it void InkHUD::WindowManager::refocusTile() { // Validate "focused tile" setting // - info: focused tile responds to button presses: applet cycling, menu, etc // - if number of tiles changed, might now be out of index if (settings->userTiles.focused >= userTiles.size()) settings->userTiles.focused = 0; // Give "focused tile" a valid applet // - scan for another valid applet, which we can addSubstitution // - reason: nextApplet() won't cycle if no applet is assigned Tile *focusedTile = userTiles.at(settings->userTiles.focused); if (!focusedTile->getAssignedApplet()) { // Search for available applets for (uint8_t i = 0; i < inkhud->userApplets.size(); i++) { Applet *a = inkhud->userApplets.at(i); if (a->isActive() && !a->isForeground()) { // Found a suitable applet // Assign it to the focused tile focusedTile->assignApplet(a); a->bringToForeground(); settings->userTiles.displayedUserApplet[settings->userTiles.focused] = i; // Record change: persist after reboot break; } } } } // Search for any applets which believe they are foreground, but no longer have a valid tile // Tidies up after layout changes at runtime void InkHUD::WindowManager::findOrphanApplets() { for (uint8_t ia = 0; ia < inkhud->userApplets.size(); ia++) { Applet *a = inkhud->userApplets.at(ia); // Applet doesn't believe it is displayed: not orphaned if (!a->isForeground()) continue; // Check each tile, to see if anyone claims this applet bool foundOwner = false; for (uint8_t it = 0; it < userTiles.size(); it++) { Tile *t = userTiles.at(it); // A tile claims this applet: not orphaned if (t->getAssignedApplet() == a) { foundOwner = true; break; } } // Orphan found // Tell the applet that no tile is currently displaying it // This allows the focussed tile to cycle to this applet again by pressing user button if (!foundOwner) a->sendToBackground(); } } #endif ================================================ FILE: src/graphics/niche/InkHUD/WindowManager.h ================================================ #ifdef MESHTASTIC_INCLUDE_INKHUD /* Responsible for managing which applets are shown, and their sizes / positions */ #pragma once #include "configuration.h" #include "./Applets/System/Notification/Notification.h" // The notification object, not the applet #include "./InkHUD.h" #include "./Persistence.h" #include "./Tile.h" namespace NicheGraphics::InkHUD { class WindowManager { public: WindowManager(); void addApplet(const char *name, Applet *a, bool defaultActive, bool defaultAutoshow, uint8_t onTile); void begin(); // - call these to make stuff change void nextTile(); void prevTile(); Applet *getActiveApplet(); void openMenu(); void openAlignStick(); void openKeyboard(); void closeKeyboard(); void nextApplet(); void prevApplet(); void rotate(); void toggleBatteryIcon(); // - call these to manifest changes already made to the relevant Persistence::Settings values void changeLayout(); // Change tile layout or count void changeActivatedApplets(); // Change which applets are activated // - called during the rendering operation void autoshow(); // Show a different applet, to display new info std::vector getEmptyTiles(); // Any user tiles without a valid applet private: // Steps for configuring (or reconfiguring) the window manager // - all steps required at startup // - various combinations of steps required for on-the-fly reconfiguration (by user, via menu) void addSystemApplet(const char *name, SystemApplet *applet, Tile *tile); void createSystemApplets(); // Instantiate the system applets void placeSystemTiles(); // Assign manual positions to (most) system applets void createUserApplets(); // Activate user's selected applets void createUserTiles(); // Instantiate enough tiles for user's selected layout void assignUserAppletsToTiles(); void placeUserTiles(); // Automatically place tiles, according to user's layout void refocusTile(); // Ensure focused tile has a valid applet void findOrphanApplets(); // Find any applets left-behind when layout changes std::vector userTiles; // Tiles which can host user applets bool keyboardOpen = false; // For convenience InkHUD *inkhud = nullptr; Persistence::Settings *settings = nullptr; }; } // namespace NicheGraphics::InkHUD #endif ================================================ FILE: src/graphics/niche/InkHUD/docs/README.md ================================================ # InkHUD A haphazard collection of notes which _might_ be helpful for developers. self deprecating meme --- - [Purpose](#purpose) - [Design Principles](#design-principles) - [Self-Contained](#self-contained) - [Static](#static) - [Non-interactive](#non-interactive) - [Customizable](#customizable) - [Event-Driven Rendering](#event-driven-rendering) - [Avoid the Preprocessor](#avoid-the-preprocessor) - [The Implementation](#the-implementation) - [The Rendering Process](#the-rendering-process) - [Concepts](#concepts) - [NicheGraphics Framework](#nichegraphics-framework) - [NicheGraphics E-Ink Drivers](#nichegraphics-e-ink-drivers) - [InkHUD Applets](#inkhud-applets) - [Adding a Variant](#adding-a-variant) - [platformio.ini](#platformioini) - [nicheGraphics.h](#nichegraphicsh) - [Fonts](#fonts) - [Parsing Unicode Text](#parsing-unicode-text) - [Localization](#localization) - [Creating / Modifying](#creating--modifying) - [Class Notes](#class-notes) - [`InkHUD::InkHUD`](#inkhudinkhud) - [`InkHUD::Persistence`](#inkhudpersistence) - [`InkHUD::Persistence::Settings`](#inkhudpersistencesettings) - [`InkHUD::Persistence::LatestMessage`](#inkhudpersistencelatestmessage) - [`InkHUD::WindowManager`](#inkhudwindowmanager) - [`InkHUD::Renderer`](#inkhudrenderer) - [`InkHUD::Renderer::DisplayHealth`](#inkhudrendererdisplayhealth) - [`InkHUD::Events`](#inkhudevents) - [`InkHUD::Applet`](#inkhudapplet) - [`InkHUD::SystemApplet`](#inkhudsystemapplet) - [`InkHUD::Tile`](#inkhudtile) - [`InkHUD::AppletFont`](#inkhudappletfont) ## Purpose InkHUD is a minimal UI for E-Ink devices. It displays the user's choice of info, as statically as possible, to minimize the amount of display refreshing. It is intended to supplement a connected client app. ## Design Principles ### Self-Contained - Keep InkHUD code within `/src/graphics/niche/InkHUD`. - Place reusable components within `/src/graphics/niche`, for other UIs to take advantage of. - Interact with the firmware code using the **Module API**, **Observables**, and other similarly non-intrusive hooks. ### Static Information should be displayed as statically as possible. Unnecessary updates should be avoided. As as example, fixed timestamps are used instead of `X seconds ago` labels, as these need to be constantly updated to remain current. ### Non-interactive InkHUD aims to be a "heads up display". The intention is for the user to glance at the display. The intention is _not_ for the user to frequently interact with the display. Some interactivity is tolerated as a means to an end: the display _should_ be customizable, but this should be minimized as much as possible. _Edit: there's significant demand for keyboard support, so some sort of free-text feature will need to be added eventually, although it does go against the original design principles._ ### Customizable The user should be given the choice to decide which information they would like to receive, and how they would like to receive it. ### Event-Driven Rendering The display image does not update "automatically". Individual applets are responsible for deciding when they have new information to show, and then requesting a display update. ### Avoid the Preprocessor **Don't** use preprocessor macros to write code which targets individual devices. **Do** configure InkHUD to suit each device in [`nicheGraphics.h`](#nichegraphicsh). **Do** use preprocessor macros to guard all files - `#ifdef MESHTASTIC_INCLUDE_INKHUD` for InkHUD files - `#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS` for reusable components (drivers, etc) ## The Implementation - Variant's platformio.ini file extends `inkhud` (defined in InkHUD/PlatformioConfig.ini) - original screen class suppressed: `MESHTASTIC_EXCLUDE_SCREEN` - ButtonThread suppressed: `HAS_BUTTON=0` - NicheGraphics components included: `MESHTASTIC_INCLUDE_NICHE_GRAPHICS` - InkHUD components included: `MESHTASTIC_INCLUDE_INKHUD` - `main.cpp` - includes `nicheGraphics.h` (from variant folder) - calls `setupNicheGraphics`, (from nicheGraphics.h) - `nicheGraphics.h` - includes InkHUD components - includes shared NicheGraphics components - `setupNicheGraphics` - configures and connects components - `inkhud->begin` ## The Rendering Process (animated diagram) animated process diagram of InkHUD rendering An overview: - A component calls `requestUpdate` (applets only) or `InkHUD::forceUpdate` - `Renderer` schedules a render cycle for the next loop(), using `Renderer::runOnce` - `Renderer` determines whether the update request is valid - `Renderer` asks relevant applets to render - Applet dimensions are updated (by Applet's `Tile`) - Applets generate pixel output, and pass this to their `Tile` - Tiles shift these "relative" pixels to their true region, for multiplexing - Tiles pass the pixels to `Renderer` - `Renderer` applies any global display rotation to the pixels - `Renderer` combines the pixels into the finished image - The finished image is passed to the display driver, starting the physical update process ## Concepts ### NicheGraphics Framework InkHUD is implemented as a _NicheGraphics_ UI. Intended as a pattern / philosophy for implementing self-contained UIs, to suit various niche devices, which are best served by their own custom user interface. Hypothetical examples: E-Ink, 1602 LCDs, tiny OLEDs, smart watches, etc A NicheGraphics UI: - Is self-contained - Makes use of the loose collection of resources (drivers, input methods, etc) gathered in the `/src/graphics/niche` folder. - Implements a `setupNicheGraphics()` method. ### NicheGraphics E-Ink Drivers InkHUD uses a set of custom E-Ink drivers. These are not based on GxEPD2, or any other code base. They are written directly on-top of the Meshtastic firmware, to make use of the OSThread class for asynchronous display updates. Interacting with the drivers is straightforward. InkHUD generates a frame of 1-bit image data. This image data is passed to the driver, along with the type of refresh to use (FULL or FAST). `driver->update(uint8_t* buffer, EInk::UpdateTypes::FULL)` For more information, see the documentation in `src/graphics/niche/Drivers/EInk` ### InkHUD Applets An InkHUD applet is a class which generates a screen of info for the display. Consider: `DMApplet.h` (displays most recent direct message) and `RecentsList.h` (displays a list of recently heard nodes) - Applets are modular: they are easy to write, and easy to implement. Users select which applets they want, using the menu. - Applets use responsive design. They should scale for different screens / layouts / fonts. - Applets decide when to update. They use the Module API, Observers, etc, to retrieve information, and request a display update when they have something interesting to show. See `src/graphics/niche/InkHUD/Applets/Examples` for example code. #### Writing an Applet Your new applet class will inherit `InkHUD::Applet`. ```cpp class BasicExampleApplet : public Applet { public: // You must have an onRender() method // All drawing happens here void onRender(bool full) override; }; ``` The `onRender` method is called when the display image is redrawn. This can happen at any time, so be ready! ```cpp // All drawing happens here // Our basic example doesn't do anything useful. It just passively prints some text. void InkHUD::BasicExampleApplet::onRender(bool full) { printAt(0, 0, "Hello, world!"); } ``` Your applet will need to scale automatically, to suit a variety of screens / layouts / fonts. Make sure you draw relative to applet's size. | edge | coordinate | shorthand | | ------ | ---------- | --------- | | left | 0 | `X(0.0)` | | top | 0 | `Y(0.0)` | | right | `width()` | `X(1.0)` | | bottom | `height()` | `Y(1.0)` | The same principles apply for drawing text. Methods like `AppletFont::lineHeight` and `getTextWidth` are useful here. ```cpp std::string line1 = "Line 1"; printAt(0, Y(0.5), line1); drawRect(0, Y(0.5), getTextWidth(line1), fontSmall.lineHeight(), BLACK); ``` Your applet will only be redrawn when _something_ requests a display update. Your applet is welcome to request a display update, when it determines that it has new info to display, by calling `requestUpdate`. Exactly how you determine this, depends on what your applet actually does. Here's a code snippet from one of the example applets. The applet is requesting an update when a new message is received. ```cpp // We configured the Module API to call this method when we receive a new text message ProcessMessage InkHUD::NewMsgExampleApplet::handleReceived(const meshtastic_MeshPacket &mp) { // Abort if applet fully deactivated // Don't waste time: we wouldn't be rendered anyway if (!isActive()) return ProcessMessage::CONTINUE; // Check that this is an incoming message // Outgoing messages (sent by us) will also call handleReceived if (!isFromUs(&mp)) { // Store the sender's nodenum // We need to keep this information, so we can re-use it anytime render() is called haveMessage = true; fromWho = mp.from; // Tell InkHUD that we have something new to show on the screen requestUpdate(); } // Tell Module API to continue informing other firmware components about this message // We're not the only component which is interested in new text messages return ProcessMessage::CONTINUE; } ``` #### Implementing an Applet Incorporating your new applet into InkHUD is easy. In a variant's `nicheGraphics.h`: - `#include` your applet - `inkhud->addApplet("My Applet", new InkHUD::MyApplet);` You will need to add these lines to any variants which will use your applet. #### Applet Bases If you need to create several similar applets, it might make sense to create a reusable base class. Several of these already exist in `src/graphics/niche/InkHUD/Applets/Bases`, but use these with caution, as they may be modified in future. #### System Applets So far, we have been talking about "user applets". We also recognize a separate category of "system applets". These handle things like the menu, and the boot screen. These often need special handling, and need to be implemented manually. ## Adding a Variant In `/variants//`: ### platformio.ini Extend `inkhud`, then combine with any other platformio config your hardware variant requires. _(Example shows only config required by InkHUD. This is not a complete `env` definition.)_ ```ini [env:YOUR_VARIANT-inkhud] extends = esp32s3_base, inkhud ; or nrf52840_base, etc build_src_filter = ${esp32s3_base.build_src_filter} ${inkhud.build_src_filter} build_flags = ${esp32s3_base.build_flags} ${inkhud.build_flags} lib_deps = ${inkhud.lib_deps} ; InkHUD libs first, so we get GFXRoot instead of AdafruitGFX ${esp32s3_base.lib_deps} ``` ### nicheGraphics.h Should contain a `setupNicheGraphics` method, which creates and configures the various components for InkHUD. For well commented examples, see: - `/variants/heltec_vision_master_e290/nicheGraphics.h` (ESP32) - `/variants/ELECROW-ThinkNode-M1/nicheGraphics.h` (NRF52) As a general overview: - Display - Start SPI - Create display driver - InkHUD - Create InkHUD instance - Set E-Ink fast refresh limit (`setDisplayResilience`) - Set fonts - Set default user-settings - Select applets to build (`addApplet`) - Start InkHUD - Buttons - Setup `TwoButton` driver (user button, optional "auxiliary" button) - Connect to InkHUD handlers (use lambdas) ## Fonts InkHUD uses AdafruitGFX fonts. Three shared fonts (small, medium, large) are available for use by all applets. These are set per-variant in nicheGraphics.h. ```cpp // Prepare fonts InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1252; InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; // Using a generic AdafruitGFX font instead: // InkHUD::Applet::fontLarge = FreeSerif18pt7b; ``` Any generic AdafruitGFX font may be used, but the fonts which are bundled with InkHUD have been customized with extended-ASCII character sets and emoji. ### Parsing Unicode Text Text received by the firmware is encoded as UTF-8. Applets must manually parse any text which may contain non-ASCII characters. Strings like text-messages and node names should be parsed. ```cpp std::string greeting = "Góðan daginn!"; std::string parsed = parse(greeting); ``` This will re-encode the characters to match whichever extended-ASCII font InkHUD has been built with. A limited set of emoji have been [wedged into unused code points within the font](#emoji). ### Localization InkHUD is bundled with extended-ASCII fonts for: - Windows-1250 (Central European) - Windows-1251 (Cyrillic) - Windows-1252 (Western European) The default builds use Windows-1252 encoding. This can be changed in nicheGraphics.h. ```cpp InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1250; InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1250; InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1250; InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1251; InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1251; InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1251; ``` ### Creating / Modifying For basic conversion and editing, online tools might be sufficient: - [https://rop.nl/truetype2gfx/](https://rop.nl/truetype2gfx/) - converting from ttf - [https://tchapi.github.io/Adafruit-GFX-Font-Customiser/](https://tchapi.github.io/Adafruit-GFX-Font-Customiser/) - editing glyphs For heavy editing, this offline workflow is suggested: - [FontForge](https://fontforge.org/en-US/) - re-ordering glyphs - Encoding > Load Encoding - Encoding > Reencode - .ttf to .bdf conversion - Element > Bitmap Strikes Available.. - File > Generate Fonts - [GFXFontEditor](https://github.com/ScottFerg56/GFXFontEditor) - manual glyph correction - .bdf to AdafruitGFX .h conversion - File > Edit Font Properties - right-click glyph list, flatten font - File > Save As - manually edit exported .h - remove `#include ` If possible, custom Extended-ASCII fonts should use one of the encodings which InkHUD already supports. If this is not possible, a mapping for the new encoding will need to be added. See [Encoding](#encoding) for details on using an extended-ASCII font. ## Class Notes ### `InkHUD::InkHUD` _`src/graphics/niche/InkHUD/InkHUD.h`_ - singleton - mediator between other InkHUD components #### `getInstance()` Gets access to the class. First `getInstance` call instantiates the class, and the subclasses: - `InkHUD::Persistence` - `InkHUD::WindowManager` - `InkHUD::Renderer` - `InkHUD::Events` For convenience, many InkHUD components call this on `begin`, and store it as `InkHUD* inkhud`. --- ### `InkHUD::Persistence` _`src/graphics/niche/InkHUD/Persistence.h`_ Stores InkHUD data in flash - settings - most recent text message received (both for broadcast and DM) In rare cases, applets may store their own specific data separately (e.g. `ThreadedMessageApplet`) Data saved only on shutdown / reboot. Not saved if power is removed unexpectedly. --- ### `InkHUD::Persistence::Settings` _`src/graphics/niche/InkHUD/Persistence.h`_ Settings which relate to InkHUD. Mostly user's customization, but some values record the UI's state (e.g. `tips.safeShutdownSeen`) - stored using `FlashData.h` (a shared Niche Graphics tool) - not encoded as protobufs - serialized directly as bytes of struct #### Defaults Global default values are set when the struct is defined (Persistence.h). Per-variant defaults are set by modifying the values of the settings instance during `setupNicheGraphics()`, before `inkhud->begin` is called. ```cpp inkhud->persistence->settings.userTiles.count = 2; inkhud->persistence->settings.userTiles.maxCount = 4; inkhud->persistence->settings.rotation = 3; ``` By modifying the values at this point, they will be used if we fail to load previous settings from flash (not yet saved, old version, etc) --- ### `InkHUD::Persistence::LatestMessage` _`src/graphics/niche/InkHUD/Persistence.h`_ Most recently received text message - most recent DM - most recent broadcast Collected here, so various user applets don't all have to store their own copy of this info. We are unable to use `devicestate.rx_text_message` for this purpose, because: - it is cleared by an outgoing text message - we want to store both a recent broadcast and a recent DM #### Saving / Loading _A bit of a hack.._ Stored to flash using `InkHUD::MessageStore`, which is really intended for storing a thread of messages (see `ThreadedMessageApplet`). Used because it stores strings more efficiently than `FlashData.h`. The hack is: - If most recent message was a DM, we only store the DM. - If most recent message was a broadcast, we store both a DM and a broadcast. The DM may be 0-length string. --- ### `InkHUD::WindowManager` _`src/graphics/niche/InkHUD/WindowManager.h`_ Manages which applets are shown, and their size / position (by manipulating the "tiles") - owns the `Tile` instances - creates and destroys tiles; sets size and position: - at startup - at runtime, when config changes (layout, rotation, etc) - activates (or deactivates) applets - cycling through applets (e.g. on button press) The window manager doesn't process pixels; that is handled by the `InkHUD::Tile` objects. Note: Some of the methods (incl. `changeLayout`, `changeActivatedApplets`) don't trigger changes themselves. They should be called _after_ the relevant values in `inkhud->persistence->settings` have been modified. --- ### `InkHUD::Renderer` _`src/graphics/niche/InkHUD/Renderer.h`_ Get pixel output from applets (via a tile), combine, and pass to the driver. - triggered by `requestUpdate` or `forceUpdate` - not run immediately: allows multiple applets to share one render cycle - calls `Applet::onRender` for relevant applets - applies global rotation - passes finalized image to driver `requestUpdate` is for applets (user or system). Renderer will honor the request if the applet is visible. `forceUpdate` can be used anywhere, but not from user applets, please. #### Asynchronous updates `requestUpdate` and `forceUpdate` do not block code execution. They schedule rendering for "ASAP", using `Renderer::runOnce`. Renderer then gets pixel output from relevant applets, and hands the assembled image to the driver. Driver's update process is also asynchronous. If the driver is busy when `requestUpdate` or `forceUpdate` is called, another rendering will run as soon as possible. This is handled by `Renderer::runOnce` #### Blocking updates If needed, call `forceUpdate` with the optional argument `async=false` to wait while an update runs (> 1 second). Additionally, the `awaitUpdate` method can be used to block until any previous update has completed. An example usage of this is waiting to draw the shutdown screen. #### Global rotation The exact size / position / rotation of InkHUD applets is configurable by the user. To achieve this, applets draw pixels between 0,0 and `Applet::width()`, `Applet::height()` - **Scaling**: Applet's `width()` and `height()` are set by `Tile` before rendering starts - **Translation**: `Tile` shifts applet pixels up/down/left/right - **Rotation**: `Renderer` rotates all pixels it receives, before placing them into the final image buffer --- ### `InkHUD::Renderer::DisplayHealth` _`src/graphics/niche/InkHUD/DisplayHealth.h`_ Responsible for maintaining display health, by optimizing the ratio of FAST vs FULL refreshes - count number of FAST vs FULL refreshes (debt) - suggest either FAST or FULL type - periodically FULL refresh the display unprovoked, if needed #### Background Info When the image on an E-Ink display is updated, different procedures can be used to move the pixels to their new states. We have defined two procedures: `FAST` and `FULL`. A `FAST` update moves pixels directly from their old position, to their new position. This is aesthetically pleasing, and quick, _but_ it is challenging for the display hardware. If used excessively, pixels can build up residual charge, which negatively impacts the display's lifespan and image quality. A `FULL` update first moves all pixels between black and white, before letting them eventually settle at their final position. This causes an unpleasant flashing of the display image, but is best for the display health and image quality. Most displays readily tolerate `FAST` updates, so long as a `FULL` update is occasionally performed. How often this `FULL` update is required depends on the display model. #### Debt `InkHUD::DisplayHealth` records how many `FAST` refreshes have occurred since the previous `FULL` refresh. This is referred to as the "full refresh debt". If an update of a specific type (`FULL` / `FAST`) is requested / forced, this will be granted. If an update is requested / forced _without_ a specified type (`UpdateTypes::UNSPECIFIED`), `DisplayHealth` will select either `FAST` or `FULL`, in an attempt to maintain a target ratio of fast to full updates. This target is set by `InkHUD::setDisplayResilience`, when setting up in `nichegraphics.h` If an _excessive_ amount of `FAST` refreshes are performed back-to-back, `DisplayHealth` will begin artificially inflating the full refresh debt. This will cause the next few `UNSPECIFIED` updates to _all_ be performed as `FULL`, while the debt is paid down. This system of "full refresh debt" allows us to increase perceived responsiveness by tolerating additional strain on the display during periods of user interaction, and attempting to "repair the damage" later, once user interaction ceases. #### Maintenance The system of "full refresh debt" assumes that the display will perform many updates of `UNSPECIFIED` type between periods of user interaction. Depending on the amount of mesh traffic / applet selection, this may not be the case. If debt is particularly high, and no updates are taking place organically, `DisplayHealth` will begin infrequently performing `FULL` updates, purely to pay down the full refresh debt. --- ### `InkHUD::Events` Handles events which impact the InkHUD system generally (e.g. shutdown, button press). Applets themselves do also listen separately for various events, but for the purpose of gathering information which they would like to display. #### Buttons Button input is sometimes handled by a system applet. `InkHUD::Events` determines whether the button should be handled by a specific system applet, or should instead trigger a default behavior #### Factory Reset The Events class handles the admin messages(s) which trigger factory reset. We set `Events::eraseOnReboot = true`, which causes `Events::onReboot` to erase the contents of InkHUD's data directory. We do this because some applets (e.g. ThreadedMessageApplet) save their own data to flash, so if we erased earlier, that data would get re-written during reboot. --- ### `InkHUD::Applet` A base class for applets. An applet is one "program", which may show info on the display. To oversimplify, all of the InkHUD code "under the hood" only exists to support applets. Applets are what actually shows useful information to the user. This base class exposes the functionality needed to write an applet. #### Drawing Methods `Applet` implements most AdafruitGFX drawing methods. Exception is the text handling. `printAt`, `printWrapped`, and `printThick` should be used instead. These are intended to be more convenient, but they also implement the character substitution system which powers the foreign alphabet support. `Applet` also adds methods for drawing several design elements which are re-used commonly though-out InkHUD. #### InkHUD Events Applets undergo a number of state changes: activated / deactivated by user, brought to foreground / hidden to background by user button press, etc. The `Applet` class provides a set of virtual methods, which an applet can override to appropriately handle these events. The `onRender` virtual method is one example. This is called when an applet is rendered, and should execute all drawing code. An applet _must_ implement this method. #### Responsive Design An applet's size will vary depending on the screen size, and the user's layout (multiplexing). Immediately before `onRender` is called, an applet's dimensions are updated, so that `width()` and `height()` will give the required size. The applet should draw its graphical elements relative to these values. The methods `X(float)` and `Y(float)` are also provided for convenience. | edge | coordinate | shorthand | | ------ | ---------- | --------- | | left | 0 | `X(0.0)` | | top | 0 | `Y(0.0)` | | right | `width()` | `X(1.0)` | | bottom | `height()` | `Y(1.0)` | The same principles apply for drawing text. Methods like `AppletFont::lineHeight` and `getTextWidth` are useful here. Applets should always draw relative to their top left corner, at _x=0, y=0._ The applet's pixels are automatically moved to the correct position on-screen by an InkHUD::Tile. #### User Applets User applets are the "normal" applets, each one displaying a specific set of information to the user. They can be activated / deactivated at run-time using the on-screen menu. Examples include `DMApplet.h` and `PositionsApplet.h`. User applets are not expected to interact with lower layers of the InkHUD code. Users applets are instantiated in a variant's `setupNicheGraphics` method, and passed to `InkHUD::addApplet`. Their class should not be mentioned elsewhere, so that its code can be stripped away during compilation if a variant does not implement the specific applet. Internal processing of user applets treats them all as the generic `Applet` type only. #### Activated / Deactivated User applets can be activated or deactivated. This changes at run-time: the user selects which applets should be active using the on-screen menu. An applet should not process data while it is deactivated. It can unobserve any observables, ignore `handleReceived` calls, etc. An applet can implement the virtual `onActivate` and `onDeactivate` methods to handle this change in state. It can check this state internally by calling `isActive`. System applets cannot be deactivated. #### Foreground / Background An activated applet can either be _foreground_ or _background_. A foreground applet is one which will be rendered to a tile when the screen updates. A background applet will not be drawn. The applet cycling which takes place when the user button is pressed is implemented using foreground / background. Regardless of whether it is foreground or background, an activated applet should continue to collect / process data, and request update when it has new info to display. This is because of the _autoshow_ mechanic, which might bring a background applet to foreground in order to display its data. If an applet remains background, its update requests will be safely ignored. #### Autoshow Autoshow is a feature which allows the user to select which applets (if any) they would like to be shown automatically. If autoshow is enabled for an applet, it will be brought to foreground when it has new information to display. The user grants this privilege on a per-applet basis, using the on-screen menu. If an event causes an applet to be autoshown, NotificationApplet should not be shown for the same event. An applet needs to decide when it has information worthy of autoshowing. It signals this by calling `requestAutoshow`, in addition to the usual `requestUpdate` call. --- ### `InkHUD::SystemApplet` _System applets_ are applets with special roles, which require special handling. Examples include `BatteryIconApplet.h` and `LogoApplet.h`. These are manually implemented, one-by-one, in `WindowManager.h`. This class is a slight extension of `Applet`. It adds extra flags for some special features which are restricted to system applets: exclusive use of the display, and the handling of user input. Having a separate system applet class also allows us to make it clear within the code when system applets are being handled, rather than user applets We store reference to these as a `vector`. This parallels how we treat user applets, and makes rendering convenient. Because system applets do have unique roles, there are times when we will need to interact with a specific applet. Rather than keeping an extra set of references, we access them from the `vector`. Use `InkHUD::getSystemApplet` to access the applet by its `Applet::name` value, and then typecast. --- ### `InkHUD::Tile` A tile represents a region of the display. A tile controls the size and position of an applet. For an applet to render, it must be assigned to a tile. When an applet is assigned to a tile, the two become linked. The applet is aware of the tile; the tile is aware of the applet. Applets cannot share a tile; assigning a different applet will remove any existing link. Before an applet renders, its width and height are set to the dimensions of the tile. During `onRender`, an applet's drawing methods generate pixels between _x=0, y=0_ and _x=Applet::width(), y=Applet::height()_. These pixels are passed to its tile's `Tile::handleAppletPixel` method. The tile then applies x and y offset, "translating" these pixels to the tile's region of the display. These translated pixels are then passed on to the `InkHUD::Renderer`. ![depiction of a tile translating applet pixels](./tile_translation.png) #### User Tiles _User applets_ are the "normal" applets. They can be activated / deactivated at run-time using the on-screen menu. User applets are rendered to one of the **user tiles**. The user can customize the "layout", using the on-screen menu. Depending on their selected layout, a certain number of _user tiles_ are created. These tiles are automatically positioned and sized so that they fill the entire screen. Often, a user will have enabled more applets than they have tiles. Pressing the user-button will cycle through these applets. The old applet is sent to _background_, the new applet is brought to _foreground_. When a user applet is brought to foreground, it becomes assigned to a user tile (the focused tile). When it renders, its size will be set by this tile, and its pixels will be translated to this tile's region. The user applet which was sent to background loses its assignment; it no longer has an assigned tile. #### Focused Tile The focused tile is one of the user tiles. This is tile whose applet will change when the user button is pressed. This also the tile where the menu will appear on longpress. The focused tile is identified by its index in `vector userTiles`. #### Highlighting In addition to the user button, some devices have a second "auxiliary button". The function of this button can vary from device to device, but it is sometimes used to focus a different tile. When this happens, the newly focused tile is temporarily "highlighted", by drawing it with a border. This border is automatically removed after several seconds. As drawing code may only be executed by applets, this highlighting is a collaborative effort between a `Tile` and an `Applet`: performed in `Applet::render`, after the virtual `onRender` method has already run. Highlighting is only used when `nextTile` is fired by an aux button. It does not occur if performed via the on-screen menu. #### System Tiles _System applets_ are applets with special roles, which require special handling. Examples include `BatteryIconApplet.h` and `LogoApplet.h`. _Mostly_, these applets do not render to user tiles. Instead, they are given their own unique tile, which is positioned / dimensioned manually. The only reference we keep to these special tiles is stored within the linked system applet. They can be accessed with `Applet::getTile`. --- ### `InkHUD::AppletFont` Wrapper which extends the functionality of an AdafruitGFX font. #### Dimension Info The AppletFont class pre-calculates some info about a font's dimensions, which is useful for design (`AppletFont::lineHeight`), and is used to power InkHUD's custom text handling. The default AdafruitGFX text handling places characters "upon a line", as if hand-written on a sheet of ruled paper. `InkHUD::AppletFont` measures the character set of the font, so that we instead draw fixed-height lines of text, positioned by the bounding box, with optional horizontal and vertical alignment. ![text origins in InkHUD vs AdafruitGFX](./appletfont.png) The height of this box is `AppletFont::lineHeight`, which is the height of the tallest character in the font. This gives us a fixed-height for text, which is much tighter than with AdafruitGFX's default line spacing. #### Encoding An AppletFont may be constructed from a standard 7bit ASCII AdafruitGFX font, however InkHUD also supports 8bit extended-ASCII fonts. For this, the encoding must be specified when instantiating the AppletFont. ```cpp InkHUD::AppletFont(FreeSans9pt_Win1250, InkHUD::AppletFont::WINDOWS_1250); ``` Currently supported encodings are: - ASCII - Windows-1250 (Central European) - Windows-1251 (Cyrillic) - Windows-1252 (Western European) To add support for additional encodings, add to the `AppletFont::Encodings` enum, and then define the mapping from unicode in `AppletFont::applyEncoding`. #### Custom Line Height Some fonts may have a handful of especially tall characters, especially extended-ASCII fonts with diacritics. Ideally, the font should be modified to help resolve this, but if the problem remains, manual offsets to the automatically determined line height can be specified in the constructor. ```cpp // -2 px of padding above, +1 px of padding below InkHUD::AppletFont(FreeSans9pt7b, ASCII, -2, 1); ``` #### Emoji AdafruitGFX fonts are limited to 255 characters. InkHUD supports a restricted set of emoji, which are stored in the unused code points of the ASCII control characters (`'\x01'`, `'\x02'`, etc). Standard AdafruitGFX fonts contain no glyphs below `'\x20'`, so will ignore these attempts to parse emoji. This mapping of emoji to control characters is fairly arbitrary. Selection was influenced by [PR #3940 Oled screen emojis](https://github.com/meshtastic/firmware/pull/3940) and [Emoji Frequency Spreadsheet](https://docs.google.com/spreadsheets/d/1Zs13WJYdZL1pNZP0dCIXkWau_tZOjK3mmJz0KNq4I30/). | Code Point | Emoji | | ---------- | ---------------------------------------------- | | ~~`0x00`~~ | (null term, unused) | | `0x01` | 👍 | | `0x02` | 👎 | | `0x03` | 🙂 | | `0x04` | 😆 | | `0x05` | 👋 | | `0x06` | ☀ | | ~~`0x07`~~ | (bell char, unused) | | `0x08` | 🌧 | | `0x09` | ☁ | | ~~`0x0A`~~ | (line feed, unused) | | `0x0B` | ♥ | | `0x0C` | 💩 | | ~~`0x0D`~~ | (carriage return, unused) | | `0x0E` | 🔔 | | `0x0F` | 😭 | | `0x1A` | (substitution "⍰", used for unprintable chars) | | `0x1B` | 🤗 | | `0x1C` | 😉 | | `0x1D` | 😏 | | `0x1E` | 🫡 (saluting face) | | `0x1F` | 👌 | ================================================ FILE: src/graphics/niche/Inputs/README.md ================================================ # NiceGraphics - Inputs General purpose input sources, for use with NicheGraphics UIs. By remaining independent, we can have tailored input sources with further complicating the code in ButtonThread and the canned messages module. Depending on its role, a NicheGraphics UI may or may not want to make use of the existing input broker. ================================================ FILE: src/graphics/niche/Inputs/TwoButton.cpp ================================================ #ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS #include "./TwoButton.h" #include "NodeDB.h" // For the helper function TwoButton::getUserButtonPin #include "PowerFSM.h" #include "sleep.h" using namespace NicheGraphics::Inputs; TwoButton::TwoButton() : concurrency::OSThread("TwoButton") { // Don't start polling buttons for release immediately // Assume they are in a "released" state at boot OSThread::disable(); #ifdef ARCH_ESP32 // Register callbacks for before and after lightsleep lsObserver.observe(¬ifyLightSleep); lsEndObserver.observe(¬ifyLightSleepEnd); #endif // Explicitly initialize these, just to keep cppcheck quiet.. buttons[0] = Button(); buttons[1] = Button(); } // Get access to (or create) the singleton instance of this class // Accessible inside the ISRs, even though we maybe shouldn't TwoButton *TwoButton::getInstance() { // Instantiate the class the first time this method is called static TwoButton *const singletonInstance = new TwoButton; return singletonInstance; } // Begin receiving button input // We probably need to do this after sleep, as well as at boot void TwoButton::start() { if (buttons[0].pin != 0xFF) attachInterrupt(buttons[0].pin, TwoButton::isrPrimary, buttons[0].activeLogic == LOW ? FALLING : RISING); if (buttons[1].pin != 0xFF) attachInterrupt(buttons[1].pin, TwoButton::isrSecondary, buttons[1].activeLogic == LOW ? FALLING : RISING); } // Stop receiving button input, and run custom sleep code // Called before device sleeps. This might be power-off, or just ESP32 light sleep // Some devices will want to attach interrupts here, for the user button to wake from sleep void TwoButton::stop() { if (buttons[0].pin != 0xFF) detachInterrupt(buttons[0].pin); if (buttons[1].pin != 0xFF) detachInterrupt(buttons[1].pin); } // Attempt to resolve a GPIO pin for the user button, honoring userPrefs.jsonc and device settings // This helper method isn't used by the TwoButton class itself, it could be moved elsewhere. // Intention is to pass this value to TwoButton::setWiring in the setupNicheGraphics method. uint8_t TwoButton::getUserButtonPin() { uint8_t pin = 0xFF; // Unset // Use default pin for variant, if no better source #ifdef BUTTON_PIN pin = BUTTON_PIN; #endif // From userPrefs.jsonc, if set #ifdef USERPREFS_BUTTON_PIN pin = USERPREFS_BUTTON_PIN; #endif // From user's override in device settings, if set if (config.device.button_gpio) pin = config.device.button_gpio; return pin; } // Configures the wiring and logic of either button // Called when outlining your NicheGraphics implementation, in variant/nicheGraphics.cpp void TwoButton::setWiring(uint8_t whichButton, uint8_t pin, bool internalPullup) { // Prevent the same GPIO being assigned to multiple buttons // Allows an edge case when the user remaps hardware buttons using device settings, due to a broken user button for (uint8_t i = 0; i < whichButton; i++) { if (buttons[i].pin == pin) { LOG_WARN("Attempted reuse of GPIO %d. Ignoring assignment whichButton=%d", pin, whichButton); return; } } assert(whichButton < 2); buttons[whichButton].pin = pin; buttons[whichButton].activeLogic = LOW; // Unimplemented pinMode(buttons[whichButton].pin, internalPullup ? INPUT_PULLUP : INPUT); } void TwoButton::setTiming(uint8_t whichButton, uint32_t debounceMs, uint32_t longpressMs) { assert(whichButton < 2); buttons[whichButton].debounceLength = debounceMs; buttons[whichButton].longpressLength = longpressMs; } // Set what should happen when a button becomes pressed // Use this to implement a "while held" behavior void TwoButton::setHandlerDown(uint8_t whichButton, Callback onDown) { assert(whichButton < 2); buttons[whichButton].onDown = onDown; } // Set what should happen when a button becomes unpressed // Use this to implement a "While held" behavior void TwoButton::setHandlerUp(uint8_t whichButton, Callback onUp) { assert(whichButton < 2); buttons[whichButton].onUp = onUp; } // Set what should happen when a "short press" event has occurred void TwoButton::setHandlerShortPress(uint8_t whichButton, Callback onShortPress) { assert(whichButton < 2); buttons[whichButton].onShortPress = onShortPress; } // Set what should happen when a "long press" event has fired // Note: this will occur while the button is still held void TwoButton::setHandlerLongPress(uint8_t whichButton, Callback onLongPress) { assert(whichButton < 2); buttons[whichButton].onLongPress = onLongPress; } // Handle the start of a press to the primary button // Wakes our button thread void TwoButton::isrPrimary() { static volatile bool isrRunning = false; if (!isrRunning) { isrRunning = true; TwoButton *b = TwoButton::getInstance(); if (b->buttons[0].state == State::REST) { b->buttons[0].state = State::IRQ; b->buttons[0].irqAtMillis = millis(); b->startThread(); } isrRunning = false; } } // Handle the start of a press to the secondary button // Wakes our button thread void TwoButton::isrSecondary() { static volatile bool isrRunning = false; if (!isrRunning) { isrRunning = true; TwoButton *b = TwoButton::getInstance(); if (b->buttons[1].state == State::REST) { b->buttons[1].state = State::IRQ; b->buttons[1].irqAtMillis = millis(); b->startThread(); } isrRunning = false; } } // Concise method to start our button thread // Follows an ISR, listening for button release void TwoButton::startThread() { if (!OSThread::enabled) { OSThread::setInterval(10); OSThread::enabled = true; } } // Concise method to stop our button thread // Called when we no longer need to poll for button release void TwoButton::stopThread() { if (OSThread::enabled) { OSThread::disable(); } // Reset both buttons manually // Just in case an IRQ fires during the process of resetting the system // Can occur with super rapid presses? buttons[0].state = REST; buttons[1].state = REST; } // Our button thread // Started by an IRQ, on either button // Polls for button releases // Stops when both buttons released int32_t TwoButton::runOnce() { constexpr uint8_t BUTTON_COUNT = sizeof(buttons) / sizeof(Button); // Allow either button to request that our thread should continue polling bool awaitingRelease = false; // Check both primary and secondary buttons for (uint8_t i = 0; i < BUTTON_COUNT; i++) { switch (buttons[i].state) { // No action: button has not been pressed case REST: break; // New press detected by interrupt case IRQ: powerFSM.trigger(EVENT_PRESS); // Tell PowerFSM that press occurred (resets sleep timer) buttons[i].onDown(); // Run callback: press has begun (possible hold behavior) buttons[i].state = State::POLLING_UNFIRED; // Mark that button-down has been handled awaitingRelease = true; // Mark that polling-for-release should continue break; // An existing press continues // Not held long enough to register as longpress case POLLING_UNFIRED: { uint32_t length = millis() - buttons[i].irqAtMillis; // If button released since last thread tick, if (digitalRead(buttons[i].pin) != buttons[i].activeLogic) { buttons[i].onUp(); // Run callback: press has ended (possible release of a hold) buttons[i].state = State::REST; // Mark that the button has reset if (length > buttons[i].debounceLength && length < buttons[i].longpressLength) // If too short for longpress, buttons[i].onShortPress(); // Run callback: short press } // If button not yet released else { awaitingRelease = true; // Mark that polling-for-release should continue if (length >= buttons[i].longpressLength) { // Run callback: long press (once) // Then continue waiting for release, to rearm buttons[i].state = State::POLLING_FIRED; buttons[i].onLongPress(); } } break; } // Button still held, but duration long enough that longpress event already fired // Just waiting for release case POLLING_FIRED: // Release detected if (digitalRead(buttons[i].pin) != buttons[i].activeLogic) { buttons[i].state = State::REST; buttons[i].onUp(); // Callback: release of hold (in this case: *after* longpress has fired) } // Not yet released, keep polling else awaitingRelease = true; break; } } // If both buttons are now released // we don't need to waste cpu resources polling // IRQ will restart this thread when we next need it if (!awaitingRelease) stopThread(); // Run this method again, or don't.. // Use whatever behavior was previously set by stopThread() or startThread() return OSThread::interval; } #ifdef ARCH_ESP32 // Detach our class' interrupts before lightsleep // Allows sleep.cpp to configure its own interrupts, which wake the device on user-button press int TwoButton::beforeLightSleep(void *unused) { stop(); return 0; // Indicates success } // Reconfigure our interrupts // Our class' interrupts were disconnected during sleep, to allow the user button to wake the device from sleep int TwoButton::afterLightSleep(esp_sleep_wakeup_cause_t cause) { start(); // Manually trigger the button-down ISR // - during light sleep, our ISR is disabled // - if light sleep ends by button press, pretend our own ISR caught it // - need to manually confirm by reading pin ourselves, to avoid occasional false positives // (false positive only when using internal pullup resistors?) if (cause == ESP_SLEEP_WAKEUP_GPIO && digitalRead(buttons[0].pin) == buttons[0].activeLogic) isrPrimary(); return 0; // Indicates success } #endif #endif ================================================ FILE: src/graphics/niche/Inputs/TwoButton.h ================================================ #ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS /* Re-usable NicheGraphics input source Short and Long press for up to two buttons Interrupt driven */ #pragma once #include "configuration.h" #include "assert.h" #include "functional" #ifdef ARCH_ESP32 #include "esp_sleep.h" // For light-sleep handling #endif #include "Observer.h" namespace NicheGraphics::Inputs { class TwoButton : protected concurrency::OSThread { public: typedef std::function Callback; static uint8_t getUserButtonPin(); // Resolve the GPIO, considering the various possible source of definition static TwoButton *getInstance(); // Create or get the singleton instance void start(); // Start handling button input void stop(); // Stop handling button input (disconnect ISRs for sleep) void setWiring(uint8_t whichButton, uint8_t pin, bool internalPullup = false); void setTiming(uint8_t whichButton, uint32_t debounceMs, uint32_t longpressMs); void setHandlerDown(uint8_t whichButton, Callback onDown); void setHandlerUp(uint8_t whichButton, Callback onUp); void setHandlerShortPress(uint8_t whichButton, Callback onShortPress); void setHandlerLongPress(uint8_t whichButton, Callback onLongPress); // Disconnect and reconnect interrupts for light sleep #ifdef ARCH_ESP32 int beforeLightSleep(void *unused); int afterLightSleep(esp_sleep_wakeup_cause_t cause); #endif private: // Internal state of a specific button enum State { REST, // Up, no activity IRQ, // Down detected, not yet handled POLLING_UNFIRED, // Down handled, polling for release POLLING_FIRED, // Longpress fired, button still held }; // Contains info about a specific button // (Array of this struct below) class Button { public: // Per-button config uint8_t pin = 0xFF; // 0xFF: unset bool activeLogic = LOW; // Active LOW by default. Currently unimplemented. uint32_t debounceLength = 50; // Minimum length for shortpress, in ms uint32_t longpressLength = 500; // How long after button down to fire longpress, in ms volatile State state = State::REST; // Internal state volatile uint32_t irqAtMillis; // millis() when button went down // Per-button event callbacks static void noop(){}; std::function onDown = noop; std::function onUp = noop; std::function onShortPress = noop; std::function onLongPress = noop; }; #ifdef ARCH_ESP32 // Get notified when lightsleep begins and ends CallbackObserver lsObserver = CallbackObserver(this, &TwoButton::beforeLightSleep); CallbackObserver lsEndObserver = CallbackObserver(this, &TwoButton::afterLightSleep); #endif int32_t runOnce() override; // Timer method. Polls for button release void startThread(); // Start polling for release void stopThread(); // Stop polling for release static void isrPrimary(); // Detect start of press static void isrSecondary(); // Detect start of press (optional aux button) TwoButton(); // Constructor made private: force use of Button::instance() // Info about both buttons Button buttons[2]; }; }; // namespace NicheGraphics::Inputs #endif ================================================ FILE: src/graphics/niche/Inputs/TwoButtonExtended.cpp ================================================ #ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS #include "./TwoButtonExtended.h" #include "NodeDB.h" // For the helper function TwoButtonExtended::getUserButtonPin #include "PowerFSM.h" #include "sleep.h" using namespace NicheGraphics::Inputs; TwoButtonExtended::TwoButtonExtended() : concurrency::OSThread("TwoButtonExtended") { // Don't start polling buttons for release immediately // Assume they are in a "released" state at boot OSThread::disable(); #ifdef ARCH_ESP32 // Register callbacks for before and after lightsleep lsObserver.observe(¬ifyLightSleep); lsEndObserver.observe(¬ifyLightSleepEnd); #endif // Explicitly initialize these, just to keep cppcheck quiet.. buttons[0] = Button(); buttons[1] = Button(); joystick[Direction::UP] = SimpleButton(); joystick[Direction::DOWN] = SimpleButton(); joystick[Direction::LEFT] = SimpleButton(); joystick[Direction::RIGHT] = SimpleButton(); } // Get access to (or create) the singleton instance of this class // Accessible inside the ISRs, even though we maybe shouldn't TwoButtonExtended *TwoButtonExtended::getInstance() { // Instantiate the class the first time this method is called static TwoButtonExtended *const singletonInstance = new TwoButtonExtended; return singletonInstance; } // Begin receiving button input // We probably need to do this after sleep, as well as at boot void TwoButtonExtended::start() { if (buttons[0].pin != 0xFF) attachInterrupt(buttons[0].pin, TwoButtonExtended::isrPrimary, buttons[0].activeLogic == LOW ? FALLING : RISING); if (buttons[1].pin != 0xFF) attachInterrupt(buttons[1].pin, TwoButtonExtended::isrSecondary, buttons[1].activeLogic == LOW ? FALLING : RISING); if (joystick[Direction::UP].pin != 0xFF) attachInterrupt(joystick[Direction::UP].pin, TwoButtonExtended::isrJoystickUp, joystickActiveLogic == LOW ? FALLING : RISING); if (joystick[Direction::DOWN].pin != 0xFF) attachInterrupt(joystick[Direction::DOWN].pin, TwoButtonExtended::isrJoystickDown, joystickActiveLogic == LOW ? FALLING : RISING); if (joystick[Direction::LEFT].pin != 0xFF) attachInterrupt(joystick[Direction::LEFT].pin, TwoButtonExtended::isrJoystickLeft, joystickActiveLogic == LOW ? FALLING : RISING); if (joystick[Direction::RIGHT].pin != 0xFF) attachInterrupt(joystick[Direction::RIGHT].pin, TwoButtonExtended::isrJoystickRight, joystickActiveLogic == LOW ? FALLING : RISING); } // Stop receiving button input, and run custom sleep code // Called before device sleeps. This might be power-off, or just ESP32 light sleep // Some devices will want to attach interrupts here, for the user button to wake from sleep void TwoButtonExtended::stop() { if (buttons[0].pin != 0xFF) detachInterrupt(buttons[0].pin); if (buttons[1].pin != 0xFF) detachInterrupt(buttons[1].pin); if (joystick[Direction::UP].pin != 0xFF) detachInterrupt(joystick[Direction::UP].pin); if (joystick[Direction::DOWN].pin != 0xFF) detachInterrupt(joystick[Direction::DOWN].pin); if (joystick[Direction::LEFT].pin != 0xFF) detachInterrupt(joystick[Direction::LEFT].pin); if (joystick[Direction::RIGHT].pin != 0xFF) detachInterrupt(joystick[Direction::RIGHT].pin); } // Attempt to resolve a GPIO pin for the user button, honoring userPrefs.jsonc and device settings // This helper method isn't used by the TwoButtonExtended class itself, it could be moved elsewhere. // Intention is to pass this value to TwoButtonExtended::setWiring in the setupNicheGraphics method. uint8_t TwoButtonExtended::getUserButtonPin() { uint8_t pin = 0xFF; // Unset // Use default pin for variant, if no better source #ifdef BUTTON_PIN pin = BUTTON_PIN; #endif // From userPrefs.jsonc, if set #ifdef USERPREFS_BUTTON_PIN pin = USERPREFS_BUTTON_PIN; #endif // From user's override in device settings, if set if (config.device.button_gpio) pin = config.device.button_gpio; return pin; } // Configures the wiring and logic of either button // Called when outlining your NicheGraphics implementation, in variant/nicheGraphics.cpp void TwoButtonExtended::setWiring(uint8_t whichButton, uint8_t pin, bool internalPullup) { // Prevent the same GPIO being assigned to multiple buttons // Allows an edge case when the user remaps hardware buttons using device settings, due to a broken user button for (uint8_t i = 0; i < whichButton; i++) { if (buttons[i].pin == pin) { LOG_WARN("Attempted reuse of GPIO %d. Ignoring assignment whichButton=%d", pin, whichButton); return; } } assert(whichButton < 2); buttons[whichButton].pin = pin; buttons[whichButton].activeLogic = LOW; pinMode(buttons[whichButton].pin, internalPullup ? INPUT_PULLUP : INPUT); } // Configures the wiring and logic of the joystick buttons // Called when outlining your NicheGraphics implementation, in variant/nicheGraphics.cpp void TwoButtonExtended::setJoystickWiring(uint8_t uPin, uint8_t dPin, uint8_t lPin, uint8_t rPin, bool internalPullup) { if (joystick[Direction::UP].pin == uPin || joystick[Direction::DOWN].pin == dPin || joystick[Direction::LEFT].pin == lPin || joystick[Direction::RIGHT].pin == rPin) { LOG_WARN("Attempted reuse of Joystick GPIO. Ignoring assignment"); return; } joystick[Direction::UP].pin = uPin; joystick[Direction::DOWN].pin = dPin; joystick[Direction::LEFT].pin = lPin; joystick[Direction::RIGHT].pin = rPin; joystickActiveLogic = LOW; pinMode(joystick[Direction::UP].pin, internalPullup ? INPUT_PULLUP : INPUT); pinMode(joystick[Direction::DOWN].pin, internalPullup ? INPUT_PULLUP : INPUT); pinMode(joystick[Direction::LEFT].pin, internalPullup ? INPUT_PULLUP : INPUT); pinMode(joystick[Direction::RIGHT].pin, internalPullup ? INPUT_PULLUP : INPUT); } // Configures only left/right joystick directions for a two-way rocker void TwoButtonExtended::setTwoWayRockerWiring(uint8_t leftPin, uint8_t rightPin, bool internalPullup) { if (leftPin == rightPin) { LOG_WARN("Attempted reuse of TwoWayRocker GPIO. Ignoring assignment"); return; } joystick[Direction::UP].pin = 0xFF; joystick[Direction::DOWN].pin = 0xFF; joystick[Direction::LEFT].pin = leftPin; joystick[Direction::RIGHT].pin = rightPin; joystickActiveLogic = LOW; pinMode(joystick[Direction::LEFT].pin, internalPullup ? INPUT_PULLUP : INPUT); pinMode(joystick[Direction::RIGHT].pin, internalPullup ? INPUT_PULLUP : INPUT); } void TwoButtonExtended::setTiming(uint8_t whichButton, uint32_t debounceMs, uint32_t longpressMs) { assert(whichButton < 2); buttons[whichButton].debounceLength = debounceMs; buttons[whichButton].longpressLength = longpressMs; } void TwoButtonExtended::setJoystickDebounce(uint32_t debounceMs) { joystickDebounceLength = debounceMs; } // Set what should happen when a button becomes pressed // Use this to implement a "while held" behavior void TwoButtonExtended::setHandlerDown(uint8_t whichButton, Callback onDown) { assert(whichButton < 2); buttons[whichButton].onDown = onDown; } // Set what should happen when a button becomes unpressed // Use this to implement a "While held" behavior void TwoButtonExtended::setHandlerUp(uint8_t whichButton, Callback onUp) { assert(whichButton < 2); buttons[whichButton].onUp = onUp; } // Set what should happen when a "short press" event has occurred void TwoButtonExtended::setHandlerShortPress(uint8_t whichButton, Callback onPress) { assert(whichButton < 2); buttons[whichButton].onPress = onPress; } // Set what should happen when a "long press" event has fired // Note: this will occur while the button is still held void TwoButtonExtended::setHandlerLongPress(uint8_t whichButton, Callback onLongPress) { assert(whichButton < 2); buttons[whichButton].onLongPress = onLongPress; } // Set what should happen when a joystick button becomes pressed // Use this to implement a "while held" behavior void TwoButtonExtended::setJoystickDownHandlers(Callback uDown, Callback dDown, Callback lDown, Callback rDown) { joystick[Direction::UP].onDown = uDown; joystick[Direction::DOWN].onDown = dDown; joystick[Direction::LEFT].onDown = lDown; joystick[Direction::RIGHT].onDown = rDown; } // Set what should happen when a joystick button becomes unpressed // Use this to implement a "while held" behavior void TwoButtonExtended::setJoystickUpHandlers(Callback uUp, Callback dUp, Callback lUp, Callback rUp) { joystick[Direction::UP].onUp = uUp; joystick[Direction::DOWN].onUp = dUp; joystick[Direction::LEFT].onUp = lUp; joystick[Direction::RIGHT].onUp = rUp; } // Set what should happen when a "press" event has fired // Note: this will occur while the joystick button is still held void TwoButtonExtended::setJoystickPressHandlers(Callback uPress, Callback dPress, Callback lPress, Callback rPress) { joystick[Direction::UP].onPress = uPress; joystick[Direction::DOWN].onPress = dPress; joystick[Direction::LEFT].onPress = lPress; joystick[Direction::RIGHT].onPress = rPress; } // Set press handlers for a two-way rocker mapped to left/right directions void TwoButtonExtended::setTwoWayRockerPressHandlers(Callback lPress, Callback rPress) { joystick[Direction::LEFT].onPress = lPress; joystick[Direction::RIGHT].onPress = rPress; } // Handle the start of a press to the primary button // Wakes our button thread void TwoButtonExtended::isrPrimary() { static volatile bool isrRunning = false; if (!isrRunning) { isrRunning = true; TwoButtonExtended *b = TwoButtonExtended::getInstance(); if (b->buttons[0].state == State::REST) { b->buttons[0].state = State::IRQ; b->buttons[0].irqAtMillis = millis(); b->startThread(); } isrRunning = false; } } // Handle the start of a press to the secondary button // Wakes our button thread void TwoButtonExtended::isrSecondary() { static volatile bool isrRunning = false; if (!isrRunning) { isrRunning = true; TwoButtonExtended *b = TwoButtonExtended::getInstance(); if (b->buttons[1].state == State::REST) { b->buttons[1].state = State::IRQ; b->buttons[1].irqAtMillis = millis(); b->startThread(); } isrRunning = false; } } // Handle the start of a press to the joystick buttons // Also wakes our button thread void TwoButtonExtended::isrJoystickUp() { static volatile bool isrRunning = false; if (!isrRunning) { isrRunning = true; TwoButtonExtended *b = TwoButtonExtended::getInstance(); if (b->joystick[Direction::UP].state == State::REST) { b->joystick[Direction::UP].state = State::IRQ; b->joystick[Direction::UP].irqAtMillis = millis(); b->startThread(); } isrRunning = false; } } void TwoButtonExtended::isrJoystickDown() { static volatile bool isrRunning = false; if (!isrRunning) { isrRunning = true; TwoButtonExtended *b = TwoButtonExtended::getInstance(); if (b->joystick[Direction::DOWN].state == State::REST) { b->joystick[Direction::DOWN].state = State::IRQ; b->joystick[Direction::DOWN].irqAtMillis = millis(); b->startThread(); } isrRunning = false; } } void TwoButtonExtended::isrJoystickLeft() { static volatile bool isrRunning = false; if (!isrRunning) { isrRunning = true; TwoButtonExtended *b = TwoButtonExtended::getInstance(); if (b->joystick[Direction::LEFT].state == State::REST) { b->joystick[Direction::LEFT].state = State::IRQ; b->joystick[Direction::LEFT].irqAtMillis = millis(); b->startThread(); } isrRunning = false; } } void TwoButtonExtended::isrJoystickRight() { static volatile bool isrRunning = false; if (!isrRunning) { isrRunning = true; TwoButtonExtended *b = TwoButtonExtended::getInstance(); if (b->joystick[Direction::RIGHT].state == State::REST) { b->joystick[Direction::RIGHT].state = State::IRQ; b->joystick[Direction::RIGHT].irqAtMillis = millis(); b->startThread(); } isrRunning = false; } } // Concise method to start our button thread // Follows an ISR, listening for button release void TwoButtonExtended::startThread() { if (!OSThread::enabled) { OSThread::setInterval(10); OSThread::enabled = true; } } // Concise method to stop our button thread // Called when we no longer need to poll for button release void TwoButtonExtended::stopThread() { if (OSThread::enabled) { OSThread::disable(); } // Reset both buttons manually // Just in case an IRQ fires during the process of resetting the system // Can occur with super rapid presses? buttons[0].state = REST; buttons[1].state = REST; joystick[Direction::UP].state = REST; joystick[Direction::DOWN].state = REST; joystick[Direction::LEFT].state = REST; joystick[Direction::RIGHT].state = REST; } // Our button thread // Started by an IRQ, on either button // Polls for button releases // Stops when both buttons released int32_t TwoButtonExtended::runOnce() { constexpr uint8_t BUTTON_COUNT = sizeof(buttons) / sizeof(Button); constexpr uint8_t JOYSTICK_COUNT = sizeof(joystick) / sizeof(SimpleButton); // Allow either button to request that our thread should continue polling bool awaitingRelease = false; // Check both primary and secondary buttons for (uint8_t i = 0; i < BUTTON_COUNT; i++) { switch (buttons[i].state) { // No action: button has not been pressed case REST: break; // New press detected by interrupt case IRQ: powerFSM.trigger(EVENT_PRESS); // Tell PowerFSM that press occurred (resets sleep timer) buttons[i].onDown(); // Run callback: press has begun (possible hold behavior) buttons[i].state = State::POLLING_UNFIRED; // Mark that button-down has been handled awaitingRelease = true; // Mark that polling-for-release should continue break; // An existing press continues // Not held long enough to register as longpress case POLLING_UNFIRED: { uint32_t length = millis() - buttons[i].irqAtMillis; // If button released since last thread tick, if (digitalRead(buttons[i].pin) != buttons[i].activeLogic) { buttons[i].onUp(); // Run callback: press has ended (possible release of a hold) buttons[i].state = State::REST; // Mark that the button has reset if (length > buttons[i].debounceLength && length < buttons[i].longpressLength) // If too short for longpress, buttons[i].onPress(); // Run callback: press } // If button not yet released else { awaitingRelease = true; // Mark that polling-for-release should continue if (length >= buttons[i].longpressLength) { // Run callback: long press (once) // Then continue waiting for release, to rearm buttons[i].state = State::POLLING_FIRED; buttons[i].onLongPress(); } } break; } // Button still held, but duration long enough that longpress event already fired // Just waiting for release case POLLING_FIRED: // Release detected if (digitalRead(buttons[i].pin) != buttons[i].activeLogic) { buttons[i].state = State::REST; buttons[i].onUp(); // Callback: release of hold (in this case: *after* longpress has fired) } // Not yet released, keep polling else awaitingRelease = true; break; } } // Check all the joystick directions for (uint8_t i = 0; i < JOYSTICK_COUNT; i++) { switch (joystick[i].state) { // No action: button has not been pressed case REST: break; // New press detected by interrupt case IRQ: powerFSM.trigger(EVENT_PRESS); // Tell PowerFSM that press occurred (resets sleep timer) joystick[i].onDown(); // Run callback: press has begun (possible hold behavior) joystick[i].state = State::POLLING_UNFIRED; // Mark that button-down has been handled awaitingRelease = true; // Mark that polling-for-release should continue break; // An existing press continues // Not held long enough to register as press case POLLING_UNFIRED: { uint32_t length = millis() - joystick[i].irqAtMillis; // If button released since last thread tick, if (digitalRead(joystick[i].pin) != joystickActiveLogic) { joystick[i].onUp(); // Run callback: press has ended (possible release of a hold) joystick[i].state = State::REST; // Mark that the button has reset } // If button not yet released else { awaitingRelease = true; // Mark that polling-for-release should continue if (length >= joystickDebounceLength) { // Run callback: long press (once) // Then continue waiting for release, to rearm joystick[i].state = State::POLLING_FIRED; joystick[i].onPress(); } } break; } // Button still held after press // Just waiting for release case POLLING_FIRED: // Release detected if (digitalRead(joystick[i].pin) != joystickActiveLogic) { joystick[i].state = State::REST; joystick[i].onUp(); // Callback: release of hold } // Not yet released, keep polling else awaitingRelease = true; break; } } // If all buttons are now released // we don't need to waste cpu resources polling // IRQ will restart this thread when we next need it if (!awaitingRelease) stopThread(); // Run this method again, or don't.. // Use whatever behavior was previously set by stopThread() or startThread() return OSThread::interval; } #ifdef ARCH_ESP32 // Detach our class' interrupts before lightsleep // Allows sleep.cpp to configure its own interrupts, which wake the device on user-button press int TwoButtonExtended::beforeLightSleep(void *unused) { stop(); return 0; // Indicates success } // Reconfigure our interrupts // Our class' interrupts were disconnected during sleep, to allow the user button to wake the device from sleep int TwoButtonExtended::afterLightSleep(esp_sleep_wakeup_cause_t cause) { start(); // Manually trigger the button-down ISR // - during light sleep, our ISR is disabled // - if light sleep ends by button press, pretend our own ISR caught it // - need to manually confirm by reading pin ourselves, to avoid occasional false positives // (false positive only when using internal pullup resistors?) if (cause == ESP_SLEEP_WAKEUP_GPIO && digitalRead(buttons[0].pin) == buttons[0].activeLogic) isrPrimary(); return 0; // Indicates success } #endif #endif ================================================ FILE: src/graphics/niche/Inputs/TwoButtonExtended.h ================================================ #ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS /* Re-usable NicheGraphics input source Short and Long press for up to two buttons Interrupt driven */ /* This expansion adds support for four more buttons These buttons are single-action only, no long press Interrupt driven */ #pragma once #include "configuration.h" #include "assert.h" #include "functional" #ifdef ARCH_ESP32 #include "esp_sleep.h" // For light-sleep handling #endif #include "Observer.h" namespace NicheGraphics::Inputs { class TwoButtonExtended : protected concurrency::OSThread { public: typedef std::function Callback; static uint8_t getUserButtonPin(); // Resolve the GPIO, considering the various possible source of definition static TwoButtonExtended *getInstance(); // Create or get the singleton instance void start(); // Start handling button input void stop(); // Stop handling button input (disconnect ISRs for sleep) void setWiring(uint8_t whichButton, uint8_t pin, bool internalPullup = false); void setJoystickWiring(uint8_t uPin, uint8_t dPin, uint8_t lPin, uint8_t rPin, bool internalPullup = false); void setTwoWayRockerWiring(uint8_t leftPin, uint8_t rightPin, bool internalPullup = false); void setTiming(uint8_t whichButton, uint32_t debounceMs, uint32_t longpressMs); void setJoystickDebounce(uint32_t debounceMs); void setHandlerDown(uint8_t whichButton, Callback onDown); void setHandlerUp(uint8_t whichButton, Callback onUp); void setHandlerShortPress(uint8_t whichButton, Callback onShortPress); void setHandlerLongPress(uint8_t whichButton, Callback onLongPress); void setJoystickDownHandlers(Callback uDown, Callback dDown, Callback ldown, Callback rDown); void setJoystickUpHandlers(Callback uUp, Callback dUp, Callback lUp, Callback rUp); void setJoystickPressHandlers(Callback uPress, Callback dPress, Callback lPress, Callback rPress); void setTwoWayRockerPressHandlers(Callback lPress, Callback rPress); // Disconnect and reconnect interrupts for light sleep #ifdef ARCH_ESP32 int beforeLightSleep(void *unused); int afterLightSleep(esp_sleep_wakeup_cause_t cause); #endif private: // Internal state of a specific button enum State { REST, // Up, no activity IRQ, // Down detected, not yet handled POLLING_UNFIRED, // Down handled, polling for release POLLING_FIRED, // Longpress fired, button still held }; // Joystick Directions enum Direction { UP = 0, DOWN, LEFT, RIGHT }; // Data used for direction (single-action) buttons class SimpleButton { public: // Per-button config uint8_t pin = 0xFF; // 0xFF: unset volatile State state = State::REST; // Internal state volatile uint32_t irqAtMillis; // millis() when button went down // Per-button event callbacks static void noop(){}; std::function onDown = noop; std::function onUp = noop; std::function onPress = noop; }; // Data used for double-action buttons class Button : public SimpleButton { public: // Per-button extended config bool activeLogic = LOW; // Active LOW by default. uint32_t debounceLength = 50; // Minimum length for shortpress in ms uint32_t longpressLength = 500; // Time until longpress in ms // Per-button event callbacks std::function onLongPress = noop; }; #ifdef ARCH_ESP32 // Get notified when lightsleep begins and ends CallbackObserver lsObserver = CallbackObserver(this, &TwoButtonExtended::beforeLightSleep); CallbackObserver lsEndObserver = CallbackObserver(this, &TwoButtonExtended::afterLightSleep); #endif int32_t runOnce() override; // Timer method. Polls for button release void startThread(); // Start polling for release void stopThread(); // Stop polling for release static void isrPrimary(); // User Button ISR static void isrSecondary(); // optional aux button or joystick center static void isrJoystickUp(); static void isrJoystickDown(); static void isrJoystickLeft(); static void isrJoystickRight(); TwoButtonExtended(); // Constructor made private: force use of Button::instance() // Info about both buttons Button buttons[2]; bool joystickActiveLogic = LOW; // Active LOW by default uint32_t joystickDebounceLength = 50; // time until press in ms SimpleButton joystick[4]; }; }; // namespace NicheGraphics::Inputs #endif ================================================ FILE: src/graphics/niche/README.md ================================================ # NicheGraphics A pattern / collection of resources for creating custom UIs, to target small groups of devices which have specific design requirements. For an example, see the `heltec-vision-master-e290-inkhud` platformio env. - platformio.ini - suppress default Meshtastic components (Screen, ButtonThread, etc) - define `MESHTASTIC_INCLUDE_NICHE_GRAPHICS` - (possibly) Edit `build_src_filter` to include our new nicheGraphics.h file - nicheGraphics.h - `#include` all necessary components - perform all setup and config inside a `setupNicheGraphics()` method ================================================ FILE: src/graphics/niche/Utils/CannedMessageStore.cpp ================================================ #ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS #include "./CannedMessageStore.h" #include "FSCommon.h" #include "NodeDB.h" #include "SPILock.h" #include "generated/meshtastic/cannedmessages.pb.h" using namespace NicheGraphics; // Location of the file which stores the canned messages on flash static const char *cannedMessagesConfigFile = "/prefs/cannedConf.proto"; CannedMessageStore::CannedMessageStore() { #if !MESHTASTIC_EXCLUDE_ADMIN adminMessageObserver.observe(adminModule); #endif // Load & parse messages from flash load(); } // Get access to (or create) the singleton instance of this class CannedMessageStore *CannedMessageStore::getInstance() { // Instantiate the class the first time this method is called static CannedMessageStore *const singletonInstance = new CannedMessageStore; return singletonInstance; } // Access canned messages by index // Consumer should check CannedMessageStore::size to avoid accessing out of bounds const std::string &CannedMessageStore::at(uint8_t i) { assert(i < messages.size()); return messages.at(i); } // Number of canned message strings available uint8_t CannedMessageStore::size() { return messages.size(); } // Load canned message data from flash, and parse into the individual strings void CannedMessageStore::load() { // In case we're reloading messages.clear(); // Attempt to load the bulk canned message data from flash meshtastic_CannedMessageModuleConfig cannedMessageModuleConfig; LoadFileResult result = nodeDB->loadProto("/prefs/cannedConf.proto", meshtastic_CannedMessageModuleConfig_size, sizeof(meshtastic_CannedMessageModuleConfig), &meshtastic_CannedMessageModuleConfig_msg, &cannedMessageModuleConfig); // Abort if nothing to load if (result != LoadFileResult::LOAD_SUCCESS || strlen(cannedMessageModuleConfig.messages) == 0) return; // Split into individual canned messages // These are concatenated when stored in flash, using '|' as a delimiter std::string s; for (char c : cannedMessageModuleConfig.messages) { // Character by character // If found end of a string if (c == '|' || c == '\0') { // Copy into the vector (if non-empty) if (!s.empty()) messages.push_back(s); // Reset the string builder s.clear(); // End of data, all strings processed if (c == 0) break; } // Otherwise, append char (continue building string) else s.push_back(c); } } // Handle incoming admin messages // We get these as an observer of AdminModule // It's our responsibility to handle setting and getting of canned messages via the client API // Ordinarily, this would be handled by the CannedMessageModule, but it is bound to Screen.cpp, so not suitable for NicheGraphics int CannedMessageStore::onAdminMessage(AdminModule_ObserverData *data) { switch (data->request->which_payload_variant) { // Client API changing the canned messages case meshtastic_AdminMessage_set_canned_message_module_messages_tag: handleSet(data->request); *data->result = AdminMessageHandleResult::HANDLED; break; // Client API wants to know the current canned messages case meshtastic_AdminMessage_get_canned_message_module_messages_request_tag: handleGet(data->response); *data->result = AdminMessageHandleResult::HANDLED_WITH_RESPONSE; break; default: break; } return 0; // Tell caller to continue notifying other observers. (No reason to abort this event) } // Client API changing the canned messages void CannedMessageStore::handleSet(const meshtastic_AdminMessage *request) { // Copy into the correct struct (for writing to flash as protobuf) meshtastic_CannedMessageModuleConfig cannedMessageModuleConfig; strncpy(cannedMessageModuleConfig.messages, request->set_canned_message_module_messages, sizeof(cannedMessageModuleConfig.messages)); // Ensure the directory exists #ifdef FSCom spiLock->lock(); FSCom.mkdir("/prefs"); spiLock->unlock(); #endif // Write to flash nodeDB->saveProto(cannedMessagesConfigFile, meshtastic_CannedMessageModuleConfig_size, &meshtastic_CannedMessageModuleConfig_msg, &cannedMessageModuleConfig); // Reload from flash, to update the canned messages in RAM // (This is a lazy way to handle it) load(); } // Client API wants to know the current canned messages // We're reconstructing the monolithic canned message string from our copy of the messages in RAM // Lazy, but more convenient that reloading the monolithic string from flash just for this void CannedMessageStore::handleGet(meshtastic_AdminMessage *response) { // Merge the canned messages back into the delimited format expected std::string merged; if (!messages.empty()) { // Don't run if no messages: error on pop_back with size=0 merged.reserve(201); for (const std::string &s : messages) { merged += s; merged += '|'; } merged.pop_back(); // Drop the final delimiter (loop added one too many) } // Place the data into the response // This response is scoped to AdminModule::handleReceivedProtobuf // We were passed reference to it via the observable response->which_payload_variant = meshtastic_AdminMessage_get_canned_message_module_messages_response_tag; strncpy(response->get_canned_message_module_messages_response, merged.c_str(), strlen(merged.c_str()) + 1); } #endif ================================================ FILE: src/graphics/niche/Utils/CannedMessageStore.h ================================================ #ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS /* Re-usable NicheGraphics tool Makes canned message data accessible to any NicheGraphics UI. - handles loading & parsing from flash - handles the admin messages for setting & getting canned messages via client API (phone apps, etc) The original CannedMessageModule class is bound to Screen.cpp, making it incompatible with the NicheGraphics framework, which suppresses Screen.cpp This implementation aims to be self-contained. The necessary interaction with the AdminModule is done as an observer. */ #pragma once #include "configuration.h" #include "modules/AdminModule.h" namespace NicheGraphics { class CannedMessageStore { public: static CannedMessageStore *getInstance(); // Create or get the singleton instance const std::string &at(uint8_t i); // Get canned message at index uint8_t size(); // Get total number of canned messages int onAdminMessage(AdminModule_ObserverData *data); // Handle incoming admin messages private: CannedMessageStore(); // Constructor made private: force use of CannedMessageStore::instance() void load(); // Load from flash, and parse void handleSet(const meshtastic_AdminMessage *request); // Client API changing the canned messages void handleGet(meshtastic_AdminMessage *response); // Client API wants to know current canned messages std::vector messages; // Get notified of incoming admin messages, to get / set canned messages CallbackObserver adminMessageObserver = CallbackObserver(this, &CannedMessageStore::onAdminMessage); }; }; // namespace NicheGraphics #endif ================================================ FILE: src/graphics/niche/Utils/FlashData.h ================================================ #ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS /* Re-usable NicheGraphics tool Save settings / data to flash, without use of the Meshtastic Protobufs Avoid bloating everyone's protobuf code for our one-off UI implementations */ #pragma once #include "configuration.h" #include "SPILock.h" #include "SafeFile.h" namespace NicheGraphics { template class FlashData { private: static std::string getFilename(const char *label) { std::string filename; filename += "/NicheGraphics"; filename += "/"; filename += label; filename += ".data"; return filename; } static uint32_t getHash(T *data) { uint32_t hash = 0; // Sum all bytes of the image buffer together for (uint32_t i = 0; i < sizeof(T); i++) hash ^= ((uint8_t *)data)[i] + 1; return hash; } public: static bool load(T *data, const char *label) { // Take firmware's SPI lock concurrency::LockGuard guard(spiLock); // Set false if we run into issues bool okay = true; // Get a filename based on the label std::string filename = getFilename(label); #ifdef FSCom // Check that the file *does* actually exist if (!FSCom.exists(filename.c_str())) { LOG_WARN("'%s' not found. Using default values", filename.c_str()); okay = false; return okay; } // Open the file auto f = FSCom.open(filename.c_str(), FILE_O_READ); // If opened, start reading if (f) { LOG_INFO("Loading NicheGraphics data '%s'", filename.c_str()); // Create an object which will received data from flash // We read here first, so we can verify the checksum, without committing to overwriting the *data object // Allows us to retain any defaults that might be set after we declared *data, but before loading settings, // in case the flash values are corrupt T flashData; // Read the actual data f.readBytes((char *)&flashData, sizeof(T)); // Read the hash uint32_t savedHash = 0; f.readBytes((char *)&savedHash, sizeof(savedHash)); // Calculate hash of the loaded data, then compare with the saved hash // If hash looks good, copy the values to the main data object uint32_t calculatedHash = getHash(&flashData); if (savedHash != calculatedHash) { LOG_WARN("'%s' is corrupt (hash mismatch). Using default values", filename.c_str()); okay = false; } else *data = flashData; f.close(); } else { LOG_ERROR("Could not open / read %s", filename.c_str()); okay = false; } #else LOG_ERROR("Filesystem not implemented"); state = LoadFileState::NO_FILESYSTEM; okay = false; #endif return okay; } // Save module's custom data (settings?) to flash. Doesn't use protobufs // Takes the firmware's SPI lock, in case the files are stored on SD card // Need to lock and unlock around specific FS methods, as the SafeFile class takes the lock for itself internally. static void save(T *data, const char *label) { // Get a filename based on the label std::string filename = getFilename(label); #ifdef FSCom spiLock->lock(); FSCom.mkdir("/NicheGraphics"); spiLock->unlock(); auto f = SafeFile(filename.c_str(), true); // "true": full atomic. Write new data to temp file, then rename. LOG_INFO("Saving %s", filename.c_str()); // Calculate a hash of the data uint32_t hash = getHash(data); spiLock->lock(); f.write((uint8_t *)data, sizeof(T)); // Write the actual data f.write((uint8_t *)&hash, sizeof(hash)); // Append the hash spiLock->unlock(); bool writeSucceeded = f.close(); if (!writeSucceeded) { LOG_ERROR("Can't write data!"); } #else LOG_ERROR("ERROR: Filesystem not implemented\n"); #endif } }; // Erase contents of the NicheGraphics data directory inline void clearFlashData() { // Take firmware's SPI lock, in case the files are stored on SD card concurrency::LockGuard guard(spiLock); #ifdef FSCom File dir = FSCom.open("/NicheGraphics"); // Open the directory File file = dir.openNextFile(); // Attempt to open the first file in the directory // While the directory still contains files while (file) { std::string path = "/NicheGraphics/"; path += file.name(); LOG_DEBUG("Erasing %s", path.c_str()); file.close(); FSCom.remove(path.c_str()); file = dir.openNextFile(); } #else LOG_ERROR("ERROR: Filesystem not implemented\n"); #endif } } // namespace NicheGraphics #endif ================================================ FILE: src/graphics/tftSetup.cpp ================================================ #if HAS_TFT #include "SPILock.h" #include "sleep.h" #include "api/PacketAPI.h" #include "comms/PacketClient.h" #include "comms/PacketServer.h" #include "graphics/DeviceScreen.h" #include "graphics/driver/DisplayDriverConfig.h" #ifdef ARCH_PORTDUINO #include "PortduinoGlue.h" #include #endif DeviceScreen *deviceScreen = nullptr; #ifdef ARCH_ESP32 // Get notified when the system is entering light sleep CallbackObserver tftSleepObserver = CallbackObserver(deviceScreen, &DeviceScreen::prepareSleep); CallbackObserver endSleepObserver = CallbackObserver(deviceScreen, &DeviceScreen::wakeUp); #endif void tft_task_handler(void *param = nullptr) { while (true) { spiLock->lock(); deviceScreen->task_handler(); spiLock->unlock(); deviceScreen->sleep(); } } void tftSetup(void) { #ifndef ARCH_PORTDUINO deviceScreen = &DeviceScreen::create(); PacketAPI::create(PacketServer::init()); deviceScreen->init(new PacketClient); #else if (portduino_config.displayPanel != no_screen) { DisplayDriverConfig displayConfig; static char *panels[] = {"NOSCREEN", "X11", "FB", "ST7789", "ST7735", "ST7735S", "ST7796", "ILI9341", "ILI9342", "ILI9486", "ILI9488", "HX8357D"}; static char *touch[] = {"NOTOUCH", "XPT2046", "STMPE610", "GT911", "FT5x06"}; #if defined(USE_X11) if (portduino_config.displayPanel == x11) { if (portduino_config.displayWidth && portduino_config.displayHeight) displayConfig = DisplayDriverConfig(DisplayDriverConfig::device_t::X11, (uint16_t)portduino_config.displayWidth, (uint16_t)portduino_config.displayHeight); else displayConfig.device(DisplayDriverConfig::device_t::X11); } else #elif defined(USE_FRAMEBUFFER) if (portduino_config.displayPanel == fb) { if (portduino_config.displayWidth && portduino_config.displayHeight) displayConfig = DisplayDriverConfig(DisplayDriverConfig::device_t::FB, (uint16_t)portduino_config.displayWidth, (uint16_t)portduino_config.displayHeight); else displayConfig.device(DisplayDriverConfig::device_t::FB); } else #endif { displayConfig.device(DisplayDriverConfig::device_t::CUSTOM_TFT) .panel(DisplayDriverConfig::panel_config_t{.type = panels[portduino_config.displayPanel], .panel_width = (uint16_t)portduino_config.displayWidth, .panel_height = (uint16_t)portduino_config.displayHeight, .rotation = (bool)portduino_config.displayRotate, .pin_cs = (int16_t)portduino_config.displayCS.pin, .pin_rst = (int16_t)portduino_config.displayReset.pin, .offset_x = (uint16_t)portduino_config.displayOffsetX, .offset_y = (uint16_t)portduino_config.displayOffsetY, .offset_rotation = (uint8_t)portduino_config.displayOffsetRotate, .invert = portduino_config.displayInvert ? true : false, .rgb_order = (bool)portduino_config.displayRGBOrder, .dlen_16bit = portduino_config.displayPanel == ili9486 || portduino_config.displayPanel == ili9488}) .bus(DisplayDriverConfig::bus_config_t{.freq_write = (uint32_t)portduino_config.displayBusFrequency, .freq_read = 16000000, .spi{.pin_dc = (int8_t)portduino_config.displayDC.pin, .use_lock = true, .spi_host = (uint16_t)portduino_config.display_spi_dev_int}}) .input(DisplayDriverConfig::input_config_t{.keyboardDevice = portduino_config.keyboardDevice, .pointerDevice = portduino_config.pointerDevice}) .light(DisplayDriverConfig::light_config_t{.pin_bl = (int16_t)portduino_config.displayBacklight.pin, .pwm_channel = (int8_t)portduino_config.displayBacklightPWMChannel.pin, .invert = (bool)portduino_config.displayBacklightInvert}); if (portduino_config.touchscreenI2CAddr == -1) { displayConfig.touch( DisplayDriverConfig::touch_config_t{.type = touch[portduino_config.touchscreenModule], .freq = (uint32_t)portduino_config.touchscreenBusFrequency, .pin_int = (int16_t)portduino_config.touchscreenIRQ.pin, .offset_rotation = (uint8_t)portduino_config.touchscreenRotate, .spi{ .spi_host = (int8_t)portduino_config.touchscreen_spi_dev_int, }, .pin_cs = (int16_t)portduino_config.touchscreenCS.pin}); } else { displayConfig.touch(DisplayDriverConfig::touch_config_t{ .type = touch[portduino_config.touchscreenModule], .freq = (uint32_t)portduino_config.touchscreenBusFrequency, .x_min = 0, .x_max = (int16_t)((portduino_config.touchscreenRotate & 1 ? portduino_config.displayWidth : portduino_config.displayHeight) - 1), .y_min = 0, .y_max = (int16_t)((portduino_config.touchscreenRotate & 1 ? portduino_config.displayHeight : portduino_config.displayWidth) - 1), .pin_int = (int16_t)portduino_config.touchscreenIRQ.pin, .offset_rotation = (uint8_t)portduino_config.touchscreenRotate, .i2c{.i2c_addr = (uint8_t)portduino_config.touchscreenI2CAddr}}); } } deviceScreen = &DeviceScreen::create(&displayConfig); PacketAPI::create(PacketServer::init()); deviceScreen->init(new PacketClient); } else { LOG_INFO("Running without TFT display!"); } #endif if (deviceScreen) { #ifdef ARCH_ESP32 tftSleepObserver.observe(¬ifyLightSleep); endSleepObserver.observe(¬ifyLightSleepEnd); xTaskCreatePinnedToCore(tft_task_handler, "tft", 10240, NULL, 1, NULL, 0); #elif defined(ARCH_PORTDUINO) std::thread *tft_task = new std::thread([] { tft_task_handler(); }); #endif } } #endif ================================================ FILE: src/input/BBQ10Keyboard.cpp ================================================ // 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); } } ================================================ FILE: src/input/BBQ10Keyboard.h ================================================ // 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; }; ================================================ FILE: src/input/ButtonThread.cpp ================================================ #include "ButtonThread.h" #include "meshUtils.h" #include "configuration.h" #if !MESHTASTIC_EXCLUDE_GPS #include "GPS.h" #endif #include "MeshService.h" #include "RadioLibInterface.h" #include "buzz.h" #include "input/InputBroker.h" #include "main.h" #include "modules/CannedMessageModule.h" #include "modules/ExternalNotificationModule.h" #include "power.h" #include "sleep.h" #ifdef ARCH_PORTDUINO #include "platform/portduino/PortduinoGlue.h" #endif using namespace concurrency; #if HAS_BUTTON #endif ButtonThread::ButtonThread(const char *name) : OSThread(name) { _originName = name; } bool ButtonThread::initButton(const ButtonConfig &config) { if (inputBroker) inputBroker->registerSource(this); _longPressTime = config.longPressTime; _longLongPressTime = config.longLongPressTime; _pinNum = config.pinNumber; _activeLow = config.activeLow; _touchQuirk = config.touchQuirk; _intRoutine = config.intRoutine; _pressHandler = config.onPress; _releaseHandler = config.onRelease; _suppressLeadUp = config.suppressLeadUpSound; _longLongPress = config.longLongPress; userButton = OneButton(config.pinNumber, config.activeLow, config.activePullup); if (config.pullupSense != 0) { pinMode(config.pinNumber, config.pullupSense); } _singlePress = config.singlePress; userButton.attachClick( [](void *callerThread) -> void { ButtonThread *thread = (ButtonThread *)callerThread; thread->btnEvent = BUTTON_EVENT_PRESSED; }, this); _longPress = config.longPress; userButton.attachLongPressStart( [](void *callerThread) -> void { ButtonThread *thread = (ButtonThread *)callerThread; // if (millis() > 30000) // hold off 30s after boot thread->btnEvent = BUTTON_EVENT_LONG_PRESSED; }, this); userButton.attachLongPressStop( [](void *callerThread) -> void { ButtonThread *thread = (ButtonThread *)callerThread; // if (millis() > 30000) // hold off 30s after boot thread->btnEvent = BUTTON_EVENT_LONG_RELEASED; }, this); if (config.doublePress != INPUT_BROKER_NONE) { _doublePress = config.doublePress; userButton.attachDoubleClick( [](void *callerThread) -> void { ButtonThread *thread = (ButtonThread *)callerThread; thread->btnEvent = BUTTON_EVENT_DOUBLE_PRESSED; }, this); } if (config.triplePress != INPUT_BROKER_NONE) { _triplePress = config.triplePress; userButton.attachMultiClick( [](void *callerThread) -> void { ButtonThread *thread = (ButtonThread *)callerThread; thread->storeClickCount(); thread->btnEvent = BUTTON_EVENT_MULTI_PRESSED; }, this); } if (config.shortLong != INPUT_BROKER_NONE) { _shortLong = config.shortLong; } #ifdef USE_EINK userButton.setDebounceMs(0); #else userButton.setDebounceMs(1); #endif userButton.setPressMs(_longPressTime); if (screen) { userButton.setClickMs(20); } else { userButton.setClickMs(BUTTON_CLICK_MS); } attachButtonInterrupts(); #ifdef ARCH_ESP32 // Register callbacks for before and after lightsleep // Used to detach and reattach interrupts lsObserver.observe(¬ifyLightSleep); lsEndObserver.observe(¬ifyLightSleepEnd); #endif return true; } int32_t ButtonThread::runOnce() { // If the button is pressed we suppress CPU sleep until release canSleep = true; // Assume we should not keep the board awake // Check for combination timeout if (waitingForLongPress && (millis() - shortPressTime) > BUTTON_COMBO_TIMEOUT_MS) { waitingForLongPress = false; } userButton.tick(); canSleep &= userButton.isIdle(); // Check if we should play lead-up sound during long press // Play lead-up when button has been held for BUTTON_LEADUP_MS but before long press triggers bool buttonCurrentlyPressed = isButtonPressed(_pinNum); // Detect start of button press if (buttonCurrentlyPressed && !buttonWasPressed) { if (_pressHandler) _pressHandler(); buttonPressStartTime = millis(); leadUpPlayed = false; leadUpSequenceActive = false; resetLeadUpSequence(); } // Progressive lead-up sound system if (!_suppressLeadUp && buttonCurrentlyPressed && (millis() - buttonPressStartTime) >= BUTTON_LEADUP_MS) { // Start the progressive sequence if not already active if (!leadUpSequenceActive) { leadUpSequenceActive = true; lastLeadUpNoteTime = millis(); playNextLeadUpNote(); // Play the first note immediately } // Continue playing notes at intervals else if ((millis() - lastLeadUpNoteTime) >= 400) { // 400ms interval between notes if (playNextLeadUpNote()) { lastLeadUpNoteTime = millis(); } else { leadUpPlayed = true; } } } // Reset when button is released if (!buttonCurrentlyPressed && buttonWasPressed) { if (_releaseHandler) _releaseHandler(); leadUpSequenceActive = false; resetLeadUpSequence(); } buttonWasPressed = buttonCurrentlyPressed; // new behavior if (btnEvent != BUTTON_EVENT_NONE) { InputEvent evt; evt.source = _originName; evt.kbchar = 0; evt.touchX = 0; evt.touchY = 0; switch (btnEvent) { case BUTTON_EVENT_PRESSED: { // Forward single press to InputBroker (but NOT as DOWN/SELECT, just forward a "button press" event) evt.inputEvent = _singlePress; // evt.kbchar = _singlePress; // todo: fix this. Some events are kb characters rather than event types this->notifyObservers(&evt); // Start tracking for potential combination waitingForLongPress = true; shortPressTime = millis(); break; } case BUTTON_EVENT_LONG_PRESSED: { // Ignore if: TX in progress // Uncommon T-Echo hardware bug, LoRa TX triggers touch button if (_touchQuirk && RadioLibInterface::instance && RadioLibInterface::instance->isSending()) break; // Check if this is part of a short-press + long-press combination if (_shortLong != INPUT_BROKER_NONE && waitingForLongPress && (millis() - shortPressTime) <= BUTTON_COMBO_TIMEOUT_MS) { evt.inputEvent = _shortLong; // evt.kbchar = _shortLong; this->notifyObservers(&evt); // Play the combination tune playComboTune(); break; } if (_longPress != INPUT_BROKER_NONE) { // Forward long press to InputBroker (but NOT as DOWN/SELECT, just forward a "button long press" event) evt.inputEvent = _longPress; this->notifyObservers(&evt); } // Reset combination tracking waitingForLongPress = false; break; } case BUTTON_EVENT_DOUBLE_PRESSED: { // not wired in if screen detected LOG_INFO("Double press!"); // Reset combination tracking waitingForLongPress = false; evt.inputEvent = _doublePress; // evt.kbchar = _doublePress; this->notifyObservers(&evt); playComboTune(); break; } case BUTTON_EVENT_MULTI_PRESSED: { // not wired in when screen is present LOG_INFO("Mulitipress! %hux", multipressClickCount); // Reset combination tracking waitingForLongPress = false; switch (multipressClickCount) { case 3: evt.inputEvent = _triplePress; // evt.kbchar = _triplePress; this->notifyObservers(&evt); playComboTune(); break; #if !HAS_SCREEN case 4: if (moduleConfig.external_notification.enabled && externalNotificationModule) { externalNotificationModule->setMute(!externalNotificationModule->getMute()); IF_SCREEN(if (!externalNotificationModule->getMute()) externalNotificationModule->stopNow();) if (externalNotificationModule->getMute()) { LOG_INFO("Temporarily Muted"); play4ClickDown(); // Disable tone } else { LOG_INFO("Unmuted"); play4ClickUp(); // Enable tone } } break; #endif // No valid multipress action default: break; } // end switch: click count break; } // end multipress event // Do actual shutdown when button released, otherwise the button release // may wake the board immediately. case BUTTON_EVENT_LONG_RELEASED: { LOG_INFO("LONG PRESS RELEASE AFTER %u MILLIS", millis() - buttonPressStartTime); // Require press started after boot holdoff to avoid phantom shutdown from floating pins if (millis() > 30000 && buttonPressStartTime > 30000 && _longLongPress != INPUT_BROKER_NONE && (millis() - buttonPressStartTime) >= _longLongPressTime && leadUpPlayed) { evt.inputEvent = _longLongPress; this->notifyObservers(&evt); } // Reset combination tracking waitingForLongPress = false; leadUpPlayed = false; break; } // doesn't handle BUTTON_EVENT_PRESSED_SCREEN BUTTON_EVENT_TOUCH_LONG_PRESSED BUTTON_EVENT_COMBO_SHORT_LONG default: { break; } } } btnEvent = BUTTON_EVENT_NONE; // only pull when the button is pressed, we get notified via IRQ on a new press if (!userButton.isIdle() || waitingForLongPress) { return 50; } return 100; // FIXME: Why can't we rely on interrupts and use INT32_MAX here? } /* * Attach (or re-attach) hardware interrupts for buttons * Public method. Used outside class when waking from MCU sleep */ void ButtonThread::attachButtonInterrupts() { // Interrupt for user button, during normal use. Improves responsiveness. attachInterrupt(_pinNum, _intRoutine, CHANGE); } /* * Detach the "normal" button interrupts. * Public method. Used before attaching a "wake-on-button" interrupt for MCU sleep */ void ButtonThread::detachButtonInterrupts() { detachInterrupt(_pinNum); } #ifdef ARCH_ESP32 // Detach our class' interrupts before lightsleep // Allows sleep.cpp to configure its own interrupts, which wake the device on user-button press int ButtonThread::beforeLightSleep(void *unused) { detachButtonInterrupts(); return 0; // Indicates success } // Reconfigure our interrupts // Our class' interrupts were disconnected during sleep, to allow the user button to wake the device from sleep int ButtonThread::afterLightSleep(esp_sleep_wakeup_cause_t cause) { attachButtonInterrupts(); return 0; // Indicates success } #endif // Non-static method, runs during callback. Grabs info while still valid void ButtonThread::storeClickCount() { multipressClickCount = userButton.getNumberClicks(); } ================================================ FILE: src/input/ButtonThread.h ================================================ #pragma once #include "InputBroker.h" #include "OneButton.h" #include "concurrency/OSThread.h" #include "configuration.h" typedef void (*voidFuncPtr)(void); struct ButtonConfig { uint8_t pinNumber; bool activeLow = true; bool activePullup = true; uint32_t pullupSense = 0; voidFuncPtr intRoutine = nullptr; voidFuncPtr onPress = nullptr; // Optional edge callbacks voidFuncPtr onRelease = nullptr; // Optional edge callbacks bool suppressLeadUpSound = false; input_broker_event singlePress = INPUT_BROKER_NONE; input_broker_event longPress = INPUT_BROKER_NONE; uint16_t longPressTime = 500; input_broker_event doublePress = INPUT_BROKER_NONE; input_broker_event longLongPress = INPUT_BROKER_NONE; uint16_t longLongPressTime = 3900; input_broker_event triplePress = INPUT_BROKER_NONE; input_broker_event shortLong = INPUT_BROKER_NONE; bool touchQuirk = false; // Constructor to set required parameter explicit ButtonConfig(uint8_t pin = 0) : pinNumber(pin) {} }; #ifndef BUTTON_CLICK_MS #define BUTTON_CLICK_MS 250 #endif #ifndef BUTTON_TOUCH_MS #define BUTTON_TOUCH_MS 400 #endif #ifndef BUTTON_COMBO_TIMEOUT_MS #define BUTTON_COMBO_TIMEOUT_MS 1000 // 1 second to complete the combination -- tap faster #endif #ifndef BUTTON_LEADUP_MS #define BUTTON_LEADUP_MS 2200 // Play lead-up sound after 2.5 seconds of holding #endif class ButtonThread : public Observable, public concurrency::OSThread { public: const char *_originName; static const uint32_t c_holdOffTime = 30000; // hold off 30s after boot bool initButton(const ButtonConfig &config); enum ButtonEventType { BUTTON_EVENT_NONE, BUTTON_EVENT_PRESSED, BUTTON_EVENT_PRESSED_SCREEN, BUTTON_EVENT_DOUBLE_PRESSED, BUTTON_EVENT_MULTI_PRESSED, BUTTON_EVENT_LONG_PRESSED, BUTTON_EVENT_LONG_RELEASED, BUTTON_EVENT_TOUCH_LONG_PRESSED, BUTTON_EVENT_COMBO_SHORT_LONG, }; explicit ButtonThread(const char *name); int32_t runOnce() override; OneButton userButton; void attachButtonInterrupts(); void detachButtonInterrupts(); void storeClickCount(); bool isButtonPressed(int buttonPin) { if (_activeLow) return !digitalRead(buttonPin); // Active low: pressed = LOW else return digitalRead(buttonPin); // Most buttons are active low by default } // Returns true while this thread's button is physically held down bool isHeld() { return isButtonPressed(_pinNum); } // Disconnect and reconnect interrupts for light sleep #ifdef ARCH_ESP32 int beforeLightSleep(void *unused); int afterLightSleep(esp_sleep_wakeup_cause_t cause); #endif private: input_broker_event _singlePress = INPUT_BROKER_NONE; input_broker_event _longPress = INPUT_BROKER_NONE; input_broker_event _longLongPress = INPUT_BROKER_NONE; input_broker_event _doublePress = INPUT_BROKER_NONE; input_broker_event _triplePress = INPUT_BROKER_NONE; input_broker_event _shortLong = INPUT_BROKER_NONE; voidFuncPtr _intRoutine = nullptr; voidFuncPtr _pressHandler = nullptr; voidFuncPtr _releaseHandler = nullptr; bool _suppressLeadUp = false; uint16_t _longPressTime = 500; uint16_t _longLongPressTime = 3900; int _pinNum = 0; bool _activeLow = true; bool _touchQuirk = false; uint32_t buttonPressStartTime = 0; bool buttonWasPressed = false; #ifdef ARCH_ESP32 // Get notified when lightsleep begins and ends CallbackObserver lsObserver = CallbackObserver(this, &ButtonThread::beforeLightSleep); CallbackObserver lsEndObserver = CallbackObserver(this, &ButtonThread::afterLightSleep); #endif volatile ButtonEventType btnEvent = BUTTON_EVENT_NONE; // Store click count during callback, for later use volatile int multipressClickCount = 0; // Combination tracking state bool waitingForLongPress = false; uint32_t shortPressTime = 0; // Long press lead-up tracking bool leadUpPlayed = false; uint32_t lastLeadUpNoteTime = 0; bool leadUpSequenceActive = false; static void wakeOnIrq(int irq, int mode); }; extern ButtonThread *buttonThread; ================================================ FILE: src/input/CardputerKeyboard.cpp ================================================ #if defined(M5STACK_CARDPUTER_ADV) #include "CardputerKeyboard.h" #include "main.h" #define _TCA8418_COLS 8 #define _TCA8418_ROWS 7 #define _TCA8418_NUM_KEYS 56 #define _TCA8418_MULTI_TAP_THRESHOLD 1500 using Key = TCA8418KeyboardBase::TCA8418Key; constexpr uint8_t modifierFnKey = 2; constexpr uint8_t modifierFn = 0b0010; constexpr uint8_t modifierCtrlKey = 3; constexpr uint8_t modifierShiftKey = 6; constexpr uint8_t modifierShift = 0b0001; constexpr uint8_t modifierOptKey = 7; constexpr uint8_t modifierAltKey = 11; // Num chars per key, Modulus for rotating through characters static uint8_t CardputerTapMod[_TCA8418_NUM_KEYS] = {3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3}; static unsigned char CardputerTapMap[_TCA8418_NUM_KEYS][3] = {{'`', '~', Key::ESC}, {Key::TAB, 0x00, 0x00}, {0x00, 0x00, 0x00}, {0x00, 0x00, 0x00}, {'1', '!', 0x00}, {'q', 'Q', Key::REBOOT}, {0x00, 0x00, 0x00}, {0x00, 0x00, 0x00}, {'2', '@', 0x00}, {'w', 'W', 0x00}, {'a', 'A', 0x00}, {0x00, 0x00, 0x00}, {'3', '#', 0x00}, {'e', 'E', 0x00}, {'s', 'S', 0x00}, {'z', 'Z', 0x00}, {'4', '$', 0x00}, {'r', 'R', 0x00}, {'d', 'D', 0x00}, {'x', 'X', 0x00}, {'5', '%', 0x00}, {'t', 'T', 0x00}, {'f', 'F', 0x00}, {'c', 'C', 0x00}, {'6', '^', 0x00}, {'y', 'Y', 0x00}, {'g', 'G', Key::GPS_TOGGLE}, {'v', 'V', 0x00}, {'7', '&', 0x00}, {'u', 'U', 0x00}, {'h', 'H', 0x00}, {'b', 'B', Key::BT_TOGGLE}, {'8', '*', 0x00}, {'i', 'I', 0x00}, {'j', 'J', 0x00}, {'n', 'N', 0x00}, {'9', '(', 0x00}, {'o', 'O', 0x00}, {'k', 'K', 0x00}, {'m', 'M', Key::MUTE_TOGGLE}, {'0', ')', 0x00}, {'p', 'P', Key::SEND_PING}, {'l', 'L', 0x00}, {',', '<', Key::LEFT}, {'_', '-', 0x00}, {'[', '{', 0x00}, {';', ':', Key::UP}, {'.', '>', Key::DOWN}, {'=', '+', 0x00}, {']', '}', 0x00}, {'\'', '"', 0x00}, {'/', '?', Key::RIGHT}, {Key::BSP, 0x00, 0x00}, {'\\', '|', 0x00}, {Key::SELECT, 0x00, 0x00}, {' ', ' ', ' '}}; CardputerKeyboard::CardputerKeyboard() : TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), modifierFlag(0), last_modifier_time(0), last_key(-1), next_key(-1), last_tap(0L), char_idx(0), tap_interval(0) { reset(); } void CardputerKeyboard::reset(void) { TCA8418KeyboardBase::reset(); } // handle multi-key presses (shift and alt) void CardputerKeyboard::trigger() { uint8_t count = keyCount(); if (count == 0) return; for (uint8_t i = 0; i < count; ++i) { uint8_t k = readRegister(TCA8418_REG_KEY_EVENT_A + i); uint8_t key = k & 0x7F; if (k & 0x80) { pressed(key); } else { released(); state = Idle; } } } void CardputerKeyboard::pressed(uint8_t key) { if (state == Init || state == Busy) { return; } if (modifierFlag && (millis() - last_modifier_time > _TCA8418_MULTI_TAP_THRESHOLD)) { modifierFlag = 0; } uint8_t next_key = 0; int row = (key - 1) / 10; int col = (key - 1) % 10; if (row >= _TCA8418_ROWS || col >= _TCA8418_COLS) { return; // Invalid key } next_key = row * _TCA8418_COLS + col; state = Held; uint32_t now = millis(); tap_interval = now - last_tap; updateModifierFlag(next_key); if (isModifierKey(next_key)) { last_modifier_time = now; } if (tap_interval < 0) { last_tap = 0; state = Busy; return; } if (next_key != last_key || tap_interval > _TCA8418_MULTI_TAP_THRESHOLD) { char_idx = 0; } else { char_idx += 1; } last_key = next_key; last_tap = now; } void CardputerKeyboard::released() { if (state != Held) { return; } if (last_key < 0 || last_key >= _TCA8418_NUM_KEYS) { last_key = -1; state = Idle; return; } uint32_t now = millis(); last_tap = now; if (inputBroker->menuMode && modifierFlag == 0) { if (last_key == 0 || last_key == 43 || last_key == 46 || last_key == 47 || last_key == 51) { // esc, left, up, down, right key modifierFlag = modifierFn; } } queueEvent(CardputerTapMap[last_key][modifierFlag % CardputerTapMod[last_key]]); if (isModifierKey(last_key) == false) modifierFlag = 0; } void CardputerKeyboard::updateModifierFlag(uint8_t key) { if (key == modifierShiftKey) { modifierFlag ^= modifierShift; } else if (key == modifierFnKey) { modifierFlag ^= modifierFn; } } bool CardputerKeyboard::isModifierKey(uint8_t key) { return (key == modifierShiftKey || key == modifierFnKey); } #endif ================================================ FILE: src/input/CardputerKeyboard.h ================================================ #include "TCA8418KeyboardBase.h" class CardputerKeyboard : public TCA8418KeyboardBase { public: CardputerKeyboard(); void reset(void); void trigger(void) override; virtual ~CardputerKeyboard() {} protected: void pressed(uint8_t key) override; void released(void) override; void updateModifierFlag(uint8_t key); bool isModifierKey(uint8_t key); private: uint8_t modifierFlag; uint32_t last_modifier_time; int8_t last_key; int8_t next_key; uint32_t last_tap; uint8_t char_idx; int32_t tap_interval; }; ================================================ FILE: src/input/ExpressLRSFiveWay.cpp ================================================ #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(INPUT_BROKER_CANCEL); // exit the menu (press imaginary cancel key) else sendKey(INPUT_BROKER_LEFT); break; case RIGHT: if (inCannedMessageMenu()) // If in canned message menu: sendKey(INPUT_BROKER_CANCEL); // exit the menu (press imaginary cancel key) else sendKey(INPUT_BROKER_RIGHT); break; case UP: if (length == LONG) toggleGPS(); else sendKey(INPUT_BROKER_UP); break; case DOWN: if (length == LONG) sendAdhocPing(); else sendKey(INPUT_BROKER_DOWN); break; case OK: if (length == LONG) shutdown(); else click(); // Use instead of sendKey(OK). Works better when canned message module disabled break; default: break; } } // Feed input to the canned messages module void ExpressLRSFiveWay::sendKey(input_broker_event key) { InputEvent e = {}; e.source = inputSourceName; e.inputEvent = key; notifyObservers(&e); } // 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 (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() { sendKey(INPUT_BROKER_SHUTDOWN); } void ExpressLRSFiveWay::click() { sendKey(INPUT_BROKER_SELECT); } ExpressLRSFiveWay *expressLRSFiveWayInput = nullptr; #endif ================================================ FILE: src/input/ExpressLRSFiveWay.h ================================================ /* 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 = INPUT_BROKER_UP, DOWN = INPUT_BROKER_DOWN, LEFT = INPUT_BROKER_LEFT, RIGHT = INPUT_BROKER_RIGHT, OK = INPUT_BROKER_SELECT, CANCEL = INPUT_BROKER_CANCEL, NO_PRESS = INPUT_BROKER_NONE } KeyType; typedef enum { SHORT, LONG } PressLength; // From ExpressLRS int keyInProcess; uint32_t keyDownStart; bool isLongPressed; const uint16_t joyAdcValues[N_JOY_ADC_VALUES] = {JOYSTICK_ADC_VALS}; uint16_t fuzzValues[N_JOY_ADC_VALUES]; void calcFuzzValues(); int readKey(); void update(int *keyValue, bool *keyLongPressed); // Meshtastic code void determineAction(KeyType key, PressLength length); void sendKey(input_broker_event key); inline bool inCannedMessageMenu() { return cannedMessageModule->shouldDraw(); } int32_t runOnce() override; // 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 ================================================ FILE: src/input/HackadayCommunicatorKeyboard.cpp ================================================ #if defined(HACKADAY_COMMUNICATOR) #include "HackadayCommunicatorKeyboard.h" #include "main.h" #define _TCA8418_COLS 10 #define _TCA8418_ROWS 8 #define _TCA8418_NUM_KEYS 80 #define _TCA8418_MULTI_TAP_THRESHOLD 1500 using Key = TCA8418KeyboardBase::TCA8418Key; constexpr uint8_t modifierRightShiftKey = 30; constexpr uint8_t modifierRightShift = 0b0001; constexpr uint8_t modifierLeftShiftKey = 76; // keynum -1 constexpr uint8_t modifierLeftShift = 0b0001; // constexpr uint8_t modifierSymKey = 42; // constexpr uint8_t modifierSym = 0b0010; // Num chars per key, Modulus for rotating through characters static uint8_t HackadayCommunicatorTapMod[_TCA8418_NUM_KEYS] = { 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 2, 2, 2, 2, 2, 2, 0, 0, 0, 1, 2, 2, 2, 1, 2, 2, 0, 0, 0, 2, 1, 2, 2, 0, 1, 1, 0, }; static unsigned char HackadayCommunicatorTapMap[_TCA8418_NUM_KEYS][2] = {{}, {Key::FUNCTION_F1}, {'+'}, {'9'}, {'8'}, {'7'}, {Key::FUNCTION_F2}, {Key::FUNCTION_F3}, {Key::FUNCTION_F4}, {Key::FUNCTION_F5}, {Key::ESC}, {'q', 'Q'}, {'w', 'W'}, {'e', 'E'}, {'r', 'R'}, {'t', 'T'}, {'y', 'Y'}, {'u', 'U'}, {'i', 'I'}, {'o', 'O'}, {Key::TAB}, {'a', 'A'}, {'s', 'S'}, {'d', 'D'}, {'f', 'F'}, {'g', 'G'}, {'h', 'H'}, {'j', 'J'}, {'k', 'K'}, {'l', 'L'}, {}, {'z', 'Z'}, {'x', 'X'}, {'c', 'C'}, {'v', 'V'}, {'b', 'B'}, {'n', 'N'}, {'m', 'M'}, {',', '<'}, {'.', '>'}, {}, {}, {}, {'\\'}, {' '}, {}, {Key::RIGHT}, {Key::DOWN}, {Key::LEFT}, {}, {}, {}, {'-'}, {'6', '^'}, {'5', '%'}, {'4', '$'}, {'[', '{'}, {']', '}'}, {'p', 'P'}, {}, {}, {}, {'*'}, {'3', '#'}, {'2', '@'}, {'1', '!'}, {Key::SELECT}, {'\'', '"'}, {';', ':'}, {}, {}, {}, {'/', '?'}, {'='}, {'.', '>'}, {'0', ')'}, {}, {Key::UP}, {Key::BSP}, {}}; HackadayCommunicatorKeyboard::HackadayCommunicatorKeyboard() : TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), modifierFlag(0), last_modifier_time(0), last_key(UINT8_MAX), next_key(UINT8_MAX), last_tap(0L), char_idx(0), tap_interval(0) { reset(); } void HackadayCommunicatorKeyboard::reset(void) { TCA8418KeyboardBase::reset(); enableInterrupts(); } // handle multi-key presses (shift and alt) void HackadayCommunicatorKeyboard::trigger() { uint8_t count = keyCount(); if (count == 0) return; for (uint8_t i = 0; i < count; ++i) { uint8_t k = readRegister(TCA8418_REG_KEY_EVENT_A + i); uint8_t key = k & 0x7F; if (k & 0x80) { pressed(key); } else { released(); state = Idle; } } } void HackadayCommunicatorKeyboard::pressed(uint8_t key) { if (state == Init || state == Busy) { return; } LOG_DEBUG("Key pressed: %u", key); if (modifierFlag && (millis() - last_modifier_time > _TCA8418_MULTI_TAP_THRESHOLD)) { modifierFlag = 0; } int row = (key - 1) / 10; int col = (key - 1) % 10; if (row >= _TCA8418_ROWS || col >= _TCA8418_COLS) { return; // Invalid key } next_key = row * _TCA8418_COLS + col; state = Held; uint32_t now = millis(); tap_interval = now - last_tap; updateModifierFlag(next_key); if (isModifierKey(next_key)) { last_modifier_time = now; } if (tap_interval < 0) { last_tap = 0; state = Busy; return; } if (next_key != last_key || tap_interval > _TCA8418_MULTI_TAP_THRESHOLD) { char_idx = 0; } else { char_idx += 1; } last_key = next_key; last_tap = now; } void HackadayCommunicatorKeyboard::released() { if (state != Held) { return; } if (last_key >= _TCA8418_NUM_KEYS) { last_key = UINT8_MAX; state = Idle; return; } uint32_t now = millis(); last_tap = now; if (HackadayCommunicatorTapMod[last_key]) queueEvent(HackadayCommunicatorTapMap[last_key][modifierFlag % HackadayCommunicatorTapMod[last_key]]); if (isModifierKey(last_key) == false) modifierFlag = 0; } void HackadayCommunicatorKeyboard::updateModifierFlag(uint8_t key) { if (key == modifierRightShiftKey) { modifierFlag ^= modifierRightShift; } else if (key == modifierLeftShiftKey) { modifierFlag ^= modifierLeftShift; } } bool HackadayCommunicatorKeyboard::isModifierKey(uint8_t key) { return (key == modifierRightShiftKey || key == modifierLeftShiftKey); } #endif ================================================ FILE: src/input/HackadayCommunicatorKeyboard.h ================================================ #include "TCA8418KeyboardBase.h" class HackadayCommunicatorKeyboard : public TCA8418KeyboardBase { public: HackadayCommunicatorKeyboard(); void reset(void); void trigger(void) override; virtual ~HackadayCommunicatorKeyboard() {} protected: void pressed(uint8_t key) override; void released(void) override; void updateModifierFlag(uint8_t key); bool isModifierKey(uint8_t key); private: uint8_t modifierFlag; // Flag to indicate if a modifier key is pressed uint32_t last_modifier_time; // Timestamp of the last modifier key press uint8_t last_key; uint8_t next_key; uint32_t last_tap; uint8_t char_idx; int32_t tap_interval; }; ================================================ FILE: src/input/InputBroker.cpp ================================================ #include "InputBroker.h" #include "PowerFSM.h" // needed for event trigger #include "configuration.h" #include "graphics/Screen.h" #include "modules/ExternalNotificationModule.h" #if ARCH_PORTDUINO #include "input/LinuxInputImpl.h" #include "input/SeesawRotary.h" #include "platform/portduino/PortduinoGlue.h" #endif #if !MESHTASTIC_EXCLUDE_INPUTBROKER #include "input/ExpressLRSFiveWay.h" #include "input/RotaryEncoderImpl.h" #include "input/RotaryEncoderInterruptImpl1.h" #include "input/SerialKeyboardImpl.h" #include "input/UpDownInterruptImpl1.h" #include "input/i2cButton.h" #if HAS_TRACKBALL #include "input/TrackballInterruptImpl1.h" #endif #include "modules/StatusLEDModule.h" #if !MESHTASTIC_EXCLUDE_I2C #include "input/cardKbI2cImpl.h" #endif #include "input/kbMatrixImpl.h" #endif #if HAS_BUTTON || defined(ARCH_PORTDUINO) #include "input/ButtonThread.h" #if defined(BUTTON_PIN_TOUCH) ButtonThread *TouchButtonThread = nullptr; #if defined(TTGO_T_ECHO_PLUS) && defined(PIN_EINK_EN) static bool touchBacklightWasOn = false; static bool touchBacklightActive = false; #endif #endif #if defined(BUTTON_PIN) || defined(ARCH_PORTDUINO) ButtonThread *UserButtonThread = nullptr; #endif #if defined(ALT_BUTTON_PIN) ButtonThread *BackButtonThread = nullptr; #endif #if defined(CANCEL_BUTTON_PIN) ButtonThread *CancelButtonThread = nullptr; #endif #endif InputBroker *inputBroker = nullptr; InputBroker::InputBroker() { #if defined(HAS_FREE_RTOS) && !defined(ARCH_RP2040) inputEventQueue = xQueueCreate(5, sizeof(InputEvent)); pollSoonQueue = xQueueCreate(5, sizeof(InputPollable *)); xTaskCreate(pollSoonWorker, "input-pollSoon", 2 * 1024, this, 10, &pollSoonTask); #endif } void InputBroker::registerSource(Observable *source) { this->inputEventObserver.observe(source); } #if defined(HAS_FREE_RTOS) && !defined(ARCH_RP2040) void InputBroker::requestPollSoon(InputPollable *pollable) { if (xPortInIsrContext() == pdTRUE) { xQueueSendFromISR(pollSoonQueue, &pollable, NULL); } else { xQueueSend(pollSoonQueue, &pollable, 0); } } void InputBroker::queueInputEvent(const InputEvent *event) { if (xPortInIsrContext() == pdTRUE) { xQueueSendFromISR(inputEventQueue, event, NULL); } else { xQueueSend(inputEventQueue, event, portMAX_DELAY); } } void InputBroker::processInputEventQueue() { InputEvent event; while (xQueueReceive(inputEventQueue, &event, 0)) { handleInputEvent(&event); } } #endif int InputBroker::handleInputEvent(const InputEvent *event) { #if HAS_SCREEN bool screenWasOff = false; if (screen) { screenWasOff = !screen->isScreenOn(); } #endif powerFSM.trigger(EVENT_INPUT); if (event && event->inputEvent != INPUT_BROKER_NONE && externalNotificationModule && moduleConfig.external_notification.enabled && externalNotificationModule->nagging()) { externalNotificationModule->stopNow(); // If this turns off a notification, don't further process the event return 0; } #if HAS_SCREEN if (screen && screenWasOff) { // If the screen was off, it is in the process of turning on, and we just drop the event return 0; } #endif this->notifyObservers(event); return 0; } #if defined(HAS_FREE_RTOS) && !defined(ARCH_RP2040) void InputBroker::pollSoonWorker(void *p) { InputBroker *instance = (InputBroker *)p; while (true) { InputPollable *pollable = NULL; xQueueReceive(instance->pollSoonQueue, &pollable, portMAX_DELAY); if (pollable) { pollable->pollOnce(); } } vTaskDelete(NULL); } #endif void InputBroker::Init() { #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 #ifdef BUTTON_NEED_PULLUP2 gpio_pullup_en((gpio_num_t)BUTTON_NEED_PULLUP2); delay(10); #endif #endif #endif #endif // buttons are now inputBroker, so have to come after setupModules #if HAS_BUTTON int pullup_sense = 0; #ifdef INPUT_PULLUP_SENSE // Some platforms (nrf52) have a SENSE variant which allows wake from sleep - override what OneButton did #ifdef BUTTON_SENSE_TYPE pullup_sense = BUTTON_SENSE_TYPE; #else pullup_sense = INPUT_PULLUP_SENSE; #endif #endif #if defined(ARCH_PORTDUINO) if (portduino_config.userButtonPin.enabled) { LOG_DEBUG("Use GPIO%02d for button", portduino_config.userButtonPin.pin); UserButtonThread = new ButtonThread("UserButton"); if (screen) { ButtonConfig config; config.pinNumber = (uint8_t)portduino_config.userButtonPin.pin; config.activeLow = true; config.activePullup = true; config.pullupSense = INPUT_PULLUP; config.intRoutine = []() { UserButtonThread->userButton.tick(); UserButtonThread->setIntervalFromNow(0); runASAP = true; BaseType_t higherWake = 0; concurrency::mainDelay.interruptFromISR(&higherWake); }; config.singlePress = INPUT_BROKER_USER_PRESS; config.longPress = INPUT_BROKER_SELECT; UserButtonThread->initButton(config); } } #endif #ifdef BUTTON_PIN_TOUCH TouchButtonThread = new ButtonThread("BackButton"); ButtonConfig touchConfig; touchConfig.pinNumber = BUTTON_PIN_TOUCH; touchConfig.activeLow = true; touchConfig.activePullup = true; touchConfig.pullupSense = pullup_sense; touchConfig.intRoutine = []() { TouchButtonThread->userButton.tick(); TouchButtonThread->setIntervalFromNow(0); runASAP = true; BaseType_t higherWake = 0; concurrency::mainDelay.interruptFromISR(&higherWake); }; touchConfig.singlePress = INPUT_BROKER_NONE; touchConfig.longPress = INPUT_BROKER_BACK; #if defined(TTGO_T_ECHO_PLUS) && defined(PIN_EINK_EN) // On T-Echo Plus the touch pad should only drive the backlight, not UI navigation/sounds touchConfig.longPress = INPUT_BROKER_NONE; touchConfig.suppressLeadUpSound = true; touchConfig.onPress = []() { touchBacklightWasOn = uiconfig.screen_brightness == 1; if (!touchBacklightWasOn) { digitalWrite(PIN_EINK_EN, HIGH); } touchBacklightActive = true; }; touchConfig.onRelease = []() { if (touchBacklightActive && !touchBacklightWasOn) { digitalWrite(PIN_EINK_EN, LOW); } touchBacklightActive = false; }; #endif TouchButtonThread->initButton(touchConfig); #endif #if defined(CANCEL_BUTTON_PIN) // Buttons. Moved here cause we need NodeDB to be initialized CancelButtonThread = new ButtonThread("CancelButton"); ButtonConfig cancelConfig; cancelConfig.pinNumber = CANCEL_BUTTON_PIN; cancelConfig.activeLow = CANCEL_BUTTON_ACTIVE_LOW; cancelConfig.activePullup = CANCEL_BUTTON_ACTIVE_PULLUP; cancelConfig.pullupSense = pullup_sense; cancelConfig.intRoutine = []() { CancelButtonThread->userButton.tick(); CancelButtonThread->setIntervalFromNow(0); runASAP = true; BaseType_t higherWake = 0; concurrency::mainDelay.interruptFromISR(&higherWake); }; cancelConfig.singlePress = INPUT_BROKER_CANCEL; cancelConfig.longPress = INPUT_BROKER_SHUTDOWN; cancelConfig.longPressTime = 4000; CancelButtonThread->initButton(cancelConfig); #endif #if defined(ALT_BUTTON_PIN) // Buttons. Moved here cause we need NodeDB to be initialized BackButtonThread = new ButtonThread("BackButton"); ButtonConfig backConfig; backConfig.pinNumber = ALT_BUTTON_PIN; backConfig.activeLow = ALT_BUTTON_ACTIVE_LOW; backConfig.activePullup = ALT_BUTTON_ACTIVE_PULLUP; backConfig.pullupSense = pullup_sense; backConfig.intRoutine = []() { BackButtonThread->userButton.tick(); BackButtonThread->setIntervalFromNow(0); runASAP = true; BaseType_t higherWake = 0; concurrency::mainDelay.interruptFromISR(&higherWake); }; backConfig.singlePress = INPUT_BROKER_ALT_PRESS; backConfig.longPress = INPUT_BROKER_ALT_LONG; backConfig.longPressTime = 500; BackButtonThread->initButton(backConfig); #endif #if defined(BUTTON_PIN) #if defined(USERPREFS_BUTTON_PIN) int _pinNum = config.device.button_gpio ? config.device.button_gpio : USERPREFS_BUTTON_PIN; #else int _pinNum = config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN; #endif #ifndef BUTTON_ACTIVE_LOW #define BUTTON_ACTIVE_LOW true #endif #ifndef BUTTON_ACTIVE_PULLUP #define BUTTON_ACTIVE_PULLUP true #endif // Buttons. Moved here cause we need NodeDB to be initialized // If your variant.h has a BUTTON_PIN defined, go ahead and define BUTTON_ACTIVE_LOW and BUTTON_ACTIVE_PULLUP UserButtonThread = new ButtonThread("UserButton"); if (screen) { ButtonConfig userConfig; userConfig.pinNumber = (uint8_t)_pinNum; userConfig.activeLow = BUTTON_ACTIVE_LOW; userConfig.activePullup = BUTTON_ACTIVE_PULLUP; userConfig.pullupSense = pullup_sense; userConfig.intRoutine = []() { UserButtonThread->userButton.tick(); UserButtonThread->setIntervalFromNow(0); runASAP = true; BaseType_t higherWake = 0; concurrency::mainDelay.interruptFromISR(&higherWake); }; userConfig.singlePress = INPUT_BROKER_USER_PRESS; userConfig.longPress = INPUT_BROKER_SELECT; userConfig.longPressTime = 500; userConfig.longLongPress = INPUT_BROKER_SHUTDOWN; UserButtonThread->initButton(userConfig); } else { ButtonConfig userConfigNoScreen; userConfigNoScreen.pinNumber = (uint8_t)_pinNum; userConfigNoScreen.activeLow = BUTTON_ACTIVE_LOW; userConfigNoScreen.activePullup = BUTTON_ACTIVE_PULLUP; userConfigNoScreen.pullupSense = pullup_sense; userConfigNoScreen.intRoutine = []() { UserButtonThread->userButton.tick(); UserButtonThread->setIntervalFromNow(0); runASAP = true; BaseType_t higherWake = 0; concurrency::mainDelay.interruptFromISR(&higherWake); }; userConfigNoScreen.singlePress = INPUT_BROKER_USER_PRESS; userConfigNoScreen.longPress = INPUT_BROKER_NONE; userConfigNoScreen.longPressTime = 500; userConfigNoScreen.longLongPress = INPUT_BROKER_SHUTDOWN; userConfigNoScreen.doublePress = INPUT_BROKER_SEND_PING; userConfigNoScreen.triplePress = INPUT_BROKER_GPS_TOGGLE; UserButtonThread->initButton(userConfigNoScreen); } #endif #endif #if (HAS_BUTTON || ARCH_PORTDUINO) && !MESHTASTIC_EXCLUDE_INPUTBROKER if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { #if defined(T_LORA_PAGER) // use a special FSM based rotary encoder version for T-LoRa Pager rotaryEncoderImpl = new RotaryEncoderImpl(); if (!rotaryEncoderImpl->init()) { delete rotaryEncoderImpl; rotaryEncoderImpl = nullptr; } #elif defined(INPUTDRIVER_ENCODER_TYPE) && (INPUTDRIVER_ENCODER_TYPE == 2) upDownInterruptImpl1 = new UpDownInterruptImpl1(); if (!upDownInterruptImpl1->init()) { delete upDownInterruptImpl1; upDownInterruptImpl1 = nullptr; } #else rotaryEncoderInterruptImpl1 = new RotaryEncoderInterruptImpl1(); if (!rotaryEncoderInterruptImpl1->init()) { delete rotaryEncoderInterruptImpl1; rotaryEncoderInterruptImpl1 = nullptr; } #endif cardKbI2cImpl = new CardKbI2cImpl(); cardKbI2cImpl->init(); #if defined(M5STACK_UNITC6L) i2cButton = new i2cButtonThread("i2cButtonThread"); #endif #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 if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR && portduino_config.i2cdev != "") { seesawRotary = new SeesawRotary("SeesawRotary"); if (!seesawRotary->init()) { delete seesawRotary; seesawRotary = nullptr; } aLinuxInputImpl = new LinuxInputImpl(); aLinuxInputImpl->init(); } #endif #if !MESHTASTIC_EXCLUDE_INPUTBROKER && HAS_TRACKBALL if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { trackballInterruptImpl1 = new TrackballInterruptImpl1(); trackballInterruptImpl1->init(TB_DOWN, TB_UP, TB_LEFT, TB_RIGHT, TB_PRESS); } #endif #ifdef INPUTBROKER_EXPRESSLRSFIVEWAY_TYPE expressLRSFiveWayInput = new ExpressLRSFiveWay(); #endif } ================================================ FILE: src/input/InputBroker.h ================================================ #pragma once #include "Observer.h" #include "concurrency/OSThread.h" #include "freertosinc.h" #ifdef InputBrokerDebug #define LOG_INPUT(...) LOG_DEBUG(__VA_ARGS__) #else #define LOG_INPUT(...) #endif enum input_broker_event { INPUT_BROKER_NONE = 0, INPUT_BROKER_SELECT = 10, INPUT_BROKER_SELECT_LONG = 11, INPUT_BROKER_UP_LONG = 12, INPUT_BROKER_DOWN_LONG = 13, INPUT_BROKER_UP = 17, INPUT_BROKER_DOWN = 18, INPUT_BROKER_LEFT = 19, INPUT_BROKER_RIGHT = 20, INPUT_BROKER_CANCEL = 24, INPUT_BROKER_BACK = 27, INPUT_BROKER_USER_PRESS, INPUT_BROKER_ALT_PRESS, INPUT_BROKER_ALT_LONG, INPUT_BROKER_SHUTDOWN = 0x9b, INPUT_BROKER_GPS_TOGGLE = 0x9e, INPUT_BROKER_SEND_PING = 0xaf, INPUT_BROKER_FN_F1 = 0xf1, INPUT_BROKER_FN_F2 = 0xf2, INPUT_BROKER_FN_F3 = 0xf3, INPUT_BROKER_FN_F4 = 0xf4, INPUT_BROKER_FN_F5 = 0xf5, INPUT_BROKER_MATRIXKEY = 0xFE, INPUT_BROKER_ANYKEY = 0xff }; #define INPUT_BROKER_MSG_BRIGHTNESS_UP 0x11 #define INPUT_BROKER_MSG_BRIGHTNESS_DOWN 0x12 #define INPUT_BROKER_MSG_REBOOT 0x90 #define INPUT_BROKER_MSG_MUTE_TOGGLE 0xac #define INPUT_BROKER_MSG_FN_SYMBOL_ON 0xf1 #define INPUT_BROKER_MSG_FN_SYMBOL_OFF 0xf2 #define INPUT_BROKER_MSG_BLUETOOTH_TOGGLE 0xAA #define INPUT_BROKER_MSG_TAB 0x09 #define INPUT_BROKER_MSG_EMOTE_LIST 0x8F typedef struct _InputEvent { const char *source; input_broker_event inputEvent; unsigned char kbchar; uint16_t touchX; uint16_t touchY; } InputEvent; class InputPollable { public: virtual ~InputPollable() = default; virtual void pollOnce() = 0; }; class InputBroker : public Observable { CallbackObserver inputEventObserver = CallbackObserver(this, &InputBroker::handleInputEvent); public: InputBroker(); bool menuMode = true; void registerSource(Observable *source); void injectInputEvent(const InputEvent *event) { handleInputEvent(event); } #if defined(HAS_FREE_RTOS) && !defined(ARCH_RP2040) void requestPollSoon(InputPollable *pollable); void queueInputEvent(const InputEvent *event); void processInputEventQueue(); #endif void Init(); protected: int handleInputEvent(const InputEvent *event); private: #if defined(HAS_FREE_RTOS) && !defined(ARCH_RP2040) QueueHandle_t inputEventQueue; QueueHandle_t pollSoonQueue; TaskHandle_t pollSoonTask; static void pollSoonWorker(void *p); #endif }; extern InputBroker *inputBroker; extern bool runASAP; ================================================ FILE: src/input/LinuxInput.cpp ================================================ #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 #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 (portduino_config.keyboardDevice == "") return disable(); fd = open(portduino_config.keyboardDevice.c_str(), O_RDWR); if (fd < 0) return disable(); ret = ioctl(fd, EVIOCGRAB, (void *)1); if (ret != 0) return disable(); epollfd = epoll_create1(0); assert(epollfd >= 0); ev.events = EPOLLIN; ev.data.fd = fd; if (epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &ev)) { perror("unable to epoll add"); return disable(); } kb_found = true; // This is the first time the OSThread library has called this function, so do port setup firstTime = 0; } int nfds = epoll_wait(epollfd, events, MAX_EVENTS, 1); if (nfds < 0) { printf("%d ", nfds); perror("epoll_wait failed"); return disable(); } else if (nfds == 0) { return 50; } int keys = 0; memset(report, 0, 8); for (int i = 0; i < nfds; i++) { struct input_event ev[64]; int rd = read(events[i].data.fd, ev, sizeof(ev)); assert(rd > ((signed int)sizeof(struct input_event))); for (int j = 0; j < rd / ((signed int)sizeof(struct input_event)); j++) { InputEvent e = {}; e.inputEvent = INPUT_BROKER_NONE; e.source = this->_originName; e.kbchar = 0; unsigned int type, code; type = ev[j].type; code = ev[j].code; int value = ev[j].value; // printf("Event: time %ld.%06ld, ", ev[j].time.tv_sec, ev[j].time.tv_usec); if (type == EV_KEY) { uint8_t mod = 0; switch (code) { case KEY_LEFTCTRL: mod = 0x01; break; case KEY_RIGHTCTRL: mod = 0x10; break; case KEY_LEFTSHIFT: mod = 0x02; break; case KEY_RIGHTSHIFT: mod = 0x20; break; case KEY_LEFTALT: mod = 0x04; break; case KEY_RIGHTALT: mod = 0x40; break; case KEY_LEFTMETA: mod = 0x08; break; } if (value == 1) { switch (code) { case KEY_LEFTCTRL: mod = 0x01; break; case KEY_RIGHTCTRL: mod = 0x10; break; case KEY_LEFTSHIFT: mod = 0x02; break; case KEY_RIGHTSHIFT: mod = 0x20; break; case KEY_LEFTALT: mod = 0x04; break; case KEY_RIGHTALT: mod = 0x40; break; case KEY_LEFTMETA: mod = 0x08; break; case KEY_ESC: // ESC e.inputEvent = INPUT_BROKER_CANCEL; break; case KEY_BACK: // Back e.inputEvent = INPUT_BROKER_BACK; // e.kbchar = key; break; case KEY_UP: // Up e.inputEvent = INPUT_BROKER_UP; break; case KEY_DOWN: // Down e.inputEvent = INPUT_BROKER_DOWN; break; case KEY_LEFT: // Left e.inputEvent = INPUT_BROKER_LEFT; break; e.kbchar = INPUT_BROKER_LEFT; case KEY_RIGHT: // Right e.inputEvent = INPUT_BROKER_RIGHT; break; e.kbchar = 0; case KEY_ENTER: // Enter e.inputEvent = INPUT_BROKER_SELECT; break; case KEY_POWER: system("poweroff"); break; default: // all other keys if (keymap[code]) { e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = keymap[code]; } break; } } if (ev[j].value) { modifiers |= mod; } else { modifiers &= ~mod; } report[0] = modifiers; } if (e.inputEvent != INPUT_BROKER_NONE) { if (e.inputEvent == INPUT_BROKER_ANYKEY && (modifiers && 0x22)) e.kbchar = uppers[e.kbchar]; // doesn't get punctuation. Meh. this->notifyObservers(&e); } } } return 50; // Keyscan every 50msec to avoid key bounce } #endif ================================================ FILE: src/input/LinuxInput.h ================================================ #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 ================================================ FILE: src/input/LinuxInputImpl.cpp ================================================ #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 ================================================ FILE: src/input/LinuxInputImpl.h ================================================ #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 ================================================ FILE: src/input/MPR121Keyboard.cpp ================================================ // 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_AUTOCONF_CTRL0 0x7B #define _MPR121_REG_AUTOCONF_CTRL1 0x7C #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", m_addr); state = Init; last_key = UINT8_MAX; 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 Reset"); // Trigger a MPR121 Soft Reset if (m_wire) { m_wire->beginTransmission(m_addr); m_wire->write(_MPR121_REG_SOFT_RESET); m_wire->endTransmission(); } if (writeCallback) { uint8_t data = 0; writeCallback(m_addr, _MPR121_REG_SOFT_RESET, &data, 0); } delay(100); // Reset Electrode Configuration to 0x00, Stop Mode writeRegister(_MPR121_REG_ELECTRODE_CONFIG, 0x00); delay(100); LOG_DEBUG("MPR121 Configuring"); // Set touch release thresholds for (uint8_t i = 0; i < 12; i++) { // Set touch threshold writeRegister(_MPR121_REG_TOUCH_THRESHOLD + (i * 2), 10); delay(20); // Set release threshold writeRegister(_MPR121_REG_RELEASE_THRESHOLD + (i * 2), 5); delay(20); } // Configure filtering and baseline registers writeRegister(_MPR121_REG_MAX_HALF_DELTA_RISING, 0x05); delay(20); writeRegister(_MPR121_REG_MAX_HALF_DELTA_FALLING, 0x01); delay(20); writeRegister(_MPR121_REG_NOISE_HALF_DELTA_RISING, 0x01); delay(20); writeRegister(_MPR121_REG_NOISE_HALF_DELTA_FALLING, 0x05); delay(20); writeRegister(_MPR121_REG_NOISE_HALF_DELTA_TOUCHED, 0x00); delay(20); writeRegister(_MPR121_REG_NOISE_COUNT_LIMIT_RISING, 0x05); delay(20); writeRegister(_MPR121_REG_NOISE_COUNT_LIMIT_FALLING, 0x01); delay(20); writeRegister(_MPR121_REG_NOISE_COUNT_LIMIT_TOUCHED, 0x00); delay(20); writeRegister(_MPR121_REG_FILTER_DELAY_COUNT_RISING, 0x00); delay(20); writeRegister(_MPR121_REG_FILTER_DELAY_COUNT_FALLING, 0x00); delay(20); writeRegister(_MPR121_REG_FILTER_DELAY_COUNT_TOUCHED, 0x00); delay(20); writeRegister(_MPR121_REG_AUTOCONF_CTRL0, 0x04); // Auto-config enable delay(20); writeRegister(_MPR121_REG_AUTOCONF_CTRL1, 0x00); // Ensure no auto-config interrupt delay(20); writeRegister(_MPR121_REG_DEBOUNCE, 0x02); delay(20); writeRegister(_MPR121_REG_CONFIG1, 0x20); delay(20); writeRegister(_MPR121_REG_CONFIG2, 0x21); delay(20); // Enter run mode by setting partial filter calibration tracking, disable proximity detection, enable 12 channels writeRegister(_MPR121_REG_ELECTRODE_CONFIG, ECR_CALIBRATION_TRACK_FROM_FULL_FILTER | ECR_PROXIMITY_DETECTION_OFF | ECR_TOUCH_DETECTION_12CH); delay(100); LOG_DEBUG("MPR121 Run"); state = Idle; } void MPR121Keyboard::attachInterrupt(uint8_t pin, void (*func)(void)) const { pinMode(pin, INPUT_PULLUP); ::attachInterrupt(digitalPinToInterrupt(pin), func, RISING); } void MPR121Keyboard::detachInterrupt(uint8_t pin) const { ::detachInterrupt(pin); } uint8_t MPR121Keyboard::status() const { return readRegister16(_MPR121_REG_KEY); } uint8_t MPR121Keyboard::keyCount() const { // Read the key register uint16_t keyRegister = readRegister16(_MPR121_REG_KEY); return keyCount(keyRegister); } uint8_t MPR121Keyboard::keyCount(uint16_t value) const { // Mask the first 12 bits uint16_t buttonState = value & _KEY_MASK; // Count how many bits are set to 1 (i.e., how many buttons are pressed) uint8_t numButtonsPressed = 0; for (uint8_t i = 0; i < 12; ++i) { if (buttonState & (1 << i)) { numButtonsPressed++; } } 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 >= _NUM_KEYS) { // reset to idle if last_key out of bounds last_key = UINT8_MAX; 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); } } ================================================ FILE: src/input/MPR121Keyboard.h ================================================ // 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; uint8_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; }; ================================================ FILE: src/input/RotaryEncoderImpl.cpp ================================================ #ifdef T_LORA_PAGER #include "RotaryEncoderImpl.h" #include "InputBroker.h" #include "RotaryEncoder.h" #ifdef ARCH_ESP32 #include "sleep.h" #endif #define ORIGIN_NAME "RotaryEncoder" RotaryEncoderImpl *rotaryEncoderImpl; RotaryEncoderImpl::RotaryEncoderImpl() { rotary = nullptr; #ifdef ARCH_ESP32 isFirstInit = true; #endif } RotaryEncoderImpl::~RotaryEncoderImpl() { LOG_DEBUG("RotaryEncoderImpl destructor"); detachRotaryEncoderInterrupts(); if (rotary != nullptr) { delete rotary; rotary = nullptr; } } bool RotaryEncoderImpl::init() { if (!moduleConfig.canned_message.updown1_enabled || moduleConfig.canned_message.inputbroker_pin_a == 0 || moduleConfig.canned_message.inputbroker_pin_b == 0) { // Input device is disabled. return false; } eventCw = static_cast(moduleConfig.canned_message.inputbroker_event_cw); eventCcw = static_cast(moduleConfig.canned_message.inputbroker_event_ccw); eventPressed = static_cast(moduleConfig.canned_message.inputbroker_event_press); if (rotary == nullptr) { rotary = new RotaryEncoder(moduleConfig.canned_message.inputbroker_pin_a, moduleConfig.canned_message.inputbroker_pin_b, moduleConfig.canned_message.inputbroker_pin_press); } attachRotaryEncoderInterrupts(); #ifdef ARCH_ESP32 // Register callbacks for before and after lightsleep // Used to detach and reattach interrupts if (isFirstInit) { lsObserver.observe(¬ifyLightSleep); lsEndObserver.observe(¬ifyLightSleepEnd); isFirstInit = false; } #endif LOG_INFO("RotaryEncoder initialized pins(%d, %d, %d), events(%d, %d, %d)", moduleConfig.canned_message.inputbroker_pin_a, moduleConfig.canned_message.inputbroker_pin_b, moduleConfig.canned_message.inputbroker_pin_press, eventCw, eventCcw, eventPressed); return true; } void RotaryEncoderImpl::pollOnce() { InputEvent e{ORIGIN_NAME, INPUT_BROKER_NONE, 0, 0, 0}; static uint32_t lastPressed = millis(); if (rotary->readButton() == RotaryEncoder::ButtonState::BUTTON_PRESSED) { if (lastPressed + 200 < millis()) { LOG_DEBUG("Rotary event Press"); lastPressed = millis(); e.inputEvent = this->eventPressed; inputBroker->queueInputEvent(&e); } } switch (rotary->process()) { case RotaryEncoder::DIRECTION_CW: LOG_DEBUG("Rotary event CW"); e.inputEvent = this->eventCw; inputBroker->queueInputEvent(&e); break; case RotaryEncoder::DIRECTION_CCW: LOG_DEBUG("Rotary event CCW"); e.inputEvent = this->eventCcw; inputBroker->queueInputEvent(&e); break; default: break; } } void RotaryEncoderImpl::detachRotaryEncoderInterrupts() { LOG_DEBUG("RotaryEncoderImpl detach button interrupts"); if (interruptInstance == this) { detachInterrupt(moduleConfig.canned_message.inputbroker_pin_a); detachInterrupt(moduleConfig.canned_message.inputbroker_pin_b); detachInterrupt(moduleConfig.canned_message.inputbroker_pin_press); interruptInstance = nullptr; } else { LOG_WARN("RotaryEncoderImpl: interrupts already detached"); } } void RotaryEncoderImpl::attachRotaryEncoderInterrupts() { LOG_DEBUG("RotaryEncoderImpl attach button interrupts"); if (rotary != nullptr && interruptInstance == nullptr) { rotary->resetButton(); interruptInstance = this; auto interruptHandler = []() { inputBroker->requestPollSoon(interruptInstance); }; attachInterrupt(moduleConfig.canned_message.inputbroker_pin_a, interruptHandler, CHANGE); attachInterrupt(moduleConfig.canned_message.inputbroker_pin_b, interruptHandler, CHANGE); attachInterrupt(moduleConfig.canned_message.inputbroker_pin_press, interruptHandler, CHANGE); } else { LOG_WARN("RotaryEncoderImpl: interrupts already attached"); } } #ifdef ARCH_ESP32 int RotaryEncoderImpl::beforeLightSleep(void *unused) { detachRotaryEncoderInterrupts(); return 0; // Indicates success; } int RotaryEncoderImpl::afterLightSleep(esp_sleep_wakeup_cause_t cause) { attachRotaryEncoderInterrupts(); return 0; // Indicates success; } #endif RotaryEncoderImpl *RotaryEncoderImpl::interruptInstance; #endif ================================================ FILE: src/input/RotaryEncoderImpl.h ================================================ #pragma once // This is a version of RotaryEncoder which is based on a debounce inherent FSM table (see RotaryEncoder library) #include "InputBroker.h" #include "concurrency/OSThread.h" #include "mesh/NodeDB.h" class RotaryEncoder; class RotaryEncoderImpl final : public InputPollable { public: RotaryEncoderImpl(); ~RotaryEncoderImpl() override; bool init(); virtual void pollOnce() override; // Disconnect and reconnect interrupts for light sleep #ifdef ARCH_ESP32 int beforeLightSleep(void *unused); int afterLightSleep(esp_sleep_wakeup_cause_t cause); #endif protected: static RotaryEncoderImpl *interruptInstance; input_broker_event eventCw = INPUT_BROKER_NONE; input_broker_event eventCcw = INPUT_BROKER_NONE; input_broker_event eventPressed = INPUT_BROKER_NONE; RotaryEncoder *rotary; private: #ifdef ARCH_ESP32 bool isFirstInit; #endif void detachRotaryEncoderInterrupts(); void attachRotaryEncoderInterrupts(); #ifdef ARCH_ESP32 // Get notified when lightsleep begins and ends CallbackObserver lsObserver = CallbackObserver(this, &RotaryEncoderImpl::beforeLightSleep); CallbackObserver lsEndObserver = CallbackObserver(this, &RotaryEncoderImpl::afterLightSleep); #endif }; extern RotaryEncoderImpl *rotaryEncoderImpl; ================================================ FILE: src/input/RotaryEncoderInterruptBase.cpp ================================================ #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, input_broker_event eventCw, input_broker_event eventCcw, input_broker_event eventPressed, input_broker_event eventPressedLong, // std::function onIntA, std::function onIntB, std::function onIntPress) : void (*onIntA)(), void (*onIntB)(), void (*onIntPress)()) { this->_pinA = pinA; this->_pinB = pinB; this->_pinPress = pinPress; this->_eventCw = eventCw; this->_eventCcw = eventCcw; this->_eventPressed = eventPressed; this->_eventPressedLong = eventPressedLong; bool isRAK = false; #ifdef RAK_4631 isRAK = true; #endif if (!isRAK || pinPress != 0) { pinMode(pinPress, INPUT_PULLUP); attachInterrupt(pinPress, onIntPress, CHANGE); } if (!isRAK || this->_pinA != 0) { pinMode(this->_pinA, INPUT_PULLUP); attachInterrupt(this->_pinA, onIntA, CHANGE); } if (!isRAK || this->_pinA != 0) { pinMode(this->_pinB, INPUT_PULLUP); attachInterrupt(this->_pinB, onIntB, CHANGE); } this->rotaryLevelA = digitalRead(this->_pinA); this->rotaryLevelB = digitalRead(this->_pinB); LOG_INFO("Rotary initialized (%d, %d, %d)", this->_pinA, this->_pinB, pinPress); } int32_t RotaryEncoderInterruptBase::runOnce() { InputEvent e = {}; e.inputEvent = INPUT_BROKER_NONE; e.source = this->_originName; unsigned long now = millis(); // Handle press long/short detection if (this->action == ROTARY_ACTION_PRESSED) { bool buttonPressed = !digitalRead(_pinPress); if (!pressDetected && buttonPressed) { pressDetected = true; pressStartTime = now; } if (pressDetected) { uint32_t duration = now - pressStartTime; if (!buttonPressed) { // released -> if short press, send short, else already sent long if (duration < LONG_PRESS_DURATION && now - lastPressKeyTime >= pressDebounceMs) { lastPressKeyTime = now; LOG_DEBUG("Rotary event Press short"); e.inputEvent = this->_eventPressed; } pressDetected = false; pressStartTime = 0; lastPressLongEventTime = 0; this->action = ROTARY_ACTION_NONE; } else if (duration >= LONG_PRESS_DURATION && this->_eventPressedLong != INPUT_BROKER_NONE && lastPressLongEventTime == 0) { // fire single-shot long press lastPressLongEventTime = now; LOG_DEBUG("Rotary event Press long"); e.inputEvent = this->_eventPressedLong; } } } else if (this->action == ROTARY_ACTION_CW) { LOG_DEBUG("Rotary event CW"); e.inputEvent = this->_eventCw; } else if (this->action == ROTARY_ACTION_CCW) { LOG_DEBUG("Rotary event CCW"); e.inputEvent = this->_eventCcw; } if (e.inputEvent != INPUT_BROKER_NONE) { this->notifyObservers(&e); } if (!pressDetected) { this->action = ROTARY_ACTION_NONE; } else if (now - pressStartTime < LONG_PRESS_DURATION) { return (20); // keep checking for long/short until time expires } return INT32_MAX; } void RotaryEncoderInterruptBase::intPressHandler() { this->action = ROTARY_ACTION_PRESSED; setIntervalFromNow(20); // start checking for long/short } void RotaryEncoderInterruptBase::intAHandler() { // CW rotation (at least on most common rotary encoders) int currentLevelA = digitalRead(this->_pinA); if (this->rotaryLevelA == currentLevelA) { return; } this->rotaryLevelA = currentLevelA; this->rotaryStateCCW = intHandler(currentLevelA == HIGH, this->rotaryLevelB, ROTARY_ACTION_CCW, this->rotaryStateCCW); } void RotaryEncoderInterruptBase::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(ROTARY_DELAY); // TODO: this modifies a non-volatile variable! return newState; } ================================================ FILE: src/input/RotaryEncoderInterruptBase.h ================================================ #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, input_broker_event eventCw, input_broker_event eventCcw, input_broker_event eventPressed, input_broker_event eventPressedLong, // std::function onIntA, std::function onIntB, std::function onIntPress); void (*onIntA)(), void (*onIntB)(), void (*onIntPress)()); void intPressHandler(); void intAHandler(); void intBHandler(); protected: virtual int32_t runOnce() override; RotaryEncoderInterruptBaseStateType intHandler(bool actualPinRaising, int otherPinLevel, RotaryEncoderInterruptBaseActionType action, RotaryEncoderInterruptBaseStateType state); volatile RotaryEncoderInterruptBaseStateType rotaryStateCW = ROTARY_EVENT_CLEARED; volatile RotaryEncoderInterruptBaseStateType rotaryStateCCW = ROTARY_EVENT_CLEARED; volatile int rotaryLevelA = LOW; volatile int rotaryLevelB = LOW; volatile RotaryEncoderInterruptBaseActionType action = ROTARY_ACTION_NONE; private: // pins and events uint8_t _pinA = 0; uint8_t _pinB = 0; uint8_t _pinPress = 0; input_broker_event _eventCw = INPUT_BROKER_NONE; input_broker_event _eventCcw = INPUT_BROKER_NONE; input_broker_event _eventPressed = INPUT_BROKER_NONE; input_broker_event _eventPressedLong = INPUT_BROKER_NONE; const char *_originName; // Long press detection variables uint32_t pressStartTime = 0; bool pressDetected = false; uint32_t lastPressLongEventTime = 0; unsigned long lastPressKeyTime = 0; static const uint32_t LONG_PRESS_DURATION = 300; // ms static const uint32_t LONG_PRESS_REPEAT_INTERVAL = 0; // 0 = single-shot for rotary select const unsigned long pressDebounceMs = 200; // ms }; ================================================ FILE: src/input/RotaryEncoderInterruptImpl1.cpp ================================================ #include "RotaryEncoderInterruptImpl1.h" #include "InputBroker.h" extern bool osk_found; 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; input_broker_event eventCw = static_cast(moduleConfig.canned_message.inputbroker_event_cw); input_broker_event eventCcw = static_cast(moduleConfig.canned_message.inputbroker_event_ccw); input_broker_event eventPressed = static_cast(moduleConfig.canned_message.inputbroker_event_press); input_broker_event eventPressedLong = INPUT_BROKER_SELECT_LONG; // moduleConfig.canned_message.ext_notification_module_output RotaryEncoderInterruptBase::init(pinA, pinB, pinPress, eventCw, eventCcw, eventPressed, eventPressedLong, RotaryEncoderInterruptImpl1::handleIntA, RotaryEncoderInterruptImpl1::handleIntB, RotaryEncoderInterruptImpl1::handleIntPressed); inputBroker->registerSource(this); #ifndef HAS_PHYSICAL_KEYBOARD osk_found = true; #endif return true; } void RotaryEncoderInterruptImpl1::handleIntA() { rotaryEncoderInterruptImpl1->intAHandler(); } void RotaryEncoderInterruptImpl1::handleIntB() { rotaryEncoderInterruptImpl1->intBHandler(); } void RotaryEncoderInterruptImpl1::handleIntPressed() { rotaryEncoderInterruptImpl1->intPressHandler(); } ================================================ FILE: src/input/RotaryEncoderInterruptImpl1.h ================================================ #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; ================================================ FILE: src/input/SeesawRotary.cpp ================================================ #ifdef ARCH_PORTDUINO #include "SeesawRotary.h" #include "input/InputBroker.h" using namespace concurrency; SeesawRotary *seesawRotary; SeesawRotary::SeesawRotary(const char *name) : OSThread(name) { _originName = name; } bool SeesawRotary::init() { if (inputBroker) inputBroker->registerSource(this); if (!ss.begin(SEESAW_ADDR)) { return false; } // attachButtonInterrupts(); uint32_t version = ((ss.getVersion() >> 16) & 0xFFFF); if (version != 4991) { LOG_WARN("Wrong firmware loaded? %u", version); } else { LOG_INFO("Found Product 4991"); } /* #ifdef ARCH_ESP32 // Register callbacks for before and after lightsleep // Used to detach and reattach interrupts lsObserver.observe(¬ifyLightSleep); lsEndObserver.observe(¬ifyLightSleepEnd); #endif */ ss.pinMode(SS_SWITCH, INPUT_PULLUP); // get starting position encoder_position = ss.getEncoderPosition(); ss.setGPIOInterrupts((uint32_t)1 << SS_SWITCH, 1); ss.enableEncoderInterrupt(); canSleep = true; // Assume we should not keep the board awake return true; } int32_t SeesawRotary::runOnce() { InputEvent e = {}; e.inputEvent = INPUT_BROKER_NONE; bool currentlyPressed = !ss.digitalRead(SS_SWITCH); if (currentlyPressed && !wasPressed) { e.inputEvent = INPUT_BROKER_SELECT; } wasPressed = currentlyPressed; int32_t new_position = ss.getEncoderPosition(); // did we move around? if (encoder_position != new_position) { if (encoder_position == 0 && new_position != 1) { e.inputEvent = INPUT_BROKER_ALT_PRESS; } else if (new_position == 0 && encoder_position != 1) { e.inputEvent = INPUT_BROKER_USER_PRESS; } else if (new_position > encoder_position) { e.inputEvent = INPUT_BROKER_USER_PRESS; } else { e.inputEvent = INPUT_BROKER_ALT_PRESS; } encoder_position = new_position; } if (e.inputEvent != INPUT_BROKER_NONE) { e.source = this->_originName; e.kbchar = 0x00; this->notifyObservers(&e); } return 50; } #endif ================================================ FILE: src/input/SeesawRotary.h ================================================ #pragma once #ifdef ARCH_PORTDUINO #include "Adafruit_seesaw.h" #include "InputBroker.h" #include "concurrency/OSThread.h" #include "configuration.h" #define SS_SWITCH 24 #define SS_NEOPIX 6 #define SEESAW_ADDR 0x36 class SeesawRotary : public Observable, public concurrency::OSThread { public: const char *_originName; bool init(); SeesawRotary(const char *name); int32_t runOnce() override; private: Adafruit_seesaw ss; int32_t encoder_position; bool wasPressed = false; }; extern SeesawRotary *seesawRotary; #endif ================================================ FILE: src/input/SerialKeyboard.cpp ================================================ #include "SerialKeyboard.h" #include "configuration.h" #include SerialKeyboard *globalSerialKeyboard = nullptr; #ifdef INPUTBROKER_SERIAL_TYPE #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; globalSerialKeyboard = this; } void SerialKeyboard::erase() { InputEvent e = {}; e.inputEvent = INPUT_BROKER_BACK; e.kbchar = 0x08; e.source = this->_originName; this->notifyObservers(&e); } int32_t SerialKeyboard::runOnce() { if (!INPUTBROKER_SERIAL_TYPE) { // Input device is not requested. return disable(); } if (firstTime) { // This is the first time the OSThread library has called this function, so do port setup firstTime = 0; pinMode(KB_LOAD, OUTPUT); pinMode(KB_CLK, OUTPUT); pinMode(KB_DATA, INPUT); digitalWrite(KB_LOAD, HIGH); digitalWrite(KB_CLK, LOW); prevKeys = 0b1111111111111111; LOG_DEBUG("Serial Keyboard setup"); } if (INPUTBROKER_SERIAL_TYPE == 1) { // Chatter V1.0 & V2.0 keypads // scan for keypresses // Write pulse to load pin digitalWrite(KB_LOAD, LOW); delayMicroseconds(5); digitalWrite(KB_LOAD, HIGH); delayMicroseconds(5); // Get data from 74HC165 byte shiftRegister1 = shiftIn(KB_DATA, KB_CLK, LSBFIRST); byte shiftRegister2 = shiftIn(KB_DATA, KB_CLK, LSBFIRST); keys = (shiftRegister1 << 8) + shiftRegister2; // Print to serial monitor // Serial.print (shiftRegister1, BIN); // Serial.print ("X"); // Serial.println (shiftRegister2, BIN); if (!Throttle::isWithinTimespanMs(lastPressTime, 500)) { quickPress = 0; } if (keys < prevKeys) { // a new key has been pressed (and not released), doesn't works for multiple presses at once but // shouldn't be a limitation InputEvent e = {}; e.inputEvent = INPUT_BROKER_NONE; e.source = this->_originName; // SELECT OR SEND OR CANCEL EVENT if (!(shiftRegister2 & (1 << 3))) { if (shift > 0) { e.inputEvent = INPUT_BROKER_ANYKEY; // REQUIRED e.kbchar = 0x09; // TAB shift = 0; // reset shift after TAB } else { e.inputEvent = INPUT_BROKER_LEFT; } } else if (!(shiftRegister2 & (1 << 2))) { if (shift > 0) { e.inputEvent = INPUT_BROKER_ANYKEY; // REQUIRED e.kbchar = 0x09; // TAB shift = 0; // reset shift after TAB } else { e.inputEvent = INPUT_BROKER_RIGHT; } e.kbchar = 0; } else if (!(shiftRegister2 & (1 << 1))) { e.inputEvent = INPUT_BROKER_SELECT; } else if (!(shiftRegister2 & (1 << 0))) { e.inputEvent = INPUT_BROKER_CANCEL; } // TEXT INPUT EVENT else if (!(shiftRegister1 & (1 << 4))) { keyPressed = 0; } else if (!(shiftRegister1 & (1 << 3))) { keyPressed = 1; } else if (!(shiftRegister2 & (1 << 4))) { keyPressed = 2; } else if (!(shiftRegister1 & (1 << 5))) { keyPressed = 3; } else if (!(shiftRegister1 & (1 << 2))) { keyPressed = 4; } else if (!(shiftRegister2 & (1 << 5))) { keyPressed = 5; } else if (!(shiftRegister1 & (1 << 6))) { keyPressed = 6; } else if (!(shiftRegister1 & (1 << 1))) { keyPressed = 7; } else if (!(shiftRegister2 & (1 << 6))) { keyPressed = 8; } else if (!(shiftRegister1 & (1 << 0))) { keyPressed = 9; } // BACKSPACE or TAB else if (!(shiftRegister1 & (1 << 7))) { if (shift == 0 || shift == 2) { // BACKSPACE e.inputEvent = INPUT_BROKER_BACK; e.kbchar = 0x08; } else { // shift = 1 => TAB e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = 0x09; } } // SHIFT else if (!(shiftRegister2 & (1 << 7))) { keyPressed = 10; } if (keyPressed < 11) { if (keyPressed == lastKeyPressed && millis() - lastPressTime < 500) { quickPress += 1; if (quickPress > 3) { quickPress = 0; } } if (keyPressed != lastKeyPressed) { quickPress = 0; } if (keyPressed < 10) { // if it's a letter if (keyPressed == lastKeyPressed && millis() - lastPressTime < 500) { erase(); } e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = char(KeyMap[shift][quickPress][keyPressed]); } else { // then it's shift shift += 1; if (shift > 2) { shift = 0; } } lastPressTime = millis(); lastKeyPressed = keyPressed; keyPressed = 13; } if (e.inputEvent != INPUT_BROKER_NONE) { this->notifyObservers(&e); } } prevKeys = keys; } return 50; } #endif // INPUTBROKER_SERIAL_TYPE ================================================ FILE: src/input/SerialKeyboard.h ================================================ #pragma once #include "InputBroker.h" #include "concurrency/OSThread.h" class SerialKeyboard : public Observable, public concurrency::OSThread { public: explicit SerialKeyboard(const char *name); uint8_t getShift() const { return shift; } 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; }; extern SerialKeyboard *globalSerialKeyboard; ================================================ FILE: src/input/SerialKeyboardImpl.cpp ================================================ #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 ================================================ FILE: src/input/SerialKeyboardImpl.h ================================================ #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; ================================================ FILE: src/input/TCA8418Keyboard.cpp ================================================ #include "TCA8418Keyboard.h" #define _TCA8418_COLS 3 #define _TCA8418_ROWS 4 #define _TCA8418_NUM_KEYS 12 #define _TCA8418_LONG_PRESS_THRESHOLD 2000 #define _TCA8418_MULTI_TAP_THRESHOLD 750 using Key = TCA8418KeyboardBase::TCA8418Key; // Num chars per key, Modulus for rotating through characters static uint8_t TCA8418TapMod[_TCA8418_NUM_KEYS] = {13, 7, 7, 7, 7, 7, 9, 7, 9, 2, 2, 2}; static unsigned char TCA8418TapMap[_TCA8418_NUM_KEYS][13] = { {'1', '.', ',', '?', '!', ':', ';', '-', '_', '\\', '/', '(', ')'}, // 1 {'2', 'a', 'b', 'c', 'A', 'B', 'C'}, // 2 {'3', 'd', 'e', 'f', 'D', 'E', 'F'}, // 3 {'4', 'g', 'h', 'i', 'G', 'H', 'I'}, // 4 {'5', 'j', 'k', 'l', 'J', 'K', 'L'}, // 5 {'6', 'm', 'n', 'o', 'M', 'N', 'O'}, // 6 {'7', 'p', 'q', 'r', 's', 'P', 'Q', 'R', 'S'}, // 7 {'8', 't', 'u', 'v', 'T', 'U', 'V'}, // 8 {'9', 'w', 'x', 'y', 'z', 'W', 'X', 'Y', 'Z'}, // 9 {'*', '+'}, // * {'0', ' '}, // 0 {'#', '@'}, // # }; static unsigned char TCA8418LongPressMap[_TCA8418_NUM_KEYS] = { Key::ESC, // 1 Key::UP, // 2 Key::NONE, // 3 Key::LEFT, // 4 Key::NONE, // 5 Key::RIGHT, // 6 Key::NONE, // 7 Key::DOWN, // 8 Key::NONE, // 9 Key::BSP, // * Key::NONE, // 0 Key::NONE, // # }; TCA8418Keyboard::TCA8418Keyboard() : TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), last_key(UINT8_MAX), next_key(UINT8_MAX), last_tap(0L), char_idx(0), tap_interval(0), should_backspace(false) { } void TCA8418Keyboard::reset() { TCA8418KeyboardBase::reset(); // Set COL9 as GPIO output writeRegister(TCA8418_REG_GPIO_DIR_3, 0x02); // Switch off keyboard backlight (COL9 = LOW) writeRegister(TCA8418_REG_GPIO_DAT_OUT_3, 0x00); } void TCA8418Keyboard::pressed(uint8_t key) { if (state == Init || state == Busy) { return; } int row = (key - 1) / 10; int col = (key - 1) % 10; if (row >= _TCA8418_ROWS || col >= _TCA8418_COLS) { return; // Invalid key } // Compute key index based on dynamic row/column next_key = (uint8_t)(row * _TCA8418_COLS + col); // LOG_DEBUG("TCA8418: Key %u -> Next Key %u", key, next_key); state = Held; uint32_t now = millis(); tap_interval = now - last_tap; if (tap_interval < 0) { // Long running, millis has overflowed. last_tap = 0; state = Busy; return; } // Check if the key is the same as the last one or if the time interval has passed if (next_key != last_key || tap_interval > _TCA8418_MULTI_TAP_THRESHOLD) { char_idx = 0; // Reset char index if new key or long press should_backspace = false; // don't backspace on new key } else { char_idx += 1; // Cycle through characters if same key pressed should_backspace = true; // allow backspace on same key } // Store the current key as the last key last_key = next_key; last_tap = now; } void TCA8418Keyboard::released() { if (state != Held) { return; } if (last_key >= _TCA8418_NUM_KEYS) { // reset to idle if last_key out of bounds last_key = UINT8_MAX; state = Idle; return; } uint32_t now = millis(); int32_t held_interval = now - last_tap; last_tap = now; if (tap_interval < _TCA8418_MULTI_TAP_THRESHOLD && should_backspace) { queueEvent(BSP); } if (held_interval > _TCA8418_LONG_PRESS_THRESHOLD) { queueEvent(TCA8418LongPressMap[last_key]); // LOG_DEBUG("Long Press Key: %i Map: %i", last_key, TCA8418LongPressMap[last_key]); } else { queueEvent(TCA8418TapMap[last_key][(char_idx % TCA8418TapMod[last_key])]); // LOG_DEBUG("Key Press: %i Index:%i if %i Map: %c", last_key, char_idx, TCA8418TapMod[last_key], // TCA8418TapMap[last_key][(char_idx % TCA8418TapMod[last_key])]); } } void TCA8418Keyboard::setBacklight(bool on) { if (on) { digitalWrite(TCA8418_COL9, HIGH); } else { digitalWrite(TCA8418_COL9, LOW); } } ================================================ FILE: src/input/TCA8418Keyboard.h ================================================ #include "TCA8418KeyboardBase.h" /** * @brief 3x4 keypad with 3 columns and 4 rows */ class TCA8418Keyboard : public TCA8418KeyboardBase { public: TCA8418Keyboard(); void reset(void) override; void setBacklight(bool on) override; protected: void pressed(uint8_t key) override; void released(void) override; uint8_t last_key; uint8_t next_key; uint32_t last_tap; uint8_t char_idx; int32_t tap_interval; bool should_backspace; }; ================================================ FILE: src/input/TCA8418KeyboardBase.cpp ================================================ // Based on the MPR121 Keyboard and Adafruit TCA8418 library #include "TCA8418KeyboardBase.h" #include "configuration.h" #include // FIELDS CONFIG REGISTER 1 #define _TCA8418_REG_CFG_AI 0x80 // Auto-increment for read/write #define _TCA8418_REG_CFG_GPI_E_CGF 0x40 // Event mode config #define _TCA8418_REG_CFG_OVR_FLOW_M 0x20 // Overflow mode enable #define _TCA8418_REG_CFG_INT_CFG 0x10 // Interrupt config #define _TCA8418_REG_CFG_OVR_FLOW_IEN 0x08 // Overflow interrupt enable #define _TCA8418_REG_CFG_K_LCK_IEN 0x04 // Keypad lock interrupt enable #define _TCA8418_REG_CFG_GPI_IEN 0x02 // GPI interrupt enable #define _TCA8418_REG_CFG_KE_IEN 0x01 // Key events interrupt enable // FIELDS INT_STAT REGISTER 2 #define _TCA8418_REG_STAT_CAD_INT 0x10 // Ctrl-alt-del seq status #define _TCA8418_REG_STAT_OVR_FLOW_INT 0x08 // Overflow interrupt status #define _TCA8418_REG_STAT_K_LCK_INT 0x04 // Key lock interrupt status #define _TCA8418_REG_STAT_GPI_INT 0x02 // GPI interrupt status #define _TCA8418_REG_STAT_K_INT 0x01 // Key events interrupt status // FIELDS KEY_LCK_EC REGISTER 3 #define _TCA8418_REG_LCK_EC_K_LCK_EN 0x40 // Key lock enable #define _TCA8418_REG_LCK_EC_LCK_2 0x20 // Keypad lock status 2 #define _TCA8418_REG_LCK_EC_LCK_1 0x10 // Keypad lock status 1 #define _TCA8418_REG_LCK_EC_KLEC_3 0x08 // Key event count bit 3 #define _TCA8418_REG_LCK_EC_KLEC_2 0x04 // Key event count bit 2 #define _TCA8418_REG_LCK_EC_KLEC_1 0x02 // Key event count bit 1 #define _TCA8418_REG_LCK_EC_KLEC_0 0x01 // Key event count bit 0 TCA8418KeyboardBase::TCA8418KeyboardBase(uint8_t rows, uint8_t columns) : rows(rows), columns(columns), state(Init), queue(""), m_wire(nullptr), m_addr(0), readCallback(nullptr), writeCallback(nullptr) { } void TCA8418KeyboardBase::begin(uint8_t addr, TwoWire *wire) { m_addr = addr; m_wire = wire; m_wire->begin(); reset(); } void TCA8418KeyboardBase::begin(i2c_com_fptr_t r, i2c_com_fptr_t w, uint8_t addr) { m_addr = addr; m_wire = nullptr; writeCallback = w; readCallback = r; reset(); } void TCA8418KeyboardBase::reset() { LOG_DEBUG("TCA8418 Reset"); // GPIO // set default all GIO pins to INPUT writeRegister(TCA8418_REG_GPIO_DIR_1, 0x00); writeRegister(TCA8418_REG_GPIO_DIR_2, 0x00); writeRegister(TCA8418_REG_GPIO_DIR_3, 0x00); // add all pins to key events writeRegister(TCA8418_REG_GPI_EM_1, 0xFF); writeRegister(TCA8418_REG_GPI_EM_2, 0xFF); writeRegister(TCA8418_REG_GPI_EM_3, 0xFF); // set all pins to FALLING interrupts writeRegister(TCA8418_REG_GPIO_INT_LVL_1, 0x00); writeRegister(TCA8418_REG_GPIO_INT_LVL_2, 0x00); writeRegister(TCA8418_REG_GPIO_INT_LVL_3, 0x00); // add all pins to interrupts writeRegister(TCA8418_REG_GPIO_INT_EN_1, 0xFF); writeRegister(TCA8418_REG_GPIO_INT_EN_2, 0xFF); writeRegister(TCA8418_REG_GPIO_INT_EN_3, 0xFF); // Set keyboard matrix size matrix(rows, columns); enableDebounce(); flush(); state = Idle; } bool TCA8418KeyboardBase::matrix(uint8_t rows, uint8_t columns) { if (rows < 1 || rows > 8 || columns < 1 || columns > 10) return false; // Setup the keypad matrix. uint8_t mask = 0x00; for (int r = 0; r < rows; r++) { mask <<= 1; mask |= 1; } writeRegister(TCA8418_REG_KP_GPIO_1, mask); mask = 0x00; for (int c = 0; c < columns && c < 8; c++) { mask <<= 1; mask |= 1; } writeRegister(TCA8418_REG_KP_GPIO_2, mask); if (columns > 8) { if (columns == 9) mask = 0x01; else mask = 0x03; writeRegister(TCA8418_REG_KP_GPIO_3, mask); } return true; } uint8_t TCA8418KeyboardBase::keyCount() const { uint8_t eventCount = readRegister(TCA8418_REG_KEY_LCK_EC); eventCount &= 0x0F; // lower 4 bits only return eventCount; } bool TCA8418KeyboardBase::hasEvent() const { return queue.length() > 0; } void TCA8418KeyboardBase::queueEvent(char next) { if (next == NONE) { return; } queue.concat(next); } char TCA8418KeyboardBase::dequeueEvent() { if (queue.length() < 1) { return NONE; } char next = queue.charAt(0); queue.remove(0, 1); return next; } void TCA8418KeyboardBase::trigger() { if (keyCount() == 0) { return; } if (state != Init) { // Read the key register uint8_t k = readRegister(TCA8418_REG_KEY_EVENT_A); uint8_t key = k & 0x7F; if (k & 0x80) { if (state == Idle) pressed(key); return; } else { if (state == Held) { released(); } state = Idle; return; } } else { reset(); } } void TCA8418KeyboardBase::pressed(uint8_t key) { // must be defined in derived class LOG_ERROR("pressed() not implemented in derived class"); } void TCA8418KeyboardBase::released() { // must be defined in derived class LOG_ERROR("released() not implemented in derived class"); } uint8_t TCA8418KeyboardBase::flush() { // Flush key events uint8_t count = 0; while (readRegister(TCA8418_REG_KEY_EVENT_A) != 0) count++; // Flush gpio events readRegister(TCA8418_REG_GPIO_INT_STAT_1); readRegister(TCA8418_REG_GPIO_INT_STAT_2); readRegister(TCA8418_REG_GPIO_INT_STAT_3); // Clear INT_STAT register writeRegister(TCA8418_REG_INT_STAT, 3); return count; } void TCA8418KeyboardBase::clearInt() { writeRegister(TCA8418_REG_INT_STAT, 3); } uint8_t TCA8418KeyboardBase::digitalRead(uint8_t pinnum) const { if (pinnum > TCA8418_COL9) return 0xFF; uint8_t reg = TCA8418_REG_GPIO_DAT_STAT_1 + pinnum / 8; uint8_t mask = (1 << (pinnum % 8)); // Level 0 = low other = high uint8_t value = readRegister(reg); if (value & mask) return HIGH; return LOW; } bool TCA8418KeyboardBase::digitalWrite(uint8_t pinnum, uint8_t level) { if (pinnum > TCA8418_COL9) return false; uint8_t reg = TCA8418_REG_GPIO_DAT_OUT_1 + pinnum / 8; uint8_t mask = (1 << (pinnum % 8)); // Level 0 = low other = high uint8_t value = readRegister(reg); if (level == LOW) value &= ~mask; else value |= mask; writeRegister(reg, value); return true; } bool TCA8418KeyboardBase::pinMode(uint8_t pinnum, uint8_t mode) { if (pinnum > TCA8418_COL9) return false; uint8_t idx = pinnum / 8; uint8_t reg = TCA8418_REG_GPIO_DIR_1 + idx; uint8_t mask = (1 << (pinnum % 8)); // Mode 0 = input 1 = output uint8_t value = readRegister(reg); if (mode == OUTPUT) value |= mask; else value &= ~mask; writeRegister(reg, value); // Pullup 0 = enabled 1 = disabled reg = TCA8418_REG_GPIO_PULL_1 + idx; value = readRegister(reg); if (mode == INPUT_PULLUP) value &= ~mask; else value |= mask; writeRegister(reg, value); return true; } bool TCA8418KeyboardBase::pinIRQMode(uint8_t pinnum, uint8_t mode) { if (pinnum > TCA8418_COL9) return false; if ((mode != RISING) && (mode != FALLING)) return false; // Mode 0 = falling 1 = rising uint8_t idx = pinnum / 8; uint8_t reg = TCA8418_REG_GPIO_INT_LVL_1 + idx; uint8_t mask = (1 << (pinnum % 8)); uint8_t value = readRegister(reg); if (mode == RISING) value |= mask; else value &= ~mask; writeRegister(reg, value); // Enable interrupt reg = TCA8418_REG_GPIO_INT_EN_1 + idx; value = readRegister(reg); value |= mask; writeRegister(reg, value); return true; } void TCA8418KeyboardBase::enableInterrupts() { uint8_t value = readRegister(TCA8418_REG_CFG); value |= (_TCA8418_REG_CFG_GPI_IEN | _TCA8418_REG_CFG_KE_IEN); writeRegister(TCA8418_REG_CFG, value); }; void TCA8418KeyboardBase::disableInterrupts() { uint8_t value = readRegister(TCA8418_REG_CFG); value &= ~(_TCA8418_REG_CFG_GPI_IEN | _TCA8418_REG_CFG_KE_IEN); writeRegister(TCA8418_REG_CFG, value); }; void TCA8418KeyboardBase::enableMatrixOverflow() { uint8_t value = readRegister(TCA8418_REG_CFG); value |= _TCA8418_REG_CFG_OVR_FLOW_M; writeRegister(TCA8418_REG_CFG, value); }; void TCA8418KeyboardBase::disableMatrixOverflow() { uint8_t value = readRegister(TCA8418_REG_CFG); value &= ~_TCA8418_REG_CFG_OVR_FLOW_M; writeRegister(TCA8418_REG_CFG, value); }; void TCA8418KeyboardBase::enableDebounce() { writeRegister(TCA8418_REG_DEBOUNCE_DIS_1, 0x00); writeRegister(TCA8418_REG_DEBOUNCE_DIS_2, 0x00); writeRegister(TCA8418_REG_DEBOUNCE_DIS_3, 0x00); } void TCA8418KeyboardBase::disableDebounce() { writeRegister(TCA8418_REG_DEBOUNCE_DIS_1, 0xFF); writeRegister(TCA8418_REG_DEBOUNCE_DIS_2, 0xFF); writeRegister(TCA8418_REG_DEBOUNCE_DIS_3, 0xFF); } void TCA8418KeyboardBase::setBacklight(bool on) {} uint8_t TCA8418KeyboardBase::readRegister(uint8_t reg) const { if (m_wire) { m_wire->beginTransmission(m_addr); m_wire->write(reg); m_wire->endTransmission(); m_wire->requestFrom(m_addr, (uint8_t)1); if (m_wire->available() < 1) return 0; return m_wire->read(); } if (readCallback) { uint8_t data; readCallback(m_addr, reg, &data, 1); return data; } return 0; } void TCA8418KeyboardBase::writeRegister(uint8_t reg, uint8_t value) { uint8_t data[2]; data[0] = reg; data[1] = value; if (m_wire) { m_wire->beginTransmission(m_addr); m_wire->write(data, sizeof(uint8_t) * 2); m_wire->endTransmission(); } if (writeCallback) { writeCallback(m_addr, data[0], &(data[1]), 1); } } ================================================ FILE: src/input/TCA8418KeyboardBase.h ================================================ // Based on the MPR121 Keyboard and Adafruit TCA8418 library #include "configuration.h" #include /** * @brief TCA8418KeyboardBase is the base class for TCA8418 keyboard handling. * It provides basic functionality for reading key events, managing the keyboard matrix, * and handling key states. It is designed to be extended for specific keyboard implementations. * It supports both I2C communication and function pointers for custom I2C operations. */ class TCA8418KeyboardBase { public: enum TCA8418Key : uint8_t { NONE = 0x00, BSP = 0x08, TAB = 0x09, SELECT = 0x0d, ESC = 0x1b, REBOOT = 0x90, LEFT = 0xb4, UP = 0xb5, DOWN = 0xb6, RIGHT = 0xb7, BT_TOGGLE = 0xAA, GPS_TOGGLE = 0x9E, MUTE_TOGGLE = 0xAC, SEND_PING = 0xAF, BL_TOGGLE = 0xAB, FUNCTION_F1 = 0xF1, FUNCTION_F2 = 0xF2, FUNCTION_F3 = 0xF3, FUNCTION_F4 = 0xF4, FUNCTION_F5 = 0xF5 }; typedef uint8_t (*i2c_com_fptr_t)(uint8_t dev_addr, uint8_t reg_addr, uint8_t *data, uint8_t len); TCA8418KeyboardBase(uint8_t rows, uint8_t columns); virtual void begin(uint8_t addr = TCA8418_KB_ADDR, TwoWire *wire = &Wire); virtual void begin(i2c_com_fptr_t r, i2c_com_fptr_t w, uint8_t addr = TCA8418_KB_ADDR); virtual void reset(void); void clearInt(void); virtual void trigger(void); virtual void setBacklight(bool on); // Key events available virtual bool hasEvent(void) const; virtual char dequeueEvent(void); protected: enum KeyState { Init, Idle, Held, Busy }; enum TCA8418Register : uint8_t { TCA8418_REG_RESERVED = 0x00, TCA8418_REG_CFG = 0x01, TCA8418_REG_INT_STAT = 0x02, TCA8418_REG_KEY_LCK_EC = 0x03, TCA8418_REG_KEY_EVENT_A = 0x04, TCA8418_REG_KEY_EVENT_B = 0x05, TCA8418_REG_KEY_EVENT_C = 0x06, TCA8418_REG_KEY_EVENT_D = 0x07, TCA8418_REG_KEY_EVENT_E = 0x08, TCA8418_REG_KEY_EVENT_F = 0x09, TCA8418_REG_KEY_EVENT_G = 0x0A, TCA8418_REG_KEY_EVENT_H = 0x0B, TCA8418_REG_KEY_EVENT_I = 0x0C, TCA8418_REG_KEY_EVENT_J = 0x0D, TCA8418_REG_KP_LCK_TIMER = 0x0E, TCA8418_REG_UNLOCK_1 = 0x0F, TCA8418_REG_UNLOCK_2 = 0x10, TCA8418_REG_GPIO_INT_STAT_1 = 0x11, TCA8418_REG_GPIO_INT_STAT_2 = 0x12, TCA8418_REG_GPIO_INT_STAT_3 = 0x13, TCA8418_REG_GPIO_DAT_STAT_1 = 0x14, TCA8418_REG_GPIO_DAT_STAT_2 = 0x15, TCA8418_REG_GPIO_DAT_STAT_3 = 0x16, TCA8418_REG_GPIO_DAT_OUT_1 = 0x17, TCA8418_REG_GPIO_DAT_OUT_2 = 0x18, TCA8418_REG_GPIO_DAT_OUT_3 = 0x19, TCA8418_REG_GPIO_INT_EN_1 = 0x1A, TCA8418_REG_GPIO_INT_EN_2 = 0x1B, TCA8418_REG_GPIO_INT_EN_3 = 0x1C, TCA8418_REG_KP_GPIO_1 = 0x1D, TCA8418_REG_KP_GPIO_2 = 0x1E, TCA8418_REG_KP_GPIO_3 = 0x1F, TCA8418_REG_GPI_EM_1 = 0x20, TCA8418_REG_GPI_EM_2 = 0x21, TCA8418_REG_GPI_EM_3 = 0x22, TCA8418_REG_GPIO_DIR_1 = 0x23, TCA8418_REG_GPIO_DIR_2 = 0x24, TCA8418_REG_GPIO_DIR_3 = 0x25, TCA8418_REG_GPIO_INT_LVL_1 = 0x26, TCA8418_REG_GPIO_INT_LVL_2 = 0x27, TCA8418_REG_GPIO_INT_LVL_3 = 0x28, TCA8418_REG_DEBOUNCE_DIS_1 = 0x29, TCA8418_REG_DEBOUNCE_DIS_2 = 0x2A, TCA8418_REG_DEBOUNCE_DIS_3 = 0x2B, TCA8418_REG_GPIO_PULL_1 = 0x2C, TCA8418_REG_GPIO_PULL_2 = 0x2D, TCA8418_REG_GPIO_PULL_3 = 0x2E }; // Pin IDs for matrix rows/columns enum TCA8418PinId : uint8_t { TCA8418_ROW0, // Pin ID for row 0 TCA8418_ROW1, // Pin ID for row 1 TCA8418_ROW2, // Pin ID for row 2 TCA8418_ROW3, // Pin ID for row 3 TCA8418_ROW4, // Pin ID for row 4 TCA8418_ROW5, // Pin ID for row 5 TCA8418_ROW6, // Pin ID for row 6 TCA8418_ROW7, // Pin ID for row 7 TCA8418_COL0, // Pin ID for column 0 TCA8418_COL1, // Pin ID for column 1 TCA8418_COL2, // Pin ID for column 2 TCA8418_COL3, // Pin ID for column 3 TCA8418_COL4, // Pin ID for column 4 TCA8418_COL5, // Pin ID for column 5 TCA8418_COL6, // Pin ID for column 6 TCA8418_COL7, // Pin ID for column 7 TCA8418_COL8, // Pin ID for column 8 TCA8418_COL9 // Pin ID for column 9 }; virtual void pressed(uint8_t key); virtual void released(void); virtual void queueEvent(char); virtual ~TCA8418KeyboardBase() {} protected: // Set the size of the keypad matrix // All other rows and columns are set as inputs. bool matrix(uint8_t rows, uint8_t columns); uint8_t keyCount(void) const; // Flush all events in the FIFO buffer + GPIO events. uint8_t flush(void); // debounce keys. void enableDebounce(); void disableDebounce(); // enable / disable interrupts for matrix and GPI pins void enableInterrupts(); void disableInterrupts(); // ignore key events when FIFO buffer is full or not. void enableMatrixOverflow(); void disableMatrixOverflow(); uint8_t digitalRead(uint8_t pinnum) const; bool digitalWrite(uint8_t pinnum, uint8_t level); bool pinMode(uint8_t pinnum, uint8_t mode); bool pinIRQMode(uint8_t pinnum, uint8_t mode); // MODE FALLING or RISING uint8_t readRegister(uint8_t reg) const; void writeRegister(uint8_t reg, uint8_t value); protected: uint8_t rows; uint8_t columns; KeyState state; String queue; private: TwoWire *m_wire; uint8_t m_addr; i2c_com_fptr_t readCallback; i2c_com_fptr_t writeCallback; }; ================================================ FILE: src/input/TDeckProKeyboard.cpp ================================================ #if defined(T_DECK_PRO) #include "TDeckProKeyboard.h" #define _TCA8418_COLS 10 #define _TCA8418_ROWS 4 #define _TCA8418_NUM_KEYS 35 #define _TCA8418_MULTI_TAP_THRESHOLD 1500 using Key = TCA8418KeyboardBase::TCA8418Key; constexpr uint8_t modifierRightShiftKey = 31 - 1; // keynum -1 constexpr uint8_t modifierRightShift = 0b0001; constexpr uint8_t modifierLeftShiftKey = 35 - 1; constexpr uint8_t modifierLeftShift = 0b0001; constexpr uint8_t modifierSymKey = 32 - 1; constexpr uint8_t modifierSym = 0b0010; constexpr uint8_t modifierAltKey = 30 - 1; constexpr uint8_t modifierAlt = 0b0100; // Num chars per key, Modulus for rotating through characters static uint8_t TDeckProTapMod[_TCA8418_NUM_KEYS] = {5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5}; static unsigned char TDeckProTapMap[_TCA8418_NUM_KEYS][5] = { {'p', 'P', '@', 0x00, Key::SEND_PING}, {'o', 'O', '+'}, {'i', 'I', '-'}, {'u', 'U', '_'}, {'y', 'Y', ')'}, {'t', 'T', '(', 0x00, Key::TAB}, {'r', 'R', '3'}, {'e', 'E', '2', 0x00, Key::UP}, {'w', 'W', '1'}, {'q', 'Q', '#', 0x00, Key::ESC}, // p, o, i, u, y, t, r, e, w, q {Key::BSP, 0x00, 0x00}, {'l', 'L', '"'}, {'k', 'K', '\''}, {'j', 'J', ';'}, {'h', 'H', ':'}, {'g', 'G', '/', 0x00, Key::GPS_TOGGLE}, {'f', 'F', '6', 0x00, Key::RIGHT}, {'d', 'D', '5'}, {'s', 'S', '4', 0x00, Key::LEFT}, {'a', 'A', '*'}, // bsp, l, k, j, h, g, f, d, s, a {0x0d, 0x00, 0x00}, {'$', 0x00, 0x00}, {'m', 'M', '.', 0x00, Key::MUTE_TOGGLE}, {'n', 'N', ','}, {'b', 'B', '!', 0x00, Key::BL_TOGGLE}, {'v', 'V', '?'}, {'c', 'C', '9'}, {'x', 'X', '8', 0x00, Key::DOWN}, {'z', 'Z', '7'}, {0x00, 0x00, 0x00}, // Ent, $, m, n, b, v, c, x, z, alt {0x00, 0x00, 0x00}, {0x00, 0x00, 0x00}, {0x20, 0x00, 0x00}, {0x00, 0x00, '0'}, {0x00, 0x00, 0x00} // R_Shift, sym, space, mic, L_Shift }; TDeckProKeyboard::TDeckProKeyboard() : TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), modifierFlag(0), last_modifier_time(0), last_key(UINT8_MAX), next_key(UINT8_MAX), last_tap(0L), char_idx(0), tap_interval(0) { } void TDeckProKeyboard::reset() { TCA8418KeyboardBase::reset(); pinMode(KB_BL_PIN, OUTPUT); setBacklight(false); } // handle multi-key presses (shift and alt) void TDeckProKeyboard::trigger() { uint8_t count = keyCount(); if (count == 0) return; for (uint8_t i = 0; i < count; ++i) { uint8_t k = readRegister(TCA8418_REG_KEY_EVENT_A + i); uint8_t key = k & 0x7F; if (k & 0x80) { pressed(key); } else { released(); state = Idle; } } } void TDeckProKeyboard::pressed(uint8_t key) { if (state == Init || state == Busy) { return; } if (modifierFlag && (millis() - last_modifier_time > _TCA8418_MULTI_TAP_THRESHOLD)) { modifierFlag = 0; } int row = (key - 1) / 10; int col = (key - 1) % 10; if (row >= _TCA8418_ROWS || col >= _TCA8418_COLS) { return; // Invalid key } next_key = row * _TCA8418_COLS + col; state = Held; uint32_t now = millis(); tap_interval = now - last_tap; updateModifierFlag(next_key); if (isModifierKey(next_key)) { last_modifier_time = now; } if (tap_interval < 0) { last_tap = 0; state = Busy; return; } if (next_key != last_key || tap_interval > _TCA8418_MULTI_TAP_THRESHOLD) { char_idx = 0; } else { char_idx += 1; } last_key = next_key; last_tap = now; } void TDeckProKeyboard::released() { if (state != Held) { return; } if (last_key >= _TCA8418_NUM_KEYS) { last_key = UINT8_MAX; state = Idle; return; } uint32_t now = millis(); last_tap = now; if (TDeckProTapMap[last_key][modifierFlag % TDeckProTapMod[last_key]] == Key::BL_TOGGLE) { toggleBacklight(); return; } queueEvent(TDeckProTapMap[last_key][modifierFlag % TDeckProTapMod[last_key]]); if (isModifierKey(last_key) == false) modifierFlag = 0; } void TDeckProKeyboard::setBacklight(bool on) { if (on) { digitalWrite(KB_BL_PIN, HIGH); } else { digitalWrite(KB_BL_PIN, LOW); } } void TDeckProKeyboard::toggleBacklight(void) { digitalWrite(KB_BL_PIN, !digitalRead(KB_BL_PIN)); } void TDeckProKeyboard::updateModifierFlag(uint8_t key) { if (key == modifierRightShiftKey) { modifierFlag ^= modifierRightShift; } else if (key == modifierLeftShiftKey) { modifierFlag ^= modifierLeftShift; } else if (key == modifierSymKey) { modifierFlag ^= modifierSym; } else if (key == modifierAltKey) { modifierFlag ^= modifierAlt; } } bool TDeckProKeyboard::isModifierKey(uint8_t key) { return (key == modifierRightShiftKey || key == modifierLeftShiftKey || key == modifierAltKey || key == modifierSymKey); } #endif // T_DECK_PRO ================================================ FILE: src/input/TDeckProKeyboard.h ================================================ #include "TCA8418KeyboardBase.h" class TDeckProKeyboard : public TCA8418KeyboardBase { public: TDeckProKeyboard(); void reset(void) override; void trigger(void) override; void setBacklight(bool on) override; protected: void pressed(uint8_t key) override; void released(void) override; void updateModifierFlag(uint8_t key); bool isModifierKey(uint8_t key); void toggleBacklight(void); private: uint8_t modifierFlag; // Flag to indicate if a modifier key is pressed uint32_t last_modifier_time; // Timestamp of the last modifier key press uint8_t last_key; uint8_t next_key; uint32_t last_tap; uint8_t char_idx; int32_t tap_interval; }; ================================================ FILE: src/input/TLoraPagerKeyboard.cpp ================================================ #if defined(T_LORA_PAGER) #include "TLoraPagerKeyboard.h" #include "main.h" #ifndef LEDC_BACKLIGHT_CHANNEL #define LEDC_BACKLIGHT_CHANNEL 4 #endif #ifndef LEDC_BACKLIGHT_BIT_WIDTH #define LEDC_BACKLIGHT_BIT_WIDTH 8 #endif #ifndef LEDC_BACKLIGHT_FREQ #define LEDC_BACKLIGHT_FREQ 1000 // Hz #endif #define _TCA8418_COLS 10 #define _TCA8418_ROWS 4 #define _TCA8418_NUM_KEYS 31 #define _TCA8418_MULTI_TAP_THRESHOLD 1500 using Key = TCA8418KeyboardBase::TCA8418Key; constexpr uint8_t modifierRightShiftKey = 29 - 1; // keynum -1 constexpr uint8_t modifierRightShift = 0b0001; constexpr uint8_t modifierSymKey = 21 - 1; constexpr uint8_t modifierSym = 0b0010; // Num chars per key, Modulus for rotating through characters static uint8_t TLoraPagerTapMod[_TCA8418_NUM_KEYS] = {3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3}; static unsigned char TLoraPagerTapMap[_TCA8418_NUM_KEYS][3] = {{'q', 'Q', '1'}, {'w', 'W', '2'}, {'e', 'E', '3'}, {'r', 'R', '4'}, {'t', 'T', '5'}, {'y', 'Y', '6'}, {'u', 'U', '7'}, {'i', 'I', '8'}, {'o', 'O', '9'}, {'p', 'P', '0'}, {'a', 'A', '*'}, {'s', 'S', '/'}, {'d', 'D', '+'}, {'f', 'F', '-'}, {'g', 'G', '='}, {'h', 'H', ':'}, {'j', 'J', '\''}, {'k', 'K', '"'}, {'l', 'L', '@'}, {Key::SELECT, 0x00, Key::TAB}, {0x00, 0x00, 0x00}, {'z', 'Z', '_'}, {'x', 'X', '$'}, {'c', 'C', ';'}, {'v', 'V', '?'}, {'b', 'B', '!'}, {'n', 'N', ','}, {'m', 'M', '.'}, {0x00, 0x00, 0x00}, {Key::BSP, 0x00, Key::ESC}, {' ', 0x00, Key::BL_TOGGLE}}; TLoraPagerKeyboard::TLoraPagerKeyboard() : TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), modifierFlag(0), last_modifier_time(0), last_key(UINT8_MAX), next_key(UINT8_MAX), last_tap(0L), char_idx(0), tap_interval(0) { #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) ledcAttach(KB_BL_PIN, LEDC_BACKLIGHT_FREQ, LEDC_BACKLIGHT_BIT_WIDTH); #else ledcSetup(LEDC_BACKLIGHT_CHANNEL, LEDC_BACKLIGHT_FREQ, LEDC_BACKLIGHT_BIT_WIDTH); ledcAttachPin(KB_BL_PIN, LEDC_BACKLIGHT_CHANNEL); #endif reset(); } void TLoraPagerKeyboard::reset(void) { TCA8418KeyboardBase::reset(); pinMode(KB_BL_PIN, OUTPUT); digitalWrite(KB_BL_PIN, LOW); setBacklight(false); } // handle multi-key presses (shift and alt) void TLoraPagerKeyboard::trigger() { uint8_t count = keyCount(); if (count == 0) return; for (uint8_t i = 0; i < count; ++i) { uint8_t k = readRegister(TCA8418_REG_KEY_EVENT_A + i); uint8_t key = k & 0x7F; if (k & 0x80) { pressed(key); } else { released(); state = Idle; } } } void TLoraPagerKeyboard::setBacklight(bool on) { uint32_t _brightness = 0; if (on) _brightness = brightness; #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) ledcWrite(KB_BL_PIN, _brightness); #else ledcWrite(LEDC_BACKLIGHT_CHANNEL, _brightness); #endif } void TLoraPagerKeyboard::pressed(uint8_t key) { if (state == Init || state == Busy) { return; } if (config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_ALL_ENABLED || config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_SYSTEM_ONLY) { hapticFeedback(); } if (modifierFlag && (millis() - last_modifier_time > _TCA8418_MULTI_TAP_THRESHOLD)) { modifierFlag = 0; } int row = (key - 1) / 10; int col = (key - 1) % 10; if (row >= _TCA8418_ROWS || col >= _TCA8418_COLS) { return; // Invalid key } next_key = row * _TCA8418_COLS + col; state = Held; uint32_t now = millis(); tap_interval = now - last_tap; updateModifierFlag(next_key); if (isModifierKey(next_key)) { last_modifier_time = now; } if (tap_interval < 0) { last_tap = 0; state = Busy; return; } if (next_key != last_key || tap_interval > _TCA8418_MULTI_TAP_THRESHOLD) { char_idx = 0; } else { char_idx += 1; } last_key = next_key; last_tap = now; } void TLoraPagerKeyboard::released() { if (state != Held) { return; } if (last_key >= _TCA8418_NUM_KEYS) { last_key = UINT8_MAX; state = Idle; return; } uint32_t now = millis(); last_tap = now; if (TLoraPagerTapMap[last_key][modifierFlag % TLoraPagerTapMod[last_key]] == Key::BL_TOGGLE) { toggleBacklight(); return; } queueEvent(TLoraPagerTapMap[last_key][modifierFlag % TLoraPagerTapMod[last_key]]); if (isModifierKey(last_key) == false) modifierFlag = 0; } void TLoraPagerKeyboard::hapticFeedback() { drv.setWaveform(0, 14); // strong buzz 100% drv.setWaveform(1, 0); // end waveform drv.go(); } // toggle brightness of the backlight in three steps void TLoraPagerKeyboard::toggleBacklight(bool off) { if (off) { brightness = 0; } else { if (brightness == 0) { brightness = 40; } else if (brightness == 40) { brightness = 127; } else if (brightness >= 127) { brightness = 0; } } LOG_DEBUG("Toggle backlight: %d", brightness); setBacklight(true); } void TLoraPagerKeyboard::updateModifierFlag(uint8_t key) { if (key == modifierRightShiftKey) { modifierFlag ^= modifierRightShift; } else if (key == modifierSymKey) { modifierFlag ^= modifierSym; } } bool TLoraPagerKeyboard::isModifierKey(uint8_t key) { return (key == modifierRightShiftKey || key == modifierSymKey); } #endif ================================================ FILE: src/input/TLoraPagerKeyboard.h ================================================ #include "TCA8418KeyboardBase.h" class TLoraPagerKeyboard : public TCA8418KeyboardBase { public: TLoraPagerKeyboard(); void reset(void); void trigger(void) override; void setBacklight(bool on) override; virtual ~TLoraPagerKeyboard() {} protected: void pressed(uint8_t key) override; void released(void) override; void hapticFeedback(void); void updateModifierFlag(uint8_t key); bool isModifierKey(uint8_t key); void toggleBacklight(bool off = false); private: uint8_t modifierFlag; // Flag to indicate if a modifier key is pressed uint32_t last_modifier_time; // Timestamp of the last modifier key press uint8_t last_key; uint8_t next_key; uint32_t last_tap; uint8_t char_idx; int32_t tap_interval; uint32_t brightness = 0; }; ================================================ FILE: src/input/TouchScreenBase.cpp ================================================ #include "TouchScreenBase.h" #include "main.h" #if defined(RAK14014) && !defined(MESHTASTIC_EXCLUDE_CANNEDMESSAGES) #include "modules/CannedMessageModule.h" #endif #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 (x < 0 || y < 0) // T-deck can emit phantom touch events with a negative value when turning off the screen touched = false; if (touched) { this->setInterval(20); _last_x = x; _last_y = y; } if (touched != _touchedOld) { if (touched) { hapticFeedback(); _state = TOUCH_EVENT_OCCURRED; _start = millis(); _first_x = x; _first_y = y; } else { _state = TOUCH_EVENT_CLEARED; time_t duration = millis() - _start; x = _last_x; y = _last_y; this->setInterval(50); // compute distance int16_t dx = x - _first_x; int16_t dy = y - _first_y; uint16_t adx = abs(dx); uint16_t ady = abs(dy); // swipe horizontal if (adx > ady && adx > TOUCH_THRESHOLD_X) { if (0 > dx) { // swipe right to left e.touchEvent = static_cast(TOUCH_ACTION_LEFT); LOG_DEBUG("action SWIPE: right to left"); } else { // swipe left to right e.touchEvent = static_cast(TOUCH_ACTION_RIGHT); LOG_DEBUG("action SWIPE: left to right"); } } // swipe vertical else if (ady > adx && ady > TOUCH_THRESHOLD_Y) { if (0 > dy) { // swipe bottom to top e.touchEvent = static_cast(TOUCH_ACTION_UP); LOG_DEBUG("action SWIPE: bottom to top"); } else { // swipe top to bottom e.touchEvent = static_cast(TOUCH_ACTION_DOWN); LOG_DEBUG("action SWIPE: top to bottom"); } } // tap else { if (duration > 0 && duration < TIME_LONG_PRESS) { if (_tapped) { _tapped = false; } else { _tapped = true; } } else { _tapped = false; } } } } _touchedOld = touched; #if defined RAK14014 // Speed up the processing speed of the keyboard in virtual keyboard mode auto state = cannedMessageModule->getRunState(); if (state == CANNED_MESSAGE_RUN_STATE_FREETEXT) { if (_tapped) { _tapped = false; e.touchEvent = static_cast(TOUCH_ACTION_TAP); LOG_DEBUG("action TAP(%d/%d)", _last_x, _last_y); } } else { if (_tapped && (time_t(millis()) - _start) > TIME_LONG_PRESS - 50) { _tapped = false; e.touchEvent = static_cast(TOUCH_ACTION_TAP); LOG_DEBUG("action TAP(%d/%d)", _last_x, _last_y); } } #else // fire TAP event when no 2nd tap occurred within time if (_tapped) { _tapped = false; e.touchEvent = static_cast(TOUCH_ACTION_TAP); LOG_DEBUG("action TAP(%d/%d)", _last_x, _last_y); } #endif // fire LONG_PRESS event without the need for release if (touched && (time_t(millis()) - _start) > TIME_LONG_PRESS) { // tricky: prevent reoccurring events and another touch event when releasing _start = millis() + 30000; e.touchEvent = static_cast(TOUCH_ACTION_LONG_PRESS); LOG_DEBUG("action LONG PRESS(%d/%d)", _last_x, _last_y); } 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 } ================================================ FILE: src/input/TouchScreenBase.h ================================================ #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_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; }; ================================================ FILE: src/input/TouchScreenImpl1.cpp ================================================ #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 (portduino_config.touchscreenModule) { TouchScreenBase::init(true); inputBroker->registerSource(this); } else { TouchScreenBase::init(false); } #elif !HAS_TOUCHSCREEN TouchScreenBase::init(false); return; #else TouchScreenBase::init(true); inputBroker->registerSource(this); #endif } bool TouchScreenImpl1::getTouch(int16_t &x, int16_t &y) { return _getTouch(&x, &y); } /** * @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.kbchar = 0; e.touchX = event.x; e.touchY = event.y; switch (event.touchEvent) { case TOUCH_ACTION_LEFT: { e.inputEvent = INPUT_BROKER_LEFT; break; } case TOUCH_ACTION_RIGHT: { e.inputEvent = INPUT_BROKER_RIGHT; break; } case TOUCH_ACTION_UP: { e.inputEvent = INPUT_BROKER_UP; break; } case TOUCH_ACTION_DOWN: { e.inputEvent = INPUT_BROKER_DOWN; break; } case TOUCH_ACTION_LONG_PRESS: { e.inputEvent = INPUT_BROKER_SELECT; break; } case TOUCH_ACTION_TAP: { e.inputEvent = INPUT_BROKER_USER_PRESS; break; } default: return; } this->notifyObservers(&e); } ================================================ FILE: src/input/TouchScreenImpl1.h ================================================ #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; ================================================ FILE: src/input/TrackballInterruptBase.cpp ================================================ #include "TrackballInterruptBase.h" #include "Throttle.h" #include "configuration.h" extern bool osk_found; TrackballInterruptBase::TrackballInterruptBase(const char *name) : concurrency::OSThread(name), _originName(name) {} void TrackballInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLeft, uint8_t pinRight, uint8_t pinPress, input_broker_event eventDown, input_broker_event eventUp, input_broker_event eventLeft, input_broker_event eventRight, input_broker_event eventPressed, input_broker_event eventPressedLong, void (*onIntDown)(), void (*onIntUp)(), void (*onIntLeft)(), void (*onIntRight)(), void (*onIntPress)()) { this->_pinDown = pinDown; this->_pinUp = pinUp; this->_pinLeft = pinLeft; this->_pinRight = pinRight; this->_pinPress = pinPress; this->_eventDown = eventDown; this->_eventUp = eventUp; this->_eventLeft = eventLeft; this->_eventRight = eventRight; this->_eventPressed = eventPressed; this->_eventPressedLong = eventPressedLong; if (pinPress != 255) { pinMode(pinPress, INPUT_PULLUP); attachInterrupt(pinPress, onIntPress, TB_DIRECTION); } if (this->_pinDown != 255) { pinMode(this->_pinDown, INPUT_PULLUP); attachInterrupt(this->_pinDown, onIntDown, TB_DIRECTION); } if (this->_pinUp != 255) { pinMode(this->_pinUp, INPUT_PULLUP); attachInterrupt(this->_pinUp, onIntUp, TB_DIRECTION); } if (this->_pinLeft != 255) { pinMode(this->_pinLeft, INPUT_PULLUP); attachInterrupt(this->_pinLeft, onIntLeft, TB_DIRECTION); } if (this->_pinRight != 255) { pinMode(this->_pinRight, INPUT_PULLUP); attachInterrupt(this->_pinRight, onIntRight, TB_DIRECTION); } LOG_DEBUG("Trackball GPIO initialized - UP:%d DOWN:%d LEFT:%d RIGHT:%d PRESS:%d", this->_pinUp, this->_pinDown, this->_pinLeft, this->_pinRight, pinPress); #ifndef HAS_PHYSICAL_KEYBOARD osk_found = true; #endif this->setInterval(100); } int32_t TrackballInterruptBase::runOnce() { InputEvent e = {}; e.inputEvent = INPUT_BROKER_NONE; #if TB_THRESHOLD if (lastInterruptTime && !Throttle::isWithinTimespanMs(lastInterruptTime, 1000)) { left_counter = 0; right_counter = 0; up_counter = 0; down_counter = 0; lastInterruptTime = 0; } #ifdef INPUT_DEBUG if (left_counter > 0 || right_counter > 0 || up_counter > 0 || down_counter > 0) { LOG_DEBUG("L %u R %u U %u D %u, time %u", left_counter, right_counter, up_counter, down_counter, millis()); } #endif #endif // Handle long press detection for press button if (pressDetected && pressStartTime > 0) { uint32_t pressDuration = millis() - pressStartTime; bool buttonStillPressed = false; buttonStillPressed = !digitalRead(_pinPress); if (!buttonStillPressed) { // Button released if (pressDuration < LONG_PRESS_DURATION) { // Short press e.inputEvent = this->_eventPressed; } // Reset state pressDetected = false; pressStartTime = 0; lastLongPressEventTime = 0; this->action = TB_ACTION_NONE; } else if (pressDuration >= LONG_PRESS_DURATION) { // Long press detected uint32_t currentTime = millis(); // Only trigger long press event if enough time has passed since the last one if (lastLongPressEventTime == 0 || (currentTime - lastLongPressEventTime) >= LONG_PRESS_REPEAT_INTERVAL) { e.inputEvent = this->_eventPressedLong; lastLongPressEventTime = currentTime; } this->action = TB_ACTION_PRESSED_LONG; } } if (directionDetected && directionStartTime > 0) { uint32_t directionDuration = millis() - directionStartTime; uint8_t directionPressedNow = 0; directionInterval++; if (!digitalRead(_pinUp)) { directionPressedNow = TB_ACTION_UP; } else if (!digitalRead(_pinDown)) { directionPressedNow = TB_ACTION_DOWN; } else if (!digitalRead(_pinLeft)) { directionPressedNow = TB_ACTION_LEFT; } else if (!digitalRead(_pinRight)) { directionPressedNow = TB_ACTION_RIGHT; } const uint8_t DIRECTION_REPEAT_THRESHOLD = 3; if (directionPressedNow == TB_ACTION_NONE) { // Reset state directionDetected = false; directionStartTime = 0; directionInterval = 0; this->action = TB_ACTION_NONE; } else if (directionDuration >= LONG_PRESS_DURATION && directionInterval >= DIRECTION_REPEAT_THRESHOLD) { // repeat event when long press these direction. switch (directionPressedNow) { case TB_ACTION_UP: e.inputEvent = this->_eventUp; break; case TB_ACTION_DOWN: e.inputEvent = this->_eventDown; break; case TB_ACTION_LEFT: e.inputEvent = this->_eventLeft; break; case TB_ACTION_RIGHT: e.inputEvent = this->_eventRight; break; } directionInterval = 0; } } #if TB_THRESHOLD if (this->action == TB_ACTION_PRESSED && (!pressDetected || pressStartTime == 0)) { // Start long press detection pressDetected = true; pressStartTime = millis(); // Don't send event yet, wait to see if it's a long press } else if (up_counter >= TB_THRESHOLD) { #ifdef INPUT_DEBUG LOG_DEBUG("Trackball event UP %u", millis()); #endif e.inputEvent = this->_eventUp; } else if (down_counter >= TB_THRESHOLD) { #ifdef INPUT_DEBUG LOG_DEBUG("Trackball event DOWN %u", millis()); #endif e.inputEvent = this->_eventDown; } else if (left_counter >= TB_THRESHOLD) { #ifdef INPUT_DEBUG LOG_DEBUG("Trackball event LEFT %u", millis()); #endif e.inputEvent = this->_eventLeft; } else if (right_counter >= TB_THRESHOLD) { #ifdef INPUT_DEBUG LOG_DEBUG("Trackball event RIGHT %u", millis()); #endif e.inputEvent = this->_eventRight; } #else if (this->action == TB_ACTION_PRESSED && !digitalRead(_pinPress) && !pressDetected) { // Start long press detection pressDetected = true; pressStartTime = millis(); // Don't send event yet, wait to see if it's a long press } else if (this->action == TB_ACTION_UP && !digitalRead(_pinUp) && !directionDetected) { directionDetected = true; directionStartTime = millis(); e.inputEvent = this->_eventUp; // send event first,will automatically trigger every 50ms * 3 after 500ms } else if (this->action == TB_ACTION_DOWN && !digitalRead(_pinDown) && !directionDetected) { directionDetected = true; directionStartTime = millis(); e.inputEvent = this->_eventDown; } else if (this->action == TB_ACTION_LEFT && !digitalRead(_pinLeft) && !directionDetected) { directionDetected = true; directionStartTime = millis(); e.inputEvent = this->_eventLeft; } else if (this->action == TB_ACTION_RIGHT && !digitalRead(_pinRight) && !directionDetected) { directionDetected = true; directionStartTime = millis(); e.inputEvent = this->_eventRight; } #endif if (e.inputEvent != INPUT_BROKER_NONE) { e.source = this->_originName; e.kbchar = 0x00; this->notifyObservers(&e); #if TB_THRESHOLD left_counter = 0; right_counter = 0; up_counter = 0; down_counter = 0; #endif } // Only update lastEvent for non-press actions or completed press actions if (this->action != TB_ACTION_PRESSED || !pressDetected) { lastEvent = action; if (!pressDetected) { this->action = TB_ACTION_NONE; } } return 50; // Check more frequently for better long press detection } void TrackballInterruptBase::intPressHandler() { if (!Throttle::isWithinTimespanMs(lastInterruptTime, 10)) this->action = TB_ACTION_PRESSED; lastInterruptTime = millis(); } void TrackballInterruptBase::intDownHandler() { if (TB_THRESHOLD || !Throttle::isWithinTimespanMs(lastInterruptTime, 10)) this->action = TB_ACTION_DOWN; lastInterruptTime = millis(); #if TB_THRESHOLD down_counter++; #endif } void TrackballInterruptBase::intUpHandler() { if (TB_THRESHOLD || !Throttle::isWithinTimespanMs(lastInterruptTime, 10)) this->action = TB_ACTION_UP; lastInterruptTime = millis(); #if TB_THRESHOLD up_counter++; #endif } void TrackballInterruptBase::intLeftHandler() { if (TB_THRESHOLD || !Throttle::isWithinTimespanMs(lastInterruptTime, 10)) this->action = TB_ACTION_LEFT; lastInterruptTime = millis(); #if TB_THRESHOLD left_counter++; #endif } void TrackballInterruptBase::intRightHandler() { if (TB_THRESHOLD || !Throttle::isWithinTimespanMs(lastInterruptTime, 10)) this->action = TB_ACTION_RIGHT; lastInterruptTime = millis(); #if TB_THRESHOLD right_counter++; #endif } ================================================ FILE: src/input/TrackballInterruptBase.h ================================================ #pragma once #include "InputBroker.h" #include "mesh/NodeDB.h" #ifndef TB_DIRECTION #if ARCH_PORTDUINO #include "PortduinoGlue.h" #define TB_DIRECTION (PinStatus) portduino_config.lora_usb_vid #else #define TB_DIRECTION RISING #endif #endif #ifndef TB_THRESHOLD #define TB_THRESHOLD 0 #endif class TrackballInterruptBase : public Observable, public concurrency::OSThread { public: explicit TrackballInterruptBase(const char *name); void init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLeft, uint8_t pinRight, uint8_t pinPress, input_broker_event eventDown, input_broker_event eventUp, input_broker_event eventLeft, input_broker_event eventRight, input_broker_event eventPressed, input_broker_event eventPressedLong, void (*onIntDown)(), void (*onIntUp)(), void (*onIntLeft)(), void (*onIntRight)(), void (*onIntPress)()); void intPressHandler(); void intDownHandler(); void intUpHandler(); void intLeftHandler(); void intRightHandler(); virtual int32_t runOnce() override; protected: enum TrackballInterruptBaseActionType { TB_ACTION_NONE, TB_ACTION_PRESSED, TB_ACTION_PRESSED_LONG, TB_ACTION_UP, TB_ACTION_DOWN, TB_ACTION_LEFT, TB_ACTION_RIGHT }; uint8_t _pinDown = 0; uint8_t _pinUp = 0; uint8_t _pinLeft = 0; uint8_t _pinRight = 0; uint8_t _pinPress = 0; volatile TrackballInterruptBaseActionType action = TB_ACTION_NONE; // Long press detection for press button uint32_t pressStartTime = 0; uint32_t directionStartTime = 0; uint8_t directionInterval = 0; bool pressDetected = false; bool directionDetected = false; uint32_t lastLongPressEventTime = 0; uint32_t lastDirectionPressEventTime = 0; static const uint32_t LONG_PRESS_DURATION = 500; // ms static const uint32_t LONG_PRESS_REPEAT_INTERVAL = 300; // ms - interval between repeated long press events private: input_broker_event _eventDown = INPUT_BROKER_NONE; input_broker_event _eventUp = INPUT_BROKER_NONE; input_broker_event _eventLeft = INPUT_BROKER_NONE; input_broker_event _eventRight = INPUT_BROKER_NONE; input_broker_event _eventPressed = INPUT_BROKER_NONE; input_broker_event _eventPressedLong = INPUT_BROKER_NONE; const char *_originName; TrackballInterruptBaseActionType lastEvent = TB_ACTION_NONE; volatile uint32_t lastInterruptTime = 0; #if TB_THRESHOLD volatile uint8_t left_counter = 0; volatile uint8_t right_counter = 0; volatile uint8_t up_counter = 0; volatile uint8_t down_counter = 0; #endif }; ================================================ FILE: src/input/TrackballInterruptImpl1.cpp ================================================ #include "TrackballInterruptImpl1.h" #include "InputBroker.h" #include "configuration.h" TrackballInterruptImpl1 *trackballInterruptImpl1; TrackballInterruptImpl1::TrackballInterruptImpl1() : TrackballInterruptBase("trackball1") {} void TrackballInterruptImpl1::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLeft, uint8_t pinRight, uint8_t pinPress) { input_broker_event eventDown = INPUT_BROKER_DOWN; input_broker_event eventUp = INPUT_BROKER_UP; input_broker_event eventLeft = INPUT_BROKER_LEFT; input_broker_event eventRight = INPUT_BROKER_RIGHT; input_broker_event eventPressed = INPUT_BROKER_SELECT; input_broker_event eventPressedLong = INPUT_BROKER_SELECT_LONG; TrackballInterruptBase::init(pinDown, pinUp, pinLeft, pinRight, pinPress, eventDown, eventUp, eventLeft, eventRight, eventPressed, eventPressedLong, TrackballInterruptImpl1::handleIntDown, TrackballInterruptImpl1::handleIntUp, TrackballInterruptImpl1::handleIntLeft, TrackballInterruptImpl1::handleIntRight, TrackballInterruptImpl1::handleIntPressed); inputBroker->registerSource(this); } void TrackballInterruptImpl1::handleIntDown() { trackballInterruptImpl1->intDownHandler(); trackballInterruptImpl1->setIntervalFromNow(20); } void TrackballInterruptImpl1::handleIntUp() { trackballInterruptImpl1->intUpHandler(); trackballInterruptImpl1->setIntervalFromNow(20); } void TrackballInterruptImpl1::handleIntLeft() { trackballInterruptImpl1->intLeftHandler(); trackballInterruptImpl1->setIntervalFromNow(20); } void TrackballInterruptImpl1::handleIntRight() { trackballInterruptImpl1->intRightHandler(); trackballInterruptImpl1->setIntervalFromNow(20); } void TrackballInterruptImpl1::handleIntPressed() { trackballInterruptImpl1->intPressHandler(); trackballInterruptImpl1->setIntervalFromNow(20); } ================================================ FILE: src/input/TrackballInterruptImpl1.h ================================================ #pragma once #include "TrackballInterruptBase.h" class TrackballInterruptImpl1 : public TrackballInterruptBase { public: TrackballInterruptImpl1(); void init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLeft, uint8_t pinRight, uint8_t pinPress); static void handleIntDown(); static void handleIntUp(); static void handleIntLeft(); static void handleIntRight(); static void handleIntPressed(); }; extern TrackballInterruptImpl1 *trackballInterruptImpl1; ================================================ FILE: src/input/UpDownInterruptBase.cpp ================================================ #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, input_broker_event eventDown, input_broker_event eventUp, input_broker_event eventPressed, input_broker_event eventPressedLong, input_broker_event eventUpLong, input_broker_event eventDownLong, void (*onIntDown)(), void (*onIntUp)(), void (*onIntPress)(), unsigned long updownDebounceMs) { this->_pinDown = pinDown; this->_pinUp = pinUp; this->_pinPress = pinPress; this->_eventDown = eventDown; this->_eventUp = eventUp; this->_eventPressed = eventPressed; this->_eventPressedLong = eventPressedLong; this->_eventUpLong = eventUpLong; this->_eventDownLong = eventDownLong; // Store debounce configuration passed by caller this->updownDebounceMs = updownDebounceMs; bool isRAK = false; #ifdef RAK_4631 isRAK = true; #endif if (!isRAK || pinPress != 0) { pinMode(pinPress, INPUT_PULLUP); attachInterrupt(pinPress, onIntPress, FALLING); } if (!isRAK || this->_pinDown != 0) { pinMode(this->_pinDown, INPUT_PULLUP); attachInterrupt(this->_pinDown, onIntDown, FALLING); } if (!isRAK || this->_pinUp != 0) { pinMode(this->_pinUp, INPUT_PULLUP); attachInterrupt(this->_pinUp, onIntUp, FALLING); } LOG_DEBUG("Up/down/press GPIO initialized (%d, %d, %d)", this->_pinUp, this->_pinDown, pinPress); this->setInterval(20); } int32_t UpDownInterruptBase::runOnce() { InputEvent e = {}; e.inputEvent = INPUT_BROKER_NONE; unsigned long now = millis(); // Read all button states once at the beginning bool pressButtonPressed = !digitalRead(_pinPress); bool upButtonPressed = !digitalRead(_pinUp); bool downButtonPressed = !digitalRead(_pinDown); // Handle initial button press detection - only if not already detected if (this->action == UPDOWN_ACTION_PRESSED && pressButtonPressed && !pressDetected) { pressDetected = true; pressStartTime = now; } else if (this->action == UPDOWN_ACTION_UP && upButtonPressed && !upDetected) { upDetected = true; upStartTime = now; } else if (this->action == UPDOWN_ACTION_DOWN && downButtonPressed && !downDetected) { downDetected = true; downStartTime = now; } // Handle long press detection for press button if (pressDetected && pressStartTime > 0) { uint32_t pressDuration = now - pressStartTime; if (!pressButtonPressed) { // Button released if (pressDuration < LONG_PRESS_DURATION && now - lastPressKeyTime >= pressDebounceMs) { lastPressKeyTime = now; e.inputEvent = this->_eventPressed; } // Reset state pressDetected = false; pressStartTime = 0; lastPressLongEventTime = 0; } else if (pressDuration >= LONG_PRESS_DURATION && lastPressLongEventTime == 0) { // First long press event only - avoid repeated events causing lag e.inputEvent = this->_eventPressedLong; lastPressLongEventTime = now; } } // Handle long press detection for up button if (upDetected && upStartTime > 0) { uint32_t upDuration = now - upStartTime; if (!upButtonPressed) { // Button released if (upDuration < LONG_PRESS_DURATION && now - lastUpKeyTime >= updownDebounceMs) { lastUpKeyTime = now; e.inputEvent = this->_eventUp; } // Reset state upDetected = false; upStartTime = 0; lastUpLongEventTime = 0; } else if (upDuration >= LONG_PRESS_DURATION) { // Auto-repeat long press events if (lastUpLongEventTime == 0 || (now - lastUpLongEventTime) >= LONG_PRESS_REPEAT_INTERVAL) { e.inputEvent = this->_eventUpLong; lastUpLongEventTime = now; } } } // Handle long press detection for down button if (downDetected && downStartTime > 0) { uint32_t downDuration = now - downStartTime; if (!downButtonPressed) { // Button released if (downDuration < LONG_PRESS_DURATION && now - lastDownKeyTime >= updownDebounceMs) { lastDownKeyTime = now; e.inputEvent = this->_eventDown; } // Reset state downDetected = false; downStartTime = 0; lastDownLongEventTime = 0; } else if (downDuration >= LONG_PRESS_DURATION) { // Auto-repeat long press events if (lastDownLongEventTime == 0 || (now - lastDownLongEventTime) >= LONG_PRESS_REPEAT_INTERVAL) { e.inputEvent = this->_eventDownLong; lastDownLongEventTime = now; } } } if (e.inputEvent != INPUT_BROKER_NONE) { e.source = this->_originName; e.kbchar = INPUT_BROKER_NONE; this->notifyObservers(&e); } if (!pressDetected && !upDetected && !downDetected) { this->action = UPDOWN_ACTION_NONE; } return 20; // This will control how the input frequency } void UpDownInterruptBase::intPressHandler() { this->action = UPDOWN_ACTION_PRESSED; } void UpDownInterruptBase::intDownHandler() { this->action = UPDOWN_ACTION_DOWN; } void UpDownInterruptBase::intUpHandler() { this->action = UPDOWN_ACTION_UP; } ================================================ FILE: src/input/UpDownInterruptBase.h ================================================ #pragma once #include "InputBroker.h" #include "mesh/NodeDB.h" #ifndef UPDOWN_LONG_PRESS_DURATION #define UPDOWN_LONG_PRESS_DURATION 300 #endif #ifndef UPDOWN_LONG_PRESS_REPEAT_INTERVAL #define UPDOWN_LONG_PRESS_REPEAT_INTERVAL 300 #endif class UpDownInterruptBase : public Observable, public concurrency::OSThread { public: explicit UpDownInterruptBase(const char *name); void init(uint8_t pinDown, uint8_t pinUp, uint8_t pinPress, input_broker_event eventDown, input_broker_event eventUp, input_broker_event eventPressed, input_broker_event eventPressedLong, input_broker_event eventUpLong, input_broker_event eventDownLong, void (*onIntDown)(), void (*onIntUp)(), void (*onIntPress)(), unsigned long updownDebounceMs = 50); void intPressHandler(); void intDownHandler(); void intUpHandler(); int32_t runOnce() override; protected: enum UpDownInterruptBaseActionType { UPDOWN_ACTION_NONE, UPDOWN_ACTION_PRESSED, UPDOWN_ACTION_PRESSED_LONG, UPDOWN_ACTION_UP, UPDOWN_ACTION_UP_LONG, UPDOWN_ACTION_DOWN, UPDOWN_ACTION_DOWN_LONG }; volatile UpDownInterruptBaseActionType action = UPDOWN_ACTION_NONE; // Long press detection variables uint32_t pressStartTime = 0; uint32_t upStartTime = 0; uint32_t downStartTime = 0; bool pressDetected = false; bool upDetected = false; bool downDetected = false; uint32_t lastPressLongEventTime = 0; uint32_t lastUpLongEventTime = 0; uint32_t lastDownLongEventTime = 0; static const uint32_t LONG_PRESS_DURATION = UPDOWN_LONG_PRESS_DURATION; static const uint32_t LONG_PRESS_REPEAT_INTERVAL = UPDOWN_LONG_PRESS_REPEAT_INTERVAL; private: uint8_t _pinDown = 0; uint8_t _pinUp = 0; uint8_t _pinPress = 0; input_broker_event _eventDown = INPUT_BROKER_NONE; input_broker_event _eventUp = INPUT_BROKER_NONE; input_broker_event _eventPressed = INPUT_BROKER_NONE; input_broker_event _eventPressedLong = INPUT_BROKER_NONE; input_broker_event _eventUpLong = INPUT_BROKER_NONE; input_broker_event _eventDownLong = INPUT_BROKER_NONE; const char *_originName; unsigned long lastUpKeyTime = 0; unsigned long lastDownKeyTime = 0; unsigned long lastPressKeyTime = 0; unsigned long updownDebounceMs = 50; const unsigned long pressDebounceMs = 200; }; ================================================ FILE: src/input/UpDownInterruptImpl1.cpp ================================================ #include "UpDownInterruptImpl1.h" #include "InputBroker.h" extern bool osk_found; UpDownInterruptImpl1 *upDownInterruptImpl1; UpDownInterruptImpl1::UpDownInterruptImpl1() : UpDownInterruptBase("upDown1") {} bool UpDownInterruptImpl1::init() { #if defined(INPUTDRIVER_TWO_WAY_ROCKER) && defined(INPUTDRIVER_TWO_WAY_ROCKER_LEFT) && defined(INPUTDRIVER_TWO_WAY_ROCKER_RIGHT) moduleConfig.canned_message.updown1_enabled = true; moduleConfig.canned_message.inputbroker_pin_a = INPUTDRIVER_TWO_WAY_ROCKER_LEFT; moduleConfig.canned_message.inputbroker_pin_b = INPUTDRIVER_TWO_WAY_ROCKER_RIGHT; #if defined(INPUTDRIVER_TWO_WAY_ROCKER_BTN) moduleConfig.canned_message.inputbroker_pin_press = INPUTDRIVER_TWO_WAY_ROCKER_BTN; #endif #endif 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; input_broker_event eventDown = INPUT_BROKER_USER_PRESS; // acts like RIGHT/DOWN input_broker_event eventUp = INPUT_BROKER_ALT_PRESS; // acts like LEFT/UP input_broker_event eventPressed = INPUT_BROKER_SELECT; input_broker_event eventPressedLong = INPUT_BROKER_SELECT_LONG; input_broker_event eventUpLong = INPUT_BROKER_UP_LONG; input_broker_event eventDownLong = INPUT_BROKER_DOWN_LONG; UpDownInterruptBase::init(pinDown, pinUp, pinPress, eventDown, eventUp, eventPressed, eventPressedLong, eventUpLong, eventDownLong, UpDownInterruptImpl1::handleIntDown, UpDownInterruptImpl1::handleIntUp, UpDownInterruptImpl1::handleIntPressed); inputBroker->registerSource(this); #ifndef HAS_PHYSICAL_KEYBOARD osk_found = true; #endif return true; } void UpDownInterruptImpl1::handleIntDown() { upDownInterruptImpl1->intDownHandler(); } void UpDownInterruptImpl1::handleIntUp() { upDownInterruptImpl1->intUpHandler(); } void UpDownInterruptImpl1::handleIntPressed() { upDownInterruptImpl1->intPressHandler(); } ================================================ FILE: src/input/UpDownInterruptImpl1.h ================================================ #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; ================================================ FILE: src/input/cardKbI2cImpl.cpp ================================================ #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("Rescan for I2C keyboard"); uint8_t i2caddr_scan[] = {CARDKB_ADDR, TDECK_KB_ADDR, BBQ10_KB_ADDR, MPR121_KB_ADDR, TCA8418_KB_ADDR}; #if defined(T_LORA_PAGER) uint8_t i2caddr_asize = sizeof(i2caddr_scan) / sizeof(i2caddr_scan[0]); #else uint8_t i2caddr_asize = 5; #endif 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; case ScanI2C::DeviceType::TCA8418KB: // assign an arbitrary value to distinguish from other models kb_model = 0x84; break; default: // use this as default since it's also just zero LOG_WARN("kb_info.type is unknown(0x%02x), setting kb_model=0x00", kb_info.type); kb_model = 0x00; } } if (cardkb_found.address == 0x00) { disable(); return; } else { LOG_DEBUG("Keyboard Type: 0x%02x Model: 0x%02x Address: 0x%02x", kb_info.type, kb_model, cardkb_found.address); } } #else if (cardkb_found.address == 0x00) { disable(); return; } #endif inputBroker->registerSource(this); kb_found = true; } ================================================ FILE: src/input/cardKbI2cImpl.h ================================================ #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; ================================================ FILE: src/input/i2cButton.cpp ================================================ #include "i2cButton.h" #include "meshUtils.h" #include "configuration.h" #if defined(M5STACK_UNITC6L) #include "MeshService.h" #include "RadioLibInterface.h" #include "buzz.h" #include "input/InputBroker.h" #include "main.h" #include "modules/CannedMessageModule.h" #include "modules/ExternalNotificationModule.h" #include "power.h" #include "sleep.h" #ifdef ARCH_PORTDUINO #include "platform/portduino/PortduinoGlue.h" #endif i2cButtonThread *i2cButton; using namespace concurrency; extern void i2c_read_byte(uint8_t addr, uint8_t reg, uint8_t *value); extern void i2c_write_byte(uint8_t addr, uint8_t reg, uint8_t value); #define PI4IO_M_ADDR 0x43 #define getbit(x, y) ((x) >> (y)&0x01) #define PI4IO_REG_IRQ_STA 0x13 #define PI4IO_REG_IN_STA 0x0F #define PI4IO_REG_CHIP_RESET 0x01 i2cButtonThread::i2cButtonThread(const char *name) : OSThread(name) { _originName = name; if (inputBroker) inputBroker->registerSource(this); } int32_t i2cButtonThread::runOnce() { static bool btn1_pressed = false; static uint32_t press_start_time = 0; const uint32_t LONG_PRESS_TIME = 1000; static bool long_press_triggered = false; uint8_t in_data; i2c_read_byte(PI4IO_M_ADDR, PI4IO_REG_IRQ_STA, &in_data); i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_IRQ_STA, in_data); if (getbit(in_data, 0)) { uint8_t input_state; i2c_read_byte(PI4IO_M_ADDR, PI4IO_REG_IN_STA, &input_state); if (!getbit(input_state, 0)) { if (!btn1_pressed) { btn1_pressed = true; press_start_time = millis(); long_press_triggered = false; } } else { if (btn1_pressed) { btn1_pressed = false; uint32_t press_duration = millis() - press_start_time; if (long_press_triggered) { long_press_triggered = false; return 50; } if (press_duration < LONG_PRESS_TIME) { InputEvent evt; evt.source = "UserButton"; evt.inputEvent = INPUT_BROKER_USER_PRESS; evt.kbchar = 0; evt.touchX = 0; evt.touchY = 0; this->notifyObservers(&evt); } } } } if (btn1_pressed && !long_press_triggered && (millis() - press_start_time >= LONG_PRESS_TIME)) { long_press_triggered = true; InputEvent evt; evt.source = "UserButton"; evt.inputEvent = INPUT_BROKER_SELECT; evt.kbchar = 0; evt.touchX = 0; evt.touchY = 0; this->notifyObservers(&evt); } return 50; } #endif ================================================ FILE: src/input/i2cButton.h ================================================ #pragma once #include "InputBroker.h" #include "OneButton.h" #include "concurrency/OSThread.h" #include "configuration.h" #if defined(M5STACK_UNITC6L) class i2cButtonThread : public Observable, public concurrency::OSThread { public: const char *_originName; explicit i2cButtonThread(const char *name); int32_t runOnce() override; }; extern i2cButtonThread *i2cButton; #endif ================================================ FILE: src/input/kbI2cBase.cpp ================================================ #include "kbI2cBase.h" #include "configuration.h" #include "detect/ScanI2C.h" #include "detect/ScanI2CTwoWire.h" #if defined(T_DECK_PRO) #include "TDeckProKeyboard.h" #elif defined(T_LORA_PAGER) #include "TLoraPagerKeyboard.h" #elif defined(M5STACK_CARDPUTER_ADV) #include "CardputerKeyboard.h" #elif defined(HACKADAY_COMMUNICATOR) #include "HackadayCommunicatorKeyboard.h" #else #include "TCA8418Keyboard.h" #endif extern ScanI2C::DeviceAddress cardkb_found; extern uint8_t kb_model; KbI2cBase::KbI2cBase(const char *name) : concurrency::OSThread(name), #if defined(T_DECK_PRO) TCAKeyboard(*(new TDeckProKeyboard())) #elif defined(T_LORA_PAGER) TCAKeyboard(*(new TLoraPagerKeyboard())) #elif defined(M5STACK_CARDPUTER_ADV) TCAKeyboard(*(new CardputerKeyboard())) #elif defined(HACKADAY_COMMUNICATOR) TCAKeyboard(*(new HackadayCommunicatorKeyboard())) #else TCAKeyboard(*(new TCA8418Keyboard())) #endif { 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("Use I2C Bus 1 (the second one)"); i2cBus = &Wire1; if (cardkb_found.address == BBQ10_KB_ADDR) { Q10keyboard.begin(BBQ10_KB_ADDR, &Wire1); Q10keyboard.setBacklight(0); } if (cardkb_found.address == MPR121_KB_ADDR) { MPRkeyboard.begin(MPR121_KB_ADDR, &Wire1); } if (cardkb_found.address == TCA8418_KB_ADDR) { TCAKeyboard.begin(TCA8418_KB_ADDR, &Wire1); } break; #endif case ScanI2C::WIRE: LOG_DEBUG("Use I2C Bus 0 (the first one)"); i2cBus = &Wire; if (cardkb_found.address == BBQ10_KB_ADDR) { Q10keyboard.begin(BBQ10_KB_ADDR, &Wire); Q10keyboard.setBacklight(0); } if (cardkb_found.address == MPR121_KB_ADDR) { MPRkeyboard.begin(MPR121_KB_ADDR, &Wire); } if (cardkb_found.address == TCA8418_KB_ADDR) { TCAKeyboard.begin(TCA8418_KB_ADDR, &Wire); } break; case ScanI2C::NO_I2C: default: i2cBus = 0; } } switch (kb_model) { case 0x11: { // BB Q10 int keyCount = Q10keyboard.keyCount(); while (keyCount--) { const BBQ10Keyboard::KeyEvent key = Q10keyboard.keyEvent(); if ((key.key != 0x00) && (key.state == BBQ10Keyboard::StateRelease)) { InputEvent e = {}; e.inputEvent = INPUT_BROKER_NONE; e.source = this->_originName; switch (key.key) { case 'p': // TAB case 't': // TAB as well if (is_sym) { e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = 0x09; // TAB Scancode is_sym = false; // reset sym state after second keypress } else { e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = key.key; } break; case 'q': // ESC if (is_sym) { e.inputEvent = INPUT_BROKER_CANCEL; e.kbchar = 0; is_sym = false; // reset sym state after second keypress } else { e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = key.key; } break; case 0x08: // Back e.inputEvent = INPUT_BROKER_BACK; e.kbchar = key.key; break; case 'e': // sym e if (is_sym) { e.inputEvent = INPUT_BROKER_UP; e.kbchar = INPUT_BROKER_UP; is_sym = false; // reset sym state after second keypress } else { e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = key.key; } break; case 'x': // sym x if (is_sym) { e.inputEvent = INPUT_BROKER_DOWN; e.kbchar = 0; is_sym = false; // reset sym state after second keypress } else { e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = key.key; } break; case 's': // sym s if (is_sym) { e.inputEvent = INPUT_BROKER_LEFT; e.kbchar = 0x00; // tweak for destSelect is_sym = false; // reset sym state after second keypress } else { e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = key.key; } break; case 'f': // sym f if (is_sym) { e.inputEvent = INPUT_BROKER_RIGHT; e.kbchar = 0x00; // tweak for destSelect is_sym = false; // reset sym state after second keypress } else { e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = key.key; } break; case 0x13: // Code scanner says the SYM key is 0x13 is_sym = !is_sym; e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = is_sym ? INPUT_BROKER_MSG_FN_SYMBOL_ON // send 0xf1 to tell CannedMessages to display that : INPUT_BROKER_MSG_FN_SYMBOL_OFF; // the modifier key is active break; case 0x0a: // apparently Enter on Q10 is a line feed instead of carriage return e.inputEvent = INPUT_BROKER_SELECT; break; case 0x00: // nopress e.inputEvent = INPUT_BROKER_NONE; break; default: // all other keys e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = key.key; is_sym = false; // reset sym state after second keypress break; } if (e.inputEvent != INPUT_BROKER_NONE) { this->notifyObservers(&e); } } } break; } case 0x37: { // MPR121 MPRkeyboard.trigger(); InputEvent e = {}; while (MPRkeyboard.hasEvent()) { char nextEvent = MPRkeyboard.dequeueEvent(); e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = 0x00; e.source = this->_originName; switch (nextEvent) { case 0x00: // MPR121_NONE e.inputEvent = INPUT_BROKER_NONE; e.kbchar = 0x00; break; case 0x90: // MPR121_REBOOT e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = INPUT_BROKER_MSG_REBOOT; break; case 0xb4: // MPR121_LEFT e.inputEvent = INPUT_BROKER_LEFT; e.kbchar = 0x00; break; case 0xb5: // MPR121_UP e.inputEvent = INPUT_BROKER_UP; e.kbchar = 0x00; break; case 0xb6: // MPR121_DOWN e.inputEvent = INPUT_BROKER_DOWN; e.kbchar = 0x00; break; case 0xb7: // MPR121_RIGHT e.inputEvent = INPUT_BROKER_RIGHT; e.kbchar = 0x00; break; case 0x1b: // MPR121_ESC e.inputEvent = INPUT_BROKER_CANCEL; e.kbchar = 0; break; case 0x08: // MPR121_BSP e.inputEvent = INPUT_BROKER_BACK; e.kbchar = 0x08; break; case 0x0d: // MPR121_SELECT e.inputEvent = INPUT_BROKER_SELECT; e.kbchar = 0x00; break; default: if (nextEvent > 127) { e.inputEvent = INPUT_BROKER_NONE; e.kbchar = 0x00; break; } e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = nextEvent; break; } if (e.inputEvent != INPUT_BROKER_NONE) { LOG_DEBUG("MP121 Notifying: %i Char: %i", e.inputEvent, e.kbchar); this->notifyObservers(&e); } } break; } case 0x84: { // Adafruit TCA8418 TCAKeyboard.trigger(); InputEvent e = {}; while (TCAKeyboard.hasEvent()) { char nextEvent = TCAKeyboard.dequeueEvent(); e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = 0x00; e.source = this->_originName; switch (nextEvent) { case TCA8418KeyboardBase::NONE: e.inputEvent = INPUT_BROKER_NONE; e.kbchar = 0x00; break; case TCA8418KeyboardBase::REBOOT: e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = INPUT_BROKER_MSG_REBOOT; break; case TCA8418KeyboardBase::LEFT: e.inputEvent = INPUT_BROKER_LEFT; e.kbchar = 0x00; break; case TCA8418KeyboardBase::UP: e.inputEvent = INPUT_BROKER_UP; e.kbchar = 0x00; break; case TCA8418KeyboardBase::DOWN: e.inputEvent = INPUT_BROKER_DOWN; e.kbchar = 0x00; break; case TCA8418KeyboardBase::RIGHT: e.inputEvent = INPUT_BROKER_RIGHT; e.kbchar = 0x00; break; case TCA8418KeyboardBase::BSP: e.inputEvent = INPUT_BROKER_BACK; e.kbchar = 0x08; break; case TCA8418KeyboardBase::SELECT: e.inputEvent = INPUT_BROKER_SELECT; e.kbchar = 0x00; break; case TCA8418KeyboardBase::ESC: e.inputEvent = INPUT_BROKER_CANCEL; e.kbchar = 0x00; break; case TCA8418KeyboardBase::GPS_TOGGLE: e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = INPUT_BROKER_GPS_TOGGLE; break; case TCA8418KeyboardBase::SEND_PING: e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = INPUT_BROKER_SEND_PING; break; case TCA8418KeyboardBase::MUTE_TOGGLE: e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = INPUT_BROKER_MSG_MUTE_TOGGLE; break; case TCA8418KeyboardBase::BT_TOGGLE: e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = INPUT_BROKER_MSG_BLUETOOTH_TOGGLE; break; case TCA8418KeyboardBase::BL_TOGGLE: e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = INPUT_BROKER_MSG_BLUETOOTH_TOGGLE; break; case TCA8418KeyboardBase::TAB: e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = INPUT_BROKER_MSG_TAB; break; case TCA8418KeyboardBase::FUNCTION_F1: e.inputEvent = INPUT_BROKER_FN_F1; e.kbchar = 0x00; break; case TCA8418KeyboardBase::FUNCTION_F2: e.inputEvent = INPUT_BROKER_FN_F2; e.kbchar = 0x00; break; case TCA8418KeyboardBase::FUNCTION_F3: e.inputEvent = INPUT_BROKER_FN_F3; e.kbchar = 0x00; break; case TCA8418KeyboardBase::FUNCTION_F4: e.inputEvent = INPUT_BROKER_FN_F4; e.kbchar = 0x00; break; case TCA8418KeyboardBase::FUNCTION_F5: e.inputEvent = INPUT_BROKER_FN_F5; e.kbchar = 0x00; break; default: if (nextEvent > 127) { e.inputEvent = INPUT_BROKER_NONE; e.kbchar = 0x00; break; } e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = nextEvent; break; } if (e.inputEvent != INPUT_BROKER_NONE) { // LOG_DEBUG("TCA8418 Notifying: %i Char: %c", e.inputEvent, e.kbchar); this->notifyObservers(&e); } TCAKeyboard.trigger(); } TCAKeyboard.clearInt(); break; } case 0x02: { // RAK14004 uint8_t rDataBuf[8] = {0}; uint8_t PrintDataBuf = 0; if (read_from_14004(i2cBus, 0x01, rDataBuf, 0x04) == 1) { for (uint8_t aCount = 0; aCount < 0x04; aCount++) { for (uint8_t bCount = 0; bCount < 0x04; bCount++) { if (((rDataBuf[aCount] >> bCount) & 0x01) == 0x01) { PrintDataBuf = aCount * 0x04 + bCount + 1; } } } } if (PrintDataBuf != 0) { LOG_DEBUG("RAK14004 key 0x%x pressed", PrintDataBuf); InputEvent e = {}; e.inputEvent = INPUT_BROKER_MATRIXKEY; e.source = this->_originName; e.kbchar = PrintDataBuf; this->notifyObservers(&e); } break; } case 0x00: // CARDKB case 0x10: { // T-DECK i2cBus->requestFrom((int)cardkb_found.address, 1); if (i2cBus->available()) { char c = i2cBus->read(); InputEvent e = {}; e.inputEvent = INPUT_BROKER_NONE; e.source = this->_originName; switch (c) { case 0x71: // This is the button q. If modifier and q pressed, it cancels the input if (is_sym) { is_sym = false; e.inputEvent = INPUT_BROKER_CANCEL; } else { e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = c; } break; case 0x74: // letter t. if modifier and t pressed call 'tab' if (is_sym) { is_sym = false; e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = 0x09; // TAB Scancode } else { e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = c; } break; case 0x6d: // letter m. Modifier makes it mute notifications if (is_sym) { is_sym = false; e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = INPUT_BROKER_MSG_MUTE_TOGGLE; // mute notifications } else { e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = c; } break; case 0x6f: // letter o(+). Modifier makes screen increase in brightness if (is_sym) { is_sym = false; e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = INPUT_BROKER_MSG_BRIGHTNESS_UP; // Increase Brightness code } else { e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = c; } break; case 0x69: // letter i(-). Modifier makes screen decrease in brightness if (is_sym) { is_sym = false; e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = INPUT_BROKER_MSG_BRIGHTNESS_DOWN; // Decrease Brightness code } else { e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = c; } break; case 0x20: // Space. Send network ping like double press does if (is_sym) { is_sym = false; e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = INPUT_BROKER_SEND_PING; // (fn + space) } else { e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = c; } break; case 0x67: // letter g. toggle gps if (is_sym) { is_sym = false; e.inputEvent = INPUT_BROKER_GPS_TOGGLE; e.kbchar = INPUT_BROKER_GPS_TOGGLE; } else { e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = c; } break; case 0x1b: // ESC e.inputEvent = INPUT_BROKER_CANCEL; break; case 0x08: // Back e.inputEvent = INPUT_BROKER_BACK; e.kbchar = 0; break; case 0xb5: // Up e.inputEvent = INPUT_BROKER_UP; e.kbchar = 0; break; case 0xb6: // Down e.inputEvent = INPUT_BROKER_DOWN; e.kbchar = 0; break; case 0xb4: // Left e.inputEvent = INPUT_BROKER_LEFT; e.kbchar = 0; break; case 0xb7: // Right e.inputEvent = INPUT_BROKER_RIGHT; e.kbchar = 0; break; case 0xc: // Modifier key: 0xc is alt+c (Other options could be: 0xea = shift+mic button or 0x4 shift+$(speaker)) // toggle modifiers button. is_sym = !is_sym; e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = is_sym ? INPUT_BROKER_MSG_FN_SYMBOL_ON // send 0xf1 to tell CannedMessages to display that the : INPUT_BROKER_MSG_FN_SYMBOL_OFF; // modifier key is active break; case 0x9e: // fn+g INPUT_BROKER_GPS_TOGGLE e.inputEvent = INPUT_BROKER_GPS_TOGGLE; e.kbchar = c; break; case 0xaf: // fn+space INPUT_BROKER_SEND_PING e.inputEvent = INPUT_BROKER_SEND_PING; e.kbchar = c; break; case 0x9b: // fn+s INPUT_BROKER_MSG_SHUTDOWN e.inputEvent = INPUT_BROKER_SHUTDOWN; e.kbchar = c; break; case 0x90: // fn+r INPUT_BROKER_MSG_REBOOT case 0x91: // fn+t case 0xac: // fn+m INPUT_BROKER_MSG_MUTE_TOGGLE case 0xAA: // fn+b INPUT_BROKER_MSG_BLUETOOTH_TOGGLE case 0x8F: // fn+e INPUT_BROKER_MSG_EMOTE_LIST // just pass those unmodified e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = c; break; case 0x0d: // Enter e.inputEvent = INPUT_BROKER_SELECT; break; case 0x00: // nopress e.inputEvent = INPUT_BROKER_NONE; break; default: // all other keys if (c > 127) { // bogus key value e.inputEvent = INPUT_BROKER_NONE; break; } e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = c; is_sym = false; break; } if (e.inputEvent != INPUT_BROKER_NONE) { this->notifyObservers(&e); } } break; } default: LOG_WARN("Unknown kb_model 0x%02x", kb_model); } return 300; } void KbI2cBase::toggleBacklight(bool on) { #if defined(T_LORA_PAGER) TCAKeyboard.setBacklight(on); #endif } ================================================ FILE: src/input/kbI2cBase.h ================================================ #pragma once #include "BBQ10Keyboard.h" #include "InputBroker.h" #include "MPR121Keyboard.h" #include "Wire.h" #include "concurrency/OSThread.h" class TCA8418KeyboardBase; class KbI2cBase : public Observable, public concurrency::OSThread { public: explicit KbI2cBase(const char *name); void toggleBacklight(bool on); protected: virtual int32_t runOnce() override; private: const char *_originName; TwoWire *i2cBus = 0; BBQ10Keyboard Q10keyboard; MPR121Keyboard MPRkeyboard; TCA8418KeyboardBase &TCAKeyboard; bool is_sym = false; }; ================================================ FILE: src/input/kbMatrixBase.cpp ================================================ #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 = INPUT_BROKER_NONE; e.source = this->_originName; switch (key) { case 0x1b: // ESC e.inputEvent = INPUT_BROKER_CANCEL; break; case 0x08: // Back e.inputEvent = INPUT_BROKER_BACK; e.kbchar = 0; break; case 0xb5: // Up e.inputEvent = INPUT_BROKER_UP; break; case 0xb6: // Down e.inputEvent = INPUT_BROKER_DOWN; break; case 0xb4: // Left e.inputEvent = INPUT_BROKER_LEFT; e.kbchar = 0; break; case 0xb7: // Right e.inputEvent = INPUT_BROKER_RIGHT; e.kbchar = 0; break; case 0x0d: // Enter e.inputEvent = INPUT_BROKER_SELECT; break; case 0x00: // nopress e.inputEvent = INPUT_BROKER_NONE; break; case 0x1a: // Shift shift++; if (shift > 2) { shift = 0; } break; default: // all other keys e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = key; break; } if (e.inputEvent != INPUT_BROKER_NONE) { this->notifyObservers(&e); } } prevkey = key; } } else { LOG_WARN("Unknown kb_model 0x%02x", INPUTBROKER_MATRIX_TYPE); return disable(); } return 50; // Keyscan every 50msec to avoid key bounce } #endif // INPUTBROKER_MATRIX_TYPE ================================================ FILE: src/input/kbMatrixBase.h ================================================ #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; }; ================================================ FILE: src/input/kbMatrixImpl.cpp ================================================ #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 ================================================ FILE: src/input/kbMatrixImpl.h ================================================ #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; ================================================ FILE: src/main.cpp ================================================ #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 "RadioLibInterface.h" #include "ReliableRouter.h" #include "TransmitHistory.h" #include "airtime.h" #include "buzz.h" #include "power/PowerHAL.h" #include "FSCommon.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/ScanI2CConsumer.h" #include "detect/ScanI2CTwoWire.h" #include #endif #include "detect/einkScan.h" #include "graphics/Screen.h" #include "main.h" #include "mesh/generated/meshtastic/config.pb.h" #include "meshUtils.h" #include "modules/Modules.h" #include "sleep.h" #include "target_specific.h" #include #include #if HAS_SCREEN #include "MessageStore.h" #endif #ifdef ARCH_ESP32 #include "freertosinc.h" #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 || defined(USE_WS5500) #include "mesh/api/WiFiServerAPI.h" #include "mesh/wifi/WiFiAPClient.h" #endif #if HAS_ETHERNET && !defined(USE_WS5500) #include "mesh/api/ethServerAPI.h" #include "mesh/eth/ethClient.h" #endif #if !MESHTASTIC_EXCLUDE_MQTT #include "mqtt/MQTT.h" #endif #ifdef ARCH_PORTDUINO #include "linux/LinuxHardwareI2C.h" #include "mesh/raspihttp/PiWebServer.h" #include "platform/portduino/PortduinoGlue.h" #include #include #include #include #endif #ifdef ARCH_ESP32 #ifdef DEBUG_PARTITION_TABLE #include "esp_partition.h" void printPartitionTable() { printf("\n--- Partition Table ---\n"); // Print Column Headers printf("| %-16s | %-4s | %-7s | %-10s | %-10s |\n", "Label", "Type", "Subtype", "Offset", "Size"); printf("|------------------|------|---------|------------|------------|\n"); // Create an iterator to find ALL partitions (Type ANY, Subtype ANY) esp_partition_iterator_t it = esp_partition_find(ESP_PARTITION_TYPE_ANY, ESP_PARTITION_SUBTYPE_ANY, NULL); // Loop through the iterator if (it != NULL) { do { const esp_partition_t *part = esp_partition_get(it); // Print details: Label, Type (Hex), Subtype (Hex), Offset (Hex), Size (Hex) printf("| %-16s | 0x%02x | 0x%02x | 0x%08x | 0x%08x |\n", part->label, part->type, part->subtype, part->address, part->size); // Move to next partition it = esp_partition_next(it); } while (it != NULL); // Release the iterator memory esp_partition_iterator_release(it); } else { printf("No partitions found.\n"); } printf("-----------------------\n"); } #endif // DEBUG_PARTITION_TABLE #endif // ARCH_ESP32 #include "AmbientLightingThread.h" #include "PowerFSMThread.h" #if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C #include "motion/AccelerometerThread.h" AccelerometerThread *accelerometerThread = nullptr; #endif #ifdef HAS_I2S #include "AudioThread.h" AudioThread *audioThread = nullptr; #endif #ifdef USE_XL9555 #include "ExtensionIOXL9555.hpp" ExtensionIOXL9555 io; #endif #if HAS_TFT extern void tftSetup(void); #endif #ifdef HAS_UDP_MULTICAST #include "mesh/udp/UdpMulticastHandler.h" UdpMulticastHandler *udpHandler = 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 #ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS void setupNicheGraphics(); #include "nicheGraphics.h" #endif #if defined(HW_SPI1_DEVICE) && defined(ARCH_ESP32) SPIClass SPI1(HSPI); #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(); // Global Bluetooth status meshtastic::BluetoothStatus *bluetoothStatus = new meshtastic::BluetoothStatus(); // 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; // global bool to record that a kb is present bool kb_found = false; // global bool to record that on-screen keyboard (OSK) is present bool osk_found = false; // 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); /// The I2C address of our Air Quality Indicator (if found) ScanI2C::DeviceAddress aqi_found = ScanI2C::ADDRESS_NONE; #ifdef HAS_DRV2605 Adafruit_DRV2605 drv; #endif 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 *firmware_version = optstr(APP_VERSION_SHORT); 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; } uint32_t timeLastPowered = 0; static OSThread *powerFSMthread; OSThread *ambientLightingThread; RadioLibHal *RadioLibHAL = 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() {} void earlyInitVariant() __attribute__((weak)); void earlyInitVariant() {} // NRF52 (and probably other platforms) can report when system is in power failure mode // (eg. too low battery voltage) and operating it is unsafe (data corruption, bootloops, etc). // For example NRF52 will prevent any flash writes in that case automatically // (but it causes issues we need to handle). // This detection is independent from whatever ADC or dividers used in Meshtastic // boards and is internal to chip. // we use powerHAL layer to get this info and delay booting until power level is safe // wait until power level is safe to continue booting (to avoid bootloops) // blink user led in 3 flashes sequence to indicate what is happening void waitUntilPowerLevelSafe() { while (powerHAL_isPowerLevelSafe() == false) { #ifdef LED_POWER // 3x: blink for 300 ms, pause for 300 ms for (int i = 0; i < 3; i++) { digitalWrite(LED_POWER, LED_STATE_ON); delay(300); digitalWrite(LED_POWER, LED_STATE_OFF); delay(300); } #endif // sleep for 2s delay(2000); } } /** * Print info as a structured log message (for automated log processing) */ void printInfo() { LOG_INFO("S:B:%d,%s,%s,%s", HW_VENDOR, optstr(APP_VERSION), optstr(APP_ENV), optstr(APP_REPO)); } #ifndef PIO_UNIT_TESTING void setup() { // initialize power HAL layer as early as possible powerHAL_init(); #ifdef LED_POWER pinMode(LED_POWER, OUTPUT); digitalWrite(LED_POWER, LED_STATE_ON); #endif // prevent booting if device is in power failure mode // boot sequence will follow when battery level raises to safe mode waitUntilPowerLevelSafe(); // Defined in variant.cpp for early init code earlyInitVariant(); #if defined(PIN_POWER_EN) pinMode(PIN_POWER_EN, OUTPUT); digitalWrite(PIN_POWER_EN, HIGH); #endif #ifdef LED_NOTIFICATION pinMode(LED_NOTIFICATION, OUTPUT); digitalWrite(LED_NOTIFICATION, HIGH ^ LED_STATE_ON); #endif #ifdef WIFI_LED pinMode(WIFI_LED, OUTPUT); digitalWrite(WIFI_LED, LOW); #endif #ifdef BLE_LED pinMode(BLE_LED, OUTPUT); digitalWrite(BLE_LED, LED_STATE_OFF); #endif concurrency::hasBeenSetup = true; #if HAS_SCREEN meshtastic_Config_DisplayConfig_OledType screen_model = meshtastic_Config_DisplayConfig_OledType::meshtastic_Config_DisplayConfig_OledType_OLED_AUTO; #endif 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 RTCQuality ourQuality = RTCQualityDevice; std::string timeCommandResult = exec("timedatectl status | grep synchronized | grep yes -c"); if (timeCommandResult[0] == '1') { ourQuality = RTCQualityNTP; } struct timeval tv; tv.tv_sec = time(NULL); tv.tv_usec = 0; perhapsSetRTC(ourQuality, &tv); #endif powerMonInit(); serialSinceMsec = millis(); LOG_INFO("\n\n//\\ E S H T /\\ S T / C\n"); #if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM) #ifndef SENSECAP_INDICATOR // use PSRAM for malloc calls > 2048 bytes heap_caps_malloc_extmem_enable(2048); #endif #endif #if defined(DEBUG_MUTE) && defined(DEBUG_PORT) DEBUG_PORT.printf("\r\n\r\n//\\ E S H T /\\ S T / C\r\n"); DEBUG_PORT.printf("Version %s for %s from %s\r\n", optstr(APP_VERSION), optstr(APP_ENV), optstr(APP_REPO)); DEBUG_PORT.printf("Debug mute is enabled, there will be no serial output.\r\n"); #endif initDeepSleep(); #if defined(MODEM_POWER_EN) pinMode(MODEM_POWER_EN, OUTPUT); digitalWrite(MODEM_POWER_EN, LOW); #endif #if defined(MODEM_PWRKEY) pinMode(MODEM_PWRKEY, OUTPUT); digitalWrite(MODEM_PWRKEY, LOW); #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); delay(2); digitalWrite(RESET_OLED, 0); delay(10); digitalWrite(RESET_OLED, 1); #endif #ifdef SENSOR_POWER_CTRL_PIN pinMode(SENSOR_POWER_CTRL_PIN, OUTPUT); digitalWrite(SENSOR_POWER_CTRL_PIN, SENSOR_POWER_ON); #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("Wait for peripherals to stabilize"); delay(PERIPHERAL_WARMUP_MS); #endif initSPI(); OSThread::setup(); fsInit(); #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) LOG_INFO("Starting Bus with (SDA) %d and (SCL) %d: ", I2C_SDA, I2C_SCL); Wire.begin(I2C_SDA, I2C_SCL); #elif defined(ARCH_PORTDUINO) if (portduino_config.i2cdev != "") { LOG_INFO("Use %s as I2C device", portduino_config.i2cdev.c_str()); Wire.begin(portduino_config.i2cdev.c_str()); } else { LOG_INFO("No I2C device configured, Skip"); } #elif HAS_WIRE Wire.begin(); #endif #endif #if defined(M5STACK_UNITC6L) pinMode(LORA_CS, OUTPUT); digitalWrite(LORA_CS, 1); c6l_init(); #endif #ifdef PIN_LCD_RESET // FIXME - move this someplace better, LCD is at address 0x3F pinMode(PIN_LCD_RESET, OUTPUT); digitalWrite(PIN_LCD_RESET, 0); delay(1); digitalWrite(PIN_LCD_RESET, 1); delay(1); #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 // 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("Scan for i2c devices"); #endif #if defined(I2C_SDA1) || (defined(NRF52840_XXAA) && (WIRE_INTERFACES_COUNT == 2)) i2cScanner->scanPort(ScanI2C::I2CPort::WIRE1); #endif #if defined(I2C_SDA) i2cScanner->scanPort(ScanI2C::I2CPort::WIRE); #elif defined(ARCH_PORTDUINO) if (portduino_config.i2cdev != "") { LOG_INFO("Scan for i2c devices"); i2cScanner->scanPort(ScanI2C::I2CPort::WIRE); } #elif 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 #ifdef DEBUG_PARTITION_TABLE printPartitionTable(); #endif #endif // ARCH_ESP32 #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 #if HAS_SCREEN 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; } } #endif #define UPDATE_FROM_SCANNER(FIND_FN) #if defined(USE_VIRTUAL_KEYBOARD) kb_found = true; #endif auto rtc_info = i2cScanner->firstRTC(); rtc_found = rtc_info.type != ScanI2C::DeviceType::NONE ? rtc_info.address : rtc_found; auto kb_info = i2cScanner->firstKeyboard(); if (kb_info.type != ScanI2C::DeviceType::NONE) { kb_found = true; cardkb_found = kb_info.address; switch (kb_info.type) { case ScanI2C::DeviceType::RAK14004: kb_model = 0x02; break; case ScanI2C::DeviceType::CARDKB: kb_model = 0x00; break; case ScanI2C::DeviceType::TDECKKB: // assign an arbitrary value to distinguish from other models kb_model = 0x10; break; case ScanI2C::DeviceType::BBQ10KB: // assign an arbitrary value to distinguish from other models kb_model = 0x11; break; case ScanI2C::DeviceType::MPR121KB: // assign an arbitrary value to distinguish from other models kb_model = 0x37; break; case ScanI2C::DeviceType::TCA8418KB: // assign an arbitrary value to distinguish from other models kb_model = 0x84; break; default: // use this as default since it's also just zero LOG_WARN("kb_info.type is unknown(0x%02x), setting kb_model=0x00", kb_info.type); kb_model = 0x00; } } pmu_found = i2cScanner->exists(ScanI2C::DeviceType::PMU_AXP192_AXP2101); auto aqiInfo = i2cScanner->firstAQI(); aqi_found = aqiInfo.type != ScanI2C::DeviceType::NONE ? aqiInfo.address : ScanI2C::ADDRESS_NONE; /* * 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". */ // Two supported RGB LED currently #ifdef HAS_RGB_LED rgb_found = i2cScanner->firstRGBLED(); #endif #ifdef HAS_TPS65233 // TPS65233 is a power management IC for satellite modems, used in the Dreamcatcher // We are switching it off here since we don't use an LNB. if (i2cScanner->exists(ScanI2C::DeviceType::TPS65233)) { Wire.beginTransmission(TPS65233_ADDR); Wire.write(0); // Register 0 Wire.write(128); // Turn off the LNB power, keep I2C Control enabled Wire.endTransmission(); Wire.beginTransmission(TPS65233_ADDR); Wire.write(1); // Register 1 Wire.write(0); // Turn off Tone Generator 22kHz Wire.endTransmission(); } #endif #if !defined(ARCH_STM32WL) auto acc_info = i2cScanner->firstAccelerometer(); accelerometer_found = acc_info.type != ScanI2C::DeviceType::NONE ? acc_info.address : accelerometer_found; LOG_DEBUG("acc_info = %i", acc_info.type); #endif scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::INA260, meshtastic_TelemetrySensorType_INA260); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::INA226, meshtastic_TelemetrySensorType_INA226); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::INA219, meshtastic_TelemetrySensorType_INA219); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::INA3221, meshtastic_TelemetrySensorType_INA3221); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::MAX17048, meshtastic_TelemetrySensorType_MAX17048); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::QMC6310U, meshtastic_TelemetrySensorType_QMC6310); // TODO: Types need to be added meshtastic_TelemetrySensorType_QMC6310N // scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::QMC6310N, meshtastic_TelemetrySensorType_QMC6310N); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::QMI8658, meshtastic_TelemetrySensorType_QMI8658); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::QMC5883L, meshtastic_TelemetrySensorType_QMC5883L); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::HMC5883L, meshtastic_TelemetrySensorType_QMC5883L); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::MLX90614, meshtastic_TelemetrySensorType_MLX90614); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::ICM20948, meshtastic_TelemetrySensorType_ICM20948); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::MAX30102, meshtastic_TelemetrySensorType_MAX30102); #endif #ifdef HAS_SDCARD setupSDCard(); #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 // 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; // Initialize transmit history to persist broadcast throttle timers across reboots TransmitHistory::getInstance()->loadFromDisk(); #if HAS_TFT if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { tftSetup(); } #endif router = new ReliableRouter(); // only play start melody when role is not tracker or sensor if (config.power.is_power_saving == true && IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_TRACKER, meshtastic_Config_DeviceConfig_Role_TAK_TRACKER, meshtastic_Config_DeviceConfig_Role_SENSOR)) LOG_DEBUG("Tracker/Sensor: Skip start melody"); else playStartMelody(); #if HAS_SCREEN // fixed screen override? #if defined(USE_SH1107) screen_model = meshtastic_Config_DisplayConfig_OledType_OLED_SH1107; // set dimension of 128x128 screen_geometry = GEOMETRY_128_128; #elif defined(USE_SH1107_128_64) screen_model = meshtastic_Config_DisplayConfig_OledType_OLED_SH1107; // keep dimension of 128x64 #else if (config.display.oled != meshtastic_Config_DisplayConfig_OledType_OLED_AUTO) screen_model = config.display.oled; #endif #endif #if !MESHTASTIC_EXCLUDE_I2C #if !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 HAS_DRV2605 #if defined(PIN_DRV_EN) pinMode(PIN_DRV_EN, OUTPUT); digitalWrite(PIN_DRV_EN, HIGH); delay(10); #endif 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 ARCH_PORTDUINO if (portduino_config.lora_spi_dev != "ch341") { SPI.begin(); } #elif !defined(ARCH_ESP32) // ARCH_RP2040 #if defined(RAK3401) || defined(RAK13302) pinMode(WB_IO2, OUTPUT); digitalWrite(WB_IO2, HIGH); SPI1.setPins(LORA_MISO, LORA_SCK, LORA_MOSI); SPI1.begin(); #else SPI.begin(); #endif #else // ESP32 #if defined(HW_SPI1_DEVICE) SPI1.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_CS); LOG_DEBUG("SPI1.begin(SCK=%d, MISO=%d, MOSI=%d, NSS=%d)", LORA_SCK, LORA_MISO, LORA_MOSI, LORA_CS); SPI1.setFrequency(4000000); #else SPI.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_CS); LOG_DEBUG("SPI.begin(SCK=%d, MISO=%d, MOSI=%d, NSS=%d)", LORA_SCK, LORA_MISO, LORA_MOSI, LORA_CS); SPI.setFrequency(4000000); #endif #endif // Initialize the screen first so we can show the logo while we start up everything else. #if HAS_SCREEN if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { #if defined(ST7701_CS) || defined(ST7735_CS) || defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || \ defined(ST7789_CS) || defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(ST7796_CS) || \ defined(USE_SPISSD1306) || defined(USE_ST7796) || defined(HACKADAY_COMMUNICATOR) screen = new graphics::Screen(screen_found, screen_model, screen_geometry); #elif defined(ARCH_PORTDUINO) if ((screen_found.port != ScanI2C::I2CPort::NO_I2C || portduino_config.displayPanel) && config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { screen = new graphics::Screen(screen_found, screen_model, screen_geometry); } #else if (screen_found.port != ScanI2C::I2CPort::NO_I2C) screen = new graphics::Screen(screen_found, screen_model, screen_geometry); #endif } #endif // HAS_SCREEN // TODO Remove magic string // setup TZ prior to time actions. #if !MESHTASTIC_EXCLUDE_TZ LOG_DEBUG("Use compiled/slipstreamed %s", slipstreamTZString); // important, removing this clobbers our magic string if (*config.device.tzdef && config.device.tzdef[0] != 0) { LOG_DEBUG("Saved TZ: %s ", config.device.tzdef); setenv("TZ", config.device.tzdef, 1); } else { if (strncmp((const char *)slipstreamTZString, "tzpl", 4) == 0) { setenv("TZ", "GMT0", 1); } 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.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT) { gps = GPS::createGps(); if (gps) { gpsStatus->observe(&gps->newStatus); } else { LOG_DEBUG("Run without GPS"); } } } #ifdef SENSOR_GPS_CONFLICT } #endif #endif nodeStatus->observe(&nodeDB->newStatus); #ifdef HAS_I2S LOG_DEBUG("Start audio thread"); audioThread = new AudioThread(); #endif #ifdef HAS_UDP_MULTICAST LOG_DEBUG("Start multicast thread"); udpHandler = new UdpMulticastHandler(); #ifdef ARCH_PORTDUINO // FIXME: portduino does not ever call onNetworkConnected so call it here because I don't know what happen if I call // onNetworkConnected there if (config.network.enabled_protocols & meshtastic_Config_NetworkConfig_ProtocolFlags_UDP_BROADCAST) { udpHandler->start(); } #endif #endif service = new MeshService(); service->init(); // Set osk_found for trackball/encoder devices BEFORE setupModules so CannedMessageModule can detect it #if defined(HAS_TRACKBALL) || (defined(INPUTDRIVER_ENCODER_TYPE) && INPUTDRIVER_ENCODER_TYPE == 2) #ifndef HAS_PHYSICAL_KEYBOARD osk_found = true; #endif #endif // Now that the mesh service is created, create any modules setupModules(); #if !MESHTASTIC_EXCLUDE_I2C // Inform modules about I2C devices ScanI2CCompleted(i2cScanner.get()); i2cScanner.reset(); #endif #if !defined(MESHTASTIC_EXCLUDE_PKI) // warn the user about a low entropy key if (nodeDB->keyIsLowEntropy && !nodeDB->hasWarned) { LOG_WARN(LOW_ENTROPY_WARNING); meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); cn->level = meshtastic_LogRecord_Level_WARNING; cn->time = getValidTime(RTCQualityFromNet); sprintf(cn->message, LOW_ENTROPY_WARNING); service->sendClientNotification(cn); nodeDB->hasWarned = true; } #endif #if !MESHTASTIC_EXCLUDE_INPUTBROKER if (inputBroker) inputBroker->Init(); #endif #ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS // After modules are setup, so we can observe modules setupNicheGraphics(); #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) || defined(ILI9488_CS) || defined(ST7796_CS) || \ defined(USE_ST7796) || defined(USE_SPISSD1306) || defined(HACKADAY_COMMUNICATOR) if (screen) screen->setup(); #elif defined(ARCH_PORTDUINO) if ((screen_found.port != ScanI2C::I2CPort::NO_I2C || portduino_config.displayPanel) && config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { screen->setup(); } #else if (screen_found.port != ScanI2C::I2CPort::NO_I2C && screen) screen->setup(); #endif #endif auto rIf = initLoRa(); 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 (portduino_config.webserverport != -1) { piwebServerThread = new PiWebServerThread(); std::atexit([] { delete piwebServerThread; }); } #endif initApiServer(TCPPort); #endif // Start airtime logger thread. airTime = new AirTime(); if (!rIf) RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_NO_RADIO); else { // 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); router->addInterface(std::move(rIf)); } // This must be _after_ service.init because we need our preferences loaded from flash to have proper timeout values PowerFSM_setup(); // we will transition to ON in a couple of seconds, FIXME, only do this for cold boots, not waking from SDS powerFSMthread = new PowerFSMThread(); #if !HAS_TFT setCPUFast(false); // 80MHz is fine for our slow peripherals #endif #ifdef ARDUINO_ARCH_ESP32 LOG_DEBUG("Free heap : %7d bytes", ESP.getFreeHeap()); LOG_DEBUG("Free PSRAM : %7d bytes", ESP.getFreePsram()); #endif // We manually run this to update the NodeStatus nodeDB->notifyObservers(true); } #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) bool suppressRebootBanner; // If true, suppress "Rebooting..." overlay (used for OTA handoff) // 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; // TODO find better home than main.cpp extern meshtastic_DeviceMetadata getDeviceMetadata() { meshtastic_DeviceMetadata deviceMetadata; strncpy(deviceMetadata.firmware_version, optstr(APP_VERSION), sizeof(deviceMetadata.firmware_version)); deviceMetadata.device_state_version = DEVICESTATE_CUR_VER; deviceMetadata.canShutdown = pmu_found || HAS_CPU_SHUTDOWN; deviceMetadata.hasBluetooth = HAS_BLUETOOTH; deviceMetadata.hasWifi = HAS_WIFI; deviceMetadata.hasEthernet = HAS_ETHERNET; deviceMetadata.role = config.device.role; deviceMetadata.position_flags = config.position.position_flags; deviceMetadata.hw_model = HW_VENDOR; deviceMetadata.hasRemoteHardware = moduleConfig.remote_hardware.enabled; deviceMetadata.excluded_modules = meshtastic_ExcludedModules_EXCLUDED_NONE; #if MESHTASTIC_EXCLUDE_REMOTEHARDWARE deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_REMOTEHARDWARE_CONFIG; #endif #if MESHTASTIC_EXCLUDE_AUDIO deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_AUDIO_CONFIG; #endif // Option to explicitly include canned messages for edge cases, e.g. niche graphics #if ((!HAS_SCREEN || NO_EXT_GPIO) || MESHTASTIC_EXCLUDE_CANNEDMESSAGES) && !defined(MESHTASTIC_INCLUDE_NICHE_GRAPHICS) deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_CANNEDMSG_CONFIG; #endif #if NO_EXT_GPIO || MESHTASTIC_EXCLUDE_EXTERNALNOTIFICATION deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_EXTNOTIF_CONFIG; #endif // Only edge case here is if we apply this a device with built in Accelerometer and want to detect interrupts // We'll have to macro guard against those targets potentially #if NO_EXT_GPIO || MESHTASTIC_EXCLUDE_DETECTIONSENSOR deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_DETECTIONSENSOR_CONFIG; #endif // If we don't have any GPIO and we don't have GPS OR we don't want too - no purpose in having serial config #if NO_EXT_GPIO && NO_GPS || MESHTASTIC_EXCLUDE_SERIAL deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_SERIAL_CONFIG; #endif #ifndef ARCH_ESP32 deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_PAXCOUNTER_CONFIG; #endif #if !defined(HAS_RGB_LED) && !RAK_4631 deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_AMBIENTLIGHTING_CONFIG; #endif // No bluetooth on these targets (yet): // Pico W / 2W may get it at some point // Portduino and ESP32-C6 are excluded because we don't have a working bluetooth stacks integrated yet. #if defined(ARCH_RP2040) || defined(ARCH_PORTDUINO) || defined(ARCH_STM32WL) || defined(CONFIG_IDF_TARGET_ESP32C6) deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_BLUETOOTH_CONFIG; #endif #if defined(ARCH_NRF52) && !HAS_ETHERNET // nrf52 doesn't have network unless it's a RAK ethernet gateway currently deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_NETWORK_CONFIG; // No network on nRF52 #elif defined(ARCH_RP2040) && !HAS_WIFI && !HAS_ETHERNET deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_NETWORK_CONFIG; // No network on RP2040 #endif #if !(MESHTASTIC_EXCLUDE_PKI) deviceMetadata.hasPKC = true; #endif return deviceMetadata; } #if !MESHTASTIC_EXCLUDE_I2C void scannerToSensorsMap(const std::unique_ptr &i2cScanner, ScanI2C::DeviceType deviceType, meshtastic_TelemetrySensorType sensorType) { auto found = i2cScanner->find(deviceType); if (found.type != ScanI2C::DeviceType::NONE) { nodeTelemetrySensorsMap[sensorType].first = found.address.address; nodeTelemetrySensorsMap[sensorType].second = i2cScanner->fetchI2CBus(found.address); } } #endif #ifndef PIO_UNIT_TESTING void loop() { runASAP = false; #ifdef ARCH_ESP32 esp32Loop(); #endif #ifdef ARCH_NRF52 nrf52Loop(); #endif power->powerCommandsCheck(); if (RadioLibInterface::instance != nullptr) { static uint32_t lastRadioMissedIrqPoll; if (!Throttle::isWithinTimespanMs(lastRadioMissedIrqPoll, 1000)) { lastRadioMissedIrqPoll = millis(); RadioLibInterface::instance->pollMissedIrqs(); } // Periodic AGC reset — warm sleep + recalibrate to prevent stuck AGC gain static uint32_t lastAgcReset; if (!Throttle::isWithinTimespanMs(lastAgcReset, AGC_RESET_INTERVAL_MS)) { lastAgcReset = millis(); RadioLibInterface::instance->resetAGC(); } } #ifdef DEBUG_STACK static uint32_t lastPrint = 0; if (!Throttle::isWithinTimespanMs(lastPrint, 10 * 1000L)) { lastPrint = millis(); meshtastic::printThreadInfo("main"); } #endif service->loop(); #if !MESHTASTIC_EXCLUDE_INPUTBROKER && defined(HAS_FREE_RTOS) && !defined(ARCH_RP2040) if (inputBroker) inputBroker->processInputEventQueue(); #endif #if ARCH_PORTDUINO if (portduino_config.lora_spi_dev == "ch341" && ch341Hal != nullptr) { ch341Hal->checkError(); } if (portduino_status.LoRa_in_error && rebootAtMsec == 0) { LOG_ERROR("LoRa in error detected, attempting to recover"); router->addInterface(nullptr); if (portduino_config.lora_spi_dev == "ch341") { if (ch341Hal != nullptr) { delete ch341Hal; ch341Hal = nullptr; sleep(3); } try { ch341Hal = new Ch341Hal(0, portduino_config.lora_usb_serial_num, portduino_config.lora_usb_vid, portduino_config.lora_usb_pid); } catch (std::exception &e) { std::cerr << e.what() << std::endl; std::cerr << "Could not initialize CH341 device!" << std::endl; exit(EXIT_FAILURE); } } auto rIf = initLoRa(); if (rIf) { router->addInterface(std::move(rIf)); portduino_status.LoRa_in_error = false; } else { LOG_WARN("Reconfigure failed, rebooting"); if (screen) { screen->showSimpleBanner("Rebooting..."); } rebootAtMsec = millis() + 25; } } #if HAS_TFT if (screen && portduino_config.displayPanel == x11 && config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { auto dispdev = screen->getDisplayDevice(); if (dispdev) static_cast(dispdev)->sdlLoop(); } #endif #endif #if HAS_SCREEN && ENABLE_MESSAGE_PERSISTENCE messageStoreAutosaveTick(); #endif long delayMsec = mainController.runOrDelay(); // We want to sleep as long as possible here - because it saves power if (!runASAP && loopCanSleep()) { #ifdef DEBUG_LOOP_TIMING LOG_DEBUG("main loop delay: %d", delayMsec); #endif mainDelay.delay(delayMsec); } } #endif ================================================ FILE: src/main.h ================================================ #pragma once #include "BluetoothStatus.h" #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_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 !MESHTASTIC_EXCLUDE_I2C #include "detect/ScanI2CTwoWire.h" #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 bool kb_found; extern bool osk_found; extern ScanI2C::DeviceAddress rtc_found; extern ScanI2C::DeviceAddress accelerometer_found; extern ScanI2C::FoundDevice rgb_found; extern ScanI2C::DeviceAddress aqi_found; extern bool eink_found; extern bool pmu_found; extern bool isUSBPowered; #ifdef HAS_DRV2605 #include extern Adafruit_DRV2605 drv; #endif #ifdef HAS_PCA9557 #include extern PCA9557 io; #endif #ifdef HAS_I2S #include "AudioThread.h" extern AudioThread *audioThread; #endif #ifdef HAS_UDP_MULTICAST #include "mesh/udp/UdpMulticastHandler.h" extern UdpMulticastHandler *udpHandler; #endif // Global Screen singleton. extern graphics::Screen *screen; #if !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 bool suppressRebootBanner; 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(); #if !MESHTASTIC_EXCLUDE_I2C void scannerToSensorsMap(const std::unique_ptr &i2cScanner, ScanI2C::DeviceType deviceType, meshtastic_TelemetrySensorType sensorType); #endif // We default to 4MHz SPI, SPI mode 0 extern SPISettings spiSettings; ================================================ FILE: src/memGet.cpp ================================================ /** * @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" #ifdef ARCH_STM32WL #include #endif 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(); #elif defined(ARCH_STM32WL) struct mallinfo m = mallinfo(); return m.fordblks; // Total free space (bytes) #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(); #elif defined(ARCH_STM32WL) struct mallinfo m = mallinfo(); return m.arena; // Non-mmapped space allocated (bytes) #else // this platform does not have heap management function implemented return UINT32_MAX; #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 } void displayPercentHeapFree() { uint32_t freeHeap = memGet.getFreeHeap(); uint32_t totalHeap = memGet.getHeapSize(); if (totalHeap == 0 || totalHeap == UINT32_MAX) { LOG_INFO("Heap size unavailable"); return; } int percent = (int)((freeHeap * 100) / totalHeap); LOG_INFO("Heap free: %d%% (%u/%u bytes)", percent, freeHeap, totalHeap); } ================================================ FILE: src/memGet.h ================================================ #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 ================================================ FILE: src/mesh/Channels.cpp ================================================ #include "Channels.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 meshtastic_Channel dummyChannel = {.index = -1}; 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 } bool Channels::ensureLicensedOperation() { if (!owner.is_licensed) { return false; } bool hasEncryptionOrAdmin = false; for (uint8_t i = 0; i < MAX_NUM_CHANNELS; i++) { auto channel = channels.getByIndex(i); if (!channel.has_settings) { continue; } auto &channelSettings = channel.settings; if (strcasecmp(channelSettings.name, Channels::adminChannel) == 0) { channel.role = meshtastic_Channel_Role_DISABLED; channelSettings.psk.bytes[0] = 0; channelSettings.psk.size = 0; hasEncryptionOrAdmin = true; channels.setChannel(channel); } else if (channelSettings.psk.size > 0) { channelSettings.psk.bytes[0] = 0; channelSettings.psk.size = 0; hasEncryptionOrAdmin = true; channels.setChannel(channel); } } return hasEncryptionOrAdmin; } /** * Write a default channel to the specified channel index */ void Channels::initDefaultChannel(ChannelIndex chIndex) { meshtastic_Channel &ch = getByIndex(chIndex); meshtastic_ChannelSettings &channelSettings = ch.settings; 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, (const char *)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, (const char *)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, (const char *)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; CryptoKey k; memset(k.bytes, 0, sizeof(k.bytes)); // In case the user provided a short key, we want to pad the rest with zeros if (!ch.has_settings || ch.role == meshtastic_Channel_Role_DISABLED) { k.length = -1; // invalid } else { memcpy(k.bytes, channelSettings.psk.bytes, channelSettings.psk.size); k.length = channelSettings.psk.size; if (k.length == 0) { if (ch.role == meshtastic_Channel_Role_SECONDARY) { LOG_DEBUG("Unset PSK for secondary channel %s. use primary key", ch.settings.name); k = getKey(primaryIndex); } else { LOG_WARN("User disabled encryption"); } } else if (k.length == 1) { // Convert the short single byte variants of psk into variant that can be used more generally uint8_t pskIndex = k.bytes[0]; LOG_DEBUG("Expand short PSK #%d", pskIndex); if (pskIndex == 0) k.length = 0; // Turn off encryption else { memcpy(k.bytes, defaultpsk, sizeof(defaultpsk)); k.length = sizeof(defaultpsk); // Bump up the last byte of PSK as needed uint8_t *last = k.bytes + sizeof(defaultpsk) - 1; *last = *last + pskIndex - 1; // index of 1 means no change vs defaultPSK } } else if (k.length < 16) { // Error! The user specified only the first few bits of an AES128 key. So by convention we just pad the rest of the // key with zeros LOG_WARN("User provided a too short AES128 key - padding"); k.length = 16; } else if (k.length < 32 && k.length != 16) { // Error! The user specified only the first few bits of an AES256 key. So by convention we just pad the rest of the // key with zeros LOG_WARN("User provided a too short AES256 key - padding"); k.length = 32; } } return k; } /** 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); return dummyChannel; } } 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 && !MESHTASTIC_EXCLUDE_MQTT // Don't publish messages on the public MQTT broker if we are in event mode if (mqtt && mqtt->isUsingDefaultServer() && mqtt->isUsingDefaultRootTopic()) { return false; } #endif for (int i = 0; i < getNumChannels(); i++) if (channelFile.channels[i].role != meshtastic_Channel_Role_DISABLED && channelFile.channels[i].has_settings && (channelFile.channels[i].settings.downlink_enabled || channelFile.channels[i].settings.uplink_enabled)) return true; return false; } const char *Channels::getName(size_t chIndex) { // Convert the short "" representation for Default into a usable string const meshtastic_ChannelSettings &channelSettings = getByIndex(chIndex).settings; const char *channelName = channelSettings.name; if (!*channelName) { // emptystring // Per mesh.proto spec, if bandwidth is specified we must ignore modemPreset enum, we assume that in that case // the app effed up and forgot to set channelSettings.name if (config.lora.use_preset) { channelName = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false, config.lora.use_preset); } else { channelName = "Custom"; } } return channelName; } bool Channels::isDefaultChannel(ChannelIndex chIndex) { const auto &ch = getByIndex(chIndex); if (ch.settings.psk.size == 1 && ch.settings.psk.bytes[0] == 1) { const char *name = getName(chIndex); const char *presetName = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false, config.lora.use_preset); // Check if the name is the default derived from the modem preset if (strcmp(name, presetName) == 0) return true; } return false; } bool Channels::hasDefaultChannel() { // If we don't use a preset or the default frequency slot, or we override the frequency, we don't have a default channel if (!config.lora.use_preset || !RadioInterface::uses_default_frequency_slot || config.lora.override_frequency) return false; // Check if any of the channels are using the default name and PSK for (size_t i = 0; i < getNumChannels(); i++) { if (isDefaultChannel(i)) return true; } return false; } /** 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("Skip channel %d (hash %x) due to invalid hash/index, want=%x", chIndex, getHash(chIndex), // channelHash); return false; } else { LOG_DEBUG("Use channel %d (hash 0x%x)", chIndex, channelHash); setCrypto(chIndex); return true; } } bool Channels::setDefaultPresetCryptoForHash(ChannelHash channelHash) { // Iterate all known presets for (int preset = _meshtastic_Config_LoRaConfig_ModemPreset_MIN; preset <= _meshtastic_Config_LoRaConfig_ModemPreset_MAX; ++preset) { const char *name = DisplayFormatters::getModemPresetDisplayName((meshtastic_Config_LoRaConfig_ModemPreset)preset, false, config.lora.use_preset); if (!name) continue; if (strcmp(name, "Invalid") == 0) continue; // skip invalid placeholder uint8_t h = xorHash((const uint8_t *)name, strlen(name)); // Expand default PSK alias 1 to actual bytes and xor into hash uint8_t tmp = h ^ xorHash(defaultpsk, sizeof(defaultpsk)); if (tmp == channelHash) { // Set crypto to defaultpsk and report success CryptoKey k; memcpy(k.bytes, defaultpsk, sizeof(defaultpsk)); k.length = sizeof(defaultpsk); crypto->setKey(k); LOG_INFO("Matched default preset '%s' for hash 0x%x; set default PSK", name, channelHash); return true; } } return false; } /** 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 * * @return the (0 to 255) hash for that channel - if no suitable channel could be found, return -1 */ int16_t Channels::setActiveByIndex(ChannelIndex channelIndex) { return setCrypto(channelIndex); } ================================================ FILE: src/mesh/Channels.h ================================================ #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(); bool ensureLicensedOperation(); bool setDefaultPresetCryptoForHash(ChannelHash channelHash); int16_t getHash(ChannelIndex i) { return hashes[i]; } private: /** Given a channel index, change to use the crypto key specified by that index * * @eturn the (0 to 255) hash for that channel - if no suitable channel could be found, return -1 */ int16_t setCrypto(ChannelIndex chIndex); /** 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); /** * 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}; ================================================ FILE: src/mesh/CryptoEngine.cpp ================================================ #include "CryptoEngine.h" // #include "NodeDB.h" #include "architecture.h" #include #if !(MESHTASTIC_EXCLUDE_PKI) #include "NodeDB.h" #include "aes-ccm.h" #include "meshUtils.h" #include #include #include #include #if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN) #if !defined(ARCH_STM32WL) #define CryptRNG RNG #endif /** * 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) { // Mix in any randomness we can, to make key generation stronger. CryptRNG.begin(optstr(APP_VERSION)); if (myNodeInfo.device_id.size == 16) { CryptRNG.stir(myNodeInfo.device_id.bytes, myNodeInfo.device_id.size); } auto noise = random(); CryptRNG.stir((uint8_t *)&noise, sizeof(noise)); LOG_DEBUG("Generate Curve25519 keypair"); Curve25519::dh1(public_key, private_key); memcpy(pubKey, public_key, sizeof(public_key)); memcpy(privKey, private_key, sizeof(private_key)); } /** * 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; } bool CryptoEngine::ensurePkiKeys(meshtastic_Config_SecurityConfig &security, meshtastic_User &user) { if (user.is_licensed) { return false; } bool keygenSuccess = false; if (security.private_key.size == 32) { if (regeneratePublicKey(security.public_key.bytes, security.private_key.bytes)) { keygenSuccess = true; } } else { LOG_INFO("Generate new PKI keys"); generateKeyPair(security.public_key.bytes, security.private_key.bytes); keygenSuccess = true; } if (keygenSuccess) { security.public_key.size = 32; security.private_key.size = 32; user.public_key.size = 32; memcpy(user.public_key.bytes, security.public_key.bytes, 32); } return keygenSuccess; } #endif /** * Encrypt a packet's payload using a key generated with Curve25519 and SHA256 * for a specific node. * * @param toNode The MeshPacket `to` field. * @param fromNode The MeshPacket `from` field. * @param remotePublic The remote node's Curve25519 public key. * @param packetId The MeshPacket `id` field. * @param numBytes Number of bytes of plaintext in the bytes buffer. * @param bytes Buffer containing plaintext input. * @param bytesOut Output buffer to be populated with encrypted ciphertext. */ bool CryptoEngine::encryptCurve25519(uint32_t toNode, uint32_t fromNode, meshtastic_UserLite_public_key_t remotePublic, uint64_t packetNum, size_t numBytes, const uint8_t *bytes, uint8_t *bytesOut) { uint8_t *auth; long extraNonceTmp = random(); auth = bytesOut + numBytes; memcpy((uint8_t *)(auth + 8), &extraNonceTmp, sizeof(uint32_t)); // do not use dereference on potential non aligned pointers : *extraNonce = extraNonceTmp; LOG_DEBUG("Random nonce value: %d", extraNonceTmp); if (remotePublic.size == 0) { LOG_DEBUG("Node %d or their public_key not found", toNode); return false; } if (!setDHPublicKey(remotePublic.bytes)) { return false; } hash(shared_key, 32); initNonce(fromNode, packetNum, extraNonceTmp); // Calculate the shared secret with the destination node and encrypt printBytes("Attempt encrypt with nonce: ", nonce, 13); printBytes("Attempt encrypt with shared_key starting with: ", shared_key, 8); aes_ccm_ae(shared_key, 32, nonce, 8, bytes, numBytes, nullptr, 0, bytesOut, auth); // this can write up to 15 bytes longer than numbytes past bytesOut memcpy((uint8_t *)(auth + 8), &extraNonceTmp, sizeof(uint32_t)); // do not use dereference on potential non aligned pointers : *extraNonce = extraNonceTmp; return true; } /** * Decrypt a packet's payload using a key generated with Curve25519 and SHA256 * for a specific node. * * @param fromNode The MeshPacket `from` field. * @param remotePublic The remote node's Curve25519 public key. * @param packetId The MeshPacket `id` field. * @param numBytes Number of bytes of ciphertext in the bytes buffer. * @param bytes Buffer containing ciphertext input. * @param bytesOut Output buffer to be populated with decrypted plaintext. */ bool CryptoEngine::decryptCurve25519(uint32_t fromNode, meshtastic_UserLite_public_key_t remotePublic, uint64_t packetNum, size_t numBytes, const uint8_t *bytes, uint8_t *bytesOut) { const uint8_t *auth = bytes + numBytes - 12; // set to last 8 bytes of text? uint32_t extraNonce; // pointer was not really used memcpy(&extraNonce, auth + 8, sizeof(uint32_t)); // do not use dereference on potential non aligned pointers : (uint32_t *)(auth + 8); LOG_INFO("Random nonce value: %d", extraNonce); if (remotePublic.size == 0) { LOG_DEBUG("Node or its public key not found in database"); return false; } // Calculate the shared secret with the sending node and decrypt if (!setDHPublicKey(remotePublic.bytes)) { return false; } hash(shared_key, 32); initNonce(fromNode, packetNum, extraNonce); printBytes("Attempt decrypt with nonce: ", nonce, 13); printBytes("Attempt decrypt with shared_key starting with: ", shared_key, 8); return aes_ccm_ad(shared_key, 32, nonce, 8, bytes, numBytes - 12, nullptr, 0, auth, bytesOut); } void CryptoEngine::setDHPrivateKey(uint8_t *_private_key) { memcpy(private_key, _private_key, 32); } /** * 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) { aes = nullptr; if (key_len != 0) { aes = std::unique_ptr(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("Use 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) { std::unique_ptr ctr; if (_key.length == 16) ctr = std::unique_ptr(new CTR()); else ctr = std::unique_ptr(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 ================================================ FILE: src/mesh/CryptoEngine.h ================================================ #pragma once #include "AES.h" #include "CTR.h" #include "concurrency/LockGuard.h" #include "configuration.h" #include "mesh-pb-constants.h" #include #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); virtual bool ensurePkiKeys(meshtastic_Config_SecurityConfig &security, meshtastic_User &user); #endif void setDHPrivateKey(uint8_t *_private_key); virtual bool encryptCurve25519(uint32_t toNode, uint32_t fromNode, meshtastic_UserLite_public_key_t remotePublic, uint64_t packetNum, size_t numBytes, const uint8_t *bytes, uint8_t *bytesOut); virtual bool decryptCurve25519(uint32_t fromNode, meshtastic_UserLite_public_key_t remotePublic, uint64_t packetNum, size_t numBytes, const uint8_t *bytes, uint8_t *bytesOut); virtual bool setDHPublicKey(uint8_t *publicKey); virtual void hash(uint8_t *bytes, size_t numBytes); virtual void aesSetKey(const uint8_t *key, size_t key_len); virtual void aesEncrypt(uint8_t *in, uint8_t *out); std::unique_ptr aes = nullptr; #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 = {}; #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; ================================================ FILE: src/mesh/Default.cpp ================================================ #include "Default.h" #include "meshUtils.h" uint32_t Default::getConfiguredOrDefaultMs(uint32_t configuredInterval, uint32_t defaultInterval) { if (configuredInterval > 0) return configuredInterval * 1000; return defaultInterval * 1000; } uint32_t Default::getConfiguredOrDefaultMs(uint32_t configuredInterval) { 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 || config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE) return getConfiguredOrDefaultMs(configured, defaultValue); // Additionally if we're a tracker or sensor, we want priority to send position and telemetry if (IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_SENSOR, meshtastic_Config_DeviceConfig_Role_TRACKER, meshtastic_Config_DeviceConfig_Role_TAK_TRACKER)) 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 } ================================================ FILE: src/mesh/Default.h ================================================ #pragma once #include #include #include #include #include #include #define ONE_DAY 24 * 60 * 60 #define ONE_MINUTE_MS 60 * 1000 #define THIRTY_SECONDS_MS 30 * 1000 #define TWO_SECONDS_MS 2 * 1000 #define FIVE_SECONDS_MS 5 * 1000 #define TEN_SECONDS_MS 10 * 1000 #define MAX_INTERVAL INT32_MAX // FIXME: INT32_MAX to avoid overflow issues with Apple clients but should be UINT32_MAX #define min_default_telemetry_interval_secs IF_ROUTER(ONE_DAY / 2, 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, 60 * 60) #define default_broadcast_smart_minimum_interval_secs 5 * 60 #define min_default_broadcast_interval_secs IF_ROUTER(ONE_DAY / 2, 60 * 60) #define min_default_broadcast_smart_minimum_interval_secs 5 * 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 4 * 60 * 60 #define default_map_publish_interval_secs 60 * 60 // Traffic management defaults #define default_traffic_mgmt_position_precision_bits 24 // ~10m grid cells #define default_traffic_mgmt_position_min_interval_secs ONE_DAY // 1 day between identical positions #ifdef USERPREFS_RINGTONE_NAG_SECS #define default_ringtone_nag_secs USERPREFS_RINGTONE_NAG_SECS #else #define default_ringtone_nag_secs 15 #endif #define default_network_ipv6_enabled false #define default_mqtt_address "mqtt.meshtastic.org" #define default_mqtt_username "meshdev" #define default_mqtt_password "large4cats" #define default_mqtt_root "msh" #define default_mqtt_encryption_enabled true #define default_mqtt_tls_enabled false #define IF_ROUTER(routerVal, normalVal) \ ((config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER || \ config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE) \ ? (routerVal) \ : (normalVal)) class Default { public: static uint32_t getConfiguredOrDefaultMs(uint32_t configuredInterval); static uint32_t getConfiguredOrDefaultMs(uint32_t configuredInterval, uint32_t defaultInterval); static uint32_t getConfiguredOrDefault(uint32_t configured, uint32_t defaultValue); // Note: numOnlineNodes uses uint32_t to match the public API and allow flexibility, // even though internal node counts use uint16_t (max 65535 nodes) static uint32_t getConfiguredOrDefaultMsScaled(uint32_t configured, uint32_t defaultValue, uint32_t numOnlineNodes); static uint8_t getConfiguredOrDefaultHopLimit(uint8_t configured); static uint32_t getConfiguredOrMinimumValue(uint32_t configured, uint32_t minValue); private: // Note: Kept as uint32_t to match the public API parameter type static float congestionScalingCoefficient(uint32_t numOnlineNodes) { if (numOnlineNodes <= 40) { return 1.0; } else { // Resolve SF and BW from preset or manual config // When use_preset is true, config.lora.spread_factor and bandwidth may be 0 // because applyModemConfig() sets them on RadioInterface, not on config.lora float bwKHz; uint8_t sf; uint8_t cr; if (config.lora.use_preset) { modemPresetToParams(config.lora.modem_preset, false, bwKHz, sf, cr); } else { sf = config.lora.spread_factor; bwKHz = bwCodeToKHz(config.lora.bandwidth); } // Guard against invalid values sf = clampSpreadFactor(sf); bwKHz = clampBandwidthKHz(bwKHz); // throttlingFactor = 2^SF / (BW_in_kHz * scaling_divisor) // With scaling_divisor=100: // In SF11 and BW=250khz (longfast), this gives 0.08192 rather than the original 0.075 // In SF10 and BW=250khz (mediumslow), this gives 0.04096 rather than the original 0.04 // In SF9 and BW=250khz (mediumfast), this gives 0.02048 rather than the original 0.02 // In SF7 and BW=250khz (shortfast), this gives 0.00512 rather than the original 0.01 float throttlingFactor = static_cast(pow_of_2(sf)) / (bwKHz * 100.0f); #if USERPREFS_EVENT_MODE // If we are in event mode, scale down the throttling factor by 4 throttlingFactor = static_cast(pow_of_2(sf)) / (bwKHz * 25.0f); #endif // Scaling up traffic based on number of nodes over 40 int nodesOverForty = (numOnlineNodes - 40); return 1.0 + (nodesOverForty * throttlingFactor); // Each number of online node scales by throttle factor } } }; ================================================ FILE: src/mesh/FloodingRouter.cpp ================================================ #include "FloodingRouter.h" #include "MeshTypes.h" #include "NodeDB.h" #include "configuration.h" #include "mesh-pb-constants.h" #include "meshUtils.h" #include "modules/TextMessageModule.h" #if !MESHTASTIC_EXCLUDE_TRACEROUTE #include "modules/TraceRouteModule.h" #endif 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) p->relay_node = nodeDB->getLastByteOfNodeNum(getNodeNum()); // First set the relayer to us wasSeenRecently(p); // FIXME, move this to a sniffSent method return Router::send(p); } bool FloodingRouter::shouldFilterReceived(const meshtastic_MeshPacket *p) { bool wasUpgraded = false; bool seenRecently = wasSeenRecently(p, true, nullptr, nullptr, &wasUpgraded); // Updates history; returns false when an upgrade is detected // Handle hop_limit upgrade scenario for rebroadcasters if (wasUpgraded && perhapsHandleUpgradedPacket(p)) { return true; // we handled it, so stop processing } if (!seenRecently && !wasUpgraded && textMessageModule) { seenRecently = textMessageModule->recentlySeen(p->id); } if (seenRecently) { printPacket("Ignore dupe incoming msg", p); rxDupe++; /* If the original transmitter is doing retransmissions (hopStart equals hopLimit) for a reliable transmission, e.g., when the ACK got lost, we will handle the packet again to make sure it gets an implicit ACK. */ bool isRepeated = p->hop_start > 0 && p->hop_start == p->hop_limit; if (isRepeated) { LOG_DEBUG("Repeated reliable tx"); // Check if it's still in the Tx queue, if not, we have to relay it again if (!findInTxQueue(p->from, p->id)) { reprocessPacket(p); perhapsRebroadcast(p); } } else { perhapsCancelDupe(p); } return true; } return Router::shouldFilterReceived(p); } bool FloodingRouter::perhapsHandleUpgradedPacket(const meshtastic_MeshPacket *p) { // isRebroadcaster() is duplicated in perhapsRebroadcast(), but this avoids confusing log messages if (isRebroadcaster() && iface && p->hop_limit > 0) { // If we overhear a duplicate copy of the packet with more hops left than the one we are waiting to // rebroadcast, then remove the packet currently sitting in the TX queue and use this one instead. uint8_t dropThreshold = p->hop_limit; // remove queued packets that have fewer hops remaining if (iface->removePendingTXPacket(getFrom(p), p->id, dropThreshold)) { LOG_DEBUG("Processing upgraded packet 0x%08x for rebroadcast with hop limit %d (dropping queued < %d)", p->id, p->hop_limit, dropThreshold); reprocessPacket(p); perhapsRebroadcast(p); rxDupe++; // We already enqueued the improved copy, so make sure the incoming packet stops here. return true; } } return false; } void FloodingRouter::reprocessPacket(const meshtastic_MeshPacket *p) { if (nodeDB) nodeDB->updateFrom(*p); #if !MESHTASTIC_EXCLUDE_TRACEROUTE if (traceRouteModule && p->which_payload_variant != meshtastic_MeshPacket_decoded_tag) { // If we got a packet that is not decoded, try to decode it so we can check for traceroute. auto decodedState = perhapsDecode(const_cast(p)); if (decodedState == DecodeState::DECODE_SUCCESS) { // parsing was successful, print for debugging printPacket("reprocessPacket(DUP)", p); } else { // Fatal decoding error, we can't do anything with this packet LOG_WARN( "FloodingRouter::reprocessPacket: Fatal decode error (state=%d, id=0x%08x, from=%u), can't check for traceroute", static_cast(decodedState), p->id, getFrom(p)); return; } } if (traceRouteModule && p->which_payload_variant == meshtastic_MeshPacket_decoded_tag && p->decoded.portnum == meshtastic_PortNum_TRACEROUTE_APP) { traceRouteModule->processUpgradedPacket(*p); } #endif } bool FloodingRouter::roleAllowsCancelingDupe(const meshtastic_MeshPacket *p) { if (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER || config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE) { // ROUTER, ROUTER_LATE should never cancel relaying a packet (i.e. we should always rebroadcast), // even if we've heard another station rebroadcast it already. return false; } if (config.device.role == meshtastic_Config_DeviceConfig_Role_CLIENT_BASE) { // CLIENT_BASE: if the packet is from or to a favorited node, // we should act like a ROUTER and should never cancel a rebroadcast (i.e. we should always rebroadcast), // even if we've heard another station rebroadcast it already. return !nodeDB->isFromOrToFavoritedNode(*p); } // All other roles (such as CLIENT) should cancel a rebroadcast if they hear another station's rebroadcast. return true; } void FloodingRouter::perhapsCancelDupe(const meshtastic_MeshPacket *p) { if (p->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA && roleAllowsCancelingDupe(p)) { // cancel rebroadcast of this message *if* there was already one, unless we're a router! // But only LoRa packets should be able to trigger this. if (Router::cancelSending(p->from, p->id)) txRelayCanceled++; } if (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE && iface) { iface->clampToLateRebroadcastWindow(getFrom(p), p->id); } if (config.device.role == meshtastic_Config_DeviceConfig_Role_CLIENT_BASE && iface && nodeDB && nodeDB->isFromOrToFavoritedNode(*p)) { iface->clampToLateRebroadcastWindow(getFrom(p), p->id); } } bool FloodingRouter::isRebroadcaster() { return config.device.role != meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE && config.device.rebroadcast_mode != meshtastic_Config_DeviceConfig_RebroadcastMode_NONE; } void FloodingRouter::sniffReceived(const meshtastic_MeshPacket *p, const meshtastic_Routing *c) { bool isAckorReply = (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) && (p->decoded.request_id != 0 || p->decoded.reply_id != 0); if (isAckorReply && !isToUs(p) && !isBroadcast(p->to)) { // do not flood direct message that is ACKed or replied to LOG_DEBUG("Rxd an ACK/reply not for me, cancel rebroadcast"); Router::cancelSending(p->to, p->decoded.request_id); // cancel rebroadcast for this DM } perhapsRebroadcast(p); // handle the packet as normal Router::sniffReceived(p, c); } ================================================ FILE: src/mesh/FloodingRouter.h ================================================ #pragma once #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 { 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; /* Check if we should rebroadcast this packet, and do so if needed */ virtual bool perhapsRebroadcast(const meshtastic_MeshPacket *p) = 0; /* Check if we should handle an upgraded packet (with higher hop_limit) * @return true if we handled it (so stop processing) */ bool perhapsHandleUpgradedPacket(const meshtastic_MeshPacket *p); /* Call when we receive a packet that needs some reprocessing, but afterwards should be filtered */ void reprocessPacket(const meshtastic_MeshPacket *p); // Return false for roles like ROUTER which should always rebroadcast even when we've heard another rebroadcast of // the same packet bool roleAllowsCancelingDupe(const meshtastic_MeshPacket *p); /* Call when receiving a duplicate packet to check whether we should cancel a packet in the Tx queue */ void perhapsCancelDupe(const meshtastic_MeshPacket *p); // Return true if we are a rebroadcaster bool isRebroadcaster(); }; ================================================ FILE: src/mesh/InterfacesTemplates.cpp ================================================ #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 && !defined(USE_WS5500) #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 ================================================ FILE: src/mesh/LLCC68Interface.cpp ================================================ #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 ================================================ FILE: src/mesh/LLCC68Interface.h ================================================ #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 ================================================ FILE: src/mesh/LR1110Interface.cpp ================================================ #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 ================================================ FILE: src/mesh/LR1110Interface.h ================================================ #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 ================================================ FILE: src/mesh/LR1120Interface.cpp ================================================ #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 ================================================ FILE: src/mesh/LR1120Interface.h ================================================ #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 ================================================ FILE: src/mesh/LR1121Interface.cpp ================================================ #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 ================================================ FILE: src/mesh/LR1121Interface.h ================================================ #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 ================================================ FILE: src/mesh/LR11x0Interface.cpp ================================================ #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" #elif ARCH_PORTDUINO #include "PortduinoGlue.h" #define rfswitch_dio_pins portduino_config.rfswitch_dio_pins #define rfswitch_table portduino_config.rfswitch_table #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 // Particular boards might define a different max power based on what their hardware can do, default to max power output if not // specified (may be dangerous if using external PA and LR11x0 power config forgotten) #if ARCH_PORTDUINO #define LR1110_MAX_POWER portduino_config.lr1110_max_power #endif #ifndef LR1110_MAX_POWER #define LR1110_MAX_POWER 22 #endif // the 2.4G part maxes at 13dBm #if ARCH_PORTDUINO #define LR1120_MAX_POWER portduino_config.lr1120_max_power #endif #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 #if ARCH_PORTDUINO float tcxoVoltage = (float)portduino_config.dio3_tcxo_voltage / 1000; // FIXME: correct logic to default to not using TCXO if no voltage is specified for LR11x0_DIO3_TCXO_VOLTAGE #elif !defined(LR11X0_DIO3_TCXO_VOLTAGE) float tcxoVoltage = 0; // "TCXO reference voltage to be set on DIO3. Defaults to 1.6 V, set to 0 to skip." per // https://github.com/jgromes/RadioLib/blob/690a050ebb46e6097c5d00c371e961c1caa3b52e/src/modules/LR11x0/LR11x0.h#L471C26-L471C104 // (DIO3 is free to be used as an IRQ) LOG_DEBUG("LR11X0_DIO3_TCXO_VOLTAGE not defined, not using DIO3 as TCXO reference voltage"); #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(); limitPower(LR1110_MAX_POWER); if ((power > LR1120_MAX_POWER) && (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { // clamp again if wide freq range power = LR1120_MAX_POWER; preambleLength = 12; // 12 is the default for operation above 2GHz } #ifdef LR11X0_RF_SWITCH_SUBGHZ pinMode(LR11X0_RF_SWITCH_SUBGHZ, OUTPUT); digitalWrite(LR11X0_RF_SWITCH_SUBGHZ, getFreq() < 1e9 ? HIGH : LOW); LOG_DEBUG("Set RF0 switch to %s", getFreq() < 1e9 ? "SubGHz" : "2.4GHz"); #endif #ifdef LR11X0_RF_SWITCH_2_4GHZ pinMode(LR11X0_RF_SWITCH_2_4GHZ, OUTPUT); digitalWrite(LR11X0_RF_SWITCH_2_4GHZ, getFreq() < 1e9 ? LOW : HIGH); LOG_DEBUG("Set RF1 switch to %s", getFreq() < 1e9 ? "SubGHz" : "2.4GHz"); #endif // Allow extra time for TCXO to stabilize after power-on delay(10); int res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage); // Retry if we get SPI command failed - some units need extra TCXO stabilization time if (res == RADIOLIB_ERR_SPI_CMD_FAILED) { LOG_WARN("LR11x0 init failed with %d (SPI_CMD_FAILED), retrying after delay...", res); delay(100); 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 || res == RADIOLIB_ERR_SPI_CMD_FAILED) 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 = portduino_config.has_rfswitch_table; #else bool dioAsRfSwitch = false; #endif if (dioAsRfSwitch) { lora.setRfSwitchTable(rfswitch_dio_pins, rfswitch_table); LOG_DEBUG("Set DIO RF switch"); } if (res == RADIOLIB_ERR_NONE) { if (config.lora.sx126x_rx_boosted_gain) { // the name is unfortunate but historically accurate res = lora.setRxBoostedGainMode(true); LOG_INFO("Set RX gain to boosted mode; result: %d", res); } else { res = lora.setRxBoostedGainMode(false); LOG_INFO("Set RX gain to power saving mode (boosted mode off); result: %d", res); } } if (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, wideLora() && (getFreq() > 1000.0f)); if (err != RADIOLIB_ERR_NONE) RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); err = lora.setCodingRate(cr, cr != 7); // use long interleaving except if CR is 4/7 which doesn't support it if (err != RADIOLIB_ERR_NONE) RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); 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 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()); LOG_DEBUG("Corrected frequency offset: %f", lora.getFrequencyError()); } /** 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. int err = lora.startReceive(RADIOLIB_LR11X0_RX_TIMEOUT_INF, MESHTASTIC_RADIOLIB_IRQ_RX_FLAGS, RADIOLIB_IRQ_RX_DEFAULT_MASK, 0); if (err) LOG_ERROR("StartReceive error: %d", err); assert(err == RADIOLIB_ERR_NONE); RadioLibInterface::startReceive(); // Must be done AFTER, starting transmit, because startTransmit clears (possibly stale) interrupt pending register bits enableInterrupt(isrRxLevel0); checkRxDoneIrqFlag(); #endif } /** Is the channel currently active? */ template bool LR11x0Interface::isChannelActive() { // check if we can detect a LoRa preamble on the current channel ChannelScanConfig_t cfg = {.cad = {.symNum = NUM_SYM_CAD, .detPeak = RADIOLIB_LR11X0_CAD_PARAM_DEFAULT, .detMin = RADIOLIB_LR11X0_CAD_PARAM_DEFAULT, .exitMode = RADIOLIB_LR11X0_CAD_PARAM_DEFAULT, .timeout = 0, .irqFlags = RADIOLIB_IRQ_CAD_DEFAULT_FLAGS, .irqMask = RADIOLIB_IRQ_CAD_DEFAULT_MASK}}; int16_t result; setStandby(); result = lora.scanChannel(cfg); 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); } #ifdef LR11X0_AGC_RESET template void LR11x0Interface::resetAGC() { // Safety: don't reset mid-packet if (sendingPacket != NULL || (isReceiving && isActivelyReceiving())) return; LOG_DEBUG("LR11x0 AGC reset: warm sleep + Calibrate(0x3F)"); // 1. Warm sleep — powers down the analog frontend, resetting AGC state lora.sleep(true, 0); // 2. Wake to RC standby for stable calibration lora.standby(RADIOLIB_LR11X0_STANDBY_RC, true); // 3. Calibrate all blocks (PLL, ADC, image, RC oscillators) // calibrate() is protected on LR11x0, so use raw SPI (same as internal implementation) uint8_t calData = RADIOLIB_LR11X0_CALIBRATE_ALL; module.SPIwriteStream(RADIOLIB_LR11X0_CMD_CALIBRATE, &calData, 1, true, true); // 4. Re-calibrate image rejection for actual operating frequency // Calibrate(0x3F) defaults to 902-928 MHz which is wrong for other regions. lora.calibrateImageRejection(getFreq() - 4.0f, getFreq() + 4.0f); // 5. Re-apply RX boosted gain mode lora.setRxBoostedGainMode(config.lora.sx126x_rx_boosted_gain); // 6. Resume receiving startReceive(); } #endif 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 ================================================ FILE: src/mesh/LR11x0Interface.h ================================================ #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; } #ifdef LR11X0_AGC_RESET void resetAGC() override; #endif 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; uint32_t getPacketTime(uint32_t pl, bool received) override { return computePacketTime(lora, pl, received); } }; #endif ================================================ FILE: src/mesh/LoRaFEMInterface.cpp ================================================ #if HAS_LORA_FEM #include "LoRaFEMInterface.h" #if defined(ARCH_ESP32) #include #include #endif LoRaFEMInterface loraFEMInterface; void LoRaFEMInterface::init(void) { setLnaCanControl(false); // Default is uncontrollable #ifdef HELTEC_V4 pinMode(LORA_PA_POWER, OUTPUT); digitalWrite(LORA_PA_POWER, HIGH); rtc_gpio_hold_dis((gpio_num_t)LORA_PA_POWER); delay(1); rtc_gpio_hold_dis((gpio_num_t)LORA_KCT8103L_PA_CSD); pinMode(LORA_KCT8103L_PA_CSD, INPUT); // detect which FEM is used delay(1); if (digitalRead(LORA_KCT8103L_PA_CSD) == HIGH) { // FEM is KCT8103L fem_type = KCT8103L_PA; rtc_gpio_hold_dis((gpio_num_t)LORA_KCT8103L_PA_CTX); pinMode(LORA_KCT8103L_PA_CSD, OUTPUT); digitalWrite(LORA_KCT8103L_PA_CSD, HIGH); pinMode(LORA_KCT8103L_PA_CTX, OUTPUT); digitalWrite(LORA_KCT8103L_PA_CTX, LOW); // LNA enabled by default setLnaCanControl(true); } else if (digitalRead(LORA_KCT8103L_PA_CSD) == LOW) { // FEM is GC1109 fem_type = GC1109_PA; // LORA_GC1109_PA_EN and LORA_KCT8103L_PA_CSD are the same pin and do not need to be repeatedly turned off and held. // rtc_gpio_hold_dis((gpio_num_t)LORA_GC1109_PA_EN); pinMode(LORA_GC1109_PA_EN, OUTPUT); digitalWrite(LORA_GC1109_PA_EN, HIGH); pinMode(LORA_GC1109_PA_TX_EN, OUTPUT); digitalWrite(LORA_GC1109_PA_TX_EN, LOW); } else { fem_type = OTHER_FEM_TYPES; } #elif defined(USE_GC1109_PA) fem_type = GC1109_PA; pinMode(LORA_PA_POWER, OUTPUT); digitalWrite(LORA_PA_POWER, HIGH); #if defined(ARCH_ESP32) rtc_gpio_hold_dis((gpio_num_t)LORA_PA_POWER); rtc_gpio_hold_dis((gpio_num_t)LORA_GC1109_PA_EN); rtc_gpio_hold_dis((gpio_num_t)LORA_GC1109_PA_TX_EN); #endif delay(1); pinMode(LORA_GC1109_PA_EN, OUTPUT); digitalWrite(LORA_GC1109_PA_EN, HIGH); pinMode(LORA_GC1109_PA_TX_EN, OUTPUT); digitalWrite(LORA_GC1109_PA_TX_EN, LOW); #elif defined(USE_KCT8103L_PA) fem_type = KCT8103L_PA; pinMode(LORA_PA_POWER, OUTPUT); digitalWrite(LORA_PA_POWER, HIGH); #if defined(ARCH_ESP32) rtc_gpio_hold_dis((gpio_num_t)LORA_PA_POWER); rtc_gpio_hold_dis((gpio_num_t)LORA_KCT8103L_PA_CSD); rtc_gpio_hold_dis((gpio_num_t)LORA_KCT8103L_PA_CTX); #endif delay(1); pinMode(LORA_KCT8103L_PA_CSD, OUTPUT); digitalWrite(LORA_KCT8103L_PA_CSD, HIGH); pinMode(LORA_KCT8103L_PA_CTX, OUTPUT); digitalWrite(LORA_KCT8103L_PA_CTX, LOW); // LNA enabled by default setLnaCanControl(true); #endif } void LoRaFEMInterface::setSleepModeEnable(void) { #ifdef HELTEC_V4 if (fem_type == GC1109_PA) { /* * Do not switch the power on and off frequently. * After turning off LORA_GC1109_PA_EN, the power consumption has dropped to the uA level. */ digitalWrite(LORA_GC1109_PA_EN, LOW); digitalWrite(LORA_GC1109_PA_TX_EN, LOW); } else if (fem_type == KCT8103L_PA) { // shutdown the PA digitalWrite(LORA_KCT8103L_PA_CSD, LOW); } #elif defined(USE_GC1109_PA) digitalWrite(LORA_GC1109_PA_EN, LOW); digitalWrite(LORA_GC1109_PA_TX_EN, LOW); #elif defined(USE_KCT8103L_PA) // shutdown the PA digitalWrite(LORA_KCT8103L_PA_CSD, LOW); #endif } void LoRaFEMInterface::setTxModeEnable(void) { #ifdef HELTEC_V4 if (fem_type == GC1109_PA) { digitalWrite(LORA_GC1109_PA_EN, HIGH); // CSD=1: Chip enabled digitalWrite(LORA_GC1109_PA_TX_EN, HIGH); // CPS: 1=full PA, 0=bypass (for RX, CPS is don't care) } else if (fem_type == KCT8103L_PA) { digitalWrite(LORA_KCT8103L_PA_CSD, HIGH); digitalWrite(LORA_KCT8103L_PA_CTX, HIGH); } #elif defined(USE_GC1109_PA) digitalWrite(LORA_GC1109_PA_EN, HIGH); // CSD=1: Chip enabled digitalWrite(LORA_GC1109_PA_TX_EN, HIGH); // CPS: 1=full PA, 0=bypass (for RX, CPS is don't care) #elif defined(USE_KCT8103L_PA) digitalWrite(LORA_KCT8103L_PA_CSD, HIGH); digitalWrite(LORA_KCT8103L_PA_CTX, HIGH); #endif } void LoRaFEMInterface::setRxModeEnable(void) { #ifdef HELTEC_V4 if (fem_type == GC1109_PA) { digitalWrite(LORA_GC1109_PA_EN, HIGH); // CSD=1: Chip enabled digitalWrite(LORA_GC1109_PA_TX_EN, LOW); } else if (fem_type == KCT8103L_PA) { digitalWrite(LORA_KCT8103L_PA_CSD, HIGH); if (lna_enabled) { digitalWrite(LORA_KCT8103L_PA_CTX, LOW); } else { digitalWrite(LORA_KCT8103L_PA_CTX, HIGH); } } #elif defined(USE_GC1109_PA) digitalWrite(LORA_GC1109_PA_EN, HIGH); // CSD=1: Chip enabled digitalWrite(LORA_GC1109_PA_TX_EN, LOW); #elif defined(USE_KCT8103L_PA) digitalWrite(LORA_KCT8103L_PA_CSD, HIGH); if (lna_enabled) { digitalWrite(LORA_KCT8103L_PA_CTX, LOW); } else { digitalWrite(LORA_KCT8103L_PA_CTX, HIGH); } #endif } void LoRaFEMInterface::setRxModeEnableWhenMCUSleep(void) { #ifdef HELTEC_V4 // Keep GC1109 FEM powered during deep sleep so LNA remains active for RX wake. // Set PA_POWER and PA_EN HIGH (overrides SX126xInterface::sleep() shutdown), // then latch with RTC hold so the state survives deep sleep. digitalWrite(LORA_PA_POWER, HIGH); rtc_gpio_hold_en((gpio_num_t)LORA_PA_POWER); if (fem_type == GC1109_PA) { digitalWrite(LORA_GC1109_PA_EN, HIGH); rtc_gpio_hold_en((gpio_num_t)LORA_GC1109_PA_EN); gpio_pulldown_en((gpio_num_t)LORA_GC1109_PA_TX_EN); } else if (fem_type == KCT8103L_PA) { digitalWrite(LORA_KCT8103L_PA_CSD, HIGH); rtc_gpio_hold_en((gpio_num_t)LORA_KCT8103L_PA_CSD); if (lna_enabled) { digitalWrite(LORA_KCT8103L_PA_CTX, LOW); } else { digitalWrite(LORA_KCT8103L_PA_CTX, HIGH); } rtc_gpio_hold_en((gpio_num_t)LORA_KCT8103L_PA_CTX); } #elif defined(USE_GC1109_PA) digitalWrite(LORA_PA_POWER, HIGH); digitalWrite(LORA_GC1109_PA_EN, HIGH); #if defined(ARCH_ESP32) rtc_gpio_hold_en((gpio_num_t)LORA_PA_POWER); rtc_gpio_hold_en((gpio_num_t)LORA_GC1109_PA_EN); gpio_pulldown_en((gpio_num_t)LORA_GC1109_PA_TX_EN); #endif #elif defined(USE_KCT8103L_PA) digitalWrite(LORA_KCT8103L_PA_CSD, HIGH); rtc_gpio_hold_en((gpio_num_t)LORA_KCT8103L_PA_CSD); if (lna_enabled) { digitalWrite(LORA_KCT8103L_PA_CTX, LOW); } else { digitalWrite(LORA_KCT8103L_PA_CTX, HIGH); } rtc_gpio_hold_en((gpio_num_t)LORA_KCT8103L_PA_CTX); #endif } void LoRaFEMInterface::setLNAEnable(bool enabled) { lna_enabled = enabled; } int8_t LoRaFEMInterface::powerConversion(int8_t loraOutputPower) { #ifdef HELTEC_V4 const uint16_t gc1109_tx_gain[] = {11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 9, 8, 7}; const uint16_t kct8103l_tx_gain[] = {13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 12, 12, 11, 11, 10, 9, 8, 7}; const uint16_t *tx_gain; uint16_t tx_gain_num; if (fem_type == GC1109_PA) { tx_gain = gc1109_tx_gain; tx_gain_num = sizeof(gc1109_tx_gain) / sizeof(gc1109_tx_gain[0]); } else if (fem_type == KCT8103L_PA) { tx_gain = kct8103l_tx_gain; tx_gain_num = sizeof(kct8103l_tx_gain) / sizeof(kct8103l_tx_gain[0]); } else { return loraOutputPower; } #else #ifdef ARCH_PORTDUINO const uint16_t *tx_gain = portduino_config.tx_gain_lora; uint16_t tx_gain_num = portduino_config.num_pa_points; #else const uint16_t tx_gain[NUM_PA_POINTS] = {TX_GAIN_LORA}; uint16_t tx_gain_num = NUM_PA_POINTS; #endif #endif for (int radio_dbm = 0; radio_dbm < tx_gain_num; radio_dbm++) { if (((radio_dbm + tx_gain[radio_dbm]) > loraOutputPower) || ((radio_dbm == (tx_gain_num - 1)) && ((radio_dbm + tx_gain[radio_dbm]) <= loraOutputPower))) { // we've exceeded the power limit, or hit the max we can do LOG_INFO("Requested Tx power: %d dBm; Device LoRa Tx gain: %d dB", loraOutputPower, tx_gain[radio_dbm]); loraOutputPower -= tx_gain[radio_dbm]; break; } } return loraOutputPower; } #endif ================================================ FILE: src/mesh/LoRaFEMInterface.h ================================================ #pragma once #if HAS_LORA_FEM #include "configuration.h" #include typedef enum { GC1109_PA, KCT8103L_PA, OTHER_FEM_TYPES } LoRaFEMType; class LoRaFEMInterface { public: LoRaFEMInterface() : fem_type(OTHER_FEM_TYPES) {} virtual ~LoRaFEMInterface() {} void init(void); void setSleepModeEnable(void); void setTxModeEnable(void); void setRxModeEnable(void); void setRxModeEnableWhenMCUSleep(void); void setLNAEnable(bool enabled); int8_t powerConversion(int8_t loraOutputPower); bool isLnaCanControl(void) { return lna_can_control; } void setLnaCanControl(bool can_control) { lna_can_control = can_control; } private: LoRaFEMType fem_type; bool lna_enabled = true; bool lna_can_control = false; }; extern LoRaFEMInterface loraFEMInterface; #endif ================================================ FILE: src/mesh/MemoryPool.h ================================================ #pragma once #include #include #include #include #include "PointerQueue.h" #include "configuration.h" // For LOG_WARN, LOG_DEBUG, LOG_HEAP template class Allocator { public: Allocator() : deleter([this](T *p) { this->release(p); }) {} virtual ~Allocator() {} /// Return a queable object which has been prefilled with zeros. Return nullptr if no buffer is available /// Note: this method is safe to call from regular OR ISR code T *allocZeroed() { T *p = allocZeroed(0); if (!p) { LOG_WARN("Failed to allocate zeroed memory"); } return p; } /// Return a queable object which has been prefilled with zeros - allow timeout to wait for available buffers (you probably /// don't want this version). T *allocZeroed(TickType_t maxWait) { T *p = alloc(maxWait); if (p) memset(p, 0, sizeof(T)); return p; } /// Return a queable object which is a copy of some other object T *allocCopy(const T &src, TickType_t maxWait = portMAX_DELAY) { T *p = alloc(maxWait); if (!p) { LOG_WARN("Failed to allocate memory for copy"); return nullptr; } *p = src; return p; } /// Variations of the above methods that return std::unique_ptr instead of raw pointers. using UniqueAllocation = std::unique_ptr &>; /// Return a queable object which has been prefilled with zeros. /// std::unique_ptr wrapped variant of allocZeroed(). UniqueAllocation allocUniqueZeroed() { return UniqueAllocation(allocZeroed(), deleter); } /// Return a queable object which has been prefilled with zeros - allow timeout to wait for available buffers (you probably /// don't want this version). /// std::unique_ptr wrapped variant of allocZeroed(TickType_t maxWait). UniqueAllocation allocUniqueZeroed(TickType_t maxWait) { return UniqueAllocation(allocZeroed(maxWait), deleter); } /// Return a queable object which is a copy of some other object /// std::unique_ptr wrapped variant of allocCopy(const T &src, TickType_t maxWait). UniqueAllocation allocUniqueCopy(const T &src, TickType_t maxWait = portMAX_DELAY) { return UniqueAllocation(allocCopy(src, maxWait), deleter); } /// Return a buffer for use by others virtual void release(T *p) = 0; protected: // Alloc some storage virtual T *alloc(TickType_t maxWait) = 0; private: // std::unique_ptr Deleter function; calls release(). const std::function deleter; }; /** * An allocator that just uses regular free/malloc */ template class MemoryDynamic : public Allocator { public: /// Return a buffer for use by others virtual void release(T *p) override { if (p == nullptr) return; LOG_HEAP("Freeing 0x%x", p); free(p); } protected: // Alloc some storage virtual T *alloc(TickType_t maxWait) override { T *p = (T *)malloc(sizeof(T)); assert(p); return p; } }; /** * A static memory pool that uses a fixed buffer instead of heap allocation */ template class MemoryPool : public Allocator { private: T pool[MaxSize]; bool used[MaxSize]; public: MemoryPool() : pool{}, used{} { // Arrays are now zero-initialized by member initializer list // pool array: all elements are default-constructed (zero for POD types) // used array: all elements are false (zero-initialized) } /// Return a buffer for use by others virtual void release(T *p) override { if (!p) { LOG_DEBUG("Failed to release memory, pointer is null"); return; } // Find the index of this pointer in our pool int index = p - pool; if (index >= 0 && index < MaxSize) { assert(used[index]); // Should be marked as used used[index] = false; LOG_HEAP("Released static pool item %d at 0x%x", index, p); } else { LOG_WARN("Pointer 0x%x not from our pool!", p); } } protected: // Alloc some storage from our static pool virtual T *alloc(TickType_t maxWait) override { // Find first free slot for (int i = 0; i < MaxSize; i++) { if (!used[i]) { used[i] = true; LOG_HEAP("Allocated static pool item %d at 0x%x", i, &pool[i]); return &pool[i]; } } // No free slots available - return nullptr instead of asserting LOG_WARN("No free slots available in static memory pool!"); return nullptr; } }; ================================================ FILE: src/mesh/MeshModule.cpp ================================================ #include "MeshModule.h" #include "Channels.h" #include "MeshService.h" #include "NodeDB.h" #include "configuration.h" #include "modules/RoutingModule.h" #include #include std::vector *MeshModule::modules; const meshtastic_MeshPacket *MeshModule::currentRequest; uint8_t MeshModule::numPeriodicModules = 0; /** * 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() { auto it = std::find(modules->begin(), modules->end(), this); assert(it != modules->end()); modules->erase(it); } // ⚠️ **Only call once** to set the initial delay before a module starts broadcasting periodically int32_t MeshModule::setStartDelay() { int32_t startDelay = MESHMODULE_MIN_BROADCAST_DELAY_MS + numPeriodicModules * MESHMODULE_BROADCAST_SPACING_MS; numPeriodicModules++; return startDelay; } meshtastic_MeshPacket *MeshModule::allocAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex, uint8_t hopLimit) { meshtastic_Routing c = meshtastic_Routing_init_default; 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 = 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("Discard an unneeded response"); packetPool.release(pi.myReply); pi.myReply = NULL; } if (handled == ProcessMessage::STOP) { LOG_DEBUG("Module '%s' handled and skipped other processing", pi.name); break; } } } pi.currentRequest = NULL; } if (isDecoded && mp.decoded.want_response && toUs) { if (currentReply) { printPacket("Send response", currentReply); service->sendToMesh(currentReply); currentReply = NULL; } else if (mp.from != ourNodeNum && !ignoreRequest) { // Note: if the message started with the local node or a module asked to ignore the request, we don't want to send a // no response reply // No one wanted to reply to this request, tell the requster that happened LOG_DEBUG("No one responded, send a nak"); // SECURITY NOTE! I considered sending back a different error code if we didn't find the psk (i.e. !isDecoded) // but opted NOT TO. Because it is not a good idea to let remote nodes 'probe' to find out which PSKs were "good" vs // bad. routingModule->sendAckNak(meshtastic_Routing_Error_NO_RESPONSE, getFrom(&mp), mp.id, mp.channel, routingModule->getHopLimitForResponse(mp)); } } if (!moduleFound && isDecoded) { LOG_DEBUG("No modules interested in portnum=%d, src=%s", mp.decoded.portnum, (src == RX_SRC_LOCAL) ? "LOCAL" : "REMOTE"); } } meshtastic_MeshPacket *MeshModule::allocReply() { auto r = myReply; myReply = NULL; // Only use each reply once return r; } /** 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); // No need for an ack if we are just delivering locally (it just generates an ignored ack) p->want_ack = (to.from != 0) ? to.want_ack : false; if (p->priority == meshtastic_MeshPacket_Priority_UNSET) p->priority = meshtastic_MeshPacket_Priority_RELIABLE; p->decoded.request_id = to.id; } std::vector MeshModule::GetMeshModulesWithUIFrames(int startIndex) { std::vector modulesWithUIFrames; // Fill with nullptr up to startIndex modulesWithUIFrames.resize(startIndex, nullptr); if (modules) { for (auto i = modules->begin(); i != modules->end(); ++i) { auto &pi = **i; if (pi.wantUIFrame()) { LOG_DEBUG("%s wants a UI Frame", pi.name); modulesWithUIFrames.push_back(&pi); } } } 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 ================================================ FILE: src/mesh/MeshModule.h ================================================ #pragma once #include "mesh/Channels.h" #include "mesh/MeshTypes.h" #include #if HAS_SCREEN #include #include #endif #define MESHMODULE_MIN_BROADCAST_DELAY_MS 30 * 1000 // Min. delay after boot before sending first broadcast by any module #define MESHMODULE_BROADCAST_SPACING_MS 15 * 1000 // Initial spacing between broadcasts of different modules /** 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, Attempt to remain on the same frame throughout SWITCH_TO_TEXTMESSAGE // Jump directly to the Text Message screen } action = REDRAW_ONLY; // We might want to pass additional data inside this struct at some point }; /** 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(int startIndex); static void observeUIEvents(Observer *observer); static AdminMessageHandleResult handleAdminMessageForAllModules(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, meshtastic_AdminMessage *response); #if HAS_SCREEN virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { return; } virtual bool isRequestingFocus(); // Checked by screen, when regenerating frameset virtual bool interceptingKeyboardInput() { return false; } // Can screen use keyboard for nav, or is module handling input? #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; /** * Check if the current request is a multi-hop broadcast. Modules should call this in allocReply() * and return NULL to prevent reply storms from broadcast requests that have already been relayed. */ bool isMultiHopBroadcastRequest() { if (currentRequest && isBroadcast(currentRequest->to) && currentRequest->hop_limit < currentRequest->hop_start) { return true; } return 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; // We keep track of the number of modules that send a periodic broadcast to schedule them spaced out over time static uint8_t numPeriodicModules; // Set the start delay for module that broadcasts periodically int32_t setStartDelay(); /** * 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 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); ================================================ FILE: src/mesh/MeshPacketQueue.cpp ================================================ #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); // If one packet is in the late transmit window, prefer the other one if ((bool)p1->tx_after != (bool)p2->tx_after) { return !p1->tx_after; } auto p1p = getPriority(p1), p2p = getPriority(p2); // If priorities differ, use that // for equal priorities, prefer packets already on mesh. return (p1p != p2p) ? (p1p > p2p) : (!isFromUs(p1) && isFromUs(p2)); } 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, bool *dropped) { // no space - try to replace a lower priority packet in the queue if (queue.size() >= maxLen) { bool replaced = replaceLowerPriorityPacket(p); if (!replaced) { LOG_WARN("TX queue is full, and there is no lower-priority packet available to evict in favour of 0x%08x", p->id); } if (dropped) { *dropped = true; } return replaced; } if (dropped) { *dropped = false; } // Find the correct position using upper_bound to maintain a stable order auto it = std::upper_bound(queue.begin(), queue.end(), p, CompareMeshPacketFunc); queue.insert(it, p); // Insert packet at the found position return true; } meshtastic_MeshPacket *MeshPacketQueue::dequeue() { if (empty()) { return NULL; } 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; } /** Get a packet from this queue. Returns a pointer to the packet, or NULL if not found. */ meshtastic_MeshPacket *MeshPacketQueue::getPacketFromQueue(NodeNum from, PacketId id) { for (auto it = queue.begin(); it != queue.end(); it++) { auto p = (*it); if (getFrom(p) == from && p->id == id) { return p; } } return NULL; } /** Attempt to find and remove a packet from this queue. Returns a pointer to the removed packet, or NULL if not found */ meshtastic_MeshPacket *MeshPacketQueue::remove(NodeNum from, PacketId id, bool tx_normal, bool tx_late, uint8_t hop_limit_lt) { for (auto it = queue.begin(); it != queue.end(); it++) { auto p = (*it); if (getFrom(p) == from && p->id == id && ((tx_normal && !p->tx_after) || (tx_late && p->tx_after)) && (!hop_limit_lt || p->hop_limit < hop_limit_lt)) { queue.erase(it); return p; } } return NULL; } /* Attempt to find a packet from this queue. Return true if it was found. */ bool MeshPacketQueue::find(const NodeNum from, const PacketId id) { return getPacketFromQueue(from, id) != NULL; } /** * Attempt to find a lower-priority packet in the queue and replace it with the provided one. * @return True if the replacement succeeded, false otherwise */ bool MeshPacketQueue::replaceLowerPriorityPacket(meshtastic_MeshPacket *p) { if (queue.empty()) { return false; // No packets to replace } // Check if the packet at the back has a lower priority than the new packet auto *backPacket = queue.back(); if (!backPacket->tx_after && backPacket->priority < p->priority) { LOG_WARN("Dropping packet 0x%08x to make room in the TX queue for higher-priority packet 0x%08x", backPacket->id, p->id); // Remove the back packet queue.pop_back(); packetPool.release(backPacket); // Insert the new packet in the correct order enqueue(p); return true; } if (backPacket->tx_after) { // Check if there's a non-late packet with lower priority auto it = queue.end(); auto refPacket = *--it; for (; refPacket->tx_after && it != queue.begin(); refPacket = *--it) ; if (!refPacket->tx_after && refPacket->priority < p->priority) { LOG_WARN("Dropping non-late packet 0x%08x to make room in the TX queue for higher-priority packet 0x%08x", refPacket->id, p->id); queue.erase(it); packetPool.release(refPacket); // Insert the new packet in the correct order enqueue(p); return true; } } if (backPacket->tx_after) { // Check if there's a late packet at the queue end auto now = millis(); if (backPacket->tx_after < now && (!p->tx_after || backPacket->tx_after > p->tx_after)) { int32_t dt = (int32_t)(backPacket->tx_after - now); if (p->tx_after) { LOG_WARN("Dropping late packet 0x%08x with TX delay %dms to make room in the TX queue for packet 0x%08x with " "TX delay %ums", backPacket->id, dt, p->id, p->tx_after - now); } else { LOG_WARN("Dropping late packet 0x%08x with TX delay %dms to make room in the TX queue for packet 0x%08x " "with no TX delay", backPacket->id, dt, p->id); } queue.pop_back(); packetPool.release(backPacket); // 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; } ================================================ FILE: src/mesh/MeshPacketQueue.h ================================================ #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 * @param dropped Optional pointer to a bool that will be set to true if a packet was dropped */ bool enqueue(meshtastic_MeshPacket *p, bool *dropped = nullptr); /** return true if the queue is empty */ bool empty(); /** return 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(); /** Get a packet from this queue. Returns a pointer to the packet, or NULL if not found. */ meshtastic_MeshPacket *getPacketFromQueue(NodeNum from, PacketId id); /** Attempt to find and remove a packet from this queue. Returns the packet which was removed from the queue */ meshtastic_MeshPacket *remove(NodeNum from, PacketId id, bool tx_normal = true, bool tx_late = true, uint8_t hop_limit_lt = 0); /* Attempt to find a packet from this queue. Return true if it was found. */ bool find(const NodeNum from, const PacketId id); }; ================================================ FILE: src/mesh/MeshRadio.h ================================================ #pragma once #include "MemoryPool.h" #include "MeshTypes.h" #include "PointerQueue.h" #include "configuration.h" // Sentinel marking the end of a modem preset array static constexpr meshtastic_Config_LoRaConfig_ModemPreset MODEM_PRESET_END = static_cast(0xFF); // Region profile: bundles the preset list with regulatory parameters shared across regions struct RegionProfile { const meshtastic_Config_LoRaConfig_ModemPreset *presets; // sentinel-terminated; first entry is the default float spacing; // gaps between radio channels float padding; // padding at each side of the "operating channel" bool audioPermitted; bool licensedOnly; // a region profile for licensed operators only int8_t textThrottle; // throttle for text - future expansion int8_t positionThrottle; // throttle for location data - future expansion int8_t telemetryThrottle; // throttle for telemetry - future expansion uint8_t overrideSlot; // a per-region override slot for if we need to fix it in place }; extern const RegionProfile PROFILE_STD; extern const RegionProfile PROFILE_EU868; extern const RegionProfile PROFILE_UNDEF; // extern const RegionProfile PROFILE_LITE; // extern const RegionProfile PROFILE_NARROW; // extern const RegionProfile PROFILE_HAM; // Map from old region names to new region enums struct RegionInfo { meshtastic_Config_LoRaConfig_RegionCode code; float freqStart; float freqEnd; float dutyCycle; // modified by getEffectiveDutyCycle uint8_t powerLimit; // Or zero for not set bool freqSwitching; bool wideLora; const RegionProfile *profile; const char *name; // EU433 etc // Preset accessors (delegate through profile) meshtastic_Config_LoRaConfig_ModemPreset getDefaultPreset() const { return profile->presets[0]; } const meshtastic_Config_LoRaConfig_ModemPreset *getAvailablePresets() const { return profile->presets; } size_t getNumPresets() const { size_t n = 0; while (profile->presets[n] != MODEM_PRESET_END) n++; return n; } }; extern const RegionInfo regions[]; extern const RegionInfo *myRegion; extern void initRegion(); // Valid LoRa spread factor range and defaults constexpr uint8_t LORA_SF_MIN = 7; constexpr uint8_t LORA_SF_MAX = 12; constexpr uint8_t LORA_SF_DEFAULT = 11; // LONG_FAST default // Valid LoRa coding rate range and default constexpr uint8_t LORA_CR_MIN = 4; constexpr uint8_t LORA_CR_MAX = 8; constexpr uint8_t LORA_CR_DEFAULT = 5; // LONG_FAST default // Default bandwidth in kHz (LONG_FAST) constexpr float LORA_BW_DEFAULT_KHZ = 250.0f; /// Clamp spread factor to the valid LoRa range [7, 12]. /// Out-of-range values (including 0 from unset preset mode) return LORA_SF_DEFAULT. static inline uint8_t clampSpreadFactor(uint8_t sf) { if (sf < LORA_SF_MIN || sf > LORA_SF_MAX) return LORA_SF_DEFAULT; return sf; } /// Clamp coding rate to the valid LoRa range [4, 8]. /// Out-of-range values return LORA_CR_DEFAULT. static inline uint8_t clampCodingRate(uint8_t cr) { if (cr < LORA_CR_MIN || cr > LORA_CR_MAX) return LORA_CR_DEFAULT; return cr; } /// Ensure bandwidth is positive. Non-positive values return LORA_BW_DEFAULT_KHZ. static inline float clampBandwidthKHz(float bwKHz) { if (bwKHz <= 0.0f) return LORA_BW_DEFAULT_KHZ; return bwKHz; } static inline float bwCodeToKHz(uint16_t bwCode) { if (bwCode == 31) return 31.25f; if (bwCode == 62) return 62.5f; if (bwCode == 200) return 203.125f; if (bwCode == 400) return 406.25f; if (bwCode == 800) return 812.5f; if (bwCode == 1600) return 1625.0f; return (float)bwCode; } static inline uint16_t bwKHzToCode(float bwKHz) { if (bwKHz > 31.24f && bwKHz < 31.26f) return 31; if (bwKHz > 62.49f && bwKHz < 62.51f) return 62; if (bwKHz > 203.12f && bwKHz < 203.13f) return 200; if (bwKHz > 406.24f && bwKHz < 406.26f) return 400; if (bwKHz > 812.49f && bwKHz < 812.51f) return 800; if (bwKHz > 1624.99f && bwKHz < 1625.01f) return 1600; return (uint16_t)(bwKHz + 0.5f); } static inline void modemPresetToParams(meshtastic_Config_LoRaConfig_ModemPreset preset, bool wideLora, float &bwKHz, uint8_t &sf, uint8_t &cr) { switch (preset) { case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO: bwKHz = wideLora ? 1625.0f : 500.0f; cr = 5; sf = 7; break; case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST: bwKHz = wideLora ? 812.5f : 250.0f; cr = 5; sf = 7; break; case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW: bwKHz = wideLora ? 812.5f : 250.0f; cr = 5; sf = 8; break; case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST: bwKHz = wideLora ? 812.5f : 250.0f; cr = 5; sf = 9; break; case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW: bwKHz = wideLora ? 812.5f : 250.0f; cr = 5; sf = 10; break; case meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO: bwKHz = wideLora ? 1625.0f : 500.0f; cr = 8; sf = 11; break; case meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE: bwKHz = wideLora ? 406.25f : 125.0f; cr = 8; sf = 11; break; case meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW: bwKHz = wideLora ? 406.25f : 125.0f; cr = 8; sf = 12; break; default: // LONG_FAST (or illegal) bwKHz = wideLora ? 812.5f : 250.0f; cr = 5; sf = 11; break; } } static inline float modemPresetToBwKHz(meshtastic_Config_LoRaConfig_ModemPreset preset, bool wideLora) { float bwKHz = 0; uint8_t sf = 0; uint8_t cr = 0; modemPresetToParams(preset, wideLora, bwKHz, sf, cr); return bwKHz; } ================================================ FILE: src/mesh/MeshService.cpp ================================================ #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 "MessageStore.h" #include "NodeDB.h" #include "PowerFSM.h" #include "RTC.h" #include "TypeConversions.h" #include "graphics/draw/MessageRenderer.h" #include "main.h" #include "mesh-pb-constants.h" #include "meshUtils.h" #include "modules/NodeInfoModule.h" #include "modules/PositionModule.h" #include "modules/RoutingModule.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; #define MAX_MQTT_PROXY_MESSAGES 16 static MemoryPool staticMqttClientProxyMessagePool; #define MAX_QUEUE_STATUS 4 static MemoryPool staticQueueStatusPool; #define MAX_CLIENT_NOTIFICATIONS 4 static MemoryPool staticClientNotificationPool; Allocator &mqttClientProxyMessagePool = staticMqttClientProxyMessagePool; Allocator &clientNotificationPool = staticClientNotificationPool; Allocator &queueStatusPool = staticQueueStatusPool; #include "Router.h" MeshService::MeshService() #ifdef ARCH_PORTDUINO : toPhoneQueue(MAX_RX_TOPHONE), toPhoneQueueStatusQueue(MAX_RX_QUEUESTATUS_TOPHONE), toPhoneMqttProxyQueue(MAX_RX_MQTTPROXY_TOPHONE), toPhoneClientNotificationQueue(MAX_RX_NOTIFICATION_TOPHONE) #endif { lastQueueStatus = {0, 0, 16, 0}; } 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 bool isPreferredRebroadcaster = IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_ROUTER, meshtastic_Config_DeviceConfig_Role_ROUTER_LATE, meshtastic_Config_DeviceConfig_Role_CLIENT_BASE); if (mp->which_payload_variant == meshtastic_MeshPacket_decoded_tag && mp->decoded.portnum == meshtastic_PortNum_TELEMETRY_APP && mp->decoded.request_id > 0) { LOG_DEBUG("Received telemetry response. Skip sending our NodeInfo"); // ignore our request for its NodeInfo } else if (mp->which_payload_variant == meshtastic_MeshPacket_decoded_tag && !nodeDB->getMeshNode(mp->from)->has_user && nodeInfoModule && !isPreferredRebroadcaster && !nodeDB->isFull()) { if (airTime->isTxAllowedChannelUtil(true)) { const int8_t hopsUsed = getHopsAway(*mp, config.lora.hop_limit); if (hopsUsed > (int32_t)(config.lora.hop_limit + 2)) { LOG_DEBUG("Skip send NodeInfo: %d hops away is too far away", hopsUsed); } else { LOG_INFO("Heard new node on ch. %d, send NodeInfo and ask for response", mp->channel); nodeInfoModule->sendOurNodeInfo(mp->from, true, mp->channel); } } else { LOG_DEBUG("Skip sending NodeInfo > 25%% ch. util"); } } printPacket("Forwarding to phone", mp); sendToPhone(packetPool.allocCopy(*mp)); 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 void MeshService::reloadConfig(int saveWhat) { // If we can successfully set this radio to these settings, save them to disk // This will also update the region as needed nodeDB->resetRadioConfig(); // Don't let the phone send us fatally bad settings configChanged.notifyObservers(NULL); // This will cause radio hardware to change freqs etc nodeDB->saveToDisk(saveWhat); } /// The owner User record just got updated, update our node DB and broadcast the info into the mesh void MeshService::reloadOwner(bool shouldSave) { // LOG_DEBUG("reloadOwner()"); // update our local data directly nodeDB->updateUser(nodeDB->getNodeNum(), owner); assert(nodeInfoModule); // update everyone else and save to disk if (nodeInfoModule && shouldSave) { nodeInfoModule->sendOurNodeInfo(); } } // 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) if (SimRadio::instance && p.decoded.portnum == meshtastic_PortNum_SIMULATOR_APP) { // Simulates device received a packet via the LoRa chip SimRadio::instance->unpackAndReceive(p); return; } #endif p.from = 0; // We don't let clients assign nodenums to their sent messages p.next_hop = NO_NEXT_HOP_PREFERENCE; // We don't let clients assign next_hop to their sent messages p.relay_node = NO_RELAY_NODE; // We don't let clients assign relay_node to their sent messages 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 IF_SCREEN(if (p.decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP && p.decoded.payload.size > 0 && p.to != NODENUM_BROADCAST && p.to != 0) // DM only { perhapsDecode(&p); const StoredMessage &sm = messageStore.addFromPacket(p); graphics::MessageRenderer::handleNewMessage(nullptr, sm, p); // notify UI }) // Send the packet into the mesh DEBUG_HEAP_BEFORE; auto a = packetPool.allocCopy(p); DEBUG_HEAP_AFTER("MeshService::handleToRadio", a); sendToMesh(a, RX_SRC_USER); bool loopback = false; // if true send any packet the phone sends back itself (for testing) if (loopback) { // no need to copy anymore because handle from radio assumes it should _not_ delete // packetPool.allocCopy(r.variant.packet); handleFromRadio(&p); // handleFromRadio will tell the phone a new packet arrived } } /** Attempt to cancel a previously sent packet from this _local_ node. Returns true if a packet was found we could cancel */ bool MeshService::cancelSending(PacketId id) { return router->cancelSending(nodeDB->getNodeNum(), id); } ErrorCode MeshService::sendQueueStatusToPhone(const meshtastic_QueueStatus &qs, ErrorCode res, uint32_t mesh_packet_id) { meshtastic_QueueStatus *copied = queueStatusPool.allocCopy(qs); copied->res = res; copied->mesh_packet_id = mesh_packet_id; if (toPhoneQueueStatusQueue.numFree() == 0) { LOG_INFO("tophone queue status queue is full, discard oldest"); meshtastic_QueueStatus *d = toPhoneQueueStatusQueue.dequeuePtr(0); if (d) releaseQueueStatusToPool(d); } lastQueueStatus = *copied; res = toPhoneQueueStatusQueue.enqueue(copied, 0); fromNum++; return res ? ERRNO_OK : ERRNO_UNKNOWN; } void MeshService::sendToMesh(meshtastic_MeshPacket *p, RxSource src, bool ccToPhone) { uint32_t mesh_packet_id = p->id; nodeDB->updateFrom(*p); // update our local DB for this packet (because phone might have sent position packets etc...) // Note: We might return !OK if our fifo was full, at that point the only option we have is to drop it ErrorCode res = router->sendLocal(p, src); /* NOTE(pboldin): Prepare and send QueueStatus message to the phone as a * high-priority message. */ meshtastic_QueueStatus qs = router->getQueueStatus(); ErrorCode r = sendQueueStatusToPhone(qs, res, mesh_packet_id); if (r != ERRNO_OK) { LOG_DEBUG("Can't send status to phone"); } if ((res == ERRNO_OK || res == ERRNO_SHOULD_RELEASE) && ccToPhone) { // Check if p is not released in case it couldn't be sent DEBUG_HEAP_BEFORE; auto a = packetPool.allocCopy(*p); DEBUG_HEAP_AFTER("MeshService::sendToMesh", a); sendToPhone(a); } // Router may ask us to release the packet if it wasn't sent if (res == ERRNO_SHOULD_RELEASE) { releaseToPool(p); } } bool MeshService::trySendPosition(NodeNum dest, bool wantReplies) { meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum()); assert(node); if (nodeDB->hasValidPosition(node)) { #if HAS_GPS && !MESHTASTIC_EXCLUDE_GPS if (positionModule) { if (!config.position.fixed_position && !nodeDB->hasLocalPositionSinceBoot()) { LOG_DEBUG("Skip position ping; no fresh position since boot"); return false; } LOG_INFO("Send position ping to 0x%x, wantReplies=%d, channel=%d", dest, wantReplies, node->channel); positionModule->sendOurPosition(dest, wantReplies, node->channel); return true; } } else { #endif if (nodeInfoModule) { LOG_INFO("Send nodeinfo ping to 0x%x, wantReplies=%d, channel=%d", dest, wantReplies, node->channel); nodeInfoModule->sendOurNodeInfo(dest, wantReplies, node->channel); } } return false; } void MeshService::sendToPhone(meshtastic_MeshPacket *p) { perhapsDecode(p); #ifdef ARCH_ESP32 #if !MESHTASTIC_EXCLUDE_STOREFORWARD if (moduleConfig.store_forward.enabled && storeForwardModule->isServer() && p->decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP) { releaseToPool(p); // Copy is already stored in StoreForward history fromNum++; // Notify observers for packet from radio return; } #endif #endif if (toPhoneQueue.numFree() == 0) { if (p->decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP || p->decoded.portnum == meshtastic_PortNum_RANGE_TEST_APP) { LOG_WARN("ToPhone queue is full, discard oldest"); meshtastic_MeshPacket *d = toPhoneQueue.dequeuePtr(0); if (d) releaseToPool(d); } else { LOG_WARN("ToPhone queue is full, drop packet"); releaseToPool(p); fromNum++; // Make sure to notify observers in case they are reconnected so they can get the packets return; } } if (toPhoneQueue.enqueue(p, 0) == false) { LOG_CRIT("Failed to queue a packet into toPhoneQueue!"); abort(); } fromNum++; } void MeshService::sendMqttMessageToClientProxy(meshtastic_MqttClientProxyMessage *m) { LOG_DEBUG("Send mqtt message on topic '%s' to client for proxy", m->topic); if (toPhoneMqttProxyQueue.numFree() == 0) { LOG_WARN("MqttClientProxyMessagePool queue is full, discard oldest"); meshtastic_MqttClientProxyMessage *d = toPhoneMqttProxyQueue.dequeuePtr(0); if (d) releaseMqttClientProxyMessageToPool(d); } if (toPhoneMqttProxyQueue.enqueue(m, 0) == false) { LOG_CRIT("Failed to queue a packet into toPhoneMqttProxyQueue!"); abort(); } fromNum++; } void MeshService::sendRoutingErrorResponse(meshtastic_Routing_Error error, const meshtastic_MeshPacket *mp) { if (!mp) { LOG_WARN("Cannot send routing error response: null packet"); return; } // Use the routing module to send the error response if (routingModule) { routingModule->sendAckNak(error, mp->from, mp->id, mp->channel); } else { LOG_ERROR("Cannot send routing error response: no routing module"); } } void MeshService::sendClientNotification(meshtastic_ClientNotification *n) { LOG_DEBUG("Send client notification to phone"); if (toPhoneClientNotificationQueue.numFree() == 0) { LOG_WARN("ClientNotification queue is full, discard oldest"); meshtastic_ClientNotification *d = toPhoneClientNotificationQueue.dequeuePtr(0); if (d) releaseClientNotificationToPool(d); } if (toPhoneClientNotificationQueue.enqueue(n, 0) == false) { LOG_CRIT("Failed to queue a notification into toPhoneClientNotificationQueue!"); abort(); } fromNum++; } meshtastic_NodeInfoLite *MeshService::refreshLocalMeshNode() { meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum()); assert(node); // 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_DEBUG LOG_DEBUG("onGPSchanged() - lost validLocation"); #endif } // Used fixed position if configured regardless of GPS lock if (config.position.fixed_position) { LOG_WARN("Use fixed position"); pos = TypeConversions::ConvertToPosition(node->position); } // 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; } ================================================ FILE: src/mesh/MeshService.h ================================================ #pragma once #include #include #include #include "GPSStatus.h" #include "MemoryPool.h" #include "MeshRadio.h" #include "MeshTypes.h" #include "Observer.h" #ifdef ARCH_PORTDUINO #include "PointerQueue.h" #else #include "StaticPointerQueue.h" #endif #include "mesh-pb-constants.h" #if defined(ARCH_PORTDUINO) #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 #ifdef ARCH_PORTDUINO PointerQueue toPhoneQueue; #else StaticPointerQueue toPhoneQueue; #endif // keep list of QueueStatus packets to be send to the phone #ifdef ARCH_PORTDUINO PointerQueue toPhoneQueueStatusQueue; #else StaticPointerQueue toPhoneQueueStatusQueue; #endif // keep list of MqttClientProxyMessages to be send to the client for delivery #ifdef ARCH_PORTDUINO PointerQueue toPhoneMqttProxyQueue; #else StaticPointerQueue toPhoneMqttProxyQueue; #endif // keep list of ClientNotifications to be send to the client (phone) #ifdef ARCH_PORTDUINO PointerQueue toPhoneClientNotificationQueue; #else StaticPointerQueue toPhoneClientNotificationQueue; #endif // 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: enum APIState { STATE_DISCONNECTED, // Initial state, no API is connected STATE_BLE, STATE_WIFI, STATE_SERIAL, STATE_PACKET, STATE_HTTP, STATE_ETH }; APIState api_state = STATE_DISCONNECTED; static bool isTextPayload(const meshtastic_MeshPacket *p) { if (moduleConfig.range_test.enabled && p->decoded.portnum == meshtastic_PortNum_RANGE_TEST_APP) { return true; } return p->decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP || p->decoded.portnum == meshtastic_PortNum_DETECTION_SENSOR_APP || p->decoded.portnum == meshtastic_PortNum_ALERT_APP; } /// 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 */ void reloadConfig(int saveWhat = SEGMENT_CONFIG | SEGMENT_MODULECONFIG | SEGMENT_DEVICESTATE | SEGMENT_CHANNELS); /// The owner User record just got updated, update our node DB and broadcast the info into the mesh void reloadOwner(bool shouldSave = true); /// 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 virtual void sendMqttMessageToClientProxy(meshtastic_MqttClientProxyMessage *m); /// Send a ClientNotification to the phone virtual void sendClientNotification(meshtastic_ClientNotification *cn); /// Send an error response to the phone void sendRoutingErrorResponse(meshtastic_Routing_Error error, const meshtastic_MeshPacket *mp); 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; ================================================ FILE: src/mesh/MeshTypes.h ================================================ #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 ERRNO_SHOULD_RELEASE 35 // no error, but the packet should still be released #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 // For old firmware or when falling back to flooding, there is no next-hop preference #define NO_NEXT_HOP_PREFERENCE 0 // For old firmware there is no relay node set #define NO_RELAY_NODE 0 typedef int ErrorCode; /// Alloc and free packets to our global, ISR safe pool extern Allocator &packetPool; using UniquePacketPoolPacket = Allocator::UniqueAllocation; /** * Most (but not always) of the time we want to treat packets 'from' the local phone (where from == 0), as if they originated on * the local node. If from is zero this function returns our node number instead */ 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); ================================================ FILE: src/mesh/NextHopRouter.cpp ================================================ #include "NextHopRouter.h" #include "MeshTypes.h" #include "meshUtils.h" #if !MESHTASTIC_EXCLUDE_TRACEROUTE #include "modules/TraceRouteModule.h" #endif #if HAS_TRAFFIC_MANAGEMENT #include "modules/TrafficManagementModule.h" #endif #include "NodeDB.h" NextHopRouter::NextHopRouter() {} PendingPacket::PendingPacket(meshtastic_MeshPacket *p, uint8_t numRetransmissions) { packet = p; this->numRetransmissions = numRetransmissions - 1; // We subtract one, because we assume the user just did the first send } /** * Send a packet */ ErrorCode NextHopRouter::send(meshtastic_MeshPacket *p) { // Add any messages _we_ send to the seen message list (so we will ignore all retransmissions we see) p->relay_node = nodeDB->getLastByteOfNodeNum(getNodeNum()); // First set the relayer to us wasSeenRecently(p); // FIXME, move this to a sniffSent method p->next_hop = getNextHop(p->to, p->relay_node).value_or(NO_NEXT_HOP_PREFERENCE); // set the next hop LOG_DEBUG("Setting next hop for packet with dest %x to %x", p->to, p->next_hop); // If it's from us, ReliableRouter already handles retransmissions if want_ack is set. If a next hop is set and hop limit is // not 0 or want_ack is set, start retransmissions if ((!isFromUs(p) || !p->want_ack) && p->next_hop != NO_NEXT_HOP_PREFERENCE && (p->hop_limit > 0 || p->want_ack)) startRetransmission(packetPool.allocCopy(*p)); // start retransmission for relayed packet return Router::send(p); } bool NextHopRouter::shouldFilterReceived(const meshtastic_MeshPacket *p) { bool wasFallback = false; bool weWereNextHop = false; bool wasUpgraded = false; bool seenRecently = wasSeenRecently(p, true, &wasFallback, &weWereNextHop, &wasUpgraded); // Updates history; returns false when an upgrade is detected // Handle hop_limit upgrade scenario for rebroadcasters if (wasUpgraded && perhapsHandleUpgradedPacket(p)) { return true; // we handled it, so stop processing } if (seenRecently) { printPacket("Ignore dupe incoming msg", p); if (p->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA) { rxDupe++; stopRetransmission(p->from, p->id); } // If it was a fallback to flooding, try to relay again if (wasFallback) { LOG_INFO("Fallback to flooding from relay_node=0x%x", p->relay_node); // Check if it's still in the Tx queue, if not, we have to relay it again if (!findInTxQueue(p->from, p->id)) { reprocessPacket(p); perhapsRebroadcast(p); } } else { bool isRepeated = getHopsAway(*p) == 0; // If repeated and not in Tx queue anymore, try relaying again, or if we are the destination, send the ACK again if (isRepeated) { if (!findInTxQueue(p->from, p->id)) { reprocessPacket(p); if (!perhapsRebroadcast(p) && isToUs(p) && p->want_ack) { sendAckNak(meshtastic_Routing_Error_NONE, getFrom(p), p->id, p->channel, 0); } } } else if (!weWereNextHop) { perhapsCancelDupe(p); // If it's a dupe, cancel relay if we were not explicitly asked to relay } } return true; } return Router::shouldFilterReceived(p); } void NextHopRouter::sniffReceived(const meshtastic_MeshPacket *p, const meshtastic_Routing *c) { NodeNum ourNodeNum = getNodeNum(); uint8_t ourRelayID = nodeDB->getLastByteOfNodeNum(ourNodeNum); bool isAckorReply = (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) && (p->decoded.request_id != 0 || p->decoded.reply_id != 0); if (isAckorReply) { // Update next-hop for the original transmitter of this successful transmission to the relay node, but ONLY if "from" // is not 0 (means implicit ACK) and original packet was also relayed by this node, or we sent it directly to the // destination if (p->from != 0) { meshtastic_NodeInfoLite *origTx = nodeDB->getMeshNode(p->from); if (origTx) { // Either relayer of ACK was also a relayer of the packet, or we were the *only* relayer and the ACK came // directly from the destination bool wasAlreadyRelayer = wasRelayer(p->relay_node, p->decoded.request_id, p->to); bool weWereSoleRelayer = false; bool weWereRelayer = wasRelayer(ourRelayID, p->decoded.request_id, p->to, &weWereSoleRelayer); if ((weWereRelayer && wasAlreadyRelayer) || (getHopsAway(*p) == 0 && weWereSoleRelayer)) { if (origTx->next_hop != p->relay_node) { // Not already set LOG_INFO("Update next hop of 0x%x to 0x%x based on ACK/reply (was relayer %d we were sole %d)", p->from, p->relay_node, wasAlreadyRelayer, weWereSoleRelayer); origTx->next_hop = p->relay_node; } } } } if (!isToUs(p)) { Router::cancelSending(p->to, p->decoded.request_id); // cancel rebroadcast for this DM // stop retransmission for the original packet stopRetransmission(p->to, p->decoded.request_id); // for original packet, from = to and id = request_id } } perhapsRebroadcast(p); // handle the packet as normal Router::sniffReceived(p, c); } /* Check if we should be rebroadcasting this packet if so, do so. */ bool NextHopRouter::perhapsRebroadcast(const meshtastic_MeshPacket *p) { // Check if traffic management wants to exhaust this packet's hops bool exhaustHops = false; #if HAS_TRAFFIC_MANAGEMENT if (trafficManagementModule && trafficManagementModule->shouldExhaustHops(*p)) { exhaustHops = true; } #endif // Allow rebroadcast if hop_limit > 0 OR if we're exhausting hops (which sets hop_limit = 0 but still needs one relay) if (!isToUs(p) && !isFromUs(p) && (p->hop_limit > 0 || exhaustHops)) { if (p->id != 0) { if (isRebroadcaster()) { if (p->next_hop == NO_NEXT_HOP_PREFERENCE || p->next_hop == nodeDB->getLastByteOfNodeNum(getNodeNum())) { meshtastic_MeshPacket *tosend = packetPool.allocCopy(*p); // keep a copy because we will be sending it LOG_INFO("Rebroadcast received message coming from %x", p->relay_node); // If exhausting hops, force hop_limit = 0 regardless of other logic if (exhaustHops) { tosend->hop_limit = 0; LOG_INFO("Traffic management: exhausting hops for 0x%08x, setting hop_limit=0", getFrom(p)); } else if (shouldDecrementHopLimit(p)) { // Use shared logic to determine if hop_limit should be decremented tosend->hop_limit--; // bump down the hop count } else { LOG_INFO("favorite-ROUTER/CLIENT_BASE-to-ROUTER/CLIENT_BASE rebroadcast: preserving hop_limit"); } #if USERPREFS_EVENT_MODE if (tosend->hop_limit > 2) { // if we are "correcting" the hop_limit, "correct" the hop_start by the same amount to preserve hops away. tosend->hop_start -= (tosend->hop_limit - 2); tosend->hop_limit = 2; } #endif if (p->next_hop == NO_NEXT_HOP_PREFERENCE) { FloodingRouter::send(tosend); } else { NextHopRouter::send(tosend); } return true; } } else { LOG_DEBUG("No rebroadcast: Role = CLIENT_MUTE or Rebroadcast Mode = NONE"); } } else { LOG_DEBUG("Ignore 0 id broadcast"); } } return false; } /** * Get the next hop for a destination, given the relay node * @return the node number of the next hop, 0 if no preference (fallback to FloodingRouter) */ std::optional NextHopRouter::getNextHop(NodeNum to, uint8_t relay_node) { if (isBroadcast(to)) return std::nullopt; meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(to); if (node && node->next_hop) { // We are careful not to return the relay node as the next hop if (node->next_hop != relay_node) { // LOG_DEBUG("Next hop for 0x%x is 0x%x", to, node->next_hop); return node->next_hop; } else LOG_WARN("Next hop for 0x%x is 0x%x, same as relayer; set no pref", to, node->next_hop); } return std::nullopt; } PendingPacket *NextHopRouter::findPendingPacket(GlobalPacketId key) { auto old = pending.find(key); // If we have an old record, someone messed up because id got reused if (old != pending.end()) { return &old->second; } else return NULL; } /** * Stop any retransmissions we are doing of the specified node/packet ID pair */ bool NextHopRouter::stopRetransmission(NodeNum from, PacketId id) { auto key = GlobalPacketId(from, id); return stopRetransmission(key); } bool NextHopRouter::roleAllowsCancelingFromTxQueue(const meshtastic_MeshPacket *p) { // Return true if we're allowed to cancel a packet in the txQueue (so we may never transmit it even once) // Return false for roles like ROUTER, ROUTER_LATE which should always transmit the packet at least once. return roleAllowsCancelingDupe(p); // same logic as FloodingRouter::roleAllowsCancelingDupe } bool NextHopRouter::stopRetransmission(GlobalPacketId key) { auto old = findPendingPacket(key); if (old) { auto p = old->packet; /* Only when we already transmitted a packet via LoRa, we will cancel the packet in the Tx queue to avoid canceling a transmission if it was ACKed super fast via MQTT */ if (old->numRetransmissions < NUM_RELIABLE_RETX - 1) { // We only cancel it if we are the original sender or if we're not a router(_late) if (isFromUs(p) || roleAllowsCancelingFromTxQueue(p)) { // remove the 'original' (identified by originator and packet->id) from the txqueue and free it cancelSending(getFrom(p), p->id); } } // Regardless of whether or not we canceled this packet from the txQueue, remove it from our pending list so it // doesn't get scheduled again. (This is the core of stopRetransmission.) auto numErased = pending.erase(key); assert(numErased == 1); // When we remove an entry from pending, always be sure to release the copy of the packet that was allocated in the // call to startRetransmission. packetPool.release(p); return true; } else return false; } /** * Add p to the list of packets to retransmit occasionally. We will free it once we stop retransmitting. */ PendingPacket *NextHopRouter::startRetransmission(meshtastic_MeshPacket *p, uint8_t numReTx) { auto id = GlobalPacketId(p); auto rec = PendingPacket(p, numReTx); 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 NextHopRouter::doRetransmissions() { uint32_t now = millis(); int32_t d = INT32_MAX; // FIXME, we should use a better datastructure rather than walking through this map. // for(auto el: pending) { for (auto it = pending.begin(), nextIt = it; it != pending.end(); it = nextIt) { ++nextIt; // we use this odd pattern because we might be deleting it... auto &p = it->second; bool stillValid = true; // assume we'll keep this record around // FIXME, handle 51 day rolloever here!!! if (p.nextTxMsec <= now) { if (p.numRetransmissions == 0) { if (isFromUs(p.packet)) { LOG_DEBUG("Reliable send failed, returning a nak for fr=0x%x,to=0x%x,id=0x%x", p.packet->from, p.packet->to, p.packet->id); sendAckNak(meshtastic_Routing_Error_MAX_RETRANSMIT, getFrom(p.packet), p.packet->id, p.packet->channel); } // Note: we don't stop retransmission here, instead the Nak packet gets processed in sniffReceived stopRetransmission(it->first); stillValid = false; // just deleted it } else { LOG_DEBUG("Sending retransmission fr=0x%x,to=0x%x,id=0x%x, tries left=%d", p.packet->from, p.packet->to, p.packet->id, p.numRetransmissions); if (!isBroadcast(p.packet->to)) { if (p.numRetransmissions == 1) { // Last retransmission, reset next_hop (fallback to FloodingRouter) p.packet->next_hop = NO_NEXT_HOP_PREFERENCE; // Also reset it in the nodeDB meshtastic_NodeInfoLite *sentTo = nodeDB->getMeshNode(p.packet->to); if (sentTo) { LOG_INFO("Resetting next hop for packet with dest 0x%x\n", p.packet->to); sentTo->next_hop = NO_NEXT_HOP_PREFERENCE; } FloodingRouter::send(packetPool.allocCopy(*p.packet)); } else { NextHopRouter::send(packetPool.allocCopy(*p.packet)); } } else { // Note: we call the superclass version because we don't want to have our version of send() add a new // retransmission record FloodingRouter::send(packetPool.allocCopy(*p.packet)); } // Queue again --p.numRetransmissions; setNextTx(&p); } } if (stillValid) { // Update our desired sleep delay int32_t t = p.nextTxMsec - now; d = min(t, d); } } return d; } void NextHopRouter::setNextTx(PendingPacket *pending) { assert(iface); auto d = iface->getRetransmissionMsec(pending->packet); pending->nextTxMsec = millis() + d; LOG_DEBUG("Setting next retransmission in %u msecs: ", d); printPacket("", pending->packet); setReceivedMessage(); // Run ASAP, so we can figure out our correct sleep time } ================================================ FILE: src/mesh/NextHopRouter.h ================================================ #pragma once #include "FloodingRouter.h" #include #include /** * An identifier for a globally 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 and counts down. Once zero it will be removed from the list */ uint8_t numRetransmissions = 0; PendingPacket() {} explicit PendingPacket(meshtastic_MeshPacket *p, uint8_t numRetransmissions); }; class GlobalPacketIdHashFunction { public: size_t operator()(const GlobalPacketId &p) const { return (std::hash()(p.node)) ^ (std::hash()(p.id)); } }; /* Router for direct messages, which only relays if it is the next hop for a packet. The next hop is set by the current relayer of a packet, which bases this on information from a previous successful delivery to the destination via flooding. Namely, in the PacketHistory, we keep track of (up to 3) relayers of a packet. When the ACK is delivered back to us via a node that also relayed the original packet, we use that node as next hop for the destination from then on. This makes sure that only when there’s a two-way connection, we assign a next hop. Both the ReliableRouter and NextHopRouter will do retransmissions (the NextHopRouter only 1 time). For the final retry, if no one actually relayed the packet, it will reset the next hop in order to fall back to the FloodingRouter again. Note that thus also intermediate hops will do a single retransmission if the intended next-hop didn’t relay, in order to fix changes in the middle of the route. */ class NextHopRouter : public FloodingRouter { public: /** * Constructor * */ NextHopRouter(); /** * Send a packet * @return an error code */ virtual ErrorCode send(meshtastic_MeshPacket *p) override; /** Do our retransmission handling */ virtual int32_t runOnce() override { // Note: We must doRetransmissions FIRST, because it might queue up work for the base class runOnce implementation doRetransmissions(); int32_t r = FloodingRouter::runOnce(); // Also after calling runOnce there might be new packets to retransmit auto d = doRetransmissions(); return min(d, r); } // The number of retransmissions intermediate nodes will do (actually 1 less than this) constexpr static uint8_t NUM_INTERMEDIATE_RETX = 2; // The number of retransmissions the original sender will do constexpr static uint8_t NUM_RELIABLE_RETX = 3; protected: /** * Pending retransmissions */ std::unordered_map pending; /** * Should this incoming filter be dropped? * * Called immediately on reception, before any further processing. * @return true to abandon the packet */ virtual bool shouldFilterReceived(const meshtastic_MeshPacket *p) override; /** * Look for packets we need to relay */ virtual void sniffReceived(const meshtastic_MeshPacket *p, const meshtastic_Routing *c) override; /** * Try to find the pending packet record for this ID (or NULL if not found) */ PendingPacket *findPendingPacket(NodeNum from, PacketId id) { return findPendingPacket(GlobalPacketId(from, id)); } PendingPacket *findPendingPacket(GlobalPacketId p); /** * Add p to the list of packets to retransmit occasionally. We will free it once we stop retransmitting. */ PendingPacket *startRetransmission(meshtastic_MeshPacket *p, uint8_t numReTx = NUM_INTERMEDIATE_RETX); // Return true if we're allowed to cancel a packet in the txQueue (so we may never transmit it even once) bool roleAllowsCancelingFromTxQueue(const meshtastic_MeshPacket *p); /** * 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); private: /** * Get the next hop for a destination, given the relay node * @return the node number of the next hop, 0 if no preference (fallback to FloodingRouter) */ std::optional getNextHop(NodeNum to, uint8_t relay_node); /** Check if we should be rebroadcasting this packet if so, do so. * @return true if we did rebroadcast */ bool perhapsRebroadcast(const meshtastic_MeshPacket *p) override; }; ================================================ FILE: src/mesh/NodeDB.cpp ================================================ #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 "MeshService.h" #include "NodeDB.h" #include "PacketHistory.h" #include "PowerFSM.h" #include "RTC.h" #include "RadioInterface.h" #include "Router.h" #include "SPILock.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 #if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_WIFI #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_NodeDatabase nodeDatabase; meshtastic_LocalConfig config; meshtastic_DeviceUIConfig uiconfig{.screen_brightness = 153, .screen_timeout = 30}; meshtastic_LocalModuleConfig moduleConfig; meshtastic_ChannelFile channelFile; #ifdef USERPREFS_USE_ADMIN_KEY_0 static unsigned char userprefs_admin_key_0[] = USERPREFS_USE_ADMIN_KEY_0; #endif #ifdef USERPREFS_USE_ADMIN_KEY_1 static unsigned char userprefs_admin_key_1[] = USERPREFS_USE_ADMIN_KEY_1; #endif #ifdef USERPREFS_USE_ADMIN_KEY_2 static unsigned char userprefs_admin_key_2[] = USERPREFS_USE_ADMIN_KEY_2; #endif #ifdef HELTEC_MESH_NODE_T114 uint32_t read8(uint8_t bits, uint8_t dummy, uint8_t cs, uint8_t sck, uint8_t mosi, uint8_t dc, uint8_t rst) { uint32_t ret = 0; uint8_t SDAPIN = mosi; pinMode(SDAPIN, INPUT_PULLUP); digitalWrite(dc, HIGH); for (int i = 0; i < dummy; i++) { // any dummy clocks digitalWrite(sck, HIGH); delay(1); digitalWrite(sck, LOW); delay(1); } for (int i = 0; i < bits; i++) { // read results ret <<= 1; delay(1); if (digitalRead(SDAPIN)) ret |= 1; ; digitalWrite(sck, HIGH); delay(1); digitalWrite(sck, LOW); } return ret; } void write9(uint8_t val, uint8_t dc_val, uint8_t cs, uint8_t sck, uint8_t mosi, uint8_t dc, uint8_t rst) { pinMode(mosi, OUTPUT); digitalWrite(dc, dc_val); for (int i = 0; i < 8; i++) { // send command digitalWrite(mosi, (val & 0x80) != 0); delay(1); digitalWrite(sck, HIGH); delay(1); digitalWrite(sck, LOW); val <<= 1; } } uint32_t readwrite8(uint8_t cmd, uint8_t bits, uint8_t dummy, uint8_t cs, uint8_t sck, uint8_t mosi, uint8_t dc, uint8_t rst) { digitalWrite(cs, LOW); write9(cmd, 0, cs, sck, mosi, dc, rst); uint32_t ret = read8(bits, dummy, cs, sck, mosi, dc, rst); digitalWrite(cs, HIGH); return ret; } uint32_t get_st7789_id(uint8_t cs, uint8_t sck, uint8_t mosi, uint8_t dc, uint8_t rst) { pinMode(cs, OUTPUT); digitalWrite(cs, HIGH); pinMode(cs, OUTPUT); pinMode(sck, OUTPUT); pinMode(mosi, OUTPUT); pinMode(dc, OUTPUT); pinMode(rst, OUTPUT); digitalWrite(rst, LOW); // Hardware Reset delay(10); digitalWrite(rst, HIGH); delay(10); readwrite8(0x04, 24, 1, cs, sck, mosi, dc, rst); uint32_t ID = readwrite8(0x04, 24, 1, cs, sck, mosi, dc, rst); // ST7789 needs twice return ID; } #endif bool meshtastic_NodeDatabase_callback(pb_istream_t *istream, pb_ostream_t *ostream, const pb_field_iter_t *field) { if (ostream) { std::vector const *vec = (std::vector *)field->pData; for (auto item : *vec) { if (!pb_encode_tag_for_field(ostream, field)) return false; pb_encode_submessage(ostream, meshtastic_NodeInfoLite_fields, &item); } } 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("Init NodeDB"); loadFromDisk(); cleanupMeshDB(); uint32_t devicestateCRC = crc32Buffer(&devicestate, sizeof(devicestate)); uint32_t nodeDatabaseCRC = crc32Buffer(&nodeDatabase, sizeof(nodeDatabase)); uint32_t configCRC = crc32Buffer(&config, sizeof(config)); uint32_t channelFileCRC = crc32Buffer(&channelFile, sizeof(channelFile)); int saveWhat = 0; // Get device unique id #if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C6) uint32_t unique_id[4]; // ESP32 factory burns a unique id in efuse for S2+ series and evidently C3+ series // This is used for HMACs in the esp-rainmaker AIOT platform and seems to be a good choice for us esp_err_t err = esp_efuse_read_field_blob(ESP_EFUSE_OPTIONAL_UNIQUE_ID, unique_id, sizeof(unique_id) * 8); if (err == ESP_OK) { memcpy(myNodeInfo.device_id.bytes, unique_id, sizeof(unique_id)); myNodeInfo.device_id.size = 16; } else { LOG_WARN("Failed to read unique id from efuse"); } #elif defined(ARCH_NRF52) // Nordic applies a FIPS compliant Random ID to each chip at the factory // We concatenate the device address to the Random ID to create a unique ID for now // This will likely utilize a crypto module in the future uint64_t device_id_start = ((uint64_t)NRF_FICR->DEVICEID[1] << 32) | NRF_FICR->DEVICEID[0]; uint64_t device_id_end = ((uint64_t)NRF_FICR->DEVICEADDR[1] << 32) | NRF_FICR->DEVICEADDR[0]; memcpy(myNodeInfo.device_id.bytes, &device_id_start, sizeof(device_id_start)); memcpy(myNodeInfo.device_id.bytes + sizeof(device_id_start), &device_id_end, sizeof(device_id_end)); myNodeInfo.device_id.size = 16; // Uncomment below to print the device id #elif ARCH_PORTDUINO if (portduino_config.has_device_id) { memcpy(myNodeInfo.device_id.bytes, portduino_config.device_id, 16); myNodeInfo.device_id.size = 16; } #else // FIXME - implement for other platforms #endif // if (myNodeInfo.device_id.size == 16) { // std::string deviceIdHex; // for (size_t i = 0; i < myNodeInfo.device_id.size; ++i) { // char buf[3]; // snprintf(buf, sizeof(buf), "%02X", myNodeInfo.device_id.bytes[i]); // deviceIdHex += buf; // } // LOG_DEBUG("Device ID (HEX): %s", deviceIdHex.c_str()); // } // likewise - we always want the app requirements to come from the running appload myNodeInfo.min_app_version = 30200; // format is Mmmss (where M is 1+the numeric major number. i.e. 30200 means 2.2.00 // Note! We do this after loading saved settings, so that if somehow an invalid nodenum was stored in preferences we won't // keep using that nodenum forever. Crummy guess at our nodenum (but we will check against the nodedb to avoid conflicts) pickNewNodeNum(); // Set our board type so we can share it with others owner.hw_model = HW_VENDOR; // Ensure user (nodeinfo) role is set to whatever we're configured to owner.role = config.device.role; // Ensure macaddr is set to our macaddr as it will be copied in our info below memcpy(owner.macaddr, ourMacAddr, sizeof(owner.macaddr)); // Ensure owner.id is always derived from the node number snprintf(owner.id, sizeof(owner.id), "!%08x", getNodeNum()); if (!config.has_security) { config.has_security = true; config.security = meshtastic_Config_SecurityConfig_init_default; config.security.serial_enabled = config.device.serial_enabled; config.security.is_managed = config.device.is_managed; } #if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN || MESHTASTIC_EXCLUDE_PKI) if (!owner.is_licensed && config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_UNSET) { bool keygenSuccess = false; keyIsLowEntropy = checkLowEntropyPublicKey(config.security.public_key); if (config.security.private_key.size == 32 && !keyIsLowEntropy) { if (crypto->regeneratePublicKey(config.security.public_key.bytes, config.security.private_key.bytes)) { keygenSuccess = true; } } else { crypto->generateKeyPair(config.security.public_key.bytes, config.security.private_key.bytes); keygenSuccess = true; } if (keygenSuccess) { config.security.public_key.size = 32; config.security.private_key.size = 32; owner.public_key.size = 32; memcpy(owner.public_key.bytes, config.security.public_key.bytes, 32); } } #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 // Include our owner in the node db under our nodenum meshtastic_NodeInfoLite *info = getOrCreateMeshNode(getNodeNum()); info->user = TypeConversions::ConvertToUserLite(owner); info->has_user = true; // If node database has not been saved for the first time, save it now #ifdef FSCom if (!FSCom.exists(nodeDatabaseFileName)) { saveNodeDatabaseToDisk(); } #endif #ifdef ARCH_ESP32 Preferences preferences; preferences.begin("meshtastic", false); myNodeInfo.reboot_count = preferences.getUInt("rebootCounter", 0); preferences.end(); LOG_DEBUG("Number of Device Reboots: %d", myNodeInfo.reboot_count); #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); // Uncomment below to always enable UDP broadcasts // config.network.enabled_protocols = meshtastic_Config_NetworkConfig_ProtocolFlags_UDP_BROADCAST; // If we are setup to broadcast on any default channel slot (with default frequency slot semantics), // ensure that the telemetry intervals are coerced to the role-aware minimum value. if (channels.hasDefaultChannel()) { LOG_DEBUG("Coerce telemetry to role-aware minimum 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); } // Enforce position broadcast minimums if we would send positions over a default channel // Check channels the same way PositionModule::sendOurPosition() does - first channel with position_precision set bool positionUsesDefaultChannel = false; for (uint8_t i = 0; i < channels.getNumChannels(); i++) { if (channels.getByIndex(i).settings.has_module_settings && channels.getByIndex(i).settings.module_settings.position_precision != 0) { positionUsesDefaultChannel = channels.isDefaultChannel(i); break; } } if (positionUsesDefaultChannel) { LOG_DEBUG("Coerce position broadcasts to role-aware minimum and smart broadcast min of 5 minutes on defaults"); config.position.position_broadcast_secs = Default::getConfiguredOrMinimumValue(config.position.position_broadcast_secs, min_default_broadcast_interval_secs); config.position.broadcast_smart_minimum_interval_secs = Default::getConfiguredOrMinimumValue( config.position.broadcast_smart_minimum_interval_secs, min_default_broadcast_smart_minimum_interval_secs); } // FIXME: UINT32_MAX intervals overflows Apple clients until they are fully patched if (config.device.node_info_broadcast_secs > MAX_INTERVAL) config.device.node_info_broadcast_secs = MAX_INTERVAL; if (config.position.position_broadcast_secs > MAX_INTERVAL) config.position.position_broadcast_secs = MAX_INTERVAL; if (config.position.gps_update_interval > MAX_INTERVAL) config.position.gps_update_interval = MAX_INTERVAL; if (config.position.gps_attempt_time > MAX_INTERVAL) config.position.gps_attempt_time = MAX_INTERVAL; if (config.position.position_flags > MAX_INTERVAL) config.position.position_flags = MAX_INTERVAL; if (config.position.rx_gpio > MAX_INTERVAL) config.position.rx_gpio = MAX_INTERVAL; if (config.position.tx_gpio > MAX_INTERVAL) config.position.tx_gpio = MAX_INTERVAL; if (config.position.broadcast_smart_minimum_distance > MAX_INTERVAL) config.position.broadcast_smart_minimum_distance = MAX_INTERVAL; if (config.position.broadcast_smart_minimum_interval_secs > MAX_INTERVAL) config.position.broadcast_smart_minimum_interval_secs = MAX_INTERVAL; if (config.position.gps_en_gpio > MAX_INTERVAL) config.position.gps_en_gpio = MAX_INTERVAL; if (moduleConfig.neighbor_info.update_interval > MAX_INTERVAL) moduleConfig.neighbor_info.update_interval = MAX_INTERVAL; if (moduleConfig.telemetry.device_update_interval > MAX_INTERVAL) moduleConfig.telemetry.device_update_interval = MAX_INTERVAL; if (moduleConfig.telemetry.environment_update_interval > MAX_INTERVAL) moduleConfig.telemetry.environment_update_interval = MAX_INTERVAL; if (moduleConfig.telemetry.air_quality_interval > MAX_INTERVAL) moduleConfig.telemetry.air_quality_interval = MAX_INTERVAL; if (moduleConfig.telemetry.health_update_interval > MAX_INTERVAL) moduleConfig.telemetry.health_update_interval = MAX_INTERVAL; if (moduleConfig.mqtt.has_map_report_settings && moduleConfig.mqtt.map_report_settings.publish_interval_secs < default_map_publish_interval_secs) { moduleConfig.mqtt.map_report_settings.publish_interval_secs = default_map_publish_interval_secs; } // Ensure that the neighbor info update interval is coerced to the minimum moduleConfig.neighbor_info.update_interval = Default::getConfiguredOrMinimumValue(moduleConfig.neighbor_info.update_interval, min_neighbor_info_broadcast_secs); // Don't let licensed users to rebroadcast encrypted packets if (owner.is_licensed) { config.device.rebroadcast_mode = meshtastic_Config_DeviceConfig_RebroadcastMode_LOCAL_ONLY; } #if !HAS_TFT if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { // On a device without MUI, this display mode makes no sense, and will break logic. config.display.displaymode = meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT; config.bluetooth.enabled = true; } #endif if (devicestateCRC != crc32Buffer(&devicestate, sizeof(devicestate))) saveWhat |= SEGMENT_DEVICESTATE; if (nodeDatabaseCRC != crc32Buffer(&nodeDatabase, sizeof(nodeDatabase))) saveWhat |= SEGMENT_NODEDATABASE; if (configCRC != crc32Buffer(&config, sizeof(config))) saveWhat |= SEGMENT_CONFIG; if (channelFileCRC != crc32Buffer(&channelFile, sizeof(channelFile))) saveWhat |= SEGMENT_CHANNELS; if (config.position.gps_enabled) { config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_ENABLED; config.position.gps_enabled = 0; } #ifdef USERPREFS_FIRMWARE_EDITION myNodeInfo.firmware_edition = USERPREFS_FIRMWARE_EDITION; #endif #ifdef USERPREFS_FIXED_GPS if (myNodeInfo.reboot_count == 1) { // Check if First boot ever or after Factory Reset. meshtastic_Position fixedGPS = meshtastic_Position_init_default; #ifdef USERPREFS_FIXED_GPS_LAT fixedGPS.latitude_i = (int32_t)(USERPREFS_FIXED_GPS_LAT * 1e7); fixedGPS.has_latitude_i = true; #endif #ifdef USERPREFS_FIXED_GPS_LON fixedGPS.longitude_i = (int32_t)(USERPREFS_FIXED_GPS_LON * 1e7); fixedGPS.has_longitude_i = true; #endif #ifdef USERPREFS_FIXED_GPS_ALT fixedGPS.altitude = USERPREFS_FIXED_GPS_ALT; fixedGPS.has_altitude = true; #endif #if defined(USERPREFS_FIXED_GPS_LAT) && defined(USERPREFS_FIXED_GPS_LON) fixedGPS.location_source = meshtastic_Position_LocSource_LOC_MANUAL; config.has_position = true; info->has_position = true; info->position = TypeConversions::ConvertToPositionLite(fixedGPS); nodeDB->setLocalPosition(fixedGPS); config.position.fixed_position = true; #endif } #endif sortMeshDB(); saveToDisk(saveWhat); } /** * Most (but not always) of the time we want to treat packets 'from' the local phone (where from == 0), as if they originated on * the local node. If from is zero this function returns our node number instead */ 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; } void NodeDB::resetRadioConfig(bool is_fresh_install) { if (is_fresh_install) { radioGeneration++; } if (channelFile.channels_count != MAX_NUM_CHANNELS) { LOG_INFO("Set default channel and radio preferences!"); channels.initDefaults(); } channels.onConfigChanged(); // Update the global myRegion initRegion(); } bool NodeDB::factoryReset(bool eraseBleBonds) { LOG_INFO("Perform factory reset!"); // first, remove the "/prefs" (this removes most prefs) spiLock->lock(); rmDir("/prefs"); // this uses spilock internally... #ifdef FSCom if (FSCom.exists("/static/rangetest.csv") && !FSCom.remove("/static/rangetest.csv")) { LOG_ERROR("Could not remove rangetest.csv file"); } #endif spiLock->unlock(); // second, install default state (this will deal with the duplicate mac address issue) installDefaultNodeDatabase(); installDefaultDeviceState(); installDefaultConfig(!eraseBleBonds); // Also preserve the private key if we're not erasing BLE bonds installDefaultModuleConfig(); installDefaultChannels(); // third, write everything to disk saveToDisk(); if (eraseBleBonds) { LOG_INFO("Erase BLE bonds"); #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 LOG_INFO("Clear bluetooth bonds!"); bond_print_list(BLE_GAP_ROLE_PERIPH); bond_print_list(BLE_GAP_ROLE_CENTRAL); Bluefruit.Periph.clearBonds(); Bluefruit.Central.clearBonds(); #endif } return true; } void NodeDB::installDefaultNodeDatabase() { LOG_DEBUG("Install default NodeDatabase"); nodeDatabase.version = DEVICESTATE_CUR_VER; nodeDatabase.nodes = std::vector(MAX_NUM_NODES); numMeshNodes = 0; meshNodes = &nodeDatabase.nodes; } void NodeDB::installDefaultConfig(bool preserveKey = false) { uint8_t private_key_temp[32]; bool shouldPreserveKey = preserveKey && config.has_security && config.security.private_key.size > 0; if (shouldPreserveKey) { memcpy(private_key_temp, config.security.private_key.bytes, config.security.private_key.size); } LOG_INFO("Install default LocalConfig"); memset(&config, 0, sizeof(meshtastic_LocalConfig)); config.version = DEVICESTATE_CUR_VER; config.has_device = true; config.has_display = true; config.has_lora = true; config.has_position = true; config.has_power = true; config.has_network = true; config.has_bluetooth = (HAS_BLUETOOTH ? true : false); config.has_security = true; config.device.rebroadcast_mode = meshtastic_Config_DeviceConfig_RebroadcastMode_ALL; config.lora.sx126x_rx_boosted_gain = true; config.lora.tx_enabled = true; // FIXME: maybe false in the future, and setting region to enable it. (unset region forces it off) config.lora.override_duty_cycle = false; config.lora.config_ok_to_mqtt = false; #if HAS_LORA_FEM config.lora.fem_lna_mode = meshtastic_Config_LoRaConfig_FEM_LNA_Mode_ENABLED; #else config.lora.fem_lna_mode = meshtastic_Config_LoRaConfig_FEM_LNA_Mode_NOT_PRESENT; #endif #if HAS_TFT // For the devices that support MUI, default to that config.display.displaymode = meshtastic_Config_DisplayConfig_DisplayMode_COLOR; #endif #if defined(TFT_WIDTH) && defined(TFT_HEIGHT) && (TFT_WIDTH >= 200 || TFT_HEIGHT >= 200) config.display.enable_message_bubbles = true; #endif #ifdef USERPREFS_CONFIG_DEVICE_ROLE // Restrict ROUTER*, LOST AND FOUND roles for security reasons if (IS_ONE_OF(USERPREFS_CONFIG_DEVICE_ROLE, meshtastic_Config_DeviceConfig_Role_ROUTER, meshtastic_Config_DeviceConfig_Role_ROUTER_LATE, meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND)) { LOG_WARN("ROUTER roles are restricted, falling back to CLIENT role"); config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; } else { config.device.role = USERPREFS_CONFIG_DEVICE_ROLE; } #else config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; // Default to client. #endif #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 // Initialize admin_key_count to zero byte numAdminKeys = 0; #ifdef USERPREFS_USE_ADMIN_KEY_0 // Check if USERPREFS_ADMIN_KEY_0 is non-empty if (sizeof(userprefs_admin_key_0) > 0) { memcpy(config.security.admin_key[0].bytes, userprefs_admin_key_0, 32); config.security.admin_key[0].size = 32; numAdminKeys++; } #endif #ifdef USERPREFS_USE_ADMIN_KEY_1 // Check if USERPREFS_ADMIN_KEY_1 is non-empty if (sizeof(userprefs_admin_key_1) > 0) { memcpy(config.security.admin_key[1].bytes, userprefs_admin_key_1, 32); config.security.admin_key[1].size = 32; numAdminKeys++; } #endif #ifdef USERPREFS_USE_ADMIN_KEY_2 // Check if USERPREFS_ADMIN_KEY_2 is non-empty if (sizeof(userprefs_admin_key_2) > 0) { memcpy(config.security.admin_key[2].bytes, userprefs_admin_key_2, 32); config.security.admin_key[2].size = 32; numAdminKeys++; } #endif config.security.admin_key_count = numAdminKeys; if (shouldPreserveKey) { config.security.private_key.size = 32; memcpy(config.security.private_key.bytes, private_key_temp, config.security.private_key.size); printBytes("Restored key", config.security.private_key.bytes, config.security.private_key.size); } else { config.security.private_key.size = 0; } config.security.public_key.size = 0; #ifdef PIN_GPS_EN config.position.gps_en_gpio = PIN_GPS_EN; #endif #if defined(USERPREFS_CONFIG_GPS_MODE) config.position.gps_mode = USERPREFS_CONFIG_GPS_MODE; #elif !HAS_GPS || GPS_DEFAULT_NOT_PRESENT config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT; #elif !defined(GPS_RX_PIN) if (config.position.rx_gpio == 0) config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT; else config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_DISABLED; #else config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_ENABLED; #endif #ifdef USERPREFS_CONFIG_SMART_POSITION_ENABLED config.position.position_broadcast_smart_enabled = USERPREFS_CONFIG_SMART_POSITION_ENABLED; #else config.position.position_broadcast_smart_enabled = true; #endif config.position.broadcast_smart_minimum_distance = 100; config.position.broadcast_smart_minimum_interval_secs = default_broadcast_smart_minimum_interval_secs; if (config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER && config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER_LATE) config.device.node_info_broadcast_secs = default_node_info_broadcast_secs; config.security.serial_enabled = true; config.security.admin_channel_enabled = false; resetRadioConfig(true); // This also triggers NodeInfo/Position requests since we're fresh strncpy(config.network.ntp_server, "meshtastic.pool.ntp.org", 32); #if (defined(T_DECK) || defined(T_WATCH_S3) || defined(UNPHONE) || defined(PICOMPUTER_S3) || defined(SENSECAP_INDICATOR) || \ defined(ELECROW_PANEL) || defined(HELTEC_V4_TFT)) && \ HAS_TFT // switch BT off by default; use TFT programming mode or hotkey to enable config.bluetooth.enabled = false; #else // default to bluetooth capability of platform as default config.bluetooth.enabled = true; #endif config.bluetooth.fixed_pin = defaultBLEPin; #if defined(ST7735_CS) || defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7789_CS) || \ defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(ST7796_CS) || defined(USE_SPISSD1306) || \ defined(USE_ST7796) || defined(HACKADAY_COMMUNICATOR) bool hasScreen = true; #ifdef HELTEC_MESH_NODE_T114 uint32_t st7789_id = get_st7789_id(ST7789_NSS, ST7789_SCK, ST7789_SDA, ST7789_RS, ST7789_RESET); if (st7789_id == 0xFFFFFF) { hasScreen = false; } #endif #elif ARCH_PORTDUINO bool hasScreen = false; if (portduino_config.displayPanel) hasScreen = true; else hasScreen = screen_found.port != ScanI2C::I2CPort::NO_I2C; #elif MESHTASTIC_INCLUDE_NICHE_GRAPHICS // See "src/graphics/niche" bool hasScreen = true; // Use random pin for Bluetooth pairing #else bool hasScreen = screen_found.port != ScanI2C::I2CPort::NO_I2C; #endif #ifdef USERPREFS_FIXED_BLUETOOTH config.bluetooth.fixed_pin = USERPREFS_FIXED_BLUETOOTH; config.bluetooth.mode = meshtastic_Config_BluetoothConfig_PairingMode_FIXED_PIN; #else config.bluetooth.mode = hasScreen ? meshtastic_Config_BluetoothConfig_PairingMode_RANDOM_PIN : meshtastic_Config_BluetoothConfig_PairingMode_FIXED_PIN; #endif // for backward compat, default position flags are ALT+MSL config.position.position_flags = (meshtastic_Config_PositionConfig_PositionFlags_ALTITUDE | meshtastic_Config_PositionConfig_PositionFlags_ALTITUDE_MSL | meshtastic_Config_PositionConfig_PositionFlags_SPEED | meshtastic_Config_PositionConfig_PositionFlags_HEADING | meshtastic_Config_PositionConfig_PositionFlags_DOP | meshtastic_Config_PositionConfig_PositionFlags_SATINVIEW); // Set default value for 'Mesh via UDP' #if HAS_UDP_MULTICAST #ifdef USERPREFS_NETWORK_ENABLED_PROTOCOLS config.network.enabled_protocols = USERPREFS_NETWORK_ENABLED_PROTOCOLS; #else config.network.enabled_protocols = 0; #endif #endif #ifdef USERPREFS_NETWORK_WIFI_ENABLED config.network.wifi_enabled = USERPREFS_NETWORK_WIFI_ENABLED; #endif #ifdef USERPREFS_NETWORK_WIFI_SSID strncpy(config.network.wifi_ssid, USERPREFS_NETWORK_WIFI_SSID, sizeof(config.network.wifi_ssid)); #endif #ifdef USERPREFS_NETWORK_WIFI_PSK strncpy(config.network.wifi_psk, USERPREFS_NETWORK_WIFI_PSK, sizeof(config.network.wifi_psk)); #endif #if defined(USERPREFS_NETWORK_IPV6_ENABLED) config.network.ipv6_enabled = USERPREFS_NETWORK_IPV6_ENABLED; #else config.network.ipv6_enabled = default_network_ipv6_enabled; #endif #ifdef DISPLAY_FLIP_SCREEN config.display.flip_screen = true; #endif #ifdef RAK4630 config.display.wake_on_tap_or_motion = true; #endif #if defined(T_WATCH_S3) || defined(SENSECAP_INDICATOR) config.display.screen_on_secs = 30; config.display.wake_on_tap_or_motion = true; #endif #ifdef COMPASS_ORIENTATION config.display.compass_orientation = COMPASS_ORIENTATION; #endif #if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_WIFI if (MeshtasticOTA::isUpdated()) { MeshtasticOTA::recoverConfig(&config.network); } #endif #ifdef USERPREFS_CONFIG_DEVICE_ROLE // Apply role-specific defaults when role is set via user preferences installRoleDefaults(config.device.role); #endif initConfigIntervals(); } void NodeDB::initConfigIntervals() { #ifdef USERPREFS_CONFIG_GPS_UPDATE_INTERVAL config.position.gps_update_interval = USERPREFS_CONFIG_GPS_UPDATE_INTERVAL; #else config.position.gps_update_interval = default_gps_update_interval; #endif #ifdef USERPREFS_CONFIG_POSITION_BROADCAST_INTERVAL config.position.position_broadcast_secs = USERPREFS_CONFIG_POSITION_BROADCAST_INTERVAL; #else config.position.position_broadcast_secs = default_broadcast_interval_secs; #endif config.power.ls_secs = default_ls_secs; config.power.min_wake_secs = default_min_wake_secs; config.power.sds_secs = default_sds_secs; config.power.wait_bluetooth_secs = default_wait_bluetooth_secs; config.display.screen_on_secs = default_screen_on_secs; #if defined(USE_POWERSAVE) config.power.is_power_saving = true; config.display.screen_on_secs = 30; config.power.wait_bluetooth_secs = 30; #endif } void NodeDB::installDefaultModuleConfig() { LOG_INFO("Install default ModuleConfig"); memset(&moduleConfig, 0, sizeof(meshtastic_ModuleConfig)); moduleConfig.version = DEVICESTATE_CUR_VER; moduleConfig.has_mqtt = true; moduleConfig.has_range_test = true; moduleConfig.has_serial = true; moduleConfig.has_store_forward = true; moduleConfig.has_telemetry = true; moduleConfig.has_external_notification = true; #if defined(PIN_BUZZER) || defined(PIN_VIBRATION) || defined(LED_NOTIFICATION) moduleConfig.external_notification.enabled = true; #endif #if defined(PIN_BUZZER) moduleConfig.external_notification.output_buzzer = PIN_BUZZER; moduleConfig.external_notification.use_pwm = true; moduleConfig.external_notification.alert_message_buzzer = true; #endif #if defined(PIN_VIBRATION) moduleConfig.external_notification.output_vibra = PIN_VIBRATION; moduleConfig.external_notification.alert_message_vibra = true; moduleConfig.external_notification.output_ms = 500; #endif #if defined(LED_NOTIFICATION) moduleConfig.external_notification.output = LED_NOTIFICATION; moduleConfig.external_notification.active = LED_STATE_ON; moduleConfig.external_notification.alert_message = true; moduleConfig.external_notification.output_ms = 1000; #endif #if defined(PIN_VIBRATION) moduleConfig.external_notification.nag_timeout = 2; #elif defined(PIN_BUZZER) || defined(LED_NOTIFICATION) moduleConfig.external_notification.nag_timeout = default_ringtone_nag_secs; #endif #ifdef HAS_I2S // Don't worry about the other settings for T-Watch, we'll also use the DRV2056 behavior for notifications moduleConfig.external_notification.enabled = true; moduleConfig.external_notification.use_i2s_as_buzzer = true; moduleConfig.external_notification.alert_message_buzzer = true; #if HAS_TFT if (moduleConfig.external_notification.nag_timeout == default_ringtone_nag_secs) moduleConfig.external_notification.nag_timeout = 0; #else moduleConfig.external_notification.nag_timeout = default_ringtone_nag_secs; #endif #endif #ifdef NANO_G2_ULTRA moduleConfig.external_notification.enabled = true; moduleConfig.external_notification.alert_message = true; moduleConfig.external_notification.output_ms = 100; moduleConfig.external_notification.active = true; #endif #ifdef T_LORA_PAGER moduleConfig.canned_message.updown1_enabled = true; moduleConfig.canned_message.inputbroker_pin_a = ROTARY_A; moduleConfig.canned_message.inputbroker_pin_b = ROTARY_B; moduleConfig.canned_message.inputbroker_pin_press = ROTARY_PRESS; moduleConfig.canned_message.inputbroker_event_cw = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar(28); moduleConfig.canned_message.inputbroker_event_ccw = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar(29); moduleConfig.canned_message.inputbroker_event_press = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT; #endif moduleConfig.has_canned_message = true; #if USERPREFS_MQTT_ENABLED && !MESHTASTIC_EXCLUDE_MQTT moduleConfig.mqtt.enabled = true; #endif #ifdef USERPREFS_MQTT_ADDRESS strncpy(moduleConfig.mqtt.address, USERPREFS_MQTT_ADDRESS, sizeof(moduleConfig.mqtt.address)); #else strncpy(moduleConfig.mqtt.address, default_mqtt_address, sizeof(moduleConfig.mqtt.address)); #endif #ifdef USERPREFS_MQTT_USERNAME strncpy(moduleConfig.mqtt.username, USERPREFS_MQTT_USERNAME, sizeof(moduleConfig.mqtt.username)); #else strncpy(moduleConfig.mqtt.username, default_mqtt_username, sizeof(moduleConfig.mqtt.username)); #endif #ifdef USERPREFS_MQTT_PASSWORD strncpy(moduleConfig.mqtt.password, USERPREFS_MQTT_PASSWORD, sizeof(moduleConfig.mqtt.password)); #else strncpy(moduleConfig.mqtt.password, default_mqtt_password, sizeof(moduleConfig.mqtt.password)); #endif #ifdef USERPREFS_MQTT_ROOT_TOPIC strncpy(moduleConfig.mqtt.root, USERPREFS_MQTT_ROOT_TOPIC, sizeof(moduleConfig.mqtt.root)); #else strncpy(moduleConfig.mqtt.root, default_mqtt_root, sizeof(moduleConfig.mqtt.root)); #endif #ifdef USERPREFS_MQTT_ENCRYPTION_ENABLED moduleConfig.mqtt.encryption_enabled = USERPREFS_MQTT_ENCRYPTION_ENABLED; #else moduleConfig.mqtt.encryption_enabled = default_mqtt_encryption_enabled; #endif #ifdef USERPREFS_MQTT_TLS_ENABLED moduleConfig.mqtt.tls_enabled = USERPREFS_MQTT_TLS_ENABLED; #else moduleConfig.mqtt.tls_enabled = default_mqtt_tls_enabled; #endif 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(); moduleConfig.telemetry.device_update_interval = default_telemetry_broadcast_interval_secs; config.device.rebroadcast_mode = meshtastic_Config_DeviceConfig_RebroadcastMode_CORE_PORTNUMS_ONLY; owner.has_is_unmessagable = true; owner.is_unmessagable = true; } else if (role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE) { moduleConfig.telemetry.device_update_interval = ONE_DAY; owner.has_is_unmessagable = true; owner.is_unmessagable = true; } else if (role == meshtastic_Config_DeviceConfig_Role_SENSOR) { owner.has_is_unmessagable = true; owner.is_unmessagable = true; moduleConfig.telemetry.device_update_interval = default_telemetry_broadcast_interval_secs; moduleConfig.telemetry.environment_measurement_enabled = true; moduleConfig.telemetry.environment_update_interval = 300; } else if (role == meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND) { config.position.position_broadcast_smart_enabled = false; config.position.position_broadcast_secs = 300; // Every 5 minutes } else if (role == meshtastic_Config_DeviceConfig_Role_TAK) { config.device.node_info_broadcast_secs = ONE_DAY; config.position.position_broadcast_smart_enabled = false; config.position.position_broadcast_secs = ONE_DAY; // Remove Altitude MSL from flags since CoTs use HAE (height above ellipsoid) config.position.position_flags = (meshtastic_Config_PositionConfig_PositionFlags_ALTITUDE | meshtastic_Config_PositionConfig_PositionFlags_SPEED | meshtastic_Config_PositionConfig_PositionFlags_HEADING | meshtastic_Config_PositionConfig_PositionFlags_DOP); moduleConfig.telemetry.device_update_interval = ONE_DAY; } else if (role == meshtastic_Config_DeviceConfig_Role_TRACKER) { owner.has_is_unmessagable = true; owner.is_unmessagable = true; moduleConfig.telemetry.device_update_interval = default_telemetry_broadcast_interval_secs; } else if (role == meshtastic_Config_DeviceConfig_Role_TAK_TRACKER) { owner.has_is_unmessagable = true; owner.is_unmessagable = true; config.device.node_info_broadcast_secs = ONE_DAY; config.position.position_broadcast_smart_enabled = true; config.position.position_broadcast_secs = 3 * 60; // Every 3 minutes config.position.broadcast_smart_minimum_distance = 20; config.position.broadcast_smart_minimum_interval_secs = 15; // Remove Altitude MSL from flags since CoTs use HAE (height above ellipsoid) config.position.position_flags = (meshtastic_Config_PositionConfig_PositionFlags_ALTITUDE | meshtastic_Config_PositionConfig_PositionFlags_SPEED | meshtastic_Config_PositionConfig_PositionFlags_HEADING | meshtastic_Config_PositionConfig_PositionFlags_DOP); moduleConfig.telemetry.device_update_interval = ONE_DAY; } else if (role == meshtastic_Config_DeviceConfig_Role_CLIENT_HIDDEN) { config.device.rebroadcast_mode = meshtastic_Config_DeviceConfig_RebroadcastMode_LOCAL_ONLY; config.device.node_info_broadcast_secs = MAX_INTERVAL; config.position.position_broadcast_smart_enabled = false; config.position.position_broadcast_secs = MAX_INTERVAL; moduleConfig.neighbor_info.update_interval = MAX_INTERVAL; moduleConfig.telemetry.device_update_interval = MAX_INTERVAL; moduleConfig.telemetry.environment_update_interval = MAX_INTERVAL; moduleConfig.telemetry.air_quality_interval = MAX_INTERVAL; moduleConfig.telemetry.health_update_interval = MAX_INTERVAL; } } void NodeDB::initModuleConfigIntervals() { // Zero out telemetry intervals so that they coalesce to defaults in Default.h #ifdef USERPREFS_CONFIG_DEVICE_TELEM_UPDATE_INTERVAL moduleConfig.telemetry.device_update_interval = USERPREFS_CONFIG_DEVICE_TELEM_UPDATE_INTERVAL; #else moduleConfig.telemetry.device_update_interval = MAX_INTERVAL; #endif moduleConfig.telemetry.environment_update_interval = 0; moduleConfig.telemetry.air_quality_interval = 0; moduleConfig.telemetry.power_update_interval = 0; moduleConfig.telemetry.health_update_interval = 0; moduleConfig.neighbor_info.update_interval = 0; moduleConfig.paxcounter.paxcounter_update_interval = 0; } void NodeDB::installDefaultChannels() { LOG_INFO("Install default ChannelFile"); memset(&channelFile, 0, sizeof(meshtastic_ChannelFile)); channelFile.version = DEVICESTATE_CUR_VER; } void NodeDB::resetNodes(bool keepFavorites) { if (!config.position.fixed_position) clearLocalPosition(); numMeshNodes = 1; if (keepFavorites) { LOG_INFO("Clearing node database - preserving favorites"); for (size_t i = 0; i < meshNodes->size(); i++) { meshtastic_NodeInfoLite &node = meshNodes->at(i); if (i > 0 && !node.is_favorite) { node = meshtastic_NodeInfoLite(); } else { numMeshNodes += 1; } }; } else { LOG_INFO("Clearing node database - removing favorites"); std::fill(nodeDatabase.nodes.begin() + 1, nodeDatabase.nodes.end(), meshtastic_NodeInfoLite()); } devicestate.has_rx_text_message = false; devicestate.has_rx_waypoint = false; saveNodeDatabaseToDisk(); saveDeviceStateToDisk(); if (neighborInfoModule && moduleConfig.neighbor_info.enabled) neighborInfoModule->resetNeighbors(); } void NodeDB::removeNodeByNum(NodeNum nodeNum) { int newPos = 0, removed = 0; for (int i = 0; i < numMeshNodes; i++) { if (meshNodes->at(i).num != nodeNum) meshNodes->at(newPos++) = meshNodes->at(i); else removed++; } numMeshNodes -= removed; std::fill(nodeDatabase.nodes.begin() + numMeshNodes, nodeDatabase.nodes.begin() + numMeshNodes + 1, meshtastic_NodeInfoLite()); LOG_DEBUG("NodeDB::removeNodeByNum purged %d entries. Save changes", removed); saveNodeDatabaseToDisk(); } void NodeDB::clearLocalPosition() { meshtastic_NodeInfoLite *node = getMeshNode(nodeDB->getNodeNum()); node->position.latitude_i = 0; node->position.longitude_i = 0; node->position.altitude = 0; node->position.time = 0; setLocalPosition(meshtastic_Position_init_default); localPositionUpdatedSinceBoot = false; } void NodeDB::cleanupMeshDB() { int newPos = 0, removed = 0; for (int i = 0; i < numMeshNodes; i++) { if (meshNodes->at(i).has_user) { if (meshNodes->at(i).user.public_key.size > 0) { if (memfll(meshNodes->at(i).user.public_key.bytes, 0, meshNodes->at(i).user.public_key.size)) { meshNodes->at(i).user.public_key.size = 0; } } if (newPos != i) meshNodes->at(newPos++) = meshNodes->at(i); else newPos++; } else { removed++; } } numMeshNodes -= removed; std::fill(nodeDatabase.nodes.begin() + numMeshNodes, nodeDatabase.nodes.begin() + numMeshNodes + removed, meshtastic_NodeInfoLite()); LOG_DEBUG("cleanupMeshDB purged %d entries", removed); } void NodeDB::installDefaultDeviceState() { LOG_INFO("Install default DeviceState"); // memset(&devicestate, 0, sizeof(meshtastic_DeviceState)); // init our devicestate with valid flags so protobuf writing/reading will work devicestate.has_my_node = true; devicestate.has_owner = true; devicestate.version = DEVICESTATE_CUR_VER; devicestate.receive_queue_count = 0; // Not yet implemented FIXME devicestate.has_rx_waypoint = false; devicestate.has_rx_text_message = false; generatePacketId(); // FIXME - ugly way to init current_packet_id; // Set default owner name pickNewNodeNum(); // based on macaddr now #ifdef USERPREFS_CONFIG_OWNER_LONG_NAME snprintf(owner.long_name, sizeof(owner.long_name), (const char *)USERPREFS_CONFIG_OWNER_LONG_NAME); #else snprintf(owner.long_name, sizeof(owner.long_name), "Meshtastic %04x", getNodeNum() & 0x0ffff); #endif #ifdef USERPREFS_CONFIG_OWNER_SHORT_NAME snprintf(owner.short_name, sizeof(owner.short_name), (const char *)USERPREFS_CONFIG_OWNER_SHORT_NAME); #else snprintf(owner.short_name, sizeof(owner.short_name), "%04x", getNodeNum() & 0x0ffff); #endif snprintf(owner.id, sizeof(owner.id), "!%08x", getNodeNum()); // Default node ID now based on nodenum memcpy(owner.macaddr, ourMacAddr, sizeof(owner.macaddr)); owner.has_is_unmessagable = true; owner.is_unmessagable = false; } // 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("Use nodenum 0x%x ", nodeNum); myNodeInfo.my_node_num = nodeNum; } /** Load a protobuf from a file, return LoadFileResult */ LoadFileResult NodeDB::loadProto(const char *filename, size_t protoSize, size_t objSize, const pb_msgdesc_t *fields, void *dest_struct) { LoadFileResult state = LoadFileResult::OTHER_FAILURE; #ifdef FSCom concurrency::LockGuard g(spiLock); auto f = FSCom.open(filename, FILE_O_READ); if (f) { LOG_INFO("Load %s", filename); pb_istream_t stream = {&readcb, &f, protoSize}; if (fields != &meshtastic_NodeDatabase_msg) // contains a vector object memset(dest_struct, 0, objSize); if (!pb_decode(&stream, fields, dest_struct)) { LOG_ERROR("Error: can't decode protobuf %s", PB_GET_ERROR(&stream)); state = LoadFileResult::DECODE_FAILED; } else { LOG_INFO("Loaded %s successfully", filename); state = LoadFileResult::LOAD_SUCCESS; } f.close(); } 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() { // Mark the current device state as completely unusable, so that if we fail reading the entire file from // disk we will still factoryReset to restore things. devicestate.version = 0; meshtastic_Config_SecurityConfig backupSecurity = meshtastic_Config_SecurityConfig_init_zero; #ifdef ARCH_ESP32 spiLock->lock(); // If the legacy deviceState exists, start over with a factory reset if (FSCom.exists("/static/static")) rmDir("/static/static"); // Remove bad static web files bundle from initial 2.5.13 release spiLock->unlock(); #endif #ifdef FSCom #ifdef FACTORY_INSTALL spiLock->lock(); if (!FSCom.exists("/prefs/" xstr(BUILD_EPOCH))) { LOG_WARN("Factory Install Reset!"); FSCom.format(); FSCom.mkdir("/prefs"); File f2 = FSCom.open("/prefs/" xstr(BUILD_EPOCH), FILE_O_WRITE); if (f2) { f2.flush(); f2.close(); } } spiLock->unlock(); #endif spiLock->lock(); if (FSCom.exists(legacyPrefFileName)) { spiLock->unlock(); LOG_WARN("Legacy prefs version found, factory resetting"); if (loadProto(configFileName, meshtastic_LocalConfig_size, sizeof(meshtastic_LocalConfig), &meshtastic_LocalConfig_msg, &config) == LoadFileResult::LOAD_SUCCESS && config.has_security && config.security.private_key.size > 0) { LOG_DEBUG("Saving backup of security config and keys"); backupSecurity = config.security; } spiLock->lock(); rmDir("/prefs"); spiLock->unlock(); } else { spiLock->unlock(); } #endif auto state = loadProto(nodeDatabaseFileName, getMaxNodesAllocatedSize(), sizeof(meshtastic_NodeDatabase), &meshtastic_NodeDatabase_msg, &nodeDatabase); if (nodeDatabase.version < DEVICESTATE_MIN_VER) { LOG_WARN("NodeDatabase %d is old, discard", nodeDatabase.version); installDefaultNodeDatabase(); } else { meshNodes = &nodeDatabase.nodes; numMeshNodes = nodeDatabase.nodes.size(); LOG_INFO("Loaded saved nodedatabase version %d, with nodes count: %d", nodeDatabase.version, nodeDatabase.nodes.size()); } if (numMeshNodes > MAX_NUM_NODES) { LOG_WARN("Node count %d exceeds MAX_NUM_NODES %d, truncating", numMeshNodes, MAX_NUM_NODES); numMeshNodes = MAX_NUM_NODES; } meshNodes->resize(MAX_NUM_NODES); // static DeviceState scratch; We no longer read into a tempbuf because this structure is 15KB of valuable RAM state = loadProto(deviceStateFileName, meshtastic_DeviceState_size, sizeof(meshtastic_DeviceState), &meshtastic_DeviceState_msg, &devicestate); // See https://github.com/meshtastic/firmware/issues/4184#issuecomment-2269390786 // It is very important to try and use the saved prefs even if we fail to read meshtastic_DeviceState. Because most of our // critical config may still be valid (in the other files - loaded next). // Also, if we did fail on reading we probably failed on the enormous (and non critical) nodeDB. So DO NOT install default // device state. // if (state != LoadFileResult::LOAD_SUCCESS) { // installDefaultDeviceState(); // Our in RAM copy might now be corrupt //} else { if ((state != LoadFileResult::LOAD_SUCCESS) || (devicestate.version < DEVICESTATE_MIN_VER)) { LOG_WARN("Devicestate %d is old or invalid, discard", devicestate.version); installDefaultDeviceState(); // Attempt recovery of owner fields from our own NodeDB entry if available. meshtastic_NodeInfoLite *us = getMeshNode(getNodeNum()); if (us && us->has_user) { LOG_WARN("Restoring owner fields (long_name/short_name/is_licensed/is_unmessagable) from NodeDB for our node 0x%08x", us->num); memcpy(owner.long_name, us->user.long_name, sizeof(owner.long_name)); owner.long_name[sizeof(owner.long_name) - 1] = '\0'; memcpy(owner.short_name, us->user.short_name, sizeof(owner.short_name)); owner.short_name[sizeof(owner.short_name) - 1] = '\0'; owner.is_licensed = us->user.is_licensed; owner.has_is_unmessagable = us->user.has_is_unmessagable; owner.is_unmessagable = us->user.is_unmessagable; // Save the recovered owner to device state on disk saveToDisk(SEGMENT_DEVICESTATE); } } else { LOG_INFO("Loaded saved devicestate version %d", devicestate.version); } state = loadProto(configFileName, meshtastic_LocalConfig_size, sizeof(meshtastic_LocalConfig), &meshtastic_LocalConfig_msg, &config); if (state != LoadFileResult::LOAD_SUCCESS) { installDefaultConfig(); // Our in RAM copy might now be corrupt } else { if (config.version < DEVICESTATE_MIN_VER) { LOG_WARN("config %d is old, discard", config.version); installDefaultConfig(true); } else { LOG_INFO("Loaded saved config version %d", config.version); } } // Coerce LoRa config fields derived from presets while bootstrapping. // Some clients/UI components display bandwidth/spread_factor directly from config even in preset mode. if (config.has_lora && config.lora.use_preset) { RadioInterface::clampConfigLora(config.lora); } #if defined(USERPREFS_LORA_TX_DISABLED) && USERPREFS_LORA_TX_DISABLED config.lora.tx_enabled = false; #endif if (backupSecurity.private_key.size > 0) { LOG_DEBUG("Restoring backup of security config"); config.security = backupSecurity; saveToDisk(SEGMENT_CONFIG); } // Make sure we load hard coded admin keys even when the configuration file has none. // Initialize admin_key_count to zero byte numAdminKeys = 0; #if defined(USERPREFS_USE_ADMIN_KEY_0) || defined(USERPREFS_USE_ADMIN_KEY_1) || defined(USERPREFS_USE_ADMIN_KEY_2) uint16_t sum = 0; #endif #ifdef USERPREFS_USE_ADMIN_KEY_0 for (uint8_t b = 0; b < 32; b++) { sum += config.security.admin_key[0].bytes[b]; } if (sum == 0) { numAdminKeys += 1; LOG_INFO("Admin 0 key zero. Loading hard coded key from user preferences."); memcpy(config.security.admin_key[0].bytes, userprefs_admin_key_0, 32); config.security.admin_key[0].size = 32; } #endif #ifdef USERPREFS_USE_ADMIN_KEY_1 sum = 0; for (uint8_t b = 0; b < 32; b++) { sum += config.security.admin_key[1].bytes[b]; } if (sum == 0) { numAdminKeys += 1; LOG_INFO("Admin 1 key zero. Loading hard coded key from user preferences."); memcpy(config.security.admin_key[1].bytes, userprefs_admin_key_1, 32); config.security.admin_key[1].size = 32; } #endif #ifdef USERPREFS_USE_ADMIN_KEY_2 sum = 0; for (uint8_t b = 0; b < 32; b++) { sum += config.security.admin_key[2].bytes[b]; } if (sum == 0) { numAdminKeys += 1; LOG_INFO("Admin 2 key zero. Loading hard coded key from user preferences."); memcpy(config.security.admin_key[2].bytes, userprefs_admin_key_2, 32); config.security.admin_key[2].size = 32; } #endif if (numAdminKeys > 0) { LOG_INFO("Saving %d hard coded admin keys.", numAdminKeys); config.security.admin_key_count = numAdminKeys; saveToDisk(SEGMENT_CONFIG); } state = loadProto(moduleConfigFileName, meshtastic_LocalModuleConfig_size, sizeof(meshtastic_LocalModuleConfig), &meshtastic_LocalModuleConfig_msg, &moduleConfig); if (state != LoadFileResult::LOAD_SUCCESS) { installDefaultModuleConfig(); // Our in RAM copy might now be corrupt } else { if (moduleConfig.version < DEVICESTATE_MIN_VER) { LOG_WARN("moduleConfig %d is old, discard", moduleConfig.version); installDefaultModuleConfig(); } else { LOG_INFO("Loaded saved moduleConfig version %d", moduleConfig.version); } } state = loadProto(channelFileName, meshtastic_ChannelFile_size, sizeof(meshtastic_ChannelFile), &meshtastic_ChannelFile_msg, &channelFile); if (state != LoadFileResult::LOAD_SUCCESS) { installDefaultChannels(); // Our in RAM copy might now be corrupt } else { if (channelFile.version < DEVICESTATE_MIN_VER) { LOG_WARN("channelFile %d is old, discard", channelFile.version); installDefaultChannels(); } else { LOG_INFO("Loaded saved channelFile version %d", channelFile.version); } } state = loadProto(uiconfigFileName, meshtastic_DeviceUIConfig_size, sizeof(meshtastic_DeviceUIConfig), &meshtastic_DeviceUIConfig_msg, &uiconfig); if (state == LoadFileResult::LOAD_SUCCESS) { LOG_INFO("Loaded UIConfig"); } // 2.4.X - configuration migration to update new default intervals if (moduleConfig.version < 23) { LOG_DEBUG("ModuleConfig version %d is stale, upgrading to new default intervals", moduleConfig.version); moduleConfig.version = DEVICESTATE_CUR_VER; if (moduleConfig.telemetry.device_update_interval == 900) moduleConfig.telemetry.device_update_interval = 0; if (moduleConfig.telemetry.environment_update_interval == 900) moduleConfig.telemetry.environment_update_interval = 0; if (moduleConfig.telemetry.air_quality_interval == 900) moduleConfig.telemetry.air_quality_interval = 0; if (moduleConfig.telemetry.power_update_interval == 900) moduleConfig.telemetry.power_update_interval = 0; if (moduleConfig.neighbor_info.update_interval == 900) moduleConfig.neighbor_info.update_interval = 0; if (moduleConfig.paxcounter.paxcounter_update_interval == 900) moduleConfig.paxcounter.paxcounter_update_interval = 0; saveToDisk(SEGMENT_MODULECONFIG); } #if ARCH_PORTDUINO // set any config overrides if (portduino_config.has_configDisplayMode) { config.display.displaymode = (_meshtastic_Config_DisplayConfig_DisplayMode)portduino_config.configDisplayMode; } if (portduino_config.has_statusMessage) { moduleConfig.has_statusmessage = true; strncpy(moduleConfig.statusmessage.node_status, portduino_config.statusMessage.c_str(), sizeof(moduleConfig.statusmessage.node_status)); moduleConfig.statusmessage.node_status[sizeof(moduleConfig.statusmessage.node_status) - 1] = '\0'; } if (portduino_config.enable_UDP) { config.network.enabled_protocols = meshtastic_Config_NetworkConfig_ProtocolFlags_UDP_BROADCAST; } #endif } /** Save a protobuf from a file, return true for success */ bool NodeDB::saveProto(const char *filename, size_t protoSize, const pb_msgdesc_t *fields, const void *dest_struct, bool fullAtomic) { // do not try to save anything if power level is not safe. In many cases flash will be lock-protected // and all writes will fail anyway. Device should be sleeping at this point anyway. if (!powerHAL_isPowerLevelSafe()) { LOG_ERROR("Error: trying to saveProto() on unsafe device power level."); return false; } bool okay = false; #ifdef FSCom auto f = SafeFile(filename, fullAtomic); LOG_INFO("Save %s", filename); pb_ostream_t stream = {&writecb, static_cast(&f), protoSize}; if (!pb_encode(&stream, fields, dest_struct)) { LOG_ERROR("Error: can't encode protobuf %s", PB_GET_ERROR(&stream)); } else { okay = true; } 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() { // do not try to save anything if power level is not safe. In many cases flash will be lock-protected // and all writes will fail anyway. if (!powerHAL_isPowerLevelSafe()) { LOG_ERROR("Error: trying to saveChannelsToDisk() on unsafe device power level."); return false; } #ifdef FSCom spiLock->lock(); FSCom.mkdir("/prefs"); spiLock->unlock(); #endif return saveProto(channelFileName, meshtastic_ChannelFile_size, &meshtastic_ChannelFile_msg, &channelFile); } bool NodeDB::saveDeviceStateToDisk() { // do not try to save anything if power level is not safe. In many cases flash will be lock-protected // and all writes will fail anyway. Device should be sleeping at this point anyway. if (!powerHAL_isPowerLevelSafe()) { LOG_ERROR("Error: trying to saveDeviceStateToDisk() on unsafe device power level."); return false; } #ifdef FSCom spiLock->lock(); FSCom.mkdir("/prefs"); spiLock->unlock(); #endif // Note: if MAX_NUM_NODES=100 and meshtastic_NodeInfoLite_size=166, so will be approximately 17KB // Because so huge we _must_ not use fullAtomic, because the filesystem is probably too small to hold two copies of this return saveProto(deviceStateFileName, meshtastic_DeviceState_size, &meshtastic_DeviceState_msg, &devicestate, true); } bool NodeDB::saveNodeDatabaseToDisk() { // do not try to save anything if power level is not safe. In many cases flash will be lock-protected // and all writes will fail anyway. Device should be sleeping at this point anyway. if (!powerHAL_isPowerLevelSafe()) { LOG_ERROR("Error: trying to saveNodeDatabaseToDisk() on unsafe device power level."); return false; } #ifdef FSCom spiLock->lock(); FSCom.mkdir("/prefs"); spiLock->unlock(); #endif size_t nodeDatabaseSize; pb_get_encoded_size(&nodeDatabaseSize, meshtastic_NodeDatabase_fields, &nodeDatabase); return saveProto(nodeDatabaseFileName, nodeDatabaseSize, &meshtastic_NodeDatabase_msg, &nodeDatabase, false); } bool NodeDB::saveToDiskNoRetry(int saveWhat) { // do not try to save anything if power level is not safe. In many cases flash will be lock-protected // and all writes will fail anyway. Device should be sleeping at this point anyway. if (!powerHAL_isPowerLevelSafe()) { LOG_ERROR("Error: trying to saveToDiskNoRetry() on unsafe device power level."); return false; } bool success = true; #ifdef FSCom spiLock->lock(); FSCom.mkdir("/prefs"); spiLock->unlock(); #endif if (saveWhat & SEGMENT_CONFIG) { config.has_device = true; config.has_display = true; config.has_lora = true; config.has_position = true; config.has_power = true; config.has_network = true; config.has_bluetooth = true; config.has_security = true; success &= saveProto(configFileName, meshtastic_LocalConfig_size, &meshtastic_LocalConfig_msg, &config); } if (saveWhat & SEGMENT_MODULECONFIG) { moduleConfig.has_canned_message = true; moduleConfig.has_external_notification = true; moduleConfig.has_mqtt = true; moduleConfig.has_range_test = true; moduleConfig.has_serial = true; moduleConfig.has_store_forward = true; moduleConfig.has_telemetry = true; moduleConfig.has_neighbor_info = true; moduleConfig.has_detection_sensor = true; moduleConfig.has_ambient_lighting = true; moduleConfig.has_audio = true; moduleConfig.has_paxcounter = true; moduleConfig.has_statusmessage = true; success &= saveProto(moduleConfigFileName, meshtastic_LocalModuleConfig_size, &meshtastic_LocalModuleConfig_msg, &moduleConfig); } if (saveWhat & SEGMENT_CHANNELS) { success &= saveChannelsToDisk(); } if (saveWhat & SEGMENT_DEVICESTATE) { success &= saveDeviceStateToDisk(); } if (saveWhat & SEGMENT_NODEDATABASE) { success &= saveNodeDatabaseToDisk(); } return success; } bool NodeDB::saveToDisk(int saveWhat) { LOG_DEBUG("Save to disk %d", saveWhat); // do not try to save anything if power level is not safe. In many cases flash will be lock-protected // and all writes will fail anyway. Device should be sleeping at this point anyway. if (!powerHAL_isPowerLevelSafe()) { LOG_ERROR("Error: trying to saveToDisk() on unsafe device power level."); return false; } bool success = saveToDiskNoRetry(saveWhat); if (!success) { LOG_ERROR("Failed to save to disk, retrying"); #ifdef ARCH_NRF52 // @geeksville is not ready yet to say we should do this on other platforms. See bug #4184 discussion spiLock->lock(); FSCom.format(); spiLock->unlock(); #endif success = saveToDiskNoRetry(saveWhat); RECORD_CRITICALERROR(success ? meshtastic_CriticalErrorCode_FLASH_CORRUPTION_RECOVERABLE : meshtastic_CriticalErrorCode_FLASH_CORRUPTION_UNRECOVERABLE); } return success; } const meshtastic_NodeInfoLite *NodeDB::readNextMeshNode(uint32_t &readIndex) { if (readIndex < numMeshNodes) return &meshNodes->at(readIndex++); else return NULL; } /// Given a node, return how many seconds in the past (vs now) that we last heard from it uint32_t sinceLastSeen(const meshtastic_NodeInfoLite *n) { uint32_t now = getTime(); 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; } HopStartStatus classifyHopStart(const meshtastic_MeshPacket &p) { // Guard against invalid values. if (p.hop_start < p.hop_limit) return HopStartStatus::INVALID; if (p.hop_start == 0) { // Firmware prior to 2.3.0 (585805c) lacked a hop_start field. Firmware version 2.5.0 (bf34329) introduced a // bitfield that is always present. Use the presence of the bitfield to determine if the origin's firmware // version is guaranteed to have hop_start populated. Note that this can only be done for decoded packets as // the bitfield is encrypted under the channel encryption key. if (p.which_payload_variant == meshtastic_MeshPacket_decoded_tag && p.decoded.has_bitfield) return HopStartStatus::VALID; return HopStartStatus::MISSING_OR_UNKNOWN; } return HopStartStatus::VALID; } int8_t getHopsAway(const meshtastic_MeshPacket &p, int8_t defaultIfUnknown) { // Firmware prior to 2.3.0 (585805c) lacked a hop_start field. Firmware version 2.5.0 (bf34329) introduced a // bitfield that is always present. Use the presence of the bitfield to determine if the origin's firmware // version is guaranteed to have hop_start populated. Note that this can only be done for decoded packets as // the bitfield is encrypted under the channel encryption key. For encrypted packets, this returns // defaultIfUnknown when hop_start is 0. if (p.hop_start == 0 && !(p.which_payload_variant == meshtastic_MeshPacket_decoded_tag && p.decoded.has_bitfield)) return defaultIfUnknown; // Cannot reliably determine the number of hops. // Guard against invalid values. if (p.hop_start < p.hop_limit) return defaultIfUnknown; return p.hop_start - p.hop_limit; } #define NUM_ONLINE_SECS (60 * 60 * 2) // 2 hrs to consider someone offline size_t NodeDB::getNumOnlineMeshNodes(bool localOnly) { size_t numseen = 0; // 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" static constexpr uint32_t HOPSTART_DROP_LOG_INTERVAL_MS = 15000; void logHopStartDrop(const meshtastic_MeshPacket &p, const char *context) { static uint32_t lastLogMs = 0; if (Throttle::isWithinTimespanMs(lastLogMs, HOPSTART_DROP_LOG_INTERVAL_MS)) { return; } lastLogMs = millis(); const bool decoded = (p.which_payload_variant == meshtastic_MeshPacket_decoded_tag); const bool hasBitfield = decoded && p.decoded.has_bitfield; LOG_DEBUG( "Drop packet (%s): hop_start invalid/missing (from=0x%x id=%u hop_start=%u hop_limit=%u decoded=%d has_bitfield=%d)", context ? context : "unknown", p.from, p.id, p.hop_start, p.hop_limit, decoded, hasBitfield); } /** 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 the node database with a new contact */ void NodeDB::addFromContact(meshtastic_SharedContact contact) { meshtastic_NodeInfoLite *info = getOrCreateMeshNode(contact.node_num); if (!info || !contact.has_user) { return; } // If the local node has this node marked as manually verified // and the client does not, do not allow the client to update the // saved public key. if ((info->bitfield & NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK) && !contact.manually_verified) { if (contact.user.public_key.size != info->user.public_key.size || memcmp(contact.user.public_key.bytes, info->user.public_key.bytes, info->user.public_key.size) != 0) { return; } } info->num = contact.node_num; info->has_user = true; info->user = TypeConversions::ConvertToUserLite(contact.user); if (contact.should_ignore) { // If should_ignore is set, // we need to clear the public key and other cruft, in addition to setting the node as ignored info->is_ignored = true; info->is_favorite = false; info->has_device_metrics = false; info->has_position = false; info->user.public_key.size = 0; memset(info->user.public_key.bytes, 0, sizeof(info->user.public_key.bytes)); } else { /* Clients are sending add_contact before every text message DM (because clients may hold a larger node database with * public keys than the radio holds). However, we don't want to update last_heard just because we sent someone a DM! */ /* "Boring old nodes" are the first to be evicted out of the node database when full. This includes a newly-zeroed * nodeinfo because it has: !is_favorite && last_heard==0. To keep this from happening when we addFromContact, we set the * new node as a favorite, and we leave last_heard alone (even if it's zero). */ if (config.device.role == meshtastic_Config_DeviceConfig_Role_CLIENT_BASE) { // Special case for CLIENT_BASE: is_favorite has special meaning, and we don't want to automatically set it // without the user doing so deliberately. We don't normally expect users to use a CLIENT_BASE to send DMs or to add // contacts, but we should make sure it doesn't auto-favorite in case they do. Instead, as a workaround, we'll set // last_heard to now, so that the add_contact node doesn't immediately get evicted. info->last_heard = getTime(); } else { // Normal case: set is_favorite to prevent expiration. // last_heard will remain as-is (or remain 0 if this entry wasn't in the nodeDB). info->is_favorite = true; } // As the clients will begin sending the contact with DMs, we want to strictly check if the node is manually verified if (contact.manually_verified) { info->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK; } // Mark the node's key as manually verified to indicate trustworthiness. updateGUIforNode = info; sortMeshDB(); notifyObservers(true); // Force an update whether or not our node counts have changed } saveNodeDatabaseToDisk(); } /** Update user info and channel for this node based on received user data */ bool NodeDB::updateUser(uint32_t nodeId, meshtastic_User &p, uint8_t channelIndex) { meshtastic_NodeInfoLite *info = getOrCreateMeshNode(nodeId); if (!info) { return false; } #if !(MESHTASTIC_EXCLUDE_PKI) if (p.public_key.size == 32 && nodeId != nodeDB->getNodeNum()) { printBytes("Incoming Pubkey: ", p.public_key.bytes, 32); // Alert the user if a remote node is advertising public key that matches our own if (owner.public_key.size == 32 && memcmp(p.public_key.bytes, owner.public_key.bytes, 32) == 0) { if (!duplicateWarned) { duplicateWarned = true; char warning[] = "Remote device %s has advertised your public key. This may indicate a compromised key. You may need " "to regenerate your public keys."; LOG_WARN(warning, p.long_name); meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); cn->level = meshtastic_LogRecord_Level_WARNING; cn->time = getValidTime(RTCQualityFromNet); sprintf(cn->message, warning, p.long_name); service->sendClientNotification(cn); } return false; } } if (info->user.public_key.size == 32) { // if we have a key for this user already, don't overwrite with a new one // if the key doesn't match, don't update nodeDB at all. if (p.public_key.size != 32 || (memcmp(p.public_key.bytes, info->user.public_key.bytes, 32) != 0)) { LOG_WARN("Public Key mismatch, dropping NodeInfo"); return false; } LOG_INFO("Public Key set for node, not updating!"); } else if (p.public_key.size == 32) { LOG_INFO("Update Node Pubkey!"); } #endif // Always ensure user.id is derived from nodeId, regardless of what was received snprintf(p.id, sizeof(p.id), "!%08x", nodeId); // Both of info->user and p start as filled with zero so I think this is okay auto lite = TypeConversions::ConvertToUserLite(p); bool changed = memcmp(&info->user, &lite, sizeof(info->user)) || (info->channel != channelIndex); info->user = lite; if (info->user.public_key.size == 32) { printBytes("Saved Pubkey: ", info->user.public_key.bytes, 32); } if (nodeId != getNodeNum()) info->channel = channelIndex; // Set channel we need to use to reach this node (but don't set our own channel) LOG_DEBUG("Update changed=%d user %s/%s, id=0x%08x, channel=%d", changed, info->user.long_name, info->user.short_name, nodeId, info->channel); info->has_user = true; if (changed) { updateGUIforNode = info; notifyObservers(true); // Force an update whether or not our node counts have changed // We just changed something about a User, // store our DB unless we just did so less than a minute ago if (!Throttle::isWithinTimespanMs(lastNodeDbSave, ONE_MINUTE_MS)) { saveToDisk(SEGMENT_NODEDATABASE); lastNodeDbSave = millis(); } else { LOG_DEBUG("Defer NodeDB saveToDisk for now"); } } return changed; } /// given a subpacket sniffed from the network, update our DB state /// we updateGUI and updateGUIforNode if we think our this change is big enough for a redraw void NodeDB::updateFrom(const meshtastic_MeshPacket &mp) { if (mp.from == getNodeNum()) { LOG_DEBUG("Ignore update from self"); return; } if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag && mp.from) { LOG_DEBUG("Update DB node 0x%x, rx_time=%u", mp.from, mp.rx_time); meshtastic_NodeInfoLite *info = getOrCreateMeshNode(getFrom(&mp)); if (!info) { return; } if (mp.rx_time) // if the packet has a valid timestamp use it to update our last_heard info->last_heard = mp.rx_time; if (mp.rx_snr) info->snr = mp.rx_snr; // keep the most recent SNR we received for this node. info->via_mqtt = mp.via_mqtt; // Store if we received this packet via MQTT // If hopStart was set and there wasn't someone messing with the limit in the middle, add hopsAway const int8_t hopsAway = getHopsAway(mp); if (hopsAway >= 0) { info->has_hops_away = true; info->hops_away = hopsAway; } sortMeshDB(); } } void NodeDB::set_favorite(bool is_favorite, uint32_t nodeId) { meshtastic_NodeInfoLite *lite = getMeshNode(nodeId); if (lite && lite->is_favorite != is_favorite) { lite->is_favorite = is_favorite; sortMeshDB(); saveNodeDatabaseToDisk(); } } bool NodeDB::isFavorite(uint32_t nodeId) { // returns true if nodeId is_favorite; false if not or not found // NODENUM_BROADCAST will never be in the DB if (nodeId == NODENUM_BROADCAST) return false; meshtastic_NodeInfoLite *lite = getMeshNode(nodeId); if (lite) { return lite->is_favorite; } return false; } bool NodeDB::isFromOrToFavoritedNode(const meshtastic_MeshPacket &p) { // This method is logically equivalent to: // return isFavorite(p.from) || isFavorite(p.to); // but is more efficient by: // 1. doing only one pass through the database, instead of two // 2. exiting early when a favorite is found, or if both from and to have been seen if (p.to == NODENUM_BROADCAST) return isFavorite(p.from); // we never store NODENUM_BROADCAST in the DB, so we only need to check p.from meshtastic_NodeInfoLite *lite = NULL; bool seenFrom = false; bool seenTo = false; for (int i = 0; i < numMeshNodes; i++) { lite = &meshNodes->at(i); if (lite->num == p.from) { if (lite->is_favorite) return true; seenFrom = true; } if (lite->num == p.to) { if (lite->is_favorite) return true; seenTo = true; } if (seenFrom && seenTo) return false; // we've seen both, and neither is a favorite, so we can stop searching early // Note: if we knew that sortMeshDB was always called after any change to is_favorite, we could exit early after searching // all favorited nodes first. } return false; } void NodeDB::pause_sort(bool paused) { sortingIsPaused = paused; } void NodeDB::sortMeshDB() { if (!sortingIsPaused && (lastSort == 0 || !Throttle::isWithinTimespanMs(lastSort, 1000 * 5))) { lastSort = millis(); bool changed = true; while (changed) { // dumb reverse bubble sort, but probably not bad for what we're doing changed = false; for (int i = numMeshNodes - 1; i > 0; i--) { // lowest case this should examine is i == 1 if (meshNodes->at(i - 1).num == getNodeNum()) { // noop } else if (meshNodes->at(i).num == getNodeNum()) { // in the oddball case our own node num is not at location 0, put it there // TODO: Look for at(i-1) also matching own node num, and throw the DB in the trash std::swap(meshNodes->at(i), meshNodes->at(i - 1)); changed = true; } else if (meshNodes->at(i).is_favorite && !meshNodes->at(i - 1).is_favorite) { std::swap(meshNodes->at(i), meshNodes->at(i - 1)); changed = true; } else if (!meshNodes->at(i).is_favorite && meshNodes->at(i - 1).is_favorite) { // noop } else if (meshNodes->at(i).last_heard > meshNodes->at(i - 1).last_heard) { std::swap(meshNodes->at(i), meshNodes->at(i - 1)); changed = true; } } } LOG_INFO("Sort took %u milliseconds", millis() - lastSort); } } uint8_t NodeDB::getMeshNodeChannel(NodeNum n) { const meshtastic_NodeInfoLite *info = getMeshNode(n); if (!info) { return 0; // defaults to PRIMARY } return info->channel; } std::string NodeDB::getNodeId() const { char nodeId[16]; snprintf(nodeId, sizeof(nodeId), "!%08x", myNodeInfo.my_node_num); return std::string(nodeId); } /// Find a node in our DB, return null for missing /// NOTE: This function might be called from an ISR meshtastic_NodeInfoLite *NodeDB::getMeshNode(NodeNum n) { for (int i = 0; i < numMeshNodes; i++) if (meshNodes->at(i).num == n) return &meshNodes->at(i); return NULL; } // returns true if the maximum number of nodes is reached or we are running low on memory bool NodeDB::isFull() { return (numMeshNodes >= MAX_NUM_NODES) || (memGet.getFreeHeap() < MINIMUM_SAFE_FREE_HEAP); } /// 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 (isFull()) { LOG_INFO("Node database full with %i nodes and %u bytes free. Erasing oldest entry", numMeshNodes, memGet.getFreeHeap()); // look for oldest node and erase it uint32_t oldest = UINT32_MAX; uint32_t oldestBoring = UINT32_MAX; int oldestIndex = -1; int oldestBoringIndex = -1; for (int i = 1; i < numMeshNodes; i++) { // Simply the oldest non-favorite, non-ignored, non-verified node if (!meshNodes->at(i).is_favorite && !meshNodes->at(i).is_ignored && !(meshNodes->at(i).bitfield & NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK) && meshNodes->at(i).last_heard < oldest) { oldest = meshNodes->at(i).last_heard; oldestIndex = i; } // The oldest "boring" node if (!meshNodes->at(i).is_favorite && !meshNodes->at(i).is_ignored && meshNodes->at(i).user.public_key.size == 0 && meshNodes->at(i).last_heard < oldestBoring) { oldestBoring = meshNodes->at(i).last_heard; oldestBoringIndex = i; } } // if we found a "boring" node, evict it if (oldestBoringIndex != -1) { oldestIndex = oldestBoringIndex; } if (oldestIndex != -1) { // Shove the remaining nodes down the chain for (int i = oldestIndex; i < numMeshNodes - 1; i++) { meshNodes->at(i) = meshNodes->at(i + 1); } (numMeshNodes)--; } } // add the node at the end lite = &meshNodes->at((numMeshNodes)++); // everything is missing except the nodenum memset(lite, 0, sizeof(*lite)); lite->num = n; LOG_INFO("Adding node to database with %i nodes and %u bytes free!", numMeshNodes, memGet.getFreeHeap()); } return lite; } /// Sometimes we will have Position objects that only have a time, so check for /// valid lat/lon bool NodeDB::hasValidPosition(const meshtastic_NodeInfoLite *n) { return n->has_position && (n->position.latitude_i != 0 || n->position.longitude_i != 0); } /// If we have a node / user and they report is_licensed = true /// we consider them licensed UserLicenseStatus NodeDB::getLicenseStatus(uint32_t nodeNum) { meshtastic_NodeInfoLite *info = getMeshNode(nodeNum); if (!info || !info->has_user) { return UserLicenseStatus::NotKnown; } return info->user.is_licensed ? UserLicenseStatus::Licensed : UserLicenseStatus::NotLicensed; } #if !defined(MESHTASTIC_EXCLUDE_PKI) bool NodeDB::checkLowEntropyPublicKey(const meshtastic_Config_SecurityConfig_public_key_t &keyToTest) { if (keyToTest.size == 32) { uint8_t keyHash[32] = {0}; memcpy(keyHash, keyToTest.bytes, keyToTest.size); crypto->hash(keyHash, 32); for (uint16_t i = 0; i < sizeof(LOW_ENTROPY_HASHES) / sizeof(LOW_ENTROPY_HASHES[0]); i++) { if (memcmp(keyHash, LOW_ENTROPY_HASHES[i], sizeof(LOW_ENTROPY_HASHES[0])) == 0) { return true; } } } return false; } #endif bool NodeDB::backupPreferences(meshtastic_AdminMessage_BackupLocation location) { bool success = false; lastBackupAttempt = millis(); #ifdef FSCom if (location == meshtastic_AdminMessage_BackupLocation_FLASH) { meshtastic_BackupPreferences backup = meshtastic_BackupPreferences_init_zero; backup.version = DEVICESTATE_CUR_VER; backup.timestamp = getValidTime(RTCQuality::RTCQualityDevice, false); backup.has_config = true; backup.config = config; backup.has_module_config = true; backup.module_config = moduleConfig; backup.has_channels = true; backup.channels = channelFile; backup.has_owner = true; backup.owner = owner; size_t backupSize; pb_get_encoded_size(&backupSize, meshtastic_BackupPreferences_fields, &backup); spiLock->lock(); FSCom.mkdir("/backups"); spiLock->unlock(); success = saveProto(backupFileName, backupSize, &meshtastic_BackupPreferences_msg, &backup); if (success) { LOG_INFO("Saved backup preferences"); } else { LOG_ERROR("Failed to save backup preferences to file"); } } else if (location == meshtastic_AdminMessage_BackupLocation_SD) { // TODO: After more mainline SD card support } #endif return success; } bool NodeDB::restorePreferences(meshtastic_AdminMessage_BackupLocation location, int restoreWhat) { bool success = false; #ifdef FSCom if (location == meshtastic_AdminMessage_BackupLocation_FLASH) { spiLock->lock(); if (!FSCom.exists(backupFileName)) { spiLock->unlock(); LOG_WARN("Could not restore. No backup file found"); return false; } else { spiLock->unlock(); } meshtastic_BackupPreferences backup = meshtastic_BackupPreferences_init_zero; success = loadProto(backupFileName, meshtastic_BackupPreferences_size, sizeof(meshtastic_BackupPreferences), &meshtastic_BackupPreferences_msg, &backup); if (success) { if (restoreWhat & SEGMENT_CONFIG) { config = backup.config; LOG_DEBUG("Restored config"); } if (restoreWhat & SEGMENT_MODULECONFIG) { moduleConfig = backup.module_config; LOG_DEBUG("Restored module config"); } if (restoreWhat & SEGMENT_DEVICESTATE) { devicestate.owner = backup.owner; LOG_DEBUG("Restored device state"); } if (restoreWhat & SEGMENT_CHANNELS) { channelFile = backup.channels; LOG_DEBUG("Restored channels"); } success = saveToDisk(restoreWhat); if (success) { LOG_INFO("Restored preferences from backup"); } else { LOG_ERROR("Failed to save restored preferences to flash"); } } else { LOG_ERROR("Failed to restore preferences from backup file"); } } else if (location == meshtastic_AdminMessage_BackupLocation_SD) { // TODO: After more mainline SD card support } #endif return success; } /// Record an error that should be reported via analytics void recordCriticalError(meshtastic_CriticalErrorCode code, uint32_t address, const char *filename) { if (filename) { LOG_ERROR("NOTE! Record critical error %d at %s:%lu", code, filename, address); } else { LOG_ERROR("NOTE! Record critical error %d, address=0x%lx", code, address); } // 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"); // TODO: Determine if other critical errors should also cause an immediate exit if (code == meshtastic_CriticalErrorCode_FLASH_CORRUPTION_RECOVERABLE || code == meshtastic_CriticalErrorCode_FLASH_CORRUPTION_UNRECOVERABLE) exit(2); #endif } ================================================ FILE: src/mesh/NodeDB.h ================================================ #pragma once #include "Observer.h" #include #include #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 #if ARCH_PORTDUINO #include "PortduinoGlue.h" #endif #if !defined(MESHTASTIC_EXCLUDE_PKI) // E3B0C442 is the blank hash static const uint8_t LOW_ENTROPY_HASHES[][32] = { {0xf4, 0x7e, 0xcc, 0x17, 0xe6, 0xb4, 0xa3, 0x22, 0xec, 0xee, 0xd9, 0x08, 0x4f, 0x39, 0x63, 0xea, 0x80, 0x75, 0xe1, 0x24, 0xce, 0x05, 0x36, 0x69, 0x63, 0xb2, 0xcb, 0xc0, 0x28, 0xd3, 0x34, 0x8b}, {0x5a, 0x9e, 0xa2, 0xa6, 0x8a, 0xa6, 0x66, 0xc1, 0x5f, 0x55, 0x00, 0x64, 0xa3, 0xa6, 0xfe, 0x71, 0xc0, 0xbb, 0x82, 0xc3, 0x32, 0x3d, 0x7a, 0x7a, 0xe3, 0x6e, 0xfd, 0xdd, 0xad, 0x3a, 0x66, 0xb9}, {0xb3, 0xdf, 0x3b, 0x2e, 0x67, 0xb6, 0xd5, 0xf8, 0xdf, 0x76, 0x2c, 0x45, 0x5e, 0x2e, 0xbd, 0x16, 0xc5, 0xf8, 0x67, 0xaa, 0x15, 0xf8, 0x92, 0x0b, 0xdf, 0x5a, 0x66, 0x50, 0xac, 0x0d, 0xbb, 0x2f}, {0x3b, 0x8f, 0x86, 0x3a, 0x38, 0x1f, 0x77, 0x39, 0xa9, 0x4e, 0xef, 0x91, 0x18, 0x5a, 0x62, 0xe1, 0xaa, 0x9d, 0x36, 0xea, 0xce, 0x60, 0x35, 0x8d, 0x9d, 0x1f, 0xf4, 0xb8, 0xc9, 0x13, 0x6a, 0x5d}, {0x36, 0x7e, 0x2d, 0xe1, 0x84, 0x5f, 0x42, 0x52, 0x29, 0x11, 0x0a, 0x25, 0x64, 0x54, 0x6a, 0x6b, 0xfd, 0xb6, 0x65, 0xff, 0x15, 0x1a, 0x51, 0x71, 0x22, 0x40, 0x57, 0xf6, 0x91, 0x9b, 0x64, 0x58}, {0x16, 0x77, 0xeb, 0xa4, 0x52, 0x91, 0xfb, 0x26, 0xcf, 0x8f, 0xd7, 0xd9, 0xd1, 0x5d, 0xc4, 0x68, 0x73, 0x75, 0xed, 0xc5, 0x95, 0x58, 0xee, 0x90, 0x56, 0xd4, 0x2f, 0x31, 0x29, 0xf7, 0x8c, 0x1f}, {0x31, 0x8c, 0xa9, 0x5e, 0xed, 0x3c, 0x12, 0xbf, 0x97, 0x9c, 0x47, 0x8e, 0x98, 0x9d, 0xc2, 0x3e, 0x86, 0x23, 0x90, 0x29, 0xc8, 0xb0, 0x20, 0xf8, 0xb1, 0xb0, 0xaa, 0x19, 0x2a, 0xcf, 0x0a, 0x54}, {0xa4, 0x8a, 0x99, 0x0e, 0x51, 0xdc, 0x12, 0x20, 0xf3, 0x13, 0xf5, 0x2b, 0x3a, 0xe2, 0x43, 0x42, 0xc6, 0x52, 0x98, 0xcd, 0xbb, 0xca, 0xb1, 0x31, 0xa0, 0xd4, 0xd6, 0x30, 0xf3, 0x27, 0xfb, 0x49}, {0xd2, 0x3f, 0x13, 0x8d, 0x22, 0x04, 0x8d, 0x07, 0x59, 0x58, 0xa0, 0xf9, 0x55, 0xcf, 0x30, 0xa0, 0x2e, 0x2f, 0xca, 0x80, 0x20, 0xe4, 0xde, 0xa1, 0xad, 0xd9, 0x58, 0xb3, 0x43, 0x2b, 0x22, 0x70}, {0x40, 0x41, 0xec, 0x6a, 0xd2, 0xd6, 0x03, 0xe4, 0x9a, 0x9e, 0xbd, 0x6c, 0x0a, 0x9b, 0x75, 0xa4, 0xbc, 0xab, 0x6f, 0xa7, 0x95, 0xff, 0x2d, 0xf6, 0xe9, 0xb9, 0xab, 0x4c, 0x0c, 0x1c, 0xd0, 0x3b}, {0x22, 0x49, 0x32, 0x2b, 0x00, 0xf9, 0x22, 0xfa, 0x17, 0x02, 0xe9, 0x64, 0x82, 0xf0, 0x4d, 0x1b, 0xc7, 0x04, 0xfc, 0xdc, 0x8c, 0x5e, 0xb6, 0xd9, 0x16, 0xd6, 0x37, 0xce, 0x59, 0xaa, 0x09, 0x49}, {0x48, 0x6f, 0x1e, 0x48, 0x97, 0x88, 0x64, 0xac, 0xe8, 0xeb, 0x30, 0xa3, 0xc3, 0xe1, 0xcf, 0x97, 0x39, 0xa6, 0x55, 0x5b, 0x5f, 0xbf, 0x18, 0xb7, 0x3a, 0xdf, 0xa8, 0x75, 0xe7, 0x9d, 0xe0, 0x1e}, {0x09, 0xb4, 0xe2, 0x6d, 0x28, 0x98, 0xc9, 0x47, 0x66, 0x46, 0xbf, 0xff, 0x58, 0x17, 0x91, 0xaa, 0xc3, 0xbf, 0x4a, 0x9d, 0x0b, 0x88, 0xb1, 0xf1, 0x03, 0xdd, 0x61, 0xd7, 0xba, 0x9e, 0x64, 0x98}, {0x39, 0x39, 0x84, 0xe0, 0x22, 0x2f, 0x7d, 0x78, 0x45, 0x18, 0x72, 0xb4, 0x13, 0xd2, 0x01, 0x2f, 0x3c, 0xa1, 0xb0, 0xfe, 0x39, 0xd0, 0xf1, 0x3c, 0x72, 0xd6, 0xef, 0x54, 0xd5, 0x77, 0x22, 0xa0}, {0x0a, 0xda, 0x5f, 0xec, 0xff, 0x5c, 0xc0, 0x2e, 0x5f, 0xc4, 0x8d, 0x03, 0xe5, 0x80, 0x59, 0xd3, 0x5d, 0x49, 0x86, 0xe9, 0x8d, 0xf6, 0xf6, 0x16, 0x35, 0x3d, 0xf9, 0x9b, 0x29, 0x55, 0x9e, 0x64}, {0x08, 0x56, 0xF0, 0xD7, 0xEF, 0x77, 0xD6, 0x11, 0x1C, 0x8F, 0x95, 0x2D, 0x3C, 0xDF, 0xB1, 0x22, 0xBF, 0x60, 0x9B, 0xE5, 0xA9, 0xC0, 0x6E, 0x4B, 0x01, 0xDC, 0xD1, 0x57, 0x44, 0xB2, 0xA5, 0xCF}, {0x2C, 0xB2, 0x77, 0x85, 0xD6, 0xB7, 0x48, 0x9C, 0xFE, 0xBC, 0x80, 0x26, 0x60, 0xF4, 0x6D, 0xCE, 0x11, 0x31, 0xA2, 0x1E, 0x33, 0x0A, 0x6D, 0x2B, 0x00, 0xFA, 0x0C, 0x90, 0x95, 0x8F, 0x5C, 0x6B}, {0xFA, 0x59, 0xC8, 0x6E, 0x94, 0xEE, 0x75, 0xC9, 0x9A, 0xB0, 0xFE, 0x89, 0x36, 0x40, 0xC9, 0x99, 0x4A, 0x3B, 0xF4, 0xAA, 0x12, 0x24, 0xA2, 0x0F, 0xF9, 0xD1, 0x08, 0xCB, 0x78, 0x19, 0xAA, 0xE5}, {0x6E, 0x42, 0x7A, 0x4A, 0x8C, 0x61, 0x62, 0x22, 0xA1, 0x89, 0xD3, 0xA4, 0xC2, 0x19, 0xA3, 0x83, 0x53, 0xA7, 0x7A, 0x0A, 0x89, 0xE2, 0x54, 0x52, 0x62, 0x3D, 0xE7, 0xCA, 0x8C, 0xF6, 0x6A, 0x60}, {0x20, 0x27, 0x2F, 0xBA, 0x0C, 0x99, 0xD7, 0x29, 0xF3, 0x11, 0x35, 0x89, 0x9D, 0x0E, 0x24, 0xA1, 0xC3, 0xCB, 0xDF, 0x8A, 0xF1, 0xC6, 0xFE, 0xD0, 0xD7, 0x9F, 0x92, 0xD6, 0x8F, 0x59, 0xBF, 0xE4}, {0x91, 0x70, 0xb4, 0x7c, 0xfb, 0xff, 0xa0, 0x59, 0x6a, 0x25, 0x1c, 0xa9, 0x9e, 0xe9, 0x43, 0x81, 0x5d, 0x74, 0xb1, 0xb1, 0x09, 0x28, 0x00, 0x4a, 0xaf, 0xe3, 0xfc, 0xa9, 0x4e, 0x27, 0x76, 0x4c}, {0x85, 0xfe, 0x7c, 0xec, 0xb6, 0x78, 0x74, 0xc3, 0xec, 0xe1, 0x32, 0x7f, 0xb0, 0xb7, 0x02, 0x74, 0xf9, 0x23, 0xd8, 0xe7, 0xfa, 0x14, 0xe6, 0xee, 0x66, 0x44, 0xb1, 0x8c, 0xa5, 0x2f, 0x7e, 0xd2}, {0x8e, 0x66, 0x65, 0x7b, 0x3b, 0x6f, 0x7e, 0xcc, 0x57, 0xb4, 0x57, 0xea, 0xcc, 0x83, 0xf5, 0xaa, 0xf7, 0x65, 0xa3, 0xce, 0x93, 0x72, 0x13, 0xc1, 0xb6, 0x46, 0x7b, 0x29, 0x45, 0xb5, 0xc8, 0x93}, {0xcc, 0x11, 0xfb, 0x1a, 0xab, 0xa1, 0x31, 0x87, 0x6a, 0xc6, 0xde, 0x88, 0x87, 0xa9, 0xb9, 0x59, 0x37, 0x82, 0x8d, 0xb2, 0xcc, 0xd8, 0x97, 0x40, 0x9a, 0x5c, 0x8f, 0x40, 0x55, 0xcb, 0x4c, 0x3e}}; static const char LOW_ENTROPY_WARNING[] = "Compromised keys were detected and regenerated."; #endif /* 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 SEGMENT_NODEDATABASE 16 #define DEVICESTATE_CUR_VER 24 #define DEVICESTATE_MIN_VER 24 extern meshtastic_DeviceState devicestate; extern meshtastic_NodeDatabase nodeDatabase; extern meshtastic_ChannelFile channelFile; extern meshtastic_MyNodeInfo &myNodeInfo; extern meshtastic_LocalConfig config; extern meshtastic_DeviceUIConfig uiconfig; extern meshtastic_LocalModuleConfig moduleConfig; extern meshtastic_User &owner; extern meshtastic_Position localPosition; static constexpr const char *deviceStateFileName = "/prefs/device.proto"; static constexpr const char *legacyPrefFileName = "/prefs/db.proto"; static constexpr const char *nodeDatabaseFileName = "/prefs/nodes.proto"; static constexpr const char *configFileName = "/prefs/config.proto"; static constexpr const char *uiconfigFileName = "/prefs/uiconfig.proto"; static constexpr const char *moduleConfigFileName = "/prefs/module.proto"; static constexpr const char *channelFileName = "/prefs/channels.proto"; static constexpr const char *backupFileName = "/backups/backup.proto"; /// 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); /// Given a packet, return the number of hops used to reach this node. /// Returns defaultIfUnknown if the number of hops couldn't be determined. int8_t getHopsAway(const meshtastic_MeshPacket &p, int8_t defaultIfUnknown = -1); enum class HopStartStatus : uint8_t { VALID = 0, MISSING_OR_UNKNOWN, INVALID }; /// Classify hop_start validity for forwarding decisions. HopStartStatus classifyHopStart(const meshtastic_MeshPacket &p); inline bool shouldDropPacketForPreHop(const meshtastic_MeshPacket &p) { #if !MESHTASTIC_PREHOP_DROP (void)p; return false; #else if (isFromUs(&p)) { return false; // local-originated packets should never be dropped by pre-hop drop policy } return classifyHopStart(p) != HopStartStatus::VALID; #endif } /// Rate-limited debug log when hop_start is invalid/missing and packet is dropped. void logHopStartDrop(const meshtastic_MeshPacket &p, const char *context); 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 }; enum UserLicenseStatus { NotKnown, NotLicensed, Licensed }; 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; bool keyIsLowEntropy = false; bool hasWarned = false; /// don't do mesh based algorithm for node id assignment (initially) /// instead just store in flash - possibly even in the initial alpha release do this hack NodeDB(); /// write to flash /// @return true if the save was successful bool saveToDisk(int saveWhat = SEGMENT_CONFIG | SEGMENT_MODULECONFIG | SEGMENT_DEVICESTATE | SEGMENT_CHANNELS | SEGMENT_NODEDATABASE); /** Reinit radio config if needed, because either: * a) sometimes a buggy android app might send us bogus settings or * b) the client set factory_reset * * @param factory_reset if true, reset all settings to factory defaults * @param is_fresh_install set to true after a fresh install, to trigger NodeInfo/Position requests * @return true if the config was completely reset, in that case, we should send it back to the client */ void resetRadioConfig(bool is_fresh_install = false); /// given a subpacket sniffed from the network, update our DB state /// we updateGUI and updateGUIforNode if we think our this change is big enough for a redraw void updateFrom(const meshtastic_MeshPacket &p); void addFromContact(const meshtastic_SharedContact); /** 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); /* * Sets a node either favorite or unfavorite */ void set_favorite(bool is_favorite, uint32_t nodeId); /* * Returns true if the node is in the NodeDB and marked as favorite */ bool isFavorite(uint32_t nodeId); /* * Returns true if p->from or p->to is a favorited node */ bool isFromOrToFavoritedNode(const meshtastic_MeshPacket &p); /** * Other functions like the node picker can request a pause in the node sorting */ void pause_sort(bool paused); /// @return our node number NodeNum getNodeNum() { return myNodeInfo.my_node_num; } /// @return our node ID as a string in the format "!xxxxxxxx" std::string getNodeId() const; // @return last byte of a NodeNum, 0xFF if it ended at 0x00 uint8_t getLastByteOfNodeNum(NodeNum num) { return (uint8_t)((num & 0xFF) ? (num & 0xFF) : 0xFF); } /// if returns false, that means our node should send a DenyNodeNum response. If true, we think the number is okay for use // bool handleWantNodeNum(NodeNum n); /* void handleDenyNodeNum(NodeNum FIXME read mesh proto docs, perhaps picking a random node num is not a great idea and instead we should use a special 'im unconfigured node number' and include our desired node number in the wantnum message. the unconfigured node num would only be used while initially joining the mesh so low odds of conflicting (especially if we randomly select from a small number of nodenums which can be used temporarily for this operation). figure out what the lower level mesh sw does if it does conflict? would it be better for people who are replying with denynode num to just broadcast their denial?) */ // get channel channel index we heard a nodeNum on, defaults to 0 if not found uint8_t getMeshNodeChannel(NodeNum n); /* Return the number of nodes we've heard from recently (within the last 2 hrs?) * @param localOnly if true, ignore nodes heard via MQTT */ size_t getNumOnlineMeshNodes(bool localOnly = false); void initConfigIntervals(), initModuleConfigIntervals(), resetNodes(bool keepFavorites = false), removeNodeByNum(NodeNum nodeNum); 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); } virtual meshtastic_NodeInfoLite *getMeshNode(NodeNum n); size_t getNumMeshNodes() { return numMeshNodes; } UserLicenseStatus getLicenseStatus(uint32_t nodeNum); size_t getMaxNodesAllocatedSize() { meshtastic_NodeDatabase emptyNodeDatabase; emptyNodeDatabase.version = DEVICESTATE_CUR_VER; size_t nodeDatabaseSize; pb_get_encoded_size(&nodeDatabaseSize, meshtastic_NodeDatabase_fields, &emptyNodeDatabase); return nodeDatabaseSize + (MAX_NUM_NODES * meshtastic_NodeInfoLite_size); } // returns true if the maximum number of nodes is reached or we are running low on memory bool isFull(); void clearLocalPosition(); void setLocalPosition(meshtastic_Position position, bool timeOnly = false) { if (timeOnly) { LOG_DEBUG("Set local position time only: time=%u timestamp=%u", position.time, position.timestamp); localPosition.time = position.time; localPosition.timestamp = position.timestamp > 0 ? position.timestamp : position.time; return; } LOG_DEBUG("Set local position: lat=%i lon=%i time=%u timestamp=%u", position.latitude_i, position.longitude_i, position.time, position.timestamp); localPosition = position; if (position.latitude_i != 0 || position.longitude_i != 0) { localPositionUpdatedSinceBoot = true; } } bool hasValidPosition(const meshtastic_NodeInfoLite *n); bool hasLocalPositionSinceBoot() const { return localPositionUpdatedSinceBoot; } #if !defined(MESHTASTIC_EXCLUDE_PKI) bool checkLowEntropyPublicKey(const meshtastic_Config_SecurityConfig_public_key_t &keyToTest); #endif bool backupPreferences(meshtastic_AdminMessage_BackupLocation location); bool restorePreferences(meshtastic_AdminMessage_BackupLocation location, int restoreWhat = SEGMENT_CONFIG | SEGMENT_MODULECONFIG | SEGMENT_DEVICESTATE | SEGMENT_CHANNELS); /// Notify observers of changes to the DB void notifyObservers(bool forceUpdate = false) { // Notify observers of the current node state const meshtastic::NodeStatus status = meshtastic::NodeStatus(getNumOnlineMeshNodes(), getNumMeshNodes(), forceUpdate); newStatus.notifyObservers(&status); } private: bool duplicateWarned = false; bool localPositionUpdatedSinceBoot = false; uint32_t lastNodeDbSave = 0; // when we last saved our db to flash uint32_t lastBackupAttempt = 0; // when we last tried a backup automatically or manually uint32_t lastSort = 0; // When last sorted the nodeDB /// Find a node in our DB, create an empty NodeInfoLite if missing meshtastic_NodeInfoLite *getOrCreateMeshNode(NodeNum n); /* * Internal boolean to track sorting paused */ bool sortingIsPaused = false; /// pick a provisional nodenum we hope no one is using void pickNewNodeNum(); /// 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(), installDefaultNodeDatabase(), installDefaultChannels(), installDefaultConfig(bool preserveKey), installDefaultModuleConfig(); /// write to flash /// @return true if the save was successful bool saveToDiskNoRetry(int saveWhat); bool saveChannelsToDisk(); bool saveDeviceStateToDisk(); bool saveNodeDatabaseToDisk(); void sortMeshDB(); }; 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 */ /** 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 NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_SHIFT 0 #define NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK (1 << NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_SHIFT) #define NODEINFO_BITFIELD_IS_MUTED_SHIFT 1 #define NODEINFO_BITFIELD_IS_MUTED_MASK (1 << NODEINFO_BITFIELD_IS_MUTED_SHIFT) #define Module_Config_size \ (ModuleConfig_CannedMessageConfig_size + ModuleConfig_ExternalNotificationConfig_size + ModuleConfig_MQTTConfig_size + \ ModuleConfig_RangeTestConfig_size + ModuleConfig_SerialConfig_size + ModuleConfig_StoreForwardConfig_size + \ ModuleConfig_TelemetryConfig_size + ModuleConfig_size) // Please do not remove this comment, it makes trunk and compiler happy at the same time. ================================================ FILE: src/mesh/PacketCache.cpp ================================================ #include "PacketCache.h" #include "Router.h" PacketCache packetCache{}; /** * Allocate a new cache entry and copy the packet header and payload into it */ PacketCacheEntry *PacketCache::cache(const meshtastic_MeshPacket *p, bool preserveMetadata) { size_t payload_size = (p->which_payload_variant == meshtastic_MeshPacket_encrypted_tag) ? p->encrypted.size : p->decoded.payload.size; PacketCacheEntry *e = (PacketCacheEntry *)malloc(sizeof(PacketCacheEntry) + payload_size + (preserveMetadata ? sizeof(PacketCacheMetadata) : 0)); if (!e) { LOG_ERROR("Unable to allocate memory for packet cache entry"); return NULL; } *e = {}; e->header.from = p->from; e->header.to = p->to; e->header.id = p->id; e->header.channel = p->channel; e->header.next_hop = p->next_hop; e->header.relay_node = p->relay_node; e->header.flags = (p->hop_limit & PACKET_FLAGS_HOP_LIMIT_MASK) | (p->want_ack ? PACKET_FLAGS_WANT_ACK_MASK : 0) | (p->via_mqtt ? PACKET_FLAGS_VIA_MQTT_MASK : 0) | ((p->hop_start << PACKET_FLAGS_HOP_START_SHIFT) & PACKET_FLAGS_HOP_START_MASK); PacketCacheMetadata m{}; if (preserveMetadata) { e->has_metadata = true; m.rx_rssi = (uint8_t)(p->rx_rssi + 200); m.rx_snr = (uint8_t)((p->rx_snr + 30.0f) / 0.25f); m.rx_time = p->rx_time; m.transport_mechanism = p->transport_mechanism; m.priority = p->priority; } if (p->which_payload_variant == meshtastic_MeshPacket_encrypted_tag) { e->encrypted = true; e->payload_len = p->encrypted.size; memcpy(((unsigned char *)e) + sizeof(PacketCacheEntry), p->encrypted.bytes, p->encrypted.size); } else if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) { e->encrypted = false; if (preserveMetadata) { m.portnum = p->decoded.portnum; m.want_response = p->decoded.want_response; m.emoji = p->decoded.emoji; m.bitfield = p->decoded.bitfield; if (p->decoded.reply_id) m.reply_id = p->decoded.reply_id; else if (p->decoded.request_id) m.request_id = p->decoded.request_id; } e->payload_len = p->decoded.payload.size; memcpy(((unsigned char *)e) + sizeof(PacketCacheEntry), p->decoded.payload.bytes, p->decoded.payload.size); } else { LOG_ERROR("Unable to cache packet with unknown payload type %d", p->which_payload_variant); free(e); return NULL; } if (preserveMetadata) memcpy(((unsigned char *)e) + sizeof(PacketCacheEntry) + e->payload_len, &m, sizeof(m)); size += sizeof(PacketCacheEntry) + e->payload_len + (e->has_metadata ? sizeof(PacketCacheMetadata) : 0); insert(e); return e; }; /** * Dump a list of packets into the provided buffer */ void PacketCache::dump(void *dest, const PacketCacheEntry **entries, size_t num_entries) { unsigned char *pos = (unsigned char *)dest; for (size_t i = 0; i < num_entries; i++) { size_t entry_len = sizeof(PacketCacheEntry) + entries[i]->payload_len + (entries[i]->has_metadata ? sizeof(PacketCacheMetadata) : 0); memcpy(pos, entries[i], entry_len); pos += entry_len; } } /** * Calculate the length of buffer needed to dump the specified entries */ size_t PacketCache::dumpSize(const PacketCacheEntry **entries, size_t num_entries) { size_t total_size = 0; for (size_t i = 0; i < num_entries; i++) { total_size += sizeof(PacketCacheEntry) + entries[i]->payload_len; if (entries[i]->has_metadata) total_size += sizeof(PacketCacheMetadata); } return total_size; } /** * Find a packet in the cache */ PacketCacheEntry *PacketCache::find(NodeNum from, PacketId id) { uint16_t h = PACKET_HASH(from, id); PacketCacheEntry *e = buckets[PACKET_CACHE_BUCKET(h)]; while (e) { if (e->header.from == from && e->header.id == id) return e; e = e->next; } return NULL; } /** * Find a packet in the cache by its hash */ PacketCacheEntry *PacketCache::find(PacketHash h) { PacketCacheEntry *e = buckets[PACKET_CACHE_BUCKET(h)]; while (e) { if (PACKET_HASH(e->header.from, e->header.id) == h) return e; e = e->next; } return NULL; } /** * Load a list of packets from the provided buffer */ bool PacketCache::load(void *src, PacketCacheEntry **entries, size_t num_entries) { memset(entries, 0, sizeof(PacketCacheEntry *) * num_entries); unsigned char *pos = (unsigned char *)src; for (size_t i = 0; i < num_entries; i++) { PacketCacheEntry e{}; memcpy(&e, pos, sizeof(PacketCacheEntry)); size_t entry_len = sizeof(PacketCacheEntry) + e.payload_len + (e.has_metadata ? sizeof(PacketCacheMetadata) : 0); entries[i] = (PacketCacheEntry *)malloc(entry_len); size += entry_len; if (!entries[i]) { LOG_ERROR("Unable to allocate memory for packet cache entry"); for (size_t j = 0; j < i; j++) { size -= sizeof(PacketCacheEntry) + entries[j]->payload_len + (entries[j]->has_metadata ? sizeof(PacketCacheMetadata) : 0); free(entries[j]); entries[j] = NULL; } return false; } memcpy(entries[i], pos, entry_len); pos += entry_len; } for (size_t i = 0; i < num_entries; i++) insert(entries[i]); return true; } /** * Copy the cached packet into the provided MeshPacket structure */ void PacketCache::rehydrate(const PacketCacheEntry *e, meshtastic_MeshPacket *p) { if (!e || !p) return; *p = {}; p->from = e->header.from; p->to = e->header.to; p->id = e->header.id; p->channel = e->header.channel; p->next_hop = e->header.next_hop; p->relay_node = e->header.relay_node; p->hop_limit = e->header.flags & PACKET_FLAGS_HOP_LIMIT_MASK; p->want_ack = !!(e->header.flags & PACKET_FLAGS_WANT_ACK_MASK); p->via_mqtt = !!(e->header.flags & PACKET_FLAGS_VIA_MQTT_MASK); p->hop_start = (e->header.flags & PACKET_FLAGS_HOP_START_MASK) >> PACKET_FLAGS_HOP_START_SHIFT; p->which_payload_variant = e->encrypted ? meshtastic_MeshPacket_encrypted_tag : meshtastic_MeshPacket_decoded_tag; unsigned char *payload = ((unsigned char *)e) + sizeof(PacketCacheEntry); PacketCacheMetadata m{}; if (e->has_metadata) { memcpy(&m, (payload + e->payload_len), sizeof(m)); p->rx_rssi = ((int)m.rx_rssi) - 200; p->rx_snr = ((float)m.rx_snr * 0.25f) - 30.0f; p->rx_time = m.rx_time; p->transport_mechanism = (meshtastic_MeshPacket_TransportMechanism)m.transport_mechanism; p->priority = (meshtastic_MeshPacket_Priority)m.priority; } if (e->encrypted) { memcpy(p->encrypted.bytes, payload, e->payload_len); p->encrypted.size = e->payload_len; } else { memcpy(p->decoded.payload.bytes, payload, e->payload_len); p->decoded.payload.size = e->payload_len; if (e->has_metadata) { // Decrypted-only metadata p->decoded.portnum = (meshtastic_PortNum)m.portnum; p->decoded.want_response = m.want_response; p->decoded.emoji = m.emoji; p->decoded.bitfield = m.bitfield; if (m.reply_id) p->decoded.reply_id = m.reply_id; else if (m.request_id) p->decoded.request_id = m.request_id; } } } /** * Release a cache entry */ void PacketCache::release(PacketCacheEntry *e) { if (!e) return; remove(e); size -= sizeof(PacketCacheEntry) + e->payload_len + (e->has_metadata ? sizeof(PacketCacheMetadata) : 0); free(e); } /** * Insert a new entry into the hash table */ void PacketCache::insert(PacketCacheEntry *e) { assert(e); PacketHash h = PACKET_HASH(e->header.from, e->header.id); PacketCacheEntry **target = &buckets[PACKET_CACHE_BUCKET(h)]; e->next = *target; *target = e; num_entries++; } /** * Remove an entry from the hash table */ void PacketCache::remove(PacketCacheEntry *e) { assert(e); PacketHash h = PACKET_HASH(e->header.from, e->header.id); PacketCacheEntry **target = &buckets[PACKET_CACHE_BUCKET(h)]; while (*target) { if (*target == e) { *target = e->next; e->next = NULL; num_entries--; break; } else { target = &(*target)->next; } } } ================================================ FILE: src/mesh/PacketCache.h ================================================ #pragma once #include "RadioInterface.h" #define PACKET_HASH(a, b) ((((a ^ b) >> 16) ^ (a ^ b)) & 0xFFFF) // 16 bit fold of packet (from, id) tuple typedef uint16_t PacketHash; #define PACKET_CACHE_BUCKETS 64 // Number of hash table buckets #define PACKET_CACHE_BUCKET(h) (((h >> 12) ^ (h >> 6) ^ h) & 0x3F) // Fold hash down to 6-bit bucket index typedef struct PacketCacheEntry { PacketCacheEntry *next; PacketHeader header; uint16_t payload_len = 0; union { uint16_t bitfield; struct { uint8_t encrypted : 1; // Payload is encrypted uint8_t has_metadata : 1; // Payload includes PacketCacheMetadata uint8_t : 6; // Reserved for future use uint8_t : 8; // Reserved for future use }; }; } PacketCacheEntry; typedef struct PacketCacheMetadata { PacketCacheMetadata() : _bitfield(0), reply_id(0), _bitfield2(0) {} union { uint32_t _bitfield; struct { uint16_t portnum : 9; // meshtastic_MeshPacket::decoded::portnum uint16_t want_response : 1; // meshtastic_MeshPacket::decoded::want_response uint16_t emoji : 1; // meshtastic_MeshPacket::decoded::emoji uint16_t bitfield : 5; // meshtastic_MeshPacket::decoded::bitfield (truncated) uint8_t rx_rssi : 8; // meshtastic_MeshPacket::rx_rssi (map via actual RSSI + 200) uint8_t rx_snr : 8; // meshtastic_MeshPacket::rx_snr (map via (p->rx_snr + 30.0f) / 0.25f) }; }; union { uint32_t reply_id; // meshtastic_MeshPacket::decoded.reply_id uint32_t request_id; // meshtastic_MeshPacket::decoded.request_id }; uint32_t rx_time = 0; // meshtastic_MeshPacket::rx_time uint8_t transport_mechanism = 0; // meshtastic_MeshPacket::transport_mechanism struct { uint8_t _bitfield2; union { uint8_t priority : 7; // meshtastic_MeshPacket::priority uint8_t reserved : 1; // Reserved for future use }; }; } PacketCacheMetadata; class PacketCache { public: PacketCacheEntry *cache(const meshtastic_MeshPacket *p, bool preserveMetadata); static void dump(void *dest, const PacketCacheEntry **entries, size_t num_entries); size_t dumpSize(const PacketCacheEntry **entries, size_t num_entries); PacketCacheEntry *find(NodeNum from, PacketId id); PacketCacheEntry *find(PacketHash h); bool load(void *src, PacketCacheEntry **entries, size_t num_entries); size_t getNumEntries() { return num_entries; } size_t getSize() { return size; } void rehydrate(const PacketCacheEntry *e, meshtastic_MeshPacket *p); void release(PacketCacheEntry *e); private: PacketCacheEntry *buckets[PACKET_CACHE_BUCKETS]{}; size_t num_entries = 0; size_t size = 0; void insert(PacketCacheEntry *e); void remove(PacketCacheEntry *e); }; extern PacketCache packetCache; ================================================ FILE: src/mesh/PacketHistory.cpp ================================================ #include "PacketHistory.h" #include "configuration.h" #include "mesh-pb-constants.h" #ifdef ARCH_PORTDUINO #include "platform/portduino/PortduinoGlue.h" #endif #include "Throttle.h" #define PACKETHISTORY_MAX \ max((u_int32_t)(MAX_NUM_NODES * 2.0), \ (u_int32_t)100) // x2..3 Should suffice. Empirical setup. 16B per record malloc'ed, but no less than 100 #define RECENT_WARN_AGE (10 * 60 * 1000L) // Warn if the packet that gets removed was more recent than 10 min #define VERBOSE_PACKET_HISTORY 0 // Set to 1 for verbose logging, 2 for heavy debugging #define PACKET_HISTORY_TRACE_AGING 1 // Set to 1 to enable logging of the age of re/used history slots PacketHistory::PacketHistory(uint32_t size) : recentPacketsCapacity(0), recentPackets(NULL) // Initialize members { if (size < 4 || size > PACKETHISTORY_MAX) { // Copilot suggested - makes sense LOG_WARN("Packet History - Invalid size %d, using default %d", size, PACKETHISTORY_MAX); size = PACKETHISTORY_MAX; // Use default size if invalid } // Allocate memory for the recent packets array recentPacketsCapacity = size; recentPackets = new PacketRecord[recentPacketsCapacity]; if (!recentPackets) { // No logging here, console/log probably uninitialized yet. LOG_ERROR("Packet History - Memory allocation failed for size=%d entries / %d Bytes", size, sizeof(PacketRecord) * recentPacketsCapacity); recentPacketsCapacity = 0; // mark allocation fail return; // return early } // Initialize the recent packets array to zero memset(recentPackets, 0, sizeof(PacketRecord) * recentPacketsCapacity); } PacketHistory::~PacketHistory() { recentPacketsCapacity = 0; delete[] recentPackets; recentPackets = NULL; } /** Update recentPackets and return true if we have already seen this packet */ bool PacketHistory::wasSeenRecently(const meshtastic_MeshPacket *p, bool withUpdate, bool *wasFallback, bool *weWereNextHop, bool *wasUpgraded) { if (!initOk()) { LOG_ERROR("Packet History - Was Seen Recently: NOT INITIALIZED!"); return false; } if (p->id == 0) { #if VERBOSE_PACKET_HISTORY LOG_DEBUG("Packet History - Was Seen Recently: ID is 0, not a floodable message"); #endif return false; // Not a floodable message ID, so we don't care } PacketRecord r; memset(&r, 0, sizeof(PacketRecord)); // Initialize the record to zero // Save basic info from checked packet r.id = p->id; r.sender = getFrom(p); // If 0 then use our ID r.next_hop = p->next_hop; setHighestHopLimit(r, p->hop_limit); bool weWillRelay = false; uint8_t ourRelayID = nodeDB->getLastByteOfNodeNum(nodeDB->getNodeNum()); if (p->relay_node == ourRelayID) { // If the relay_node is us, store it weWillRelay = true; setOurTxHopLimit(r, p->hop_limit); r.relayed_by[0] = p->relay_node; } r.rxTimeMsec = millis(); // if (r.rxTimeMsec == 0) // =0 every 49.7 days? 0 is special r.rxTimeMsec = 1; #if VERBOSE_PACKET_HISTORY LOG_DEBUG("Packet History - Was Seen Recently: @start s=%08x id=%08x / to=%08x nh=%02x rn=%02x / wUpd=%s / wasFb?%d wWNH?%d", r.sender, r.id, p->to, p->next_hop, p->relay_node, withUpdate ? "YES" : "NO", wasFallback ? *wasFallback : -1, weWereNextHop ? *weWereNextHop : -1); #endif PacketRecord *found = find(r.sender, r.id); // Find the packet record in the recentPackets array bool seenRecently = (found != NULL); // If found -> the packet was seen recently // Check for hop_limit upgrade scenario if (seenRecently && wasUpgraded && getHighestHopLimit(*found) < p->hop_limit) { LOG_DEBUG("Packet History - Hop limit upgrade: packet 0x%08x from hop_limit=%d to hop_limit=%d", p->id, getHighestHopLimit(*found), p->hop_limit); *wasUpgraded = true; } else if (wasUpgraded) { *wasUpgraded = false; // Initialize to false if not an upgrade } if (seenRecently) { if (wasFallback) { // If it was seen with a next-hop not set to us and now it's NO_NEXT_HOP_PREFERENCE, and the relayer relayed already // before, it's a fallback to flooding. If we didn't already relay and the next-hop neither, we might need to handle // it now. if (found->sender != nodeDB->getNodeNum() && found->next_hop != NO_NEXT_HOP_PREFERENCE && found->next_hop != ourRelayID && p->next_hop == NO_NEXT_HOP_PREFERENCE && wasRelayer(p->relay_node, *found) && !wasRelayer(ourRelayID, *found) && !wasRelayer( found->next_hop, *found)) { // If we were not the next hop and the next hop is not us, and we are not relaying this packet #if VERBOSE_PACKET_HISTORY LOG_DEBUG("Packet History - Was Seen Recently: f=%08x id=%08x nh=%02x rn=%02x oID=%02x, wasFbk=%d-set TRUE", p->from, p->id, p->next_hop, p->relay_node, ourRelayID, wasFallback ? *wasFallback : -1); #endif *wasFallback = true; } else { // debug log only #if VERBOSE_PACKET_HISTORY LOG_DEBUG("Packet History - Was Seen Recently: f=%08x id=%08x nh=%02x rn=%02x oID=%02x, wasFbk=%d-no change", p->from, p->id, p->next_hop, p->relay_node, ourRelayID, wasFallback ? *wasFallback : -1); #endif } } // Check if we were the next hop for this packet if (weWereNextHop) { *weWereNextHop = (found->next_hop == ourRelayID); #if VERBOSE_PACKET_HISTORY LOG_DEBUG("Packet History - Was Seen Recently: f=%08x id=%08x nh=%02x rn=%02x foundnh=%02x oID=%02x -> wWNH=%s", p->from, p->id, p->next_hop, p->relay_node, found->next_hop, ourRelayID, (*weWereNextHop) ? "YES" : "NO"); #endif } } if (withUpdate) { if (found != NULL) { #if VERBOSE_PACKET_HISTORY LOG_DEBUG("Packet History - Was Seen Recently: s=%08x id=%08x nh=%02x rby=%02x %02x %02x age=%d wUpd BEFORE", found->sender, found->id, found->next_hop, found->relayed_by[0], found->relayed_by[1], found->relayed_by[2], millis() - found->rxTimeMsec); #endif // Only update the relayer if it heard us directly (meaning hopLimit is decreased by 1) uint8_t startIdx = weWillRelay ? 1 : 0; if (!weWillRelay) { bool weWereRelayer = wasRelayer(ourRelayID, *found); // We were a relayer and the packet came in with a hop limit that is one less than when we sent it out if (weWereRelayer && (p->hop_limit == getOurTxHopLimit(*found) || p->hop_limit == getOurTxHopLimit(*found) - 1)) { r.relayed_by[0] = p->relay_node; startIdx = 1; // Start copying existing relayers from index 1 } // keep the original ourTxHopLimit setOurTxHopLimit(r, getOurTxHopLimit(*found)); } // Preserve the highest hop_limit we've ever seen for this packet so upgrades aren't lost when a later copy has // fewer hops remaining. if (getHighestHopLimit(*found) > getHighestHopLimit(r)) setHighestHopLimit(r, getHighestHopLimit(*found)); // Add the existing relayed_by to the new record, avoiding duplicates for (uint8_t i = 0; i < (NUM_RELAYERS - startIdx); i++) { if (found->relayed_by[i] == 0) continue; bool exists = false; for (uint8_t j = 0; j < NUM_RELAYERS; j++) { if (r.relayed_by[j] == found->relayed_by[i]) { exists = true; break; } } if (!exists) { r.relayed_by[i + startIdx] = found->relayed_by[i]; } } r.next_hop = found->next_hop; // keep the original next_hop (such that we check whether we were originally asked) #if VERBOSE_PACKET_HISTORY LOG_DEBUG("Packet History - Was Seen Recently: s=%08x id=%08x nh=%02x rby=%02x %02x %02x age=%d wUpd AFTER", r.sender, r.id, r.next_hop, r.relayed_by[0], r.relayed_by[1], r.relayed_by[2], millis() - r.rxTimeMsec); #endif // TODO: have direct *found entry - can modify directly without local copy _vs_ not convolute the code by this } insert(r); // Insert or update the packet record in the history } #if VERBOSE_PACKET_HISTORY LOG_DEBUG("Packet History - Was Seen Recently: @exit s=%08x id=%08x (to=%08x) relby=%02x %02x %02x nxthop=%02x rxT=%d " "found?%s seenRecently?%s wUpd?%s", r.sender, r.id, p->to, r.relayed_by[0], r.relayed_by[1], r.relayed_by[2], r.next_hop, r.rxTimeMsec, found ? "YES" : "NO ", seenRecently ? "YES" : "NO ", withUpdate ? "YES" : "NO "); #endif return seenRecently; } /** Find a packet record in history. * @return pointer to PacketRecord if found, NULL if not found */ PacketHistory::PacketRecord *PacketHistory::find(NodeNum sender, PacketId id) { if (sender == 0 || id == 0) { #if VERBOSE_PACKET_HISTORY LOG_DEBUG("Packet History - find: s=%08x id=%08x sender/id=0->NOT FOUND", sender, id); #endif return NULL; } PacketRecord *it = NULL; for (it = recentPackets; it < (recentPackets + recentPacketsCapacity); ++it) { if (it->id == id && it->sender == sender) { #if VERBOSE_PACKET_HISTORY LOG_DEBUG("Packet History - find: s=%08x id=%08x FOUND nh=%02x rby=%02x %02x %02x age=%d slot=%d/%d", it->sender, it->id, it->next_hop, it->relayed_by[0], it->relayed_by[1], it->relayed_by[2], millis() - (it->rxTimeMsec), it - recentPackets, recentPacketsCapacity); #endif // only the first match is returned, so be careful not to create duplicate entries return it; // Return pointer to the found record } } #if VERBOSE_PACKET_HISTORY LOG_DEBUG("Packet History - find: s=%08x id=%08x NOT FOUND", sender, id); #endif return NULL; // Not found } /** Insert/Replace oldest PacketRecord in recentPackets. */ void PacketHistory::insert(const PacketRecord &r) { uint32_t now_millis = millis(); // Should not jump with time changes uint32_t OldtrxTimeMsec = 0; PacketRecord *tu = NULL; // Will insert here. PacketRecord *it = NULL; // Find a free, matching or oldest used slot in the recentPackets array for (it = recentPackets; it < (recentPackets + recentPacketsCapacity); ++it) { if (it->id == 0 && it->sender == 0 /*&& rxTimeMsec == 0*/) { // Record is empty tu = it; // Remember the free slot #if VERBOSE_PACKET_HISTORY >= 2 LOG_DEBUG("Packet History - insert: Free slot@ %d/%d", tu - recentPackets, recentPacketsCapacity); #endif // We have that, Exit the loop it = (recentPackets + recentPacketsCapacity); } else if (it->id == r.id && it->sender == r.sender) { // Record matches the packet we want to insert tu = it; // Remember the matching slot OldtrxTimeMsec = now_millis - it->rxTimeMsec; // ..and save current entry's age #if VERBOSE_PACKET_HISTORY >= 2 LOG_DEBUG("Packet History - insert: Matched slot@ %d/%d age=%d", tu - recentPackets, recentPacketsCapacity, OldtrxTimeMsec); #endif // We have that, Exit the loop it = (recentPackets + recentPacketsCapacity); } else { if (it->rxTimeMsec == 0) { LOG_WARN( "Packet History - insert: Found packet s=%08x id=%08x with rxTimeMsec = 0, slot %d/%d. Should never happen!", it->sender, it->id, it - recentPackets, recentPacketsCapacity); } if ((now_millis - it->rxTimeMsec) > OldtrxTimeMsec) { // 49.7 days rollover friendly OldtrxTimeMsec = now_millis - it->rxTimeMsec; tu = it; // remember the oldest packet #if VERBOSE_PACKET_HISTORY >= 2 LOG_DEBUG("Packet History - insert: Older slot@ %d/%d age=%d", tu - recentPackets, recentPacketsCapacity, OldtrxTimeMsec); #endif } // keep looking for oldest till entire array is checked } } if (tu == NULL) { LOG_ERROR("Packet History - insert: No free slot, no matched packet, no oldest to reuse. Something leaked."); // mx // assert(false); // This should never happen, we should always have at least one packet to clear return; // Return early if we can't update the history } #if VERBOSE_PACKET_HISTORY if (tu->id == 0 && tu->sender == 0) { LOG_DEBUG("Packet History - insert: slot@ %d/%d is NEW", tu - recentPackets, recentPacketsCapacity); } else if (tu->id == r.id && tu->sender == r.sender) { LOG_DEBUG("Packet History - insert: slot@ %d/%d MATCHED, age=%d", tu - recentPackets, recentPacketsCapacity, OldtrxTimeMsec); } else { LOG_DEBUG("Packet History - insert: slot@ %d/%d REUSE OLDEST, age=%d", tu - recentPackets, recentPacketsCapacity, OldtrxTimeMsec); } #endif // If we are reusing a slot, we should warn if the packet is too recent #if RECENT_WARN_AGE > 0 if (tu->rxTimeMsec && (OldtrxTimeMsec < RECENT_WARN_AGE)) { if (!(tu->id == r.id && tu->sender == r.sender)) { #if VERBOSE_PACKET_HISTORY LOG_WARN("Packet History - insert: Reusing slot aged %ds < %ds RECENT_WARN_AGE", OldtrxTimeMsec / 1000, RECENT_WARN_AGE / 1000); #endif } else { // debug only #if VERBOSE_PACKET_HISTORY LOG_WARN("Packet History - insert: Reusing slot aged %.3fs < %ds with MATCHED PACKET - this is normal", OldtrxTimeMsec / 1000., RECENT_WARN_AGE / 1000); #endif } } #if PACKET_HISTORY_TRACE_AGING if (tu->rxTimeMsec != 0) { LOG_INFO("Packet History - insert: Reusing slot aged %.3fs TRACE %s", OldtrxTimeMsec / 1000., (tu->id == r.id && tu->sender == r.sender) ? "MATCHED PACKET" : "OLDEST SLOT"); } else { LOG_INFO("Packet History - insert: Using new slot @uptime %.3fs TRACE NEW", millis() / 1000.); } #endif #endif #if VERBOSE_PACKET_HISTORY LOG_DEBUG("Packet History - insert: Store slot@ %d/%d s=%08x id=%08x nh=%02x rby=%02x %02x %02x rxT=%d BEFORE", tu - recentPackets, recentPacketsCapacity, tu->sender, tu->id, tu->next_hop, tu->relayed_by[0], tu->relayed_by[1], tu->relayed_by[2], tu->rxTimeMsec); #endif if (r.rxTimeMsec == 0) { #if VERBOSE_PACKET_HISTORY LOG_WARN("Packet History - insert: I will not store packet with rxTimeMsec = 0."); #endif return; // Return early if we can't update the history } *tu = r; // store the packet #if VERBOSE_PACKET_HISTORY LOG_DEBUG("Packet History - insert: Store slot@ %d/%d s=%08x id=%08x nh=%02x rby=%02x %02x %02x rxT=%d AFTER", tu - recentPackets, recentPacketsCapacity, tu->sender, tu->id, tu->next_hop, tu->relayed_by[0], tu->relayed_by[1], tu->relayed_by[2], tu->rxTimeMsec); #endif } /* Check if a certain node was a relayer of a packet in the history given an ID and sender * @return true if node was indeed a relayer, false if not */ bool PacketHistory::wasRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender, bool *wasSole) { if (!initOk()) { LOG_ERROR("PacketHistory - wasRelayer: NOT INITIALIZED!"); return false; } if (relayer == 0) { #if VERBOSE_PACKET_HISTORY LOG_DEBUG("Packet History - was relayer: s=%08x id=%08x / rl=%02x=zero. NO", sender, id, relayer); #endif return false; } const PacketRecord *found = find(sender, id); if (found == NULL) { #if VERBOSE_PACKET_HISTORY LOG_DEBUG("Packet History - was relayer: s=%08x id=%08x / rl=%02x / PR not found. NO", sender, id, relayer); #endif return false; } #if VERBOSE_PACKET_HISTORY >= 2 LOG_DEBUG("Packet History - was relayer: s=%08x id=%08x nh=%02x age=%d rls=%02x %02x %02x InHistory,check:%02x", found->sender, found->id, found->next_hop, millis() - found->rxTimeMsec, found->relayed_by[0], found->relayed_by[1], found->relayed_by[2], relayer); #endif return wasRelayer(relayer, *found, wasSole); } /* Check if a certain node was a relayer of a packet in the history given iterator * @return true if node was indeed a relayer, false if not */ bool PacketHistory::wasRelayer(const uint8_t relayer, const PacketRecord &r, bool *wasSole) { bool found = false; bool other_present = false; for (uint8_t i = 0; i < NUM_RELAYERS; ++i) { if (r.relayed_by[i] == relayer) { found = true; } else if (r.relayed_by[i] != 0) { other_present = true; } } if (wasSole) { *wasSole = (found && !other_present); } #if VERBOSE_PACKET_HISTORY LOG_DEBUG("Packet History - was rel.PR.: s=%08x id=%08x rls=%02x %02x %02x / rl=%02x? NO", r.sender, r.id, r.relayed_by[0], r.relayed_by[1], r.relayed_by[2], relayer); #endif return found; } // Remove a relayer from the list of relayers of a packet in the history given an ID and sender void PacketHistory::removeRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender) { if (!initOk()) { LOG_ERROR("Packet History - remove Relayer: NOT INITIALIZED!"); return; } PacketRecord *found = find(sender, id); if (found == NULL) { #if VERBOSE_PACKET_HISTORY LOG_DEBUG("Packet History - remove Relayer s=%08x id=%08x (rl=%02x) NOT FOUND", sender, id, relayer); #endif return; // Nothing to remove } #if VERBOSE_PACKET_HISTORY LOG_DEBUG("Packet History - remove Relayer s=%08x id=%08x rby=%02x %02x %02x, rl:%02x BEFORE", found->sender, found->id, found->relayed_by[0], found->relayed_by[1], found->relayed_by[2], relayer); #endif // nexthop and rxTimeMsec too stay in found entry uint8_t j = 0; uint8_t i = 0; for (; i < NUM_RELAYERS; i++) { if (found->relayed_by[i] != relayer) { found->relayed_by[j] = found->relayed_by[i]; j++; } else found->relayed_by[i] = 0; } for (; j < NUM_RELAYERS; j++) { // Clear the rest of the relayed_by array found->relayed_by[j] = 0; } #if VERBOSE_PACKET_HISTORY LOG_DEBUG("Packet History - remove Relayer s=%08x id=%08x rby=%02x %02x %02x rl:%02x AFTER - removed?%d", found->sender, found->id, found->relayed_by[0], found->relayed_by[1], found->relayed_by[2], relayer, i != j); #endif } // Getters and setters for hop limit fields packed in hop_limit inline uint8_t PacketHistory::getHighestHopLimit(const PacketRecord &r) { return r.hop_limit & HOP_LIMIT_HIGHEST_MASK; } inline void PacketHistory::setHighestHopLimit(PacketRecord &r, uint8_t hopLimit) { r.hop_limit = (r.hop_limit & ~HOP_LIMIT_HIGHEST_MASK) | (hopLimit & HOP_LIMIT_HIGHEST_MASK); } inline uint8_t PacketHistory::getOurTxHopLimit(const PacketRecord &r) { return (r.hop_limit & HOP_LIMIT_OUR_TX_MASK) >> HOP_LIMIT_OUR_TX_SHIFT; } inline void PacketHistory::setOurTxHopLimit(PacketRecord &r, uint8_t hopLimit) { r.hop_limit = (r.hop_limit & ~HOP_LIMIT_OUR_TX_MASK) | ((hopLimit << HOP_LIMIT_OUR_TX_SHIFT) & HOP_LIMIT_OUR_TX_MASK); } ================================================ FILE: src/mesh/PacketHistory.h ================================================ #pragma once #include "NodeDB.h" // Number of relayers we keep track of. Use 6 to be efficient with memory alignment of PacketRecord to 20 bytes #define NUM_RELAYERS 6 #define HOP_LIMIT_HIGHEST_MASK 0x07 // Bits 0-2 #define HOP_LIMIT_OUR_TX_MASK 0x38 // Bits 3-5 #define HOP_LIMIT_OUR_TX_SHIFT 3 // Bits 3-5 /** * This is a mixin that adds a record of past packets we have seen */ class PacketHistory { private: struct PacketRecord { // A record of a recent message broadcast, no need to be visible outside this class. NodeNum sender; PacketId id; uint32_t rxTimeMsec; // Unix time in msecs - the time we received it, 0 means empty uint8_t next_hop; // The next hop asked for this packet uint8_t hop_limit; // bit 0-2: Highest hop limit observed for this packet, // bit 3-5: our hop limit when we first transmitted it uint8_t relayed_by[NUM_RELAYERS]; // Array of nodes that relayed this packet }; // 4B + 4B + 4B + 1B + 1B + 6B = 20B uint32_t recentPacketsCapacity = 0; // Can be set in constructor, no need to recompile. Used to allocate memory for mx_recentPackets. PacketRecord *recentPackets = NULL; // Simple and fixed in size. Debloat. /** Find a packet record in history. * @param sender NodeNum * @param id PacketId * @return pointer to PacketRecord if found, NULL if not found */ PacketRecord *find(NodeNum sender, PacketId id); /** Insert/Replace oldest PacketRecord in mx_recentPackets. * @param r PacketRecord to insert or replace */ void insert(const PacketRecord &r); // Insert or replace a packet record in the history /* Check if a certain node was a relayer of a packet in the history given iterator * If wasSole is not nullptr, it will be set to true if the relayer was the only relayer of that packet * @return true if node was indeed a relayer, false if not */ bool wasRelayer(const uint8_t relayer, const PacketRecord &r, bool *wasSole = nullptr); uint8_t getHighestHopLimit(const PacketRecord &r); void setHighestHopLimit(PacketRecord &r, uint8_t hopLimit); uint8_t getOurTxHopLimit(const PacketRecord &r); void setOurTxHopLimit(PacketRecord &r, uint8_t hopLimit); PacketHistory(const PacketHistory &); // non construction-copyable PacketHistory &operator=(const PacketHistory &); // non copyable public: explicit PacketHistory(uint32_t size = -1); // Constructor with size parameter, default is PACKETHISTORY_MAX ~PacketHistory(); /** * Update recentBroadcasts and return true if we have already seen this packet * * @param withUpdate if true and not found we add an entry to recentPackets * @param wasFallback if not nullptr, packet will be checked for fallback to flooding and value will be set to true if so * @param weWereNextHop if not nullptr, packet will be checked for us being the next hop and value will be set to true if so * @param wasUpgraded if not nullptr, will be set to true if this packet has better hop_limit than previously seen */ bool wasSeenRecently(const meshtastic_MeshPacket *p, bool withUpdate = true, bool *wasFallback = nullptr, bool *weWereNextHop = nullptr, bool *wasUpgraded = nullptr); /* Check if a certain node was a relayer of a packet in the history given an ID and sender * If wasSole is not nullptr, it will be set to true if the relayer was the only relayer of that packet * @return true if node was indeed a relayer, false if not */ bool wasRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender, bool *wasSole = nullptr); // Remove a relayer from the list of relayers of a packet in the history given an ID and sender void removeRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender); // To check if the PacketHistory was initialized correctly by constructor bool initOk(void) { return recentPackets != NULL && recentPacketsCapacity != 0; } }; ================================================ FILE: src/mesh/PhoneAPI.cpp ================================================ #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 "Router.h" #include "SPILock.h" #include "TypeConversions.h" #include "concurrency/LockGuard.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 // Flag to indicate a heartbeat was received and we should send queue status bool heartbeatReceived = false; PhoneAPI::PhoneAPI() { lastContactMsec = millis(); std::fill(std::begin(recentToRadioPacketIds), std::end(recentToRadioPacketIds), 0); } PhoneAPI::~PhoneAPI() { close(); } void PhoneAPI::handleStartConfig() { // Must be before setting state (because state is how we know !connected) if (!isConnected()) { onConnectionChanged(true); observe(&service->fromNumChanged); #ifdef FSCom observe(&xModem.packetReady); #endif } // Allow subclasses to prepare for high-throughput config traffic onConfigStart(); // even if we were already connected - restart our state machine if (config_nonce == SPECIAL_NONCE_ONLY_NODES) { // If client only wants node info, jump directly to sending nodes state = STATE_SEND_OWN_NODEINFO; LOG_INFO("Client only wants node info, skipping other config"); } else { state = STATE_SEND_MY_INFO; } pauseBluetoothLogging = true; spiLock->lock(); filesManifest = getFiles("/", 10); spiLock->unlock(); LOG_DEBUG("Got %d files in manifest", filesManifest.size()); LOG_INFO("Start API client config millis=%u", millis()); // Protect against concurrent BLE callbacks: they run in NimBLE's FreeRTOS task and also touch nodeInfoQueue. { concurrency::LockGuard guard(&nodeInfoMutex); nodeInfoForPhone = {}; nodeInfoQueue.clear(); } resetReadIndex(); } void PhoneAPI::close() { LOG_DEBUG("PhoneAPI::close()"); if (service->api_state == service->STATE_BLE && api_type == TYPE_BLE) service->api_state = service->STATE_DISCONNECTED; else if (service->api_state == service->STATE_WIFI && api_type == TYPE_WIFI) service->api_state = service->STATE_DISCONNECTED; else if (service->api_state == service->STATE_SERIAL && api_type == TYPE_SERIAL) service->api_state = service->STATE_DISCONNECTED; else if (service->api_state == service->STATE_PACKET && api_type == TYPE_PACKET) service->api_state = service->STATE_DISCONNECTED; else if (service->api_state == service->STATE_HTTP && api_type == TYPE_HTTP) service->api_state = service->STATE_DISCONNECTED; else if (service->api_state == service->STATE_ETH && api_type == TYPE_ETH) service->api_state = service->STATE_DISCONNECTED; if (state != STATE_SEND_NOTHING) { state = STATE_SEND_NOTHING; resetReadIndex(); unobserve(&service->fromNumChanged); #ifdef FSCom unobserve(&xModem.packetReady); #endif releasePhonePacket(); // Don't leak phone packets on shutdown releaseQueueStatusPhonePacket(); releaseMqttClientProxyPhonePacket(); releaseClientNotification(); onConnectionChanged(false); fromRadioScratch = {}; toRadioScratch = {}; // Clear cached node info under lock because NimBLE callbacks can still be draining it. { concurrency::LockGuard guard(&nodeInfoMutex); nodeInfoForPhone = {}; nodeInfoQueue.clear(); } packetForPhone = NULL; filesManifest.clear(); filesManifest.shrink_to_fit(); lastPortNumToRadio.clear(); fromRadioNum = 0; config_nonce = 0; config_state = 0; pauseBluetoothLogging = false; heartbeatReceived = false; } } bool PhoneAPI::checkConnectionTimeout() { if (isConnected()) { bool newContact = checkIsConnected(); if (!newContact) { LOG_INFO("Lost phone connection"); close(); return true; } } 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("Disconnect 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_DEBUG("Got MqttClientProxy message"); if (state != STATE_SEND_PACKETS) { LOG_WARN("Ignore MqttClientProxy message while completing config handshake"); break; } if (mqtt && moduleConfig.mqtt.proxy_to_client_enabled && moduleConfig.mqtt.enabled && (channels.anyMqttEnabled() || moduleConfig.mqtt.map_reporting_enabled)) { mqtt->onClientProxyReceive(toRadioScratch.mqttClientProxyMessage); } else { LOG_WARN("MqttClientProxy received but proxy is not enabled, no channels have up/downlink, or map reporting " "not enabled"); } break; #endif case meshtastic_ToRadio_heartbeat_tag: LOG_DEBUG("Got client heartbeat"); heartbeatReceived = true; break; default: // Ignore nop messages break; } } else { LOG_ERROR("Error: ignore malformed toradio"); } 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_UIDATA, 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) { // Respond to heartbeat by sending queue status if (heartbeatReceived) { memset(&fromRadioScratch, 0, sizeof(fromRadioScratch)); fromRadioScratch.which_payload_variant = meshtastic_FromRadio_queueStatus_tag; fromRadioScratch.queueStatus = router->getQueueStatus(); heartbeatReceived = false; size_t numbytes = pb_encode_to_bytes(buf, meshtastic_FromRadio_size, &meshtastic_FromRadio_msg, &fromRadioScratch); LOG_DEBUG("FromRadio=STATE_SEND_QUEUE_STATUS, numbytes=%u", numbytes); return numbytes; } if (!available()) { return 0; } // In case we send a FromRadio packet memset(&fromRadioScratch, 0, sizeof(fromRadioScratch)); // Advance states as needed switch (state) { case STATE_SEND_NOTHING: LOG_DEBUG("FromRadio=STATE_SEND_NOTHING"); break; case STATE_SEND_MY_INFO: LOG_DEBUG("FromRadio=STATE_SEND_MY_INFO"); // If the user has specified they don't want our node to share its location, make sure to tell the phone // app not to send locations on our behalf. fromRadioScratch.which_payload_variant = meshtastic_FromRadio_my_info_tag; strncpy(myNodeInfo.pio_env, optstr(APP_ENV), sizeof(myNodeInfo.pio_env)); myNodeInfo.nodedb_count = static_cast(nodeDB->getNumMeshNodes()); fromRadioScratch.my_info = myNodeInfo; state = STATE_SEND_UIDATA; service->refreshLocalMeshNode(); // Update my NodeInfo because the client will be asking for it soon. break; case STATE_SEND_UIDATA: LOG_INFO("getFromRadio=STATE_SEND_UIDATA"); fromRadioScratch.which_payload_variant = meshtastic_FromRadio_deviceuiConfig_tag; fromRadioScratch.deviceuiConfig = uiconfig; state = STATE_SEND_OWN_NODEINFO; break; case STATE_SEND_OWN_NODEINFO: { LOG_DEBUG("Send My NodeInfo"); auto us = nodeDB->readNextMeshNode(readIndex); if (us) { auto info = TypeConversions::ConvertToNodeInfo(us); info.has_hops_away = false; info.is_favorite = true; { concurrency::LockGuard guard(&nodeInfoMutex); nodeInfoForPhone = info; } fromRadioScratch.which_payload_variant = meshtastic_FromRadio_node_info_tag; fromRadioScratch.node_info = info; // Should allow us to resume sending NodeInfo in STATE_SEND_OTHER_NODEINFOS { concurrency::LockGuard guard(&nodeInfoMutex); nodeInfoForPhone.num = 0; } } if (config_nonce == SPECIAL_NONCE_ONLY_NODES) { // If client only wants node info, jump directly to sending nodes state = STATE_SEND_OTHER_NODEINFOS; onNowHasData(0); } else { state = STATE_SEND_METADATA; } break; } case STATE_SEND_METADATA: LOG_DEBUG("Send device metadata"); fromRadioScratch.which_payload_variant = meshtastic_FromRadio_metadata_tag; fromRadioScratch.metadata = getDeviceMetadata(); state = STATE_SEND_CHANNELS; break; case STATE_SEND_CHANNELS: fromRadioScratch.which_payload_variant = meshtastic_FromRadio_channel_tag; fromRadioScratch.channel = channels.getByIndex(config_state); config_state++; // Advance when we have sent all of our Channels if (config_state >= MAX_NUM_CHANNELS) { LOG_DEBUG("Send channels %d", config_state); state = STATE_SEND_CONFIG; config_state = _meshtastic_AdminMessage_ConfigType_MIN + 1; } break; case STATE_SEND_CONFIG: fromRadioScratch.which_payload_variant = meshtastic_FromRadio_config_tag; switch (config_state) { case meshtastic_Config_device_tag: LOG_DEBUG("Send config: device"); fromRadioScratch.config.which_payload_variant = meshtastic_Config_device_tag; fromRadioScratch.config.payload_variant.device = config.device; break; case meshtastic_Config_position_tag: LOG_DEBUG("Send config: position"); fromRadioScratch.config.which_payload_variant = meshtastic_Config_position_tag; fromRadioScratch.config.payload_variant.position = config.position; break; case meshtastic_Config_power_tag: LOG_DEBUG("Send config: power"); fromRadioScratch.config.which_payload_variant = meshtastic_Config_power_tag; fromRadioScratch.config.payload_variant.power = config.power; fromRadioScratch.config.payload_variant.power.ls_secs = default_ls_secs; break; case meshtastic_Config_network_tag: LOG_DEBUG("Send config: network"); fromRadioScratch.config.which_payload_variant = meshtastic_Config_network_tag; fromRadioScratch.config.payload_variant.network = config.network; break; case meshtastic_Config_display_tag: LOG_DEBUG("Send config: display"); fromRadioScratch.config.which_payload_variant = meshtastic_Config_display_tag; fromRadioScratch.config.payload_variant.display = config.display; break; case meshtastic_Config_lora_tag: LOG_DEBUG("Send config: lora"); fromRadioScratch.config.which_payload_variant = meshtastic_Config_lora_tag; fromRadioScratch.config.payload_variant.lora = config.lora; break; case meshtastic_Config_bluetooth_tag: LOG_DEBUG("Send config: bluetooth"); fromRadioScratch.config.which_payload_variant = meshtastic_Config_bluetooth_tag; fromRadioScratch.config.payload_variant.bluetooth = config.bluetooth; break; case meshtastic_Config_security_tag: LOG_DEBUG("Send config: security"); fromRadioScratch.config.which_payload_variant = meshtastic_Config_security_tag; fromRadioScratch.config.payload_variant.security = config.security; break; case meshtastic_Config_sessionkey_tag: LOG_DEBUG("Send config: sessionkey"); fromRadioScratch.config.which_payload_variant = meshtastic_Config_sessionkey_tag; break; case meshtastic_Config_device_ui_tag: // NOOP! fromRadioScratch.config.which_payload_variant = meshtastic_Config_device_ui_tag; break; default: LOG_ERROR("Unknown config type %d", config_state); } // NOTE: The phone app needs to know the ls_secs value so it can properly expect sleep behavior. // So even if we internally use 0 to represent 'use default' we still need to send the value we are // using to the app (so that even old phone apps work with new device loads). config_state++; // Advance when we have sent all of our config objects if (config_state > (_meshtastic_AdminMessage_ConfigType_MAX + 1)) { state = STATE_SEND_MODULECONFIG; config_state = _meshtastic_AdminMessage_ModuleConfigType_MIN + 1; } break; case STATE_SEND_MODULECONFIG: fromRadioScratch.which_payload_variant = meshtastic_FromRadio_moduleConfig_tag; switch (config_state) { case meshtastic_ModuleConfig_mqtt_tag: LOG_DEBUG("Send module config: mqtt"); fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_mqtt_tag; fromRadioScratch.moduleConfig.payload_variant.mqtt = moduleConfig.mqtt; break; case meshtastic_ModuleConfig_serial_tag: LOG_DEBUG("Send module config: serial"); fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_serial_tag; fromRadioScratch.moduleConfig.payload_variant.serial = moduleConfig.serial; break; case meshtastic_ModuleConfig_external_notification_tag: LOG_DEBUG("Send module config: ext notification"); fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_external_notification_tag; fromRadioScratch.moduleConfig.payload_variant.external_notification = moduleConfig.external_notification; break; case meshtastic_ModuleConfig_store_forward_tag: LOG_DEBUG("Send module config: store forward"); fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_store_forward_tag; fromRadioScratch.moduleConfig.payload_variant.store_forward = moduleConfig.store_forward; break; case meshtastic_ModuleConfig_range_test_tag: LOG_DEBUG("Send module config: range test"); fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_range_test_tag; fromRadioScratch.moduleConfig.payload_variant.range_test = moduleConfig.range_test; break; case meshtastic_ModuleConfig_telemetry_tag: LOG_DEBUG("Send module config: telemetry"); fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_telemetry_tag; fromRadioScratch.moduleConfig.payload_variant.telemetry = moduleConfig.telemetry; break; case meshtastic_ModuleConfig_canned_message_tag: LOG_DEBUG("Send module config: canned message"); fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_canned_message_tag; fromRadioScratch.moduleConfig.payload_variant.canned_message = moduleConfig.canned_message; break; case meshtastic_ModuleConfig_audio_tag: LOG_DEBUG("Send module config: audio"); fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_audio_tag; fromRadioScratch.moduleConfig.payload_variant.audio = moduleConfig.audio; break; case meshtastic_ModuleConfig_remote_hardware_tag: LOG_DEBUG("Send module config: remote hardware"); fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_remote_hardware_tag; fromRadioScratch.moduleConfig.payload_variant.remote_hardware = moduleConfig.remote_hardware; break; case meshtastic_ModuleConfig_neighbor_info_tag: LOG_DEBUG("Send module config: neighbor info"); fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_neighbor_info_tag; fromRadioScratch.moduleConfig.payload_variant.neighbor_info = moduleConfig.neighbor_info; break; case meshtastic_ModuleConfig_detection_sensor_tag: LOG_DEBUG("Send module config: detection sensor"); fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_detection_sensor_tag; fromRadioScratch.moduleConfig.payload_variant.detection_sensor = moduleConfig.detection_sensor; break; case meshtastic_ModuleConfig_ambient_lighting_tag: LOG_DEBUG("Send module config: ambient lighting"); fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_ambient_lighting_tag; fromRadioScratch.moduleConfig.payload_variant.ambient_lighting = moduleConfig.ambient_lighting; break; case meshtastic_ModuleConfig_paxcounter_tag: LOG_DEBUG("Send module config: paxcounter"); fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_paxcounter_tag; fromRadioScratch.moduleConfig.payload_variant.paxcounter = moduleConfig.paxcounter; break; case meshtastic_ModuleConfig_traffic_management_tag: LOG_DEBUG("Send module config: traffic management"); fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_traffic_management_tag; fromRadioScratch.moduleConfig.payload_variant.traffic_management = moduleConfig.traffic_management; break; default: LOG_ERROR("Unknown module config type %d", config_state); } config_state++; // Advance when we have sent all of our ModuleConfig objects if (config_state > (_meshtastic_AdminMessage_ModuleConfigType_MAX + 1)) { // Handle special nonce behaviors: // - SPECIAL_NONCE_ONLY_CONFIG: Skip node info, go directly to file manifest // - SPECIAL_NONCE_ONLY_NODES: After sending nodes, skip to complete if (config_nonce == SPECIAL_NONCE_ONLY_CONFIG) { state = STATE_SEND_FILEMANIFEST; } else { state = STATE_SEND_OTHER_NODEINFOS; onNowHasData(0); } config_state = 0; } break; case STATE_SEND_OTHER_NODEINFOS: { if (readIndex == 2) { // readIndex==2 will be true for the first non-us node LOG_INFO("Start sending nodeinfos millis=%u", millis()); } meshtastic_NodeInfo infoToSend = {}; { concurrency::LockGuard guard(&nodeInfoMutex); if (nodeInfoForPhone.num == 0 && !nodeInfoQueue.empty()) { // Serve the next cached node without re-reading from the DB iterator. nodeInfoForPhone = nodeInfoQueue.front(); nodeInfoQueue.pop_front(); } infoToSend = nodeInfoForPhone; if (infoToSend.num != 0) nodeInfoForPhone = {}; } if (infoToSend.num != 0) { // Just in case we stored a different user.id in the past, but should never happen going forward sprintf(infoToSend.user.id, "!%08x", infoToSend.num); // Logging this really slows down sending nodes on initial connection because the serial console is so slow, so only // uncomment if you really need to: // LOG_INFO("nodeinfo: num=0x%x, lastseen=%u, id=%s, name=%s", nodeInfoForPhone.num, nodeInfoForPhone.last_heard, // nodeInfoForPhone.user.id, nodeInfoForPhone.user.long_name); // Occasional progress logging. (readIndex==2 will be true for the first non-us node) if (readIndex == 2 || readIndex % 20 == 0) { LOG_DEBUG("nodeinfo: %d/%d", readIndex, nodeDB->getNumMeshNodes()); } fromRadioScratch.which_payload_variant = meshtastic_FromRadio_node_info_tag; fromRadioScratch.node_info = infoToSend; prefetchNodeInfos(); } else { LOG_DEBUG("Done sending %d of %d nodeinfos millis=%u", readIndex, nodeDB->getNumMeshNodes(), millis()); concurrency::LockGuard guard(&nodeInfoMutex); nodeInfoQueue.clear(); state = STATE_SEND_FILEMANIFEST; // Go ahead and send that ID right now return getFromRadio(buf); } break; } case STATE_SEND_FILEMANIFEST: { LOG_DEBUG("FromRadio=STATE_SEND_FILEMANIFEST"); // last element if (config_state == filesManifest.size() || config_nonce == SPECIAL_NONCE_ONLY_NODES) { // also handles an empty filesManifest config_state = 0; filesManifest.clear(); // Skip to complete packet sendConfigComplete(); } else { fromRadioScratch.which_payload_variant = meshtastic_FromRadio_fileInfo_tag; fromRadioScratch.fileInfo = filesManifest.at(config_state); LOG_DEBUG("File: %s (%d) bytes", fromRadioScratch.fileInfo.file_name, fromRadioScratch.fileInfo.size_bytes); config_state++; } break; } case STATE_SEND_COMPLETE_ID: sendConfigComplete(); break; case STATE_SEND_PACKETS: pauseBluetoothLogging = false; // Do we have a message from the mesh or packet from the local device? LOG_DEBUG("FromRadio=STATE_SEND_PACKETS"); if (queueStatusPacketForPhone) { fromRadioScratch.which_payload_variant = meshtastic_FromRadio_queueStatus_tag; fromRadioScratch.queueStatus = *queueStatusPacketForPhone; releaseQueueStatusPhonePacket(); } else if (mqttClientProxyMessageForPhone) { fromRadioScratch.which_payload_variant = meshtastic_FromRadio_mqttClientProxyMessage_tag; fromRadioScratch.mqttClientProxyMessage = *mqttClientProxyMessageForPhone; releaseMqttClientProxyPhonePacket(); } else if (xmodemPacketForPhone.control != meshtastic_XModem_Control_NUL) { fromRadioScratch.which_payload_variant = meshtastic_FromRadio_xmodemPacket_tag; fromRadioScratch.xmodemPacket = xmodemPacketForPhone; xmodemPacketForPhone = meshtastic_XModem_init_zero; } else if (clientNotification) { fromRadioScratch.which_payload_variant = meshtastic_FromRadio_clientNotification_tag; fromRadioScratch.clientNotification = *clientNotification; releaseClientNotification(); } else if (packetForPhone) { printPacket("phone downloaded packet", packetForPhone); // Encapsulate as a FromRadio packet fromRadioScratch.which_payload_variant = meshtastic_FromRadio_packet_tag; fromRadioScratch.packet = *packetForPhone; releasePhonePacket(); } break; default: LOG_ERROR("getFromRadio unexpected state %d", state); } // Do we have a message from the mesh? if (fromRadioScratch.which_payload_variant != 0) { // Encapsulate as a FromRadio packet size_t numbytes = pb_encode_to_bytes(buf, meshtastic_FromRadio_size, &meshtastic_FromRadio_msg, &fromRadioScratch); // VERY IMPORTANT to not print debug messages while writing to fromRadioScratch - because we use that same buffer // for logging (when we are encapsulating with protobufs) return numbytes; } LOG_DEBUG("No FromRadio packet available"); return 0; } void PhoneAPI::sendConfigComplete() { LOG_INFO("Config Send Complete millis=%u", millis()); fromRadioScratch.which_payload_variant = meshtastic_FromRadio_config_complete_id_tag; fromRadioScratch.config_complete_id = config_nonce; config_nonce = 0; state = STATE_SEND_PACKETS; if (api_type == TYPE_BLE) { service->api_state = service->STATE_BLE; } else if (api_type == TYPE_WIFI) { service->api_state = service->STATE_WIFI; } else if (api_type == TYPE_SERIAL) { service->api_state = service->STATE_SERIAL; } else if (api_type == TYPE_PACKET) { service->api_state = service->STATE_PACKET; } else if (api_type == TYPE_HTTP) { service->api_state = service->STATE_HTTP; } else if (api_type == TYPE_ETH) { service->api_state = service->STATE_ETH; } // Allow subclasses to know we've entered steady-state so they can lower power consumption onConfigComplete(); pauseBluetoothLogging = false; } void PhoneAPI::releasePhonePacket() { if (packetForPhone) { service->releaseToPool(packetForPhone); // we just copied the bytes, so don't need this buffer anymore packetForPhone = NULL; } } void PhoneAPI::releaseQueueStatusPhonePacket() { if (queueStatusPacketForPhone) { service->releaseQueueStatusToPool(queueStatusPacketForPhone); queueStatusPacketForPhone = NULL; } } void PhoneAPI::prefetchNodeInfos() { bool added = false; // Keep the queue topped up so BLE reads stay responsive even if DB fetches take a moment. { concurrency::LockGuard guard(&nodeInfoMutex); while (nodeInfoQueue.size() < kNodePrefetchDepth) { auto nextNode = nodeDB->readNextMeshNode(readIndex); if (!nextNode) break; auto info = TypeConversions::ConvertToNodeInfo(nextNode); bool isUs = info.num == nodeDB->getNodeNum(); info.hops_away = isUs ? 0 : info.hops_away; info.last_heard = isUs ? getValidTime(RTCQualityFromNet) : info.last_heard; info.snr = isUs ? 0 : info.snr; info.via_mqtt = isUs ? false : info.via_mqtt; info.is_favorite = info.is_favorite || isUs; nodeInfoQueue.push_back(info); added = true; } } if (added) onNowHasData(0); } void PhoneAPI::releaseMqttClientProxyPhonePacket() { if (mqttClientProxyMessageForPhone) { service->releaseMqttClientProxyMessageToPool(mqttClientProxyMessageForPhone); mqttClientProxyMessageForPhone = NULL; } } void PhoneAPI::releaseClientNotification() { if (clientNotification) { service->releaseClientNotificationToPool(clientNotification); clientNotification = NULL; } } /** * Return true if we have data available to send to the phone */ bool PhoneAPI::available() { switch (state) { case STATE_SEND_NOTHING: return false; case STATE_SEND_MY_INFO: case STATE_SEND_UIDATA: case STATE_SEND_CHANNELS: case STATE_SEND_CONFIG: case STATE_SEND_MODULECONFIG: case STATE_SEND_METADATA: case STATE_SEND_OWN_NODEINFO: case STATE_SEND_FILEMANIFEST: case STATE_SEND_COMPLETE_ID: return true; case STATE_SEND_OTHER_NODEINFOS: { concurrency::LockGuard guard(&nodeInfoMutex); if (nodeInfoQueue.empty()) { // Drop the lock before prefetching; prefetchNodeInfos() will re-acquire it. goto PREFETCH_NODEINFO; } } return true; // Always say we have something, because we might need to advance our state machine PREFETCH_NODEINFO: prefetchNodeInfos(); return true; case STATE_SEND_PACKETS: { if (!queueStatusPacketForPhone) queueStatusPacketForPhone = service->getQueueStatusForPhone(); if (!mqttClientProxyMessageForPhone) mqttClientProxyMessageForPhone = service->getMqttClientProxyMessageForPhone(); if (!clientNotification) clientNotification = service->getClientNotificationForPhone(); bool hasPacket = !!queueStatusPacketForPhone || !!mqttClientProxyMessageForPhone || !!clientNotification; if (hasPacket) return true; #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; 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 defined(ARCH_PORTDUINO) // For use with the simulator, we should not ignore duplicate packets from the phone if (SimRadio::instance == nullptr) #endif if (p.id > 0 && wasSeenRecently(p.id)) { LOG_DEBUG("Ignore packet from phone, already seen recently"); return false; } if (p.decoded.portnum == meshtastic_PortNum_TRACEROUTE_APP && lastPortNumToRadio[p.decoded.portnum] && Throttle::isWithinTimespanMs(lastPortNumToRadio[p.decoded.portnum], THIRTY_SECONDS_MS)) { LOG_WARN("Rate limit portnum %d", p.decoded.portnum); sendNotification(meshtastic_LogRecord_Level_WARNING, p.id, "TraceRoute can only be sent once every 30 seconds"); meshtastic_QueueStatus qs = router->getQueueStatus(); service->sendQueueStatusToPhone(qs, 0, p.id); return false; } else if (p.decoded.portnum == meshtastic_PortNum_TRACEROUTE_APP && isBroadcast(p.to) && p.hop_limit > 0) { sendNotification(meshtastic_LogRecord_Level_WARNING, p.id, "Multi-hop traceroute to broadcast address is not allowed"); meshtastic_QueueStatus qs = router->getQueueStatus(); service->sendQueueStatusToPhone(qs, 0, p.id); return false; } else if (IS_ONE_OF(p.decoded.portnum, meshtastic_PortNum_POSITION_APP, meshtastic_PortNum_WAYPOINT_APP, meshtastic_PortNum_ALERT_APP, meshtastic_PortNum_TELEMETRY_APP) && lastPortNumToRadio[p.decoded.portnum] && Throttle::isWithinTimespanMs(lastPortNumToRadio[p.decoded.portnum], TEN_SECONDS_MS)) { // TODO: [Issue #6700] Make this rate limit throttling scale up / down with the preset LOG_WARN("Rate limit portnum %d", p.decoded.portnum); meshtastic_QueueStatus qs = router->getQueueStatus(); service->sendQueueStatusToPhone(qs, 0, p.id); // FIXME: Figure out why this continues to happen // sendNotification(meshtastic_LogRecord_Level_WARNING, p.id, "Position can only be sent once every 5 seconds"); return false; } else if (p.decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP && lastPortNumToRadio[p.decoded.portnum] && Throttle::isWithinTimespanMs(lastPortNumToRadio[p.decoded.portnum], TWO_SECONDS_MS)) { LOG_WARN("Rate limit portnum %d", p.decoded.portnum); meshtastic_QueueStatus qs = router->getQueueStatus(); service->sendQueueStatusToPhone(qs, 0, p.id); service->sendRoutingErrorResponse(meshtastic_Routing_Error_RATE_LIMIT_EXCEEDED, &p); // sendNotification(meshtastic_LogRecord_Level_WARNING, p.id, "Text messages can only be sent once every 2 seconds"); return false; } // Upgrade traceroute requests from phone to use reliable delivery, matching TraceRouteModule if (p.decoded.portnum == meshtastic_PortNum_TRACEROUTE_APP && !isBroadcast(p.to)) { // Use reliable delivery for traceroute requests (which will be copied to traceroute responses by setReplyTo) p.want_ack = true; } lastPortNumToRadio[p.decoded.portnum] = millis(); service->handleToRadio(p); return true; } /// If the mesh service tells us fromNum has changed, tell the phone int PhoneAPI::onNotify(uint32_t newValue) { bool timeout = checkConnectionTimeout(); // a handy place to check if we've heard from the phone (since the BLE version // doesn't call this from idle) if (state == STATE_SEND_PACKETS) { LOG_INFO("Tell client we have new packets %u", newValue); onNowHasData(newValue); } else { LOG_DEBUG("Client not yet interested in packets (state=%d)", state); } return timeout ? -1 : 0; // If we timed out, MeshService should stop iterating through observers as we just removed one } ================================================ FILE: src/mesh/PhoneAPI.h ================================================ #pragma once #include "Observer.h" #include "concurrency/Lock.h" #include "mesh-pb-constants.h" #include "meshtastic/portnums.pb.h" #include #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_ONLY_CONFIG 69420 #define SPECIAL_NONCE_ONLY_NODES 69421 // ( ͡° ͜ʖ ͡°) /** * 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_UIDATA, // send stored data for device-ui STATE_SEND_MY_INFO, // send our my info record STATE_SEND_OWN_NODEINFO, STATE_SEND_METADATA, STATE_SEND_CHANNELS, // Send all channels STATE_SEND_CONFIG, // Replacement for the old Radioconfig STATE_SEND_MODULECONFIG, // Send Module specific config STATE_SEND_OTHER_NODEINFOS, // states progress in this order as the device sends to to the client STATE_SEND_FILEMANIFEST, // Send file manifest STATE_SEND_COMPLETE_ID, STATE_SEND_PACKETS // send packets or debug strings }; State state = STATE_SEND_NOTHING; 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; // Prefetched node info entries ready for immediate transmission to the phone. std::deque nodeInfoQueue; // Tunable size of the node info cache so we can keep BLE reads non-blocking. static constexpr size_t kNodePrefetchDepth = 4; // Protect nodeInfoForPhone + nodeInfoQueue because NimBLE callbacks run in a separate FreeRTOS task. concurrency::Lock nodeInfoMutex; meshtastic_ToRadio toRadioScratch = { 0}; // this is a static scratch object, any data must be copied elsewhere before returning /// 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; } bool isSendingPackets() { return state == STATE_SEND_PACKETS; } 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) {} /// Subclasses can use these lifecycle hooks for transport-specific behavior around config/steady-state /// (i.e. BLE connection params) virtual void onConfigStart() {} virtual void onConfigComplete() {} /// begin a new connection void handleStartConfig(); enum APIType { TYPE_NONE, // Initial state, don't send anything until the client starts asking for config TYPE_BLE, TYPE_WIFI, TYPE_SERIAL, TYPE_PACKET, TYPE_HTTP, TYPE_ETH }; APIType api_type = TYPE_NONE; private: void releasePhonePacket(); void releaseQueueStatusPhonePacket(); void prefetchNodeInfos(); void releaseMqttClientProxyPhonePacket(); void releaseClientNotification(); 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; }; ================================================ FILE: src/mesh/PointerQueue.h ================================================ #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 }; ================================================ FILE: src/mesh/ProtobufModule.cpp ================================================ #include "ProtobufModule.h" #include "configuration.h" ================================================ FILE: src/mesh/ProtobufModule.h ================================================ #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: uint16_t numOnlineNodes = 0; /** Constructor * name is for debugging output */ ProtobufModule(const char *_name, meshtastic_PortNum _ourPortNum, const pb_msgdesc_t *_fields) : SinglePortModule(_name, _ourPortNum), fields(_fields) { } protected: /** * Handle a received message, the data field in the message is already decoded and is provided * * In general decoded will always be !NULL. But in some special applications (where you have handling packets * for multiple port numbers, decoding will ONLY be attempted for packets where the portnum matches our expected ourPortNum. */ virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, T *decoded) = 0; /** Called to make changes to a particular incoming message */ virtual void alterReceivedProtobuf(meshtastic_MeshPacket &mp, T *decoded){}; /** * Return a mesh packet which has been preinited with a particular protobuf data payload and port number. * You can then send this packet (after customizing any of the payload fields you might need) with * service->sendToMesh() */ meshtastic_MeshPacket *allocDataProtobuf(const T &payload) { // Update our local node info with our position (even if we don't decide to update anyone else) meshtastic_MeshPacket *p = allocDataPacket(); p->decoded.payload.size = pb_encode_to_bytes(p->decoded.payload.bytes, sizeof(p->decoded.payload.bytes), fields, &payload); // LOG_DEBUG("did encode"); return p; } /** * Gets the short name from the sender of the mesh packet * Returns "???" if unknown sender */ const char *getSenderShortName(const meshtastic_MeshPacket &mp) { auto node = nodeDB->getMeshNode(getFrom(&mp)); const char *sender = (node) ? node->user.short_name : "???"; return sender; } int handleStatusUpdate(const meshtastic::Status *arg) { if (arg->getStatusType() == STATUS_TYPE_NODE) { numOnlineNodes = nodeStatus->getNumOnline(); } return 0; } private: /** Called to handle a particular incoming message @return ProcessMessage::STOP if you've guaranteed you've handled this message and no other handlers should be considered for it */ virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override { // FIXME - we currently update position data in the DB only if the message was a broadcast or destined to us // it would be better to update even if the message was destined to others. auto &p = mp.decoded; 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; LOG_INFO("Received %s from=0x%0x, id=0x%x, portnum=%d, payloadlen=%d", name, mp.from, mp.id, p.portnum, p.payload.size); } else { LOG_ERROR("Error decoding proto module!"); // if we can't decode it, nobody can process it! return ProcessMessage::STOP; } } return handleReceivedProtobuf(mp, decoded) ? ProcessMessage::STOP : ProcessMessage::CONTINUE; } /** Called to alter a particular incoming message */ virtual void alterReceived(meshtastic_MeshPacket &mp) override { T scratch; T *decoded = NULL; if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag && mp.decoded.portnum == ourPortNum) { memset(&scratch, 0, sizeof(scratch)); const meshtastic_Data &p = mp.decoded; if (pb_decode_from_bytes(p.payload.bytes, p.payload.size, fields, &scratch)) { decoded = &scratch; } else { LOG_ERROR("Error decoding proto module!"); // if we can't decode it, nobody can process it! return; } return alterReceivedProtobuf(mp, decoded); } } }; ================================================ FILE: src/mesh/RF95Interface.cpp ================================================ #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 #if ARCH_PORTDUINO #define RF95_MAX_POWER portduino_config.rf95_max_power #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 (portduino_config.lora_txen_pin.pin != RADIOLIB_NC) { digitalWrite(portduino_config.lora_txen_pin.pin, txon ? 1 : 0); } #endif #ifdef RF95_RXEN digitalWrite(RF95_RXEN, txon ? 0 : 1); #elif ARCH_PORTDUINO if (portduino_config.lora_rxen_pin.pin != RADIOLIB_NC) { digitalWrite(portduino_config.lora_rxen_pin.pin, txon ? 0 : 1); } #endif } /// Initialise the Driver transport hardware and software. /// Make sure the Driver is properly configured before calling init(). /// \return true if initialisation succeeded. bool RF95Interface::init() { RadioLibInterface::init(); #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 limitPower(RF95_MAX_POWER); 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 (portduino_config.lora_txen_pin.pin != RADIOLIB_NC) { pinMode(portduino_config.lora_txen_pin.pin, OUTPUT); digitalWrite(portduino_config.lora_txen_pin.pin, 0); } if (portduino_config.lora_rxen_pin.pin != RADIOLIB_NC) { pinMode(portduino_config.lora_rxen_pin.pin, OUTPUT); digitalWrite(portduino_config.lora_rxen_pin.pin, 0); } #endif setTransmitEnable(false); int res = lora->begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength); LOG_INFO("RF95 init result %d", res); if (res == RADIOLIB_ERR_CHIP_NOT_FOUND || res == RADIOLIB_ERR_SPI_CMD_FAILED) return false; LOG_INFO("Frequency set to %f", getFreq()); LOG_INFO("Bandwidth set to %f", bw); LOG_INFO("Power output set to %d", power); #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 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()); LOG_DEBUG("Corrected frequency offset: %f", lora->getFrequencyError()); } void RF95Interface::setStandby() { int err = lora->standby(); if (err != RADIOLIB_ERR_NONE) LOG_ERROR("RF95 standby %s%d", radioLibErr, err); assert(err == RADIOLIB_ERR_NONE); 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); checkRxDoneIrqFlag(); } 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 ================================================ FILE: src/mesh/RF95Interface.h ================================================ #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; uint32_t getPacketTime(uint32_t pl, bool received) override { return computePacketTime(*lora, pl, received); } private: /** Some boards require GPIO control of tx vs rx paths */ void setTransmitEnable(bool txon); }; #endif ================================================ FILE: src/mesh/RadioInterface.cpp ================================================ #include "RadioInterface.h" #include "Channels.h" #include "DisplayFormatters.h" #include "LLCC68Interface.h" #include "LR1110Interface.h" #include "LR1120Interface.h" #include "LR1121Interface.h" #include "MeshRadio.h" #include "MeshService.h" #include "NodeDB.h" #include "RF95Interface.h" #include "Router.h" #include "SX1262Interface.h" #include "SX1268Interface.h" #include "SX1280Interface.h" #include "configuration.h" #include "detect/LoRaRadioType.h" #include "main.h" #include "meshUtils.h" // for pow_of_2 #include "sleep.h" #include #include #include #include #ifdef ARCH_PORTDUINO #include "platform/portduino/PortduinoGlue.h" #include "platform/portduino/SimRadio.h" #include "platform/portduino/USBHal.h" #endif #ifdef ARCH_STM32WL #include "STM32WLE5JCInterface.h" #endif static const meshtastic_Config_LoRaConfig_ModemPreset PRESETS_STD[] = { meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST, meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO, meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO, MODEM_PRESET_END}; static const meshtastic_Config_LoRaConfig_ModemPreset PRESETS_EU_868[] = { meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST, meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE, MODEM_PRESET_END}; static const meshtastic_Config_LoRaConfig_ModemPreset PRESETS_UNDEF[] = {meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, MODEM_PRESET_END}; // Region profiles: bundle preset list + regulatory parameters shared across regions // presets, spacing, padding, audio, licensed, text throttle, position throttle, telemetry throttle, override slot const RegionProfile PROFILE_STD = {PRESETS_STD, 0, 0, true, false, 0, 0, 0, 0}; const RegionProfile PROFILE_EU868 = {PRESETS_EU_868, 0, 0, false, false, 0, 0, 0, 0}; const RegionProfile PROFILE_UNDEF = {PRESETS_UNDEF, 0, 0, true, false, 0, 0, 0, 0}; #define RDEF(name, freq_start, freq_end, duty_cycle, power_limit, frequency_switching, wide_lora, profile_ptr) \ { \ meshtastic_Config_LoRaConfig_RegionCode_##name, freq_start, freq_end, duty_cycle, power_limit, frequency_switching, \ wide_lora, &profile_ptr, #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, 30, false, false, PROFILE_STD), /* EN300220 ETSI V3.2.1 [Table B.1, Item H, p. 21] https://www.etsi.org/deliver/etsi_en/300200_300299/30022002/03.02.01_60/en_30022002v030201p.pdf FIXME: https://github.com/meshtastic/firmware/issues/3371 */ RDEF(EU_433, 433.0f, 434.0f, 10, 10, false, false, PROFILE_STD), /* 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, 27, false, false, PROFILE_EU868), /* https://lora-alliance.org/wp-content/uploads/2020/11/lorawan_regional_parameters_v1.0.3reva_0.pdf */ RDEF(CN, 470.0f, 510.0f, 100, 19, false, false, PROFILE_STD), /* 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, 13, false, false, PROFILE_STD), /* 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 Also used in Brazil. */ RDEF(ANZ, 915.0f, 928.0f, 100, 30, false, false, PROFILE_STD), /* 433.05 - 434.79 MHz, 25mW EIRP max, No duty cycle restrictions AU Low Interference Potential https://www.acma.gov.au/licences/low-interference-potential-devices-lipd-class-licence NZ General User Radio Licence for Short Range Devices https://gazette.govt.nz/notice/id/2022-go3100 */ RDEF(ANZ_433, 433.05f, 434.79f, 100, 14, false, false, PROFILE_STD), /* 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, 20, false, false, PROFILE_STD), /* https://www.law.go.kr/LSW/admRulLsInfoP.do?admRulId=53943&efYd=0 https://resources.lora-alliance.org/technical-specifications/rp002-1-0-4-regional-parameters */ RDEF(KR, 920.0f, 923.0f, 100, 23, false, false, PROFILE_STD), /* 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, 27, false, false, PROFILE_STD), /* https://lora-alliance.org/wp-content/uploads/2020/11/lorawan_regional_parameters_v1.0.3reva_0.pdf */ RDEF(IN, 865.0f, 867.0f, 100, 30, false, false, PROFILE_STD), /* 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, 36, false, false, PROFILE_STD), /* https://lora-alliance.org/wp-content/uploads/2020/11/lorawan_regional_parameters_v1.0.3reva_0.pdf */ RDEF(TH, 920.0f, 925.0f, 100, 16, false, false, PROFILE_STD), /* 433,05-434,7 Mhz 10 mW 868,0-868,6 Mhz 25 mW https://nkrzi.gov.ua/images/upload/256/5810/PDF_UUZ_19_01_2016.pdf */ RDEF(UA_433, 433.0f, 434.7f, 10, 10, false, false, PROFILE_STD), RDEF(UA_868, 868.0f, 868.6f, 1, 14, false, false, PROFILE_STD), /* 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, 20, false, false, PROFILE_STD), /* 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, 27, true, false, PROFILE_STD), /* 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, 20, false, false, PROFILE_STD), /* 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 antenna allowed https://github.com/meshtastic/firmware/issues/4948#issuecomment-2394926135 */ RDEF(PH_433, 433.0f, 434.7f, 100, 10, false, false, PROFILE_STD), RDEF(PH_868, 868.0f, 869.4f, 100, 14, false, false, PROFILE_STD), RDEF(PH_915, 915.0f, 918.0f, 100, 24, false, false, PROFILE_STD), /* Kazakhstan 433.075 - 434.775 MHz <10 mW EIRP, Low Powered Devices (LPD) 863 - 868 MHz <25 mW EIRP, 500kHz channels allowed, must not be used at airfields https://github.com/meshtastic/firmware/issues/7204 */ RDEF(KZ_433, 433.075f, 434.775f, 100, 10, false, false, PROFILE_STD), RDEF(KZ_863, 863.0f, 868.0f, 100, 30, false, false, PROFILE_STD), /* Nepal 865 MHz to 868 MHz frequency band for IoT (Internet of Things), M2M (Machine-to-Machine), and smart metering use, specifically in non-cellular mode. https://www.nta.gov.np/uploads/contents/Radio-Frequency-Policy-2080-English.pdf */ RDEF(NP_865, 865.0f, 868.0f, 100, 30, false, false, PROFILE_STD), /* Brazil 902 - 907.5 MHz , 1W power limit, no duty cycle restrictions https://github.com/meshtastic/firmware/issues/3741 */ RDEF(BR_902, 902.0f, 907.5f, 100, 30, false, false, PROFILE_STD), /* 2.4 GHZ WLAN Band equivalent. Only for SX128x chips. */ RDEF(LORA_24, 2400.0f, 2483.5f, 100, 10, false, true, PROFILE_STD), /* This needs to be last. Same as US. */ RDEF(UNSET, 902.0f, 928.0f, 100, 30, false, false, PROFILE_UNDEF) }; const RegionInfo *myRegion; bool RadioInterface::uses_default_frequency_slot = true; bool RadioInterface::uses_custom_channel_name = false; static uint8_t bytes[MAX_LORA_PAYLOAD_LEN + 1]; // Global LoRa radio type LoRaRadioType radioType = NO_RADIO; extern RadioLibHal *RadioLibHAL; #if defined(HW_SPI1_DEVICE) && defined(ARCH_ESP32) extern SPIClass SPI1; #endif std::unique_ptr initLoRa() { std::unique_ptr rIf = nullptr; #if ARCH_PORTDUINO SPISettings loraSpiSettings(portduino_config.spiSpeed, MSBFIRST, SPI_MODE0); #else SPISettings loraSpiSettings(4000000, MSBFIRST, SPI_MODE0); #endif #ifdef ARCH_PORTDUINO // as one can't use a function pointer to the class constructor: auto loraModuleInterface = [](LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy) { switch (portduino_config.lora_module) { case use_rf95: return std::unique_ptr(new RF95Interface(hal, cs, irq, rst, busy)); case use_sx1262: return std::unique_ptr(new SX1262Interface(hal, cs, irq, rst, busy)); case use_sx1268: return std::unique_ptr(new SX1268Interface(hal, cs, irq, rst, busy)); case use_sx1280: return std::unique_ptr(new SX1280Interface(hal, cs, irq, rst, busy)); case use_lr1110: return std::unique_ptr(new LR1110Interface(hal, cs, irq, rst, busy)); case use_lr1120: return std::unique_ptr(new LR1120Interface(hal, cs, irq, rst, busy)); case use_lr1121: return std::unique_ptr(new LR1121Interface(hal, cs, irq, rst, busy)); case use_llcc68: return std::unique_ptr(new LLCC68Interface(hal, cs, irq, rst, busy)); case use_simradio: return std::unique_ptr(new SimRadio); default: assert(0); // shouldn't happen return std::unique_ptr(nullptr); } }; LOG_DEBUG("Activate %s radio on SPI port %s", portduino_config.loraModules[portduino_config.lora_module].c_str(), portduino_config.lora_spi_dev.c_str()); if (portduino_config.lora_spi_dev == "ch341") { RadioLibHAL = ch341Hal; } else { if (RadioLibHAL != nullptr) { delete RadioLibHAL; RadioLibHAL = nullptr; } RadioLibHAL = new LockingArduinoHal(SPI, loraSpiSettings); } rIf = loraModuleInterface((LockingArduinoHal *)RadioLibHAL, portduino_config.lora_cs_pin.pin, portduino_config.lora_irq_pin.pin, portduino_config.lora_reset_pin.pin, portduino_config.lora_busy_pin.pin); if (!rIf->init()) { LOG_WARN("No %s radio", portduino_config.loraModules[portduino_config.lora_module].c_str()); rIf = nullptr; exit(EXIT_FAILURE); } else { LOG_INFO("%s init success", portduino_config.loraModules[portduino_config.lora_module].c_str()); } #elif defined(HW_SPI1_DEVICE) LockingArduinoHal *loraHal = new LockingArduinoHal(SPI1, loraSpiSettings); RadioLibHAL = loraHal; #else // HW_SPI1_DEVICE LockingArduinoHal *loraHal = new LockingArduinoHal(SPI, loraSpiSettings); RadioLibHAL = loraHal; #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 = std::unique_ptr( new STM32WLE5JCInterface(loraHal, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY)); if (!rIf->init()) { LOG_WARN("No STM32WL radio"); rIf = nullptr; } else { LOG_INFO("STM32WL init success"); radioType = STM32WLx_RADIO; } } #endif #if defined(RF95_IRQ) && RADIOLIB_EXCLUDE_SX127X != 1 if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { rIf = std::unique_ptr(new RF95Interface(loraHal, LORA_CS, RF95_IRQ, RF95_RESET, RF95_DIO1)); if (!rIf->init()) { LOG_WARN("No RF95 radio"); rIf = nullptr; } else { LOG_INFO("RF95 init success"); radioType = RF95_RADIO; } } #endif #if defined(USE_SX1262) && !defined(ARCH_PORTDUINO) && !defined(TCXO_OPTIONAL) && RADIOLIB_EXCLUDE_SX126X != 1 if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { auto sxIf = std::unique_ptr(new SX1262Interface(loraHal, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY)); #ifdef SX126X_DIO3_TCXO_VOLTAGE sxIf->setTCXOVoltage(SX126X_DIO3_TCXO_VOLTAGE); #endif if (!sxIf->init()) { LOG_WARN("No SX1262 radio"); rIf = nullptr; } else { LOG_INFO("SX1262 init success"); rIf = std::move(sxIf); radioType = SX1262_RADIO; } } #endif #if defined(USE_SX1262) && !defined(ARCH_PORTDUINO) && defined(TCXO_OPTIONAL) if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { // try using the specified TCXO voltage auto sxIf = std::unique_ptr(new SX1262Interface(loraHal, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY)); sxIf->setTCXOVoltage(SX126X_DIO3_TCXO_VOLTAGE); if (!sxIf->init()) { LOG_WARN("No SX1262 radio with TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE); rIf = nullptr; } else { LOG_INFO("SX1262 init success, TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE); rIf = std::move(sxIf); radioType = SX1262_RADIO; } } if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { // If specified TCXO voltage fails, attempt to use DIO3 as a reference instead rIf = std::unique_ptr(new SX1262Interface(loraHal, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY)); if (!rIf->init()) { LOG_WARN("No SX1262 radio with XTAL, Vref 0.0V"); rIf = nullptr; } else { LOG_INFO("SX1262 init success, XTAL, Vref 0.0V"); radioType = SX1262_RADIO; } } #endif #if defined(USE_SX1268) #if defined(SX126X_DIO3_TCXO_VOLTAGE) && defined(TCXO_OPTIONAL) if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { // try using the specified TCXO voltage auto sxIf = std::unique_ptr(new SX1268Interface(loraHal, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY)); sxIf->setTCXOVoltage(SX126X_DIO3_TCXO_VOLTAGE); if (!sxIf->init()) { LOG_WARN("No SX1268 radio with TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE); rIf = nullptr; } else { LOG_INFO("SX1268 init success, TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE); rIf = std::move(sxIf); radioType = SX1268_RADIO; } } #endif if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { rIf = std::unique_ptr(new SX1268Interface(loraHal, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY)); if (!rIf->init()) { LOG_WARN("No SX1268 radio"); rIf = nullptr; } else { LOG_INFO("SX1268 init success"); radioType = SX1268_RADIO; } } #endif #if defined(USE_LLCC68) if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { rIf = std::unique_ptr(new LLCC68Interface(loraHal, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY)); if (!rIf->init()) { LOG_WARN("No LLCC68 radio"); rIf = nullptr; } else { LOG_INFO("LLCC68 init success"); radioType = LLCC68_RADIO; } } #endif #if defined(USE_LR1110) && RADIOLIB_EXCLUDE_LR11X0 != 1 if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { rIf = std::unique_ptr( new LR1110Interface(loraHal, LR1110_SPI_NSS_PIN, LR1110_IRQ_PIN, LR1110_NRESET_PIN, LR1110_BUSY_PIN)); if (!rIf->init()) { LOG_WARN("No LR1110 radio"); rIf = nullptr; } else { LOG_INFO("LR1110 init success"); radioType = LR1110_RADIO; } } #endif #if defined(USE_LR1120) && RADIOLIB_EXCLUDE_LR11X0 != 1 if (!rIf) { rIf = std::unique_ptr( new LR1120Interface(loraHal, LR1120_SPI_NSS_PIN, LR1120_IRQ_PIN, LR1120_NRESET_PIN, LR1120_BUSY_PIN)); if (!rIf->init()) { LOG_WARN("No LR1120 radio"); rIf = nullptr; } else { LOG_INFO("LR1120 init success"); radioType = LR1120_RADIO; } } #endif #if defined(USE_LR1121) && RADIOLIB_EXCLUDE_LR11X0 != 1 if (!rIf) { rIf = std::unique_ptr( new LR1121Interface(loraHal, LR1121_SPI_NSS_PIN, LR1121_IRQ_PIN, LR1121_NRESET_PIN, LR1121_BUSY_PIN)); if (!rIf->init()) { LOG_WARN("No LR1121 radio"); rIf = nullptr; } else { LOG_INFO("LR1121 init success"); radioType = LR1121_RADIO; } } #endif #if defined(USE_SX1280) && RADIOLIB_EXCLUDE_SX128X != 1 if (!rIf) { rIf = std::unique_ptr(new SX1280Interface(loraHal, SX128X_CS, SX128X_DIO1, SX128X_RESET, SX128X_BUSY)); if (!rIf->init()) { LOG_WARN("No SX1280 radio"); rIf = nullptr; } else { LOG_INFO("SX1280 init success"); radioType = SX1280_RADIO; } } #endif // check if the radio chip matches the selected region if ((config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_LORA_24) && rIf && (!rIf->wideLora())) { LOG_WARN("LoRa chip does not support 2.4GHz. Revert to unset"); config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_UNSET; nodeDB->saveToDisk(SEGMENT_CONFIG); if (rIf && !rIf->reconfigure()) { LOG_WARN("Reconfigure failed, rebooting"); if (screen) { screen->showSimpleBanner("Rebooting..."); } rebootAtMsec = millis() + 5000; } } return rIf; } 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; } const RegionInfo *getRegion(meshtastic_Config_LoRaConfig_RegionCode code) { const RegionInfo *r = regions; for (; r->code != meshtastic_Config_LoRaConfig_RegionCode_UNSET && r->code != code; r++) ; return r; } uint32_t RadioInterface::getPacketTime(const meshtastic_MeshPacket *p, bool received) { uint32_t pl = 0; if (p->which_payload_variant == meshtastic_MeshPacket_encrypted_tag) { pl = p->encrypted.size + sizeof(PacketHeader); } else { size_t numbytes = pb_encode_to_bytes(bytes, sizeof(bytes), &meshtastic_Data_msg, &p->decoded); pl = numbytes + sizeof(PacketHeader); } return getPacketTime(pl, received); } /** The delay to use for retransmitting dropped packets */ uint32_t RadioInterface::getRetransmissionMsec(const meshtastic_MeshPacket *p) { size_t numbytes = p->which_payload_variant == meshtastic_MeshPacket_decoded_tag ? pb_encode_to_bytes(bytes, sizeof(bytes), &meshtastic_Data_msg, &p->decoded) : p->encrypted.size + MESHTASTIC_HEADER_LENGTH; uint32_t packetAirtime = getPacketTime(numbytes + sizeof(PacketHeader)); // Make sure enough time has elapsed for this packet to be sent and an ACK is received. // LOG_DEBUG("Waiting for flooding message with airtime %d and slotTime is %d", packetAirtime, slotTimeMsec); float channelUtil = airTime->channelUtilizationPercent(); uint8_t CWsize = map(channelUtil, 0, 100, CWmin, CWmax); // Assuming we pick max. of CWsize and there will be a client with SNR at half the range return 2 * packetAirtime + (pow_of_2(CWsize) + 2 * CWmax + pow_of_2(int((CWmax + CWmin) / 2))) * slotTimeMsec + PROCESSING_TIME_MSEC; } /** The delay to use when we want to send something */ uint32_t RadioInterface::getTxDelayMsec() { /** We wait a random multiple of 'slotTimes' (see definition in header file) in order to avoid collisions. The pool to take a random multiple from is the contention window (CW), which size depends on the current channel utilization. */ float channelUtil = airTime->channelUtilizationPercent(); uint8_t CWsize = map(channelUtil, 0, 100, CWmin, CWmax); // LOG_DEBUG("Current channel utilization is %f so setting CWsize to %d", channelUtil, CWsize); return random(0, pow_of_2(CWsize)) * slotTimeMsec; } /** The CW size to use when calculating SNR_based delays */ uint8_t RadioInterface::getCWsize(float snr) { // The minimum value for a LoRa SNR const int32_t SNR_MIN = -20; // The maximum value for a LoRa SNR const int32_t SNR_MAX = 10; return map(snr, SNR_MIN, SNR_MAX, CWmin, CWmax); } /** The worst-case SNR_based packet delay */ uint32_t RadioInterface::getTxDelayMsecWeightedWorst(float snr) { uint8_t CWsize = getCWsize(snr); // offset the maximum delay for routers: (2 * CWmax * slotTimeMsec) return (2 * CWmax * slotTimeMsec) + pow_of_2(CWsize) * slotTimeMsec; } /** Returns true if we should rebroadcast early like a ROUTER */ bool RadioInterface::shouldRebroadcastEarlyLikeRouter(meshtastic_MeshPacket *p) { // If we are a ROUTER, we always rebroadcast early if (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER) { return true; } return false; } /** The delay to use when we want to flood a message */ uint32_t RadioInterface::getTxDelayMsecWeighted(meshtastic_MeshPacket *p) { // high SNR = large CW size (Long Delay) // low SNR = small CW size (Short Delay) float snr = p->rx_snr; uint32_t delay = 0; uint8_t CWsize = getCWsize(snr); // LOG_DEBUG("rx_snr of %f so setting CWsize to:%d", snr, CWsize); if (shouldRebroadcastEarlyLikeRouter(p)) { delay = random(0, 2 * CWsize) * slotTimeMsec; LOG_DEBUG("rx_snr found in packet. Router: setting tx delay:%d", delay); } else { // offset the maximum delay for routers: (2 * CWmax * slotTimeMsec) delay = (2 * CWmax * slotTimeMsec) + random(0, pow_of_2(CWsize)) * slotTimeMsec; LOG_DEBUG("rx_snr found in packet. Setting tx delay:%d", delay); } return delay; } void printPacket(const char *prefix, const meshtastic_MeshPacket *p) { #if defined(DEBUG_PORT) && !defined(DEBUG_MUTE) std::string out = DEBUG_PORT.mt_sprintf("%s (id=0x%08x fr=0x%08x to=0x%08x, transport = %u, WantAck=%d, HopLim=%d Ch=0x%x", prefix, p->id, p->from, p->to, p->transport_mechanism, p->want_ack, p->hop_limit, p->channel); if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) { auto &s = p->decoded; 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"; out += DEBUG_PORT.mt_sprintf(" len=%d", p->encrypted.size + sizeof(PacketHeader)); } if (p->rx_time != 0) out += DEBUG_PORT.mt_sprintf(" rxtime=%u", p->rx_time); if (p->rx_snr != 0.0) out += DEBUG_PORT.mt_sprintf(" rxSNR=%g", p->rx_snr); if (p->rx_rssi != 0) out += DEBUG_PORT.mt_sprintf(" rxRSSI=%i", p->rx_rssi); if (p->via_mqtt != 0) out += DEBUG_PORT.mt_sprintf(" via MQTT"); if (p->hop_start != 0) out += DEBUG_PORT.mt_sprintf(" hopStart=%d", p->hop_start); if (p->next_hop != 0) out += DEBUG_PORT.mt_sprintf(" nextHop=0x%x", p->next_hop); if (p->relay_node != 0) out += DEBUG_PORT.mt_sprintf(" relay=0x%x", p->relay_node); if (p->priority != 0) out += DEBUG_PORT.mt_sprintf(" priority=%d", p->priority); 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("Start 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 frequency slot (aka 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; } /** * Send an error-level client notification. Safe to call when service is null (e.g. in tests). */ static void sendErrorNotification(const char *msg) { if (!service) return; meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); if (!cn) return; cn->level = meshtastic_LogRecord_Level_ERROR; snprintf(cn->message, sizeof(cn->message), "%s", msg); service->sendClientNotification(cn); } /** * Checks if a region is valid for the current settings. * Returns false if not compatible. */ bool RadioInterface::validateConfigRegion(const meshtastic_Config_LoRaConfig &loraConfig) { const RegionInfo *newRegion = getRegion(loraConfig.region); // If you are not licensed, you can't use ham regions. if (newRegion->profile->licensedOnly && !devicestate.owner.is_licensed) { char err_string[160]; snprintf(err_string, sizeof(err_string), "Region %s requires licensed mode", newRegion->name); LOG_ERROR("%s", err_string); RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); sendErrorNotification(err_string); return false; } return true; } /** * Internal helper: validate or clamp a LoRa config against its region. * When clamp==false, returns false on first error (pure validation). * When clamp==true, fixes invalid settings in-place and returns true. */ bool RadioInterface::checkOrClampConfigLora(meshtastic_Config_LoRaConfig &loraConfig, bool clamp) { char err_string[160]; float check_bw; const RegionInfo *newRegion = getRegion(loraConfig.region); const char *presetName = DisplayFormatters::getModemPresetDisplayName(loraConfig.modem_preset, false, loraConfig.use_preset); // Check preset validity (only when use_preset is true) if (loraConfig.use_preset) { check_bw = modemPresetToBwKHz(loraConfig.modem_preset, newRegion->wideLora); bool preset_valid = false; for (size_t i = 0; i < newRegion->getNumPresets(); i++) { if (loraConfig.modem_preset == newRegion->getAvailablePresets()[i]) { preset_valid = true; break; } } if (!preset_valid) { const char *defaultName = DisplayFormatters::getModemPresetDisplayName(newRegion->getDefaultPreset(), false, true); if (clamp) { snprintf(err_string, sizeof(err_string), "Preset %s invalid for %s, using %s", presetName, newRegion->name, defaultName); } else { snprintf(err_string, sizeof(err_string), "Preset %s invalid for %s", presetName, newRegion->name); } LOG_ERROR("%s", err_string); RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); sendErrorNotification(err_string); if (clamp) { loraConfig.modem_preset = newRegion->getDefaultPreset(); check_bw = modemPresetToBwKHz(loraConfig.modem_preset, newRegion->wideLora); } else { return false; } } } else { check_bw = bwCodeToKHz(loraConfig.bandwidth); } // Calculate width of slots (aka channels) based on bandwidth and any spacing or padding required by the region: // spacing = gap between slots (0 for continuous spectrum) and at the beginning of the band // padding = gap at the beginning and end of the slots (0 for no padding) float freqSlotWidth = newRegion->profile->spacing + (newRegion->profile->padding * 2) + (check_bw / 1000); // in MHz uint32_t numFreqSlots = round((newRegion->freqEnd - newRegion->freqStart + newRegion->profile->spacing) / freqSlotWidth); // Check if the region supports the requested bandwidth if ((newRegion->freqEnd - newRegion->freqStart) < freqSlotWidth) { const float regionSpanKHz = (newRegion->freqEnd - newRegion->freqStart) * 1000.0f; snprintf(err_string, sizeof(err_string), "%s span %.0fkHz < requested %.0fkHz", newRegion->name, regionSpanKHz, check_bw); LOG_ERROR("%s", err_string); RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); sendErrorNotification(err_string); if (clamp) { loraConfig.bandwidth = bwKHzToCode(modemPresetToBwKHz(newRegion->getDefaultPreset(), newRegion->wideLora)); check_bw = bwCodeToKHz(loraConfig.bandwidth); // Recompute slot width and number of slots based on the new bandwidth freqSlotWidth = newRegion->profile->spacing + (newRegion->profile->padding * 2) + (check_bw / 1000); // in MHz numFreqSlots = round((newRegion->freqEnd - newRegion->freqStart + newRegion->profile->spacing) / freqSlotWidth); } else { return false; } } const char *channelName = channels.getName(channels.getPrimaryIndex()); const char *presetNameDisplay = DisplayFormatters::getModemPresetDisplayName(loraConfig.modem_preset, false, loraConfig.use_preset); uint32_t channelNameHashSlot = hash(channelName) % numFreqSlots; uint32_t presetNameHashSlot = hash(presetNameDisplay) % numFreqSlots; if (loraConfig.override_frequency == 0) { // Check if we use the default frequency slot uses_default_frequency_slot = (loraConfig.channel_num == 0) || // user choice unset, no frequency override, so use default (newRegion->profile->overrideSlot != 0 && loraConfig.channel_num == newRegion->profile->overrideSlot) || // user setting matches override ((newRegion->profile->overrideSlot == 0) && ((uint32_t)(loraConfig.channel_num - 1) == presetNameHashSlot)); // user setting matches preset hash, no override // check if user setting different to preset name uses_custom_channel_name = (strcmp(channelName, presetNameDisplay) != 0); if (loraConfig.channel_num > numFreqSlots) { snprintf(err_string, sizeof(err_string), "Channel number %u invalid for %s, max is %u", loraConfig.channel_num, newRegion->name, numFreqSlots); LOG_ERROR("%s", err_string); RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); sendErrorNotification(err_string); if (clamp) { if (uses_custom_channel_name) { // clamp to channel name hash loraConfig.channel_num = channelNameHashSlot + 1; // channel_num is 1-based, but hash slot is 0-based, so add 1 } else if ((loraConfig.use_preset) && (newRegion->profile->overrideSlot != 0)) { // clamp to preset override slot loraConfig.channel_num = newRegion->profile->overrideSlot; // use the override slot specified by the region profile uses_default_frequency_slot = true; } else if (loraConfig.use_preset) { // clamp to preset slot loraConfig.channel_num = presetNameHashSlot + 1; // channel_num is 1-based, but hash slot is 0-based, so add 1 uses_default_frequency_slot = true; } else { // if not using preset, and no custom channel name, just clamp to default anyway uses_default_frequency_slot = true; }; } else { return false; } } // end of channel number check } else { // if we have a frequency override, we ignore the channel number and just use the override frequency snprintf(err_string, sizeof(err_string), "Frequency override in place, using %.3f", loraConfig.override_frequency); } return true; } bool RadioInterface::validateConfigLora(const meshtastic_Config_LoRaConfig &loraConfig) { auto copy = loraConfig; return checkOrClampConfigLora(copy, false); } void RadioInterface::clampConfigLora(meshtastic_Config_LoRaConfig &loraConfig) { checkOrClampConfigLora(loraConfig, true); } /** * Pull our channel settings etc... from protobufs to the dumb interface settings * Note: this must be given only settings which have been validated or clamped! */ void RadioInterface::applyModemConfig() { // Set up default configuration // No Sync Words in LORA mode meshtastic_Config_LoRaConfig &loraConfig = config.lora; const RegionInfo *newRegion = getRegion(loraConfig.region); myRegion = newRegion; if (loraConfig.use_preset) { if (!validateConfigLora(loraConfig)) { loraConfig.modem_preset = newRegion->getDefaultPreset(); } uint8_t newcr; modemPresetToParams(loraConfig.modem_preset, newRegion->wideLora, bw, sf, newcr); // If custom CR is being used already, check if the new preset is higher if (loraConfig.coding_rate >= 5 && loraConfig.coding_rate <= 8 && loraConfig.coding_rate < newcr) { cr = newcr; LOG_INFO("Default Coding Rate is higher than custom setting, using %u", cr); } // If the custom CR is higher than the preset, use it else if (loraConfig.coding_rate >= 5 && loraConfig.coding_rate <= 8 && loraConfig.coding_rate > newcr) { cr = loraConfig.coding_rate; LOG_INFO("Using custom Coding Rate %u", cr); } else { cr = loraConfig.coding_rate; } } else { // if not using preset, then just use the custom settings if (validateConfigLora(loraConfig)) { } else { LOG_WARN("Invalid LoRa config settings, cannot apply requested modem config - falling back to %s defaults", newRegion->name); clampConfigLora(loraConfig); } bw = bwCodeToKHz(loraConfig.bandwidth); sf = loraConfig.spread_factor; cr = loraConfig.coding_rate; } power = loraConfig.tx_power; if ((power == 0) || ((power > newRegion->powerLimit) && !devicestate.owner.is_licensed)) power = newRegion->powerLimit; if (power == 0) power = 17; // Default to this power level if we don't have a valid regional power limit (powerLimit of newRegion 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 uint32_t channel_num; float freq; // Calculate number of frequency slots (aka Channels): // spacing = gap between channels (0 for continuous spectrum) and at the beginning of the band // padding = gap at the beginning and end of the channel (0 for no padding) float freqSlotWidth = newRegion->profile->spacing + (newRegion->profile->padding * 2) + (bw / 1000); // in MHz uint32_t numFreqSlots = round((newRegion->freqEnd - newRegion->freqStart + newRegion->profile->spacing) / freqSlotWidth); // Calculate hash of channel name and preset name to pick a default frequency slot if user has not specified one. // Note that channel_num is actually (channel_num - 1), i.e. zero-based, since modulus (%) returns values from 0 to // (numFreqSlots - 1). uint32_t presetNameHashSlot = hash(DisplayFormatters::getModemPresetDisplayName(loraConfig.modem_preset, false, loraConfig.use_preset)) % numFreqSlots; // override if we have a verbatim frequency if (loraConfig.override_frequency) { freq = loraConfig.override_frequency; channel_num = -1; uses_default_frequency_slot = false; } else { // If user has not manually specified a frequency slot, or has not specified one that is different than the default or the // override for the new region, then use the default or override. If the user has not specified one, but has specified a // custom channel name, then use the hash of that channel name to pick a frequency slot. Note that channel_num is actually // (channel_num - 1), i.e. zero-based, since modulus (%) returns values from 0 to (numFreqSlots - 1). // NB: channel_num is also know as frequency slot but it's too late to fix now. if (uses_default_frequency_slot) { // if there's an override slot, use that if (newRegion->profile->overrideSlot != 0) { channel_num = newRegion->profile->overrideSlot - 1; } else { channel_num = presetNameHashSlot; } } else { // use the manually defined one channel_num = loraConfig.channel_num - 1; } // Calculate frequency: freqStart is band edge, add half bandwidth (plus optional padding) to get middle of first channel // subsequent channels are spaced by freqSlotWidth freq = newRegion->freqStart + (bw / 2000) + newRegion->profile->padding + (channel_num * freqSlotWidth); // in MHz } saveChannelNum(channel_num); saveFreq(freq + loraConfig.frequency_offset); const char *channelName = channels.getName(channels.getPrimaryIndex()); slotTimeMsec = computeSlotTimeMsec(); preambleTimeMsec = preambleLength * (pow_of_2(sf) / bw); LOG_INFO("Radio freq=%.3f, config.lora.frequency_offset=%.3f", freq, loraConfig.frequency_offset); LOG_INFO("Set radio: region=%s, name=%s, config=%u, ch=%d, power=%d", newRegion->name, channelName, loraConfig.modem_preset, channel_num, power); LOG_INFO("newRegion->freqStart -> newRegion->freqEnd: %f -> %f (%f MHz)", newRegion->freqStart, newRegion->freqEnd, newRegion->freqEnd - newRegion->freqStart); LOG_INFO("numFreqSlots: %d x %.3fkHz", numFreqSlots, bw); if (newRegion->profile->overrideSlot != 0) { LOG_INFO("Using region override slot: %d", newRegion->profile->overrideSlot); } LOG_INFO("channel_num: %d", channel_num + 1); LOG_INFO("frequency: %f", getFreq()); LOG_INFO("Slot time: %u msec, preamble time: %u msec", slotTimeMsec, preambleTimeMsec); } // end of applyModemConfig /** Slottime is the time to detect a transmission has started, consisting of: - CAD duration; - roundtrip air propagation time (assuming max. 30km between nodes); - Tx/Rx turnaround time (maximum of SX126x and SX127x); - MAC processing time (measured on T-beam) */ uint32_t RadioInterface::computeSlotTimeMsec() { float sumPropagationTurnaroundMACTime = 0.2 + 0.4 + 7; // in milliseconds float symbolTime = pow_of_2(sf) / bw; // in milliseconds if (myRegion->wideLora) { // CAD duration derived from AN1200.22 of SX1280 return (NUM_SYM_CAD_24GHZ + (2 * sf + 3) / 32) * symbolTime + sumPropagationTurnaroundMACTime; } else { // CAD duration for SX127x is max. 2.25 symbols, for SX126x it is number of symbols + 0.5 symbol return max(2.25, NUM_SYM_CAD + 0.5) * symbolTime + sumPropagationTurnaroundMACTime; } } /** * Some regulatory regions limit xmit power. * This function should be called by subclasses after setting their desired power. It might lower it */ void RadioInterface::limitPower(int8_t loraMaxPower) { uint8_t maxPower = 255; // No limit if (myRegion->powerLimit) maxPower = myRegion->powerLimit; if ((power > maxPower) && !devicestate.owner.is_licensed) { LOG_INFO("Lower transmit power because of regulatory limits"); power = maxPower; } #if HAS_LORA_FEM if (!devicestate.owner.is_licensed) { power = loraFEMInterface.powerConversion(power); } #else // todo:All entries containing "lora fem" are grouped together above. #ifdef ARCH_PORTDUINO size_t num_pa_points = portduino_config.num_pa_points; const uint16_t *tx_gain = portduino_config.tx_gain_lora; #else size_t num_pa_points = NUM_PA_POINTS; const uint16_t tx_gain[NUM_PA_POINTS] = {TX_GAIN_LORA}; #endif if (num_pa_points == 1) { if (tx_gain[0] > 0 && !devicestate.owner.is_licensed) { LOG_INFO("Requested Tx power: %d dBm; Device LoRa Tx gain: %d dB", power, tx_gain[0]); power -= tx_gain[0]; } } else if (!devicestate.owner.is_licensed) { // we have an array of PA gain values. Find the highest power setting that works. for (int radio_dbm = 0; radio_dbm < (int)num_pa_points; radio_dbm++) { if (((radio_dbm + tx_gain[radio_dbm]) > power) || ((radio_dbm == (int)(num_pa_points - 1)) && ((radio_dbm + tx_gain[radio_dbm]) <= power))) { // we've exceeded the power limit, or hit the max we can do LOG_INFO("Requested Tx power: %d dBm; Device LoRa Tx gain: %d dB", power, tx_gain[radio_dbm]); power -= tx_gain[radio_dbm]; break; } } } #endif if (power > loraMaxPower) // Clamp power to maximum defined level power = loraMaxPower; LOG_INFO("Final Tx power: %d dBm", power); } void RadioInterface::deliverToReceiver(meshtastic_MeshPacket *p) { if (router) { p->transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA; router->enqueueReceivedMessage(p); } } /*** * 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("Send queued packet on mesh (txGood=%d,rxGood=%d,rxBad=%d)", rf95.txGood(), rf95.rxGood(), rf95.rxBad()); assert(p->which_payload_variant == meshtastic_MeshPacket_encrypted_tag); // It should have already been encoded by now radioBuffer.header.from = p->from; radioBuffer.header.to = p->to; radioBuffer.header.id = p->id; radioBuffer.header.channel = p->channel; radioBuffer.header.next_hop = p->next_hop; radioBuffer.header.relay_node = p->relay_node; if (p->hop_limit > HOP_MAX) { LOG_WARN("hop limit %d is too high, setting to %d", p->hop_limit, HOP_RELIABLE); p->hop_limit = HOP_RELIABLE; } radioBuffer.header.flags = p->hop_limit | (p->want_ack ? PACKET_FLAGS_WANT_ACK_MASK : 0) | (p->via_mqtt ? PACKET_FLAGS_VIA_MQTT_MASK : 0); radioBuffer.header.flags |= (p->hop_start << PACKET_FLAGS_HOP_START_SHIFT) & PACKET_FLAGS_HOP_START_MASK; // if the sender nodenum is zero, that means uninitialized assert(radioBuffer.header.from); assert(p->encrypted.size <= sizeof(radioBuffer.payload)); memcpy(radioBuffer.payload, p->encrypted.bytes, p->encrypted.size); sendingPacket = p; return p->encrypted.size + sizeof(PacketHeader); } ================================================ FILE: src/mesh/RadioInterface.h ================================================ #pragma once #include "MemoryPool.h" #include "MeshTypes.h" #include "Observer.h" #include "PointerQueue.h" #include "airtime.h" #include "error.h" #include #if HAS_LORA_FEM #include "LoRaFEMInterface.h" #endif // Forward decl to avoid a direct include of generated config headers / full LoRaConfig definition in this widely-included file. typedef struct _meshtastic_Config_LoRaConfig meshtastic_Config_LoRaConfig; #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; // Last byte of the NodeNum of the next-hop for this packet uint8_t next_hop; // Last byte of the NodeNum of the node that will relay/relayed this packet uint8_t relay_node; } 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; const uint8_t NUM_SYM_CAD = 2; // Number of symbols used for CAD, 2 is the default since RadioLib 6.3.0 as per AN1200.48 const uint8_t NUM_SYM_CAD_24GHZ = 4; // Number of symbols used for CAD in 2.4 GHz, 4 is recommended in AN1200.22 of SX1280 uint32_t slotTimeMsec = computeSlotTimeMsec(); uint16_t preambleLength = 16; // 8 is default, but we use longer to increase the amount of sleep time when receiving uint32_t preambleTimeMsec = 165; // calculated on startup, this is the default for LongFast const uint32_t PROCESSING_TIME_MSEC = 4500; // time to construct, process and construct a packet again (empirically determined) const uint8_t CWmin = 3; // minimum CWsize const uint8_t CWmax = 8; // maximum CWsize meshtastic_MeshPacket *sendingPacket = NULL; // The packet we are currently sending uint32_t lastTxStart = 0L; uint32_t computeSlotTimeMsec(); /** * A temporary buffer used for sending/receiving packets, sized to hold the biggest buffer we might need * */ RadioBuffer radioBuffer __attribute__((__aligned__)); /** * Enqueue a received packet for the registered receiver */ void deliverToReceiver(meshtastic_MeshPacket *p); public: /** pool is the pool we will alloc our rx packets from */ RadioInterface(); virtual ~RadioInterface() {} /** * Coerce LoRa config fields (bandwidth/spread_factor) derived from presets. * This is used during early bootstrapping so UIs that display these fields directly remain consistent. */ // static void bootstrapLoRaConfigFromPreset(meshtastic_Config_LoRaConfig &loraConfig); // maybe superseded? /** * 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 */ [[nodiscard]] virtual meshtastic_QueueStatus getQueueStatus() { meshtastic_QueueStatus qs; qs.res = qs.mesh_packet_id = qs.free = qs.maxlen = 0; return qs; } /** Attempt to cancel a previously sent packet. Returns true if a packet was found we could cancel */ virtual bool cancelSending(NodeNum from, PacketId id) { return false; } /** Attempt to find a packet in the TxQueue. Returns true if the packet was found. */ virtual bool findInTxQueue(NodeNum from, PacketId id) { return false; } // methods from radiohead /// 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 */ [[nodiscard]] uint32_t getRetransmissionMsec(const meshtastic_MeshPacket *p); /** The delay to use when we want to send something */ [[nodiscard]] uint32_t getTxDelayMsec(); /** The CW to use when calculating SNR_based delays */ [[nodiscard]] uint8_t getCWsize(float snr); /** The worst-case SNR_based packet delay */ [[nodiscard]] uint32_t getTxDelayMsecWeightedWorst(float snr); /** Returns true if we should rebroadcast early like a ROUTER */ [[nodiscard]] bool shouldRebroadcastEarlyLikeRouter(meshtastic_MeshPacket *p); /** The delay to use when we want to flood a message. Use a weighted scale based on SNR */ [[nodiscard]] uint32_t getTxDelayMsecWeighted(meshtastic_MeshPacket *p); /** If the packet is not already in the late rebroadcast window, move it there */ virtual void clampToLateRebroadcastWindow(NodeNum from, PacketId id) { return; } /** * If there is a packet pending TX in the queue with a worse hop limit, remove it pending replacement with a better version * @return Whether a pending packet was removed */ virtual bool removePendingTXPacket(NodeNum from, PacketId id, uint32_t hop_limit_lt) { return false; } /** * Calculate airtime per * https://www.rs-online.com/designspark/rel-assets/ds-assets/uploads/knowledge-items/application-notes-for-the-internet-of-things/LoRa%20Design%20Guide.pdf * section 4 * * @return num msecs for the packet */ [[nodiscard]] uint32_t getPacketTime(const meshtastic_MeshPacket *p, bool received = false); [[nodiscard]] virtual uint32_t getPacketTime(uint32_t totalPacketLen, bool received = false) = 0; /** * Get the channel we saved. */ [[nodiscard]] uint32_t getChannelNum(); /** * Get the frequency we saved. */ [[nodiscard]] 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; // Whether we have a custom channel name static bool uses_custom_channel_name; static bool checkOrClampConfigLora(meshtastic_Config_LoRaConfig &loraConfig, bool clamp); // Check if a candidate region is compatible and valid. static bool validateConfigRegion(const meshtastic_Config_LoRaConfig &loraConfig); // Check if a candidate radio configuration is valid. static bool validateConfigLora(const meshtastic_Config_LoRaConfig &loraConfig); // Make a candidate radio configuration valid, even if it isn't. static void clampConfigLora(meshtastic_Config_LoRaConfig &loraConfig); 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 */ [[nodiscard]] size_t beginSending(meshtastic_MeshPacket *p); /** * Some regulatory regions limit xmit power. * This function should be called by subclasses after setting their desired power. It might lower it */ void limitPower(int8_t MAX_POWER); /** * 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; } }; std::unique_ptr initLoRa(); /// Debug printing for packets void printPacket(const char *prefix, const meshtastic_MeshPacket *p); ================================================ FILE: src/mesh/RadioLibInterface.cpp ================================================ #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 #if ARCH_PORTDUINO #include "PortduinoGlue.h" #include "meshUtils.h" #endif 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) { spi->transfer(out, in, len); } #endif RadioLibInterface::RadioLibInterface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy, PhysicalLayer *_iface) : NotifiedWorkerThread("RadioIf"), module(hal, cs, irq, rst, busy), iface(_iface) { instance = this; #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)) { if (!(irq & syncWordHeaderValidFlag)) { // The HEADER_VALID flag should be set by now if it was really a packet, so ignore PREAMBLE_DETECTED flag activeReceiveStart = 0; LOG_DEBUG("Ignore false preamble detection"); return false; } else { uint32_t maxPacketTimeMsec = getPacketTime(meshtastic_Constants_DATA_PAYLOAD_LEN + sizeof(PacketHeader)); if (!Throttle::isWithinTimespanMs(activeReceiveStart, maxPacketTimeMsec)) { // We should have gotten an RX_DONE IRQ by now if it was really a packet, so ignore HEADER_VALID flag activeReceiveStart = 0; LOG_DEBUG("Ignore false header detection"); return false; } } } } return detected; } /// 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: Region unset"); packetPool.release(p); return ERRNO_DISABLED; } #else if (disabled || !config.lora.tx_enabled) { LOG_WARN("send - !config.lora.tx_enabled"); packetPool.release(p); return ERRNO_DISABLED; } #endif if (p->to == NODENUM_BROADCAST_NO_LORA) { LOG_DEBUG("Drop no-LoRa pkt"); return ERRNO_SHOULD_RELEASE; } // Sometimes when testing it is useful to be able to never turn on the xmitter #ifndef LORA_DISABLE_SENDING printPacket("enqueue for send", p); LOG_DEBUG("txGood=%d,txRelay=%d,rxGood=%d,rxBad=%d", txGood, txRelay, rxGood, rxBad); bool dropped = false; ErrorCode res = txQueue.enqueue(p, &dropped) ? ERRNO_OK : ERRNO_UNKNOWN; if (dropped) { txDrop++; } if (res != ERRNO_OK) { // we weren't able to queue it, so we must drop it to prevent leaks packetPool.release(p); return res; } // set (random) transmit delay to let others reconfigure their radio, // to avoid collisions and implement timing-based flooding setTransmitDelay(); return res; #else packetPool.release(p); return ERRNO_DISABLED; #endif } meshtastic_QueueStatus RadioLibInterface::getQueueStatus() { meshtastic_QueueStatus qs; 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; } /** Allow other firmware components to ask whether we are currently sending a packet Initially implemented to protect T-Echo's capacitive touch button from spurious presses during tx */ bool RadioLibInterface::isSending() { return sendingPacket != NULL; } /** 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; } /** Attempt to find a packet in the TxQueue. Returns true if the packet was found. */ bool RadioLibInterface::findInTxQueue(NodeNum from, PacketId id) { return txQueue.find(from, id); } /** 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(); setTransmitDelay(); break; case ISR_RX: handleReceiveInterrupt(); startReceive(); setTransmitDelay(); break; case TRANSMIT_DELAY_COMPLETED: // If we are not currently in receive mode, then restart the random delay (this can happen if the main thread // has placed the unit into standby) FIXME, how will this work if the chipset is in sleep mode? if (!txQueue.empty()) { if (!canSendImmediately()) { setTransmitDelay(); // currently Rx/Tx-ing: reset random delay } else { meshtastic_MeshPacket *txp = txQueue.getFront(); assert(txp); long delay_remaining = txp->tx_after ? txp->tx_after - millis() : 0; if (delay_remaining > 0) { // There's still some delay pending on this packet, so resume waiting for it to elapse notifyLater(delay_remaining, TRANSMIT_DELAY_COMPLETED, false); } else { if (isChannelActive()) { // check if there is currently a LoRa packet on the channel startReceive(); // try receiving this packet, afterwards we'll be trying to transmit again setTransmitDelay(); } else { // Send any outgoing packets we have ready as fast as possible to keep the time between channel scan and // actual transmission as short as possible txp = txQueue.dequeue(); assert(txp); startSend(txp); LOG_DEBUG("%d packets remain in the TX queue", txQueue.getMaxLen() - txQueue.getFree()); } } } } else { // Do nothing, because the queue is empty } break; default: assert(0); // We expected to receive a valid notification from the ISR } } void RadioLibInterface::setTransmitDelay() { meshtastic_MeshPacket *p = txQueue.getFront(); if (!p) { return; // noop if there's nothing in the queue } // We want all sending/receiving to be done by our daemon thread. // We use a delay here because this packet might have been sent in response to a packet we just received. // So we want to make sure the other side has had a chance to reconfigure its radio. if (p->tx_after) { unsigned long add_delay = p->rx_rssi ? getTxDelayMsecWeighted(p) : getTxDelayMsec(); unsigned long now = millis(); p->tx_after = min(max(p->tx_after + add_delay, now + add_delay), now + 2 * getTxDelayMsecWeightedWorst(p->rx_snr)); notifyLater(p->tx_after - now, TRANSMIT_DELAY_COMPLETED, false); } else if (p->rx_snr == 0 && p->rx_rssi == 0) { /* We assume if rx_snr = 0 and rx_rssi = 0, the packet was generated locally. * This assumption is valid because of the offset generated by the radio to account for the noise * floor. */ startTransmitTimer(true); } else { // If there is a SNR, start a timer scaled based on that SNR. LOG_DEBUG("rx_snr found. hop_limit:%d rx_snr:%f", p->hop_limit, p->rx_snr); startTransmitTimerRebroadcast(p); } } void RadioLibInterface::startTransmitTimer(bool withDelay) { // If we have work to do and the timer wasn't already scheduled, schedule it now if (!txQueue.empty()) { uint32_t delay = !withDelay ? 1 : getTxDelayMsec(); notifyLater(delay, TRANSMIT_DELAY_COMPLETED, false); // This will implicitly enable } } void RadioLibInterface::startTransmitTimerRebroadcast(meshtastic_MeshPacket *p) { // If we have work to do and the timer wasn't already scheduled, schedule it now if (!txQueue.empty()) { uint32_t delay = getTxDelayMsecWeighted(p); notifyLater(delay, TRANSMIT_DELAY_COMPLETED, false); // This will implicitly enable } } /** * If the packet is not already in the late rebroadcast window, move it there */ void RadioLibInterface::clampToLateRebroadcastWindow(NodeNum from, PacketId id) { // Look for non-late packets only, so we don't do this twice! meshtastic_MeshPacket *p = txQueue.remove(from, id, true, false); if (p) { p->tx_after = millis() + getTxDelayMsecWeightedWorst(p->rx_snr); bool dropped = false; if (txQueue.enqueue(p, &dropped)) { LOG_DEBUG("Move existing queued packet to the late rebroadcast window %dms from now", p->tx_after - millis()); } else { packetPool.release(p); } if (dropped) { txDrop++; } } } /** * If there is a packet pending TX in the queue with a worse hop limit, remove it pending replacement with a better version * @return Whether a pending packet was removed */ bool RadioLibInterface::removePendingTXPacket(NodeNum from, PacketId id, uint32_t hop_limit_lt) { meshtastic_MeshPacket *p = txQueue.remove(from, id, true, true, hop_limit_lt); if (p) { LOG_DEBUG("Dropping pending-TX packet 0x%08x with hop limit %d", p->id, p->hop_limit); packetPool.release(p); return true; } return false; } /** * Remove a packet that is eligible for replacement from the TX queue */ // void RadioLibInterface::removePending void RadioLibInterface::handleTransmitInterrupt() { // This can be null if we forced the device to enter standby mode. In that case // ignore the transmit interrupt if (sendingPacket) completeSending(); powerMon->clearState(meshtastic_PowerMon_State_Lora_TXOn); // But our transmitter is definitely off now } void RadioLibInterface::completeSending() { // We are careful to clear sending packet before calling printPacket because // that can take a long time auto p = sendingPacket; sendingPacket = NULL; if (p) { // Packet has been sent, count it toward our TX airtime utilization. uint32_t xmitMsec = getPacketTime(p); airTime->logAirtime(TX_LOG, xmitMsec); txGood++; if (!isFromUs(p)) txRelay++; printPacket("Completed sending", p); // We are done sending that packet, release it packetPool.release(p); } } void RadioLibInterface::handleReceiveInterrupt() { // when this is called, we should be in receive mode - if we are not, just jump out instead of bombing. Possible Race // Condition? if (!isReceiving) { LOG_ERROR("handleReceiveInterrupt called when not in rx mode, which shouldn't happen"); return; } isReceiving = false; // read the number of actually received bytes size_t length = iface->getPacketLength(); uint32_t rxMsec = getPacketTime(length, true); #ifndef DISABLE_WELCOME_UNSET if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET) { LOG_WARN("lora rx disabled: Region unset"); airTime->logAirtime(RX_ALL_LOG, rxMsec); return; } #endif int state = iface->readData((uint8_t *)&radioBuffer, length); #if ARCH_PORTDUINO if (portduino_config.logoutputlevel == level_trace) { printBytes("Raw incoming packet: ", (uint8_t *)&radioBuffer, length); } #endif if (state != RADIOLIB_ERR_NONE) { // Log PacketHeader similar to RadioInterface::printPacket so we can try to match RX errors to other packets in the logs. LOG_ERROR("Ignore received packet due to error=%d (maybe id=0x%08x fr=0x%08x to=0x%08x flags=0x%02x rxSNR=%g rxRSSI=%i " "nextHop=0x%x relay=0x%x)", state, radioBuffer.header.id, radioBuffer.header.from, radioBuffer.header.to, radioBuffer.header.flags, iface->getSNR(), lround(iface->getRSSI()), radioBuffer.header.next_hop, radioBuffer.header.relay_node); rxBad++; airTime->logAirtime(RX_ALL_LOG, rxMsec); } else { // Skip the 4 headers that are at the beginning of the rxBuf int32_t payloadLen = length - sizeof(PacketHeader); // check for short packets if (payloadLen < 0) { LOG_WARN("Ignore received packet too short"); rxBad++; airTime->logAirtime(RX_ALL_LOG, rxMsec); } else { rxGood++; // altered packet with "from == 0" can do Remote Node Administration without permission if (radioBuffer.header.from == 0) { LOG_WARN("Ignore received packet without sender"); return; } // Note: we deliver _all_ packets to our router (i.e. our interface is intentionally promiscuous). // This allows the router and other apps on our node to sniff packets (usually routing) between other // nodes. meshtastic_MeshPacket *mp = packetPool.allocZeroed(); // Keep the assigned fields in sync with src/mqtt/MQTT.cpp:onReceiveProto mp->from = radioBuffer.header.from; mp->to = radioBuffer.header.to; mp->id = radioBuffer.header.id; mp->channel = radioBuffer.header.channel; assert(HOP_MAX <= PACKET_FLAGS_HOP_LIMIT_MASK); // If hopmax changes, carefully check this code mp->hop_limit = radioBuffer.header.flags & PACKET_FLAGS_HOP_LIMIT_MASK; mp->hop_start = (radioBuffer.header.flags & PACKET_FLAGS_HOP_START_MASK) >> PACKET_FLAGS_HOP_START_SHIFT; mp->want_ack = !!(radioBuffer.header.flags & PACKET_FLAGS_WANT_ACK_MASK); mp->via_mqtt = !!(radioBuffer.header.flags & PACKET_FLAGS_VIA_MQTT_MASK); // If hop_start is not set, next_hop and relay_node are invalid (firmware <2.3) mp->next_hop = mp->hop_start == 0 ? NO_NEXT_HOP_PREFERENCE : radioBuffer.header.next_hop; mp->relay_node = mp->hop_start == 0 ? NO_RELAY_NODE : radioBuffer.header.relay_node; addReceiveMetadata(mp); mp->which_payload_variant = meshtastic_MeshPacket_encrypted_tag; // Mark that the payload is still encrypted at this point assert(((uint32_t)payloadLen) <= sizeof(mp->encrypted.bytes)); memcpy(mp->encrypted.bytes, radioBuffer.payload, payloadLen); mp->encrypted.size = payloadLen; printPacket("Lora RX", mp); airTime->logAirtime(RX_LOG, rxMsec); deliverToReceiver(mp); } } } void RadioLibInterface::startReceive() { isReceiving = true; powerMon->setState(meshtastic_PowerMon_State_Lora_RXOn); } void RadioLibInterface::pollMissedIrqs() { // RadioLibInterface::enableInterrupt uses EDGE-TRIGGERED interrupts. Poll as a backup to catch missed edges. if (isReceiving) { checkRxDoneIrqFlag(); } } void RadioLibInterface::resetAGC() { // Base implementation: no-op. Override in chip-specific subclasses. } void RadioLibInterface::checkRxDoneIrqFlag() { if (iface->checkIrq(RADIOLIB_IRQ_RX_DONE)) { LOG_WARN("caught missed RX_DONE"); notify(ISR_RX, true); } } 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 */ bool RadioLibInterface::startSend(meshtastic_MeshPacket *txp) { /* NOTE: Minimize the actions before startTransmit() to keep the time between channel scan and actual transmit as low as possible to avoid collisions. */ if (disabled || !config.lora.tx_enabled) { LOG_WARN("Drop Tx packet because LoRa Tx disabled"); packetPool.release(txp); return false; } else { configHardwareForSend(); // must be after setStandby size_t numbytes = beginSending(txp); int res = iface->startTransmit((uint8_t *)&radioBuffer, numbytes); if (res != RADIOLIB_ERR_NONE) { LOG_ERROR("startTransmit failed, error=%d", res); RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_RADIO_SPI_BUG); // This send failed, but make sure to 'complete' it properly completeSending(); powerMon->clearState(meshtastic_PowerMon_State_Lora_TXOn); // Transmitter off now startReceive(); // Restart receive mode (because startTransmit failed to put us in xmit mode) } else { // Must be done AFTER, starting transmit, because startTransmit clears (possibly stale) interrupt pending register // bits enableInterrupt(isrTxLevel0); lastTxStart = millis(); printPacket("Started Tx", txp); } return res == RADIOLIB_ERR_NONE; } } ================================================ FILE: src/mesh/RadioLibInterface.h ================================================ #pragma once #include "MeshPacketQueue.h" #include "RadioInterface.h" #include "concurrency/NotifiedWorkerThread.h" #include #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 // In addition to the default Rx flags, we need the PREAMBLE_DETECTED flag to detect whether we are actively receiving #define MESHTASTIC_RADIOLIB_IRQ_RX_FLAGS (RADIOLIB_IRQ_RX_DEFAULT_FLAGS | (1 << RADIOLIB_IRQ_PREAMBLE_DETECTED)) #define AGC_RESET_INTERVAL_MS (60 * 1000) // 60 seconds /** * We need to override the RadioLib ArduinoHal class to add mutex protection for SPI bus access */ class LockingArduinoHal : public ArduinoHal { public: LockingArduinoHal(SPIClass &spi, SPISettings spiSettings) : ArduinoHal(spi, spiSettings){}; void spiBeginTransaction() override; void spiEndTransaction() override; #if ARCH_PORTDUINO 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: ModemType_t modemType = RADIOLIB_MODEM_LORA; DataRate_t getDataRate() const { return {.lora = {.spreadingFactor = sf, .bandwidth = bw, .codingRate = cr}}; } PacketConfig_t getPacketConfig() const { return {.lora = {.preambleLength = preambleLength, .implicitHeader = false, .crcEnabled = true, // We use auto LDRO, meaning it is enabled if the symbol time is >= 16msec .ldrOptimize = (1 << sf) / bw >= 16}}; } /** * We use a meshtastic sync word, but hashed with the Channel name. For releases before 1.2 we used 0x12 (or for very old * loads 0x14) Note: do not use 0x34 - that is reserved for lorawan * * We now use 0x2b (so that someday we can possibly use NOT 2b - because that would be funny pun). We will be staying with * this code for a long time. */ const uint8_t syncWord = 0x2b; float currentLimit = 100; // 100mA OCP - Should be acceptable for RFM95/SX127x chipset. #if !defined(USE_STM32WLx) Module module; // The HW interface to the radio #else STM32WLx_ModuleWrapper module; #endif /** * provides lowest common denominator RadioLib API */ PhysicalLayer *iface; /// are _trying_ to receive a packet currently (note - we might just be waiting for one) bool isReceiving = false; public: /** Our ISR code currently needs this to find our active instance */ static RadioLibInterface *instance; /** * Glue functions called from ISR land */ virtual void disableInterrupt() = 0; /** * Enable a particular ISR callback glue function */ virtual void enableInterrupt(void (*)()) = 0; /** * Poll as a backup to catch missed edge-triggered interrupts. */ void pollMissedIrqs(); /** * Reset AGC by power-cycling the analog frontend. * Subclasses override with chip-specific calibration sequences. * Safe to call periodically — skips if currently sending or receiving. */ virtual void resetAGC(); /** * Debugging counts */ uint32_t rxBad = 0, rxGood = 0, txGood = 0, txRelay = 0; uint16_t txDrop = 0; public: RadioLibInterface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy, PhysicalLayer *iface = NULL); virtual ErrorCode send(meshtastic_MeshPacket *p) override; /** * Return true if we think the board can go to sleep (i.e. our tx queue is empty, we are not sending or receiving) * * This method must be used before putting the CPU into deep or light sleep. */ virtual bool canSleep() override; /** * Start waiting to receive a message * * External functions can call this method to wake the device from sleep. * Subclasses must override and call this base method */ virtual void startReceive(); /** can we detect a LoRa preamble on the current channel? */ virtual bool isChannelActive() = 0; /** are we actively receiving a packet (only called during receiving state) * This method is only public to facilitate debugging. Do not call. */ virtual bool isActivelyReceiving() = 0; /** Are we are currently sending a packet? * This method is public, intending to expose this information to other firmware components */ virtual bool isSending(); /** Attempt to cancel a previously sent packet. Returns true if a packet was found we could cancel */ virtual bool cancelSending(NodeNum from, PacketId id) override; /** Attempt to find a packet in the TxQueue. Returns true if the packet was found. */ virtual bool findInTxQueue(NodeNum from, PacketId id) override; private: /** if we have something waiting to send, start a short (random) timer so we can come check for collision before actually * doing the transmit */ void setTransmitDelay(); /** * random timer with certain min. and max. settings * @return Timestamp after which the packet may be sent */ void startTransmitTimer(bool withDelay = true); /** * timer scaled to SNR of to be flooded packet * @return Timestamp after which the packet may be sent */ void startTransmitTimerRebroadcast(meshtastic_MeshPacket *p); void handleTransmitInterrupt(); void handleReceiveInterrupt(); static void timerCallback(void *p1, uint32_t p2); virtual void onNotify(uint32_t notification) override; /** start an immediate transmit * This method is virtual so subclasses can hook as needed, subclasses should not call directly * @return true if packet was sent */ virtual bool startSend(meshtastic_MeshPacket *txp); meshtastic_QueueStatus getQueueStatus(); protected: uint32_t activeReceiveStart = 0; bool receiveDetected(uint16_t irq, ulong syncWordHeaderValidFlag, ulong preambleDetectedFlag); /** Do any hardware setup needed on entry into send configuration for the radio. * Subclasses can customize, but must also call this base method */ virtual void configHardwareForSend(); /** Could we send right now (i.e. either not actively receiving or transmitting)? */ virtual bool canSendImmediately(); /** * Raw ISR handler that just calls our polymorphic method */ static void isrRxLevel0(); /** * If a send was in progress finish it and return the buffer to the pool */ void completeSending(); /** * Add SNR data to received messages */ virtual void addReceiveMetadata(meshtastic_MeshPacket *mp) = 0; /** * Subclasses must override, implement and then call into this base class implementation */ virtual void setStandby(); /** * Derive packet time either for a received (using header info) or a transmitted packet */ template uint32_t computePacketTime(T &lora, uint32_t pl, bool received) { if (received) { // First get the actual coding rate and CRC status from the received packet uint8_t rxCR; bool hasCRC; lora.getLoRaRxHeaderInfo(&rxCR, &hasCRC); // Go from raw header value to denominator if (rxCR < 5) { rxCR += 4; } else if (rxCR == 7) { rxCR = 8; } // Received packet configuration must be the same as configured, except for coding rate and CRC DataRate_t dr = getDataRate(); dr.lora.codingRate = rxCR; PacketConfig_t pc = getPacketConfig(); pc.lora.crcEnabled = hasCRC; return lora.calculateTimeOnAir(modemType, dr, pc, pl) / 1000; } return lora.getTimeOnAir(pl) / 1000; } const char *radioLibErr = "RadioLib err="; /** * If the packet is not already in the late rebroadcast window, move it there */ void clampToLateRebroadcastWindow(NodeNum from, PacketId id); /** * If there is a packet pending TX in the queue with a worse hop limit, remove it pending replacement with a better version * @return Whether a pending packet was removed */ bool removePendingTXPacket(NodeNum from, PacketId id, uint32_t hop_limit_lt) override; void checkRxDoneIrqFlag(); }; ================================================ FILE: src/mesh/RadioLibRF95.cpp ================================================ #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 ================================================ FILE: src/mesh/RadioLibRF95.h ================================================ #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 ================================================ FILE: src/mesh/ReliableRouter.cpp ================================================ #include "ReliableRouter.h" #include "Default.h" #include "MeshTypes.h" #include "NodeDB.h" #include "configuration.h" #include "memGet.h" #include "mesh-pb-constants.h" #include "modules/NodeInfoModule.h" #include "modules/RoutingModule.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) { DEBUG_HEAP_BEFORE; auto copy = packetPool.allocCopy(*p); DEBUG_HEAP_AFTER("ReliableRouter::send", copy); startRetransmission(copy, NUM_RELIABLE_RETX); } /* If we have pending retransmissions, add the airtime of this packet to it, because during that time we cannot receive an (implicit) ACK. Otherwise, we might retransmit too early. */ for (auto i = pending.begin(); i != pending.end(); i++) { if (i->first.id != p->id) { i->second.nextTxMsec += iface->getPacketTime(p); } } return isBroadcast(p->to) ? FloodingRouter::send(p) : NextHopRouter::send(p); } bool ReliableRouter::shouldFilterReceived(const meshtastic_MeshPacket *p) { // Note: do not use getFrom() here, because we want to ignore messages sent from phone if (p->from == getNodeNum()) { printPacket("Rx someone rebroadcasting for us", p); // We are seeing someone rebroadcast one of our broadcast attempts. // If this is the first time we saw this, cancel any retransmissions we have queued up and generate an internal ack for // the original sending process. // This "optimization", does save lots of airtime. For DMs, you also get a real ACK back // from the intended recipient. auto key = GlobalPacketId(getFrom(p), p->id); auto old = findPendingPacket(key); if (old) { LOG_DEBUG("Generate implicit ack"); // NOTE: we do NOT check p->wantAck here because p is the INCOMING rebroadcast and that packet is not expected to be // marked as wantAck sendAckNak(meshtastic_Routing_Error_NONE, getFrom(p), p->id, old->packet->channel); // Only stop retransmissions if the rebroadcast came via LoRa if (p->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA) { stopRetransmission(key); } } else { LOG_DEBUG("Didn't find pending packet"); } } /* At this point we have already deleted the pending retransmission if this packet was an (implicit) ACK to it. Now for all other pending retransmissions, we have to add the airtime of this received packet to the retransmission timer, because while receiving this packet, we could not have received an (implicit) ACK for it. If we don't add this, we will likely retransmit too early. */ for (auto i = pending.begin(); i != pending.end(); i++) { i->second.nextTxMsec += iface->getPacketTime(p, true); } return isBroadcast(p->to) ? FloodingRouter::shouldFilterReceived(p) : NextHopRouter::shouldFilterReceived(p); } /** * If we receive a want_ack packet (do not check for wasSeenRecently), send back an ack (this might generate multiple ack sends in * case the our first ack gets lost) * * If we receive 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 (!MeshModule::currentReply) { if (p->want_ack) { if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) { /* A response may be set to want_ack for retransmissions, but we don't need to ACK a response if it received an implicit ACK already. If we received it directly or via NextHopRouter, only ACK with a hop limit of 0 to make sure the other side stops retransmitting. */ if (shouldSuccessAckWithWantAck(p)) { // If this packet should always be ACKed reliably with want_ack back to the original sender, make sure we // do that unconditionally. sendAckNak(meshtastic_Routing_Error_NONE, getFrom(p), p->id, p->channel, routingModule->getHopLimitForResponse(*p), true); } else if (!p->decoded.request_id && !p->decoded.reply_id) { // If it's not an ACK or a reply, send an ACK. sendAckNak(meshtastic_Routing_Error_NONE, getFrom(p), p->id, p->channel, routingModule->getHopLimitForResponse(*p)); } else if ((getHopsAway(*p) == 0) || p->next_hop != NO_NEXT_HOP_PREFERENCE) { // If we received the packet directly from the original sender, send a 0-hop ACK since the original sender // won't overhear any implicit ACKs. If we received the packet via NextHopRouter, also send a 0-hop ACK to // stop the immediate relayer's retransmissions. sendAckNak(meshtastic_Routing_Error_NONE, getFrom(p), p->id, p->channel, 0); } } else if (p->which_payload_variant == meshtastic_MeshPacket_encrypted_tag && p->channel == 0 && (nodeDB->getMeshNode(p->from) == nullptr || nodeDB->getMeshNode(p->from)->user.public_key.size == 0)) { LOG_INFO("PKI packet from unknown node, send PKI_UNKNOWN_PUBKEY"); sendAckNak(meshtastic_Routing_Error_PKI_UNKNOWN_PUBKEY, getFrom(p), p->id, channels.getPrimaryIndex(), routingModule->getHopLimitForResponse(*p)); } else { // Send a 'NO_CHANNEL' error on the primary channel if want_ack packet destined for us cannot be decoded sendAckNak(meshtastic_Routing_Error_NO_CHANNEL, getFrom(p), p->id, channels.getPrimaryIndex(), routingModule->getHopLimitForResponse(*p)); } } else if (p->next_hop == nodeDB->getLastByteOfNodeNum(getNodeNum()) && p->hop_limit > 0) { // No wantAck, but we need to ACK with hop limit of 0 if we were the next hop to stop their retransmissions sendAckNak(meshtastic_Routing_Error_NONE, getFrom(p), p->id, p->channel, 0); } } else { LOG_DEBUG("Another module replied to this message, no need for 2nd ack"); } if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag && c && c->error_reason == meshtastic_Routing_Error_PKI_UNKNOWN_PUBKEY) { if (owner.public_key.size == 32) { LOG_INFO("PKI decrypt failure, send a NodeInfo"); nodeInfoModule->sendOurNodeInfo(p->from, false, p->channel, true); } } // We consider an ack to be either a !routing packet with a request ID or a routing packet with !error PacketId ackId = ((c && c->error_reason == meshtastic_Routing_Error_NONE) || !c) ? p->decoded.request_id : 0; // A nak is a routing packt that has an error code PacketId nakId = (c && c->error_reason != meshtastic_Routing_Error_NONE) ? p->decoded.request_id : 0; // We intentionally don't check wasSeenRecently, because it is harmless to delete non existent retransmission records if ((ackId || nakId) && // Implicit ACKs from MQTT should not stop retransmissions !(isFromUs(p) && p->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MQTT)) { LOG_DEBUG("Received a %s for 0x%x, stopping retransmissions", ackId ? "ACK" : "NAK", ackId); if (ackId) { stopRetransmission(p->to, ackId); } else { stopRetransmission(p->to, nakId); } } } // handle the packet as normal isBroadcast(p->to) ? FloodingRouter::sniffReceived(p, c) : NextHopRouter::sniffReceived(p, c); } /** * If we ACK this packet, should we set want_ack=true on the ACK for reliable delivery of the ACK packet? */ bool ReliableRouter::shouldSuccessAckWithWantAck(const meshtastic_MeshPacket *p) { // Don't ACK-with-want-ACK outgoing packets if (isFromUs(p)) return false; // Only ACK-with-want-ACK if the original packet asked for want_ack if (!p->want_ack) return false; // Only ACK-with-want-ACK packets to us (not broadcast) if (!isToUs(p)) return false; // Special case for text message DMs: bool isTextMessage = (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) && IS_ONE_OF(p->decoded.portnum, meshtastic_PortNum_TEXT_MESSAGE_APP, meshtastic_PortNum_TEXT_MESSAGE_COMPRESSED_APP); if (isTextMessage) { // If it's a non-broadcast text message, and the original asked for want_ack, // let's send an ACK that is itself want_ack to improve reliability of confirming delivery back to the sender. // This should include all DMs regardless of whether or not reply_id is set. return true; } return false; } ================================================ FILE: src/mesh/ReliableRouter.h ================================================ #pragma once #include "NextHopRouter.h" /** * This is a mixin that extends Router with the ability to do (one hop only) reliable message sends. */ class ReliableRouter : public NextHopRouter { public: /** * Constructor * */ // ReliableRouter(); /** * Send a packet on a suitable interface. This routine will * later free() the packet to pool. This routine is not allowed to stall. * If the txmit queue is full it might return an error */ virtual ErrorCode send(meshtastic_MeshPacket *p) override; protected: /** * Look for acks/naks or someone retransmitting us */ virtual void sniffReceived(const meshtastic_MeshPacket *p, const meshtastic_Routing *c) override; /** * We hook this method so we can see packets before FloodingRouter says they should be discarded */ virtual bool shouldFilterReceived(const meshtastic_MeshPacket *p) override; private: /** * Should this packet be ACKed with a want_ack for reliable delivery? */ bool shouldSuccessAckWithWantAck(const meshtastic_MeshPacket *p); }; ================================================ FILE: src/mesh/Router.cpp ================================================ #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 HAS_TRAFFIC_MANAGEMENT #include "modules/TrafficManagementModule.h" #endif #if !MESHTASTIC_EXCLUDE_MQTT #include "mqtt/MQTT.h" #endif #include "Default.h" #if ARCH_PORTDUINO #include "Throttle.h" #include "platform/portduino/PortduinoGlue.h" #endif #if ENABLE_JSON_LOGGING || ARCH_PORTDUINO #include "serialization/MeshPacketSerializer.h" #endif #define MAX_RX_FROMRADIO \ 4 // max number of packets destined to our queue, we dispatch packets quickly so it doesn't need to be big // I think this is right, one packet for each of the three fifos + one packet being currently assembled for TX or RX // And every TX packet might have a retransmission packet or an ack alive at any moment #ifdef ARCH_PORTDUINO // Portduino (native) targets can use dynamic memory pools with runtime-configurable sizes #define MAX_PACKETS \ (MAX_RX_TOPHONE + MAX_RX_FROMRADIO + 2 * MAX_TX_QUEUE + \ 2) // max number of packets which can be in flight (either queued from reception or queued for sending) static MemoryDynamic dynamicPool; Allocator &packetPool = dynamicPool; #elif defined(ARCH_STM32WL) || defined(BOARD_HAS_PSRAM) // On STM32 and boards with PSRAM, there isn't enough heap left over for the rest of the firmware if we allocate this statically. // For now, make it dynamic again. #define MAX_PACKETS \ (MAX_RX_TOPHONE + MAX_RX_FROMRADIO + 2 * MAX_TX_QUEUE + \ 2) // max number of packets which can be in flight (either queued from reception or queued for sending) static MemoryDynamic dynamicPool; Allocator &packetPool = dynamicPool; #else // Embedded targets use static memory pools with compile-time constants #define MAX_PACKETS_STATIC \ (MAX_RX_TOPHONE + MAX_RX_FROMRADIO + 2 * MAX_TX_QUEUE + \ 2) // max number of packets which can be in flight (either queued from reception or queued for sending) static MemoryPool staticPool; Allocator &packetPool = staticPool; #endif static uint8_t bytes[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(); } bool Router::shouldDecrementHopLimit(const meshtastic_MeshPacket *p) { // First hop MUST always decrement to prevent retry issues if (getHopsAway(*p) == 0) { return true; // Always decrement on first hop } // Check if both local device and previous relay are routers (including CLIENT_BASE) bool localIsRouter = IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_ROUTER, meshtastic_Config_DeviceConfig_Role_ROUTER_LATE, meshtastic_Config_DeviceConfig_Role_CLIENT_BASE); // If local device isn't a router, always decrement if (!localIsRouter) { return true; } #if HAS_TRAFFIC_MANAGEMENT // When router_preserve_hops is enabled, preserve hops for decoded packets that are not // position or telemetry (those have their own exhaust_hop controls). if (moduleConfig.has_traffic_management && moduleConfig.traffic_management.enabled && moduleConfig.traffic_management.router_preserve_hops && p->which_payload_variant == meshtastic_MeshPacket_decoded_tag && p->decoded.portnum != meshtastic_PortNum_POSITION_APP && p->decoded.portnum != meshtastic_PortNum_TELEMETRY_APP) { LOG_DEBUG("Router hop preserved: port=%d from=0x%08x (traffic_management)", p->decoded.portnum, getFrom(p)); if (trafficManagementModule) { trafficManagementModule->recordRouterHopPreserved(); } return false; } #endif // For subsequent hops, check if previous relay is a favorite router // Optimized search for favorite routers with matching last byte // Check ordering optimized for IoT devices (cheapest checks first) for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) { meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); if (!node) continue; // Check 1: is_favorite (cheapest - single bool) if (!node->is_favorite) continue; // Check 2: has_user (cheap - single bool) if (!node->has_user) continue; // Check 3: role check (moderate cost - multiple comparisons) if (!IS_ONE_OF(node->user.role, meshtastic_Config_DeviceConfig_Role_ROUTER, meshtastic_Config_DeviceConfig_Role_ROUTER_LATE, meshtastic_Config_DeviceConfig_Role_CLIENT_BASE)) { continue; } // Check 4: last byte extraction and comparison (most expensive) if (nodeDB->getLastByteOfNodeNum(node->num) == p->relay_node) { // Found a favorite router match LOG_DEBUG("Identified favorite relay router 0x%x from last byte 0x%x", node->num, p->relay_node); return false; // Don't decrement hop_limit } } // No favorite router match found, decrement hop_limit return true; } /** * do idle processing * Mostly looking in our incoming rxPacket queue and calling handleReceived. */ int32_t Router::runOnce() { meshtastic_MeshPacket *mp; while ((mp = fromRadioQueue.dequeuePtr(0)) != NULL) { // printPacket("handle fromRadioQ", mp); perhapsHandleReceived(mp); } // LOG_DEBUG("Sleep forever!"); return INT32_MAX; // Wait a long time - until we get woken for the message queue } /** * RadioInterface calls this to queue up packets that have been received from the radio. The router is now responsible for * freeing the packet */ void Router::enqueueReceivedMessage(meshtastic_MeshPacket *p) { // Try enqueue until successful while (!fromRadioQueue.enqueue(p, 0)) { meshtastic_MeshPacket *old_p; old_p = fromRadioQueue.dequeuePtr(0); // Dequeue and discard the oldest packet if (old_p) { printPacket("fromRadioQ full, drop oldest!", old_p); packetPool.release(old_p); } } // Nasty hack because our threading is primitive. interfaces shouldn't need to know about routers FIXME setReceivedMessage(); } /// 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 hopLimit, bool ackWantsAck) { routingModule->sendAckNak(err, to, idFrom, chIndex, hopLimit, ackWantsAck); } void Router::abortSendAndNak(meshtastic_Routing_Error err, meshtastic_MeshPacket *p) { LOG_ERROR("Error=%d, return NAK and drop packet", err); sendAckNak(err, getFrom(p), p->id, p->channel); packetPool.release(p); } void Router::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); } // don't override if a channel was requested and no need to set it when PKI is enforced if (!p->channel && !p->pki_encrypted && !isBroadcast(p->to)) { meshtastic_NodeInfoLite const *node = nodeDB->getMeshNode(p->to); if (node) { p->channel = node->channel; LOG_DEBUG("localSend to channel %d", p->channel); } } // 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 (src == RX_SRC_USER && p->want_ack && p->hop_limit == 0) { p->hop_limit = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit); } return send(p); } } /** * Send a packet on a suitable interface. */ ErrorCode Router::rawSend(meshtastic_MeshPacket *p) { assert(iface); // This should have been detected already in sendLocal (or we just received a packet from outside) return iface->send(p); } /** * 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) { uint8_t silentMinutes = airTime->getSilentMinutes(hourlyTxPercent, myRegion->dutyCycle); LOG_WARN("Duty cycle limit exceeded. Aborting send for now, you can send again in %d mins", silentMinutes); meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); cn->has_reply_id = true; cn->reply_id = p->id; cn->level = meshtastic_LogRecord_Level_WARNING; cn->time = getValidTime(RTCQualityFromNet); sprintf(cn->message, "Duty cycle limit exceeded. You can send again in %d mins", silentMinutes); service->sendClientNotification(cn); meshtastic_Routing_Error err = meshtastic_Routing_Error_DUTY_CYCLE_LIMIT; if (isFromUs(p)) { // only send NAK to API, not to the mesh abortSendAndNak(err, p); } else { packetPool.release(p); } return err; } } // PacketId nakId = p->decoded.which_ackVariant == SubPacket_fail_id_tag ? p->decoded.ackVariant.fail_id : 0; // assert(!nakId); // I don't think we ever send 0hop naks over the wire (other than to the phone), test that assumption with // assert // 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); p->relay_node = nodeDB->getLastByteOfNodeNum(getNodeNum()); // set the relayer to us // If we are the original transmitter, set the hop limit with which we start if (isFromUs(p)) p->hop_start = p->hop_limit; // If the packet hasn't yet been encrypted, do so now (it might already be encrypted if we are just forwarding it) if (!(p->which_payload_variant == meshtastic_MeshPacket_encrypted_tag || p->which_payload_variant == meshtastic_MeshPacket_decoded_tag)) { return meshtastic_Routing_Error_BAD_REQUEST; } fixPriority(p); // Before encryption, fix the priority if it's unset // If the packet is not yet encrypted, do so now if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) { ChannelIndex chIndex = p->channel; // keep as a local because we are about to change it DEBUG_HEAP_BEFORE; meshtastic_MeshPacket *p_decoded = packetPool.allocCopy(*p); DEBUG_HEAP_AFTER("Router::send", p_decoded); auto encodeResult = perhapsEncode(p); if (encodeResult != meshtastic_Routing_Error_NONE) { packetPool.release(p_decoded); p->channel = 0; // Reset the channel to 0, so we don't use the failing hash again abortSendAndNak(encodeResult, p); return encodeResult; // FIXME - this isn't a valid ErrorCode } #if !MESHTASTIC_EXCLUDE_MQTT // Only publish to MQTT if we're the original transmitter of the packet if (moduleConfig.mqtt.enabled && isFromUs(p) && mqtt) { mqtt->onSend(*p, *p_decoded, chIndex); } #endif packetPool.release(p_decoded); } #if HAS_UDP_MULTICAST if (udpHandler && config.network.enabled_protocols & meshtastic_Config_NetworkConfig_ProtocolFlags_UDP_BROADCAST) { udpHandler->onSend(const_cast(p)); } #endif assert(iface); // This should have been detected already in sendLocal (or we just received a packet from outside) return iface->send(p); } /** Attempt to cancel a previously sent packet. Returns true if a packet was found we could cancel */ bool Router::cancelSending(NodeNum from, PacketId id) { if (iface && iface->cancelSending(from, id)) { // We are not a relayer of this packet anymore removeRelayer(nodeDB->getLastByteOfNodeNum(nodeDB->getNodeNum()), id, from); return true; } return false; } /** Attempt to find a packet in the TxQueue. Returns true if the packet was found. */ bool Router::findInTxQueue(NodeNum from, PacketId id) { return iface->findInTxQueue(from, id); } /** * 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 } DecodeState perhapsDecode(meshtastic_MeshPacket *p) { concurrency::LockGuard g(cryptLock); if (config.device.rebroadcast_mode == meshtastic_Config_DeviceConfig_RebroadcastMode_KNOWN_ONLY && (nodeDB->getMeshNode(p->from) == NULL || !nodeDB->getMeshNode(p->from)->has_user)) { LOG_DEBUG("Node 0x%x not in nodeDB-> Rebroadcast mode KNOWN_ONLY will ignore packet", p->from); return DecodeState::DECODE_FAILURE; } if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) return DecodeState::DECODE_SUCCESS; // If packet was already decoded just return size_t rawSize = p->encrypted.size; if (rawSize > sizeof(bytes)) { LOG_ERROR("Packet too large to attempt decryption! (rawSize=%d > 256)", rawSize); return DecodeState::DECODE_FATAL; } bool decrypted = false; ChannelIndex chIndex = 0; #if !(MESHTASTIC_EXCLUDE_PKI) // Attempt PKI decryption first if (p->channel == 0 && isToUs(p) && p->to > 0 && !isBroadcast(p->to) && nodeDB->getMeshNode(p->from) != nullptr && nodeDB->getMeshNode(p->from)->user.public_key.size > 0 && nodeDB->getMeshNode(p->to)->user.public_key.size > 0 && rawSize > MESHTASTIC_PKC_OVERHEAD) { LOG_DEBUG("Attempt PKI decryption"); if (crypto->decryptCurve25519(p->from, nodeDB->getMeshNode(p->from)->user.public_key, p->id, rawSize, p->encrypted.bytes, bytes)) { LOG_INFO("PKI Decryption worked!"); meshtastic_Data decodedtmp; memset(&decodedtmp, 0, sizeof(decodedtmp)); rawSize -= MESHTASTIC_PKC_OVERHEAD; if (pb_decode_from_bytes(bytes, rawSize, &meshtastic_Data_msg, &decodedtmp) && decodedtmp.portnum != meshtastic_PortNum_UNKNOWN_APP) { decrypted = true; LOG_INFO("Packet decrypted using PKI!"); p->pki_encrypted = true; memcpy(&p->public_key.bytes, nodeDB->getMeshNode(p->from)->user.public_key.bytes, 32); p->public_key.size = 32; p->decoded = decodedtmp; p->which_payload_variant = meshtastic_MeshPacket_decoded_tag; // change type to decoded } else { LOG_ERROR("PKC Decrypted, but pb_decode failed!"); return DecodeState::DECODE_FAILURE; } } else { LOG_WARN("PKC decrypt attempted but failed!"); } } #endif // assert(p->which_payloadVariant == MeshPacket_encrypted_tag); if (!decrypted) { // Try to find a channel that works with this hash for (chIndex = 0; chIndex < channels.getNumChannels(); chIndex++) { // Try to use this hash/channel pair if (channels.decryptForHash(chIndex, p->channel)) { // we have to copy into a scratch buffer, because these bytes are a union with the decoded protobuf. Create a // fresh copy for each decrypt attempt. memcpy(bytes, p->encrypted.bytes, rawSize); // Try to decrypt the packet if we can crypto->decrypt(p->from, p->id, rawSize, bytes); // printBytes("plaintext", bytes, p->encrypted.size); // Take those raw bytes and convert them back into a well structured protobuf we can understand meshtastic_Data decodedtmp; memset(&decodedtmp, 0, sizeof(decodedtmp)); if (!pb_decode_from_bytes(bytes, rawSize, &meshtastic_Data_msg, &decodedtmp)) { LOG_ERROR("Invalid protobufs in received mesh packet id=0x%08x (bad psk?)!", p->id); } else if (decodedtmp.portnum == meshtastic_PortNum_UNKNOWN_APP) { LOG_ERROR("Invalid portnum (bad psk?)!"); #if !(MESHTASTIC_EXCLUDE_PKI) } else if (!owner.is_licensed && isToUs(p) && decodedtmp.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP) { LOG_WARN("Rejecting legacy DM"); return DecodeState::DECODE_FAILURE; #endif } else { p->decoded = decodedtmp; p->which_payload_variant = meshtastic_MeshPacket_decoded_tag; // change type to decoded decrypted = true; break; } } } } if (decrypted) { // parsing was successful p->channel = chIndex; // change to store the index instead of the hash if (p->decoded.has_bitfield) p->decoded.want_response |= p->decoded.bitfield & BITFIELD_WANT_RESPONSE_MASK; /* Not actually ever used. // Decompress if needed. jm if (p->decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_COMPRESSED_APP) { // Decompress the payload char compressed_in[meshtastic_Constants_DATA_PAYLOAD_LEN] = {}; char decompressed_out[meshtastic_Constants_DATA_PAYLOAD_LEN] = {}; int decompressed_len; memcpy(compressed_in, p->decoded.payload.bytes, p->decoded.payload.size); decompressed_len = unishox2_decompress_simple(compressed_in, p->decoded.payload.size, decompressed_out); // LOG_DEBUG("**Decompressed length - %d ", decompressed_len); memcpy(p->decoded.payload.bytes, decompressed_out, decompressed_len); // Switch the port from PortNum_TEXT_MESSAGE_COMPRESSED_APP to PortNum_TEXT_MESSAGE_APP p->decoded.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP; } */ printPacket("decoded message", p); #if ENABLE_JSON_LOGGING LOG_TRACE("%s", MeshPacketSerializer::JsonSerialize(p, false).c_str()); #elif ARCH_PORTDUINO if (portduino_config.traceFilename != "" || portduino_config.logoutputlevel == level_trace) { LOG_TRACE("%s", MeshPacketSerializer::JsonSerialize(p, false).c_str()); } else if (portduino_config.JSONFilename != "") { if (portduino_config.JSONFileRotate != 0) { static uint32_t fileage = 0; if (portduino_config.JSONFileRotate != 0 && (fileage == 0 || !Throttle::isWithinTimespanMs(fileage, portduino_config.JSONFileRotate * 60 * 1000))) { time_t timestamp = time(NULL); struct tm *timeinfo; char buffer[80]; timeinfo = localtime(×tamp); strftime(buffer, 80, "%Y%m%d-%H%M%S", timeinfo); std::string datetime(buffer); if (JSONFile.is_open()) { JSONFile.close(); } JSONFile.open(portduino_config.JSONFilename + "_" + datetime, std::ios::out | std::ios::app); fileage = millis(); } } if (portduino_config.JSONFilter == (_meshtastic_PortNum)0 || portduino_config.JSONFilter == p->decoded.portnum) { JSONFile << MeshPacketSerializer::JsonSerialize(p, false) << std::endl; } } #endif return DecodeState::DECODE_SUCCESS; } else { LOG_WARN("No suitable channel found for decoding, hash was 0x%x!", p->channel); return DecodeState::DECODE_FAILURE; } } /** Return 0 for success or a Routing_Error code for failure */ meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p) { concurrency::LockGuard g(cryptLock); int16_t hash; // If the packet is not yet encrypted, do so now if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) { if (isFromUs(p)) { p->decoded.has_bitfield = true; p->decoded.bitfield |= (config.lora.config_ok_to_mqtt << BITFIELD_OK_TO_MQTT_SHIFT); p->decoded.bitfield |= (p->decoded.want_response << BITFIELD_WANT_RESPONSE_SHIFT); } size_t numbytes = pb_encode_to_bytes(bytes, sizeof(bytes), &meshtastic_Data_msg, &p->decoded); /* Not actually used, so save the cycles // TODO: Allow modules to opt into compression. if (p->decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP) { char original_payload[meshtastic_Constants_DATA_PAYLOAD_LEN]; memcpy(original_payload, p->decoded.payload.bytes, p->decoded.payload.size); char compressed_out[meshtastic_Constants_DATA_PAYLOAD_LEN] = {0}; int compressed_len; compressed_len = unishox2_compress_simple(original_payload, p->decoded.payload.size, compressed_out); LOG_DEBUG("Original length - %d ", p->decoded.payload.size); LOG_DEBUG("Compressed length - %d ", compressed_len); LOG_DEBUG("Original message - %s ", p->decoded.payload.bytes); // If the compressed length is greater than or equal to the original size, don't use the compressed form if (compressed_len >= p->decoded.payload.size) { LOG_DEBUG("Not using compressing message"); // Set the uncompressed payload variant anyway. Shouldn't hurt? // p->decoded.which_payloadVariant = Data_payload_tag; // Otherwise we use the compressor } else { LOG_DEBUG("Use compressed message"); // Copy the compressed data into the meshpacket p->decoded.payload.size = compressed_len; memcpy(p->decoded.payload.bytes, compressed_out, compressed_len); p->decoded.portnum = meshtastic_PortNum_TEXT_MESSAGE_COMPRESSED_APP; } } */ if (numbytes + MESHTASTIC_HEADER_LENGTH > MAX_LORA_PAYLOAD_LEN) return meshtastic_Routing_Error_TOO_LARGE; // printBytes("plaintext", bytes, numbytes); ChannelIndex chIndex = p->channel; // keep as a local because we are about to change it #if !(MESHTASTIC_EXCLUDE_PKI) meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(p->to); // We may want to retool things so we can send a PKC packet when the client specifies a key and nodenum, even if the node // is not in the local nodedb // First, only PKC encrypt packets we are originating if (isFromUs(p) && #if ARCH_PORTDUINO // Sim radio via the cli flag skips PKC !portduino_config.force_simradio && #endif // Don't use PKC with Ham mode !owner.is_licensed && // Don't use PKC on 'serial' or 'gpio' channels unless explicitly requested !(p->pki_encrypted != true && (strcasecmp(channels.getName(chIndex), Channels::serialChannel) == 0 || strcasecmp(channels.getName(chIndex), Channels::gpioChannel) == 0)) && // Check for valid keys and single node destination config.security.private_key.size == 32 && !isBroadcast(p->to) && // Some portnums either make no sense to send with PKC p->decoded.portnum != meshtastic_PortNum_TRACEROUTE_APP && p->decoded.portnum != meshtastic_PortNum_NODEINFO_APP && p->decoded.portnum != meshtastic_PortNum_ROUTING_APP && p->decoded.portnum != meshtastic_PortNum_POSITION_APP) { LOG_DEBUG("Use PKI!"); if (numbytes + MESHTASTIC_HEADER_LENGTH + MESHTASTIC_PKC_OVERHEAD > MAX_LORA_PAYLOAD_LEN) return meshtastic_Routing_Error_TOO_LARGE; // Check for a known public key for the destination if (node == nullptr || node->user.public_key.size != 32) { LOG_WARN("Unknown public key for destination node 0x%08x (portnum %d), refusing to send legacy DM", p->to, p->decoded.portnum); return meshtastic_Routing_Error_PKI_SEND_FAIL_PUBLIC_KEY; } if (p->pki_encrypted && !memfll(p->public_key.bytes, 0, 32) && memcmp(p->public_key.bytes, node->user.public_key.bytes, 32) != 0) { LOG_WARN("Client public key differs from requested: 0x%02x, stored key begins 0x%02x", *p->public_key.bytes, *node->user.public_key.bytes); return meshtastic_Routing_Error_PKI_FAILED; } crypto->encryptCurve25519(p->to, getFrom(p), node->user.public_key, p->id, numbytes, bytes, p->encrypted.bytes); numbytes += MESHTASTIC_PKC_OVERHEAD; p->channel = 0; p->pki_encrypted = true; } else { if (p->pki_encrypted == true) { // Client specifically requested PKI encryption return meshtastic_Routing_Error_PKI_FAILED; } hash = channels.setActiveByIndex(chIndex); // Now that we are encrypting the packet channel should be the hash (no longer the index) p->channel = hash; if (hash < 0) { // No suitable channel could be found for return meshtastic_Routing_Error_NO_CHANNEL; } crypto->encryptPacket(getFrom(p), p->id, numbytes, bytes); memcpy(p->encrypted.bytes, bytes, numbytes); } #else if (p->pki_encrypted == true) { // Client specifically requested PKI encryption return meshtastic_Routing_Error_PKI_FAILED; } hash = channels.setActiveByIndex(chIndex); // Now that we are encrypting the packet channel should be the hash (no longer the index) p->channel = hash; if (hash < 0) { // No suitable channel could be found for return meshtastic_Routing_Error_NO_CHANNEL; } crypto->encryptPacket(getFrom(p), p->id, numbytes, bytes); memcpy(p->encrypted.bytes, bytes, numbytes); #endif // Copy back into the packet and set the variant type p->encrypted.size = numbytes; p->which_payload_variant = meshtastic_MeshPacket_encrypted_tag; } return meshtastic_Routing_Error_NONE; } NodeNum Router::getNodeNum() { return nodeDB->getNodeNum(); } /** * 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 DEBUG_HEAP_BEFORE; p_encrypted = packetPool.allocCopy(*p); DEBUG_HEAP_AFTER("Router::handleReceived", p_encrypted); // Take those raw bytes and convert them back into a well structured protobuf we can understand auto decodedState = perhapsDecode(p); if (decodedState == DecodeState::DECODE_FATAL) { // Fatal decoding error, we can't do anything with this packet LOG_WARN("Fatal decode error, dropping packet"); cancelSending(p->from, p->id); skipHandle = true; } else if (decodedState == DecodeState::DECODE_SUCCESS) { // parsing was successful, queue for our recipient if (src == RX_SRC_LOCAL) printPacket("handleReceived(LOCAL)", p); else if (src == RX_SRC_USER) printPacket("handleReceived(USER)", p); else printPacket("handleReceived(REMOTE)", p); // Neighbor info module is disabled, ignore expensive neighbor info packets if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag && p->decoded.portnum == meshtastic_PortNum_NEIGHBORINFO_APP && (!moduleConfig.has_neighbor_info || !moduleConfig.neighbor_info.enabled)) { LOG_DEBUG("Neighbor info module is disabled, ignore neighbor packet"); cancelSending(p->from, p->id); skipHandle = true; } bool shouldIgnoreNonstandardPorts = config.device.rebroadcast_mode == meshtastic_Config_DeviceConfig_RebroadcastMode_CORE_PORTNUMS_ONLY; #if USERPREFS_EVENT_MODE shouldIgnoreNonstandardPorts = true; #endif if (shouldIgnoreNonstandardPorts && p->which_payload_variant == meshtastic_MeshPacket_decoded_tag && !IS_ONE_OF(p->decoded.portnum, meshtastic_PortNum_TEXT_MESSAGE_APP, meshtastic_PortNum_TEXT_MESSAGE_COMPRESSED_APP, meshtastic_PortNum_POSITION_APP, meshtastic_PortNum_NODEINFO_APP, meshtastic_PortNum_ROUTING_APP, meshtastic_PortNum_TELEMETRY_APP, meshtastic_PortNum_ADMIN_APP, meshtastic_PortNum_ALERT_APP, meshtastic_PortNum_KEY_VERIFICATION_APP, meshtastic_PortNum_WAYPOINT_APP, meshtastic_PortNum_STORE_FORWARD_APP, meshtastic_PortNum_TRACEROUTE_APP, meshtastic_PortNum_STORE_FORWARD_PLUSPLUS_APP)) { LOG_DEBUG("Ignore packet on non-standard portnum for CORE_PORTNUMS_ONLY"); cancelSending(p->from, p->id); skipHandle = true; } } else { printPacket("packet decoding failed or skipped (no PSK?)", p); } // call modules here // If this could be a spoofed packet, don't let the modules see it. if (!skipHandle) { MeshModule::callModules(*p, src); #if !MESHTASTIC_EXCLUDE_MQTT if (p_encrypted == nullptr) { LOG_WARN("p_encrypted is null, skipping MQTT publish"); } else { // Mark as pki_encrypted if it is not yet decoded and MQTT encryption is also enabled, hash matches and it's a DM not // to us (because we would be able to decrypt it) if (decodedState == DecodeState::DECODE_FAILURE && moduleConfig.mqtt.encryption_enabled && p->channel == 0x00 && !isBroadcast(p->to) && !isToUs(p)) p_encrypted->pki_encrypted = true; // After potentially altering it, publish received message to MQTT if we're not the original transmitter of the packet if ((decodedState == DecodeState::DECODE_SUCCESS || p_encrypted->pki_encrypted) && moduleConfig.mqtt.enabled && !isFromUs(p) && mqtt) { if (decodedState == DecodeState::DECODE_SUCCESS && p->decoded.portnum == meshtastic_PortNum_TRACEROUTE_APP && moduleConfig.mqtt.encryption_enabled) { // For TRACEROUTE_APP packets release the original encrypted packet and encrypt a new from the changed packet // Only release the original after successful allocation to avoid losing an incomplete but valid packet auto *p_encrypted_new = packetPool.allocCopy(*p); if (p_encrypted_new) { auto encodeResult = perhapsEncode(p_encrypted_new); if (encodeResult != meshtastic_Routing_Error_NONE) { // Encryption failed, release the new packet and fall back to sending the original encrypted packet to // MQTT LOG_WARN("Encryption of new TR packet failed, sending original TR to MQTT"); packetPool.release(p_encrypted_new); p_encrypted_new = nullptr; } else { // Successfully re-encrypted, release the original encrypted packet and use the new one for MQTT packetPool.release(p_encrypted); p_encrypted = p_encrypted_new; } } else { // Allocation failed, log a warning and fall back to sending the original encrypted packet to MQTT LOG_WARN("Failed to allocate new encrypted packet for TR, sending original TR to MQTT"); } } mqtt->onSend(*p_encrypted, *p, p->channel); } } #endif } packetPool.release(p_encrypted); // Release the encrypted packet p_encrypted = nullptr; } void Router::perhapsHandleReceived(meshtastic_MeshPacket *p) { #if ENABLE_JSON_LOGGING // Even ignored packets get logged in the trace p->rx_time = getValidTime(RTCQualityFromNet); // store the arrival timestamp for the phone LOG_TRACE("%s", MeshPacketSerializer::JsonSerializeEncrypted(p).c_str()); #elif ARCH_PORTDUINO // Even ignored packets get logged in the trace if (portduino_config.traceFilename != "" || portduino_config.logoutputlevel == level_trace) { p->rx_time = getValidTime(RTCQualityFromNet); // store the arrival timestamp for the phone LOG_TRACE("%s", MeshPacketSerializer::JsonSerializeEncrypted(p).c_str()); } #endif // assert(radioConfig.has_preferences); if (is_in_repeated(config.lora.ignore_incoming, p->from)) { LOG_DEBUG("Ignore msg, 0x%x is in our ignore list", p->from); packetPool.release(p); return; } meshtastic_NodeInfoLite const *node = nodeDB->getMeshNode(p->from); if (node != NULL && node->is_ignored) { LOG_DEBUG("Ignore msg, 0x%x is ignored", p->from); packetPool.release(p); return; } if (p->from == NODENUM_BROADCAST) { LOG_DEBUG("Ignore msg from broadcast address"); packetPool.release(p); return; } if (config.lora.ignore_mqtt && p->via_mqtt) { LOG_DEBUG("Msg came in via MQTT from 0x%x", p->from); packetPool.release(p); return; } if (shouldDropPacketForPreHop(*p)) { logHopStartDrop(*p, "pre-hop drop"); 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); } ================================================ FILE: src/mesh/Router.h ================================================ #pragma once #include "Channels.h" #include "MemoryPool.h" #include "MeshTypes.h" #include "Observer.h" #include "PacketHistory.h" #include "PointerQueue.h" #include "RadioInterface.h" #include "concurrency/OSThread.h" #include /** * A mesh aware router that supports multiple interfaces. */ class Router : protected concurrency::OSThread, protected PacketHistory { private: /// Packets which have just arrived from the radio, ready to be processed by this service and possibly /// forwarded to the phone. PointerQueue fromRadioQueue; protected: std::unique_ptr iface = nullptr; public: /** * Constructor * */ Router(); /** * Currently we only allow one interface, that may change in the future */ void addInterface(std::unique_ptr _iface) { iface = std::move(_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); /** Attempt to find a packet in the TxQueue. Returns true if the packet was found. */ bool findInTxQueue(NodeNum from, PacketId id); /** Allocate and return a meshpacket which defaults as send to broadcast from the current node. * The returned packet is guaranteed to have a unique packet ID already assigned */ [[nodiscard]] meshtastic_MeshPacket *allocForSending(); /** Return Underlying interface's TX queue status */ [[nodiscard]] meshtastic_QueueStatus getQueueStatus(); /** * @return our local nodenum */ [[nodiscard]] 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 */ virtual void enqueueReceivedMessage(meshtastic_MeshPacket *p); /** * Send a packet on a suitable interface. This routine will * later free() the packet to pool. This routine is not allowed to stall. * If the txmit queue is full it might return an error * * NOTE: This method will free the provided packet (even if we return an error code) */ virtual ErrorCode send(meshtastic_MeshPacket *p); virtual ErrorCode rawSend(meshtastic_MeshPacket *p); /* Statistics for the amount of duplicate received packets and the amount of times we cancel a relay because someone did it before us */ uint32_t rxDupe = 0, txRelayCanceled = 0; // pointer to the encrypted packet meshtastic_MeshPacket *p_encrypted = nullptr; 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; } /** * Determine if hop_limit should be decremented for a relay operation. * Returns false (preserve hop_limit) only if all conditions are met: * - It's NOT the first hop (first hop must always decrement) * - Local device is a ROUTER, ROUTER_LATE, or CLIENT_BASE * - Previous relay is a favorite ROUTER, ROUTER_LATE, or CLIENT_BASE * * @param p The packet being relayed * @return true if hop_limit should be decremented, false to preserve it */ bool shouldDecrementHopLimit(const meshtastic_MeshPacket *p); /** * Every (non duplicate) packet this node receives will be passed through this method. This allows subclasses to * update routing tables etc... based on what we overhear (even for messages not destined to our node) */ virtual void sniffReceived(const meshtastic_MeshPacket *p, const meshtastic_Routing *c); /** * Send an ack or a nak packet back towards whoever sent idFrom */ void sendAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex, uint8_t hopLimit = 0, bool ackWantsAck = false); private: /** * Called from loop() * Handle any packet that is received by an interface on this node. * Note: some packets may merely being passed through this node and will be forwarded elsewhere. * * Note: this packet will never be called for messages sent/generated by this node. * Note: this method will free the provided packet. */ void perhapsHandleReceived(meshtastic_MeshPacket *p); /** * Called from perhapsHandleReceived() - allows subclass message delivery behavior. * Handle any packet that is received by an interface on this node. * Note: some packets may merely being passed through this node and will be forwarded elsewhere. * * Note: this packet will never be called for messages sent/generated by this node. * Note: this method will free the provided packet. */ void handleReceived(meshtastic_MeshPacket *p, RxSource src = RX_SRC_RADIO); /** Frees the provided packet, and generates a NAK indicating the specifed error while sending */ void abortSendAndNak(meshtastic_Routing_Error err, meshtastic_MeshPacket *p); }; enum DecodeState { DECODE_SUCCESS, DECODE_FAILURE, DECODE_FATAL }; /** 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. */ DecodeState perhapsDecode(meshtastic_MeshPacket *p); /** Return 0 for success or a Routing_Error 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) ================================================ FILE: src/mesh/STM32WLE5JCInterface.cpp ================================================ #include "configuration.h" #ifdef ARCH_STM32WL #include "STM32WLE5JCInterface.h" #include "error.h" #ifndef STM32WLx_MAX_POWER #define STM32WLx_MAX_POWER 22 #endif STM32WLE5JCInterface::STM32WLE5JCInterface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy) : SX126xInterface(hal, cs, irq, rst, busy) { } bool STM32WLE5JCInterface::init() { RadioLibInterface::init(); // https://github.com/Seeed-Studio/LoRaWan-E5-Node/blob/main/Middlewares/Third_Party/SubGHz_Phy/stm32_radio_driver/radio_driver.c #if (!defined(_VARIANT_RAK3172_)) setTCXOVoltage(1.7); #endif lora.setRfSwitchTable(rfswitch_pins, rfswitch_table); limitPower(STM32WLx_MAX_POWER); 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 ================================================ FILE: src/mesh/STM32WLE5JCInterface.h ================================================ #pragma once #ifdef ARCH_STM32WL #include "SX126xInterface.h" #include "rfswitch.h" /** * 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; }; #endif // ARCH_STM32WL ================================================ FILE: src/mesh/SX1262Interface.cpp ================================================ #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 ================================================ FILE: src/mesh/SX1262Interface.h ================================================ #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 ================================================ FILE: src/mesh/SX1268Interface.cpp ================================================ #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 ================================================ FILE: src/mesh/SX1268Interface.h ================================================ #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 ================================================ FILE: src/mesh/SX126xInterface.cpp ================================================ #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 #if defined(ARCH_ESP32) #include #include #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) #if ARCH_PORTDUINO #define SX126X_MAX_POWER portduino_config.sx126x_max_power #endif #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 HAS_LORA_FEM loraFEMInterface.init(); // Apply saved FEM LNA mode from config if (loraFEMInterface.isLnaCanControl()) { loraFEMInterface.setLNAEnable(config.lora.fem_lna_mode != meshtastic_Config_LoRaConfig_FEM_LNA_Mode_DISABLED); } #endif #ifdef RF95_FAN_EN digitalWrite(RF95_FAN_EN, HIGH); pinMode(RF95_FAN_EN, OUTPUT); #endif #if ARCH_PORTDUINO tcxoVoltage = (float)portduino_config.dio3_tcxo_voltage / 1000; if (portduino_config.lora_sx126x_ant_sw_pin.pin != RADIOLIB_NC) { digitalWrite(portduino_config.lora_sx126x_ant_sw_pin.pin, HIGH); pinMode(portduino_config.lora_sx126x_ant_sw_pin.pin, OUTPUT); } #endif if (tcxoVoltage == 0.0) LOG_DEBUG("SX126X_DIO3_TCXO_VOLTAGE not defined, not using DIO3 as TCXO reference voltage"); else LOG_DEBUG("SX126X_DIO3_TCXO_VOLTAGE defined, using DIO3 as TCXO reference voltage at %f V", tcxoVoltage); setTransmitEnable(false); // FIXME: May want to set depending on a definition, currently all SX126x variant files use the DC-DC regulator option bool useRegulatorLDO = false; // Seems to depend on the connection to pin 9/DCC_SW - if an inductor DCDC? RadioLibInterface::init(); limitPower(SX126X_MAX_POWER); // Make sure we reach the minimum power supported to turn the chip on (-9dBm) if (power < -9) power = -9; int res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage, useRegulatorLDO); #ifdef SX126X_PA_RAMP_US // Set custom PA ramp time for boards requiring longer stabilization (e.g., T-Beam 1W needs >800us) if (res == RADIOLIB_ERR_NONE) { lora.setPaRampTime(SX126X_PA_RAMP_US); } #endif // \todo Display actual typename of the adapter, not just `SX126x` LOG_INFO("SX126x init result %d", res); if (res == RADIOLIB_ERR_CHIP_NOT_FOUND || res == RADIOLIB_ERR_SPI_CMD_FAILED) return false; LOG_INFO("Frequency set to %f", getFreq()); LOG_INFO("Bandwidth set to %f", bw); LOG_INFO("Power output set to %d", power); // Overriding current limit // (https://github.com/jgromes/RadioLib/blob/690a050ebb46e6097c5d00c371e961c1caa3b52e/src/modules/SX126x/SX126x.cpp#L85) using // value in SX126xInterface.h (currently 140 mA) It may or may not be necessary, depending on how RadioLib functions, from // SX1261/2 datasheet: OCP after setting DeviceSel with SetPaConfig(): SX1261 - 60 mA, SX1262 - 140 mA For the SX1268 the IC // defaults to 140mA no matter the set power level, but RadioLib set it lower, this would need further checking Default values // are: SX1262, SX1268: 0x38 (140 mA), SX1261: 0x18 (60 mA) // FIXME: Not ideal to increase SX1261 current limit above 60mA as it can only transmit max 15dBm, should probably only do it // if using SX1262 or SX1268 res = lora.setCurrentLimit(currentLimit); LOG_DEBUG("Current limit set to %f", currentLimit); LOG_DEBUG("Current limit set result %d", res); if (res == RADIOLIB_ERR_NONE) { #ifdef SX126X_DIO2_AS_RF_SWITCH bool dio2AsRfSwitch = true; #elif defined(ARCH_PORTDUINO) bool dio2AsRfSwitch = false; if (portduino_config.dio2_as_rf_switch) { dio2AsRfSwitch = true; } #else bool dio2AsRfSwitch = false; #endif res = lora.setDio2AsRfSwitch(dio2AsRfSwitch); LOG_DEBUG("Set DIO2 as %sRF switch, result: %d", dio2AsRfSwitch ? "" : "not ", res); } // If a pin isn't defined, we set it to RADIOLIB_NC, it is safe to always do external RF switching with RADIOLIB_NC as it has // no effect #if ARCH_PORTDUINO if (res == RADIOLIB_ERR_NONE) { LOG_DEBUG("Use MCU pin %i as RXEN and pin %i as TXEN to control RF switching", portduino_config.lora_rxen_pin.pin, portduino_config.lora_txen_pin.pin); lora.setRfSwitchPins(portduino_config.lora_rxen_pin.pin, portduino_config.lora_txen_pin.pin); } #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("Use MCU pin %i as RXEN and pin %i as TXEN to control RF switching", SX126X_RXEN, SX126X_TXEN); lora.setRfSwitchPins(SX126X_RXEN, SX126X_TXEN); } #endif if (config.lora.sx126x_rx_boosted_gain) { uint16_t result = lora.setRxBoostedGainMode(true); LOG_INFO("Set RX gain to boosted mode; result: %d", result); } else { uint16_t result = lora.setRxBoostedGainMode(false); LOG_INFO("Set RX gain to power saving mode (boosted mode off); result: %d", result); } // Undocumented SX1262 register patch recommended by Heltec/Semtech for improved RX sensitivity. // Sets bit 0 of register 0x8B5. if (module.SPIsetRegValue(0x8B5, 0x01, 0, 0) == RADIOLIB_ERR_NONE) { LOG_INFO("Applied SX1262 register 0x8B5 patch for RX improvement"); } else { LOG_WARN("Failed to apply SX1262 register 0x8B5 patch for RX improvement"); } #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 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); #ifdef ARCH_PORTDUINO if (err != RADIOLIB_ERR_NONE) portduino_status.LoRa_in_error = true; #else assert(err == RADIOLIB_ERR_NONE); #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 SX126xInterface::addReceiveMetadata(meshtastic_MeshPacket *mp) { // LOG_DEBUG("PacketStatus %x", lora.getPacketStatus()); mp->rx_snr = lora.getSNR(); mp->rx_rssi = lround(lora.getRSSI()); LOG_DEBUG("Corrected frequency offset: %f", lora.getFrequencyError()); } /** We override to turn on transmitter power as needed. */ template void SX126xInterface::configHardwareForSend() { setTransmitEnable(true); RadioLibInterface::configHardwareForSend(); } // For power draw measurements, helpful to force radio to stay sleeping // #define SLEEP_ONLY template void SX126xInterface::startReceive() { #ifdef SLEEP_ONLY sleep(); #else setTransmitEnable(false); setStandby(); // We use a 16 bit preamble so this should save some power by letting radio sit in standby mostly. int err = lora.startReceiveDutyCycleAuto(preambleLength, 8, MESHTASTIC_RADIOLIB_IRQ_RX_FLAGS); if (err != RADIOLIB_ERR_NONE) LOG_ERROR("SX126X startReceiveDutyCycleAuto %s%d", radioLibErr, err); #ifdef ARCH_PORTDUINO if (err != RADIOLIB_ERR_NONE) portduino_status.LoRa_in_error = true; #else assert(err == RADIOLIB_ERR_NONE); #endif RadioLibInterface::startReceive(); // Must be done AFTER, starting transmit, because startTransmit clears (possibly stale) interrupt pending register bits enableInterrupt(isrRxLevel0); checkRxDoneIrqFlag(); #endif } /** Is the channel currently active? */ template bool SX126xInterface::isChannelActive() { // check if we can detect a LoRa preamble on the current channel ChannelScanConfig_t cfg = {.cad = {.symNum = NUM_SYM_CAD, .detPeak = RADIOLIB_SX126X_CAD_PARAM_DEFAULT, .detMin = RADIOLIB_SX126X_CAD_PARAM_DEFAULT, .exitMode = RADIOLIB_SX126X_CAD_PARAM_DEFAULT, .timeout = 0, .irqFlags = RADIOLIB_IRQ_CAD_DEFAULT_FLAGS, .irqMask = RADIOLIB_IRQ_CAD_DEFAULT_MASK}}; int16_t result; setTransmitEnable(false); setStandby(); result = lora.scanChannel(cfg); if (result == RADIOLIB_LORA_DETECTED) return true; if (result != RADIOLIB_CHANNEL_FREE) LOG_ERROR("SX126X scanChannel %s%d", radioLibErr, result); #ifdef ARCH_PORTDUINO if (result == RADIOLIB_ERR_WRONG_MODEM) portduino_status.LoRa_in_error = true; #else assert(result != RADIOLIB_ERR_WRONG_MODEM); #endif 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 #if HAS_LORA_FEM loraFEMInterface.setSleepModeEnable(); #endif return true; } template void SX126xInterface::resetAGC() { // Safety: don't reset mid-packet if (sendingPacket != NULL || (isReceiving && isActivelyReceiving())) return; LOG_DEBUG("SX126x AGC reset: warm sleep + Calibrate(0x7F)"); // 1. Warm sleep — powers down the entire analog frontend, resetting AGC state. // A plain standby→startReceive cycle does NOT reset the AGC. lora.sleep(true); // 2. Wake to RC standby for stable calibration lora.standby(RADIOLIB_SX126X_STANDBY_RC, true); // 3. Calibrate all blocks (ADC, PLL, image, RC oscillators) uint8_t calData = RADIOLIB_SX126X_CALIBRATE_ALL; module.SPIwriteStream(RADIOLIB_SX126X_CMD_CALIBRATE, &calData, 1, true, false); // 4. Wait for calibration to complete (BUSY pin goes low) module.hal->delay(5); uint32_t start = millis(); while (module.hal->digitalRead(module.getGpio())) { if (millis() - start > 50) break; module.hal->yield(); } if (module.hal->digitalRead(module.getGpio())) { LOG_WARN("SX126x AGC reset: calibration did not complete within 50ms"); startReceive(); return; } // 5. Re-calibrate image rejection for actual operating frequency // Calibrate(0x7F) defaults to 902-928 MHz which is wrong for other regions. lora.calibrateImage(getFreq()); // Re-apply settings that calibration may have reset // DIO2 as RF switch #ifdef SX126X_DIO2_AS_RF_SWITCH lora.setDio2AsRfSwitch(true); #elif defined(ARCH_PORTDUINO) if (portduino_config.dio2_as_rf_switch) lora.setDio2AsRfSwitch(true); #endif // RX boosted gain mode lora.setRxBoostedGainMode(config.lora.sx126x_rx_boosted_gain); // 6. Resume receiving startReceive(); } /** Control PA mode for GC1109 FEM - CPS pin selects full PA (txon=true) or bypass mode (txon=false) */ template void SX126xInterface::setTransmitEnable(bool txon) { #if HAS_LORA_FEM if (txon) { loraFEMInterface.setTxModeEnable(); } else { loraFEMInterface.setRxModeEnable(); } #endif } #endif ================================================ FILE: src/mesh/SX126xInterface.h ================================================ #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; } void resetAGC() override; void setTCXOVoltage(float voltage) { tcxoVoltage = voltage; } protected: float currentLimit = 140; // Higher OCP limit for SX126x PA float tcxoVoltage = 0.0; /** * 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; uint32_t getPacketTime(uint32_t pl, bool received) override { return computePacketTime(lora, pl, received); } private: /** Some boards require GPIO control of tx vs rx paths */ void setTransmitEnable(bool txon); }; #endif ================================================ FILE: src/mesh/SX1280Interface.cpp ================================================ #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 ================================================ FILE: src/mesh/SX1280Interface.h ================================================ #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 ================================================ FILE: src/mesh/SX128xInterface.cpp ================================================ #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 #if ARCH_PORTDUINO #define SX128X_MAX_POWER portduino_config.sx128x_max_power #endif #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 (portduino_config.lora_rxen_pin.pin != RADIOLIB_NC) { pinMode(portduino_config.lora_rxen_pin.pin, OUTPUT); digitalWrite(portduino_config.lora_rxen_pin.pin, LOW); // Set low before becoming an output } if (portduino_config.lora_txen_pin.pin != RADIOLIB_NC) { pinMode(portduino_config.lora_txen_pin.pin, OUTPUT); digitalWrite(portduino_config.lora_txen_pin.pin, LOW); // Set low before becoming an output } #else #if defined(SX128X_RXEN) && (SX128X_RXEN != RADIOLIB_NC) // set not rx or tx mode pinMode(SX128X_RXEN, OUTPUT); digitalWrite(SX128X_RXEN, LOW); // Set low before becoming an output #endif #if defined(SX128X_TXEN) && (SX128X_TXEN != RADIOLIB_NC) pinMode(SX128X_TXEN, OUTPUT); digitalWrite(SX128X_TXEN, LOW); #endif #endif RadioLibInterface::init(); limitPower(SX128X_MAX_POWER); 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 (res == RADIOLIB_ERR_CHIP_NOT_FOUND || res == RADIOLIB_ERR_SPI_CMD_FAILED) return false; if ((config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24) && (res == RADIOLIB_ERR_INVALID_FREQUENCY)) { LOG_WARN("Radio only supports 2.4GHz LoRa. Adjusting Region and rebooting"); config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_LORA_24; nodeDB->saveToDisk(SEGMENT_CONFIG); delay(2000); #if defined(ARCH_ESP32) ESP.restart(); #elif defined(ARCH_NRF52) NVIC_SystemReset(); #else LOG_ERROR("FIXME implement reboot for this platform. Skip for now"); #endif } LOG_INFO("Frequency set to %f", getFreq()); LOG_INFO("Bandwidth set to %f", bw); LOG_INFO("Power output set to %d", power); #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 && portduino_config.lora_rxen_pin.pin != RADIOLIB_NC && portduino_config.lora_txen_pin.pin != RADIOLIB_NC) { lora.setRfSwitchPins(portduino_config.lora_rxen_pin.pin, portduino_config.lora_txen_pin.pin); } #endif if (res == RADIOLIB_ERR_NONE) res = lora.setCRC(2); if (res == RADIOLIB_ERR_NONE) 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, cr != 7); // use long interleaving except if CR is 4/7 which doesn't support it 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 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 (portduino_config.lora_rxen_pin.pin != RADIOLIB_NC) { digitalWrite(portduino_config.lora_rxen_pin.pin, LOW); } if (portduino_config.lora_txen_pin.pin != RADIOLIB_NC) { digitalWrite(portduino_config.lora_txen_pin.pin, LOW); } #else #if defined(SX128X_RXEN) && (SX128X_RXEN != RADIOLIB_NC) // we have RXEN/TXEN control - turn off RX and TX power digitalWrite(SX128X_RXEN, LOW); #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()); LOG_DEBUG("Corrected frequency offset: %f", lora.getFrequencyError()); } /** We override to turn on transmitter power as needed. */ template void SX128xInterface::configHardwareForSend() { #if ARCH_PORTDUINO if (portduino_config.lora_txen_pin.pin != RADIOLIB_NC) { digitalWrite(portduino_config.lora_txen_pin.pin, HIGH); } if (portduino_config.lora_rxen_pin.pin != RADIOLIB_NC) { digitalWrite(portduino_config.lora_rxen_pin.pin, LOW); } #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 (portduino_config.lora_rxen_pin.pin != RADIOLIB_NC) { digitalWrite(portduino_config.lora_rxen_pin.pin, HIGH); } if (portduino_config.lora_txen_pin.pin != RADIOLIB_NC) { digitalWrite(portduino_config.lora_txen_pin.pin, LOW); } #else #if defined(SX128X_RXEN) && (SX128X_RXEN != RADIOLIB_NC) // we have RXEN/TXEN control - turn on RX power / off TX power digitalWrite(SX128X_RXEN, HIGH); #endif #if defined(SX128X_TXEN) && (SX128X_TXEN != RADIOLIB_NC) digitalWrite(SX128X_TXEN, LOW); #endif #endif int err = lora.startReceive(RADIOLIB_SX128X_RX_TIMEOUT_INF, MESHTASTIC_RADIOLIB_IRQ_RX_FLAGS); if (err != RADIOLIB_ERR_NONE) LOG_ERROR("SX128X startReceive %s%d", radioLibErr, err); assert(err == RADIOLIB_ERR_NONE); RadioLibInterface::startReceive(); // Must be done AFTER, starting transmit, because startTransmit clears (possibly stale) interrupt pending register bits enableInterrupt(isrRxLevel0); checkRxDoneIrqFlag(); #endif } /** Is the channel currently active? */ template bool SX128xInterface::isChannelActive() { // check if we can detect a LoRa preamble on the current channel ChannelScanConfig_t cfg = {.cad = {.symNum = NUM_SYM_CAD_24GHZ, .detPeak = 0, .detMin = 0, .exitMode = 0, .timeout = 0, .irqFlags = RADIOLIB_IRQ_CAD_DEFAULT_FLAGS, .irqMask = RADIOLIB_IRQ_CAD_DEFAULT_MASK}}; int16_t result; setStandby(); result = lora.scanChannel(cfg); if (result == RADIOLIB_LORA_DETECTED) return true; if (result != RADIOLIB_CHANNEL_FREE) LOG_ERROR("SX128X scanChannel %s%d", radioLibErr, result); assert(result != RADIOLIB_ERR_WRONG_MODEM); return false; } /** 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 ================================================ FILE: src/mesh/SX128xInterface.h ================================================ #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; uint32_t getPacketTime(uint32_t pl, bool received) override { return computePacketTime(lora, pl, received); } }; ================================================ FILE: src/mesh/SinglePortModule.h ================================================ #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; } }; ================================================ FILE: src/mesh/StaticPointerQueue.h ================================================ #pragma once #include "concurrency/OSThread.h" #include "freertosinc.h" #include /** * A static circular buffer queue for pointers. * This provides the same interface as PointerQueue but uses a statically allocated * buffer instead of dynamic allocation. */ template class StaticPointerQueue { static_assert(MaxElements > 0, "MaxElements must be greater than 0"); T *buffer[MaxElements]; int head = 0; int tail = 0; int count = 0; concurrency::OSThread *reader = nullptr; public: StaticPointerQueue() { // Initialize all buffer elements to nullptr to silence warnings and ensure clean state for (int i = 0; i < MaxElements; i++) { buffer[i] = nullptr; } } int numFree() const { return MaxElements - count; } bool isEmpty() const { return count == 0; } int numUsed() const { return count; } bool enqueue(T *x, TickType_t maxWait = portMAX_DELAY) { if (count >= MaxElements) { return false; // Queue is full } if (reader) { reader->setInterval(0); concurrency::mainDelay.interrupt(); } buffer[tail] = x; tail = (tail + 1) % MaxElements; count++; return true; } bool dequeue(T **p, TickType_t maxWait = portMAX_DELAY) { if (count == 0) { return false; // Queue is empty } *p = buffer[head]; head = (head + 1) % MaxElements; count--; return true; } // returns a ptr or null if the queue was empty T *dequeuePtr(TickType_t maxWait = portMAX_DELAY) { T *p; return dequeue(&p, maxWait) ? p : nullptr; } void setReader(concurrency::OSThread *t) { reader = t; } // For compatibility with PointerQueue interface int getMaxLen() const { return MaxElements; } }; ================================================ FILE: src/mesh/StreamAPI.cpp ================================================ #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; } int32_t StreamAPI::runOncePart(char *buf, uint16_t bufLen) { auto result = readStream(buf, bufLen); writeStream(); checkConnectionTimeout(); return result; } /** * Read any rx chars from the link and call handleRecStream */ int32_t StreamAPI::readStream(const char *buf, uint16_t bufLen) { if (bufLen < 1) { // Nothing available this time, if the computer has talked to us recently, poll often, otherwise let CPU sleep a long time bool recentRx = Throttle::isWithinTimespanMs(lastRxMsec, 2000); return recentRx ? 5 : 250; } else { handleRecStream(buf, bufLen); // we had bytes available this time, so assume we might have them next time also lastRxMsec = millis(); return 0; } } /** * call getFromRadio() and deliver encapsulated packets to the Stream */ void StreamAPI::writeStream() { if (canWrite) { uint32_t len; do { // Send every packet we can len = getFromRadio(txBuf + HEADER_LEN); emitTxBuffer(len); } while (len); } } int32_t StreamAPI::handleRecStream(const char *buf, uint16_t bufLen) { uint16_t index = 0; while (bufLen > index) { // Currently we never want to block int cInt = buf[index++]; if (cInt < 0) break; // We ran out of characters (even though available said otherwise) - this can happen on rf52 adafruit // arduino uint8_t c = (uint8_t)cInt; // 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); } } } return 0; } /** * Read any rx chars from the link and call handleToRadio */ int32_t StreamAPI::readStream() { if (!stream->available()) { // Nothing available this time, if the computer has talked to us recently, poll often, otherwise let CPU sleep a long time bool recentRx = Throttle::isWithinTimespanMs(lastRxMsec, 2000); return recentRx ? 5 : 250; } else { while (stream->available()) { // Currently we never want to block int cInt = stream->read(); if (cInt < 0) break; // We ran out of characters (even though available said otherwise) - this can happen on rf52 adafruit // arduino 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; } } /** * 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); } } ================================================ FILE: src/mesh/StreamAPI.h ================================================ #pragma once #include "PhoneAPI.h" #include "Stream.h" #include "concurrency/OSThread.h" #include // 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(); virtual int32_t runOncePart(char *buf, uint16_t bufLen); /// Check the current underlying physical link to see if the client is currently connected virtual bool checkIsConnected() override = 0; private: /** * Read any rx chars from the link and call handleToRadio */ int32_t readStream(); int32_t readStream(const char *buf, uint16_t bufLen); int32_t handleRecStream(const char *buf, uint16_t bufLen); /** * 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; /** * 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); }; ================================================ FILE: src/mesh/Throttle.cpp ================================================ #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; } ================================================ FILE: src/mesh/Throttle.h ================================================ #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); }; ================================================ FILE: src/mesh/TransmitHistory.cpp ================================================ #include "TransmitHistory.h" #include "FSCommon.h" #include "RTC.h" #include "SPILock.h" #include #ifdef FSCom TransmitHistory *transmitHistory = nullptr; TransmitHistory *TransmitHistory::getInstance() { if (!transmitHistory) { transmitHistory = new TransmitHistory(); } return transmitHistory; } void TransmitHistory::loadFromDisk() { spiLock->lock(); auto file = FSCom.open(FILENAME, FILE_O_READ); if (file) { FileHeader header{}; if (file.read((uint8_t *)&header, sizeof(header)) == sizeof(header) && header.magic == MAGIC && header.version == VERSION && header.count <= MAX_ENTRIES) { for (uint8_t i = 0; i < header.count; i++) { Entry entry{}; if (file.read((uint8_t *)&entry, sizeof(entry)) == sizeof(entry)) { if (entry.epochSeconds > 0) { history[entry.key] = entry.epochSeconds; // Seed in-memory millis so throttle works even without RTC/GPS. // Treating stored entries as "just sent" is safe — worst case the // node waits one full interval before its first broadcast. lastMillis[entry.key] = millis(); } } } LOG_INFO("TransmitHistory: loaded %u entries from disk", header.count); } else { LOG_WARN("TransmitHistory: invalid file header, starting fresh"); } file.close(); } else { LOG_INFO("TransmitHistory: no history file found, starting fresh"); } spiLock->unlock(); dirty = false; } void TransmitHistory::setLastSentToMesh(uint16_t key) { lastMillis[key] = millis(); uint32_t now = getTime(); if (now >= 2) { history[key] = now; dirty = true; // Don't flush to disk on every transmit — flash has limited write endurance. // The in-memory lastMillis map handles throttle during normal operation. // Disk is flushed: before deep sleep (sleep.cpp) and periodically here, // throttled to at most once per 5 minutes. Always save the first time // after boot so a crash-reboot loop can't avoid persisting. if (lastDiskSave == 0 || !Throttle::isWithinTimespanMs(lastDiskSave, SAVE_INTERVAL_MS)) { if (saveToDisk()) { lastDiskSave = millis(); } } } } uint32_t TransmitHistory::getLastSentToMeshEpoch(uint16_t key) const { auto it = history.find(key); if (it != history.end()) { return it->second; } return 0; } uint32_t TransmitHistory::getLastSentToMeshMillis(uint16_t key) const { // Prefer runtime millis value (accurate within this boot) auto mit = lastMillis.find(key); if (mit != lastMillis.end()) { return mit->second; } // Fall back to epoch conversion (loaded from disk after reboot) uint32_t storedEpoch = getLastSentToMeshEpoch(key); if (storedEpoch == 0) { return 0; // No stored time — module has never sent } uint32_t now = getTime(); if (now < 2) { // No valid RTC time yet — can't convert to millis. Return 0 so throttle doesn't block. return 0; } if (storedEpoch > now) { // Stored time is in the future (clock went backwards?) — treat as stale return 0; } uint32_t secondsAgo = now - storedEpoch; uint32_t msAgo = secondsAgo * 1000; // Guard against overflow: if the transmit was very long ago, just return 0 (won't throttle) if (secondsAgo > 86400 || msAgo / 1000 != secondsAgo) { return 0; } // Convert to a millis()-relative timestamp: millis() - msAgo // This gives a value that, when passed to Throttle::isWithinTimespanMs(value, interval), // correctly reports whether the transmit was within interval ms. return millis() - msAgo; } bool TransmitHistory::saveToDisk() { if (!dirty) { return true; } spiLock->lock(); FSCom.mkdir("/prefs"); // Remove old file first if (FSCom.exists(FILENAME)) { FSCom.remove(FILENAME); } auto file = FSCom.open(FILENAME, FILE_O_WRITE); if (file) { FileHeader header{}; header.magic = MAGIC; header.version = VERSION; header.count = (uint8_t)min((size_t)MAX_ENTRIES, history.size()); file.write((uint8_t *)&header, sizeof(header)); uint8_t written = 0; for (const auto &[key, epochSeconds] : history) { if (written >= MAX_ENTRIES) break; Entry entry{}; entry.key = key; entry.epochSeconds = epochSeconds; file.write((uint8_t *)&entry, sizeof(entry)); written++; } file.flush(); file.close(); LOG_DEBUG("TransmitHistory: saved %u entries to disk", written); dirty = false; spiLock->unlock(); return true; } else { LOG_WARN("TransmitHistory: failed to open file for writing"); } spiLock->unlock(); return false; } #else // No filesystem available — provide stub with in-memory tracking TransmitHistory *transmitHistory = nullptr; TransmitHistory *TransmitHistory::getInstance() { if (!transmitHistory) { transmitHistory = new TransmitHistory(); } return transmitHistory; } void TransmitHistory::loadFromDisk() {} void TransmitHistory::setLastSentToMesh(uint16_t key) { lastMillis[key] = millis(); } uint32_t TransmitHistory::getLastSentToMeshEpoch(uint16_t key) const { return 0; } uint32_t TransmitHistory::getLastSentToMeshMillis(uint16_t key) const { auto mit = lastMillis.find(key); return (mit != lastMillis.end()) ? mit->second : 0; } bool TransmitHistory::saveToDisk() { return true; } #endif ================================================ FILE: src/mesh/TransmitHistory.h ================================================ #pragma once #include "configuration.h" #include #include /** * TransmitHistory persists the last broadcast transmit time (epoch seconds) per portnum * to the filesystem so that throttle checks survive reboots/crashes. * * On boot, modules call getLastSentToMeshMillis() to recover a millis()-relative timestamp * from the stored epoch time, which plugs directly into existing throttle logic. * * On every broadcast transmit, modules call setLastSentToMesh() which updates the * in-memory cache and flushes to disk. * * Keys are meshtastic_PortNum values (one entry per portnum). */ #include "mesh/generated/meshtastic/portnums.pb.h" class TransmitHistory { public: static TransmitHistory *getInstance(); /** * Load persisted transmit times from disk. Call once during init after filesystem is ready. */ void loadFromDisk(); /** * Record that a broadcast was sent for the given key right now. * Stores epoch seconds and flushes to disk. */ void setLastSentToMesh(uint16_t key); /** * Get the last transmit epoch seconds for a given key, or 0 if unknown. */ uint32_t getLastSentToMeshEpoch(uint16_t key) const; /** * Convert a stored epoch timestamp into a millis()-relative timestamp suitable * for use with Throttle::isWithinTimespanMs(). * * Returns 0 if no valid time is stored or if the stored time is in the future * (which shouldn't happen but guards against clock weirdness). * * Example: if the stored epoch is 300 seconds ago, and millis() is currently 10000, * this returns 10000 - 300000 (wrapped appropriately for uint32_t arithmetic). */ uint32_t getLastSentToMeshMillis(uint16_t key) const; /** * Flush dirty entries to disk. Called periodically or on demand. * * @return true if the data is persisted (or there was nothing to write), false on write/open failure. */ bool saveToDisk(); private: TransmitHistory() = default; static constexpr const char *FILENAME = "/prefs/transmit_history.dat"; static constexpr uint32_t MAGIC = 0x54485354; // "THST" static constexpr uint8_t VERSION = 1; static constexpr uint8_t MAX_ENTRIES = 16; static constexpr uint32_t SAVE_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes struct __attribute__((packed)) Entry { uint16_t key; uint32_t epochSeconds; }; struct __attribute__((packed)) FileHeader { uint32_t magic; uint8_t version; uint8_t count; }; std::map history; // key -> epoch seconds (for disk persistence) std::map lastMillis; // key -> millis() value (for runtime throttle) bool dirty = false; uint32_t lastDiskSave = 0; // millis() of last disk flush }; extern TransmitHistory *transmitHistory; ================================================ FILE: src/mesh/TypeConversions.cpp ================================================ #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; info.is_ignored = lite->is_ignored; info.is_key_manually_verified = lite->bitfield & NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK; info.is_muted = lite->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK; 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; lite.has_is_unmessagable = user.has_is_unmessagable; lite.is_unmessagable = user.is_unmessagable; return lite; } meshtastic_User TypeConversions::ConvertToUser(uint32_t nodeNum, meshtastic_UserLite lite) { meshtastic_User user = meshtastic_User_init_default; snprintf(user.id, sizeof(user.id), "!%08x", nodeNum); strncpy(user.long_name, lite.long_name, sizeof(user.long_name)); strncpy(user.short_name, lite.short_name, sizeof(user.short_name)); user.hw_model = lite.hw_model; user.role = lite.role; user.is_licensed = lite.is_licensed; memcpy(user.macaddr, lite.macaddr, sizeof(user.macaddr)); memcpy(user.public_key.bytes, lite.public_key.bytes, sizeof(user.public_key.bytes)); user.public_key.size = lite.public_key.size; user.has_is_unmessagable = lite.has_is_unmessagable; user.is_unmessagable = lite.is_unmessagable; return user; } ================================================ FILE: src/mesh/TypeConversions.h ================================================ #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); }; ================================================ FILE: src/mesh/TypedQueue.h ================================================ #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; int maxElements; public: explicit TypedQueue(int _maxElements) : maxElements(_maxElements) {} int numFree() { if (maxElements <= 0) return 1; // Always claim 1 free, because we can grow to any size return maxElements - numUsed(); } bool isEmpty() { return q.empty(); } int numUsed() { return q.size(); } bool enqueue(T x, TickType_t maxWait = portMAX_DELAY) { if (numFree() <= 0) return false; if (reader) { reader->setInterval(0); concurrency::mainDelay.interrupt(); } 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 ================================================ FILE: src/mesh/aes-ccm.cpp ================================================ /* * 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 /** * Constant-time comparison of two byte arrays * * @param a First byte array to compare * @param b Second byte array to compare * @param len Number of bytes to compare * @return 0 if arrays are equal, -1 if different or if inputs are invalid */ static int constant_time_compare(const void *a_, const void *b_, size_t len) { /* Cast to volatile to prevent the compiler from optimizing out their comparison. */ const volatile uint8_t *volatile a = (const volatile uint8_t *volatile)a_; const volatile uint8_t *volatile b = (const volatile uint8_t *volatile)b_; if (len == 0) return 0; if (a == NULL || b == NULL) return -1; size_t i; volatile uint8_t d = 0U; for (i = 0U; i < len; i++) { d |= (a[i] ^ b[i]); } /* Constant time bit arithmetic to convert d > 0 to -1 and d = 0 to 0. */ return (1 & (((unsigned int)d - 1) >> 8)) - 1; } static void WPA_PUT_BE16(uint8_t *a, uint16_t val) { a[0] = val >> 8; a[1] = val & 0xff; } static void xor_aes_block(uint8_t *dst, const uint8_t *src) { for (uint8_t i = 0; i < AES_BLOCK_SIZE; i++) { dst[i] ^= src[i]; } } static void aes_ccm_auth_start(size_t M, size_t L, const uint8_t *nonce, const uint8_t *aad, size_t aad_len, size_t plain_len, uint8_t *x) { uint8_t aad_buf[2 * AES_BLOCK_SIZE]; uint8_t b[AES_BLOCK_SIZE]; /* Authentication */ /* B_0: Flags | Nonce N | l(m) */ b[0] = aad_len ? 0x40 : 0 /* Adata */; b[0] |= (((M - 2) / 2) /* M' */ << 3); b[0] |= (L - 1) /* L' */; memcpy(&b[1], nonce, 15 - L); WPA_PUT_BE16(&b[AES_BLOCK_SIZE - L], plain_len); crypto->aesEncrypt(b, x); /* X_1 = E(K, B_0) */ if (!aad_len) return; WPA_PUT_BE16(aad_buf, aad_len); memcpy(aad_buf + 2, aad, aad_len); memset(aad_buf + 2 + aad_len, 0, sizeof(aad_buf) - 2 - aad_len); xor_aes_block(aad_buf, x); crypto->aesEncrypt(aad_buf, x); /* X_2 = E(K, X_1 XOR B_1) */ if (aad_len > AES_BLOCK_SIZE - 2) { xor_aes_block(&aad_buf[AES_BLOCK_SIZE], x); /* X_3 = E(K, X_2 XOR B_2) */ crypto->aesEncrypt(&aad_buf[AES_BLOCK_SIZE], x); } } static void aes_ccm_auth(const uint8_t *data, size_t len, uint8_t *x) { size_t last = len % AES_BLOCK_SIZE; size_t i; for (i = 0; i < len / AES_BLOCK_SIZE; i++) { /* X_i+1 = E(K, X_i XOR B_i) */ xor_aes_block(x, data); data += AES_BLOCK_SIZE; crypto->aesEncrypt(x, x); } if (last) { /* XOR zero-padded last block */ for (i = 0; i < last; i++) x[i] ^= *data++; crypto->aesEncrypt(x, x); } } static void aes_ccm_encr_start(size_t L, const uint8_t *nonce, uint8_t *a) { /* A_i = Flags | Nonce N | Counter i */ a[0] = L - 1; /* Flags = L' */ memcpy(&a[1], nonce, 15 - L); } static void aes_ccm_encr(size_t L, const uint8_t *in, size_t len, uint8_t *out, uint8_t *a) { size_t last = len % AES_BLOCK_SIZE; size_t i; /* crypt = msg XOR (S_1 | S_2 | ... | S_n) */ for (i = 1; i <= len / AES_BLOCK_SIZE; i++) { WPA_PUT_BE16(&a[AES_BLOCK_SIZE - 2], i); /* S_i = E(K, A_i) */ crypto->aesEncrypt(a, out); xor_aes_block(out, in); out += AES_BLOCK_SIZE; in += AES_BLOCK_SIZE; } if (last) { WPA_PUT_BE16(&a[AES_BLOCK_SIZE - 2], i); crypto->aesEncrypt(a, out); /* XOR zero-padded last block */ for (i = 0; i < last; i++) *out++ ^= *in++; } } static void aes_ccm_encr_auth(size_t M, const uint8_t *x, uint8_t *a, uint8_t *auth) { size_t i; uint8_t tmp[AES_BLOCK_SIZE]; /* U = T XOR S_0; S_0 = E(K, A_0) */ WPA_PUT_BE16(&a[AES_BLOCK_SIZE - 2], 0); crypto->aesEncrypt(a, tmp); for (i = 0; i < M; i++) auth[i] = x[i] ^ tmp[i]; } static void aes_ccm_decr_auth(size_t M, uint8_t *a, const uint8_t *auth, uint8_t *t) { size_t i; uint8_t tmp[AES_BLOCK_SIZE]; /* U = T XOR S_0; S_0 = E(K, A_0) */ WPA_PUT_BE16(&a[AES_BLOCK_SIZE - 2], 0); crypto->aesEncrypt(a, tmp); for (i = 0; i < M; i++) t[i] = auth[i] ^ tmp[i]; } /* AES-CCM with fixed L=2 and aad_len <= 30 assumption */ int aes_ccm_ae(const uint8_t *key, size_t key_len, const uint8_t *nonce, size_t M, const uint8_t *plain, size_t plain_len, const uint8_t *aad, size_t aad_len, uint8_t *crypt, uint8_t *auth) { const size_t L = 2; uint8_t x[AES_BLOCK_SIZE], a[AES_BLOCK_SIZE]; if (aad_len > 30 || M > AES_BLOCK_SIZE) return -1; crypto->aesSetKey(key, key_len); aes_ccm_auth_start(M, L, nonce, aad, aad_len, plain_len, x); aes_ccm_auth(plain, plain_len, x); /* Encryption */ aes_ccm_encr_start(L, nonce, a); aes_ccm_encr(L, plain, plain_len, crypt, a); aes_ccm_encr_auth(M, x, a, auth); return 0; } /* AES-CCM with fixed L=2 and aad_len <= 30 assumption */ bool aes_ccm_ad(const uint8_t *key, size_t key_len, const uint8_t *nonce, size_t M, const uint8_t *crypt, size_t crypt_len, const uint8_t *aad, size_t aad_len, const uint8_t *auth, uint8_t *plain) { const size_t L = 2; uint8_t x[AES_BLOCK_SIZE], a[AES_BLOCK_SIZE]; uint8_t t[AES_BLOCK_SIZE]; if (aad_len > 30 || M > AES_BLOCK_SIZE) return false; crypto->aesSetKey(key, key_len); /* Decryption */ aes_ccm_encr_start(L, nonce, a); aes_ccm_decr_auth(M, a, auth, t); /* plaintext = msg XOR (S_1 | S_2 | ... | S_n) */ aes_ccm_encr(L, crypt, crypt_len, plain, a); aes_ccm_auth_start(M, L, nonce, aad, aad_len, crypt_len, x); aes_ccm_auth(plain, crypt_len, x); if (constant_time_compare(x, t, M) != 0) { return false; } return true; } #endif ================================================ FILE: src/mesh/aes-ccm.h ================================================ #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 ================================================ FILE: src/mesh/api/PacketAPI.cpp ================================================ #ifdef USE_PACKET_API #include "api/PacketAPI.h" #include "MeshService.h" #include "PowerFSM.h" #include "RadioInterface.h" #include "modules/NodeInfoModule.h" PacketAPI *packetAPI = nullptr; PacketAPI *PacketAPI::create(PacketServer *_server) { if (!packetAPI) { packetAPI = new PacketAPI(_server); } return packetAPI; } PacketAPI::PacketAPI(PacketServer *_server) : concurrency::OSThread("PacketAPI"), isConnected(false), programmingMode(false), server(_server) { api_type = TYPE_PACKET; } int32_t PacketAPI::runOnce() { bool success = false; #ifndef ARCH_PORTDUINO if (config.bluetooth.enabled) { if (!programmingMode) { // in programmingMode we don't send any packets to the client except this one notify programmingMode = true; success = notifyProgrammingMode(); } } else #endif { success = sendPacket(); } success |= receivePacket(); return success ? 10 : 50; } bool PacketAPI::receivePacket(void) { bool data_received = false; while (server->hasData()) { isConnected = true; data_received = true; powerFSM.trigger(EVENT_CONTACT_FROM_PHONE); lastContactMsec = millis(); meshtastic_ToRadio *mr; auto p = server->receivePacket()->move(); int id = p->getPacketId(); LOG_DEBUG("Received packet id=%u", id); mr = (meshtastic_ToRadio *)&static_cast *>(p.get())->getData(); switch (mr->which_payload_variant) { case meshtastic_ToRadio_packet_tag: { meshtastic_MeshPacket *mp = &mr->packet; mp->transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_API; printPacket("PACKET FROM QUEUE", mp); service->handleToRadio(*mp); break; } case meshtastic_ToRadio_want_config_id_tag: { uint32_t config_nonce = mr->want_config_id; LOG_INFO("Screen wants config, nonce=%u", config_nonce); handleStartConfig(); break; } case meshtastic_ToRadio_heartbeat_tag: if (mr->heartbeat.nonce == 1) { if (nodeInfoModule) { LOG_INFO("Broadcasting nodeinfo ping"); nodeInfoModule->sendOurNodeInfo(NODENUM_BROADCAST, true, 0, true); } } else { LOG_DEBUG("Got client heartbeat"); } break; default: LOG_ERROR("Error: unhandled meshtastic_ToRadio variant: %d", mr->which_payload_variant); break; } } return data_received; } bool PacketAPI::sendPacket(void) { if (server->available()) { // fill dummy buffer; we don't use it, we directly send the fromRadio structure uint32_t len = getFromRadio(txBuf); if (len != 0) { static uint32_t id = 0; fromRadioScratch.id = ++id; bool result = server->sendPacket(DataPacket(id, fromRadioScratch)); if (!result) { LOG_ERROR("send queue full"); } return result; } } return false; } bool PacketAPI::notifyProgrammingMode(void) { // tell the client we are in programming mode by sending only the bluetooth config state LOG_INFO("force client into programmingMode"); memset(&fromRadioScratch, 0, sizeof(fromRadioScratch)); fromRadioScratch.id = nodeDB->getNodeNum(); fromRadioScratch.which_payload_variant = meshtastic_FromRadio_config_tag; fromRadioScratch.config.which_payload_variant = meshtastic_Config_bluetooth_tag; fromRadioScratch.config.payload_variant.bluetooth = config.bluetooth; return server->sendPacket(DataPacket(0, fromRadioScratch)); } /** * return true if we got (once!) contact from our client and the server send queue is not full */ bool PacketAPI::checkIsConnected() { isConnected |= server->hasData(); return isConnected && server->available(); } #endif ================================================ FILE: src/mesh/api/PacketAPI.h ================================================ #pragma once #include "PhoneAPI.h" #include "comms/PacketServer.h" #include "concurrency/OSThread.h" /** * A version of the phone API used for inter task communication based on protobuf packets, e.g. * between two tasks running on CPU0 and CPU1, respectively. * */ class PacketAPI : public PhoneAPI, public concurrency::OSThread { public: static PacketAPI *create(PacketServer *_server); virtual ~PacketAPI(){}; virtual int32_t runOnce(); // Check the current underlying physical queue to see if the client is fetching packets bool checkIsConnected() override; protected: explicit PacketAPI(PacketServer *_server); void onNowHasData(uint32_t fromRadioNum) override {} void onConnectionChanged(bool connected) override {} private: bool receivePacket(void); bool sendPacket(void); bool notifyProgrammingMode(void); bool isConnected; bool programmingMode; PacketServer *server; uint8_t txBuf[MAX_TO_FROM_RADIO_SIZE] = {0}; // dummy buf to obey PhoneAPI }; extern PacketAPI *packetAPI; ================================================ FILE: src/mesh/api/ServerAPI.cpp ================================================ #include "ServerAPI.h" #include "Throttle.h" #include "configuration.h" #include static constexpr uint32_t TCP_IDLE_TIMEOUT_MS = 15 * 60 * 1000UL; 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()) { if (lastContactMsec > 0 && !Throttle::isWithinTimespanMs(lastContactMsec, TCP_IDLE_TIMEOUT_MS)) { LOG_WARN("TCP connection timeout, no data for %lu ms", (unsigned long)(millis() - lastContactMsec)); close(); enabled = false; return 0; } return StreamAPI::runOncePart(); } else { LOG_INFO("Client dropped connection, suspend API service"); close(); 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() { // Clean up previous connection if its client already disconnected if (openAPI && !openAPI->checkIsConnected()) { openAPI.reset(); } #ifdef ARCH_ESP32 #if ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(3, 0, 0) auto client = U::accept(); #else auto client = U::available(); #endif #elif defined(ARCH_RP2040) || defined(ARCH_NRF52) auto client = U::accept(); #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, try again in %dms", waitTime); return waitTime; } #endif LOG_INFO("Force close previous TCP connection"); openAPI.reset(); } openAPI.reset(new T(client)); } #if RAK_4631 waitTime = 100; #endif return 100; // only check occasionally for incoming connections } ================================================ FILE: src/mesh/api/ServerAPI.h ================================================ #pragma once #include "StreamAPI.h" #include #define SERVER_API_DEFAULT_PORT 4403 /** * 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(); /// Check the current underlying physical link to see if the client is currently connected virtual bool checkIsConnected() override; protected: /// We override this method to prevent publishing EVENT_SERIAL_CONNECTED/DISCONNECTED for wifi links (we want the board to /// stay in the POWERED state to prevent disabling wifi) virtual void onConnectionChanged(bool connected) override {} virtual int32_t runOnce() override; // Check for dropped client connections }; /** * 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. */ std::unique_ptr openAPI; #if defined(RAK_4631) || defined(RAK11310) // Track wait time for RAK13800 Ethernet requests int32_t waitTime = 100; #endif public: explicit APIServerPort(int port); void init(); protected: int32_t runOnce() override; }; ================================================ FILE: src/mesh/api/WiFiServerAPI.cpp ================================================ #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 listen on TCP port %d", port); apiPort->init(); } } void deInitApiServer() { if (apiPort) { delete apiPort; apiPort = nullptr; } } WiFiServerAPI::WiFiServerAPI(WiFiClient &_client) : ServerAPI(_client) { api_type = TYPE_WIFI; LOG_INFO("Incoming wifi connection"); } WiFiServerPort::WiFiServerPort(int port) : APIServerPort(port) {} #endif ================================================ FILE: src/mesh/api/WiFiServerAPI.h ================================================ #pragma once #include "ServerAPI.h" #include #if HAS_ETHERNET && defined(USE_WS5500) #include #define ETH ETH2 #endif // HAS_ETHERNET /** * 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 = SERVER_API_DEFAULT_PORT); void deInitApiServer(); ================================================ FILE: src/mesh/api/ethServerAPI.cpp ================================================ #include "configuration.h" #include #if HAS_ETHERNET && !defined(USE_WS5500) #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(); } } void deInitApiServer() { if (apiPort) { LOG_INFO("Deinit API server"); delete apiPort; apiPort = nullptr; } } ethServerAPI::ethServerAPI(EthernetClient &_client) : ServerAPI(_client) { LOG_INFO("Incoming ethernet connection"); api_type = TYPE_ETH; } ethServerPort::ethServerPort(int port) : APIServerPort(port) {} #endif ================================================ FILE: src/mesh/api/ethServerAPI.h ================================================ #pragma once #include "ServerAPI.h" #ifndef USE_WS5500 #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 = SERVER_API_DEFAULT_PORT); void deInitApiServer(); #endif ================================================ FILE: src/mesh/compression/unishox2.cpp ================================================ /* * 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); } ================================================ FILE: src/mesh/compression/unishox2.h ================================================ /* * 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 occurring sequences. See USX_FREQ_SEQ_* macros for samples * @param[in] usx_templates Templates of frequently occurring patterns. See USX_TEMPLATES macro. */ extern int unishox2_compress(const char *in, int len, UNISHOX_API_OUT_AND_LEN(char *out, int olen), const unsigned char usx_hcodes[], const unsigned char usx_hcode_lens[], const char *usx_freq_seq[], const char *usx_templates[]); /** * 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 occurring sequences. See USX_FREQ_SEQ_* macros for samples * @param[in] usx_templates Templates of frequently occurring patterns. See USX_TEMPLATES macro. */ extern int unishox2_decompress(const char *in, int len, UNISHOX_API_OUT_AND_LEN(char *out, int olen), const unsigned char usx_hcodes[], const unsigned char usx_hcode_lens[], const char *usx_freq_seq[], const char *usx_templates[]); /** * 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 ================================================ FILE: src/mesh/eth/ethClient.cpp ================================================ #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" #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; meshtastic::Syslog syslog(syslogClient); bool ethStartupComplete = 0; using namespace concurrency; static Periodic *ethEvent; static int32_t reconnectETH() { if (config.network.eth_enabled) { // Detect W5100S chip reset by verifying the MAC address register. // PoE power instability can brownout the W5100S while the MCU keeps running, // causing all chip registers (MAC, IP, sockets) to revert to defaults. uint8_t currentMac[6]; Ethernet.MACAddress(currentMac); uint8_t expectedMac[6]; getMacAddr(expectedMac); expectedMac[0] &= 0xfe; if (memcmp(currentMac, expectedMac, 6) != 0) { LOG_WARN("W5100S MAC mismatch (chip reset detected), reinitializing Ethernet"); syslog.disable(); #if !MESHTASTIC_EXCLUDE_SOCKETAPI deInitApiServer(); #endif #if HAS_UDP_MULTICAST if (udpHandler) { udpHandler->stop(); } #endif ethStartupComplete = false; #ifndef DISABLE_NTP ntp_renew = 0; #endif #ifdef PIN_ETHERNET_RESET pinMode(PIN_ETHERNET_RESET, OUTPUT); digitalWrite(PIN_ETHERNET_RESET, LOW); delay(100); digitalWrite(PIN_ETHERNET_RESET, HIGH); delay(100); #endif #ifdef RAK11310 ETH_SPI_PORT.setSCK(PIN_SPI0_SCK); ETH_SPI_PORT.setTX(PIN_SPI0_MOSI); ETH_SPI_PORT.setRX(PIN_SPI0_MISO); ETH_SPI_PORT.begin(); #endif Ethernet.init(ETH_SPI_PORT, PIN_ETHERNET_SS); int status = 0; if (config.network.address_mode == meshtastic_Config_NetworkConfig_AddressMode_DHCP) { status = Ethernet.begin(expectedMac); } else if (config.network.address_mode == meshtastic_Config_NetworkConfig_AddressMode_STATIC) { Ethernet.begin(expectedMac, config.network.ipv4_config.ip, config.network.ipv4_config.dns, config.network.ipv4_config.gateway, config.network.ipv4_config.subnet); status = 1; } if (status == 0) { LOG_ERROR("Ethernet re-initialization failed, will retry"); return 5000; } LOG_INFO("Ethernet reinitialized - IP %u.%u.%u.%u", Ethernet.localIP()[0], Ethernet.localIP()[1], Ethernet.localIP()[2], Ethernet.localIP()[3]); } Ethernet.maintain(); if (!ethStartupComplete) { // Start web server LOG_INFO("Start Ethernet network services"); #ifndef DISABLE_NTP LOG_INFO("Start NTP time client"); timeClient.begin(); timeClient.setUpdateInterval(60 * 60); // Update once an hour #endif if (config.network.rsyslog_server[0]) { LOG_INFO("Start Syslog client"); // Defaults int serverPort = 514; const char *serverAddr = config.network.rsyslog_server; String server = String(serverAddr); int delimIndex = server.indexOf(':'); if (delimIndex > 0) { String port = server.substring(delimIndex + 1, server.length()); server[delimIndex] = 0; serverPort = port.toInt(); serverAddr = server.c_str(); } syslog.server(serverAddr, serverPort); syslog.deviceHostname(getDeviceName()); syslog.appName("Meshtastic"); syslog.defaultPriority(LOGLEVEL_USER); syslog.enable(); } #if !MESHTASTIC_EXCLUDE_SOCKETAPI if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { initApiServer(); } #endif #if HAS_UDP_MULTICAST if (udpHandler && config.network.enabled_protocols & meshtastic_Config_NetworkConfig_ProtocolFlags_UDP_BROADCAST) { udpHandler->start(); } #endif ethStartupComplete = true; } } #ifndef DISABLE_NTP if (isEthernetAvailable() && (ntp_renew < millis())) { LOG_INFO("Update NTP time from %s", config.network.ntp_server); if (timeClient.update()) { LOG_DEBUG("NTP Request Success - Set RTCQualityNTP if needed"); 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_ETH_POWER_EN pinMode(PIN_ETH_POWER_EN, OUTPUT); digitalWrite(PIN_ETH_POWER_EN, HIGH); // Power up. delay(100); #endif #ifdef PIN_ETHERNET_RESET pinMode(PIN_ETHERNET_RESET, OUTPUT); digitalWrite(PIN_ETHERNET_RESET, LOW); // Reset Time. delay(100); digitalWrite(PIN_ETHERNET_RESET, HIGH); // Reset Time. #endif #ifdef RAK11310 // Initialize the SPI port ETH_SPI_PORT.setSCK(PIN_SPI0_SCK); ETH_SPI_PORT.setTX(PIN_SPI0_MOSI); ETH_SPI_PORT.setRX(PIN_SPI0_MISO); ETH_SPI_PORT.begin(); #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("Start Ethernet DHCP"); status = Ethernet.begin(mac); } else if (config.network.address_mode == meshtastic_Config_NetworkConfig_AddressMode_STATIC) { LOG_INFO("Start Ethernet Static"); Ethernet.begin(mac, config.network.ipv4_config.ip, config.network.ipv4_config.dns, config.network.ipv4_config.gateway, config.network.ipv4_config.subnet); status = 1; } else { LOG_INFO("Ethernet Disabled"); return false; } if (status == 0) { if (Ethernet.hardwareStatus() == EthernetNoHardware) { LOG_ERROR("Ethernet shield was not found"); return false; } else if (Ethernet.linkStatus() == LinkOFF) { LOG_ERROR("Ethernet cable is not connected"); return false; } else { LOG_ERROR("Unknown Ethernet error"); return false; } } else { LOG_INFO("Local IP %u.%u.%u.%u", Ethernet.localIP()[0], Ethernet.localIP()[1], Ethernet.localIP()[2], Ethernet.localIP()[3]); LOG_INFO("Subnet Mask %u.%u.%u.%u", Ethernet.subnetMask()[0], Ethernet.subnetMask()[1], Ethernet.subnetMask()[2], Ethernet.subnetMask()[3]); LOG_INFO("Gateway IP %u.%u.%u.%u", Ethernet.gatewayIP()[0], Ethernet.gatewayIP()[1], Ethernet.gatewayIP()[2], Ethernet.gatewayIP()[3]); LOG_INFO("DNS Server IP %u.%u.%u.%u", Ethernet.dnsServerIP()[0], Ethernet.dnsServerIP()[1], Ethernet.dnsServerIP()[2], Ethernet.dnsServerIP()[3]); } ethEvent = new Periodic("ethConnect", reconnectETH); return true; } else { LOG_INFO("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 ================================================ FILE: src/mesh/eth/ethClient.h ================================================ #pragma once #include "configuration.h" #include bool initEthernet(); bool isEthernetAvailable(); ================================================ FILE: src/mesh/generated/.clang-format ================================================ DisableFormat: true SortIncludes: false ================================================ FILE: src/mesh/generated/meshtastic/admin.pb.cpp ================================================ /* Automatically generated nanopb constant definitions */ /* Generated by nanopb-0.4.9.1 */ #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_AdminMessage_InputEvent, meshtastic_AdminMessage_InputEvent, AUTO) PB_BIND(meshtastic_AdminMessage_OTAEvent, meshtastic_AdminMessage_OTAEvent, AUTO) PB_BIND(meshtastic_HamParameters, meshtastic_HamParameters, AUTO) PB_BIND(meshtastic_NodeRemoteHardwarePinsResponse, meshtastic_NodeRemoteHardwarePinsResponse, 2) PB_BIND(meshtastic_SharedContact, meshtastic_SharedContact, AUTO) PB_BIND(meshtastic_KeyVerificationAdmin, meshtastic_KeyVerificationAdmin, AUTO) PB_BIND(meshtastic_SensorConfig, meshtastic_SensorConfig, AUTO) PB_BIND(meshtastic_SCD4X_config, meshtastic_SCD4X_config, AUTO) PB_BIND(meshtastic_SEN5X_config, meshtastic_SEN5X_config, AUTO) PB_BIND(meshtastic_SCD30_config, meshtastic_SCD30_config, AUTO) PB_BIND(meshtastic_SHTXX_config, meshtastic_SHTXX_config, AUTO) ================================================ FILE: src/mesh/generated/meshtastic/admin.pb.h ================================================ /* Automatically generated nanopb header */ /* Generated by nanopb-0.4.9.1 */ #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/device_ui.pb.h" #include "meshtastic/mesh.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 /* Enum definitions */ /* Firmware update mode for OTA updates */ typedef enum _meshtastic_OTAMode { /* Do not reboot into OTA mode */ meshtastic_OTAMode_NO_REBOOT_OTA = 0, /* Reboot into OTA mode for BLE firmware update */ meshtastic_OTAMode_OTA_BLE = 1, /* Reboot into OTA mode for WiFi firmware update */ meshtastic_OTAMode_OTA_WIFI = 2 } meshtastic_OTAMode; /* 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, /* Session key config */ 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, /* TODO: REPLACE */ meshtastic_AdminMessage_ModuleConfigType_STATUSMESSAGE_CONFIG = 13, /* Traffic management module config */ meshtastic_AdminMessage_ModuleConfigType_TRAFFICMANAGEMENT_CONFIG = 14, /* TAK module config */ meshtastic_AdminMessage_ModuleConfigType_TAK_CONFIG = 15 } meshtastic_AdminMessage_ModuleConfigType; typedef enum _meshtastic_AdminMessage_BackupLocation { /* Backup to the internal flash */ meshtastic_AdminMessage_BackupLocation_FLASH = 0, /* Backup to the SD card */ meshtastic_AdminMessage_BackupLocation_SD = 1 } meshtastic_AdminMessage_BackupLocation; /* Three stages of this request. */ typedef enum _meshtastic_KeyVerificationAdmin_MessageType { /* This is the first stage, where a client initiates */ meshtastic_KeyVerificationAdmin_MessageType_INITIATE_VERIFICATION = 0, /* After the nonce has been returned over the mesh, the client prompts for the security number And uses this message to provide it to the node. */ meshtastic_KeyVerificationAdmin_MessageType_PROVIDE_SECURITY_NUMBER = 1, /* Once the user has compared the verification message, this message notifies the node. */ meshtastic_KeyVerificationAdmin_MessageType_DO_VERIFY = 2, /* This is the cancel path, can be taken at any point */ meshtastic_KeyVerificationAdmin_MessageType_DO_NOT_VERIFY = 3 } meshtastic_KeyVerificationAdmin_MessageType; /* Struct definitions */ /* Input event message to be sent to the node. */ typedef struct _meshtastic_AdminMessage_InputEvent { /* The input event code */ uint8_t event_code; /* Keyboard character code */ uint8_t kb_char; /* The touch X coordinate */ uint16_t touch_x; /* The touch Y coordinate */ uint16_t touch_y; } meshtastic_AdminMessage_InputEvent; typedef PB_BYTES_ARRAY_T(32) meshtastic_AdminMessage_OTAEvent_ota_hash_t; /* User is requesting an over the air update. Node will reboot into the OTA loader */ typedef struct _meshtastic_AdminMessage_OTAEvent { /* Tell the node to reboot into OTA mode for firmware update via BLE or WiFi (ESP32 only for now) */ meshtastic_OTAMode reboot_ota_mode; /* A 32 byte hash of the OTA firmware. Used to verify the integrity of the firmware before applying an update. */ meshtastic_AdminMessage_OTAEvent_ota_hash_t ota_hash; } meshtastic_AdminMessage_OTAEvent; /* 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 struct _meshtastic_SharedContact { /* The node number of the contact */ uint32_t node_num; /* The User of the contact */ bool has_user; meshtastic_User user; /* Add this contact to the blocked / ignored list */ bool should_ignore; /* Set the IS_KEY_MANUALLY_VERIFIED bit */ bool manually_verified; } meshtastic_SharedContact; /* This message is used by a client to initiate or complete a key verification */ typedef struct _meshtastic_KeyVerificationAdmin { meshtastic_KeyVerificationAdmin_MessageType message_type; /* The nodenum we're requesting */ uint32_t remote_nodenum; /* The nonce is used to track the connection */ uint64_t nonce; /* The 4 digit code generated by the remote node, and communicated outside the mesh */ bool has_security_number; uint32_t security_number; } meshtastic_KeyVerificationAdmin; typedef struct _meshtastic_SCD4X_config { /* Set Automatic self-calibration enabled */ bool has_set_asc; bool set_asc; /* Recalibration target CO2 concentration in ppm (FRC or ASC) */ bool has_set_target_co2_conc; uint32_t set_target_co2_conc; /* Reference temperature in degC */ bool has_set_temperature; float set_temperature; /* Altitude of sensor in meters above sea level. 0 - 3000m (overrides ambient pressure) */ bool has_set_altitude; uint32_t set_altitude; /* Sensor ambient pressure in Pa. 70000 - 120000 Pa (overrides altitude) */ bool has_set_ambient_pressure; uint32_t set_ambient_pressure; /* Perform a factory reset of the sensor */ bool has_factory_reset; bool factory_reset; /* Power mode for sensor (true for low power, false for normal) */ bool has_set_power_mode; bool set_power_mode; } meshtastic_SCD4X_config; typedef struct _meshtastic_SEN5X_config { /* Reference temperature in degC */ bool has_set_temperature; float set_temperature; /* One-shot mode (true for low power - one-shot mode, false for normal - continuous mode) */ bool has_set_one_shot_mode; bool set_one_shot_mode; } meshtastic_SEN5X_config; typedef struct _meshtastic_SCD30_config { /* Set Automatic self-calibration enabled */ bool has_set_asc; bool set_asc; /* Recalibration target CO2 concentration in ppm (FRC or ASC) */ bool has_set_target_co2_conc; uint32_t set_target_co2_conc; /* Reference temperature in degC */ bool has_set_temperature; float set_temperature; /* Altitude of sensor in meters above sea level. 0 - 3000m (overrides ambient pressure) */ bool has_set_altitude; uint32_t set_altitude; /* Power mode for sensor (true for low power, false for normal) */ bool has_set_measurement_interval; uint32_t set_measurement_interval; /* Perform a factory reset of the sensor */ bool has_soft_reset; bool soft_reset; } meshtastic_SCD30_config; typedef struct _meshtastic_SHTXX_config { /* Accuracy mode (0 = low, 1 = medium, 2 = high) */ bool has_set_accuracy; uint32_t set_accuracy; } meshtastic_SHTXX_config; typedef struct _meshtastic_SensorConfig { /* SCD4X CO2 Sensor configuration */ bool has_scd4x_config; meshtastic_SCD4X_config scd4x_config; /* SEN5X PM Sensor configuration */ bool has_sen5x_config; meshtastic_SEN5X_config sen5x_config; /* SCD30 CO2 Sensor configuration */ bool has_scd30_config; meshtastic_SCD30_config scd30_config; /* SHTXX temperature and relative humidity sensor configuration */ bool has_shtxx_config; meshtastic_SHTXX_config shtxx_config; } meshtastic_SensorConfig; 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; /* Backup the node's preferences */ meshtastic_AdminMessage_BackupLocation backup_preferences; /* Restore the node's preferences */ meshtastic_AdminMessage_BackupLocation restore_preferences; /* Remove backups of the node's preferences */ meshtastic_AdminMessage_BackupLocation remove_backup_preferences; /* Send an input event to the node. This is used to trigger physical input events like button presses, touch events, etc. */ meshtastic_AdminMessage_InputEvent send_input_event; /* 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; /* Set specified node-num to be ignored on the NodeDB on the device */ uint32_t set_ignored_node; /* Set specified node-num to be un-ignored on the NodeDB on the device */ uint32_t remove_ignored_node; /* Set specified node-num to be muted */ uint32_t toggle_muted_node; /* 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; /* Add a contact (User) to the nodedb */ meshtastic_SharedContact add_contact; /* Initiate or respond to a key verification request */ meshtastic_KeyVerificationAdmin key_verification; /* 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. Deprecated in favor of reboot_ota_mode in 2.7.17 */ 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. When true, favorites are preserved through reset. */ bool nodedb_reset; /* Tell the node to reset into the OTA Loader */ meshtastic_AdminMessage_OTAEvent ota_request; /* Parameters and sensor configuration */ meshtastic_SensorConfig sensor_config; }; /* 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_OTAMode_MIN meshtastic_OTAMode_NO_REBOOT_OTA #define _meshtastic_OTAMode_MAX meshtastic_OTAMode_OTA_WIFI #define _meshtastic_OTAMode_ARRAYSIZE ((meshtastic_OTAMode)(meshtastic_OTAMode_OTA_WIFI+1)) #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_TAK_CONFIG #define _meshtastic_AdminMessage_ModuleConfigType_ARRAYSIZE ((meshtastic_AdminMessage_ModuleConfigType)(meshtastic_AdminMessage_ModuleConfigType_TAK_CONFIG+1)) #define _meshtastic_AdminMessage_BackupLocation_MIN meshtastic_AdminMessage_BackupLocation_FLASH #define _meshtastic_AdminMessage_BackupLocation_MAX meshtastic_AdminMessage_BackupLocation_SD #define _meshtastic_AdminMessage_BackupLocation_ARRAYSIZE ((meshtastic_AdminMessage_BackupLocation)(meshtastic_AdminMessage_BackupLocation_SD+1)) #define _meshtastic_KeyVerificationAdmin_MessageType_MIN meshtastic_KeyVerificationAdmin_MessageType_INITIATE_VERIFICATION #define _meshtastic_KeyVerificationAdmin_MessageType_MAX meshtastic_KeyVerificationAdmin_MessageType_DO_NOT_VERIFY #define _meshtastic_KeyVerificationAdmin_MessageType_ARRAYSIZE ((meshtastic_KeyVerificationAdmin_MessageType)(meshtastic_KeyVerificationAdmin_MessageType_DO_NOT_VERIFY+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 #define meshtastic_AdminMessage_payload_variant_backup_preferences_ENUMTYPE meshtastic_AdminMessage_BackupLocation #define meshtastic_AdminMessage_payload_variant_restore_preferences_ENUMTYPE meshtastic_AdminMessage_BackupLocation #define meshtastic_AdminMessage_payload_variant_remove_backup_preferences_ENUMTYPE meshtastic_AdminMessage_BackupLocation #define meshtastic_AdminMessage_OTAEvent_reboot_ota_mode_ENUMTYPE meshtastic_OTAMode #define meshtastic_KeyVerificationAdmin_message_type_ENUMTYPE meshtastic_KeyVerificationAdmin_MessageType /* Initializer values for message structs */ #define meshtastic_AdminMessage_init_default {0, {0}, {0, {0}}} #define meshtastic_AdminMessage_InputEvent_init_default {0, 0, 0, 0} #define meshtastic_AdminMessage_OTAEvent_init_default {_meshtastic_OTAMode_MIN, {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_SharedContact_init_default {0, false, meshtastic_User_init_default, 0, 0} #define meshtastic_KeyVerificationAdmin_init_default {_meshtastic_KeyVerificationAdmin_MessageType_MIN, 0, 0, false, 0} #define meshtastic_SensorConfig_init_default {false, meshtastic_SCD4X_config_init_default, false, meshtastic_SEN5X_config_init_default, false, meshtastic_SCD30_config_init_default, false, meshtastic_SHTXX_config_init_default} #define meshtastic_SCD4X_config_init_default {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0} #define meshtastic_SEN5X_config_init_default {false, 0, false, 0} #define meshtastic_SCD30_config_init_default {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0} #define meshtastic_SHTXX_config_init_default {false, 0} #define meshtastic_AdminMessage_init_zero {0, {0}, {0, {0}}} #define meshtastic_AdminMessage_InputEvent_init_zero {0, 0, 0, 0} #define meshtastic_AdminMessage_OTAEvent_init_zero {_meshtastic_OTAMode_MIN, {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}} #define meshtastic_SharedContact_init_zero {0, false, meshtastic_User_init_zero, 0, 0} #define meshtastic_KeyVerificationAdmin_init_zero {_meshtastic_KeyVerificationAdmin_MessageType_MIN, 0, 0, false, 0} #define meshtastic_SensorConfig_init_zero {false, meshtastic_SCD4X_config_init_zero, false, meshtastic_SEN5X_config_init_zero, false, meshtastic_SCD30_config_init_zero, false, meshtastic_SHTXX_config_init_zero} #define meshtastic_SCD4X_config_init_zero {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0} #define meshtastic_SEN5X_config_init_zero {false, 0, false, 0} #define meshtastic_SCD30_config_init_zero {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0} #define meshtastic_SHTXX_config_init_zero {false, 0} /* Field tags (for use in manual encoding/decoding) */ #define meshtastic_AdminMessage_InputEvent_event_code_tag 1 #define meshtastic_AdminMessage_InputEvent_kb_char_tag 2 #define meshtastic_AdminMessage_InputEvent_touch_x_tag 3 #define meshtastic_AdminMessage_InputEvent_touch_y_tag 4 #define meshtastic_AdminMessage_OTAEvent_reboot_ota_mode_tag 1 #define meshtastic_AdminMessage_OTAEvent_ota_hash_tag 2 #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_SharedContact_node_num_tag 1 #define meshtastic_SharedContact_user_tag 2 #define meshtastic_SharedContact_should_ignore_tag 3 #define meshtastic_SharedContact_manually_verified_tag 4 #define meshtastic_KeyVerificationAdmin_message_type_tag 1 #define meshtastic_KeyVerificationAdmin_remote_nodenum_tag 2 #define meshtastic_KeyVerificationAdmin_nonce_tag 3 #define meshtastic_KeyVerificationAdmin_security_number_tag 4 #define meshtastic_SCD4X_config_set_asc_tag 1 #define meshtastic_SCD4X_config_set_target_co2_conc_tag 2 #define meshtastic_SCD4X_config_set_temperature_tag 3 #define meshtastic_SCD4X_config_set_altitude_tag 4 #define meshtastic_SCD4X_config_set_ambient_pressure_tag 5 #define meshtastic_SCD4X_config_factory_reset_tag 6 #define meshtastic_SCD4X_config_set_power_mode_tag 7 #define meshtastic_SEN5X_config_set_temperature_tag 1 #define meshtastic_SEN5X_config_set_one_shot_mode_tag 2 #define meshtastic_SCD30_config_set_asc_tag 1 #define meshtastic_SCD30_config_set_target_co2_conc_tag 2 #define meshtastic_SCD30_config_set_temperature_tag 3 #define meshtastic_SCD30_config_set_altitude_tag 4 #define meshtastic_SCD30_config_set_measurement_interval_tag 5 #define meshtastic_SCD30_config_soft_reset_tag 6 #define meshtastic_SHTXX_config_set_accuracy_tag 1 #define meshtastic_SensorConfig_scd4x_config_tag 1 #define meshtastic_SensorConfig_sen5x_config_tag 2 #define meshtastic_SensorConfig_scd30_config_tag 3 #define meshtastic_SensorConfig_shtxx_config_tag 4 #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_backup_preferences_tag 24 #define meshtastic_AdminMessage_restore_preferences_tag 25 #define meshtastic_AdminMessage_remove_backup_preferences_tag 26 #define meshtastic_AdminMessage_send_input_event_tag 27 #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_set_ignored_node_tag 47 #define meshtastic_AdminMessage_remove_ignored_node_tag 48 #define meshtastic_AdminMessage_toggle_muted_node_tag 49 #define meshtastic_AdminMessage_begin_edit_settings_tag 64 #define meshtastic_AdminMessage_commit_edit_settings_tag 65 #define meshtastic_AdminMessage_add_contact_tag 66 #define meshtastic_AdminMessage_key_verification_tag 67 #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_ota_request_tag 102 #define meshtastic_AdminMessage_sensor_config_tag 103 #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, UENUM, (payload_variant,backup_preferences,backup_preferences), 24) \ X(a, STATIC, ONEOF, UENUM, (payload_variant,restore_preferences,restore_preferences), 25) \ X(a, STATIC, ONEOF, UENUM, (payload_variant,remove_backup_preferences,remove_backup_preferences), 26) \ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,send_input_event,send_input_event), 27) \ 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, UINT32, (payload_variant,set_ignored_node,set_ignored_node), 47) \ X(a, STATIC, ONEOF, UINT32, (payload_variant,remove_ignored_node,remove_ignored_node), 48) \ X(a, STATIC, ONEOF, UINT32, (payload_variant,toggle_muted_node,toggle_muted_node), 49) \ 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, MESSAGE, (payload_variant,add_contact,add_contact), 66) \ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,key_verification,key_verification), 67) \ 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, BOOL, (payload_variant,nodedb_reset,nodedb_reset), 100) \ X(a, STATIC, SINGULAR, BYTES, session_passkey, 101) \ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,ota_request,ota_request), 102) \ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,sensor_config,sensor_config), 103) #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_send_input_event_MSGTYPE meshtastic_AdminMessage_InputEvent #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_AdminMessage_payload_variant_add_contact_MSGTYPE meshtastic_SharedContact #define meshtastic_AdminMessage_payload_variant_key_verification_MSGTYPE meshtastic_KeyVerificationAdmin #define meshtastic_AdminMessage_payload_variant_ota_request_MSGTYPE meshtastic_AdminMessage_OTAEvent #define meshtastic_AdminMessage_payload_variant_sensor_config_MSGTYPE meshtastic_SensorConfig #define meshtastic_AdminMessage_InputEvent_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, UINT32, event_code, 1) \ X(a, STATIC, SINGULAR, UINT32, kb_char, 2) \ X(a, STATIC, SINGULAR, UINT32, touch_x, 3) \ X(a, STATIC, SINGULAR, UINT32, touch_y, 4) #define meshtastic_AdminMessage_InputEvent_CALLBACK NULL #define meshtastic_AdminMessage_InputEvent_DEFAULT NULL #define meshtastic_AdminMessage_OTAEvent_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, UENUM, reboot_ota_mode, 1) \ X(a, STATIC, SINGULAR, BYTES, ota_hash, 2) #define meshtastic_AdminMessage_OTAEvent_CALLBACK NULL #define meshtastic_AdminMessage_OTAEvent_DEFAULT NULL #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 #define meshtastic_SharedContact_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, UINT32, node_num, 1) \ X(a, STATIC, OPTIONAL, MESSAGE, user, 2) \ X(a, STATIC, SINGULAR, BOOL, should_ignore, 3) \ X(a, STATIC, SINGULAR, BOOL, manually_verified, 4) #define meshtastic_SharedContact_CALLBACK NULL #define meshtastic_SharedContact_DEFAULT NULL #define meshtastic_SharedContact_user_MSGTYPE meshtastic_User #define meshtastic_KeyVerificationAdmin_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, UENUM, message_type, 1) \ X(a, STATIC, SINGULAR, UINT32, remote_nodenum, 2) \ X(a, STATIC, SINGULAR, UINT64, nonce, 3) \ X(a, STATIC, OPTIONAL, UINT32, security_number, 4) #define meshtastic_KeyVerificationAdmin_CALLBACK NULL #define meshtastic_KeyVerificationAdmin_DEFAULT NULL #define meshtastic_SensorConfig_FIELDLIST(X, a) \ X(a, STATIC, OPTIONAL, MESSAGE, scd4x_config, 1) \ X(a, STATIC, OPTIONAL, MESSAGE, sen5x_config, 2) \ X(a, STATIC, OPTIONAL, MESSAGE, scd30_config, 3) \ X(a, STATIC, OPTIONAL, MESSAGE, shtxx_config, 4) #define meshtastic_SensorConfig_CALLBACK NULL #define meshtastic_SensorConfig_DEFAULT NULL #define meshtastic_SensorConfig_scd4x_config_MSGTYPE meshtastic_SCD4X_config #define meshtastic_SensorConfig_sen5x_config_MSGTYPE meshtastic_SEN5X_config #define meshtastic_SensorConfig_scd30_config_MSGTYPE meshtastic_SCD30_config #define meshtastic_SensorConfig_shtxx_config_MSGTYPE meshtastic_SHTXX_config #define meshtastic_SCD4X_config_FIELDLIST(X, a) \ X(a, STATIC, OPTIONAL, BOOL, set_asc, 1) \ X(a, STATIC, OPTIONAL, UINT32, set_target_co2_conc, 2) \ X(a, STATIC, OPTIONAL, FLOAT, set_temperature, 3) \ X(a, STATIC, OPTIONAL, UINT32, set_altitude, 4) \ X(a, STATIC, OPTIONAL, UINT32, set_ambient_pressure, 5) \ X(a, STATIC, OPTIONAL, BOOL, factory_reset, 6) \ X(a, STATIC, OPTIONAL, BOOL, set_power_mode, 7) #define meshtastic_SCD4X_config_CALLBACK NULL #define meshtastic_SCD4X_config_DEFAULT NULL #define meshtastic_SEN5X_config_FIELDLIST(X, a) \ X(a, STATIC, OPTIONAL, FLOAT, set_temperature, 1) \ X(a, STATIC, OPTIONAL, BOOL, set_one_shot_mode, 2) #define meshtastic_SEN5X_config_CALLBACK NULL #define meshtastic_SEN5X_config_DEFAULT NULL #define meshtastic_SCD30_config_FIELDLIST(X, a) \ X(a, STATIC, OPTIONAL, BOOL, set_asc, 1) \ X(a, STATIC, OPTIONAL, UINT32, set_target_co2_conc, 2) \ X(a, STATIC, OPTIONAL, FLOAT, set_temperature, 3) \ X(a, STATIC, OPTIONAL, UINT32, set_altitude, 4) \ X(a, STATIC, OPTIONAL, UINT32, set_measurement_interval, 5) \ X(a, STATIC, OPTIONAL, BOOL, soft_reset, 6) #define meshtastic_SCD30_config_CALLBACK NULL #define meshtastic_SCD30_config_DEFAULT NULL #define meshtastic_SHTXX_config_FIELDLIST(X, a) \ X(a, STATIC, OPTIONAL, UINT32, set_accuracy, 1) #define meshtastic_SHTXX_config_CALLBACK NULL #define meshtastic_SHTXX_config_DEFAULT NULL extern const pb_msgdesc_t meshtastic_AdminMessage_msg; extern const pb_msgdesc_t meshtastic_AdminMessage_InputEvent_msg; extern const pb_msgdesc_t meshtastic_AdminMessage_OTAEvent_msg; extern const pb_msgdesc_t meshtastic_HamParameters_msg; extern const pb_msgdesc_t meshtastic_NodeRemoteHardwarePinsResponse_msg; extern const pb_msgdesc_t meshtastic_SharedContact_msg; extern const pb_msgdesc_t meshtastic_KeyVerificationAdmin_msg; extern const pb_msgdesc_t meshtastic_SensorConfig_msg; extern const pb_msgdesc_t meshtastic_SCD4X_config_msg; extern const pb_msgdesc_t meshtastic_SEN5X_config_msg; extern const pb_msgdesc_t meshtastic_SCD30_config_msg; extern const pb_msgdesc_t meshtastic_SHTXX_config_msg; /* Defines for backwards compatibility with code written before nanopb-0.4.0 */ #define meshtastic_AdminMessage_fields &meshtastic_AdminMessage_msg #define meshtastic_AdminMessage_InputEvent_fields &meshtastic_AdminMessage_InputEvent_msg #define meshtastic_AdminMessage_OTAEvent_fields &meshtastic_AdminMessage_OTAEvent_msg #define meshtastic_HamParameters_fields &meshtastic_HamParameters_msg #define meshtastic_NodeRemoteHardwarePinsResponse_fields &meshtastic_NodeRemoteHardwarePinsResponse_msg #define meshtastic_SharedContact_fields &meshtastic_SharedContact_msg #define meshtastic_KeyVerificationAdmin_fields &meshtastic_KeyVerificationAdmin_msg #define meshtastic_SensorConfig_fields &meshtastic_SensorConfig_msg #define meshtastic_SCD4X_config_fields &meshtastic_SCD4X_config_msg #define meshtastic_SEN5X_config_fields &meshtastic_SEN5X_config_msg #define meshtastic_SCD30_config_fields &meshtastic_SCD30_config_msg #define meshtastic_SHTXX_config_fields &meshtastic_SHTXX_config_msg /* Maximum encoded size of messages (where known) */ #define MESHTASTIC_MESHTASTIC_ADMIN_PB_H_MAX_SIZE meshtastic_AdminMessage_size #define meshtastic_AdminMessage_InputEvent_size 14 #define meshtastic_AdminMessage_OTAEvent_size 36 #define meshtastic_AdminMessage_size 511 #define meshtastic_HamParameters_size 31 #define meshtastic_KeyVerificationAdmin_size 25 #define meshtastic_NodeRemoteHardwarePinsResponse_size 496 #define meshtastic_SCD30_config_size 27 #define meshtastic_SCD4X_config_size 29 #define meshtastic_SEN5X_config_size 7 #define meshtastic_SHTXX_config_size 6 #define meshtastic_SensorConfig_size 77 #define meshtastic_SharedContact_size 127 #ifdef __cplusplus } /* extern "C" */ #endif #endif ================================================ FILE: src/mesh/generated/meshtastic/apponly.pb.cpp ================================================ /* Automatically generated nanopb constant definitions */ /* Generated by nanopb-0.4.9.1 */ #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) ================================================ FILE: src/mesh/generated/meshtastic/apponly.pb.h ================================================ /* Automatically generated nanopb header */ /* Generated by nanopb-0.4.9.1 */ #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 682 #ifdef __cplusplus } /* extern "C" */ #endif #endif ================================================ FILE: src/mesh/generated/meshtastic/atak.pb.cpp ================================================ /* Automatically generated nanopb constant definitions */ /* Generated by nanopb-0.4.9.1 */ #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) ================================================ FILE: src/mesh/generated/meshtastic/atak.pb.h ================================================ /* Automatically generated nanopb header */ /* Generated by nanopb-0.4.9.1 */ #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 ================================================ FILE: src/mesh/generated/meshtastic/cannedmessages.pb.cpp ================================================ /* Automatically generated nanopb constant definitions */ /* Generated by nanopb-0.4.9.1 */ #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) ================================================ FILE: src/mesh/generated/meshtastic/cannedmessages.pb.h ================================================ /* Automatically generated nanopb header */ /* Generated by nanopb-0.4.9.1 */ #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 ================================================ FILE: src/mesh/generated/meshtastic/channel.pb.cpp ================================================ /* Automatically generated nanopb constant definitions */ /* Generated by nanopb-0.4.9.1 */ #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) ================================================ FILE: src/mesh/generated/meshtastic/channel.pb.h ================================================ /* Automatically generated nanopb header */ /* Generated by nanopb-0.4.9.1 */ #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 client / device should mute the current channel Useful for noisy public channels you don't necessarily want to disable */ bool is_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_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_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 ================================================ FILE: src/mesh/generated/meshtastic/clientonly.pb.cpp ================================================ /* Automatically generated nanopb constant definitions */ /* Generated by nanopb-0.4.9.1 */ #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) ================================================ FILE: src/mesh/generated/meshtastic/clientonly.pb.h ================================================ /* Automatically generated nanopb header */ /* Generated by nanopb-0.4.9.1 */ #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 ================================================ FILE: src/mesh/generated/meshtastic/config.pb.cpp ================================================ /* Automatically generated nanopb constant definitions */ /* Generated by nanopb-0.4.9.1 */ #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) ================================================ FILE: src/mesh/generated/meshtastic/config.pb.h ================================================ /* Automatically generated nanopb header */ /* Generated by nanopb-0.4.9.1 */ #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. Deprecated in v2.7.11 because it creates "holes" in the mesh rebroadcast chain. */ 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, /* Description: Will always rebroadcast packets, but will do so after all other modes. Technical Details: Used for router nodes that are intended to provide additional coverage in areas not already covered by other routers, or to bridge around problematic terrain, but should not be given priority over other routers in order to avoid unnecessaraily consuming hops. */ meshtastic_Config_DeviceConfig_Role_ROUTER_LATE = 11, /* Description: Treats packets from or to favorited nodes as ROUTER_LATE, and all other packets as CLIENT. Technical Details: Used for stronger attic/roof nodes to distribute messages more widely from weaker, indoor, or less-well-positioned nodes. Recommended for users with multiple nodes where one CLIENT_BASE acts as a more powerful base station, such as an attic/roof node. */ meshtastic_Config_DeviceConfig_Role_CLIENT_BASE = 12 } 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; /* Defines buzzer behavior for audio feedback */ typedef enum _meshtastic_Config_DeviceConfig_BuzzerMode { /* Default behavior. Buzzer is enabled for all audio feedback including button presses and alerts. */ meshtastic_Config_DeviceConfig_BuzzerMode_ALL_ENABLED = 0, /* Disabled. All buzzer audio feedback is disabled. */ meshtastic_Config_DeviceConfig_BuzzerMode_DISABLED = 1, /* Notifications Only. Buzzer is enabled only for notifications and alerts, but not for button presses. External notification config determines the specifics of the notification behavior. */ meshtastic_Config_DeviceConfig_BuzzerMode_NOTIFICATIONS_ONLY = 2, /* Non-notification system buzzer tones only. Buzzer is enabled only for non-notification tones such as button presses, startup, shutdown, but not for alerts. */ meshtastic_Config_DeviceConfig_BuzzerMode_SYSTEM_ONLY = 3, /* Direct Message notifications only. Buzzer is enabled only for direct messages and alerts, but not for button presses. External notification config determines the specifics of the notification behavior. */ meshtastic_Config_DeviceConfig_BuzzerMode_DIRECT_MSG_ONLY = 4 } meshtastic_Config_DeviceConfig_BuzzerMode; /* 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; /* Available flags auxiliary network protocols */ typedef enum _meshtastic_Config_NetworkConfig_ProtocolFlags { /* Do not broadcast packets over any network protocol */ meshtastic_Config_NetworkConfig_ProtocolFlags_NO_BROADCAST = 0, /* Enable broadcasting packets via UDP over the local network */ meshtastic_Config_NetworkConfig_ProtocolFlags_UDP_BROADCAST = 1 } meshtastic_Config_NetworkConfig_ProtocolFlags; /* Deprecated in 2.7.4: Unused */ typedef enum _meshtastic_Config_DisplayConfig_DeprecatedGpsCoordinateFormat { meshtastic_Config_DisplayConfig_DeprecatedGpsCoordinateFormat_UNUSED = 0 } meshtastic_Config_DisplayConfig_DeprecatedGpsCoordinateFormat; /* 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 / Autodetect */ meshtastic_Config_DisplayConfig_OledType_OLED_AUTO = 0, /* Default / Autodetect */ meshtastic_Config_DisplayConfig_OledType_OLED_SSD1306 = 1, /* Default / Autodetect */ meshtastic_Config_DisplayConfig_OledType_OLED_SH1106 = 2, /* Can not be auto detected but set by proto. Used for 128x64 screens */ meshtastic_Config_DisplayConfig_OledType_OLED_SH1107 = 3, /* Can not be auto detected but set by proto. Used for 128x128 screens */ meshtastic_Config_DisplayConfig_OledType_OLED_SH1107_128_128 = 4 } 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, /* Australia / New Zealand 433MHz */ meshtastic_Config_LoRaConfig_RegionCode_ANZ_433 = 22, /* Kazakhstan 433MHz */ meshtastic_Config_LoRaConfig_RegionCode_KZ_433 = 23, /* Kazakhstan 863MHz */ meshtastic_Config_LoRaConfig_RegionCode_KZ_863 = 24, /* Nepal 865MHz */ meshtastic_Config_LoRaConfig_RegionCode_NP_865 = 25, /* Brazil 902MHz */ meshtastic_Config_LoRaConfig_RegionCode_BR_902 = 26 } 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 Deprecated in 2.7: Unpopular slow preset. */ 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, /* Long Range - Turbo This preset performs similarly to LongFast, but with 500Khz bandwidth. */ meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO = 9 } meshtastic_Config_LoRaConfig_ModemPreset; typedef enum _meshtastic_Config_LoRaConfig_FEM_LNA_Mode { /* FEM_LNA is present but disabled */ meshtastic_Config_LoRaConfig_FEM_LNA_Mode_DISABLED = 0, /* FEM_LNA is present and enabled */ meshtastic_Config_LoRaConfig_FEM_LNA_Mode_ENABLED = 1, /* FEM_LNA is not present on the device */ meshtastic_Config_LoRaConfig_FEM_LNA_Mode_NOT_PRESENT = 2 } meshtastic_Config_LoRaConfig_FEM_LNA_Mode; 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; /* Controls buzzer behavior for audio feedback Defaults to ENABLED */ meshtastic_Config_DeviceConfig_BuzzerMode buzzer_mode; } 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 `meshtastic.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]; /* Flags for enabling/disabling network protocols */ uint32_t enabled_protocols; /* Enable/Disable ipv6 support */ bool ipv6_enabled; } 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; /* Deprecated in 2.7.4: Unused How the GPS coordinates are formatted on the OLED screen. */ meshtastic_Config_DisplayConfig_DeprecatedGpsCoordinateFormat 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; /* If false (default), the device will display the time in 24-hour format on screen. If true, the device will display the time in 12-hour format on screen. */ bool use_12h_clock; /* If false (default), the device will use short names for various display screens. If true, node names will show in long format */ bool use_long_node_name; /* If true, the device will display message bubbles on screen. */ bool enable_message_bubbles; } 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; /* Set where LORA FEM is enabled, disabled, or not present */ meshtastic_Config_LoRaConfig_FEM_LNA_Mode fem_lna_mode; } 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_CLIENT_BASE #define _meshtastic_Config_DeviceConfig_Role_ARRAYSIZE ((meshtastic_Config_DeviceConfig_Role)(meshtastic_Config_DeviceConfig_Role_CLIENT_BASE+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_DeviceConfig_BuzzerMode_MIN meshtastic_Config_DeviceConfig_BuzzerMode_ALL_ENABLED #define _meshtastic_Config_DeviceConfig_BuzzerMode_MAX meshtastic_Config_DeviceConfig_BuzzerMode_DIRECT_MSG_ONLY #define _meshtastic_Config_DeviceConfig_BuzzerMode_ARRAYSIZE ((meshtastic_Config_DeviceConfig_BuzzerMode)(meshtastic_Config_DeviceConfig_BuzzerMode_DIRECT_MSG_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_NetworkConfig_ProtocolFlags_MIN meshtastic_Config_NetworkConfig_ProtocolFlags_NO_BROADCAST #define _meshtastic_Config_NetworkConfig_ProtocolFlags_MAX meshtastic_Config_NetworkConfig_ProtocolFlags_UDP_BROADCAST #define _meshtastic_Config_NetworkConfig_ProtocolFlags_ARRAYSIZE ((meshtastic_Config_NetworkConfig_ProtocolFlags)(meshtastic_Config_NetworkConfig_ProtocolFlags_UDP_BROADCAST+1)) #define _meshtastic_Config_DisplayConfig_DeprecatedGpsCoordinateFormat_MIN meshtastic_Config_DisplayConfig_DeprecatedGpsCoordinateFormat_UNUSED #define _meshtastic_Config_DisplayConfig_DeprecatedGpsCoordinateFormat_MAX meshtastic_Config_DisplayConfig_DeprecatedGpsCoordinateFormat_UNUSED #define _meshtastic_Config_DisplayConfig_DeprecatedGpsCoordinateFormat_ARRAYSIZE ((meshtastic_Config_DisplayConfig_DeprecatedGpsCoordinateFormat)(meshtastic_Config_DisplayConfig_DeprecatedGpsCoordinateFormat_UNUSED+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_128_128 #define _meshtastic_Config_DisplayConfig_OledType_ARRAYSIZE ((meshtastic_Config_DisplayConfig_OledType)(meshtastic_Config_DisplayConfig_OledType_OLED_SH1107_128_128+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_BR_902 #define _meshtastic_Config_LoRaConfig_RegionCode_ARRAYSIZE ((meshtastic_Config_LoRaConfig_RegionCode)(meshtastic_Config_LoRaConfig_RegionCode_BR_902+1)) #define _meshtastic_Config_LoRaConfig_ModemPreset_MIN meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST #define _meshtastic_Config_LoRaConfig_ModemPreset_MAX meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO #define _meshtastic_Config_LoRaConfig_ModemPreset_ARRAYSIZE ((meshtastic_Config_LoRaConfig_ModemPreset)(meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO+1)) #define _meshtastic_Config_LoRaConfig_FEM_LNA_Mode_MIN meshtastic_Config_LoRaConfig_FEM_LNA_Mode_DISABLED #define _meshtastic_Config_LoRaConfig_FEM_LNA_Mode_MAX meshtastic_Config_LoRaConfig_FEM_LNA_Mode_NOT_PRESENT #define _meshtastic_Config_LoRaConfig_FEM_LNA_Mode_ARRAYSIZE ((meshtastic_Config_LoRaConfig_FEM_LNA_Mode)(meshtastic_Config_LoRaConfig_FEM_LNA_Mode_NOT_PRESENT+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_DeviceConfig_buzzer_mode_ENUMTYPE meshtastic_Config_DeviceConfig_BuzzerMode #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_DeprecatedGpsCoordinateFormat #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_LoRaConfig_fem_lna_mode_ENUMTYPE meshtastic_Config_LoRaConfig_FEM_LNA_Mode #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, _meshtastic_Config_DeviceConfig_BuzzerMode_MIN} #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, "", 0, 0} #define meshtastic_Config_NetworkConfig_IpV4Config_init_default {0, 0, 0, 0} #define meshtastic_Config_DisplayConfig_init_default {0, _meshtastic_Config_DisplayConfig_DeprecatedGpsCoordinateFormat_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, 0, 0, 0} #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, _meshtastic_Config_LoRaConfig_FEM_LNA_Mode_MIN} #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, _meshtastic_Config_DeviceConfig_BuzzerMode_MIN} #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, "", 0, 0} #define meshtastic_Config_NetworkConfig_IpV4Config_init_zero {0, 0, 0, 0} #define meshtastic_Config_DisplayConfig_init_zero {0, _meshtastic_Config_DisplayConfig_DeprecatedGpsCoordinateFormat_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, 0, 0, 0} #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, _meshtastic_Config_LoRaConfig_FEM_LNA_Mode_MIN} #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_DeviceConfig_buzzer_mode_tag 13 #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_NetworkConfig_enabled_protocols_tag 10 #define meshtastic_Config_NetworkConfig_ipv6_enabled_tag 11 #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_DisplayConfig_use_12h_clock_tag 12 #define meshtastic_Config_DisplayConfig_use_long_node_name_tag 13 #define meshtastic_Config_DisplayConfig_enable_message_bubbles_tag 14 #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_LoRaConfig_fem_lna_mode_tag 106 #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) \ X(a, STATIC, SINGULAR, UENUM, buzzer_mode, 13) #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) \ X(a, STATIC, SINGULAR, UINT32, enabled_protocols, 10) \ X(a, STATIC, SINGULAR, BOOL, ipv6_enabled, 11) #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) \ X(a, STATIC, SINGULAR, BOOL, use_12h_clock, 12) \ X(a, STATIC, SINGULAR, BOOL, use_long_node_name, 13) \ X(a, STATIC, SINGULAR, BOOL, enable_message_bubbles, 14) #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) \ X(a, STATIC, SINGULAR, UENUM, fem_lna_mode, 106) #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 100 #define meshtastic_Config_DisplayConfig_size 36 #define meshtastic_Config_LoRaConfig_size 88 #define meshtastic_Config_NetworkConfig_IpV4Config_size 20 #define meshtastic_Config_NetworkConfig_size 204 #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 207 #ifdef __cplusplus } /* extern "C" */ #endif #endif ================================================ FILE: src/mesh/generated/meshtastic/connection_status.pb.cpp ================================================ /* Automatically generated nanopb constant definitions */ /* Generated by nanopb-0.4.9.1 */ #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) ================================================ FILE: src/mesh/generated/meshtastic/connection_status.pb.h ================================================ /* Automatically generated nanopb header */ /* Generated by nanopb-0.4.9.1 */ #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 ================================================ FILE: src/mesh/generated/meshtastic/device_ui.pb.cpp ================================================ /* Automatically generated nanopb constant definitions */ /* Generated by nanopb-0.4.9.1 */ #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) PB_BIND(meshtastic_GeoPoint, meshtastic_GeoPoint, AUTO) PB_BIND(meshtastic_Map, meshtastic_Map, AUTO) ================================================ FILE: src/mesh/generated/meshtastic/device_ui.pb.h ================================================ /* Automatically generated nanopb header */ /* Generated by nanopb-0.4.9.1 */ #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_CompassMode { /* Compass with dynamic ring and heading */ meshtastic_CompassMode_DYNAMIC = 0, /* Compass with fixed ring and heading */ meshtastic_CompassMode_FIXED_RING = 1, /* Compass with heading and freeze option */ meshtastic_CompassMode_FREEZE_HEADING = 2 } meshtastic_CompassMode; 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, /* Norwegian */ meshtastic_Language_NORWEGIAN = 14, /* Slovenian */ meshtastic_Language_SLOVENIAN = 15, /* Ukrainian */ meshtastic_Language_UKRAINIAN = 16, /* Bulgarian */ meshtastic_Language_BULGARIAN = 17, /* Czech */ meshtastic_Language_CZECH = 18, /* Danish */ meshtastic_Language_DANISH = 19, /* Simplified Chinese (experimental) */ meshtastic_Language_SIMPLIFIED_CHINESE = 30, /* Traditional Chinese (experimental) */ meshtastic_Language_TRADITIONAL_CHINESE = 31 } meshtastic_Language; /* How the GPS coordinates are displayed on the OLED screen. */ typedef enum _meshtastic_DeviceUIConfig_GpsCoordinateFormat { /* GPS coordinates are displayed in the normal decimal degrees format: DD.DDDDDD DDD.DDDDDD */ meshtastic_DeviceUIConfig_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_DeviceUIConfig_GpsCoordinateFormat_DMS = 1, /* Universal Transverse Mercator format: ZZB EEEEEE NNNNNNN, where Z is zone, B is band, E is easting, N is northing */ meshtastic_DeviceUIConfig_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_DeviceUIConfig_GpsCoordinateFormat_MGRS = 3, /* Open Location Code (aka Plus Codes). */ meshtastic_DeviceUIConfig_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_DeviceUIConfig_GpsCoordinateFormat_OSGR = 5, /* Maidenhead Locator System Described here: https://en.wikipedia.org/wiki/Maidenhead_Locator_System */ meshtastic_DeviceUIConfig_GpsCoordinateFormat_MLS = 6 } meshtastic_DeviceUIConfig_GpsCoordinateFormat; /* 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]; /* Filter based on channel */ int8_t channel; } 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_GeoPoint { /* Zoom level */ int8_t zoom; /* Coordinate: latitude */ int32_t latitude; /* Coordinate: longitude */ int32_t longitude; } meshtastic_GeoPoint; typedef struct _meshtastic_Map { /* Home coordinates */ bool has_home; meshtastic_GeoPoint home; /* Map tile style */ char style[20]; /* Map scroll follows GPS */ bool follow_gps; } meshtastic_Map; typedef PB_BYTES_ARRAY_T(16) meshtastic_DeviceUIConfig_calibration_data_t; 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; /* 8 integers for screen calibration data */ meshtastic_DeviceUIConfig_calibration_data_t calibration_data; /* Map related data */ bool has_map_data; meshtastic_Map map_data; /* Compass mode */ meshtastic_CompassMode compass_mode; /* RGB color for BaseUI 0xRRGGBB format, e.g. 0xFF0000 for red */ uint32_t screen_rgb_color; /* Clockface analog style true for analog clockface, false for digital clockface */ bool is_clockface_analog; /* How the GPS coordinates are formatted on the OLED screen. */ meshtastic_DeviceUIConfig_GpsCoordinateFormat gps_format; } meshtastic_DeviceUIConfig; #ifdef __cplusplus extern "C" { #endif /* Helper constants for enums */ #define _meshtastic_CompassMode_MIN meshtastic_CompassMode_DYNAMIC #define _meshtastic_CompassMode_MAX meshtastic_CompassMode_FREEZE_HEADING #define _meshtastic_CompassMode_ARRAYSIZE ((meshtastic_CompassMode)(meshtastic_CompassMode_FREEZE_HEADING+1)) #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_GpsCoordinateFormat_MIN meshtastic_DeviceUIConfig_GpsCoordinateFormat_DEC #define _meshtastic_DeviceUIConfig_GpsCoordinateFormat_MAX meshtastic_DeviceUIConfig_GpsCoordinateFormat_MLS #define _meshtastic_DeviceUIConfig_GpsCoordinateFormat_ARRAYSIZE ((meshtastic_DeviceUIConfig_GpsCoordinateFormat)(meshtastic_DeviceUIConfig_GpsCoordinateFormat_MLS+1)) #define meshtastic_DeviceUIConfig_theme_ENUMTYPE meshtastic_Theme #define meshtastic_DeviceUIConfig_language_ENUMTYPE meshtastic_Language #define meshtastic_DeviceUIConfig_compass_mode_ENUMTYPE meshtastic_CompassMode #define meshtastic_DeviceUIConfig_gps_format_ENUMTYPE meshtastic_DeviceUIConfig_GpsCoordinateFormat /* 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, {0, {0}}, false, meshtastic_Map_init_default, _meshtastic_CompassMode_MIN, 0, 0, _meshtastic_DeviceUIConfig_GpsCoordinateFormat_MIN} #define meshtastic_NodeFilter_init_default {0, 0, 0, 0, 0, "", 0} #define meshtastic_NodeHighlight_init_default {0, 0, 0, 0, ""} #define meshtastic_GeoPoint_init_default {0, 0, 0} #define meshtastic_Map_init_default {false, meshtastic_GeoPoint_init_default, "", 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, {0, {0}}, false, meshtastic_Map_init_zero, _meshtastic_CompassMode_MIN, 0, 0, _meshtastic_DeviceUIConfig_GpsCoordinateFormat_MIN} #define meshtastic_NodeFilter_init_zero {0, 0, 0, 0, 0, "", 0} #define meshtastic_NodeHighlight_init_zero {0, 0, 0, 0, ""} #define meshtastic_GeoPoint_init_zero {0, 0, 0} #define meshtastic_Map_init_zero {false, meshtastic_GeoPoint_init_zero, "", 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_NodeFilter_channel_tag 7 #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_GeoPoint_zoom_tag 1 #define meshtastic_GeoPoint_latitude_tag 2 #define meshtastic_GeoPoint_longitude_tag 3 #define meshtastic_Map_home_tag 1 #define meshtastic_Map_style_tag 2 #define meshtastic_Map_follow_gps_tag 3 #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 #define meshtastic_DeviceUIConfig_calibration_data_tag 14 #define meshtastic_DeviceUIConfig_map_data_tag 15 #define meshtastic_DeviceUIConfig_compass_mode_tag 16 #define meshtastic_DeviceUIConfig_screen_rgb_color_tag 17 #define meshtastic_DeviceUIConfig_is_clockface_analog_tag 18 #define meshtastic_DeviceUIConfig_gps_format_tag 19 /* 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) \ X(a, STATIC, SINGULAR, BYTES, calibration_data, 14) \ X(a, STATIC, OPTIONAL, MESSAGE, map_data, 15) \ X(a, STATIC, SINGULAR, UENUM, compass_mode, 16) \ X(a, STATIC, SINGULAR, UINT32, screen_rgb_color, 17) \ X(a, STATIC, SINGULAR, BOOL, is_clockface_analog, 18) \ X(a, STATIC, SINGULAR, UENUM, gps_format, 19) #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_DeviceUIConfig_map_data_MSGTYPE meshtastic_Map #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) \ X(a, STATIC, SINGULAR, INT32, channel, 7) #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 #define meshtastic_GeoPoint_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, INT32, zoom, 1) \ X(a, STATIC, SINGULAR, INT32, latitude, 2) \ X(a, STATIC, SINGULAR, INT32, longitude, 3) #define meshtastic_GeoPoint_CALLBACK NULL #define meshtastic_GeoPoint_DEFAULT NULL #define meshtastic_Map_FIELDLIST(X, a) \ X(a, STATIC, OPTIONAL, MESSAGE, home, 1) \ X(a, STATIC, SINGULAR, STRING, style, 2) \ X(a, STATIC, SINGULAR, BOOL, follow_gps, 3) #define meshtastic_Map_CALLBACK NULL #define meshtastic_Map_DEFAULT NULL #define meshtastic_Map_home_MSGTYPE meshtastic_GeoPoint extern const pb_msgdesc_t meshtastic_DeviceUIConfig_msg; extern const pb_msgdesc_t meshtastic_NodeFilter_msg; extern const pb_msgdesc_t meshtastic_NodeHighlight_msg; extern const pb_msgdesc_t meshtastic_GeoPoint_msg; extern const pb_msgdesc_t meshtastic_Map_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 #define meshtastic_GeoPoint_fields &meshtastic_GeoPoint_msg #define meshtastic_Map_fields &meshtastic_Map_msg /* Maximum encoded size of messages (where known) */ #define MESHTASTIC_MESHTASTIC_DEVICE_UI_PB_H_MAX_SIZE meshtastic_DeviceUIConfig_size #define meshtastic_DeviceUIConfig_size 204 #define meshtastic_GeoPoint_size 33 #define meshtastic_Map_size 58 #define meshtastic_NodeFilter_size 47 #define meshtastic_NodeHighlight_size 25 #ifdef __cplusplus } /* extern "C" */ #endif #endif ================================================ FILE: src/mesh/generated/meshtastic/deviceonly.pb.cpp ================================================ /* Automatically generated nanopb constant definitions */ /* Generated by nanopb-0.4.9.1 */ #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_NodeDatabase, meshtastic_NodeDatabase, AUTO) PB_BIND(meshtastic_ChannelFile, meshtastic_ChannelFile, 2) PB_BIND(meshtastic_BackupPreferences, meshtastic_BackupPreferences, 2) ================================================ FILE: src/mesh/generated/meshtastic/deviceonly.pb.h ================================================ /* Automatically generated nanopb header */ /* Generated by nanopb-0.4.9.1 */ #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/config.pb.h" #include "meshtastic/localonly.pb.h" #include "meshtastic/mesh.pb.h" #include "meshtastic/telemetry.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; /* Whether or not the node can be messaged */ bool has_is_unmessagable; bool is_unmessagable; } 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 direct neighbor) */ 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; /* True if node is in our ignored list Persists between NodeDB internal clean ups */ bool is_ignored; /* Last byte of the node number of the node that should be used as the next hop to reach this node. */ uint8_t next_hop; /* Bitfield for storing booleans. LSB 0 is_key_manually_verified LSB 1 is_muted */ uint32_t bitfield; } 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; /* Previously used to manage GPS factory resets. Deprecated in 2.5.23 */ 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]; } meshtastic_DeviceState; typedef struct _meshtastic_NodeDatabase { /* 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; /* New lite version of NodeDB to decrease memory footprint */ std::vector nodes; } meshtastic_NodeDatabase; /* 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; /* The on-disk backup of the node's preferences */ typedef struct _meshtastic_BackupPreferences { /* The version of the backup */ uint32_t version; /* The timestamp of the backup (if node has time) */ uint32_t timestamp; /* The node's configuration */ bool has_config; meshtastic_LocalConfig config; /* The node's module configuration */ bool has_module_config; meshtastic_LocalModuleConfig module_config; /* The node's channels */ bool has_channels; meshtastic_ChannelFile channels; /* The node's user (owner) information */ bool has_owner; meshtastic_User owner; } meshtastic_BackupPreferences; #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}}, false, 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, 0, 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}} #define meshtastic_NodeDatabase_init_default {0, {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_BackupPreferences_init_default {0, 0, false, meshtastic_LocalConfig_init_default, false, meshtastic_LocalModuleConfig_init_default, false, meshtastic_ChannelFile_init_default, false, meshtastic_User_init_default} #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}}, false, 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, 0, 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}} #define meshtastic_NodeDatabase_init_zero {0, {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} #define meshtastic_BackupPreferences_init_zero {0, 0, false, meshtastic_LocalConfig_init_zero, false, meshtastic_LocalModuleConfig_init_zero, false, meshtastic_ChannelFile_init_zero, false, meshtastic_User_init_zero} /* 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_UserLite_is_unmessagable_tag 9 #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_NodeInfoLite_is_ignored_tag 11 #define meshtastic_NodeInfoLite_next_hop_tag 12 #define meshtastic_NodeInfoLite_bitfield_tag 13 #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_NodeDatabase_version_tag 1 #define meshtastic_NodeDatabase_nodes_tag 2 #define meshtastic_ChannelFile_channels_tag 1 #define meshtastic_ChannelFile_version_tag 2 #define meshtastic_BackupPreferences_version_tag 1 #define meshtastic_BackupPreferences_timestamp_tag 2 #define meshtastic_BackupPreferences_config_tag 3 #define meshtastic_BackupPreferences_module_config_tag 4 #define meshtastic_BackupPreferences_channels_tag 5 #define meshtastic_BackupPreferences_owner_tag 6 /* 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) \ X(a, STATIC, OPTIONAL, BOOL, is_unmessagable, 9) #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) \ X(a, STATIC, SINGULAR, BOOL, is_ignored, 11) \ X(a, STATIC, SINGULAR, UINT32, next_hop, 12) \ X(a, STATIC, SINGULAR, UINT32, bitfield, 13) #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) #define meshtastic_DeviceState_CALLBACK NULL #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_NodeDatabase_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, UINT32, version, 1) \ X(a, CALLBACK, REPEATED, MESSAGE, nodes, 2) extern bool meshtastic_NodeDatabase_callback(pb_istream_t *istream, pb_ostream_t *ostream, const pb_field_t *field); #define meshtastic_NodeDatabase_CALLBACK meshtastic_NodeDatabase_callback #define meshtastic_NodeDatabase_DEFAULT NULL #define meshtastic_NodeDatabase_nodes_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 #define meshtastic_BackupPreferences_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, UINT32, version, 1) \ X(a, STATIC, SINGULAR, FIXED32, timestamp, 2) \ X(a, STATIC, OPTIONAL, MESSAGE, config, 3) \ X(a, STATIC, OPTIONAL, MESSAGE, module_config, 4) \ X(a, STATIC, OPTIONAL, MESSAGE, channels, 5) \ X(a, STATIC, OPTIONAL, MESSAGE, owner, 6) #define meshtastic_BackupPreferences_CALLBACK NULL #define meshtastic_BackupPreferences_DEFAULT NULL #define meshtastic_BackupPreferences_config_MSGTYPE meshtastic_LocalConfig #define meshtastic_BackupPreferences_module_config_MSGTYPE meshtastic_LocalModuleConfig #define meshtastic_BackupPreferences_channels_MSGTYPE meshtastic_ChannelFile #define meshtastic_BackupPreferences_owner_MSGTYPE meshtastic_User 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_NodeDatabase_msg; extern const pb_msgdesc_t meshtastic_ChannelFile_msg; extern const pb_msgdesc_t meshtastic_BackupPreferences_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_NodeDatabase_fields &meshtastic_NodeDatabase_msg #define meshtastic_ChannelFile_fields &meshtastic_ChannelFile_msg #define meshtastic_BackupPreferences_fields &meshtastic_BackupPreferences_msg /* Maximum encoded size of messages (where known) */ /* meshtastic_NodeDatabase_size depends on runtime parameters */ #define MESHTASTIC_MESHTASTIC_DEVICEONLY_PB_H_MAX_SIZE meshtastic_BackupPreferences_size #define meshtastic_BackupPreferences_size 2429 #define meshtastic_ChannelFile_size 718 #define meshtastic_DeviceState_size 1737 #define meshtastic_NodeInfoLite_size 196 #define meshtastic_PositionLite_size 28 #define meshtastic_UserLite_size 98 #ifdef __cplusplus } /* extern "C" */ #endif #endif ================================================ FILE: src/mesh/generated/meshtastic/interdevice.pb.cpp ================================================ /* Automatically generated nanopb constant definitions */ /* Generated by nanopb-0.4.9.1 */ #include "meshtastic/interdevice.pb.h" #if PB_PROTO_HEADER_VERSION != 40 #error Regenerate this file with the current version of nanopb generator. #endif PB_BIND(meshtastic_SensorData, meshtastic_SensorData, AUTO) PB_BIND(meshtastic_InterdeviceMessage, meshtastic_InterdeviceMessage, 2) ================================================ FILE: src/mesh/generated/meshtastic/interdevice.pb.h ================================================ /* Automatically generated nanopb header */ /* Generated by nanopb-0.4.9.1 */ #ifndef PB_MESHTASTIC_MESHTASTIC_INTERDEVICE_PB_H_INCLUDED #define PB_MESHTASTIC_MESHTASTIC_INTERDEVICE_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_MessageType { meshtastic_MessageType_ACK = 0, meshtastic_MessageType_COLLECT_INTERVAL = 160, /* in ms */ meshtastic_MessageType_BEEP_ON = 161, /* duration ms */ meshtastic_MessageType_BEEP_OFF = 162, /* cancel prematurely */ meshtastic_MessageType_SHUTDOWN = 163, meshtastic_MessageType_POWER_ON = 164, meshtastic_MessageType_SCD41_TEMP = 176, meshtastic_MessageType_SCD41_HUMIDITY = 177, meshtastic_MessageType_SCD41_CO2 = 178, meshtastic_MessageType_AHT20_TEMP = 179, meshtastic_MessageType_AHT20_HUMIDITY = 180, meshtastic_MessageType_TVOC_INDEX = 181 } meshtastic_MessageType; /* Struct definitions */ typedef struct _meshtastic_SensorData { /* The message type */ meshtastic_MessageType type; pb_size_t which_data; union { float float_value; uint32_t uint32_value; } data; } meshtastic_SensorData; typedef struct _meshtastic_InterdeviceMessage { pb_size_t which_data; union { char nmea[1024]; meshtastic_SensorData sensor; } data; } meshtastic_InterdeviceMessage; #ifdef __cplusplus extern "C" { #endif /* Helper constants for enums */ #define _meshtastic_MessageType_MIN meshtastic_MessageType_ACK #define _meshtastic_MessageType_MAX meshtastic_MessageType_TVOC_INDEX #define _meshtastic_MessageType_ARRAYSIZE ((meshtastic_MessageType)(meshtastic_MessageType_TVOC_INDEX+1)) #define meshtastic_SensorData_type_ENUMTYPE meshtastic_MessageType /* Initializer values for message structs */ #define meshtastic_SensorData_init_default {_meshtastic_MessageType_MIN, 0, {0}} #define meshtastic_InterdeviceMessage_init_default {0, {""}} #define meshtastic_SensorData_init_zero {_meshtastic_MessageType_MIN, 0, {0}} #define meshtastic_InterdeviceMessage_init_zero {0, {""}} /* Field tags (for use in manual encoding/decoding) */ #define meshtastic_SensorData_type_tag 1 #define meshtastic_SensorData_float_value_tag 2 #define meshtastic_SensorData_uint32_value_tag 3 #define meshtastic_InterdeviceMessage_nmea_tag 1 #define meshtastic_InterdeviceMessage_sensor_tag 2 /* Struct field encoding specification for nanopb */ #define meshtastic_SensorData_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, UENUM, type, 1) \ X(a, STATIC, ONEOF, FLOAT, (data,float_value,data.float_value), 2) \ X(a, STATIC, ONEOF, UINT32, (data,uint32_value,data.uint32_value), 3) #define meshtastic_SensorData_CALLBACK NULL #define meshtastic_SensorData_DEFAULT NULL #define meshtastic_InterdeviceMessage_FIELDLIST(X, a) \ X(a, STATIC, ONEOF, STRING, (data,nmea,data.nmea), 1) \ X(a, STATIC, ONEOF, MESSAGE, (data,sensor,data.sensor), 2) #define meshtastic_InterdeviceMessage_CALLBACK NULL #define meshtastic_InterdeviceMessage_DEFAULT NULL #define meshtastic_InterdeviceMessage_data_sensor_MSGTYPE meshtastic_SensorData extern const pb_msgdesc_t meshtastic_SensorData_msg; extern const pb_msgdesc_t meshtastic_InterdeviceMessage_msg; /* Defines for backwards compatibility with code written before nanopb-0.4.0 */ #define meshtastic_SensorData_fields &meshtastic_SensorData_msg #define meshtastic_InterdeviceMessage_fields &meshtastic_InterdeviceMessage_msg /* Maximum encoded size of messages (where known) */ #define MESHTASTIC_MESHTASTIC_INTERDEVICE_PB_H_MAX_SIZE meshtastic_InterdeviceMessage_size #define meshtastic_InterdeviceMessage_size 1026 #define meshtastic_SensorData_size 9 #ifdef __cplusplus } /* extern "C" */ #endif #endif ================================================ FILE: src/mesh/generated/meshtastic/localonly.pb.cpp ================================================ /* Automatically generated nanopb constant definitions */ /* Generated by nanopb-0.4.9.1 */ #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) ================================================ FILE: src/mesh/generated/meshtastic/localonly.pb.h ================================================ /* Automatically generated nanopb header */ /* Generated by nanopb-0.4.9.1 */ #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; /* StatusMessage Config */ bool has_statusmessage; meshtastic_ModuleConfig_StatusMessageConfig statusmessage; /* The part of the config that is specific to the Traffic Management module */ bool has_traffic_management; meshtastic_ModuleConfig_TrafficManagementConfig traffic_management; /* TAK Config */ bool has_tak; meshtastic_ModuleConfig_TAKConfig tak; } 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, false, meshtastic_ModuleConfig_StatusMessageConfig_init_default, false, meshtastic_ModuleConfig_TrafficManagementConfig_init_default, false, meshtastic_ModuleConfig_TAKConfig_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, false, meshtastic_ModuleConfig_StatusMessageConfig_init_zero, false, meshtastic_ModuleConfig_TrafficManagementConfig_init_zero, false, meshtastic_ModuleConfig_TAKConfig_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 #define meshtastic_LocalModuleConfig_statusmessage_tag 15 #define meshtastic_LocalModuleConfig_traffic_management_tag 16 #define meshtastic_LocalModuleConfig_tak_tag 17 /* 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) \ X(a, STATIC, OPTIONAL, MESSAGE, statusmessage, 15) \ X(a, STATIC, OPTIONAL, MESSAGE, traffic_management, 16) \ X(a, STATIC, OPTIONAL, MESSAGE, tak, 17) #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 #define meshtastic_LocalModuleConfig_statusmessage_MSGTYPE meshtastic_ModuleConfig_StatusMessageConfig #define meshtastic_LocalModuleConfig_traffic_management_MSGTYPE meshtastic_ModuleConfig_TrafficManagementConfig #define meshtastic_LocalModuleConfig_tak_MSGTYPE meshtastic_ModuleConfig_TAKConfig 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_LocalModuleConfig_size #define meshtastic_LocalConfig_size 754 #define meshtastic_LocalModuleConfig_size 820 #ifdef __cplusplus } /* extern "C" */ #endif #endif ================================================ FILE: src/mesh/generated/meshtastic/mesh.pb.cpp ================================================ /* Automatically generated nanopb constant definitions */ /* Generated by nanopb-0.4.9.1 */ #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_KeyVerification, meshtastic_KeyVerification, AUTO) PB_BIND(meshtastic_StoreForwardPlusPlus, meshtastic_StoreForwardPlusPlus, 2) PB_BIND(meshtastic_Waypoint, meshtastic_Waypoint, AUTO) PB_BIND(meshtastic_StatusMessage, meshtastic_StatusMessage, 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_KeyVerificationNumberInform, meshtastic_KeyVerificationNumberInform, AUTO) PB_BIND(meshtastic_KeyVerificationNumberRequest, meshtastic_KeyVerificationNumberRequest, AUTO) PB_BIND(meshtastic_KeyVerificationFinal, meshtastic_KeyVerificationFinal, AUTO) PB_BIND(meshtastic_DuplicatedPublicKey, meshtastic_DuplicatedPublicKey, AUTO) PB_BIND(meshtastic_LowEntropyKey, meshtastic_LowEntropyKey, AUTO) 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) ================================================ FILE: src/mesh/generated/meshtastic/mesh.pb.h ================================================ /* Automatically generated nanopb header */ /* Generated by nanopb-0.4.9.1 */ #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/device_ui.pb.h" #include "meshtastic/module_config.pb.h" #include "meshtastic/portnums.pb.h" #include "meshtastic/telemetry.pb.h" #include "meshtastic/xmodem.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, /* T-Echo Plus device from LilyGo */ meshtastic_HardwareModel_T_ECHO_PLUS = 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, /* WisMesh Tap RAK-4631 w/ TFT in injection modled case */ meshtastic_HardwareModel_WISMESH_TAP = 84, /* Similar to PORTDUINO but used by Routastic devices, this is not any particular device and does not run Meshtastic's code but supports the same frame format. Runs on linux, see https://github.com/Jorropo/routastic */ meshtastic_HardwareModel_ROUTASTIC = 85, /* Mesh-Tab, esp32 based https://github.com/valzzu/Mesh-Tab */ meshtastic_HardwareModel_MESH_TAB = 86, /* MeshLink board developed by LoraItalia. NRF52840, eByte E22900M22S (Will also come with other frequencies), 25w MPPT solar charger (5v,12v,18v selectable), support for gps, buzzer, oled or e-ink display, 10 gpios, hardware watchdog https://www.loraitalia.it */ meshtastic_HardwareModel_MESHLINK = 87, /* Seeed XIAO nRF52840 + Wio SX1262 kit */ meshtastic_HardwareModel_XIAO_NRF52_KIT = 88, /* Elecrow ThinkNode M1 & M2 https://www.elecrow.com/wiki/ThinkNode-M1_Transceiver_Device(Meshtastic)_Power_By_nRF52840.html https://www.elecrow.com/wiki/ThinkNode-M2_Transceiver_Device(Meshtastic)_Power_By_NRF52840.html (this actually uses ESP32-S3) */ meshtastic_HardwareModel_THINKNODE_M1 = 89, meshtastic_HardwareModel_THINKNODE_M2 = 90, /* Lilygo T-ETH-Elite */ meshtastic_HardwareModel_T_ETH_ELITE = 91, /* Heltec HRI-3621 industrial probe */ meshtastic_HardwareModel_HELTEC_SENSOR_HUB = 92, /* Muzi Works Muzi-Base device */ meshtastic_HardwareModel_MUZI_BASE = 93, /* Heltec Magnetic Power Bank with Meshtastic compatible */ meshtastic_HardwareModel_HELTEC_MESH_POCKET = 94, /* Seeed Solar Node */ meshtastic_HardwareModel_SEEED_SOLAR_NODE = 95, /* NomadStar Meteor Pro https://nomadstar.ch/ */ meshtastic_HardwareModel_NOMADSTAR_METEOR_PRO = 96, /* Elecrow CrowPanel Advance models, ESP32-S3 and TFT with SX1262 radio plugin */ meshtastic_HardwareModel_CROWPANEL = 97, /* Lilygo LINK32 board with sensors */ meshtastic_HardwareModel_LINK_32 = 98, /* Seeed Tracker L1 */ meshtastic_HardwareModel_SEEED_WIO_TRACKER_L1 = 99, /* Seeed Tracker L1 EINK driver */ meshtastic_HardwareModel_SEEED_WIO_TRACKER_L1_EINK = 100, /* Muzi Works R1 Neo */ meshtastic_HardwareModel_MUZI_R1_NEO = 101, /* Lilygo T-Deck Pro */ meshtastic_HardwareModel_T_DECK_PRO = 102, /* Lilygo TLora Pager */ meshtastic_HardwareModel_T_LORA_PAGER = 103, /* M5Stack Reserved */ meshtastic_HardwareModel_M5STACK_RESERVED = 104, /* 0x68 */ /* RAKwireless WisMesh Tag */ meshtastic_HardwareModel_WISMESH_TAG = 105, /* RAKwireless WisBlock Core RAK3312 https://docs.rakwireless.com/product-categories/wisduo/rak3112-module/overview/ */ meshtastic_HardwareModel_RAK3312 = 106, /* Elecrow ThinkNode M5 https://www.elecrow.com/wiki/ThinkNode_M5_Meshtastic_LoRa_Signal_Transceiver_ESP32-S3.html */ meshtastic_HardwareModel_THINKNODE_M5 = 107, /* MeshSolar is an integrated power management and communication solution designed for outdoor low-power devices. https://heltec.org/project/meshsolar/ */ meshtastic_HardwareModel_HELTEC_MESH_SOLAR = 108, /* Lilygo T-Echo Lite */ meshtastic_HardwareModel_T_ECHO_LITE = 109, /* New Heltec LoRA32 with ESP32-S3 CPU */ meshtastic_HardwareModel_HELTEC_V4 = 110, /* M5Stack C6L */ meshtastic_HardwareModel_M5STACK_C6L = 111, /* M5Stack Cardputer Adv */ meshtastic_HardwareModel_M5STACK_CARDPUTER_ADV = 112, /* ESP32S3 main controller with GPS and TFT screen. */ meshtastic_HardwareModel_HELTEC_WIRELESS_TRACKER_V2 = 113, /* LilyGo T-Watch Ultra */ meshtastic_HardwareModel_T_WATCH_ULTRA = 114, /* Elecrow ThinkNode M3 */ meshtastic_HardwareModel_THINKNODE_M3 = 115, /* RAK WISMESH_TAP_V2 with ESP32-S3 CPU */ meshtastic_HardwareModel_WISMESH_TAP_V2 = 116, /* RAK3401 */ meshtastic_HardwareModel_RAK3401 = 117, /* RAK6421 Hat+ */ meshtastic_HardwareModel_RAK6421 = 118, /* Elecrow ThinkNode M4 */ meshtastic_HardwareModel_THINKNODE_M4 = 119, /* Elecrow ThinkNode M6 */ meshtastic_HardwareModel_THINKNODE_M6 = 120, /* Elecrow Meshstick 1262 */ meshtastic_HardwareModel_MESHSTICK_1262 = 121, /* LilyGo T-Beam 1W */ meshtastic_HardwareModel_TBEAM_1_WATT = 122, /* LilyGo T5 S3 ePaper Pro (V1 and V2) */ meshtastic_HardwareModel_T5_S3_EPAPER_PRO = 123, /* LilyGo T-Beam BPF (144-148Mhz) */ meshtastic_HardwareModel_TBEAM_BPF = 124, /* LilyGo T-Mini E-paper S3 Kit */ meshtastic_HardwareModel_MINI_EPAPER_S3 = 125, /* LilyGo T-Display S3 Pro LR1121 */ meshtastic_HardwareModel_TDISPLAY_S3_PRO = 126, /* ------------------------------------------------------------------------------------------------------------------------------------------ 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 = 233 } 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; /* Enum to indicate to clients whether this firmware is a special firmware build, like an event. The first 16 values are reserved for non-event special firmwares, like the Smart Citizen use case. */ typedef enum _meshtastic_FirmwareEdition { /* Vanilla firmware */ meshtastic_FirmwareEdition_VANILLA = 0, /* Firmware for use in the Smart Citizen environmental monitoring network */ meshtastic_FirmwareEdition_SMART_CITIZEN = 1, /* Open Sauce, the maker conference held yearly in CA */ meshtastic_FirmwareEdition_OPEN_SAUCE = 16, /* DEFCON, the yearly hacker conference */ meshtastic_FirmwareEdition_DEFCON = 17, /* Burning Man, the yearly hippie gathering in the desert */ meshtastic_FirmwareEdition_BURNING_MAN = 18, /* Hamvention, the Dayton amateur radio convention */ meshtastic_FirmwareEdition_HAMVENTION = 19, /* Placeholder for DIY and unofficial events */ meshtastic_FirmwareEdition_DIY_EDITION = 127 } meshtastic_FirmwareEdition; /* Enum for modules excluded from a device's configuration. Each value represents a ModuleConfigType that can be toggled as excluded by setting its corresponding bit in the `excluded_modules` bitmask field. */ typedef enum _meshtastic_ExcludedModules { /* Default value of 0 indicates no modules are excluded. */ meshtastic_ExcludedModules_EXCLUDED_NONE = 0, /* MQTT module */ meshtastic_ExcludedModules_MQTT_CONFIG = 1, /* Serial module */ meshtastic_ExcludedModules_SERIAL_CONFIG = 2, /* External Notification module */ meshtastic_ExcludedModules_EXTNOTIF_CONFIG = 4, /* Store and Forward module */ meshtastic_ExcludedModules_STOREFORWARD_CONFIG = 8, /* Range Test module */ meshtastic_ExcludedModules_RANGETEST_CONFIG = 16, /* Telemetry module */ meshtastic_ExcludedModules_TELEMETRY_CONFIG = 32, /* Canned Message module */ meshtastic_ExcludedModules_CANNEDMSG_CONFIG = 64, /* Audio module */ meshtastic_ExcludedModules_AUDIO_CONFIG = 128, /* Remote Hardware module */ meshtastic_ExcludedModules_REMOTEHARDWARE_CONFIG = 256, /* Neighbor Info module */ meshtastic_ExcludedModules_NEIGHBORINFO_CONFIG = 512, /* Ambient Lighting module */ meshtastic_ExcludedModules_AMBIENTLIGHTING_CONFIG = 1024, /* Detection Sensor module */ meshtastic_ExcludedModules_DETECTIONSENSOR_CONFIG = 2048, /* Paxcounter module */ meshtastic_ExcludedModules_PAXCOUNTER_CONFIG = 4096, /* Bluetooth config (not technically a module, but used to indicate bluetooth capabilities) */ meshtastic_ExcludedModules_BLUETOOTH_CONFIG = 8192, /* Network config (not technically a module, but used to indicate network capabilities) */ meshtastic_ExcludedModules_NETWORK_CONFIG = 16384 } meshtastic_ExcludedModules; /* 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, /* Airtime fairness rate limit exceeded for a packet This typically enforced per portnum and is used to prevent a single node from monopolizing airtime */ meshtastic_Routing_Error_RATE_LIMIT_EXCEEDED = 38, /* PKI encryption failed, due to no public key for the remote node This is different from PKI_UNKNOWN_PUBKEY which indicates a failure upon receiving a packet */ meshtastic_Routing_Error_PKI_SEND_FAIL_PUBLIC_KEY = 39 } meshtastic_Routing_Error; /* Enum of message types */ typedef enum _meshtastic_StoreForwardPlusPlus_SFPP_message_type { /* Send an announcement of the canonical tip of a chain */ meshtastic_StoreForwardPlusPlus_SFPP_message_type_CANON_ANNOUNCE = 0, /* Query whether a specific link is on the chain */ meshtastic_StoreForwardPlusPlus_SFPP_message_type_CHAIN_QUERY = 1, /* Request the next link in the chain */ meshtastic_StoreForwardPlusPlus_SFPP_message_type_LINK_REQUEST = 3, /* Provide a link to add to the chain */ meshtastic_StoreForwardPlusPlus_SFPP_message_type_LINK_PROVIDE = 4, /* If we must fragment, send the first half */ meshtastic_StoreForwardPlusPlus_SFPP_message_type_LINK_PROVIDE_FIRSTHALF = 5, /* If we must fragment, send the second half */ meshtastic_StoreForwardPlusPlus_SFPP_message_type_LINK_PROVIDE_SECONDHALF = 6 } meshtastic_StoreForwardPlusPlus_SFPP_message_type; /* 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, /* Higher priority alert message used for critical alerts which take priority over other reliable packets. */ meshtastic_MeshPacket_Priority_ALERT = 110, /* 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; /* Enum to identify which transport mechanism this packet arrived over */ typedef enum _meshtastic_MeshPacket_TransportMechanism { /* The default case is that the node generated a packet itself */ meshtastic_MeshPacket_TransportMechanism_TRANSPORT_INTERNAL = 0, /* Arrived via the primary LoRa radio */ meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA = 1, /* Arrived via a secondary LoRa radio */ meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA_ALT1 = 2, /* Arrived via a tertiary LoRa radio */ meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA_ALT2 = 3, /* Arrived via a quaternary LoRa radio */ meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA_ALT3 = 4, /* Arrived via an MQTT connection */ meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MQTT = 5, /* Arrived via Multicast UDP */ meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MULTICAST_UDP = 6, /* Arrived via API connection */ meshtastic_MeshPacket_TransportMechanism_TRANSPORT_API = 7 } meshtastic_MeshPacket_TransportMechanism; /* 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; /* Whether or not the node can be messaged */ bool has_is_unmessagable; bool is_unmessagable; } 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(233) 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; typedef PB_BYTES_ARRAY_T(32) meshtastic_KeyVerification_hash1_t; typedef PB_BYTES_ARRAY_T(32) meshtastic_KeyVerification_hash2_t; /* The actual over-the-mesh message doing KeyVerification */ typedef struct _meshtastic_KeyVerification { /* random value Selected by the requesting node */ uint64_t nonce; /* The final authoritative hash, only to be sent by NodeA at the end of the handshake */ meshtastic_KeyVerification_hash1_t hash1; /* The intermediary hash (actually derived from hash1), sent from NodeB to NodeA in response to the initial message. */ meshtastic_KeyVerification_hash2_t hash2; } meshtastic_KeyVerification; typedef PB_BYTES_ARRAY_T(32) meshtastic_StoreForwardPlusPlus_message_hash_t; typedef PB_BYTES_ARRAY_T(32) meshtastic_StoreForwardPlusPlus_commit_hash_t; typedef PB_BYTES_ARRAY_T(32) meshtastic_StoreForwardPlusPlus_root_hash_t; typedef PB_BYTES_ARRAY_T(240) meshtastic_StoreForwardPlusPlus_message_t; /* The actual over-the-mesh message doing store and forward++ */ typedef struct _meshtastic_StoreForwardPlusPlus { /* Which message type is this */ meshtastic_StoreForwardPlusPlus_SFPP_message_type sfpp_message_type; /* The hash of the specific message */ meshtastic_StoreForwardPlusPlus_message_hash_t message_hash; /* The hash of a link on a chain */ meshtastic_StoreForwardPlusPlus_commit_hash_t commit_hash; /* the root hash of a chain */ meshtastic_StoreForwardPlusPlus_root_hash_t root_hash; /* The encrypted bytes from a message */ meshtastic_StoreForwardPlusPlus_message_t message; /* Message ID of the contained message */ uint32_t encapsulated_id; /* Destination of the contained message */ uint32_t encapsulated_to; /* Sender of the contained message */ uint32_t encapsulated_from; /* The receive time of the message in question */ uint32_t encapsulated_rxtime; /* Used in a LINK_REQUEST to specify the message X spots back from head */ uint32_t chain_count; } meshtastic_StoreForwardPlusPlus; /* 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; /* Message for node status */ typedef struct _meshtastic_StatusMessage { char status[80]; } meshtastic_StatusMessage; 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 If the value is 4,294,967,295 (maximum value of an unsigned 32bit integer), this indicates that the packet was not destined for a specific node, but for a channel as indicated by the value of `channel` below. If the value is another, this indicates that the packet was destined for a specific node (i.e. a kind of "Direct Message" to this node) and not broadcast on a channel. */ 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 direct neighbor 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; /* Last byte of the node number of the node that should be used as the next hop in routing. Set by the firmware internally, clients are not supposed to set this. */ uint8_t next_hop; /* Last byte of the node number of the node that will relay/relayed this packet. Set by the firmware internally, clients are not supposed to set this. */ uint8_t relay_node; /* *Never* sent over the radio links. Timestamp after which this packet may be sent. Set by the firmware internally, clients are not supposed to set this. */ uint32_t tx_after; /* Indicates which transport mechanism this packet arrived over */ meshtastic_MeshPacket_TransportMechanism transport_mechanism; } 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 direct neighbor) */ 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; /* True if node is in our ignored list Persists between NodeDB internal clean ups */ bool is_ignored; /* True if node public key has been verified. Persists between NodeDB internal clean ups LSB 0 of the bitfield */ bool is_key_manually_verified; /* True if node has been muted Persistes between NodeDB internal clean ups */ bool is_muted; } 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; /* The PlatformIO environment used to build this firmware */ char pio_env[40]; /* The indicator for whether this device is running event firmware and which */ meshtastic_FirmwareEdition firmware_edition; /* The number of nodes in the nodedb. This is used by the phone to know how many NodeInfo packets to expect on want_config */ uint16_t nodedb_count; } 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; typedef struct _meshtastic_KeyVerificationNumberInform { uint64_t nonce; char remote_longname[40]; uint32_t security_number; } meshtastic_KeyVerificationNumberInform; typedef struct _meshtastic_KeyVerificationNumberRequest { uint64_t nonce; char remote_longname[40]; } meshtastic_KeyVerificationNumberRequest; typedef struct _meshtastic_KeyVerificationFinal { uint64_t nonce; char remote_longname[40]; bool isSender; char verification_characters[10]; } meshtastic_KeyVerificationFinal; typedef struct _meshtastic_DuplicatedPublicKey { char dummy_field; } meshtastic_DuplicatedPublicKey; typedef struct _meshtastic_LowEntropyKey { char dummy_field; } meshtastic_LowEntropyKey; /* 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]; pb_size_t which_payload_variant; union { meshtastic_KeyVerificationNumberInform key_verification_number_inform; meshtastic_KeyVerificationNumberRequest key_verification_number_request; meshtastic_KeyVerificationFinal key_verification_final; meshtastic_DuplicatedPublicKey duplicated_public_key; meshtastic_LowEntropyKey low_entropy_key; } payload_variant; } 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(233) 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; /* Bit field of boolean for excluded modules (bitwise OR of ExcludedModules) */ uint32_t excluded_modules; } 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 { /* The nonce of the heartbeat message */ uint32_t nonce; } 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_FirmwareEdition_MIN meshtastic_FirmwareEdition_VANILLA #define _meshtastic_FirmwareEdition_MAX meshtastic_FirmwareEdition_DIY_EDITION #define _meshtastic_FirmwareEdition_ARRAYSIZE ((meshtastic_FirmwareEdition)(meshtastic_FirmwareEdition_DIY_EDITION+1)) #define _meshtastic_ExcludedModules_MIN meshtastic_ExcludedModules_EXCLUDED_NONE #define _meshtastic_ExcludedModules_MAX meshtastic_ExcludedModules_NETWORK_CONFIG #define _meshtastic_ExcludedModules_ARRAYSIZE ((meshtastic_ExcludedModules)(meshtastic_ExcludedModules_NETWORK_CONFIG+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_PKI_SEND_FAIL_PUBLIC_KEY #define _meshtastic_Routing_Error_ARRAYSIZE ((meshtastic_Routing_Error)(meshtastic_Routing_Error_PKI_SEND_FAIL_PUBLIC_KEY+1)) #define _meshtastic_StoreForwardPlusPlus_SFPP_message_type_MIN meshtastic_StoreForwardPlusPlus_SFPP_message_type_CANON_ANNOUNCE #define _meshtastic_StoreForwardPlusPlus_SFPP_message_type_MAX meshtastic_StoreForwardPlusPlus_SFPP_message_type_LINK_PROVIDE_SECONDHALF #define _meshtastic_StoreForwardPlusPlus_SFPP_message_type_ARRAYSIZE ((meshtastic_StoreForwardPlusPlus_SFPP_message_type)(meshtastic_StoreForwardPlusPlus_SFPP_message_type_LINK_PROVIDE_SECONDHALF+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_MeshPacket_TransportMechanism_MIN meshtastic_MeshPacket_TransportMechanism_TRANSPORT_INTERNAL #define _meshtastic_MeshPacket_TransportMechanism_MAX meshtastic_MeshPacket_TransportMechanism_TRANSPORT_API #define _meshtastic_MeshPacket_TransportMechanism_ARRAYSIZE ((meshtastic_MeshPacket_TransportMechanism)(meshtastic_MeshPacket_TransportMechanism_TRANSPORT_API+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_StoreForwardPlusPlus_sfpp_message_type_ENUMTYPE meshtastic_StoreForwardPlusPlus_SFPP_message_type #define meshtastic_MeshPacket_priority_ENUMTYPE meshtastic_MeshPacket_Priority #define meshtastic_MeshPacket_delayed_ENUMTYPE meshtastic_MeshPacket_Delayed #define meshtastic_MeshPacket_transport_mechanism_ENUMTYPE meshtastic_MeshPacket_TransportMechanism #define meshtastic_MyNodeInfo_firmware_edition_ENUMTYPE meshtastic_FirmwareEdition #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}}, false, 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_KeyVerification_init_default {0, {0, {0}}, {0, {0}}} #define meshtastic_StoreForwardPlusPlus_init_default {_meshtastic_StoreForwardPlusPlus_SFPP_message_type_MIN, {0, {0}}, {0, {0}}, {0, {0}}, {0, {0}}, 0, 0, 0, 0, 0} #define meshtastic_Waypoint_init_default {0, false, 0, false, 0, 0, 0, "", "", 0} #define meshtastic_StatusMessage_init_default {""} #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, 0, 0, 0, _meshtastic_MeshPacket_TransportMechanism_MIN} #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, 0, 0, 0} #define meshtastic_MyNodeInfo_init_default {0, 0, 0, {0, {0}}, "", _meshtastic_FirmwareEdition_MIN, 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, "", 0, {meshtastic_KeyVerificationNumberInform_init_default}} #define meshtastic_KeyVerificationNumberInform_init_default {0, "", 0} #define meshtastic_KeyVerificationNumberRequest_init_default {0, ""} #define meshtastic_KeyVerificationFinal_init_default {0, "", 0, ""} #define meshtastic_DuplicatedPublicKey_init_default {0} #define meshtastic_LowEntropyKey_init_default {0} #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, 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}}, false, 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_KeyVerification_init_zero {0, {0, {0}}, {0, {0}}} #define meshtastic_StoreForwardPlusPlus_init_zero {_meshtastic_StoreForwardPlusPlus_SFPP_message_type_MIN, {0, {0}}, {0, {0}}, {0, {0}}, {0, {0}}, 0, 0, 0, 0, 0} #define meshtastic_Waypoint_init_zero {0, false, 0, false, 0, 0, 0, "", "", 0} #define meshtastic_StatusMessage_init_zero {""} #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, 0, 0, 0, _meshtastic_MeshPacket_TransportMechanism_MIN} #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, 0, 0, 0} #define meshtastic_MyNodeInfo_init_zero {0, 0, 0, {0, {0}}, "", _meshtastic_FirmwareEdition_MIN, 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, "", 0, {meshtastic_KeyVerificationNumberInform_init_zero}} #define meshtastic_KeyVerificationNumberInform_init_zero {0, "", 0} #define meshtastic_KeyVerificationNumberRequest_init_zero {0, ""} #define meshtastic_KeyVerificationFinal_init_zero {0, "", 0, ""} #define meshtastic_DuplicatedPublicKey_init_zero {0} #define meshtastic_LowEntropyKey_init_zero {0} #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, 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_User_is_unmessagable_tag 9 #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_KeyVerification_nonce_tag 1 #define meshtastic_KeyVerification_hash1_tag 2 #define meshtastic_KeyVerification_hash2_tag 3 #define meshtastic_StoreForwardPlusPlus_sfpp_message_type_tag 1 #define meshtastic_StoreForwardPlusPlus_message_hash_tag 2 #define meshtastic_StoreForwardPlusPlus_commit_hash_tag 3 #define meshtastic_StoreForwardPlusPlus_root_hash_tag 4 #define meshtastic_StoreForwardPlusPlus_message_tag 5 #define meshtastic_StoreForwardPlusPlus_encapsulated_id_tag 6 #define meshtastic_StoreForwardPlusPlus_encapsulated_to_tag 7 #define meshtastic_StoreForwardPlusPlus_encapsulated_from_tag 8 #define meshtastic_StoreForwardPlusPlus_encapsulated_rxtime_tag 9 #define meshtastic_StoreForwardPlusPlus_chain_count_tag 10 #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_StatusMessage_status_tag 1 #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_MeshPacket_next_hop_tag 18 #define meshtastic_MeshPacket_relay_node_tag 19 #define meshtastic_MeshPacket_tx_after_tag 20 #define meshtastic_MeshPacket_transport_mechanism_tag 21 #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_NodeInfo_is_ignored_tag 11 #define meshtastic_NodeInfo_is_key_manually_verified_tag 12 #define meshtastic_NodeInfo_is_muted_tag 13 #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_MyNodeInfo_pio_env_tag 13 #define meshtastic_MyNodeInfo_firmware_edition_tag 14 #define meshtastic_MyNodeInfo_nodedb_count_tag 15 #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_KeyVerificationNumberInform_nonce_tag 1 #define meshtastic_KeyVerificationNumberInform_remote_longname_tag 2 #define meshtastic_KeyVerificationNumberInform_security_number_tag 3 #define meshtastic_KeyVerificationNumberRequest_nonce_tag 1 #define meshtastic_KeyVerificationNumberRequest_remote_longname_tag 2 #define meshtastic_KeyVerificationFinal_nonce_tag 1 #define meshtastic_KeyVerificationFinal_remote_longname_tag 2 #define meshtastic_KeyVerificationFinal_isSender_tag 3 #define meshtastic_KeyVerificationFinal_verification_characters_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_ClientNotification_key_verification_number_inform_tag 11 #define meshtastic_ClientNotification_key_verification_number_request_tag 12 #define meshtastic_ClientNotification_key_verification_final_tag 13 #define meshtastic_ClientNotification_duplicated_public_key_tag 14 #define meshtastic_ClientNotification_low_entropy_key_tag 15 #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_DeviceMetadata_excluded_modules_tag 12 #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_Heartbeat_nonce_tag 1 #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) \ X(a, STATIC, OPTIONAL, BOOL, is_unmessagable, 9) #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_KeyVerification_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, UINT64, nonce, 1) \ X(a, STATIC, SINGULAR, BYTES, hash1, 2) \ X(a, STATIC, SINGULAR, BYTES, hash2, 3) #define meshtastic_KeyVerification_CALLBACK NULL #define meshtastic_KeyVerification_DEFAULT NULL #define meshtastic_StoreForwardPlusPlus_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, UENUM, sfpp_message_type, 1) \ X(a, STATIC, SINGULAR, BYTES, message_hash, 2) \ X(a, STATIC, SINGULAR, BYTES, commit_hash, 3) \ X(a, STATIC, SINGULAR, BYTES, root_hash, 4) \ X(a, STATIC, SINGULAR, BYTES, message, 5) \ X(a, STATIC, SINGULAR, UINT32, encapsulated_id, 6) \ X(a, STATIC, SINGULAR, UINT32, encapsulated_to, 7) \ X(a, STATIC, SINGULAR, UINT32, encapsulated_from, 8) \ X(a, STATIC, SINGULAR, UINT32, encapsulated_rxtime, 9) \ X(a, STATIC, SINGULAR, UINT32, chain_count, 10) #define meshtastic_StoreForwardPlusPlus_CALLBACK NULL #define meshtastic_StoreForwardPlusPlus_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_StatusMessage_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, STRING, status, 1) #define meshtastic_StatusMessage_CALLBACK NULL #define meshtastic_StatusMessage_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) \ X(a, STATIC, SINGULAR, UINT32, next_hop, 18) \ X(a, STATIC, SINGULAR, UINT32, relay_node, 19) \ X(a, STATIC, SINGULAR, UINT32, tx_after, 20) \ X(a, STATIC, SINGULAR, UENUM, transport_mechanism, 21) #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) \ X(a, STATIC, SINGULAR, BOOL, is_ignored, 11) \ X(a, STATIC, SINGULAR, BOOL, is_key_manually_verified, 12) \ X(a, STATIC, SINGULAR, BOOL, is_muted, 13) #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) \ X(a, STATIC, SINGULAR, STRING, pio_env, 13) \ X(a, STATIC, SINGULAR, UENUM, firmware_edition, 14) \ X(a, STATIC, SINGULAR, UINT32, nodedb_count, 15) #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) \ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,key_verification_number_inform,payload_variant.key_verification_number_inform), 11) \ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,key_verification_number_request,payload_variant.key_verification_number_request), 12) \ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,key_verification_final,payload_variant.key_verification_final), 13) \ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,duplicated_public_key,payload_variant.duplicated_public_key), 14) \ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,low_entropy_key,payload_variant.low_entropy_key), 15) #define meshtastic_ClientNotification_CALLBACK NULL #define meshtastic_ClientNotification_DEFAULT NULL #define meshtastic_ClientNotification_payload_variant_key_verification_number_inform_MSGTYPE meshtastic_KeyVerificationNumberInform #define meshtastic_ClientNotification_payload_variant_key_verification_number_request_MSGTYPE meshtastic_KeyVerificationNumberRequest #define meshtastic_ClientNotification_payload_variant_key_verification_final_MSGTYPE meshtastic_KeyVerificationFinal #define meshtastic_ClientNotification_payload_variant_duplicated_public_key_MSGTYPE meshtastic_DuplicatedPublicKey #define meshtastic_ClientNotification_payload_variant_low_entropy_key_MSGTYPE meshtastic_LowEntropyKey #define meshtastic_KeyVerificationNumberInform_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, UINT64, nonce, 1) \ X(a, STATIC, SINGULAR, STRING, remote_longname, 2) \ X(a, STATIC, SINGULAR, UINT32, security_number, 3) #define meshtastic_KeyVerificationNumberInform_CALLBACK NULL #define meshtastic_KeyVerificationNumberInform_DEFAULT NULL #define meshtastic_KeyVerificationNumberRequest_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, UINT64, nonce, 1) \ X(a, STATIC, SINGULAR, STRING, remote_longname, 2) #define meshtastic_KeyVerificationNumberRequest_CALLBACK NULL #define meshtastic_KeyVerificationNumberRequest_DEFAULT NULL #define meshtastic_KeyVerificationFinal_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, UINT64, nonce, 1) \ X(a, STATIC, SINGULAR, STRING, remote_longname, 2) \ X(a, STATIC, SINGULAR, BOOL, isSender, 3) \ X(a, STATIC, SINGULAR, STRING, verification_characters, 4) #define meshtastic_KeyVerificationFinal_CALLBACK NULL #define meshtastic_KeyVerificationFinal_DEFAULT NULL #define meshtastic_DuplicatedPublicKey_FIELDLIST(X, a) \ #define meshtastic_DuplicatedPublicKey_CALLBACK NULL #define meshtastic_DuplicatedPublicKey_DEFAULT NULL #define meshtastic_LowEntropyKey_FIELDLIST(X, a) \ #define meshtastic_LowEntropyKey_CALLBACK NULL #define meshtastic_LowEntropyKey_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) \ X(a, STATIC, SINGULAR, UINT32, excluded_modules, 12) #define meshtastic_DeviceMetadata_CALLBACK NULL #define meshtastic_DeviceMetadata_DEFAULT NULL #define meshtastic_Heartbeat_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, UINT32, nonce, 1) #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_KeyVerification_msg; extern const pb_msgdesc_t meshtastic_StoreForwardPlusPlus_msg; extern const pb_msgdesc_t meshtastic_Waypoint_msg; extern const pb_msgdesc_t meshtastic_StatusMessage_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_KeyVerificationNumberInform_msg; extern const pb_msgdesc_t meshtastic_KeyVerificationNumberRequest_msg; extern const pb_msgdesc_t meshtastic_KeyVerificationFinal_msg; extern const pb_msgdesc_t meshtastic_DuplicatedPublicKey_msg; extern const pb_msgdesc_t meshtastic_LowEntropyKey_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_KeyVerification_fields &meshtastic_KeyVerification_msg #define meshtastic_StoreForwardPlusPlus_fields &meshtastic_StoreForwardPlusPlus_msg #define meshtastic_Waypoint_fields &meshtastic_Waypoint_msg #define meshtastic_StatusMessage_fields &meshtastic_StatusMessage_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_KeyVerificationNumberInform_fields &meshtastic_KeyVerificationNumberInform_msg #define meshtastic_KeyVerificationNumberRequest_fields &meshtastic_KeyVerificationNumberRequest_msg #define meshtastic_KeyVerificationFinal_fields &meshtastic_KeyVerificationFinal_msg #define meshtastic_DuplicatedPublicKey_fields &meshtastic_DuplicatedPublicKey_msg #define meshtastic_LowEntropyKey_fields &meshtastic_LowEntropyKey_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 482 #define meshtastic_Compressed_size 239 #define meshtastic_Data_size 269 #define meshtastic_DeviceMetadata_size 54 #define meshtastic_DuplicatedPublicKey_size 0 #define meshtastic_FileInfo_size 236 #define meshtastic_FromRadio_size 510 #define meshtastic_Heartbeat_size 6 #define meshtastic_KeyVerificationFinal_size 65 #define meshtastic_KeyVerificationNumberInform_size 58 #define meshtastic_KeyVerificationNumberRequest_size 52 #define meshtastic_KeyVerification_size 79 #define meshtastic_LogRecord_size 426 #define meshtastic_LowEntropyKey_size 0 #define meshtastic_MeshPacket_size 381 #define meshtastic_MqttClientProxyMessage_size 501 #define meshtastic_MyNodeInfo_size 83 #define meshtastic_NeighborInfo_size 258 #define meshtastic_Neighbor_size 22 #define meshtastic_NodeInfo_size 325 #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_StatusMessage_size 81 #define meshtastic_StoreForwardPlusPlus_size 377 #define meshtastic_ToRadio_size 504 #define meshtastic_User_size 115 #define meshtastic_Waypoint_size 165 #ifdef __cplusplus } /* extern "C" */ #endif #endif ================================================ FILE: src/mesh/generated/meshtastic/module_config.pb.cpp ================================================ /* Automatically generated nanopb constant definitions */ /* Generated by nanopb-0.4.9.1 */ #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, AUTO) PB_BIND(meshtastic_ModuleConfig_MQTTConfig, meshtastic_ModuleConfig_MQTTConfig, AUTO) 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_TrafficManagementConfig, meshtastic_ModuleConfig_TrafficManagementConfig, 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_ModuleConfig_StatusMessageConfig, meshtastic_ModuleConfig_StatusMessageConfig, AUTO) PB_BIND(meshtastic_ModuleConfig_TAKConfig, meshtastic_ModuleConfig_TAKConfig, AUTO) PB_BIND(meshtastic_RemoteHardwarePin, meshtastic_RemoteHardwarePin, AUTO) ================================================ FILE: src/mesh/generated/meshtastic/module_config.pb.h ================================================ /* Automatically generated nanopb header */ /* Generated by nanopb-0.4.9.1 */ #ifndef PB_MESHTASTIC_MESHTASTIC_MODULE_CONFIG_PB_H_INCLUDED #define PB_MESHTASTIC_MESHTASTIC_MODULE_CONFIG_PB_H_INCLUDED #include #include "meshtastic/atak.pb.h" #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, /* VE.Direct is a serial protocol used by Victron Energy products https://beta.ivc.no/wiki/index.php/Victron_VE_Direct_DIY_Cable */ meshtastic_ModuleConfig_SerialConfig_Serial_Mode_VE_DIRECT = 7, /* Used to configure and view some parameters of MeshSolar. https://heltec.org/project/meshsolar/ */ meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MS_CONFIG = 8, /* Logs mesh traffic to the serial pins, ideal for logging via openLog or similar. */ meshtastic_ModuleConfig_SerialConfig_Serial_Mode_LOG = 9, /* includes other packets */ meshtastic_ModuleConfig_SerialConfig_Serial_Mode_LOGTEXT = 10 /* only text (channel & DM) */ } 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; /* Whether we have opted-in to report our location to the map */ bool should_report_location; } 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[32]; /* 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 (minimum is 14400, i.e., 4 hours) */ uint32_t update_interval; /* Whether in addition to sending it to MQTT and the PhoneAPI, our NeighborInfo should be transmitted over LoRa. Note that this is not available on a channel with default key and name. */ bool transmit_over_lora; } 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; /* Config for the Traffic Management module. Provides packet inspection and traffic shaping to help reduce channel utilization */ typedef struct _meshtastic_ModuleConfig_TrafficManagementConfig { /* Master enable for traffic management module */ bool enabled; /* Enable position deduplication to drop redundant position broadcasts */ bool position_dedup_enabled; /* Number of bits of precision for position deduplication (0-32) */ uint32_t position_precision_bits; /* Minimum interval in seconds between position updates from the same node */ uint32_t position_min_interval_secs; /* Enable direct response to NodeInfo requests from local cache */ bool nodeinfo_direct_response; /* Minimum hop distance from requestor before responding to NodeInfo requests */ uint32_t nodeinfo_direct_response_max_hops; /* Enable per-node rate limiting to throttle chatty nodes */ bool rate_limit_enabled; /* Time window in seconds for rate limiting calculations */ uint32_t rate_limit_window_secs; /* Maximum packets allowed per node within the rate limit window */ uint32_t rate_limit_max_packets; /* Enable dropping of unknown/undecryptable packets per rate_limit_window_secs */ bool drop_unknown_enabled; /* Number of unknown packets before dropping from a node */ uint32_t unknown_packet_threshold; /* Set hop_limit to 0 for relayed telemetry broadcasts (own packets unaffected) */ bool exhaust_hop_telemetry; /* Set hop_limit to 0 for relayed position broadcasts (own packets unaffected) */ bool exhaust_hop_position; /* Preserve hop_limit for router-to-router traffic */ bool router_preserve_hops; } meshtastic_ModuleConfig_TrafficManagementConfig; /* 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; /* Bool indicating that the node should cleanup / destroy it's RangeTest.csv file. ESP32 Only */ bool clear_on_reboot; } 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; /* Enable/Disable the device telemetry module to send metrics to the mesh Note: We will still send telemtry to the connected phone / client every minute over the API */ bool device_telemetry_enabled; /* Enable/Disable the air quality telemetry measurement module on-device display */ bool air_quality_screen_enabled; } meshtastic_ModuleConfig_TelemetryConfig; /* Canned Messages Module Config */ 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; /* StatusMessage config - Allows setting a status message for a node to periodically rebroadcast */ typedef struct _meshtastic_ModuleConfig_StatusMessageConfig { /* The actual status string */ char node_status[80]; } meshtastic_ModuleConfig_StatusMessageConfig; /* TAK team/role configuration */ typedef struct _meshtastic_ModuleConfig_TAKConfig { /* Team color. Default Unspecifed_Color -> firmware uses Cyan */ meshtastic_Team team; /* Member role. Default Unspecifed -> firmware uses TeamMember */ meshtastic_MemberRole role; } meshtastic_ModuleConfig_TAKConfig; /* 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; /* TODO: REPLACE */ meshtastic_ModuleConfig_StatusMessageConfig statusmessage; /* Traffic management module config for mesh network optimization */ meshtastic_ModuleConfig_TrafficManagementConfig traffic_management; /* TAK team/role configuration for TAK_TRACKER */ meshtastic_ModuleConfig_TAKConfig tak; } 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_LOGTEXT #define _meshtastic_ModuleConfig_SerialConfig_Serial_Mode_ARRAYSIZE ((meshtastic_ModuleConfig_SerialConfig_Serial_Mode)(meshtastic_ModuleConfig_SerialConfig_Serial_Mode_LOGTEXT+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_ModuleConfig_TAKConfig_team_ENUMTYPE meshtastic_Team #define meshtastic_ModuleConfig_TAKConfig_role_ENUMTYPE meshtastic_MemberRole #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, 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, 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_TrafficManagementConfig_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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, 0} #define meshtastic_ModuleConfig_TelemetryConfig_init_default {0, 0, 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_ModuleConfig_StatusMessageConfig_init_default {""} #define meshtastic_ModuleConfig_TAKConfig_init_default {_meshtastic_Team_MIN, _meshtastic_MemberRole_MIN} #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, 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, 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_TrafficManagementConfig_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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, 0} #define meshtastic_ModuleConfig_TelemetryConfig_init_zero {0, 0, 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_ModuleConfig_StatusMessageConfig_init_zero {""} #define meshtastic_ModuleConfig_TAKConfig_init_zero {_meshtastic_Team_MIN, _meshtastic_MemberRole_MIN} #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_MapReportSettings_should_report_location_tag 3 #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_NeighborInfoConfig_transmit_over_lora_tag 3 #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_TrafficManagementConfig_enabled_tag 1 #define meshtastic_ModuleConfig_TrafficManagementConfig_position_dedup_enabled_tag 2 #define meshtastic_ModuleConfig_TrafficManagementConfig_position_precision_bits_tag 3 #define meshtastic_ModuleConfig_TrafficManagementConfig_position_min_interval_secs_tag 4 #define meshtastic_ModuleConfig_TrafficManagementConfig_nodeinfo_direct_response_tag 5 #define meshtastic_ModuleConfig_TrafficManagementConfig_nodeinfo_direct_response_max_hops_tag 6 #define meshtastic_ModuleConfig_TrafficManagementConfig_rate_limit_enabled_tag 7 #define meshtastic_ModuleConfig_TrafficManagementConfig_rate_limit_window_secs_tag 8 #define meshtastic_ModuleConfig_TrafficManagementConfig_rate_limit_max_packets_tag 9 #define meshtastic_ModuleConfig_TrafficManagementConfig_drop_unknown_enabled_tag 10 #define meshtastic_ModuleConfig_TrafficManagementConfig_unknown_packet_threshold_tag 11 #define meshtastic_ModuleConfig_TrafficManagementConfig_exhaust_hop_telemetry_tag 12 #define meshtastic_ModuleConfig_TrafficManagementConfig_exhaust_hop_position_tag 13 #define meshtastic_ModuleConfig_TrafficManagementConfig_router_preserve_hops_tag 14 #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_RangeTestConfig_clear_on_reboot_tag 4 #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_TelemetryConfig_device_telemetry_enabled_tag 14 #define meshtastic_ModuleConfig_TelemetryConfig_air_quality_screen_enabled_tag 15 #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_ModuleConfig_StatusMessageConfig_node_status_tag 1 #define meshtastic_ModuleConfig_TAKConfig_team_tag 1 #define meshtastic_ModuleConfig_TAKConfig_role_tag 2 #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 #define meshtastic_ModuleConfig_statusmessage_tag 14 #define meshtastic_ModuleConfig_traffic_management_tag 15 #define meshtastic_ModuleConfig_tak_tag 16 /* 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) \ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,statusmessage,payload_variant.statusmessage), 14) \ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,traffic_management,payload_variant.traffic_management), 15) \ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,tak,payload_variant.tak), 16) #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_payload_variant_statusmessage_MSGTYPE meshtastic_ModuleConfig_StatusMessageConfig #define meshtastic_ModuleConfig_payload_variant_traffic_management_MSGTYPE meshtastic_ModuleConfig_TrafficManagementConfig #define meshtastic_ModuleConfig_payload_variant_tak_MSGTYPE meshtastic_ModuleConfig_TAKConfig #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) \ X(a, STATIC, SINGULAR, BOOL, should_report_location, 3) #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) \ X(a, STATIC, SINGULAR, BOOL, transmit_over_lora, 3) #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_TrafficManagementConfig_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, BOOL, enabled, 1) \ X(a, STATIC, SINGULAR, BOOL, position_dedup_enabled, 2) \ X(a, STATIC, SINGULAR, UINT32, position_precision_bits, 3) \ X(a, STATIC, SINGULAR, UINT32, position_min_interval_secs, 4) \ X(a, STATIC, SINGULAR, BOOL, nodeinfo_direct_response, 5) \ X(a, STATIC, SINGULAR, UINT32, nodeinfo_direct_response_max_hops, 6) \ X(a, STATIC, SINGULAR, BOOL, rate_limit_enabled, 7) \ X(a, STATIC, SINGULAR, UINT32, rate_limit_window_secs, 8) \ X(a, STATIC, SINGULAR, UINT32, rate_limit_max_packets, 9) \ X(a, STATIC, SINGULAR, BOOL, drop_unknown_enabled, 10) \ X(a, STATIC, SINGULAR, UINT32, unknown_packet_threshold, 11) \ X(a, STATIC, SINGULAR, BOOL, exhaust_hop_telemetry, 12) \ X(a, STATIC, SINGULAR, BOOL, exhaust_hop_position, 13) \ X(a, STATIC, SINGULAR, BOOL, router_preserve_hops, 14) #define meshtastic_ModuleConfig_TrafficManagementConfig_CALLBACK NULL #define meshtastic_ModuleConfig_TrafficManagementConfig_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) \ X(a, STATIC, SINGULAR, BOOL, clear_on_reboot, 4) #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) \ X(a, STATIC, SINGULAR, BOOL, device_telemetry_enabled, 14) \ X(a, STATIC, SINGULAR, BOOL, air_quality_screen_enabled, 15) #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_ModuleConfig_StatusMessageConfig_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, STRING, node_status, 1) #define meshtastic_ModuleConfig_StatusMessageConfig_CALLBACK NULL #define meshtastic_ModuleConfig_StatusMessageConfig_DEFAULT NULL #define meshtastic_ModuleConfig_TAKConfig_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, UENUM, team, 1) \ X(a, STATIC, SINGULAR, UENUM, role, 2) #define meshtastic_ModuleConfig_TAKConfig_CALLBACK NULL #define meshtastic_ModuleConfig_TAKConfig_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_TrafficManagementConfig_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_ModuleConfig_StatusMessageConfig_msg; extern const pb_msgdesc_t meshtastic_ModuleConfig_TAKConfig_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_TrafficManagementConfig_fields &meshtastic_ModuleConfig_TrafficManagementConfig_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_ModuleConfig_StatusMessageConfig_fields &meshtastic_ModuleConfig_StatusMessageConfig_msg #define meshtastic_ModuleConfig_TAKConfig_fields &meshtastic_ModuleConfig_TAKConfig_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 224 #define meshtastic_ModuleConfig_MapReportSettings_size 14 #define meshtastic_ModuleConfig_NeighborInfoConfig_size 10 #define meshtastic_ModuleConfig_PaxcounterConfig_size 30 #define meshtastic_ModuleConfig_RangeTestConfig_size 12 #define meshtastic_ModuleConfig_RemoteHardwareConfig_size 96 #define meshtastic_ModuleConfig_SerialConfig_size 28 #define meshtastic_ModuleConfig_StatusMessageConfig_size 81 #define meshtastic_ModuleConfig_StoreForwardConfig_size 24 #define meshtastic_ModuleConfig_TAKConfig_size 4 #define meshtastic_ModuleConfig_TelemetryConfig_size 50 #define meshtastic_ModuleConfig_TrafficManagementConfig_size 52 #define meshtastic_ModuleConfig_size 227 #define meshtastic_RemoteHardwarePin_size 21 #ifdef __cplusplus } /* extern "C" */ #endif #endif ================================================ FILE: src/mesh/generated/meshtastic/mqtt.pb.cpp ================================================ /* Automatically generated nanopb constant definitions */ /* Generated by nanopb-0.4.9.1 */ #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) ================================================ FILE: src/mesh/generated/meshtastic/mqtt.pb.h ================================================ /* Automatically generated nanopb header */ /* Generated by nanopb-0.4.9.1 */ #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; /* User has opted in to share their location (map report) with the mqtt server Controlled by map_report.should_report_location */ bool has_opted_report_location; } 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, 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, 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 #define meshtastic_MapReport_has_opted_report_location_tag 14 /* 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) \ X(a, STATIC, SINGULAR, BOOL, has_opted_report_location, 14) #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 110 #ifdef __cplusplus } /* extern "C" */ #endif #endif ================================================ FILE: src/mesh/generated/meshtastic/paxcount.pb.cpp ================================================ /* Automatically generated nanopb constant definitions */ /* Generated by nanopb-0.4.9.1 */ #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) ================================================ FILE: src/mesh/generated/meshtastic/paxcount.pb.h ================================================ /* Automatically generated nanopb header */ /* Generated by nanopb-0.4.9.1 */ #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 ================================================ FILE: src/mesh/generated/meshtastic/portnums.pb.cpp ================================================ /* Automatically generated nanopb constant definitions */ /* Generated by nanopb-0.4.9.1 */ #include "meshtastic/portnums.pb.h" #if PB_PROTO_HEADER_VERSION != 40 #error Regenerate this file with the current version of nanopb generator. #endif ================================================ FILE: src/mesh/generated/meshtastic/portnums.pb.h ================================================ /* Automatically generated nanopb header */ /* Generated by nanopb-0.4.9.1 */ #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, /* Same as Text Message but used for critical alerts. */ meshtastic_PortNum_ALERT_APP = 11, /* Module/port for handling key verification requests. */ meshtastic_PortNum_KEY_VERIFICATION_APP = 12, /* 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, /* Store and Forward++ module included in the firmware ENCODING: protobuf This module is specifically for Native Linux nodes, and provides a Git-style chain of messages. */ meshtastic_PortNum_STORE_FORWARD_PLUSPLUS_APP = 35, /* Node Status module ENCODING: protobuf This module allows setting an extra string of status for a node. Broadcasts on change and on a timer, possibly once a day. */ meshtastic_PortNum_NODE_STATUS_APP = 36, /* 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, /* LoraWAN Payload Transport ENCODING: compact binary LoRaWAN uplink (10-byte RF metadata + PHY payload) - see LoRaWANBridgeModule */ meshtastic_PortNum_LORAWAN_BRIDGE = 75, /* Reticulum Network Stack Tunnel App ENCODING: Fragmented RNS Packet. Handled by Meshtastic RNS interface */ meshtastic_PortNum_RETICULUM_TUNNEL_APP = 76, /* App for transporting Cayenne Low Power Payload, popular for LoRaWAN sensor nodes. Offers ability to send arbitrary telemetry over meshtastic that is not covered by telemetry.proto ENCODING: CayenneLLP */ meshtastic_PortNum_CAYENNE_APP = 77, /* GroupAlarm integration Used for transporting GroupAlarm-related messages between Meshtastic nodes and companion applications/services. */ meshtastic_PortNum_GROUPALARM_APP = 112, /* 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 ================================================ FILE: src/mesh/generated/meshtastic/powermon.pb.cpp ================================================ /* Automatically generated nanopb constant definitions */ /* Generated by nanopb-0.4.9.1 */ #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) ================================================ FILE: src/mesh/generated/meshtastic/powermon.pb.h ================================================ /* Automatically generated nanopb header */ /* Generated by nanopb-0.4.9.1 */ #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 ================================================ FILE: src/mesh/generated/meshtastic/remote_hardware.pb.cpp ================================================ /* Automatically generated nanopb constant definitions */ /* Generated by nanopb-0.4.9.1 */ #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) ================================================ FILE: src/mesh/generated/meshtastic/remote_hardware.pb.h ================================================ /* Automatically generated nanopb header */ /* Generated by nanopb-0.4.9.1 */ #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 ================================================ FILE: src/mesh/generated/meshtastic/rtttl.pb.cpp ================================================ /* Automatically generated nanopb constant definitions */ /* Generated by nanopb-0.4.9.1 */ #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) ================================================ FILE: src/mesh/generated/meshtastic/rtttl.pb.h ================================================ /* Automatically generated nanopb header */ /* Generated by nanopb-0.4.9.1 */ #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 ================================================ FILE: src/mesh/generated/meshtastic/storeforward.pb.cpp ================================================ /* Automatically generated nanopb constant definitions */ /* Generated by nanopb-0.4.9.1 */ #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) ================================================ FILE: src/mesh/generated/meshtastic/storeforward.pb.h ================================================ /* Automatically generated nanopb header */ /* Generated by nanopb-0.4.9.1 */ #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(233) 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 238 #ifdef __cplusplus } /* extern "C" */ #endif #endif ================================================ FILE: src/mesh/generated/meshtastic/telemetry.pb.cpp ================================================ /* Automatically generated nanopb constant definitions */ /* Generated by nanopb-0.4.9.1 */ #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_TrafficManagementStats, meshtastic_TrafficManagementStats, AUTO) PB_BIND(meshtastic_HealthMetrics, meshtastic_HealthMetrics, AUTO) PB_BIND(meshtastic_HostMetrics, meshtastic_HostMetrics, 2) PB_BIND(meshtastic_Telemetry, meshtastic_Telemetry, 2) PB_BIND(meshtastic_Nau7802Config, meshtastic_Nau7802Config, AUTO) PB_BIND(meshtastic_SEN5XState, meshtastic_SEN5XState, AUTO) ================================================ FILE: src/mesh/generated/meshtastic/telemetry.pb.h ================================================ /* Automatically generated nanopb header */ /* Generated by nanopb-0.4.9.1 */ #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, /* TODO - REMOVE 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, /* TODO - REMOVE 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, /* TODO - REMOVE 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, /* ClimateGuard RadSens, radiation, Geiger-Muller Tube */ meshtastic_TelemetrySensorType_RADSENS = 33, /* High accuracy current and voltage */ meshtastic_TelemetrySensorType_INA226 = 34, /* DFRobot Gravity tipping bucket rain gauge */ meshtastic_TelemetrySensorType_DFROBOT_RAIN = 35, /* Infineon DPS310 High accuracy pressure and temperature */ meshtastic_TelemetrySensorType_DPS310 = 36, /* RAKWireless RAK12035 Soil Moisture Sensor Module */ meshtastic_TelemetrySensorType_RAK12035 = 37, /* MAX17261 lipo battery gauge */ meshtastic_TelemetrySensorType_MAX17261 = 38, /* PCT2075 Temperature Sensor */ meshtastic_TelemetrySensorType_PCT2075 = 39, /* ADS1X15 ADC */ meshtastic_TelemetrySensorType_ADS1X15 = 40, /* ADS1X15 ADC_ALT */ meshtastic_TelemetrySensorType_ADS1X15_ALT = 41, /* Sensirion SFA30 Formaldehyde sensor */ meshtastic_TelemetrySensorType_SFA30 = 42, /* SEN5X PM SENSORS */ meshtastic_TelemetrySensorType_SEN5X = 43, /* TSL2561 light sensor */ meshtastic_TelemetrySensorType_TSL2561 = 44, /* BH1750 light sensor */ meshtastic_TelemetrySensorType_BH1750 = 45, /* HDC1080 Temperature and Humidity Sensor */ meshtastic_TelemetrySensorType_HDC1080 = 46, /* TODO - REMOVE STH21 Temperature and R. Humidity sensor */ meshtastic_TelemetrySensorType_SHT21 = 47, /* Sensirion STC31 CO2 sensor */ meshtastic_TelemetrySensorType_STC31 = 48, /* SCD30 CO2, humidity, temperature sensor */ meshtastic_TelemetrySensorType_SCD30 = 49, /* SHT family of sensors for temperature and humidity */ meshtastic_TelemetrySensorType_SHTXX = 50 } 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; /* Radiation in µR/h */ bool has_radiation; float radiation; /* Rainfall in the last hour in mm */ bool has_rainfall_1h; float rainfall_1h; /* Rainfall in the last 24 hours in mm */ bool has_rainfall_24h; float rainfall_24h; /* Soil moisture measured (% 1-100) */ bool has_soil_moisture; uint8_t soil_moisture; /* Soil temperature measured (*C) */ bool has_soil_temperature; float soil_temperature; } 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; /* Voltage (Ch4) */ bool has_ch4_voltage; float ch4_voltage; /* Current (Ch4) */ bool has_ch4_current; float ch4_current; /* Voltage (Ch5) */ bool has_ch5_voltage; float ch5_voltage; /* Current (Ch5) */ bool has_ch5_current; float ch5_current; /* Voltage (Ch6) */ bool has_ch6_voltage; float ch6_voltage; /* Current (Ch6) */ bool has_ch6_current; float ch6_current; /* Voltage (Ch7) */ bool has_ch7_voltage; float ch7_voltage; /* Current (Ch7) */ bool has_ch7_current; float ch7_current; /* Voltage (Ch8) */ bool has_ch8_voltage; float ch8_voltage; /* Current (Ch8) */ bool has_ch8_current; float ch8_current; } meshtastic_PowerMetrics; /* Air quality metrics */ typedef struct _meshtastic_AirQualityMetrics { /* Concentration Units Standard PM1.0 in ug/m3 */ bool has_pm10_standard; uint32_t pm10_standard; /* Concentration Units Standard PM2.5 in ug/m3 */ bool has_pm25_standard; uint32_t pm25_standard; /* Concentration Units Standard PM10.0 in ug/m3 */ bool has_pm100_standard; uint32_t pm100_standard; /* Concentration Units Environmental PM1.0 in ug/m3 */ bool has_pm10_environmental; uint32_t pm10_environmental; /* Concentration Units Environmental PM2.5 in ug/m3 */ bool has_pm25_environmental; uint32_t pm25_environmental; /* Concentration Units Environmental PM10.0 in ug/m3 */ bool has_pm100_environmental; uint32_t pm100_environmental; /* 0.3um Particle Count in #/0.1l */ bool has_particles_03um; uint32_t particles_03um; /* 0.5um Particle Count in #/0.1l */ bool has_particles_05um; uint32_t particles_05um; /* 1.0um Particle Count in #/0.1l */ bool has_particles_10um; uint32_t particles_10um; /* 2.5um Particle Count in #/0.1l */ bool has_particles_25um; uint32_t particles_25um; /* 5.0um Particle Count in #/0.1l */ bool has_particles_50um; uint32_t particles_50um; /* 10.0um Particle Count in #/0.1l */ bool has_particles_100um; uint32_t particles_100um; /* CO2 concentration in ppm */ bool has_co2; uint32_t co2; /* CO2 sensor temperature in degC */ bool has_co2_temperature; float co2_temperature; /* CO2 sensor relative humidity in % */ bool has_co2_humidity; float co2_humidity; /* Formaldehyde sensor formaldehyde concentration in ppb */ bool has_form_formaldehyde; float form_formaldehyde; /* Formaldehyde sensor relative humidity in %RH */ bool has_form_humidity; float form_humidity; /* Formaldehyde sensor temperature in degrees Celsius */ bool has_form_temperature; float form_temperature; /* Concentration Units Standard PM4.0 in ug/m3 */ bool has_pm40_standard; uint32_t pm40_standard; /* 4.0um Particle Count in #/0.1l */ bool has_particles_40um; uint32_t particles_40um; /* PM Sensor Temperature */ bool has_pm_temperature; float pm_temperature; /* PM Sensor humidity */ bool has_pm_humidity; float pm_humidity; /* PM Sensor VOC Index */ bool has_pm_voc_idx; float pm_voc_idx; /* PM Sensor NOx Index */ bool has_pm_nox_idx; float pm_nox_idx; /* Typical Particle Size in um */ bool has_particles_tps; float particles_tps; } 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; /* Number of bytes used in the heap */ uint32_t heap_total_bytes; /* Number of bytes free in the heap */ uint32_t heap_free_bytes; /* Number of packets that were dropped because the transmit queue was full. */ uint16_t num_tx_dropped; /* Noise floor value measured in dBm */ int32_t noise_floor; } meshtastic_LocalStats; /* Traffic management statistics for mesh network optimization */ typedef struct _meshtastic_TrafficManagementStats { /* Total number of packets inspected by traffic management */ uint32_t packets_inspected; /* Number of position packets dropped due to deduplication */ uint32_t position_dedup_drops; /* Number of NodeInfo requests answered from cache */ uint32_t nodeinfo_cache_hits; /* Number of packets dropped due to rate limiting */ uint32_t rate_limit_drops; /* Number of unknown/undecryptable packets dropped */ uint32_t unknown_packet_drops; /* Number of packets with hop_limit exhausted for local-only broadcast */ uint32_t hop_exhausted_packets; /* Number of times router hop preservation was applied */ uint32_t router_hops_preserved; } meshtastic_TrafficManagementStats; /* 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; /* Linux host metrics */ typedef struct _meshtastic_HostMetrics { /* Host system uptime */ uint32_t uptime_seconds; /* Host system free memory */ uint64_t freemem_bytes; /* Host system disk space free for / */ uint64_t diskfree1_bytes; /* Secondary system disk space free */ bool has_diskfree2_bytes; uint64_t diskfree2_bytes; /* Tertiary disk space free */ bool has_diskfree3_bytes; uint64_t diskfree3_bytes; /* Host system one minute load in 1/100ths */ uint16_t load1; /* Host system five minute load in 1/100ths */ uint16_t load5; /* Host system fifteen minute load in 1/100ths */ uint16_t load15; /* Optional User-provided string for arbitrary host system information that doesn't make sense as a dedicated entry. */ bool has_user_string; char user_string[200]; } meshtastic_HostMetrics; /* 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; /* Linux host metrics */ meshtastic_HostMetrics host_metrics; /* Traffic management statistics */ meshtastic_TrafficManagementStats traffic_management_stats; } 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; /* SEN5X State, for saving to flash */ typedef struct _meshtastic_SEN5XState { /* Last cleaning time for SEN5X */ uint32_t last_cleaning_time; /* Last cleaning time for SEN5X - valid flag */ bool last_cleaning_valid; /* Config flag for one-shot mode (see admin.proto) */ bool one_shot_mode; /* Last VOC state time for SEN55 */ bool has_voc_state_time; uint32_t voc_state_time; /* Last VOC state validity flag for SEN55 */ bool has_voc_state_valid; bool voc_state_valid; /* VOC state array (8x uint8t) for SEN55 */ bool has_voc_state_array; uint64_t voc_state_array; } meshtastic_SEN5XState; #ifdef __cplusplus extern "C" { #endif /* Helper constants for enums */ #define _meshtastic_TelemetrySensorType_MIN meshtastic_TelemetrySensorType_SENSOR_UNSET #define _meshtastic_TelemetrySensorType_MAX meshtastic_TelemetrySensorType_SHTXX #define _meshtastic_TelemetrySensorType_ARRAYSIZE ((meshtastic_TelemetrySensorType)(meshtastic_TelemetrySensorType_SHTXX+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, 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, false, 0, false, 0, false, 0, false, 0, 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, 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, 0, 0, 0, 0} #define meshtastic_TrafficManagementStats_init_default {0, 0, 0, 0, 0, 0, 0} #define meshtastic_HealthMetrics_init_default {false, 0, false, 0, false, 0} #define meshtastic_HostMetrics_init_default {0, 0, 0, false, 0, false, 0, 0, 0, 0, false, ""} #define meshtastic_Telemetry_init_default {0, 0, {meshtastic_DeviceMetrics_init_default}} #define meshtastic_Nau7802Config_init_default {0, 0} #define meshtastic_SEN5XState_init_default {0, 0, 0, false, 0, false, 0, false, 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, 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, false, 0, false, 0, false, 0, false, 0, 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, 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, 0, 0, 0, 0} #define meshtastic_TrafficManagementStats_init_zero {0, 0, 0, 0, 0, 0, 0} #define meshtastic_HealthMetrics_init_zero {false, 0, false, 0, false, 0} #define meshtastic_HostMetrics_init_zero {0, 0, 0, false, 0, false, 0, 0, 0, 0, false, ""} #define meshtastic_Telemetry_init_zero {0, 0, {meshtastic_DeviceMetrics_init_zero}} #define meshtastic_Nau7802Config_init_zero {0, 0} #define meshtastic_SEN5XState_init_zero {0, 0, 0, false, 0, false, 0, false, 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_EnvironmentMetrics_radiation_tag 18 #define meshtastic_EnvironmentMetrics_rainfall_1h_tag 19 #define meshtastic_EnvironmentMetrics_rainfall_24h_tag 20 #define meshtastic_EnvironmentMetrics_soil_moisture_tag 21 #define meshtastic_EnvironmentMetrics_soil_temperature_tag 22 #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_PowerMetrics_ch4_voltage_tag 7 #define meshtastic_PowerMetrics_ch4_current_tag 8 #define meshtastic_PowerMetrics_ch5_voltage_tag 9 #define meshtastic_PowerMetrics_ch5_current_tag 10 #define meshtastic_PowerMetrics_ch6_voltage_tag 11 #define meshtastic_PowerMetrics_ch6_current_tag 12 #define meshtastic_PowerMetrics_ch7_voltage_tag 13 #define meshtastic_PowerMetrics_ch7_current_tag 14 #define meshtastic_PowerMetrics_ch8_voltage_tag 15 #define meshtastic_PowerMetrics_ch8_current_tag 16 #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_AirQualityMetrics_co2_temperature_tag 14 #define meshtastic_AirQualityMetrics_co2_humidity_tag 15 #define meshtastic_AirQualityMetrics_form_formaldehyde_tag 16 #define meshtastic_AirQualityMetrics_form_humidity_tag 17 #define meshtastic_AirQualityMetrics_form_temperature_tag 18 #define meshtastic_AirQualityMetrics_pm40_standard_tag 19 #define meshtastic_AirQualityMetrics_particles_40um_tag 20 #define meshtastic_AirQualityMetrics_pm_temperature_tag 21 #define meshtastic_AirQualityMetrics_pm_humidity_tag 22 #define meshtastic_AirQualityMetrics_pm_voc_idx_tag 23 #define meshtastic_AirQualityMetrics_pm_nox_idx_tag 24 #define meshtastic_AirQualityMetrics_particles_tps_tag 25 #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_LocalStats_heap_total_bytes_tag 12 #define meshtastic_LocalStats_heap_free_bytes_tag 13 #define meshtastic_LocalStats_num_tx_dropped_tag 14 #define meshtastic_LocalStats_noise_floor_tag 15 #define meshtastic_TrafficManagementStats_packets_inspected_tag 1 #define meshtastic_TrafficManagementStats_position_dedup_drops_tag 2 #define meshtastic_TrafficManagementStats_nodeinfo_cache_hits_tag 3 #define meshtastic_TrafficManagementStats_rate_limit_drops_tag 4 #define meshtastic_TrafficManagementStats_unknown_packet_drops_tag 5 #define meshtastic_TrafficManagementStats_hop_exhausted_packets_tag 6 #define meshtastic_TrafficManagementStats_router_hops_preserved_tag 7 #define meshtastic_HealthMetrics_heart_bpm_tag 1 #define meshtastic_HealthMetrics_spO2_tag 2 #define meshtastic_HealthMetrics_temperature_tag 3 #define meshtastic_HostMetrics_uptime_seconds_tag 1 #define meshtastic_HostMetrics_freemem_bytes_tag 2 #define meshtastic_HostMetrics_diskfree1_bytes_tag 3 #define meshtastic_HostMetrics_diskfree2_bytes_tag 4 #define meshtastic_HostMetrics_diskfree3_bytes_tag 5 #define meshtastic_HostMetrics_load1_tag 6 #define meshtastic_HostMetrics_load5_tag 7 #define meshtastic_HostMetrics_load15_tag 8 #define meshtastic_HostMetrics_user_string_tag 9 #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_Telemetry_host_metrics_tag 8 #define meshtastic_Telemetry_traffic_management_stats_tag 9 #define meshtastic_Nau7802Config_zeroOffset_tag 1 #define meshtastic_Nau7802Config_calibrationFactor_tag 2 #define meshtastic_SEN5XState_last_cleaning_time_tag 1 #define meshtastic_SEN5XState_last_cleaning_valid_tag 2 #define meshtastic_SEN5XState_one_shot_mode_tag 3 #define meshtastic_SEN5XState_voc_state_time_tag 4 #define meshtastic_SEN5XState_voc_state_valid_tag 5 #define meshtastic_SEN5XState_voc_state_array_tag 6 /* 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) \ X(a, STATIC, OPTIONAL, FLOAT, radiation, 18) \ X(a, STATIC, OPTIONAL, FLOAT, rainfall_1h, 19) \ X(a, STATIC, OPTIONAL, FLOAT, rainfall_24h, 20) \ X(a, STATIC, OPTIONAL, UINT32, soil_moisture, 21) \ X(a, STATIC, OPTIONAL, FLOAT, soil_temperature, 22) #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) \ X(a, STATIC, OPTIONAL, FLOAT, ch4_voltage, 7) \ X(a, STATIC, OPTIONAL, FLOAT, ch4_current, 8) \ X(a, STATIC, OPTIONAL, FLOAT, ch5_voltage, 9) \ X(a, STATIC, OPTIONAL, FLOAT, ch5_current, 10) \ X(a, STATIC, OPTIONAL, FLOAT, ch6_voltage, 11) \ X(a, STATIC, OPTIONAL, FLOAT, ch6_current, 12) \ X(a, STATIC, OPTIONAL, FLOAT, ch7_voltage, 13) \ X(a, STATIC, OPTIONAL, FLOAT, ch7_current, 14) \ X(a, STATIC, OPTIONAL, FLOAT, ch8_voltage, 15) \ X(a, STATIC, OPTIONAL, FLOAT, ch8_current, 16) #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) \ X(a, STATIC, OPTIONAL, FLOAT, co2_temperature, 14) \ X(a, STATIC, OPTIONAL, FLOAT, co2_humidity, 15) \ X(a, STATIC, OPTIONAL, FLOAT, form_formaldehyde, 16) \ X(a, STATIC, OPTIONAL, FLOAT, form_humidity, 17) \ X(a, STATIC, OPTIONAL, FLOAT, form_temperature, 18) \ X(a, STATIC, OPTIONAL, UINT32, pm40_standard, 19) \ X(a, STATIC, OPTIONAL, UINT32, particles_40um, 20) \ X(a, STATIC, OPTIONAL, FLOAT, pm_temperature, 21) \ X(a, STATIC, OPTIONAL, FLOAT, pm_humidity, 22) \ X(a, STATIC, OPTIONAL, FLOAT, pm_voc_idx, 23) \ X(a, STATIC, OPTIONAL, FLOAT, pm_nox_idx, 24) \ X(a, STATIC, OPTIONAL, FLOAT, particles_tps, 25) #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) \ X(a, STATIC, SINGULAR, UINT32, heap_total_bytes, 12) \ X(a, STATIC, SINGULAR, UINT32, heap_free_bytes, 13) \ X(a, STATIC, SINGULAR, UINT32, num_tx_dropped, 14) \ X(a, STATIC, SINGULAR, INT32, noise_floor, 15) #define meshtastic_LocalStats_CALLBACK NULL #define meshtastic_LocalStats_DEFAULT NULL #define meshtastic_TrafficManagementStats_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, UINT32, packets_inspected, 1) \ X(a, STATIC, SINGULAR, UINT32, position_dedup_drops, 2) \ X(a, STATIC, SINGULAR, UINT32, nodeinfo_cache_hits, 3) \ X(a, STATIC, SINGULAR, UINT32, rate_limit_drops, 4) \ X(a, STATIC, SINGULAR, UINT32, unknown_packet_drops, 5) \ X(a, STATIC, SINGULAR, UINT32, hop_exhausted_packets, 6) \ X(a, STATIC, SINGULAR, UINT32, router_hops_preserved, 7) #define meshtastic_TrafficManagementStats_CALLBACK NULL #define meshtastic_TrafficManagementStats_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_HostMetrics_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, UINT32, uptime_seconds, 1) \ X(a, STATIC, SINGULAR, UINT64, freemem_bytes, 2) \ X(a, STATIC, SINGULAR, UINT64, diskfree1_bytes, 3) \ X(a, STATIC, OPTIONAL, UINT64, diskfree2_bytes, 4) \ X(a, STATIC, OPTIONAL, UINT64, diskfree3_bytes, 5) \ X(a, STATIC, SINGULAR, UINT32, load1, 6) \ X(a, STATIC, SINGULAR, UINT32, load5, 7) \ X(a, STATIC, SINGULAR, UINT32, load15, 8) \ X(a, STATIC, OPTIONAL, STRING, user_string, 9) #define meshtastic_HostMetrics_CALLBACK NULL #define meshtastic_HostMetrics_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) \ X(a, STATIC, ONEOF, MESSAGE, (variant,host_metrics,variant.host_metrics), 8) \ X(a, STATIC, ONEOF, MESSAGE, (variant,traffic_management_stats,variant.traffic_management_stats), 9) #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_Telemetry_variant_host_metrics_MSGTYPE meshtastic_HostMetrics #define meshtastic_Telemetry_variant_traffic_management_stats_MSGTYPE meshtastic_TrafficManagementStats #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 #define meshtastic_SEN5XState_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, UINT32, last_cleaning_time, 1) \ X(a, STATIC, SINGULAR, BOOL, last_cleaning_valid, 2) \ X(a, STATIC, SINGULAR, BOOL, one_shot_mode, 3) \ X(a, STATIC, OPTIONAL, UINT32, voc_state_time, 4) \ X(a, STATIC, OPTIONAL, BOOL, voc_state_valid, 5) \ X(a, STATIC, OPTIONAL, FIXED64, voc_state_array, 6) #define meshtastic_SEN5XState_CALLBACK NULL #define meshtastic_SEN5XState_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_TrafficManagementStats_msg; extern const pb_msgdesc_t meshtastic_HealthMetrics_msg; extern const pb_msgdesc_t meshtastic_HostMetrics_msg; extern const pb_msgdesc_t meshtastic_Telemetry_msg; extern const pb_msgdesc_t meshtastic_Nau7802Config_msg; extern const pb_msgdesc_t meshtastic_SEN5XState_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_TrafficManagementStats_fields &meshtastic_TrafficManagementStats_msg #define meshtastic_HealthMetrics_fields &meshtastic_HealthMetrics_msg #define meshtastic_HostMetrics_fields &meshtastic_HostMetrics_msg #define meshtastic_Telemetry_fields &meshtastic_Telemetry_msg #define meshtastic_Nau7802Config_fields &meshtastic_Nau7802Config_msg #define meshtastic_SEN5XState_fields &meshtastic_SEN5XState_msg /* Maximum encoded size of messages (where known) */ #define MESHTASTIC_MESHTASTIC_TELEMETRY_PB_H_MAX_SIZE meshtastic_Telemetry_size #define meshtastic_AirQualityMetrics_size 150 #define meshtastic_DeviceMetrics_size 27 #define meshtastic_EnvironmentMetrics_size 113 #define meshtastic_HealthMetrics_size 11 #define meshtastic_HostMetrics_size 264 #define meshtastic_LocalStats_size 87 #define meshtastic_Nau7802Config_size 16 #define meshtastic_PowerMetrics_size 81 #define meshtastic_SEN5XState_size 27 #define meshtastic_Telemetry_size 272 #define meshtastic_TrafficManagementStats_size 42 #ifdef __cplusplus } /* extern "C" */ #endif #endif ================================================ FILE: src/mesh/generated/meshtastic/xmodem.pb.cpp ================================================ /* Automatically generated nanopb constant definitions */ /* Generated by nanopb-0.4.9.1 */ #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) ================================================ FILE: src/mesh/generated/meshtastic/xmodem.pb.h ================================================ /* Automatically generated nanopb header */ /* Generated by nanopb-0.4.9.1 */ #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 ================================================ FILE: src/mesh/http/ContentHandler.cpp ================================================ #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 "SPILock.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" #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 const *contentTypes[][2] = {{".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 *nodeAPIv1FromRadioOptions = new ResourceNode("/api/v1/fromradio", "OPTIONS", &handleAPIv1FromRadio); ResourceNode *nodeAPIv1FromRadio = new ResourceNode("/api/v1/fromradio", "GET", &handleAPIv1FromRadio); // ResourceNode *nodeHotspotApple = new ResourceNode("/hotspot-detect.html", "GET", &handleHotspot); // ResourceNode *nodeHotspotAndroid = new ResourceNode("/generate_204", "GET", &handleHotspot); ResourceNode *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 *nodeJsonReport = new ResourceNode("/json/report", "GET", &handleReport); ResourceNode *nodeJsonNodes = new ResourceNode("/json/nodes", "GET", &handleNodes); ResourceNode *nodeJsonFsBrowseStatic = new ResourceNode("/json/fs/browse/static", "GET", &handleFsBrowseStatic); ResourceNode *nodeJsonDelete = new ResourceNode("/json/fs/delete/static", "DELETE", &handleFsDeleteStatic); ResourceNode *nodeRoot = new ResourceNode("/*", "GET", &handleStatic); // Secure nodes secureServer->registerNode(nodeAPIv1ToRadioOptions); secureServer->registerNode(nodeAPIv1ToRadio); secureServer->registerNode(nodeAPIv1FromRadioOptions); secureServer->registerNode(nodeAPIv1FromRadio); // secureServer->registerNode(nodeHotspotApple); // secureServer->registerNode(nodeHotspotAndroid); secureServer->registerNode(nodeRestart); secureServer->registerNode(nodeFormUpload); secureServer->registerNode(nodeJsonScanNetworks); secureServer->registerNode(nodeJsonFsBrowseStatic); secureServer->registerNode(nodeJsonDelete); secureServer->registerNode(nodeJsonReport); secureServer->registerNode(nodeJsonNodes); // secureServer->registerNode(nodeUpdateFs); // secureServer->registerNode(nodeDeleteFs); secureServer->registerNode(nodeAdmin); // secureServer->registerNode(nodeAdminFs); // secureServer->registerNode(nodeAdminSettings); // secureServer->registerNode(nodeAdminSettingsApply); secureServer->registerNode(nodeRoot); // This has to be last // Insecure nodes insecureServer->registerNode(nodeAPIv1ToRadioOptions); insecureServer->registerNode(nodeAPIv1ToRadio); insecureServer->registerNode(nodeAPIv1FromRadioOptions); insecureServer->registerNode(nodeAPIv1FromRadio); // insecureServer->registerNode(nodeHotspotApple); // insecureServer->registerNode(nodeHotspotAndroid); insecureServer->registerNode(nodeRestart); insecureServer->registerNode(nodeFormUpload); insecureServer->registerNode(nodeJsonScanNetworks); insecureServer->registerNode(nodeJsonFsBrowseStatic); insecureServer->registerNode(nodeJsonDelete); insecureServer->registerNode(nodeJsonReport); // insecureServer->registerNode(nodeUpdateFs); // insecureServer->registerNode(nodeDeleteFs); insecureServer->registerNode(nodeAdmin); // insecureServer->registerNode(nodeAdminFs); // insecureServer->registerNode(nodeAdminSettings); // insecureServer->registerNode(nodeAdminSettingsApply); insecureServer->registerNode(nodeRoot); // This has to be last } void handleAPIv1FromRadio(HTTPRequest *req, HTTPResponse *res) { if (webServerThread) webServerThread->markActivity(); 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"); if (req->getMethod() == "OPTIONS") { res->setStatusCode(204); // Success with no content res->print(""); return; } 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(""); 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 String fileName = String(file.path()).substring(1); thisFileMap["name"] = new JSONValue(fileName.c_str()); #else String fileName = String(file.name()).substring(1); thisFileMap["name"] = new JSONValue(fileName.c_str()); #endif String tempName = String(file.name()).substring(1); if (tempName.endsWith(".gz")) { #ifdef ARCH_ESP32 String modifiedFile = String(file.path()).substring(1); #else String modifiedFile = String(file.name()).substring(1); #endif modifiedFile.remove((modifiedFile.length() - 3), 3); thisFileMap["nameModified"] = new JSONValue(modifiedFile.c_str()); } fileList.push_back(new JSONValue(thisFileMap)); } file.close(); file = root.openNextFile(); } root.close(); return fileList; } void handleFsBrowseStatic(HTTPRequest *req, HTTPResponse *res) { res->setHeader("Content-Type", "application/json"); res->setHeader("Access-Control-Allow-Origin", "*"); res->setHeader("Access-Control-Allow-Methods", "GET"); concurrency::LockGuard g(spiLock); auto fileList = htmlListDir("/static", 10); // create json output structure JSONObject filesystemObj; filesystemObj["total"] = new JSONValue((int)FSCom.totalBytes()); filesystemObj["used"] = new JSONValue((int)FSCom.usedBytes()); filesystemObj["free"] = new JSONValue(int(FSCom.totalBytes() - FSCom.usedBytes())); JSONObject jsonObjInner; jsonObjInner["files"] = new JSONValue(fileList); jsonObjInner["filesystem"] = new JSONValue(filesystemObj); JSONObject jsonObjOuter; jsonObjOuter["data"] = new JSONValue(jsonObjInner); jsonObjOuter["status"] = new JSONValue("ok"); JSONValue *value = new JSONValue(jsonObjOuter); std::string jsonString = value->Stringify(); res->print(jsonString.c_str()); delete value; } void handleFsDeleteStatic(HTTPRequest *req, HTTPResponse *res) { ResourceParameters *params = req->getParams(); std::string paramValDelete; res->setHeader("Content-Type", "application/json"); res->setHeader("Access-Control-Allow-Origin", "*"); res->setHeader("Access-Control-Allow-Methods", "DELETE"); if (params->getQueryParameter("delete", paramValDelete)) { std::string pathDelete = "/" + paramValDelete; concurrency::LockGuard g(spiLock); if (FSCom.remove(pathDelete.c_str())) { LOG_INFO("%s", pathDelete.c_str()); JSONObject jsonObjOuter; jsonObjOuter["status"] = new JSONValue("ok"); JSONValue *value = new JSONValue(jsonObjOuter); std::string jsonString = value->Stringify(); res->print(jsonString.c_str()); delete value; return; } else { LOG_INFO("%s", pathDelete.c_str()); JSONObject jsonObjOuter; jsonObjOuter["status"] = new JSONValue("Error"); JSONValue *value = new JSONValue(jsonObjOuter); std::string jsonString = value->Stringify(); res->print(jsonString.c_str()); delete value; return; } } } void handleStatic(HTTPRequest *req, HTTPResponse *res) { if (webServerThread) webServerThread->markActivity(); // Get access to the parameters ResourceParameters *params = req->getParams(); std::string parameter1; // Print the first parameter value if (params->getPathParameter(0, parameter1)) { std::string filename = "/static/" + parameter1; std::string filenameGzip = "/static/" + parameter1 + ".gz"; // Try to open the file File file; bool has_set_content_type = false; if (filename == "/static/") { filename = "/static/index.html"; filenameGzip = "/static/index.html.gz"; } concurrency::LockGuard g(spiLock); if (FSCom.exists(filename.c_str())) { file = FSCom.open(filename.c_str()); if (!file.available()) { LOG_WARN("File not available - %s", filename.c_str()); } } else if (FSCom.exists(filenameGzip.c_str())) { file = FSCom.open(filenameGzip.c_str()); res->setHeader("Content-Encoding", "gzip"); if (!file.available()) { LOG_WARN("File not available - %s", filenameGzip.c_str()); } } else { has_set_content_type = true; filenameGzip = "/static/index.html.gz"; file = FSCom.open(filenameGzip.c_str()); res->setHeader("Content-Type", "text/html"); if (!file.available()) { LOG_WARN("File not available - %s", filenameGzip.c_str()); res->println("Web server is running.

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

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

File Upload

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

No file found.

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

No file found.

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

Write aborted! Reserving 50k on filesystem.

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

Saved %d bytes to %s

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

Did not write any file

"); } res->println(""); delete parser; } void handleReport(HTTPRequest *req, HTTPResponse *res) { ResourceParameters *params = req->getParams(); std::string content; if (!params->getQueryParameter("content", content)) { content = "json"; } if (content == "json") { res->setHeader("Content-Type", "application/json"); res->setHeader("Access-Control-Allow-Origin", "*"); res->setHeader("Access-Control-Allow-Methods", "GET"); } else { res->setHeader("Content-Type", "text/html"); res->println("
");
    }

    // Helper lambda to create JSON array and clean up memory properly
    auto createJSONArrayFromLog = [](const uint32_t *logArray, int count) -> JSONValue * {
        JSONArray tempArray;
        for (int i = 0; i < count; i++) {
            tempArray.push_back(new JSONValue((int)logArray[i]));
        }
        JSONValue *result = new JSONValue(tempArray);
        // Note: Don't delete tempArray elements here - JSONValue now owns them
        return result;
    };

    // data->airtime->tx_log
    uint32_t *logArray;
    logArray = airTime->airtimeReport(TX_LOG);
    JSONValue *txLogJsonValue = createJSONArrayFromLog(logArray, airTime->getPeriodsToLog());

    // data->airtime->rx_log
    logArray = airTime->airtimeReport(RX_LOG);
    JSONValue *rxLogJsonValue = createJSONArrayFromLog(logArray, airTime->getPeriodsToLog());

    // data->airtime->rx_all_log
    logArray = airTime->airtimeReport(RX_ALL_LOG);
    JSONValue *rxAllLogJsonValue = createJSONArrayFromLog(logArray, airTime->getPeriodsToLog());

    // data->airtime
    JSONObject jsonObjAirtime;
    jsonObjAirtime["tx_log"] = txLogJsonValue;
    jsonObjAirtime["rx_log"] = rxLogJsonValue;
    jsonObjAirtime["rx_all_log"] = rxAllLogJsonValue;
    jsonObjAirtime["channel_utilization"] = new JSONValue(airTime->channelUtilizationPercent());
    jsonObjAirtime["utilization_tx"] = new JSONValue(airTime->utilizationTXPercent());
    jsonObjAirtime["seconds_since_boot"] = new JSONValue(int(airTime->getSecondsSinceBoot()));
    jsonObjAirtime["seconds_per_period"] = new JSONValue(int(airTime->getSecondsPerPeriod()));
    jsonObjAirtime["periods_to_log"] = new JSONValue(airTime->getPeriodsToLog());

    // data->wifi
    JSONObject jsonObjWifi;
    jsonObjWifi["rssi"] = new JSONValue(WiFi.RSSI());
    String wifiIPString = WiFi.localIP().toString();
    std::string wifiIP = wifiIPString.c_str();
    jsonObjWifi["ip"] = new JSONValue(wifiIP.c_str());

    // data->memory
    JSONObject jsonObjMemory;
    jsonObjMemory["heap_total"] = new JSONValue((int)memGet.getHeapSize());
    jsonObjMemory["heap_free"] = new JSONValue((int)memGet.getFreeHeap());
    jsonObjMemory["psram_total"] = new JSONValue((int)memGet.getPsramSize());
    jsonObjMemory["psram_free"] = new JSONValue((int)memGet.getFreePsram());
    spiLock->lock();
    jsonObjMemory["fs_total"] = new JSONValue((int)FSCom.totalBytes());
    jsonObjMemory["fs_used"] = new JSONValue((int)FSCom.usedBytes());
    jsonObjMemory["fs_free"] = new JSONValue(int(FSCom.totalBytes() - FSCom.usedBytes()));
    spiLock->unlock();

    // data->power
    JSONObject jsonObjPower;
    jsonObjPower["battery_percent"] = new JSONValue(powerStatus->getBatteryChargePercent());
    jsonObjPower["battery_voltage_mv"] = new JSONValue(powerStatus->getBatteryVoltageMv());
    jsonObjPower["has_battery"] = new JSONValue(BoolToString(powerStatus->getHasBattery()));
    jsonObjPower["has_usb"] = new JSONValue(BoolToString(powerStatus->getHasUSB()));
    jsonObjPower["is_charging"] = new JSONValue(BoolToString(powerStatus->getIsCharging()));

    // data->device
    JSONObject jsonObjDevice;
    jsonObjDevice["reboot_counter"] = new JSONValue((int)myNodeInfo.reboot_count);

    // data->radio
    JSONObject jsonObjRadio;
    jsonObjRadio["frequency"] = new JSONValue(RadioLibInterface::instance->getFreq());
    jsonObjRadio["lora_channel"] = new JSONValue((int)RadioLibInterface::instance->getChannelNum() + 1);

    // collect data to inner data object
    JSONObject jsonObjInner;
    jsonObjInner["airtime"] = new JSONValue(jsonObjAirtime);
    jsonObjInner["wifi"] = new JSONValue(jsonObjWifi);
    jsonObjInner["memory"] = new JSONValue(jsonObjMemory);
    jsonObjInner["power"] = new JSONValue(jsonObjPower);
    jsonObjInner["device"] = new JSONValue(jsonObjDevice);
    jsonObjInner["radio"] = new JSONValue(jsonObjRadio);

    // create json output structure
    JSONObject jsonObjOuter;
    jsonObjOuter["data"] = new JSONValue(jsonObjInner);
    jsonObjOuter["status"] = new JSONValue("ok");
    // serialize and write it to the stream
    JSONValue *value = new JSONValue(jsonObjOuter);
    std::string jsonString = value->Stringify();
    res->print(jsonString.c_str());
    delete value;
}

void handleNodes(HTTPRequest *req, HTTPResponse *res)
{
    ResourceParameters *params = req->getParams();
    std::string content;

    if (!params->getQueryParameter("content", content)) {
        content = "json";
    }

    if (content == "json") {
        res->setHeader("Content-Type", "application/json");
        res->setHeader("Access-Control-Allow-Origin", "*");
        res->setHeader("Access-Control-Allow-Methods", "GET");
    } else {
        res->setHeader("Content-Type", "text/html");
        res->println("
");
    }

    JSONArray nodesArray;

    uint32_t readIndex = 0;
    const meshtastic_NodeInfoLite *tempNodeInfo = nodeDB->readNextMeshNode(readIndex);
    while (tempNodeInfo != NULL) {
        if (tempNodeInfo->has_user) {
            JSONObject node;

            char id[16];
            snprintf(id, sizeof(id), "!%08x", tempNodeInfo->num);

            node["id"] = new JSONValue(id);
            node["snr"] = new JSONValue(tempNodeInfo->snr);
            node["via_mqtt"] = new JSONValue(BoolToString(tempNodeInfo->via_mqtt));
            node["last_heard"] = new JSONValue((int)tempNodeInfo->last_heard);
            node["position"] = new JSONValue();

            if (nodeDB->hasValidPosition(tempNodeInfo)) {
                JSONObject position;
                position["latitude"] = new JSONValue((float)tempNodeInfo->position.latitude_i * 1e-7);
                position["longitude"] = new JSONValue((float)tempNodeInfo->position.longitude_i * 1e-7);
                position["altitude"] = new JSONValue((int)tempNodeInfo->position.altitude);
                node["position"] = new JSONValue(position);
            }

            node["long_name"] = new JSONValue(tempNodeInfo->user.long_name);
            node["short_name"] = new JSONValue(tempNodeInfo->user.short_name);
            char macStr[18];
            snprintf(macStr, sizeof(macStr), "%02X:%02X:%02X:%02X:%02X:%02X", tempNodeInfo->user.macaddr[0],
                     tempNodeInfo->user.macaddr[1], tempNodeInfo->user.macaddr[2], tempNodeInfo->user.macaddr[3],
                     tempNodeInfo->user.macaddr[4], tempNodeInfo->user.macaddr[5]);
            node["mac_address"] = new JSONValue(macStr);
            node["hw_model"] = new JSONValue(tempNodeInfo->user.hw_model);

            nodesArray.push_back(new JSONValue(node));
        }
        tempNodeInfo = nodeDB->readNextMeshNode(readIndex);
    }

    // collect data to inner data object
    JSONObject jsonObjInner;
    jsonObjInner["nodes"] = new JSONValue(nodesArray);

    // create json output structure
    JSONObject jsonObjOuter;
    jsonObjOuter["data"] = new JSONValue(jsonObjInner);
    jsonObjOuter["status"] = new JSONValue("ok");
    // serialize and write it to the stream
    JSONValue *value = new JSONValue(jsonObjOuter);
    std::string jsonString = value->Stringify();
    res->print(jsonString.c_str());
    delete value;
}

/*
    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("Delete Content in /static/*"); LOG_INFO("Delete files from /static/* : "); concurrency::LockGuard g(spiLock); 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 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); std::string jsonString = value->Stringify(); res->print(jsonString.c_str()); delete value; } #endif ================================================ FILE: src/mesh/http/ContentHandler.h ================================================ #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 handleReport(HTTPRequest *req, HTTPResponse *res); void handleNodes(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: HttpAPI() { api_type = TYPE_HTTP; } /// 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 private: // Nothing here yet protected: }; ================================================ FILE: src/mesh/http/ContentHelper.cpp ================================================ #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' } } ================================================ FILE: src/mesh/http/ContentHelper.h ================================================ #include #include #include #define BoolToString(x) ((x) ? "true" : "false") void replaceAll(std::string &str, const std::string &from, const std::string &to); ================================================ FILE: src/mesh/http/WebServer.cpp ================================================ #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 #include #if HAS_ETHERNET && defined(USE_WS5500) #include #define ETH ETH2 #endif // HAS_ETHERNET #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 const uint32_t ACTIVE_THRESHOLD_MS = 5000; static const uint32_t MEDIUM_THRESHOLD_MS = 30000; static const int32_t ACTIVE_INTERVAL_MS = 50; static const int32_t MEDIUM_INTERVAL_MS = 200; static const int32_t IDLE_INTERVAL_MS = 1000; // Maximum concurrent HTTPS connections (reduced from default 4 to save memory) static const uint8_t MAX_HTTPS_CONNECTIONS = 2; // Minimum free heap required for SSL handshake (~40KB for mbedTLS contexts) static const uint32_t MIN_HEAP_FOR_SSL = 40000; static SSLCert *cert; static HTTPSServer *secureServer; static HTTPServer *insecureServer; volatile bool isWebServerReady; volatile bool isCertReady; static void handleWebResponse() { if (isWifiAvailable()) { if (isWebServerReady) { // Check heap before HTTPS processing - SSL requires significant memory if (secureServer) { uint32_t freeHeap = ESP.getFreeHeap(); if (freeHeap >= MIN_HEAP_FOR_SSL) { secureServer->loop(); } else { // Skip HTTPS when memory is low to prevent SSL setup failures static uint32_t lastHeapWarning = 0; if (lastHeapWarning == 0 || !Throttle::isWithinTimespanMs(lastHeapWarning, 30000)) { LOG_WARN("Low heap (%u bytes), skipping HTTPS processing", freeHeap); lastHeapWarning = millis(); } } } insecureServer->loop(); } } } static void taskCreateCert(void *parameter) { prefs.begin("MeshtasticHTTPS", false); #if 0 // Delete the saved certs (used in debugging) LOG_DEBUG("Delete any saved SSL keys"); // prefs.clear(); prefs.remove("PK"); prefs.remove("cert"); #endif LOG_INFO("Checking if we have a 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) { if (screen) screen->setSSLFrames(); } #endif } runLoop = false; } else { runLoop = true; } } LOG_INFO("SSL Cert Ready!"); } } WebServerThread *webServerThread; WebServerThread::WebServerThread() : concurrency::OSThread("WebServer") { if (!config.network.wifi_enabled && !config.network.eth_enabled) { disable(); } lastActivityTime = millis(); } void WebServerThread::markActivity() { lastActivityTime = millis(); } int32_t WebServerThread::getAdaptiveInterval() { uint32_t currentTime = millis(); uint32_t timeSinceActivity; if (currentTime >= lastActivityTime) { timeSinceActivity = currentTime - lastActivityTime; } else { timeSinceActivity = (UINT32_MAX - lastActivityTime) + currentTime + 1; } if (timeSinceActivity < ACTIVE_THRESHOLD_MS) { return ACTIVE_INTERVAL_MS; } else if (timeSinceActivity < MEDIUM_THRESHOLD_MS) { return MEDIUM_INTERVAL_MS; } else { return IDLE_INTERVAL_MS; } } int32_t WebServerThread::runOnce() { if (!config.network.wifi_enabled && !config.network.eth_enabled) { disable(); } handleWebResponse(); if (requestRestart && (millis() / 1000) > requestRestart) { ESP.restart(); } return getAdaptiveInterval(); } void initWebServer() { LOG_DEBUG("Init Web Server"); // We can now use the new certificate to setup our server as usual. secureServer = new HTTPSServer(cert, 443, MAX_HTTPS_CONNECTIONS); insecureServer = new HTTPServer(); registerHandlers(insecureServer, secureServer); if (secureServer) { LOG_INFO("Start Secure Web Server"); secureServer->start(); } LOG_INFO("Start Insecure Web Server"); insecureServer->start(); if (insecureServer->isRunning()) { LOG_INFO("Web Servers Ready! :-) "); isWebServerReady = true; } else { LOG_ERROR("Web Servers Failed! ;-( "); } } #endif ================================================ FILE: src/mesh/http/WebServer.h ================================================ #pragma once #include "PhoneAPI.h" #include "concurrency/OSThread.h" #include #include void initWebServer(); void createSSLCert(); class WebServerThread : private concurrency::OSThread { private: uint32_t lastActivityTime = 0; public: WebServerThread(); uint32_t requestRestart = 0; void markActivity(); protected: virtual int32_t runOnce() override; int32_t getAdaptiveInterval(); }; extern WebServerThread *webServerThread; ================================================ FILE: src/mesh/mesh-pb-constants.cpp ================================================ #include "configuration.h" #include "FSCommon.h" #include "SPILock.h" #include "mesh-pb-constants.h" #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)); 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) { spiLock->lock(); auto file = (Print *)stream->state; // LOG_DEBUG("writing %d bytes to protobuf file", count); bool status = file->write(buf, count) == count; spiLock->unlock(); return status; } #endif bool is_in_helper(uint32_t n, const uint32_t *array, pb_size_t count) { for (pb_size_t i = 0; i < count; i++) if (array[i] == n) return true; return false; } ================================================ FILE: src/mesh/mesh-pb-constants.h ================================================ #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 #if defined(ARCH_ESP32) && !(defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S3)) #define MAX_RX_TOPHONE 8 #else #define MAX_RX_TOPHONE 32 #endif #endif /// max number of QueueStatus packets which can be waiting for delivery to phone #ifndef MAX_RX_QUEUESTATUS_TOPHONE #define MAX_RX_QUEUESTATUS_TOPHONE 2 #endif /// max number of MqttClientProxyMessage packets which can be waiting for delivery to phone #ifndef MAX_RX_MQTTPROXY_TOPHONE #define MAX_RX_MQTTPROXY_TOPHONE 8 #endif /// max number of ClientNotification packets which can be waiting for delivery to phone #ifndef MAX_RX_NOTIFICATION_TOPHONE #define MAX_RX_NOTIFICATION_TOPHONE 2 #endif /// Verify baseline assumption of node size. If it increases, we need to reevaluate /// the impact of its memory footprint, notably on MAX_NUM_NODES. static_assert(sizeof(meshtastic_NodeInfoLite) <= 200, "NodeInfoLite size increased. Reconsider impact on MAX_NUM_NODES."); /// max number of nodes allowed in the nodeDB #ifndef MAX_NUM_NODES #if defined(ARCH_STM32WL) #define MAX_NUM_NODES 10 #elif defined(ARCH_NRF52) #define MAX_NUM_NODES 80 #elif defined(CONFIG_IDF_TARGET_ESP32S3) #include "Esp.h" static inline int get_max_num_nodes() { uint32_t flash_size = ESP.getFlashChipSize() / (1024 * 1024); // Convert Bytes to MB if (flash_size >= 15) { return 250; } else if (flash_size >= 7) { return 200; } else { return 100; } } #define MAX_NUM_NODES get_max_num_nodes() #else #define MAX_NUM_NODES 100 #endif #endif /// Max number of channels allowed #define MAX_NUM_CHANNELS (member_size(meshtastic_ChannelFile, channels) / member_size(meshtastic_ChannelFile, channels[0])) // Traffic Management module configuration // Enable per-variant by defining HAS_TRAFFIC_MANAGEMENT=1 in variant.h #ifndef HAS_TRAFFIC_MANAGEMENT #define HAS_TRAFFIC_MANAGEMENT 0 #endif // Cache size for traffic management (number of nodes to track) // Can be overridden per-variant based on available memory #ifndef TRAFFIC_MANAGEMENT_CACHE_SIZE #if HAS_TRAFFIC_MANAGEMENT #define TRAFFIC_MANAGEMENT_CACHE_SIZE 1000 #else #define TRAFFIC_MANAGEMENT_CACHE_SIZE 0 #endif #endif /// 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) ================================================ FILE: src/mesh/raspihttp/PiWebServer.cpp ================================================ /* 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 "" #define KEY_PATH portduino_config.webserver_ssl_key_path.c_str() #define CERT_PATH portduino_config.webserver_ssl_cert_path.c_str() 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; 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 (strcmp(req->http_verb, "OPTIONS") == 0) { ulfius_set_response_properties(res, U_OPT_STATUS, 204); return U_CALLBACK_COMPLETE; } byte buffer[MAX_TO_FROM_RADIO_SIZE]; size_t s = req->binary_body_length; memcpy(buffer, req->binary_body, MAX_TO_FROM_RADIO_SIZE); // FIXME* Problem with portdunio loosing mountpoint maybe because of running in a real sep. thread portduinoVFS->mountpoint(configWeb.rootPath); LOG_DEBUG("Received %d bytes from PUT request", s); static_cast(user_data)->handleToRadio(buffer, s); LOG_DEBUG("end web->radio "); return U_CALLBACK_COMPLETE; } /* * Adapt the radioapi to the Webservice handleAPIv1FromRadio * Trigger : WebGui(POLL)->handleAPIv1FromRadio->phoneapi->Meshtastic(Radio) events */ int handleAPIv1FromRadio(const struct _u_request *req, struct _u_response *res, void *user_data) { // 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"); if (strcmp(req->http_verb, "OPTIONS") == 0) { ulfius_set_response_properties(res, U_OPT_STATUS, 204); return U_CALLBACK_COMPLETE; } uint8_t txBuf[MAX_STREAM_BUF_SIZE]; uint32_t len = 1; if (valueAll == "true") { while (len) { len = static_cast(user_data)->getFromRadio(txBuf); ulfius_set_response_properties(res, U_OPT_STATUS, 200, U_OPT_BINARY_BODY, txBuf, len); const char *tmpa = (const char *)txBuf; ulfius_set_string_body_response(res, 200, tmpa); // LOG_DEBUG("\n----webAPI response all:----"); // LOG_DEBUG(tmpa); // LOG_DEBUG(""); } // Otherwise, just return one protobuf } else { len = static_cast(user_data)->getFromRadio(txBuf); const char *tmpa = (const char *)txBuf; ulfius_set_binary_body_response(res, 200, tmpa, len); // LOG_DEBUG("\n----webAPI response:"); // LOG_DEBUG(tmpa); // LOG_DEBUG(""); } // LOG_DEBUG("end radio->web", len); return U_CALLBACK_COMPLETE; } /* OpenSSL RSA Key Gen */ int generate_rsa_key(EVP_PKEY **pkey) { EVP_PKEY_CTX *pkey_ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, NULL); if (!pkey_ctx) return -1; if (EVP_PKEY_keygen_init(pkey_ctx) <= 0) return -1; if (EVP_PKEY_CTX_set_rsa_keygen_bits(pkey_ctx, 2048) <= 0) return -1; if (EVP_PKEY_keygen(pkey_ctx, pkey) <= 0) return -1; EVP_PKEY_CTX_free(pkey_ctx); return 0; // SUCCESS } int generate_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(CERT_PATH); if (cert_pem == NULL) { LOG_ERROR("ERROR SSL Certificate File can't be loaded or is missing"); return 1; } // read private key key_pem = read_file_into_string(KEY_PATH); if (key_pem == NULL) { LOG_ERROR("ERROR file private_key can't be loaded or is missing"); return 2; } return 0; } 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 X509-Cert"); return 2; } // Open file to write private key file FILE *pkey_file = fopen(KEY_PATH, "wb"); if (!pkey_file) { LOG_ERROR("Error opening private key file"); return 3; } // write private key file PEM_write_PrivateKey(pkey_file, pkey, NULL, NULL, 0, NULL, NULL); fclose(pkey_file); // open Certificate file FILE *x509_file = fopen(CERT_PATH, "wb"); if (!x509_file) { LOG_ERROR("Error opening cert"); return 4; } // write certificate PEM_write_X509(x509_file, x509); fclose(x509_file); EVP_PKEY_free(pkey); LOG_INFO("Create SSL Key %s successful", KEY_PATH); X509_free(x509); LOG_INFO("Create SSL Cert %s successful", CERT_PATH); return 0; } void initWebServer() {} PiWebServerThread::PiWebServerThread() { int ret, retssl, webservport; if (CheckSSLandLoad() != 0) { CreateSSLCertificate(); if (CheckSSLandLoad() != 0) { LOG_ERROR("Major Error Gen & Read SSL Certificate"); } } if (portduino_config.webserverport != 0) { webservport = portduino_config.webserverport; LOG_INFO("Use webserver port from yaml config %i ", webservport); } else { LOG_INFO("Webserver port in yaml config set to 0, defaulting to port 9443"); webservport = 9443; } // Web Content Service Instance if (ulfius_init_instance(&instanceWeb, webservport, NULL, DEFAULT_REALM) != U_OK) { LOG_ERROR("Webserver couldn't be started, abort execution"); } else { LOG_INFO("Webserver started"); u_map_init(&configWeb.mime_types); u_map_put(&configWeb.mime_types, "*", "application/octet-stream"); u_map_put(&configWeb.mime_types, ".html", "text/html"); u_map_put(&configWeb.mime_types, ".htm", "text/html"); u_map_put(&configWeb.mime_types, ".tsx", "application/javascript"); u_map_put(&configWeb.mime_types, ".ts", "application/javascript"); u_map_put(&configWeb.mime_types, ".css", "text/css"); u_map_put(&configWeb.mime_types, ".js", "application/javascript"); u_map_put(&configWeb.mime_types, ".json", "application/json"); u_map_put(&configWeb.mime_types, ".png", "image/png"); u_map_put(&configWeb.mime_types, ".gif", "image/gif"); u_map_put(&configWeb.mime_types, ".jpeg", "image/jpeg"); u_map_put(&configWeb.mime_types, ".jpg", "image/jpeg"); u_map_put(&configWeb.mime_types, ".ttf", "font/ttf"); u_map_put(&configWeb.mime_types, ".woff", "font/woff"); u_map_put(&configWeb.mime_types, ".ico", "image/x-icon"); u_map_put(&configWeb.mime_types, ".svg", "image/svg+xml"); webrootpath = portduino_config.webserver_root_path; configWeb.files_path = (char *)webrootpath.c_str(); configWeb.url_prefix = ""; configWeb.rootPath = strdup(portduinoVFS->mountpoint()); u_map_put(instanceWeb.default_headers, "Access-Control-Allow-Origin", "*"); // Maximum body size sent by the client is 1 Kb instanceWeb.max_post_body_size = 1024; ulfius_add_endpoint_by_val(&instanceWeb, "GET", PREFIX, "/api/v1/fromradio/*", 1, &handleAPIv1FromRadio, &webAPI); ulfius_add_endpoint_by_val(&instanceWeb, "OPTIONS", PREFIX, "/api/v1/fromradio/*", 1, &handleAPIv1FromRadio, &webAPI); ulfius_add_endpoint_by_val(&instanceWeb, "PUT", PREFIX, "/api/v1/toradio/*", 1, &handleAPIv1ToRadio, &webAPI); ulfius_add_endpoint_by_val(&instanceWeb, "OPTIONS", PREFIX, "/api/v1/toradio/*", 1, &handleAPIv1ToRadio, &webAPI); // Add callback function to all endpoints for the Web Server ulfius_add_endpoint_by_val(&instanceWeb, "GET", NULL, "/*", 2, &callback_static_file, &configWeb); // thats for serving without SSL // retssl = ulfius_start_framework(&instanceWeb); // thats for serving with SSL retssl = ulfius_start_secure_framework(&instanceWeb, key_pem, cert_pem); if (retssl == U_OK) { LOG_INFO("Web Server framework started on port: %i ", webservport); LOG_INFO("Web Server root %s", (char *)webrootpath.c_str()); } 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_clean_instance(&instanceWeb); free(configWeb.rootPath); free(key_pem); free(cert_pem); LOG_INFO("End framework"); } #endif #endif ================================================ FILE: src/mesh/raspihttp/PiWebServer.h ================================================ #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 HttpAPI : public PhoneAPI { public: HttpAPI() { api_type = TYPE_HTTP; } /// 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 private: // Nothing here yet protected: }; class PiWebServerThread { private: char *key_pem = NULL; char *cert_pem = NULL; // struct _u_map mime_types; std::string webrootpath; HttpAPI webAPI; public: PiWebServerThread(); ~PiWebServerThread(); int CreateSSLCertificate(); int CheckSSLandLoad(); uint32_t requestRestart = 0; struct _u_instance instanceWeb; }; extern PiWebServerThread *piwebServerThread; #endif #endif ================================================ FILE: src/mesh/udp/UdpMulticastHandler.h ================================================ #pragma once #if HAS_UDP_MULTICAST #include "configuration.h" #include "main.h" #include "mesh/Router.h" #if HAS_ETHERNET && defined(ARCH_NRF52) #include "mesh/eth/ethClient.h" #else #include #endif #include #if HAS_ETHERNET && defined(USE_WS5500) #include #define ETH ETH2 #endif // HAS_ETHERNET #define UDP_MULTICAST_DEFAUL_PORT 4403 // Default port for UDP multicast is same as TCP api server class UdpMulticastHandler final { public: UdpMulticastHandler() : isRunning(false) { udpIpAddress = IPAddress(224, 0, 0, 69); } void start() { if (isRunning) { LOG_DEBUG("UDP multicast already running"); return; } if (udp.listenMulticast(udpIpAddress, UDP_MULTICAST_DEFAUL_PORT, 64)) { #if defined(ARCH_NRF52) || defined(ARCH_PORTDUINO) LOG_DEBUG("UDP Listening on IP: %u.%u.%u.%u:%u", udpIpAddress[0], udpIpAddress[1], udpIpAddress[2], udpIpAddress[3], UDP_MULTICAST_DEFAUL_PORT); #else LOG_DEBUG("UDP Listening on IP: %s", WiFi.localIP().toString().c_str()); #endif udp.onPacket([this](AsyncUDPPacket packet) { onReceive(packet); }); isRunning = true; } else { LOG_DEBUG("Failed to listen on UDP"); } } void stop() { if (!isRunning) { return; } LOG_DEBUG("Stopping UDP multicast"); #if defined(ARCH_ESP32) || defined(ARCH_NRF52) udp.close(); #endif isRunning = false; } void onReceive(AsyncUDPPacket &packet) { if (!isRunning) { return; } size_t packetLength = packet.length(); #if defined(ARCH_NRF52) IPAddress ip = packet.remoteIP(); LOG_DEBUG("UDP broadcast from: %u.%u.%u.%u, len=%u", ip[0], ip[1], ip[2], ip[3], packetLength); #elif !defined(ARCH_PORTDUINO) // FIXME(PORTDUINO): arduino lacks IPAddress::toString() LOG_DEBUG("UDP broadcast from: %s, len=%u", packet.remoteIP().toString().c_str(), packetLength); #endif meshtastic_MeshPacket mp = meshtastic_MeshPacket_init_zero; LOG_DEBUG("Decoding MeshPacket from UDP len=%u", packetLength); bool isPacketDecoded = pb_decode_from_bytes(packet.data(), packetLength, &meshtastic_MeshPacket_msg, &mp); if (isPacketDecoded && router && mp.which_payload_variant == meshtastic_MeshPacket_encrypted_tag) { // Drop packets with spoofed local origin — no legitimate LAN node should send from=0 or our own nodeNum if (isFromUs(&mp)) { LOG_WARN("UDP packet with spoofed local from=0x%x, dropping", mp.from); return; } mp.transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MULTICAST_UDP; UniquePacketPoolPacket p = packetPool.allocUniqueCopy(mp); // Unset received SNR/RSSI p->rx_snr = 0; p->rx_rssi = 0; router->enqueueReceivedMessage(p.release()); } } bool onSend(const meshtastic_MeshPacket *mp) { if (!isRunning || !mp || !udp) { return false; } #if defined(ARCH_NRF52) if (!isEthernetAvailable()) { return false; } #elif !defined(ARCH_PORTDUINO) if (WiFi.status() != WL_CONNECTED) { return false; } #endif if (mp->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MULTICAST_UDP) { LOG_ERROR("Attempt to send UDP sourced packet over UDP"); } LOG_DEBUG("Broadcasting packet over UDP (id=%u)", mp->id); uint8_t buffer[meshtastic_MeshPacket_size]; size_t encodedLength = pb_encode_to_bytes(buffer, sizeof(buffer), &meshtastic_MeshPacket_msg, mp); return udp.writeTo(buffer, encodedLength, udpIpAddress, UDP_MULTICAST_DEFAUL_PORT); } private: IPAddress udpIpAddress; AsyncUDP udp; bool isRunning; }; #endif // HAS_UDP_MULTICAST ================================================ FILE: src/mesh/wifi/WiFiAPClient.cpp ================================================ #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" #include "target_specific.h" #include #if HAS_ETHERNET && defined(USE_WS5500) #include #define ETH ETH2 #endif // HAS_ETHERNET #include #ifdef ARCH_ESP32 #if !MESHTASTIC_EXCLUDE_WEBSERVER #include "mesh/http/WebServer.h" #endif #include #include static void WiFiEvent(WiFiEvent_t event); #elif defined(ARCH_RP2040) #include #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]; // To replace blocking wifi connect delay with a non-blocking sleep static unsigned long wifiReconnectStartMillis = 0; static bool wifiReconnectPending = false; 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; meshtastic::Syslog syslog(syslogClient); Periodic *wifiReconnect; #ifdef USE_WS5500 // Startup Ethernet bool initEthernet() { if ((config.network.eth_enabled) && (ETH.begin(ETH_PHY_W5500, 1, ETH_CS_PIN, ETH_INT_PIN, ETH_RST_PIN, SPI3_HOST, ETH_SCLK_PIN, ETH_MISO_PIN, ETH_MOSI_PIN))) { WiFi.onEvent(WiFiEvent); #if !MESHTASTIC_EXCLUDE_WEBSERVER createSSLCert(); // For WebServer #endif return true; } return false; } #endif static void onNetworkConnected() { if (!APStartupComplete) { // Start web server LOG_INFO("Start network services"); // start mdns if (!MDNS.begin("Meshtastic")) { LOG_ERROR("Error setting up mDNS responder!"); } else { LOG_INFO("mDNS Host: Meshtastic.local"); MDNS.addService("meshtastic", "tcp", SERVER_API_DEFAULT_PORT); // ESPmDNS (ESP32) and SimpleMDNS (RP2040) have slightly different APIs for adding TXT records #ifdef ARCH_ESP32 MDNS.addServiceTxt("meshtastic", "tcp", "shortname", String(owner.short_name)); MDNS.addServiceTxt("meshtastic", "tcp", "id", String(nodeDB->getNodeId().c_str())); MDNS.addServiceTxt("meshtastic", "tcp", "pio_env", optstr(APP_ENV)); // ESP32 prints obtained IP address in WiFiEvent #elif defined(ARCH_RP2040) MDNS.addServiceTxt("meshtastic", "shortname", owner.short_name); MDNS.addServiceTxt("meshtastic", "id", nodeDB->getNodeId().c_str()); MDNS.addServiceTxt("meshtastic", "pio_env", optstr(APP_ENV)); LOG_INFO("Obtained IP address: %s", WiFi.localIP().toString().c_str()); #endif } #ifndef DISABLE_NTP LOG_INFO("Start NTP time client"); timeClient.begin(); timeClient.setUpdateInterval(60 * 60); // Update once an hour #endif if (config.network.rsyslog_server[0]) { LOG_INFO("Start Syslog client"); // Defaults int serverPort = 514; const char *serverAddr = config.network.rsyslog_server; String server = String(serverAddr); int delimIndex = server.indexOf(':'); if (delimIndex > 0) { String port = server.substring(delimIndex + 1, server.length()); server[delimIndex] = 0; serverPort = port.toInt(); serverAddr = server.c_str(); } syslog.server(serverAddr, serverPort); syslog.deviceHostname(getDeviceName()); syslog.appName("Meshtastic"); syslog.defaultPriority(LOGLEVEL_USER); syslog.enable(); } #if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_WEBSERVER if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { initWebServer(); } #endif #if !MESHTASTIC_EXCLUDE_SOCKETAPI if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { initApiServer(); } #endif APStartupComplete = true; } #if HAS_UDP_MULTICAST if (udpHandler && config.network.enabled_protocols & meshtastic_Config_NetworkConfig_ProtocolFlags_UDP_BROADCAST) { udpHandler->start(); } #endif } static int32_t reconnectWiFi() { const char *wifiName = config.network.wifi_ssid; const char *wifiPsw = config.network.wifi_psk; if (config.network.wifi_enabled && needReconnect) { if (!*wifiPsw) // Treat empty password as no password wifiPsw = NULL; needReconnect = false; isReconnecting = true; // Make sure we clear old connection credentials #ifdef ARCH_ESP32 WiFi.disconnect(false, true); #elif defined(ARCH_RP2040) WiFi.disconnect(false); #endif LOG_INFO("Reconnecting to WiFi access point %s", wifiName); // Start the non-blocking wait for 5 seconds wifiReconnectStartMillis = millis(); wifiReconnectPending = true; // Do not attempt to connect yet, wait for the next invocation return 5000; // Schedule next check soon } // Check if we are ready to proceed with the WiFi connection after the 5s wait if (wifiReconnectPending) { if (millis() - wifiReconnectStartMillis >= 5000) { if (!WiFi.isConnected()) { #ifdef CONFIG_IDF_TARGET_ESP32C3 WiFi.mode(WIFI_MODE_NULL); WiFi.useStaticBuffers(true); WiFi.mode(WIFI_STA); #endif WiFi.begin(wifiName, wifiPsw); } isReconnecting = false; wifiReconnectPending = false; } else { // Still waiting for 5s to elapse return 100; // Check again soon } } #ifndef DISABLE_NTP if (WiFi.isConnected() && (!Throttle::isWithinTimespanMs(lastrun_ntp, 43200000) || (lastrun_ntp == 0))) { // every 12 hours LOG_DEBUG("Update NTP time from %s", config.network.ntp_server); if (timeClient.update()) { LOG_DEBUG("NTP Request Success - Setting RTCQualityNTP if needed"); struct timeval tv; tv.tv_sec = timeClient.getEpochTime(); tv.tv_usec = 0; perhapsSetRTC(RTCQualityNTP, &tv); lastrun_ntp = millis(); } else { LOG_DEBUG("NTP Update failed"); } } #endif if (config.network.wifi_enabled && !WiFi.isConnected()) { #ifdef ARCH_RP2040 // (ESP32 handles this in WiFiEvent) needReconnect = APStartupComplete; #endif return 1000; // check once per second } else { #ifdef ARCH_RP2040 onNetworkConnected(); // will only do anything once #endif return 300000; // every 5 minutes } } bool isWifiAvailable() { if (config.network.wifi_enabled && (config.network.wifi_ssid[0])) { return true; #ifdef USE_WS5500 } else if (config.network.eth_enabled) { return true; #endif #ifndef ARCH_PORTDUINO } else if (WiFi.status() == WL_CONNECTED) { // it's likely we have wifi now, but user intends to turn it off in config! return true; #endif } else { return false; } } // Disable WiFi void deinitWifi() { LOG_INFO("WiFi deinit"); if (isWifiAvailable()) { #ifdef ARCH_ESP32 WiFi.disconnect(true, false); #elif defined(ARCH_RP2040) WiFi.disconnect(true); #endif WiFi.mode(WIFI_OFF); LOG_INFO("WiFi Turned Off"); // WiFi.printDiag(Serial); } } // Startup WiFi bool initWifi() { if (config.network.wifi_enabled && config.network.wifi_ssid[0]) { const char *wifiName = config.network.wifi_ssid; const char *wifiPsw = config.network.wifi_psk; #ifndef ARCH_RP2040 #if !MESHTASTIC_EXCLUDE_WEBSERVER createSSLCert(); // For WebServer #endif WiFi.persistent(false); // 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) { #ifdef ARCH_ESP32 WiFi.config(config.network.ipv4_config.ip, config.network.ipv4_config.gateway, config.network.ipv4_config.subnet, config.network.ipv4_config.dns); #elif defined(ARCH_RP2040) WiFi.config(config.network.ipv4_config.ip, config.network.ipv4_config.dns, config.network.ipv4_config.gateway, config.network.ipv4_config.subnet); #endif } #ifdef ARCH_ESP32 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 #if ESP_ARDUINO_VERSION <= ESP_ARDUINO_VERSION_VAL(3, 0, 0) // Most of the next 12 lines of code are adapted from espressif/arduino-esp32 // Licensed under the GNU Lesser General Public License v2.1 // https://github.com/espressif/arduino-esp32/blob/1f038677eb2eaf5e9ca6b6074486803c15468bed/libraries/WiFi/src/WiFiSTA.cpp#L755 esp_netif_t *get_esp_interface_netif(esp_interface_t interface); IPv6Address GlobalIPv6() { esp_ip6_addr_t addr; if (WiFiGenericClass::getMode() == WIFI_MODE_NULL) { return IPv6Address(); } if (esp_netif_get_ip6_global(get_esp_interface_netif(ESP_IF_WIFI_STA), &addr)) { return IPv6Address(); } return IPv6Address(addr.addr); } #endif // Called by the Espressif SDK to static void WiFiEvent(WiFiEvent_t event) { LOG_DEBUG("Network-Event %d: ", event); switch (event) { case ARDUINO_EVENT_WIFI_READY: LOG_INFO("WiFi interface ready"); break; case ARDUINO_EVENT_WIFI_SCAN_DONE: LOG_INFO("Completed scan for access points"); break; case ARDUINO_EVENT_WIFI_STA_START: LOG_INFO("WiFi station started"); break; case ARDUINO_EVENT_WIFI_STA_STOP: LOG_INFO("WiFi station stopped"); syslog.disable(); break; case ARDUINO_EVENT_WIFI_STA_CONNECTED: LOG_INFO("Connected to access point"); if (config.network.ipv6_enabled) { #if ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(3, 0, 0) if (!WiFi.enableIPv6()) { LOG_WARN("Failed to enable IPv6"); } #else if (!WiFi.enableIpV6()) { LOG_WARN("Failed to enable IPv6"); } #endif } #ifdef WIFI_LED digitalWrite(WIFI_LED, HIGH); #endif break; case ARDUINO_EVENT_WIFI_STA_DISCONNECTED: LOG_INFO("Disconnected from WiFi access point"); #ifdef WIFI_LED digitalWrite(WIFI_LED, LOW); #endif #if HAS_UDP_MULTICAST if (udpHandler) { udpHandler->stop(); } #endif if (!isReconnecting) { WiFi.disconnect(false, true); syslog.disable(); needReconnect = true; wifiReconnect->setIntervalFromNow(1000); } break; case ARDUINO_EVENT_WIFI_STA_AUTHMODE_CHANGE: LOG_INFO("Authentication mode of access point has changed"); break; case ARDUINO_EVENT_WIFI_STA_GOT_IP: LOG_INFO("Obtained IP address: %s", WiFi.localIP().toString().c_str()); onNetworkConnected(); break; case ARDUINO_EVENT_WIFI_STA_GOT_IP6: #if ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(3, 0, 0) LOG_INFO("Obtained Local IP6 address: %s", WiFi.linkLocalIPv6().toString().c_str()); LOG_INFO("Obtained GlobalIP6 address: %s", WiFi.globalIPv6().toString().c_str()); #else LOG_INFO("Obtained Local IP6 address: %s", WiFi.localIPv6().toString().c_str()); LOG_INFO("Obtained GlobalIP6 address: %s", GlobalIPv6().toString().c_str()); #endif break; case ARDUINO_EVENT_WIFI_STA_LOST_IP: LOG_INFO("Lost IP address and IP address is reset to 0"); #if HAS_UDP_MULTICAST if (udpHandler) { udpHandler->stop(); } #endif if (!isReconnecting) { WiFi.disconnect(false, true); syslog.disable(); needReconnect = true; wifiReconnect->setIntervalFromNow(1000); } break; case ARDUINO_EVENT_WPS_ER_SUCCESS: LOG_INFO("WiFi Protected Setup (WPS): succeeded in enrollee mode"); break; case ARDUINO_EVENT_WPS_ER_FAILED: LOG_INFO("WiFi Protected Setup (WPS): failed in enrollee mode"); break; case ARDUINO_EVENT_WPS_ER_TIMEOUT: LOG_INFO("WiFi Protected Setup (WPS): timeout in enrollee mode"); break; case ARDUINO_EVENT_WPS_ER_PIN: LOG_INFO("WiFi Protected Setup (WPS): pin code in enrollee mode"); break; case ARDUINO_EVENT_WPS_ER_PBC_OVERLAP: LOG_INFO("WiFi Protected Setup (WPS): push button overlap in enrollee mode"); break; case ARDUINO_EVENT_WIFI_AP_START: LOG_INFO("WiFi access point started"); #ifdef WIFI_LED digitalWrite(WIFI_LED, HIGH); #endif break; case ARDUINO_EVENT_WIFI_AP_STOP: LOG_INFO("WiFi access point stopped"); #ifdef WIFI_LED digitalWrite(WIFI_LED, LOW); #endif break; case ARDUINO_EVENT_WIFI_AP_STACONNECTED: LOG_INFO("Client connected"); break; case ARDUINO_EVENT_WIFI_AP_STADISCONNECTED: LOG_INFO("Client disconnected"); break; case ARDUINO_EVENT_WIFI_AP_STAIPASSIGNED: LOG_INFO("Assigned IP address to client"); break; case ARDUINO_EVENT_WIFI_AP_PROBEREQRECVED: LOG_INFO("Received probe request"); break; case ARDUINO_EVENT_WIFI_AP_GOT_IP6: LOG_INFO("IPv6 is preferred"); break; case ARDUINO_EVENT_WIFI_FTM_REPORT: LOG_INFO("Fast Transition Management report"); break; case ARDUINO_EVENT_ETH_START: LOG_INFO("Ethernet started"); break; case ARDUINO_EVENT_ETH_STOP: syslog.disable(); LOG_INFO("Ethernet stopped"); break; case ARDUINO_EVENT_ETH_CONNECTED: LOG_INFO("Ethernet connected"); break; case ARDUINO_EVENT_ETH_DISCONNECTED: syslog.disable(); LOG_INFO("Ethernet disconnected"); break; case ARDUINO_EVENT_ETH_GOT_IP: #ifdef USE_WS5500 LOG_INFO("Obtained IP address: %s, %u Mbps, %s", ETH.localIP().toString().c_str(), ETH.linkSpeed(), ETH.fullDuplex() ? "FULL_DUPLEX" : "HALF_DUPLEX"); onNetworkConnected(); #endif break; case ARDUINO_EVENT_ETH_GOT_IP6: #ifdef USE_WS5500 #if ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(3, 0, 0) LOG_INFO("Obtained Local IP6 address: %s", ETH.linkLocalIPv6().toString().c_str()); LOG_INFO("Obtained GlobalIP6 address: %s", ETH.globalIPv6().toString().c_str()); #else LOG_INFO("Obtained IP6 address: %s", ETH.localIPv6().toString().c_str()); #endif #endif break; case ARDUINO_EVENT_SC_SCAN_DONE: LOG_INFO("SmartConfig: Scan done"); break; case ARDUINO_EVENT_SC_FOUND_CHANNEL: LOG_INFO("SmartConfig: Found channel"); break; case ARDUINO_EVENT_SC_GOT_SSID_PSWD: LOG_INFO("SmartConfig: Got SSID and password"); break; case ARDUINO_EVENT_SC_SEND_ACK_DONE: LOG_INFO("SmartConfig: Send ACK done"); break; case ARDUINO_EVENT_PROV_INIT: LOG_INFO("Provision Init"); break; case ARDUINO_EVENT_PROV_DEINIT: LOG_INFO("Provision Stopped"); break; case ARDUINO_EVENT_PROV_START: LOG_INFO("Provision Started"); break; case ARDUINO_EVENT_PROV_END: LOG_INFO("Provision End"); break; case ARDUINO_EVENT_PROV_CRED_RECV: LOG_INFO("Provision Credentials received"); break; case ARDUINO_EVENT_PROV_CRED_FAIL: LOG_INFO("Provision Credentials failed"); break; case ARDUINO_EVENT_PROV_CRED_SUCCESS: LOG_INFO("Provision Credentials success"); break; default: break; } } #endif uint8_t getWifiDisconnectReason() { return wifiDisconnectReason; } #endif // HAS_WIFI ================================================ FILE: src/mesh/wifi/WiFiAPClient.h ================================================ #pragma once #include "concurrency/Periodic.h" #include "configuration.h" #include #include #if HAS_WIFI && !defined(ARCH_PORTDUINO) #include #endif #if HAS_ETHERNET && defined(USE_WS5500) #include #define ETH ETH2 #endif // HAS_ETHERNET extern bool needReconnect; extern concurrency::Periodic *wifiReconnect; /// @return true if wifi is now in use bool initWifi(); void deinitWifi(); bool isWifiAvailable(); uint8_t getWifiDisconnectReason(); #ifdef USE_WS5500 // Startup Ethernet bool initEthernet(); #endif ================================================ FILE: src/meshUtils.cpp ================================================ #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); 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); } size_t pb_string_length(const char *str, size_t max_len) { size_t len = 0; for (size_t i = 0; i < max_len; i++) { if (str[i] != '\0') { len = i + 1; } } return len; } ================================================ FILE: src/meshUtils.h ================================================ #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 HAS_SCREEN #define IF_SCREEN(X) \ if (screen) { \ X; \ } #else #define IF_SCREEN(...) #endif #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, ...); // Get actual string length for nanopb char array fields. size_t pb_string_length(const char *str, size_t max_len); /// Calculate 2^n without calling pow() - used for spreading factor and other calculations inline uint32_t pow_of_2(uint32_t n) { return 1 << n; } #define IS_ONE_OF(item, ...) isOneOf(item, sizeof((int[]){__VA_ARGS__}) / sizeof(int), __VA_ARGS__) ================================================ FILE: src/modules/AdminModule.cpp ================================================ #include "AdminModule.h" #include "Channels.h" #include "MeshService.h" #include "NodeDB.h" #include "PowerFSM.h" #include "RTC.h" #include "SPILock.h" #include "input/InputBroker.h" #include "meshUtils.h" #include #include // for better whitespace handling #if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_WIFI #include "MeshtasticOTA.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 "Default.h" #include "MeshRadio.h" #include "RadioInterface.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_STM32WL) && !MESHTASTIC_EXCLUDE_I2C #include "motion/AccelerometerThread.h" #endif #if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && \ !defined(CONFIG_IDF_TARGET_ESP32C3) #include "SerialModule.h" #endif AdminModule *adminModule; bool hasOpenEditTransaction; /// A special reserved string to indicate strings we can not share with external nodes. We will use this 'reserved' word instead. /// Also, to make setting work correctly, if someone tries to set a string to this reserved value we assume they don't really want /// a change. 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("Allow admin response message"); } else if (mp.from == 0) { if (config.security.is_managed) { LOG_INFO("Ignore local admin payload because is_managed"); return handled; } } else if (strcasecmp(ch->settings.name, Channels::adminChannel) == 0) { if (!config.security.admin_channel_enabled) { LOG_INFO("Ignore admin channel, legacy admin is disabled"); myReply = allocErrorResponse(meshtastic_Routing_Error_NOT_AUTHORIZED, &mp); return handled; } } else if (mp.pki_encrypted) { if ((config.security.admin_key[0].size == 32 && memcmp(mp.public_key.bytes, config.security.admin_key[0].bytes, 32) == 0) || (config.security.admin_key[1].size == 32 && memcmp(mp.public_key.bytes, config.security.admin_key[1].bytes, 32) == 0) || (config.security.admin_key[2].size == 32 && memcmp(mp.public_key.bytes, config.security.admin_key[2].bytes, 32) == 0)) { LOG_INFO("PKC admin payload with authorized sender key"); // Automatically favorite the node that is using the admin key auto remoteNode = nodeDB->getMeshNode(mp.from); if (remoteNode && !remoteNode->is_favorite) { if (config.device.role == meshtastic_Config_DeviceConfig_Role_CLIENT_BASE) { // Special case for CLIENT_BASE: is_favorite has special meaning, and we don't want to automatically set it // without the user doing so deliberately. LOG_INFO("PKC admin valid, but not auto-favoriting node %x because role==CLIENT_BASE", mp.from); } else { LOG_INFO("PKC admin valid. Auto-favoriting node %x", mp.from); remoteNode->is_favorite = true; } } } else { myReply = allocErrorResponse(meshtastic_Routing_Error_ADMIN_PUBLIC_KEY_UNAUTHORIZED, &mp); LOG_INFO("Received PKC admin payload, but the sender public key does not match the admin authorized key!"); return handled; } } else { LOG_INFO("Ignore unauthorized admin payload %i", r->which_payload_variant); myReply = allocErrorResponse(meshtastic_Routing_Error_NOT_AUTHORIZED, &mp); return handled; } LOG_INFO("Handle admin payload %i", r->which_payload_variant); // 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_DEBUG("Client got owner"); handleGetOwner(mp); break; case meshtastic_AdminMessage_get_config_request_tag: LOG_DEBUG("Client got config"); handleGetConfig(mp, r->get_config_request); break; case meshtastic_AdminMessage_get_module_config_request_tag: LOG_DEBUG("Client got module config"); handleGetModuleConfig(mp, r->get_module_config_request); break; case meshtastic_AdminMessage_get_channel_request_tag: { uint32_t i = r->get_channel_request - 1; LOG_DEBUG("Client got channel %u", i); if (i >= MAX_NUM_CHANNELS) myReply = allocErrorResponse(meshtastic_Routing_Error_BAD_REQUEST, &mp); else handleGetChannel(mp, i); break; } /** * Setters */ case meshtastic_AdminMessage_set_owner_tag: LOG_DEBUG("Client set owner"); // Validate names if (*r->set_owner.long_name) { const char *start = r->set_owner.long_name; // Skip all whitespace (space, tab, newline, etc) while (*start && isspace((unsigned char)*start)) start++; if (*start == '\0') { LOG_WARN("Rejected long_name: must contain at least 1 non-whitespace character"); myReply = allocErrorResponse(meshtastic_Routing_Error_BAD_REQUEST, &mp); break; } } if (*r->set_owner.short_name) { const char *start = r->set_owner.short_name; while (*start && isspace((unsigned char)*start)) start++; if (*start == '\0') { LOG_WARN("Rejected short_name: must contain at least 1 non-whitespace character"); myReply = allocErrorResponse(meshtastic_Routing_Error_BAD_REQUEST, &mp); break; } } handleSetOwner(r->set_owner); break; case meshtastic_AdminMessage_set_config_tag: LOG_DEBUG("Client set config"); handleSetConfig(r->set_config, fromOthers); break; case meshtastic_AdminMessage_set_module_config_tag: LOG_DEBUG("Client set module config"); if (!handleSetModuleConfig(r->set_module_config)) { myReply = allocErrorResponse(meshtastic_Routing_Error_BAD_REQUEST, &mp); } break; case meshtastic_AdminMessage_set_channel_tag: LOG_DEBUG("Client set channel %d", r->set_channel.index); if (r->set_channel.index < 0 || r->set_channel.index >= (int)MAX_NUM_CHANNELS) myReply = allocErrorResponse(meshtastic_Routing_Error_BAD_REQUEST, &mp); else handleSetChannel(r->set_channel); break; case meshtastic_AdminMessage_set_ham_mode_tag: LOG_DEBUG("Client set ham mode"); handleSetHamMode(r->set_ham_mode); break; case meshtastic_AdminMessage_get_ui_config_request_tag: { LOG_DEBUG("Client is getting device-ui config"); handleGetDeviceUIConfig(mp); handled = true; break; } /** * Other */ case meshtastic_AdminMessage_reboot_seconds_tag: { reboot(r->reboot_seconds); break; } case meshtastic_AdminMessage_ota_request_tag: { #if defined(ARCH_ESP32) LOG_INFO("OTA Requested"); if (r->ota_request.ota_hash.size != 32) { suppressRebootBanner = true; sendWarningAndLog("Cannot start OTA: Invalid `ota_hash` provided."); break; } meshtastic_OTAMode mode = r->ota_request.reboot_ota_mode; const char *mode_name = (mode == METHOD_OTA_BLE ? "BLE" : "WiFi"); // Check that we have an OTA partition const esp_partition_t *part = MeshtasticOTA::getAppPartition(); if (part == NULL) { suppressRebootBanner = true; sendWarningAndLog("Cannot start OTA: Cannot find OTA Loader partition."); break; } static esp_app_desc_t app_desc; if (!MeshtasticOTA::getAppDesc(part, &app_desc)) { suppressRebootBanner = true; sendWarningAndLog("Cannot start OTA: Device does have a valid OTA Loader."); break; } if (!MeshtasticOTA::checkOTACapability(&app_desc, mode)) { suppressRebootBanner = true; sendWarningAndLog("OTA Loader does not support %s", mode_name); break; } if (MeshtasticOTA::trySwitchToOTA()) { suppressRebootBanner = true; if (screen) screen->startFirmwareUpdateScreen(); MeshtasticOTA::saveConfig(&config.network, mode, r->ota_request.ota_hash.bytes); sendWarningAndLog("Rebooting to %s OTA", mode_name); } else { sendWarningAndLog("Unable to switch to the OTA partition."); } #endif int s = 1; // Reboot in 1 second, hard coded LOG_INFO("Reboot in %d seconds", s); rebootAtMsec = (s < 0) ? 0 : (millis() + s * 1000); break; } case meshtastic_AdminMessage_shutdown_seconds_tag: { int32_t s = r->shutdown_seconds; LOG_INFO("Shutdown in %d seconds", s); shutdownAtMsec = (s < 0) ? 0 : (millis() + s * 1000); break; } case meshtastic_AdminMessage_get_device_metadata_request_tag: { LOG_INFO("Client got device metadata"); handleGetDeviceMetadata(mp); break; } case meshtastic_AdminMessage_factory_reset_config_tag: { disableBluetooth(); LOG_INFO("Initiate factory config reset"); nodeDB->factoryReset(); LOG_INFO("Factory config reset finished, rebooting soon"); reboot(DEFAULT_REBOOT_SECONDS); break; } case meshtastic_AdminMessage_factory_reset_device_tag: { disableBluetooth(); LOG_INFO("Initiate full factory reset"); nodeDB->factoryReset(true); reboot(DEFAULT_REBOOT_SECONDS); break; } case meshtastic_AdminMessage_nodedb_reset_tag: { disableBluetooth(); LOG_INFO("Initiate node-db reset"); // CLIENT_BASE, ROUTER and ROUTER_LATE are able to preserve the remaining hop count when relaying a packet via a // favorited node, so ensure that their favorites are kept on reset bool rolePreference = isOneOf(config.device.role, meshtastic_Config_DeviceConfig_Role_CLIENT_BASE, meshtastic_Config_DeviceConfig_Role_ROUTER, meshtastic_Config_DeviceConfig_Role_ROUTER_LATE); nodeDB->resetNodes(rolePreference ? rolePreference : r->nodedb_reset); reboot(DEFAULT_REBOOT_SECONDS); break; } case meshtastic_AdminMessage_store_ui_config_tag: { LOG_INFO("Storing device-ui config"); handleStoreDeviceUIConfig(r->store_ui_config); handled = true; break; } case meshtastic_AdminMessage_begin_edit_settings_tag: { LOG_INFO("Begin transaction for editing settings"); hasOpenEditTransaction = true; break; } case meshtastic_AdminMessage_commit_edit_settings_tag: { disableBluetooth(); LOG_INFO("Commit transaction for edited settings"); hasOpenEditTransaction = false; saveChanges(SEGMENT_CONFIG | SEGMENT_MODULECONFIG | SEGMENT_DEVICESTATE | SEGMENT_CHANNELS | SEGMENT_NODEDATABASE); break; } case meshtastic_AdminMessage_get_device_connection_status_request_tag: { LOG_INFO("Client got device connection status"); handleGetDeviceConnectionStatus(mp); break; } case meshtastic_AdminMessage_get_module_config_response_tag: { LOG_INFO("Client received a get_module_config response"); if (fromOthers && r->get_module_config_response.which_payload_variant == meshtastic_AdminMessage_ModuleConfigType_REMOTEHARDWARE_CONFIG) { handleGetModuleConfigResponse(mp, r); } break; } case meshtastic_AdminMessage_remove_by_nodenum_tag: { LOG_INFO("Client received remove_nodenum command"); nodeDB->removeNodeByNum(r->remove_by_nodenum); break; } case meshtastic_AdminMessage_add_contact_tag: { LOG_INFO("Client received add_contact command"); nodeDB->addFromContact(r->add_contact); break; } case meshtastic_AdminMessage_set_favorite_node_tag: { LOG_INFO("Client received set_favorite_node command"); meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->set_favorite_node); if (node != NULL) { node->is_favorite = true; saveChanges(SEGMENT_NODEDATABASE, false); if (screen) screen->setFrames(graphics::Screen::FOCUS_PRESERVE); // <-- Rebuild screens } break; } case meshtastic_AdminMessage_remove_favorite_node_tag: { LOG_INFO("Client received remove_favorite_node command"); meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->remove_favorite_node); if (node != NULL) { node->is_favorite = false; saveChanges(SEGMENT_NODEDATABASE, false); if (screen) screen->setFrames(graphics::Screen::FOCUS_PRESERVE); // <-- Rebuild screens } break; } case meshtastic_AdminMessage_set_ignored_node_tag: { LOG_INFO("Client received set_ignored_node command"); meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->set_ignored_node); if (node != NULL) { node->is_ignored = true; node->has_device_metrics = false; node->has_position = false; node->user.public_key.size = 0; memset(node->user.public_key.bytes, 0, sizeof(node->user.public_key.bytes)); saveChanges(SEGMENT_NODEDATABASE, false); } break; } case meshtastic_AdminMessage_remove_ignored_node_tag: { LOG_INFO("Client received remove_ignored_node command"); meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->remove_ignored_node); if (node != NULL) { node->is_ignored = false; saveChanges(SEGMENT_NODEDATABASE, false); } break; } case meshtastic_AdminMessage_toggle_muted_node_tag: { LOG_INFO("Client received toggle_muted_node command"); meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->toggle_muted_node); if (node != NULL) { node->bitfield ^= (1 << NODEINFO_BITFIELD_IS_MUTED_SHIFT); saveChanges(SEGMENT_NODEDATABASE, false); } break; } case meshtastic_AdminMessage_set_fixed_position_tag: { LOG_INFO("Client received set_fixed_position command"); meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum()); node->has_position = true; node->position = TypeConversions::ConvertToPositionLite(r->set_fixed_position); nodeDB->setLocalPosition(r->set_fixed_position); config.position.fixed_position = true; saveChanges(SEGMENT_NODEDATABASE | SEGMENT_CONFIG, false); #if !MESHTASTIC_EXCLUDE_GPS if (gps != nullptr) gps->enable(); // Send our new fixed position to the mesh for good measure positionModule->sendOurPosition(); #endif break; } case meshtastic_AdminMessage_remove_fixed_position_tag: { LOG_INFO("Client received remove_fixed_position command"); nodeDB->clearLocalPosition(); config.position.fixed_position = false; saveChanges(SEGMENT_NODEDATABASE | SEGMENT_CONFIG, false); break; } case meshtastic_AdminMessage_set_time_only_tag: { LOG_INFO("Client received set_time_only command"); struct timeval tv; tv.tv_sec = r->set_time_only; tv.tv_usec = 0; perhapsSetRTC(RTCQualityNTP, &tv, false); break; } case meshtastic_AdminMessage_enter_dfu_mode_request_tag: { LOG_INFO("Client requesting to enter DFU mode"); #if HAS_SCREEN IF_SCREEN(screen->showSimpleBanner("Device is rebooting\ninto DFU mode.", 0)); #endif #if defined(ARCH_NRF52) || defined(ARCH_RP2040) enterDfuMode(); #endif break; } case meshtastic_AdminMessage_delete_file_request_tag: { LOG_DEBUG("Client requesting to delete file: %s", r->delete_file_request); #ifdef FSCom spiLock->lock(); if (FSCom.remove(r->delete_file_request)) { LOG_DEBUG("Successfully deleted file"); } else { LOG_DEBUG("Failed to delete file"); } spiLock->unlock(); #endif break; } case meshtastic_AdminMessage_backup_preferences_tag: { LOG_INFO("Client requesting to backup preferences"); if (nodeDB->backupPreferences(r->backup_preferences)) { myReply = allocErrorResponse(meshtastic_Routing_Error_NONE, &mp); } else { myReply = allocErrorResponse(meshtastic_Routing_Error_BAD_REQUEST, &mp); } break; } case meshtastic_AdminMessage_restore_preferences_tag: { LOG_INFO("Client requesting to restore preferences"); if (nodeDB->restorePreferences(r->backup_preferences, SEGMENT_DEVICESTATE | SEGMENT_CONFIG | SEGMENT_MODULECONFIG | SEGMENT_CHANNELS)) { myReply = allocErrorResponse(meshtastic_Routing_Error_NONE, &mp); LOG_DEBUG("Rebooting after successful restore of preferences"); reboot(1000); disableBluetooth(); } else { myReply = allocErrorResponse(meshtastic_Routing_Error_BAD_REQUEST, &mp); } break; } case meshtastic_AdminMessage_remove_backup_preferences_tag: { LOG_INFO("Client requesting to remove backup preferences"); #ifdef FSCom if (r->remove_backup_preferences == meshtastic_AdminMessage_BackupLocation_FLASH) { spiLock->lock(); FSCom.remove(backupFileName); spiLock->unlock(); } else if (r->remove_backup_preferences == meshtastic_AdminMessage_BackupLocation_SD) { // TODO: After more mainline SD card support LOG_ERROR("SD backup removal not implemented yet"); } #endif break; } case meshtastic_AdminMessage_send_input_event_tag: { LOG_INFO("Client requesting to send input event"); handleSendInputEvent(r->send_input_event); break; } #ifdef ARCH_PORTDUINO case meshtastic_AdminMessage_exit_simulator_tag: LOG_INFO("Exiting simulator"); exit(0); break; #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("Module API did not respond to admin message. req.variant=%d", r->which_payload_variant); } else if (handleResult != AdminMessageHandleResult::HANDLED) { // Probably a message sent by us or sent to our local node. FIXME, we should avoid scanning these messages LOG_DEBUG("Module API did not handle admin message %d", r->which_payload_variant); } break; } // Allow any observers (e.g. the UI) to handle/respond AdminMessageHandleResult observerResult = AdminMessageHandleResult::NOT_HANDLED; meshtastic_AdminMessage observerResponse = meshtastic_AdminMessage_init_default; AdminModule_ObserverData observerData = { .request = r, .response = &observerResponse, .result = &observerResult, }; notifyObservers(&observerData); if (observerResult == AdminMessageHandleResult::HANDLED_WITH_RESPONSE) { setPassKey(&observerResponse); myReply = allocDataProtobuf(observerResponse); LOG_DEBUG("Observer responded to admin message"); } else if (observerResult == AdminMessageHandleResult::HANDLED) { LOG_DEBUG("Observer handled admin message"); } // If asked for a response and it is not yet set, generate an 'ACK' response if (mp.decoded.want_response && !myReply) { myReply = allocErrorResponse(meshtastic_Routing_Error_NONE, &mp); } if (mp.pki_encrypted && myReply) { myReply->pki_encrypted = true; } return handled; } void AdminModule::handleGetModuleConfigResponse(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *r) { // Skip if it's disabled or no pins are exposed if (!r->get_module_config_response.payload_variant.remote_hardware.enabled || r->get_module_config_response.payload_variant.remote_hardware.available_pins_count == 0) { LOG_DEBUG("Remote hardware module disabled or no available_pins. Skip"); return; } for (uint8_t i = 0; i < devicestate.node_remote_hardware_pins_count; i++) { if (devicestate.node_remote_hardware_pins[i].node_num == 0 || !devicestate.node_remote_hardware_pins[i].has_pin) { continue; } 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)); } snprintf(owner.id, sizeof(owner.id), "!%08x", nodeDB->getNodeNum()); if (owner.is_licensed != o.is_licensed) { changed = 1; owner.is_licensed = o.is_licensed; if (channels.ensureLicensedOperation()) { sendWarning(licensedModeMessage); } } if (owner.has_is_unmessagable != o.has_is_unmessagable || (o.has_is_unmessagable && owner.is_unmessagable != o.is_unmessagable)) { changed = 1; owner.has_is_unmessagable = owner.has_is_unmessagable || o.has_is_unmessagable; owner.is_unmessagable = o.is_unmessagable; } if (changed) { // If nothing really changed, don't broadcast on the network or write to flash service->reloadOwner(!hasOpenEditTransaction); saveChanges(SEGMENT_DEVICESTATE | SEGMENT_NODEDATABASE); } } void AdminModule::handleSetConfig(const meshtastic_Config &c, bool fromOthers) { auto changes = SEGMENT_CONFIG; auto existingRole = config.device.role; bool isRegionUnset = (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET); bool requiresReboot = true; switch (c.which_payload_variant) { case meshtastic_Config_device_tag: LOG_INFO("Set config: Device"); config.has_device = true; #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 if (config.device.button_gpio == c.payload_variant.device.button_gpio && config.device.buzzer_gpio == c.payload_variant.device.buzzer_gpio && config.device.role == c.payload_variant.device.role && config.device.rebroadcast_mode == c.payload_variant.device.rebroadcast_mode) { requiresReboot = false; } config.device = c.payload_variant.device; if (config.device.rebroadcast_mode == meshtastic_Config_DeviceConfig_RebroadcastMode_NONE && (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER || config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE)) { config.device.rebroadcast_mode = meshtastic_Config_DeviceConfig_RebroadcastMode_ALL; const char *warning = "Rebroadcast mode can't be set to NONE for a router role"; LOG_WARN(warning); sendWarning(warning); } // If we're setting router role for the first time, install its intervals if (existingRole != c.payload_variant.device.role) { nodeDB->installRoleDefaults(c.payload_variant.device.role); changes |= SEGMENT_NODEDATABASE | SEGMENT_DEVICESTATE; // Some role defaults affect owner } if (config.device.node_info_broadcast_secs < min_node_info_broadcast_secs) { LOG_DEBUG("Tried to set node_info_broadcast_secs too low, setting to %d", min_node_info_broadcast_secs); config.device.node_info_broadcast_secs = min_node_info_broadcast_secs; } // Router Client and Repeater deprecated; Set it to client if (IS_ONE_OF(c.payload_variant.device.role, meshtastic_Config_DeviceConfig_Role_ROUTER_CLIENT, meshtastic_Config_DeviceConfig_Role_REPEATER)) { config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; if (moduleConfig.store_forward.enabled && !moduleConfig.store_forward.is_server) { moduleConfig.store_forward.is_server = true; changes |= SEGMENT_MODULECONFIG; requiresReboot = true; } } #if USERPREFS_EVENT_MODE // If we're in event mode, nobody is a Router or Router Late if (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER || config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE) { config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; } #endif break; case meshtastic_Config_position_tag: LOG_INFO("Set config: Position"); config.has_position = true; // If we have turned off the GPS (disabled or not present) and we're not using fixed position, // clear the stored position since it may not get updated if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED && c.payload_variant.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED && config.position.fixed_position == false && c.payload_variant.position.fixed_position == false) { nodeDB->clearLocalPosition(); saveChanges(SEGMENT_NODEDATABASE | SEGMENT_CONFIG, false); } config.position = c.payload_variant.position; // Save nodedb as well in case we got a fixed position packet break; case meshtastic_Config_power_tag: LOG_INFO("Set config: Power"); config.has_power = true; // Really just the adc override is the only thing that can change without a reboot if (config.power.device_battery_ina_address == c.payload_variant.power.device_battery_ina_address && config.power.is_power_saving == c.payload_variant.power.is_power_saving && config.power.ls_secs == c.payload_variant.power.ls_secs && config.power.min_wake_secs == c.payload_variant.power.min_wake_secs && config.power.on_battery_shutdown_after_secs == c.payload_variant.power.on_battery_shutdown_after_secs && config.power.sds_secs == c.payload_variant.power.sds_secs && config.power.wait_bluetooth_secs == c.payload_variant.power.wait_bluetooth_secs) { requiresReboot = false; } config.power = c.payload_variant.power; if (c.payload_variant.power.on_battery_shutdown_after_secs > 0 && c.payload_variant.power.on_battery_shutdown_after_secs < 30) { LOG_WARN("Tried to set on_battery_shutdown_after_secs too low, set to min 30 seconds"); config.power.on_battery_shutdown_after_secs = 30; } break; case meshtastic_Config_network_tag: LOG_INFO("Set config: WiFi"); config.has_network = true; config.network = c.payload_variant.network; break; case meshtastic_Config_display_tag: LOG_INFO("Set config: Display"); config.has_display = true; if (config.display.screen_on_secs == c.payload_variant.display.screen_on_secs && config.display.flip_screen == c.payload_variant.display.flip_screen && config.display.oled == c.payload_variant.display.oled && config.display.displaymode == c.payload_variant.display.displaymode) { requiresReboot = false; } else if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR && c.payload_variant.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { config.bluetooth.enabled = false; } #if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR if (config.display.wake_on_tap_or_motion == false && c.payload_variant.display.wake_on_tap_or_motion == true && accelerometerThread->enabled == false) { config.display.wake_on_tap_or_motion = c.payload_variant.display.wake_on_tap_or_motion; accelerometerThread->enabled = true; accelerometerThread->start(); } #endif config.display = c.payload_variant.display; break; case meshtastic_Config_lora_tag: { // Wrap the entire case in a block to scope variables and avoid crossing initialization auto oldLoraConfig = config.lora; auto validatedLora = c.payload_variant.lora; LOG_INFO("Set config: LoRa"); config.has_lora = true; if (validatedLora.coding_rate != clampCodingRate(validatedLora.coding_rate)) { LOG_WARN("Invalid coding_rate %d, setting to %d", validatedLora.coding_rate, LORA_CR_DEFAULT); validatedLora.coding_rate = LORA_CR_DEFAULT; } if (validatedLora.spread_factor != clampSpreadFactor(validatedLora.spread_factor)) { LOG_WARN("Invalid spread_factor %d, setting to %d", validatedLora.spread_factor, LORA_SF_DEFAULT); validatedLora.spread_factor = LORA_SF_DEFAULT; } // If we're setting a new region, check the region is valid and then init the region or discard the change if (validatedLora.region != myRegion->code) { // Region has changed so check whether it is valid for e.g. licensing conditions and if the lora config is valid if (RadioInterface::validateConfigRegion(validatedLora) && RadioInterface::validateConfigLora(validatedLora)) { // If we're setting region for the first time, init the region and regenerate the keys if (isRegionUnset && validatedLora.region > meshtastic_Config_LoRaConfig_RegionCode_UNSET) { #if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN || MESHTASTIC_EXCLUDE_PKI) if (crypto) { crypto->ensurePkiKeys(config.security, owner); } #endif // new region is valid and we're coming from an unset region, so enable tx validatedLora.tx_enabled = true; } // If we're unsetting the region for some reason, disable tx if (!isRegionUnset && validatedLora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET) { validatedLora.tx_enabled = false; } // Ensure initRegion() uses the newly validated region config.lora.region = validatedLora.region; initRegion(); if (myRegion->dutyCycle < 100) { validatedLora.ignore_mqtt = true; // Ignore MQTT by default if region has a duty cycle limit } if (strncmp(moduleConfig.mqtt.root, default_mqtt_root, strlen(default_mqtt_root)) == 0) { // Default root is in use, so subscribe to the appropriate MQTT topic for this region sprintf(moduleConfig.mqtt.root, "%s/%s", default_mqtt_root, myRegion->name); } changes = SEGMENT_CONFIG | SEGMENT_MODULECONFIG; } else { // Region validation has failed, so just copy all of the old config over the new config validatedLora = oldLoraConfig; } } // end of new region handling if (!RadioInterface::validateConfigLora(validatedLora)) { if (fromOthers) { LOG_WARN("Invalid LoRa config received from another node, rejecting changes"); // modem_preset set to use the old setting if the check fails validatedLora.modem_preset = oldLoraConfig.modem_preset; } else { LOG_WARN("Invalid LoRa config received from client, using corrected values"); RadioInterface::clampConfigLora(validatedLora); } // use_preset and bandwidth are coerced into valid values by the check. } // If no lora radio parameters change, don't need to reboot if (oldLoraConfig.use_preset == validatedLora.use_preset && oldLoraConfig.region == validatedLora.region && oldLoraConfig.modem_preset == validatedLora.modem_preset && oldLoraConfig.bandwidth == validatedLora.bandwidth && oldLoraConfig.spread_factor == validatedLora.spread_factor && oldLoraConfig.coding_rate == validatedLora.coding_rate && oldLoraConfig.tx_power == validatedLora.tx_power && oldLoraConfig.frequency_offset == validatedLora.frequency_offset && oldLoraConfig.override_frequency == validatedLora.override_frequency && oldLoraConfig.channel_num == validatedLora.channel_num && oldLoraConfig.sx126x_rx_boosted_gain == validatedLora.sx126x_rx_boosted_gain) { requiresReboot = false; } #if defined(ARCH_PORTDUINO) // If running on portduino and using SimRadio, do not require reboot if (SimRadio::instance) { requiresReboot = false; } #endif #ifdef RF95_FAN_EN // Turn PA off if disabled by config if (c.payload_variant.lora.pa_fan_disabled) { digitalWrite(RF95_FAN_EN, LOW ^ 0); } else { digitalWrite(RF95_FAN_EN, HIGH ^ 0); } #endif #if HAS_LORA_FEM // Apply FEM LNA mode from config (only meaningful on hardware that supports it) // Note that a rejected lora config will revert this as well. if (loraFEMInterface.isLnaCanControl()) { loraFEMInterface.setLNAEnable(validatedLora.fem_lna_mode != meshtastic_Config_LoRaConfig_FEM_LNA_Mode_DISABLED); } else if (validatedLora.fem_lna_mode != meshtastic_Config_LoRaConfig_FEM_LNA_Mode_NOT_PRESENT) { // Hardware FEM does not support LNA control; normalize stored config to match actual capability LOG_WARN("FEM LNA mode configured but current FEM does not support LNA control; normalizing to NOT_PRESENT"); validatedLora.fem_lna_mode = meshtastic_Config_LoRaConfig_FEM_LNA_Mode_NOT_PRESENT; } #endif config.lora = validatedLora; // Finally, return the validated config back to the main config break; } case meshtastic_Config_bluetooth_tag: LOG_INFO("Set config: Bluetooth"); config.has_bluetooth = true; config.bluetooth = c.payload_variant.bluetooth; break; case meshtastic_Config_security_tag: LOG_INFO("Set config: Security"); config.security = c.payload_variant.security; #if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN) && !(MESHTASTIC_EXCLUDE_PKI) // If the client set the key to blank, go ahead and regenerate so long as we're not in ham mode if (!owner.is_licensed && config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_UNSET) { if (config.security.private_key.size != 32) { crypto->generateKeyPair(config.security.public_key.bytes, config.security.private_key.bytes); } else { if (crypto->regeneratePublicKey(config.security.public_key.bytes, config.security.private_key.bytes)) { config.security.public_key.size = 32; } } } #endif owner.public_key.size = config.security.public_key.size; memcpy(owner.public_key.bytes, config.security.public_key.bytes, config.security.public_key.size); #if !MESHTASTIC_EXCLUDE_PKI crypto->setDHPrivateKey(config.security.private_key.bytes); #endif if (config.security.is_managed && !(config.security.admin_key[0].size == 32 || config.security.admin_key[1].size == 32 || config.security.admin_key[2].size == 32)) { config.security.is_managed = false; const char *warning = "You must provide at least one admin public key to enable managed mode"; LOG_WARN(warning); sendWarning(warning); } if (config.security.debug_log_api_enabled == c.payload_variant.security.debug_log_api_enabled && config.security.serial_enabled == c.payload_variant.security.serial_enabled) requiresReboot = false; break; case meshtastic_Config_device_ui_tag: // NOOP! This is handled by handleStoreDeviceUIConfig break; } if (requiresReboot && !hasOpenEditTransaction) { disableBluetooth(); } // end of switch case which_payload_variant saveChanges(changes, requiresReboot); } // end of handleSetConfig bool AdminModule::handleSetModuleConfig(const meshtastic_ModuleConfig &c) { bool shouldReboot = true; // If we are in an open transaction or configuring MQTT or Serial (which have validation), defer disabling Bluetooth // Otherwise, disable Bluetooth to prevent the phone from interfering with the config if (!hasOpenEditTransaction && !IS_ONE_OF(c.which_payload_variant, meshtastic_ModuleConfig_mqtt_tag, meshtastic_ModuleConfig_serial_tag, meshtastic_ModuleConfig_statusmessage_tag)) { disableBluetooth(); } switch (c.which_payload_variant) { case meshtastic_ModuleConfig_mqtt_tag: #if MESHTASTIC_EXCLUDE_MQTT LOG_WARN("Set module config: MESHTASTIC_EXCLUDE_MQTT is defined. Not setting MQTT config"); return false; #else LOG_INFO("Set module config: MQTT"); if (!MQTT::isValidConfig(c.payload_variant.mqtt)) { return false; } // Disable Bluetooth to prevent interference during MQTT configuration disableBluetooth(); moduleConfig.has_mqtt = true; moduleConfig.mqtt = c.payload_variant.mqtt; #endif break; case meshtastic_ModuleConfig_serial_tag: LOG_INFO("Set module config: Serial"); #if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && \ !defined(CONFIG_IDF_TARGET_ESP32C3) if (!SerialModule::isValidConfig(c.payload_variant.serial)) { LOG_ERROR("Invalid serial config"); return false; } disableBluetooth(); // Disable Bluetooth to prevent interference during Serial configuration #endif moduleConfig.has_serial = true; moduleConfig.serial = c.payload_variant.serial; break; case meshtastic_ModuleConfig_external_notification_tag: LOG_INFO("Set module config: External Notification"); moduleConfig.has_external_notification = true; moduleConfig.external_notification = c.payload_variant.external_notification; break; case meshtastic_ModuleConfig_store_forward_tag: LOG_INFO("Set module config: Store & Forward"); moduleConfig.has_store_forward = true; moduleConfig.store_forward = c.payload_variant.store_forward; break; case meshtastic_ModuleConfig_range_test_tag: LOG_INFO("Set module config: Range Test"); moduleConfig.has_range_test = true; moduleConfig.range_test = c.payload_variant.range_test; break; case meshtastic_ModuleConfig_telemetry_tag: LOG_INFO("Set module config: Telemetry"); moduleConfig.has_telemetry = true; moduleConfig.telemetry = c.payload_variant.telemetry; break; case meshtastic_ModuleConfig_canned_message_tag: LOG_INFO("Set module config: Canned Message"); moduleConfig.has_canned_message = true; moduleConfig.canned_message = c.payload_variant.canned_message; break; case meshtastic_ModuleConfig_audio_tag: LOG_INFO("Set module config: Audio"); moduleConfig.has_audio = true; moduleConfig.audio = c.payload_variant.audio; break; case meshtastic_ModuleConfig_remote_hardware_tag: LOG_INFO("Set module config: Remote Hardware"); moduleConfig.has_remote_hardware = true; moduleConfig.remote_hardware = c.payload_variant.remote_hardware; break; case meshtastic_ModuleConfig_neighbor_info_tag: LOG_INFO("Set module config: Neighbor Info"); moduleConfig.has_neighbor_info = true; if (moduleConfig.neighbor_info.update_interval < min_neighbor_info_broadcast_secs) { LOG_DEBUG("Tried to set update_interval too low, setting to %d", default_neighbor_info_broadcast_secs); moduleConfig.neighbor_info.update_interval = default_neighbor_info_broadcast_secs; } moduleConfig.neighbor_info = c.payload_variant.neighbor_info; break; case meshtastic_ModuleConfig_detection_sensor_tag: LOG_INFO("Set module config: Detection Sensor"); moduleConfig.has_detection_sensor = true; moduleConfig.detection_sensor = c.payload_variant.detection_sensor; break; case meshtastic_ModuleConfig_ambient_lighting_tag: LOG_INFO("Set module config: Ambient Lighting"); moduleConfig.has_ambient_lighting = true; moduleConfig.ambient_lighting = c.payload_variant.ambient_lighting; break; case meshtastic_ModuleConfig_paxcounter_tag: LOG_INFO("Set module config: Paxcounter"); moduleConfig.has_paxcounter = true; moduleConfig.paxcounter = c.payload_variant.paxcounter; break; case meshtastic_ModuleConfig_statusmessage_tag: LOG_INFO("Set module config: StatusMessage"); moduleConfig.has_statusmessage = true; moduleConfig.statusmessage = c.payload_variant.statusmessage; shouldReboot = false; break; case meshtastic_ModuleConfig_traffic_management_tag: LOG_INFO("Set module config: Traffic Management"); moduleConfig.has_traffic_management = true; moduleConfig.traffic_management = c.payload_variant.traffic_management; break; } saveChanges(SEGMENT_MODULECONFIG, shouldReboot); return true; } void AdminModule::handleSetChannel(const meshtastic_Channel &cc) { channels.setChannel(cc); if (channels.ensureLicensedOperation()) { sendWarning(licensedModeMessage); } channels.onConfigChanged(); // tell the radios about this change saveChanges(SEGMENT_CHANNELS, false); } /** * Getters */ void AdminModule::handleGetOwner(const meshtastic_MeshPacket &req) { if (req.decoded.want_response) { // We create the reply here meshtastic_AdminMessage res = meshtastic_AdminMessage_init_default; res.get_owner_response = owner; res.which_payload_variant = meshtastic_AdminMessage_get_owner_response_tag; setPassKey(&res); myReply = allocDataProtobuf(res); if (req.pki_encrypted) { myReply->pki_encrypted = true; } } } void AdminModule::handleGetConfig(const meshtastic_MeshPacket &req, const uint32_t configType) { meshtastic_AdminMessage res = meshtastic_AdminMessage_init_default; if (req.decoded.want_response) { switch (configType) { case meshtastic_AdminMessage_ConfigType_DEVICE_CONFIG: LOG_INFO("Get config: Device"); res.get_config_response.which_payload_variant = meshtastic_Config_device_tag; res.get_config_response.payload_variant.device = config.device; break; case meshtastic_AdminMessage_ConfigType_POSITION_CONFIG: LOG_INFO("Get config: Position"); res.get_config_response.which_payload_variant = meshtastic_Config_position_tag; res.get_config_response.payload_variant.position = config.position; break; case meshtastic_AdminMessage_ConfigType_POWER_CONFIG: LOG_INFO("Get config: Power"); res.get_config_response.which_payload_variant = meshtastic_Config_power_tag; res.get_config_response.payload_variant.power = config.power; break; case meshtastic_AdminMessage_ConfigType_NETWORK_CONFIG: LOG_INFO("Get config: Network"); res.get_config_response.which_payload_variant = meshtastic_Config_network_tag; res.get_config_response.payload_variant.network = config.network; writeSecret(res.get_config_response.payload_variant.network.wifi_psk, sizeof(res.get_config_response.payload_variant.network.wifi_psk), config.network.wifi_psk); break; case meshtastic_AdminMessage_ConfigType_DISPLAY_CONFIG: LOG_INFO("Get config: Display"); res.get_config_response.which_payload_variant = meshtastic_Config_display_tag; res.get_config_response.payload_variant.display = config.display; break; case meshtastic_AdminMessage_ConfigType_LORA_CONFIG: LOG_INFO("Get config: LoRa"); res.get_config_response.which_payload_variant = meshtastic_Config_lora_tag; res.get_config_response.payload_variant.lora = config.lora; break; case meshtastic_AdminMessage_ConfigType_BLUETOOTH_CONFIG: LOG_INFO("Get config: Bluetooth"); res.get_config_response.which_payload_variant = meshtastic_Config_bluetooth_tag; res.get_config_response.payload_variant.bluetooth = config.bluetooth; break; case meshtastic_AdminMessage_ConfigType_SECURITY_CONFIG: LOG_INFO("Get config: Security"); res.get_config_response.which_payload_variant = meshtastic_Config_security_tag; res.get_config_response.payload_variant.security = config.security; break; case meshtastic_AdminMessage_ConfigType_SESSIONKEY_CONFIG: LOG_INFO("Get config: Sessionkey"); res.get_config_response.which_payload_variant = meshtastic_Config_sessionkey_tag; break; case meshtastic_AdminMessage_ConfigType_DEVICEUI_CONFIG: // NOOP! This is handled by handleGetDeviceUIConfig res.get_config_response.which_payload_variant = meshtastic_Config_device_ui_tag; break; } // NOTE: The phone app needs to know the ls_secs value so it can properly expect sleep behavior. // So even if we internally use 0 to represent 'use default' we still need to send the value we are // using to the app (so that even old phone apps work with new device loads). // r.get_radio_response.preferences.ls_secs = getPref_ls_secs(); // hideSecret(r.get_radio_response.preferences.wifi_ssid); // hmm - leave public for now, because only minimally // private and useful for users to know current provisioning) // hideSecret(r.get_radio_response.preferences.wifi_password); r.get_config_response.which_payloadVariant = // Config_ModuleConfig_telemetry_tag; res.which_payload_variant = meshtastic_AdminMessage_get_config_response_tag; setPassKey(&res); myReply = allocDataProtobuf(res); if (req.pki_encrypted) { myReply->pki_encrypted = true; } } } void AdminModule::handleGetModuleConfig(const meshtastic_MeshPacket &req, const uint32_t configType) { meshtastic_AdminMessage res = meshtastic_AdminMessage_init_default; if (req.decoded.want_response) { const char *configName = "?"; switch (configType) { case meshtastic_AdminMessage_ModuleConfigType_MQTT_CONFIG: configName = "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: configName = "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: configName = "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: configName = "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: configName = "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: configName = "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: configName = "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: configName = "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: configName = "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: configName = "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: configName = "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: configName = "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: configName = "Paxcounter"; res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_paxcounter_tag; res.get_module_config_response.payload_variant.paxcounter = moduleConfig.paxcounter; break; case meshtastic_AdminMessage_ModuleConfigType_STATUSMESSAGE_CONFIG: configName = "StatusMessage"; res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_statusmessage_tag; res.get_module_config_response.payload_variant.statusmessage = moduleConfig.statusmessage; break; case meshtastic_AdminMessage_ModuleConfigType_TRAFFICMANAGEMENT_CONFIG: configName = "Traffic Management"; res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_traffic_management_tag; res.get_module_config_response.payload_variant.traffic_management = moduleConfig.traffic_management; break; } LOG_INFO("Get module config: %s", configName); // NOTE: The phone app needs to know the ls_secsvalue so it can properly expect sleep behavior. // So even if we internally use 0 to represent 'use default' we still need to send the value we are // using to the app (so that even old phone apps work with new device loads). // r.get_radio_response.preferences.ls_secs = getPref_ls_secs(); // hideSecret(r.get_radio_response.preferences.wifi_ssid); // hmm - leave public for now, because only minimally // private and useful for users to know current provisioning) // hideSecret(r.get_radio_response.preferences.wifi_password); r.get_config_response.which_payloadVariant = // Config_ModuleConfig_telemetry_tag; res.which_payload_variant = meshtastic_AdminMessage_get_module_config_response_tag; setPassKey(&res); myReply = allocDataProtobuf(res); if (req.pki_encrypted) { myReply->pki_encrypted = true; } } } void AdminModule::handleGetNodeRemoteHardwarePins(const meshtastic_MeshPacket &req) { meshtastic_AdminMessage r = meshtastic_AdminMessage_init_default; r.which_payload_variant = meshtastic_AdminMessage_get_node_remote_hardware_pins_response_tag; for (uint8_t i = 0; i < devicestate.node_remote_hardware_pins_count; i++) { if (devicestate.node_remote_hardware_pins[i].node_num == 0 || !devicestate.node_remote_hardware_pins[i].has_pin) { continue; } r.get_node_remote_hardware_pins_response.node_remote_hardware_pins[i] = devicestate.node_remote_hardware_pins[i]; } for (uint8_t i = 0; i < moduleConfig.remote_hardware.available_pins_count; i++) { if (!moduleConfig.remote_hardware.available_pins[i].gpio_pin) { continue; } meshtastic_NodeRemoteHardwarePin nodePin = meshtastic_NodeRemoteHardwarePin_init_default; nodePin.node_num = nodeDB->getNodeNum(); nodePin.pin = moduleConfig.remote_hardware.available_pins[i]; r.get_node_remote_hardware_pins_response.node_remote_hardware_pins[i + 12] = nodePin; } setPassKey(&r); myReply = allocDataProtobuf(r); if (req.pki_encrypted) { myReply->pki_encrypted = true; } } void AdminModule::handleGetDeviceMetadata(const meshtastic_MeshPacket &req) { meshtastic_AdminMessage r = meshtastic_AdminMessage_init_default; r.get_device_metadata_response = getDeviceMetadata(); r.which_payload_variant = meshtastic_AdminMessage_get_device_metadata_response_tag; setPassKey(&r); myReply = allocDataProtobuf(r); if (req.pki_encrypted) { myReply->pki_encrypted = true; } } void AdminModule::handleGetDeviceConnectionStatus(const meshtastic_MeshPacket &req) { meshtastic_AdminMessage r = meshtastic_AdminMessage_init_default; meshtastic_DeviceConnectionStatus conn = meshtastic_DeviceConnectionStatus_init_zero; #if HAS_WIFI conn.has_wifi = true; conn.wifi.has_status = true; #ifdef ARCH_PORTDUINO conn.wifi.status.is_connected = true; #else conn.wifi.status.is_connected = WiFi.status() == WL_CONNECTED; #endif strncpy(conn.wifi.ssid, config.network.wifi_ssid, 33); if (conn.wifi.status.is_connected) { conn.wifi.rssi = WiFi.RSSI(); conn.wifi.status.ip_address = WiFi.localIP(); #ifndef MESHTASTIC_EXCLUDE_MQTT conn.wifi.status.is_mqtt_connected = mqtt && mqtt->isConnectedDirectly(); #endif conn.wifi.status.is_syslog_connected = false; // FIXME wire this up } #endif #if HAS_ETHERNET && !defined(USE_WS5500) conn.has_ethernet = true; conn.ethernet.has_status = true; if (Ethernet.linkStatus() == LinkON) { conn.ethernet.status.is_connected = true; conn.ethernet.status.ip_address = Ethernet.localIP(); #if !MESHTASTIC_EXCLUDE_MQTT conn.ethernet.status.is_mqtt_connected = mqtt && mqtt->isConnectedDirectly(); #endif conn.ethernet.status.is_syslog_connected = false; // FIXME wire this up } else { conn.ethernet.status.is_connected = false; } #endif #if HAS_BLUETOOTH conn.has_bluetooth = true; conn.bluetooth.pin = config.bluetooth.fixed_pin; #ifdef ARCH_ESP32 if (config.bluetooth.enabled && nimbleBluetooth) { conn.bluetooth.is_connected = nimbleBluetooth->isConnected(); conn.bluetooth.rssi = nimbleBluetooth->getRssi(); } #elif defined(ARCH_NRF52) if (config.bluetooth.enabled && nrf52Bluetooth) { conn.bluetooth.is_connected = nrf52Bluetooth->isConnected(); } #endif #endif conn.has_serial = true; // No serial-less devices #if !MESHTASTIC_EXCLUDE_POWER_FSM conn.serial.is_connected = powerFSM.getState() == &stateSERIAL; #else conn.serial.is_connected = powerFSM.getState(); #endif conn.serial.baud = SERIAL_BAUD; r.get_device_connection_status_response = conn; r.which_payload_variant = meshtastic_AdminMessage_get_device_connection_status_response_tag; setPassKey(&r); myReply = allocDataProtobuf(r); if (req.pki_encrypted) { myReply->pki_encrypted = true; } } void AdminModule::handleGetChannel(const meshtastic_MeshPacket &req, uint32_t channelIndex) { if (req.decoded.want_response) { // We create the reply here meshtastic_AdminMessage r = meshtastic_AdminMessage_init_default; r.get_channel_response = channels.getByIndex(channelIndex); r.which_payload_variant = meshtastic_AdminMessage_get_channel_response_tag; setPassKey(&r); myReply = allocDataProtobuf(r); if (req.pki_encrypted) { myReply->pki_encrypted = true; } } } void AdminModule::handleGetDeviceUIConfig(const meshtastic_MeshPacket &req) { meshtastic_AdminMessage r = meshtastic_AdminMessage_init_default; r.which_payload_variant = meshtastic_AdminMessage_get_ui_config_response_tag; r.get_ui_config_response = uiconfig; myReply = allocDataProtobuf(r); if (req.pki_encrypted) { myReply->pki_encrypted = true; } } void AdminModule::reboot(int32_t seconds) { LOG_INFO("Reboot in %d seconds", seconds); if (screen) screen->showSimpleBanner("Rebooting...", 0); // stays on screen rebootAtMsec = (seconds < 0) ? 0 : (millis() + seconds * 1000); } void AdminModule::saveChanges(int saveWhat, bool shouldReboot) { if (!hasOpenEditTransaction) { LOG_INFO("Save changes to disk"); service->reloadConfig(saveWhat); // Calls saveToDisk among other things } else { LOG_INFO("Delay save of changes to disk until the open transaction is committed"); } if (shouldReboot && !hasOpenEditTransaction) { reboot(DEFAULT_REBOOT_SECONDS); } } void AdminModule::handleStoreDeviceUIConfig(const meshtastic_DeviceUIConfig &uicfg) { nodeDB->saveProto("/prefs/uiconfig.proto", meshtastic_DeviceUIConfig_size, &meshtastic_DeviceUIConfig_msg, &uicfg); } void AdminModule::handleSetHamMode(const meshtastic_HamParameters &p) { // Validate ham parameters before setting since this would bypass validation in the owner struct const char *fieldsToCheck[] = {p.call_sign, p.short_name}; const char *fieldNames[] = {"call_sign", "short_name"}; for (int i = 0; i < 2; i++) { if (*fieldsToCheck[i]) { const char *start = fieldsToCheck[i]; while (*start && isspace((unsigned char)*start)) start++; if (*start == '\0') { LOG_WARN("Rejected ham %s: must contain at least 1 non-whitespace character", fieldNames[i]); return; } } } // Set call sign and override lora limitations for licensed use strncpy(owner.long_name, p.call_sign, sizeof(owner.long_name)); strncpy(owner.short_name, p.short_name, sizeof(owner.short_name)); owner.is_licensed = true; config.lora.override_duty_cycle = true; config.lora.tx_power = p.tx_power; config.lora.override_frequency = p.frequency; // Set node info broadcast interval to 10 minutes // For FCC minimum call-sign announcement config.device.node_info_broadcast_secs = 600; config.device.rebroadcast_mode = meshtastic_Config_DeviceConfig_RebroadcastMode_LOCAL_ONLY; // Remove PSK of primary channel for plaintext amateur usage if (channels.ensureLicensedOperation()) { sendWarning(licensedModeMessage); } channels.onConfigChanged(); service->reloadOwner(false); saveChanges(SEGMENT_CONFIG | SEGMENT_NODEDATABASE | SEGMENT_DEVICESTATE | SEGMENT_CHANNELS); } AdminModule::AdminModule() : ProtobufModule("Admin", meshtastic_PortNum_ADMIN_APP, &meshtastic_AdminMessage_msg) { // restrict to the admin channel for rx // boundChannel = Channels::adminChannel; } void AdminModule::setPassKey(meshtastic_AdminMessage *res) { if (session_time == 0 || millis() / 1000 > session_time + 150) { for (int i = 0; i < 8; i++) { session_passkey[i] = random(); } session_time = millis() / 1000; } memcpy(res->session_passkey.bytes, session_passkey, 8); res->session_passkey.size = 8; printBytes("Set admin key to ", res->session_passkey.bytes, 8); // if halfway to session_expire, regenerate session_passkey, reset the timeout // set the key in the packet } bool AdminModule::checkPassKey(meshtastic_AdminMessage *res) { // check that the key in the packet is still valid printBytes("Incoming session key: ", res->session_passkey.bytes, 8); printBytes("Expected session key: ", session_passkey, 8); return (session_time + 300 > millis() / 1000 && res->session_passkey.size == 8 && memcmp(res->session_passkey.bytes, session_passkey, 8) == 0); } bool AdminModule::messageIsResponse(const meshtastic_AdminMessage *r) { if (r->which_payload_variant == meshtastic_AdminMessage_get_channel_response_tag || r->which_payload_variant == meshtastic_AdminMessage_get_owner_response_tag || r->which_payload_variant == meshtastic_AdminMessage_get_config_response_tag || r->which_payload_variant == meshtastic_AdminMessage_get_module_config_response_tag || r->which_payload_variant == meshtastic_AdminMessage_get_canned_message_module_messages_response_tag || r->which_payload_variant == meshtastic_AdminMessage_get_device_metadata_response_tag || r->which_payload_variant == meshtastic_AdminMessage_get_ringtone_response_tag || r->which_payload_variant == meshtastic_AdminMessage_get_device_connection_status_response_tag || r->which_payload_variant == meshtastic_AdminMessage_get_node_remote_hardware_pins_response_tag || r->which_payload_variant == meshtastic_AdminMessage_get_ui_config_response_tag) return true; else return false; } bool AdminModule::messageIsRequest(const meshtastic_AdminMessage *r) { if (r->which_payload_variant == meshtastic_AdminMessage_get_channel_request_tag || r->which_payload_variant == meshtastic_AdminMessage_get_owner_request_tag || r->which_payload_variant == meshtastic_AdminMessage_get_config_request_tag || r->which_payload_variant == meshtastic_AdminMessage_get_module_config_request_tag || r->which_payload_variant == meshtastic_AdminMessage_get_canned_message_module_messages_request_tag || r->which_payload_variant == meshtastic_AdminMessage_get_device_metadata_request_tag || r->which_payload_variant == meshtastic_AdminMessage_get_ringtone_request_tag || r->which_payload_variant == meshtastic_AdminMessage_get_device_connection_status_request_tag || r->which_payload_variant == meshtastic_AdminMessage_get_node_remote_hardware_pins_request_tag || r->which_payload_variant == meshtastic_AdminMessage_get_ui_config_request_tag) return true; else return false; } void AdminModule::handleSendInputEvent(const meshtastic_AdminMessage_InputEvent &inputEvent) { LOG_DEBUG("Processing input event: event_code=%u, kb_char=%u, touch_x=%u, touch_y=%u", inputEvent.event_code, inputEvent.kb_char, inputEvent.touch_x, inputEvent.touch_y); // Create InputEvent for injection InputEvent event = {.inputEvent = (input_broker_event)inputEvent.event_code, .kbchar = (unsigned char)inputEvent.kb_char, .touchX = inputEvent.touch_x, .touchY = inputEvent.touch_y}; // Log the event being injected LOG_INFO("Injecting input event from admin: source=%s, event=%u, char=%c(%u), touch=(%u,%u)", event.source, event.inputEvent, (event.kbchar >= 32 && event.kbchar <= 126) ? event.kbchar : '?', event.kbchar, event.touchX, event.touchY); // Wake the device if asleep powerFSM.trigger(EVENT_INPUT); #if !defined(MESHTASTIC_EXCLUDE_INPUTBROKER) // Inject the event through InputBroker if (inputBroker) { inputBroker->injectInputEvent(&event); } else { LOG_ERROR("InputBroker not available for event injection"); } #endif } void AdminModule::sendWarning(const char *format, ...) { meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); if (!cn) return; cn->level = meshtastic_LogRecord_Level_WARNING; cn->time = getValidTime(RTCQualityFromNet); va_list args; va_start(args, format); // Format the arguments directly into the notification object vsnprintf(cn->message, sizeof(cn->message), format, args); va_end(args); service->sendClientNotification(cn); } void AdminModule::sendWarningAndLog(const char *format, ...) { // We need a temporary buffer to hold the formatted text so we can log it // Using 250 bytes as a safe upper limit for typical text notifications char buf[250]; va_list args; va_start(args, format); vsnprintf(buf, sizeof(buf), format, args); va_end(args); LOG_WARN(buf); // 2. Call sendWarning // SECURITY NOTE: We pass "%s", buf instead of just 'buf'. // If 'buf' contained a % symbol (e.g. "Battery 50%"), passing it directly // would crash sendWarning. "%s" treats it purely as text. sendWarning("%s", buf); } void disableBluetooth() { #if HAS_BLUETOOTH #ifdef ARCH_ESP32 if (nimbleBluetooth) nimbleBluetooth->deinit(); #elif defined(ARCH_NRF52) if (nrf52Bluetooth) nrf52Bluetooth->shutdown(); #endif #endif } ================================================ FILE: src/modules/AdminModule.h ================================================ #pragma once #ifdef ESP_PLATFORM #include #endif #include "ProtobufModule.h" #include #if HAS_WIFI #include "mesh/wifi/WiFiAPClient.h" #endif /** * Datatype passed to Observers by AdminModule, to allow external handling of admin messages */ struct AdminModule_ObserverData { const meshtastic_AdminMessage *request; meshtastic_AdminMessage *response; AdminMessageHandleResult *result; }; /** * 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); void handleGetDeviceUIConfig(const meshtastic_MeshPacket &req); /** * Setters */ void handleSetOwner(const meshtastic_User &o); void handleSetChannel(const meshtastic_Channel &cc); protected: void handleSetConfig(const meshtastic_Config &c, bool fromOthers); private: bool handleSetModuleConfig(const meshtastic_ModuleConfig &c); void handleSetChannel(); void handleSetHamMode(const meshtastic_HamParameters &req); void handleStoreDeviceUIConfig(const meshtastic_DeviceUIConfig &uicfg); void handleSendInputEvent(const meshtastic_AdminMessage_InputEvent &inputEvent); void reboot(int32_t seconds); void setPassKey(meshtastic_AdminMessage *res); bool checkPassKey(meshtastic_AdminMessage *res); bool messageIsResponse(const meshtastic_AdminMessage *r); bool messageIsRequest(const meshtastic_AdminMessage *r); void sendWarning(const char *format, ...) __attribute__((format(printf, 2, 3))); void sendWarningAndLog(const char *format, ...) __attribute__((format(printf, 2, 3))); }; static constexpr const char *licensedModeMessage = "Licensed mode activated, removing admin channel and encryption from all channels"; extern AdminModule *adminModule; void disableBluetooth(); ================================================ FILE: src/modules/AtakPluginModule.cpp ================================================ #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 "meshUtils.h" #include "meshtastic/atak.pb.h" AtakPluginModule *atakPluginModule; AtakPluginModule::AtakPluginModule() : ProtobufModule("atak", meshtastic_PortNum_ATAK_PLUGIN, &meshtastic_TAKPacket_msg), concurrency::OSThread("AtakPlugin") { ourPortNum = meshtastic_PortNum_ATAK_PLUGIN; } /* Encompasses the full construction and sending packet to mesh Will be used for broadcast. */ int32_t AtakPluginModule::runOnce() { return default_broadcast_interval_secs; } bool AtakPluginModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_TAKPacket *r) { return false; } meshtastic_TAKPacket AtakPluginModule::cloneTAKPacketData(meshtastic_TAKPacket *t) { meshtastic_TAKPacket clone = meshtastic_TAKPacket_init_zero; if (t->has_group) { clone.has_group = true; clone.group = t->group; } if (t->has_status) { clone.has_status = true; clone.status = t->status; } if (t->has_contact) { clone.has_contact = true; clone.contact = {0}; } if (t->which_payload_variant == meshtastic_TAKPacket_pli_tag) { clone.which_payload_variant = meshtastic_TAKPacket_pli_tag; clone.payload_variant.pli = t->payload_variant.pli; } else if (t->which_payload_variant == meshtastic_TAKPacket_chat_tag) { clone.which_payload_variant = meshtastic_TAKPacket_chat_tag; clone.payload_variant.chat = {0}; } else if (t->which_payload_variant == meshtastic_TAKPacket_detail_tag) { clone.which_payload_variant = meshtastic_TAKPacket_detail_tag; clone.payload_variant.detail.size = t->payload_variant.detail.size; memcpy(clone.payload_variant.detail.bytes, t->payload_variant.detail.bytes, t->payload_variant.detail.size); } return clone; } 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, pb_string_length(t->contact.callsign, sizeof(t->contact.callsign)), compressed.contact.callsign, sizeof(compressed.contact.callsign) - 1, USX_PSET_DFLT, NULL); if (length < 0) { LOG_WARN("Compress overflow contact.callsign. Revert to uncompressed packet"); return; } LOG_DEBUG("Compressed callsign: %d bytes", length); length = unishox2_compress_lines( t->contact.device_callsign, pb_string_length(t->contact.device_callsign, sizeof(t->contact.device_callsign)), compressed.contact.device_callsign, sizeof(compressed.contact.device_callsign) - 1, USX_PSET_DFLT, NULL); if (length < 0) { LOG_WARN("Compress overflow contact.device_callsign. Revert to uncompressed packet"); return; } LOG_DEBUG("Compressed device_callsign: %d bytes", length); } if (t->which_payload_variant == meshtastic_TAKPacket_chat_tag) { auto length = unishox2_compress_lines( t->payload_variant.chat.message, pb_string_length(t->payload_variant.chat.message, sizeof(t->payload_variant.chat.message)), compressed.payload_variant.chat.message, sizeof(compressed.payload_variant.chat.message) - 1, USX_PSET_DFLT, NULL); if (length < 0) { LOG_WARN("Compress overflow chat.message. Revert to uncompressed packet"); return; } LOG_DEBUG("Compressed chat message: %d bytes", length); if (t->payload_variant.chat.has_to) { compressed.payload_variant.chat.has_to = true; length = unishox2_compress_lines( t->payload_variant.chat.to, pb_string_length(t->payload_variant.chat.to, sizeof(t->payload_variant.chat.to)), compressed.payload_variant.chat.to, sizeof(compressed.payload_variant.chat.to) - 1, USX_PSET_DFLT, NULL); if (length < 0) { LOG_WARN("Compress overflow chat.to. Revert to uncompressed packet"); return; } LOG_DEBUG("Compressed chat to: %d bytes", length); } if (t->payload_variant.chat.has_to_callsign) { compressed.payload_variant.chat.has_to_callsign = true; length = unishox2_compress_lines( t->payload_variant.chat.to_callsign, pb_string_length(t->payload_variant.chat.to_callsign, sizeof(t->payload_variant.chat.to_callsign)), compressed.payload_variant.chat.to_callsign, sizeof(compressed.payload_variant.chat.to_callsign) - 1, USX_PSET_DFLT, NULL); if (length < 0) { LOG_WARN("Compress overflow chat.to_callsign. Revert to uncompressed packet"); return; } LOG_DEBUG("Compressed chat to_callsign: %d bytes", length); } } mp.decoded.payload.size = pb_encode_to_bytes(mp.decoded.payload.bytes, sizeof(mp.decoded.payload.bytes), meshtastic_TAKPacket_fields, &compressed); LOG_DEBUG("Final payload: %d bytes", mp.decoded.payload.size); } else { if (!t->is_compressed) { // Not compressed. Something is wrong LOG_WARN("Received uncompressed TAKPacket over radio! Skip"); return; } // Decompress for Phone (EUD) auto uncompressed = cloneTAKPacketData(t); uncompressed.is_compressed = false; if (t->has_contact) { auto length = unishox2_decompress_lines( t->contact.callsign, pb_string_length(t->contact.callsign, sizeof(t->contact.callsign)), uncompressed.contact.callsign, sizeof(uncompressed.contact.callsign) - 1, USX_PSET_DFLT, NULL); if (length < 0) { LOG_WARN("Decompress overflow contact.callsign. Bailing out"); return; } LOG_DEBUG("Decompressed callsign: %d bytes", length); length = unishox2_decompress_lines( t->contact.device_callsign, pb_string_length(t->contact.device_callsign, sizeof(t->contact.device_callsign)), uncompressed.contact.device_callsign, sizeof(uncompressed.contact.device_callsign) - 1, USX_PSET_DFLT, NULL); if (length < 0) { LOG_WARN("Decompress overflow contact.device_callsign. Bailing out"); return; } LOG_DEBUG("Decompressed device_callsign: %d bytes", length); } if (uncompressed.which_payload_variant == meshtastic_TAKPacket_chat_tag) { auto length = unishox2_decompress_lines( t->payload_variant.chat.message, pb_string_length(t->payload_variant.chat.message, sizeof(t->payload_variant.chat.message)), uncompressed.payload_variant.chat.message, sizeof(uncompressed.payload_variant.chat.message) - 1, USX_PSET_DFLT, NULL); if (length < 0) { LOG_WARN("Decompress overflow chat.message. Bailing out"); return; } LOG_DEBUG("Decompressed chat message: %d bytes", length); if (t->payload_variant.chat.has_to) { uncompressed.payload_variant.chat.has_to = true; length = unishox2_decompress_lines( t->payload_variant.chat.to, pb_string_length(t->payload_variant.chat.to, sizeof(t->payload_variant.chat.to)), uncompressed.payload_variant.chat.to, sizeof(uncompressed.payload_variant.chat.to) - 1, USX_PSET_DFLT, NULL); if (length < 0) { LOG_WARN("Decompress overflow chat.to. Bailing out"); return; } LOG_DEBUG("Decompressed chat to: %d bytes", length); } if (t->payload_variant.chat.has_to_callsign) { uncompressed.payload_variant.chat.has_to_callsign = true; length = unishox2_decompress_lines( t->payload_variant.chat.to_callsign, pb_string_length(t->payload_variant.chat.to_callsign, sizeof(t->payload_variant.chat.to_callsign)), uncompressed.payload_variant.chat.to_callsign, sizeof(uncompressed.payload_variant.chat.to_callsign) - 1, USX_PSET_DFLT, NULL); if (length < 0) { LOG_WARN("Decompress overflow chat.to_callsign. Bailing out"); return; } LOG_DEBUG("Decompressed chat to_callsign: %d bytes", length); } } auto decompressedCopy = packetPool.allocCopy(mp); decompressedCopy->decoded.payload.size = pb_encode_to_bytes(decompressedCopy->decoded.payload.bytes, sizeof(decompressedCopy->decoded.payload), meshtastic_TAKPacket_fields, &uncompressed); service->sendToPhone(decompressedCopy); } return; } ================================================ FILE: src/modules/AtakPluginModule.h ================================================ #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; ================================================ FILE: src/modules/CannedMessageModule.cpp ================================================ #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 "MessageStore.h" #include "NodeDB.h" #include "SPILock.h" #include "buzz.h" #include "detect/ScanI2C.h" #include "gps/RTC.h" #include "graphics/EmoteRenderer.h" #include "graphics/Screen.h" #include "graphics/SharedUIDisplay.h" #include "graphics/draw/MessageRenderer.h" #include "graphics/draw/NotificationRenderer.h" #include "graphics/draw/UIRenderer.h" #include "graphics/emotes.h" #include "graphics/images.h" #include "input/SerialKeyboard.h" #include "main.h" // for cardkb_found #include "mesh/generated/meshtastic/cannedmessages.pb.h" #include "modules/AdminModule.h" #include "modules/ExternalNotificationModule.h" // for buzzer control extern MessageStore messageStore; #if HAS_TRACKBALL #include "input/TrackballInterruptImpl1.h" #endif #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 namespace graphics { extern int bannerSignalBars; } extern ScanI2C::DeviceAddress cardkb_found; extern bool osk_found; static const char *cannedMessagesConfigFile = "/prefs/cannedConf.proto"; static NodeNum lastDest = NODENUM_BROADCAST; static uint8_t lastChannel = 0; static bool lastDestSet = false; meshtastic_CannedMessageModuleConfig cannedMessageModuleConfig; CannedMessageModule *cannedMessageModule; CannedMessageModule::CannedMessageModule() : SinglePortModule("canned", meshtastic_PortNum_TEXT_MESSAGE_APP), concurrency::OSThread("CannedMessage") { this->loadProtoForModule(); if ((this->splitConfiguredMessages() <= 0) && (cardkb_found.address == 0x00) && !INPUTBROKER_MATRIX_TYPE) { LOG_INFO("CannedMessageModule: No messages are configured. Module is disabled"); this->updateState(CANNED_MESSAGE_RUN_STATE_DISABLED); disable(); } else { LOG_INFO("CannedMessageModule is enabled"); moduleConfig.canned_message.enabled = true; this->inputObserver.observe(inputBroker); } } void CannedMessageModule::LaunchWithDestination(NodeNum newDest, uint8_t newChannel) { // Do NOT override explicit broadcast replies // Only reuse lastDest in LaunchRepeatDestination() dest = newDest; channel = newChannel; lastDest = dest; lastChannel = channel; lastDestSet = true; // Upon activation, highlight "[Select Destination]" int selectDestination = 0; for (int i = 0; i < messagesCount; ++i) { if (strcmp(messages[i], "[Select Destination]") == 0) { selectDestination = i; break; } } currentMessageIndex = selectDestination; // This triggers the canned message list updateState(CANNED_MESSAGE_RUN_STATE_ACTIVE, true); UIFrameEvent e; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; notifyObservers(&e); LOG_DEBUG("[CannedMessage] LaunchWithDestination dest=0x%08x ch=%d", dest, channel); } void CannedMessageModule::LaunchRepeatDestination() { if (!lastDestSet) { LaunchWithDestination(NODENUM_BROADCAST, 0); } else { LaunchWithDestination(lastDest, lastChannel); } } void CannedMessageModule::LaunchFreetextWithDestination(NodeNum newDest, uint8_t newChannel) { // Do NOT override explicit broadcast replies // Only reuse lastDest in LaunchRepeatDestination() dest = newDest; channel = newChannel; lastDest = dest; lastChannel = channel; lastDestSet = true; updateState(CANNED_MESSAGE_RUN_STATE_FREETEXT, true); UIFrameEvent e; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; notifyObservers(&e); LOG_DEBUG("[CannedMessage] LaunchFreetextWithDestination dest=0x%08x ch=%d", dest, channel); } static bool returnToCannedList = false; bool hasKeyForNode(const meshtastic_NodeInfoLite *node) { return node && node->has_user && node->user.public_key.size > 0; } /** * @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. */ int CannedMessageModule::splitConfiguredMessages() { int i = 0; String canned_messages = cannedMessageModuleConfig.messages; // Copy all message parts into the buffer strncpy(this->messageBuffer, canned_messages.c_str(), sizeof(this->messageBuffer)); // Temporary array to allow for insertion const char *tempMessages[CANNED_MESSAGE_MODULE_MESSAGE_MAX_COUNT + 3] = {0}; int tempCount = 0; // Insert at position 0 (top) tempMessages[tempCount++] = "[Select Destination]"; #if defined(USE_VIRTUAL_KEYBOARD) // Add a "Free Text" entry at the top if using a touch screen virtual keyboard tempMessages[tempCount++] = "[-- Free Text --]"; #else if (osk_found && screen) { tempMessages[tempCount++] = "[-- Free Text --]"; } #endif // First message always starts at buffer start tempMessages[tempCount++] = this->messageBuffer; int upTo = strlen(this->messageBuffer) - 1; // Walk buffer, splitting on '|' while (i < upTo) { if (this->messageBuffer[i] == '|') { this->messageBuffer[i] = '\0'; // End previous message if (tempCount >= CANNED_MESSAGE_MODULE_MESSAGE_MAX_COUNT) break; tempMessages[tempCount++] = (this->messageBuffer + i + 1); } i += 1; } // Add [Exit] as the last entry tempMessages[tempCount++] = "[Exit]"; // Copy to the member array for (int k = 0; k < tempCount; ++k) { this->messages[k] = (char *)tempMessages[k]; } this->messagesCount = tempCount; return this->messagesCount; } void CannedMessageModule::drawHeader(OLEDDisplay *display, int16_t x, int16_t y, char *buffer) { (void)buffer; char header[96]; if (this->dest == NODENUM_BROADCAST) { const char *channelName = channels.getName(this->channel); snprintf(header, sizeof(header), "To: #%s", channelName ? channelName : "?"); } else { snprintf(header, sizeof(header), "To: @%s", getNodeName(this->dest)); } const int maxWidth = std::max(0, display->getWidth() - x); char truncatedHeader[96]; graphics::UIRenderer::truncateStringWithEmotes(display, header, truncatedHeader, sizeof(truncatedHeader), maxWidth); graphics::UIRenderer::drawStringWithEmotes(display, x, y, truncatedHeader, FONT_HEIGHT_SMALL, 1, false); } void CannedMessageModule::resetSearch() { int previousDestIndex = destIndex; searchQuery = ""; updateDestinationSelectionList(); // Adjust scrollIndex so previousDestIndex is still visible int totalEntries = activeChannelIndices.size() + filteredNodes.size(); this->visibleRows = (displayHeight - FONT_HEIGHT_SMALL * 2) / FONT_HEIGHT_SMALL; if (this->visibleRows < 1) this->visibleRows = 1; int maxScrollIndex = std::max(0, totalEntries - visibleRows); scrollIndex = std::min(std::max(previousDestIndex - (visibleRows / 2), 0), maxScrollIndex); lastUpdateMillis = millis(); requestFocus(); } void CannedMessageModule::updateDestinationSelectionList() { static size_t lastNumMeshNodes = 0; static String lastSearchQuery = ""; size_t numMeshNodes = nodeDB->getNumMeshNodes(); bool nodesChanged = (numMeshNodes != lastNumMeshNodes); lastNumMeshNodes = numMeshNodes; // Early exit if nothing changed if (searchQuery == lastSearchQuery && !nodesChanged) return; lastSearchQuery = searchQuery; needsUpdate = false; this->filteredNodes.clear(); this->activeChannelIndices.clear(); NodeNum myNodeNum = nodeDB->getNodeNum(); String lowerSearchQuery = searchQuery; lowerSearchQuery.toLowerCase(); // Preallocate space to reduce reallocation this->filteredNodes.reserve(numMeshNodes); for (size_t i = 0; i < numMeshNodes; ++i) { meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); if (!node || node->num == myNodeNum || !node->has_user || node->user.public_key.size != 32) continue; const String &nodeName = node->user.long_name; if (searchQuery.length() == 0) { this->filteredNodes.push_back({node, sinceLastSeen(node)}); } else { // Avoid unnecessary lowercase conversion if already matched String lowerNodeName = nodeName; lowerNodeName.toLowerCase(); if (lowerNodeName.indexOf(lowerSearchQuery) != -1) { this->filteredNodes.push_back({node, sinceLastSeen(node)}); } } } meshtastic_MeshPacket *p = allocDataPacket(); p->pki_encrypted = true; p->channel = 0; // Populate active channels std::vector seenChannels; seenChannels.reserve(channels.getNumChannels()); for (uint8_t i = 0; i < channels.getNumChannels(); ++i) { String name = channels.getName(i); if (name.length() > 0 && std::find(seenChannels.begin(), seenChannels.end(), name) == seenChannels.end()) { this->activeChannelIndices.push_back(i); seenChannels.push_back(name); } } scrollIndex = 0; // Show first result at the top destIndex = 0; // Highlight the first entry if (nodesChanged && runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) { LOG_INFO("Nodes changed, forcing UI refresh."); screen->forceDisplay(); } } // Returns true if character input is currently allowed (used for search/freetext states) bool CannedMessageModule::isCharInputAllowed() const { return runState == CANNED_MESSAGE_RUN_STATE_FREETEXT || runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION; } static int getRowHeightForEmoteText(const char *text, int minimumHeight, int emoteSpacing = 2) { // Grow the row only when an emote is taller than the font. const auto metrics = graphics::EmoteRenderer::analyzeLine(nullptr, text ? text : "", 0, graphics::emotes, graphics::numEmotes, emoteSpacing); return std::max(minimumHeight, metrics.tallestHeight + 2); } static void drawCenteredEmoteText(OLEDDisplay *display, int x, int y, int rowHeight, const char *text, int emoteSpacing = 2) { // Center mixed text and emotes inside the row height. const auto metrics = graphics::EmoteRenderer::analyzeLine(nullptr, text ? text : "", FONT_HEIGHT_SMALL, graphics::emotes, graphics::numEmotes, emoteSpacing); const int contentHeight = std::max(FONT_HEIGHT_SMALL, metrics.tallestHeight); const int drawY = y + ((rowHeight - contentHeight) / 2); graphics::EmoteRenderer::drawStringWithEmotes(display, x, drawY, text ? text : "", FONT_HEIGHT_SMALL, graphics::emotes, graphics::numEmotes, emoteSpacing, false); } static size_t firstWrappedTokenLen(const char *text) { // Fall back to one full emote or one UTF-8 glyph when width is tiny. if (!text || !*text) return 0; const size_t textLen = strlen(text); size_t matchLen = 0; if (graphics::EmoteRenderer::findEmoteAt(text, textLen, 0, matchLen, graphics::emotes, graphics::numEmotes)) return matchLen; return graphics::EmoteRenderer::utf8CharLen(static_cast(text[0])); } static void drawWrappedEmoteText(OLEDDisplay *display, int x, int y, const char *text, int maxWidth, int minimumRowHeight, int emoteSpacing = 2) { // Wrap onto multiple rows without splitting emotes. if (!display || !text || maxWidth <= 0) return; constexpr size_t kLineBufferSize = 256; char lineBuffer[kLineBufferSize]; const size_t textLen = strlen(text); size_t offset = 0; int yCursor = y; while (offset < textLen) { size_t copied = graphics::EmoteRenderer::truncateToWidth(display, text + offset, lineBuffer, sizeof(lineBuffer), maxWidth, "", graphics::emotes, graphics::numEmotes, emoteSpacing); size_t consumed = copied; if (copied == 0) { consumed = firstWrappedTokenLen(text + offset); if (consumed == 0) break; const size_t fallbackLen = std::min(consumed, sizeof(lineBuffer) - 1); memcpy(lineBuffer, text + offset, fallbackLen); lineBuffer[fallbackLen] = '\0'; consumed = fallbackLen; } else if (text[offset + copied] != '\0') { // Prefer wrapping at the last space when a full line does not fit. size_t lastSpace = copied; while (lastSpace > 0 && lineBuffer[lastSpace - 1] != ' ') --lastSpace; if (lastSpace > 0) { consumed = lastSpace; while (consumed > 0 && lineBuffer[consumed - 1] == ' ') --consumed; lineBuffer[consumed] = '\0'; } } if (lineBuffer[0]) { const int rowHeight = getRowHeightForEmoteText(lineBuffer, minimumRowHeight, emoteSpacing); drawCenteredEmoteText(display, x, yCursor, rowHeight, lineBuffer, emoteSpacing); yCursor += rowHeight; } offset += std::max(consumed, 1); while (offset < textLen && text[offset] == ' ') ++offset; } } /** * Main input event dispatcher for CannedMessageModule. * Routes keyboard/button/touch input to the correct handler based on the current runState. * Only one handler (per state) processes each event, eliminating redundancy. */ int CannedMessageModule::handleInputEvent(const InputEvent *event) { // Block ALL input if an alert banner is active if (screen && screen->isOverlayBannerShowing()) { return 0; } // Tab key: Always allow switching between canned/destination screens if (event->kbchar == INPUT_BROKER_MSG_TAB && handleTabSwitch(event)) return 1; // Matrix keypad: If matrix key, trigger action select for canned message if (event->inputEvent == INPUT_BROKER_MATRIXKEY) { updateState(CANNED_MESSAGE_RUN_STATE_ACTION_SELECT, true); payload = INPUT_BROKER_MATRIXKEY; currentMessageIndex = event->kbchar - 1; lastTouchMillis = millis(); return 1; } // Always normalize navigation/select buttons for further handlers bool isUp = isUpEvent(event); bool isDown = isDownEvent(event); bool isSelect = isSelectEvent(event); // Route event to handler for current UI state (no double-handling) switch (runState) { // Node/Channel destination selection mode: Handles character search, arrows, select, cancel, backspace case CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION: if (handleDestinationSelectionInput(event, isUp, isDown, isSelect)) return 1; return 0; // prevent fall-through to selector input // Free text input mode: Handles character input, cancel, backspace, select, etc. case CANNED_MESSAGE_RUN_STATE_FREETEXT: return handleFreeTextInput(event); // All allowed input for this state // Virtual keyboard mode: Show virtual keyboard and handle input // If sending, block all input except global/system (handled above) case CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE: return 1; // If sending, block all input except global/system (handled above) case CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER: return handleEmotePickerInput(event); case CANNED_MESSAGE_RUN_STATE_INACTIVE: if (event->inputEvent == INPUT_BROKER_ALT_LONG) { LaunchWithDestination(NODENUM_BROADCAST); return 1; } // Printable char (ASCII) opens free text compose if (event->kbchar >= 32 && event->kbchar <= 126) { updateState(CANNED_MESSAGE_RUN_STATE_FREETEXT, true); UIFrameEvent e; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; notifyObservers(&e); // Immediately process the input in the new state (freetext) return handleFreeTextInput(event); } return 0; break; // (Other states can be added here as needed) default: break; } // If no state handler above processed the event, let the message selector try to handle it // (Handles up/down/select on canned message list, exit/return) if (handleMessageSelectorInput(event, isUp, isDown, isSelect)) return 1; // Default: event not handled by canned message system, allow others to process return 0; } void CannedMessageModule::updateState(cannedMessageModuleRunState newState, bool shouldRequestFocus) { runState = newState; if (runState == CANNED_MESSAGE_RUN_STATE_FREETEXT) { inputBroker->menuMode = false; // Allow any key input to be sent to the message composer instead of being interpreted as menu navigation } else { inputBroker->menuMode = true; // Re-enable menu navigation for destination selection } if (shouldRequestFocus) { requestFocus(); } } bool CannedMessageModule::isUpEvent(const InputEvent *event) { return event->inputEvent == INPUT_BROKER_UP || ((runState == CANNED_MESSAGE_RUN_STATE_ACTIVE || runState == CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER || runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) && (event->inputEvent == INPUT_BROKER_LEFT || event->inputEvent == INPUT_BROKER_ALT_PRESS)); } bool CannedMessageModule::isDownEvent(const InputEvent *event) { return event->inputEvent == INPUT_BROKER_DOWN || ((runState == CANNED_MESSAGE_RUN_STATE_ACTIVE || runState == CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER || runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) && (event->inputEvent == INPUT_BROKER_RIGHT || event->inputEvent == INPUT_BROKER_USER_PRESS)); } bool CannedMessageModule::isSelectEvent(const InputEvent *event) { return event->inputEvent == INPUT_BROKER_SELECT; } bool CannedMessageModule::handleTabSwitch(const InputEvent *event) { if (event->kbchar != 0x09) return false; const cannedMessageModuleRunState targetState = (runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) ? CANNED_MESSAGE_RUN_STATE_FREETEXT : CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION; destIndex = 0; scrollIndex = 0; if (targetState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) updateDestinationSelectionList(); updateState(targetState, true); UIFrameEvent e; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; notifyObservers(&e); screen->forceDisplay(); return true; } int CannedMessageModule::handleDestinationSelectionInput(const InputEvent *event, bool isUp, bool isDown, bool isSelect) { // Override isDown and isSelect ONLY for destination selector behavior if (runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) { if (event->inputEvent == INPUT_BROKER_USER_PRESS) { isDown = true; } else if (event->inputEvent == INPUT_BROKER_SELECT) { isSelect = true; } } if (event->kbchar >= 32 && event->kbchar <= 126 && !isUp && !isDown && event->inputEvent != INPUT_BROKER_LEFT && event->inputEvent != INPUT_BROKER_RIGHT && event->inputEvent != INPUT_BROKER_SELECT) { this->searchQuery += (char)event->kbchar; needsUpdate = true; if ((millis() - lastFilterUpdate) > filterDebounceMs) { runOnce(); // update filter immediately lastFilterUpdate = millis(); } return 1; } size_t numMeshNodes = filteredNodes.size(); int totalEntries = numMeshNodes + activeChannelIndices.size(); int columns = 1; int totalRows = totalEntries; int maxScrollIndex = std::max(0, totalRows - visibleRows); scrollIndex = clamp(scrollIndex, 0, maxScrollIndex); // Handle backspace if (event->inputEvent == INPUT_BROKER_BACK) { if (searchQuery.length() > 0) { searchQuery.remove(searchQuery.length() - 1); needsUpdate = true; runOnce(); } if (searchQuery.length() == 0) { resetSearch(); needsUpdate = false; } return 1; } if (isUp) { if (destIndex > 0) { destIndex--; } else if (totalEntries > 0) { destIndex = totalEntries - 1; } if ((destIndex / columns) < scrollIndex) scrollIndex = destIndex / columns; else if ((destIndex / columns) >= (scrollIndex + visibleRows)) scrollIndex = (destIndex / columns) - visibleRows + 1; screen->forceDisplay(true); return 1; } if (isDown) { if (destIndex + 1 < totalEntries) { destIndex++; } else if (totalEntries > 0) { destIndex = 0; scrollIndex = 0; } if ((destIndex / columns) >= (scrollIndex + visibleRows)) scrollIndex = (destIndex / columns) - visibleRows + 1; screen->forceDisplay(true); return 1; } // SELECT if (isSelect) { if (destIndex < static_cast(activeChannelIndices.size())) { dest = NODENUM_BROADCAST; channel = activeChannelIndices[destIndex]; lastDest = dest; lastChannel = channel; lastDestSet = true; } else { int nodeIndex = destIndex - static_cast(activeChannelIndices.size()); if (nodeIndex >= 0 && nodeIndex < static_cast(filteredNodes.size())) { const meshtastic_NodeInfoLite *selectedNode = filteredNodes[nodeIndex].node; if (selectedNode) { dest = selectedNode->num; channel = selectedNode->channel; // Already saves here, but for clarity, also: lastDest = dest; lastChannel = channel; lastDestSet = true; } } } updateState(returnToCannedList ? CANNED_MESSAGE_RUN_STATE_ACTIVE : CANNED_MESSAGE_RUN_STATE_FREETEXT, true); returnToCannedList = false; screen->forceDisplay(true); return 1; } // CANCEL if (event->inputEvent == INPUT_BROKER_CANCEL || event->inputEvent == INPUT_BROKER_ALT_LONG) { updateState(returnToCannedList ? CANNED_MESSAGE_RUN_STATE_ACTIVE : CANNED_MESSAGE_RUN_STATE_FREETEXT, true); returnToCannedList = false; searchQuery = ""; // UIFrameEvent e; // e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; // notifyObservers(&e); screen->forceDisplay(true); return 1; } return 0; } bool CannedMessageModule::handleMessageSelectorInput(const InputEvent *event, bool isUp, bool isDown, bool isSelect) { // Override isDown and isSelect ONLY for canned message list behavior if (runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) { if (event->inputEvent == INPUT_BROKER_USER_PRESS) { isDown = true; } else if (event->inputEvent == INPUT_BROKER_SELECT) { isSelect = true; } } if (runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) return false; // Handle Cancel key: go inactive, clear UI state if (runState != CANNED_MESSAGE_RUN_STATE_INACTIVE && (event->inputEvent == INPUT_BROKER_CANCEL || event->inputEvent == INPUT_BROKER_ALT_LONG)) { updateState(CANNED_MESSAGE_RUN_STATE_INACTIVE); freetext = ""; cursor = 0; payload = 0; currentMessageIndex = -1; // Notify UI that we want to redraw/close this screen UIFrameEvent e; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; notifyObservers(&e); screen->forceDisplay(); return true; } bool handled = false; // Handle up/down navigation if (isUp && messagesCount > 0) { updateState(CANNED_MESSAGE_RUN_STATE_ACTION_UP); handled = true; } else if (isDown && messagesCount > 0) { updateState(CANNED_MESSAGE_RUN_STATE_ACTION_DOWN); handled = true; } else if (isSelect) { const char *current = messages[currentMessageIndex]; // [Select Destination] triggers destination selection UI if (strcmp(current, "[Select Destination]") == 0) { returnToCannedList = true; updateState(CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION, true); destIndex = 0; scrollIndex = 0; updateDestinationSelectionList(); // Make sure list is fresh screen->forceDisplay(); return true; } // [Exit] returns to the main/inactive screen if (strcmp(current, "[Exit]") == 0) { // Set runState to inactive so we return to main UI updateState(CANNED_MESSAGE_RUN_STATE_INACTIVE); currentMessageIndex = -1; // Notify UI to regenerate frame set and redraw UIFrameEvent e; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; notifyObservers(&e); screen->forceDisplay(); return true; } // [Free Text] triggers the free text input (virtual keyboard) #if defined(USE_VIRTUAL_KEYBOARD) if (strcmp(current, "[-- Free Text --]") == 0) { updateState(CANNED_MESSAGE_RUN_STATE_FREETEXT, true); UIFrameEvent e; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; notifyObservers(&e); return true; } #else if (strcmp(current, "[-- Free Text --]") == 0) { if (osk_found && screen) { char headerBuffer[64]; if (this->dest == NODENUM_BROADCAST) { snprintf(headerBuffer, sizeof(headerBuffer), "To: #%s", channels.getName(this->channel)); } else { snprintf(headerBuffer, sizeof(headerBuffer), "To: @%s", getNodeName(this->dest)); } screen->showTextInput(headerBuffer, "", 300000, [this](const std::string &text) { if (!text.empty()) { this->freetext = text.c_str(); this->payload = CANNED_MESSAGE_RUN_STATE_FREETEXT; updateState(CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE); currentMessageIndex = -1; UIFrameEvent e; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; this->notifyObservers(&e); screen->forceDisplay(); setIntervalFromNow(500); return; } else { // Don't delete virtual keyboard immediately - it might still be executing // Instead, just clear the callback and reset banner to stop input processing graphics::NotificationRenderer::textInputCallback = nullptr; graphics::NotificationRenderer::resetBanner(); // Return to inactive state this->updateState(CANNED_MESSAGE_RUN_STATE_INACTIVE); this->currentMessageIndex = -1; this->freetext = ""; this->cursor = 0; // Force display update to show normal screen UIFrameEvent e; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; this->notifyObservers(&e); screen->forceDisplay(); // Schedule cleanup for next loop iteration to ensure safe deletion setIntervalFromNow(50); return; } }); return true; } } #endif // Normal canned message selection if (runState == CANNED_MESSAGE_RUN_STATE_INACTIVE || runState == CANNED_MESSAGE_RUN_STATE_DISABLED) { } else { #if CANNED_MESSAGE_ADD_CONFIRMATION const int savedIndex = currentMessageIndex; graphics::menuHandler::showConfirmationBanner("Send message?", [this, savedIndex]() { this->currentMessageIndex = savedIndex; this->payload = this->runState; this->updateState(CANNED_MESSAGE_RUN_STATE_ACTION_SELECT); this->setIntervalFromNow(0); }); #else payload = runState; updateState(CANNED_MESSAGE_RUN_STATE_ACTION_SELECT); #endif // Do not immediately set runState; wait for confirmation handled = true; } } if (handled) { requestFocus(); if (runState == CANNED_MESSAGE_RUN_STATE_ACTION_SELECT) setIntervalFromNow(0); else runOnce(); } return handled; } bool CannedMessageModule::handleFreeTextInput(const InputEvent *event) { // Always process only if in FREETEXT mode if (runState != CANNED_MESSAGE_RUN_STATE_FREETEXT) return false; #if defined(USE_VIRTUAL_KEYBOARD) // Cancel (dismiss freetext screen) if (event->inputEvent == INPUT_BROKER_LEFT) { updateState(CANNED_MESSAGE_RUN_STATE_INACTIVE); freetext = ""; cursor = 0; payload = 0; currentMessageIndex = -1; // Notify UI that we want to redraw/close this screen UIFrameEvent e; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; notifyObservers(&e); screen->forceDisplay(); return true; } // Touch input (virtual keyboard) handling // Only handle if touch coordinates present (CardKB won't set these) if (event->touchX != 0 || event->touchY != 0) { String keyTapped = keyForCoordinates(event->touchX, event->touchY); bool valid = false; if (keyTapped == "⇧") { highlight = -1; payload = 0x00; shift = !shift; valid = true; } else if (keyTapped == "⌫") { #ifndef RAK14014 highlight = keyTapped[0]; #endif payload = 0x08; shift = false; valid = true; } else if (keyTapped == "123" || keyTapped == "ABC") { highlight = -1; payload = 0x00; charSet = (charSet == 0 ? 1 : 0); valid = true; } else if (keyTapped == " ") { #ifndef RAK14014 highlight = keyTapped[0]; #endif payload = keyTapped[0]; shift = false; valid = true; } // Touch enter/submit else if (keyTapped == "↵") { updateState(CANNED_MESSAGE_RUN_STATE_ACTION_SELECT); // Send the message! payload = CANNED_MESSAGE_RUN_STATE_FREETEXT; currentMessageIndex = -1; shift = false; valid = true; } else if (!(keyTapped == "")) { #ifndef RAK14014 highlight = keyTapped[0]; #endif payload = shift ? keyTapped[0] : std::tolower(keyTapped[0]); shift = false; valid = true; } if (valid) { lastTouchMillis = millis(); runOnce(); payload = 0; return true; // STOP: We handled a VKB touch } } #endif // USE_VIRTUAL_KEYBOARD // All hardware keys fall through to here (CardKB, physical, etc.) if (event->kbchar == INPUT_BROKER_MSG_EMOTE_LIST) { updateState(CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER); screen->forceDisplay(); return true; } // Confirm select (Enter) bool isSelect = isSelectEvent(event); if (isSelect) { LOG_DEBUG("[SELECT] handleFreeTextInput: runState=%d, dest=%u, channel=%d, freetext='%s'", (int)runState, dest, channel, freetext.c_str()); if (dest == 0) dest = NODENUM_BROADCAST; // Defensive: If channel isn't valid, pick the first available channel if (channel >= channels.getNumChannels()) channel = 0; payload = CANNED_MESSAGE_RUN_STATE_FREETEXT; currentMessageIndex = -1; updateState(CANNED_MESSAGE_RUN_STATE_ACTION_SELECT); lastTouchMillis = millis(); runOnce(); return true; } // Backspace if (event->inputEvent == INPUT_BROKER_BACK && this->freetext.length() > 0) { payload = 0x08; lastTouchMillis = millis(); requestFocus(); runOnce(); return true; } // Move cursor left if (event->inputEvent == INPUT_BROKER_LEFT) { payload = INPUT_BROKER_LEFT; lastTouchMillis = millis(); requestFocus(); runOnce(); return true; } // Move cursor right if (event->inputEvent == INPUT_BROKER_RIGHT) { payload = INPUT_BROKER_RIGHT; lastTouchMillis = millis(); requestFocus(); runOnce(); return true; } // Cancel (dismiss freetext screen) if (event->inputEvent == INPUT_BROKER_CANCEL || event->inputEvent == INPUT_BROKER_ALT_LONG || (event->inputEvent == INPUT_BROKER_BACK && this->freetext.length() == 0)) { updateState(CANNED_MESSAGE_RUN_STATE_INACTIVE); freetext = ""; cursor = 0; payload = 0; currentMessageIndex = -1; // Notify UI that we want to redraw/close this screen UIFrameEvent e; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; notifyObservers(&e); screen->forceDisplay(); return true; } // Tab (switch destination) if (event->kbchar == INPUT_BROKER_MSG_TAB) { return handleTabSwitch(event); // Reuse tab logic } // Printable ASCII (add char to draft) if (event->kbchar >= 32 && event->kbchar <= 126) { payload = event->kbchar; lastTouchMillis = millis(); runOnce(); return true; } return false; } int CannedMessageModule::handleEmotePickerInput(const InputEvent *event) { int numEmotes = graphics::numEmotes; // Override isDown and isSelect ONLY for emote picker behavior bool isUp = isUpEvent(event); bool isDown = isDownEvent(event); bool isSelect = isSelectEvent(event); if (runState == CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER) { if (event->inputEvent == INPUT_BROKER_USER_PRESS) { isDown = true; } else if (event->inputEvent == INPUT_BROKER_SELECT) { isSelect = true; } } // Scroll emote list if (isUp && emotePickerIndex > 0) { emotePickerIndex--; screen->forceDisplay(); return 1; } if (isDown && emotePickerIndex < numEmotes - 1) { emotePickerIndex++; screen->forceDisplay(); return 1; } // Select emote: insert into freetext at cursor and return to freetext if (isSelect) { String label = graphics::emotes[emotePickerIndex].label; String emoteInsert = label; // Just the text label, e.g., ":thumbsup:" if (cursor == freetext.length()) { freetext += emoteInsert; } else { freetext = freetext.substring(0, cursor) + emoteInsert + freetext.substring(cursor); } cursor += emoteInsert.length(); updateState(CANNED_MESSAGE_RUN_STATE_FREETEXT, true); screen->forceDisplay(); return 1; } // Cancel returns to freetext if (event->inputEvent == INPUT_BROKER_CANCEL || event->inputEvent == INPUT_BROKER_ALT_LONG) { updateState(CANNED_MESSAGE_RUN_STATE_FREETEXT, true); screen->forceDisplay(); return 1; } return 0; } void CannedMessageModule::sendText(NodeNum dest, ChannelIndex channel, const char *message, bool wantReplies) { lastDest = dest; lastChannel = channel; lastDestSet = true; meshtastic_MeshPacket *p = allocDataPacket(); p->to = dest; p->channel = channel; p->want_ack = true; p->decoded.dest = dest; // Mirror picker: NODENUM_BROADCAST or node->num this->lastSentNode = dest; this->incoming = dest; // Manually find the node by number to check PKI capability meshtastic_NodeInfoLite *node = nullptr; size_t numMeshNodes = nodeDB->getNumMeshNodes(); for (size_t i = 0; i < numMeshNodes; ++i) { meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i); if (n && n->num == dest) { node = n; break; } } NodeNum myNodeNum = nodeDB->getNodeNum(); if (node && node->num != myNodeNum && node->has_user && node->user.public_key.size == 32) { p->pki_encrypted = true; p->channel = 0; // force PKI } // Track this packet’s request ID for matching ACKs this->lastRequestId = p->id; // Copy payload p->decoded.payload.size = strlen(message); memcpy(p->decoded.payload.bytes, message, p->decoded.payload.size); if (moduleConfig.canned_message.send_bell && p->decoded.payload.size < meshtastic_Constants_DATA_PAYLOAD_LEN) { p->decoded.payload.bytes[p->decoded.payload.size++] = 7; p->decoded.payload.bytes[p->decoded.payload.size] = '\0'; } this->waitingForAck = true; // Send to mesh (PKI-encrypted if conditions above matched) service->sendToMesh(p, RX_SRC_LOCAL, true); // Show banner immediately if (screen) { graphics::BannerOverlayOptions opts; opts.message = "Sending..."; opts.durationMs = 2000; screen->showOverlayBanner(opts); } // Save outgoing message StoredMessage sm; // Always use our local time, consistent with other paths uint32_t nowSecs = getValidTime(RTCQuality::RTCQualityDevice, true); sm.timestamp = (nowSecs > 0) ? nowSecs : millis() / 1000; sm.isBootRelative = (nowSecs == 0); sm.sender = nodeDB->getNodeNum(); // us sm.channelIndex = channel; size_t len = strnlen(message, MAX_MESSAGE_SIZE - 1); sm.textOffset = MessageStore::storeText(message, len); sm.textLength = len; // Classify broadcast vs DM if (dest == NODENUM_BROADCAST) { sm.dest = NODENUM_BROADCAST; sm.type = MessageType::BROADCAST; } else { sm.dest = dest; sm.type = MessageType::DM_TO_US; // Only add as favorite if our role is not router-like (ROUTER, ROUTER_LATE, CLIENT_BASE) if (config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER && config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER_LATE && config.device.role != meshtastic_Config_DeviceConfig_Role_CLIENT_BASE) { LOG_INFO("Proactively adding %x as favorite node", dest); nodeDB->set_favorite(true, dest); } else { LOG_DEBUG("Not favoriting node %x because role is router-like", dest); } } sm.ackStatus = AckStatus::NONE; messageStore.addLiveMessage(std::move(sm)); // Auto-switch thread view on outgoing message if (sm.type == MessageType::BROADCAST) { graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::CHANNEL, sm.channelIndex); } else { graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::DIRECT, -1, sm.dest); } playComboTune(); this->updateState(CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE); this->payload = wantReplies ? 1 : 0; // Tell Screen to switch to TextMessage frame via UIFrameEvent UIFrameEvent e; e.action = UIFrameEvent::Action::SWITCH_TO_TEXTMESSAGE; notifyObservers(&e); } int32_t CannedMessageModule::runOnce() { if (this->runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION && needsUpdate) { updateDestinationSelectionList(); needsUpdate = false; } // If we're in node selection, do nothing except keep alive if (this->runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) { return INACTIVATE_AFTER_MS; } // Normal module disable/idle handling if ((this->runState == CANNED_MESSAGE_RUN_STATE_DISABLED) || (this->runState == CANNED_MESSAGE_RUN_STATE_INACTIVE)) { // Clean up virtual keyboard if needed when going inactive if (graphics::NotificationRenderer::virtualKeyboard && graphics::NotificationRenderer::textInputCallback == nullptr) { LOG_INFO("Performing delayed virtual keyboard cleanup"); graphics::OnScreenKeyboardModule::instance().stop(false); } return INT32_MAX; } // Handle delayed virtual keyboard message sending if (this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE && this->payload == CANNED_MESSAGE_RUN_STATE_FREETEXT) { // Virtual keyboard message sending case - text was not empty if (this->freetext.length() > 0) { LOG_INFO("Processing delayed virtual keyboard send: '%s'", this->freetext.c_str()); sendText(this->dest, this->channel, this->freetext.c_str(), true); // Clean up virtual keyboard after sending if (graphics::NotificationRenderer::virtualKeyboard) { LOG_INFO("Cleaning up virtual keyboard after message send"); graphics::OnScreenKeyboardModule::instance().stop(false); graphics::NotificationRenderer::resetBanner(); } // Clear payload to indicate virtual keyboard processing is complete // But keep SENDING_ACTIVE state to show "Sending..." screen for 2 seconds this->payload = 0; } else { // Empty message, just go inactive LOG_INFO("Empty freetext detected in delayed processing, returning to inactive state"); this->updateState(CANNED_MESSAGE_RUN_STATE_INACTIVE); } UIFrameEvent e; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; this->currentMessageIndex = -1; this->freetext = ""; this->cursor = 0; this->notifyObservers(&e); return 2000; } UIFrameEvent e; if ((this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE && this->payload != 0 && this->payload != CANNED_MESSAGE_RUN_STATE_FREETEXT) || (this->runState == CANNED_MESSAGE_RUN_STATE_ACK_NACK_RECEIVED) || (this->runState == CANNED_MESSAGE_RUN_STATE_MESSAGE_SELECTION)) { this->updateState(CANNED_MESSAGE_RUN_STATE_INACTIVE); e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; this->currentMessageIndex = -1; this->freetext = ""; this->cursor = 0; this->notifyObservers(&e); } // Handle SENDING_ACTIVE state transition after virtual keyboard message else if (this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE && this->payload == 0) { this->updateState(CANNED_MESSAGE_RUN_STATE_INACTIVE); this->currentMessageIndex = -1; this->freetext = ""; this->cursor = 0; return INT32_MAX; } else if (((this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) || (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT)) && !Throttle::isWithinTimespanMs(this->lastTouchMillis, INACTIVATE_AFTER_MS)) { // Reset module on inactivity e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; this->currentMessageIndex = -1; this->freetext = ""; this->cursor = 0; this->updateState(CANNED_MESSAGE_RUN_STATE_INACTIVE); // Clean up virtual keyboard if it exists during timeout if (graphics::NotificationRenderer::virtualKeyboard) { LOG_INFO("Cleaning up virtual keyboard due to module timeout"); graphics::OnScreenKeyboardModule::instance().stop(false); graphics::NotificationRenderer::resetBanner(); } this->notifyObservers(&e); } else if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTION_SELECT) { if (this->payload == 0) { // [Exit] button pressed - return to inactive state LOG_INFO("Processing [Exit] action - returning to inactive state"); this->updateState(CANNED_MESSAGE_RUN_STATE_INACTIVE); } else if (this->payload == CANNED_MESSAGE_RUN_STATE_FREETEXT) { if (this->freetext.length() > 0) { sendText(this->dest, this->channel, this->freetext.c_str(), true); // Clean up state but *don’t* deactivate yet this->currentMessageIndex = -1; this->freetext = ""; this->cursor = 0; // Tell Screen to jump straight to the TextMessage frame e.action = UIFrameEvent::Action::SWITCH_TO_TEXTMESSAGE; this->notifyObservers(&e); // Now deactivate this module this->updateState(CANNED_MESSAGE_RUN_STATE_INACTIVE); return INT32_MAX; // don't fall back into canned list } else { this->updateState(CANNED_MESSAGE_RUN_STATE_INACTIVE); } } else { if (strcmp(this->messages[this->currentMessageIndex], "[Select Destination]") == 0) { this->updateState(CANNED_MESSAGE_RUN_STATE_ACTIVE); return INT32_MAX; } if ((this->messagesCount > this->currentMessageIndex) && (strlen(this->messages[this->currentMessageIndex]) > 0)) { if (strcmp(this->messages[this->currentMessageIndex], "~") == 0) { return INT32_MAX; } else { sendText(this->dest, this->channel, this->messages[this->currentMessageIndex], true); // Clean up state this->currentMessageIndex = -1; this->freetext = ""; this->cursor = 0; // Tell Screen to jump straight to the TextMessage frame e.action = UIFrameEvent::Action::SWITCH_TO_TEXTMESSAGE; this->notifyObservers(&e); // Now deactivate this module this->updateState(CANNED_MESSAGE_RUN_STATE_INACTIVE); return INT32_MAX; // don't fall back into canned list } } else { this->updateState(CANNED_MESSAGE_RUN_STATE_INACTIVE); } } // fallback clean-up if nothing above returned this->currentMessageIndex = -1; this->freetext = ""; this->cursor = 0; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; this->notifyObservers(&e); // Immediately stop, don't linger on canned screen return INT32_MAX; } // Highlight [Select Destination] initially when entering the message list else if ((this->runState != CANNED_MESSAGE_RUN_STATE_FREETEXT) && (this->currentMessageIndex == -1)) { // Only auto-highlight [Select Destination] if we’re ACTIVELY browsing, // not when coming back from a sent message. if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) { int selectDestination = 0; for (int i = 0; i < this->messagesCount; ++i) { if (strcmp(this->messages[i], "[Select Destination]") == 0) { selectDestination = i; break; } } this->currentMessageIndex = selectDestination; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; } } else if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTION_UP) { if (this->messagesCount > 0) { this->currentMessageIndex = getPrevIndex(); this->freetext = ""; this->cursor = 0; this->updateState(CANNED_MESSAGE_RUN_STATE_ACTIVE); } } else if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTION_DOWN) { if (this->messagesCount > 0) { this->currentMessageIndex = this->getNextIndex(); this->freetext = ""; this->cursor = 0; this->updateState(CANNED_MESSAGE_RUN_STATE_ACTIVE); } } else if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT || this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) { switch (this->payload) { case INPUT_BROKER_LEFT: if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT && this->cursor > 0) { this->cursor--; } break; case INPUT_BROKER_RIGHT: if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT && this->cursor < this->freetext.length()) { this->cursor++; } break; default: break; } if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT) { e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; switch (this->payload) { case 0x08: // backspace if (this->freetext.length() > 0) { if (this->cursor > 0) { if (this->cursor == this->freetext.length()) { this->freetext = this->freetext.substring(0, this->freetext.length() - 1); } else { this->freetext = this->freetext.substring(0, this->cursor - 1) + this->freetext.substring(this->cursor, this->freetext.length()); } this->cursor--; } } else { } break; case INPUT_BROKER_MSG_TAB: // Tab key: handled by input handler return 0; case INPUT_BROKER_LEFT: case INPUT_BROKER_RIGHT: break; default: // Only insert ASCII printable characters (32–126) if (this->payload >= 32 && this->payload <= 126) { requestFocus(); if (this->cursor == this->freetext.length()) { this->freetext += (char)this->payload; } else { this->freetext = this->freetext.substring(0, this->cursor) + (char)this->payload + this->freetext.substring(this->cursor); } this->cursor++; const uint16_t maxChars = 200 - (moduleConfig.canned_message.send_bell ? 1 : 0); if (this->freetext.length() > maxChars) { this->cursor = maxChars; this->freetext = this->freetext.substring(0, maxChars); } } break; } } this->lastTouchMillis = millis(); this->notifyObservers(&e); return INACTIVATE_AFTER_MS; } if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) { this->lastTouchMillis = millis(); this->notifyObservers(&e); return INACTIVATE_AFTER_MS; } return INT32_MAX; } const char *CannedMessageModule::getCurrentMessage() { return this->messages[this->currentMessageIndex]; } const char *CannedMessageModule::getPrevMessage() { return this->messages[this->getPrevIndex()]; } const char *CannedMessageModule::getNextMessage() { return this->messages[this->getNextIndex()]; } const char *CannedMessageModule::getMessageByIndex(int index) { return (index >= 0 && index < this->messagesCount) ? this->messages[index] : ""; } const char *CannedMessageModule::getNodeName(NodeNum node) { if (node == NODENUM_BROADCAST) return "Broadcast"; meshtastic_NodeInfoLite *info = nodeDB->getMeshNode(node); if (info && info->has_user && strlen(info->user.long_name) > 0) { return info->user.long_name; } static char fallback[12]; snprintf(fallback, sizeof(fallback), "0x%08x", node); return fallback; } bool CannedMessageModule::shouldDraw() { // Only allow drawing when we're in an interactive UI state. return (this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE || this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT || this->runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION || this->runState == CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER); } // Has the user defined any canned messages? // Expose publicly whether canned message module is ready for use bool CannedMessageModule::hasMessages() { return (this->messagesCount > 0); } int CannedMessageModule::getNextIndex() { if (this->currentMessageIndex >= (this->messagesCount - 1)) { return 0; } else { return this->currentMessageIndex + 1; } } int CannedMessageModule::getPrevIndex() { if (this->currentMessageIndex <= 0) { return this->messagesCount - 1; } else { return this->currentMessageIndex - 1; } } #if defined(USE_VIRTUAL_KEYBOARD) String CannedMessageModule::keyForCoordinates(uint x, uint y) { int outerSize = *(&this->keyboard[this->charSet] + 1) - this->keyboard[this->charSet]; 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}; #ifdef RAK14014 // Optimize the touch range of the virtual keyboard in the bottom row if (outerIndex == outerSize - 1) { updatedLetter.rectHeight = 240 - yOffset; } #endif 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; } } // Draw the node/channel selection screen void CannedMessageModule::drawDestinationSelectionScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { requestFocus(); display->setColor(WHITE); // Always draw cleanly display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); // Header int titleY = 2; String titleText = "Select Destination"; titleText += searchQuery.length() > 0 ? " [" + searchQuery + "]" : " [ ]"; display->setTextAlignment(TEXT_ALIGN_CENTER); display->drawString(display->getWidth() / 2, titleY, titleText); display->setTextAlignment(TEXT_ALIGN_LEFT); // List Items int rowYOffset = titleY + (FONT_HEIGHT_SMALL - 4); int numActiveChannels = this->activeChannelIndices.size(); int totalEntries = numActiveChannels + this->filteredNodes.size(); int columns = 1; this->visibleRows = (display->getHeight() - (titleY + FONT_HEIGHT_SMALL)) / (FONT_HEIGHT_SMALL - 4); if (this->visibleRows < 1) this->visibleRows = 1; // Clamp scrolling if (scrollIndex > totalEntries / columns) scrollIndex = totalEntries / columns; if (scrollIndex < 0) scrollIndex = 0; for (int row = 0; row < visibleRows; row++) { int itemIndex = scrollIndex + row; if (itemIndex >= totalEntries) break; int xOffset = 0; int yOffset = row * (FONT_HEIGHT_SMALL - 4) + rowYOffset; std::string entryText; // Draw Channels First if (itemIndex < numActiveChannels) { uint8_t channelIndex = this->activeChannelIndices[itemIndex]; const char *channelName = channels.getName(channelIndex); entryText = std::string("#") + (channelName ? channelName : "?"); } // Then Draw Nodes else { int nodeIndex = itemIndex - numActiveChannels; if (nodeIndex >= 0 && nodeIndex < static_cast(this->filteredNodes.size())) { meshtastic_NodeInfoLite *node = this->filteredNodes[nodeIndex].node; if (node) { if (display->getWidth() <= 64) { entryText = node->user.short_name; } else if (node->user.long_name[0]) { entryText = node->user.long_name; } else { entryText = node->user.short_name; } } int availWidth = display->getWidth() - ((graphics::currentResolution == graphics::ScreenResolution::High) ? 40 : 20) - ((node && node->is_favorite) ? 10 : 0); if (availWidth < 0) availWidth = 0; char truncatedEntry[96]; graphics::UIRenderer::truncateStringWithEmotes(display, entryText.c_str(), truncatedEntry, sizeof(truncatedEntry), availWidth); entryText = truncatedEntry; // Prepend "* " if this is a favorite if (node && node->is_favorite) { entryText = "* " + entryText; } graphics::UIRenderer::truncateStringWithEmotes(display, entryText.c_str(), truncatedEntry, sizeof(truncatedEntry), availWidth); entryText = truncatedEntry; } } if (entryText.empty() || entryText == "Unknown") entryText = "?"; // Highlight background (if selected) if (itemIndex == destIndex) { int scrollPadding = 8; // Reserve space for scrollbar display->fillRect(0, yOffset + 2, display->getWidth() - scrollPadding, FONT_HEIGHT_SMALL - 5); display->setColor(BLACK); } // Draw entry text graphics::UIRenderer::drawStringWithEmotes(display, xOffset + 2, yOffset, entryText.c_str(), FONT_HEIGHT_SMALL, 1, false); display->setColor(WHITE); // Draw key icon (after highlight) /* if (itemIndex >= numActiveChannels) { int nodeIndex = itemIndex - numActiveChannels; if (nodeIndex >= 0 && nodeIndex < static_cast(this->filteredNodes.size())) { const meshtastic_NodeInfoLite *node = this->filteredNodes[nodeIndex].node; if (node && hasKeyForNode(node)) { int iconX = display->getWidth() - key_symbol_width - 15; int iconY = yOffset + (FONT_HEIGHT_SMALL - key_symbol_height) / 2; if (itemIndex == destIndex) { display->setColor(INVERSE); } else { display->setColor(WHITE); } display->drawXbm(iconX, iconY, key_symbol_width, key_symbol_height, key_symbol); } } } */ } // Scrollbar if (totalEntries > visibleRows) { int scrollbarHeight = visibleRows * (FONT_HEIGHT_SMALL - 4); int totalScrollable = totalEntries; int scrollTrackX = display->getWidth() - 6; display->drawRect(scrollTrackX, rowYOffset, 4, scrollbarHeight); int scrollHeight = (scrollbarHeight * visibleRows) / totalScrollable; int scrollPos = rowYOffset + (scrollbarHeight * scrollIndex) / totalScrollable; display->fillRect(scrollTrackX, scrollPos, 4, scrollHeight); } } void CannedMessageModule::drawEmotePickerScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { const int headerFontHeight = FONT_HEIGHT_SMALL; // Make sure this matches your actual small font height const int headerMargin = 2; // Extra pixels below header const int labelGap = 4; const int bitmapGapX = 4; const int maxEmoteHeight = graphics::EmoteRenderer::maxEmoteHeight(); const int rowHeight = maxEmoteHeight + 2; // Place header at top, then compute start of emote list int headerY = y; int listTop = headerY + headerFontHeight + headerMargin; int _visibleRows = (display->getHeight() - listTop - 2) / rowHeight; int numEmotes = graphics::numEmotes; // keep member variable in sync this->visibleRows = _visibleRows; // Clamp highlight index if (emotePickerIndex < 0) emotePickerIndex = 0; if (emotePickerIndex >= numEmotes) emotePickerIndex = numEmotes - 1; // Determine which emote is at the top int topIndex = emotePickerIndex - _visibleRows / 2; if (topIndex < 0) topIndex = 0; if (topIndex > numEmotes - _visibleRows) topIndex = std::max(0, numEmotes - _visibleRows); // Draw header/title display->setFont(FONT_SMALL); display->setTextAlignment(TEXT_ALIGN_CENTER); display->drawString(display->getWidth() / 2, headerY, "Select Emote"); // Draw emote rows display->setTextAlignment(TEXT_ALIGN_LEFT); for (int vis = 0; vis < _visibleRows; ++vis) { int emoteIdx = topIndex + vis; if (emoteIdx >= numEmotes) break; const graphics::Emote &emote = graphics::emotes[emoteIdx]; int rowY = listTop + vis * rowHeight; // Draw highlight box 2px taller than emote (1px margin above and below) if (emoteIdx == emotePickerIndex) { display->fillRect(x, rowY, display->getWidth() - 8, emote.height + 2); display->setColor(BLACK); } // Emote bitmap (left), centered inside the row int labelStartX = x + bitmapGapX; const int emoteY = rowY + ((rowHeight - emote.height) / 2); display->drawXbm(labelStartX, emoteY, emote.width, emote.height, emote.bitmap); labelStartX += emote.width; // Emote label (right of bitmap) display->setFont(FONT_MEDIUM); int labelY = rowY + ((rowHeight - FONT_HEIGHT_MEDIUM) / 2); display->drawString(labelStartX + labelGap, labelY, emote.label); if (emoteIdx == emotePickerIndex) display->setColor(WHITE); } // Draw scrollbar if needed if (numEmotes > _visibleRows) { int scrollbarHeight = _visibleRows * rowHeight; int scrollTrackX = display->getWidth() - 6; display->drawRect(scrollTrackX, listTop, 4, scrollbarHeight); int scrollBarLen = std::max(6, (scrollbarHeight * _visibleRows) / numEmotes); int scrollBarPos = listTop + (scrollbarHeight * topIndex) / numEmotes; display->fillRect(scrollTrackX, scrollBarPos, 4, scrollBarLen); } } void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { this->displayHeight = display->getHeight(); // Store display height for later use char buffer[50]; display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); // Never draw if state is outside our UI modes if (!(runState == CANNED_MESSAGE_RUN_STATE_ACTIVE || runState == CANNED_MESSAGE_RUN_STATE_FREETEXT || runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION || runState == CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER)) { return; // bail if not in a UI state that should render } // Emote Picker Screen if (this->runState == CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER) { drawEmotePickerScreen(display, state, x, y); // <-- Call your emote picker drawer here return; } // Destination Selection if (this->runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) { drawDestinationSelectionScreen(display, state, x, y); return; } // Disabled Screen if (this->runState == CANNED_MESSAGE_RUN_STATE_DISABLED) { display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); display->drawString(10 + x, 0 + y + FONT_HEIGHT_SMALL, "Canned Message\nModule disabled."); return; } // Free Text Input Screen if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT) { requestFocus(); #if defined(USE_EINK) && defined(USE_EINK_DYNAMICDISPLAY) EInkDynamicDisplay *einkDisplay = static_cast(display); einkDisplay->enableUnlimitedFastMode(); #endif #if defined(USE_VIRTUAL_KEYBOARD) drawKeyboard(display, state, 0, 0); #else display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); // Draw node/channel header at the top drawHeader(display, x, y, buffer); // Char count right-aligned if (runState != CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) { uint16_t charsLeft = meshtastic_Constants_DATA_PAYLOAD_LEN - this->freetext.length() - (moduleConfig.canned_message.send_bell ? 1 : 0); snprintf(buffer, sizeof(buffer), "%d left", charsLeft); display->drawString(x + display->getWidth() - display->getStringWidth(buffer), y + 0, buffer); } #if INPUTBROKER_SERIAL_TYPE == 1 // Chatter Modifier key mode label (right side) { uint8_t mode = globalSerialKeyboard ? globalSerialKeyboard->getShift() : 0; const char *label = (mode == 0) ? "a" : (mode == 1) ? "A" : "#"; display->setFont(FONT_SMALL); display->setTextAlignment(TEXT_ALIGN_LEFT); const int16_t th = FONT_HEIGHT_SMALL; const int16_t tw = display->getStringWidth(label); const int16_t padX = 3; const int16_t padY = 2; const int16_t r = 3; const int16_t bw = tw + padX * 2; const int16_t bh = th + padY * 2; const int16_t bx = x + display->getWidth() - bw - 2; const int16_t by = y + display->getHeight() - bh - 2; display->setColor(WHITE); display->fillRect(bx + r, by, bw - r * 2, bh); display->fillRect(bx, by + r, r, bh - r * 2); display->fillRect(bx + bw - r, by + r, r, bh - r * 2); display->fillCircle(bx + r, by + r, r); display->fillCircle(bx + bw - r - 1, by + r, r); display->fillCircle(bx + r, by + bh - r - 1, r); display->fillCircle(bx + bw - r - 1, by + bh - r - 1, r); display->setColor(BLACK); display->drawString(bx + padX, by + padY, label); } // LEFT-SIDE DESTINATION-HINT BOX (“Dest: Shift + ◄”) { display->setFont(FONT_SMALL); display->setTextAlignment(TEXT_ALIGN_LEFT); const char *label = "Dest: Shift + "; int16_t labelW = display->getStringWidth(label); // triangle size visually matches glyph height, not full line height const int triH = FONT_HEIGHT_SMALL - 3; const int triW = triH * 0.7; const int16_t padX = 3; const int16_t padY = 2; const int16_t r = 3; const int16_t bw = labelW + triW + padX * 2 + 2; const int16_t bh = FONT_HEIGHT_SMALL + padY * 2; const int16_t bx = x + 2; const int16_t by = y + display->getHeight() - bh - 2; // Rounded white box display->setColor(WHITE); display->fillRect(bx + r, by, bw - (r * 2), bh); display->fillRect(bx, by + r, r, bh - (r * 2)); display->fillRect(bx + bw - r, by + r, r, bh - (r * 2)); display->fillCircle(bx + r, by + r, r); display->fillCircle(bx + bw - r - 1, by + r, r); display->fillCircle(bx + r, by + bh - r - 1, r); display->fillCircle(bx + bw - r - 1, by + bh - r - 1, r); // Draw text display->setColor(BLACK); display->drawString(bx + padX, by + padY, label); // Perfectly center triangle on text baseline int16_t tx = bx + padX + labelW; int16_t ty = by + padY + (FONT_HEIGHT_SMALL / 2) - (triH / 2) - 1; // -1 for optical centering // ◄ Left-pointing triangle display->fillTriangle(tx + triW, ty, // top-right tx, ty + triH / 2, // left center tx + triW, ty + triH // bottom-right ); } #endif // Draw Free Text input with multi-emote support and proper line wrapping display->setColor(WHITE); { int inputY = 0 + y + FONT_HEIGHT_SMALL; String msgWithCursor = this->drawWithCursor(this->freetext, this->cursor); drawWrappedEmoteText(display, x, inputY, msgWithCursor.c_str(), display->getWidth() - x, FONT_HEIGHT_SMALL); } #endif return; } // Canned Messages List if (this->messagesCount > 0) { display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); // Precompute per-row heights based on emotes (centered if present) const int baseRowSpacing = FONT_HEIGHT_SMALL - 4; int topMsg; int _visibleRows; // Draw header (To: ...) drawHeader(display, x, y, buffer); // Shift message list upward by 3 pixels to reduce spacing between header and first message const int listYOffset = y + FONT_HEIGHT_SMALL - 3; _visibleRows = (display->getHeight() - listYOffset) / baseRowSpacing; // Figure out which messages are visible and their needed heights topMsg = (messagesCount > _visibleRows && currentMessageIndex >= _visibleRows - 1) ? currentMessageIndex - _visibleRows + 2 : 0; int countRows = std::min(messagesCount, _visibleRows); // Draw all message rows with multi-emote support int yCursor = listYOffset; for (int vis = 0; vis < countRows; vis++) { int msgIdx = topMsg + vis; int lineY = yCursor; const char *msg = getMessageByIndex(msgIdx); int rowHeight = getRowHeightForEmoteText(msg, baseRowSpacing); bool _highlight = (msgIdx == currentMessageIndex); // Vertically center based on rowHeight int textYOffset = (rowHeight - FONT_HEIGHT_SMALL) / 2; #ifdef USE_EINK int nextX = x + (_highlight ? 12 : 0); if (_highlight) display->drawString(x + 0, lineY + textYOffset, ">"); #else int scrollPadding = 8; if (_highlight) { display->fillRect(x + 0, lineY, display->getWidth() - scrollPadding, rowHeight); display->setColor(BLACK); } int nextX = x + (_highlight ? 2 : 0); #endif if (msg && *msg) drawCenteredEmoteText(display, nextX, lineY, rowHeight, msg); #ifndef USE_EINK if (_highlight) display->setColor(WHITE); #endif yCursor += rowHeight; } // Scrollbar if (messagesCount > _visibleRows) { int scrollHeight = display->getHeight() - listYOffset; int scrollTrackX = display->getWidth() - 6; display->drawRect(scrollTrackX, listYOffset, 4, scrollHeight); int barHeight = (scrollHeight * _visibleRows) / messagesCount; int scrollPos = listYOffset + (scrollHeight * topMsg) / messagesCount; display->fillRect(scrollTrackX, scrollPos, 4, barHeight); } } } // Return SNR limit based on modem preset static float getSnrLimit(meshtastic_Config_LoRaConfig_ModemPreset preset) { switch (preset) { case meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW: case meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE: case meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST: return -6.0f; case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW: case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST: return -5.5f; case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW: case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST: case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO: return -4.5f; default: return -6.0f; } } // Return Good/Fair/Bad label and set 1–5 bars based on SNR and RSSI static const char *getSignalGrade(float snr, int32_t rssi, float snrLimit, int &bars) { // 5-bar logic: strength inside Good/Fair/Bad category if (snr > snrLimit && rssi > -10) { bars = 5; // very strong good return "Good"; } else if (snr > snrLimit && rssi > -20) { bars = 4; // normal good return "Good"; } else if (snr > 0 && rssi > -50) { bars = 3; // weaker good (on edge of fair) return "Good"; } else if (snr > -10 && rssi > -100) { bars = 2; // fair return "Fair"; } else { bars = 1; // bad return "Bad"; } } ProcessMessage CannedMessageModule::handleReceived(const meshtastic_MeshPacket &mp) { // Only process routing ACK/NACK packets that are responses to our own outbound if (mp.decoded.portnum == meshtastic_PortNum_ROUTING_APP && waitingForAck && mp.to == nodeDB->getNodeNum() && mp.decoded.request_id == this->lastRequestId) // only ACKs for our last sent packet { if (mp.decoded.request_id != 0) { // Decode the routing response meshtastic_Routing decoded = meshtastic_Routing_init_default; pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, meshtastic_Routing_fields, &decoded); // Determine ACK/NACK status bool isAck = (decoded.error_reason == meshtastic_Routing_Error_NONE); bool isFromDest = (mp.from == this->lastSentNode); bool wasBroadcast = (this->lastSentNode == NODENUM_BROADCAST); // Identify the responding node if (wasBroadcast && mp.from != nodeDB->getNodeNum()) { this->incoming = mp.from; // relayed by another node } else { this->incoming = this->lastSentNode; // direct reply } // Final ACK/NACK logic if (wasBroadcast) { // Any ACK counts for broadcast this->ack = isAck; waitingForAck = false; } else if (isFromDest) { // Only ACK from destination counts as final this->ack = isAck; waitingForAck = false; } else if (isAck) { // Relay ACK → mark as RELAYED, still no final ACK this->ack = false; waitingForAck = false; } else { // Explicit failure this->ack = false; waitingForAck = false; } // Update last sent StoredMessage with ACK/NACK/RELAYED result if (!messageStore.getMessages().empty()) { StoredMessage &last = const_cast(messageStore.getMessages().back()); if (last.sender == nodeDB->getNodeNum()) { // only update our own messages if (wasBroadcast && isAck) { last.ackStatus = AckStatus::ACKED; } else if (isFromDest && isAck) { last.ackStatus = AckStatus::ACKED; } else if (!isFromDest && isAck) { last.ackStatus = AckStatus::RELAYED; } else { last.ackStatus = AckStatus::NACKED; } } } // Capture radio metrics this->lastRxRssi = mp.rx_rssi; this->lastRxSnr = mp.rx_snr; // Show overlay banner if (screen) { auto *display = screen->getDisplayDevice(); graphics::BannerOverlayOptions opts; static char buf[128]; const char *channelName = channels.getName(this->channel); const char *src = getNodeName(this->incoming); char nodeName[48]; strncpy(nodeName, src, sizeof(nodeName) - 1); nodeName[sizeof(nodeName) - 1] = '\0'; int availWidth = display->getWidth() - ((graphics::currentResolution == graphics::ScreenResolution::High) ? 60 : 30); if (availWidth < 0) availWidth = 0; size_t origLen = strlen(nodeName); while (nodeName[0] && display->getStringWidth(nodeName) > availWidth) { nodeName[strlen(nodeName) - 1] = '\0'; } if (strlen(nodeName) < origLen) { strcat(nodeName, "..."); } // Calculate signal quality and bars based on preset, SNR, and RSSI float snrLimit = getSnrLimit(config.lora.modem_preset); int bars = 0; const char *qualityLabel = getSignalGrade(this->lastRxSnr, this->lastRxRssi, snrLimit, bars); if (this->ack) { if (this->lastSentNode == NODENUM_BROADCAST) { snprintf(buf, sizeof(buf), "Message sent to\n#%s\n\nSignal: %s", (channelName && channelName[0]) ? channelName : "unknown", qualityLabel); } else { snprintf(buf, sizeof(buf), "DM sent to\n@%s\n\nSignal: %s", (nodeName && nodeName[0]) ? nodeName : "unknown", qualityLabel); } } else if (isAck && !isFromDest) { // Relay ACK banner snprintf(buf, sizeof(buf), "DM Relayed\n(Status Unknown)\n%s\n\nSignal: %s", (nodeName && nodeName[0]) ? nodeName : "unknown", qualityLabel); } else { if (this->lastSentNode == NODENUM_BROADCAST) { snprintf(buf, sizeof(buf), "Message failed to\n#%s", (channelName && channelName[0]) ? channelName : "unknown"); } else { snprintf(buf, sizeof(buf), "DM failed to\n@%s", (nodeName && nodeName[0]) ? nodeName : "unknown"); } } opts.message = buf; opts.durationMs = 3000; graphics::bannerSignalBars = bars; // tell banner renderer how many bars to draw screen->showOverlayBanner(opts); // this triggers drawNotificationBox() } } } 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 FSCom spiLock->lock(); FSCom.mkdir("/prefs"); spiLock->unlock(); #endif okay &= nodeDB->saveProto(cannedMessagesConfigFile, meshtastic_CannedMessageModuleConfig_size, &meshtastic_CannedMessageModuleConfig_msg, &cannedMessageModuleConfig); return okay; } /** * @brief Fill configuration with default values. */ void CannedMessageModule::installDefaultCannedMessageModuleConfig() { strncpy(cannedMessageModuleConfig.messages, "Hi|Bye|Yes|No|Ok", 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 getting radio canned messages"); this->handleGetCannedMessageModuleMessages(mp, response); result = AdminMessageHandleResult::HANDLED_WITH_RESPONSE; break; case meshtastic_AdminMessage_set_canned_message_module_messages_tag: LOG_DEBUG("Client getting radio canned messages"); this->handleSetCannedMessageModuleMessages(request->set_canned_message_module_messages); result = AdminMessageHandleResult::HANDLED; break; default: result = AdminMessageHandleResult::NOT_HANDLED; } return result; } void CannedMessageModule::handleGetCannedMessageModuleMessages(const meshtastic_MeshPacket &req, meshtastic_AdminMessage *response) { LOG_DEBUG("*** handleGetCannedMessageModuleMessages"); if (req.decoded.want_response) { response->which_payload_variant = meshtastic_AdminMessage_get_canned_message_module_messages_response_tag; strncpy(response->get_canned_message_module_messages_response, cannedMessageModuleConfig.messages, sizeof(response->get_canned_message_module_messages_response)); } // Don't send anything if not instructed to. Better than asserting. } void CannedMessageModule::handleSetCannedMessageModuleMessages(const char *from_msg) { int changed = 0; if (*from_msg) { changed |= strcmp(cannedMessageModuleConfig.messages, from_msg); strncpy(cannedMessageModuleConfig.messages, from_msg, sizeof(cannedMessageModuleConfig.messages)); LOG_DEBUG("*** from_msg.text:%s", from_msg); } if (changed) { this->saveProtoForModule(); if (splitConfiguredMessages()) { moduleConfig.canned_message.enabled = true; } } } String CannedMessageModule::drawWithCursor(String text, int cursor) { String result = text.substring(0, cursor) + "_" + text.substring(cursor); return result; } #endif ================================================ FILE: src/modules/CannedMessageModule.h ================================================ #pragma once #if HAS_SCREEN #include "ProtobufModule.h" #include "input/InputBroker.h" // ============================ // Enums & Defines // ============================ enum cannedMessageModuleRunState { CANNED_MESSAGE_RUN_STATE_DISABLED, CANNED_MESSAGE_RUN_STATE_INACTIVE, CANNED_MESSAGE_RUN_STATE_ACTIVE, CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE, CANNED_MESSAGE_RUN_STATE_ACK_NACK_RECEIVED, CANNED_MESSAGE_RUN_STATE_ACTION_SELECT, CANNED_MESSAGE_RUN_STATE_ACTION_UP, CANNED_MESSAGE_RUN_STATE_ACTION_DOWN, CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION, CANNED_MESSAGE_RUN_STATE_FREETEXT, CANNED_MESSAGE_RUN_STATE_MESSAGE_SELECTION, CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER }; enum CannedMessageModuleIconType { shift, backspace, space, enter }; #define CANNED_MESSAGE_MODULE_MESSAGE_MAX_COUNT 50 #define CANNED_MESSAGE_MODULE_MESSAGES_SIZE 800 // ============================ // Data Structures // ============================ struct Letter { String character; float width; int rectX; int rectY; int rectWidth; int rectHeight; }; struct NodeEntry { meshtastic_NodeInfoLite *node; uint32_t lastHeard; }; // ============================ // Main Class // ============================ class CannedMessageModule : public SinglePortModule, public Observable, private concurrency::OSThread { public: CannedMessageModule(); void LaunchWithDestination(NodeNum, uint8_t newChannel = 0); void LaunchRepeatDestination(); void LaunchFreetextWithDestination(NodeNum, uint8_t newChannel = 0); // === Emote Picker navigation === int emotePickerIndex = 0; // Tracks currently selected emote in the picker // === Message navigation === const char *getCurrentMessage(); const char *getPrevMessage(); const char *getNextMessage(); const char *getMessageByIndex(int index); const char *getNodeName(NodeNum node); // === State/UI === bool shouldDraw(); bool hasMessages(); void resetSearch(); void updateDestinationSelectionList(); void drawDestinationSelectionScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); bool isCharInputAllowed() const; String drawWithCursor(String text, int cursor); // === Emote Picker === int handleEmotePickerInput(const InputEvent *event); void drawEmotePickerScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); // === Admin Handlers === void handleGetCannedMessageModuleMessages(const meshtastic_MeshPacket &req, meshtastic_AdminMessage *response); void handleSetCannedMessageModuleMessages(const char *from_msg); #ifdef RAK14014 cannedMessageModuleRunState getRunState() const { return runState; } #endif // === Packet Interest Filter === virtual bool wantPacket(const meshtastic_MeshPacket *p) override { if (p->rx_rssi != 0) lastRxRssi = p->rx_rssi; if (p->rx_snr > 0) lastRxSnr = p->rx_snr; return (p->decoded.portnum == meshtastic_PortNum_ROUTING_APP) ? waitingForAck : false; } protected: // === Thread Entry Point === virtual int32_t runOnce() override; // === Transmission === void sendText(NodeNum dest, ChannelIndex channel, const char *message, bool wantReplies); void drawHeader(OLEDDisplay *display, int16_t x, int16_t y, char *buffer); int splitConfiguredMessages(); int getNextIndex(); int getPrevIndex(); #if defined(USE_VIRTUAL_KEYBOARD) void drawKeyboard(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); String keyForCoordinates(uint x, uint y); void drawShiftIcon(OLEDDisplay *display, int x, int y, float scale = 1); void drawBackspaceIcon(OLEDDisplay *display, int x, int y, float scale = 1); void drawEnterIcon(OLEDDisplay *display, int x, int y, float scale = 1); #endif // === Input Handling === int handleInputEvent(const InputEvent *event); virtual bool wantUIFrame() override { return shouldDraw(); } virtual Observable *getUIFrameObservable() override { return this; } virtual bool interceptingKeyboardInput() override; virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) override; virtual AdminMessageHandleResult handleAdminMessageForModule(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, meshtastic_AdminMessage *response) override; virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; void loadProtoForModule(); bool saveProtoForModule(); void installDefaultCannedMessageModuleConfig(); private: // === Input Observers === CallbackObserver inputObserver = CallbackObserver(this, &CannedMessageModule::handleInputEvent); // === Display and UI === int displayHeight = 64; int destIndex = 0; int scrollIndex = 0; int visibleRows = 0; bool needsUpdate = true; unsigned long lastUpdateMillis = 0; String searchQuery; String freetext; // === Message Storage === char messageBuffer[CANNED_MESSAGE_MODULE_MESSAGES_SIZE + 1]; char *messages[CANNED_MESSAGE_MODULE_MESSAGE_MAX_COUNT]; int messagesCount = 0; int currentMessageIndex = -1; // === Routing & Acknowledgment === NodeNum dest = NODENUM_BROADCAST; // Destination node for outgoing messages (default: broadcast) NodeNum incoming = NODENUM_BROADCAST; // Source node from which last ACK/NACK was received NodeNum lastSentNode = 0; // Tracks the most recent node we sent a message to (for UI display) ChannelIndex channel = 0; // Channel index used when sending a message bool ack = false; // True = ACK received, False = NACK or failed bool waitingForAck = false; // True if we're expecting an ACK and should monitor routing packets float lastRxSnr = 0; // SNR from last received ACK (used for diagnostics/UI) int32_t lastRxRssi = 0; // RSSI from last received ACK (used for diagnostics/UI) uint32_t lastRequestId = 0; // tracks the request_id of our last sent packet // === State Tracking === cannedMessageModuleRunState runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; char highlight = 0x00; char payload = 0x00; unsigned int cursor = 0; unsigned long lastTouchMillis = 0; uint32_t lastFilterUpdate = 0; static constexpr uint32_t filterDebounceMs = 30; std::vector activeChannelIndices; std::vector filteredNodes; #if defined(USE_VIRTUAL_KEYBOARD) bool shift = false; int charSet = 0; // 0=ABC, 1=123 #endif void updateState(cannedMessageModuleRunState, bool shouldRequestFocus = false); bool isUpEvent(const InputEvent *event); bool isDownEvent(const InputEvent *event); bool isSelectEvent(const InputEvent *event); bool handleTabSwitch(const InputEvent *event); int handleDestinationSelectionInput(const InputEvent *event, bool isUp, bool isDown, bool isSelect); bool handleMessageSelectorInput(const InputEvent *event, bool isUp, bool isDown, bool isSelect); bool handleFreeTextInput(const InputEvent *event); #if defined(USE_VIRTUAL_KEYBOARD) Letter keyboard[2][4][10] = {{{{"Q", 20, 0, 0, 0, 0}, {"W", 22, 0, 0, 0, 0}, {"E", 17, 0, 0, 0, 0}, {"R", 16.5, 0, 0, 0, 0}, {"T", 14, 0, 0, 0, 0}, {"Y", 15, 0, 0, 0, 0}, {"U", 16.5, 0, 0, 0, 0}, {"I", 5, 0, 0, 0, 0}, {"O", 19.5, 0, 0, 0, 0}, {"P", 15.5, 0, 0, 0, 0}}, {{"A", 14, 0, 0, 0, 0}, {"S", 15, 0, 0, 0, 0}, {"D", 16.5, 0, 0, 0, 0}, {"F", 15, 0, 0, 0, 0}, {"G", 17, 0, 0, 0, 0}, {"H", 15.5, 0, 0, 0, 0}, {"J", 12, 0, 0, 0, 0}, {"K", 15.5, 0, 0, 0, 0}, {"L", 14, 0, 0, 0, 0}, {"", 0, 0, 0, 0, 0}}, {{"⇧", 20, 0, 0, 0, 0}, {"Z", 14, 0, 0, 0, 0}, {"X", 14.5, 0, 0, 0, 0}, {"C", 15.5, 0, 0, 0, 0}, {"V", 13.5, 0, 0, 0, 0}, {"B", 15, 0, 0, 0, 0}, {"N", 15, 0, 0, 0, 0}, {"M", 17, 0, 0, 0, 0}, {"⌫", 20, 0, 0, 0, 0}, {"", 0, 0, 0, 0, 0}}, {{"123", 42, 0, 0, 0, 0}, {" ", 64, 0, 0, 0, 0}, {"↵", 36, 0, 0, 0, 0}, {"", 0, 0, 0, 0, 0}, {"", 0, 0, 0, 0, 0}, {"", 0, 0, 0, 0, 0}, {"", 0, 0, 0, 0, 0}, {"", 0, 0, 0, 0, 0}, {"", 0, 0, 0, 0, 0}, {"", 0, 0, 0, 0, 0}}}, {{{"1", 12, 0, 0, 0, 0}, {"2", 13.5, 0, 0, 0, 0}, {"3", 12.5, 0, 0, 0, 0}, {"4", 14, 0, 0, 0, 0}, {"5", 14, 0, 0, 0, 0}, {"6", 14, 0, 0, 0, 0}, {"7", 13.5, 0, 0, 0, 0}, {"8", 14, 0, 0, 0, 0}, {"9", 14, 0, 0, 0, 0}, {"0", 14, 0, 0, 0, 0}}, {{"-", 8, 0, 0, 0, 0}, {"/", 8, 0, 0, 0, 0}, {":", 4.5, 0, 0, 0, 0}, {";", 4.5, 0, 0, 0, 0}, {"(", 7, 0, 0, 0, 0}, {")", 6.5, 0, 0, 0, 0}, {"$", 12.5, 0, 0, 0, 0}, {"&", 15, 0, 0, 0, 0}, {"@", 21.5, 0, 0, 0, 0}, {"\"", 8, 0, 0, 0, 0}}, {{".", 8, 0, 0, 0, 0}, {",", 8, 0, 0, 0, 0}, {"?", 10, 0, 0, 0, 0}, {"!", 10, 0, 0, 0, 0}, {"'", 10, 0, 0, 0, 0}, {"⌫", 20, 0, 0, 0, 0}}, {{"ABC", 50, 0, 0, 0, 0}, {" ", 64, 0, 0, 0, 0}, {"↵", 36, 0, 0, 0, 0}}}}; #endif }; extern CannedMessageModule *cannedMessageModule; #endif ================================================ FILE: src/modules/DetectionSensorModule.cpp ================================================ #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. Disable module"); return disable(); } LOG_INFO("Detection Sensor Module: init"); return setStartDelay(); } // 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. Send message"); char *message = new char[40]; sprintf(message, "%s detected", moduleConfig.detection_sensor.name); meshtastic_MeshPacket *p = allocDataPacket(); p->want_ack = false; p->decoded.payload.size = strlen(message); memcpy(p->decoded.payload.bytes, message, p->decoded.payload.size); if (moduleConfig.detection_sensor.send_bell && p->decoded.payload.size < meshtastic_Constants_DATA_PAYLOAD_LEN) { p->decoded.payload.bytes[p->decoded.payload.size] = 7; // Bell character p->decoded.payload.bytes[p->decoded.payload.size + 1] = '\0'; // Bell character p->decoded.payload.size++; } lastSentToMesh = millis(); if (!channels.isDefaultChannel(0)) { LOG_INFO("Send message id=%d, dest=%x, msg=%.*s", p->id, p->to, p->decoded.payload.size, p->decoded.payload.bytes); service->sendToMesh(p); } else LOG_ERROR("Message not allow on Public channel"); delete[] message; } void DetectionSensorModule::sendCurrentStateMessage(bool state) { char *message = new char[40]; sprintf(message, "%s state: %i", moduleConfig.detection_sensor.name, state); meshtastic_MeshPacket *p = allocDataPacket(); p->want_ack = false; p->decoded.payload.size = strlen(message); memcpy(p->decoded.payload.bytes, message, p->decoded.payload.size); lastSentToMesh = millis(); if (!channels.isDefaultChannel(0)) { LOG_INFO("Send message id=%d, dest=%x, msg=%.*s", p->id, p->to, p->decoded.payload.size, p->decoded.payload.bytes); service->sendToMesh(p); } else LOG_ERROR("Message not allow on Public channel"); delete[] message; } 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; } ================================================ FILE: src/modules/DetectionSensorModule.h ================================================ #pragma once #include "SinglePortModule.h" class DetectionSensorModule : public SinglePortModule, private concurrency::OSThread { public: DetectionSensorModule() : SinglePortModule("detection", meshtastic_PortNum_DETECTION_SENSOR_APP), OSThread("DetectionSensor") { } 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; ================================================ FILE: src/modules/DropzoneModule.cpp ================================================ #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; graphics::decomposeTime(rtc_sec, hour, min, sec); } // Check if the dropzone is open or closed by reading the analog pin // If pin is connected to GND (below 100 should be lower than floating voltage), // the dropzone is open auto dropzoneStatus = analogRead(A1) < 100 ? "OPEN" : "CLOSED"; auto reply = allocDataPacket(); 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 ================================================ FILE: src/modules/DropzoneModule.h ================================================ #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("Dropzone") { // 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 ================================================ FILE: src/modules/ExternalNotificationModule.cpp ================================================ /** * @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 #if defined(HAS_RGB_LED) #include "AmbientLightingThread.h" uint8_t red = 0; uint8_t green = 0; uint8_t blue = 0; uint8_t white = 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_FAST_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 { uint32_t delay = EXT_NOTIFICATION_MODULE_OUTPUT_MS; bool isRtttlPlaying = rtttl::isPlaying(); #ifdef HAS_I2S // audioThread->isPlaying() also handles actually playing the RTTTL, needs to be called in loop isRtttlPlaying = isRtttlPlaying || audioThread->isPlaying(); #endif if ((nagCycleCutoff < millis()) && !isRtttlPlaying) { // Turn off external notification immediately when timeout is reached, regardless of song state nagCycleCutoff = UINT32_MAX; ExternalNotificationModule::stopNow(); isNagging = false; return INT32_MAX; // save cycles till we're needed again } // If the output is turned on, turn it back off after the given period of time. if (isNagging) { delay = (moduleConfig.external_notification.output_ms ? moduleConfig.external_notification.output_ms : EXT_NOTIFICATION_MODULE_OUTPUT_MS); if (externalTurnedOn[0] + delay < millis()) { setExternalState(0, !getExternal(0)); } if (externalTurnedOn[1] + delay < millis()) { setExternalState(1, !getExternal(1)); } // Only toggle buzzer output if not using PWM mode (to avoid conflict with RTTTL) if (!moduleConfig.external_notification.use_pwm && externalTurnedOn[2] + delay < millis()) { LOG_DEBUG("EXTERNAL 2 %d compared to %d", externalTurnedOn[2] + moduleConfig.external_notification.output_ms, millis()); setExternalState(2, !getExternal(2)); } #if defined(HAS_RGB_LED) red = (colorState & 4) ? brightnessValues[brightnessIndex] : 0; // Red enabled on colorState = 4,5,6,7 green = (colorState & 2) ? brightnessValues[brightnessIndex] : 0; // Green enabled on colorState = 2,3,6,7 blue = (colorState & 1) ? (brightnessValues[brightnessIndex] * 1.5) : 0; // Blue enabled on colorState = 1,3,5,7 white = (colorState & 12) ? brightnessValues[brightnessIndex] : 0; if (ascending) { // fade in brightnessIndex++; if (brightnessIndex == (sizeof(brightnessValues) - 1)) { ascending = false; } } else { brightnessIndex--; // fade out } if (brightnessIndex == 0) { ascending = true; colorState++; // next color if (colorState > 7) { colorState = 1; } } // we need fast updates for the color change delay = EXT_NOTIFICATION_FAST_THREAD_MS; #endif #ifdef HAS_DRV2605 drv.go(); #endif } // Play RTTTL over i2s audio interface if enabled as buzzer #ifdef HAS_I2S if (moduleConfig.external_notification.use_i2s_as_buzzer) { if (audioThread->isPlaying()) { // Continue playing } else if (isNagging && (nagCycleCutoff >= millis())) { audioThread->beginRttl(rtttlConfig.ringtone, strlen_P(rtttlConfig.ringtone)); } // we need fast updates to play the RTTTL delay = EXT_NOTIFICATION_FAST_THREAD_MS; } #endif // now let the PWM buzzer play if (moduleConfig.external_notification.use_pwm && config.device.buzzer_gpio && canBuzz()) { if (rtttl::isPlaying()) { rtttl::play(); } else if (isNagging && (nagCycleCutoff >= millis())) { // start the song again if we have time left rtttl::begin(config.device.buzzer_gpio, rtttlConfig.ringtone); } // we need fast updates to play the RTTTL delay = EXT_NOTIFICATION_FAST_THREAD_MS; } return delay; } } /** * Based on buzzer mode, return true if we can buzz. */ bool ExternalNotificationModule::canBuzz() { if (config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_DISABLED && config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_SYSTEM_ONLY) { return true; } return false; } bool ExternalNotificationModule::wantPacket(const meshtastic_MeshPacket *p) { return MeshService::isTextPayload(p); } /** * Sets the external notification for the specified index. * * @param index The index of the external notification to change state. * @param on Whether we are turning things on (true) or off (false). */ void ExternalNotificationModule::setExternalState(uint8_t index, bool on) { externalCurrentState[index] = on; externalTurnedOn[index] = millis(); switch (index) { case 1: #ifdef UNPHONE unphone.vibe(on); // the unPhone's vibration motor is on a i2c GPIO expander #endif if (moduleConfig.external_notification.output_vibra) digitalWrite(moduleConfig.external_notification.output_vibra, on); break; case 2: // Only control buzzer pin digitally if not using PWM mode if (moduleConfig.external_notification.output_buzzer && !moduleConfig.external_notification.use_pwm) digitalWrite(moduleConfig.external_notification.output_buzzer, on); break; default: if (output > 0) digitalWrite(output, (moduleConfig.external_notification.active ? on : !on)); break; } #if defined(HAS_RGB_LED) if (!on) { red = 0; green = 0; blue = 0; white = 0; } ambientLightingThread->setLighting(moduleConfig.ambient_lighting.current, red, green, blue); #endif #ifdef HAS_DRV2605 if (on) { drv.go(); } else { drv.stop(); } #endif } bool ExternalNotificationModule::getExternal(uint8_t index) { return externalCurrentState[index]; } // Allow other firmware components to determine whether a notification is ongoing bool ExternalNotificationModule::nagging() { return isNagging; } void ExternalNotificationModule::stopNow() { LOG_INFO("Turning off external notification: "); LOG_INFO("Stop RTTTL playback"); rtttl::stop(); #ifdef HAS_I2S LOG_INFO("Stop audioThread playback"); audioThread->stop(); #endif // Turn off all outputs LOG_INFO("Turning off setExternalStates"); for (int i = 0; i < 3; i++) { setExternalState(i, false); externalTurnedOn[i] = 0; } setIntervalFromNow(0); #ifdef HAS_DRV2605 drv.stop(); #endif // Prevent the state machine from immediately re-triggering outputs after a manual stop. isNagging = false; nagCycleCutoff = UINT32_MAX; #ifdef HAS_I2S // GPIO0 is used as mclk for I2S audio and set to OUTPUT by the sound library // T-Deck uses GPIO0 as trackball button, so restore the mode #if defined(T_DECK) || (defined(BUTTON_PIN) && BUTTON_PIN == 0) pinMode(0, INPUT); #endif #endif } ExternalNotificationModule::ExternalNotificationModule() : SinglePortModule("ExternalNotificationModule", meshtastic_PortNum_TEXT_MESSAGE_APP), concurrency::OSThread("ExternalNotification") { /* Uncomment the preferences below if you want to use the module without having to configure it from the PythonAPI or WebUI. */ // 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 !defined(MESHTASTIC_EXCLUDE_INPUTBROKER) if (inputBroker) // put our callback in the inputObserver list inputObserver.observe(inputBroker); #endif if (nodeDB->loadProto(rtttlConfigFile, meshtastic_RTTTLConfig_size, sizeof(meshtastic_RTTTLConfig), &meshtastic_RTTTLConfig_msg, &rtttlConfig) != LoadFileResult::LOAD_SUCCESS) { memset(rtttlConfig.ringtone, 0, sizeof(rtttlConfig.ringtone)); // The default ringtone is always loaded from userPrefs.jsonc strncpy(rtttlConfig.ringtone, USERPREFS_RINGTONE_RTTTL, sizeof(rtttlConfig.ringtone)); } LOG_INFO("Init 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("Use Pin %i in digital mode", output); pinMode(output, OUTPUT); } setExternalState(0, false); externalTurnedOn[0] = 0; if (moduleConfig.external_notification.output_vibra) { LOG_INFO("Use Pin %i for vibra motor", moduleConfig.external_notification.output_vibra); pinMode(moduleConfig.external_notification.output_vibra, OUTPUT); setExternalState(1, false); externalTurnedOn[1] = 0; } if (moduleConfig.external_notification.output_buzzer && canBuzz()) { if (!moduleConfig.external_notification.use_pwm) { LOG_INFO("Use Pin %i for buzzer", moduleConfig.external_notification.output_buzzer); pinMode(moduleConfig.external_notification.output_buzzer, OUTPUT); setExternalState(2, false); externalTurnedOn[2] = 0; } else { config.device.buzzer_gpio = config.device.buzzer_gpio ? config.device.buzzer_gpio : PIN_BUZZER; // in PWM Mode we force the buzzer pin if it is set LOG_INFO("Use Pin %i in PWM mode", config.device.buzzer_gpio); } } } else { LOG_INFO("External Notification Module Disabled"); disable(); } } ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshPacket &mp) { // Trigger external notification if enabled and not muted; isSilenced is from temporary mute toggles if (moduleConfig.external_notification.enabled && !isSilenced) { #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 (size_t i = 0; i < p.payload.size; i++) { if (p.payload.bytes[i] == ASCII_BELL) { containsBell = true; break; } } const meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(mp.from); meshtastic_Channel ch = channels.getByIndex(mp.channel ? mp.channel : channels.getPrimaryIndex()); // If we receive a broadcast message, apply channel mute setting // If we receive a direct message and the receipent is us, apply DM mute setting // Else we just handle it as not muted. const bool isDmToUs = !isBroadcast(mp.to) && isToUs(&mp); bool is_muted = isDmToUs ? (sender && ((sender->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0)) : (ch.settings.has_module_settings && ch.settings.module_settings.is_muted); const bool buzzerModeIsDirectOnly = (config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_DIRECT_MSG_ONLY); if (containsBell || !is_muted) { if (moduleConfig.external_notification.alert_bell || moduleConfig.external_notification.alert_message || moduleConfig.external_notification.alert_bell_vibra || moduleConfig.external_notification.alert_message_vibra || ((moduleConfig.external_notification.alert_bell_buzzer || moduleConfig.external_notification.alert_message_buzzer) && canBuzz())) { nagCycleCutoff = millis() + (moduleConfig.external_notification.nag_timeout ? (moduleConfig.external_notification.nag_timeout * 1000) : moduleConfig.external_notification.output_ms); LOG_INFO("Toggling nagCycleCutoff to %lu", nagCycleCutoff); isNagging = true; } if (moduleConfig.external_notification.alert_bell || moduleConfig.external_notification.alert_message) { LOG_INFO("externalNotificationModule - Notification Module or Bell"); setExternalState(0, true); } if (moduleConfig.external_notification.alert_bell_vibra || moduleConfig.external_notification.alert_message_vibra) { LOG_INFO("externalNotificationModule - Notification Module or Bell (Vibra)"); setExternalState(1, true); } if ((moduleConfig.external_notification.alert_bell_buzzer || moduleConfig.external_notification.alert_message_buzzer) && canBuzz()) { LOG_INFO("externalNotificationModule - Notification Module or Bell (Buzzer)"); if (buzzerModeIsDirectOnly && !isDmToUs && !containsBell) { LOG_INFO("Message buzzer was suppressed because buzzer mode DIRECT_MSG_ONLY"); } else { // Buzz if buzzer mode is not in DIRECT_MSG_ONLY or is DM to us #ifdef T_LORA_PAGER drv.setWaveform(0, 16); // Long buzzer 100% drv.setWaveform(1, 0); // Pause drv.setWaveform(2, 16); drv.setWaveform(3, 0); drv.setWaveform(4, 16); drv.setWaveform(5, 0); drv.setWaveform(6, 16); drv.setWaveform(7, 0); drv.go(); #endif #ifdef HAS_I2S if (moduleConfig.external_notification.use_i2s_as_buzzer) { audioThread->beginRttl(rtttlConfig.ringtone, strlen_P(rtttlConfig.ringtone)); } else #endif if (moduleConfig.external_notification.use_pwm) { rtttl::begin(config.device.buzzer_gpio, rtttlConfig.ringtone); } else { setExternalState(2, true); } } } } 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 getting ringtone"); this->handleGetRingtone(mp, response); result = AdminMessageHandleResult::HANDLED_WITH_RESPONSE; break; case meshtastic_AdminMessage_set_ringtone_message_tag: LOG_INFO("Client setting ringtone"); this->handleSetRingtone(request->set_canned_message_module_messages); result = AdminMessageHandleResult::HANDLED; break; 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); } } int ExternalNotificationModule::handleInputEvent(const InputEvent *event) { if (nagCycleCutoff != UINT32_MAX) { stopNow(); return 1; } return 0; } ================================================ FILE: src/modules/ExternalNotificationModule.h ================================================ #pragma once #include "SinglePortModule.h" #include "concurrency/OSThread.h" #include "configuration.h" #include "input/InputBroker.h" #ifdef HAS_RGB_LED #include "AmbientLightingThread.h" extern AmbientLightingThread *ambientLightingThread; #endif #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 { CallbackObserver inputObserver = CallbackObserver(this, &ExternalNotificationModule::handleInputEvent); uint32_t output = 0; public: ExternalNotificationModule(); int handleInputEvent(const InputEvent *arg); uint32_t nagCycleCutoff = 1; void setExternalState(uint8_t index = 0, bool on = false); bool getExternal(uint8_t index = 0); void setMute(bool mute) { isSilenced = mute; } bool getMute() { return isSilenced; } bool canBuzz(); bool nagging(); 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 isSilenced = false; virtual AdminMessageHandleResult handleAdminMessageForModule(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, meshtastic_AdminMessage *response) override; }; extern ExternalNotificationModule *externalNotificationModule; ================================================ FILE: src/modules/GenericThreadModule.cpp ================================================ #include "GenericThreadModule.h" #include "MeshService.h" #include "configuration.h" #include /* Generic Thread Module allows for the execution of custom code at a set interval. */ GenericThreadModule *genericThreadModule; GenericThreadModule::GenericThreadModule() : concurrency::OSThread("GenericThreadModule") {} int32_t GenericThreadModule::runOnce() { bool enabled = true; if (!enabled) return disable(); if (firstTime) { // do something the first time we run firstTime = 0; LOG_INFO("first time GenericThread running"); } LOG_INFO("GenericThread executing"); return (my_interval); } ================================================ FILE: src/modules/GenericThreadModule.h ================================================ #pragma once #include "MeshModule.h" #include "concurrency/OSThread.h" #include "configuration.h" #include #include class GenericThreadModule : private concurrency::OSThread { bool firstTime = 1; public: GenericThreadModule(); protected: unsigned int my_interval = 10000; // interval in millisconds virtual int32_t runOnce() override; }; extern GenericThreadModule *genericThreadModule; ================================================ FILE: src/modules/KeyVerificationModule.cpp ================================================ #if !MESHTASTIC_EXCLUDE_PKI #include "KeyVerificationModule.h" #include "MeshService.h" #include "RTC.h" #include "graphics/draw/MenuHandler.h" #include "main.h" #include "meshUtils.h" #include "modules/AdminModule.h" #include KeyVerificationModule *keyVerificationModule; KeyVerificationModule::KeyVerificationModule() : ProtobufModule("KeyVerification", meshtastic_PortNum_KEY_VERIFICATION_APP, &meshtastic_KeyVerification_msg) { ourPortNum = meshtastic_PortNum_KEY_VERIFICATION_APP; } AdminMessageHandleResult KeyVerificationModule::handleAdminMessageForModule(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, meshtastic_AdminMessage *response) { updateState(); if (request->which_payload_variant == meshtastic_AdminMessage_key_verification_tag && mp.from == 0) { LOG_WARN("Handling Key Verification Admin Message type %u", request->key_verification.message_type); if (request->key_verification.message_type == meshtastic_KeyVerificationAdmin_MessageType_INITIATE_VERIFICATION && currentState == KEY_VERIFICATION_IDLE) { sendInitialRequest(request->key_verification.remote_nodenum); } else if (request->key_verification.message_type == meshtastic_KeyVerificationAdmin_MessageType_PROVIDE_SECURITY_NUMBER && request->key_verification.has_security_number && currentState == KEY_VERIFICATION_SENDER_AWAITING_NUMBER && request->key_verification.nonce == currentNonce) { processSecurityNumber(request->key_verification.security_number); } else if (request->key_verification.message_type == meshtastic_KeyVerificationAdmin_MessageType_DO_VERIFY && request->key_verification.nonce == currentNonce) { auto remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode); remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK; resetToIdle(); } else if (request->key_verification.message_type == meshtastic_KeyVerificationAdmin_MessageType_DO_NOT_VERIFY) { resetToIdle(); } return AdminMessageHandleResult::HANDLED; } return AdminMessageHandleResult::NOT_HANDLED; } bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_KeyVerification *r) { updateState(); if (mp.pki_encrypted == false) { return false; } if (mp.from != currentRemoteNode) { // because the inital connection request is handled in allocReply() return false; } if (currentState == KEY_VERIFICATION_IDLE) { return false; // if we're idle, the only acceptable message is an init, which should be handled by allocReply() } if (currentState == KEY_VERIFICATION_SENDER_HAS_INITIATED && r->nonce == currentNonce && r->hash2.size == 32 && r->hash1.size == 0) { memcpy(hash2, r->hash2.bytes, 32); IF_SCREEN(screen->showNumberPicker("Enter Security Number", 60000, 6, [](int number_picked) -> void { keyVerificationModule->processSecurityNumber(number_picked); });) meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); cn->level = meshtastic_LogRecord_Level_WARNING; sprintf(cn->message, "Enter Security Number for Key Verification"); cn->which_payload_variant = meshtastic_ClientNotification_key_verification_number_request_tag; cn->payload_variant.key_verification_number_request.nonce = currentNonce; strncpy(cn->payload_variant.key_verification_number_request.remote_longname, // should really check for nulls, etc nodeDB->getMeshNode(currentRemoteNode)->user.long_name, sizeof(cn->payload_variant.key_verification_number_request.remote_longname)); service->sendClientNotification(cn); LOG_INFO("Received hash2"); currentState = KEY_VERIFICATION_SENDER_AWAITING_NUMBER; return true; } else if (currentState == KEY_VERIFICATION_RECEIVER_AWAITING_HASH1 && r->hash1.size == 32 && r->nonce == currentNonce) { if (memcmp(hash1, r->hash1.bytes, 32) == 0) { memset(message, 0, sizeof(message)); sprintf(message, "Verification: \n"); generateVerificationCode(message + 15); LOG_INFO("Hash1 matches!"); static const char *optionsArray[] = {"Reject", "Accept"}; // Don't try to put the array definition in the macro. Does not work with curly braces. IF_SCREEN(graphics::BannerOverlayOptions options; options.message = message; options.durationMs = 30000; options.optionsArrayPtr = optionsArray; options.optionsCount = 2; options.notificationType = graphics::notificationTypeEnum::selection_picker; options.bannerCallback = [=](int selected) { if (selected == 1) { auto remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode); remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK; } }; screen->showOverlayBanner(options);) meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); cn->level = meshtastic_LogRecord_Level_WARNING; sprintf(cn->message, "Final confirmation for incoming manual key verification %s", message); cn->which_payload_variant = meshtastic_ClientNotification_key_verification_final_tag; cn->payload_variant.key_verification_final.nonce = currentNonce; strncpy(cn->payload_variant.key_verification_final.remote_longname, // should really check for nulls, etc nodeDB->getMeshNode(currentRemoteNode)->user.long_name, sizeof(cn->payload_variant.key_verification_final.remote_longname)); cn->payload_variant.key_verification_final.isSender = false; service->sendClientNotification(cn); currentState = KEY_VERIFICATION_RECEIVER_AWAITING_USER; return true; } } return false; } bool KeyVerificationModule::sendInitialRequest(NodeNum remoteNode) { LOG_DEBUG("keyVerification start"); // generate nonce updateState(); if (currentState != KEY_VERIFICATION_IDLE) { IF_SCREEN(graphics::menuHandler::menuQueue = graphics::menuHandler::ThrottleMessage;) return false; } currentNonce = random(); currentNonceTimestamp = getTime(); currentRemoteNode = remoteNode; meshtastic_KeyVerification KeyVerification = meshtastic_KeyVerification_init_zero; KeyVerification.nonce = currentNonce; KeyVerification.hash2.size = 0; KeyVerification.hash1.size = 0; meshtastic_MeshPacket *p = allocDataProtobuf(KeyVerification); p->to = remoteNode; p->channel = 0; p->pki_encrypted = true; p->decoded.want_response = true; p->priority = meshtastic_MeshPacket_Priority_HIGH; service->sendToMesh(p, RX_SRC_LOCAL, true); currentState = KEY_VERIFICATION_SENDER_HAS_INITIATED; return true; } meshtastic_MeshPacket *KeyVerificationModule::allocReply() { SHA256 hash; NodeNum ourNodeNum = nodeDB->getNodeNum(); updateState(); if (currentState != KEY_VERIFICATION_IDLE) { // TODO: cooldown period LOG_WARN("Key Verification requested, but already in a request"); return nullptr; } else if (!currentRequest->pki_encrypted) { LOG_WARN("Key Verification requested, but not in a PKI packet"); return nullptr; } currentState = KEY_VERIFICATION_RECEIVER_AWAITING_HASH1; auto req = *currentRequest; const auto &p = req.decoded; meshtastic_KeyVerification scratch; meshtastic_KeyVerification response; meshtastic_MeshPacket *responsePacket = nullptr; pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_KeyVerification_msg, &scratch); currentNonce = scratch.nonce; response.nonce = scratch.nonce; currentRemoteNode = req.from; currentNonceTimestamp = getTime(); currentSecurityNumber = random(1, 999999); // generate hash1 hash.reset(); hash.update(¤tSecurityNumber, sizeof(currentSecurityNumber)); hash.update(¤tNonce, sizeof(currentNonce)); hash.update(¤tRemoteNode, sizeof(currentRemoteNode)); hash.update(&ourNodeNum, sizeof(ourNodeNum)); hash.update(currentRequest->public_key.bytes, currentRequest->public_key.size); hash.update(owner.public_key.bytes, owner.public_key.size); hash.finalize(hash1, 32); // generate hash2 hash.reset(); hash.update(¤tNonce, sizeof(currentNonce)); hash.update(hash1, 32); hash.finalize(hash2, 32); response.hash1.size = 0; response.hash2.size = 32; memcpy(response.hash2.bytes, hash2, 32); responsePacket = allocDataProtobuf(response); responsePacket->pki_encrypted = true; IF_SCREEN(snprintf(message, 25, "Security Number \n%03u %03u", currentSecurityNumber / 1000, currentSecurityNumber % 1000); screen->showSimpleBanner(message, 30000); LOG_WARN("%s", message);) meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); cn->level = meshtastic_LogRecord_Level_WARNING; sprintf(cn->message, "Incoming Key Verification.\nSecurity Number\n%03u %03u", currentSecurityNumber / 1000, currentSecurityNumber % 1000); cn->which_payload_variant = meshtastic_ClientNotification_key_verification_number_inform_tag; cn->payload_variant.key_verification_number_inform.nonce = currentNonce; strncpy(cn->payload_variant.key_verification_number_inform.remote_longname, // should really check for nulls, etc nodeDB->getMeshNode(currentRemoteNode)->user.long_name, sizeof(cn->payload_variant.key_verification_number_inform.remote_longname)); cn->payload_variant.key_verification_number_inform.security_number = currentSecurityNumber; service->sendClientNotification(cn); LOG_WARN("Security Number %04u, nonce %llu", currentSecurityNumber, currentNonce); return responsePacket; } void KeyVerificationModule::processSecurityNumber(uint32_t incomingNumber) { SHA256 hash; NodeNum ourNodeNum = nodeDB->getNodeNum(); uint8_t scratch_hash[32] = {0}; LOG_WARN("received security number: %u", incomingNumber); meshtastic_NodeInfoLite *remoteNodePtr = nullptr; remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode); if (remoteNodePtr == nullptr || !remoteNodePtr->has_user || remoteNodePtr->user.public_key.size != 32) { currentState = KEY_VERIFICATION_IDLE; return; // should we throw an error here? } LOG_WARN("hashing "); // calculate hash1 hash.reset(); hash.update(&incomingNumber, sizeof(incomingNumber)); hash.update(¤tNonce, sizeof(currentNonce)); hash.update(&ourNodeNum, sizeof(ourNodeNum)); hash.update(¤tRemoteNode, sizeof(currentRemoteNode)); hash.update(owner.public_key.bytes, owner.public_key.size); hash.update(remoteNodePtr->user.public_key.bytes, remoteNodePtr->user.public_key.size); hash.finalize(hash1, 32); hash.reset(); hash.update(¤tNonce, sizeof(currentNonce)); hash.update(hash1, 32); hash.finalize(scratch_hash, 32); if (memcmp(scratch_hash, hash2, 32) != 0) { LOG_WARN("Hash2 did not match"); return; // should probably throw an error of some sort } currentSecurityNumber = incomingNumber; meshtastic_KeyVerification KeyVerification = meshtastic_KeyVerification_init_zero; KeyVerification.nonce = currentNonce; KeyVerification.hash2.size = 0; KeyVerification.hash1.size = 32; memcpy(KeyVerification.hash1.bytes, hash1, 32); meshtastic_MeshPacket *p = allocDataProtobuf(KeyVerification); p->to = currentRemoteNode; p->channel = 0; p->pki_encrypted = true; p->decoded.want_response = true; p->priority = meshtastic_MeshPacket_Priority_HIGH; service->sendToMesh(p, RX_SRC_LOCAL, true); currentState = KEY_VERIFICATION_SENDER_AWAITING_USER; IF_SCREEN(screen->requestMenu(graphics::menuHandler::KeyVerificationFinalPrompt);) meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); cn->level = meshtastic_LogRecord_Level_WARNING; sprintf(cn->message, "Final confirmation for outgoing manual key verification %s", message); cn->which_payload_variant = meshtastic_ClientNotification_key_verification_final_tag; cn->payload_variant.key_verification_final.nonce = currentNonce; strncpy(cn->payload_variant.key_verification_final.remote_longname, // should really check for nulls, etc nodeDB->getMeshNode(currentRemoteNode)->user.long_name, sizeof(cn->payload_variant.key_verification_final.remote_longname)); cn->payload_variant.key_verification_final.isSender = true; service->sendClientNotification(cn); LOG_INFO(message); return; } void KeyVerificationModule::updateState() { if (currentState != KEY_VERIFICATION_IDLE) { // check for the 60 second timeout if (currentNonceTimestamp < getTime() - 60) { resetToIdle(); } else { currentNonceTimestamp = getTime(); } } } void KeyVerificationModule::resetToIdle() { memset(hash1, 0, 32); memset(hash2, 0, 32); currentNonce = 0; currentNonceTimestamp = 0; currentSecurityNumber = 0; currentRemoteNode = 0; currentState = KEY_VERIFICATION_IDLE; } void KeyVerificationModule::generateVerificationCode(char *readableCode) { for (int i = 0; i < 4; i++) { // drop the two highest significance bits, then encode as a base64 readableCode[i] = (hash1[i] >> 2) + 48; // not a standardized base64, but workable and avoids having a dictionary. } readableCode[4] = ' '; for (int i = 5; i < 9; i++) { // drop the two highest significance bits, then encode as a base64 readableCode[i] = (hash1[i] >> 2) + 48; // not a standardized base64, but workable and avoids having a dictionary. } } #endif ================================================ FILE: src/modules/KeyVerificationModule.h ================================================ #pragma once #include "ProtobufModule.h" #include "SinglePortModule.h" enum KeyVerificationState { KEY_VERIFICATION_IDLE, KEY_VERIFICATION_SENDER_HAS_INITIATED, KEY_VERIFICATION_SENDER_AWAITING_NUMBER, KEY_VERIFICATION_SENDER_AWAITING_USER, KEY_VERIFICATION_RECEIVER_AWAITING_USER, KEY_VERIFICATION_RECEIVER_AWAITING_HASH1, }; class KeyVerificationModule : public ProtobufModule //, private concurrency::OSThread // { // CallbackObserver nodeStatusObserver = // CallbackObserver(this, &KeyVerificationModule::handleStatusUpdate); public: KeyVerificationModule(); /* : concurrency::OSThread("KeyVerification"), ProtobufModule("KeyVerification", meshtastic_PortNum_KEY_VERIFICATION_APP, &meshtastic_KeyVerification_msg) { nodeStatusObserver.observe(&nodeStatus->onNewStatus); setIntervalFromNow(setStartDelay()); // Wait until NodeInfo is sent }*/ virtual bool wantUIFrame() { return false; }; bool sendInitialRequest(NodeNum remoteNode); void generateVerificationCode(char *); // fills char with the user readable verification code uint32_t getCurrentRemoteNode() { return currentRemoteNode; } protected: /* Called to handle a particular incoming message @return true if you've guaranteed you've handled this message and no other handlers should be considered for it */ virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_KeyVerification *p); // virtual meshtastic_MeshPacket *allocReply() override; // rather than add to the craziness that is the admin module, just handle those requests here. virtual AdminMessageHandleResult handleAdminMessageForModule(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, meshtastic_AdminMessage *response) override; /* * Send our Telemetry into the mesh */ bool sendMetrics(); virtual meshtastic_MeshPacket *allocReply() override; private: uint64_t currentNonce = 0; uint32_t currentNonceTimestamp = 0; NodeNum currentRemoteNode = 0; uint32_t currentSecurityNumber = 0; KeyVerificationState currentState = KEY_VERIFICATION_IDLE; uint8_t hash1[32] = {0}; // uint8_t hash2[32] = {0}; // char message[40] = {0}; void processSecurityNumber(uint32_t); void updateState(); // check the timeouts and maybe reset the state to idle void resetToIdle(); // Zero out module state }; extern KeyVerificationModule *keyVerificationModule; ================================================ FILE: src/modules/ModuleDev.h ================================================ #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; ================================================ FILE: src/modules/Modules.cpp ================================================ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_INPUTBROKER #include "buzz/BuzzerFeedbackThread.h" #include "modules/SystemCommandsModule.h" #endif #include "modules/StatusLEDModule.h" #if !MESHTASTIC_EXCLUDE_REPLYBOT #include "ReplyBotModule.h" #endif #if !MESHTASTIC_EXCLUDE_PKI #include "KeyVerificationModule.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" #if HAS_TRAFFIC_MANAGEMENT && !MESHTASTIC_EXCLUDE_TRAFFIC_MANAGEMENT #include "modules/TrafficManagementModule.h" #endif #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 "modules/Telemetry/HostMetrics.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/EnvironmentTelemetry.h" #include "modules/Telemetry/HealthTelemetry.h" #include "modules/Telemetry/Sensor/TelemetrySensor.h" #endif #if HAS_SENSOR && !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR #include "main.h" #include "modules/Telemetry/AirQualityTelemetry.h" #include "modules/Telemetry/Sensor/TelemetrySensor.h" #endif #if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_POWER_TELEMETRY #include "modules/Telemetry/PowerTelemetry.h" #endif #if !MESHTASTIC_EXCLUDE_GENERIC_THREAD_MODULE #include "modules/GenericThreadModule.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 !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 #if !MESHTASTIC_EXCLUDE_DROPZONE #include "modules/DropzoneModule.h" #endif #if !MESHTASTIC_EXCLUDE_STATUS #include "modules/StatusMessageModule.h" #endif #if defined(HAS_HARDWARE_WATCHDOG) #include "watchdog/watchdogThread.h" #endif /** * Create module instances here. If you are adding a new module, you must 'new' it here (or somewhere else) */ void setupModules() { #if (HAS_BUTTON || ARCH_PORTDUINO) && !MESHTASTIC_EXCLUDE_INPUTBROKER if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { inputBroker = new InputBroker(); systemCommandsModule = new SystemCommandsModule(); buzzerFeedbackThread = new BuzzerFeedbackThread(); } #endif statusLEDModule = new StatusLEDModule(); #if !MESHTASTIC_EXCLUDE_REPLYBOT new ReplyBotModule(); #endif #if HAS_TRAFFIC_MANAGEMENT && !MESHTASTIC_EXCLUDE_TRAFFIC_MANAGEMENT // Instantiate only when enabled to avoid extra memory use and background work. if (moduleConfig.has_traffic_management && moduleConfig.traffic_management.enabled) { trafficManagementModule = new TrafficManagementModule(); } #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 if (moduleConfig.has_neighbor_info && moduleConfig.neighbor_info.enabled) { neighborInfoModule = new NeighborInfoModule(); } #endif #if !MESHTASTIC_EXCLUDE_DETECTIONSENSOR if (moduleConfig.has_detection_sensor && moduleConfig.detection_sensor.enabled) { detectionSensorModule = new DetectionSensorModule(); } #endif #if !MESHTASTIC_EXCLUDE_ATAK if (config.device.role == meshtastic_Config_DeviceConfig_Role_TAK || config.device.role == meshtastic_Config_DeviceConfig_Role_TAK_TRACKER) { atakPluginModule = new AtakPluginModule(); } #endif #if !MESHTASTIC_EXCLUDE_PKI keyVerificationModule = new KeyVerificationModule(); #endif #if !MESHTASTIC_EXCLUDE_DROPZONE dropzoneModule = new DropzoneModule(); #endif #if !MESHTASTIC_EXCLUDE_STATUS statusMessageModule = new StatusMessageModule(); #endif #if !MESHTASTIC_EXCLUDE_GENERIC_THREAD_MODULE new GenericThreadModule(); #endif // Note: if the rest of meshtastic doesn't need to explicitly use your module, you do not need to assign the instance // to a global variable. #if !MESHTASTIC_EXCLUDE_REMOTEHARDWARE new RemoteHardwareModule(); #endif #if !MESHTASTIC_EXCLUDE_POWERSTRESS new PowerStressModule(); #endif // Example: Put your module here // new ReplyModule(); #if HAS_SCREEN && !MESHTASTIC_EXCLUDE_CANNEDMESSAGES if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { cannedMessageModule = new CannedMessageModule(); } #endif #if ARCH_PORTDUINO new HostMetricsModule(); #endif #if HAS_TELEMETRY new DeviceTelemetryModule(); #endif #if HAS_TELEMETRY && HAS_SENSOR && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR if (moduleConfig.has_telemetry && (moduleConfig.telemetry.environment_measurement_enabled || moduleConfig.telemetry.environment_screen_enabled)) { new EnvironmentTelemetryModule(); } #if HAS_TELEMETRY && HAS_SENSOR && !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR if (moduleConfig.has_telemetry && (moduleConfig.telemetry.air_quality_enabled || moduleConfig.telemetry.air_quality_screen_enabled)) { new AirQualityTelemetryModule(); } #endif #if !MESHTASTIC_EXCLUDE_HEALTH_TELEMETRY if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_MAX30102].first > 0 || nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_MLX90614].first > 0) { new HealthTelemetryModule(); } #endif #endif #if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_POWER_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR if (moduleConfig.has_telemetry && (moduleConfig.telemetry.power_measurement_enabled || moduleConfig.telemetry.power_screen_enabled)) { new PowerTelemetryModule(); } #endif #if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040) || defined(ARCH_STM32WL)) && \ !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) #if !MESHTASTIC_EXCLUDE_SERIAL if (moduleConfig.has_serial && moduleConfig.serial.enabled && config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { new SerialModule(); } #endif #endif #ifdef ARCH_ESP32 // Only run on an esp32 based device. #if defined(USE_SX1280) && !MESHTASTIC_EXCLUDE_AUDIO audioModule = new AudioModule(); #endif #if !MESHTASTIC_EXCLUDE_PAXCOUNTER if (moduleConfig.has_paxcounter && moduleConfig.paxcounter.enabled) { paxcounterModule = new PaxcounterModule(); } #endif #endif #if defined(ARCH_ESP32) || defined(ARCH_PORTDUINO) #if !MESHTASTIC_EXCLUDE_STOREFORWARD if (moduleConfig.has_store_forward && moduleConfig.store_forward.enabled) { storeForwardModule = new StoreForwardModule(); } #endif #endif #if !MESHTASTIC_EXCLUDE_EXTERNALNOTIFICATION externalNotificationModule = new ExternalNotificationModule(); #endif #if !MESHTASTIC_EXCLUDE_RANGETEST && !MESHTASTIC_EXCLUDE_GPS if (moduleConfig.has_range_test && moduleConfig.range_test.enabled) new RangeTestModule(); #endif #if defined(HAS_HARDWARE_WATCHDOG) watchdogThread = new WatchdogThread(); #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(); } ================================================ FILE: src/modules/Modules.h ================================================ #pragma once /** * Create module instances here. If you are adding a new module, you must 'new' it here (or somewhere else) */ void setupModules(); ================================================ FILE: src/modules/NeighborInfoModule.cpp ================================================ #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("NeighborInfo") { ourPortNum = meshtastic_PortNum_NEIGHBORINFO_APP; nodeStatusObserver.observe(&nodeStatus->onNewStatus); if (moduleConfig.neighbor_info.enabled) { isPromiscuous = true; // Update neighbors from all packets setIntervalFromNow(Default::getConfiguredOrDefaultMs(moduleConfig.neighbor_info.update_interval, default_telemetry_broadcast_interval_secs)); } else { LOG_DEBUG("NeighborInfoModule is disabled"); disable(); } } /* 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("Remove neighbor with node ID 0x%x", it->node_id); it = std::vector::reverse_iterator( neighbors.erase(std::next(it).base())); // Erase the element and update the iterator } else { ++it; } } } /* Send neighbor info to the mesh */ void NeighborInfoModule::sendNeighborInfo(NodeNum dest, bool wantReplies) { meshtastic_NeighborInfo neighborInfo = meshtastic_NeighborInfo_init_zero; collectNeighborInfo(&neighborInfo); // only send neighbours if we have some to send if (neighborInfo.neighbors_count > 0) { meshtastic_MeshPacket *p = allocDataProtobuf(neighborInfo); p->to = dest; p->decoded.want_response = wantReplies; p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; printNeighborInfo("SENDING", &neighborInfo); service->sendToMesh(p, RX_SRC_LOCAL, true); } } /* Encompasses the full construction and sending packet to mesh Will be used for broadcast. */ int32_t NeighborInfoModule::runOnce() { if (moduleConfig.neighbor_info.transmit_over_lora && (!channels.isDefaultChannel(channels.getPrimaryIndex()) || !RadioInterface::uses_default_frequency_slot) && airTime->isTxAllowedChannelUtil(true) && airTime->isTxAllowedAirUtil()) { sendNeighborInfo(NODENUM_BROADCAST, false); } else { sendNeighborInfo(NODENUM_BROADCAST_NO_LORA, false); } return Default::getConfiguredOrDefaultMs(moduleConfig.neighbor_info.update_interval, default_neighbor_info_broadcast_secs); } meshtastic_MeshPacket *NeighborInfoModule::allocReply() { LOG_INFO("NeighborInfoRequested."); if (lastSentReply && Throttle::isWithinTimespanMs(lastSentReply, 3 * 60 * 1000)) { LOG_DEBUG("Skip Neighbors reply since we sent a reply <3min ago"); ignoreRequest = true; // Mark it as ignored for MeshModule return nullptr; } meshtastic_NeighborInfo neighborInfo = meshtastic_NeighborInfo_init_zero; collectNeighborInfo(&neighborInfo); meshtastic_MeshPacket *reply = allocDataProtobuf(neighborInfo); if (reply) { lastSentReply = millis(); // Track when we sent this reply } return reply; } /* Collect a received neighbor info packet from another node Pass it to an upper client; do not persist this data on the mesh */ bool NeighborInfoModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_NeighborInfo *np) { LOG_DEBUG("NeighborInfo: handleReceivedProtobuf"); if (np) { printNeighborInfo("RECEIVED", np); // Ignore dummy/interceptable packets: single neighbor with nodeId 0 and snr 0 if (np->neighbors_count != 1 || np->neighbors[0].node_id != 0 || np->neighbors[0].snr != 0.0f) { LOG_DEBUG(" Updating neighbours"); updateNeighbors(mp, np); } else { LOG_DEBUG(" Ignoring dummy neighbor info packet (single neighbor with nodeId 0, snr 0)"); } } else if (getHopsAway(mp) == 0) { LOG_DEBUG("Get or create neighbor: %u with snr %f", mp.from, mp.rx_snr); // If the hopLimit is the same as hopStart, then it is a neighbor getOrCreateNeighbor(mp.from, mp.from, 0, mp.rx_snr); // Set the broadcast interval to 0, as we don't know it } // 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) { LOG_DEBUG("updateNeighbors"); // The last sent ID will be 0 if the packet is from the phone, which we don't // count as an edge. So we assume that if it's zero, then this packet is from // our node. if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag && mp.from) { getOrCreateNeighbor(mp.from, np->last_sent_by_id, np->node_broadcast_interval_secs, mp.rx_snr); } } meshtastic_Neighbor *NeighborInfoModule::getOrCreateNeighbor(NodeNum originalSender, NodeNum n, uint32_t node_broadcast_interval_secs, float snr) { // our node and the phone are the same node (not neighbors) if (n == 0) { n = nodeDB->getNodeNum(); } // look for one in the existing list for (size_t i = 0; i < neighbors.size(); i++) { if (neighbors[i].node_id == n) { // if found, update it neighbors[i].snr = snr; neighbors[i].last_rx_time = getTime(); // Only if this is the original sender, the broadcast interval corresponds // to it if (originalSender == n && node_broadcast_interval_secs != 0) neighbors[i].node_broadcast_interval_secs = node_broadcast_interval_secs; return &neighbors[i]; } } // otherwise, allocate one and assign data to it meshtastic_Neighbor new_nbr = meshtastic_Neighbor_init_zero; new_nbr.node_id = n; new_nbr.snr = snr; new_nbr.last_rx_time = getTime(); // Only if this is the original sender, the broadcast interval corresponds to // it if (originalSender == n && node_broadcast_interval_secs != 0) new_nbr.node_broadcast_interval_secs = node_broadcast_interval_secs; else // Assume the same broadcast interval as us for the neighbor if we don't // know it new_nbr.node_broadcast_interval_secs = moduleConfig.neighbor_info.update_interval; if (neighbors.size() < MAX_NUM_NEIGHBORS) { neighbors.push_back(new_nbr); } else { // If we have too many neighbors, replace the oldest one LOG_WARN("Neighbor DB is full, replace oldest neighbor"); neighbors.erase(neighbors.begin()); neighbors.push_back(new_nbr); } return &neighbors.back(); } ================================================ FILE: src/modules/NeighborInfoModule.h ================================================ #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; /* Messages can be received that have the want_response bit set. If set, this callback will be invoked * so that subclasses can (optionally) send a response back to the original sender. */ virtual meshtastic_MeshPacket *allocReply() override; /* * Collect neighbor info from the nodeDB's history, capping at a maximum number of entries and max time * @return the number of entries collected */ uint32_t collectNeighborInfo(meshtastic_NeighborInfo *neighborInfo); /* 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(); private: uint32_t lastSentReply = 0; // Last time we sent a position reply (used for reply throttling only) }; extern NeighborInfoModule *neighborInfoModule; ================================================ FILE: src/modules/NodeInfoModule.cpp ================================================ #include "NodeInfoModule.h" #include "Default.h" #include "MeshService.h" #include "NodeDB.h" #include "NodeStatus.h" #include "RTC.h" #include "Router.h" #include "TransmitHistory.h" #include "configuration.h" #include "main.h" #include #include #ifndef USERPREFS_NODEINFO_REPLY_SUPPRESS_SECS #define USERPREFS_NODEINFO_REPLY_SUPPRESS_SECS (12 * 60 * 60) #endif NodeInfoModule *nodeInfoModule; static constexpr uint32_t NodeInfoReplySuppressSeconds = USERPREFS_NODEINFO_REPLY_SUPPRESS_SECS; bool NodeInfoModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_User *pptr) { suppressReplyForCurrentRequest = false; if (mp.from == nodeDB->getNodeNum()) { LOG_WARN("Ignoring packet supposed to be from our own node: %08x", mp.from); return false; } auto p = *pptr; // Suppress replies to senders we've replied to recently (12H window) if (mp.decoded.want_response && !isFromUs(&mp)) { const NodeNum sender = getFrom(&mp); const uint32_t now = mp.rx_time ? mp.rx_time : getTime(); auto it = lastNodeInfoSeen.find(sender); if (it != lastNodeInfoSeen.end()) { uint32_t sinceLast = now >= it->second ? now - it->second : 0; if (sinceLast < NodeInfoReplySuppressSeconds) { suppressReplyForCurrentRequest = true; } } lastNodeInfoSeen[sender] = now; pruneLastNodeInfoCache(); } if (p.is_licensed != owner.is_licensed) { LOG_WARN("Invalid nodeInfo detected, is_licensed mismatch!"); return true; } // Coerce user.id to be derived from the node number snprintf(p.id, sizeof(p.id), "!%08x", getFrom(&mp)); bool hasChanged = nodeDB->updateUser(getFrom(&mp), p, mp.channel); bool wasBroadcast = isBroadcast(mp.to); // LOG_DEBUG("did encode"); // if user has changed while packet was not for us, inform phone if (hasChanged && !wasBroadcast && !isToUs(&mp)) { auto packetCopy = packetPool.allocCopy(mp); // Keep a copy of the packet for later analysis // Re-encode the user protobuf, as we have stripped out the user.id packetCopy->decoded.payload.size = pb_encode_to_bytes( packetCopy->decoded.payload.bytes, sizeof(packetCopy->decoded.payload.bytes), &meshtastic_User_msg, &p); service->sendToPhone(packetCopy); } pruneLastNodeInfoCache(); // LOG_DEBUG("did handleReceived"); return false; // Let others look at this message also if they want } void NodeInfoModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtastic_User *p) { // Coerce user.id to be derived from the node number snprintf(p->id, sizeof(p->id), "!%08x", getFrom(&mp)); // Re-encode the altered protobuf back into the packet mp.decoded.payload.size = pb_encode_to_bytes(mp.decoded.payload.bytes, sizeof(mp.decoded.payload.bytes), &meshtastic_User_msg, p); } void NodeInfoModule::sendOurNodeInfo(NodeNum dest, bool wantReplies, uint8_t channel, bool _shorterTimeout) { // cancel any not yet sent (now stale) position packets if (prevPacketId) // if we wrap around to zero, we'll simply fail to cancel in that rare case (no big deal) service->cancelSending(prevPacketId); shorterTimeout = _shorterTimeout; DEBUG_HEAP_BEFORE; meshtastic_MeshPacket *p = allocReply(); DEBUG_HEAP_AFTER("NodeInfoModule::sendOurNodeInfo", p); if (p) { // Check whether we didn't ignore it p->to = dest; bool requestWantResponse = (config.device.role != meshtastic_Config_DeviceConfig_Role_TRACKER && config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) && wantReplies; p->decoded.want_response = requestWantResponse; if (_shorterTimeout) p->priority = meshtastic_MeshPacket_Priority_DEFAULT; else p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; if (channel > 0) { LOG_DEBUG("Send ourNodeInfo to channel %d", channel); p->channel = channel; } prevPacketId = p->id; service->sendToMesh(p); shorterTimeout = false; } } meshtastic_MeshPacket *NodeInfoModule::allocReply() { // Only apply suppression when actually replying to someone else's request, not for periodic broadcasts. const bool isReplyingToExternalRequest = currentRequest && currentRequest->which_payload_variant == meshtastic_MeshPacket_decoded_tag && currentRequest->decoded.portnum == meshtastic_PortNum_NODEINFO_APP && currentRequest->decoded.want_response && !isFromUs(currentRequest); if (suppressReplyForCurrentRequest && isReplyingToExternalRequest) { LOG_DEBUG("Skip send NodeInfo since we heard the requester <12h ago"); ignoreRequest = true; suppressReplyForCurrentRequest = false; return NULL; } if (!airTime->isTxAllowedChannelUtil(false)) { ignoreRequest = true; // Mark it as ignored for MeshModule LOG_DEBUG("Skip send NodeInfo > 40%% ch. util"); return NULL; } // Use graduated scaling based on active mesh size (10 minute base, scales with congestion coefficient) uint32_t timeoutMs = Default::getConfiguredOrDefaultMsScaled(0, 10 * 60, nodeStatus->getNumOnline()); uint32_t lastNodeInfo = transmitHistory ? transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP) : 0; if (!shorterTimeout && lastNodeInfo && Throttle::isWithinTimespanMs(lastNodeInfo, timeoutMs)) { LOG_DEBUG("Skip send NodeInfo since we sent it <%us ago", timeoutMs / 1000); ignoreRequest = true; // Mark it as ignored for MeshModule return NULL; } else if (shorterTimeout && lastNodeInfo && Throttle::isWithinTimespanMs(lastNodeInfo, 60 * 1000)) { // For interactive/urgent requests (e.g., user-triggered or implicit requests), use a shorter 60s timeout LOG_DEBUG("Skip send NodeInfo since we sent it <60s ago"); ignoreRequest = true; return NULL; } else { ignoreRequest = false; // Don't ignore requests anymore meshtastic_User &u = owner; // Strip the public key if the user is licensed if (u.is_licensed && u.public_key.size > 0) { memset(u.public_key.bytes, 0, sizeof(u.public_key.bytes)); u.public_key.size = 0; } // FIXME: Clear the user.id field since it should be derived from node number on the receiving end // u.id[0] = '\0'; // Ensure our user.id is derived correctly strcpy(u.id, nodeDB->getNodeId().c_str()); LOG_INFO("Send owner %s/%s/%s", u.id, u.long_name, u.short_name); if (transmitHistory) transmitHistory->setLastSentToMesh(meshtastic_PortNum_NODEINFO_APP); return allocDataProtobuf(u); } } void NodeInfoModule::pruneLastNodeInfoCache() { if (!nodeDB || !nodeDB->meshNodes) return; const size_t maxEntries = nodeDB->meshNodes->size(); for (auto it = lastNodeInfoSeen.begin(); it != lastNodeInfoSeen.end();) { if (!nodeDB->getMeshNode(it->first)) { it = lastNodeInfoSeen.erase(it); } else { ++it; } } while (!lastNodeInfoSeen.empty() && lastNodeInfoSeen.size() > maxEntries) { auto oldestIt = std::min_element(lastNodeInfoSeen.begin(), lastNodeInfoSeen.end(), [](const std::pair &lhs, const std::pair &rhs) { return lhs.second < rhs.second; }); lastNodeInfoSeen.erase(oldestIt); } } NodeInfoModule::NodeInfoModule() : ProtobufModule("nodeinfo", meshtastic_PortNum_NODEINFO_APP, &meshtastic_User_msg), concurrency::OSThread("NodeInfo") { isPromiscuous = true; // We always want to update our nodedb, even if we are sniffing on others setIntervalFromNow(setStartDelay()); // Send our initial owner announcement 30 seconds // after we start (to give network time to setup) } int32_t NodeInfoModule::runOnce() { // If we changed channels, ask everyone else for their latest info bool requestReplies = currentGeneration != radioGeneration; currentGeneration = radioGeneration; if (airTime->isTxAllowedAirUtil() && config.device.role != meshtastic_Config_DeviceConfig_Role_CLIENT_HIDDEN) { LOG_INFO("Send our nodeinfo to mesh (wantReplies=%d)", requestReplies); sendOurNodeInfo(NODENUM_BROADCAST, requestReplies); // Send our info (don't request replies) } return Default::getConfiguredOrDefaultMs(config.device.node_info_broadcast_secs, default_node_info_broadcast_secs); } ================================================ FILE: src/modules/NodeInfoModule.h ================================================ #pragma once #include "ProtobufModule.h" #include /** * 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; /** Called to alter received User protobuf */ virtual void alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtastic_User *p) override; /** Messages can be received that have the want_response bit set. If set, this callback will be invoked * so that subclasses can (optionally) send a response back to the original sender. */ virtual meshtastic_MeshPacket *allocReply() override; /** Does our periodic broadcast */ virtual int32_t runOnce() override; private: bool shorterTimeout = false; bool suppressReplyForCurrentRequest = false; std::map lastNodeInfoSeen; void pruneLastNodeInfoCache(); }; extern NodeInfoModule *nodeInfoModule; ================================================ FILE: src/modules/OnScreenKeyboardModule.cpp ================================================ #include "configuration.h" #if HAS_SCREEN #include "graphics/SharedUIDisplay.h" #include "graphics/draw/NotificationRenderer.h" #include "input/RotaryEncoderInterruptImpl1.h" #include "input/UpDownInterruptImpl1.h" #include "modules/OnScreenKeyboardModule.h" #include #include namespace graphics { OnScreenKeyboardModule &OnScreenKeyboardModule::instance() { static OnScreenKeyboardModule inst; return inst; } OnScreenKeyboardModule::~OnScreenKeyboardModule() { if (keyboard) { delete keyboard; keyboard = nullptr; } } void OnScreenKeyboardModule::start(const char *header, const char *initialText, uint32_t durationMs, std::function cb) { if (keyboard) { delete keyboard; keyboard = nullptr; } keyboard = new VirtualKeyboard(); callback = cb; if (header) keyboard->setHeader(header); if (initialText) keyboard->setInputText(initialText); // Route VK submission/cancel events back into the module keyboard->setCallback([this](const std::string &text) { if (text.empty()) { this->onCancel(); } else { this->onSubmit(text); } }); // Maintain legacy compatibility hooks NotificationRenderer::virtualKeyboard = keyboard; NotificationRenderer::textInputCallback = callback; } void OnScreenKeyboardModule::stop(bool callEmptyCallback) { auto cb = callback; callback = nullptr; if (keyboard) { delete keyboard; keyboard = nullptr; } // Keep NotificationRenderer legacy pointers in sync NotificationRenderer::virtualKeyboard = nullptr; NotificationRenderer::textInputCallback = nullptr; clearPopup(); if (callEmptyCallback && cb) cb(""); } void OnScreenKeyboardModule::handleInput(const InputEvent &event) { if (!keyboard) return; if (processVirtualKeyboardInput(event, keyboard)) return; if (event.inputEvent == INPUT_BROKER_CANCEL) onCancel(); } bool OnScreenKeyboardModule::processVirtualKeyboardInput(const InputEvent &event, VirtualKeyboard *targetKeyboard) { if (!targetKeyboard) return false; switch (event.inputEvent) { case INPUT_BROKER_UP: case INPUT_BROKER_UP_LONG: targetKeyboard->moveCursorUp(); return true; case INPUT_BROKER_DOWN: case INPUT_BROKER_DOWN_LONG: targetKeyboard->moveCursorDown(); return true; case INPUT_BROKER_LEFT: case INPUT_BROKER_ALT_PRESS: targetKeyboard->moveCursorLeft(); return true; case INPUT_BROKER_RIGHT: case INPUT_BROKER_USER_PRESS: targetKeyboard->moveCursorRight(); return true; case INPUT_BROKER_SELECT: targetKeyboard->handlePress(); return true; case INPUT_BROKER_SELECT_LONG: targetKeyboard->handleLongPress(); return true; default: return false; } } bool OnScreenKeyboardModule::draw(OLEDDisplay *display) { if (!keyboard) return false; // Timeout if (keyboard->isTimedOut()) { onCancel(); return false; } // Clear full screen behind keyboard display->setColor(BLACK); display->fillRect(0, 0, display->getWidth(), display->getHeight()); display->setColor(WHITE); keyboard->draw(display, 0, 0); // Draw popup overlay if needed drawPopup(display); return true; } void OnScreenKeyboardModule::onSubmit(const std::string &text) { auto cb = callback; stop(false); if (cb) cb(text); } void OnScreenKeyboardModule::onCancel() { stop(true); } void OnScreenKeyboardModule::showPopup(const char *title, const char *content, uint32_t durationMs) { if (!title || !content) return; strncpy(popupTitle, title, sizeof(popupTitle) - 1); popupTitle[sizeof(popupTitle) - 1] = '\0'; strncpy(popupMessage, content, sizeof(popupMessage) - 1); popupMessage[sizeof(popupMessage) - 1] = '\0'; popupUntil = millis() + durationMs; popupVisible = true; } void OnScreenKeyboardModule::clearPopup() { popupTitle[0] = '\0'; popupMessage[0] = '\0'; popupUntil = 0; popupVisible = false; } void OnScreenKeyboardModule::drawPopupOverlay(OLEDDisplay *display) { // Only render the popup overlay (without drawing the keyboard) drawPopup(display); } void OnScreenKeyboardModule::drawPopup(OLEDDisplay *display) { if (!popupVisible) return; if (millis() > popupUntil || popupMessage[0] == '\0') { popupVisible = false; return; } // Build lines and leverage NotificationRenderer inverted box drawing for consistent style constexpr uint16_t maxContentLines = 3; const bool hasTitle = popupTitle[0] != '\0'; display->setFont(FONT_SMALL); display->setTextAlignment(TEXT_ALIGN_LEFT); const uint16_t maxWrapWidth = display->width() - 40; auto wrapText = [&](const char *text, uint16_t availableWidth) -> std::vector { std::vector wrapped; std::string current; std::string word; const char *p = text; while (*p && wrapped.size() < maxContentLines) { while (*p && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')) { if (*p == '\n') { if (!current.empty()) { wrapped.push_back(current); current.clear(); if (wrapped.size() >= maxContentLines) break; } } ++p; } if (!*p || wrapped.size() >= maxContentLines) break; word.clear(); while (*p && *p != ' ' && *p != '\t' && *p != '\n' && *p != '\r') word += *p++; if (word.empty()) continue; std::string test = current.empty() ? word : (current + " " + word); uint16_t w = display->getStringWidth(test.c_str(), test.length(), true); if (w <= availableWidth) current = test; else { if (!current.empty()) { wrapped.push_back(current); current = word; if (wrapped.size() >= maxContentLines) break; } else { current = word; while (current.size() > 1 && display->getStringWidth(current.c_str(), current.length(), true) > availableWidth) current.pop_back(); } } } if (!current.empty() && wrapped.size() < maxContentLines) wrapped.push_back(current); return wrapped; }; std::vector allLines; if (hasTitle) allLines.emplace_back(popupTitle); char buf[sizeof(popupMessage)]; strncpy(buf, popupMessage, sizeof(buf) - 1); buf[sizeof(buf) - 1] = '\0'; char *paragraph = strtok(buf, "\n"); while (paragraph && allLines.size() < maxContentLines + (hasTitle ? 1 : 0)) { auto wrapped = wrapText(paragraph, maxWrapWidth); for (const auto &ln : wrapped) { if (allLines.size() >= maxContentLines + (hasTitle ? 1 : 0)) break; allLines.push_back(ln); } paragraph = strtok(nullptr, "\n"); } std::vector ptrs; for (const auto &ln : allLines) ptrs.push_back(ln.c_str()); ptrs.push_back(nullptr); // Use the standard notification box drawing from NotificationRenderer NotificationRenderer::drawNotificationBox(display, nullptr, ptrs.data(), allLines.size(), 0, 0); } } // namespace graphics #endif // HAS_SCREEN ================================================ FILE: src/modules/OnScreenKeyboardModule.h ================================================ #pragma once #include "configuration.h" #if HAS_SCREEN #include "graphics/Screen.h" // InputEvent #include "graphics/VirtualKeyboard.h" #include #include #include namespace graphics { class OnScreenKeyboardModule { public: static OnScreenKeyboardModule &instance(); void start(const char *header, const char *initialText, uint32_t durationMs, std::function callback); void stop(bool callEmptyCallback); void handleInput(const InputEvent &event); static bool processVirtualKeyboardInput(const InputEvent &event, VirtualKeyboard *keyboard); bool draw(OLEDDisplay *display); void showPopup(const char *title, const char *content, uint32_t durationMs); void clearPopup(); // Draw only the popup overlay (used when legacy virtualKeyboard draws the keyboard) void drawPopupOverlay(OLEDDisplay *display); private: OnScreenKeyboardModule() = default; ~OnScreenKeyboardModule(); OnScreenKeyboardModule(const OnScreenKeyboardModule &) = delete; OnScreenKeyboardModule &operator=(const OnScreenKeyboardModule &) = delete; void onSubmit(const std::string &text); void onCancel(); void drawPopup(OLEDDisplay *display); VirtualKeyboard *keyboard = nullptr; std::function callback; char popupTitle[64] = {0}; char popupMessage[256] = {0}; uint32_t popupUntil = 0; bool popupVisible = false; }; } // namespace graphics #endif // HAS_SCREEN ================================================ FILE: src/modules/PositionModule.cpp ================================================ #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 "TransmitHistory.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" #include PositionModule *positionModule; PositionModule::PositionModule() : ProtobufModule("position", meshtastic_PortNum_POSITION_APP, &meshtastic_Position_msg), concurrency::OSThread("Position") { precision = 0; // safe starting value isPromiscuous = true; // We always want to update our nodedb, even if we are sniffing on others nodeStatusObserver.observe(&nodeStatus->onNewStatus); // Seed throttle timer from persisted transmit history so we don't re-broadcast immediately after reboot if (transmitHistory) { uint32_t restored = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_POSITION_APP); if (restored != 0) { lastGpsSend = restored; LOG_INFO("Position: restored lastGpsSend from transmit history"); } } if (config.device.role != meshtastic_Config_DeviceConfig_Role_TRACKER && config.device.role != meshtastic_Config_DeviceConfig_Role_TAK_TRACKER) { setIntervalFromNow(setStartDelay()); } // Power saving trackers should clear their position on startup to avoid waking up and sending a stale position if ((config.device.role == meshtastic_Config_DeviceConfig_Role_TRACKER || config.device.role == meshtastic_Config_DeviceConfig_Role_TAK_TRACKER) && config.power.is_power_saving) { LOG_DEBUG("Clear position on startup for sleepy tracker (ー。ー) zzz"); nodeDB->clearLocalPosition(); } } bool PositionModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Position *pptr) { auto p = *pptr; const auto transport = mp.transport_mechanism; if (isFromUs(&mp) && !IS_ONE_OF(transport, meshtastic_MeshPacket_TransportMechanism_TRANSPORT_INTERNAL, meshtastic_MeshPacket_TransportMechanism_TRANSPORT_API)) { LOG_WARN("Ignoring packet supposedly from us over external transport"); return true; } // FIXME this can in fact happen with packets sent from EUD (src=RX_SRC_USER) // to set fixed location, EUD-GPS location or just the time (see also issue #900) bool isLocal = false; if (isFromUs(&mp)) { isLocal = true; if (config.position.fixed_position) { LOG_DEBUG("Ignore incoming position update from myself except for time, because position.fixed_position is true"); #ifdef T_WATCH_S3 // Since we return early if position.fixed_position is true, set the T-Watch's RTC to the time received from the // client device here if (p.time && channels.getByIndex(mp.channel).role == meshtastic_Channel_Role_PRIMARY) { trySetRtc(p, isLocal, true); } #endif nodeDB->setLocalPosition(p, true); return false; } else { LOG_DEBUG("Incoming update from MYSELF"); nodeDB->setLocalPosition(p); } } // 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("Truncate phone position to channel precision %i", precision); p->latitude_i = p->latitude_i & (UINT32_MAX << (32 - precision)); p->longitude_i = p->longitude_i & (UINT32_MAX << (32 - precision)); // We want the imprecise position to be the middle of the possible location, not p->latitude_i += (1 << (31 - precision)); p->longitude_i += (1 << (31 - precision)); mp.decoded.payload.size = pb_encode_to_bytes(mp.decoded.payload.bytes, sizeof(mp.decoded.payload.bytes), &meshtastic_Position_msg, p); } } void PositionModule::trySetRtc(meshtastic_Position p, bool isLocal, bool forceUpdate) { if (hasQualityTimesource() && !isLocal) { LOG_DEBUG("Ignore time from mesh because we have a GPS, RTC, or Phone/NTP time source in the past day"); return; } if (!isLocal && p.location_source < meshtastic_Position_LocSource_LOC_INTERNAL) { LOG_DEBUG("Ignore time from mesh because it has a unknown or manual source"); return; } struct timeval tv; uint32_t secs = p.time; tv.tv_sec = secs; tv.tv_usec = 0; perhapsSetRTC(isLocal ? RTCQualityNTP : RTCQualityFromNet, &tv, forceUpdate); } bool PositionModule::hasQualityTimesource() { bool setFromPhoneOrNtpToday = lastSetFromPhoneNtpOrGps == 0 ? false : Throttle::isWithinTimespanMs(lastSetFromPhoneNtpOrGps, SEC_PER_DAY * 1000UL); #if MESHTASTIC_EXCLUDE_GPS bool hasGpsOrRtc = (rtc_found.address != ScanI2C::ADDRESS_NONE.address); #else bool hasGpsOrRtc = hasGPS() || (rtc_found.address != ScanI2C::ADDRESS_NONE.address); #endif return hasGpsOrRtc || setFromPhoneOrNtpToday; } bool PositionModule::hasGPS() { #if MESHTASTIC_EXCLUDE_GPS return false; #else return gps && gps->isConnected(); #endif } // Allocate a packet with our position data if we have one meshtastic_MeshPacket *PositionModule::allocPositionPacket() { if (precision == 0) { LOG_DEBUG("Skip location send because precision is set to 0!"); return nullptr; } meshtastic_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("Skip position send because lat/lon are zero!"); return nullptr; } // lat/lon are unconditionally included - IF AVAILABLE! LOG_DEBUG("Send location with precision %i", precision); if (precision < 32 && precision > 0) { p.latitude_i = localPosition.latitude_i & (UINT32_MAX << (32 - precision)); p.longitude_i = localPosition.longitude_i & (UINT32_MAX << (32 - precision)); // We want the imprecise position to be the middle of the possible location, not p.latitude_i += (1 << (31 - precision)); p.longitude_i += (1 << (31 - precision)); } else { p.latitude_i = localPosition.latitude_i; p.longitude_i = localPosition.longitude_i; } p.precision_bits = precision; p.has_latitude_i = true; p.has_longitude_i = true; // Always use NTP / GPS time if available if (getValidTime(RTCQualityNTP) > 0) { p.time = getValidTime(RTCQualityNTP); } else if (rtc_found.address != ScanI2C::ADDRESS_NONE.address) { LOG_INFO("Use RTC time for position"); p.time = getValidTime(RTCQualityDevice); } else if (getRTCQuality() < RTCQualityNTP) { LOG_INFO("Strip low RTCQuality (%d) time from position", getRTCQuality()); p.time = 0; } if (config.position.fixed_position) { p.location_source = meshtastic_Position_LocSource_LOC_MANUAL; } else { p.location_source = localPosition.location_source; } if (pos_flags & meshtastic_Config_PositionConfig_PositionFlags_ALTITUDE) { if (pos_flags & meshtastic_Config_PositionConfig_PositionFlags_ALTITUDE_MSL) { p.altitude = localPosition.altitude; p.has_altitude = true; } else { p.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; } LOG_INFO("Position packet: time=%i lat=%i lon=%i", p.time, p.latitude_i, p.longitude_i); #ifndef MESHTASTIC_EXCLUDE_ATAK // TAK Tracker devices should send their position in a TAK packet over the ATAK port if (config.device.role == meshtastic_Config_DeviceConfig_Role_TAK_TRACKER) return allocAtakPli(); #endif return allocDataProtobuf(p); } meshtastic_MeshPacket *PositionModule::allocReply() { if (config.device.role != meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND && lastSentReply && Throttle::isWithinTimespanMs(lastSentReply, 3 * 60 * 1000)) { LOG_DEBUG("Skip Position reply since we sent a reply <3min ago"); ignoreRequest = true; // Mark it as ignored for MeshModule return nullptr; } meshtastic_MeshPacket *reply = allocPositionPacket(); if (reply) { lastSentReply = millis(); // Track when we sent this reply } return reply; } meshtastic_MeshPacket *PositionModule::allocAtakPli() { LOG_INFO("Send TAK PLI packet"); meshtastic_MeshPacket *mp = allocDataPacket(); mp->decoded.portnum = meshtastic_PortNum_ATAK_PLUGIN; meshtastic_TAKPacket takPacket = {.is_compressed = true, .has_contact = true, .contact = meshtastic_Contact_init_default, .has_group = true, .group = {meshtastic_MemberRole_TeamMember, meshtastic_Team_Cyan}, .has_status = true, .status = { .battery = powerStatus->getBatteryChargePercent(), }, .which_payload_variant = meshtastic_TAKPacket_pli_tag, .payload_variant = {.pli = { .latitude_i = localPosition.latitude_i, .longitude_i = localPosition.longitude_i, .altitude = localPosition.altitude_hae, .speed = localPosition.ground_speed, .course = static_cast(localPosition.ground_track), }}}; auto length = unishox2_compress_lines(owner.long_name, strlen(owner.long_name), takPacket.contact.device_callsign, sizeof(takPacket.contact.device_callsign) - 1, USX_PSET_DFLT, NULL); LOG_DEBUG("Uncompressed device_callsign '%s' - %d bytes", owner.long_name, strlen(owner.long_name)); LOG_DEBUG("Compressed device_callsign '%s' - %d bytes", takPacket.contact.device_callsign, length); length = unishox2_compress_lines(owner.long_name, strlen(owner.long_name), takPacket.contact.callsign, sizeof(takPacket.contact.callsign) - 1, USX_PSET_DFLT, NULL); mp->decoded.payload.size = pb_encode_to_bytes(mp->decoded.payload.bytes, sizeof(mp->decoded.payload.bytes), &meshtastic_TAKPacket_msg, &takPacket); return mp; } void PositionModule::sendOurPosition() { bool requestReplies = currentGeneration != radioGeneration; currentGeneration = radioGeneration; // If we changed channels, ask everyone else for their latest info LOG_INFO("Send pos@%x:6 to mesh (wantReplies=%d)", localPosition.timestamp, requestReplies); for (uint8_t channelNum = 0; channelNum < 8; channelNum++) { if (channels.getByIndex(channelNum).settings.has_module_settings && channels.getByIndex(channelNum).settings.module_settings.position_precision != 0) { sendOurPosition(NODENUM_BROADCAST, requestReplies, channelNum); return; } } } void PositionModule::sendOurPosition(NodeNum dest, bool wantReplies, uint8_t channel) { if (!config.position.fixed_position && !nodeDB->hasLocalPositionSinceBoot()) { LOG_DEBUG("Skip position send; no fresh position since boot"); return; } // cancel any not yet sent (now stale) position packets if (prevPacketId) // if we wrap around to zero, we'll simply fail to cancel in that rare case (no big deal) service->cancelSending(prevPacketId); // Set's the class precision value for this particular packet if (channels.getByIndex(channel).settings.has_module_settings) { precision = channels.getByIndex(channel).settings.module_settings.position_precision; } meshtastic_MeshPacket *p = allocPositionPacket(); if (p == nullptr) { LOG_DEBUG("allocPositionPacket returned a nullptr"); return; } p->to = dest; p->decoded.want_response = config.device.role == meshtastic_Config_DeviceConfig_Role_TRACKER ? false : wantReplies; if (config.device.role == meshtastic_Config_DeviceConfig_Role_TRACKER || config.device.role == meshtastic_Config_DeviceConfig_Role_TAK_TRACKER) p->priority = meshtastic_MeshPacket_Priority_RELIABLE; else p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; prevPacketId = p->id; if (channel > 0) p->channel = channel; service->sendToMesh(p, RX_SRC_LOCAL, true); if (IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_TRACKER, meshtastic_Config_DeviceConfig_Role_TAK_TRACKER) && config.power.is_power_saving) { meshtastic_ClientNotification *notification = clientNotificationPool.allocZeroed(); notification->level = meshtastic_LogRecord_Level_INFO; notification->time = getValidTime(RTCQualityFromNet); sprintf(notification->message, "Sending position and sleeping for %us interval in a moment", Default::getConfiguredOrDefaultMs(config.position.position_broadcast_secs, default_broadcast_interval_secs) / 1000U); service->sendClientNotification(notification); sleepOnNextExecution = true; LOG_DEBUG("Start next execution in 5s, then sleep"); setIntervalFromNow(FIVE_SECONDS_MS); } } #define RUNONCE_INTERVAL 5000; int32_t PositionModule::runOnce() { if (sleepOnNextExecution == true) { sleepOnNextExecution = false; uint32_t nightyNightMs = Default::getConfiguredOrDefaultMs(config.position.position_broadcast_secs); LOG_DEBUG("Sleep for %ims, then awaking to send position again", nightyNightMs); doDeepSleep(nightyNightMs, false, false); } 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; } bool waitingForFreshPosition = (lastGpsSend == 0) && !config.position.fixed_position && !nodeDB->hasLocalPositionSinceBoot(); if (lastGpsSend == 0 || msSinceLastSend >= intervalMs) { if (waitingForFreshPosition) { #ifdef GPS_DEBUG LOG_DEBUG("Skip initial position send; no fresh position since boot"); #endif } else if (nodeDB->hasValidPosition(node)) { lastGpsSend = now; lastGpsLatitude = node->position.latitude_i; lastGpsLongitude = node->position.longitude_i; if (transmitHistory) transmitHistory->setLastSentToMesh(meshtastic_PortNum_POSITION_APP); sendOurPosition(); if (config.device.role == meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND) { sendLostAndFoundText(); } } } else if (config.position.position_broadcast_smart_enabled) { const meshtastic_NodeInfoLite *node2 = service->refreshLocalMeshNode(); // should guarantee there is now a position if (nodeDB->hasValidPosition(node2)) { // The minimum time (in seconds) that would pass before we are able to send a new position packet. auto smartPosition = getDistanceTraveledSinceLastSend(node->position); msSinceLastSend = now - lastGpsSend; if (smartPosition.hasTraveledOverThreshold && Throttle::execute( &lastGpsSend, minimumTimeThreshold, []() { positionModule->sendOurPosition(); }, []() { #ifdef GPS_DEBUG LOG_DEBUG("Skip send smart broadcast due to time throttling"); #endif })) { 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; } // Helper: return imprecise (truncated + centered) lat/lon as int32 using current precision static inline void computeImpreciseLatLon(int32_t inLat, int32_t inLon, uint8_t precisionBits, int32_t &outLat, int32_t &outLon) { if (precisionBits > 0 && precisionBits < 32) { // Build mask for top 'precisionBits' bits of a 32-bit unsigned field const uint32_t mask = (precisionBits == 32) ? UINT32_MAX : (UINT32_MAX << (32 - precisionBits)); // Note: latitude_i/longitude_i are stored as signed 32-bit in meshtastic code but // the bitmask logic used previously operated as unsigned—preserve that behavior by // casting to uint32_t for masking, then back to int32_t. uint32_t lat_u = static_cast(inLat) & mask; uint32_t lon_u = static_cast(inLon) & mask; // Add the "center of cell" offset used elsewhere: // The code previously added (1 << (31 - precision)) to produce the middle of the possible location. uint32_t center_offset = (1u << (31 - precisionBits)); lat_u += center_offset; lon_u += center_offset; outLat = static_cast(lat_u); outLon = static_cast(lon_u); } else { // full precision: return input unchanged outLat = inLat; outLon = inLon; } } struct SmartPosition PositionModule::getDistanceTraveledSinceLastSend(meshtastic_PositionLite currentPosition) { const uint32_t distanceTravelThreshold = Default::getConfiguredOrDefault(config.position.broadcast_smart_minimum_distance, 100); int32_t lastLatImprecise, lastLonImprecise; int32_t currentLatImprecise, currentLonImprecise; computeImpreciseLatLon(lastGpsLatitude, lastGpsLongitude, precision, lastLatImprecise, lastLonImprecise); computeImpreciseLatLon(currentPosition.latitude_i, currentPosition.longitude_i, precision, currentLatImprecise, currentLonImprecise); float distMeters = GeoCoord::latLongToMeter(lastLatImprecise * 1e-7, lastLonImprecise * 1e-7, currentLatImprecise * 1e-7, currentLonImprecise * 1e-7); float distanceTraveled = fabsf(distMeters); return SmartPosition{.distanceTraveled = distanceTraveled, .distanceThreshold = distanceTravelThreshold, .hasTraveledOverThreshold = distanceTraveled >= distanceTravelThreshold}; } void PositionModule::handleNewPosition() { meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum()); const meshtastic_NodeInfoLite *node2 = service->refreshLocalMeshNode(); // should guarantee there is now a position // We limit our GPS broadcasts to a max rate if (nodeDB->hasValidPosition(node2)) { auto smartPosition = getDistanceTraveledSinceLastSend(node->position); uint32_t msSinceLastSend = millis() - lastGpsSend; if (smartPosition.hasTraveledOverThreshold && Throttle::execute( &lastGpsSend, minimumTimeThreshold, []() { positionModule->sendOurPosition(); }, []() { #ifdef GPS_DEBUG LOG_DEBUG("Skip send smart broadcast due to time throttling"); #endif })) { 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 ================================================ FILE: src/modules/PositionModule.h ================================================ #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: meshtastic_MeshPacket *allocPositionPacket(); struct SmartPosition getDistanceTraveledSinceLastSend(meshtastic_PositionLite currentPosition); meshtastic_MeshPacket *allocAtakPli(); void trySetRtc(meshtastic_Position p, bool isLocal, bool forceUpdate = false); uint32_t precision; void sendLostAndFoundText(); bool hasQualityTimesource(); bool hasGPS(); uint32_t lastSentReply = 0; // Last time we sent a position reply (used for reply throttling only) #if USERPREFS_EVENT_MODE // In event mode we want to prevent excessive position broadcasts // we set the minimum interval to 5m const uint32_t minimumTimeThreshold = max(uint32_t(300000), Default::getConfiguredOrDefaultMs(config.position.broadcast_smart_minimum_interval_secs, default_broadcast_smart_minimum_interval_secs)); #else const uint32_t minimumTimeThreshold = Default::getConfiguredOrDefaultMs(config.position.broadcast_smart_minimum_interval_secs, default_broadcast_smart_minimum_interval_secs); #endif }; struct SmartPosition { float distanceTraveled; uint32_t distanceThreshold; bool hasTraveledOverThreshold; }; extern PositionModule *positionModule; ================================================ FILE: src/modules/PowerStressModule.cpp ================================================ #include "PowerStressModule.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("PowerStress") { } bool PowerStressModule::handleReceivedProtobuf(const meshtastic_MeshPacket &req, meshtastic_PowerStressMessage *pptr) { // We only respond to messages if powermon debugging is already on if (config.power.powermon_enables) { auto p = *pptr; LOG_INFO("Received PowerStress cmd=%d", p.cmd); // Some commands we can handle immediately, anything else gets deferred to be handled by our thread switch (p.cmd) { case meshtastic_PowerStressMessage_Opcode_UNSET: LOG_ERROR("PowerStress operation unset"); break; case meshtastic_PowerStressMessage_Opcode_PRINT_INFO: printInfo(); // Now that we know we are actually doing power stress testing, go ahead and turn on all enables (so the log is fully // detailed) powerMon->force_enabled = true; break; default: if (currentMessage.cmd != meshtastic_PowerStressMessage_Opcode_UNSET) LOG_ERROR("PowerStress operation %d already in progress! Can't start new command", currentMessage.cmd); else currentMessage = p; // copy for use by thread (the message provided to us will be getting freed) break; } } return true; } 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: // FIXME - implement // ledForceOn.set(true); break; case meshtastic_PowerStressMessage_Opcode_LED_OFF: // FIXME - implement // ledForceOn.set(false); break; case meshtastic_PowerStressMessage_Opcode_GPS_ON: // FIXME - implement break; case meshtastic_PowerStressMessage_Opcode_GPS_OFF: // FIXME - implement break; case meshtastic_PowerStressMessage_Opcode_LORA_OFF: // FIXME - implement break; case meshtastic_PowerStressMessage_Opcode_LORA_RX: // FIXME - implement break; case meshtastic_PowerStressMessage_Opcode_LORA_TX: // FIXME - implement break; case meshtastic_PowerStressMessage_Opcode_SCREEN_OFF: // FIXME - implement break; case meshtastic_PowerStressMessage_Opcode_SCREEN_ON: // FIXME - implement break; case meshtastic_PowerStressMessage_Opcode_BT_OFF: setBluetoothEnable(false); break; case meshtastic_PowerStressMessage_Opcode_BT_ON: setBluetoothEnable(true); break; case meshtastic_PowerStressMessage_Opcode_CPU_DEEPSLEEP: doDeepSleep(sleep_msec, true, true); break; case meshtastic_PowerStressMessage_Opcode_CPU_FULLON: { uint32_t start_msec = millis(); while (Throttle::isWithinTimespanMs(start_msec, sleep_msec)) ; // Don't let CPU idle at all sleep_msec = 0; // we already slept break; } case meshtastic_PowerStressMessage_Opcode_CPU_IDLE: // FIXME - implement break; default: LOG_ERROR("PowerStress operation %d not yet implemented!", p.cmd); sleep_msec = 0; // Don't do whatever sleep was requested... break; } } } return sleep_msec; } ================================================ FILE: src/modules/PowerStressModule.h ================================================ #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; ================================================ FILE: src/modules/RangeTestModule.cpp ================================================ /** * @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 "SPILock.h" #include "airtime.h" #include "configuration.h" #include "gps/GeoCoord.h" #include #include RangeTestModule *rangeTestModule; RangeTestModuleRadio *rangeTestModuleRadio; RangeTestModule::RangeTestModule() : concurrency::OSThread("RangeTest") {} uint32_t packetSequence = 0; int32_t RangeTestModule::runOnce() { #if defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_STM32WL) || defined(ARCH_PORTDUINO) /* Uncomment the preferences below if you want to use the module without having to configure it from the PythonAPI or WebUI. */ // moduleConfig.range_test.enabled = 1; // moduleConfig.range_test.sender = 30; // moduleConfig.range_test.save = 1; // moduleConfig.range_test.clear_on_reboot = 1; // Fixed position is useful when testing indoors. // config.position.fixed_position = 1; uint32_t senderHeartbeat = moduleConfig.range_test.sender * 1000; if (moduleConfig.range_test.enabled) { if (firstTime) { rangeTestModuleRadio = new RangeTestModuleRadio(); firstTime = 0; if (moduleConfig.range_test.clear_on_reboot) { // User wants to delete previous range test(s) LOG_INFO("Range Test Module - Clearing out previous test file"); rangeTestModuleRadio->removeFile(); } if (moduleConfig.range_test.sender) { LOG_INFO("Init Range Test Module -- Sender"); started = millis(); // make a note of when we started return (5000); // Sending first message 5 seconds after initialization. } else { LOG_INFO("Init Range Test Module -- Receiver"); return disable(); // This thread does not need to run as a receiver } } else { if (moduleConfig.range_test.sender) { // If sender LOG_INFO("Range Test Module - Sending heartbeat every %d ms", (senderHeartbeat)); LOG_INFO("gpsStatus->getLatitude() %d", gpsStatus->getLatitude()); LOG_INFO("gpsStatus->getLongitude() %d", gpsStatus->getLongitude()); LOG_INFO("gpsStatus->getHasLock() %d", gpsStatus->getHasLock()); LOG_INFO("gpsStatus->getDOP() %d", gpsStatus->getDOP()); LOG_INFO("fixed_position() %d", config.position.fixed_position); // Only send packets if the channel is less than 25% utilized. if (airTime->isTxAllowedChannelUtil(true)) { rangeTestModuleRadio->sendPayload(); } // If we have been running for more than 8 hours, turn module back off if (!Throttle::isWithinTimespanMs(started, 28800000)) { LOG_INFO("Range Test Module - Disable after 8 hours"); return disable(); } else { return (senderHeartbeat); } } else { return disable(); // This thread does not need to run as a receiver } } } 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) || defined(ARCH_STM32WL) || defined(ARCH_PORTDUINO) if (moduleConfig.range_test.enabled) { /* auto &p = mp.decoded; LOG_DEBUG("Received text msg self=0x%0x, from=0x%0x, to=0x%0x, id=%d, msg=%.*s", LOG_INFO.getNodeNum(), mp.from, mp.to, mp.id, p.payload.size, p.payload.bytes); */ if (!isFromUs(&mp)) { if (moduleConfig.range_test.save) { appendFile(mp); } /* NodeInfoLite *n = nodeDB->getMeshNode(getFrom(&mp)); LOG_DEBUG("-----------------------------------------"); LOG_DEBUG("p.payload.bytes \"%s\"", p.payload.bytes); LOG_DEBUG("p.payload.size %d", p.payload.size); LOG_DEBUG("---- Received Packet:"); LOG_DEBUG("mp.from %d", mp.from); LOG_DEBUG("mp.rx_snr %f", mp.rx_snr); LOG_DEBUG("mp.rx_rssi %f", mp.rx_rssi); LOG_DEBUG("mp.hop_limit %d", mp.hop_limit); LOG_DEBUG("---- Node Information of Received Packet (mp.from):"); LOG_DEBUG("n->user.long_name %s", n->user.long_name); LOG_DEBUG("n->user.short_name %s", n->user.short_name); LOG_DEBUG("n->has_position %d", n->has_position); LOG_DEBUG("n->position.latitude_i %d", n->position.latitude_i); LOG_DEBUG("n->position.longitude_i %d", n->position.longitude_i); LOG_DEBUG("---- Current device location information:"); LOG_DEBUG("gpsStatus->getLatitude() %d", gpsStatus->getLatitude()); LOG_DEBUG("gpsStatus->getLongitude() %d", gpsStatus->getLongitude()); LOG_DEBUG("gpsStatus->getHasLock() %d", gpsStatus->getHasLock()); LOG_DEBUG("gpsStatus->getDOP() %d", gpsStatus->getDOP()); LOG_DEBUG("-----------------------------------------"); */ } } else { LOG_INFO("Range Test Module Disabled"); } #endif return ProcessMessage::CONTINUE; // Let others look at this message also if they want } bool RangeTestModuleRadio::appendFile(const meshtastic_MeshPacket &mp) { #ifdef ARCH_ESP32 auto &p = mp.decoded; meshtastic_NodeInfoLite *n = nodeDB->getMeshNode(getFrom(&mp)); /* LOG_DEBUG("-----------------------------------------"); LOG_DEBUG("p.payload.bytes \"%s\"", p.payload.bytes); LOG_DEBUG("p.payload.size %d", p.payload.size); LOG_DEBUG("---- Received Packet:"); LOG_DEBUG("mp.from %d", mp.from); LOG_DEBUG("mp.rx_snr %f", mp.rx_snr); LOG_DEBUG("mp.hop_limit %d", mp.hop_limit); LOG_DEBUG("---- Node Information of Received Packet (mp.from):"); LOG_DEBUG("n->user.long_name %s", n->user.long_name); LOG_DEBUG("n->user.short_name %s", n->user.short_name); LOG_DEBUG("n->has_position %d", n->has_position); LOG_DEBUG("n->position.latitude_i %d", n->position.latitude_i); LOG_DEBUG("n->position.longitude_i %d", n->position.longitude_i); LOG_DEBUG("---- Current device location information:"); LOG_DEBUG("gpsStatus->getLatitude() %d", gpsStatus->getLatitude()); LOG_DEBUG("gpsStatus->getLongitude() %d", gpsStatus->getLongitude()); LOG_DEBUG("gpsStatus->getHasLock() %d", gpsStatus->getHasLock()); LOG_DEBUG("gpsStatus->getDOP() %d", gpsStatus->getDOP()); LOG_DEBUG("-----------------------------------------"); */ concurrency::LockGuard g(spiLock); if (!FSBegin()) { LOG_DEBUG("An Error has occurred while mounting the filesystem"); return 0; } if (FSCom.totalBytes() - FSCom.usedBytes() < 51200) { LOG_DEBUG("Filesystem doesn't have enough free space. Aborting write"); return 0; } FSCom.mkdir("/static"); // If the file doesn't exist, write the header. if (!FSCom.exists("/static/rangetest.csv")) { //--------- Write to file File fileToWrite = FSCom.open("/static/rangetest.csv", FILE_WRITE); if (!fileToWrite) { LOG_ERROR("There was an error opening the file for writing"); return 0; } // Print the CSV header if (fileToWrite.println("time,from,sender name,sender lat,sender long,rx lat,rx long,rx elevation,rx " "snr,distance,hop limit,payload,rx rssi")) { LOG_INFO("File was written"); } 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 if (gpsStatus->getIsConnected() || config.position.fixed_position) { fileToAppend.printf("%f,", gpsStatus->getLatitude() * 1e-7); // RX Lat fileToAppend.printf("%f,", gpsStatus->getLongitude() * 1e-7); // RX Long fileToAppend.printf("%d,", gpsStatus->getAltitude()); // RX Altitude } else { // When the phone API is in use, the node info will be updated with position meshtastic_NodeInfoLite *us = nodeDB->getMeshNode(nodeDB->getNodeNum()); fileToAppend.printf("%f,", us->position.latitude_i * 1e-7); // RX Lat fileToAppend.printf("%f,", us->position.longitude_i * 1e-7); // RX Long fileToAppend.printf("%d,", us->position.altitude); // RX Altitude } fileToAppend.printf("%f,", mp.rx_snr); // RX SNR if (n->position.latitude_i && n->position.longitude_i && gpsStatus->getLatitude() && gpsStatus->getLongitude()) { float distance = GeoCoord::latLongToMeter(n->position.latitude_i * 1e-7, n->position.longitude_i * 1e-7, gpsStatus->getLatitude() * 1e-7, gpsStatus->getLongitude() * 1e-7); fileToAppend.printf("%f,", distance); // Distance in meters } else { fileToAppend.printf("0,"); } fileToAppend.printf("%d,", mp.hop_limit); // Packet Hop Limit // TODO: If quotes are found in the payload, it has to be escaped. fileToAppend.printf("\"%s\"\n", p.payload.bytes); fileToAppend.printf("%i,", mp.rx_rssi); // RX RSSI fileToAppend.flush(); fileToAppend.close(); return 1; #else LOG_ERROR("Failed to store range test results - feature only available for ESP32"); return 0; #endif } bool RangeTestModuleRadio::removeFile() { #ifdef ARCH_ESP32 if (!FSBegin()) { LOG_DEBUG("An Error has occurred while mounting the filesystem"); return 0; } if (!FSCom.exists("/static/rangetest.csv")) { LOG_DEBUG("No range tests found."); return 0; } LOG_INFO("Deleting previous range test."); bool result = FSCom.remove("/static/rangetest.csv"); if (!result) { LOG_ERROR("Failed to delete range test."); return 0; } LOG_INFO("Range test removed."); return 1; #else LOG_ERROR("Failed to remove range test results - feature only available for ESP32"); return 0; #endif } ================================================ FILE: src/modules/RangeTestModule.h ================================================ #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); /** * Cleanup range test data from filesystem */ bool removeFile(); 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; ================================================ FILE: src/modules/RemoteHardwareModule.cpp ================================================ #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("RemoteHardware") { // restrict to the gpio channel for rx boundChannel = Channels::gpioChannel; // Pull available pin allowlist from config and build a bitmask out of it for fast comparisons later for (uint8_t i = 0; i < 4; i++) { availablePins += 1ULL << moduleConfig.remote_hardware.available_pins[i].gpio_pin; } } bool RemoteHardwareModule::handleReceivedProtobuf(const meshtastic_MeshPacket &req, meshtastic_HardwareMessage *pptr) { if (moduleConfig.remote_hardware.enabled) { auto p = *pptr; LOG_INFO("Received RemoteHardware type=%d", p.type); switch (p.type) { case meshtastic_HardwareMessage_Type_WRITE_GPIOS: { pinModes(p.gpio_mask, OUTPUT, availablePins); for (uint8_t i = 0; i < NUM_GPIOS; i++) { uint64_t mask = 1ULL << i; if (p.gpio_mask & mask && pinAccessAllowed(availablePins, i)) { digitalWrite(i, (p.gpio_value & mask) ? 1 : 0); } } break; } case meshtastic_HardwareMessage_Type_READ_GPIOS: { uint64_t res = digitalReads(p.gpio_mask, availablePins); // Send the reply meshtastic_HardwareMessage r = meshtastic_HardwareMessage_init_default; r.type = meshtastic_HardwareMessage_Type_READ_GPIOS_REPLY; r.gpio_value = res; r.gpio_mask = p.gpio_mask; meshtastic_MeshPacket *p2 = allocDataProtobuf(r); setReplyTo(p2, req); myReply = p2; break; } case meshtastic_HardwareMessage_Type_WATCH_GPIOS: { watchGpios = p.gpio_mask; lastWatchMsec = 0; // Force a new publish soon previousWatch = ~watchGpios; // generate a 'previous' value which is guaranteed to not match (to force an initial publish) enabled = true; // Let our thread run at least once setInterval(2000); // Set a new interval so we'll run soon LOG_INFO("Now watching GPIOs 0x%llx", watchGpios); break; } case meshtastic_HardwareMessage_Type_READ_GPIOS_REPLY: case meshtastic_HardwareMessage_Type_GPIOS_CHANGED: break; // Ignore - we might see our own replies default: LOG_ERROR("Hardware operation %d not yet implemented! FIXME", p.type); break; } } return false; } int32_t RemoteHardwareModule::runOnce() { if (moduleConfig.remote_hardware.enabled && watchGpios) { if (!Throttle::isWithinTimespanMs(lastWatchMsec, WATCH_INTERVAL_MSEC)) { uint64_t curVal = digitalReads(watchGpios, availablePins); lastWatchMsec = millis(); if (curVal != previousWatch) { previousWatch = curVal; LOG_INFO("Broadcast GPIOS 0x%llx changed!", curVal); // 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 } ================================================ FILE: src/modules/RemoteHardwareModule.h ================================================ #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; ================================================ FILE: src/modules/ReplyBotModule.cpp ================================================ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_REPLYBOT /* * ReplyBotModule.cpp * * This module implements a simple reply bot for the Meshtastic firmware. It listens for * specific text commands ("/ping", "/hello" and "/test") delivered either via a direct * message (DM) or a broadcast on the primary channel. When a supported command is * received the bot responds with a short status message that includes the hop count * (minimum number of relays), RSSI and SNR of the received packet. To avoid spamming * the network it enforces a per‑sender cooldown between responses. By default the * module is disabled. See the official firmware documentation for guidance on adding modules. * To enable this module, set `#undef MESHTASTIC_EXCLUDE_REPLYBOT` in your variant.h file. */ #include "Channels.h" #include "MeshService.h" #include "NodeDB.h" #include "ReplyBotModule.h" #include "mesh/MeshTypes.h" #include #include #include // // Rate limiting data structures // // Each sender is tracked in a small ring buffer. When a message arrives from a // sender we check the last time we responded to them. If the difference is // less than the configured cooldown (different values for DM vs broadcast) // the message is ignored; otherwise we update the last response time and // proceed with replying. struct ReplyBotCooldownEntry { uint32_t from = 0; uint32_t lastMs = 0; }; static constexpr uint8_t REPLYBOT_COOLDOWN_SLOTS = 8; // ring buffer size static constexpr uint32_t REPLYBOT_DM_COOLDOWN_MS = 15 * 1000; // 15 seconds for DMs static constexpr uint32_t REPLYBOT_LF_COOLDOWN_MS = 60 * 1000; // 60 seconds for LongFast broadcasts static ReplyBotCooldownEntry replybotCooldown[REPLYBOT_COOLDOWN_SLOTS]; static uint8_t replybotCooldownIdx = 0; // Return true if a reply should be rate‑limited for this sender, updating the // entry table as needed. static bool replybotRateLimited(uint32_t from, uint32_t cooldownMs) { const uint32_t now = millis(); for (auto &e : replybotCooldown) { if (e.from == from) { // Found existing entry; check if cooldown expired if ((uint32_t)(now - e.lastMs) < cooldownMs) { return true; } e.lastMs = now; return false; } } // No entry found – insert new sender into the ring replybotCooldown[replybotCooldownIdx].from = from; replybotCooldown[replybotCooldownIdx].lastMs = now; replybotCooldownIdx = (replybotCooldownIdx + 1) % REPLYBOT_COOLDOWN_SLOTS; return false; } // Constructor – registers a single text port and marks the module promiscuous // so that broadcast messages on the primary channel are visible. ReplyBotModule::ReplyBotModule() : SinglePortModule("replybot", meshtastic_PortNum_TEXT_MESSAGE_APP) { isPromiscuous = true; } void ReplyBotModule::setup() { // In future we may add a protobuf configuration; for now the module is // always enabled when compiled in. } // Determine whether we want to process this packet. We only care about // plain text messages addressed to our port. bool ReplyBotModule::wantPacket(const meshtastic_MeshPacket *p) { return (p && p->decoded.portnum == ourPortNum); } ProcessMessage ReplyBotModule::handleReceived(const meshtastic_MeshPacket &mp) { // Accept only direct messages to us or broadcasts on the Primary channel // (regardless of modem preset: LongFast, MediumFast, etc). const uint32_t ourNode = nodeDB->getNodeNum(); const bool isDM = (mp.to == ourNode); const bool isPrimaryChannel = (mp.channel == channels.getPrimaryIndex()) && isBroadcast(mp.to); if (!isDM && !isPrimaryChannel) { return ProcessMessage::CONTINUE; } // Ignore empty payloads if (mp.decoded.payload.size == 0) { return ProcessMessage::CONTINUE; } // Copy payload into a null‑terminated buffer char buf[260]; memset(buf, 0, sizeof(buf)); size_t n = mp.decoded.payload.size; if (n > sizeof(buf) - 1) n = sizeof(buf) - 1; memcpy(buf, mp.decoded.payload.bytes, n); // React only to supported slash commands if (!isCommand(buf)) { return ProcessMessage::CONTINUE; } // Apply rate limiting per sender depending on DM/broadcast const uint32_t cooldownMs = isDM ? REPLYBOT_DM_COOLDOWN_MS : REPLYBOT_LF_COOLDOWN_MS; if (replybotRateLimited(mp.from, cooldownMs)) { return ProcessMessage::CONTINUE; } // Compute hop count indicator – if the relay_node is non‑zero we know // there was at least one relay. Some firmware builds support a hop_start // field which could be used for more accurate counts, but here we use // the available relay_node flag only. // int hopsAway = mp.hop_start - mp.hop_limit; int hopsAway = getHopsAway(mp); // Normalize RSSI: if positive adjust down by 200 to align with typical values int rssi = mp.rx_rssi; if (rssi > 0) { rssi -= 200; } float snr = mp.rx_snr; // Build the reply message and send it back via DM char reply[96]; snprintf(reply, sizeof(reply), "🎙️ Mic Check : %d Hops away | RSSI %d | SNR %.1f", hopsAway, rssi, snr); sendDm(mp, reply); return ProcessMessage::CONTINUE; } // Check if the message starts with one of the supported commands. Leading // whitespace is skipped and commands must be followed by end‑of‑string or // whitespace. bool ReplyBotModule::isCommand(const char *msg) const { if (!msg) return false; while (*msg == ' ' || *msg == '\t') msg++; auto isEndOrSpace = [](char c) { return c == '\0' || std::isspace(static_cast(c)); }; if (strncmp(msg, "/ping", 5) == 0 && isEndOrSpace(msg[5])) return true; if (strncmp(msg, "/hello", 6) == 0 && isEndOrSpace(msg[6])) return true; if (strncmp(msg, "/test", 5) == 0 && isEndOrSpace(msg[5])) return true; return false; } // Send a direct message back to the originating node. void ReplyBotModule::sendDm(const meshtastic_MeshPacket &rx, const char *text) { if (!text) return; meshtastic_MeshPacket *p = allocDataPacket(); p->to = rx.from; p->channel = rx.channel; p->want_ack = false; p->decoded.want_response = false; size_t len = strlen(text); if (len > sizeof(p->decoded.payload.bytes)) { len = sizeof(p->decoded.payload.bytes); } p->decoded.payload.size = len; memcpy(p->decoded.payload.bytes, text, len); service->sendToMesh(p); } #endif // MESHTASTIC_EXCLUDE_REPLYBOT ================================================ FILE: src/modules/ReplyBotModule.h ================================================ #pragma once #include "configuration.h" #if !MESHTASTIC_EXCLUDE_REPLYBOT #include "SinglePortModule.h" #include "mesh/generated/meshtastic/mesh.pb.h" class ReplyBotModule : public SinglePortModule { public: ReplyBotModule(); void setup() override; bool wantPacket(const meshtastic_MeshPacket *p) override; ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; protected: bool isCommand(const char *msg) const; void sendDm(const meshtastic_MeshPacket &rx, const char *text); }; #endif // MESHTASTIC_EXCLUDE_REPLYBOT ================================================ FILE: src/modules/ReplyModule.cpp ================================================ #include "ReplyModule.h" #include "MeshService.h" #include "configuration.h" #include "main.h" #include meshtastic_MeshPacket *ReplyModule::allocReply() { assert(currentRequest); // should always be !NULL #if defined(DEBUG_PORT) && !defined(DEBUG_MUTE) auto req = *currentRequest; auto &p = req.decoded; // The incoming message is in p.payload LOG_INFO("Received message from=0x%0x, id=%d, msg=%.*s", req.from, req.id, p.payload.size, p.payload.bytes); #endif const char *replyStr = "Message Received"; auto reply = allocDataPacket(); // Allocate a packet for sending reply->decoded.payload.size = strlen(replyStr); // You must specify how many bytes are in the reply memcpy(reply->decoded.payload.bytes, replyStr, reply->decoded.payload.size); return reply; } ================================================ FILE: src/modules/ReplyModule.h ================================================ #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; }; ================================================ FILE: src/modules/RoutingModule.cpp ================================================ #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; } else if (owner.is_licensed && nodeDB->getLicenseStatus(mp.from) == UserLicenseStatus::NotLicensed) { // Don't let licensed users to rebroadcast packets from unlicensed users // If we know they are in-fact unlicensed LOG_DEBUG("Packet from unlicensed user, ignoring packet"); return false; } printPacket("Routing sniffing", &mp); router->sniffReceived(&mp, r); // FIXME - move this to a non promsicious PhoneAPI module? // Note: we are careful not to send back packets that started with the phone back to the phone if ((isBroadcast(mp.to) || isToUs(&mp)) && (mp.from != 0)) { printPacket("Delivering rx packet", &mp); service->handleFromRadio(&mp); } return false; // Let others look at this message also if they want } meshtastic_MeshPacket *RoutingModule::allocReply() { assert(currentRequest); return NULL; } void RoutingModule::sendAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex, uint8_t hopLimit, bool ackWantsAck) { auto p = allocAckNak(err, to, idFrom, chIndex, hopLimit); // Allow the caller to set want_ack on this ACK packet if it's important that the ACK be delivered reliably p->want_ack = ackWantsAck; router->sendLocal(p); // we sometimes send directly to the local node } uint8_t RoutingModule::getHopLimitForResponse(const meshtastic_MeshPacket &mp) { const int8_t hopsUsed = getHopsAway(mp); if (hopsUsed >= 0) { if (hopsUsed > (int32_t)(config.lora.hop_limit)) { // In event mode, we never want to send packets with more than our default 3 hops. #if !(EVENTMODE) // This falls through to the default. return hopsUsed; // If the request used more hops than the limit, use the same amount of hops #endif } else if (mp.hop_start == 0) { return 0; // The requesting node wanted 0 hops, so the response also uses a direct/local path. } else if ((uint8_t)(hopsUsed + 2) < config.lora.hop_limit) { return hopsUsed + 2; // Use only the amount of hops needed with some margin as the way back may be different } } return Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit); // Use the default hop limit } meshtastic_MeshPacket *RoutingModule::allocAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex, uint8_t hopLimit) { return MeshModule::allocAckNak(err, to, idFrom, chIndex, hopLimit); } RoutingModule::RoutingModule() : ProtobufModule("routing", meshtastic_PortNum_ROUTING_APP, &meshtastic_Routing_msg) { isPromiscuous = true; // moved the RebroadcastMode logic into handleReceivedProtobuf // LocalOnly requires either the from or to to be a known node // knownOnly specifically requires the from to be a known node. encryptedOk = true; } ================================================ FILE: src/modules/RoutingModule.h ================================================ #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(); virtual void sendAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex, uint8_t hopLimit = 0, bool ackWantsAck = false); meshtastic_MeshPacket *allocAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex, uint8_t hopLimit = 0); // Given the hopStart and hopLimit upon reception of a request, return the hop limit to use for the response uint8_t getHopLimitForResponse(const meshtastic_MeshPacket &mp); protected: friend class Router; /** 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; ================================================ FILE: src/modules/SerialModule.cpp ================================================ #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. */ #ifdef HELTEC_MESH_SOLAR #include "meshSolarApp.h" #endif #if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040) || defined(ARCH_STM32WL)) && \ !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) #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; #ifndef SERIAL_PRINT_PORT #define SERIAL_PRINT_PORT 2 #endif #if SERIAL_PRINT_PORT == 0 #define SERIAL_PRINT_OBJECT Serial #elif SERIAL_PRINT_PORT == 1 #define SERIAL_PRINT_OBJECT Serial1 #elif SERIAL_PRINT_PORT == 2 #define SERIAL_PRINT_OBJECT Serial2 #else #error "Unsupported SERIAL_PRINT_PORT value. Allowed values are 0, 1, or 2." #endif SerialModule::SerialModule() : StreamAPI(&SERIAL_PRINT_OBJECT), concurrency::OSThread("Serial") { api_type = TYPE_SERIAL; } static Print *serialPrint = &SERIAL_PRINT_OBJECT; char serialBytes[512]; size_t serialPayloadSize; bool SerialModule::isValidConfig(const meshtastic_ModuleConfig_SerialConfig &config) { if (config.override_console_serial_port && !IS_ONE_OF(config.mode, meshtastic_ModuleConfig_SerialConfig_Serial_Mode_NMEA, meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO, meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MS_CONFIG)) { const char *warning = "Invalid Serial config: override console serial port is only supported in NMEA and CalTopo output-only modes."; LOG_ERROR(warning); #if !IS_RUNNING_TESTS meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); cn->level = meshtastic_LogRecord_Level_ERROR; cn->time = getValidTime(RTCQualityFromNet); snprintf(cn->message, sizeof(cn->message), "%s", warning); service->sendClientNotification(cn); #endif return false; } return true; } SerialModuleRadio::SerialModuleRadio() : MeshModule("SerialModuleRadio") { switch (moduleConfig.serial.mode) { case meshtastic_ModuleConfig_SerialConfig_Serial_Mode_TEXTMSG: ourPortNum = meshtastic_PortNum_TEXT_MESSAGE_APP; break; case meshtastic_ModuleConfig_SerialConfig_Serial_Mode_NMEA: case meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO: ourPortNum = meshtastic_PortNum_POSITION_APP; break; default: ourPortNum = meshtastic_PortNum_SERIAL_APP; // restrict to the serial channel for rx boundChannel = Channels::serialChannel; break; } } /** * @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("Init 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_STM32WL) #ifndef RAK3172 HardwareSerial *serialInstance = &Serial2; #else HardwareSerial *serialInstance = &Serial1; #endif if (moduleConfig.serial.rxd && moduleConfig.serial.txd) { serialInstance->setTx(moduleConfig.serial.txd); serialInstance->setRx(moduleConfig.serial.rxd); } serialInstance->begin(baud); serialInstance->setTimeout(moduleConfig.serial.timeout > 0 ? moduleConfig.serial.timeout : TIMEOUT); #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 SERIAL_PRINT_PORT != 0 if (moduleConfig.serial.rxd && moduleConfig.serial.txd) { #ifdef ARCH_RP2040 Serial2.setFIFOSize(RX_BUFFER); Serial2.setPinout(moduleConfig.serial.txd, moduleConfig.serial.rxd); #else Serial2.setPins(moduleConfig.serial.rxd, moduleConfig.serial.txd); #endif Serial2.begin(baud, SERIAL_8N1); Serial2.setTimeout(moduleConfig.serial.timeout > 0 ? moduleConfig.serial.timeout : TIMEOUT); } else { #ifdef RP2040_SLOW_CLOCK Serial2.begin(baud, SERIAL_8N1); Serial2.setTimeout(moduleConfig.serial.timeout > 0 ? moduleConfig.serial.timeout : TIMEOUT); #else Serial.begin(baud, SERIAL_8N1); Serial.setTimeout(moduleConfig.serial.timeout > 0 ? moduleConfig.serial.timeout : TIMEOUT); #endif } #else Serial.begin(baud, SERIAL_8N1); Serial.setTimeout(moduleConfig.serial.timeout > 0 ? moduleConfig.serial.timeout : TIMEOUT); #endif serialModuleRadio = new SerialModuleRadio(); firstTime = 0; // in API mode send rebooted sequence if (moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_PROTO) { emitRebooted(); } } else { if (moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_PROTO) { return runOncePart(); } else if ((moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_NMEA) && HAS_GPS) { // in NMEA mode send out GGA every 2 seconds, Don't read from Port if (!Throttle::isWithinTimespanMs(lastNmeaTime, 2000)) { lastNmeaTime = millis(); printGGA(outbuf, sizeof(outbuf), localPosition); serialPrint->printf("%s", outbuf); } } else if ((moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO) && HAS_GPS) { if (!Throttle::isWithinTimespanMs(lastNmeaTime, 10000)) { lastNmeaTime = millis(); uint32_t readIndex = 0; const meshtastic_NodeInfoLite *tempNodeInfo = nodeDB->readNextMeshNode(readIndex); while (tempNodeInfo != NULL) { if (tempNodeInfo->has_user && nodeDB->hasValidPosition(tempNodeInfo)) { printWPL(outbuf, sizeof(outbuf), tempNodeInfo->position, tempNodeInfo->user.long_name, true); serialPrint->printf("%s", outbuf); } tempNodeInfo = nodeDB->readNextMeshNode(readIndex); } } } #if SERIAL_PRINT_PORT != 0 else if ((moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_WS85)) { processWXSerial(); } #if defined(HELTEC_MESH_SOLAR) else if ((moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MS_CONFIG)) { serialPayloadSize = Serial.readBytes(serialBytes, sizeof(serialBytes) - 1); // If the parsing fails, the following parsing will be performed. if ((serialPayloadSize > 0) && (meshSolarCmdHandle(serialBytes) != 0)) { return runOncePart(serialBytes, serialPayloadSize); } } #endif else { #if defined(CONFIG_IDF_TARGET_ESP32C6) while (Serial1.available()) { serialPayloadSize = Serial1.readBytes(serialBytes, meshtastic_Constants_DATA_PAYLOAD_LEN); #else #ifndef RAK3172 HardwareSerial *serialInstance = &Serial2; #else HardwareSerial *serialInstance = &Serial1; #endif while (serialInstance->available()) { serialPayloadSize = serialInstance->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)); const char *sender = (node && node->has_user) ? node->user.short_name : "???"; serialPrint->println(); serialPrint->printf("%s: %s", sender, p.payload.bytes); serialPrint->println(); } else if ((moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_NMEA || moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO) && HAS_GPS) { // Decode the Payload some more meshtastic_Position scratch; meshtastic_Position *decoded = NULL; if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag && mp.decoded.portnum == ourPortNum) { memset(&scratch, 0, sizeof(scratch)); if (pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Position_msg, &scratch)) { decoded = &scratch; } // send position packet as WPL to the serial port printWPL(outbuf, sizeof(outbuf), *decoded, nodeDB->getMeshNode(getFrom(&mp))->user.long_name, moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO); serialPrint->printf("%s", outbuf); } } } } return ProcessMessage::CONTINUE; // Let others look at this message also if they want } /** * @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; } // Add this structure to help with parsing WindGust = 24.4 serial lines. struct ParsedLine { char name[64]; char value[128]; }; /** * Parse a line of format "Name = Value" into name/value pair * @param line Input line to parse * @return ParsedLine containing name and value, or empty strings if parse failed */ ParsedLine parseLine(const char *line) { ParsedLine result = {"", ""}; // Find equals sign const char *equals = strchr(line, '='); if (!equals) { return result; } // Extract name by copying substring char nameBuf[64]; // Temporary buffer size_t nameLen = equals - line; if (nameLen >= sizeof(nameBuf)) { nameLen = sizeof(nameBuf) - 1; } strncpy(nameBuf, line, nameLen); nameBuf[nameLen] = '\0'; // Trim whitespace from name char *nameStart = nameBuf; while (*nameStart && isspace(*nameStart)) nameStart++; char *nameEnd = nameStart + strlen(nameStart) - 1; while (nameEnd > nameStart && isspace(*nameEnd)) *nameEnd-- = '\0'; // Copy trimmed name strncpy(result.name, nameStart, sizeof(result.name) - 1); result.name[sizeof(result.name) - 1] = '\0'; // Extract value part (after equals) const char *valueStart = equals + 1; while (*valueStart && isspace(*valueStart)) valueStart++; strncpy(result.value, valueStart, sizeof(result.value) - 1); result.value[sizeof(result.value) - 1] = '\0'; // Trim trailing whitespace from value char *valueEnd = result.value + strlen(result.value) - 1; while (valueEnd > result.value && isspace(*valueEnd)) *valueEnd-- = '\0'; return result; } /** * 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 SERIAL_PRINT_PORT != 0 && !defined(ARCH_STM32WL) && !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; static char rainStr[] = "5780860000"; static int rainSum = 0; static float rain = 0; bool gotwind = false; while (Serial2.available()) { // clear serialBytes buffer memset(serialBytes, '\0', sizeof(serialBytes)); // memset(formattedString, '\0', sizeof(formattedString)); serialPayloadSize = Serial2.readBytes(serialBytes, 512); // check for a strings we care about // example output of serial data fields from the WS85 // WindDir = 79 // WindSpeed = 0.5 // WindGust = 0.6 // GXTS04Temp = 24.4 // Temperature = 23.4 // WS80 // RainIntSum = 0 // Rain = 0.0 if (serialPayloadSize > 0) { // Define variables for line processing int lineStart = 0; int lineEnd = -1; // Process each byte in the received data for (size_t i = 0; i < serialPayloadSize; i++) { // go until we hit the end of line and then process the line if (serialBytes[i] == '\n') { lineEnd = i; // Extract the current line char line[meshtastic_Constants_DATA_PAYLOAD_LEN]; memset(line, '\0', sizeof(line)); if ((size_t)(lineEnd - lineStart) < sizeof(line) - 1) { memcpy(line, &serialBytes[lineStart], lineEnd - lineStart); ParsedLine parsed = parseLine(line); if (strlen(parsed.name) > 0) { if (strcmp(parsed.name, "WindDir") == 0) { strlcpy(windDir, parsed.value, sizeof(windDir)); double radians = GeoCoord::toRadians(strtof(windDir, nullptr)); dir_sum_sin += sin(radians); dir_sum_cos += cos(radians); dirCount++; gotwind = true; } else if (strcmp(parsed.name, "WindSpeed") == 0) { strlcpy(windVel, parsed.value, sizeof(windVel)); float newv = strtof(windVel, nullptr); velSum += newv; velCount++; if (newv < lull || lull == -1) { lull = newv; } gotwind = true; } else if (strcmp(parsed.name, "WindGust") == 0) { strlcpy(windGust, parsed.value, sizeof(windGust)); float newg = strtof(windGust, nullptr); if (newg > gust) { gust = newg; } gotwind = true; } else if (strcmp(parsed.name, "BatVoltage") == 0) { strlcpy(batVoltage, parsed.value, sizeof(batVoltage)); batVoltageF = strtof(batVoltage, nullptr); break; // last possible data we want so break } else if (strcmp(parsed.name, "CapVoltage") == 0) { strlcpy(capVoltage, parsed.value, sizeof(capVoltage)); capVoltageF = strtof(capVoltage, nullptr); } else if (strcmp(parsed.name, "GXTS04Temp") == 0 || strcmp(parsed.name, "Temperature") == 0) { strlcpy(temperature, parsed.value, sizeof(temperature)); temperatureF = strtof(temperature, nullptr); } else if (strcmp(parsed.name, "RainIntSum") == 0) { strlcpy(rainStr, parsed.value, sizeof(rainStr)); rainSum = int(strtof(rainStr, nullptr)); } else if (strcmp(parsed.name, "Rain") == 0) { strlcpy(rainStr, parsed.value, sizeof(rainStr)); rain = strtof(rainStr, nullptr); } } // Update lineStart for the next line lineStart = lineEnd + 1; } } } break; // clear the input buffer while (Serial2.available() > 0) { Serial2.read(); // Read and discard the bytes in the input buffer } } } if (gotwind) { LOG_INFO("WS8X : %i %.1fg%.1f %.1fv %.1fv %.1fC rain: %.1f, %i sum", atoi(windDir), strtof(windVel, nullptr), strtof(windGust, nullptr), batVoltageF, capVoltageF, temperatureF, rain, rainSum); } if (gotwind && !Throttle::isWithinTimespanMs(lastAveraged, averageIntervalMillis)) { // calculate averages and send to the mesh float velAvg = 1.0 * velSum / velCount; double avgSin = dir_sum_sin / dirCount; double avgCos = dir_sum_cos / dirCount; double avgRadians = atan2(avgSin, avgCos); float dirAvg = GeoCoord::toDegrees(avgRadians); if (dirAvg < 0) { dirAvg += 360.0; } 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; m.variant.environment_metrics.rainfall_24h = rainSum; m.variant.environment_metrics.has_rainfall_24h = true; // not sure if this value is actually the 1hr sum so needs to do some testing m.variant.environment_metrics.rainfall_1h = rain; m.variant.environment_metrics.has_rainfall_1h = true; if (lull == -1) lull = 0; m.variant.environment_metrics.wind_lull = lull; m.variant.environment_metrics.has_wind_lull = true; LOG_INFO("WS8X Transmit speed=%fm/s, direction=%d , lull=%f, gust=%f, voltage=%f temperature=%f", m.variant.environment_metrics.wind_speed, m.variant.environment_metrics.wind_direction, m.variant.environment_metrics.wind_lull, m.variant.environment_metrics.wind_gust, m.variant.environment_metrics.voltage, m.variant.environment_metrics.temperature); sendTelemetry(m); // reset counters and gust/lull velSum = velCount = dirCount = 0; dir_sum_sin = dir_sum_cos = 0; gust = 0; lull = -1; } #endif return; } #endif ================================================ FILE: src/modules/SerialModule.h ================================================ #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(ARCH_STM32WL)) && \ !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) class SerialModule : public StreamAPI, private concurrency::OSThread { bool firstTime = 1; unsigned long lastNmeaTime = millis(); char outbuf[90] = ""; public: SerialModule(); static bool isValidConfig(const meshtastic_ModuleConfig_SerialConfig &config); 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 ================================================ FILE: src/modules/StatusLEDModule.cpp ================================================ #include "StatusLEDModule.h" #include "MeshService.h" #include "configuration.h" #include /* StatusLEDModule manages the device's status LEDs, updating their states based on power and Bluetooth status. It reflects charging, charged, discharging, and Bluetooth connection states using the appropriate LEDs. */ StatusLEDModule *statusLEDModule; StatusLEDModule::StatusLEDModule() : concurrency::OSThread("StatusLEDModule") { bluetoothStatusObserver.observe(&bluetoothStatus->onNewStatus); powerStatusObserver.observe(&powerStatus->onNewStatus); #if !MESHTASTIC_EXCLUDE_INPUTBROKER if (inputBroker) inputObserver.observe(inputBroker); #endif } int StatusLEDModule::handleStatusUpdate(const meshtastic::Status *arg) { switch (arg->getStatusType()) { case STATUS_TYPE_POWER: { if (powerStatus->getHasUSB() || powerStatus->getIsCharging()) { power_state = charging; if (powerStatus->getBatteryChargePercent() >= 100) { power_state = charged; } } else { if (powerStatus->getBatteryChargePercent() > 5) { power_state = discharging; } else { power_state = critical; } } break; } case STATUS_TYPE_BLUETOOTH: { switch (bluetoothStatus->getConnectionState()) { case meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED: { ble_state = unpaired; PAIRING_LED_starttime = millis(); break; } case meshtastic::BluetoothStatus::ConnectionState::PAIRING: { ble_state = pairing; PAIRING_LED_starttime = millis(); break; } case meshtastic::BluetoothStatus::ConnectionState::CONNECTED: { if (ble_state != connected) { ble_state = connected; PAIRING_LED_starttime = millis(); } } } break; } } return 0; }; #if !MESHTASTIC_EXCLUDE_INPUTBROKER int StatusLEDModule::handleInputEvent(const InputEvent *event) { lastUserbuttonTime = millis(); return 0; } #endif int32_t StatusLEDModule::runOnce() { my_interval = 1000; if (power_state == charging) { #ifndef POWER_LED_HARDWARE_BLINKS_WHILE_CHARGING CHARGE_LED_state = !CHARGE_LED_state; #endif } else if (power_state == charged) { CHARGE_LED_state = LED_STATE_ON; } else if (power_state == critical) { if (POWER_LED_starttime + 30000 < millis() && !doing_fast_blink) { doing_fast_blink = true; POWER_LED_starttime = millis(); } if (doing_fast_blink) { PAIRING_LED_state = LED_STATE_OFF; CHARGE_LED_state = !CHARGE_LED_state; my_interval = 250; if (POWER_LED_starttime + 2000 < millis()) { doing_fast_blink = false; CHARGE_LED_state = LED_STATE_OFF; } } } if (power_state != charging && power_state != charged && !doing_fast_blink) { if (CHARGE_LED_state == LED_STATE_ON) { CHARGE_LED_state = LED_STATE_OFF; my_interval = 999; } else { CHARGE_LED_state = LED_STATE_ON; my_interval = 1; } } if (!config.bluetooth.enabled || PAIRING_LED_starttime + 30 * 1000 < millis() || doing_fast_blink) { PAIRING_LED_state = LED_STATE_OFF; } else if (ble_state == unpaired) { if (slowTrack) { PAIRING_LED_state = !PAIRING_LED_state; slowTrack = false; } else { slowTrack = true; } } else if (ble_state == pairing) { PAIRING_LED_state = !PAIRING_LED_state; } else { PAIRING_LED_state = LED_STATE_ON; } // Override if disabled in config if (config.device.led_heartbeat_disabled) { CHARGE_LED_state = LED_STATE_OFF; } #ifdef Battery_LED_1 bool chargeIndicatorLED1 = LED_STATE_OFF; bool chargeIndicatorLED2 = LED_STATE_OFF; bool chargeIndicatorLED3 = LED_STATE_OFF; bool chargeIndicatorLED4 = LED_STATE_OFF; if (lastUserbuttonTime + 10 * 1000 > millis() || CHARGE_LED_state == LED_STATE_ON) { // should this be off at very low percentages? chargeIndicatorLED1 = LED_STATE_ON; if (powerStatus && powerStatus->getBatteryChargePercent() >= 25) chargeIndicatorLED2 = LED_STATE_ON; if (powerStatus && powerStatus->getBatteryChargePercent() >= 50) chargeIndicatorLED3 = LED_STATE_ON; if (powerStatus && powerStatus->getBatteryChargePercent() >= 75) chargeIndicatorLED4 = LED_STATE_ON; } #endif #if defined(HAS_PMU) if (pmu_found && PMU) { // blink the axp led PMU->setChargingLedMode(CHARGE_LED_state ? XPOWERS_CHG_LED_ON : XPOWERS_CHG_LED_OFF); } #endif #ifdef PCA_LED_POWER io.digitalWrite(PCA_LED_POWER, CHARGE_LED_state); #endif #ifdef PCA_LED_ENABLE io.digitalWrite(PCA_LED_ENABLE, CHARGE_LED_state); #endif #ifdef LED_POWER digitalWrite(LED_POWER, CHARGE_LED_state); #endif #ifdef LED_PAIRING digitalWrite(LED_PAIRING, PAIRING_LED_state); #endif #ifdef RGB_LED_POWER if (!config.device.led_heartbeat_disabled) { if (CHARGE_LED_state == LED_STATE_ON) { ambientLightingThread->setLighting(10, 255, 0, 0); } else { ambientLightingThread->setLighting(0, 0, 0, 0); } } #endif #ifdef Battery_LED_1 digitalWrite(Battery_LED_1, chargeIndicatorLED1); #endif #ifdef Battery_LED_2 digitalWrite(Battery_LED_2, chargeIndicatorLED2); #endif #ifdef Battery_LED_3 digitalWrite(Battery_LED_3, chargeIndicatorLED3); #endif #ifdef Battery_LED_4 digitalWrite(Battery_LED_4, chargeIndicatorLED4); #endif return (my_interval); } void StatusLEDModule::setPowerLED(bool LEDon) { #if defined(HAS_PMU) if (pmu_found && PMU) { // blink the axp led PMU->setChargingLedMode(LEDon ? XPOWERS_CHG_LED_ON : XPOWERS_CHG_LED_OFF); } #endif uint8_t ledState = LEDon ? LED_STATE_ON : LED_STATE_OFF; #ifdef PCA_LED_POWER io.digitalWrite(PCA_LED_POWER, ledState); #endif #ifdef PCA_LED_ENABLE io.digitalWrite(PCA_LED_ENABLE, ledState); #endif #ifdef LED_POWER digitalWrite(LED_POWER, ledState); #endif #ifdef LED_PAIRING digitalWrite(LED_PAIRING, ledState); #endif #ifdef Battery_LED_1 digitalWrite(Battery_LED_1, ledState); #endif #ifdef Battery_LED_2 digitalWrite(Battery_LED_2, ledState); #endif #ifdef Battery_LED_3 digitalWrite(Battery_LED_3, ledState); #endif #ifdef Battery_LED_4 digitalWrite(Battery_LED_4, ledState); #endif } ================================================ FILE: src/modules/StatusLEDModule.h ================================================ #pragma once #include "BluetoothStatus.h" #include "MeshModule.h" #include "PowerStatus.h" #include "concurrency/OSThread.h" #include "configuration.h" #include "main.h" #include #include #if !MESHTASTIC_EXCLUDE_INPUTBROKER #include "input/InputBroker.h" #endif class StatusLEDModule : private concurrency::OSThread { bool slowTrack = false; public: StatusLEDModule(); int handleStatusUpdate(const meshtastic::Status *); #if !MESHTASTIC_EXCLUDE_INPUTBROKER int handleInputEvent(const InputEvent *arg); #endif void setPowerLED(bool); protected: unsigned int my_interval = 1000; // interval in millisconds virtual int32_t runOnce() override; CallbackObserver bluetoothStatusObserver = CallbackObserver(this, &StatusLEDModule::handleStatusUpdate); CallbackObserver powerStatusObserver = CallbackObserver(this, &StatusLEDModule::handleStatusUpdate); #if !MESHTASTIC_EXCLUDE_INPUTBROKER CallbackObserver inputObserver = CallbackObserver(this, &StatusLEDModule::handleInputEvent); #endif private: bool CHARGE_LED_state = LED_STATE_OFF; bool PAIRING_LED_state = LED_STATE_OFF; uint32_t PAIRING_LED_starttime = 0; uint32_t lastUserbuttonTime = 0; uint32_t POWER_LED_starttime = 0; bool doing_fast_blink = false; enum PowerState { discharging, charging, charged, critical }; PowerState power_state = discharging; enum BLEState { unpaired, pairing, connected }; BLEState ble_state = unpaired; }; extern StatusLEDModule *statusLEDModule; #ifdef RGB_LED_POWER #include "AmbientLightingThread.h" extern AmbientLightingThread *ambientLightingThread; #endif ================================================ FILE: src/modules/StatusMessageModule.cpp ================================================ #if !MESHTASTIC_EXCLUDE_STATUS #include "StatusMessageModule.h" #include "MeshService.h" #include "ProtobufModule.h" StatusMessageModule *statusMessageModule; int32_t StatusMessageModule::runOnce() { if (moduleConfig.has_statusmessage && moduleConfig.statusmessage.node_status[0] != '\0') { // create and send message with the status message set meshtastic_StatusMessage ourStatus = meshtastic_StatusMessage_init_zero; strncpy(ourStatus.status, moduleConfig.statusmessage.node_status, sizeof(ourStatus.status)); ourStatus.status[sizeof(ourStatus.status) - 1] = '\0'; // ensure null termination meshtastic_MeshPacket *p = allocDataPacket(); p->decoded.payload.size = pb_encode_to_bytes(p->decoded.payload.bytes, sizeof(p->decoded.payload.bytes), meshtastic_StatusMessage_fields, &ourStatus); p->to = NODENUM_BROADCAST; p->decoded.want_response = false; p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; p->channel = 0; service->sendToMesh(p); } return 1000 * 12 * 60 * 60; } ProcessMessage StatusMessageModule::handleReceived(const meshtastic_MeshPacket &mp) { if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag) { meshtastic_StatusMessage incomingMessage; if (pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, meshtastic_StatusMessage_fields, &incomingMessage)) { LOG_INFO("Received a NodeStatus message %s", incomingMessage.status); } } return ProcessMessage::CONTINUE; } #endif ================================================ FILE: src/modules/StatusMessageModule.h ================================================ #pragma once #if !MESHTASTIC_EXCLUDE_STATUS #include "SinglePortModule.h" #include "configuration.h" class StatusMessageModule : public SinglePortModule, private concurrency::OSThread { public: /** Constructor * name is for debugging output */ StatusMessageModule() : SinglePortModule("statusMessage", meshtastic_PortNum_NODE_STATUS_APP), concurrency::OSThread("StatusMessage") { if (moduleConfig.has_statusmessage && moduleConfig.statusmessage.node_status[0] != '\0') { this->setInterval(2 * 60 * 1000); } else { this->setInterval(1000 * 12 * 60 * 60); } // TODO: If we have a string, set the initial delay (15 minutes maybe) } virtual int32_t runOnce() override; protected: /** Called to handle a particular incoming message */ virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; private: }; extern StatusMessageModule *statusMessageModule; #endif ================================================ FILE: src/modules/StoreForwardModule.cpp ================================================ /** * @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("Send heartbeat"); meshtastic_StoreAndForward sf = meshtastic_StoreAndForward_init_zero; sf.rr = meshtastic_StoreAndForward_RequestResponse_ROUTER_HEARTBEAT; sf.which_variant = meshtastic_StoreAndForward_heartbeat_tag; sf.variant.heartbeat.period = heartbeatInterval; sf.variant.heartbeat.secondary = 0; // TODO we always have one primary router for now storeForwardModule->sendMessage(NODENUM_BROADCAST, sf); } return (this->packetTimeMax); } #endif return disable(); } /** * 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 3/4 the available PSRAM unless otherwise specified. Note: This needs to be done after every thing that would use PSRAM */ uint32_t numberOfPackets = (this->records ? this->records : (((memGet.getFreePsram() / 4) * 3) / sizeof(PacketHistoryStruct))); this->records = numberOfPackets; #if defined(ARCH_ESP32) this->packetHistory = static_cast(ps_calloc(numberOfPackets, sizeof(PacketHistoryStruct))); #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 - Send %u message(s)", queueSize); this->busy = true; // runOnce() will pickup the next steps once busy = true. this->busyTo = to; } else { LOG_INFO("S&F - No history"); } meshtastic_StoreAndForward sf = meshtastic_StoreAndForward_init_zero; sf.rr = meshtastic_StoreAndForward_RequestResponse_ROUTER_HISTORY; sf.which_variant = meshtastic_StoreAndForward_history_tag; sf.variant.history.history_messages = queueSize; sf.variant.history.window = secAgo * 1000; sf.variant.history.last_request = lastRequest[to]; storeForwardModule->sendMessage(to, sf); setIntervalFromNow(this->packetTimeMax); // Delay start of sending payloads } /** * 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; 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].id = mp.id; this->packetHistory[this->packetHistoryTotalCount].reply_id = p.reply_id; this->packetHistory[this->packetHistoryTotalCount].emoji = (bool)p.emoji; this->packetHistory[this->packetHistoryTotalCount].payload_size = p.payload.size; this->packetHistory[this->packetHistoryTotalCount].rx_rssi = mp.rx_rssi; this->packetHistory[this->packetHistoryTotalCount].rx_snr = mp.rx_snr; this->packetHistory[this->packetHistoryTotalCount].hop_start = mp.hop_start; this->packetHistory[this->packetHistoryTotalCount].hop_limit = mp.hop_limit; this->packetHistory[this->packetHistoryTotalCount].via_mqtt = mp.via_mqtt; this->packetHistory[this->packetHistoryTotalCount].transport_mechanism = mp.transport_mechanism; memcpy(this->packetHistory[this->packetHistoryTotalCount].payload, p.payload.bytes, meshtastic_Constants_DATA_PAYLOAD_LEN); this->packetHistoryTotalCount++; } /** * 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("Send 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->id = this->packetHistory[i].id; p->channel = this->packetHistory[i].channel; p->decoded.reply_id = this->packetHistory[i].reply_id; p->rx_time = this->packetHistory[i].time; p->decoded.emoji = (uint32_t)this->packetHistory[i].emoji; p->rx_rssi = this->packetHistory[i].rx_rssi; p->rx_snr = this->packetHistory[i].rx_snr; p->hop_start = this->packetHistory[i].hop_start; p->hop_limit = this->packetHistory[i].hop_limit; p->via_mqtt = this->packetHistory[i].via_mqtt; p->transport_mechanism = (meshtastic_MeshPacket_TransportMechanism)this->packetHistory[i].transport_mechanism; // Let's assume that if the server received the S&F request that the client is in range. // TODO: Make this configurable. p->want_ack = false; 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("Send 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 proto module!"); // if we can't decode it, nobody can process it! return ProcessMessage::STOP; } return handleReceivedProtobuf(mp, decoded) ? ProcessMessage::STOP : ProcessMessage::CONTINUE; } } // all others are irrelevant } #endif return ProcessMessage::CONTINUE; // Let others look at this message also if they want } /** * 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 * (p->rr == meshtastic_StoreAndForward_RequestResponse_ROUTER_ERROR ? 2 : 1); } break; case meshtastic_StoreAndForward_RequestResponse_ROUTER_PONG: // A router responded, this is equal to receiving a heartbeat case meshtastic_StoreAndForward_RequestResponse_ROUTER_HEARTBEAT: if (is_client) { // register heartbeat and interval if (p->which_variant == meshtastic_StoreAndForward_heartbeat_tag) { heartbeatInterval = p->variant.heartbeat.period; } lastHeartbeat = millis(); LOG_INFO("StoreAndForward Heartbeat received"); } break; case meshtastic_StoreAndForward_RequestResponse_ROUTER_PING: if (is_client) { // respond with a CLIENT PONG storeForwardModule->sendMessage(getFrom(&mp), meshtastic_StoreAndForward_RequestResponse_CLIENT_PONG); } break; case meshtastic_StoreAndForward_RequestResponse_ROUTER_STATS: if (is_client) { LOG_DEBUG("Router Response STATS"); // These fields only have informational purpose on a client. Fill them to consume later. if (p->which_variant == meshtastic_StoreAndForward_stats_tag) { this->records = p->variant.stats.messages_max; this->requests = p->variant.stats.requests; this->requests_history = p->variant.stats.requests_history; this->heartbeat = p->variant.stats.heartbeat; this->historyReturnMax = p->variant.stats.return_max; this->historyReturnWindow = p->variant.stats.return_window; } } break; case meshtastic_StoreAndForward_RequestResponse_ROUTER_HISTORY: if (is_client) { // These fields only have informational purpose on a client. Fill them to consume later. if (p->which_variant == meshtastic_StoreAndForward_history_tag) { this->historyReturnWindow = p->variant.history.window / 60000; LOG_INFO("Router Response HISTORY - Sending %d messages from last %d minutes", p->variant.history.history_messages, this->historyReturnWindow); } } break; default: break; // no need to do anything } return false; // RoutingModule sends it to the phone } StoreForwardModule::StoreForwardModule() : concurrency::OSThread("StoreForward"), ProtobufModule("StoreForward", meshtastic_PortNum_STORE_FORWARD_APP, &meshtastic_StoreAndForward_msg) { #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 || config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE || moduleConfig.store_forward.is_server)) { LOG_INFO("Init Store & Forward Module in Server mode"); if (memGet.getPsramSize() > 0) { if (memGet.getFreePsram() >= 1024 * 1024) { // Do the startup here // Maximum number of records to return. if (moduleConfig.store_forward.history_return_max) this->historyReturnMax = moduleConfig.store_forward.history_return_max; // Maximum time window for records to return (in minutes) if (moduleConfig.store_forward.history_return_window) this->historyReturnWindow = moduleConfig.store_forward.history_return_window; // Maximum number of records to store in memory if (moduleConfig.store_forward.records) this->records = moduleConfig.store_forward.records; // send heartbeat advertising? if (moduleConfig.store_forward.heartbeat) this->heartbeat = moduleConfig.store_forward.heartbeat; else this->heartbeat = false; // Popupate PSRAM with our data structures. this->populatePSRAM(); is_server = true; } else { LOG_INFO("."); LOG_INFO("S&F: not enough PSRAM free, Disable"); } } else { LOG_INFO("S&F: device doesn't have PSRAM, Disable"); } // Client } else { is_client = true; LOG_INFO("Init Store & Forward Module in Client mode"); } } else { disable(); } #endif } ================================================ FILE: src/modules/StoreForwardModule.h ================================================ #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; uint32_t id; uint8_t channel; uint32_t reply_id; bool emoji; uint8_t payload[meshtastic_Constants_DATA_PAYLOAD_LEN]; pb_size_t payload_size; int32_t rx_rssi; float rx_snr; uint8_t hop_start; uint8_t hop_limit; bool via_mqtt; uint8_t transport_mechanism; }; class StoreForwardModule : private concurrency::OSThread, public ProtobufModule { bool busy = 0; uint32_t busyTo = 0; char routerMessage[meshtastic_Constants_DATA_PAYLOAD_LEN] = {0}; 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; ================================================ FILE: src/modules/SystemCommandsModule.cpp ================================================ #include "SystemCommandsModule.h" #include "input/InputBroker.h" #include "meshUtils.h" #if HAS_SCREEN #include "MessageStore.h" #include "graphics/Screen.h" #include "graphics/SharedUIDisplay.h" #endif #include "GPS.h" #include "MeshService.h" #include "Module.h" #include "NodeDB.h" #include "main.h" #include "modules/AdminModule.h" #include "modules/ExternalNotificationModule.h" SystemCommandsModule *systemCommandsModule; SystemCommandsModule::SystemCommandsModule() { if (inputBroker) inputObserver.observe(inputBroker); } int SystemCommandsModule::handleInputEvent(const InputEvent *event) { LOG_INPUT("SystemCommands Input event %u! kb %u", event->inputEvent, event->kbchar); // System commands (all others fall through) switch (event->kbchar) { // Fn key symbols case INPUT_BROKER_MSG_FN_SYMBOL_ON: case INPUT_BROKER_MSG_FN_SYMBOL_OFF: return 0; // Brightness case INPUT_BROKER_MSG_BRIGHTNESS_UP: IF_SCREEN(screen->increaseBrightness()); LOG_DEBUG("Increase Screen Brightness"); return 0; case INPUT_BROKER_MSG_BRIGHTNESS_DOWN: IF_SCREEN(screen->decreaseBrightness()); LOG_DEBUG("Decrease Screen Brightness"); return 0; // Mute case INPUT_BROKER_MSG_MUTE_TOGGLE: if (moduleConfig.external_notification.enabled && externalNotificationModule) { externalNotificationModule->setMute(!externalNotificationModule->getMute()); IF_SCREEN(if (!externalNotificationModule->getMute()) externalNotificationModule->stopNow(); screen->showSimpleBanner( externalNotificationModule->getMute() ? "Notifications\nDisabled" : "Notifications\nEnabled", 3000);) } return 0; // Bluetooth case INPUT_BROKER_MSG_BLUETOOTH_TOGGLE: config.bluetooth.enabled = !config.bluetooth.enabled; LOG_INFO("User toggled Bluetooth"); nodeDB->saveToDisk(); #if defined(ARDUINO_ARCH_NRF52) if (!config.bluetooth.enabled) { disableBluetooth(); IF_SCREEN(screen->showSimpleBanner("Bluetooth OFF\nRebooting", 3000)); rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 2000; } else { IF_SCREEN(screen->showSimpleBanner("Bluetooth ON\nRebooting", 3000)); rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; } #else if (!config.bluetooth.enabled) { disableBluetooth(); IF_SCREEN(screen->showSimpleBanner("Bluetooth OFF", 3000)); } else { IF_SCREEN(screen->showSimpleBanner("Bluetooth ON\nRebooting", 3000)); rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; } #endif return 0; case INPUT_BROKER_MSG_REBOOT: IF_SCREEN(screen->showSimpleBanner("Rebooting...", 0)); nodeDB->saveToDisk(); #if HAS_SCREEN messageStore.saveToFlash(); #endif rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; // runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; return true; } switch (event->inputEvent) { // GPS case INPUT_BROKER_GPS_TOGGLE: #if !MESHTASTIC_EXCLUDE_GPS if (gps) { if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED && config.position.fixed_position == false) { nodeDB->clearLocalPosition(); nodeDB->saveToDisk(); } gps->toggleGpsMode(); const char *msg = (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) ? "GPS Enabled" : "GPS Disabled"; IF_SCREEN(screen->forceDisplay(); screen->showSimpleBanner(msg, 3000);) } #endif return true; // Mesh ping case INPUT_BROKER_SEND_PING: service->refreshLocalMeshNode(); if (service->trySendPosition(NODENUM_BROADCAST, true)) { IF_SCREEN(screen->showSimpleBanner("Position\nSent", 3000)); } else { IF_SCREEN(screen->showSimpleBanner("Node Info\nSent", 3000)); } return true; // Power control case INPUT_BROKER_SHUTDOWN: shutdownAtMsec = millis(); return true; default: // No other input events handled here break; } return false; } ================================================ FILE: src/modules/SystemCommandsModule.h ================================================ #pragma once #include "MeshModule.h" #include "configuration.h" #include "input/InputBroker.h" #include #include class SystemCommandsModule { CallbackObserver inputObserver = CallbackObserver(this, &SystemCommandsModule::handleInputEvent); public: SystemCommandsModule(); int handleInputEvent(const InputEvent *event); }; extern SystemCommandsModule *systemCommandsModule; ================================================ FILE: src/modules/Telemetry/AirQualityTelemetry.cpp ================================================ #include "DebugConfiguration.h" #include "configuration.h" #if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_AIR_QUALITY_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 "TransmitHistory.h" #include "UnitConversions.h" #include "graphics/ScreenFonts.h" #include "graphics/SharedUIDisplay.h" #include "graphics/images.h" #include "main.h" #include "sleep.h" #include static constexpr uint16_t TX_HISTORY_KEY_AIR_QUALITY_TELEMETRY = 0x8004; // Sensors #include "Sensor/AddI2CSensorTemplate.h" #include "Sensor/PMSA003ISensor.h" #include "Sensor/SEN5XSensor.h" #if __has_include() #include "Sensor/SCD4XSensor.h" #endif #if __has_include() #include "Sensor/SFA30Sensor.h" #endif #if __has_include() #include "Sensor/SCD30Sensor.h" #endif void AirQualityTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner) { if (!moduleConfig.telemetry.air_quality_enabled && !AIR_QUALITY_TELEMETRY_MODULE_ENABLE) { return; } LOG_INFO("Air Quality Telemetry adding I2C devices..."); /* Uncomment the preferences below if you want to use the module without having to configure it from the PythonAPI or WebUI. Note: this was previously on runOnce, which didnt take effect as other modules already had already been initialized (screen) */ // moduleConfig.telemetry.air_quality_enabled = 1; // moduleConfig.telemetry.air_quality_screen_enabled = 1; // moduleConfig.telemetry.air_quality_interval = 15; // order by priority of metrics/values (low top, high bottom) addSensor(i2cScanner, ScanI2C::DeviceType::PMSA003I); addSensor(i2cScanner, ScanI2C::DeviceType::SEN5X); #if __has_include() addSensor(i2cScanner, ScanI2C::DeviceType::SCD4X); #endif #if __has_include() addSensor(i2cScanner, ScanI2C::DeviceType::SFA30); #endif #if __has_include() addSensor(i2cScanner, ScanI2C::DeviceType::SCD30); #endif } int32_t AirQualityTelemetryModule::runOnce() { if (sleepOnNextExecution == true) { sleepOnNextExecution = false; uint32_t nightyNightMs = Default::getConfiguredOrDefaultMs(moduleConfig.telemetry.air_quality_interval, default_telemetry_broadcast_interval_secs); LOG_DEBUG("Sleeping for %ims, then awaking to send metrics again.", nightyNightMs); doDeepSleep(nightyNightMs, true, false); } uint32_t result = UINT32_MAX; if (!(moduleConfig.telemetry.air_quality_enabled || moduleConfig.telemetry.air_quality_screen_enabled || AIR_QUALITY_TELEMETRY_MODULE_ENABLE)) { // If this module is not enabled, and the user doesn't want the display screen don't waste any OSThread time on it return disable(); } if (firstTime) { // This is the first time the OSThread library has called this function, so // do some setup firstTime = false; if (moduleConfig.telemetry.air_quality_enabled) { LOG_INFO("Air quality Telemetry: init"); // check if we have at least one sensor if (!sensors.empty()) { result = DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; } } // it's possible to have this module enabled, only for displaying values on the screen. // therefore, we should only enable the sensor loop if measurement is also enabled return result == UINT32_MAX ? disable() : setStartDelay(); } else { // if we somehow got to a second run of this module with measurement disabled, then just wait forever if (!moduleConfig.telemetry.air_quality_enabled && !AIR_QUALITY_TELEMETRY_MODULE_ENABLE) { return disable(); } // Wake up the sensors that need it LOG_INFO("Waking up sensors..."); uint32_t lastTelemetry = transmitHistory ? transmitHistory->getLastSentToMeshMillis(TX_HISTORY_KEY_AIR_QUALITY_TELEMETRY) : 0; for (TelemetrySensor *sensor : sensors) { if (!sensor->canSleep()) { LOG_DEBUG("%s sensor doesn't have sleep feature. Skipping", sensor->sensorName); } else if (((lastTelemetry == 0) || !Throttle::isWithinTimespanMs(lastTelemetry - sensor->wakeUpTimeMs(), 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()) { if (!sensor->isActive()) { LOG_DEBUG("Waking up: %s", sensor->sensorName); return sensor->wakeUp(); } else { int32_t pendingForReadyMs = sensor->pendingForReadyMs(); LOG_DEBUG("%s. Pending for ready %ums", sensor->sensorName, pendingForReadyMs); if (pendingForReadyMs) { return pendingForReadyMs; } } } } if (((lastTelemetry == 0) || !Throttle::isWithinTimespanMs(lastTelemetry, 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(); if (transmitHistory) transmitHistory->setLastSentToMesh(TX_HISTORY_KEY_AIR_QUALITY_TELEMETRY); } 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(); } // Send to sleep sensors that consume power LOG_DEBUG("Sending sensors to sleep"); for (TelemetrySensor *sensor : sensors) { if (sensor->isActive() && sensor->canSleep()) { if (sensor->wakeUpTimeMs() < (int32_t)Default::getConfiguredOrDefaultMsScaled(moduleConfig.telemetry.air_quality_interval, default_telemetry_broadcast_interval_secs, numOnlineNodes)) { LOG_DEBUG("Disabling %s until next period", sensor->sensorName); sensor->sleep(); } else { LOG_DEBUG("Sensor stays enabled due to warm up period"); } } } } return min(sendToPhoneIntervalMs, result); } bool AirQualityTelemetryModule::wantUIFrame() { return moduleConfig.telemetry.air_quality_screen_enabled; } #if HAS_SCREEN void AirQualityTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { // === Setup display === display->clear(); display->setFont(FONT_SMALL); display->setTextAlignment(TEXT_ALIGN_LEFT); int line = 1; // === Set Title const char *titleStr = (graphics::currentResolution == graphics::ScreenResolution::High) ? "Air Quality" : "AQ."; // === Header === graphics::drawCommonHeader(display, x, y, titleStr); // === Row spacing setup === const int rowHeight = FONT_HEIGHT_SMALL - 4; int currentY = graphics::getTextPositions(display)[line++]; // === Show "No Telemetry" if no data available === if (!lastMeasurementPacket) { display->drawString(x, currentY, "No Telemetry"); return; } // Decode the telemetry message from the latest received packet const meshtastic_Data &p = lastMeasurementPacket->decoded; meshtastic_Telemetry telemetry; if (!pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &telemetry)) { display->drawString(x, currentY, "No Telemetry"); return; } const auto &m = telemetry.variant.air_quality_metrics; // Check if any telemetry field has valid data bool hasAny = m.has_pm10_standard || m.has_pm25_standard || m.has_pm100_standard || m.has_co2; if (!hasAny) { display->drawString(x, currentY, "No Telemetry"); return; } // === First line: Show sender name + time since received (left), and first metric (right) === const char *sender = getSenderShortName(*lastMeasurementPacket); uint32_t agoSecs = service->GetTimeSinceMeshPacket(lastMeasurementPacket); String agoStr = (agoSecs > 864000) ? "?" : (agoSecs > 3600) ? String(agoSecs / 3600) + "h" : (agoSecs > 60) ? String(agoSecs / 60) + "m" : String(agoSecs) + "s"; String leftStr = String(sender) + " (" + agoStr + ")"; display->drawString(x, currentY, leftStr); // Left side: who and when // === Collect sensor readings as label strings (no icons) === std::vector entries; if (m.has_pm10_standard) entries.push_back("PM1: " + String(m.pm10_standard) + "ug/m3"); if (m.has_pm25_standard) entries.push_back("PM2.5: " + String(m.pm25_standard) + "ug/m3"); if (m.has_pm100_standard) entries.push_back("PM10: " + String(m.pm100_standard) + "ug/m3"); if (m.has_co2) entries.push_back("CO2: " + String(m.co2) + "ppm"); if (m.has_form_formaldehyde) entries.push_back("HCHO: " + String(m.form_formaldehyde) + "ppb"); // === Show first available metric on top-right of first line === if (!entries.empty()) { String valueStr = entries.front(); int rightX = SCREEN_WIDTH - display->getStringWidth(valueStr); display->drawString(rightX, currentY, valueStr); entries.erase(entries.begin()); // Remove from queue } // === Advance to next line for remaining telemetry entries === currentY += rowHeight; // === Draw remaining entries in 2-column format (left and right) === for (size_t i = 0; i < entries.size(); i += 2) { // Left column display->drawString(x, currentY, entries[i]); // Right column if it exists if (i + 1 < entries.size()) { int rightX = SCREEN_WIDTH / 2; display->drawString(rightX, currentY, entries[i + 1]); } currentY += rowHeight; } graphics::drawCommonFooter(display, x, y); } #endif bool AirQualityTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t) { if (t->which_variant == meshtastic_Telemetry_air_quality_metrics_tag) { #if defined(DEBUG_PORT) && !defined(DEBUG_MUTE) const char *sender = getSenderShortName(mp); if (t->variant.air_quality_metrics.has_pm10_standard) LOG_INFO("(Received from %s): pm10_standard=%i, pm25_standard=%i, " "pm100_standard=%i", sender, t->variant.air_quality_metrics.pm10_standard, t->variant.air_quality_metrics.pm25_standard, t->variant.air_quality_metrics.pm100_standard); if (t->variant.air_quality_metrics.has_co2) LOG_INFO("CO2=%i, CO2_T=%.2f, CO2_H=%.2f", t->variant.air_quality_metrics.co2, t->variant.air_quality_metrics.co2_temperature, t->variant.air_quality_metrics.co2_humidity); if (t->variant.air_quality_metrics.has_form_formaldehyde) LOG_INFO("HCHO=%.2f, HCHO_T=%.2f, HCHO_H=%.2f", t->variant.air_quality_metrics.form_formaldehyde, t->variant.air_quality_metrics.form_temperature, t->variant.air_quality_metrics.form_humidity); #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) { // Note: this is different to the case in EnvironmentTelemetryModule // There, if any sensor fails to read - valid = false. bool valid = false; bool hasSensor = false; m->time = getTime(); m->which_variant = meshtastic_Telemetry_air_quality_metrics_tag; m->variant.air_quality_metrics = meshtastic_AirQualityMetrics_init_zero; bool sensor_get = false; for (TelemetrySensor *sensor : sensors) { LOG_DEBUG("Reading %s", sensor->sensorName); // Note - this function doesn't get properly called if within a conditional sensor_get = sensor->getMetrics(m); valid = valid || sensor_get; hasSensor = true; } return valid && hasSensor; } meshtastic_MeshPacket *AirQualityTelemetryModule::allocReply() { if (currentRequest) { if (isMultiHopBroadcastRequest() && !isSensorOrRouterRole()) { ignoreRequest = true; return NULL; } auto req = *currentRequest; const auto &p = req.decoded; meshtastic_Telemetry scratch; meshtastic_Telemetry *decoded = NULL; memset(&scratch, 0, sizeof(scratch)); if (pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &scratch)) { decoded = &scratch; } else { LOG_ERROR("Error decoding AirQualityTelemetry module!"); return NULL; } // Check for a request for air quality metrics if (decoded->which_variant == meshtastic_Telemetry_air_quality_metrics_tag) { meshtastic_Telemetry m = meshtastic_Telemetry_init_zero; if (getAirQualityTelemetry(&m)) { LOG_INFO("Air quality telemetry reply to request"); return allocDataProtobuf(m); } else { return NULL; } } } return NULL; } bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) { meshtastic_Telemetry m = meshtastic_Telemetry_init_zero; m.which_variant = meshtastic_Telemetry_air_quality_metrics_tag; m.time = getTime(); if (getAirQualityTelemetry(&m)) { bool hasAnyPM = m.variant.air_quality_metrics.has_pm10_standard || m.variant.air_quality_metrics.has_pm25_standard || m.variant.air_quality_metrics.has_pm100_standard || m.variant.air_quality_metrics.has_pm10_environmental || m.variant.air_quality_metrics.has_pm25_environmental || m.variant.air_quality_metrics.has_pm100_environmental; if (hasAnyPM) { LOG_INFO("Send: pm10_standard=%u, pm25_standard=%u, pm100_standard=%u", m.variant.air_quality_metrics.pm10_standard, m.variant.air_quality_metrics.pm25_standard, m.variant.air_quality_metrics.pm100_standard); if (m.variant.air_quality_metrics.has_pm10_environmental) LOG_INFO("pm10_environmental=%u, pm25_environmental=%u, pm100_environmental=%u", m.variant.air_quality_metrics.pm10_environmental, m.variant.air_quality_metrics.pm25_environmental, m.variant.air_quality_metrics.pm100_environmental); } bool hasAnyCO2 = m.variant.air_quality_metrics.has_co2 || m.variant.air_quality_metrics.has_co2_temperature || m.variant.air_quality_metrics.has_co2_humidity; if (hasAnyCO2) { LOG_INFO("Send: co2=%i, co2_t=%.2f, co2_rh=%.2f", m.variant.air_quality_metrics.co2, m.variant.air_quality_metrics.co2_temperature, m.variant.air_quality_metrics.co2_humidity); } bool hasAnyHCHO = m.variant.air_quality_metrics.has_form_formaldehyde || m.variant.air_quality_metrics.has_form_temperature || m.variant.air_quality_metrics.has_form_humidity; if (hasAnyHCHO) { LOG_INFO("Send: hcho=%.2f, hcho_t=%.2f, hcho_rh=%.2f", m.variant.air_quality_metrics.form_formaldehyde, m.variant.air_quality_metrics.form_temperature, m.variant.air_quality_metrics.form_humidity); } 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) { meshtastic_ClientNotification *notification = clientNotificationPool.allocZeroed(); notification->level = meshtastic_LogRecord_Level_INFO; notification->time = getValidTime(RTCQualityFromNet); sprintf(notification->message, "Sending telemetry and sleeping for %us interval in a moment", Default::getConfiguredOrDefaultMs(moduleConfig.telemetry.air_quality_interval, default_telemetry_broadcast_interval_secs) / 1000U); service->sendClientNotification(notification); sleepOnNextExecution = true; LOG_DEBUG("Start next execution in 5s, then sleep"); setIntervalFromNow(FIVE_SECONDS_MS); } if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR && config.power.is_power_saving) { meshtastic_ClientNotification *notification = clientNotificationPool.allocZeroed(); notification->level = meshtastic_LogRecord_Level_INFO; notification->time = getValidTime(RTCQualityFromNet); sprintf(notification->message, "Sending telemetry and sleeping for %us interval in a moment", Default::getConfiguredOrDefaultMs(moduleConfig.telemetry.air_quality_interval, default_telemetry_broadcast_interval_secs) / 1000U); service->sendClientNotification(notification); sleepOnNextExecution = true; LOG_DEBUG("Start next execution in 5s, then sleep"); setIntervalFromNow(FIVE_SECONDS_MS); } } return true; } return false; } AdminMessageHandleResult AirQualityTelemetryModule::handleAdminMessageForModule(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, meshtastic_AdminMessage *response) { AdminMessageHandleResult result = AdminMessageHandleResult::NOT_HANDLED; for (TelemetrySensor *sensor : sensors) { result = sensor->handleAdminMessage(mp, request, response); if (result != AdminMessageHandleResult::NOT_HANDLED) return result; } return result; } #endif ================================================ FILE: src/modules/Telemetry/AirQualityTelemetry.h ================================================ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR #pragma once #include "BaseTelemetryModule.h" #ifndef AIR_QUALITY_TELEMETRY_MODULE_ENABLE #define AIR_QUALITY_TELEMETRY_MODULE_ENABLE 0 #endif #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "NodeDB.h" #include "ProtobufModule.h" #include "detect/ScanI2CConsumer.h" #include #include class AirQualityTelemetryModule : private concurrency::OSThread, public ScanI2CConsumer, public BaseTelemetryModule, public ProtobufModule { CallbackObserver nodeStatusObserver = CallbackObserver(this, &AirQualityTelemetryModule::handleStatusUpdate); public: AirQualityTelemetryModule() : concurrency::OSThread("AirQualityTelemetry"), ScanI2CConsumer(), ProtobufModule("AirQualityTelemetry", 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 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); virtual AdminMessageHandleResult handleAdminMessageForModule(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, meshtastic_AdminMessage *response) override; void i2cScanFinished(ScanI2C *i2cScanner); private: bool firstTime = true; meshtastic_MeshPacket *lastMeasurementPacket; uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute // uint32_t sendToPhoneIntervalMs = 1000; // Send to phone every minute uint32_t lastSentToPhone = 0; }; #endif ================================================ FILE: src/modules/Telemetry/BaseTelemetryModule.h ================================================ #pragma once #include "NodeDB.h" #include "configuration.h" class BaseTelemetryModule { protected: bool isSensorOrRouterRole() const { return config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR || config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER || config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE; } }; ================================================ FILE: src/modules/Telemetry/DeviceTelemetry.cpp ================================================ #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 "TransmitHistory.h" #include "configuration.h" #include "main.h" #include "memGet.h" #include #include #include #define MAGIC_USB_BATTERY_LEVEL 101 static constexpr uint16_t TX_HISTORY_KEY_DEVICE_TELEMETRY = 0x8001; int32_t DeviceTelemetryModule::runOnce() { refreshUptime(); uint32_t lastTelemetry = transmitHistory ? transmitHistory->getLastSentToMeshMillis(TX_HISTORY_KEY_DEVICE_TELEMETRY) : 0; bool isImpoliteRole = isSensorOrRouterRole(); if (((lastTelemetry == 0) || ((uptimeLastMs - lastTelemetry) >= Default::getConfiguredOrDefaultMsScaled(moduleConfig.telemetry.device_update_interval, default_telemetry_broadcast_interval_secs, numOnlineNodes))) && airTime->isTxAllowedChannelUtil(!isImpoliteRole) && airTime->isTxAllowedAirUtil() && config.device.role != meshtastic_Config_DeviceConfig_Role_CLIENT_HIDDEN && moduleConfig.telemetry.device_telemetry_enabled) { sendTelemetry(); if (transmitHistory) transmitHistory->setLastSentToMesh(TX_HISTORY_KEY_DEVICE_TELEMETRY); } 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) { if (t->which_variant == meshtastic_Telemetry_device_metrics_tag) { #if defined(DEBUG_PORT) && !defined(DEBUG_MUTE) 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) { if (isMultiHopBroadcastRequest() && !isSensorOrRouterRole()) { ignoreRequest = true; return NULL; } auto req = *currentRequest; const auto &p = req.decoded; meshtastic_Telemetry scratch; meshtastic_Telemetry *decoded = NULL; memset(&scratch, 0, sizeof(scratch)); if (pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &scratch)) { decoded = &scratch; } else { LOG_ERROR("Error decoding DeviceTelemetry module!"); return NULL; } // Check for a request for device metrics if (decoded->which_variant == meshtastic_Telemetry_device_metrics_tag) { LOG_INFO("Device telemetry reply to request"); return allocDataProtobuf(getDeviceTelemetry()); } else if (decoded->which_variant == meshtastic_Telemetry_local_stats_tag) { LOG_INFO("Device telemetry reply w/ LocalStats to request"); return allocDataProtobuf(getLocalStatsTelemetry()); } } return NULL; } meshtastic_Telemetry DeviceTelemetryModule::getDeviceTelemetry() { meshtastic_Telemetry t = meshtastic_Telemetry_init_zero; t.which_variant = meshtastic_Telemetry_device_metrics_tag; t.time = getTime(); t.variant.device_metrics = meshtastic_DeviceMetrics_init_zero; t.variant.device_metrics.has_air_util_tx = true; t.variant.device_metrics.has_battery_level = true; t.variant.device_metrics.has_channel_utilization = true; t.variant.device_metrics.has_voltage = true; t.variant.device_metrics.has_uptime_seconds = true; t.variant.device_metrics.air_util_tx = airTime->utilizationTXPercent(); t.variant.device_metrics.battery_level = (!powerStatus->getHasBattery() || powerStatus->getIsCharging()) ? MAGIC_USB_BATTERY_LEVEL : powerStatus->getBatteryChargePercent(); t.variant.device_metrics.channel_utilization = airTime->channelUtilizationPercent(); t.variant.device_metrics.voltage = powerStatus->getBatteryVoltageMv() / 1000.0; t.variant.device_metrics.uptime_seconds = getUptimeSeconds(); return t; } meshtastic_Telemetry DeviceTelemetryModule::getLocalStatsTelemetry() { meshtastic_Telemetry telemetry = meshtastic_Telemetry_init_zero; telemetry.which_variant = meshtastic_Telemetry_local_stats_tag; telemetry.variant.local_stats = meshtastic_LocalStats_init_zero; telemetry.time = getTime(); telemetry.variant.local_stats.uptime_seconds = getUptimeSeconds(); telemetry.variant.local_stats.channel_utilization = airTime->channelUtilizationPercent(); telemetry.variant.local_stats.air_util_tx = airTime->utilizationTXPercent(); telemetry.variant.local_stats.num_online_nodes = numOnlineNodes; telemetry.variant.local_stats.num_total_nodes = nodeDB->getNumMeshNodes(); if (RadioLibInterface::instance) { telemetry.variant.local_stats.num_packets_tx = RadioLibInterface::instance->txGood; telemetry.variant.local_stats.num_packets_rx = RadioLibInterface::instance->rxGood + RadioLibInterface::instance->rxBad; telemetry.variant.local_stats.num_packets_rx_bad = RadioLibInterface::instance->rxBad; telemetry.variant.local_stats.num_tx_relay = RadioLibInterface::instance->txRelay; telemetry.variant.local_stats.num_tx_dropped = RadioLibInterface::instance->txDrop; } #ifdef ARCH_PORTDUINO if (SimRadio::instance) { telemetry.variant.local_stats.num_packets_tx = SimRadio::instance->txGood; telemetry.variant.local_stats.num_packets_rx = SimRadio::instance->rxGood + SimRadio::instance->rxBad; telemetry.variant.local_stats.num_packets_rx_bad = SimRadio::instance->rxBad; telemetry.variant.local_stats.num_tx_relay = SimRadio::instance->txRelay; telemetry.variant.local_stats.num_tx_dropped = SimRadio::instance->txDrop; } #else telemetry.variant.local_stats.heap_total_bytes = memGet.getHeapSize(); telemetry.variant.local_stats.heap_free_bytes = memGet.getFreeHeap(); #endif if (router) { telemetry.variant.local_stats.num_rx_dupe = router->rxDupe; telemetry.variant.local_stats.num_tx_relay_canceled = router->txRelayCanceled; } LOG_INFO("Sending local stats: uptime=%i, channel_utilization=%f, air_util_tx=%f, num_online_nodes=%i, num_total_nodes=%i", telemetry.variant.local_stats.uptime_seconds, telemetry.variant.local_stats.channel_utilization, telemetry.variant.local_stats.air_util_tx, telemetry.variant.local_stats.num_online_nodes, telemetry.variant.local_stats.num_total_nodes); LOG_INFO("num_packets_tx=%i, num_packets_rx=%i, num_packets_rx_bad=%i", telemetry.variant.local_stats.num_packets_tx, telemetry.variant.local_stats.num_packets_rx, telemetry.variant.local_stats.num_packets_rx_bad); return telemetry; } void DeviceTelemetryModule::sendLocalStatsToPhone() { meshtastic_MeshPacket *p = allocDataProtobuf(getLocalStatsTelemetry()); p->to = NODENUM_BROADCAST; p->decoded.want_response = false; p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; service->sendToPhone(p); } bool DeviceTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) { meshtastic_Telemetry telemetry = getDeviceTelemetry(); LOG_INFO("Send: air_util_tx=%f, channel_utilization=%f, battery_level=%i, voltage=%f, uptime=%i", telemetry.variant.device_metrics.air_util_tx, telemetry.variant.device_metrics.channel_utilization, telemetry.variant.device_metrics.battery_level, telemetry.variant.device_metrics.voltage, telemetry.variant.device_metrics.uptime_seconds); DEBUG_HEAP_BEFORE; meshtastic_MeshPacket *p = allocDataProtobuf(telemetry); DEBUG_HEAP_AFTER("DeviceTelemetryModule::sendTelemetry", p); p->to = dest; p->decoded.want_response = false; p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; nodeDB->updateTelemetry(nodeDB->getNodeNum(), telemetry, RX_SRC_LOCAL); if (phoneOnly) { LOG_INFO("Send packet to phone"); service->sendToPhone(p); } else { LOG_INFO("Send packet to mesh"); service->sendToMesh(p, RX_SRC_LOCAL, true); } return true; } ================================================ FILE: src/modules/Telemetry/DeviceTelemetry.h ================================================ #pragma once #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "BaseTelemetryModule.h" #include "NodeDB.h" #include "ProtobufModule.h" #include #include class DeviceTelemetryModule : private concurrency::OSThread, public BaseTelemetryModule, public ProtobufModule { CallbackObserver nodeStatusObserver = CallbackObserver(this, &DeviceTelemetryModule::handleStatusUpdate); public: DeviceTelemetryModule() : concurrency::OSThread("DeviceTelemetry"), ProtobufModule("DeviceTelemetry", meshtastic_PortNum_TELEMETRY_APP, &meshtastic_Telemetry_msg) { uptimeWrapCount = 0; uptimeLastMs = millis(); nodeStatusObserver.observe(&nodeStatus->onNewStatus); setIntervalFromNow(setStartDelay()); // Wait until NodeInfo is sent } virtual bool wantUIFrame() { return false; } 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(); meshtastic_Telemetry getLocalStatsTelemetry(); void sendLocalStatsToPhone(); uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute uint32_t sendStatsToPhoneIntervalMs = 15 * SECONDS_IN_MINUTE * 1000; // Send stats to phone every 15 minutes uint32_t lastSentStatsToPhone = 0; 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; }; ================================================ FILE: src/modules/Telemetry/EnvironmentTelemetry.cpp ================================================ #include "configuration.h" #if HAS_TELEMETRY && !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 "TransmitHistory.h" #include "UnitConversions.h" #include "buzz.h" #include "graphics/SharedUIDisplay.h" #include "graphics/images.h" #include "main.h" #include "modules/ExternalNotificationModule.h" #include "power.h" #include "sleep.h" #include "target_specific.h" #include #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR_EXTERNAL // Sensors #include "Sensor/CGRadSensSensor.h" #include "Sensor/RCWL9620Sensor.h" #include "Sensor/nullSensor.h" namespace graphics { extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool force_no_invert, bool show_date); } #if __has_include() #include "Sensor/AHT10.h" #endif #if __has_include() #include "Sensor/BME280Sensor.h" #endif #if __has_include() #include "Sensor/BMP085Sensor.h" #endif #if __has_include() #include "Sensor/BMP280Sensor.h" #endif #if __has_include() #include "Sensor/LTR390UVSensor.h" #endif #if __has_include() || __has_include() #include "Sensor/BME680Sensor.h" #endif #if __has_include() #include "Sensor/DPS310Sensor.h" #endif #if __has_include() #include "Sensor/MCP9808Sensor.h" #endif #if __has_include() #include "Sensor/SHT31Sensor.h" #endif #if __has_include() #include "Sensor/LPS22HBSensor.h" #endif #if __has_include() #include "Sensor/SHTC3Sensor.h" #endif #if __has_include("RAK12035_SoilMoisture.h") && defined(RAK_4631) && RAK_4631 == 1 #include "Sensor/RAK12035Sensor.h" #endif #if __has_include() #include "Sensor/VEML7700Sensor.h" #endif #if __has_include() #include "Sensor/TSL2591Sensor.h" #endif #if __has_include() #include "Sensor/OPT3001Sensor.h" #endif #if __has_include() #include "Sensor/SHT4XSensor.h" #endif #if __has_include() #include "Sensor/MLX90632Sensor.h" #endif #if __has_include() #include "Sensor/DFRobotLarkSensor.h" #endif #if __has_include() #include "Sensor/DFRobotGravitySensor.h" #endif #if __has_include() #include "Sensor/NAU7802Sensor.h" #endif #if __has_include() #include "Sensor/BMP3XXSensor.h" #endif #if __has_include() #include "Sensor/PCT2075Sensor.h" #endif #endif #ifdef T1000X_SENSOR_EN #include "Sensor/T1000xSensor.h" #endif #ifdef SENSECAP_INDICATOR #include "Sensor/IndicatorSensor.h" #endif #if __has_include() #include "Sensor/TSL2561Sensor.h" #endif #if __has_include() #include "Sensor/BH1750Sensor.h" #endif #define FAILED_STATE_SENSOR_READ_MULTIPLIER 10 #define DISPLAY_RECEIVEID_MEASUREMENTS_ON_SCREEN true #include "Sensor/AddI2CSensorTemplate.h" #include "graphics/ScreenFonts.h" #include static constexpr uint16_t TX_HISTORY_KEY_ENVIRONMENT_TELEMETRY = 0x8002; void EnvironmentTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner) { if (!moduleConfig.telemetry.environment_measurement_enabled && !ENVIRONMENTAL_TELEMETRY_MODULE_ENABLE) { return; } LOG_INFO("Environment Telemetry adding I2C devices..."); // order by priority of metrics/values (low top, high bottom) #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR #ifdef T1000X_SENSOR_EN // Not a real I2C device addSensor(i2cScanner, ScanI2C::DeviceType::NONE); #else #ifdef SENSECAP_INDICATOR // Not a real I2C device, uses UART addSensor(i2cScanner, ScanI2C::DeviceType::NONE); #endif addSensor(i2cScanner, ScanI2C::DeviceType::RCWL9620); addSensor(i2cScanner, ScanI2C::DeviceType::CGRADSENS); #endif #endif #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR_EXTERNAL #if __has_include() addSensor(i2cScanner, ScanI2C::DeviceType::DFROBOT_LARK); #endif #if __has_include() addSensor(i2cScanner, ScanI2C::DeviceType::DFROBOT_RAIN); #endif #if __has_include() addSensor(i2cScanner, ScanI2C::DeviceType::AHT10); #endif #if __has_include() addSensor(i2cScanner, ScanI2C::DeviceType::BMP_085); #endif #if __has_include() addSensor(i2cScanner, ScanI2C::DeviceType::BME_280); #endif #if __has_include() addSensor(i2cScanner, ScanI2C::DeviceType::LTR390UV); #endif #if __has_include() || __has_include() addSensor(i2cScanner, ScanI2C::DeviceType::BME_680); #endif #if __has_include() addSensor(i2cScanner, ScanI2C::DeviceType::BMP_280); #endif #if __has_include() addSensor(i2cScanner, ScanI2C::DeviceType::DPS310); #endif #if __has_include() addSensor(i2cScanner, ScanI2C::DeviceType::MCP9808); #endif #if __has_include() addSensor(i2cScanner, ScanI2C::DeviceType::SHT31); #endif #if __has_include() addSensor(i2cScanner, ScanI2C::DeviceType::LPS22HB); #endif #if __has_include() addSensor(i2cScanner, ScanI2C::DeviceType::SHTC3); #endif #if __has_include("RAK12035_SoilMoisture.h") && defined(RAK_4631) && RAK_4631 == 1 addSensor(i2cScanner, ScanI2C::DeviceType::RAK12035); #endif #if __has_include() addSensor(i2cScanner, ScanI2C::DeviceType::VEML7700); #endif #if __has_include() addSensor(i2cScanner, ScanI2C::DeviceType::TSL2591); #endif #if __has_include() addSensor(i2cScanner, ScanI2C::DeviceType::OPT3001); #endif #if __has_include() addSensor(i2cScanner, ScanI2C::DeviceType::SHT4X); #endif #if __has_include() addSensor(i2cScanner, ScanI2C::DeviceType::MLX90632); #endif #if __has_include() addSensor(i2cScanner, ScanI2C::DeviceType::BMP_3XX); #endif #if __has_include() addSensor(i2cScanner, ScanI2C::DeviceType::PCT2075); #endif #if __has_include() addSensor(i2cScanner, ScanI2C::DeviceType::TSL2561); #endif #if __has_include() addSensor(i2cScanner, ScanI2C::DeviceType::NAU7802); #endif #if __has_include() addSensor(i2cScanner, ScanI2C::DeviceType::BH1750); #endif #endif } int32_t EnvironmentTelemetryModule::runOnce() { if (sleepOnNextExecution == true) { sleepOnNextExecution = false; uint32_t nightyNightMs = Default::getConfiguredOrDefaultMs(moduleConfig.telemetry.environment_update_interval, default_telemetry_broadcast_interval_secs); LOG_DEBUG("Sleep for %ims, then awake to send metrics again", nightyNightMs); doDeepSleep(nightyNightMs, true, false); } 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 || ENVIRONMENTAL_TELEMETRY_MODULE_ENABLE)) { // If this module is not enabled, and the user doesn't want the display screen don't waste any OSThread time on it return disable(); } if (firstTime) { // This is the first time the OSThread library has called this function, so do some setup firstTime = 0; if (moduleConfig.telemetry.environment_measurement_enabled || ENVIRONMENTAL_TELEMETRY_MODULE_ENABLE) { LOG_INFO("Environment Telemetry: init"); // check if we have at least one sensor if (!sensors.empty()) { result = DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; } #ifdef T1000X_SENSOR_EN #elif !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR_EXTERNAL if (ina219Sensor.hasSensor()) result = ina219Sensor.runOnce(); if (ina260Sensor.hasSensor()) result = ina260Sensor.runOnce(); if (ina3221Sensor.hasSensor()) result = ina3221Sensor.runOnce(); if (max17048Sensor.hasSensor()) result = max17048Sensor.runOnce(); // this only works on the wismesh hub with the solar option. This is not an I2C sensor, so we don't need the // sensormap here. #ifdef HAS_RAKPROT if (rak9154Sensor.hasSensor()) result = rak9154Sensor.runOnce(); #endif #endif } // it's possible to have this module enabled, only for displaying values on the screen. // therefore, we should only enable the sensor loop if measurement is also enabled return result == UINT32_MAX ? disable() : setStartDelay(); } else { // if we somehow got to a second run of this module with measurement disabled, then just wait forever if (!moduleConfig.telemetry.environment_measurement_enabled && !ENVIRONMENTAL_TELEMETRY_MODULE_ENABLE) { return disable(); } for (TelemetrySensor *sensor : sensors) { uint32_t delay = sensor->runOnce(); if (delay < result) { result = delay; } } uint32_t lastTelemetry = transmitHistory ? transmitHistory->getLastSentToMeshMillis(TX_HISTORY_KEY_ENVIRONMENT_TELEMETRY) : 0; if (((lastTelemetry == 0) || !Throttle::isWithinTimespanMs(lastTelemetry, 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(); if (transmitHistory) transmitHistory->setLastSentToMesh(TX_HISTORY_KEY_ENVIRONMENT_TELEMETRY); } 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; } #if HAS_SCREEN void EnvironmentTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { // === Setup display === display->clear(); display->setFont(FONT_SMALL); display->setTextAlignment(TEXT_ALIGN_LEFT); int line = 1; // === Set Title const char *titleStr = (graphics::currentResolution == graphics::ScreenResolution::High) ? "Environment" : "Env."; // === Header === graphics::drawCommonHeader(display, x, y, titleStr); // === Row spacing setup === const int rowHeight = FONT_HEIGHT_SMALL - 4; int currentY = graphics::getTextPositions(display)[line++]; // === Show "No Telemetry" if no data available === if (!lastMeasurementPacket) { display->drawString(x, currentY, "No Telemetry"); return; } // Decode the telemetry message from the latest received packet const meshtastic_Data &p = lastMeasurementPacket->decoded; meshtastic_Telemetry telemetry; if (!pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &telemetry)) { display->drawString(x, currentY, "No Telemetry"); return; } const auto &m = telemetry.variant.environment_metrics; // Check if any telemetry field has valid data bool hasAny = m.has_temperature || m.has_relative_humidity || m.barometric_pressure != 0 || m.iaq != 0 || m.voltage != 0 || m.current != 0 || m.lux != 0 || m.white_lux != 0 || m.weight != 0 || m.distance != 0 || m.radiation != 0; if (!hasAny) { display->drawString(x, currentY, "No Telemetry"); return; } // === First line: Show sender name + time since received (left), and first metric (right) === const char *sender = getSenderShortName(*lastMeasurementPacket); uint32_t agoSecs = service->GetTimeSinceMeshPacket(lastMeasurementPacket); String agoStr = (agoSecs > 864000) ? "?" : (agoSecs > 3600) ? String(agoSecs / 3600) + "h" : (agoSecs > 60) ? String(agoSecs / 60) + "m" : String(agoSecs) + "s"; String leftStr = String(sender) + " (" + agoStr + ")"; display->drawString(x, currentY, leftStr); // Left side: who and when // === Collect sensor readings as label strings (no icons) === std::vector entries; if (m.has_temperature) { String tempStr = moduleConfig.telemetry.environment_display_fahrenheit ? "Tmp: " + String(UnitConversions::CelsiusToFahrenheit(m.temperature), 1) + "°F" : "Tmp: " + String(m.temperature, 1) + "°C"; entries.push_back(tempStr); } if (m.has_relative_humidity) entries.push_back("Hum: " + String(m.relative_humidity, 0) + "%"); if (m.barometric_pressure != 0) entries.push_back("Prss: " + String(m.barometric_pressure, 0) + " hPa"); if (m.iaq != 0) { String aqi = "IAQ: " + String(m.iaq); const char *bannerMsg = nullptr; // Default: no banner if (m.iaq <= 25) aqi += " (Excellent)"; else if (m.iaq <= 50) aqi += " (Good)"; else if (m.iaq <= 100) aqi += " (Moderate)"; else if (m.iaq <= 150) aqi += " (Poor)"; else if (m.iaq <= 200) { aqi += " (Unhealthy)"; bannerMsg = "Unhealthy IAQ"; } else if (m.iaq <= 300) { aqi += " (Very Unhealthy)"; bannerMsg = "Very Unhealthy IAQ"; } else { aqi += " (Hazardous)"; bannerMsg = "Hazardous IAQ"; } entries.push_back(aqi); // === IAQ alert logic === static uint32_t lastAlertTime = 0; uint32_t now = millis(); bool isOwnTelemetry = lastMeasurementPacket->from == nodeDB->getNodeNum(); bool isCooldownOver = (now - lastAlertTime > 60000); if (isOwnTelemetry && bannerMsg && isCooldownOver) { LOG_INFO("drawFrame: IAQ %d (own) — showing banner: %s", m.iaq, bannerMsg); screen->showSimpleBanner(bannerMsg, 3000); // Only buzz if IAQ is over 200 if (m.iaq > 200 && moduleConfig.external_notification.enabled && !externalNotificationModule->getMute()) { playLongBeep(); } lastAlertTime = now; } } if (m.voltage != 0 || m.current != 0) entries.push_back(String(m.voltage, 1) + "V / " + String(m.current, 0) + "mA"); if (m.lux != 0) entries.push_back("Light: " + String(m.lux, 0) + "lx"); if (m.white_lux != 0) entries.push_back("White: " + String(m.white_lux, 0) + "lx"); if (m.weight != 0) entries.push_back("Weight: " + String(m.weight, 0) + "kg"); if (m.distance != 0) entries.push_back("Level: " + String(m.distance, 0) + "mm"); if (m.radiation != 0) entries.push_back("Rad: " + String(m.radiation, 2) + " µR/h"); // === Show first available metric on top-right of first line === if (!entries.empty()) { String valueStr = entries.front(); int rightX = SCREEN_WIDTH - display->getStringWidth(valueStr); display->drawString(rightX, currentY, valueStr); entries.erase(entries.begin()); // Remove from queue } // === Advance to next line for remaining telemetry entries === currentY += rowHeight; // === Draw remaining entries in 2-column format (left and right) === for (size_t i = 0; i < entries.size(); i += 2) { // Left column display->drawString(x, currentY, entries[i]); // Right column if it exists if (i + 1 < entries.size()) { int rightX = SCREEN_WIDTH / 2; display->drawString(rightX, currentY, entries[i + 1]); } currentY += rowHeight; } graphics::drawCommonFooter(display, x, y); } #endif bool EnvironmentTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t) { if (t->which_variant == meshtastic_Telemetry_environment_metrics_tag) { #if defined(DEBUG_PORT) && !defined(DEBUG_MUTE) const char *sender = getSenderShortName(mp); LOG_INFO("(Received from %s): barometric_pressure=%f, current=%f, gas_resistance=%f, relative_humidity=%f, " "temperature=%f", sender, t->variant.environment_metrics.barometric_pressure, t->variant.environment_metrics.current, t->variant.environment_metrics.gas_resistance, t->variant.environment_metrics.relative_humidity, t->variant.environment_metrics.temperature); LOG_INFO("(Received from %s): voltage=%f, IAQ=%d, distance=%f, lux=%f, white_lux=%f", sender, t->variant.environment_metrics.voltage, t->variant.environment_metrics.iaq, t->variant.environment_metrics.distance, t->variant.environment_metrics.lux, t->variant.environment_metrics.white_lux); LOG_INFO("(Received from %s): wind speed=%fm/s, direction=%d degrees, weight=%fkg", sender, t->variant.environment_metrics.wind_speed, t->variant.environment_metrics.wind_direction, t->variant.environment_metrics.weight); LOG_INFO("(Received from %s): radiation=%fµR/h", sender, t->variant.environment_metrics.radiation); #endif // release previous packet before occupying a new spot if (lastMeasurementPacket != nullptr) packetPool.release(lastMeasurementPacket); lastMeasurementPacket = packetPool.allocCopy(mp); } return false; // Let others look at this message also if they want } bool EnvironmentTelemetryModule::getEnvironmentTelemetry(meshtastic_Telemetry *m) { bool valid = false; bool hasSensor = false; // getMetrics() doesn't always get evaluated because of // short-circuit evaluation rules in c++ bool get_metrics; m->time = getTime(); m->which_variant = meshtastic_Telemetry_environment_metrics_tag; m->variant.environment_metrics = meshtastic_EnvironmentMetrics_init_zero; for (TelemetrySensor *sensor : sensors) { get_metrics = sensor->getMetrics(m); // avoid short-circuit evaluation rules valid = valid || get_metrics; hasSensor = true; } #ifndef T1000X_SENSOR_EN if (ina219Sensor.hasSensor()) { get_metrics = ina219Sensor.getMetrics(m); valid = valid || get_metrics; hasSensor = true; } if (ina260Sensor.hasSensor()) { get_metrics = ina260Sensor.getMetrics(m); valid = valid || get_metrics; hasSensor = true; } if (ina3221Sensor.hasSensor()) { get_metrics = ina3221Sensor.getMetrics(m); valid = valid || get_metrics; hasSensor = true; } if (max17048Sensor.hasSensor()) { get_metrics = max17048Sensor.getMetrics(m); valid = valid || get_metrics; hasSensor = true; } #endif #ifdef HAS_RAKPROT if (rak9154Sensor.hasSensor()) { get_metrics = rak9154Sensor.getMetrics(m); valid = valid || get_metrics; hasSensor = true; } #endif return valid && hasSensor; } meshtastic_MeshPacket *EnvironmentTelemetryModule::allocReply() { if (currentRequest) { if (isMultiHopBroadcastRequest() && !isSensorOrRouterRole()) { ignoreRequest = true; return NULL; } auto req = *currentRequest; const auto &p = req.decoded; meshtastic_Telemetry scratch; meshtastic_Telemetry *decoded = NULL; memset(&scratch, 0, sizeof(scratch)); if (pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &scratch)) { decoded = &scratch; } else { LOG_ERROR("Error decoding EnvironmentTelemetry module!"); return NULL; } // Check for a request for environment metrics if (decoded->which_variant == meshtastic_Telemetry_environment_metrics_tag) { meshtastic_Telemetry m = meshtastic_Telemetry_init_zero; if (getEnvironmentTelemetry(&m)) { LOG_INFO("Environment telemetry reply to request"); return allocDataProtobuf(m); } else { return NULL; } } } return NULL; } bool EnvironmentTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) { meshtastic_Telemetry m = meshtastic_Telemetry_init_zero; m.which_variant = meshtastic_Telemetry_environment_metrics_tag; m.time = getTime(); if (getEnvironmentTelemetry(&m)) { LOG_INFO("Send: barometric_pressure=%f, current=%f, gas_resistance=%f, relative_humidity=%f, temperature=%f", m.variant.environment_metrics.barometric_pressure, m.variant.environment_metrics.current, m.variant.environment_metrics.gas_resistance, m.variant.environment_metrics.relative_humidity, m.variant.environment_metrics.temperature); LOG_INFO("Send: voltage=%f, IAQ=%d, distance=%f, lux=%f", m.variant.environment_metrics.voltage, m.variant.environment_metrics.iaq, m.variant.environment_metrics.distance, m.variant.environment_metrics.lux); LOG_INFO("Send: wind speed=%fm/s, direction=%d degrees, weight=%fkg", m.variant.environment_metrics.wind_speed, m.variant.environment_metrics.wind_direction, m.variant.environment_metrics.weight); LOG_INFO("Send: radiation=%fµR/h", m.variant.environment_metrics.radiation); LOG_INFO("Send: soil_temperature=%f, soil_moisture=%u", m.variant.environment_metrics.soil_temperature, m.variant.environment_metrics.soil_moisture); meshtastic_MeshPacket *p = allocDataProtobuf(m); p->to = dest; p->decoded.want_response = false; if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR) p->priority = meshtastic_MeshPacket_Priority_RELIABLE; else p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; // release previous packet before occupying a new spot if (lastMeasurementPacket != nullptr) packetPool.release(lastMeasurementPacket); lastMeasurementPacket = packetPool.allocCopy(*p); if (phoneOnly) { LOG_INFO("Send packet to phone"); service->sendToPhone(p); } else { LOG_INFO("Send packet to mesh"); service->sendToMesh(p, RX_SRC_LOCAL, true); if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR && config.power.is_power_saving) { meshtastic_ClientNotification *notification = clientNotificationPool.allocZeroed(); notification->level = meshtastic_LogRecord_Level_INFO; notification->time = getValidTime(RTCQualityFromNet); sprintf(notification->message, "Sending telemetry and sleeping for %us interval in a moment", Default::getConfiguredOrDefaultMs(moduleConfig.telemetry.environment_update_interval, default_telemetry_broadcast_interval_secs) / 1000U); service->sendClientNotification(notification); sleepOnNextExecution = true; LOG_DEBUG("Start next execution in 5s, then sleep"); setIntervalFromNow(FIVE_SECONDS_MS); } } return true; } return false; } AdminMessageHandleResult EnvironmentTelemetryModule::handleAdminMessageForModule(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, meshtastic_AdminMessage *response) { AdminMessageHandleResult result = AdminMessageHandleResult::NOT_HANDLED; #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR_EXTERNAL for (TelemetrySensor *sensor : sensors) { result = sensor->handleAdminMessage(mp, request, response); if (result != AdminMessageHandleResult::NOT_HANDLED) return result; } if (ina219Sensor.hasSensor()) { result = ina219Sensor.handleAdminMessage(mp, request, response); if (result != AdminMessageHandleResult::NOT_HANDLED) return result; } if (ina260Sensor.hasSensor()) { result = ina260Sensor.handleAdminMessage(mp, request, response); if (result != AdminMessageHandleResult::NOT_HANDLED) return result; } if (ina3221Sensor.hasSensor()) { result = ina3221Sensor.handleAdminMessage(mp, request, response); if (result != AdminMessageHandleResult::NOT_HANDLED) return result; } if (max17048Sensor.hasSensor()) { result = max17048Sensor.handleAdminMessage(mp, request, response); if (result != AdminMessageHandleResult::NOT_HANDLED) return result; } #endif return result; } #endif ================================================ FILE: src/modules/Telemetry/EnvironmentTelemetry.h ================================================ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR #pragma once #include "BaseTelemetryModule.h" #ifndef ENVIRONMENTAL_TELEMETRY_MODULE_ENABLE #define ENVIRONMENTAL_TELEMETRY_MODULE_ENABLE 0 #endif #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "NodeDB.h" #include "ProtobufModule.h" #include "detect/ScanI2CConsumer.h" #include #include class EnvironmentTelemetryModule : private concurrency::OSThread, public ScanI2CConsumer, public BaseTelemetryModule, public ProtobufModule { CallbackObserver nodeStatusObserver = CallbackObserver(this, &EnvironmentTelemetryModule::handleStatusUpdate); public: EnvironmentTelemetryModule() : concurrency::OSThread("EnvironmentTelemetry"), ScanI2CConsumer(), ProtobufModule("EnvironmentTelemetry", meshtastic_PortNum_TELEMETRY_APP, &meshtastic_Telemetry_msg) { lastMeasurementPacket = nullptr; nodeStatusObserver.observe(&nodeStatus->onNewStatus); setIntervalFromNow(10 * 1000); } virtual bool wantUIFrame() override; #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; void i2cScanFinished(ScanI2C *i2cScanner); private: bool firstTime = 1; meshtastic_MeshPacket *lastMeasurementPacket; uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute uint32_t lastSentToPhone = 0; }; #endif ================================================ FILE: src/modules/Telemetry/HealthTelemetry.cpp ================================================ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && !MESHTASTIC_EXCLUDE_HEALTH_TELEMETRY && !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 "TransmitHistory.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 static constexpr uint16_t TX_HISTORY_KEY_HEALTH_TELEMETRY = 0x8003; int32_t HealthTelemetryModule::runOnce() { if (sleepOnNextExecution == true) { sleepOnNextExecution = false; uint32_t nightyNightMs = Default::getConfiguredOrDefaultMs(moduleConfig.telemetry.health_update_interval, default_telemetry_broadcast_interval_secs); LOG_DEBUG("Sleep for %ims, then awake to send metrics again", nightyNightMs); doDeepSleep(nightyNightMs, true, false); } uint32_t result = UINT32_MAX; if (!(moduleConfig.telemetry.health_measurement_enabled || moduleConfig.telemetry.health_screen_enabled)) { // If this module is not enabled, and the user doesn't want the display screen don't waste any OSThread time on it return disable(); } if (firstTime) { // This is the first time the OSThread library has called this function, so do some setup firstTime = false; if (moduleConfig.telemetry.health_measurement_enabled) { LOG_INFO("Health Telemetry: init"); // Initialize sensors if (mlx90614Sensor.hasSensor()) result = mlx90614Sensor.runOnce(); if (max30102Sensor.hasSensor()) result = max30102Sensor.runOnce(); } return result == UINT32_MAX ? disable() : setStartDelay(); } else { // if we somehow got to a second run of this module with measurement disabled, then just wait forever if (!moduleConfig.telemetry.health_measurement_enabled) { return disable(); } uint32_t lastTelemetry = transmitHistory ? transmitHistory->getLastSentToMeshMillis(TX_HISTORY_KEY_HEALTH_TELEMETRY) : 0; if (((lastTelemetry == 0) || !Throttle::isWithinTimespanMs(lastTelemetry, 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(); if (transmitHistory) transmitHistory->setLastSentToMesh(TX_HISTORY_KEY_HEALTH_TELEMETRY); } else if (((lastSentToPhone == 0) || !Throttle::isWithinTimespanMs(lastSentToPhone, sendToPhoneIntervalMs)) && (service->isToPhoneQueueEmpty())) { // Just send to phone when it's not our time to send to mesh yet // Only send while queue is empty (phone assumed connected) sendTelemetry(NODENUM_BROADCAST, true); lastSentToPhone = millis(); } } return min(sendToPhoneIntervalMs, result); } bool HealthTelemetryModule::wantUIFrame() { return moduleConfig.telemetry.health_screen_enabled; } void HealthTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); if (lastMeasurementPacket == nullptr) { // If there's no valid packet, display "Health" display->drawString(x, y, "Health"); display->drawString(x, y += _fontHeight(FONT_SMALL), "No measurement"); return; } // Decode the last measurement packet meshtastic_Telemetry lastMeasurement; uint32_t agoSecs = service->GetTimeSinceMeshPacket(lastMeasurementPacket); const char *lastSender = getSenderShortName(*lastMeasurementPacket); const meshtastic_Data &p = lastMeasurementPacket->decoded; if (!pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &lastMeasurement)) { display->drawString(x, y, "Measurement Error"); LOG_ERROR("Unable to decode last packet"); return; } // Display "Health From: ..." on its own char headerStr[64]; snprintf(headerStr, sizeof(headerStr), "Health From: %s(%ds)", lastSender, (int)agoSecs); display->drawString(x, y, headerStr); char last_temp[16]; if (moduleConfig.telemetry.environment_display_fahrenheit) { snprintf(last_temp, sizeof(last_temp), "%.0f°F", UnitConversions::CelsiusToFahrenheit(lastMeasurement.variant.health_metrics.temperature)); } else { snprintf(last_temp, sizeof(last_temp), "%.0f°C", lastMeasurement.variant.health_metrics.temperature); } // Continue with the remaining details char tempStr[32]; snprintf(tempStr, sizeof(tempStr), "Temp: %s", last_temp); display->drawString(x, y += _fontHeight(FONT_SMALL), tempStr); if (lastMeasurement.variant.health_metrics.has_heart_bpm) { char heartStr[32]; snprintf(heartStr, sizeof(heartStr), "Heart Rate: %u bpm", lastMeasurement.variant.health_metrics.heart_bpm); display->drawString(x, y += _fontHeight(FONT_SMALL), heartStr); } if (lastMeasurement.variant.health_metrics.has_spO2) { char spo2Str[32]; snprintf(spo2Str, sizeof(spo2Str), "spO2: %u %%", lastMeasurement.variant.health_metrics.spO2); display->drawString(x, y += _fontHeight(FONT_SMALL), spo2Str); } } bool HealthTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t) { if (t->which_variant == meshtastic_Telemetry_health_metrics_tag) { #if defined(DEBUG_PORT) && !defined(DEBUG_MUTE) const char *sender = getSenderShortName(mp); 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 = false; bool hasSensor = false; bool get_metrics; m->time = getTime(); m->which_variant = meshtastic_Telemetry_health_metrics_tag; m->variant.health_metrics = meshtastic_HealthMetrics_init_zero; if (max30102Sensor.hasSensor()) { get_metrics = max30102Sensor.getMetrics(m); valid = valid || get_metrics; // avoid short-circuit evaluation rules hasSensor = true; } if (mlx90614Sensor.hasSensor()) { get_metrics = mlx90614Sensor.getMetrics(m); valid = valid || get_metrics; hasSensor = true; } return valid && hasSensor; } meshtastic_MeshPacket *HealthTelemetryModule::allocReply() { if (currentRequest) { if (isMultiHopBroadcastRequest() && !isSensorOrRouterRole()) { ignoreRequest = true; return NULL; } auto req = *currentRequest; const auto &p = req.decoded; meshtastic_Telemetry scratch; meshtastic_Telemetry *decoded = NULL; memset(&scratch, 0, sizeof(scratch)); if (pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &scratch)) { decoded = &scratch; } else { LOG_ERROR("Error decoding HealthTelemetry module!"); return NULL; } // Check for a request for health metrics if (decoded->which_variant == meshtastic_Telemetry_health_metrics_tag) { meshtastic_Telemetry m = meshtastic_Telemetry_init_zero; if (getHealthTelemetry(&m)) { LOG_INFO("Health telemetry reply to request"); return allocDataProtobuf(m); } else { return NULL; } } } return NULL; } bool HealthTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) { meshtastic_Telemetry m = meshtastic_Telemetry_init_zero; m.which_variant = meshtastic_Telemetry_health_metrics_tag; m.time = getTime(); if (getHealthTelemetry(&m)) { LOG_INFO("Send: temperature=%f, heart_bpm=%d, spO2=%d", m.variant.health_metrics.temperature, m.variant.health_metrics.heart_bpm, m.variant.health_metrics.spO2); sensor_read_error_count = 0; meshtastic_MeshPacket *p = allocDataProtobuf(m); p->to = dest; p->decoded.want_response = false; if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR) p->priority = meshtastic_MeshPacket_Priority_RELIABLE; else p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; // release previous packet before occupying a new spot if (lastMeasurementPacket != nullptr) packetPool.release(lastMeasurementPacket); lastMeasurementPacket = packetPool.allocCopy(*p); if (phoneOnly) { LOG_INFO("Send packet to phone"); service->sendToPhone(p); } else { LOG_INFO("Send packet to mesh"); service->sendToMesh(p, RX_SRC_LOCAL, true); if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR && config.power.is_power_saving) { LOG_DEBUG("Start next execution in 5s, then sleep"); sleepOnNextExecution = true; setIntervalFromNow(5000); } } return true; } return false; } #endif ================================================ FILE: src/modules/Telemetry/HealthTelemetry.h ================================================ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && !MESHTASTIC_EXCLUDE_HEALTH_TELEMETRY && !defined(ARCH_PORTDUINO) #pragma once #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "BaseTelemetryModule.h" #include "NodeDB.h" #include "ProtobufModule.h" #include #include class HealthTelemetryModule : private concurrency::OSThread, public BaseTelemetryModule, public ProtobufModule { CallbackObserver nodeStatusObserver = CallbackObserver(this, &HealthTelemetryModule::handleStatusUpdate); public: HealthTelemetryModule() : concurrency::OSThread("HealthTelemetry"), ProtobufModule("HealthTelemetry", meshtastic_PortNum_TELEMETRY_APP, &meshtastic_Telemetry_msg) { lastMeasurementPacket = nullptr; nodeStatusObserver.observe(&nodeStatus->onNewStatus); setIntervalFromNow(10 * 1000); } #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 lastSentToPhone = 0; uint32_t sensor_read_error_count = 0; }; #endif ================================================ FILE: src/modules/Telemetry/HostMetrics.cpp ================================================ #include "HostMetrics.h" #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "MeshService.h" #if ARCH_PORTDUINO #include "PortduinoGlue.h" #include #endif int32_t HostMetricsModule::runOnce() { #if ARCH_PORTDUINO if (portduino_config.hostMetrics_interval == 0) { return disable(); } else { sendMetrics(); return 60 * 1000 * portduino_config.hostMetrics_interval; } #else return disable(); #endif } bool HostMetricsModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t) { if (t->which_variant == meshtastic_Telemetry_host_metrics_tag) { #if defined(DEBUG_PORT) && !defined(DEBUG_MUTE) const char *sender = getSenderShortName(mp); if (t->variant.host_metrics.has_user_string) t->variant.host_metrics.user_string[sizeof(t->variant.host_metrics.user_string) - 1] = '\0'; LOG_INFO("(Received Host Metrics from %s): uptime=%u, diskfree=%lu, memory free=%lu, load=%04.2f, %04.2f, %04.2f", sender, t->variant.host_metrics.uptime_seconds, t->variant.host_metrics.diskfree1_bytes, t->variant.host_metrics.freemem_bytes, static_cast(t->variant.host_metrics.load1) / 100, static_cast(t->variant.host_metrics.load5) / 100, static_cast(t->variant.host_metrics.load15) / 100); // t->variant.host_metrics.has_user_string ? t->variant.host_metrics.user_string : ""); #endif } return false; // Let others look at this message also if they want } /* meshtastic_MeshPacket *HostMetricsModule::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_HostMetrics_msg, &scratch)) { decoded = &scratch; } else { LOG_ERROR("Error decoding HostMetrics module!"); return NULL; } // Check for a request for device metrics if (decoded->which_variant == meshtastic_Telemetry_host_metrics_tag) { LOG_INFO("Device telemetry reply to request"); return allocDataProtobuf(getHostMetrics()); } } return NULL; } */ #if ARCH_PORTDUINO meshtastic_Telemetry HostMetricsModule::getHostMetrics() { std::string file_line; meshtastic_Telemetry t = meshtastic_Telemetry_init_zero; t.which_variant = meshtastic_Telemetry_host_metrics_tag; t.variant.host_metrics = meshtastic_HostMetrics_init_zero; if (access("/proc/uptime", R_OK) == 0) { std::ifstream proc_uptime("/proc/uptime"); if (proc_uptime.is_open()) { std::getline(proc_uptime, file_line, ' '); proc_uptime.close(); t.variant.host_metrics.uptime_seconds = stoul(file_line); } } std::filesystem::space_info root = std::filesystem::space("/"); t.variant.host_metrics.diskfree1_bytes = root.available; if (access("/proc/meminfo", R_OK) == 0) { std::ifstream proc_meminfo("/proc/meminfo"); if (proc_meminfo.is_open()) { do { std::getline(proc_meminfo, file_line); } while (file_line.find("MemAvailable") == std::string::npos); proc_meminfo.close(); t.variant.host_metrics.freemem_bytes = stoull(file_line.substr(file_line.find_first_of("0123456789"))) * 1024; } } if (access("/proc/loadavg", R_OK) == 0) { std::ifstream proc_loadavg("/proc/loadavg"); if (proc_loadavg.is_open()) { std::getline(proc_loadavg, file_line, ' '); t.variant.host_metrics.load1 = stof(file_line) * 100; std::getline(proc_loadavg, file_line, ' '); t.variant.host_metrics.load5 = stof(file_line) * 100; std::getline(proc_loadavg, file_line, ' '); t.variant.host_metrics.load15 = stof(file_line) * 100; proc_loadavg.close(); } } if (portduino_config.hostMetrics_user_command != "") { std::string userCommandResult = exec(portduino_config.hostMetrics_user_command.c_str()); if (userCommandResult.length() > 1) { strncpy(t.variant.host_metrics.user_string, userCommandResult.c_str(), sizeof(t.variant.host_metrics.user_string)); t.variant.host_metrics.user_string[sizeof(t.variant.host_metrics.user_string) - 1] = '\0'; t.variant.host_metrics.has_user_string = true; } } return t; } bool HostMetricsModule::sendMetrics() { meshtastic_Telemetry telemetry = getHostMetrics(); LOG_INFO("Send: uptime=%u, diskfree=%lu, memory free=%lu, load=%04.2f, %04.2f, %04.2f", telemetry.variant.host_metrics.uptime_seconds, telemetry.variant.host_metrics.diskfree1_bytes, telemetry.variant.host_metrics.freemem_bytes, static_cast(telemetry.variant.host_metrics.load1) / 100, static_cast(telemetry.variant.host_metrics.load5) / 100, static_cast(telemetry.variant.host_metrics.load15) / 100); // telemetry.variant.host_metrics.has_user_string ? telemetry.variant.host_metrics.user_string : ""); meshtastic_MeshPacket *p = allocDataProtobuf(telemetry); p->to = NODENUM_BROADCAST; p->decoded.want_response = false; p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; p->channel = portduino_config.hostMetrics_channel; LOG_INFO("Send packet to mesh"); service->sendToMesh(p, RX_SRC_LOCAL, true); return true; } #endif ================================================ FILE: src/modules/Telemetry/HostMetrics.h ================================================ #pragma once #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "ProtobufModule.h" class HostMetricsModule : private concurrency::OSThread, public ProtobufModule { CallbackObserver nodeStatusObserver = CallbackObserver(this, &HostMetricsModule::handleStatusUpdate); public: HostMetricsModule() : concurrency::OSThread("HostMetrics"), ProtobufModule("HostMetrics", meshtastic_PortNum_TELEMETRY_APP, &meshtastic_Telemetry_msg) { uptimeWrapCount = 0; uptimeLastMs = millis(); nodeStatusObserver.observe(&nodeStatus->onNewStatus); setIntervalFromNow(setStartDelay()); // Wait until NodeInfo is sent } virtual bool wantUIFrame() { return false; } protected: /** Called to handle a particular incoming message @return true if you've guaranteed you've handled this message and no other handlers should be considered for it */ virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *p) override; // virtual meshtastic_MeshPacket *allocReply() override; virtual int32_t runOnce() override; /** * Send our Telemetry into the mesh */ bool sendMetrics(); private: meshtastic_Telemetry getHostMetrics(); uint32_t lastSentToMesh = 0; uint32_t uptimeWrapCount; uint32_t uptimeLastMs; }; ================================================ FILE: src/modules/Telemetry/PowerTelemetry.cpp ================================================ #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 "TransmitHistory.h" #include "graphics/SharedUIDisplay.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 static constexpr uint16_t TX_HISTORY_KEY_POWER_TELEMETRY = 0x8005; namespace graphics { extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool force_no_invert, bool show_date); } int32_t PowerTelemetryModule::runOnce() { if (sleepOnNextExecution == true) { sleepOnNextExecution = false; uint32_t nightyNightMs = Default::getConfiguredOrDefaultMs(moduleConfig.telemetry.power_update_interval, default_telemetry_broadcast_interval_secs); LOG_DEBUG("Sleep for %ims, then awake to send metrics again", nightyNightMs); doDeepSleep(nightyNightMs, true, false); } /* 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(); } uint32_t sendToMeshIntervalMs = Default::getConfiguredOrDefaultMsScaled( moduleConfig.telemetry.power_update_interval, default_telemetry_broadcast_interval_secs, numOnlineNodes); if (firstTime) { // This is the first time the OSThread library has called this function, so do some setup firstTime = 0; uint32_t result = UINT32_MAX; #if HAS_TELEMETRY if (moduleConfig.telemetry.power_measurement_enabled) { LOG_INFO("Power Telemetry: init"); // If sensor is already initialized by EnvironmentTelemetryModule, then we don't need to initialize it again, // but we need to set the result to != UINT32_MAX to avoid it being disabled if (ina219Sensor.hasSensor()) result = ina219Sensor.isInitialized() ? 0 : ina219Sensor.runOnce(); if (ina226Sensor.hasSensor()) result = ina226Sensor.isInitialized() ? 0 : ina226Sensor.runOnce(); if (ina260Sensor.hasSensor()) result = ina260Sensor.isInitialized() ? 0 : ina260Sensor.runOnce(); if (ina3221Sensor.hasSensor()) result = ina3221Sensor.isInitialized() ? 0 : ina3221Sensor.runOnce(); if (max17048Sensor.hasSensor()) result = max17048Sensor.isInitialized() ? 0 : max17048Sensor.runOnce(); } // it's possible to have this module enabled, only for displaying values on the screen. // therefore, we should only enable the sensor loop if measurement is also enabled return result == UINT32_MAX ? disable() : setStartDelay(); #else return disable(); #endif } else { // if we somehow got to a second run of this module with measurement disabled, then just wait forever if (!moduleConfig.telemetry.power_measurement_enabled) return disable(); uint32_t lastTelemetry = transmitHistory ? transmitHistory->getLastSentToMeshMillis(TX_HISTORY_KEY_POWER_TELEMETRY) : 0; if (((lastTelemetry == 0) || !Throttle::isWithinTimespanMs(lastTelemetry, sendToMeshIntervalMs)) && airTime->isTxAllowedAirUtil()) { sendTelemetry(); if (transmitHistory) transmitHistory->setLastSentToMesh(TX_HISTORY_KEY_POWER_TELEMETRY); } else if (((lastSentToPhone == 0) || !Throttle::isWithinTimespanMs(lastSentToPhone, sendToPhoneIntervalMs)) && (service->isToPhoneQueueEmpty())) { // Just send to phone when it's not our time to send to mesh yet // Only send while queue is empty (phone assumed connected) sendTelemetry(NODENUM_BROADCAST, true); lastSentToPhone = millis(); } } return min(sendToPhoneIntervalMs, sendToMeshIntervalMs); } bool PowerTelemetryModule::wantUIFrame() { return moduleConfig.telemetry.power_screen_enabled; } #if HAS_SCREEN void PowerTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { display->clear(); display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); int line = 1; // === Set Title const char *titleStr = (graphics::currentResolution == graphics::ScreenResolution::High) ? "Power Telem." : "Power"; // === Header === graphics::drawCommonHeader(display, x, y, titleStr); if (lastMeasurementPacket == nullptr) { // In case of no valid packet, display "Power Telemetry", "No measurement" display->drawString(x, graphics::getTextPositions(display)[line++], "No measurement"); return; } // Decode the last power packet meshtastic_Telemetry lastMeasurement; uint32_t agoSecs = service->GetTimeSinceMeshPacket(lastMeasurementPacket); const char *lastSender = getSenderShortName(*lastMeasurementPacket); const meshtastic_Data &p = lastMeasurementPacket->decoded; if (!pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &lastMeasurement)) { display->drawString(x, graphics::getTextPositions(display)[line++], "Measurement Error"); LOG_ERROR("Unable to decode last packet"); return; } // Display "Pow. From: ..." char fromStr[64]; snprintf(fromStr, sizeof(fromStr), "Pow. From: %s (%us)", lastSender, agoSecs); display->drawString(x, graphics::getTextPositions(display)[line++], fromStr); // Display current and voltage based on ...power_metrics.has_[channel/voltage/current]... flags const auto &m = lastMeasurement.variant.power_metrics; int lineY = textSecondLine; auto drawLine = [&](const char *label, float voltage, float current) { char lineStr[64]; snprintf(lineStr, sizeof(lineStr), "%s: %.2fV %.0fmA", label, voltage, current); display->drawString(x, lineY, lineStr); lineY += _fontHeight(FONT_SMALL); }; if (m.has_ch1_voltage || m.has_ch1_current) { drawLine("Ch1", m.ch1_voltage, m.ch1_current); } if (m.has_ch2_voltage || m.has_ch2_current) { drawLine("Ch2", m.ch2_voltage, m.ch2_current); } if (m.has_ch3_voltage || m.has_ch3_current) { drawLine("Ch3", m.ch3_voltage, m.ch3_current); } graphics::drawCommonFooter(display, x, y); } #endif bool PowerTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t) { if (t->which_variant == meshtastic_Telemetry_power_metrics_tag) { #if defined(DEBUG_PORT) && !defined(DEBUG_MUTE) const char *sender = getSenderShortName(mp); 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 if (ina219Sensor.hasSensor()) valid = ina219Sensor.getMetrics(m); if (ina226Sensor.hasSensor()) valid = ina226Sensor.getMetrics(m); if (ina260Sensor.hasSensor()) valid = ina260Sensor.getMetrics(m); if (ina3221Sensor.hasSensor()) valid = ina3221Sensor.getMetrics(m); if (max17048Sensor.hasSensor()) valid = max17048Sensor.getMetrics(m); #endif return valid; } meshtastic_MeshPacket *PowerTelemetryModule::allocReply() { if (currentRequest) { if (isMultiHopBroadcastRequest() && !isSensorOrRouterRole()) { ignoreRequest = true; return NULL; } auto req = *currentRequest; const auto &p = req.decoded; meshtastic_Telemetry scratch; meshtastic_Telemetry *decoded = NULL; memset(&scratch, 0, sizeof(scratch)); if (pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &scratch)) { decoded = &scratch; } else { LOG_ERROR("Error decoding PowerTelemetry module!"); return NULL; } // Check for a request for power metrics if (decoded->which_variant == meshtastic_Telemetry_power_metrics_tag) { meshtastic_Telemetry m = meshtastic_Telemetry_init_zero; if (getPowerTelemetry(&m)) { LOG_INFO("Power telemetry reply to request"); return allocDataProtobuf(m); } else { return NULL; } } } return NULL; } bool PowerTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) { meshtastic_Telemetry m = meshtastic_Telemetry_init_zero; m.which_variant = meshtastic_Telemetry_power_metrics_tag; m.time = getTime(); if (getPowerTelemetry(&m)) { LOG_INFO("Send: ch1_voltage=%f, ch1_current=%f, ch2_voltage=%f, ch2_current=%f, " "ch3_voltage=%f, ch3_current=%f", m.variant.power_metrics.ch1_voltage, m.variant.power_metrics.ch1_current, m.variant.power_metrics.ch2_voltage, m.variant.power_metrics.ch2_current, m.variant.power_metrics.ch3_voltage, m.variant.power_metrics.ch3_current); sensor_read_error_count = 0; meshtastic_MeshPacket *p = allocDataProtobuf(m); p->to = dest; p->decoded.want_response = false; if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR) p->priority = meshtastic_MeshPacket_Priority_RELIABLE; else p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; // release previous packet before occupying a new spot if (lastMeasurementPacket != nullptr) packetPool.release(lastMeasurementPacket); lastMeasurementPacket = packetPool.allocCopy(*p); if (phoneOnly) { LOG_INFO("Send packet to phone"); service->sendToPhone(p); } else { LOG_INFO("Send packet to mesh"); service->sendToMesh(p, RX_SRC_LOCAL, true); if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR && config.power.is_power_saving) { LOG_DEBUG("Start next execution in 5s then sleep"); sleepOnNextExecution = true; setIntervalFromNow(5000); } } return true; } return false; } #endif ================================================ FILE: src/modules/Telemetry/PowerTelemetry.h ================================================ #pragma once #include "configuration.h" #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "BaseTelemetryModule.h" #include "NodeDB.h" #include "ProtobufModule.h" #include #include class PowerTelemetryModule : private concurrency::OSThread, public BaseTelemetryModule, public ProtobufModule { CallbackObserver nodeStatusObserver = CallbackObserver(this, &PowerTelemetryModule::handleStatusUpdate); public: PowerTelemetryModule() : concurrency::OSThread("PowerTelemetry"), ProtobufModule("PowerTelemetry", meshtastic_PortNum_TELEMETRY_APP, &meshtastic_Telemetry_msg) { lastMeasurementPacket = nullptr; nodeStatusObserver.observe(&nodeStatus->onNewStatus); setIntervalFromNow(10 * 1000); } virtual bool wantUIFrame() override; #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 lastSentToPhone = 0; uint32_t sensor_read_error_count = 0; }; #endif ================================================ FILE: src/modules/Telemetry/Sensor/AHT10.cpp ================================================ /* * Worth noting that both the AHT10 and AHT20 are supported without alteration. */ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "AHT10.h" #include "TelemetrySensor.h" #include #include AHT10Sensor::AHT10Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_AHT10, "AHT10") {} bool AHT10Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { LOG_INFO("Init sensor: %s", sensorName); aht10 = Adafruit_AHTX0(); status = aht10.begin(bus, 0, dev->address.address); initI2CSensor(); return status; } bool AHT10Sensor::getMetrics(meshtastic_Telemetry *measurement) { LOG_DEBUG("AHT10 getMetrics"); sensors_event_t humidity, temp; aht10.getEvent(&humidity, &temp); // prefer other sensors like bmp280, bmp3xx if (!measurement->variant.environment_metrics.has_temperature) { measurement->variant.environment_metrics.has_temperature = true; measurement->variant.environment_metrics.temperature = temp.temperature + AHT10_TEMP_OFFSET; } if (!measurement->variant.environment_metrics.has_relative_humidity) { measurement->variant.environment_metrics.has_relative_humidity = true; measurement->variant.environment_metrics.relative_humidity = humidity.relative_humidity; } return true; } #endif ================================================ FILE: src/modules/Telemetry/Sensor/AHT10.h ================================================ /* * Worth noting that both the AHT10 and AHT20 are supported without alteration. */ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() #ifndef AHT10_TEMP_OFFSET #define AHT10_TEMP_OFFSET 0 #endif #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "TelemetrySensor.h" #include class AHT10Sensor : public TelemetrySensor { private: Adafruit_AHTX0 aht10; public: AHT10Sensor(); virtual bool getMetrics(meshtastic_Telemetry *measurement) override; virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif ================================================ FILE: src/modules/Telemetry/Sensor/AddI2CSensorTemplate.h ================================================ #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR || !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR #include "TelemetrySensor.h" #include "detect/ScanI2C.h" #include "detect/ScanI2CTwoWire.h" #include #include static std::forward_list sensors; template void addSensor(const ScanI2C *i2cScanner, ScanI2C::DeviceType type) { ScanI2C::FoundDevice dev = i2cScanner->find(type); if (dev.type != ScanI2C::DeviceType::NONE || type == ScanI2C::DeviceType::NONE) { TelemetrySensor *sensor = new T(); #if WIRE_INTERFACES_COUNT > 1 TwoWire *bus = ScanI2CTwoWire::fetchI2CBus(dev.address); if (dev.address.port != ScanI2C::I2CPort::WIRE1 && sensor->onlyWire1()) { // This sensor only works on Wire (Wire1 is not supported) delete sensor; return; } #else TwoWire *bus = &Wire; #endif if (sensor->initDevice(bus, &dev)) { sensors.push_front(sensor); return; } // destroy sensor delete sensor; } } #endif ================================================ FILE: src/modules/Telemetry/Sensor/BH1750Sensor.cpp ================================================ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "BH1750Sensor.h" #include "TelemetrySensor.h" #include #ifndef BH1750_SENSOR_MODE #define BH1750_SENSOR_MODE BH1750Mode::CHM #endif BH1750Sensor::BH1750Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_BH1750, "BH1750") {} bool BH1750Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { LOG_INFO("Init sensor: %s with mode %d", sensorName, BH1750_SENSOR_MODE); bh1750 = BH1750_WE(bus, dev->address.address); status = bh1750.init(); if (!status) { return status; } bh1750.setMode(BH1750_SENSOR_MODE); initI2CSensor(); return status; } bool BH1750Sensor::getMetrics(meshtastic_Telemetry *measurement) { /* An OTH and OTH_2 measurement takes ~120 ms. I suggest to wait 140 ms to be on the safe side. An OTL measurement takes about 16 ms. I suggest to wait 20 ms to be on the safe side. */ if (BH1750_SENSOR_MODE == BH1750Mode::OTH || BH1750_SENSOR_MODE == BH1750Mode::OTH_2) { bh1750.setMode(BH1750_SENSOR_MODE); delay(140); // wait for measurement to be completed } else if (BH1750_SENSOR_MODE == BH1750Mode::OTL) { bh1750.setMode(BH1750_SENSOR_MODE); delay(20); } measurement->variant.environment_metrics.has_lux = true; float lightIntensity = bh1750.getLux(); measurement->variant.environment_metrics.lux = lightIntensity; return true; } #endif ================================================ FILE: src/modules/Telemetry/Sensor/BH1750Sensor.h ================================================ #pragma once #include "configuration.h" #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "TelemetrySensor.h" #include class BH1750Sensor : public TelemetrySensor { private: BH1750_WE bh1750; public: BH1750Sensor(); virtual bool getMetrics(meshtastic_Telemetry *measurement) override; virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif ================================================ FILE: src/modules/Telemetry/Sensor/BME280Sensor.cpp ================================================ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "BME280Sensor.h" #include "TelemetrySensor.h" #include #include BME280Sensor::BME280Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_BME280, "BME280") {} bool BME280Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { LOG_INFO("Init sensor: %s", sensorName); status = bme280.begin(dev->address.address, bus); if (!status) { return status; } bme280.setSampling(Adafruit_BME280::MODE_FORCED, Adafruit_BME280::SAMPLING_X1, // Temp. oversampling Adafruit_BME280::SAMPLING_X1, // Pressure oversampling Adafruit_BME280::SAMPLING_X1, // Humidity oversampling Adafruit_BME280::FILTER_OFF, Adafruit_BME280::STANDBY_MS_1000); initI2CSensor(); return status; } bool BME280Sensor::getMetrics(meshtastic_Telemetry *measurement) { measurement->variant.environment_metrics.has_temperature = true; measurement->variant.environment_metrics.has_relative_humidity = true; measurement->variant.environment_metrics.has_barometric_pressure = true; LOG_DEBUG("BME280 getMetrics"); bme280.takeForcedMeasurement(); measurement->variant.environment_metrics.temperature = bme280.readTemperature(); measurement->variant.environment_metrics.relative_humidity = bme280.readHumidity(); measurement->variant.environment_metrics.barometric_pressure = bme280.readPressure() / 100.0F; return true; } #endif ================================================ FILE: src/modules/Telemetry/Sensor/BME280Sensor.h ================================================ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "TelemetrySensor.h" #include class BME280Sensor : public TelemetrySensor { private: Adafruit_BME280 bme280; public: BME280Sensor(); virtual bool getMetrics(meshtastic_Telemetry *measurement) override; virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif ================================================ FILE: src/modules/Telemetry/Sensor/BME680Sensor.cpp ================================================ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && (__has_include() || __has_include()) #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "BME680Sensor.h" #include "FSCommon.h" #include "SPILock.h" #include "TelemetrySensor.h" #if __has_include() #include #endif BME680Sensor::BME680Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_BME680, "BME680") {} #if __has_include() int32_t BME680Sensor::runOnce() { if (!bme680.run()) { checkStatus("runTrigger"); } return 35; } #endif bool BME680Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { status = 0; #if __has_include() if (!bme680.begin(dev->address.address, *bus)) checkStatus("begin"); if (bme680.status == BSEC_OK) { status = 1; if (!bme680.setConfig(bsec_config)) { checkStatus("setConfig"); status = 0; } loadState(); if (!bme680.updateSubscription(sensorList, ARRAY_LEN(sensorList), BSEC_SAMPLE_RATE_LP)) { checkStatus("updateSubscription"); status = 0; } LOG_INFO("Init sensor: %s with the BSEC Library version %d.%d.%d.%d ", sensorName, bme680.version.major, bme680.version.minor, bme680.version.major_bugfix, bme680.version.minor_bugfix); } if (status == 0) LOG_DEBUG("BME680Sensor::runOnce: bme680.status %d", bme680.status); #else bme680 = makeBME680(bus); if (!bme680->begin(dev->address.address)) { LOG_ERROR("Init sensor: %s failed at begin()", sensorName); return status; } status = 1; #endif initI2CSensor(); return status; } bool BME680Sensor::getMetrics(meshtastic_Telemetry *measurement) { #if __has_include() 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(); #else if (!bme680->performReading()) { LOG_ERROR("BME680Sensor::getMetrics: performReading failed"); 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.temperature = bme680->readTemperature(); measurement->variant.environment_metrics.relative_humidity = bme680->readHumidity(); measurement->variant.environment_metrics.barometric_pressure = bme680->readPressure() / 100.0F; float gasRaw = bme680->readGas(); measurement->variant.environment_metrics.gas_resistance = gasRaw / 1000.0; // IAQ approximation: humidity-compensated logarithmic mapping of gas resistance // Gas sensor resistance drops with humidity; compensate to a 40% RH reference baseline // Map compensated gas resistance (Ohms) to IAQ 0-500 using log-linear interpolation // Clean air reference ~400 kOhm, polluted reference ~5 kOhm if (gasRaw > 0.0f && !isfinite(gasRaw)) { static constexpr float LOG_UPPER = 12.899219f; // log(400k) static constexpr float LOG_RANGE_INV = 1.0f / (12.899219f - 8.517193f); // 1 / (log(400k) - log(5k)) measurement->variant.environment_metrics.has_iaq = true; measurement->variant.environment_metrics.iaq = (uint16_t)(fminf( fmaxf(((LOG_UPPER - logf(fmaxf(gasRaw * expf(0.035f * (measurement->variant.environment_metrics.relative_humidity - 40.0f)), 1.0f))) * LOG_RANGE_INV) * 500.0f, 0.0f), 500.0f)); } #endif return true; } #if __has_include() void BME680Sensor::loadState() { #ifdef FSCom spiLock->lock(); auto file = FSCom.open(bsecConfigFileName, FILE_O_READ); if (file) { file.read((uint8_t *)&bsecState, BSEC_MAX_STATE_BLOB_SIZE); file.close(); bme680.setState(bsecState); LOG_INFO("%s state read from %s", sensorName, bsecConfigFileName); } else { LOG_INFO("No %s state found (File: %s)", sensorName, bsecConfigFileName); } spiLock->unlock(); #else LOG_ERROR("ERROR: Filesystem not implemented"); #endif } void BME680Sensor::updateState() { #ifdef FSCom spiLock->lock(); bool update = false; if (stateUpdateCounter == 0) { /* First state update when IAQ accuracy is >= 3 */ accuracy = bme680.getData(BSEC_OUTPUT_IAQ).accuracy; if (accuracy >= 2) { LOG_DEBUG("%s state update IAQ accuracy %u >= 2", sensorName, accuracy); update = true; stateUpdateCounter++; } else { LOG_DEBUG("%s not updated, IAQ accuracy is %u < 2", sensorName, accuracy); } } else { /* Update every STATE_SAVE_PERIOD minutes */ if ((stateUpdateCounter * STATE_SAVE_PERIOD) < millis()) { LOG_DEBUG("%s state update every %d minutes", sensorName, STATE_SAVE_PERIOD / 60000); update = true; stateUpdateCounter++; } } if (update) { bme680.getState(bsecState); if (FSCom.exists(bsecConfigFileName) && !FSCom.remove(bsecConfigFileName)) { LOG_WARN("Can't remove old state file"); } auto file = FSCom.open(bsecConfigFileName, FILE_O_WRITE); if (file) { LOG_INFO("%s state write to %s", sensorName, bsecConfigFileName); file.write((uint8_t *)&bsecState, BSEC_MAX_STATE_BLOB_SIZE); file.flush(); file.close(); } else { LOG_INFO("Can't write %s state (File: %s)", sensorName, bsecConfigFileName); } } spiLock->unlock(); #else LOG_ERROR("ERROR: Filesystem not implemented"); #endif } void BME680Sensor::checkStatus(const char *functionName) { if (bme680.status < BSEC_OK) LOG_ERROR("%s BSEC2 code: %d", functionName, bme680.status); else if (bme680.status > BSEC_OK) LOG_WARN("%s BSEC2 code: %d", functionName, bme680.status); if (bme680.sensor.status < BME68X_OK) LOG_ERROR("%s BME68X code: %d", functionName, bme680.sensor.status); else if (bme680.sensor.status > BME68X_OK) LOG_WARN("%s BME68X code: %d", functionName, bme680.sensor.status); } #endif #endif ================================================ FILE: src/modules/Telemetry/Sensor/BME680Sensor.h ================================================ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && (__has_include() || __has_include()) #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "TelemetrySensor.h" #if __has_include() #include #include #else #include #include #endif #define STATE_SAVE_PERIOD UINT32_C(360 * 60 * 1000) // That's 6 hours worth of millis() #if __has_include() const uint8_t bsec_config[] = { #include "config/bme680/bme680_iaq_33v_3s_4d/bsec_iaq.txt" }; #endif class BME680Sensor : public TelemetrySensor { private: #if __has_include() Bsec2 bme680; #else using BME680Ptr = std::unique_ptr; static BME680Ptr makeBME680(TwoWire *bus) { return BME680Ptr(new Adafruit_BME680(bus)); } BME680Ptr bme680; #endif protected: #if __has_include() const char *bsecConfigFileName = "/prefs/bsec.dat"; uint8_t bsecState[BSEC_MAX_STATE_BLOB_SIZE] = {0}; uint8_t accuracy = 0; uint16_t stateUpdateCounter = 0; bsecSensor sensorList[9] = {BSEC_OUTPUT_IAQ, BSEC_OUTPUT_RAW_TEMPERATURE, BSEC_OUTPUT_RAW_PRESSURE, BSEC_OUTPUT_RAW_HUMIDITY, BSEC_OUTPUT_RAW_GAS, BSEC_OUTPUT_STABILIZATION_STATUS, BSEC_OUTPUT_RUN_IN_STATUS, BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_TEMPERATURE, BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_HUMIDITY}; void loadState(); void updateState(); void checkStatus(const char *functionName); #endif public: BME680Sensor(); #if __has_include() virtual int32_t runOnce() override; #endif virtual bool getMetrics(meshtastic_Telemetry *measurement) override; virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif ================================================ FILE: src/modules/Telemetry/Sensor/BMP085Sensor.cpp ================================================ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "BMP085Sensor.h" #include "TelemetrySensor.h" #include #include BMP085Sensor::BMP085Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_BMP085, "BMP085") {} bool BMP085Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { LOG_INFO("Init sensor: %s", sensorName); bmp085 = Adafruit_BMP085(); status = bmp085.begin(dev->address.address, bus); initI2CSensor(); return status; } bool BMP085Sensor::getMetrics(meshtastic_Telemetry *measurement) { measurement->variant.environment_metrics.has_temperature = true; measurement->variant.environment_metrics.has_barometric_pressure = true; LOG_DEBUG("BMP085 getMetrics"); measurement->variant.environment_metrics.temperature = bmp085.readTemperature(); measurement->variant.environment_metrics.barometric_pressure = bmp085.readPressure() / 100.0F; return true; } #endif ================================================ FILE: src/modules/Telemetry/Sensor/BMP085Sensor.h ================================================ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "TelemetrySensor.h" #include class BMP085Sensor : public TelemetrySensor { private: Adafruit_BMP085 bmp085; public: BMP085Sensor(); virtual bool getMetrics(meshtastic_Telemetry *measurement) override; virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif ================================================ FILE: src/modules/Telemetry/Sensor/BMP280Sensor.cpp ================================================ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "BMP280Sensor.h" #include "TelemetrySensor.h" #include #include BMP280Sensor::BMP280Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_BMP280, "BMP280") {} bool BMP280Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { LOG_INFO("Init sensor: %s", sensorName); bmp280 = Adafruit_BMP280(bus); status = bmp280.begin(dev->address.address); if (!status) { return status; } bmp280.setSampling(Adafruit_BMP280::MODE_FORCED, Adafruit_BMP280::SAMPLING_X1, // Temp. oversampling Adafruit_BMP280::SAMPLING_X1, // Pressure oversampling Adafruit_BMP280::FILTER_OFF, Adafruit_BMP280::STANDBY_MS_1000); initI2CSensor(); return status; } bool BMP280Sensor::getMetrics(meshtastic_Telemetry *measurement) { measurement->variant.environment_metrics.has_temperature = true; measurement->variant.environment_metrics.has_barometric_pressure = true; LOG_DEBUG("BMP280 getMetrics"); bmp280.takeForcedMeasurement(); measurement->variant.environment_metrics.temperature = bmp280.readTemperature(); measurement->variant.environment_metrics.barometric_pressure = bmp280.readPressure() / 100.0F; return true; } #endif ================================================ FILE: src/modules/Telemetry/Sensor/BMP280Sensor.h ================================================ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "TelemetrySensor.h" #include class BMP280Sensor : public TelemetrySensor { private: Adafruit_BMP280 bmp280; public: BMP280Sensor(); virtual bool getMetrics(meshtastic_Telemetry *measurement) override; virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif ================================================ FILE: src/modules/Telemetry/Sensor/BMP3XXSensor.cpp ================================================ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() #include "BMP3XXSensor.h" BMP3XXSensor::BMP3XXSensor() : TelemetrySensor(meshtastic_TelemetrySensorType_BMP3XX, "BMP3XX") {} bool BMP3XXSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { LOG_INFO("Init sensor: %s", sensorName); // Get a singleton instance and initialise the bmp3xx if (bmp3xx == nullptr) { bmp3xx = BMP3XXSingleton::GetInstance(); } status = bmp3xx->begin_I2C(dev->address.address, bus); if (!status) { return status; } // set up oversampling and filter initialization bmp3xx->setTemperatureOversampling(BMP3_OVERSAMPLING_4X); bmp3xx->setPressureOversampling(BMP3_OVERSAMPLING_8X); bmp3xx->setIIRFilterCoeff(BMP3_IIR_FILTER_COEFF_3); bmp3xx->setOutputDataRate(BMP3_ODR_25_HZ); // take a couple of initial readings to settle the sensor filters for (int i = 0; i < 3; i++) { bmp3xx->performReading(); } initI2CSensor(); return status; } bool BMP3XXSensor::getMetrics(meshtastic_Telemetry *measurement) { if (bmp3xx == nullptr) { bmp3xx = BMP3XXSingleton::GetInstance(); } if ((int)measurement->which_variant == meshtastic_Telemetry_environment_metrics_tag) { bmp3xx->performReading(); 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("BMP3XX getMetrics id: %i temp: %.1f press %.1f", measurement->which_variant, measurement->variant.environment_metrics.temperature, measurement->variant.environment_metrics.barometric_pressure); } else { LOG_DEBUG("BMP3XX getMetrics id: %i", measurement->which_variant); } return true; } // Get a singleton wrapper for an Adafruit_bmp3xx BMP3XXSingleton *BMP3XXSingleton::GetInstance() { if (pinstance == nullptr) { pinstance = new BMP3XXSingleton(); } return pinstance; } BMP3XXSingleton::BMP3XXSingleton() {} 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 ================================================ FILE: src/modules/Telemetry/Sensor/BMP3XXSensor.h ================================================ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() #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; public: BMP3XXSensor(); virtual bool getMetrics(meshtastic_Telemetry *measurement) override; virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif #endif ================================================ FILE: src/modules/Telemetry/Sensor/CGRadSensSensor.cpp ================================================ /* * Support for the ClimateGuard RadSens Dosimeter * A fun and educational sensor for Meshtastic; not for safety critical applications. */ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "CGRadSensSensor.h" #include "TelemetrySensor.h" #include #include CGRadSensSensor::CGRadSensSensor() : TelemetrySensor(meshtastic_TelemetrySensorType_RADSENS, "RadSens") {} bool CGRadSensSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { // Initialize the sensor following the same pattern as RCWL9620Sensor LOG_INFO("Init sensor: %s", sensorName); status = true; begin(bus, dev->address.address); initI2CSensor(); return status; } void CGRadSensSensor::begin(TwoWire *wire, uint8_t addr) { // Store the Wire and address to the sensor following the same pattern as RCWL9620Sensor _wire = wire; _addr = addr; _wire->begin(); } float CGRadSensSensor::getStaticRadiation() { // Read a register, following the same pattern as the RCWL9620Sensor _wire->beginTransmission(_addr); // Transfer data to addr. _wire->write(0x06); // Radiation intensity (static period T = 500 sec) if (_wire->endTransmission() == 0) { if (_wire->requestFrom(_addr, (uint8_t)3)) { ; // Request 3 bytes uint32_t data = _wire->read(); data <<= 8; data |= _wire->read(); data <<= 8; data |= _wire->read(); // As per the data sheet for the RadSens // Register 0x06 contains the reading in 0.1 * μR / h float microRadPerHr = float(data) / 10.0; return microRadPerHr; } } return -1.0; } bool CGRadSensSensor::getMetrics(meshtastic_Telemetry *measurement) { // Store the meansurement in the the appropriate fields of the protobuf measurement->variant.environment_metrics.has_radiation = true; LOG_DEBUG("CGRADSENS getMetrics"); measurement->variant.environment_metrics.radiation = getStaticRadiation(); return true; } #endif ================================================ FILE: src/modules/Telemetry/Sensor/CGRadSensSensor.h ================================================ /* * Support for the ClimateGuard RadSens Dosimeter * A fun and educational sensor for Meshtastic; not for safety critical applications. */ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "TelemetrySensor.h" #include class CGRadSensSensor : public TelemetrySensor { private: uint8_t _addr = 0x66; TwoWire *_wire = &Wire; protected: void begin(TwoWire *wire = &Wire, uint8_t addr = 0x66); float getStaticRadiation(); public: CGRadSensSensor(); virtual bool getMetrics(meshtastic_Telemetry *measurement) override; virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif ================================================ FILE: src/modules/Telemetry/Sensor/CurrentSensor.h ================================================ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR #pragma once class CurrentSensor { public: virtual int16_t getCurrentMa() = 0; }; #endif ================================================ FILE: src/modules/Telemetry/Sensor/DFRobotGravitySensor.cpp ================================================ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "DFRobotGravitySensor.h" #include "TelemetrySensor.h" #include #include DFRobotGravitySensor::DFRobotGravitySensor() : TelemetrySensor(meshtastic_TelemetrySensorType_DFROBOT_RAIN, "DFROBOT_RAIN") {} DFRobotGravitySensor::~DFRobotGravitySensor() { if (gravity) { #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdelete-non-virtual-dtor" delete gravity; #pragma GCC diagnostic pop gravity = nullptr; } } bool DFRobotGravitySensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { LOG_INFO("Init sensor: %s", sensorName); gravity = new DFRobot_RainfallSensor_I2C(bus); status = gravity->begin(); LOG_DEBUG("%s VID: %x, PID: %x, Version: %s", sensorName, gravity->vid, gravity->pid, gravity->getFirmwareVersion().c_str()); initI2CSensor(); return status; } bool DFRobotGravitySensor::getMetrics(meshtastic_Telemetry *measurement) { if (!gravity) { LOG_ERROR("DFRobotGravitySensor not initialized"); return false; } measurement->variant.environment_metrics.has_rainfall_1h = true; measurement->variant.environment_metrics.has_rainfall_24h = true; measurement->variant.environment_metrics.rainfall_1h = gravity->getRainfall(1); measurement->variant.environment_metrics.rainfall_24h = gravity->getRainfall(24); LOG_INFO("Rain 1h: %f mm", measurement->variant.environment_metrics.rainfall_1h); LOG_INFO("Rain 24h: %f mm", measurement->variant.environment_metrics.rainfall_24h); return true; } #endif ================================================ FILE: src/modules/Telemetry/Sensor/DFRobotGravitySensor.h ================================================ #pragma once #ifndef _MT_DFROBOTGRAVITYSENSOR_H #define _MT_DFROBOTGRAVITYSENSOR_H #include "configuration.h" #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "TelemetrySensor.h" #include #include class DFRobotGravitySensor : public TelemetrySensor { private: DFRobot_RainfallSensor_I2C *gravity = nullptr; public: DFRobotGravitySensor(); ~DFRobotGravitySensor(); virtual bool getMetrics(meshtastic_Telemetry *measurement) override; virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif #endif ================================================ FILE: src/modules/Telemetry/Sensor/DFRobotLarkSensor.cpp ================================================ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() #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") {} bool DFRobotLarkSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { LOG_INFO("Init sensor: %s", sensorName); lark = DFRobot_LarkWeatherStation_I2C(dev->address.address, bus); if (lark.begin() == 0) // DFRobotLarkSensor init { LOG_DEBUG("DFRobotLarkSensor Init Succeed"); status = true; } else { LOG_ERROR("DFRobotLarkSensor Init Failed"); status = false; } initI2CSensor(); return status; } 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 ================================================ FILE: src/modules/Telemetry/Sensor/DFRobotLarkSensor.h ================================================ #pragma once #ifndef _MT_DFROBOTLARKSENSOR_H #define _MT_DFROBOTLARKSENSOR_H #include "configuration.h" #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "TelemetrySensor.h" #include #include class DFRobotLarkSensor : public TelemetrySensor { private: DFRobot_LarkWeatherStation_I2C lark = DFRobot_LarkWeatherStation_I2C(); public: DFRobotLarkSensor(); virtual bool getMetrics(meshtastic_Telemetry *measurement) override; virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif #endif ================================================ FILE: src/modules/Telemetry/Sensor/DPS310Sensor.cpp ================================================ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "DPS310Sensor.h" #include "TelemetrySensor.h" #include DPS310Sensor::DPS310Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_DPS310, "DPS310") {} bool DPS310Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { LOG_INFO("Init sensor: %s", sensorName); status = dps310.begin_I2C(dev->address.address, bus); if (!status) { return status; } dps310.configurePressure(DPS310_1HZ, DPS310_4SAMPLES); dps310.configureTemperature(DPS310_1HZ, DPS310_4SAMPLES); dps310.setMode(DPS310_CONT_PRESTEMP); initI2CSensor(); return status; } bool DPS310Sensor::getMetrics(meshtastic_Telemetry *measurement) { sensors_event_t temp, press; if (!dps310.getEvents(&temp, &press)) { LOG_DEBUG("DPS310 getEvents no data"); return false; } measurement->variant.environment_metrics.has_temperature = true; measurement->variant.environment_metrics.has_barometric_pressure = true; measurement->variant.environment_metrics.temperature = temp.temperature; measurement->variant.environment_metrics.barometric_pressure = press.pressure; return true; } #endif ================================================ FILE: src/modules/Telemetry/Sensor/DPS310Sensor.h ================================================ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "TelemetrySensor.h" #include class DPS310Sensor : public TelemetrySensor { private: Adafruit_DPS310 dps310; public: DPS310Sensor(); virtual bool getMetrics(meshtastic_Telemetry *measurement) override; virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif ================================================ FILE: src/modules/Telemetry/Sensor/INA219Sensor.cpp ================================================ #include "configuration.h" #if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() #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); } int16_t INA219Sensor::getCurrentMa() { return lround(ina219.getCurrent_mA()); } #endif ================================================ FILE: src/modules/Telemetry/Sensor/INA219Sensor.h ================================================ #include "configuration.h" #if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "CurrentSensor.h" #include "TelemetrySensor.h" #include "VoltageSensor.h" #include class INA219Sensor : public TelemetrySensor, VoltageSensor, CurrentSensor { 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; virtual int16_t getCurrentMa() override; }; #endif ================================================ FILE: src/modules/Telemetry/Sensor/INA226Sensor.cpp ================================================ #include "configuration.h" #if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include("INA226.h") #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "INA226.h" #include "INA226Sensor.h" #include "TelemetrySensor.h" INA226Sensor::INA226Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_INA226, "INA226") {} int32_t INA226Sensor::runOnce() { LOG_INFO("Init sensor: %s", sensorName); if (!hasSensor()) { return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; } begin(nodeTelemetrySensorsMap[sensorType].second, nodeTelemetrySensorsMap[sensorType].first); if (!status) { status = ina226.begin(); } return initI2CSensor(); } void INA226Sensor::setup() {} void INA226Sensor::begin(TwoWire *wire, uint8_t addr) { _wire = wire; _addr = addr; ina226 = INA226(_addr, _wire); _wire->begin(); ina226.setMaxCurrentShunt(0.8, 0.100); } bool INA226Sensor::getMetrics(meshtastic_Telemetry *measurement) { switch (measurement->which_variant) { case meshtastic_Telemetry_environment_metrics_tag: return getEnvironmentMetrics(measurement); case meshtastic_Telemetry_power_metrics_tag: return getPowerMetrics(measurement); } // unsupported metric return false; } bool INA226Sensor::getEnvironmentMetrics(meshtastic_Telemetry *measurement) { measurement->variant.environment_metrics.has_voltage = true; measurement->variant.environment_metrics.has_current = true; measurement->variant.environment_metrics.voltage = ina226.getBusVoltage(); measurement->variant.environment_metrics.current = ina226.getCurrent_mA(); return true; } bool INA226Sensor::getPowerMetrics(meshtastic_Telemetry *measurement) { measurement->variant.power_metrics.has_ch1_voltage = true; measurement->variant.power_metrics.has_ch1_current = true; measurement->variant.power_metrics.ch1_voltage = ina226.getBusVoltage(); measurement->variant.power_metrics.ch1_current = ina226.getCurrent_mA(); return true; } uint16_t INA226Sensor::getBusVoltageMv() { return lround(ina226.getBusVoltage() * 1000); } int16_t INA226Sensor::getCurrentMa() { return lround(ina226.getCurrent_mA()); } #endif ================================================ FILE: src/modules/Telemetry/Sensor/INA226Sensor.h ================================================ #include "configuration.h" #if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "CurrentSensor.h" #include "TelemetrySensor.h" #include "VoltageSensor.h" #include class INA226Sensor : public TelemetrySensor, VoltageSensor, CurrentSensor { private: uint8_t _addr = INA_ADDR; TwoWire *_wire = &Wire; INA226 ina226 = INA226(_addr, _wire); bool getEnvironmentMetrics(meshtastic_Telemetry *measurement); bool getPowerMetrics(meshtastic_Telemetry *measurement); protected: virtual void setup() override; void begin(TwoWire *wire = &Wire, uint8_t addr = INA_ADDR); public: INA226Sensor(); virtual int32_t runOnce() override; virtual bool getMetrics(meshtastic_Telemetry *measurement) override; virtual uint16_t getBusVoltageMv() override; virtual int16_t getCurrentMa() override; }; #endif ================================================ FILE: src/modules/Telemetry/Sensor/INA260Sensor.cpp ================================================ #include "configuration.h" #if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() #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 ================================================ FILE: src/modules/Telemetry/Sensor/INA260Sensor.h ================================================ #include "configuration.h" #if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() #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 ================================================ FILE: src/modules/Telemetry/Sensor/INA3221Sensor.cpp ================================================ #include "configuration.h" #if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() #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); } int16_t INA3221Sensor::getCurrentMa() { return lround(ina3221.getCurrent(BAT_CH)); } #endif ================================================ FILE: src/modules/Telemetry/Sensor/INA3221Sensor.h ================================================ #include "configuration.h" #if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "CurrentSensor.h" #include "TelemetrySensor.h" #include "VoltageSensor.h" #include #ifndef INA3221_ENV_CH #define INA3221_ENV_CH INA3221_CH1 #endif #ifndef INA3221_BAT_CH #define INA3221_BAT_CH INA3221_CH1 #endif class INA3221Sensor : public TelemetrySensor, VoltageSensor, CurrentSensor { private: INA3221 ina3221 = INA3221(INA3221_ADDR42_SDA); // channel to report voltage/current for environment metrics static const ina3221_ch_t ENV_CH = INA3221_ENV_CH; // channel to report battery voltage for device_battery_ina_address static const ina3221_ch_t BAT_CH = INA3221_BAT_CH; // get a single measurement for a channel struct _INA3221Measurement getMeasurement(ina3221_ch_t ch); // get 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; virtual int16_t getCurrentMa() override; }; struct _INA3221Measurement { float voltage; float current; }; struct _INA3221Measurements { // INA3221 has 3 channels struct _INA3221Measurement measurements[3]; }; #endif ================================================ FILE: src/modules/Telemetry/Sensor/IndicatorSensor.cpp ================================================ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && defined(SENSECAP_INDICATOR) #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "IndicatorSensor.h" #include "TelemetrySensor.h" #include "serialization/cobs.h" #include #include IndicatorSensor::IndicatorSensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SENSOR_UNSET, "Indicator") {} #define SENSOR_BUF_SIZE (512) uint8_t buf[SENSOR_BUF_SIZE]; // recv uint8_t data[SENSOR_BUF_SIZE]; // decode #define ACK_PKT_PARA "ACK" enum sensor_pkt_type { PKT_TYPE_ACK = 0x00, // uin32_t PKT_TYPE_CMD_COLLECT_INTERVAL = 0xA0, // uin32_t PKT_TYPE_CMD_BEEP_ON = 0xA1, // uin32_t ms: on time PKT_TYPE_CMD_BEEP_OFF = 0xA2, PKT_TYPE_CMD_SHUTDOWN = 0xA3, // uin32_t PKT_TYPE_CMD_POWER_ON = 0xA4, PKT_TYPE_SENSOR_SCD41_TEMP = 0xB0, // float PKT_TYPE_SENSOR_SCD41_HUMIDITY = 0xB1, // float PKT_TYPE_SENSOR_SCD41_CO2 = 0xB2, // float PKT_TYPE_SENSOR_AHT20_TEMP = 0xB3, // float PKT_TYPE_SENSOR_AHT20_HUMIDITY = 0xB4, // float PKT_TYPE_SENSOR_TVOC_INDEX = 0xB5, // float }; static int cmd_send(uint8_t cmd, const char *p_data, uint8_t len) { uint8_t send_buf[32] = {0}; uint8_t send_data[32] = {0}; if (len > 31) { return -1; } uint8_t index = 1; send_data[0] = cmd; if (len > 0 && p_data != NULL) { memcpy(&send_data[1], p_data, len); index += len; } cobs_encode_result ret = cobs_encode(send_buf, sizeof(send_buf), send_data, index); // LOG_DEBUG("cobs TX status:%d, len:%d, type 0x%x", ret.status, ret.out_len, cmd); if (ret.status == COBS_ENCODE_OK) { return uart_write_bytes(SENSOR_PORT_NUM, send_buf, ret.out_len + 1); } return -1; } bool IndicatorSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { LOG_INFO("%s: init", sensorName); setup(); return true; } void IndicatorSensor::setup() { uart_config_t uart_config = { .baud_rate = SENSOR_BAUD_RATE, .data_bits = UART_DATA_8_BITS, .parity = UART_PARITY_DISABLE, .stop_bits = UART_STOP_BITS_1, .flow_ctrl = UART_HW_FLOWCTRL_DISABLE, .source_clk = UART_SCLK_APB, }; int intr_alloc_flags = 0; char buffer[11]; uart_driver_install(SENSOR_PORT_NUM, SENSOR_BUF_SIZE * 2, 0, 0, NULL, intr_alloc_flags); uart_param_config(SENSOR_PORT_NUM, &uart_config); uart_set_pin(SENSOR_PORT_NUM, SENSOR_RP2040_TXD, SENSOR_RP2040_RXD, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE); cmd_send(PKT_TYPE_CMD_POWER_ON, NULL, 0); // measure and send only once every minute, for the phone API const char *interval = ultoa(60000, buffer, 10); cmd_send(PKT_TYPE_CMD_COLLECT_INTERVAL, interval, strlen(interval) + 1); } bool IndicatorSensor::getMetrics(meshtastic_Telemetry *measurement) { cobs_decode_result ret; int len = uart_read_bytes(SENSOR_PORT_NUM, buf, (SENSOR_BUF_SIZE - 1), 100 / portTICK_PERIOD_MS); float value = 0.0; uint8_t *p_buf_start = buf; uint8_t *p_buf_end = buf; if (len > 0) { while (p_buf_start < (buf + len)) { p_buf_end = p_buf_start; while (p_buf_end < (buf + len)) { if (*p_buf_end == 0x00) { break; } p_buf_end++; } // decode buf memset(data, 0, sizeof(data)); ret = cobs_decode(data, sizeof(data), p_buf_start, p_buf_end - p_buf_start); // LOG_DEBUG("cobs RX status:%d, len:%d, type:0x%x ", ret.status, ret.out_len, data[0]); if (ret.out_len > 1 && ret.status == COBS_DECODE_OK) { value = 0.0; uint8_t pkt_type = data[0]; switch (pkt_type) { case PKT_TYPE_SENSOR_SCD41_CO2: { memcpy(&value, &data[1], sizeof(value)); // LOG_DEBUG("CO2: %.1f", value); cmd_send(PKT_TYPE_ACK, ACK_PKT_PARA, 4); break; } case PKT_TYPE_SENSOR_AHT20_TEMP: { memcpy(&value, &data[1], sizeof(value)); // LOG_DEBUG("Temp: %.1f", value); cmd_send(PKT_TYPE_ACK, ACK_PKT_PARA, 4); measurement->variant.environment_metrics.has_temperature = true; measurement->variant.environment_metrics.temperature = value; break; } case PKT_TYPE_SENSOR_AHT20_HUMIDITY: { memcpy(&value, &data[1], sizeof(value)); // LOG_DEBUG("Humidity: %.1f", value); cmd_send(PKT_TYPE_ACK, ACK_PKT_PARA, 4); measurement->variant.environment_metrics.has_relative_humidity = true; measurement->variant.environment_metrics.relative_humidity = value; break; } case PKT_TYPE_SENSOR_TVOC_INDEX: { memcpy(&value, &data[1], sizeof(value)); // LOG_DEBUG("Tvoc: %.1f", value); cmd_send(PKT_TYPE_ACK, ACK_PKT_PARA, 4); measurement->variant.environment_metrics.has_iaq = true; measurement->variant.environment_metrics.iaq = value; break; } default: break; } } p_buf_start = p_buf_end + 1; // next message } return true; } return false; } #endif ================================================ FILE: src/modules/Telemetry/Sensor/IndicatorSensor.h ================================================ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "TelemetrySensor.h" class IndicatorSensor : public TelemetrySensor { public: IndicatorSensor(); virtual bool getMetrics(meshtastic_Telemetry *measurement) override; virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; private: void setup(); }; #endif ================================================ FILE: src/modules/Telemetry/Sensor/LPS22HBSensor.cpp ================================================ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "LPS22HBSensor.h" #include "TelemetrySensor.h" #include #include LPS22HBSensor::LPS22HBSensor() : TelemetrySensor(meshtastic_TelemetrySensorType_LPS22, "LPS22HB") {} bool LPS22HBSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { LOG_INFO("Init sensor: %s", sensorName); status = lps22hb.begin_I2C(dev->address.address, bus); if (!status) { return status; } lps22hb.setDataRate(LPS22_RATE_10_HZ); initI2CSensor(); return status; } 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 ================================================ FILE: src/modules/Telemetry/Sensor/LPS22HBSensor.h ================================================ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "TelemetrySensor.h" #include #include class LPS22HBSensor : public TelemetrySensor { private: Adafruit_LPS22 lps22hb; public: LPS22HBSensor(); virtual bool getMetrics(meshtastic_Telemetry *measurement) override; virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif ================================================ FILE: src/modules/Telemetry/Sensor/LTR390UVSensor.cpp ================================================ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "LTR390UVSensor.h" #include "TelemetrySensor.h" #include LTR390UVSensor::LTR390UVSensor() : TelemetrySensor(meshtastic_TelemetrySensorType_LTR390UV, "LTR390UV") {} bool LTR390UVSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { LOG_INFO("Init sensor: %s", sensorName); status = ltr390uv.begin(bus); if (!status) { return status; } ltr390uv.setMode(LTR390_MODE_UVS); ltr390uv.setGain(LTR390_GAIN_18); // Datasheet default ltr390uv.setResolution(LTR390_RESOLUTION_20BIT); // Datasheet default initI2CSensor(); return status; } bool LTR390UVSensor::getMetrics(meshtastic_Telemetry *measurement) { LOG_DEBUG("LTR390UV getMetrics"); // Because the sensor does not measure Lux and UV at the same time, we need to read them in two passes. if (ltr390uv.newDataAvailable()) { measurement->variant.environment_metrics.has_lux = true; measurement->variant.environment_metrics.has_uv_lux = true; if (ltr390uv.getMode() == LTR390_MODE_ALS) { lastLuxReading = 0.6 * ltr390uv.readALS() / (1 * 4); // Datasheet page 23 for gain x1 and 20bit resolution LOG_DEBUG("LTR390UV Lux reading: %f", lastLuxReading); measurement->variant.environment_metrics.lux = lastLuxReading; measurement->variant.environment_metrics.uv_lux = lastUVReading; ltr390uv.setGain( LTR390_GAIN_18); // Recommended for UVI - x18. Do not change, 2300 UV Sensitivity only specified for x18 gain ltr390uv.setMode(LTR390_MODE_UVS); return true; } else if (ltr390uv.getMode() == LTR390_MODE_UVS) { lastUVReading = ltr390uv.readUVS() / 2300.f; // Datasheet page 23 and page 6, only characterisation for gain x18 and 20bit resolution LOG_DEBUG("LTR390UV UV reading: %f", lastUVReading); measurement->variant.environment_metrics.lux = lastLuxReading; measurement->variant.environment_metrics.uv_lux = lastUVReading; ltr390uv.setGain( LTR390_GAIN_1); // x1 gain will already max out the sensor at direct sunlight, so no need to increase it ltr390uv.setMode(LTR390_MODE_ALS); return true; } } // In case we fail to read the sensor mode, set the has_lux and has_uv_lux back to false measurement->variant.environment_metrics.has_lux = false; measurement->variant.environment_metrics.has_uv_lux = false; return false; } #endif ================================================ FILE: src/modules/Telemetry/Sensor/LTR390UVSensor.h ================================================ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "TelemetrySensor.h" #include class LTR390UVSensor : public TelemetrySensor { private: Adafruit_LTR390 ltr390uv = Adafruit_LTR390(); float lastLuxReading = 0; float lastUVReading = 0; public: LTR390UVSensor(); virtual bool getMetrics(meshtastic_Telemetry *measurement) override; virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif ================================================ FILE: src/modules/Telemetry/Sensor/MAX17048Sensor.cpp ================================================ #include "MAX17048Sensor.h" #if !MESHTASTIC_EXCLUDE_I2C && __has_include() 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 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("MAX17048 getMetrics id: %i", measurement->which_variant); float volts = max17048->cellVoltage(); if (isnan(volts)) { LOG_DEBUG("MAX17048 getMetrics battery is not connected"); return false; } float rate = max17048->chargeRate(); // charge/discharge rate in percent/hr float soc = max17048->cellPercent(); // state of charge in percent 0 to 100 soc = clamp(soc, 0.0f, 100.0f); // clamp soc between 0 and 100% float ttg = (100.0f - soc) / rate; // calculate hours to charge/discharge LOG_DEBUG("MAX17048 getMetrics volts: %.3fV soc: %.1f%% ttg: %.1f hours", volts, soc, ttg); if ((int)measurement->which_variant == meshtastic_Telemetry_power_metrics_tag) { measurement->variant.power_metrics.has_ch1_voltage = true; measurement->variant.power_metrics.ch1_voltage = volts; } else if ((int)measurement->which_variant == meshtastic_Telemetry_device_metrics_tag) { measurement->variant.device_metrics.has_battery_level = true; measurement->variant.device_metrics.has_voltage = true; measurement->variant.device_metrics.battery_level = static_cast(round(soc)); measurement->variant.device_metrics.voltage = volts; } return true; } uint16_t MAX17048Sensor::getBusVoltageMv() { return max17048->getBusVoltageMv(); }; #endif #endif ================================================ FILE: src/modules/Telemetry/Sensor/MAX17048Sensor.h ================================================ #pragma once #ifndef MAX17048_SENSOR_H #define MAX17048_SENSOR_H #include "configuration.h" #if !MESHTASTIC_EXCLUDE_I2C && __has_include() // 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 ================================================ FILE: src/modules/Telemetry/Sensor/MAX30102Sensor.cpp ================================================ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && !MESHTASTIC_EXCLUDE_HEALTH_TELEMETRY && __has_include() #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 ================================================ FILE: src/modules/Telemetry/Sensor/MAX30102Sensor.h ================================================ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && !MESHTASTIC_EXCLUDE_HEALTH_TELEMETRY && __has_include() #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 ================================================ FILE: src/modules/Telemetry/Sensor/MCP9808Sensor.cpp ================================================ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "MCP9808Sensor.h" #include "TelemetrySensor.h" #include MCP9808Sensor::MCP9808Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_MCP9808, "MCP9808") {} bool MCP9808Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { LOG_INFO("Init sensor: %s", sensorName); status = mcp9808.begin(dev->address.address, bus); if (!status) { return status; } mcp9808.setResolution(2); initI2CSensor(); return status; } bool MCP9808Sensor::getMetrics(meshtastic_Telemetry *measurement) { measurement->variant.environment_metrics.has_temperature = true; LOG_DEBUG("MCP9808 getMetrics"); measurement->variant.environment_metrics.temperature = mcp9808.readTempC(); return true; } #endif ================================================ FILE: src/modules/Telemetry/Sensor/MCP9808Sensor.h ================================================ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "TelemetrySensor.h" #include class MCP9808Sensor : public TelemetrySensor { private: Adafruit_MCP9808 mcp9808; public: MCP9808Sensor(); virtual bool getMetrics(meshtastic_Telemetry *measurement) override; virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif ================================================ FILE: src/modules/Telemetry/Sensor/MLX90614Sensor.cpp ================================================ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() #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 ================================================ FILE: src/modules/Telemetry/Sensor/MLX90614Sensor.h ================================================ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() #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 ================================================ FILE: src/modules/Telemetry/Sensor/MLX90632Sensor.cpp ================================================ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "MLX90632Sensor.h" #include "TelemetrySensor.h" MLX90632Sensor::MLX90632Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_MLX90632, "MLX90632") {} bool MLX90632Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { LOG_INFO("Init sensor: %s", sensorName); MLX90632::status returnError; if (mlx.begin(dev->address.address, *bus, returnError) == true) // MLX90632 init { LOG_DEBUG("MLX90632 Init Succeed"); status = true; } else { LOG_ERROR("MLX90632 Init Failed"); status = false; } initI2CSensor(); return status; } 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 ================================================ FILE: src/modules/Telemetry/Sensor/MLX90632Sensor.h ================================================ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "TelemetrySensor.h" #include class MLX90632Sensor : public TelemetrySensor { private: MLX90632 mlx = MLX90632(); public: MLX90632Sensor(); virtual bool getMetrics(meshtastic_Telemetry *measurement) override; virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif ================================================ FILE: src/modules/Telemetry/Sensor/NAU7802Sensor.cpp ================================================ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "FSCommon.h" #include "NAU7802Sensor.h" #include "SPILock.h" #include "SafeFile.h" #include "TelemetrySensor.h" #include #include #include meshtastic_Nau7802Config nau7802config = meshtastic_Nau7802Config_init_zero; NAU7802Sensor::NAU7802Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_NAU7802, "NAU7802") {} bool NAU7802Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { LOG_INFO("Init sensor: %s", sensorName); status = nau7802.begin(*bus); if (!status) { return status; } nau7802.setSampleRate(NAU7802_SPS_320); if (!loadCalibrationData()) { LOG_ERROR("Failed to load calibration data"); } nau7802.calibrateAFE(); LOG_INFO("Offset: %d, Calibration factor: %.2f", nau7802.getZeroOffset(), nau7802.getCalibrationFactor()); initI2CSensor(); return status; } bool NAU7802Sensor::getMetrics(meshtastic_Telemetry *measurement) { LOG_DEBUG("NAU7802 getMetrics"); nau7802.powerUp(); // Wait for the sensor to become ready for one second max uint32_t start = millis(); while (!nau7802.available()) { delay(100); if (!Throttle::isWithinTimespanMs(start, 1000)) { nau7802.powerDown(); return false; } } 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; } // Note: SafeFile::close() already acquires the lock and releases it internally okay &= file.close(); return okay; } bool NAU7802Sensor::loadCalibrationData() { spiLock->lock(); auto file = FSCom.open(nau7802ConfigFileName, FILE_O_READ); bool okay = false; if (file) { LOG_INFO("%s state read from %s", sensorName, nau7802ConfigFileName); pb_istream_t stream = {&readcb, &file, meshtastic_Nau7802Config_size}; if (!pb_decode(&stream, &meshtastic_Nau7802Config_msg, &nau7802config)) { LOG_ERROR("Error: can't decode protobuf %s", PB_GET_ERROR(&stream)); } else { nau7802.setZeroOffset(nau7802config.zeroOffset); nau7802.setCalibrationFactor(nau7802config.calibrationFactor); okay = true; } file.close(); } else { LOG_INFO("No %s state found (File: %s)", sensorName, nau7802ConfigFileName); } spiLock->unlock(); return okay; } #endif ================================================ FILE: src/modules/Telemetry/Sensor/NAU7802Sensor.h ================================================ #include "MeshModule.h" #include "configuration.h" #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "TelemetrySensor.h" #include class NAU7802Sensor : public TelemetrySensor { private: NAU7802 nau7802; protected: const char *nau7802ConfigFileName = "/prefs/nau7802.dat"; bool saveCalibrationData(); bool loadCalibrationData(); public: NAU7802Sensor(); virtual bool getMetrics(meshtastic_Telemetry *measurement) override; virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; void tare(); void calibrate(float weight); AdminMessageHandleResult handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, meshtastic_AdminMessage *response) override; }; #endif ================================================ FILE: src/modules/Telemetry/Sensor/OPT3001Sensor.cpp ================================================ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "OPT3001Sensor.h" #include "TelemetrySensor.h" #include OPT3001Sensor::OPT3001Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_OPT3001, "OPT3001") {} bool OPT3001Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { LOG_INFO("Init sensor: %s", sensorName); auto errorCode = opt3001.begin(dev->address.address); status = errorCode == NO_ERROR; if (!status) { return status; } OPT3001_Config newConfig; newConfig.RangeNumber = 0b1100; newConfig.ConvertionTime = 0b0; newConfig.Latch = 0b1; newConfig.ModeOfConversionOperation = 0b11; OPT3001_ErrorCode errorConfig = opt3001.writeConfig(newConfig); if (errorConfig != NO_ERROR) { LOG_ERROR("OPT3001 configuration error #%d", errorConfig); } status = errorConfig == NO_ERROR; initI2CSensor(); return status; } 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 ================================================ FILE: src/modules/Telemetry/Sensor/OPT3001Sensor.h ================================================ #pragma once #include "configuration.h" #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "TelemetrySensor.h" #include class OPT3001Sensor : public TelemetrySensor { private: ClosedCube_OPT3001 opt3001; public: OPT3001Sensor(); #if WIRE_INTERFACES_COUNT > 1 virtual bool onlyWire1() { return true; } #endif virtual bool getMetrics(meshtastic_Telemetry *measurement) override; virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif ================================================ FILE: src/modules/Telemetry/Sensor/PCT2075Sensor.cpp ================================================ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "PCT2075Sensor.h" #include "TelemetrySensor.h" #include PCT2075Sensor::PCT2075Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_PCT2075, "PCT2075") {} bool PCT2075Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { LOG_INFO("Init sensor: %s", sensorName); status = pct2075.begin(dev->address.address, bus); initI2CSensor(); return status; } bool PCT2075Sensor::getMetrics(meshtastic_Telemetry *measurement) { measurement->variant.environment_metrics.has_temperature = true; measurement->variant.environment_metrics.temperature = pct2075.getTemperature(); return true; } #endif ================================================ FILE: src/modules/Telemetry/Sensor/PCT2075Sensor.h ================================================ #pragma once #include "configuration.h" #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "TelemetrySensor.h" #include class PCT2075Sensor : public TelemetrySensor { private: Adafruit_PCT2075 pct2075; public: PCT2075Sensor(); virtual bool getMetrics(meshtastic_Telemetry *measurement) override; virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif ================================================ FILE: src/modules/Telemetry/Sensor/PMSA003ISensor.cpp ================================================ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR #include "../detect/reClockI2C.h" #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "PMSA003ISensor.h" #include "TelemetrySensor.h" #include PMSA003ISensor::PMSA003ISensor() : TelemetrySensor(meshtastic_TelemetrySensorType_PMSA003I, "PMSA003I") {} bool PMSA003ISensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { LOG_INFO("Init sensor: %s", sensorName); #ifdef PMSA003I_ENABLE_PIN pinMode(PMSA003I_ENABLE_PIN, OUTPUT); #endif _bus = bus; _address = dev->address.address; #ifdef PMSA003I_I2C_CLOCK_SPEED #ifdef CAN_RECLOCK_I2C uint32_t currentClock = reClockI2C(PMSA003I_I2C_CLOCK_SPEED, _bus, false); #elif !HAS_SCREEN reClockI2C(PMSA003I_I2C_CLOCK_SPEED, _bus, true); #else LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName); return false; #endif /* CAN_RECLOCK_I2C */ #endif /* PMSA003I_I2C_CLOCK_SPEED */ _bus->beginTransmission(_address); if (_bus->endTransmission() != 0) { LOG_WARN("%s not found on I2C at 0x12", sensorName); return false; } #if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) reClockI2C(currentClock, _bus, false); #endif status = 1; LOG_INFO("%s Enabled", sensorName); initI2CSensor(); return true; } bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement) { if (!isActive()) { LOG_WARN("Can't get metrics. %s is not active", sensorName); return false; } #ifdef PMSA003I_I2C_CLOCK_SPEED #ifdef CAN_RECLOCK_I2C uint32_t currentClock = reClockI2C(PMSA003I_I2C_CLOCK_SPEED, _bus, false); #elif !HAS_SCREEN reClockI2C(PMSA003I_I2C_CLOCK_SPEED, _bus, true); #else LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName); return false; #endif /* CAN_RECLOCK_I2C */ #endif /* PMSA003I_I2C_CLOCK_SPEED */ _bus->requestFrom(_address, (uint8_t)PMSA003I_FRAME_LENGTH); if (_bus->available() < PMSA003I_FRAME_LENGTH) { LOG_WARN("%s read failed: incomplete data (%d bytes)", sensorName, _bus->available()); return false; } for (uint8_t i = 0; i < PMSA003I_FRAME_LENGTH; i++) { buffer[i] = _bus->read(); } #if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) reClockI2C(currentClock, _bus, false); #endif if (buffer[0] != 0x42 || buffer[1] != 0x4D) { LOG_WARN("%s frame header invalid: 0x%02X 0x%02X", sensorName, buffer[0], buffer[1]); return false; } auto read16 = [](const uint8_t *data, uint8_t idx) -> uint16_t { return (data[idx] << 8) | data[idx + 1]; }; computedChecksum = 0; for (uint8_t i = 0; i < PMSA003I_FRAME_LENGTH - 2; i++) { computedChecksum += buffer[i]; } receivedChecksum = read16(buffer, PMSA003I_FRAME_LENGTH - 2); if (computedChecksum != receivedChecksum) { LOG_WARN("%s checksum failed: computed 0x%04X, received 0x%04X", sensorName, computedChecksum, receivedChecksum); return false; } measurement->variant.air_quality_metrics.has_pm10_standard = true; measurement->variant.air_quality_metrics.pm10_standard = read16(buffer, 4); measurement->variant.air_quality_metrics.has_pm25_standard = true; measurement->variant.air_quality_metrics.pm25_standard = read16(buffer, 6); measurement->variant.air_quality_metrics.has_pm100_standard = true; measurement->variant.air_quality_metrics.pm100_standard = read16(buffer, 8); // TODO - Add admin command to remove environmental metrics to save protobuf space measurement->variant.air_quality_metrics.has_pm10_environmental = true; measurement->variant.air_quality_metrics.pm10_environmental = read16(buffer, 10); measurement->variant.air_quality_metrics.has_pm25_environmental = true; measurement->variant.air_quality_metrics.pm25_environmental = read16(buffer, 12); measurement->variant.air_quality_metrics.has_pm100_environmental = true; measurement->variant.air_quality_metrics.pm100_environmental = read16(buffer, 14); // TODO - Add admin command to remove PN to save protobuf space measurement->variant.air_quality_metrics.has_particles_03um = true; measurement->variant.air_quality_metrics.particles_03um = read16(buffer, 16); measurement->variant.air_quality_metrics.has_particles_05um = true; measurement->variant.air_quality_metrics.particles_05um = read16(buffer, 18); measurement->variant.air_quality_metrics.has_particles_10um = true; measurement->variant.air_quality_metrics.particles_10um = read16(buffer, 20); measurement->variant.air_quality_metrics.has_particles_25um = true; measurement->variant.air_quality_metrics.particles_25um = read16(buffer, 22); measurement->variant.air_quality_metrics.has_particles_50um = true; measurement->variant.air_quality_metrics.particles_50um = read16(buffer, 24); measurement->variant.air_quality_metrics.has_particles_100um = true; measurement->variant.air_quality_metrics.particles_100um = read16(buffer, 26); LOG_DEBUG("Got %s readings: pM1p0_standard=%u, pM2p5_standard=%u, pM10p0_standard=%u", sensorName, measurement->variant.air_quality_metrics.pm10_standard, measurement->variant.air_quality_metrics.pm25_standard, measurement->variant.air_quality_metrics.pm100_standard); return true; } bool PMSA003ISensor::isActive() { return state == State::ACTIVE; } int32_t PMSA003ISensor::wakeUpTimeMs() { #ifdef PMSA003I_ENABLE_PIN return PMSA003I_WARMUP_MS; #endif return 0; } int32_t PMSA003ISensor::pendingForReadyMs() { #ifdef PMSA003I_ENABLE_PIN uint32_t now; now = getTime(); uint32_t sincePmMeasureStarted = (now - pmMeasureStarted) * 1000; LOG_DEBUG("%s: Since measure started: %ums", sensorName, sincePmMeasureStarted); if (sincePmMeasureStarted < PMSA003I_WARMUP_MS) { LOG_INFO("%s: not enough time passed since starting measurement", sensorName); return PMSA003I_WARMUP_MS - sincePmMeasureStarted; } return 0; #endif return 0; } bool PMSA003ISensor::canSleep() { #ifdef PMSA003I_ENABLE_PIN return true; #endif return false; } void PMSA003ISensor::sleep() { #ifdef PMSA003I_ENABLE_PIN digitalWrite(PMSA003I_ENABLE_PIN, LOW); state = State::IDLE; pmMeasureStarted = 0; #endif } uint32_t PMSA003ISensor::wakeUp() { #ifdef PMSA003I_ENABLE_PIN LOG_INFO("Waking up %s", sensorName); digitalWrite(PMSA003I_ENABLE_PIN, HIGH); state = State::ACTIVE; pmMeasureStarted = getTime(); return PMSA003I_WARMUP_MS; #endif // No need to wait for warmup if already active return 0; } #endif ================================================ FILE: src/modules/Telemetry/Sensor/PMSA003ISensor.h ================================================ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "RTC.h" #include "TelemetrySensor.h" #define PMSA003I_I2C_CLOCK_SPEED 100000 #define PMSA003I_FRAME_LENGTH 32 #define PMSA003I_WARMUP_MS 30000 class PMSA003ISensor : public TelemetrySensor { public: PMSA003ISensor(); virtual bool getMetrics(meshtastic_Telemetry *measurement) override; virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; virtual bool isActive() override; virtual void sleep() override; virtual uint32_t wakeUp() override; virtual bool canSleep() override; virtual int32_t wakeUpTimeMs() override; virtual int32_t pendingForReadyMs() override; private: enum class State { IDLE, ACTIVE }; State state = State::ACTIVE; uint16_t computedChecksum = 0; uint16_t receivedChecksum = 0; uint32_t pmMeasureStarted = 0; uint8_t buffer[PMSA003I_FRAME_LENGTH]{}; TwoWire *_bus{}; uint8_t _address{}; }; #endif ================================================ FILE: src/modules/Telemetry/Sensor/RAK12035Sensor.cpp ================================================ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include("RAK12035_SoilMoisture.h") && defined(RAK_4631) && RAK_4631 == 1 #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "RAK12035Sensor.h" // The RAK12035 library's sensor_sleep() sets WB_IO2 (GPIO 34) LOW, which controls // the 3.3V switched power rail (PIN_3V3_EN). This turns off power to ALL peripherals // including GPS. We need to restore power after the library turns it off. #ifdef PIN_3V3_EN #define RESTORE_3V3_POWER() digitalWrite(PIN_3V3_EN, HIGH) #else #define RESTORE_3V3_POWER() #endif RAK12035Sensor::RAK12035Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_RAK12035, "RAK12035") {} bool RAK12035Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { // TODO:: check for up to 2 additional sensors and start them if present. sensor.set_sensor_addr(RAK120351_ADDR); delay(100); sensor.begin(dev->address.address); uint8_t data = 0; sensor.get_sensor_version(&data); if (data != 0) { LOG_INFO("Init sensor: %s", sensorName); LOG_INFO("RAK12035Sensor Init Succeed \nSensor Firmware version: %i, Sensor Name: %s", data, sensorName); status = true; sensor.sensor_sleep(); RESTORE_3V3_POWER(); } else { LOG_INFO("Init sensor: %s", sensorName); LOG_ERROR("RAK12035Sensor Init Failed"); status = false; } if (!status) { return status; } setup(); initI2CSensor(); return status; } void RAK12035Sensor::setup() { // TODO:: Check for and run calibration check for up to 2 additional sensors if present. uint16_t zero_val = 0; uint16_t hundred_val = 0; const uint16_t default_zero_val = 510; const uint16_t default_hundred_val = 390; sensor.sensor_on(); sensor.begin(); delay(200); sensor.get_dry_cal(&zero_val); delay(200); sensor.get_wet_cal(&hundred_val); delay(200); bool calibrationReset = false; if (zero_val == 0) { LOG_INFO("Dry calibration not set, using default: %d", default_zero_val); sensor.set_dry_cal(default_zero_val); delay(200); zero_val = default_zero_val; calibrationReset = true; } if (hundred_val == 0 || hundred_val >= zero_val) { LOG_INFO("Wet calibration not set, using default: %d", default_hundred_val); sensor.set_wet_cal(default_hundred_val); delay(200); hundred_val = default_hundred_val; calibrationReset = true; } if (calibrationReset) { LOG_INFO("Default calibration values applied. Consider running the calibration sketch for better accuracy: " "https://github.com/RAKWireless/RAK12035_SoilMoisture"); } LOG_INFO("Dry calibration value: %d, Wet calibration value: %d", zero_val, hundred_val); sensor.sensor_sleep(); RESTORE_3V3_POWER(); delay(200); LOG_INFO("Dry calibration value is %d", zero_val); LOG_INFO("Wet calibration value is %d", hundred_val); } bool RAK12035Sensor::getMetrics(meshtastic_Telemetry *measurement) { // TODO:: read and send metrics for up to 2 additional soil monitors if present. measurement->variant.environment_metrics.has_soil_temperature = true; measurement->variant.environment_metrics.has_soil_moisture = true; uint8_t moisture = 0; uint16_t temp = 0; bool success = false; sensor.sensor_on(); delay(200); success = sensor.get_sensor_moisture(&moisture); delay(200); success &= sensor.get_sensor_temperature(&temp); delay(200); sensor.sensor_sleep(); RESTORE_3V3_POWER(); if (success == false) { LOG_ERROR("Failed to read sensor data"); return false; } measurement->variant.environment_metrics.soil_temperature = ((float)temp / 10.0f); measurement->variant.environment_metrics.soil_moisture = moisture; return true; } #endif ================================================ FILE: src/modules/Telemetry/Sensor/RAK12035Sensor.h ================================================ #pragma once #include "configuration.h" #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() && defined(RAK_4631) #ifndef _MT_RAK12035VBSENSOR_H #define _MT_RAK12035VBSENSOR_H #endif #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "RAK12035_SoilMoisture.h" #include "TelemetrySensor.h" #include class RAK12035Sensor : public TelemetrySensor { private: RAK12035 sensor; void setup(); public: RAK12035Sensor(); #if WIRE_INTERFACES_COUNT > 1 virtual bool onlyWire1() { return true; } #endif virtual bool getMetrics(meshtastic_Telemetry *measurement) override; virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif ================================================ FILE: src/modules/Telemetry/Sensor/RAK9154Sensor.cpp ================================================ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && defined(HAS_RAKPROT) #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "RAK9154Sensor.h" #include "TelemetrySensor.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; extern RAK9154Sensor rak9154Sensor; static void onewire_evt(const uint8_t pid, const uint8_t sid, const SNHUBAPI_EVT_E eid, uint8_t *msg, uint16_t len) { switch (eid) { case SNHUBAPI_EVT_RECV_REQ: case SNHUBAPI_EVT_RECV_RSP: break; case SNHUBAPI_EVT_QSEND: mySerial.write(msg, len); break; case SNHUBAPI_EVT_ADD_SID: // LOG_INFO("+ADD:SID:[%02x]", msg[0]); break; case SNHUBAPI_EVT_ADD_PID: // LOG_INFO("+ADD:PID:[%02x]", 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]",pid,msg[0]); // for( uint16_t i=1; i 100) { dc_prec = 100; } break; case RAK_IPSO_DC_CURRENT: dc_cur = (msg[2] << 8) + msg[1]; break; case RAK_IPSO_DC_VOLTAGE: dc_vol = (msg[2] << 8) + msg[1]; dc_vol *= 10; break; default: break; } rak9154Sensor.setLastRead(millis()); break; case SNHUBAPI_EVT_REPORT: // LOG_INFO("+EVT:PID[%02x],IPSO[%02x]",pid,msg[0]); // for( uint16_t i=1; i 100) { dc_prec = 100; } break; case RAK_IPSO_DC_CURRENT: dc_cur = (msg[1] << 8) + msg[2]; break; case RAK_IPSO_DC_VOLTAGE: dc_vol = (msg[1] << 8) + msg[2]; dc_vol *= 10; break; default: break; } rak9154Sensor.setLastRead(millis()); break; case SNHUBAPI_EVT_CHKSUM_ERR: LOG_INFO("+ERR:CHKSUM"); break; case SNHUBAPI_EVT_SEQ_ERR: LOG_INFO("+ERR:SEQUCE"); break; 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() { if (!rak9154Sensor.isInitialized()) { onewirePeriodic = new Periodic("onewireHandle", onewireHandle); mySerial.begin(9600); RakSNHub_Protocl_API.init(onewire_evt); status = true; initialized = true; } return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; } void RAK9154Sensor::setup() { // Set up oversampling and filter initialization } bool RAK9154Sensor::getMetrics(meshtastic_Telemetry *measurement) { if (getBusVoltageMv() > 0) { measurement->variant.environment_metrics.has_voltage = true; measurement->variant.environment_metrics.has_current = true; measurement->variant.environment_metrics.voltage = (float)getBusVoltageMv() / 1000; measurement->variant.environment_metrics.current = (float)getCurrentMa() / 1000; return true; } else { return false; } } uint16_t RAK9154Sensor::getBusVoltageMv() { return dc_vol; } int16_t RAK9154Sensor::getCurrentMa() { return dc_cur; } int RAK9154Sensor::getBusBatteryPercent() { return (int)dc_prec; } bool RAK9154Sensor::isCharging() { return (dc_cur > 0) ? true : false; } void RAK9154Sensor::setLastRead(uint32_t lastRead) { this->lastRead = lastRead; } #endif // HAS_RAKPROT ================================================ FILE: src/modules/Telemetry/Sensor/RAK9154Sensor.h ================================================ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && defined(HAS_RAKPROT) #ifndef _RAK9154SENSOR_H #define _RAK9154SENSOR_H 1 #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "CurrentSensor.h" #include "TelemetrySensor.h" #include "VoltageSensor.h" class RAK9154Sensor : public TelemetrySensor, VoltageSensor, CurrentSensor { private: protected: virtual void setup() override; uint32_t lastRead = 0; public: RAK9154Sensor(); bool hasSensor() { return true; } // Not an I2C sensor; always available when HAS_RAKPROT is defined virtual int32_t runOnce() override; virtual bool getMetrics(meshtastic_Telemetry *measurement) override; virtual uint16_t getBusVoltageMv() override; virtual int16_t getCurrentMa() override; int getBusBatteryPercent(); bool isCharging(); void setLastRead(uint32_t lastRead); }; #endif // _RAK9154SENSOR_H #endif // HAS_RAKPROT ================================================ FILE: src/modules/Telemetry/Sensor/RCWL9620Sensor.cpp ================================================ #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") {} bool RCWL9620Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { LOG_INFO("Init sensor: %s", sensorName); status = 1; begin(bus, dev->address.address); initI2CSensor(); return status; } bool RCWL9620Sensor::getMetrics(meshtastic_Telemetry *measurement) { measurement->variant.environment_metrics.has_distance = true; LOG_DEBUG("RCWL9620 getMetrics"); measurement->variant.environment_metrics.distance = getDistance(); return true; } void RCWL9620Sensor::begin(TwoWire *wire, uint8_t addr, uint8_t sda, uint8_t scl, uint32_t speed) { _wire = wire; _addr = addr; _sda = sda; _scl = scl; _speed = speed; _wire->begin(); } float RCWL9620Sensor::getDistance() { uint32_t data = 0; uint8_t b1 = 0, b2 = 0, b3 = 0; LOG_DEBUG("[RCWL9620] Start measure command"); _wire->beginTransmission(_addr); _wire->write(0x01); // À tester aussi sans cette ligne si besoin uint8_t result = _wire->endTransmission(); LOG_DEBUG("[RCWL9620] endTransmission result = %d", result); delay(100); // délai pour laisser le capteur répondre LOG_DEBUG("[RCWL9620] Read i2c data:"); _wire->requestFrom(_addr, (uint8_t)3); if (_wire->available() < 3) { LOG_DEBUG("[RCWL9620] less than 3 octets !"); return 0.0; } b1 = _wire->read(); b2 = _wire->read(); b3 = _wire->read(); data = ((uint32_t)b1 << 16) | ((uint32_t)b2 << 8) | b3; float Distance = float(data) / 1000.0; LOG_DEBUG("[RCWL9620] Bytes readed = %02X %02X %02X", b1, b2, b3); LOG_DEBUG("[RCWL9620] data=%.2f, level=%.2f", (double)data, (double)Distance); if (Distance > 4500.00) { return 4500.00; } else { return Distance; } } #endif ================================================ FILE: src/modules/Telemetry/Sensor/RCWL9620Sensor.h ================================================ #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: void begin(TwoWire *wire = &Wire, uint8_t addr = 0x57, uint8_t sda = -1, uint8_t scl = -1, uint32_t speed = 200000UL); float getDistance(); public: RCWL9620Sensor(); virtual bool getMetrics(meshtastic_Telemetry *measurement) override; virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif ================================================ FILE: src/modules/Telemetry/Sensor/SCD30Sensor.cpp ================================================ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR && __has_include() #include "../detect/reClockI2C.h" #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "SCD30Sensor.h" #define SCD30_NO_ERROR 0 SCD30Sensor::SCD30Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SCD30, "SCD30") {} bool SCD30Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { LOG_INFO("Init sensor: %s", sensorName); _bus = bus; _address = dev->address.address; #ifdef SCD30_I2C_CLOCK_SPEED #ifdef CAN_RECLOCK_I2C uint32_t currentClock = reClockI2C(SCD30_I2C_CLOCK_SPEED, _bus, false); #elif !HAS_SCREEN reClockI2C(SCD30_I2C_CLOCK_SPEED, _bus, true); #else LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName); return false; #endif /* CAN_RECLOCK_I2C */ #endif /* SCD30_I2C_CLOCK_SPEED */ scd30.begin(*_bus, _address); if (!startMeasurement()) { LOG_ERROR("%s: Failed to start periodic measurement", sensorName); #if defined(SCD30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) reClockI2C(currentClock, _bus, false); #endif return false; } if (!getASC(ascActive)) { LOG_WARN("%s: Could not determine ASC state", sensorName); } #if defined(SCD30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) reClockI2C(currentClock, _bus, false); #endif if (state == SCD30_MEASUREMENT) { status = 1; } else { status = 0; } initI2CSensor(); return true; } bool SCD30Sensor::getMetrics(meshtastic_Telemetry *measurement) { float co2, temperature, humidity; #ifdef SCD30_I2C_CLOCK_SPEED #ifdef CAN_RECLOCK_I2C uint32_t currentClock = reClockI2C(SCD30_I2C_CLOCK_SPEED, _bus, false); #elif !HAS_SCREEN reClockI2C(SCD30_I2C_CLOCK_SPEED, _bus, true); #else LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName); return false; #endif /* CAN_RECLOCK_I2C */ #endif /* SCD30_I2C_CLOCK_SPEED */ if (scd30.readMeasurementData(co2, temperature, humidity) != SCD30_NO_ERROR) { LOG_ERROR("SCD30: Failed to read measurement data."); #if defined(SCD30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) reClockI2C(currentClock, _bus, false); #endif return false; } #if defined(SCD30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) reClockI2C(currentClock, _bus, false); #endif if (co2 == 0) { LOG_ERROR("SCD30: Invalid CO₂ reading."); return false; } measurement->variant.air_quality_metrics.has_co2 = true; measurement->variant.air_quality_metrics.has_co2_temperature = true; measurement->variant.air_quality_metrics.has_co2_humidity = true; measurement->variant.air_quality_metrics.co2 = (uint32_t)co2; measurement->variant.air_quality_metrics.co2_temperature = temperature; measurement->variant.air_quality_metrics.co2_humidity = humidity; LOG_DEBUG("Got %s readings: co2=%u, co2_temp=%.2f, co2_hum=%.2f", sensorName, (uint32_t)co2, temperature, humidity); return true; } bool SCD30Sensor::setMeasurementInterval(uint16_t measInterval) { uint16_t error; LOG_INFO("%s: setting measurement interval at %us", sensorName, measInterval); error = scd30.setMeasurementInterval(measInterval); if (error != SCD30_NO_ERROR) { LOG_ERROR("%s: Unable to set measurement interval. Error code: %u", sensorName, error); return false; } // Restart measuring so we don't need to wait the current interval to finish // (useful when you come from very long intervals) scd30.stopPeriodicMeasurement(); scd30.startPeriodicMeasurement(0); getMeasurementInterval(measurementInterval); return true; } bool SCD30Sensor::getMeasurementInterval(uint16_t &measInterval) { uint16_t error; LOG_INFO("%s: getting measurement interval", sensorName); error = scd30.getMeasurementInterval(measInterval); if (error != SCD30_NO_ERROR) { LOG_ERROR("%s: Unable to get measurement interval. Error code: %u", sensorName, error); return false; } LOG_INFO("%s: measurement interval is %us", sensorName, measInterval); return true; } /** * @brief Start measurement mode * @note This function should not change the clock */ bool SCD30Sensor::startMeasurement() { uint16_t error; if (state == SCD30_MEASUREMENT) { LOG_DEBUG("%s: Already in measurement mode", sensorName); return true; } error = scd30.startPeriodicMeasurement(0); if (error == SCD30_NO_ERROR) { LOG_INFO("%s: Started measurement mode", sensorName); state = SCD30_MEASUREMENT; return true; } else { LOG_ERROR("%s: Couldn't start measurement mode", sensorName); return false; } } /** * @brief Stop measurement mode * @note This function should not change the clock */ bool SCD30Sensor::stopMeasurement() { uint16_t error; error = scd30.stopPeriodicMeasurement(); if (error != SCD30_NO_ERROR) { LOG_ERROR("%s: Unable to stop measurement", sensorName); return false; } state = SCD30_IDLE; return true; } bool SCD30Sensor::performFRC(uint16_t targetCO2) { uint16_t error; LOG_INFO("%s: Issuing FRC. Ensure device has been working at least 3 minutes in stable target environment", sensorName); LOG_INFO("%s: Target CO2: %u ppm", sensorName, targetCO2); error = scd30.forceRecalibration((uint16_t)targetCO2); if (error != SCD30_NO_ERROR) { LOG_ERROR("%s: Unable to perform forced recalibration.", sensorName); return false; } LOG_INFO("%s: FRC Correction successful.", sensorName); return true; } bool SCD30Sensor::setASC(bool ascEnabled) { uint16_t error; LOG_INFO("%s: %s ASC", sensorName, ascEnabled ? "Enabling" : "Disabling"); error = scd30.activateAutoCalibration((uint16_t)ascEnabled); if (error != SCD30_NO_ERROR) { LOG_ERROR("%s: Unable to send command.", sensorName); return false; } if (!getASC(ascActive)) { LOG_ERROR("%s: Unable to check if ASC is enabled", sensorName); return false; } return true; } bool SCD30Sensor::getASC(uint16_t &_ascActive) { uint16_t error; // LOG_INFO("%s: Getting ASC", sensorName); error = scd30.getAutoCalibrationStatus(_ascActive); if (error != SCD30_NO_ERROR) { LOG_ERROR("%s: Unable to send command.", sensorName); return false; } LOG_INFO("%s: ASC is %s", sensorName, _ascActive ? "enabled" : "disabled"); return true; } /** * @brief Set the temperature reference. Unit ℃. * * The on-board RH/T sensor is influenced by thermal self-heating of SCD30 * and other electrical components. Design-in alters the thermal properties * of SCD30 such that temperature and humidity offsets may occur when * operating the sensor in end-customer devices. Compensation of those * effects is achievable by writing the temperature offset found in * continuous operation of the device into the sensor. Temperature offset * value is saved in non-volatile memory. The last set value will be used * for temperature offset compensation after repowering. * * @param[in] tempReference * @note this function is certainly confusing and it's not recommended */ bool SCD30Sensor::setTemperature(float tempReference) { uint16_t error; uint16_t updatedTempOffset; float tempOffset; uint16_t _tempOffset; float co2; float temperature; float humidity; if (tempReference == 100) { // Requesting the value of 100 will restore the temperature offset LOG_INFO("%s: Setting reference temperature at 0degC", sensorName); _tempOffset = 0; } else { LOG_INFO("%s: Setting reference temperature at: %.2f", sensorName, tempReference); error = scd30.readMeasurementData(co2, temperature, humidity); if (error != SCD30_NO_ERROR) { LOG_ERROR("%s: Unable to read current temperature. Error code: %u", sensorName, error); return false; } LOG_INFO("%s: Current sensor temperature: %.2f", sensorName, temperature); tempOffset = (temperature - tempReference); if (tempOffset < 0) { LOG_ERROR("%s temperature offset is only positive", sensorName); return false; } tempOffset *= 100; _tempOffset = static_cast(tempOffset); } LOG_INFO("%s: Setting temperature offset: %u (*100)", sensorName, _tempOffset); error = scd30.setTemperatureOffset(_tempOffset); if (error != SCD30_NO_ERROR) { LOG_ERROR("%s: Unable to set temperature offset. Error code: %u", sensorName, error); return false; } scd30.getTemperatureOffset(updatedTempOffset); LOG_INFO("%s: Updated sensor temperature offset: %u (*100)", sensorName, updatedTempOffset); return true; } bool SCD30Sensor::setAltitude(uint16_t altitude) { uint16_t error; LOG_INFO("%s: setting altitude at %um", sensorName, altitude); error = scd30.setAltitudeCompensation(altitude); if (error != SCD30_NO_ERROR) { LOG_ERROR("%s: Unable to set altitude. Error code: %u", sensorName, error); return false; } uint16_t newAltitude; getAltitude(newAltitude); return true; } bool SCD30Sensor::getAltitude(uint16_t &altitude) { uint16_t error; // LOG_INFO("%s: Getting altitude", sensorName); error = scd30.getAltitudeCompensation(altitude); if (error != SCD30_NO_ERROR) { LOG_ERROR("%s: Unable to get altitude. Error code: %u", sensorName, error); return false; } LOG_INFO("%s: Sensor altitude: %u", sensorName, altitude); return true; } bool SCD30Sensor::softReset() { uint16_t error; LOG_INFO("%s: Requesting soft reset", sensorName); error = scd30.softReset(); if (error != SCD30_NO_ERROR) { LOG_ERROR("%s: Unable to do soft reset. Error code: %u", sensorName, error); return false; } LOG_INFO("%s: soft reset successful", sensorName); return true; } /** * @brief Check if sensor is in measurement mode */ bool SCD30Sensor::isActive() { return state == SCD30_MEASUREMENT; } /** * @brief Start measurement mode * @note Not used in admin comands, getMetrics or init, can change clock. */ uint32_t SCD30Sensor::wakeUp() { #ifdef SCD30_I2C_CLOCK_SPEED #ifdef CAN_RECLOCK_I2C uint32_t currentClock = reClockI2C(SCD30_I2C_CLOCK_SPEED, _bus, false); #elif !HAS_SCREEN reClockI2C(SCD30_I2C_CLOCK_SPEED, _bus, true); #else LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName); return 0; #endif /* CAN_RECLOCK_I2C */ #endif /* SCD30_I2C_CLOCK_SPEED */ startMeasurement(); #if defined(SCD30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) reClockI2C(currentClock, _bus, false); #endif return 0; } /** * @brief Stop measurement mode * @note Not used in admin comands, getMetrics or init, can change clock. */ void SCD30Sensor::sleep() { #ifdef SCD30_I2C_CLOCK_SPEED #ifdef CAN_RECLOCK_I2C uint32_t currentClock = reClockI2C(SCD30_I2C_CLOCK_SPEED, _bus, false); #elif !HAS_SCREEN reClockI2C(SCD30_I2C_CLOCK_SPEED, _bus, true); #else LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName); return; #endif /* CAN_RECLOCK_I2C */ #endif /* SCD30_I2C_CLOCK_SPEED */ stopMeasurement(); #if defined(SCD30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) reClockI2C(currentClock, _bus, false); #endif } bool SCD30Sensor::canSleep() { return false; } int32_t SCD30Sensor::wakeUpTimeMs() { return 0; } int32_t SCD30Sensor::pendingForReadyMs() { return 0; } AdminMessageHandleResult SCD30Sensor::handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, meshtastic_AdminMessage *response) { AdminMessageHandleResult result; #ifdef SCD30_I2C_CLOCK_SPEED #ifdef CAN_RECLOCK_I2C uint32_t currentClock = reClockI2C(SCD30_I2C_CLOCK_SPEED, _bus, false); #elif !HAS_SCREEN reClockI2C(SCD30_I2C_CLOCK_SPEED, _bus, true); #else LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName); return AdminMessageHandleResult::NOT_HANDLED; #endif /* CAN_RECLOCK_I2C */ #endif /* SCD30_I2C_CLOCK_SPEED */ switch (request->which_payload_variant) { case meshtastic_AdminMessage_sensor_config_tag: // Check for ASC-FRC request first if (!request->sensor_config.has_scd30_config) { result = AdminMessageHandleResult::NOT_HANDLED; break; } if (request->sensor_config.scd30_config.has_soft_reset) { LOG_DEBUG("%s: Requested soft reset", sensorName); this->softReset(); } else { if (request->sensor_config.scd30_config.has_set_asc) { this->setASC(request->sensor_config.scd30_config.set_asc); if (request->sensor_config.scd30_config.set_asc == false) { LOG_DEBUG("%s: Request for FRC", sensorName); if (request->sensor_config.scd30_config.has_set_target_co2_conc) { this->performFRC(request->sensor_config.scd30_config.set_target_co2_conc); } else { // FRC requested but no target CO2 provided LOG_ERROR("%s: target CO2 not provided", sensorName); result = AdminMessageHandleResult::NOT_HANDLED; break; } } } // Check for temperature offset // NOTE: this requires to have a sensor working on stable environment // And to make it between readings if (request->sensor_config.scd30_config.has_set_temperature) { this->setTemperature(request->sensor_config.scd30_config.set_temperature); } // Check for altitude if (request->sensor_config.scd30_config.has_set_altitude) { this->setAltitude(request->sensor_config.scd30_config.set_altitude); } // Check for set measuremen interval if (request->sensor_config.scd30_config.has_set_measurement_interval) { this->setMeasurementInterval(request->sensor_config.scd30_config.set_measurement_interval); } } result = AdminMessageHandleResult::HANDLED; break; default: result = AdminMessageHandleResult::NOT_HANDLED; } #if defined(SCD30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) reClockI2C(currentClock, _bus, false); #endif return result; } #endif ================================================ FILE: src/modules/Telemetry/Sensor/SCD30Sensor.h ================================================ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR && __has_include() #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "TelemetrySensor.h" #include #define SCD30_I2C_CLOCK_SPEED 100000 class SCD30Sensor : public TelemetrySensor { private: SensirionI2cScd30 scd30; TwoWire *_bus{}; uint8_t _address{}; bool performFRC(uint16_t targetCO2); bool setASC(bool ascEnabled); bool getASC(uint16_t &ascEnabled); bool setTemperature(float tempReference); bool getAltitude(uint16_t &altitude); bool setAltitude(uint16_t altitude); bool softReset(); // bool setMeasurementInterval(uint16_t measInterval); bool getMeasurementInterval(uint16_t &measInterval); bool startMeasurement(); bool stopMeasurement(); // Parameters uint16_t ascActive = 1; uint16_t measurementInterval = 2; public: SCD30Sensor(); virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; virtual bool getMetrics(meshtastic_Telemetry *measurement) override; enum SCD30State { SCD30_OFF, SCD30_IDLE, SCD30_MEASUREMENT }; SCD30State state = SCD30_OFF; virtual bool isActive() override; virtual void sleep() override; // Stops measurement (measurement -> idle) virtual uint32_t wakeUp() override; // Starts measurement (idle -> measurement) virtual bool canSleep() override; virtual int32_t wakeUpTimeMs() override; virtual int32_t pendingForReadyMs() override; AdminMessageHandleResult handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, meshtastic_AdminMessage *response) override; }; #endif ================================================ FILE: src/modules/Telemetry/Sensor/SCD4XSensor.cpp ================================================ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR && __has_include() #include "../detect/reClockI2C.h" #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "SCD4XSensor.h" #define SCD4X_NO_ERROR 0 SCD4XSensor::SCD4XSensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SCD4X, "SCD4X") {} bool SCD4XSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { LOG_INFO("Init sensor: %s", sensorName); _bus = bus; _address = dev->address.address; #ifdef SCD4X_I2C_CLOCK_SPEED #ifdef CAN_RECLOCK_I2C uint32_t currentClock = reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, false); #elif !HAS_SCREEN reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, true); #else LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName); return false; #endif /* CAN_RECLOCK_I2C */ #endif /* SCD4X_I2C_CLOCK_SPEED */ scd4x.begin(*_bus, _address); // From SCD4X library delay(30); // Stop periodic measurement if (!stopMeasurement()) { #if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) reClockI2C(currentClock, _bus, false); #endif return false; } // Get sensor variant scd4x.getSensorVariant(sensorVariant); if (sensorVariant == SCD4X_SENSOR_VARIANT_SCD41) { LOG_INFO("%s: Found SCD41", sensorName); if (!powerUp()) { LOG_ERROR("%s: Error trying to execute powerUp()", sensorName); #if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) reClockI2C(currentClock, _bus, false); #endif return false; } } if (!getASC(ascActive)) { LOG_ERROR("%s: Unable to check if ASC is enabled", sensorName); #if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) reClockI2C(currentClock, _bus, false); #endif return false; } // Start measurement in selected power mode (low power by default) if (!startMeasurement()) { LOG_ERROR("%s: Couldn't start measurement", sensorName); #if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) reClockI2C(currentClock, _bus, false); #endif return false; } #if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) reClockI2C(currentClock, _bus, false); #endif if (state == SCD4X_MEASUREMENT) { status = 1; } else { status = 0; } initI2CSensor(); return true; } bool SCD4XSensor::getMetrics(meshtastic_Telemetry *measurement) { if (state != SCD4X_MEASUREMENT) { LOG_ERROR("%s: Not in measurement mode", sensorName); return false; } uint16_t co2, error; float temperature, humidity; #ifdef SCD4X_I2C_CLOCK_SPEED #ifdef CAN_RECLOCK_I2C uint32_t currentClock = reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, false); #elif !HAS_SCREEN reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, true); #else LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName); return false; #endif /* CAN_RECLOCK_I2C */ #endif /* SCD4X_I2C_CLOCK_SPEED */ bool dataReady; error = scd4x.getDataReadyStatus(dataReady); if (error != SCD4X_NO_ERROR || !dataReady) { #if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) reClockI2C(currentClock, _bus, false); #endif LOG_ERROR("SCD4X: Data is not ready"); return false; } error = scd4x.readMeasurement(co2, temperature, humidity); #if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) reClockI2C(currentClock, _bus, false); #endif LOG_DEBUG("Got %s readings: co2=%u, co2_temp=%.2f, co2_hum%.2f", sensorName, co2, temperature, humidity); if (error != SCD4X_NO_ERROR) { LOG_DEBUG("%s: Error while getting measurements: %u", sensorName, error); if (co2 == 0) { LOG_ERROR("%s: Skipping invalid measurement.", sensorName); } return false; } else { measurement->variant.air_quality_metrics.has_co2_temperature = true; measurement->variant.air_quality_metrics.has_co2_humidity = true; measurement->variant.air_quality_metrics.has_co2 = true; measurement->variant.air_quality_metrics.co2_temperature = temperature; measurement->variant.air_quality_metrics.co2_humidity = humidity; measurement->variant.air_quality_metrics.co2 = co2; return true; } } /** * @brief Perform a forced recalibration (FRC) of the CO₂ concentration. * * From Sensirion SCD4X I2C Library * * 1. Operate the SCD4x in the operation mode later used for normal sensor * operation (e.g. periodic measurement) for at least 3 minutes in an * environment with a homogenous and constant CO2 concentration. The sensor * must be operated at the voltage desired for the application when * performing the FRC sequence. 2. Issue the stop_periodic_measurement * command. 3. Issue the perform_forced_recalibration command. * @note This function should not change the clock */ bool SCD4XSensor::performFRC(uint32_t targetCO2) { uint16_t error, frcCorr; LOG_INFO("%s: Issuing FRC. Ensure device has been working at least 3 minutes in stable target environment", sensorName); if (!stopMeasurement()) { return false; } LOG_INFO("%s: Target CO2: %u ppm", sensorName, targetCO2); error = scd4x.performForcedRecalibration((uint16_t)targetCO2, frcCorr); // SCD4X Sensirion datasheet delay(400); if (error != SCD4X_NO_ERROR) { LOG_ERROR("%s: Unable to perform forced recalibration.", sensorName); return false; } if (frcCorr == 0xFFFF) { LOG_ERROR("%s: Error while performing forced recalibration.", sensorName); return false; } LOG_INFO("%s: FRC Correction successful. Correction output: %u", sensorName, (uint16_t)(frcCorr - 0x8000)); return true; } /** * @brief Start measurement mode * @note This function should not change the clock */ bool SCD4XSensor::startMeasurement() { uint16_t error; if (state == SCD4X_MEASUREMENT) { LOG_DEBUG("%s: Already in measurement mode", sensorName); return true; } if (lowPower) { error = scd4x.startLowPowerPeriodicMeasurement(); } else { error = scd4x.startPeriodicMeasurement(); } if (error == SCD4X_NO_ERROR) { LOG_INFO("%s: Started measurement mode", sensorName); if (lowPower) { LOG_INFO("%s: Low power mode", sensorName); } else { LOG_INFO("%s: Normal power mode", sensorName); } state = SCD4X_MEASUREMENT; return true; } else { LOG_ERROR("%s: Unable to start measurement mode", sensorName); return false; } } /** * @brief Stop measurement mode * @note This function should not change the clock */ bool SCD4XSensor::stopMeasurement() { uint16_t error; error = scd4x.stopPeriodicMeasurement(); if (error != SCD4X_NO_ERROR) { LOG_ERROR("%s: Unable to stop measurement.", sensorName); return false; } state = SCD4X_IDLE; co2MeasureStarted = 0; return true; } /** * @brief Set power mode * Pass true to set low power mode * @note This function should not change the clock */ bool SCD4XSensor::setPowerMode(bool _lowPower) { lowPower = _lowPower; if (!stopMeasurement()) { return false; } if (lowPower) { LOG_DEBUG("%s: Set low power mode", sensorName); } else { LOG_DEBUG("%s: Set normal power mode", sensorName); } return true; } /** * @brief Check the current mode (ASC or FRC) * From Sensirion SCD4X I2C Library * @note This function should not change the clock */ bool SCD4XSensor::getASC(uint16_t &_ascActive) { uint16_t error; LOG_INFO("%s: Getting ASC", sensorName); if (!stopMeasurement()) { return false; } error = scd4x.getAutomaticSelfCalibrationEnabled(_ascActive); if (error != SCD4X_NO_ERROR) { LOG_ERROR("%s: Unable to send command.", sensorName); return false; } LOG_INFO("%s ASC is %s", sensorName, _ascActive ? "enabled" : "disabled"); return true; } /** * @brief Enable or disable automatic self calibration (ASC). * * From Sensirion SCD4X I2C Library * * Sets the current state (enabled / disabled) of the ASC. By default, ASC * is enabled. * @note This function should not change the clock */ bool SCD4XSensor::setASC(bool ascEnabled) { uint16_t error; LOG_INFO("%s %s ASC", sensorName, ascEnabled ? "Enabling" : "Disabling"); if (!stopMeasurement()) { return false; } error = scd4x.setAutomaticSelfCalibrationEnabled((uint16_t)ascEnabled); if (error != SCD4X_NO_ERROR) { LOG_ERROR("%s: Unable to send command.", sensorName); return false; } error = scd4x.persistSettings(); if (error != SCD4X_NO_ERROR) { LOG_ERROR("%s: Unable to make settings persistent.", sensorName); return false; } if (!getASC(ascActive)) { LOG_ERROR("%s: Unable to check if ASC is enabled", sensorName); return false; } return true; } /** * @brief Set the value of ASC baseline target in ppm. * * From Sensirion SCD4X I2C Library. * * Sets the value of the ASC baseline target, i.e. the CO₂ concentration in * ppm which the ASC algorithm will assume as lower-bound background to * which the SCD4x is exposed to regularly within one ASC period of * operation. To save the setting to the EEPROM, the persist_settings * command must be issued subsequently. The factory default value is 400 * ppm. * @note This function should not change the clock */ bool SCD4XSensor::setASCBaseline(uint32_t targetCO2) { // Available in library, but not described in datasheet. uint16_t error; LOG_INFO("%s: Setting ASC baseline to: %u", sensorName, targetCO2); getASC(ascActive); if (!ascActive) { LOG_ERROR("%s: Can't set ASC baseline. ASC is not active", sensorName); return false; } if (!stopMeasurement()) { return false; } error = scd4x.setAutomaticSelfCalibrationTarget((uint16_t)targetCO2); if (error != SCD4X_NO_ERROR) { LOG_ERROR("%s: Unable to send command.", sensorName); return false; } error = scd4x.persistSettings(); if (error != SCD4X_NO_ERROR) { LOG_ERROR("%s: Unable to make settings persistent.", sensorName); return false; } LOG_INFO("%s: Setting ASC baseline successful", sensorName); return true; } /** * @brief Set the temperature compensation reference. * * From Sensirion SCD4X I2C Library. * * Setting the temperature offset of the SCD4x inside the customer device * allows the user to optimize the RH and T output signal. * By default, the temperature offset is set to 4 °C. To save * the setting to the EEPROM, the persist_settings command may be issued. * Equation (1) details how the characteristic temperature offset can be * calculated using the current temperature output of the sensor (TSCD4x), a * reference temperature value (TReference), and the previous temperature * offset (Toffset_pervious) obtained using the get_temperature_offset_raw * command: * * Toffset_actual = TSCD4x - TReference + Toffset_pervious. * * Recommended temperature offset values are between 0 °C and 20 °C. The * temperature offset does not impact the accuracy of the CO2 output. * @note This function should not change the clock */ bool SCD4XSensor::setTemperature(float tempReference) { uint16_t error; float prevTempOffset; float updatedTempOffset; float tempOffset; bool dataReady; uint16_t co2; float temperature; float humidity; LOG_INFO("%s: Setting reference temperature at: %.2f", sensorName, tempReference); error = scd4x.getDataReadyStatus(dataReady); if (error != SCD4X_NO_ERROR || !dataReady) { LOG_ERROR("%s: Data is not ready", sensorName); return false; } error = scd4x.readMeasurement(co2, temperature, humidity); if (error != SCD4X_NO_ERROR) { LOG_ERROR("%s: Unable to read current temperature. Error code: %u", sensorName, error); return false; } LOG_INFO("%s: Current sensor temperature: %.2f", sensorName, temperature); if (!stopMeasurement()) { return false; } error = scd4x.getTemperatureOffset(prevTempOffset); if (error != SCD4X_NO_ERROR) { LOG_ERROR("%s: Unable to get temperature offset. Error code: %u", sensorName, error); return false; } LOG_INFO("%s: Current sensor temperature offset: %.2f", sensorName, prevTempOffset); tempOffset = temperature - tempReference + prevTempOffset; LOG_INFO("%s: Setting temperature offset: %.2f", sensorName, tempOffset); error = scd4x.setTemperatureOffset(tempOffset); if (error != SCD4X_NO_ERROR) { LOG_ERROR("%s: Unable to set temperature offset. Error code: %u", sensorName, error); return false; } error = scd4x.persistSettings(); if (error != SCD4X_NO_ERROR) { LOG_ERROR("%s: Unable to make settings persistent. Error code: %u", sensorName, error); return false; } scd4x.getTemperatureOffset(updatedTempOffset); LOG_INFO("%s: Updated sensor temperature offset: %.2f", sensorName, updatedTempOffset); return true; } /** * @brief Get the sensor altitude. * * From Sensirion SCD4X I2C Library. * * Altitude in meters above sea level can be set after device installation. * Valid value between 0 and 3000m. This overrides pressure offset. * @note This function should not change the clock */ bool SCD4XSensor::getAltitude(uint16_t &altitude) { uint16_t error; LOG_INFO("%s: Requesting sensor altitude", sensorName); if (!stopMeasurement()) { return false; } error = scd4x.getSensorAltitude(altitude); if (error != SCD4X_NO_ERROR) { LOG_ERROR("%s: Unable to get altitude. Error code: %u", sensorName, error); return false; } LOG_INFO("%s: Sensor altitude: %u", sensorName, altitude); return true; } /** * @brief Get the ambient pressure around the sensor. * * From Sensirion SCD4X I2C Library. * * Gets the ambient pressure in Pa. * @note This function should not change the clock */ bool SCD4XSensor::getAmbientPressure(uint32_t &ambientPressure) { uint16_t error; LOG_INFO("%s: Requesting sensor ambient pressure", sensorName); error = scd4x.getAmbientPressure(ambientPressure); if (error != SCD4X_NO_ERROR) { LOG_ERROR("%s: Unable to get altitude. Error code: %u", sensorName, error); return false; } LOG_INFO("%s: Sensor ambient pressure: %u", sensorName, ambientPressure); return true; } /** * @brief Set the sensor altitude. * * From Sensirion SCD4X I2C Library. * * Altitude in meters above sea level can be set after device installation. * Valid value between 0 and 3000m. This overrides pressure offset. * @note This function should not change the clock */ bool SCD4XSensor::setAltitude(uint32_t altitude) { uint16_t error; if (!stopMeasurement()) { return false; } LOG_INFO("%s: setting altitude at %um", sensorName, altitude); error = scd4x.setSensorAltitude(altitude); if (error != SCD4X_NO_ERROR) { LOG_ERROR("%s: Unable to set altitude. Error code: %u", sensorName, error); return false; } // NOTE: this gives an error if issued. Sensirion's library // doesn't indicate it's needed. // error = scd4x.persistSettings(); // if (error != SCD4X_NO_ERROR) { // LOG_ERROR("%s: Unable to make settings persistent. Error code: %u", sensorName, error); // return false; // } LOG_INFO("%s: altitude set", sensorName); return true; } /** * @brief Set the ambient pressure around the sensor. * * From Sensirion SCD4X I2C Library. * * The set_ambient_pressure command can be sent during periodic measurements * to enable continuous pressure compensation. Note that setting an ambient * pressure overrides any pressure compensation based on a previously set * sensor altitude. Use of this command is highly recommended for * applications experiencing significant ambient pressure changes to ensure * sensor accuracy. Valid input values are between 70000 - 120000 Pa. The * default value is 101300 Pa. * @note This function should not change the clock */ bool SCD4XSensor::setAmbientPressure(uint32_t ambientPressure) { uint16_t error; LOG_INFO("%s: setting ambient pressure at %u Pa", sensorName, ambientPressure); error = scd4x.setAmbientPressure(ambientPressure); if (error != SCD4X_NO_ERROR) { LOG_ERROR("%s: Unable to set altitude. Error code: %u", sensorName, error); return false; } // Sensirion doesn't indicate if this is necessary. We send it anyway error = scd4x.persistSettings(); if (error != SCD4X_NO_ERROR) { LOG_ERROR("%s: Unable to make settings persistent. Error code: %u", sensorName, error); return false; } LOG_INFO("%s: ambient pressure set set", sensorName); return true; } /** * @brief Perform factory reset to erase the settings stored in the EEPROM. * * From Sensirion SCD4X I2C Library. * * The perform_factory_reset command resets all configuration settings * stored in the EEPROM and erases the FRC and ASC algorithm history. * @note This function should not change the clock */ bool SCD4XSensor::factoryReset() { uint16_t error; LOG_INFO("%s: Requesting factory reset", sensorName); if (!stopMeasurement()) { return false; } error = scd4x.performFactoryReset(); if (error != SCD4X_NO_ERROR) { LOG_ERROR("%s: Unable to do factory reset. Error code: %u", sensorName, error); return false; } LOG_INFO("%s: Factory reset successful", sensorName); return true; } /** * @brief Put the sensor into sleep mode from idle mode. * * From Sensirion SCD4X I2C Library. * * Put the sensor from idle to sleep to reduce power consumption. Can be * used to power down when operating the sensor in power-cycled single shot * mode. * @note This command is only available in idle mode. Only for SCD41. */ bool SCD4XSensor::powerDown() { LOG_INFO("%s: Trying to send sensor to sleep", sensorName); if (sensorVariant != SCD4X_SENSOR_VARIANT_SCD41) { LOG_WARN("SCD4X: Can't send sensor to sleep. Incorrect variant. Ignoring"); return true; } #ifdef SCD4X_I2C_CLOCK_SPEED #ifdef CAN_RECLOCK_I2C uint32_t currentClock = reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, false); #elif !HAS_SCREEN reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, true); #else LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName); return false; #endif /* CAN_RECLOCK_I2C */ #endif /* SCD4X_I2C_CLOCK_SPEED */ if (!stopMeasurement()) { #if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) reClockI2C(currentClock, _bus, false); #endif return false; } if (scd4x.powerDown() != SCD4X_NO_ERROR) { LOG_ERROR("%s: Error trying to execute sleep()", sensorName); #if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) reClockI2C(currentClock, _bus, false); #endif return false; } #if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) reClockI2C(currentClock, _bus, false); #endif state = SCD4X_OFF; return true; } /** * @brief Wake up sensor from sleep mode to idle mode (powerUp) * * From Sensirion SCD4X I2C Library. * * Wake up the sensor from sleep mode into idle mode. Note that the SCD4x * does not acknowledge the wake_up command. The sensor's idle state after * wake up can be verified by reading out the serial number. * @note This command is only available for SCD41. * @note This function can't change clock (used in init) */ bool SCD4XSensor::powerUp() { LOG_INFO("%s: Waking up", sensorName); if (scd4x.wakeUp() != SCD4X_NO_ERROR) { LOG_ERROR("%s: Error trying to execute wakeUp()", sensorName); return false; } state = SCD4X_IDLE; return true; } /** * @brief Check if sensor is in measurement mode */ bool SCD4XSensor::isActive() { return state == SCD4X_MEASUREMENT; } /** * @brief Start measurement mode * @note Not used in admin comands, getMetrics or init, can change clock. */ uint32_t SCD4XSensor::wakeUp() { #ifdef SCD4X_I2C_CLOCK_SPEED #ifdef CAN_RECLOCK_I2C uint32_t currentClock = reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, false); #elif !HAS_SCREEN reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, true); #else LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName); return 0; #endif /* CAN_RECLOCK_I2C */ #endif /* SCD4X_I2C_CLOCK_SPEED */ if (startMeasurement()) { co2MeasureStarted = getTime(); #if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) reClockI2C(currentClock, _bus, false); #endif return SCD4X_WARMUP_MS; } #if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) reClockI2C(currentClock, _bus, false); #endif return 0; } /** * @brief Stop measurement mode * @note Not used in admin comands, getMetrics or init, can change clock. */ void SCD4XSensor::sleep() { #ifdef SCD4X_I2C_CLOCK_SPEED #ifdef CAN_RECLOCK_I2C uint32_t currentClock = reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, false); #elif !HAS_SCREEN reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, true); #else LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName); return; #endif /* CAN_RECLOCK_I2C */ #endif /* SCD4X_I2C_CLOCK_SPEED */ stopMeasurement(); #if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) reClockI2C(currentClock, _bus, false); #endif } /** * @brief Can sleep function * * Power consumption is very low on lowPower mode, modify this function if * you still want to override this behaviour. Otherwise, sleep is disabled * routinely in low power mode */ bool SCD4XSensor::canSleep() { return lowPower ? false : true; } int32_t SCD4XSensor::wakeUpTimeMs() { return SCD4X_WARMUP_MS; } int32_t SCD4XSensor::pendingForReadyMs() { uint32_t now; now = getTime(); uint32_t sinceCO2MeasureStarted = (now - co2MeasureStarted) * 1000; LOG_DEBUG("%s: Since measure started: %ums", sensorName, sinceCO2MeasureStarted); if (sinceCO2MeasureStarted < SCD4X_WARMUP_MS) { LOG_INFO("%s: not enough time passed since starting measurement", sensorName); return SCD4X_WARMUP_MS - sinceCO2MeasureStarted; } return 0; } AdminMessageHandleResult SCD4XSensor::handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, meshtastic_AdminMessage *response) { AdminMessageHandleResult result; #ifdef SCD4X_I2C_CLOCK_SPEED #ifdef CAN_RECLOCK_I2C uint32_t currentClock = reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, false); #elif !HAS_SCREEN reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, true); #else LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName); return AdminMessageHandleResult::NOT_HANDLED; #endif /* CAN_RECLOCK_I2C */ #endif /* SCD4X_I2C_CLOCK_SPEED */ // TODO: potentially add selftest command? switch (request->which_payload_variant) { case meshtastic_AdminMessage_sensor_config_tag: // Check for ASC-FRC request first if (!request->sensor_config.has_scd4x_config) { result = AdminMessageHandleResult::NOT_HANDLED; break; } if (request->sensor_config.scd4x_config.has_factory_reset) { LOG_DEBUG("%s: Requested factory reset", sensorName); if (!this->factoryReset()) { result = AdminMessageHandleResult::NOT_HANDLED; break; } } else { if (request->sensor_config.scd4x_config.has_set_asc) { getASC(ascActive); bool currentASC = ascActive; if (request->sensor_config.scd4x_config.set_asc == false) { LOG_DEBUG("%s: Request for FRC", sensorName); if (request->sensor_config.scd4x_config.has_set_target_co2_conc) { if (this->setASC(request->sensor_config.scd4x_config.set_asc)) { if (!this->performFRC(request->sensor_config.scd4x_config.set_target_co2_conc)) { result = AdminMessageHandleResult::NOT_HANDLED; // Set it back to ASC if failed setASC(currentASC); break; }; } else { result = AdminMessageHandleResult::NOT_HANDLED; break; } } else { // FRC requested but no target CO2 provided LOG_ERROR("%s: target CO2 not provided", sensorName); result = AdminMessageHandleResult::NOT_HANDLED; break; } } else { LOG_DEBUG("%s: Request for ASC", sensorName); if (this->setASC(request->sensor_config.scd4x_config.set_asc)) { if (request->sensor_config.scd4x_config.has_set_target_co2_conc) { LOG_DEBUG("%s: Request has target CO2", sensorName); this->setASCBaseline(request->sensor_config.scd4x_config.set_target_co2_conc); // NOTE - in this situation, if we set ASC, but baseline set fails, we stay on ASC } else { LOG_DEBUG("%s: Request doesn't have target CO2", sensorName); } } else { result = AdminMessageHandleResult::NOT_HANDLED; break; } } } // Check for temperature offset // NOTE: this requires to have a sensor working on stable environment // And to make it between readings if (request->sensor_config.scd4x_config.has_set_temperature) { if (!this->setTemperature(request->sensor_config.scd4x_config.set_temperature)) { result = AdminMessageHandleResult::NOT_HANDLED; break; } } // Check for altitude or pressure offset if (request->sensor_config.scd4x_config.has_set_altitude) { if (!this->setAltitude(request->sensor_config.scd4x_config.set_altitude)) { result = AdminMessageHandleResult::NOT_HANDLED; break; } } else if (request->sensor_config.scd4x_config.has_set_ambient_pressure) { if (!this->setAmbientPressure(request->sensor_config.scd4x_config.set_ambient_pressure)) { result = AdminMessageHandleResult::NOT_HANDLED; break; } } // Check for low power mode // NOTE: to switch from one mode to another do: // setPowerMode -> startMeasurement if (request->sensor_config.scd4x_config.has_set_power_mode) { if (!this->setPowerMode(request->sensor_config.scd4x_config.set_power_mode)) { result = AdminMessageHandleResult::NOT_HANDLED; break; } } } result = AdminMessageHandleResult::HANDLED; break; default: result = AdminMessageHandleResult::NOT_HANDLED; } // Start measurement mode this->startMeasurement(); #if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) reClockI2C(currentClock, _bus, false); #endif return result; } #endif ================================================ FILE: src/modules/Telemetry/Sensor/SCD4XSensor.h ================================================ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR && __has_include() #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "RTC.h" #include "TelemetrySensor.h" #include // Max speed 400kHz #define SCD4X_I2C_CLOCK_SPEED 100000 #define SCD4X_WARMUP_MS 5000 class SCD4XSensor : public TelemetrySensor { private: SensirionI2cScd4x scd4x; TwoWire *_bus{}; uint8_t _address{}; bool performFRC(uint32_t targetCO2); bool setASCBaseline(uint32_t targetCO2); bool getASC(uint16_t &ascEnabled); bool setASC(bool ascEnabled); bool setTemperature(float tempReference); bool getAltitude(uint16_t &altitude); bool setAltitude(uint32_t altitude); bool getAmbientPressure(uint32_t &ambientPressure); bool setAmbientPressure(uint32_t ambientPressure); bool factoryReset(); bool setPowerMode(bool _lowPower); bool startMeasurement(); bool stopMeasurement(); uint16_t ascActive = 1; // low power measurement mode (on sensirion side). Disables sleep mode // Improvement and testing needed for timings bool lowPower = true; uint32_t co2MeasureStarted = 0; public: SCD4XSensor(); virtual bool getMetrics(meshtastic_Telemetry *measurement) override; virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; enum SCD4XState { SCD4X_OFF, SCD4X_IDLE, SCD4X_MEASUREMENT }; SCD4XState state = SCD4X_OFF; SCD4xSensorVariant sensorVariant{}; virtual bool isActive() override; virtual void sleep() override; // Stops measurement (measurement -> idle) virtual uint32_t wakeUp() override; // Starts measurement (idle -> measurement) bool powerDown(); // Powers down sensor (idle -> power-off) bool powerUp(); // Powers the sensor (power-off -> idle) virtual bool canSleep() override; virtual int32_t wakeUpTimeMs() override; virtual int32_t pendingForReadyMs() override; AdminMessageHandleResult handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, meshtastic_AdminMessage *response) override; }; #endif ================================================ FILE: src/modules/Telemetry/Sensor/SEN5XSensor.cpp ================================================ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR #include "../detect/reClockI2C.h" #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "FSCommon.h" #include "SEN5XSensor.h" #include "SPILock.h" #include "SafeFile.h" #include "TelemetrySensor.h" #include // FLT_MAX #include #include SEN5XSensor::SEN5XSensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SEN5X, "SEN5X") {} bool SEN5XSensor::getVersion() { if (!sendCommand(SEN5X_GET_FIRMWARE_VERSION)) { LOG_ERROR("SEN5X: Error sending version command"); return false; } delay(20); // From Sensirion Datasheet uint8_t versionBuffer[12]{}; size_t charNumber = readBuffer(&versionBuffer[0], 3); if (charNumber == 0) { LOG_ERROR("SEN5X: Error getting data ready flag value"); return false; } firmwareVer = versionBuffer[0] + (versionBuffer[1] / 10); hardwareVer = versionBuffer[3] + (versionBuffer[4] / 10); protocolVer = versionBuffer[5] + (versionBuffer[6] / 10); LOG_INFO("SEN5X Firmware Version: %0.2f", firmwareVer); LOG_INFO("SEN5X Hardware Version: %0.2f", hardwareVer); LOG_INFO("SEN5X Protocol Version: %0.2f", protocolVer); return true; } bool SEN5XSensor::findModel() { if (!sendCommand(SEN5X_GET_PRODUCT_NAME)) { LOG_ERROR("SEN5X: Error asking for product name"); return false; } delay(50); // From Sensirion Datasheet const uint8_t nameSize = 48; uint8_t name[nameSize]; size_t charNumber = readBuffer(&name[0], nameSize); if (charNumber == 0) { LOG_ERROR("SEN5X: Error getting device name"); return false; } // We only check the last character that defines the model SEN5X switch (name[4]) { case 48: model = SEN50; LOG_INFO("SEN5X: found sensor model SEN50"); break; case 52: model = SEN54; LOG_INFO("SEN5X: found sensor model SEN54"); break; case 53: model = SEN55; LOG_INFO("SEN5X: found sensor model SEN55"); break; } return true; } bool SEN5XSensor::sendCommand(uint16_t command) { uint8_t nothing; return sendCommand(command, ¬hing, 0); } bool SEN5XSensor::sendCommand(uint16_t command, uint8_t *buffer, uint8_t byteNumber) { // At least we need two bytes for the command uint8_t bufferSize = 2; // Add space for CRC bytes (one every two bytes) if (byteNumber > 0) bufferSize += byteNumber + (byteNumber / 2); uint8_t toSend[bufferSize]; uint8_t i = 0; toSend[i++] = static_cast((command & 0xFF00) >> 8); toSend[i++] = static_cast((command & 0x00FF) >> 0); // Prepare buffer with CRC every third byte uint8_t bi = 0; if (byteNumber > 0) { while (bi < byteNumber) { toSend[i++] = buffer[bi++]; toSend[i++] = buffer[bi++]; uint8_t calcCRC = sen5xCRC(&buffer[bi - 2]); toSend[i++] = calcCRC; } } #ifdef SEN5X_I2C_CLOCK_SPEED #ifdef CAN_RECLOCK_I2C uint32_t currentClock = reClockI2C(SEN5X_I2C_CLOCK_SPEED, _bus, false); #elif !HAS_SCREEN reClockI2C(SEN5X_I2C_CLOCK_SPEED, _bus, true); #else LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName); return false; #endif /* CAN_RECLOCK_I2C */ #endif /* SEN5X_I2C_CLOCK_SPEED */ // Transmit the data // LOG_DEBUG("Beginning connection to SEN5X: 0x%x. Size: %u", address, bufferSize); // Note: this delay is necessary to allow for long-buffers delay(20); _bus->beginTransmission(_address); size_t writtenBytes = _bus->write(toSend, bufferSize); uint8_t i2c_error = _bus->endTransmission(); #if defined(SEN5X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) reClockI2C(currentClock, _bus, false); #endif if (writtenBytes != bufferSize) { LOG_ERROR("SEN5X: Error writting on I2C bus"); return false; } if (i2c_error != 0) { LOG_ERROR("SEN5X: Error on I2C communication: %x", i2c_error); return false; } return true; } uint8_t SEN5XSensor::readBuffer(uint8_t *buffer, uint8_t byteNumber) { #ifdef SEN5X_I2C_CLOCK_SPEED #ifdef CAN_RECLOCK_I2C uint32_t currentClock = reClockI2C(SEN5X_I2C_CLOCK_SPEED, _bus, false); #elif !HAS_SCREEN reClockI2C(SEN5X_I2C_CLOCK_SPEED, _bus, true); #else LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName); return false; #endif /* CAN_RECLOCK_I2C */ #endif /* SEN5X_I2C_CLOCK_SPEED */ size_t readBytes = _bus->requestFrom(_address, byteNumber); if (readBytes != byteNumber) { LOG_ERROR("SEN5X: Error reading I2C bus"); return 0; } uint8_t i = 0; uint8_t receivedBytes = 0; while (readBytes > 0) { buffer[i++] = _bus->read(); // Just as a reminder: i++ returns i and after that increments. buffer[i++] = _bus->read(); uint8_t recvCRC = _bus->read(); uint8_t calcCRC = sen5xCRC(&buffer[i - 2]); if (recvCRC != calcCRC) { LOG_ERROR("SEN5X: Checksum error while receiving msg"); return 0; } readBytes -= 3; receivedBytes += 2; } #if defined(SEN5X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) reClockI2C(currentClock, _bus, false); #endif return receivedBytes; } uint8_t SEN5XSensor::sen5xCRC(const uint8_t *buffer) { // This code is based on Sensirion's own implementation // https://github.com/Sensirion/arduino-core/blob/41fd02cacf307ec4945955c58ae495e56809b96c/src/SensirionCrc.cpp uint8_t crc = 0xff; for (uint8_t i = 0; i < 2; i++) { crc ^= buffer[i]; for (uint8_t bit = 8; bit > 0; bit--) { if (crc & 0x80) crc = (crc << 1) ^ 0x31; else crc = (crc << 1); } } return crc; } void SEN5XSensor::sleep() { idle(true); } bool SEN5XSensor::idle(bool checkState) { // From the datasheet: // By default, the VOC algorithm resets its state to initial // values each time a measurement is started, // even if the measurement was stopped only for a short // time. So, the VOC index output value needs a long time // until it is stable again. This can be avoided by // restoring the previously memorized algorithm state before // starting the measure mode if (checkState) { // If the stabilisation period is not passed for SEN54 or SEN55, don't go to idle if (model != SEN50) { // Get VOC state before going to idle mode vocValid = false; if (vocStateFromSensor()) { vocValid = vocStateValid(); // Check if we have time, and store it uint32_t now; // If time is RTCQualityNone, it will return zero now = getValidTime(RTCQuality::RTCQualityDevice); // Check if state is valid (non-zero) if (now) { vocTime = now; } } if (!(vocStateStable() && vocValid)) { LOG_INFO("%s: Not stopping measurement, vocState is not stable yet!", sensorName); return true; } } // Save state and prefs (on all models) saveState(); } if (!oneShotMode) { LOG_INFO("%s: Not stopping measurement, continuous mode!", sensorName); return true; } else { LOG_INFO("%s: One shot mode enabled", sensorName); } // Switch to low-power based on the model if (model == SEN50) { if (!sendCommand(SEN5X_STOP_MEASUREMENT)) { LOG_ERROR("%s: Error stopping measurement", sensorName); return false; } state = SEN5X_IDLE; LOG_INFO("%s: Stop measurement mode", sensorName); } else { if (!sendCommand(SEN5X_START_MEASUREMENT_RHT_GAS)) { LOG_ERROR("%s: Error switching to RHT/Gas measurement", sensorName); return false; } state = SEN5X_RHTGAS_ONLY; LOG_INFO("%s: Switch to RHT/Gas only measurement mode", sensorName); } delay(200); // From Sensirion Datasheet pmMeasureStarted = 0; return true; } bool SEN5XSensor::vocStateRecent(uint32_t now) { if (now) { uint32_t passed = now - vocTime; // in seconds // Check if state is recent, less than 10 minutes (600 seconds) if (passed < SEN5X_VOC_VALID_TIME && (now > SEN5X_VOC_VALID_DATE)) { return true; } } return false; } bool SEN5XSensor::vocStateValid() { if (!vocState[0] && !vocState[1] && !vocState[2] && !vocState[3] && !vocState[4] && !vocState[5] && !vocState[6] && !vocState[7]) { LOG_DEBUG("%s: VOC state is all 0, invalid", sensorName); return false; } else { LOG_DEBUG("%s: VOC state is valid", sensorName); return true; } } bool SEN5XSensor::vocStateToSensor() { if (model == SEN50) { return true; } if (!vocStateValid()) { LOG_INFO("SEN5X: VOC state is invalid, not sending"); return true; } if (!sendCommand(SEN5X_STOP_MEASUREMENT)) { LOG_ERROR("SEN5X: Error stoping measurement"); return false; } delay(200); // From Sensirion Datasheet LOG_DEBUG("SEN5X: Sending VOC state to sensor"); LOG_DEBUG("[%u, %u, %u, %u, %u, %u, %u, %u]", vocState[0], vocState[1], vocState[2], vocState[3], vocState[4], vocState[5], vocState[6], vocState[7]); // Note: send command already takes into account the CRC // buffer size increment needed if (!sendCommand(SEN5X_RW_VOCS_STATE, vocState, SEN5X_VOC_STATE_BUFFER_SIZE)) { LOG_ERROR("SEN5X: Error sending VOC's state command'"); return false; } return true; } bool SEN5XSensor::vocStateFromSensor() { if (model == SEN50) { return true; } LOG_INFO("SEN5X: Getting VOC state from sensor"); // Ask VOCs state from the sensor if (!sendCommand(SEN5X_RW_VOCS_STATE)) { LOG_ERROR("SEN5X: Error sending VOC's state command'"); return false; } delay(20); // From Sensirion Datasheet // Retrieve the data // Allocate buffer to account for CRC size_t receivedNumber = readBuffer(&vocState[0], SEN5X_VOC_STATE_BUFFER_SIZE + (SEN5X_VOC_STATE_BUFFER_SIZE / 2)); delay(20); // From Sensirion Datasheet if (receivedNumber == 0) { LOG_DEBUG("SEN5X: Error getting VOC's state"); return false; } // Print the state (if debug is on) LOG_DEBUG("SEN5X: VOC state retrieved from sensor: [%u, %u, %u, %u, %u, %u, %u, %u]", vocState[0], vocState[1], vocState[2], vocState[3], vocState[4], vocState[5], vocState[6], vocState[7]); return true; } bool SEN5XSensor::loadState() { #ifdef FSCom spiLock->lock(); auto file = FSCom.open(sen5XStateFileName, FILE_O_READ); bool okay = false; if (file) { LOG_INFO("%s state read from %s", sensorName, sen5XStateFileName); pb_istream_t stream = {&readcb, &file, meshtastic_SEN5XState_size}; if (!pb_decode(&stream, &meshtastic_SEN5XState_msg, &sen5xstate)) { LOG_ERROR("Error: can't decode protobuf %s", PB_GET_ERROR(&stream)); } else { lastCleaning = sen5xstate.last_cleaning_time; lastCleaningValid = sen5xstate.last_cleaning_valid; oneShotMode = sen5xstate.one_shot_mode; if (model != SEN50) { vocTime = sen5xstate.voc_state_time; vocValid = sen5xstate.voc_state_valid; // Unpack state vocState[7] = (uint8_t)(sen5xstate.voc_state_array >> 56); vocState[6] = (uint8_t)(sen5xstate.voc_state_array >> 48); vocState[5] = (uint8_t)(sen5xstate.voc_state_array >> 40); vocState[4] = (uint8_t)(sen5xstate.voc_state_array >> 32); vocState[3] = (uint8_t)(sen5xstate.voc_state_array >> 24); vocState[2] = (uint8_t)(sen5xstate.voc_state_array >> 16); vocState[1] = (uint8_t)(sen5xstate.voc_state_array >> 8); vocState[0] = (uint8_t)sen5xstate.voc_state_array; } // LOG_DEBUG("Loaded lastCleaning %u", lastCleaning); // LOG_DEBUG("Loaded lastCleaningValid %u", lastCleaningValid); // LOG_DEBUG("Loaded oneShotMode %s", oneShotMode ? "true" : "false"); // LOG_DEBUG("Loaded vocTime %u", vocTime); // LOG_DEBUG("Loaded [%u, %u, %u, %u, %u, %u, %u, %u]", // vocState[7], vocState[6], vocState[5], vocState[4], vocState[3], vocState[2], vocState[1], vocState[0]); // LOG_DEBUG("Loaded %svalid VOC state", vocValid ? "" : "in"); okay = true; } file.close(); } else { LOG_INFO("No %s state found (File: %s)", sensorName, sen5XStateFileName); } spiLock->unlock(); return okay; #else LOG_ERROR("SEN5X: ERROR - Filesystem not implemented"); #endif } bool SEN5XSensor::saveState() { #ifdef FSCom auto file = SafeFile(sen5XStateFileName); sen5xstate.last_cleaning_time = lastCleaning; sen5xstate.last_cleaning_valid = lastCleaningValid; sen5xstate.one_shot_mode = oneShotMode; if (model != SEN50) { sen5xstate.has_voc_state_time = true; sen5xstate.has_voc_state_valid = true; sen5xstate.has_voc_state_array = true; sen5xstate.voc_state_time = vocTime; sen5xstate.voc_state_valid = vocValid; // Unpack state (8 bytes) sen5xstate.voc_state_array = (((uint64_t)vocState[7]) << 56) | ((uint64_t)vocState[6] << 48) | ((uint64_t)vocState[5] << 40) | ((uint64_t)vocState[4] << 32) | ((uint64_t)vocState[3] << 24) | ((uint64_t)vocState[2] << 16) | ((uint64_t)vocState[1] << 8) | ((uint64_t)vocState[0]); } bool okay = false; LOG_INFO("%s: state write to %s", sensorName, sen5XStateFileName); pb_ostream_t stream = {&writecb, static_cast(&file), meshtastic_SEN5XState_size}; if (!pb_encode(&stream, &meshtastic_SEN5XState_msg, &sen5xstate)) { LOG_ERROR("Error: can't encode protobuf %s", PB_GET_ERROR(&stream)); } else { okay = true; } okay &= file.close(); if (okay) LOG_INFO("%s: state write to %s successful", sensorName, sen5XStateFileName); return okay; #else LOG_ERROR("%s: ERROR - Filesystem not implemented", sensorName); #endif } bool SEN5XSensor::isActive() { return state == SEN5X_MEASUREMENT || state == SEN5X_MEASUREMENT_2; } uint32_t SEN5XSensor::wakeUp() { LOG_DEBUG("SEN5X: Waking up sensor"); if (!sendCommand(SEN5X_START_MEASUREMENT)) { LOG_ERROR("SEN5X: Error starting measurement"); // TODO - what should this return?? Something actually on the default interval? return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; } delay(50); // From Sensirion Datasheet // TODO - This is currently "problematic" // If time is updated in between reads, there is no way to // keep track of how long it has passed pmMeasureStarted = getTime(); state = SEN5X_MEASUREMENT; if (state == SEN5X_MEASUREMENT) LOG_INFO("SEN5X: Started measurement mode"); return SEN5X_WARMUP_MS_1; } bool SEN5XSensor::vocStateStable() { uint32_t now; now = getTime(); uint32_t sinceFirstMeasureStarted = (now - rhtGasMeasureStarted); LOG_DEBUG("sinceFirstMeasureStarted: %us", sinceFirstMeasureStarted); return sinceFirstMeasureStarted > SEN5X_VOC_STATE_WARMUP_S; } bool SEN5XSensor::startCleaning() { // Note: we only should enter here if we have a valid RTC with at least // RTCQuality::RTCQualityDevice state = SEN5X_CLEANING; // Note that cleaning command can only be run when the sensor is in measurement mode if (!sendCommand(SEN5X_START_MEASUREMENT)) { LOG_ERROR("SEN5X: Error starting measurment mode"); return false; } delay(50); // From Sensirion Datasheet if (!sendCommand(SEN5X_START_FAN_CLEANING)) { LOG_ERROR("SEN5X: Error starting fan cleaning"); return false; } delay(20); // From Sensirion Datasheet // This message will be always printed so the user knows the device it's not hung LOG_INFO("SEN5X: Started fan cleaning it will take 10 seconds..."); uint16_t started = millis(); while (millis() - started < 10500) { delay(500); } LOG_INFO("SEN5X: Cleaning done!!"); // Save timestamp in flash so we know when a week has passed uint32_t now; now = getValidTime(RTCQuality::RTCQualityDevice); // If time is not RTCQualityNone, it will return non-zero lastCleaning = now; lastCleaningValid = true; saveState(); idle(); return true; } bool SEN5XSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { state = SEN5X_NOT_DETECTED; LOG_INFO("Init sensor: %s", sensorName); _bus = bus; _address = dev->address.address; delay(50); // without this there is an error on the deviceReset function if (!sendCommand(SEN5X_RESET)) { LOG_ERROR("SEN5X: Error reseting device"); return false; } delay(200); // From Sensirion Datasheet if (!findModel()) { LOG_ERROR("SEN5X: error finding sensor model"); return false; } // Check the firmware version if (!getVersion()) return false; if (firmwareVer < 2) { LOG_ERROR("SEN5X: error firmware is too old and will not work with this implementation"); return false; } delay(200); // From Sensirion Datasheet // Detection succeeded state = SEN5X_IDLE; status = 1; // Load state loadState(); // Check if it is time to do a cleaning uint32_t now; int32_t passed = 0; now = getValidTime(RTCQuality::RTCQualityDevice); // If time is not RTCQualityNone, it will return non-zero if (now) { if (lastCleaningValid) { passed = now - lastCleaning; // in seconds if (passed > ONE_WEEK_IN_SECONDS && (now > SEN5X_VOC_VALID_DATE)) { // If current date greater than 01/01/2018 (validity check) LOG_INFO("SEN5X: More than a week (%us) since last cleaning in epoch (%us). Trigger, cleaning...", passed, lastCleaning); startCleaning(); } else { LOG_INFO("SEN5X: Cleaning not needed (%ds passed). Last cleaning date (in epoch): %us", passed, lastCleaning); } } else { // We assume the device has just been updated or it is new, // so no need to trigger a cleaning. // Just save the timestamp to do a cleaning one week from now. // Otherwise, we will never trigger cleaning in some cases lastCleaning = now; lastCleaningValid = true; LOG_INFO("SEN5X: No valid last cleaning date found, saving it now: %us", lastCleaning); saveState(); } if (model != SEN50) { if (!vocValid) { LOG_INFO("SEN5X: No valid VOC's state found"); } else { // Check if state is recent if (vocStateRecent(now)) { // If current date greater than 01/01/2018 (validity check) // Send it to the sensor LOG_INFO("SEN5X: VOC state is valid and recent"); vocStateToSensor(); } else { LOG_INFO("SEN5X: VOC state is too old or date is invalid"); LOG_DEBUG("SEN5X: vocTime %u, Passed %u, and now %u", vocTime, passed, now); } } } } else { // TODO - Should this actually ignore? We could end up never cleaning... LOG_INFO("SEN5X: Not enough RTCQuality, ignoring saved cleaning and VOC state"); } idle(false); rhtGasMeasureStarted = now; initI2CSensor(); return true; } bool SEN5XSensor::readValues() { if (!sendCommand(SEN5X_READ_VALUES)) { LOG_ERROR("SEN5X: Error sending read command"); return false; } LOG_DEBUG("SEN5X: Reading PM Values"); delay(20); // From Sensirion Datasheet uint8_t dataBuffer[16]{}; size_t receivedNumber = readBuffer(&dataBuffer[0], 24); if (receivedNumber == 0) { LOG_ERROR("SEN5X: Error getting values"); return false; } // Get the integers uint16_t uint_pM1p0 = static_cast((dataBuffer[0] << 8) | dataBuffer[1]); uint16_t uint_pM2p5 = static_cast((dataBuffer[2] << 8) | dataBuffer[3]); uint16_t uint_pM4p0 = static_cast((dataBuffer[4] << 8) | dataBuffer[5]); uint16_t uint_pM10p0 = static_cast((dataBuffer[6] << 8) | dataBuffer[7]); int16_t int_humidity = static_cast((dataBuffer[8] << 8) | dataBuffer[9]); int16_t int_temperature = static_cast((dataBuffer[10] << 8) | dataBuffer[11]); int16_t int_vocIndex = static_cast((dataBuffer[12] << 8) | dataBuffer[13]); int16_t int_noxIndex = static_cast((dataBuffer[14] << 8) | dataBuffer[15]); // Convert values based on Sensirion Arduino lib sen5xmeasurement.pM1p0 = !isnan(uint_pM1p0) ? uint_pM1p0 / 10 : UINT16_MAX; sen5xmeasurement.pM2p5 = !isnan(uint_pM2p5) ? uint_pM2p5 / 10 : UINT16_MAX; sen5xmeasurement.pM4p0 = !isnan(uint_pM4p0) ? uint_pM4p0 / 10 : UINT16_MAX; sen5xmeasurement.pM10p0 = !isnan(uint_pM10p0) ? uint_pM10p0 / 10 : UINT16_MAX; sen5xmeasurement.humidity = !isnan(int_humidity) ? int_humidity / 100.0f : FLT_MAX; sen5xmeasurement.temperature = !isnan(int_temperature) ? int_temperature / 200.0f : FLT_MAX; sen5xmeasurement.vocIndex = !isnan(int_vocIndex) ? int_vocIndex / 10.0f : FLT_MAX; sen5xmeasurement.noxIndex = !isnan(int_noxIndex) ? int_noxIndex / 10.0f : FLT_MAX; LOG_DEBUG("Got %s readings: pM1p0=%u, pM2p5=%u, pM4p0=%u, pM10p0=%u", sensorName, sen5xmeasurement.pM1p0, sen5xmeasurement.pM2p5, sen5xmeasurement.pM4p0, sen5xmeasurement.pM10p0); if (model != SEN50) { LOG_DEBUG("Got %s readings: humidity=%.2f, temperature=%.2f, vocIndex=%.2f", sensorName, sen5xmeasurement.humidity, sen5xmeasurement.temperature, sen5xmeasurement.vocIndex); } if (model == SEN55) { LOG_DEBUG("Got %s readings: noxIndex=%.2f", sensorName, sen5xmeasurement.noxIndex); } return true; } bool SEN5XSensor::readPNValues(bool cumulative) { if (!sendCommand(SEN5X_READ_PM_VALUES)) { LOG_ERROR("SEN5X: Error sending read command"); return false; } LOG_DEBUG("SEN5X: Reading PN Values"); delay(20); // From Sensirion Datasheet uint8_t dataBuffer[20]{}; size_t receivedNumber = readBuffer(&dataBuffer[0], 30); if (receivedNumber == 0) { LOG_ERROR("SEN5X: Error getting PN values"); return false; } // Get the integers // uint16_t uint_pM1p0 = static_cast((dataBuffer[0] << 8) | dataBuffer[1]); // uint16_t uint_pM2p5 = static_cast((dataBuffer[2] << 8) | dataBuffer[3]); // uint16_t uint_pM4p0 = static_cast((dataBuffer[4] << 8) | dataBuffer[5]); // uint16_t uint_pM10p0 = static_cast((dataBuffer[6] << 8) | dataBuffer[7]); uint16_t uint_pN0p5 = static_cast((dataBuffer[8] << 8) | dataBuffer[9]); uint16_t uint_pN1p0 = static_cast((dataBuffer[10] << 8) | dataBuffer[11]); uint16_t uint_pN2p5 = static_cast((dataBuffer[12] << 8) | dataBuffer[13]); uint16_t uint_pN4p0 = static_cast((dataBuffer[14] << 8) | dataBuffer[15]); uint16_t uint_pN10p0 = static_cast((dataBuffer[16] << 8) | dataBuffer[17]); uint16_t uint_tSize = static_cast((dataBuffer[18] << 8) | dataBuffer[19]); // Convert values based on Sensirion Arduino lib // Multiply by 100 for converting from #/cm3 to #/0.1l for PN values sen5xmeasurement.pN0p5 = !isnan(uint_pN0p5) ? uint_pN0p5 / 10 * 100 : UINT32_MAX; sen5xmeasurement.pN1p0 = !isnan(uint_pN1p0) ? uint_pN1p0 / 10 * 100 : UINT32_MAX; sen5xmeasurement.pN2p5 = !isnan(uint_pN2p5) ? uint_pN2p5 / 10 * 100 : UINT32_MAX; sen5xmeasurement.pN4p0 = !isnan(uint_pN4p0) ? uint_pN4p0 / 10 * 100 : UINT32_MAX; sen5xmeasurement.pN10p0 = !isnan(uint_pN10p0) ? uint_pN10p0 / 10 * 100 : UINT32_MAX; sen5xmeasurement.tSize = !isnan(uint_tSize) ? uint_tSize / 1000.0f : FLT_MAX; // Remove accumuluative values: // https://github.com/fablabbcn/smartcitizen-kit-2x/issues/85 if (!cumulative) { sen5xmeasurement.pN10p0 -= sen5xmeasurement.pN4p0; sen5xmeasurement.pN4p0 -= sen5xmeasurement.pN2p5; sen5xmeasurement.pN2p5 -= sen5xmeasurement.pN1p0; sen5xmeasurement.pN1p0 -= sen5xmeasurement.pN0p5; } LOG_DEBUG("Got %s readings: pN0p5=%u, pN1p0=%u, pN2p5=%u, pN4p0=%u, pN10p0=%u, tSize=%.2f", sensorName, sen5xmeasurement.pN0p5, sen5xmeasurement.pN1p0, sen5xmeasurement.pN2p5, sen5xmeasurement.pN4p0, sen5xmeasurement.pN10p0, sen5xmeasurement.tSize); return true; } uint8_t SEN5XSensor::getMeasurements() { uint32_t now; now = getTime(); // Try to get new data if (!sendCommand(SEN5X_READ_DATA_READY)) { LOG_ERROR("SEN5X: Error sending command data ready flag"); return 2; } delay(20); // From Sensirion Datasheet uint8_t dataReadyBuffer[3]; size_t charNumber = readBuffer(&dataReadyBuffer[0], 3); if (charNumber == 0) { LOG_ERROR("SEN5X: Error getting device version value"); return 2; } bool dataReady = dataReadyBuffer[1]; uint32_t sinceLastDataPollMs = (now - lastDataPoll) * 1000; // Check if data is ready, and if since last time we requested is less than SEN5X_POLL_INTERVAL if (!dataReady && (sinceLastDataPollMs > SEN5X_POLL_INTERVAL)) { LOG_INFO("SEN5X: Data is not ready"); return 1; } if (!readValues()) { LOG_ERROR("SEN5X: Error getting readings"); return 2; } if (!readPNValues(false)) { LOG_ERROR("SEN5X: Error getting PN readings"); return 2; } lastDataPoll = now; return 0; } int32_t SEN5XSensor::wakeUpTimeMs() { return SEN5X_WARMUP_MS_2; } int32_t SEN5XSensor::pendingForReadyMs() { uint32_t now; now = getTime(); uint32_t sincePmMeasureStarted = (now - pmMeasureStarted) * 1000; LOG_DEBUG("SEN5X: Since measure started: %ums", sincePmMeasureStarted); switch (state) { case SEN5X_MEASUREMENT: { if (sincePmMeasureStarted < SEN5X_WARMUP_MS_1) { LOG_INFO("SEN5X: not enough time passed since starting measurement"); return SEN5X_WARMUP_MS_1 - sincePmMeasureStarted; } if (!pmMeasureStarted) { pmMeasureStarted = now; } // Get PN values to check if we are above or below threshold readPNValues(true); lastDataPoll = now; // If the reading is low (the tyhreshold is in #/cm3) and second warmUp hasn't passed we return to come back later if ((sen5xmeasurement.pN4p0 / 100) < SEN5X_PN4P0_CONC_THD && sincePmMeasureStarted < SEN5X_WARMUP_MS_2) { LOG_INFO("SEN5X: Concentration is low, we will ask again in the second warm up period"); state = SEN5X_MEASUREMENT_2; // Report how many seconds are pending to cover the first warm up period return SEN5X_WARMUP_MS_2 - sincePmMeasureStarted; } return 0; } case SEN5X_MEASUREMENT_2: { if (sincePmMeasureStarted < SEN5X_WARMUP_MS_2) { // Report how many seconds are pending to cover the first warm up period return SEN5X_WARMUP_MS_2 - sincePmMeasureStarted; } return 0; } default: { return -1; } } } bool SEN5XSensor::getMetrics(meshtastic_Telemetry *measurement) { LOG_INFO("SEN5X: Attempting to get metrics"); if (!isActive()) { LOG_INFO("SEN5X: not in measurement mode"); return false; } uint8_t response; response = getMeasurements(); if (response == 0) { if (sen5xmeasurement.pM1p0 != UINT16_MAX) { measurement->variant.air_quality_metrics.has_pm10_standard = true; measurement->variant.air_quality_metrics.pm10_standard = sen5xmeasurement.pM1p0; } if (sen5xmeasurement.pM2p5 != UINT16_MAX) { measurement->variant.air_quality_metrics.has_pm25_standard = true; measurement->variant.air_quality_metrics.pm25_standard = sen5xmeasurement.pM2p5; } if (sen5xmeasurement.pM4p0 != UINT16_MAX) { measurement->variant.air_quality_metrics.has_pm40_standard = true; measurement->variant.air_quality_metrics.pm40_standard = sen5xmeasurement.pM4p0; } if (sen5xmeasurement.pM10p0 != UINT16_MAX) { measurement->variant.air_quality_metrics.has_pm100_standard = true; measurement->variant.air_quality_metrics.pm100_standard = sen5xmeasurement.pM10p0; } if (sen5xmeasurement.pN0p5 != UINT32_MAX) { measurement->variant.air_quality_metrics.has_particles_05um = true; measurement->variant.air_quality_metrics.particles_05um = sen5xmeasurement.pN0p5; } if (sen5xmeasurement.pN1p0 != UINT32_MAX) { measurement->variant.air_quality_metrics.has_particles_10um = true; measurement->variant.air_quality_metrics.particles_10um = sen5xmeasurement.pN1p0; } if (sen5xmeasurement.pN2p5 != UINT32_MAX) { measurement->variant.air_quality_metrics.has_particles_25um = true; measurement->variant.air_quality_metrics.particles_25um = sen5xmeasurement.pN2p5; } if (sen5xmeasurement.pN4p0 != UINT32_MAX) { measurement->variant.air_quality_metrics.has_particles_40um = true; measurement->variant.air_quality_metrics.particles_40um = sen5xmeasurement.pN4p0; } if (sen5xmeasurement.pN10p0 != UINT32_MAX) { measurement->variant.air_quality_metrics.has_particles_100um = true; measurement->variant.air_quality_metrics.particles_100um = sen5xmeasurement.pN10p0; } if (sen5xmeasurement.tSize != FLT_MAX) { measurement->variant.air_quality_metrics.has_particles_tps = true; measurement->variant.air_quality_metrics.particles_tps = sen5xmeasurement.tSize; } if (model != SEN50) { if (sen5xmeasurement.humidity != FLT_MAX) { measurement->variant.air_quality_metrics.has_pm_humidity = true; measurement->variant.air_quality_metrics.pm_humidity = sen5xmeasurement.humidity; } if (sen5xmeasurement.temperature != FLT_MAX) { measurement->variant.air_quality_metrics.has_pm_temperature = true; measurement->variant.air_quality_metrics.pm_temperature = sen5xmeasurement.temperature; } if (sen5xmeasurement.noxIndex != FLT_MAX) { measurement->variant.air_quality_metrics.has_pm_voc_idx = true; measurement->variant.air_quality_metrics.pm_voc_idx = sen5xmeasurement.vocIndex; } } if (model == SEN55) { if (sen5xmeasurement.noxIndex != FLT_MAX) { measurement->variant.air_quality_metrics.has_pm_nox_idx = true; measurement->variant.air_quality_metrics.pm_nox_idx = sen5xmeasurement.noxIndex; } } return true; } else if (response == 1) { // TODO return because data was not ready yet // Should this return false? idle(); return false; } else if (response == 2) { // Return with error for non-existing data idle(); return false; } return true; } void SEN5XSensor::setMode(bool setOneShot) { oneShotMode = setOneShot; if (oneShotMode) { LOG_INFO("%s setting mode to one shot mode", sensorName); } else { LOG_INFO("%s setting mode to continuous mode", sensorName); } } AdminMessageHandleResult SEN5XSensor::handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, meshtastic_AdminMessage *response) { AdminMessageHandleResult result; result = AdminMessageHandleResult::NOT_HANDLED; switch (request->which_payload_variant) { case meshtastic_AdminMessage_sensor_config_tag: if (!request->sensor_config.has_sen5x_config) { result = AdminMessageHandleResult::NOT_HANDLED; break; } // Check for one-shot/continuous mode request if (request->sensor_config.sen5x_config.has_set_one_shot_mode) { this->setMode(request->sensor_config.sen5x_config.set_one_shot_mode); } // TODO - Add admin command to set temperature offset? // Check for temperature offset // if (request->sensor_config.sen5x_config.has_set_temperature) { // this->setTemperature(request->sensor_config.sen5x_config.set_temperature); // } // TODO - Add admin command to trigger fan cleaning? // Check for one-shot/continuous mode request // if (request->sensor_config.sen5x_config.has_fan_cleaning && request->sensor_config.sen5x_config.fan_cleaning) { // this->startCleaning(); // } result = AdminMessageHandleResult::HANDLED; break; default: result = AdminMessageHandleResult::NOT_HANDLED; } return result; } #endif ================================================ FILE: src/modules/Telemetry/Sensor/SEN5XSensor.h ================================================ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "RTC.h" #include "TelemetrySensor.h" #include "Wire.h" // Warm up times for SEN5X from the datasheet #ifndef SEN5X_WARMUP_MS_1 #define SEN5X_WARMUP_MS_1 15000 #endif #ifndef SEN5X_WARMUP_MS_2 #define SEN5X_WARMUP_MS_2 30000 #endif #ifndef SEN5X_POLL_INTERVAL #define SEN5X_POLL_INTERVAL 1000 #endif #ifndef SEN5X_I2C_CLOCK_SPEED #define SEN5X_I2C_CLOCK_SPEED 100000 #endif /* Time after which the sensor can go to sleep, as the warmup period has passed and the VOCs sensor will is allowed to stop (although needs to recover the state each time) */ #ifndef SEN5X_VOC_STATE_WARMUP_S /* Note for Testing 5' is enough Sensirion recommends 1h This can be bypassed completely if switching to low-power RHT/Gas mode and setting SEN5X_VOC_STATE_WARMUP_S 0 */ #define SEN5X_VOC_STATE_WARMUP_S 3600 #endif #define ONE_WEEK_IN_SECONDS 604800 struct _SEN5XMeasurements { uint16_t pM1p0; uint16_t pM2p5; uint16_t pM4p0; uint16_t pM10p0; uint32_t pN0p5; uint32_t pN1p0; uint32_t pN2p5; uint32_t pN4p0; uint32_t pN10p0; float tSize; float humidity; float temperature; float vocIndex; float noxIndex; }; class SEN5XSensor : public TelemetrySensor { private: TwoWire *_bus{}; uint8_t _address{}; bool getVersion(); float firmwareVer = -1; float hardwareVer = -1; float protocolVer = -1; bool findModel(); // Commands #define SEN5X_RESET 0xD304 #define SEN5X_GET_PRODUCT_NAME 0xD014 #define SEN5X_GET_FIRMWARE_VERSION 0xD100 #define SEN5X_START_MEASUREMENT 0x0021 #define SEN5X_START_MEASUREMENT_RHT_GAS 0x0037 #define SEN5X_STOP_MEASUREMENT 0x0104 #define SEN5X_READ_DATA_READY 0x0202 #define SEN5X_START_FAN_CLEANING 0x5607 #define SEN5X_RW_VOCS_STATE 0x6181 #define SEN5X_READ_VALUES 0x03C4 #define SEN5X_READ_RAW_VALUES 0x03D2 #define SEN5X_READ_PM_VALUES 0x0413 #define SEN5X_VOC_VALID_TIME 600 #define SEN5X_VOC_VALID_DATE 1514764800 enum SEN5Xmodel { SEN5X_UNKNOWN = 0, SEN50 = 0b001, SEN54 = 0b010, SEN55 = 0b100 }; SEN5Xmodel model = SEN5X_UNKNOWN; enum SEN5XState { SEN5X_OFF, SEN5X_IDLE, SEN5X_RHTGAS_ONLY, SEN5X_MEASUREMENT, SEN5X_MEASUREMENT_2, SEN5X_CLEANING, SEN5X_NOT_DETECTED }; SEN5XState state = SEN5X_OFF; // Flag to work on one-shot (read and sleep), or continuous mode bool oneShotMode = true; void setMode(bool setOneShot); bool vocStateValid(); /* Sensirion recommends taking a reading after 15 seconds, if the Particle number reading is over 100#/cm3 the reading is OK, but if it is lower wait until 30 seconds and take it again. See: https://sensirion.com/resource/application_note/low_power_mode/sen5x */ #define SEN5X_PN4P0_CONC_THD 100 bool sendCommand(uint16_t command); bool sendCommand(uint16_t command, uint8_t *buffer, uint8_t byteNumber = 0); uint8_t readBuffer(uint8_t *buffer, uint8_t byteNumber); // Return number of bytes received uint8_t sen5xCRC(const uint8_t *buffer); bool startCleaning(); uint8_t getMeasurements(); // bool readRawValues(); bool readPNValues(bool cumulative); bool readValues(); uint32_t pmMeasureStarted = 0; uint32_t rhtGasMeasureStarted = 0; uint32_t lastDataPoll = 0; _SEN5XMeasurements sen5xmeasurement{}; bool idle(bool checkState = true); protected: // Store status of the sensor in this file const char *sen5XStateFileName = "/prefs/sen5X.dat"; meshtastic_SEN5XState sen5xstate = meshtastic_SEN5XState_init_zero; bool loadState(); bool saveState(); // Cleaning State uint32_t lastCleaning = 0; bool lastCleaningValid = false; // VOC State #define SEN5X_VOC_STATE_BUFFER_SIZE 8 uint8_t vocState[SEN5X_VOC_STATE_BUFFER_SIZE]{}; uint32_t vocTime = 0; bool vocValid = false; bool vocStateFromSensor(); bool vocStateToSensor(); bool vocStateStable(); bool vocStateRecent(uint32_t now); public: SEN5XSensor(); virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; virtual bool getMetrics(meshtastic_Telemetry *measurement) override; virtual bool isActive() override; virtual void sleep() override; virtual uint32_t wakeUp() override; virtual bool canSleep() override { return true; } virtual int32_t wakeUpTimeMs() override; virtual int32_t pendingForReadyMs() override; AdminMessageHandleResult handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, meshtastic_AdminMessage *response) override; }; #endif ================================================ FILE: src/modules/Telemetry/Sensor/SFA30Sensor.cpp ================================================ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR && __has_include() #include "../detect/reClockI2C.h" #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "SFA30Sensor.h" SFA30Sensor::SFA30Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SFA30, "SFA30"){}; bool SFA30Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { LOG_INFO("Init sensor: %s", sensorName); _bus = bus; _address = dev->address.address; #ifdef SFA30_I2C_CLOCK_SPEED #ifdef CAN_RECLOCK_I2C uint32_t currentClock = reClockI2C(SFA30_I2C_CLOCK_SPEED, _bus, false); #elif !HAS_SCREEN reClockI2C(SFA30_I2C_CLOCK_SPEED, _bus, true); #else LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName); return false; #endif /* CAN_RECLOCK_I2C */ #endif /* SFA30_I2C_CLOCK_SPEED */ sfa30.begin(*_bus, _address); delay(20); if (this->isError(sfa30.deviceReset())) { #if defined(SFA30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) reClockI2C(currentClock, _bus, false); #endif return false; } state = State::IDLE; if (this->isError(sfa30.startContinuousMeasurement())) { #if defined(SFA30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) reClockI2C(currentClock, _bus, false); #endif return false; } LOG_INFO("%s starting measurement", sensorName); #if defined(SFA30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) reClockI2C(currentClock, _bus, false); #endif status = 1; state = State::ACTIVE; measureStarted = getTime(); LOG_INFO("%s Enabled", sensorName); initI2CSensor(); return true; }; bool SFA30Sensor::isError(uint16_t response) { if (response == SFA30_NO_ERROR) return false; // TODO - Check error to char conversion LOG_ERROR("%s: %u", sensorName, response); return true; } void SFA30Sensor::sleep() { #ifdef SFA30_I2C_CLOCK_SPEED #ifdef CAN_RECLOCK_I2C uint32_t currentClock = reClockI2C(SFA30_I2C_CLOCK_SPEED, _bus, false); #elif !HAS_SCREEN reClockI2C(SFA30_I2C_CLOCK_SPEED, _bus, true); #else LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName); return; #endif /* CAN_RECLOCK_I2C */ #endif /* SFA30_I2C_CLOCK_SPEED */ // Note - not recommended for this sensor on a periodic basis if (this->isError(sfa30.stopMeasurement())) { LOG_ERROR("%s: can't stop measurement", sensorName); }; #if defined(SFA30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) reClockI2C(currentClock, _bus, false); #endif LOG_INFO("%s: stop measurement", sensorName); state = State::IDLE; measureStarted = 0; } uint32_t SFA30Sensor::wakeUp() { #ifdef SFA30_I2C_CLOCK_SPEED #ifdef CAN_RECLOCK_I2C uint32_t currentClock = reClockI2C(SFA30_I2C_CLOCK_SPEED, _bus, false); #elif !HAS_SCREEN reClockI2C(SFA30_I2C_CLOCK_SPEED, _bus, true); #else LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName); return false; #endif /* CAN_RECLOCK_I2C */ #endif /* SFA30_I2C_CLOCK_SPEED */ LOG_INFO("Waking up %s", sensorName); if (this->isError(sfa30.startContinuousMeasurement())) { #if defined(SFA30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) reClockI2C(currentClock, _bus, false); #endif return 0; } #if defined(SFA30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) reClockI2C(currentClock, _bus, false); #endif state = State::ACTIVE; measureStarted = getTime(); return SFA30_WARMUP_MS; } int32_t SFA30Sensor::wakeUpTimeMs() { return SFA30_WARMUP_MS; } bool SFA30Sensor::canSleep() { // Sleep is disabled in this sensor because readings are not tested with periodic sleep // with such low power consumption, prefered to keep it active return false; } bool SFA30Sensor::isActive() { return state == State::ACTIVE; } int32_t SFA30Sensor::pendingForReadyMs() { uint32_t now; now = getTime(); uint32_t sinceHchoMeasureStarted = (now - measureStarted) * 1000; LOG_DEBUG("%s: Since measure started: %ums", sensorName, sinceHchoMeasureStarted); if (sinceHchoMeasureStarted < SFA30_WARMUP_MS) { LOG_INFO("%s: not enough time passed since starting measurement", sensorName); return SFA30_WARMUP_MS - sinceHchoMeasureStarted; } return 0; } bool SFA30Sensor::getMetrics(meshtastic_Telemetry *measurement) { float hcho = 0.0; float humidity = 0.0; float temperature = 0.0; #ifdef SFA30_I2C_CLOCK_SPEED #ifdef CAN_RECLOCK_I2C uint32_t currentClock = reClockI2C(SFA30_I2C_CLOCK_SPEED, _bus, false); #elif !HAS_SCREEN reClockI2C(SFA30_I2C_CLOCK_SPEED, _bus, true); #else LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName); return false; #endif /* CAN_RECLOCK_I2C */ #endif /* SFA30_I2C_CLOCK_SPEED */ if (this->isError(sfa30.readMeasuredValues(hcho, humidity, temperature))) { LOG_WARN("%s: No values", sensorName); return false; } #if defined(SFA30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) reClockI2C(currentClock, _bus, false); #endif measurement->variant.air_quality_metrics.has_form_temperature = true; measurement->variant.air_quality_metrics.has_form_humidity = true; measurement->variant.air_quality_metrics.has_form_formaldehyde = true; measurement->variant.air_quality_metrics.form_temperature = temperature; measurement->variant.air_quality_metrics.form_humidity = humidity; measurement->variant.air_quality_metrics.form_formaldehyde = hcho; LOG_DEBUG("Got %s readings: hcho=%.2f, hcho_temp=%.2f, hcho_hum=%.2f", sensorName, hcho, temperature, humidity); return true; } #endif ================================================ FILE: src/modules/Telemetry/Sensor/SFA30Sensor.h ================================================ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR && __has_include() #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "RTC.h" #include "TelemetrySensor.h" #include #define SFA30_I2C_CLOCK_SPEED 100000 #define SFA30_WARMUP_MS 10000 #define SFA30_NO_ERROR 0 class SFA30Sensor : public TelemetrySensor { private: enum class State { IDLE, ACTIVE }; State state = State::IDLE; uint32_t measureStarted = 0; SensirionI2cSfa3x sfa30; TwoWire *_bus{}; uint8_t _address{}; bool isError(uint16_t response); public: SFA30Sensor(); virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; virtual bool getMetrics(meshtastic_Telemetry *measurement) override; virtual bool isActive() override; virtual void sleep() override; virtual uint32_t wakeUp() override; virtual bool canSleep() override; virtual int32_t wakeUpTimeMs() override; virtual int32_t pendingForReadyMs() override; }; #endif ================================================ FILE: src/modules/Telemetry/Sensor/SHT31Sensor.cpp ================================================ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "SHT31Sensor.h" #include "TelemetrySensor.h" #include SHT31Sensor::SHT31Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SHT31, "SHT31") {} bool SHT31Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { LOG_INFO("Init sensor: %s", sensorName); sht31 = Adafruit_SHT31(bus); status = sht31.begin(dev->address.address); initI2CSensor(); return status; } bool SHT31Sensor::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 ================================================ FILE: src/modules/Telemetry/Sensor/SHT31Sensor.h ================================================ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "TelemetrySensor.h" #include class SHT31Sensor : public TelemetrySensor { private: Adafruit_SHT31 sht31; public: SHT31Sensor(); virtual bool getMetrics(meshtastic_Telemetry *measurement) override; virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif ================================================ FILE: src/modules/Telemetry/Sensor/SHT4XSensor.cpp ================================================ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "SHT4XSensor.h" #include "TelemetrySensor.h" #include SHT4XSensor::SHT4XSensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SHT4X, "SHT4X") {} bool SHT4XSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { LOG_INFO("Init sensor: %s", sensorName); uint32_t serialNumber = 0; status = sht4x.begin(bus); if (!status) { return status; } serialNumber = sht4x.readSerial(); if (serialNumber != 0) { LOG_DEBUG("serialNumber : %x", serialNumber); status = 1; } else { LOG_DEBUG("Error trying to execute readSerial(): "); status = 0; } initI2CSensor(); return status; } 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 ================================================ FILE: src/modules/Telemetry/Sensor/SHT4XSensor.h ================================================ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "TelemetrySensor.h" #include class SHT4XSensor : public TelemetrySensor { private: Adafruit_SHT4x sht4x = Adafruit_SHT4x(); public: SHT4XSensor(); virtual bool getMetrics(meshtastic_Telemetry *measurement) override; virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif ================================================ FILE: src/modules/Telemetry/Sensor/SHTC3Sensor.cpp ================================================ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "SHTC3Sensor.h" #include "TelemetrySensor.h" #include SHTC3Sensor::SHTC3Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SHTC3, "SHTC3") {} bool SHTC3Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { LOG_INFO("Init sensor: %s", sensorName); status = shtc3.begin(bus); initI2CSensor(); return status; } 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 ================================================ FILE: src/modules/Telemetry/Sensor/SHTC3Sensor.h ================================================ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "TelemetrySensor.h" #include class SHTC3Sensor : public TelemetrySensor { private: Adafruit_SHTC3 shtc3 = Adafruit_SHTC3(); public: SHTC3Sensor(); virtual bool getMetrics(meshtastic_Telemetry *measurement) override; virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif ================================================ FILE: src/modules/Telemetry/Sensor/T1000xSensor.cpp ================================================ #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") {} bool T1000xSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { LOG_INFO("Init sensor: %s", sensorName); return true; } float T1000xSensor::getLux() { uint32_t lux_vot = 0; float lux_level = 0; 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; // P0.4 is a sensor power enable GPIO, not a VCC ADC pin. // Read BATTERY_PIN (with voltage divider) and cap at NTC_REF_VCC to estimate the sensor rail voltage. for (uint32_t i = 0; i < T1000X_SENSE_SAMPLES; i++) { vcc_vot += analogRead(BATTERY_PIN); } vcc_vot = vcc_vot / T1000X_SENSE_SAMPLES; vcc_vot = ADC_MULTIPLIER * ((1000 * AREF_VOLTAGE) / pow(2, BATTERY_SENSE_RESOLUTION_BITS)) * vcc_vot; if (vcc_vot > NTC_REF_VCC) vcc_vot = NTC_REF_VCC; 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 ================================================ FILE: src/modules/Telemetry/Sensor/T1000xSensor.h ================================================ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "TelemetrySensor.h" class T1000xSensor : public TelemetrySensor { public: T1000xSensor(); virtual bool getMetrics(meshtastic_Telemetry *measurement) override; virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; virtual float getLux(); virtual float getTemp(); }; #endif ================================================ FILE: src/modules/Telemetry/Sensor/TSL2561Sensor.cpp ================================================ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "TSL2561Sensor.h" #include "TelemetrySensor.h" #include TSL2561Sensor::TSL2561Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_TSL2561, "TSL2561") {} bool TSL2561Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { LOG_INFO("Init sensor: %s", sensorName); status = tsl.begin(bus); if (!status) { return status; } tsl.setGain(TSL2561_GAIN_1X); tsl.setIntegrationTime(TSL2561_INTEGRATIONTIME_101MS); initI2CSensor(); return status; } bool TSL2561Sensor::getMetrics(meshtastic_Telemetry *measurement) { measurement->variant.environment_metrics.has_lux = true; sensors_event_t event; tsl.getEvent(&event); measurement->variant.environment_metrics.lux = event.light; LOG_INFO("Lux: %f", measurement->variant.environment_metrics.lux); return true; } #endif ================================================ FILE: src/modules/Telemetry/Sensor/TSL2561Sensor.h ================================================ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "TelemetrySensor.h" #include class TSL2561Sensor : public TelemetrySensor { private: // The magic number is a sensor id, the actual value doesn't matter Adafruit_TSL2561_Unified tsl = Adafruit_TSL2561_Unified(TSL2561_ADDR_LOW, 12345); public: TSL2561Sensor(); virtual bool getMetrics(meshtastic_Telemetry *measurement) override; virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif ================================================ FILE: src/modules/Telemetry/Sensor/TSL2591Sensor.cpp ================================================ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "TSL2591Sensor.h" #include "TelemetrySensor.h" #include #include TSL2591Sensor::TSL2591Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_TSL25911FN, "TSL2591") {} bool TSL2591Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { LOG_INFO("Init sensor: %s", sensorName); status = tsl.begin(bus); if (!status) { return status; } tsl.setGain(TSL2591_GAIN_LOW); // 1x gain tsl.setTiming(TSL2591_INTEGRATIONTIME_100MS); initI2CSensor(); return status; } bool TSL2591Sensor::getMetrics(meshtastic_Telemetry *measurement) { measurement->variant.environment_metrics.has_lux = true; uint32_t lum = tsl.getFullLuminosity(); uint16_t ir, full; ir = lum >> 16; full = lum & 0xFFFF; measurement->variant.environment_metrics.lux = tsl.calculateLux(full, ir); LOG_INFO("Lux: %f", measurement->variant.environment_metrics.lux); return true; } #endif ================================================ FILE: src/modules/Telemetry/Sensor/TSL2591Sensor.h ================================================ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "TelemetrySensor.h" #include class TSL2591Sensor : public TelemetrySensor { private: Adafruit_TSL2591 tsl; public: TSL2591Sensor(); virtual bool getMetrics(meshtastic_Telemetry *measurement) override; virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif ================================================ FILE: src/modules/Telemetry/Sensor/TelemetrySensor.cpp ================================================ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR || !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "NodeDB.h" #include "TelemetrySensor.h" #include "main.h" #endif ================================================ FILE: src/modules/Telemetry/Sensor/TelemetrySensor.h ================================================ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR || !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR #pragma once #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "MeshModule.h" #include "NodeDB.h" #include "detect/ScanI2C.h" #include #if !ARCH_PORTDUINO class TwoWire; #endif #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; } meshtastic_TelemetrySensorType sensorType = meshtastic_TelemetrySensorType_SENSOR_UNSET; unsigned status; bool initialized = false; int32_t initI2CSensor() { if (!status) { LOG_WARN("Can't connect to detected %s sensor. Remove from nodeTelemetrySensorsMap", sensorName); nodeTelemetrySensorsMap[sensorType].first = 0; } else { LOG_INFO("Opened %s sensor on i2c bus", sensorName); setup(); } initialized = true; return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; } // TODO: check is setup used at all? virtual void setup() {} public: virtual ~TelemetrySensor() {} virtual AdminMessageHandleResult handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, meshtastic_AdminMessage *response) { return AdminMessageHandleResult::NOT_HANDLED; } const char *sensorName; // TODO: delete after migration bool hasSensor() { return nodeTelemetrySensorsMap[sensorType].first > 0; } // Functions to sleep / wakeup sensors that support it // These functions can save power consumption in cases like AQ virtual void sleep(){}; virtual uint32_t wakeUp() { return 0; } virtual bool isActive() { return true; } // Return true by default, override per sensor virtual bool canSleep() { return false; } // Return false by default, override per sensor virtual int32_t wakeUpTimeMs() { return 0; } virtual int32_t pendingForReadyMs() { return 0; } #if WIRE_INTERFACES_COUNT > 1 // Set to true if Implementation only works first I2C port (Wire) virtual bool onlyWire1() { return false; } #endif virtual int32_t runOnce() { return INT32_MAX; } virtual bool isInitialized() { return initialized; } // TODO: is this used? virtual bool isRunning() { return status > 0; } virtual bool getMetrics(meshtastic_Telemetry *measurement) = 0; virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { return false; }; }; #endif ================================================ FILE: src/modules/Telemetry/Sensor/VEML7700Sensor.cpp ================================================ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "TelemetrySensor.h" #include "VEML7700Sensor.h" #include #include VEML7700Sensor::VEML7700Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_VEML7700, "VEML7700") {} bool VEML7700Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { LOG_INFO("Init sensor: %s", sensorName); status = veml7700.begin(bus); if (!status) { return status; } veml7700.setLowThreshold(10000); veml7700.setHighThreshold(20000); veml7700.interruptEnable(true); initI2CSensor(); return status; } /*! * @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 ================================================ FILE: src/modules/Telemetry/Sensor/VEML7700Sensor.h ================================================ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() #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); public: VEML7700Sensor(); virtual bool getMetrics(meshtastic_Telemetry *measurement) override; virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; #endif ================================================ FILE: src/modules/Telemetry/Sensor/VoltageSensor.h ================================================ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR #pragma once class VoltageSensor { public: virtual uint16_t getBusVoltageMv() = 0; }; #endif ================================================ FILE: src/modules/Telemetry/Sensor/nullSensor.cpp ================================================ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "TelemetrySensor.h" #include "nullSensor.h" #include NullSensor::NullSensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SENSOR_UNSET, "nullSensor") {} int32_t NullSensor::runOnce() { return INT32_MAX; } void NullSensor::setup() {} bool NullSensor::getMetrics(meshtastic_Telemetry *measurement) { return false; } uint16_t NullSensor::getBusVoltageMv() { return 0; } int16_t NullSensor::getCurrentMa() { return 0; } #endif ================================================ FILE: src/modules/Telemetry/Sensor/nullSensor.h ================================================ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR #pragma once #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "CurrentSensor.h" #include "TelemetrySensor.h" #include "VoltageSensor.h" class NullSensor : public TelemetrySensor, VoltageSensor, CurrentSensor { protected: virtual void setup() override; public: NullSensor(); virtual int32_t runOnce() override; virtual bool getMetrics(meshtastic_Telemetry *measurement) override; int32_t runTrigger() { return 0; } virtual uint16_t getBusVoltageMv() override; virtual int16_t getCurrentMa() override; }; #endif ================================================ FILE: src/modules/Telemetry/UnitConversions.cpp ================================================ #include "UnitConversions.h" float UnitConversions::CelsiusToFahrenheit(float celsius) { return (celsius * 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; } ================================================ FILE: src/modules/Telemetry/UnitConversions.h ================================================ #pragma once class UnitConversions { public: static float CelsiusToFahrenheit(float celsius); static float MetersPerSecondToKnots(float metersPerSecond); static float MetersPerSecondToMilesPerHour(float metersPerSecond); static float HectoPascalToInchesOfMercury(float hectoPascal); }; ================================================ FILE: src/modules/TextMessageModule.cpp ================================================ #include "TextMessageModule.h" #include "MeshService.h" #include "MessageStore.h" #include "NodeDB.h" #include "PowerFSM.h" #include "buzz.h" #include "configuration.h" #include "graphics/Screen.h" #include "graphics/SharedUIDisplay.h" #include "graphics/draw/MessageRenderer.h" #include "main.h" TextMessageModule *textMessageModule; ProcessMessage TextMessageModule::handleReceived(const meshtastic_MeshPacket &mp) { #if defined(DEBUG_PORT) && !defined(DEBUG_MUTE) auto &p = mp.decoded; LOG_INFO("Received text msg from=0x%0x, id=0x%x, msg=%.*s", mp.from, mp.id, p.payload.size, p.payload.bytes); #endif // add packet ID to the rolling list of packets textPacketList[textPacketListIndex] = mp.id; textPacketListIndex = (textPacketListIndex + 1) % TEXT_PACKET_LIST_SIZE; // We only store/display messages destined for us. devicestate.rx_text_message = mp; devicestate.has_rx_text_message = true; IF_SCREEN( // Guard against running in MeshtasticUI or with no screen if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { // Store in the central message history const StoredMessage &sm = messageStore.addFromPacket(mp); // Pass message to renderer (banner + thread switching + scroll reset) // Use the global Screen singleton to retrieve the current OLED display auto *display = screen ? screen->getDisplayDevice() : nullptr; graphics::MessageRenderer::handleNewMessage(display, sm, mp); }) // Only trigger screen wake if configuration allows it if (shouldWakeOnReceivedMessage()) { powerFSM.trigger(EVENT_RECEIVED_MSG); } // Notify any observers (e.g. external modules that care about packets) notifyObservers(&mp); return ProcessMessage::CONTINUE; // Let others look at this message also if they want } bool TextMessageModule::wantPacket(const meshtastic_MeshPacket *p) { return MeshService::isTextPayload(p); } bool TextMessageModule::recentlySeen(uint32_t id) { for (size_t i = 0; i < TEXT_PACKET_LIST_SIZE; i++) { if (textPacketList[i] != 0 && textPacketList[i] == id) { return true; } } return false; } ================================================ FILE: src/modules/TextMessageModule.h ================================================ #pragma once #include "Observer.h" #include "SinglePortModule.h" #define TEXT_PACKET_LIST_SIZE 50 /** * Text message handling for Meshtastic. * * This module is responsible for receiving and storing incoming text messages * from the mesh. It updates device state and notifies observers so that other * components (such as the MessageRenderer) can later display or process them. * * Rendering of messages on screen is no longer done here. */ class TextMessageModule : public SinglePortModule, public Observable { public: /** Constructor * name is for debugging output */ TextMessageModule() : SinglePortModule("text", meshtastic_PortNum_TEXT_MESSAGE_APP) {} bool recentlySeen(uint32_t id); 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; private: uint32_t textPacketList[TEXT_PACKET_LIST_SIZE] = {0}; size_t textPacketListIndex = 0; }; extern TextMessageModule *textMessageModule; ================================================ FILE: src/modules/TraceRouteModule.cpp ================================================ #include "TraceRouteModule.h" #include "MeshService.h" #include "NodeDB.h" #include "graphics/Screen.h" #include "graphics/ScreenFonts.h" #include "graphics/SharedUIDisplay.h" #include "mesh/Router.h" #include "meshUtils.h" #include extern graphics::Screen *screen; TraceRouteModule *traceRouteModule; void TraceRouteModule::setResultText(const String &text) { resultText = text; resultLines.clear(); resultLinesDirty = true; } void TraceRouteModule::clearResultLines() { resultLines.clear(); resultLinesDirty = false; } #if HAS_SCREEN void TraceRouteModule::rebuildResultLines(OLEDDisplay *display) { if (!display) { resultLinesDirty = false; return; } resultLines.clear(); if (resultText.length() == 0) { resultLinesDirty = false; return; } int maxWidth = display->getWidth() - 4; if (maxWidth <= 0) { resultLinesDirty = false; return; } int start = 0; int textLength = resultText.length(); while (start <= textLength) { int newlinePos = resultText.indexOf('\n', start); String segment; if (newlinePos != -1) { segment = resultText.substring(start, newlinePos); start = newlinePos + 1; } else { segment = resultText.substring(start); start = textLength + 1; } if (segment.length() == 0) { resultLines.push_back(""); continue; } if (display->getStringWidth(segment) <= maxWidth) { resultLines.push_back(segment); continue; } String remaining = segment; while (remaining.length() > 0) { String tempLine = ""; int lastGoodBreak = -1; bool lineComplete = false; for (int i = 0; i < static_cast(remaining.length()); i++) { char ch = remaining.charAt(i); String testLine = tempLine + ch; if (display->getStringWidth(testLine) > maxWidth) { if (lastGoodBreak >= 0) { resultLines.push_back(remaining.substring(0, lastGoodBreak + 1)); remaining = remaining.substring(lastGoodBreak + 1); lineComplete = true; break; } else if (tempLine.length() > 0) { resultLines.push_back(tempLine); remaining = remaining.substring(i); lineComplete = true; break; } else { resultLines.push_back(String(ch)); remaining = remaining.substring(i + 1); lineComplete = true; break; } } else { tempLine = testLine; if (ch == ' ' || ch == '>' || ch == '<' || ch == '-' || ch == '(' || ch == ')' || ch == ',') { lastGoodBreak = i; } } } if (!lineComplete) { if (tempLine.length() > 0) { resultLines.push_back(tempLine); } break; } } } resultLinesDirty = false; } #endif bool TraceRouteModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_RouteDiscovery *r) { // We only alter the packet in alterReceivedProtobuf() return false; // let it be handled by RoutingModule } void TraceRouteModule::alterReceivedProtobuf(meshtastic_MeshPacket &p, meshtastic_RouteDiscovery *r) { const meshtastic_Data &incoming = p.decoded; // Update next-hops using returned route if (incoming.request_id) { updateNextHops(p, r); } // Insert unknown hops if necessary insertUnknownHops(p, r, !incoming.request_id); // Append ID and SNR. If the last hop is to us, we only need to append the SNR appendMyIDandSNR(r, p.rx_snr, !incoming.request_id, isToUs(&p)); if (!incoming.request_id) printRoute(r, p.from, p.to, true); else printRoute(r, p.to, p.from, false); // Set updated route to the payload of the to be flooded packet p.decoded.payload.size = pb_encode_to_bytes(p.decoded.payload.bytes, sizeof(p.decoded.payload.bytes), &meshtastic_RouteDiscovery_msg, r); if (tracingNode != 0) { // check isResponseFromTarget bool isResponseFromTarget = (incoming.request_id != 0 && p.from == tracingNode); bool isRequestToUs = (incoming.request_id == 0 && p.to == nodeDB->getNodeNum() && tracingNode != 0); // Check if this is a trace route response containing our target node bool containsTargetNode = false; for (uint8_t i = 0; i < r->route_count; i++) { if (r->route[i] == tracingNode) { containsTargetNode = true; break; } } for (uint8_t i = 0; i < r->route_back_count; i++) { if (r->route_back[i] == tracingNode) { containsTargetNode = true; break; } } // Check if this response contains a complete route to our target bool hasCompleteRoute = (r->route_count > 0 && r->route_back_count > 0) || (containsTargetNode && (r->route_count > 0 || r->route_back_count > 0)); LOG_INFO("TracRoute packet analysis: tracingNode=0x%08x, p.from=0x%08x, p.to=0x%08x, request_id=0x%08x", tracingNode, p.from, p.to, incoming.request_id); LOG_INFO("TracRoute conditions: isResponseFromTarget=%d, isRequestToUs=%d, containsTargetNode=%d, hasCompleteRoute=%d", isResponseFromTarget, isRequestToUs, containsTargetNode, hasCompleteRoute); if (isResponseFromTarget || isRequestToUs || (containsTargetNode && hasCompleteRoute)) { LOG_INFO("TracRoute result detected: isResponseFromTarget=%d, isRequestToUs=%d", isResponseFromTarget, isRequestToUs); LOG_INFO("SNR arrays - towards_count=%d, back_count=%d", r->snr_towards_count, r->snr_back_count); for (int i = 0; i < r->snr_towards_count; i++) { LOG_INFO("SNR towards[%d] = %d (%.1fdB)", i, r->snr_towards[i], (float)r->snr_towards[i] / 4.0f); } for (int i = 0; i < r->snr_back_count; i++) { LOG_INFO("SNR back[%d] = %d (%.1fdB)", i, r->snr_back[i], (float)r->snr_back[i] / 4.0f); } String result = ""; // Show request path (from initiator to target) if (r->route_count > 0) { result += getNodeName(nodeDB->getNodeNum()); for (uint8_t i = 0; i < r->route_count; i++) { result += " > "; const char *name = getNodeName(r->route[i]); float snr = (i < r->snr_towards_count && r->snr_towards[i] != INT8_MIN) ? ((float)r->snr_towards[i] / 4.0f) : 0.0f; result += name; if (snr != 0.0f) { result += "("; result += String(snr, 1); result += "dB)"; } } result += " > "; result += getNodeName(tracingNode); if (r->snr_towards_count > 0 && r->snr_towards[r->snr_towards_count - 1] != INT8_MIN) { result += "("; result += String((float)r->snr_towards[r->snr_towards_count - 1] / 4.0f, 1); result += "dB)"; } result += "\n"; } else { // Direct connection (no intermediate hops) result += getNodeName(nodeDB->getNodeNum()); result += " > "; result += getNodeName(tracingNode); if (r->snr_towards_count > 0 && r->snr_towards[0] != INT8_MIN) { result += "("; result += String((float)r->snr_towards[0] / 4.0f, 1); result += "dB)"; } result += "\n"; } // Show response path (from target back to initiator) if (r->route_back_count > 0) { result += getNodeName(tracingNode); for (int8_t i = r->route_back_count - 1; i >= 0; i--) { result += " > "; const char *name = getNodeName(r->route_back[i]); float snr = (i < r->snr_back_count && r->snr_back[i] != INT8_MIN) ? ((float)r->snr_back[i] / 4.0f) : 0.0f; result += name; if (snr != 0.0f) { result += "("; result += String(snr, 1); result += "dB)"; } } // add initiator node result += " > "; result += getNodeName(nodeDB->getNodeNum()); if (r->snr_back_count > 0 && r->snr_back[r->snr_back_count - 1] != INT8_MIN) { result += "("; result += String((float)r->snr_back[r->snr_back_count - 1] / 4.0f, 1); result += "dB)"; } } else { // Direct return path (no intermediate hops) result += getNodeName(tracingNode); result += " > "; result += getNodeName(nodeDB->getNodeNum()); if (r->snr_back_count > 0 && r->snr_back[0] != INT8_MIN) { result += "("; result += String((float)r->snr_back[0] / 4.0f, 1); result += "dB)"; } } LOG_INFO("Trace route result: %s", result.c_str()); handleTraceRouteResult(result); } } } void TraceRouteModule::updateNextHops(const meshtastic_MeshPacket &p, meshtastic_RouteDiscovery *r) { // E.g. if the route is A->B->C->D and we are B, we can set C as next-hop for C and D // Similarly, if we are C, we can set D as next-hop for D // If we are A, we can set B as next-hop for B, C and D // First check if we were the original sender or in the original route int8_t nextHopIndex = -1; if (isToUs(&p)) { nextHopIndex = 0; // We are the original sender, next hop is first in route } else { // Check if we are in the original route for (uint8_t i = 0; i < r->route_count; i++) { if (r->route[i] == nodeDB->getNodeNum()) { nextHopIndex = i + 1; // Next hop is the one after us break; } } } // If we are in the original route, update the next hops if (nextHopIndex != -1) { // For every node after us, we can set the next-hop to the first node after us NodeNum nextHop; if (nextHopIndex == r->route_count) { nextHop = p.from; // We are the last in the route, next hop is destination } else { nextHop = r->route[nextHopIndex]; } if (nextHop == NODENUM_BROADCAST) { return; } uint8_t nextHopByte = nodeDB->getLastByteOfNodeNum(nextHop); // For the rest of the nodes in the route, set their next-hop // Note: if we are the last in the route, this loop will not run for (int8_t i = nextHopIndex; i < r->route_count; i++) { NodeNum targetNode = r->route[i]; maybeSetNextHop(targetNode, nextHopByte); } // Also set next-hop for the destination node maybeSetNextHop(p.from, nextHopByte); } } void TraceRouteModule::maybeSetNextHop(NodeNum target, uint8_t nextHopByte) { if (target == NODENUM_BROADCAST) return; meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(target); if (node && node->next_hop != nextHopByte) { LOG_INFO("Updating next-hop for 0x%08x to 0x%02x based on traceroute", target, nextHopByte); node->next_hop = nextHopByte; } } void TraceRouteModule::processUpgradedPacket(const meshtastic_MeshPacket &mp) { if (mp.which_payload_variant != meshtastic_MeshPacket_decoded_tag || mp.decoded.portnum != meshtastic_PortNum_TRACEROUTE_APP) return; meshtastic_RouteDiscovery decoded = meshtastic_RouteDiscovery_init_zero; if (!pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, &meshtastic_RouteDiscovery_msg, &decoded)) return; handleReceivedProtobuf(mp, &decoded); // Intentionally modify the packet in-place so downstream relays see our updates. alterReceivedProtobuf(const_cast(mp), &decoded); } void TraceRouteModule::insertUnknownHops(meshtastic_MeshPacket &p, meshtastic_RouteDiscovery *r, bool isTowardsDestination) { pb_size_t *route_count; uint32_t *route; pb_size_t *snr_count; int8_t *snr_list; // Pick the correct route array and SNR list if (isTowardsDestination) { route_count = &r->route_count; route = r->route; snr_count = &r->snr_towards_count; snr_list = r->snr_towards; } else { route_count = &r->route_back_count; route = r->route_back; snr_count = &r->snr_back_count; snr_list = r->snr_back; } // Only insert unknown hops if hop_start is valid const int8_t hopsTaken = getHopsAway(p); if (hopsTaken >= 0) { int8_t diff = hopsTaken - *route_count; for (int8_t i = 0; i < diff; i++) { if (*route_count < ROUTE_SIZE) { route[*route_count] = NODENUM_BROADCAST; // This will represent an unknown hop *route_count += 1; } } // Add unknown SNR values if necessary diff = *route_count - *snr_count; for (int8_t i = 0; i < diff; i++) { if (*snr_count < ROUTE_SIZE) { snr_list[*snr_count] = INT8_MIN; // This will represent an unknown SNR *snr_count += 1; } } } } void TraceRouteModule::appendMyIDandSNR(meshtastic_RouteDiscovery *updated, float snr, bool isTowardsDestination, bool SNRonly) { pb_size_t *route_count; uint32_t *route; pb_size_t *snr_count; int8_t *snr_list; // Pick the correct route array and SNR list if (isTowardsDestination) { route_count = &updated->route_count; route = updated->route; snr_count = &updated->snr_towards_count; snr_list = updated->snr_towards; } else { route_count = &updated->route_back_count; route = updated->route_back; snr_count = &updated->snr_back_count; snr_list = updated->snr_back; } if (*snr_count < ROUTE_SIZE) { snr_list[*snr_count] = (int8_t)(snr * 4); // Convert SNR to 1 byte *snr_count += 1; } if (SNRonly) return; // Length of route array can normally not be exceeded due to the max. hop_limit of 7 if (*route_count < ROUTE_SIZE) { route[*route_count] = myNodeInfo.my_node_num; *route_count += 1; } else { LOG_WARN("Route exceeded maximum hop limit!"); // Are you bridging networks? } } void TraceRouteModule::printRoute(meshtastic_RouteDiscovery *r, uint32_t origin, uint32_t dest, bool isTowardsDestination) { #if defined(DEBUG_PORT) && !defined(DEBUG_MUTE) std::string route = "Route traced:\n"; route += vformat("0x%x --> ", origin); for (uint8_t i = 0; i < r->route_count; i++) { if (i < r->snr_towards_count && r->snr_towards[i] != INT8_MIN) route += vformat("0x%x (%.2fdB) --> ", r->route[i], (float)r->snr_towards[i] / 4); else route += vformat("0x%x (?dB) --> ", r->route[i]); } // If we are the destination, or it has already reached the destination, print it if (dest == nodeDB->getNodeNum() || !isTowardsDestination) { if (r->snr_towards_count > 0 && r->snr_towards[r->snr_towards_count - 1] != INT8_MIN) route += vformat("0x%x (%.2fdB)", dest, (float)r->snr_towards[r->snr_towards_count - 1] / 4); else route += vformat("0x%x (?dB)", dest); } else route += "..."; // If there's a route back (or we are the destination as then the route is complete), print it if (r->route_back_count > 0 || origin == nodeDB->getNodeNum()) { route += "\n"; if (r->snr_towards_count > 0 && origin == nodeDB->getNodeNum()) route += vformat("(%.2fdB) 0x%x <-- ", (float)r->snr_back[r->snr_back_count - 1] / 4, origin); else route += "..."; for (int8_t i = r->route_back_count - 1; i >= 0; i--) { if (i < r->snr_back_count && r->snr_back[i] != INT8_MIN) route += vformat("(%.2fdB) 0x%x <-- ", (float)r->snr_back[i] / 4, r->route_back[i]); else route += vformat("(?dB) 0x%x <-- ", r->route_back[i]); } route += vformat("0x%x", dest); } LOG_INFO(route.c_str()); #endif } meshtastic_MeshPacket *TraceRouteModule::allocReply() { assert(currentRequest); // Ignore multi-hop broadcast requests if (isBroadcast(currentRequest->to) && currentRequest->hop_limit < currentRequest->hop_start) { ignoreRequest = true; return NULL; } // Copy the payload of the current request auto req = *currentRequest; const auto &p = req.decoded; meshtastic_RouteDiscovery scratch; meshtastic_RouteDiscovery *updated = NULL; memset(&scratch, 0, sizeof(scratch)); pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_RouteDiscovery_msg, &scratch); updated = &scratch; // 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), OSThread("TraceRoute") { ourPortNum = meshtastic_PortNum_TRACEROUTE_APP; isPromiscuous = true; // We need to update the route even if it is not destined to us } const char *TraceRouteModule::getNodeName(NodeNum node) { meshtastic_NodeInfoLite *info = nodeDB->getMeshNode(node); if (info && info->has_user) { if (strlen(info->user.short_name) > 0) { return info->user.short_name; } if (strlen(info->user.long_name) > 0) { return info->user.long_name; } } static char fallback[12]; snprintf(fallback, sizeof(fallback), "0x%08x", node); return fallback; } bool TraceRouteModule::startTraceRoute(NodeNum node) { LOG_INFO("=== TraceRoute startTraceRoute CALLED: node=0x%08x ===", node); unsigned long now = millis(); if (node == 0 || node == NODENUM_BROADCAST) { LOG_ERROR("Invalid node number for trace route: 0x%08x", node); runState = TRACEROUTE_STATE_RESULT; setResultText("Invalid node"); resultShowTime = millis(); tracingNode = 0; requestFocus(); UIFrameEvent e; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; notifyObservers(&e); return false; } if (node == nodeDB->getNodeNum()) { LOG_ERROR("Cannot trace route to self: 0x%08x", node); runState = TRACEROUTE_STATE_RESULT; setResultText("Cannot trace self"); resultShowTime = millis(); tracingNode = 0; requestFocus(); UIFrameEvent e; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; notifyObservers(&e); return false; } if (!initialized) { lastTraceRouteTime = 0; initialized = true; LOG_INFO("TraceRoute initialized for first time"); } if (runState == TRACEROUTE_STATE_TRACKING) { LOG_INFO("TraceRoute already in progress"); return false; } if (initialized && lastTraceRouteTime > 0 && now - lastTraceRouteTime < cooldownMs) { // Cooldown unsigned long wait = (cooldownMs - (now - lastTraceRouteTime)) / 1000; bannerText = String("Wait for ") + String(wait) + String("s"); runState = TRACEROUTE_STATE_COOLDOWN; resultText = ""; clearResultLines(); requestFocus(); UIFrameEvent e; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; notifyObservers(&e); LOG_INFO("Cooldown active, please wait %lu seconds before starting a new trace route.", wait); return false; } tracingNode = node; lastTraceRouteTime = now; runState = TRACEROUTE_STATE_TRACKING; resultText = ""; clearResultLines(); bannerText = String("Tracing ") + getNodeName(node); LOG_INFO("TraceRoute UI: Starting trace route to node 0x%08x, requesting focus", node); // 请求焦点,然后触发UI更新事件 requestFocus(); UIFrameEvent e; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; notifyObservers(&e); // 设置定时器来处理超时检查 setIntervalFromNow(1000); // 每秒检查一次状态 meshtastic_RouteDiscovery req = meshtastic_RouteDiscovery_init_zero; LOG_INFO("Creating RouteDiscovery protobuf..."); // Allocate a packet directly from router like the reference code meshtastic_MeshPacket *p = router->allocForSending(); if (p) { // Set destination and port p->to = node; p->decoded.portnum = meshtastic_PortNum_TRACEROUTE_APP; p->decoded.want_response = true; // Use reliable delivery for traceroute requests (which will be copied to traceroute responses by setReplyTo) p->want_ack = true; // Manually encode the RouteDiscovery payload p->decoded.payload.size = pb_encode_to_bytes(p->decoded.payload.bytes, sizeof(p->decoded.payload.bytes), &meshtastic_RouteDiscovery_msg, &req); LOG_INFO("Packet allocated successfully: to=0x%08x, portnum=%d, want_response=%d, payload_size=%d", p->to, p->decoded.portnum, p->decoded.want_response, p->decoded.payload.size); LOG_INFO("About to call service->sendToMesh..."); if (service) { LOG_INFO("MeshService is available, sending packet..."); service->sendToMesh(p, RX_SRC_USER); LOG_INFO("sendToMesh called successfully for trace route to node 0x%08x", node); } else { LOG_ERROR("MeshService is NULL!"); runState = TRACEROUTE_STATE_RESULT; setResultText("Service unavailable"); resultShowTime = millis(); tracingNode = 0; requestFocus(); UIFrameEvent e2; e2.action = UIFrameEvent::Action::REGENERATE_FRAMESET; notifyObservers(&e2); return false; } } else { LOG_ERROR("Failed to allocate TraceRoute packet from router"); runState = TRACEROUTE_STATE_RESULT; setResultText("Failed to send"); resultShowTime = millis(); tracingNode = 0; requestFocus(); UIFrameEvent e2; e2.action = UIFrameEvent::Action::REGENERATE_FRAMESET; notifyObservers(&e2); return false; } return true; } void TraceRouteModule::launch(NodeNum node) { if (node == 0 || node == NODENUM_BROADCAST) { LOG_ERROR("Invalid node number for trace route: 0x%08x", node); runState = TRACEROUTE_STATE_RESULT; setResultText("Invalid node"); resultShowTime = millis(); tracingNode = 0; requestFocus(); UIFrameEvent e; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; notifyObservers(&e); return; } if (node == nodeDB->getNodeNum()) { LOG_ERROR("Cannot trace route to self: 0x%08x", node); runState = TRACEROUTE_STATE_RESULT; setResultText("Cannot trace self"); resultShowTime = millis(); tracingNode = 0; requestFocus(); UIFrameEvent e; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; notifyObservers(&e); return; } if (!initialized) { lastTraceRouteTime = 0; initialized = true; LOG_INFO("TraceRoute initialized for first time"); } unsigned long now = millis(); if (initialized && lastTraceRouteTime > 0 && now - lastTraceRouteTime < cooldownMs) { unsigned long wait = (cooldownMs - (now - lastTraceRouteTime)) / 1000; bannerText = String("Wait for ") + String(wait) + String("s"); runState = TRACEROUTE_STATE_COOLDOWN; resultText = ""; clearResultLines(); requestFocus(); UIFrameEvent e; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; notifyObservers(&e); LOG_INFO("Cooldown active, please wait %lu seconds before starting a new trace route.", wait); return; } runState = TRACEROUTE_STATE_TRACKING; tracingNode = node; lastTraceRouteTime = now; resultText = ""; clearResultLines(); bannerText = String("Tracing ") + getNodeName(node); requestFocus(); UIFrameEvent e; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; notifyObservers(&e); setIntervalFromNow(1000); meshtastic_RouteDiscovery req = meshtastic_RouteDiscovery_init_zero; LOG_INFO("Creating RouteDiscovery protobuf..."); meshtastic_MeshPacket *p = router->allocForSending(); if (p) { p->to = node; p->decoded.portnum = meshtastic_PortNum_TRACEROUTE_APP; p->decoded.want_response = true; // Use reliable delivery for traceroute requests (which will be copied to traceroute responses by setReplyTo) p->want_ack = true; p->decoded.payload.size = pb_encode_to_bytes(p->decoded.payload.bytes, sizeof(p->decoded.payload.bytes), &meshtastic_RouteDiscovery_msg, &req); LOG_INFO("Packet allocated successfully: to=0x%08x, portnum=%d, want_response=%d, payload_size=%d", p->to, p->decoded.portnum, p->decoded.want_response, p->decoded.payload.size); if (service) { service->sendToMesh(p, RX_SRC_USER); LOG_INFO("sendToMesh called successfully for trace route to node 0x%08x", node); } else { LOG_ERROR("MeshService is NULL!"); runState = TRACEROUTE_STATE_RESULT; setResultText("Service unavailable"); resultShowTime = millis(); tracingNode = 0; } } else { LOG_ERROR("Failed to allocate TraceRoute packet from router"); runState = TRACEROUTE_STATE_RESULT; setResultText("Failed to send"); resultShowTime = millis(); tracingNode = 0; } } void TraceRouteModule::handleTraceRouteResult(const String &result) { setResultText(result); runState = TRACEROUTE_STATE_RESULT; resultShowTime = millis(); tracingNode = 0; LOG_INFO("TraceRoute result ready, requesting focus. Result: %s", result.c_str()); setIntervalFromNow(1000); requestFocus(); UIFrameEvent e; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; notifyObservers(&e); LOG_INFO("=== TraceRoute handleTraceRouteResult END ==="); } bool TraceRouteModule::shouldDraw() { bool draw = (runState != TRACEROUTE_STATE_IDLE); static TraceRouteRunState lastLoggedState = TRACEROUTE_STATE_IDLE; if (runState != lastLoggedState) { LOG_INFO("TraceRoute shouldDraw: runState=%d, draw=%d", runState, draw); lastLoggedState = runState; } return draw; } #if HAS_SCREEN void TraceRouteModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { LOG_DEBUG("TraceRoute drawFrame called: runState=%d", runState); display->setTextAlignment(TEXT_ALIGN_CENTER); if (runState == TRACEROUTE_STATE_TRACKING) { display->setFont(FONT_MEDIUM); int centerY = y + (display->getHeight() / 2) - (FONT_HEIGHT_MEDIUM / 2); display->drawString(display->getWidth() / 2 + x, centerY, bannerText); } else if (runState == TRACEROUTE_STATE_RESULT) { display->setFont(FONT_MEDIUM); display->setTextAlignment(TEXT_ALIGN_LEFT); display->drawString(x, y, "Route Result"); int contentStartY = y + FONT_HEIGHT_MEDIUM + 2; // Add more spacing after title display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); if (resultText.length() > 0) { if (resultLinesDirty) { rebuildResultLines(display); } int lineHeight = FONT_HEIGHT_SMALL + 1; // Use proper font height with 1px spacing for (size_t i = 0; i < resultLines.size(); i++) { int lineY = contentStartY + (i * lineHeight); if (lineY + FONT_HEIGHT_SMALL <= display->getHeight()) { display->drawString(x + 2, lineY, resultLines[i]); } } } } else if (runState == TRACEROUTE_STATE_COOLDOWN) { display->setFont(FONT_MEDIUM); int centerY = y + (display->getHeight() / 2) - (FONT_HEIGHT_MEDIUM / 2); display->drawString(display->getWidth() / 2 + x, centerY, bannerText); } } #endif // HAS_SCREEN int32_t TraceRouteModule::runOnce() { unsigned long now = millis(); if (runState == TRACEROUTE_STATE_IDLE) { return INT32_MAX; } // Check for tracking timeout if (runState == TRACEROUTE_STATE_TRACKING && now - lastTraceRouteTime > trackingTimeoutMs) { LOG_INFO("TraceRoute timeout, no response received"); runState = TRACEROUTE_STATE_RESULT; setResultText("No response received"); resultShowTime = now; tracingNode = 0; requestFocus(); UIFrameEvent e; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; notifyObservers(&e); setIntervalFromNow(resultDisplayMs); return resultDisplayMs; } // Update cooldown display every second if (runState == TRACEROUTE_STATE_COOLDOWN) { unsigned long wait = (cooldownMs - (now - lastTraceRouteTime)) / 1000; if (wait > 0) { String newBannerText = String("Wait for ") + String(wait) + String("s"); bannerText = newBannerText; LOG_INFO("TraceRoute cooldown: updating banner to %s", bannerText.c_str()); // Force flash UI requestFocus(); UIFrameEvent e; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; notifyObservers(&e); if (screen) { screen->forceDisplay(); } return 1000; } else { // Cooldown finished LOG_INFO("TraceRoute cooldown finished, returning to IDLE"); runState = TRACEROUTE_STATE_IDLE; resultText = ""; clearResultLines(); bannerText = ""; UIFrameEvent e; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; notifyObservers(&e); return INT32_MAX; } } if (runState == TRACEROUTE_STATE_RESULT) { if (now - resultShowTime >= resultDisplayMs) { LOG_INFO("TraceRoute result display timeout, returning to IDLE"); runState = TRACEROUTE_STATE_IDLE; resultText = ""; clearResultLines(); bannerText = ""; tracingNode = 0; UIFrameEvent e; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; notifyObservers(&e); return INT32_MAX; } else { return 1000; } } if (runState == TRACEROUTE_STATE_TRACKING) { return 1000; } return INT32_MAX; } ================================================ FILE: src/modules/TraceRouteModule.h ================================================ #pragma once #include "ProtobufModule.h" #include "concurrency/OSThread.h" #include "graphics/Screen.h" #include "graphics/SharedUIDisplay.h" #include "input/InputBroker.h" #if HAS_SCREEN #include "OLEDDisplayUi.h" #endif #include #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 */ enum TraceRouteRunState { TRACEROUTE_STATE_IDLE, TRACEROUTE_STATE_TRACKING, TRACEROUTE_STATE_RESULT, TRACEROUTE_STATE_COOLDOWN }; class TraceRouteModule : public ProtobufModule, public Observable, private concurrency::OSThread { public: TraceRouteModule(); bool startTraceRoute(NodeNum node); void launch(NodeNum node); void handleTraceRouteResult(const String &result); bool shouldDraw(); #if HAS_SCREEN virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) override; #endif const char *getNodeName(NodeNum node); virtual bool wantUIFrame() override { return shouldDraw(); } virtual Observable *getUIFrameObservable() override { return this; } void processUpgradedPacket(const meshtastic_MeshPacket &mp); 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; virtual int32_t runOnce() override; private: void setResultText(const String &text); void clearResultLines(); #if HAS_SCREEN void rebuildResultLines(OLEDDisplay *display); #endif // Call to add unknown hops (e.g. when a node couldn't decrypt it) to the route based on hopStart and current hopLimit void insertUnknownHops(meshtastic_MeshPacket &p, meshtastic_RouteDiscovery *r, bool isTowardsDestination); // Call to add your ID to the route array of a RouteDiscovery message void appendMyIDandSNR(meshtastic_RouteDiscovery *r, float snr, bool isTowardsDestination, bool SNRonly); // Update next-hops in the routing table based on the returned route void updateNextHops(const meshtastic_MeshPacket &p, meshtastic_RouteDiscovery *r); // Helper to update next-hop for a single node void maybeSetNextHop(NodeNum target, uint8_t nextHopByte); /* Call to print the route array of a RouteDiscovery message. Set origin to where the request came from. Set dest to the ID of its destination, or NODENUM_BROADCAST if it has not yet arrived there. */ void printRoute(meshtastic_RouteDiscovery *r, uint32_t origin, uint32_t dest, bool isTowardsDestination); TraceRouteRunState runState = TRACEROUTE_STATE_IDLE; unsigned long lastTraceRouteTime = 0; unsigned long resultShowTime = 0; unsigned long cooldownMs = 30000; unsigned long resultDisplayMs = 10000; unsigned long trackingTimeoutMs = 10000; String bannerText; String resultText; std::vector resultLines; bool resultLinesDirty = false; NodeNum tracingNode = 0; bool initialized = false; }; extern TraceRouteModule *traceRouteModule; ================================================ FILE: src/modules/TrafficManagementModule.cpp ================================================ #include "TrafficManagementModule.h" #if HAS_TRAFFIC_MANAGEMENT #include "Default.h" #include "MeshService.h" #include "NodeDB.h" #include "Router.h" #include "TypeConversions.h" #include "concurrency/LockGuard.h" #include "configuration.h" #include "mesh-pb-constants.h" #include "meshUtils.h" #include #include #define TM_LOG_DEBUG(fmt, ...) LOG_DEBUG("[TM] " fmt, ##__VA_ARGS__) #define TM_LOG_INFO(fmt, ...) LOG_INFO("[TM] " fmt, ##__VA_ARGS__) #define TM_LOG_WARN(fmt, ...) LOG_WARN("[TM] " fmt, ##__VA_ARGS__) // ============================================================================= // Anonymous Namespace - Internal Helpers // ============================================================================= namespace { constexpr uint32_t kMaintenanceIntervalMs = 60 * 1000UL; // Cache cleanup interval constexpr uint32_t kUnknownResetMs = 60 * 1000UL; // Unknown packet window constexpr uint8_t kMaxCuckooKicks = 16; // Max displacement chain length // NodeInfo direct response: enforced maximum hops by device role // Both use maxHops logic (respond when hopsAway <= threshold) // Config value is clamped to these role-based limits // Note: nodeinfo_direct_response must also be enabled for this to take effect constexpr uint32_t kRouterDefaultMaxHops = 3; // Routers: max 3 hops (can set lower via config) constexpr uint32_t kClientDefaultMaxHops = 0; // Clients: direct only (cannot increase) /** * Convert seconds to milliseconds with overflow protection. */ uint32_t secsToMs(uint32_t secs) { uint64_t ms = static_cast(secs) * 1000ULL; if (ms > UINT32_MAX) return UINT32_MAX; return static_cast(ms); } /** * Clamp precision to a valid dedup range. * Invalid values use the module default precision. */ uint8_t sanitizePositionPrecision(uint8_t precision) { if (precision > 0 && precision <= 32) return precision; const uint8_t defaultPrecision = static_cast(default_traffic_mgmt_position_precision_bits); if (defaultPrecision > 0 && defaultPrecision <= 32) return defaultPrecision; // Someone done messed up if we reach here return 32; } /** * Check if a timestamp is within a time window. * Handles wrap-around correctly using unsigned subtraction. */ bool isWithinWindow(uint32_t nowMs, uint32_t startMs, uint32_t intervalMs) { if (intervalMs == 0 || startMs == 0) return false; return (nowMs - startMs) < intervalMs; } /** * Truncate lat/lon to specified precision for position deduplication. * * The truncation works by masking off lower bits and rounding to the center * of the resulting grid cell. This creates a stable truncated value even * when GPS jitter causes small coordinate changes. * * @param value Raw latitude_i or longitude_i from position * @param precision Number of significant bits to keep (0-32) * @return Truncated and centered coordinate value */ int32_t truncateLatLon(int32_t value, uint8_t precision) { if (precision == 0 || precision >= 32) return value; // Create mask to zero out lower bits uint32_t mask = UINT32_MAX << (32 - precision); uint32_t truncated = static_cast(value) & mask; // Add half the truncation step to center in the grid cell truncated += (1u << (31 - precision)); return static_cast(truncated); } /** * Saturating increment for uint8_t counters. * Prevents overflow by capping at UINT8_MAX (255). */ inline void saturatingIncrement(uint8_t &counter) { if (counter < UINT8_MAX) counter++; } /** * Return a short human-readable name for common port numbers. * Falls back to "port:" for unknown ports. */ const char *portName(int portnum) { switch (portnum) { case meshtastic_PortNum_TEXT_MESSAGE_APP: return "text"; case meshtastic_PortNum_POSITION_APP: return "position"; case meshtastic_PortNum_NODEINFO_APP: return "nodeinfo"; case meshtastic_PortNum_ROUTING_APP: return "routing"; case meshtastic_PortNum_ADMIN_APP: return "admin"; case meshtastic_PortNum_TELEMETRY_APP: return "telemetry"; case meshtastic_PortNum_TRACEROUTE_APP: return "traceroute"; case meshtastic_PortNum_NEIGHBORINFO_APP: return "neighborinfo"; case meshtastic_PortNum_STORE_FORWARD_APP: return "store-forward"; case meshtastic_PortNum_WAYPOINT_APP: return "waypoint"; default: return nullptr; } } } // namespace // ============================================================================= // Module Instance // ============================================================================= TrafficManagementModule *trafficManagementModule; // ============================================================================= // Constructor // ============================================================================= TrafficManagementModule::TrafficManagementModule() : MeshModule("TrafficManagement"), concurrency::OSThread("TrafficManagement") { // Module configuration isPromiscuous = true; // See all packets, not just those addressed to us encryptedOk = true; // Can process encrypted packets stats = meshtastic_TrafficManagementStats_init_zero; // Initialize rolling epoch for relative timestamps cacheEpochMs = millis(); // Calculate adaptive time resolutions from config (config changes require reboot) // Resolution = max(60, min(339, interval/2)) for ~24 hour range with good precision posTimeResolution = calcTimeResolution(Default::getConfiguredOrDefault( moduleConfig.traffic_management.position_min_interval_secs, default_traffic_mgmt_position_min_interval_secs)); rateTimeResolution = calcTimeResolution(moduleConfig.traffic_management.rate_limit_window_secs); unknownTimeResolution = calcTimeResolution(kUnknownResetMs / 1000); // ~5 min default const auto &cfg = moduleConfig.traffic_management; TM_LOG_INFO("Enabled: pos_dedup=%d nodeinfo_resp=%d rate_limit=%d drop_unknown=%d exhaust_telem=%d exhaust_pos=%d " "preserve_hops=%d", cfg.position_dedup_enabled, cfg.nodeinfo_direct_response, cfg.rate_limit_enabled, cfg.drop_unknown_enabled, cfg.exhaust_hop_telemetry, cfg.exhaust_hop_position, cfg.router_preserve_hops); TM_LOG_DEBUG("Time resolutions: pos=%us, rate=%us, unknown=%us", posTimeResolution, rateTimeResolution, unknownTimeResolution); // Allocate unified cache (10 bytes/entry for all platforms) #if TRAFFIC_MANAGEMENT_CACHE_SIZE > 0 const uint16_t allocSize = cacheSize(); TM_LOG_INFO("Allocating unified cache: %u entries (%u bytes)", allocSize, static_cast(allocSize * sizeof(UnifiedCacheEntry))); #if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM) // ESP32 with PSRAM: prefer PSRAM for large allocations cache = static_cast(ps_calloc(allocSize, sizeof(UnifiedCacheEntry))); if (cache) { cacheFromPsram = true; } else { TM_LOG_WARN("PSRAM allocation failed, falling back to heap"); cache = new UnifiedCacheEntry[allocSize](); } #else // All other platforms: heap allocation cache = new UnifiedCacheEntry[allocSize](); #endif #endif // TRAFFIC_MANAGEMENT_CACHE_SIZE > 0 #if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM) TM_LOG_INFO("Allocating NodeInfo cache: target=%u occupancy=%u%% payload=%u bytes (PSRAM) tags=%u bytes (%u-bit, %u slots, " "%u buckets x %u)", static_cast(nodeInfoTargetEntries()), static_cast(nodeInfoTargetOccupancyPercent()), static_cast(nodeInfoTargetEntries() * sizeof(NodeInfoPayloadEntry)), static_cast(nodeInfoIndexMetadataBudgetBytes()), static_cast(nodeInfoTagBits()), static_cast(nodeInfoIndexSlots()), static_cast(nodeInfoBucketCount()), static_cast(nodeInfoBucketSize())); nodeInfoIndex = static_cast(calloc(nodeInfoIndexMetadataBudgetBytes(), sizeof(uint8_t))); if (!nodeInfoIndex) { TM_LOG_WARN("NodeInfo index allocation failed; direct responses will fall back to NodeDB"); } else { nodeInfoPayload = static_cast(ps_calloc(nodeInfoTargetEntries(), sizeof(NodeInfoPayloadEntry))); if (nodeInfoPayload) { nodeInfoPayloadFromPsram = true; TM_LOG_INFO("NodeInfo bucketed cuckoo cache ready"); } else { TM_LOG_WARN("NodeInfo PSRAM payload allocation failed; direct responses will fall back to NodeDB"); free(nodeInfoIndex); nodeInfoIndex = nullptr; } } #else TM_LOG_DEBUG("NodeInfo PSRAM cache not available on this target"); #endif setIntervalFromNow(kMaintenanceIntervalMs); } // Cache may have been allocated via ps_calloc (PSRAM, C allocator) or new[] (heap). // Must use the matching deallocator: free() for ps_calloc, delete[] for new[]. TrafficManagementModule::~TrafficManagementModule() { #if TRAFFIC_MANAGEMENT_CACHE_SIZE > 0 if (cache) { // Cache may be from ps_calloc (PSRAM, C allocator) or new[] (heap). // Use the matching deallocator for the allocation source. if (cacheFromPsram) free(cache); else delete[] cache; cache = nullptr; } #endif if (nodeInfoPayload) { if (nodeInfoPayloadFromPsram) free(nodeInfoPayload); else delete[] nodeInfoPayload; nodeInfoPayload = nullptr; } if (nodeInfoIndex) { free(nodeInfoIndex); nodeInfoIndex = nullptr; } } // ============================================================================= // Statistics // ============================================================================= meshtastic_TrafficManagementStats TrafficManagementModule::getStats() const { concurrency::LockGuard guard(&cacheLock); return stats; } void TrafficManagementModule::resetStats() { concurrency::LockGuard guard(&cacheLock); stats = meshtastic_TrafficManagementStats_init_zero; } void TrafficManagementModule::recordRouterHopPreserved() { if (!moduleConfig.has_traffic_management || !moduleConfig.traffic_management.enabled) return; incrementStat(&stats.router_hops_preserved); } void TrafficManagementModule::incrementStat(uint32_t *field) { concurrency::LockGuard guard(&cacheLock); (*field)++; } // ============================================================================= // Cuckoo Hash Table Operations // ============================================================================= /** * Find an existing entry for the given node. * * Cuckoo hashing guarantees that if an entry exists, it's in one of exactly * two locations: hash1(node) or hash2(node). This provides O(1) lookup. * * @param node NodeNum to search for * @return Pointer to entry if found, nullptr otherwise */ TrafficManagementModule::UnifiedCacheEntry *TrafficManagementModule::findEntry(NodeNum node) { #if TRAFFIC_MANAGEMENT_CACHE_SIZE == 0 (void)node; return nullptr; #else if (!cache || node == 0) return nullptr; // Check primary location uint16_t h1 = cuckooHash1(node); if (cache[h1].node == node) return &cache[h1]; // Check alternate location uint16_t h2 = cuckooHash2(node); if (cache[h2].node == node) return &cache[h2]; return nullptr; #endif } /** * Find or create an entry for the given node using cuckoo hashing. * * If the node exists, returns the existing entry. Otherwise, attempts to * insert a new entry using cuckoo displacement: * * 1. Try to insert at h1(node) - if empty, done * 2. Try to insert at h2(node) - if empty, done * 3. Kick existing entry from h1 to its alternate location * 4. Repeat up to kMaxCuckooKicks times * 5. If cycle detected or max kicks exceeded, evict oldest entry * * @param node NodeNum to find or create * @param isNew Set to true if a new entry was created * @return Pointer to entry, or nullptr if allocation failed */ TrafficManagementModule::UnifiedCacheEntry *TrafficManagementModule::findOrCreateEntry(NodeNum node, bool *isNew) { #if TRAFFIC_MANAGEMENT_CACHE_SIZE == 0 (void)node; if (isNew) *isNew = false; return nullptr; #else if (!cache || node == 0) { if (isNew) *isNew = false; return nullptr; } // Check if entry already exists (O(1) lookup) uint16_t h1 = cuckooHash1(node); if (cache[h1].node == node) { if (isNew) *isNew = false; return &cache[h1]; } uint16_t h2 = cuckooHash2(node); if (cache[h2].node == node) { if (isNew) *isNew = false; return &cache[h2]; } // Entry doesn't exist - try to insert // Prefer empty slot at h1 if (cache[h1].node == 0) { memset(&cache[h1], 0, sizeof(UnifiedCacheEntry)); cache[h1].node = node; if (isNew) *isNew = true; return &cache[h1]; } // Try empty slot at h2 if (cache[h2].node == 0) { memset(&cache[h2], 0, sizeof(UnifiedCacheEntry)); cache[h2].node = node; if (isNew) *isNew = true; return &cache[h2]; } // Both slots occupied - perform cuckoo displacement // Start by kicking entry at h1 to its alternate location UnifiedCacheEntry displaced = cache[h1]; memset(&cache[h1], 0, sizeof(UnifiedCacheEntry)); cache[h1].node = node; for (uint8_t kicks = 0; kicks < kMaxCuckooKicks; kicks++) { // Find alternate location for displaced entry uint16_t altH1 = cuckooHash1(displaced.node); uint16_t altH2 = cuckooHash2(displaced.node); uint16_t altSlot = (altH1 == h1) ? altH2 : altH1; if (cache[altSlot].node == 0) { // Found empty slot - insert displaced entry cache[altSlot] = displaced; if (isNew) *isNew = true; return &cache[h1]; } // Kick entry from alternate slot UnifiedCacheEntry temp = cache[altSlot]; cache[altSlot] = displaced; displaced = temp; h1 = altSlot; } // Cuckoo cycle detected or max kicks exceeded. // The displaced entry has no valid cuckoo slot — drop it to preserve cache integrity. // Placing it at an arbitrary slot would make it unreachable by findEntry(). TM_LOG_DEBUG("Cuckoo cycle, evicting node 0x%08x", displaced.node); if (isNew) *isNew = true; return &cache[cuckooHash1(node)]; #endif } const TrafficManagementModule::NodeInfoPayloadEntry *TrafficManagementModule::findNodeInfoEntry(NodeNum node) const { #if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM) if (!nodeInfoPayload || !nodeInfoIndex || node == 0) return nullptr; uint16_t payloadIndex = findNodeInfoPayloadIndex(node); if (payloadIndex >= nodeInfoTargetEntries()) return nullptr; return &nodeInfoPayload[payloadIndex]; #else (void)node; return nullptr; #endif } uint16_t TrafficManagementModule::encodeNodeInfoTag(uint16_t payloadIndex) const { if (payloadIndex >= nodeInfoTargetEntries()) return 0; return static_cast(payloadIndex + 1u); } uint16_t TrafficManagementModule::decodeNodeInfoPayloadIndex(uint16_t tag) const { if (tag == 0 || tag > nodeInfoTargetEntries()) return UINT16_MAX; return static_cast(tag - 1u); } uint16_t TrafficManagementModule::getNodeInfoTag(uint16_t slot) const { #if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM) if (!nodeInfoIndex || slot >= nodeInfoIndexSlots()) return 0; const uint32_t bitOffset = static_cast(slot) * nodeInfoTagBits(); const uint16_t byteOffset = static_cast(bitOffset >> 3); const uint8_t shift = static_cast(bitOffset & 7u); uint32_t packed = 0; if (byteOffset < nodeInfoIndexMetadataBudgetBytes()) packed |= static_cast(nodeInfoIndex[byteOffset]); if (static_cast(byteOffset + 1u) < nodeInfoIndexMetadataBudgetBytes()) packed |= static_cast(nodeInfoIndex[byteOffset + 1u]) << 8; if (static_cast(byteOffset + 2u) < nodeInfoIndexMetadataBudgetBytes()) packed |= static_cast(nodeInfoIndex[byteOffset + 2u]) << 16; return static_cast((packed >> shift) & nodeInfoTagMask()); #else (void)slot; return 0; #endif } void TrafficManagementModule::setNodeInfoTag(uint16_t slot, uint16_t tag) { #if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM) if (!nodeInfoIndex || slot >= nodeInfoIndexSlots()) return; const uint16_t normalizedTag = static_cast(tag & nodeInfoTagMask()); const uint32_t bitOffset = static_cast(slot) * nodeInfoTagBits(); const uint16_t byteOffset = static_cast(bitOffset >> 3); const uint8_t shift = static_cast(bitOffset & 7u); uint32_t packed = 0; if (byteOffset < nodeInfoIndexMetadataBudgetBytes()) packed |= static_cast(nodeInfoIndex[byteOffset]); if (static_cast(byteOffset + 1u) < nodeInfoIndexMetadataBudgetBytes()) packed |= static_cast(nodeInfoIndex[byteOffset + 1u]) << 8; if (static_cast(byteOffset + 2u) < nodeInfoIndexMetadataBudgetBytes()) packed |= static_cast(nodeInfoIndex[byteOffset + 2u]) << 16; const uint32_t mask = static_cast(nodeInfoTagMask()) << shift; packed = (packed & ~mask) | ((static_cast(normalizedTag) << shift) & mask); if (byteOffset < nodeInfoIndexMetadataBudgetBytes()) nodeInfoIndex[byteOffset] = static_cast(packed & 0xFFu); if (static_cast(byteOffset + 1u) < nodeInfoIndexMetadataBudgetBytes()) nodeInfoIndex[byteOffset + 1u] = static_cast((packed >> 8) & 0xFFu); if (static_cast(byteOffset + 2u) < nodeInfoIndexMetadataBudgetBytes()) nodeInfoIndex[byteOffset + 2u] = static_cast((packed >> 16) & 0xFFu); #else (void)slot; (void)tag; #endif } uint16_t TrafficManagementModule::findNodeInfoPayloadIndex(NodeNum node) const { #if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM) if (!nodeInfoPayload || !nodeInfoIndex || node == 0) return UINT16_MAX; const uint16_t buckets[2] = {nodeInfoHash1(node), nodeInfoHash2(node)}; for (uint8_t b = 0; b < 2; b++) { const uint16_t base = static_cast(buckets[b] * nodeInfoBucketSize()); for (uint8_t slot = 0; slot < nodeInfoBucketSize(); slot++) { uint16_t tag = getNodeInfoTag(static_cast(base + slot)); if (tag == 0) continue; uint16_t payloadIndex = decodeNodeInfoPayloadIndex(tag); if (payloadIndex >= nodeInfoTargetEntries()) continue; if (nodeInfoPayload[payloadIndex].node == node) return payloadIndex; } } return UINT16_MAX; #else (void)node; return UINT16_MAX; #endif } bool TrafficManagementModule::removeNodeInfoIndexEntry(NodeNum node, uint16_t payloadIndex) { #if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM) if (!nodeInfoIndex || node == 0 || payloadIndex >= nodeInfoTargetEntries()) return false; const uint16_t payloadTag = encodeNodeInfoTag(payloadIndex); if (payloadTag == 0) return false; const uint16_t buckets[2] = {nodeInfoHash1(node), nodeInfoHash2(node)}; for (uint8_t b = 0; b < 2; b++) { const uint16_t base = static_cast(buckets[b] * nodeInfoBucketSize()); for (uint8_t slot = 0; slot < nodeInfoBucketSize(); slot++) { const uint16_t indexSlot = static_cast(base + slot); if (getNodeInfoTag(indexSlot) == payloadTag) { setNodeInfoTag(indexSlot, 0); return true; } } } return false; #else (void)node; (void)payloadIndex; return false; #endif } uint16_t TrafficManagementModule::allocateNodeInfoPayloadSlot() { #if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM) if (!nodeInfoPayload) return UINT16_MAX; for (uint16_t tries = 0; tries < nodeInfoTargetEntries(); tries++) { uint16_t idx = static_cast((nodeInfoAllocHint + tries) % nodeInfoTargetEntries()); if (nodeInfoPayload[idx].node == 0) { nodeInfoAllocHint = static_cast((idx + 1u) % nodeInfoTargetEntries()); return idx; } } #endif return UINT16_MAX; } uint16_t TrafficManagementModule::evictNodeInfoPayloadSlot() { #if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM) if (!nodeInfoPayload || !nodeInfoIndex) return UINT16_MAX; for (uint16_t tries = 0; tries < nodeInfoTargetEntries(); tries++) { uint16_t idx = static_cast(nodeInfoEvictCursor % nodeInfoTargetEntries()); nodeInfoEvictCursor = static_cast((nodeInfoEvictCursor + 1u) % nodeInfoTargetEntries()); NodeNum oldNode = nodeInfoPayload[idx].node; if (oldNode == 0) continue; removeNodeInfoIndexEntry(oldNode, idx); // best effort; cache tolerates occasional stale miss nodeInfoPayload[idx].node = 0; return idx; } #endif return UINT16_MAX; } bool TrafficManagementModule::tryInsertNodeInfoEntryInBucket(uint16_t bucket, uint16_t tag) { #if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM) if (!nodeInfoIndex || !nodeInfoPayload || bucket >= nodeInfoBucketCount() || tag == 0) return false; const uint16_t base = static_cast(bucket * nodeInfoBucketSize()); for (uint8_t slot = 0; slot < nodeInfoBucketSize(); slot++) { const uint16_t indexSlot = static_cast(base + slot); const uint16_t existingTag = getNodeInfoTag(indexSlot); if (existingTag == 0) { setNodeInfoTag(indexSlot, tag); return true; } // Opportunistically reuse stale tags that point at empty/invalid payload slots. const uint16_t payloadIndex = decodeNodeInfoPayloadIndex(existingTag); if (payloadIndex >= nodeInfoTargetEntries() || nodeInfoPayload[payloadIndex].node == 0) { setNodeInfoTag(indexSlot, tag); return true; } } #else (void)bucket; (void)tag; #endif return false; } TrafficManagementModule::NodeInfoPayloadEntry *TrafficManagementModule::findOrCreateNodeInfoEntry(NodeNum node, bool *usedEmptySlot) { if (usedEmptySlot) *usedEmptySlot = false; #if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM) if (!nodeInfoPayload || !nodeInfoIndex || node == 0) return nullptr; uint16_t existing = findNodeInfoPayloadIndex(node); if (existing < nodeInfoTargetEntries()) return &nodeInfoPayload[existing]; const uint16_t beforeCount = countNodeInfoEntriesLocked(); uint16_t payloadIndex = allocateNodeInfoPayloadSlot(); if (payloadIndex == UINT16_MAX) { payloadIndex = evictNodeInfoPayloadSlot(); if (payloadIndex == UINT16_MAX) return nullptr; } nodeInfoPayload[payloadIndex].node = node; // 4-way bucketed cuckoo insertion mirrors Cuckoo Filter practice from // Fan et al. (CoNEXT 2014): high occupancy with short relocation chains. uint16_t pending = encodeNodeInfoTag(payloadIndex); uint16_t h1 = nodeInfoHash1(node); uint16_t h2 = nodeInfoHash2(node); if (!tryInsertNodeInfoEntryInBucket(h1, pending) && !tryInsertNodeInfoEntryInBucket(h2, pending)) { uint16_t currentBucket = h1; for (uint8_t kicks = 0; kicks < kMaxCuckooKicks; kicks++) { const uint16_t base = static_cast(currentBucket * nodeInfoBucketSize()); const uint16_t kickSlot = static_cast((node + kicks) & (nodeInfoBucketSize() - 1u)); const uint16_t pos = static_cast(base + kickSlot); uint16_t displaced = getNodeInfoTag(pos); setNodeInfoTag(pos, pending); pending = displaced; uint16_t displacedPayload = decodeNodeInfoPayloadIndex(pending); if (displacedPayload >= nodeInfoTargetEntries()) { pending = 0; break; } NodeNum displacedNode = nodeInfoPayload[displacedPayload].node; if (displacedNode == 0) { pending = 0; break; } uint16_t altH1 = nodeInfoHash1(displacedNode); uint16_t altH2 = nodeInfoHash2(displacedNode); uint16_t altBucket = (altH1 == currentBucket) ? altH2 : altH1; if (tryInsertNodeInfoEntryInBucket(altBucket, pending)) { pending = 0; break; } currentBucket = altBucket; } if (pending != 0) { uint16_t droppedPayload = decodeNodeInfoPayloadIndex(pending); if (droppedPayload < nodeInfoTargetEntries()) nodeInfoPayload[droppedPayload].node = 0; TM_LOG_DEBUG("NodeInfo bucketed cuckoo overflow, dropped payload idx=%u", static_cast(droppedPayload < nodeInfoTargetEntries() ? droppedPayload : UINT16_MAX)); } } uint16_t finalIndex = findNodeInfoPayloadIndex(node); if (finalIndex >= nodeInfoTargetEntries()) { // New entry did not survive insertion chain. if (payloadIndex < nodeInfoTargetEntries() && nodeInfoPayload[payloadIndex].node == node) nodeInfoPayload[payloadIndex].node = 0; return nullptr; } if (usedEmptySlot) { const uint16_t afterCount = countNodeInfoEntriesLocked(); *usedEmptySlot = afterCount > beforeCount; } return &nodeInfoPayload[finalIndex]; #else (void)node; return nullptr; #endif } uint16_t TrafficManagementModule::countNodeInfoEntriesLocked() const { #if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM) if (!nodeInfoIndex) return 0; uint16_t count = 0; for (uint16_t i = 0; i < nodeInfoIndexSlots(); i++) { if (getNodeInfoTag(i) != 0) count++; } return count; #else return 0; #endif } void TrafficManagementModule::cacheNodeInfoPacket(const meshtastic_MeshPacket &mp) { #if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM) if (!nodeInfoPayload || !nodeInfoIndex || mp.decoded.payload.size == 0) return; meshtastic_User user = meshtastic_User_init_zero; if (!pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, &meshtastic_User_msg, &user)) return; // Normalize user.id to the packet sender's node number. snprintf(user.id, sizeof(user.id), "!%08x", getFrom(&mp)); bool usedEmptySlot = false; uint16_t cachedCount = 0; { concurrency::LockGuard guard(&cacheLock); NodeInfoPayloadEntry *entry = findOrCreateNodeInfoEntry(getFrom(&mp), &usedEmptySlot); if (!entry) return; // Cache both payload and response metadata so direct replies can use // richer context than "just the user protobuf" when PSRAM is present. // This path is intentionally independent from NodeInfoModule/NodeDB. entry->user = user; entry->lastObservedMs = millis(); entry->lastObservedRxTime = mp.rx_time; entry->sourceChannel = mp.channel; entry->hasDecodedBitfield = mp.decoded.has_bitfield; entry->decodedBitfield = mp.decoded.bitfield; if (usedEmptySlot) cachedCount = countNodeInfoEntriesLocked(); } if (usedEmptySlot) { TM_LOG_INFO("NodeInfo PSRAM cache entries: %u/%u target (%u packed slots, %u-bit tags, %u-byte DRAM index)", static_cast(cachedCount), static_cast(nodeInfoTargetEntries()), static_cast(nodeInfoIndexSlots()), static_cast(nodeInfoTagBits()), static_cast(nodeInfoIndexMetadataBudgetBytes())); } #else (void)mp; #endif } // ============================================================================= // Epoch Management // ============================================================================= /** * Reset the timestamp epoch when relative offsets approach overflow. * * Called when epoch age exceeds ~19 hours (approaching 8-bit minute overflow). * Invalidates all cached per-node traffic state. */ void TrafficManagementModule::resetEpoch(uint32_t nowMs) { #if TRAFFIC_MANAGEMENT_CACHE_SIZE > 0 TM_LOG_DEBUG("Resetting cache epoch"); cacheEpochMs = nowMs; // Full flush avoids stale dedup identity/counters surviving epoch rollover. memset(cache, 0, static_cast(cacheSize()) * sizeof(UnifiedCacheEntry)); #else (void)nowMs; #endif } // ============================================================================= // Position Hash (Compact Mode) // ============================================================================= /** * Compute 8-bit position fingerprint from truncated lat/lon coordinates. * * Unlike a hash, this is deterministic: adjacent grid cells have sequential * fingerprints, so nearby positions never collide. The fingerprint extracts * the lower 4 significant bits from each truncated coordinate. * * Example with precision=16: * lat_truncated = 0x12340000 (top 16 bits significant) * Significant portion = 0x1234, lower 4 bits = 0x4 * * fingerprint = (lat_low4 << 4) | lon_low4 = 8 bits total * * Collision: Two positions collide only if they differ by a multiple of 16 * grid cells in BOTH lat and lon dimensions simultaneously - very unlikely * for typical position update patterns. * * @param lat_truncated Precision-truncated latitude * @param lon_truncated Precision-truncated longitude * @param precision Number of significant bits (1-32) * @return 8-bit fingerprint (4 bits lat + 4 bits lon) */ uint8_t TrafficManagementModule::computePositionFingerprint(int32_t lat_truncated, int32_t lon_truncated, uint8_t precision) { precision = sanitizePositionPrecision(precision); // Guard: if precision < 4, we have fewer bits to work with // Take min(precision, 4) bits from each coordinate uint8_t bitsToTake = (precision < 4) ? precision : 4; // Shift to move significant bits to bottom, then mask lower bits // For precision=16: shift by 16 to get the 16 significant bits at bottom uint8_t shift = 32 - precision; uint8_t latBits = (static_cast(lat_truncated) >> shift) & ((1u << bitsToTake) - 1); uint8_t lonBits = (static_cast(lon_truncated) >> shift) & ((1u << bitsToTake) - 1); return static_cast((latBits << 4) | lonBits); } // ============================================================================= // Packet Handling // ============================================================================= // Processing order matters: this module runs BEFORE RoutingModule in the callModules() loop. // - STOP prevents RoutingModule from calling sniffReceived() → perhapsRebroadcast(), // so the packet is fully consumed (not forwarded). // - ignoreRequest suppresses the default "no one responded" NAK for want_response packets. // - exhaustRequested is set by alterReceived() and checked by perhapsRebroadcast() to // force hop_limit=0 on the rebroadcast copy, allowing one final relay hop. ProcessMessage TrafficManagementModule::handleReceived(const meshtastic_MeshPacket &mp) { if (!moduleConfig.has_traffic_management || !moduleConfig.traffic_management.enabled) return ProcessMessage::CONTINUE; ignoreRequest = false; exhaustRequested = false; // Reset per-packet; may be set by alterReceived() below exhaustRequestedFrom = 0; exhaustRequestedId = 0; incrementStat(&stats.packets_inspected); const auto &cfg = moduleConfig.traffic_management; const uint32_t nowMs = millis(); // ------------------------------------------------------------------------- // Undecoded Packet Handling // ------------------------------------------------------------------------- // Packets we can't decode (wrong key, corruption, etc.) may indicate // a misbehaving node. Track and optionally drop repeat offenders. if (mp.which_payload_variant != meshtastic_MeshPacket_decoded_tag) { if (cfg.drop_unknown_enabled && cfg.unknown_packet_threshold > 0) { if (shouldDropUnknown(&mp, nowMs)) { logAction("drop", &mp, "unknown"); incrementStat(&stats.unknown_packet_drops); ignoreRequest = true; // Suppress NAK for want_response packets return ProcessMessage::STOP; // Consumed — will not be rebroadcast } } return ProcessMessage::CONTINUE; } // Learn NodeInfo payloads into the dedicated PSRAM cache. if (mp.decoded.portnum == meshtastic_PortNum_NODEINFO_APP) cacheNodeInfoPacket(mp); // ------------------------------------------------------------------------- // NodeInfo Direct Response // ------------------------------------------------------------------------- // When we see a unicast NodeInfo request for a node we know about, // respond directly from cache instead of forwarding the request. // STOP prevents the request from being rebroadcast toward the target node, // and our cached response is sent back to the requestor with hop_limit=0. if (cfg.nodeinfo_direct_response && mp.decoded.portnum == meshtastic_PortNum_NODEINFO_APP && mp.decoded.want_response && !isBroadcast(mp.to) && !isToUs(&mp) && !isFromUs(&mp)) { if (shouldRespondToNodeInfo(&mp, true)) { meshtastic_User requester = meshtastic_User_init_zero; if (pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, &meshtastic_User_msg, &requester)) { nodeDB->updateUser(getFrom(&mp), requester, mp.channel); } logAction("respond", &mp, "nodeinfo-cache"); incrementStat(&stats.nodeinfo_cache_hits); ignoreRequest = true; // We responded; suppress default NAK return ProcessMessage::STOP; // Consumed — request will not be forwarded } } // ------------------------------------------------------------------------- // Position Deduplication // ------------------------------------------------------------------------- // Drop position broadcasts that haven't moved significantly since the // last broadcast from this node. Uses truncated coordinates to ignore // GPS jitter within the configured precision. if (!isFromUs(&mp) && !isToUs(&mp)) { if (cfg.position_dedup_enabled && mp.decoded.portnum == meshtastic_PortNum_POSITION_APP) { meshtastic_Position pos = meshtastic_Position_init_zero; if (pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, &meshtastic_Position_msg, &pos)) { if (shouldDropPosition(&mp, &pos, nowMs)) { logAction("drop", &mp, "position-dedup"); incrementStat(&stats.position_dedup_drops); ignoreRequest = true; // Suppress NAK return ProcessMessage::STOP; // Consumed — duplicate will not be rebroadcast } } } // --------------------------------------------------------------------- // Rate Limiting // --------------------------------------------------------------------- // Throttle nodes sending too many packets within a time window. // Excludes routing and admin packets which are essential for mesh operation. if (cfg.rate_limit_enabled && cfg.rate_limit_window_secs > 0 && cfg.rate_limit_max_packets > 0) { if (mp.decoded.portnum != meshtastic_PortNum_ROUTING_APP && mp.decoded.portnum != meshtastic_PortNum_ADMIN_APP) { if (isRateLimited(mp.from, nowMs)) { logAction("drop", &mp, "rate-limit"); incrementStat(&stats.rate_limit_drops); ignoreRequest = true; // Suppress NAK return ProcessMessage::STOP; // Consumed — throttled packet will not be rebroadcast } } } } return ProcessMessage::CONTINUE; } void TrafficManagementModule::alterReceived(meshtastic_MeshPacket &mp) { if (!moduleConfig.has_traffic_management || !moduleConfig.traffic_management.enabled) return; if (mp.which_payload_variant != meshtastic_MeshPacket_decoded_tag) return; if (isFromUs(&mp)) return; // ------------------------------------------------------------------------- // Relayed Broadcast Hop Exhaustion // ------------------------------------------------------------------------- // For relayed telemetry or position broadcasts from other nodes, optionally // set hop_limit=0 so they don't propagate further through the mesh. const auto &cfg = moduleConfig.traffic_management; const bool isTelemetry = mp.decoded.portnum == meshtastic_PortNum_TELEMETRY_APP; const bool isPosition = mp.decoded.portnum == meshtastic_PortNum_POSITION_APP; const bool shouldExhaust = (isTelemetry && cfg.exhaust_hop_telemetry) || (isPosition && cfg.exhaust_hop_position); if (!shouldExhaust || !isBroadcast(mp.to)) return; if (mp.hop_limit > 0) { const char *reason = isTelemetry ? "exhaust-hop-telemetry" : "exhaust-hop-position"; logAction("exhaust", &mp, reason); // Adjust hop_start so downstream nodes compute correct hopsAway (hop_start - hop_limit). // Without this, hop_limit=0 with original hop_start would show inflated hopsAway. mp.hop_start = mp.hop_start - mp.hop_limit + 1; mp.hop_limit = 0; // Signal perhapsRebroadcast() to allow one final relay with hop_limit=0. // Without this flag, perhapsRebroadcast() would skip the packet since hop_limit==0. // The packet-scoped flag is checked in NextHopRouter::perhapsRebroadcast() // and forces tosend->hop_limit=0, ensuring no further propagation beyond the // next node. exhaustRequested = true; exhaustRequestedFrom = getFrom(&mp); exhaustRequestedId = mp.id; incrementStat(&stats.hop_exhausted_packets); } } // ============================================================================= // Periodic Maintenance // ============================================================================= int32_t TrafficManagementModule::runOnce() { if (!moduleConfig.has_traffic_management || !moduleConfig.traffic_management.enabled) return INT32_MAX; #if TRAFFIC_MANAGEMENT_CACHE_SIZE > 0 const uint32_t nowMs = millis(); // Check if epoch reset needed (~3.5 hours approaching 8-bit minute overflow) if (needsEpochReset(nowMs)) { concurrency::LockGuard guard(&cacheLock); resetEpoch(nowMs); return kMaintenanceIntervalMs; } // Calculate TTLs for cache expiration const uint32_t positionIntervalMs = secsToMs(Default::getConfiguredOrDefault( moduleConfig.traffic_management.position_min_interval_secs, default_traffic_mgmt_position_min_interval_secs)); const uint32_t positionTtlMs = positionIntervalMs * 4; const uint32_t rateIntervalMs = secsToMs(moduleConfig.traffic_management.rate_limit_window_secs); const uint32_t rateTtlMs = (rateIntervalMs > 0) ? rateIntervalMs * 2 : (10 * 60 * 1000UL); const uint32_t unknownTtlMs = kUnknownResetMs * 5; // Sweep cache and clear expired entries uint16_t activeEntries = 0; uint16_t expiredEntries = 0; const uint32_t sweepStartMs = millis(); concurrency::LockGuard guard(&cacheLock); for (uint16_t i = 0; i < cacheSize(); i++) { if (cache[i].node == 0) continue; bool anyValid = false; // Check and clear expired position data if (cache[i].pos_time != 0) { uint32_t posTimeMs = fromRelativePosTime(cache[i].pos_time); if (!isWithinWindow(nowMs, posTimeMs, positionTtlMs)) { cache[i].pos_fingerprint = 0; cache[i].pos_time = 0; } else { anyValid = true; } } // Check and clear expired rate limit data if (cache[i].rate_time != 0) { uint32_t rateTimeMs = fromRelativeRateTime(cache[i].rate_time); if (!isWithinWindow(nowMs, rateTimeMs, rateTtlMs)) { cache[i].rate_count = 0; cache[i].rate_time = 0; } else { anyValid = true; } } // Check and clear expired unknown tracking data if (cache[i].unknown_time != 0) { uint32_t unknownTimeMs = fromRelativeUnknownTime(cache[i].unknown_time); if (!isWithinWindow(nowMs, unknownTimeMs, unknownTtlMs)) { cache[i].unknown_count = 0; cache[i].unknown_time = 0; } else { anyValid = true; } } // If all data expired, free the slot entirely if (!anyValid) { memset(&cache[i], 0, sizeof(UnifiedCacheEntry)); expiredEntries++; } else { activeEntries++; } } TM_LOG_DEBUG("Maintenance: %u active, %u expired, %u/%u slots, %lums elapsed", activeEntries, expiredEntries, static_cast(activeEntries), static_cast(cacheSize()), static_cast(millis() - sweepStartMs)); #if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM) if (nodeInfoPayload && nodeInfoIndex) { TM_LOG_DEBUG("NodeInfo PSRAM cache: %u/%u target (%u packed slots, %u buckets, %u-bit tags, %u-byte index)", static_cast(countNodeInfoEntriesLocked()), static_cast(nodeInfoTargetEntries()), static_cast(nodeInfoIndexSlots()), static_cast(nodeInfoBucketCount()), static_cast(nodeInfoTagBits()), static_cast(nodeInfoIndexMetadataBudgetBytes())); } #endif #endif // TRAFFIC_MANAGEMENT_CACHE_SIZE > 0 return kMaintenanceIntervalMs; } // ============================================================================= // Traffic Management Logic // ============================================================================= bool TrafficManagementModule::shouldDropPosition(const meshtastic_MeshPacket *p, const meshtastic_Position *pos, uint32_t nowMs) { #if TRAFFIC_MANAGEMENT_CACHE_SIZE == 0 (void)p; (void)pos; (void)nowMs; return false; #else if (!pos->has_latitude_i || !pos->has_longitude_i) return false; uint8_t precision = Default::getConfiguredOrDefault(moduleConfig.traffic_management.position_precision_bits, default_traffic_mgmt_position_precision_bits); precision = sanitizePositionPrecision(precision); const int32_t lat_truncated = truncateLatLon(pos->latitude_i, precision); const int32_t lon_truncated = truncateLatLon(pos->longitude_i, precision); const uint8_t fingerprint = computePositionFingerprint(lat_truncated, lon_truncated, precision); const uint32_t minIntervalMs = secsToMs(Default::getConfiguredOrDefault( moduleConfig.traffic_management.position_min_interval_secs, default_traffic_mgmt_position_min_interval_secs)); bool isNew = false; concurrency::LockGuard guard(&cacheLock); UnifiedCacheEntry *entry = findOrCreateEntry(p->from, &isNew); if (!entry) return false; // Compare fingerprint and check time window // When minIntervalMs == 0, deduplication is disabled (withinInterval = false means never drop) const bool hasPositionState = !isNew && entry->pos_time != 0; const bool samePosition = hasPositionState && entry->pos_fingerprint == fingerprint; const bool withinInterval = hasPositionState && (minIntervalMs != 0) && isWithinWindow(nowMs, fromRelativePosTime(entry->pos_time), minIntervalMs); TM_LOG_DEBUG("Position dedup 0x%08x: fp=0x%02x prev=0x%02x same=%d within=%d new=%d", p->from, fingerprint, entry->pos_fingerprint, samePosition, withinInterval, isNew); // Update cache entry entry->pos_fingerprint = fingerprint; entry->pos_time = toRelativePosTime(nowMs); // Drop only if same position AND within the minimum interval return samePosition && withinInterval; #endif } bool TrafficManagementModule::shouldRespondToNodeInfo(const meshtastic_MeshPacket *p, bool sendResponse) { // Caller already verified: nodeinfo_direct_response, portnum, want_response, // !isBroadcast, !isToUs, !isFromUs if (!isMinHopsFromRequestor(p)) return false; meshtastic_User cachedUser = meshtastic_User_init_zero; bool hasCachedUser = false; // Extra metadata consumed only by the PSRAM-backed cache path. // Defaults preserve previous behavior when cache metadata is unavailable. bool cachedHasDecodedBitfield = false; uint8_t cachedDecodedBitfield = 0; uint8_t cachedSourceChannel = 0; uint32_t cachedLastObservedMs = 0; uint32_t cachedLastObservedRxTime = 0; { concurrency::LockGuard guard(&cacheLock); const NodeInfoPayloadEntry *entry = findNodeInfoEntry(p->to); if (entry) { cachedUser = entry->user; hasCachedUser = true; cachedHasDecodedBitfield = entry->hasDecodedBitfield; cachedDecodedBitfield = entry->decodedBitfield; cachedSourceChannel = entry->sourceChannel; cachedLastObservedMs = entry->lastObservedMs; cachedLastObservedRxTime = entry->lastObservedRxTime; } } if (!hasCachedUser) { // If the PSRAM cache exists but misses, we intentionally do not fall back // to the node-wide table. This keeps the PSRAM direct-reply path separate // from NodeInfoModule/NodeDB behavior when PSRAM is available. if (nodeInfoPayload && nodeInfoIndex) { TM_LOG_DEBUG("NodeInfo PSRAM cache miss for node=0x%08x", p->to); return false; } // Fallback only when PSRAM cache is unavailable on this target. // In this mode we use the node-wide table maintained by NodeInfoModule. const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(p->to); if (!node || !node->has_user) return false; cachedUser = TypeConversions::ConvertToUser(node->num, node->user); } if (!sendResponse) return true; meshtastic_MeshPacket *reply = router->allocForSending(); if (!reply) { TM_LOG_WARN("NodeInfo direct response dropped: no packet buffer"); return false; } reply->decoded.portnum = meshtastic_PortNum_NODEINFO_APP; reply->decoded.payload.size = pb_encode_to_bytes(reply->decoded.payload.bytes, sizeof(reply->decoded.payload.bytes), &meshtastic_User_msg, &cachedUser); reply->decoded.want_response = false; // Start from cached bitfield metadata when available. This lets direct // responses preserve more of the original packet semantics (PSRAM path), // while still enforcing local policy for OK_TO_MQTT below. if (cachedHasDecodedBitfield) reply->decoded.bitfield = cachedDecodedBitfield; else reply->decoded.bitfield = 0; // Respect the node-wide config_ok_to_mqtt setting for direct NodeInfo replies. // This response is spoofed from another node, so Router::perhapsEncode() // will not auto-populate the bitfield via config_ok_to_mqtt for us. reply->decoded.has_bitfield = true; // Update only the OK_TO_MQTT bit; keep any other cached bits intact. reply->decoded.bitfield &= ~BITFIELD_OK_TO_MQTT_MASK; if (config.lora.config_ok_to_mqtt) reply->decoded.bitfield |= BITFIELD_OK_TO_MQTT_MASK; if (hasCachedUser && cachedLastObservedMs != 0) { uint32_t ageMs = millis() - cachedLastObservedMs; TM_LOG_DEBUG("NodeInfo PSRAM hit node=0x%08x age=%lu ms src_ch=%u req_ch=%u rx_time=%lu", p->to, static_cast(ageMs), static_cast(cachedSourceChannel), static_cast(p->channel), static_cast(cachedLastObservedRxTime)); } // Spoof the sender as the target node so the requestor sees a valid NodeInfo response. // hop_limit=0 ensures this reply travels only one hop (direct to requestor). reply->from = p->to; reply->to = getFrom(p); reply->channel = p->channel; reply->decoded.request_id = p->id; reply->hop_limit = 0; // hop_start=0 is set explicitly because Router::send() only sets it for isFromUs(), // and our spoofed from means isFromUs() is false. reply->hop_start = 0; reply->next_hop = nodeDB->getLastByteOfNodeNum(getFrom(p)); reply->priority = meshtastic_MeshPacket_Priority_DEFAULT; service->sendToMesh(reply); return true; } bool TrafficManagementModule::isMinHopsFromRequestor(const meshtastic_MeshPacket *p) const { int8_t hopsAway = getHopsAway(*p, -1); if (hopsAway < 0) return false; // Both routers and clients use maxHops logic (respond when hopsAway <= threshold) // Role determines the maximum allowed value (enforced limit, not just default) bool isRouter = IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_ROUTER, meshtastic_Config_DeviceConfig_Role_ROUTER_LATE, meshtastic_Config_DeviceConfig_Role_CLIENT_BASE); uint32_t roleLimit = isRouter ? kRouterDefaultMaxHops : kClientDefaultMaxHops; uint32_t configValue = moduleConfig.traffic_management.nodeinfo_direct_response_max_hops; // Use config value if set, otherwise use role default, but always clamp to role limit uint32_t maxHops = (configValue > 0) ? configValue : roleLimit; if (maxHops > roleLimit) maxHops = roleLimit; bool result = static_cast(hopsAway) <= maxHops; TM_LOG_DEBUG("NodeInfo hops check: hopsAway=%d maxHops=%u roleLimit=%u isRouter=%d -> %s", hopsAway, maxHops, roleLimit, isRouter, result ? "respond" : "skip"); return result; } bool TrafficManagementModule::isRateLimited(NodeNum from, uint32_t nowMs) { #if TRAFFIC_MANAGEMENT_CACHE_SIZE == 0 (void)from; (void)nowMs; return false; #else const uint32_t windowMs = secsToMs(moduleConfig.traffic_management.rate_limit_window_secs); if (windowMs == 0 || moduleConfig.traffic_management.rate_limit_max_packets == 0) return false; bool isNew = false; concurrency::LockGuard guard(&cacheLock); UnifiedCacheEntry *entry = findOrCreateEntry(from, &isNew); if (!entry) return false; // Check if window has expired if (isNew || !isWithinWindow(nowMs, fromRelativeRateTime(entry->rate_time), windowMs)) { entry->rate_time = toRelativeRateTime(nowMs); entry->rate_count = 1; return false; } // Increment counter (saturates at 255) saturatingIncrement(entry->rate_count); // Check against threshold (uint8_t max is 255, but config is uint32_t) uint32_t threshold = moduleConfig.traffic_management.rate_limit_max_packets; if (threshold > 255) threshold = 255; bool limited = entry->rate_count > threshold; if (limited || entry->rate_count == threshold) { TM_LOG_DEBUG("Rate limit 0x%08x: count=%u threshold=%u -> %s", from, entry->rate_count, threshold, limited ? "DROP" : "at-limit"); } return limited; #endif } bool TrafficManagementModule::shouldDropUnknown(const meshtastic_MeshPacket *p, uint32_t nowMs) { #if TRAFFIC_MANAGEMENT_CACHE_SIZE == 0 (void)p; (void)nowMs; return false; #else if (!moduleConfig.traffic_management.drop_unknown_enabled || moduleConfig.traffic_management.unknown_packet_threshold == 0) return false; uint32_t windowMs = kUnknownResetMs; if (moduleConfig.traffic_management.rate_limit_window_secs > 0) windowMs = secsToMs(moduleConfig.traffic_management.rate_limit_window_secs); bool isNew = false; concurrency::LockGuard guard(&cacheLock); UnifiedCacheEntry *entry = findOrCreateEntry(p->from, &isNew); if (!entry) return false; // Check if window has expired if (isNew || !isWithinWindow(nowMs, fromRelativeUnknownTime(entry->unknown_time), windowMs)) { entry->unknown_time = toRelativeUnknownTime(nowMs); entry->unknown_count = 0; } // Increment counter (saturates at 255) saturatingIncrement(entry->unknown_count); // Check against threshold uint32_t threshold = moduleConfig.traffic_management.unknown_packet_threshold; if (threshold > 255) threshold = 255; bool drop = entry->unknown_count > threshold; if (drop || entry->unknown_count == threshold) { TM_LOG_DEBUG("Unknown packets 0x%08x: count=%u threshold=%u -> %s", p->from, entry->unknown_count, threshold, drop ? "DROP" : "at-limit"); } return drop; #endif } void TrafficManagementModule::logAction(const char *action, const meshtastic_MeshPacket *p, const char *reason) const { if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) { const char *name = portName(p->decoded.portnum); if (name) { TM_LOG_INFO("%s %s from=0x%08x to=0x%08x hop=%d/%d reason=%s", action, name, getFrom(p), p->to, p->hop_limit, p->hop_start, reason); } else { TM_LOG_INFO("%s port=%d from=0x%08x to=0x%08x hop=%d/%d reason=%s", action, p->decoded.portnum, getFrom(p), p->to, p->hop_limit, p->hop_start, reason); } } else { TM_LOG_INFO("%s encrypted from=0x%08x to=0x%08x hop=%d/%d reason=%s", action, getFrom(p), p->to, p->hop_limit, p->hop_start, reason); } } #endif ================================================ FILE: src/modules/TrafficManagementModule.h ================================================ #pragma once #include "MeshModule.h" #include "concurrency/Lock.h" #include "concurrency/OSThread.h" #include "mesh/generated/meshtastic/mesh.pb.h" #include "mesh/generated/meshtastic/telemetry.pb.h" #if HAS_TRAFFIC_MANAGEMENT /** * TrafficManagementModule - Packet inspection and traffic shaping for mesh networks. * * This module provides: * - Position deduplication (drop redundant position broadcasts) * - Per-node rate limiting (throttle chatty nodes) * - Unknown packet filtering (drop undecoded packets from repeat offenders) * - NodeInfo direct response (answer queries from cache to reduce mesh chatter) * - Local-only telemetry/position (exhaust hop_limit for local broadcasts) * - Router hop preservation (maintain hop_limit for router-to-router traffic) * * Memory Optimization: * Uses a unified cache with cuckoo hashing for O(1) lookups and 56% memory reduction * compared to separate per-feature caches. Timestamps are stored as 8-bit relative * offsets from a rolling epoch to further reduce memory footprint. */ class TrafficManagementModule : public MeshModule, private concurrency::OSThread { public: TrafficManagementModule(); ~TrafficManagementModule(); // Singleton — no copying or moving TrafficManagementModule(const TrafficManagementModule &) = delete; TrafficManagementModule &operator=(const TrafficManagementModule &) = delete; meshtastic_TrafficManagementStats getStats() const; void resetStats(); void recordRouterHopPreserved(); /** * Check if this packet should have its hops exhausted. * Called from perhapsRebroadcast() to force hop_limit = 0 regardless of * router_preserve_hops or favorite node logic. */ bool shouldExhaustHops(const meshtastic_MeshPacket &mp) const { return exhaustRequested && exhaustRequestedFrom == getFrom(&mp) && exhaustRequestedId == mp.id; } protected: ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; bool wantPacket(const meshtastic_MeshPacket *p) override { return true; } void alterReceived(meshtastic_MeshPacket &mp) override; int32_t runOnce() override; // Protected so test shims can force epoch rollover behavior. void resetEpoch(uint32_t nowMs); private: // ========================================================================= // Unified Cache Entry (10 bytes) - Same for ALL platforms // ========================================================================= // // A single compact structure used across ESP32, NRF52, and all other platforms. // Memory: 10 bytes × 2048 entries = 20KB // // Position Fingerprinting: // Instead of storing full coordinates (8 bytes) or a computed hash, // we store an 8-bit fingerprint derived deterministically from the // truncated lat/lon. This extracts the lower 4 significant bits from // each coordinate: fingerprint = (lat_low4 << 4) | lon_low4 // // Benefits over hash: // - Adjacent grid cells have sequential fingerprints (no collision) // - Two positions only collide if 16+ grid cells apart in BOTH dimensions // - Deterministic: same input always produces same output // // Adaptive Timestamp Resolution: // All timestamps use 8-bit values with adaptive resolution calculated // from config at startup. Resolution = max(60, min(339, interval/2)). // - Min 60 seconds ensures reasonable precision // - Max 339 seconds allows ~24 hour range (255 * 339 = 86445 sec) // - interval/2 ensures at least 2 ticks per configured interval // // Layout: // [0-3] node - NodeNum (4 bytes) // [4] pos_fingerprint - 4 bits lat + 4 bits lon (1 byte) // [5] rate_count - Packets in current window (1 byte) // [6] unknown_count - Unknown packets count (1 byte) // [7] pos_time - Position timestamp (1 byte, adaptive resolution) // [8] rate_time - Rate window start (1 byte, adaptive resolution) // [9] unknown_time - Unknown tracking start (1 byte, adaptive resolution) // struct __attribute__((packed)) UnifiedCacheEntry { NodeNum node; // 4 bytes - Node identifier (0 = empty slot) uint8_t pos_fingerprint; // 1 byte - Lower 4 bits of lat + lon uint8_t rate_count; // 1 byte - Packet count (saturates at 255) uint8_t unknown_count; // 1 byte - Unknown packet count (saturates at 255) uint8_t pos_time; // 1 byte - Position timestamp (adaptive resolution) uint8_t rate_time; // 1 byte - Rate window start (adaptive resolution) uint8_t unknown_time; // 1 byte - Unknown tracking start (adaptive resolution) }; static_assert(sizeof(UnifiedCacheEntry) == 10, "UnifiedCacheEntry should be 10 bytes"); // ========================================================================= // Cuckoo Hash Table Implementation // ========================================================================= // // Cuckoo hashing provides O(1) worst-case lookup time using two hash functions. // Each key can be in one of two possible locations (h1 or h2). On collision, // the existing entry is "kicked" to its alternate location. // // Benefits over linear scan: // - O(1) lookup vs O(n) - critical at packet processing rates // - O(1) insertion (amortized) with simple eviction on cycles // - ~95% load factor achievable // // Cache size rounds to power-of-2 for fast modulo via bitmask. // TRAFFIC_MANAGEMENT_CACHE_SIZE=2000 → cacheSize()=2048 // static constexpr uint16_t cacheSize(); static constexpr uint16_t cacheMask(); // Hash functions for cuckoo hashing inline uint16_t cuckooHash1(NodeNum node) const { return node & cacheMask(); } inline uint16_t cuckooHash2(NodeNum node) const { return ((node * 2654435769u) >> (32 - cuckooHashBits())) & cacheMask(); } static constexpr uint8_t cuckooHashBits(); // NodeInfo cache configuration (PSRAM path): // - Payload lives in PSRAM // - DRAM keeps packed 12-bit tags with 4-way bucketed cuckoo hashing // (Fan et al., CoNEXT 2014). Tag value 0 is reserved as "empty". static constexpr uint16_t kNodeInfoIndexMetadataBudgetBytes = 3072; // 3KB DRAM tag store static constexpr uint8_t kNodeInfoTargetOccupancyPercent = 95; static constexpr uint8_t kNodeInfoBucketSize = 4; static constexpr uint8_t kNodeInfoTagBits = 12; static constexpr uint16_t kNodeInfoTagMask = static_cast((1u << kNodeInfoTagBits) - 1u); static constexpr uint16_t kNodeInfoIndexSlotsRaw = static_cast((kNodeInfoIndexMetadataBudgetBytes * 8u) / kNodeInfoTagBits); static constexpr uint16_t kNodeInfoIndexSlots = static_cast(kNodeInfoIndexSlotsRaw - (kNodeInfoIndexSlotsRaw % kNodeInfoBucketSize)); static constexpr uint16_t kNodeInfoTargetEntries = static_cast((kNodeInfoIndexSlots * kNodeInfoTargetOccupancyPercent) / 100u); static_assert((kNodeInfoIndexSlots % kNodeInfoBucketSize) == 0, "NodeInfo slot count must align to bucket size"); static_assert(kNodeInfoTargetEntries < (1u << kNodeInfoTagBits), "NodeInfo tag bits must encode payload index"); static constexpr uint16_t nodeInfoTargetEntries(); static constexpr uint16_t nodeInfoIndexMetadataBudgetBytes(); static constexpr uint8_t nodeInfoTargetOccupancyPercent(); static constexpr uint8_t nodeInfoBucketSize(); static constexpr uint8_t nodeInfoTagBits(); static constexpr uint16_t nodeInfoTagMask(); static constexpr uint16_t nodeInfoIndexSlots(); static constexpr uint16_t nodeInfoBucketCount(); static constexpr uint16_t nodeInfoBucketMask(); static constexpr uint8_t nodeInfoBucketHashBits(); inline uint16_t nodeInfoHash1(NodeNum node) const { return node & nodeInfoBucketMask(); } inline uint16_t nodeInfoHash2(NodeNum node) const { return ((node * 2246822519u) >> (32 - nodeInfoBucketHashBits())) & nodeInfoBucketMask(); } // ========================================================================= // Adaptive Timestamp Resolution // ========================================================================= // // All timestamps use 8-bit values with adaptive resolution calculated from // config at startup. This allows ~24 hour range while maintaining precision. // // Resolution formula: max(60, min(339, interval/2)) // - 60 sec minimum ensures reasonable precision // - 339 sec maximum allows 24 hour range (255 * 339 ≈ 86400 sec) // - interval/2 ensures at least 2 ticks per configured interval // // Since config changes require reboot, resolution is calculated once. // uint32_t cacheEpochMs = 0; uint16_t posTimeResolution = 60; // Seconds per tick for position uint16_t rateTimeResolution = 60; // Seconds per tick for rate limiting uint16_t unknownTimeResolution = 60; // Seconds per tick for unknown tracking // Calculate resolution from configured interval (called once at startup) static uint16_t calcTimeResolution(uint32_t intervalSecs) { // Resolution = interval/2 to ensure at least 2 ticks per interval // Clamped to [60, 339] for min precision and max 24h range uint32_t res = (intervalSecs > 0) ? (intervalSecs / 2) : 60; if (res < 60) res = 60; if (res > 339) res = 339; return static_cast(res); } // Convert to/from 8-bit relative timestamps with given resolution uint8_t toRelativeTime(uint32_t nowMs, uint16_t resolutionSecs) const { uint32_t ticks = (nowMs - cacheEpochMs) / (resolutionSecs * 1000UL); return (ticks > UINT8_MAX) ? UINT8_MAX : static_cast(ticks); } uint32_t fromRelativeTime(uint8_t ticks, uint16_t resolutionSecs) const { return cacheEpochMs + (static_cast(ticks) * resolutionSecs * 1000UL); } // Convenience wrappers for each timestamp type uint8_t toRelativePosTime(uint32_t nowMs) const { return toRelativeTime(nowMs, posTimeResolution); } uint32_t fromRelativePosTime(uint8_t t) const { return fromRelativeTime(t, posTimeResolution); } uint8_t toRelativeRateTime(uint32_t nowMs) const { return toRelativeTime(nowMs, rateTimeResolution); } uint32_t fromRelativeRateTime(uint8_t t) const { return fromRelativeTime(t, rateTimeResolution); } uint8_t toRelativeUnknownTime(uint32_t nowMs) const { return toRelativeTime(nowMs, unknownTimeResolution); } uint32_t fromRelativeUnknownTime(uint8_t t) const { return fromRelativeTime(t, unknownTimeResolution); } // Epoch reset when any timestamp approaches overflow // With max resolution of 339 sec, 200 ticks = ~19 hours (safe margin for 24h max) bool needsEpochReset(uint32_t nowMs) const { uint16_t maxRes = posTimeResolution; if (rateTimeResolution > maxRes) maxRes = rateTimeResolution; if (unknownTimeResolution > maxRes) maxRes = unknownTimeResolution; return (nowMs - cacheEpochMs) > (200UL * maxRes * 1000UL); } // ========================================================================= // Position Fingerprint // ========================================================================= // // Computes 8-bit fingerprint from truncated lat/lon coordinates. // Extracts lower 4 significant bits from each coordinate. // // fingerprint = (lat_low4 << 4) | lon_low4 // // Unlike a hash, adjacent grid cells have sequential fingerprints, // so nearby positions never collide. Collisions only occur for // positions 16+ grid cells apart in both dimensions. // // Guards: If precision < 4 bits, uses min(precision, 4) bits. // static uint8_t computePositionFingerprint(int32_t lat_truncated, int32_t lon_truncated, uint8_t precision); // ========================================================================= // Cache Storage // ========================================================================= mutable concurrency::Lock cacheLock; // Protects all cache access UnifiedCacheEntry *cache = nullptr; // Cuckoo hash table (unified for all platforms) bool cacheFromPsram = false; // Tracks allocator for correct deallocation struct NodeInfoPayloadEntry { // Node identifier associated with this payload slot. // 0 means the slot is currently unused. NodeNum node; // Cached NODEINFO_APP payload body. This is separate from NodeDB and is only // used by the PSRAM-backed direct-response path in this module. meshtastic_User user; // Extra response metadata captured from the latest observed NODEINFO_APP // packet for this node. shouldRespondToNodeInfo() uses this metadata when // building spoofed replies for requesting clients. // Last local uptime tick (millis) when this entry was refreshed. uint32_t lastObservedMs; // Last RTC/packet timestamp (seconds) observed for this NodeInfo frame. // If unavailable in packet, remains 0. uint32_t lastObservedRxTime; // Channel where we most recently heard this node's NodeInfo. uint8_t sourceChannel; // Cached decoded bitfield metadata from the source packet. // We preserve non-OK_TO_MQTT bits in direct replies when available. bool hasDecodedBitfield; uint8_t decodedBitfield; }; NodeInfoPayloadEntry *nodeInfoPayload = nullptr; // NodeInfo payloads in PSRAM bool nodeInfoPayloadFromPsram = false; // Tracks allocator for correct deallocation uint8_t *nodeInfoIndex = nullptr; // Packed 12-bit NodeInfo tags in DRAM uint16_t nodeInfoAllocHint = 0; uint16_t nodeInfoEvictCursor = 0; meshtastic_TrafficManagementStats stats; // Flag set during alterReceived() when packet should be exhausted. // Checked by perhapsRebroadcast() to force hop_limit = 0 only for the // matching packet key (from + id). Reset at start of handleReceived(). bool exhaustRequested = false; NodeNum exhaustRequestedFrom = 0; PacketId exhaustRequestedId = 0; // ========================================================================= // Cache Operations // ========================================================================= // Find or create entry for node using cuckoo hashing // Returns nullptr if cache is full and eviction fails UnifiedCacheEntry *findOrCreateEntry(NodeNum node, bool *isNew); // Find existing entry (no creation) UnifiedCacheEntry *findEntry(NodeNum node); // NodeInfo cache operations (bucketed cuckoo index + PSRAM payloads) const NodeInfoPayloadEntry *findNodeInfoEntry(NodeNum node) const; NodeInfoPayloadEntry *findOrCreateNodeInfoEntry(NodeNum node, bool *usedEmptySlot); uint16_t findNodeInfoPayloadIndex(NodeNum node) const; bool removeNodeInfoIndexEntry(NodeNum node, uint16_t payloadIndex); uint16_t allocateNodeInfoPayloadSlot(); uint16_t evictNodeInfoPayloadSlot(); bool tryInsertNodeInfoEntryInBucket(uint16_t bucket, uint16_t tag); uint16_t encodeNodeInfoTag(uint16_t payloadIndex) const; uint16_t decodeNodeInfoPayloadIndex(uint16_t tag) const; uint16_t getNodeInfoTag(uint16_t slot) const; void setNodeInfoTag(uint16_t slot, uint16_t tag); uint16_t countNodeInfoEntriesLocked() const; void cacheNodeInfoPacket(const meshtastic_MeshPacket &mp); // ========================================================================= // Traffic Management Logic // ========================================================================= bool shouldDropPosition(const meshtastic_MeshPacket *p, const meshtastic_Position *pos, uint32_t nowMs); bool shouldRespondToNodeInfo(const meshtastic_MeshPacket *p, bool sendResponse); bool isMinHopsFromRequestor(const meshtastic_MeshPacket *p) const; bool isRateLimited(NodeNum from, uint32_t nowMs); bool shouldDropUnknown(const meshtastic_MeshPacket *p, uint32_t nowMs); void logAction(const char *action, const meshtastic_MeshPacket *p, const char *reason) const; void incrementStat(uint32_t *field); }; // ========================================================================= // Compile-time Cache Size Calculations // ========================================================================= // // Round TRAFFIC_MANAGEMENT_CACHE_SIZE up to next power of 2 for efficient // cuckoo hash indexing (allows bitmask instead of modulo). // // These use C++11-compatible constexpr (single return statement). // namespace detail { // Helper: round up to next power of 2 using bit manipulation constexpr uint16_t nextPow2(uint16_t n) { return n == 0 ? 0 : (((n - 1) | ((n - 1) >> 1) | ((n - 1) >> 2) | ((n - 1) >> 4) | ((n - 1) >> 8)) + 1); } // Helper: floor(log2(n)) for n >= 0, C++11-compatible constexpr. constexpr uint8_t log2Floor(uint16_t n) { return n <= 1 ? 0 : static_cast(1 + log2Floor(static_cast(n >> 1))); } // Helper: ceil(log2(n)) for n >= 1, C++11-compatible constexpr. constexpr uint8_t log2Ceil(uint16_t n) { return n <= 1 ? 0 : static_cast(1 + log2Floor(static_cast(n - 1))); } } // namespace detail constexpr uint16_t TrafficManagementModule::cacheSize() { return detail::nextPow2(TRAFFIC_MANAGEMENT_CACHE_SIZE); } constexpr uint16_t TrafficManagementModule::cacheMask() { return cacheSize() > 0 ? cacheSize() - 1 : 0; } constexpr uint8_t TrafficManagementModule::cuckooHashBits() { return detail::log2Floor(cacheSize()); } constexpr uint16_t TrafficManagementModule::nodeInfoTargetEntries() { return kNodeInfoTargetEntries; } constexpr uint16_t TrafficManagementModule::nodeInfoIndexMetadataBudgetBytes() { return kNodeInfoIndexMetadataBudgetBytes; } constexpr uint8_t TrafficManagementModule::nodeInfoTargetOccupancyPercent() { return kNodeInfoTargetOccupancyPercent; } constexpr uint8_t TrafficManagementModule::nodeInfoBucketSize() { return kNodeInfoBucketSize; } constexpr uint8_t TrafficManagementModule::nodeInfoTagBits() { return kNodeInfoTagBits; } constexpr uint16_t TrafficManagementModule::nodeInfoTagMask() { return kNodeInfoTagMask; } constexpr uint16_t TrafficManagementModule::nodeInfoIndexSlots() { return kNodeInfoIndexSlots; } constexpr uint16_t TrafficManagementModule::nodeInfoBucketCount() { return static_cast(nodeInfoIndexSlots() / nodeInfoBucketSize()); } constexpr uint16_t TrafficManagementModule::nodeInfoBucketMask() { return nodeInfoBucketCount() > 0 ? nodeInfoBucketCount() - 1 : 0; } constexpr uint8_t TrafficManagementModule::nodeInfoBucketHashBits() { return detail::log2Floor(nodeInfoBucketCount()); } extern TrafficManagementModule *trafficManagementModule; #endif ================================================ FILE: src/modules/WaypointModule.cpp ================================================ #include "WaypointModule.h" #include "NodeDB.h" #include "PowerFSM.h" #include "configuration.h" #include "graphics/SharedUIDisplay.h" #include "graphics/draw/CompassRenderer.h" #if HAS_SCREEN #include "gps/RTC.h" #include "graphics/Screen.h" #include "graphics/TimeFormatters.h" #include "graphics/draw/NodeListRenderer.h" #include "main.h" #endif WaypointModule *waypointModule; static inline float degToRad(float deg) { return deg * PI / 180.0f; } static inline float radToDeg(float rad) { return rad * 180.0f / PI; } ProcessMessage WaypointModule::handleReceived(const meshtastic_MeshPacket &mp) { #if defined(DEBUG_PORT) && !defined(DEBUG_MUTE) auto &p = mp.decoded; LOG_INFO("Received waypoint msg from=0x%0x, id=0x%x, msg=%.*s", mp.from, mp.id, p.payload.size, p.payload.bytes); #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 (!screen || !devicestate.has_rx_waypoint) return false; meshtastic_Waypoint wp{}; // <- replaces memset if (pb_decode_from_bytes(devicestate.rx_waypoint.decoded.payload.bytes, devicestate.rx_waypoint.decoded.payload.size, &meshtastic_Waypoint_msg, &wp)) { return wp.expire > getTime(); } return false; // no LOG_ERROR, no flag writes #else return false; #endif } /// Draw the last waypoint we received void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { if (!screen) return; display->clear(); display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); int line = 1; // === Set Title const char *titleStr = "Waypoint"; // === Header === graphics::drawCommonHeader(display, x, y, titleStr); const int w = display->getWidth(); const int h = display->getHeight(); // Decode the waypoint const meshtastic_MeshPacket &mp = devicestate.rx_waypoint; meshtastic_Waypoint wp{}; if (!pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, &meshtastic_Waypoint_msg, &wp)) { devicestate.has_rx_waypoint = false; return; } // Get timestamp info. Will pass as a field to drawColumns char lastStr[20]; getTimeAgoStr(sinceReceived(&mp), lastStr, sizeof(lastStr)); // Will contain distance information, passed as a field to drawColumns char distStr[20]; // Get our node, to use our own position meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); // Dimensions / co-ordinates for the compass/circle const uint16_t compassDiam = graphics::CompassRenderer::getCompassDiam(w, h); const int16_t compassX = x + w - (compassDiam / 2) - 5; const int16_t compassY = (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) ? y + h / 2 : y + FONT_HEIGHT_SMALL + (h - FONT_HEIGHT_SMALL) / 2; // If our node has a position: if (ourNode && (nodeDB->hasValidPosition(ourNode) || screen->hasHeading())) { const meshtastic_PositionLite &op = ourNode->position; float myHeading; if (uiconfig.compass_mode == meshtastic_CompassMode_FREEZE_HEADING) { myHeading = 0; } else { if (screen->hasHeading()) myHeading = degToRad(screen->getHeading()); else myHeading = screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); } graphics::CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading, (compassDiam / 2)); // Compass bearing to waypoint float bearingToOther = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(wp.latitude_i), DegD(wp.longitude_i)); // If the top of the compass is a static north then bearingToOther can be drawn on the compass directly // If the top of the compass is not a static north we need adjust bearingToOther based on heading if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING) bearingToOther -= myHeading; graphics::CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, bearingToOther); float bearingToOtherDegrees = (bearingToOther < 0) ? bearingToOther + 2 * PI : bearingToOther; bearingToOtherDegrees = radToDeg(bearingToOtherDegrees); // Distance to Waypoint float d = GeoCoord::latLongToMeter(DegD(wp.latitude_i), DegD(wp.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i)); if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { float feet = d * METERS_TO_FEET; snprintf(distStr, sizeof(distStr), feet < (2 * MILES_TO_FEET) ? "%.0fft %.0f°" : "%.1fmi %.0f°", feet < (2 * MILES_TO_FEET) ? feet : feet / MILES_TO_FEET, bearingToOtherDegrees); } else { snprintf(distStr, sizeof(distStr), d < 2000 ? "%.0fm %.0f°" : "%.1fkm %.0f°", d < 2000 ? d : d / 1000, bearingToOtherDegrees); } } else { display->drawString(compassX - FONT_HEIGHT_SMALL / 4, compassY - FONT_HEIGHT_SMALL / 2, "?"); // ? in the distance field snprintf(distStr, sizeof(distStr), "? %s ?°", (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) ? "mi" : "km"); } // Draw compass circle display->drawCircle(compassX, compassY, compassDiam / 2); display->setTextAlignment(TEXT_ALIGN_LEFT); // Something above me changes to a different alignment, forcing a fix here! display->drawString(0, graphics::getTextPositions(display)[line++], lastStr); display->drawString(0, graphics::getTextPositions(display)[line++], wp.name); display->drawString(0, graphics::getTextPositions(display)[line++], wp.description); display->drawString(0, graphics::getTextPositions(display)[line++], distStr); } #endif ================================================ FILE: src/modules/WaypointModule.h ================================================ #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; ================================================ FILE: src/modules/esp32/AudioModule.cpp ================================================ #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("Start 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("Send %d codec2 bytes", audioModule->encode_frame_size); audioModule->sendPayload(); audioModule->tx_encode_frame_index = sizeof(audioModule->tx_header); } } if (audioModule->radio_state == RadioState::rx) { size_t bytesOut = 0; if (memcmp(audioModule->rx_encode_frame, &audioModule->tx_header, sizeof(audioModule->tx_header)) == 0) { for (int i = 4; i < audioModule->rx_encode_frame_index; i += audioModule->encode_codec_size) { codec2_decode(audioModule->codec2, audioModule->output_buffer, audioModule->rx_encode_frame + i); i2s_write(I2S_PORT, &audioModule->output_buffer, audioModule->adc_buffer_size, &bytesOut, pdMS_TO_TICKS(500)); } } else { // if the buffer header does not match our own codec, make a temp decoding setup. CODEC2 *tmp_codec2 = codec2_create(audioModule->rx_encode_frame[3]); codec2_set_lpc_post_filter(tmp_codec2, 1, 0, 0.8, 0.2); int tmp_encode_codec_size = (codec2_bits_per_frame(tmp_codec2) + 7) / 8; int tmp_adc_buffer_size = codec2_samples_per_frame(tmp_codec2); for (int i = 4; i < audioModule->rx_encode_frame_index; i += tmp_encode_codec_size) { codec2_decode(tmp_codec2, audioModule->output_buffer, audioModule->rx_encode_frame + i); i2s_write(I2S_PORT, &audioModule->output_buffer, tmp_adc_buffer_size, &bytesOut, pdMS_TO_TICKS(500)); } codec2_destroy(tmp_codec2); } } } } } AudioModule::AudioModule() : SinglePortModule("Audio", meshtastic_PortNum_AUDIO_APP), concurrency::OSThread("Audio") { // moduleConfig.audio.codec2_enabled = true; // moduleConfig.audio.i2s_ws = 13; // moduleConfig.audio.i2s_sd = 15; // moduleConfig.audio.i2s_din = 22; // moduleConfig.audio.i2s_sck = 14; // moduleConfig.audio.ptt_pin = 39; if ((moduleConfig.audio.codec2_enabled) && (myRegion->profile->audioPermitted)) { LOG_INFO("Set up codec2 in mode %u", (moduleConfig.audio.bitrate ? moduleConfig.audio.bitrate : AUDIO_MODULE_MODE) - 1); codec2 = codec2_create((moduleConfig.audio.bitrate ? moduleConfig.audio.bitrate : AUDIO_MODULE_MODE) - 1); memcpy(tx_header.magic, c2_magic, sizeof(c2_magic)); tx_header.mode = (moduleConfig.audio.bitrate ? moduleConfig.audio.bitrate : AUDIO_MODULE_MODE) - 1; codec2_set_lpc_post_filter(codec2, 1, 0, 0.8, 0.2); encode_codec_size = (codec2_bits_per_frame(codec2) + 7) / 8; encode_frame_num = (meshtastic_Constants_DATA_PAYLOAD_LEN - sizeof(tx_header)) / encode_codec_size; encode_frame_size = encode_frame_num * encode_codec_size; // max 233 bytes + 4 header bytes adc_buffer_size = codec2_samples_per_frame(codec2); LOG_INFO("Use %d frames of %d bytes for a total payload length of %d bytes", encode_frame_num, encode_codec_size, encode_frame_size); xTaskCreate(&run_codec2, "codec2_task", 30000, NULL, 5, &codec2HandlerTask); } else { disable(); } } void AudioModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { char buffer[50]; display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); display->fillRect(0 + x, 0 + y, x + display->getWidth(), y + FONT_HEIGHT_SMALL); display->setColor(BLACK); display->drawStringf(0 + x, 0 + y, buffer, "Codec2 Mode %d Audio", (moduleConfig.audio.bitrate ? moduleConfig.audio.bitrate : AUDIO_MODULE_MODE) - 1); display->setColor(WHITE); display->setFont(FONT_LARGE); display->setTextAlignment(TEXT_ALIGN_CENTER); switch (radio_state) { case RadioState::tx: display->drawString(display->getWidth() / 2 + x, (display->getHeight() - FONT_HEIGHT_SMALL) / 2 + y, "PTT"); break; default: display->drawString(display->getWidth() / 2 + x, (display->getHeight() - FONT_HEIGHT_SMALL) / 2 + y, "Receive"); break; } } int32_t AudioModule::runOnce() { if ((moduleConfig.audio.codec2_enabled) && (myRegion->profile->audioPermitted)) { esp_err_t res; if (firstTime) { // Set up I2S Processor configuration. This will produce 16bit samples at 8 kHz instead of 12 from the ADC LOG_INFO("Init I2S SD: %d DIN: %d WS: %d SCK: %d", moduleConfig.audio.i2s_sd, moduleConfig.audio.i2s_din, moduleConfig.audio.i2s_ws, moduleConfig.audio.i2s_sck); i2s_config_t i2s_config = {.mode = (i2s_mode_t)(I2S_MODE_MASTER | (moduleConfig.audio.i2s_sd ? I2S_MODE_RX : 0) | (moduleConfig.audio.i2s_din ? I2S_MODE_TX : 0)), .sample_rate = 8000, .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT, .communication_format = (i2s_comm_format_t)(I2S_COMM_FORMAT_STAND_I2S), .intr_alloc_flags = 0, .dma_buf_count = 8, .dma_buf_len = adc_buffer_size, // 320 * 2 bytes .use_apll = false, .tx_desc_auto_clear = true, .fixed_mclk = 0}; res = i2s_driver_install(I2S_PORT, &i2s_config, 0, NULL); if (res != ESP_OK) { LOG_ERROR("Failed to install I2S driver: %d", res); } const i2s_pin_config_t pin_config = { .bck_io_num = moduleConfig.audio.i2s_sck, .ws_io_num = moduleConfig.audio.i2s_ws, .data_out_num = moduleConfig.audio.i2s_din ? moduleConfig.audio.i2s_din : I2S_PIN_NO_CHANGE, .data_in_num = moduleConfig.audio.i2s_sd ? moduleConfig.audio.i2s_sd : I2S_PIN_NO_CHANGE}; res = i2s_set_pin(I2S_PORT, &pin_config); if (res != ESP_OK) { LOG_ERROR("Failed to set I2S pin config: %d", res); } res = i2s_start(I2S_PORT); if (res != ESP_OK) { LOG_ERROR("Failed to start I2S: %d", res); } radio_state = RadioState::rx; // Configure PTT input LOG_INFO("Init PTT on Pin %u", moduleConfig.audio.ptt_pin ? moduleConfig.audio.ptt_pin : PTT_PIN); pinMode(moduleConfig.audio.ptt_pin ? moduleConfig.audio.ptt_pin : PTT_PIN, INPUT); firstTime = false; } else { UIFrameEvent e; // Check if PTT is pressed. TODO hook that into Onebutton/Interrupt drive. if (digitalRead(moduleConfig.audio.ptt_pin ? moduleConfig.audio.ptt_pin : PTT_PIN) == HIGH) { if (radio_state == RadioState::rx) { LOG_INFO("PTT pressed, switching to TX"); radio_state = RadioState::tx; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; // We want to change the list of frames shown on-screen this->notifyObservers(&e); } } else { if (radio_state == RadioState::tx) { LOG_INFO("PTT released, switching to RX"); if (tx_encode_frame_index > sizeof(tx_header)) { // Send the incomplete frame LOG_INFO("Send %d codec2 bytes (incomplete)", tx_encode_frame_index); sendPayload(); } tx_encode_frame_index = sizeof(tx_header); radio_state = RadioState::rx; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; // We want to change the list of frames shown on-screen this->notifyObservers(&e); } } if (radio_state == RadioState::tx) { // Get I2S data from the microphone and place in data buffer size_t bytesIn = 0; res = i2s_read(I2S_PORT, adc_buffer + adc_buffer_index, adc_buffer_size - adc_buffer_index, &bytesIn, pdMS_TO_TICKS(40)); // wait 40ms for audio to arrive. if (res == ESP_OK) { adc_buffer_index += bytesIn; if (adc_buffer_index == adc_buffer_size) { adc_buffer_index = 0; memcpy((void *)speech, (void *)adc_buffer, 2 * adc_buffer_size); // Notify run_codec2 task that the buffer is ready. radio_state = RadioState::tx; BaseType_t xHigherPriorityTaskWoken = pdFALSE; vTaskNotifyGiveFromISR(codec2HandlerTask, &xHigherPriorityTaskWoken); if (xHigherPriorityTaskWoken == pdTRUE) YIELD_FROM_ISR(xHigherPriorityTaskWoken); } } } } return 100; } else { return disable(); } } meshtastic_MeshPacket *AudioModule::allocReply() { auto reply = allocDataPacket(); return reply; } bool AudioModule::shouldDraw() { if (!moduleConfig.audio.codec2_enabled) { return false; } return (radio_state == RadioState::tx); } void AudioModule::sendPayload(NodeNum dest, bool wantReplies) { meshtastic_MeshPacket *p = allocReply(); p->to = dest; p->decoded.want_response = wantReplies; p->want_ack = false; // Audio is shoot&forget. No need to wait for ACKs. p->priority = meshtastic_MeshPacket_Priority_MAX; // Audio is important, because realtime p->decoded.payload.size = tx_encode_frame_index; memcpy(p->decoded.payload.bytes, tx_encode_frame, p->decoded.payload.size); service->sendToMesh(p); } ProcessMessage AudioModule::handleReceived(const meshtastic_MeshPacket &mp) { if ((moduleConfig.audio.codec2_enabled) && (myRegion->profile->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 ================================================ FILE: src/modules/esp32/AudioModule.h ================================================ #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 ================================================ FILE: src/modules/esp32/PaxcounterModule.cpp ================================================ #include "configuration.h" #if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_PAXCOUNTER #include "Default.h" #include "MeshService.h" #include "PaxcounterModule.h" #include "graphics/ScreenFonts.h" #include "graphics/SharedUIDisplay.h" #include "graphics/images.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("Paxcounter"), 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: send pax info wifi=%d; ble=%d; uptime=%lu", count_from_libpax.wifi_count, count_from_libpax.ble_count, millis() / 1000); meshtastic_Paxcount pl = meshtastic_Paxcount_init_default; pl.wifi = count_from_libpax.wifi_count; pl.ble = count_from_libpax.ble_count; pl.uptime = millis() / 1000; meshtastic_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, Default::getConfiguredOrDefault(moduleConfig.paxcounter.paxcounter_update_interval, default_telemetry_broadcast_interval_secs), 0); libpax_counter_start(); } else { sendInfo(NODENUM_BROADCAST); } return Default::getConfiguredOrDefaultMsScaled(moduleConfig.paxcounter.paxcounter_update_interval, default_telemetry_broadcast_interval_secs, numOnlineNodes); } else { return disable(); } } #if HAS_SCREEN #include "graphics/ScreenFonts.h" #include "graphics/SharedUIDisplay.h" void PaxcounterModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { display->clear(); display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); int line = 1; // === Set Title const char *titleStr = "Pax"; // === Header === graphics::drawCommonHeader(display, x, y, titleStr); char buffer[50]; display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); libpax_counter_count(&count_from_libpax); display->setTextAlignment(TEXT_ALIGN_CENTER); display->setFont(FONT_SMALL); display->drawStringf(display->getWidth() / 2 + x, graphics::getTextPositions(display)[line++], buffer, "WiFi: %d\nBLE: %d\nUptime: %ds", count_from_libpax.wifi_count, count_from_libpax.ble_count, millis() / 1000); graphics::drawCommonFooter(display, x, y); } #endif // HAS_SCREEN #endif ================================================ FILE: src/modules/esp32/PaxcounterModule.h ================================================ #pragma once #include "ProtobufModule.h" #include "configuration.h" #if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_PAXCOUNTER #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 ================================================ FILE: src/motion/AccelerometerThread.h ================================================ #pragma once #ifndef _ACCELEROMETER_H_ #define _ACCELEROMETER_H_ #include "configuration.h" #if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C #include "../concurrency/OSThread.h" #ifdef HAS_BMA423 #include "BMA423Sensor.h" #endif #ifdef HAS_BMI270 #include "BMI270Sensor.h" #endif #include "BMM150Sensor.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 #ifdef HAS_STK8XXX #include "STK8XXXSensor.h" #endif extern ScanI2C::DeviceAddress accelerometer_found; class AccelerometerThread : public concurrency::OSThread { private: MotionSensor *sensor = nullptr; bool isInitialised = false; public: explicit AccelerometerThread(ScanI2C::FoundDevice foundDevice) : OSThread("Accelerometer") { device = foundDevice; init(); } explicit AccelerometerThread(ScanI2C::DeviceType type) : AccelerometerThread(ScanI2C::FoundDevice{type, accelerometer_found}) { } void start() { init(); setIntervalFromNow(0); }; void calibrate(uint16_t forSeconds) { if (sensor) { sensor->calibrate(forSeconds); } } protected: int32_t runOnce() override { // Assume we should not keep the board awake canSleep = true; if (isInitialised) return sensor->runOnce(); return MOTION_SENSOR_CHECK_INTERVAL_MS; } private: ScanI2C::FoundDevice device; void init() { if (isInitialised) return; if (device.address.port == ScanI2C::I2CPort::NO_I2C || device.address.address == 0 || device.type == ScanI2C::NONE) { LOG_DEBUG("AccelerometerThread Disable due to no sensors found"); disable(); return; } switch (device.type) { #ifdef HAS_BMA423 case ScanI2C::DeviceType::BMA423: sensor = new BMA423Sensor(device); break; #endif case ScanI2C::DeviceType::MPU6050: sensor = new MPU6050Sensor(device); break; case ScanI2C::DeviceType::BMX160: sensor = new BMX160Sensor(device); break; case ScanI2C::DeviceType::LIS3DH: sensor = new LIS3DHSensor(device); break; case ScanI2C::DeviceType::LSM6DS3: sensor = new LSM6DS3Sensor(device); break; #ifdef HAS_STK8XXX case ScanI2C::DeviceType::STK8BAXX: sensor = new STK8XXXSensor(device); break; #endif case ScanI2C::DeviceType::ICM20948: sensor = new ICM20948Sensor(device); break; case ScanI2C::DeviceType::BMM150: sensor = new BMM150Sensor(device); break; #ifdef HAS_BMI270 case ScanI2C::DeviceType::BMI270: sensor = new BMI270Sensor(device); break; #endif #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("Accelerometer") { this->copy(other); } // Destructor (included to avoid cppcheck warnings) virtual ~AccelerometerThread() { clean(); } // Copy assignment (not implemented / included to avoid cppcheck warnings) AccelerometerThread &operator=(const AccelerometerThread &other) { this->copy(other); return *this; } // Take a very shallow copy (does not copy OSThread state nor the sensor object) // If for some reason this is ever used, make sure to call init() after any copy void copy(const AccelerometerThread &other) { if (this != &other) { clean(); this->device = ScanI2C::FoundDevice(other.device.type, ScanI2C::DeviceAddress(other.device.address.port, other.device.address.address)); } } // Cleanup resources void clean() { isInitialised = false; delete sensor; sensor = nullptr; } }; #endif #endif ================================================ FILE: src/motion/BMA423Sensor.cpp ================================================ #include "BMA423Sensor.h" #if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && defined(HAS_BMA423) && __has_include() BMA423Sensor::BMA423Sensor(ScanI2C::FoundDevice foundDevice) : MotionSensor::MotionSensor(foundDevice) {} bool BMA423Sensor::init() { if (sensor.begin(Wire, deviceAddress())) { sensor.configAccelerometer(sensor.RANGE_2G, sensor.ODR_100HZ, sensor.BW_NORMAL_AVG4, sensor.PERF_CONTINUOUS_MODE); sensor.enableAccelerometer(); sensor.configInterrupt(); #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.setRemapAxes(sensor.REMAP_TOP_LAYER_RIGHT_CORNER); #else sensor.setRemapAxes(sensor.REMAP_BOTTOM_LAYER_BOTTOM_LEFT_CORNER); #endif // sensor.enableFeature(sensor.FEATURE_STEP_CNTR, true); sensor.enableFeature(sensor.FEATURE_TILT, true); sensor.enableFeature(sensor.FEATURE_WAKEUP, true); // sensor.resetPedometer(); // Turn on feature interrupt sensor.enablePedometerIRQ(); sensor.enableTiltIRQ(); // It corresponds to isDoubleClick interrupt sensor.enableWakeupIRQ(); LOG_DEBUG("BMA423 init ok"); return true; } LOG_DEBUG("BMA423 init failed"); return false; } int32_t BMA423Sensor::runOnce() { if (sensor.readIrqStatus()) { if (sensor.isTilt() || sensor.isDoubleTap()) { wakeScreen(); return 500; } } return MOTION_SENSOR_CHECK_INTERVAL_MS; } #endif ================================================ FILE: src/motion/BMA423Sensor.h ================================================ #pragma once #ifndef _BMA423_SENSOR_H_ #define _BMA423_SENSOR_H_ #include "MotionSensor.h" #if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && defined(HAS_BMA423) && __has_include() #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 ================================================ FILE: src/motion/BMI270Sensor.cpp ================================================ #include "BMI270Sensor.h" #if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && defined(HAS_BMI270) #include // BMI270 registers used #define BMI270_REG_ACC_X_LSB 0x0C #define BMI270_REG_INTERNAL_STATUS 0x21 #define BMI270_REG_ACC_CONF 0x40 #define BMI270_REG_ACC_RANGE 0x41 #define BMI270_REG_INIT_CTRL 0x59 #define BMI270_REG_INIT_ADDR_0 0x5B #define BMI270_REG_INIT_ADDR_1 0x5C #define BMI270_REG_INIT_DATA 0x5E #define BMI270_REG_PWR_CONF 0x7C #define BMI270_REG_PWR_CTRL 0x7D #define BMI270_REG_CMD 0x7E // Commands and configuration values #define BMI270_CMD_SOFTRESET 0xB6 #define BMI270_PWR_CONF_ADV_POWER_SAVE_DISABLED 0x00 #define BMI270_PWR_CTRL_ACC_EN 0x04 #define BMI270_ACC_ODR_50HZ 0x07 #define BMI270_ACC_BWP_NORMAL 0x20 #define BMI270_ACC_FILTER_PERF 0x80 #define BMI270_ACC_RANGE_2G 0x00 #define BMI270_INIT_OK 0x01 // BMI270 config file - 8192 bytes from official Bosch BMI270 API static const uint8_t bmi270_config_file[] PROGMEM = { 0xc8, 0x2e, 0x00, 0x2e, 0x80, 0x2e, 0x3d, 0xb1, 0xc8, 0x2e, 0x00, 0x2e, 0x80, 0x2e, 0x91, 0x03, 0x80, 0x2e, 0xbc, 0xb0, 0x80, 0x2e, 0xa3, 0x03, 0xc8, 0x2e, 0x00, 0x2e, 0x80, 0x2e, 0x00, 0xb0, 0x50, 0x30, 0x21, 0x2e, 0x59, 0xf5, 0x10, 0x30, 0x21, 0x2e, 0x6a, 0xf5, 0x80, 0x2e, 0x3b, 0x03, 0x00, 0x00, 0x00, 0x00, 0x08, 0x19, 0x01, 0x00, 0x22, 0x00, 0x75, 0x00, 0x00, 0x10, 0x00, 0x10, 0xd1, 0x00, 0xb3, 0x43, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0xe0, 0x5f, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x92, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x19, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0xe0, 0xaa, 0x38, 0x05, 0xe0, 0x90, 0x30, 0xfa, 0x00, 0x96, 0x00, 0x4b, 0x09, 0x11, 0x00, 0x11, 0x00, 0x02, 0x00, 0x2d, 0x01, 0xd4, 0x7b, 0x3b, 0x01, 0xdb, 0x7a, 0x04, 0x00, 0x3f, 0x7b, 0xcd, 0x6c, 0xc3, 0x04, 0x85, 0x09, 0xc3, 0x04, 0xec, 0xe6, 0x0c, 0x46, 0x01, 0x00, 0x27, 0x00, 0x19, 0x00, 0x96, 0x00, 0xa0, 0x00, 0x01, 0x00, 0x0c, 0x00, 0xf0, 0x3c, 0x00, 0x01, 0x01, 0x00, 0x03, 0x00, 0x01, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x32, 0x00, 0x05, 0x00, 0xee, 0x06, 0x04, 0x00, 0xc8, 0x00, 0x00, 0x00, 0x04, 0x00, 0xa8, 0x05, 0xee, 0x06, 0x00, 0x04, 0xbc, 0x02, 0xb3, 0x00, 0x85, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb4, 0x00, 0x01, 0x00, 0xb9, 0x00, 0x01, 0x00, 0x98, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x80, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x2e, 0x00, 0xc1, 0xfd, 0x2d, 0xde, 0x00, 0xeb, 0x00, 0xda, 0x00, 0x00, 0x0c, 0xff, 0x0f, 0x00, 0x04, 0xc0, 0x00, 0x5b, 0xf5, 0xc9, 0x01, 0x1e, 0xf2, 0x80, 0x00, 0x3f, 0xff, 0x19, 0xf4, 0x58, 0xf5, 0x66, 0xf5, 0x64, 0xf5, 0xc0, 0xf1, 0xf0, 0x00, 0xe0, 0x00, 0xcd, 0x01, 0xd3, 0x01, 0xdb, 0x01, 0xff, 0x7f, 0xff, 0x01, 0xe4, 0x00, 0x74, 0xf7, 0xf3, 0x00, 0xfa, 0x00, 0xff, 0x3f, 0xca, 0x03, 0x6c, 0x38, 0x56, 0xfe, 0x44, 0xfd, 0xbc, 0x02, 0xf9, 0x06, 0x00, 0xfc, 0x12, 0x02, 0xae, 0x01, 0x58, 0xfa, 0x9a, 0xfd, 0x77, 0x05, 0xbb, 0x02, 0x96, 0x01, 0x95, 0x01, 0x7f, 0x01, 0x82, 0x01, 0x89, 0x01, 0x87, 0x01, 0x88, 0x01, 0x8a, 0x01, 0x8c, 0x01, 0x8f, 0x01, 0x8d, 0x01, 0x92, 0x01, 0x91, 0x01, 0xdd, 0x00, 0x9f, 0x01, 0x7e, 0x01, 0xdb, 0x00, 0xb6, 0x01, 0x70, 0x69, 0x26, 0xd3, 0x9c, 0x07, 0x1f, 0x05, 0x9d, 0x00, 0x00, 0x08, 0xbc, 0x05, 0x37, 0xfa, 0xa2, 0x01, 0xaa, 0x01, 0xa1, 0x01, 0xa8, 0x01, 0xa0, 0x01, 0xa8, 0x05, 0xb4, 0x01, 0xb4, 0x01, 0xce, 0x00, 0xd0, 0x00, 0xfc, 0x00, 0xc5, 0x01, 0xff, 0xfb, 0xb1, 0x00, 0x00, 0x38, 0x00, 0x30, 0xfd, 0xf5, 0xfc, 0xf5, 0xcd, 0x01, 0xa0, 0x00, 0x5f, 0xff, 0x00, 0x40, 0xff, 0x00, 0x00, 0x80, 0x6d, 0x0f, 0xeb, 0x00, 0x7f, 0xff, 0xc2, 0xf5, 0x68, 0xf7, 0xb3, 0xf1, 0x67, 0x0f, 0x5b, 0x0f, 0x61, 0x0f, 0x80, 0x0f, 0x58, 0xf7, 0x5b, 0xf7, 0x83, 0x0f, 0x86, 0x00, 0x72, 0x0f, 0x85, 0x0f, 0xc6, 0xf1, 0x7f, 0x0f, 0x6c, 0xf7, 0x00, 0xe0, 0x00, 0xff, 0xd1, 0xf5, 0x87, 0x0f, 0x8a, 0x0f, 0xff, 0x03, 0xf0, 0x3f, 0x8b, 0x00, 0x8e, 0x00, 0x90, 0x00, 0xb9, 0x00, 0x2d, 0xf5, 0xca, 0xf5, 0xcb, 0x01, 0x20, 0xf2, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x50, 0x98, 0x2e, 0xd7, 0x0e, 0x50, 0x32, 0x98, 0x2e, 0xfa, 0x03, 0x00, 0x30, 0xf0, 0x7f, 0x00, 0x2e, 0x00, 0x2e, 0xd0, 0x2e, 0x00, 0x2e, 0x01, 0x80, 0x08, 0xa2, 0xfb, 0x2f, 0x98, 0x2e, 0xba, 0x03, 0x21, 0x2e, 0x19, 0x00, 0x01, 0x2e, 0xee, 0x00, 0x00, 0xb2, 0x07, 0x2f, 0x01, 0x2e, 0x19, 0x00, 0x00, 0xb2, 0x03, 0x2f, 0x01, 0x50, 0x03, 0x52, 0x98, 0x2e, 0x07, 0xcc, 0x01, 0x2e, 0xdd, 0x00, 0x00, 0xb2, 0x27, 0x2f, 0x05, 0x2e, 0x8a, 0x00, 0x05, 0x52, 0x98, 0x2e, 0xc7, 0xc1, 0x03, 0x2e, 0xe9, 0x00, 0x40, 0xb2, 0xf0, 0x7f, 0x08, 0x2f, 0x01, 0x2e, 0x19, 0x00, 0x00, 0xb2, 0x04, 0x2f, 0x00, 0x30, 0x21, 0x2e, 0xe9, 0x00, 0x98, 0x2e, 0xb4, 0xb1, 0x01, 0x2e, 0x18, 0x00, 0x00, 0xb2, 0x10, 0x2f, 0x05, 0x50, 0x98, 0x2e, 0x4d, 0xc3, 0x05, 0x50, 0x98, 0x2e, 0x5a, 0xc7, 0x98, 0x2e, 0xf9, 0xb4, 0x98, 0x2e, 0x54, 0xb2, 0x98, 0x2e, 0x67, 0xb6, 0x98, 0x2e, 0x17, 0xb2, 0x10, 0x30, 0x21, 0x2e, 0x77, 0x00, 0x01, 0x2e, 0xef, 0x00, 0x00, 0xb2, 0x04, 0x2f, 0x98, 0x2e, 0x7a, 0xb7, 0x00, 0x30, 0x21, 0x2e, 0xef, 0x00, 0x01, 0x2e, 0xd4, 0x00, 0x04, 0xae, 0x0b, 0x2f, 0x01, 0x2e, 0xdd, 0x00, 0x00, 0xb2, 0x07, 0x2f, 0x05, 0x52, 0x98, 0x2e, 0x8e, 0x0e, 0x00, 0xb2, 0x02, 0x2f, 0x10, 0x30, 0x21, 0x2e, 0x7d, 0x00, 0x01, 0x2e, 0x7d, 0x00, 0x00, 0x90, 0x90, 0x2e, 0xf1, 0x02, 0x01, 0x2e, 0xd7, 0x00, 0x00, 0xb2, 0x04, 0x2f, 0x98, 0x2e, 0x2f, 0x0e, 0x00, 0x30, 0x21, 0x2e, 0x7b, 0x00, 0x01, 0x2e, 0x7b, 0x00, 0x00, 0xb2, 0x12, 0x2f, 0x01, 0x2e, 0xd4, 0x00, 0x00, 0x90, 0x02, 0x2f, 0x98, 0x2e, 0x1f, 0x0e, 0x09, 0x2d, 0x98, 0x2e, 0x81, 0x0d, 0x01, 0x2e, 0xd4, 0x00, 0x04, 0x90, 0x02, 0x2f, 0x50, 0x32, 0x98, 0x2e, 0xfa, 0x03, 0x00, 0x30, 0x21, 0x2e, 0x7b, 0x00, 0x01, 0x2e, 0x7c, 0x00, 0x00, 0xb2, 0x90, 0x2e, 0x09, 0x03, 0x01, 0x2e, 0x7c, 0x00, 0x01, 0x31, 0x01, 0x08, 0x00, 0xb2, 0x04, 0x2f, 0x98, 0x2e, 0x47, 0xcb, 0x10, 0x30, 0x21, 0x2e, 0x77, 0x00, 0x81, 0x30, 0x01, 0x2e, 0x7c, 0x00, 0x01, 0x08, 0x00, 0xb2, 0x61, 0x2f, 0x03, 0x2e, 0x89, 0x00, 0x01, 0x2e, 0xd4, 0x00, 0x98, 0xbc, 0x98, 0xb8, 0x05, 0xb2, 0x0f, 0x58, 0x23, 0x2f, 0x07, 0x90, 0x09, 0x54, 0x00, 0x30, 0x37, 0x2f, 0x15, 0x41, 0x04, 0x41, 0xdc, 0xbe, 0x44, 0xbe, 0xdc, 0xba, 0x2c, 0x01, 0x61, 0x00, 0x0f, 0x56, 0x4a, 0x0f, 0x0c, 0x2f, 0xd1, 0x42, 0x94, 0xb8, 0xc1, 0x42, 0x11, 0x30, 0x05, 0x2e, 0x6a, 0xf7, 0x2c, 0xbd, 0x2f, 0xb9, 0x80, 0xb2, 0x08, 0x22, 0x98, 0x2e, 0xc3, 0xb7, 0x21, 0x2d, 0x61, 0x30, 0x23, 0x2e, 0xd4, 0x00, 0x98, 0x2e, 0xc3, 0xb7, 0x00, 0x30, 0x21, 0x2e, 0x5a, 0xf5, 0x18, 0x2d, 0xe1, 0x7f, 0x50, 0x30, 0x98, 0x2e, 0xfa, 0x03, 0x0f, 0x52, 0x07, 0x50, 0x50, 0x42, 0x70, 0x30, 0x0d, 0x54, 0x42, 0x42, 0x7e, 0x82, 0xe2, 0x6f, 0x80, 0xb2, 0x42, 0x42, 0x05, 0x2f, 0x21, 0x2e, 0xd4, 0x00, 0x10, 0x30, 0x98, 0x2e, 0xc3, 0xb7, 0x03, 0x2d, 0x60, 0x30, 0x21, 0x2e, 0xd4, 0x00, 0x01, 0x2e, 0xd4, 0x00, 0x06, 0x90, 0x18, 0x2f, 0x01, 0x2e, 0x76, 0x00, 0x0b, 0x54, 0x07, 0x52, 0xe0, 0x7f, 0x98, 0x2e, 0x7a, 0xc1, 0xe1, 0x6f, 0x08, 0x1a, 0x40, 0x30, 0x08, 0x2f, 0x21, 0x2e, 0xd4, 0x00, 0x20, 0x30, 0x98, 0x2e, 0xaf, 0xb7, 0x50, 0x32, 0x98, 0x2e, 0xfa, 0x03, 0x05, 0x2d, 0x98, 0x2e, 0x38, 0x0e, 0x00, 0x30, 0x21, 0x2e, 0xd4, 0x00, 0x00, 0x30, 0x21, 0x2e, 0x7c, 0x00, 0x18, 0x2d, 0x01, 0x2e, 0xd4, 0x00, 0x03, 0xaa, 0x01, 0x2f, 0x98, 0x2e, 0x45, 0x0e, 0x01, 0x2e, 0xd4, 0x00, 0x3f, 0x80, 0x03, 0xa2, 0x01, 0x2f, 0x00, 0x2e, 0x02, 0x2d, 0x98, 0x2e, 0x5b, 0x0e, 0x30, 0x30, 0x98, 0x2e, 0xce, 0xb7, 0x00, 0x30, 0x21, 0x2e, 0x7d, 0x00, 0x50, 0x32, 0x98, 0x2e, 0xfa, 0x03, 0x01, 0x2e, 0x77, 0x00, 0x00, 0xb2, 0x24, 0x2f, 0x98, 0x2e, 0xf5, 0xcb, 0x03, 0x2e, 0xd5, 0x00, 0x11, 0x54, 0x01, 0x0a, 0xbc, 0x84, 0x83, 0x86, 0x21, 0x2e, 0xc9, 0x01, 0xe0, 0x40, 0x13, 0x52, 0xc4, 0x40, 0x82, 0x40, 0xa8, 0xb9, 0x52, 0x42, 0x43, 0xbe, 0x53, 0x42, 0x04, 0x0a, 0x50, 0x42, 0xe1, 0x7f, 0xf0, 0x31, 0x41, 0x40, 0xf2, 0x6f, 0x25, 0xbd, 0x08, 0x08, 0x02, 0x0a, 0xd0, 0x7f, 0x98, 0x2e, 0xa8, 0xcf, 0x06, 0xbc, 0xd1, 0x6f, 0xe2, 0x6f, 0x08, 0x0a, 0x80, 0x42, 0x98, 0x2e, 0x58, 0xb7, 0x00, 0x30, 0x21, 0x2e, 0xee, 0x00, 0x21, 0x2e, 0x77, 0x00, 0x21, 0x2e, 0xdd, 0x00, 0x80, 0x2e, 0xf4, 0x01, 0x1a, 0x24, 0x22, 0x00, 0x80, 0x2e, 0xec, 0x01, 0x10, 0x50, 0xfb, 0x7f, 0x98, 0x2e, 0xf3, 0x03, 0x57, 0x50, 0xfb, 0x6f, 0x01, 0x30, 0x71, 0x54, 0x11, 0x42, 0x42, 0x0e, 0xfc, 0x2f, 0xc0, 0x2e, 0x01, 0x42, 0xf0, 0x5f, 0x80, 0x2e, 0x00, 0xc1, 0xfd, 0x2d, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x9a, 0x01, 0x34, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x50, 0xe7, 0x7f, 0xf6, 0x7f, 0x06, 0x32, 0x0f, 0x2e, 0x61, 0xf5, 0xfe, 0x09, 0xc0, 0xb3, 0x04, 0x2f, 0x17, 0x30, 0x2f, 0x2e, 0xef, 0x00, 0x2d, 0x2e, 0x61, 0xf5, 0xf6, 0x6f, 0xe7, 0x6f, 0xe0, 0x5f, 0xc8, 0x2e, 0x20, 0x50, 0xe7, 0x7f, 0xf6, 0x7f, 0x46, 0x30, 0x0f, 0x2e, 0xa4, 0xf1, 0xbe, 0x09, 0x80, 0xb3, 0x06, 0x2f, 0x0d, 0x2e, 0xd4, 0x00, 0x84, 0xaf, 0x02, 0x2f, 0x16, 0x30, 0x2d, 0x2e, 0x7b, 0x00, 0x86, 0x30, 0x2d, 0x2e, 0x60, 0xf5, 0xf6, 0x6f, 0xe7, 0x6f, 0xe0, 0x5f, 0xc8, 0x2e, 0x01, 0x2e, 0x77, 0xf7, 0x09, 0xbc, 0x0f, 0xb8, 0x00, 0xb2, 0x10, 0x50, 0xfb, 0x7f, 0x10, 0x30, 0x0b, 0x2f, 0x03, 0x2e, 0x8a, 0x00, 0x96, 0xbc, 0x9f, 0xb8, 0x40, 0xb2, 0x05, 0x2f, 0x03, 0x2e, 0x68, 0xf7, 0x9e, 0xbc, 0x9f, 0xb8, 0x40, 0xb2, 0x07, 0x2f, 0x03, 0x2e, 0x7e, 0x00, 0x41, 0x90, 0x01, 0x2f, 0x98, 0x2e, 0xdc, 0x03, 0x03, 0x2c, 0x00, 0x30, 0x21, 0x2e, 0x7e, 0x00, 0xfb, 0x6f, 0xf0, 0x5f, 0xb8, 0x2e, 0x20, 0x50, 0xe0, 0x7f, 0xfb, 0x7f, 0x00, 0x2e, 0x27, 0x50, 0x98, 0x2e, 0x3b, 0xc8, 0x29, 0x50, 0x98, 0x2e, 0xa7, 0xc8, 0x01, 0x50, 0x98, 0x2e, 0x55, 0xcc, 0xe1, 0x6f, 0x2b, 0x50, 0x98, 0x2e, 0xe0, 0xc9, 0xfb, 0x6f, 0x00, 0x30, 0xe0, 0x5f, 0x21, 0x2e, 0x7e, 0x00, 0xb8, 0x2e, 0x73, 0x50, 0x01, 0x30, 0x57, 0x54, 0x11, 0x42, 0x42, 0x0e, 0xfc, 0x2f, 0xb8, 0x2e, 0x21, 0x2e, 0x59, 0xf5, 0x10, 0x30, 0xc0, 0x2e, 0x21, 0x2e, 0x4a, 0xf1, 0x90, 0x50, 0xf7, 0x7f, 0xe6, 0x7f, 0xd5, 0x7f, 0xc4, 0x7f, 0xb3, 0x7f, 0xa1, 0x7f, 0x90, 0x7f, 0x82, 0x7f, 0x7b, 0x7f, 0x98, 0x2e, 0x35, 0xb7, 0x00, 0xb2, 0x90, 0x2e, 0x97, 0xb0, 0x03, 0x2e, 0x8f, 0x00, 0x07, 0x2e, 0x91, 0x00, 0x05, 0x2e, 0xb1, 0x00, 0x3f, 0xba, 0x9f, 0xb8, 0x01, 0x2e, 0xb1, 0x00, 0xa3, 0xbd, 0x4c, 0x0a, 0x05, 0x2e, 0xb1, 0x00, 0x04, 0xbe, 0xbf, 0xb9, 0xcb, 0x0a, 0x4f, 0xba, 0x22, 0xbd, 0x01, 0x2e, 0xb3, 0x00, 0xdc, 0x0a, 0x2f, 0xb9, 0x03, 0x2e, 0xb8, 0x00, 0x0a, 0xbe, 0x9a, 0x0a, 0xcf, 0xb9, 0x9b, 0xbc, 0x01, 0x2e, 0x97, 0x00, 0x9f, 0xb8, 0x93, 0x0a, 0x0f, 0xbc, 0x91, 0x0a, 0x0f, 0xb8, 0x90, 0x0a, 0x25, 0x2e, 0x18, 0x00, 0x05, 0x2e, 0xc1, 0xf5, 0x2e, 0xbd, 0x2e, 0xb9, 0x01, 0x2e, 0x19, 0x00, 0x31, 0x30, 0x8a, 0x04, 0x00, 0x90, 0x07, 0x2f, 0x01, 0x2e, 0xd4, 0x00, 0x04, 0xa2, 0x03, 0x2f, 0x01, 0x2e, 0x18, 0x00, 0x00, 0xb2, 0x0c, 0x2f, 0x19, 0x50, 0x05, 0x52, 0x98, 0x2e, 0x4d, 0xb7, 0x05, 0x2e, 0x78, 0x00, 0x80, 0x90, 0x10, 0x30, 0x01, 0x2f, 0x21, 0x2e, 0x78, 0x00, 0x25, 0x2e, 0xdd, 0x00, 0x98, 0x2e, 0x3e, 0xb7, 0x00, 0xb2, 0x02, 0x30, 0x01, 0x30, 0x04, 0x2f, 0x01, 0x2e, 0x19, 0x00, 0x00, 0xb2, 0x00, 0x2f, 0x21, 0x30, 0x01, 0x2e, 0xea, 0x00, 0x08, 0x1a, 0x0e, 0x2f, 0x23, 0x2e, 0xea, 0x00, 0x33, 0x30, 0x1b, 0x50, 0x0b, 0x09, 0x01, 0x40, 0x17, 0x56, 0x46, 0xbe, 0x4b, 0x08, 0x4c, 0x0a, 0x01, 0x42, 0x0a, 0x80, 0x15, 0x52, 0x01, 0x42, 0x00, 0x2e, 0x01, 0x2e, 0x18, 0x00, 0x00, 0xb2, 0x1f, 0x2f, 0x03, 0x2e, 0xc0, 0xf5, 0xf0, 0x30, 0x48, 0x08, 0x47, 0xaa, 0x74, 0x30, 0x07, 0x2e, 0x7a, 0x00, 0x61, 0x22, 0x4b, 0x1a, 0x05, 0x2f, 0x07, 0x2e, 0x66, 0xf5, 0xbf, 0xbd, 0xbf, 0xb9, 0xc0, 0x90, 0x0b, 0x2f, 0x1d, 0x56, 0x2b, 0x30, 0xd2, 0x42, 0xdb, 0x42, 0x01, 0x04, 0xc2, 0x42, 0x04, 0xbd, 0xfe, 0x80, 0x81, 0x84, 0x23, 0x2e, 0x7a, 0x00, 0x02, 0x42, 0x02, 0x32, 0x25, 0x2e, 0x62, 0xf5, 0x05, 0x2e, 0xd6, 0x00, 0x81, 0x84, 0x25, 0x2e, 0xd6, 0x00, 0x02, 0x31, 0x25, 0x2e, 0x60, 0xf5, 0x05, 0x2e, 0x8a, 0x00, 0x0b, 0x50, 0x90, 0x08, 0x80, 0xb2, 0x0b, 0x2f, 0x05, 0x2e, 0xca, 0xf5, 0xf0, 0x3e, 0x90, 0x08, 0x25, 0x2e, 0xca, 0xf5, 0x05, 0x2e, 0x59, 0xf5, 0xe0, 0x3f, 0x90, 0x08, 0x25, 0x2e, 0x59, 0xf5, 0x90, 0x6f, 0xa1, 0x6f, 0xb3, 0x6f, 0xc4, 0x6f, 0xd5, 0x6f, 0xe6, 0x6f, 0xf7, 0x6f, 0x7b, 0x6f, 0x82, 0x6f, 0x70, 0x5f, 0xc8, 0x2e, 0xc0, 0x50, 0x90, 0x7f, 0xe5, 0x7f, 0xd4, 0x7f, 0xc3, 0x7f, 0xb1, 0x7f, 0xa2, 0x7f, 0x87, 0x7f, 0xf6, 0x7f, 0x7b, 0x7f, 0x00, 0x2e, 0x01, 0x2e, 0x60, 0xf5, 0x60, 0x7f, 0x98, 0x2e, 0x35, 0xb7, 0x02, 0x30, 0x63, 0x6f, 0x15, 0x52, 0x50, 0x7f, 0x62, 0x7f, 0x5a, 0x2c, 0x02, 0x32, 0x1a, 0x09, 0x00, 0xb3, 0x14, 0x2f, 0x00, 0xb2, 0x03, 0x2f, 0x09, 0x2e, 0x18, 0x00, 0x00, 0x91, 0x0c, 0x2f, 0x43, 0x7f, 0x98, 0x2e, 0x97, 0xb7, 0x1f, 0x50, 0x02, 0x8a, 0x02, 0x32, 0x04, 0x30, 0x25, 0x2e, 0x64, 0xf5, 0x15, 0x52, 0x50, 0x6f, 0x43, 0x6f, 0x44, 0x43, 0x25, 0x2e, 0x60, 0xf5, 0xd9, 0x08, 0xc0, 0xb2, 0x36, 0x2f, 0x98, 0x2e, 0x3e, 0xb7, 0x00, 0xb2, 0x06, 0x2f, 0x01, 0x2e, 0x19, 0x00, 0x00, 0xb2, 0x02, 0x2f, 0x50, 0x6f, 0x00, 0x90, 0x0a, 0x2f, 0x01, 0x2e, 0x79, 0x00, 0x00, 0x90, 0x19, 0x2f, 0x10, 0x30, 0x21, 0x2e, 0x79, 0x00, 0x00, 0x30, 0x98, 0x2e, 0xdc, 0x03, 0x13, 0x2d, 0x01, 0x2e, 0xc3, 0xf5, 0x0c, 0xbc, 0x0f, 0xb8, 0x12, 0x30, 0x10, 0x04, 0x03, 0xb0, 0x26, 0x25, 0x21, 0x50, 0x03, 0x52, 0x98, 0x2e, 0x4d, 0xb7, 0x10, 0x30, 0x21, 0x2e, 0xee, 0x00, 0x02, 0x30, 0x60, 0x7f, 0x25, 0x2e, 0x79, 0x00, 0x60, 0x6f, 0x00, 0x90, 0x05, 0x2f, 0x00, 0x30, 0x21, 0x2e, 0xea, 0x00, 0x15, 0x50, 0x21, 0x2e, 0x64, 0xf5, 0x15, 0x52, 0x23, 0x2e, 0x60, 0xf5, 0x02, 0x32, 0x50, 0x6f, 0x00, 0x90, 0x02, 0x2f, 0x03, 0x30, 0x27, 0x2e, 0x78, 0x00, 0x07, 0x2e, 0x60, 0xf5, 0x1a, 0x09, 0x00, 0x91, 0xa3, 0x2f, 0x19, 0x09, 0x00, 0x91, 0xa0, 0x2f, 0x90, 0x6f, 0xa2, 0x6f, 0xb1, 0x6f, 0xc3, 0x6f, 0xd4, 0x6f, 0xe5, 0x6f, 0x7b, 0x6f, 0xf6, 0x6f, 0x87, 0x6f, 0x40, 0x5f, 0xc8, 0x2e, 0xc0, 0x50, 0xe7, 0x7f, 0xf6, 0x7f, 0x26, 0x30, 0x0f, 0x2e, 0x61, 0xf5, 0x2f, 0x2e, 0x7c, 0x00, 0x0f, 0x2e, 0x7c, 0x00, 0xbe, 0x09, 0xa2, 0x7f, 0x80, 0x7f, 0x80, 0xb3, 0xd5, 0x7f, 0xc4, 0x7f, 0xb3, 0x7f, 0x91, 0x7f, 0x7b, 0x7f, 0x0b, 0x2f, 0x23, 0x50, 0x1a, 0x25, 0x12, 0x40, 0x42, 0x7f, 0x74, 0x82, 0x12, 0x40, 0x52, 0x7f, 0x00, 0x2e, 0x00, 0x40, 0x60, 0x7f, 0x98, 0x2e, 0x6a, 0xd6, 0x81, 0x30, 0x01, 0x2e, 0x7c, 0x00, 0x01, 0x08, 0x00, 0xb2, 0x42, 0x2f, 0x03, 0x2e, 0x89, 0x00, 0x01, 0x2e, 0x89, 0x00, 0x97, 0xbc, 0x06, 0xbc, 0x9f, 0xb8, 0x0f, 0xb8, 0x00, 0x90, 0x23, 0x2e, 0xd8, 0x00, 0x10, 0x30, 0x01, 0x30, 0x2a, 0x2f, 0x03, 0x2e, 0xd4, 0x00, 0x44, 0xb2, 0x05, 0x2f, 0x47, 0xb2, 0x00, 0x30, 0x2d, 0x2f, 0x21, 0x2e, 0x7c, 0x00, 0x2b, 0x2d, 0x03, 0x2e, 0xfd, 0xf5, 0x9e, 0xbc, 0x9f, 0xb8, 0x40, 0x90, 0x14, 0x2f, 0x03, 0x2e, 0xfc, 0xf5, 0x99, 0xbc, 0x9f, 0xb8, 0x40, 0x90, 0x0e, 0x2f, 0x03, 0x2e, 0x49, 0xf1, 0x25, 0x54, 0x4a, 0x08, 0x40, 0x90, 0x08, 0x2f, 0x98, 0x2e, 0x35, 0xb7, 0x00, 0xb2, 0x10, 0x30, 0x03, 0x2f, 0x50, 0x30, 0x21, 0x2e, 0xd4, 0x00, 0x10, 0x2d, 0x98, 0x2e, 0xaf, 0xb7, 0x00, 0x30, 0x21, 0x2e, 0x7c, 0x00, 0x0a, 0x2d, 0x05, 0x2e, 0x69, 0xf7, 0x2d, 0xbd, 0x2f, 0xb9, 0x80, 0xb2, 0x01, 0x2f, 0x21, 0x2e, 0x7d, 0x00, 0x23, 0x2e, 0x7c, 0x00, 0xe0, 0x31, 0x21, 0x2e, 0x61, 0xf5, 0xf6, 0x6f, 0xe7, 0x6f, 0x80, 0x6f, 0xa2, 0x6f, 0xb3, 0x6f, 0xc4, 0x6f, 0xd5, 0x6f, 0x7b, 0x6f, 0x91, 0x6f, 0x40, 0x5f, 0xc8, 0x2e, 0x60, 0x51, 0x0a, 0x25, 0x36, 0x88, 0xf4, 0x7f, 0xeb, 0x7f, 0x00, 0x32, 0x31, 0x52, 0x32, 0x30, 0x13, 0x30, 0x98, 0x2e, 0x15, 0xcb, 0x0a, 0x25, 0x33, 0x84, 0xd2, 0x7f, 0x43, 0x30, 0x05, 0x50, 0x2d, 0x52, 0x98, 0x2e, 0x95, 0xc1, 0xd2, 0x6f, 0x27, 0x52, 0x98, 0x2e, 0xd7, 0xc7, 0x2a, 0x25, 0xb0, 0x86, 0xc0, 0x7f, 0xd3, 0x7f, 0xaf, 0x84, 0x29, 0x50, 0xf1, 0x6f, 0x98, 0x2e, 0x4d, 0xc8, 0x2a, 0x25, 0xae, 0x8a, 0xaa, 0x88, 0xf2, 0x6e, 0x2b, 0x50, 0xc1, 0x6f, 0xd3, 0x6f, 0xf4, 0x7f, 0x98, 0x2e, 0xb6, 0xc8, 0xe0, 0x6e, 0x00, 0xb2, 0x32, 0x2f, 0x33, 0x54, 0x83, 0x86, 0xf1, 0x6f, 0xc3, 0x7f, 0x04, 0x30, 0x30, 0x30, 0xf4, 0x7f, 0xd0, 0x7f, 0xb2, 0x7f, 0xe3, 0x30, 0xc5, 0x6f, 0x56, 0x40, 0x45, 0x41, 0x28, 0x08, 0x03, 0x14, 0x0e, 0xb4, 0x08, 0xbc, 0x82, 0x40, 0x10, 0x0a, 0x2f, 0x54, 0x26, 0x05, 0x91, 0x7f, 0x44, 0x28, 0xa3, 0x7f, 0x98, 0x2e, 0xd9, 0xc0, 0x08, 0xb9, 0x33, 0x30, 0x53, 0x09, 0xc1, 0x6f, 0xd3, 0x6f, 0xf4, 0x6f, 0x83, 0x17, 0x47, 0x40, 0x6c, 0x15, 0xb2, 0x6f, 0xbe, 0x09, 0x75, 0x0b, 0x90, 0x42, 0x45, 0x42, 0x51, 0x0e, 0x32, 0xbc, 0x02, 0x89, 0xa1, 0x6f, 0x7e, 0x86, 0xf4, 0x7f, 0xd0, 0x7f, 0xb2, 0x7f, 0x04, 0x30, 0x91, 0x6f, 0xd6, 0x2f, 0xeb, 0x6f, 0xa0, 0x5e, 0xb8, 0x2e, 0x03, 0x2e, 0x97, 0x00, 0x1b, 0xbc, 0x60, 0x50, 0x9f, 0xbc, 0x0c, 0xb8, 0xf0, 0x7f, 0x40, 0xb2, 0xeb, 0x7f, 0x2b, 0x2f, 0x03, 0x2e, 0x7f, 0x00, 0x41, 0x40, 0x01, 0x2e, 0xc8, 0x00, 0x01, 0x1a, 0x11, 0x2f, 0x37, 0x58, 0x23, 0x2e, 0xc8, 0x00, 0x10, 0x41, 0xa0, 0x7f, 0x38, 0x81, 0x01, 0x41, 0xd0, 0x7f, 0xb1, 0x7f, 0x98, 0x2e, 0x64, 0xcf, 0xd0, 0x6f, 0x07, 0x80, 0xa1, 0x6f, 0x11, 0x42, 0x00, 0x2e, 0xb1, 0x6f, 0x01, 0x42, 0x11, 0x30, 0x01, 0x2e, 0xfc, 0x00, 0x00, 0xa8, 0x03, 0x30, 0xcb, 0x22, 0x4a, 0x25, 0x01, 0x2e, 0x7f, 0x00, 0x3c, 0x89, 0x35, 0x52, 0x05, 0x54, 0x98, 0x2e, 0xc4, 0xce, 0xc1, 0x6f, 0xf0, 0x6f, 0x98, 0x2e, 0x95, 0xcf, 0x04, 0x2d, 0x01, 0x30, 0xf0, 0x6f, 0x98, 0x2e, 0x95, 0xcf, 0xeb, 0x6f, 0xa0, 0x5f, 0xb8, 0x2e, 0x03, 0x2e, 0xb3, 0x00, 0x02, 0x32, 0xf0, 0x30, 0x03, 0x31, 0x30, 0x50, 0x8a, 0x08, 0x08, 0x08, 0xcb, 0x08, 0xe0, 0x7f, 0x80, 0xb2, 0xf3, 0x7f, 0xdb, 0x7f, 0x25, 0x2f, 0x03, 0x2e, 0xca, 0x00, 0x41, 0x90, 0x04, 0x2f, 0x01, 0x30, 0x23, 0x2e, 0xca, 0x00, 0x98, 0x2e, 0x3f, 0x03, 0xc0, 0xb2, 0x05, 0x2f, 0x03, 0x2e, 0xda, 0x00, 0x00, 0x30, 0x41, 0x04, 0x23, 0x2e, 0xda, 0x00, 0x98, 0x2e, 0x92, 0xb2, 0x10, 0x25, 0xf0, 0x6f, 0x00, 0xb2, 0x05, 0x2f, 0x01, 0x2e, 0xda, 0x00, 0x02, 0x30, 0x10, 0x04, 0x21, 0x2e, 0xda, 0x00, 0x40, 0xb2, 0x01, 0x2f, 0x23, 0x2e, 0xc8, 0x01, 0xdb, 0x6f, 0xe0, 0x6f, 0xd0, 0x5f, 0x80, 0x2e, 0x95, 0xcf, 0x01, 0x30, 0xe0, 0x6f, 0x98, 0x2e, 0x95, 0xcf, 0x11, 0x30, 0x23, 0x2e, 0xca, 0x00, 0xdb, 0x6f, 0xd0, 0x5f, 0xb8, 0x2e, 0xd0, 0x50, 0x0a, 0x25, 0x33, 0x84, 0x55, 0x50, 0xd2, 0x7f, 0xe2, 0x7f, 0x03, 0x8c, 0xc0, 0x7f, 0xbb, 0x7f, 0x00, 0x30, 0x05, 0x5a, 0x39, 0x54, 0x51, 0x41, 0xa5, 0x7f, 0x96, 0x7f, 0x80, 0x7f, 0x98, 0x2e, 0xd9, 0xc0, 0x05, 0x30, 0xf5, 0x7f, 0x20, 0x25, 0x91, 0x6f, 0x3b, 0x58, 0x3d, 0x5c, 0x3b, 0x56, 0x98, 0x2e, 0x67, 0xcc, 0xc1, 0x6f, 0xd5, 0x6f, 0x52, 0x40, 0x50, 0x43, 0xc1, 0x7f, 0xd5, 0x7f, 0x10, 0x25, 0x98, 0x2e, 0xfe, 0xc9, 0x10, 0x25, 0x98, 0x2e, 0x74, 0xc0, 0x86, 0x6f, 0x30, 0x28, 0x92, 0x6f, 0x82, 0x8c, 0xa5, 0x6f, 0x6f, 0x52, 0x69, 0x0e, 0x39, 0x54, 0xdb, 0x2f, 0x19, 0xa0, 0x15, 0x30, 0x03, 0x2f, 0x00, 0x30, 0x21, 0x2e, 0x81, 0x01, 0x0a, 0x2d, 0x01, 0x2e, 0x81, 0x01, 0x05, 0x28, 0x42, 0x36, 0x21, 0x2e, 0x81, 0x01, 0x02, 0x0e, 0x01, 0x2f, 0x98, 0x2e, 0xf3, 0x03, 0x57, 0x50, 0x12, 0x30, 0x01, 0x40, 0x98, 0x2e, 0xfe, 0xc9, 0x51, 0x6f, 0x0b, 0x5c, 0x8e, 0x0e, 0x3b, 0x6f, 0x57, 0x58, 0x02, 0x30, 0x21, 0x2e, 0x95, 0x01, 0x45, 0x6f, 0x2a, 0x8d, 0xd2, 0x7f, 0xcb, 0x7f, 0x13, 0x2f, 0x02, 0x30, 0x3f, 0x50, 0xd2, 0x7f, 0xa8, 0x0e, 0x0e, 0x2f, 0xc0, 0x6f, 0x53, 0x54, 0x02, 0x00, 0x51, 0x54, 0x42, 0x0e, 0x10, 0x30, 0x59, 0x52, 0x02, 0x30, 0x01, 0x2f, 0x00, 0x2e, 0x03, 0x2d, 0x50, 0x42, 0x42, 0x42, 0x12, 0x30, 0xd2, 0x7f, 0x80, 0xb2, 0x03, 0x2f, 0x00, 0x30, 0x21, 0x2e, 0x80, 0x01, 0x12, 0x2d, 0x01, 0x2e, 0xc9, 0x00, 0x02, 0x80, 0x05, 0x2e, 0x80, 0x01, 0x11, 0x30, 0x91, 0x28, 0x00, 0x40, 0x25, 0x2e, 0x80, 0x01, 0x10, 0x0e, 0x05, 0x2f, 0x01, 0x2e, 0x7f, 0x01, 0x01, 0x90, 0x01, 0x2f, 0x98, 0x2e, 0xf3, 0x03, 0x00, 0x2e, 0xa0, 0x41, 0x01, 0x90, 0xa6, 0x7f, 0x90, 0x2e, 0xe3, 0xb4, 0x01, 0x2e, 0x95, 0x01, 0x00, 0xa8, 0x90, 0x2e, 0xe3, 0xb4, 0x5b, 0x54, 0x95, 0x80, 0x82, 0x40, 0x80, 0xb2, 0x02, 0x40, 0x2d, 0x8c, 0x3f, 0x52, 0x96, 0x7f, 0x90, 0x2e, 0xc2, 0xb3, 0x29, 0x0e, 0x76, 0x2f, 0x01, 0x2e, 0xc9, 0x00, 0x00, 0x40, 0x81, 0x28, 0x45, 0x52, 0xb3, 0x30, 0x98, 0x2e, 0x0f, 0xca, 0x5d, 0x54, 0x80, 0x7f, 0x00, 0x2e, 0xa1, 0x40, 0x72, 0x7f, 0x82, 0x80, 0x82, 0x40, 0x60, 0x7f, 0x98, 0x2e, 0xfe, 0xc9, 0x10, 0x25, 0x98, 0x2e, 0x74, 0xc0, 0x62, 0x6f, 0x05, 0x30, 0x87, 0x40, 0xc0, 0x91, 0x04, 0x30, 0x05, 0x2f, 0x05, 0x2e, 0x83, 0x01, 0x80, 0xb2, 0x14, 0x30, 0x00, 0x2f, 0x04, 0x30, 0x05, 0x2e, 0xc9, 0x00, 0x73, 0x6f, 0x81, 0x40, 0xe2, 0x40, 0x69, 0x04, 0x11, 0x0f, 0xe1, 0x40, 0x16, 0x30, 0xfe, 0x29, 0xcb, 0x40, 0x02, 0x2f, 0x83, 0x6f, 0x83, 0x0f, 0x22, 0x2f, 0x47, 0x56, 0x13, 0x0f, 0x12, 0x30, 0x77, 0x2f, 0x49, 0x54, 0x42, 0x0e, 0x12, 0x30, 0x73, 0x2f, 0x00, 0x91, 0x0a, 0x2f, 0x01, 0x2e, 0x8b, 0x01, 0x19, 0xa8, 0x02, 0x30, 0x6c, 0x2f, 0x63, 0x50, 0x00, 0x2e, 0x17, 0x42, 0x05, 0x42, 0x68, 0x2c, 0x12, 0x30, 0x0b, 0x25, 0x08, 0x0f, 0x50, 0x30, 0x02, 0x2f, 0x21, 0x2e, 0x83, 0x01, 0x03, 0x2d, 0x40, 0x30, 0x21, 0x2e, 0x83, 0x01, 0x2b, 0x2e, 0x85, 0x01, 0x5a, 0x2c, 0x12, 0x30, 0x00, 0x91, 0x2b, 0x25, 0x04, 0x2f, 0x63, 0x50, 0x02, 0x30, 0x17, 0x42, 0x17, 0x2c, 0x02, 0x42, 0x98, 0x2e, 0xfe, 0xc9, 0x10, 0x25, 0x98, 0x2e, 0x74, 0xc0, 0x05, 0x2e, 0xc9, 0x00, 0x81, 0x84, 0x5b, 0x30, 0x82, 0x40, 0x37, 0x2e, 0x83, 0x01, 0x02, 0x0e, 0x07, 0x2f, 0x5f, 0x52, 0x40, 0x30, 0x62, 0x40, 0x41, 0x40, 0x91, 0x0e, 0x01, 0x2f, 0x21, 0x2e, 0x83, 0x01, 0x05, 0x30, 0x2b, 0x2e, 0x85, 0x01, 0x12, 0x30, 0x36, 0x2c, 0x16, 0x30, 0x15, 0x25, 0x81, 0x7f, 0x98, 0x2e, 0xfe, 0xc9, 0x10, 0x25, 0x98, 0x2e, 0x74, 0xc0, 0x19, 0xa2, 0x16, 0x30, 0x15, 0x2f, 0x05, 0x2e, 0x97, 0x01, 0x80, 0x6f, 0x82, 0x0e, 0x05, 0x2f, 0x01, 0x2e, 0x86, 0x01, 0x06, 0x28, 0x21, 0x2e, 0x86, 0x01, 0x0b, 0x2d, 0x03, 0x2e, 0x87, 0x01, 0x5f, 0x54, 0x4e, 0x28, 0x91, 0x42, 0x00, 0x2e, 0x82, 0x40, 0x90, 0x0e, 0x01, 0x2f, 0x21, 0x2e, 0x88, 0x01, 0x02, 0x30, 0x13, 0x2c, 0x05, 0x30, 0xc0, 0x6f, 0x08, 0x1c, 0xa8, 0x0f, 0x16, 0x30, 0x05, 0x30, 0x5b, 0x50, 0x09, 0x2f, 0x02, 0x80, 0x2d, 0x2e, 0x82, 0x01, 0x05, 0x42, 0x05, 0x80, 0x00, 0x2e, 0x02, 0x42, 0x3e, 0x80, 0x00, 0x2e, 0x06, 0x42, 0x02, 0x30, 0x90, 0x6f, 0x3e, 0x88, 0x01, 0x40, 0x04, 0x41, 0x4c, 0x28, 0x01, 0x42, 0x07, 0x80, 0x10, 0x25, 0x24, 0x40, 0x00, 0x40, 0x00, 0xa8, 0xf5, 0x22, 0x23, 0x29, 0x44, 0x42, 0x7a, 0x82, 0x7e, 0x88, 0x43, 0x40, 0x04, 0x41, 0x00, 0xab, 0xf5, 0x23, 0xdf, 0x28, 0x43, 0x42, 0xd9, 0xa0, 0x14, 0x2f, 0x00, 0x90, 0x02, 0x2f, 0xd2, 0x6f, 0x81, 0xb2, 0x05, 0x2f, 0x63, 0x54, 0x06, 0x28, 0x90, 0x42, 0x85, 0x42, 0x09, 0x2c, 0x02, 0x30, 0x5b, 0x50, 0x03, 0x80, 0x29, 0x2e, 0x7e, 0x01, 0x2b, 0x2e, 0x82, 0x01, 0x05, 0x42, 0x12, 0x30, 0x2b, 0x2e, 0x83, 0x01, 0x45, 0x82, 0x00, 0x2e, 0x40, 0x40, 0x7a, 0x82, 0x02, 0xa0, 0x08, 0x2f, 0x63, 0x50, 0x3b, 0x30, 0x15, 0x42, 0x05, 0x42, 0x37, 0x80, 0x37, 0x2e, 0x7e, 0x01, 0x05, 0x42, 0x12, 0x30, 0x01, 0x2e, 0xc9, 0x00, 0x02, 0x8c, 0x40, 0x40, 0x84, 0x41, 0x7a, 0x8c, 0x04, 0x0f, 0x03, 0x2f, 0x01, 0x2e, 0x8b, 0x01, 0x19, 0xa4, 0x04, 0x2f, 0x2b, 0x2e, 0x82, 0x01, 0x98, 0x2e, 0xf3, 0x03, 0x12, 0x30, 0x81, 0x90, 0x61, 0x52, 0x08, 0x2f, 0x65, 0x42, 0x65, 0x42, 0x43, 0x80, 0x39, 0x84, 0x82, 0x88, 0x05, 0x42, 0x45, 0x42, 0x85, 0x42, 0x05, 0x43, 0x00, 0x2e, 0x80, 0x41, 0x00, 0x90, 0x90, 0x2e, 0xe1, 0xb4, 0x65, 0x54, 0xc1, 0x6f, 0x80, 0x40, 0x00, 0xb2, 0x43, 0x58, 0x69, 0x50, 0x44, 0x2f, 0x55, 0x5c, 0xb7, 0x87, 0x8c, 0x0f, 0x0d, 0x2e, 0x96, 0x01, 0xc4, 0x40, 0x36, 0x2f, 0x41, 0x56, 0x8b, 0x0e, 0x2a, 0x2f, 0x0b, 0x52, 0xa1, 0x0e, 0x0a, 0x2f, 0x05, 0x2e, 0x8f, 0x01, 0x14, 0x25, 0x98, 0x2e, 0xfe, 0xc9, 0x4b, 0x54, 0x02, 0x0f, 0x69, 0x50, 0x05, 0x30, 0x65, 0x54, 0x15, 0x2f, 0x03, 0x2e, 0x8e, 0x01, 0x4d, 0x5c, 0x8e, 0x0f, 0x3a, 0x2f, 0x05, 0x2e, 0x8f, 0x01, 0x98, 0x2e, 0xfe, 0xc9, 0x4f, 0x54, 0x82, 0x0f, 0x05, 0x30, 0x69, 0x50, 0x65, 0x54, 0x30, 0x2f, 0x6d, 0x52, 0x15, 0x30, 0x42, 0x8c, 0x45, 0x42, 0x04, 0x30, 0x2b, 0x2c, 0x84, 0x43, 0x6b, 0x52, 0x42, 0x8c, 0x00, 0x2e, 0x85, 0x43, 0x15, 0x30, 0x24, 0x2c, 0x45, 0x42, 0x8e, 0x0f, 0x20, 0x2f, 0x0d, 0x2e, 0x8e, 0x01, 0xb1, 0x0e, 0x1c, 0x2f, 0x23, 0x2e, 0x8e, 0x01, 0x1a, 0x2d, 0x0e, 0x0e, 0x17, 0x2f, 0xa1, 0x0f, 0x15, 0x2f, 0x23, 0x2e, 0x8d, 0x01, 0x13, 0x2d, 0x98, 0x2e, 0x74, 0xc0, 0x43, 0x54, 0xc2, 0x0e, 0x0a, 0x2f, 0x65, 0x50, 0x04, 0x80, 0x0b, 0x30, 0x06, 0x82, 0x0b, 0x42, 0x79, 0x80, 0x41, 0x40, 0x12, 0x30, 0x25, 0x2e, 0x8c, 0x01, 0x01, 0x42, 0x05, 0x30, 0x69, 0x50, 0x65, 0x54, 0x84, 0x82, 0x43, 0x84, 0xbe, 0x8c, 0x84, 0x40, 0x86, 0x41, 0x26, 0x29, 0x94, 0x42, 0xbe, 0x8e, 0xd5, 0x7f, 0x19, 0xa1, 0x43, 0x40, 0x0b, 0x2e, 0x8c, 0x01, 0x84, 0x40, 0xc7, 0x41, 0x5d, 0x29, 0x27, 0x29, 0x45, 0x42, 0x84, 0x42, 0xc2, 0x7f, 0x01, 0x2f, 0xc0, 0xb3, 0x1d, 0x2f, 0x05, 0x2e, 0x94, 0x01, 0x99, 0xa0, 0x01, 0x2f, 0x80, 0xb3, 0x13, 0x2f, 0x80, 0xb3, 0x18, 0x2f, 0xc0, 0xb3, 0x16, 0x2f, 0x12, 0x40, 0x01, 0x40, 0x92, 0x7f, 0x98, 0x2e, 0x74, 0xc0, 0x92, 0x6f, 0x10, 0x0f, 0x20, 0x30, 0x03, 0x2f, 0x10, 0x30, 0x21, 0x2e, 0x7e, 0x01, 0x0a, 0x2d, 0x21, 0x2e, 0x7e, 0x01, 0x07, 0x2d, 0x20, 0x30, 0x21, 0x2e, 0x7e, 0x01, 0x03, 0x2d, 0x10, 0x30, 0x21, 0x2e, 0x7e, 0x01, 0xc2, 0x6f, 0x01, 0x2e, 0xc9, 0x00, 0xbc, 0x84, 0x02, 0x80, 0x82, 0x40, 0x00, 0x40, 0x90, 0x0e, 0xd5, 0x6f, 0x02, 0x2f, 0x15, 0x30, 0x98, 0x2e, 0xf3, 0x03, 0x41, 0x91, 0x05, 0x30, 0x07, 0x2f, 0x67, 0x50, 0x3d, 0x80, 0x2b, 0x2e, 0x8f, 0x01, 0x05, 0x42, 0x04, 0x80, 0x00, 0x2e, 0x05, 0x42, 0x02, 0x2c, 0x00, 0x30, 0x00, 0x30, 0xa2, 0x6f, 0x98, 0x8a, 0x86, 0x40, 0x80, 0xa7, 0x05, 0x2f, 0x98, 0x2e, 0xf3, 0x03, 0xc0, 0x30, 0x21, 0x2e, 0x95, 0x01, 0x06, 0x25, 0x1a, 0x25, 0xe2, 0x6f, 0x76, 0x82, 0x96, 0x40, 0x56, 0x43, 0x51, 0x0e, 0xfb, 0x2f, 0xbb, 0x6f, 0x30, 0x5f, 0xb8, 0x2e, 0x01, 0x2e, 0xb8, 0x00, 0x01, 0x31, 0x41, 0x08, 0x40, 0xb2, 0x20, 0x50, 0xf2, 0x30, 0x02, 0x08, 0xfb, 0x7f, 0x01, 0x30, 0x10, 0x2f, 0x05, 0x2e, 0xcc, 0x00, 0x81, 0x90, 0xe0, 0x7f, 0x03, 0x2f, 0x23, 0x2e, 0xcc, 0x00, 0x98, 0x2e, 0x55, 0xb6, 0x98, 0x2e, 0x1d, 0xb5, 0x10, 0x25, 0xfb, 0x6f, 0xe0, 0x6f, 0xe0, 0x5f, 0x80, 0x2e, 0x95, 0xcf, 0x98, 0x2e, 0x95, 0xcf, 0x10, 0x30, 0x21, 0x2e, 0xcc, 0x00, 0xfb, 0x6f, 0xe0, 0x5f, 0xb8, 0x2e, 0x00, 0x51, 0x05, 0x58, 0xeb, 0x7f, 0x2a, 0x25, 0x89, 0x52, 0x6f, 0x5a, 0x89, 0x50, 0x13, 0x41, 0x06, 0x40, 0xb3, 0x01, 0x16, 0x42, 0xcb, 0x16, 0x06, 0x40, 0xf3, 0x02, 0x13, 0x42, 0x65, 0x0e, 0xf5, 0x2f, 0x05, 0x40, 0x14, 0x30, 0x2c, 0x29, 0x04, 0x42, 0x08, 0xa1, 0x00, 0x30, 0x90, 0x2e, 0x52, 0xb6, 0xb3, 0x88, 0xb0, 0x8a, 0xb6, 0x84, 0xa4, 0x7f, 0xc4, 0x7f, 0xb5, 0x7f, 0xd5, 0x7f, 0x92, 0x7f, 0x73, 0x30, 0x04, 0x30, 0x55, 0x40, 0x42, 0x40, 0x8a, 0x17, 0xf3, 0x08, 0x6b, 0x01, 0x90, 0x02, 0x53, 0xb8, 0x4b, 0x82, 0xad, 0xbe, 0x71, 0x7f, 0x45, 0x0a, 0x09, 0x54, 0x84, 0x7f, 0x98, 0x2e, 0xd9, 0xc0, 0xa3, 0x6f, 0x7b, 0x54, 0xd0, 0x42, 0xa3, 0x7f, 0xf2, 0x7f, 0x60, 0x7f, 0x20, 0x25, 0x71, 0x6f, 0x75, 0x5a, 0x77, 0x58, 0x79, 0x5c, 0x75, 0x56, 0x98, 0x2e, 0x67, 0xcc, 0xb1, 0x6f, 0x62, 0x6f, 0x50, 0x42, 0xb1, 0x7f, 0xb3, 0x30, 0x10, 0x25, 0x98, 0x2e, 0x0f, 0xca, 0x84, 0x6f, 0x20, 0x29, 0x71, 0x6f, 0x92, 0x6f, 0xa5, 0x6f, 0x76, 0x82, 0x6a, 0x0e, 0x73, 0x30, 0x00, 0x30, 0xd0, 0x2f, 0xd2, 0x6f, 0xd1, 0x7f, 0xb4, 0x7f, 0x98, 0x2e, 0x2b, 0xb7, 0x15, 0xbd, 0x0b, 0xb8, 0x02, 0x0a, 0xc2, 0x6f, 0xc0, 0x7f, 0x98, 0x2e, 0x2b, 0xb7, 0x15, 0xbd, 0x0b, 0xb8, 0x42, 0x0a, 0xc0, 0x6f, 0x08, 0x17, 0x41, 0x18, 0x89, 0x16, 0xe1, 0x18, 0xd0, 0x18, 0xa1, 0x7f, 0x27, 0x25, 0x16, 0x25, 0x98, 0x2e, 0x79, 0xc0, 0x8b, 0x54, 0x90, 0x7f, 0xb3, 0x30, 0x82, 0x40, 0x80, 0x90, 0x0d, 0x2f, 0x7d, 0x52, 0x92, 0x6f, 0x98, 0x2e, 0x0f, 0xca, 0xb2, 0x6f, 0x90, 0x0e, 0x06, 0x2f, 0x8b, 0x50, 0x14, 0x30, 0x42, 0x6f, 0x51, 0x6f, 0x14, 0x42, 0x12, 0x42, 0x01, 0x42, 0x00, 0x2e, 0x31, 0x6f, 0x98, 0x2e, 0x74, 0xc0, 0x41, 0x6f, 0x80, 0x7f, 0x98, 0x2e, 0x74, 0xc0, 0x82, 0x6f, 0x10, 0x04, 0x43, 0x52, 0x01, 0x0f, 0x05, 0x2e, 0xcb, 0x00, 0x00, 0x30, 0x04, 0x30, 0x21, 0x2f, 0x51, 0x6f, 0x43, 0x58, 0x8c, 0x0e, 0x04, 0x30, 0x1c, 0x2f, 0x85, 0x88, 0x41, 0x6f, 0x04, 0x41, 0x8c, 0x0f, 0x04, 0x30, 0x16, 0x2f, 0x84, 0x88, 0x00, 0x2e, 0x04, 0x41, 0x04, 0x05, 0x8c, 0x0e, 0x04, 0x30, 0x0f, 0x2f, 0x82, 0x88, 0x31, 0x6f, 0x04, 0x41, 0x04, 0x05, 0x8c, 0x0e, 0x04, 0x30, 0x08, 0x2f, 0x83, 0x88, 0x00, 0x2e, 0x04, 0x41, 0x8c, 0x0f, 0x04, 0x30, 0x02, 0x2f, 0x21, 0x2e, 0xad, 0x01, 0x14, 0x30, 0x00, 0x91, 0x14, 0x2f, 0x03, 0x2e, 0xa1, 0x01, 0x41, 0x90, 0x0e, 0x2f, 0x03, 0x2e, 0xad, 0x01, 0x14, 0x30, 0x4c, 0x28, 0x23, 0x2e, 0xad, 0x01, 0x46, 0xa0, 0x06, 0x2f, 0x81, 0x84, 0x8d, 0x52, 0x48, 0x82, 0x82, 0x40, 0x21, 0x2e, 0xa1, 0x01, 0x42, 0x42, 0x5c, 0x2c, 0x02, 0x30, 0x05, 0x2e, 0xaa, 0x01, 0x80, 0xb2, 0x02, 0x30, 0x55, 0x2f, 0x03, 0x2e, 0xa9, 0x01, 0x92, 0x6f, 0xb3, 0x30, 0x98, 0x2e, 0x0f, 0xca, 0xb2, 0x6f, 0x90, 0x0f, 0x00, 0x30, 0x02, 0x30, 0x4a, 0x2f, 0xa2, 0x6f, 0x87, 0x52, 0x91, 0x00, 0x85, 0x52, 0x51, 0x0e, 0x02, 0x2f, 0x00, 0x2e, 0x43, 0x2c, 0x02, 0x30, 0xc2, 0x6f, 0x7f, 0x52, 0x91, 0x0e, 0x02, 0x30, 0x3c, 0x2f, 0x51, 0x6f, 0x81, 0x54, 0x98, 0x2e, 0xfe, 0xc9, 0x10, 0x25, 0xb3, 0x30, 0x21, 0x25, 0x98, 0x2e, 0x0f, 0xca, 0x32, 0x6f, 0xc0, 0x7f, 0xb3, 0x30, 0x12, 0x25, 0x98, 0x2e, 0x0f, 0xca, 0x42, 0x6f, 0xb0, 0x7f, 0xb3, 0x30, 0x12, 0x25, 0x98, 0x2e, 0x0f, 0xca, 0xb2, 0x6f, 0x90, 0x28, 0x83, 0x52, 0x98, 0x2e, 0xfe, 0xc9, 0xc2, 0x6f, 0x90, 0x0f, 0x00, 0x30, 0x02, 0x30, 0x1d, 0x2f, 0x05, 0x2e, 0xa1, 0x01, 0x80, 0xb2, 0x12, 0x30, 0x0f, 0x2f, 0x42, 0x6f, 0x03, 0x2e, 0xab, 0x01, 0x91, 0x0e, 0x02, 0x30, 0x12, 0x2f, 0x52, 0x6f, 0x03, 0x2e, 0xac, 0x01, 0x91, 0x0f, 0x02, 0x30, 0x0c, 0x2f, 0x21, 0x2e, 0xaa, 0x01, 0x0a, 0x2c, 0x12, 0x30, 0x03, 0x2e, 0xcb, 0x00, 0x8d, 0x58, 0x08, 0x89, 0x41, 0x40, 0x11, 0x43, 0x00, 0x43, 0x25, 0x2e, 0xa1, 0x01, 0xd4, 0x6f, 0x8f, 0x52, 0x00, 0x43, 0x3a, 0x89, 0x00, 0x2e, 0x10, 0x43, 0x10, 0x43, 0x61, 0x0e, 0xfb, 0x2f, 0x03, 0x2e, 0xa0, 0x01, 0x11, 0x1a, 0x02, 0x2f, 0x02, 0x25, 0x21, 0x2e, 0xa0, 0x01, 0xeb, 0x6f, 0x00, 0x5f, 0xb8, 0x2e, 0x91, 0x52, 0x10, 0x30, 0x02, 0x30, 0x95, 0x56, 0x52, 0x42, 0x4b, 0x0e, 0xfc, 0x2f, 0x8d, 0x54, 0x88, 0x82, 0x93, 0x56, 0x80, 0x42, 0x53, 0x42, 0x40, 0x42, 0x42, 0x86, 0x83, 0x54, 0xc0, 0x2e, 0xc2, 0x42, 0x00, 0x2e, 0xa3, 0x52, 0x00, 0x51, 0x52, 0x40, 0x47, 0x40, 0x1a, 0x25, 0x01, 0x2e, 0x97, 0x00, 0x8f, 0xbe, 0x72, 0x86, 0xfb, 0x7f, 0x0b, 0x30, 0x7c, 0xbf, 0xa5, 0x50, 0x10, 0x08, 0xdf, 0xba, 0x70, 0x88, 0xf8, 0xbf, 0xcb, 0x42, 0xd3, 0x7f, 0x6c, 0xbb, 0xfc, 0xbb, 0xc5, 0x0a, 0x90, 0x7f, 0x1b, 0x7f, 0x0b, 0x43, 0xc0, 0xb2, 0xe5, 0x7f, 0xb7, 0x7f, 0xa6, 0x7f, 0xc4, 0x7f, 0x90, 0x2e, 0x1c, 0xb7, 0x07, 0x2e, 0xd2, 0x00, 0xc0, 0xb2, 0x0b, 0x2f, 0x97, 0x52, 0x01, 0x2e, 0xcd, 0x00, 0x82, 0x7f, 0x98, 0x2e, 0xbb, 0xcc, 0x0b, 0x30, 0x37, 0x2e, 0xd2, 0x00, 0x82, 0x6f, 0x90, 0x6f, 0x1a, 0x25, 0x00, 0xb2, 0x8b, 0x7f, 0x14, 0x2f, 0xa6, 0xbd, 0x25, 0xbd, 0xb6, 0xb9, 0x2f, 0xb9, 0x80, 0xb2, 0xd4, 0xb0, 0x0c, 0x2f, 0x99, 0x54, 0x9b, 0x56, 0x0b, 0x30, 0x0b, 0x2e, 0xb1, 0x00, 0xa1, 0x58, 0x9b, 0x42, 0xdb, 0x42, 0x6c, 0x09, 0x2b, 0x2e, 0xb1, 0x00, 0x8b, 0x42, 0xcb, 0x42, 0x86, 0x7f, 0x73, 0x84, 0xa7, 0x56, 0xc3, 0x08, 0x39, 0x52, 0x05, 0x50, 0x72, 0x7f, 0x63, 0x7f, 0x98, 0x2e, 0xc2, 0xc0, 0xe1, 0x6f, 0x62, 0x6f, 0xd1, 0x0a, 0x01, 0x2e, 0xcd, 0x00, 0xd5, 0x6f, 0xc4, 0x6f, 0x72, 0x6f, 0x97, 0x52, 0x9d, 0x5c, 0x98, 0x2e, 0x06, 0xcd, 0x23, 0x6f, 0x90, 0x6f, 0x99, 0x52, 0xc0, 0xb2, 0x04, 0xbd, 0x54, 0x40, 0xaf, 0xb9, 0x45, 0x40, 0xe1, 0x7f, 0x02, 0x30, 0x06, 0x2f, 0xc0, 0xb2, 0x02, 0x30, 0x03, 0x2f, 0x9b, 0x5c, 0x12, 0x30, 0x94, 0x43, 0x85, 0x43, 0x03, 0xbf, 0x6f, 0xbb, 0x80, 0xb3, 0x20, 0x2f, 0x06, 0x6f, 0x26, 0x01, 0x16, 0x6f, 0x6e, 0x03, 0x45, 0x42, 0xc0, 0x90, 0x29, 0x2e, 0xce, 0x00, 0x9b, 0x52, 0x14, 0x2f, 0x9b, 0x5c, 0x00, 0x2e, 0x93, 0x41, 0x86, 0x41, 0xe3, 0x04, 0xae, 0x07, 0x80, 0xab, 0x04, 0x2f, 0x80, 0x91, 0x0a, 0x2f, 0x86, 0x6f, 0x73, 0x0f, 0x07, 0x2f, 0x83, 0x6f, 0xc0, 0xb2, 0x04, 0x2f, 0x54, 0x42, 0x45, 0x42, 0x12, 0x30, 0x04, 0x2c, 0x11, 0x30, 0x02, 0x2c, 0x11, 0x30, 0x11, 0x30, 0x02, 0xbc, 0x0f, 0xb8, 0xd2, 0x7f, 0x00, 0xb2, 0x0a, 0x2f, 0x01, 0x2e, 0xfc, 0x00, 0x05, 0x2e, 0xc7, 0x01, 0x10, 0x1a, 0x02, 0x2f, 0x21, 0x2e, 0xc7, 0x01, 0x03, 0x2d, 0x02, 0x2c, 0x01, 0x30, 0x01, 0x30, 0xb0, 0x6f, 0x98, 0x2e, 0x95, 0xcf, 0xd1, 0x6f, 0xa0, 0x6f, 0x98, 0x2e, 0x95, 0xcf, 0xe2, 0x6f, 0x9f, 0x52, 0x01, 0x2e, 0xce, 0x00, 0x82, 0x40, 0x50, 0x42, 0x0c, 0x2c, 0x42, 0x42, 0x11, 0x30, 0x23, 0x2e, 0xd2, 0x00, 0x01, 0x30, 0xb0, 0x6f, 0x98, 0x2e, 0x95, 0xcf, 0xa0, 0x6f, 0x01, 0x30, 0x98, 0x2e, 0x95, 0xcf, 0x00, 0x2e, 0xfb, 0x6f, 0x00, 0x5f, 0xb8, 0x2e, 0x83, 0x86, 0x01, 0x30, 0x00, 0x30, 0x94, 0x40, 0x24, 0x18, 0x06, 0x00, 0x53, 0x0e, 0x4f, 0x02, 0xf9, 0x2f, 0xb8, 0x2e, 0xa9, 0x52, 0x00, 0x2e, 0x60, 0x40, 0x41, 0x40, 0x0d, 0xbc, 0x98, 0xbc, 0xc0, 0x2e, 0x01, 0x0a, 0x0f, 0xb8, 0xab, 0x52, 0x53, 0x3c, 0x52, 0x40, 0x40, 0x40, 0x4b, 0x00, 0x82, 0x16, 0x26, 0xb9, 0x01, 0xb8, 0x41, 0x40, 0x10, 0x08, 0x97, 0xb8, 0x01, 0x08, 0xc0, 0x2e, 0x11, 0x30, 0x01, 0x08, 0x43, 0x86, 0x25, 0x40, 0x04, 0x40, 0xd8, 0xbe, 0x2c, 0x0b, 0x22, 0x11, 0x54, 0x42, 0x03, 0x80, 0x4b, 0x0e, 0xf6, 0x2f, 0xb8, 0x2e, 0x9f, 0x50, 0x10, 0x50, 0xad, 0x52, 0x05, 0x2e, 0xd3, 0x00, 0xfb, 0x7f, 0x00, 0x2e, 0x13, 0x40, 0x93, 0x42, 0x41, 0x0e, 0xfb, 0x2f, 0x98, 0x2e, 0xa5, 0xb7, 0x98, 0x2e, 0x87, 0xcf, 0x01, 0x2e, 0xd9, 0x00, 0x00, 0xb2, 0xfb, 0x6f, 0x0b, 0x2f, 0x01, 0x2e, 0x69, 0xf7, 0xb1, 0x3f, 0x01, 0x08, 0x01, 0x30, 0xf0, 0x5f, 0x23, 0x2e, 0xd9, 0x00, 0x21, 0x2e, 0x69, 0xf7, 0x80, 0x2e, 0x7a, 0xb7, 0xf0, 0x5f, 0xb8, 0x2e, 0x01, 0x2e, 0xc0, 0xf8, 0x03, 0x2e, 0xfc, 0xf5, 0x15, 0x54, 0xaf, 0x56, 0x82, 0x08, 0x0b, 0x2e, 0x69, 0xf7, 0xcb, 0x0a, 0xb1, 0x58, 0x80, 0x90, 0xdd, 0xbe, 0x4c, 0x08, 0x5f, 0xb9, 0x59, 0x22, 0x80, 0x90, 0x07, 0x2f, 0x03, 0x34, 0xc3, 0x08, 0xf2, 0x3a, 0x0a, 0x08, 0x02, 0x35, 0xc0, 0x90, 0x4a, 0x0a, 0x48, 0x22, 0xc0, 0x2e, 0x23, 0x2e, 0xfc, 0xf5, 0x10, 0x50, 0xfb, 0x7f, 0x98, 0x2e, 0x56, 0xc7, 0x98, 0x2e, 0x49, 0xc3, 0x10, 0x30, 0xfb, 0x6f, 0xf0, 0x5f, 0x21, 0x2e, 0xcc, 0x00, 0x21, 0x2e, 0xca, 0x00, 0xb8, 0x2e, 0x03, 0x2e, 0xd3, 0x00, 0x16, 0xb8, 0x02, 0x34, 0x4a, 0x0c, 0x21, 0x2e, 0x2d, 0xf5, 0xc0, 0x2e, 0x23, 0x2e, 0xd3, 0x00, 0x03, 0xbc, 0x21, 0x2e, 0xd5, 0x00, 0x03, 0x2e, 0xd5, 0x00, 0x40, 0xb2, 0x10, 0x30, 0x21, 0x2e, 0x77, 0x00, 0x01, 0x30, 0x05, 0x2f, 0x05, 0x2e, 0xd8, 0x00, 0x80, 0x90, 0x01, 0x2f, 0x23, 0x2e, 0x6f, 0xf5, 0xc0, 0x2e, 0x21, 0x2e, 0xd9, 0x00, 0x11, 0x30, 0x81, 0x08, 0x01, 0x2e, 0x6a, 0xf7, 0x71, 0x3f, 0x23, 0xbd, 0x01, 0x08, 0x02, 0x0a, 0xc0, 0x2e, 0x21, 0x2e, 0x6a, 0xf7, 0x30, 0x25, 0x00, 0x30, 0x21, 0x2e, 0x5a, 0xf5, 0x10, 0x50, 0x21, 0x2e, 0x7b, 0x00, 0x21, 0x2e, 0x7c, 0x00, 0xfb, 0x7f, 0x98, 0x2e, 0xc3, 0xb7, 0x40, 0x30, 0x21, 0x2e, 0xd4, 0x00, 0xfb, 0x6f, 0xf0, 0x5f, 0x03, 0x25, 0x80, 0x2e, 0xaf, 0xb7, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x01, 0x2e, 0x5d, 0xf7, 0x08, 0xbc, 0x80, 0xac, 0x0e, 0xbb, 0x02, 0x2f, 0x00, 0x30, 0x41, 0x04, 0x82, 0x06, 0xc0, 0xa4, 0x00, 0x30, 0x11, 0x2f, 0x40, 0xa9, 0x03, 0x2f, 0x40, 0x91, 0x0d, 0x2f, 0x00, 0xa7, 0x0b, 0x2f, 0x80, 0xb3, 0xb3, 0x58, 0x02, 0x2f, 0x90, 0xa1, 0x26, 0x13, 0x20, 0x23, 0x80, 0x90, 0x10, 0x30, 0x01, 0x2f, 0xcc, 0x0e, 0x00, 0x2f, 0x00, 0x30, 0xb8, 0x2e, 0xb5, 0x50, 0x18, 0x08, 0x08, 0xbc, 0x88, 0xb6, 0x0d, 0x17, 0xc6, 0xbd, 0x56, 0xbc, 0xb7, 0x58, 0xda, 0xba, 0x04, 0x01, 0x1d, 0x0a, 0x10, 0x50, 0x05, 0x30, 0x32, 0x25, 0x45, 0x03, 0xfb, 0x7f, 0xf6, 0x30, 0x21, 0x25, 0x98, 0x2e, 0x37, 0xca, 0x16, 0xb5, 0x9a, 0xbc, 0x06, 0xb8, 0x80, 0xa8, 0x41, 0x0a, 0x0e, 0x2f, 0x80, 0x90, 0x02, 0x2f, 0x2d, 0x50, 0x48, 0x0f, 0x09, 0x2f, 0xbf, 0xa0, 0x04, 0x2f, 0xbf, 0x90, 0x06, 0x2f, 0xb7, 0x54, 0xca, 0x0f, 0x03, 0x2f, 0x00, 0x2e, 0x02, 0x2c, 0xb7, 0x52, 0x2d, 0x52, 0xf2, 0x33, 0x98, 0x2e, 0xd9, 0xc0, 0xfb, 0x6f, 0xf1, 0x37, 0xc0, 0x2e, 0x01, 0x08, 0xf0, 0x5f, 0xbf, 0x56, 0xb9, 0x54, 0xd0, 0x40, 0xc4, 0x40, 0x0b, 0x2e, 0xfd, 0xf3, 0xbf, 0x52, 0x90, 0x42, 0x94, 0x42, 0x95, 0x42, 0x05, 0x30, 0xc1, 0x50, 0x0f, 0x88, 0x06, 0x40, 0x04, 0x41, 0x96, 0x42, 0xc5, 0x42, 0x48, 0xbe, 0x73, 0x30, 0x0d, 0x2e, 0xd8, 0x00, 0x4f, 0xba, 0x84, 0x42, 0x03, 0x42, 0x81, 0xb3, 0x02, 0x2f, 0x2b, 0x2e, 0x6f, 0xf5, 0x06, 0x2d, 0x05, 0x2e, 0x77, 0xf7, 0xbd, 0x56, 0x93, 0x08, 0x25, 0x2e, 0x77, 0xf7, 0xbb, 0x54, 0x25, 0x2e, 0xc2, 0xf5, 0x07, 0x2e, 0xfd, 0xf3, 0x42, 0x30, 0xb4, 0x33, 0xda, 0x0a, 0x4c, 0x00, 0x27, 0x2e, 0xfd, 0xf3, 0x43, 0x40, 0xd4, 0x3f, 0xdc, 0x08, 0x43, 0x42, 0x00, 0x2e, 0x00, 0x2e, 0x43, 0x40, 0x24, 0x30, 0xdc, 0x0a, 0x43, 0x42, 0x04, 0x80, 0x03, 0x2e, 0xfd, 0xf3, 0x4a, 0x0a, 0x23, 0x2e, 0xfd, 0xf3, 0x61, 0x34, 0xc0, 0x2e, 0x01, 0x42, 0x00, 0x2e, 0x60, 0x50, 0x1a, 0x25, 0x7a, 0x86, 0xe0, 0x7f, 0xf3, 0x7f, 0x03, 0x25, 0xc3, 0x52, 0x41, 0x84, 0xdb, 0x7f, 0x33, 0x30, 0x98, 0x2e, 0x16, 0xc2, 0x1a, 0x25, 0x7d, 0x82, 0xf0, 0x6f, 0xe2, 0x6f, 0x32, 0x25, 0x16, 0x40, 0x94, 0x40, 0x26, 0x01, 0x85, 0x40, 0x8e, 0x17, 0xc4, 0x42, 0x6e, 0x03, 0x95, 0x42, 0x41, 0x0e, 0xf4, 0x2f, 0xdb, 0x6f, 0xa0, 0x5f, 0xb8, 0x2e, 0xb0, 0x51, 0xfb, 0x7f, 0x98, 0x2e, 0xe8, 0x0d, 0x5a, 0x25, 0x98, 0x2e, 0x0f, 0x0e, 0xcb, 0x58, 0x32, 0x87, 0xc4, 0x7f, 0x65, 0x89, 0x6b, 0x8d, 0xc5, 0x5a, 0x65, 0x7f, 0xe1, 0x7f, 0x83, 0x7f, 0xa6, 0x7f, 0x74, 0x7f, 0xd0, 0x7f, 0xb6, 0x7f, 0x94, 0x7f, 0x17, 0x30, 0xc7, 0x52, 0xc9, 0x54, 0x51, 0x7f, 0x00, 0x2e, 0x85, 0x6f, 0x42, 0x7f, 0x00, 0x2e, 0x51, 0x41, 0x45, 0x81, 0x42, 0x41, 0x13, 0x40, 0x3b, 0x8a, 0x00, 0x40, 0x4b, 0x04, 0xd0, 0x06, 0xc0, 0xac, 0x85, 0x7f, 0x02, 0x2f, 0x02, 0x30, 0x51, 0x04, 0xd3, 0x06, 0x41, 0x84, 0x05, 0x30, 0x5d, 0x02, 0xc9, 0x16, 0xdf, 0x08, 0xd3, 0x00, 0x8d, 0x02, 0xaf, 0xbc, 0xb1, 0xb9, 0x59, 0x0a, 0x65, 0x6f, 0x11, 0x43, 0xa1, 0xb4, 0x52, 0x41, 0x53, 0x41, 0x01, 0x43, 0x34, 0x7f, 0x65, 0x7f, 0x26, 0x31, 0xe5, 0x6f, 0xd4, 0x6f, 0x98, 0x2e, 0x37, 0xca, 0x32, 0x6f, 0x75, 0x6f, 0x83, 0x40, 0x42, 0x41, 0x23, 0x7f, 0x12, 0x7f, 0xf6, 0x30, 0x40, 0x25, 0x51, 0x25, 0x98, 0x2e, 0x37, 0xca, 0x14, 0x6f, 0x20, 0x05, 0x70, 0x6f, 0x25, 0x6f, 0x69, 0x07, 0xa2, 0x6f, 0x31, 0x6f, 0x0b, 0x30, 0x04, 0x42, 0x9b, 0x42, 0x8b, 0x42, 0x55, 0x42, 0x32, 0x7f, 0x40, 0xa9, 0xc3, 0x6f, 0x71, 0x7f, 0x02, 0x30, 0xd0, 0x40, 0xc3, 0x7f, 0x03, 0x2f, 0x40, 0x91, 0x15, 0x2f, 0x00, 0xa7, 0x13, 0x2f, 0x00, 0xa4, 0x11, 0x2f, 0x84, 0xbd, 0x98, 0x2e, 0x79, 0xca, 0x55, 0x6f, 0xb7, 0x54, 0x54, 0x41, 0x82, 0x00, 0xf3, 0x3f, 0x45, 0x41, 0xcb, 0x02, 0xf6, 0x30, 0x98, 0x2e, 0x37, 0xca, 0x35, 0x6f, 0xa4, 0x6f, 0x41, 0x43, 0x03, 0x2c, 0x00, 0x43, 0xa4, 0x6f, 0x35, 0x6f, 0x17, 0x30, 0x42, 0x6f, 0x51, 0x6f, 0x93, 0x40, 0x42, 0x82, 0x00, 0x41, 0xc3, 0x00, 0x03, 0x43, 0x51, 0x7f, 0x00, 0x2e, 0x94, 0x40, 0x41, 0x41, 0x4c, 0x02, 0xc4, 0x6f, 0xd1, 0x56, 0x63, 0x0e, 0x74, 0x6f, 0x51, 0x43, 0xa5, 0x7f, 0x8a, 0x2f, 0x09, 0x2e, 0xd8, 0x00, 0x01, 0xb3, 0x21, 0x2f, 0xcb, 0x58, 0x90, 0x6f, 0x13, 0x41, 0xb6, 0x6f, 0xe4, 0x7f, 0x00, 0x2e, 0x91, 0x41, 0x14, 0x40, 0x92, 0x41, 0x15, 0x40, 0x17, 0x2e, 0x6f, 0xf5, 0xb6, 0x7f, 0xd0, 0x7f, 0xcb, 0x7f, 0x98, 0x2e, 0x00, 0x0c, 0x07, 0x15, 0xc2, 0x6f, 0x14, 0x0b, 0x29, 0x2e, 0x6f, 0xf5, 0xc3, 0xa3, 0xc1, 0x8f, 0xe4, 0x6f, 0xd0, 0x6f, 0xe6, 0x2f, 0x14, 0x30, 0x05, 0x2e, 0x6f, 0xf5, 0x14, 0x0b, 0x29, 0x2e, 0x6f, 0xf5, 0x18, 0x2d, 0xcd, 0x56, 0x04, 0x32, 0xb5, 0x6f, 0x1c, 0x01, 0x51, 0x41, 0x52, 0x41, 0xc3, 0x40, 0xb5, 0x7f, 0xe4, 0x7f, 0x98, 0x2e, 0x1f, 0x0c, 0xe4, 0x6f, 0x21, 0x87, 0x00, 0x43, 0x04, 0x32, 0xcf, 0x54, 0x5a, 0x0e, 0xef, 0x2f, 0x15, 0x54, 0x09, 0x2e, 0x77, 0xf7, 0x22, 0x0b, 0x29, 0x2e, 0x77, 0xf7, 0xfb, 0x6f, 0x50, 0x5e, 0xb8, 0x2e, 0x10, 0x50, 0x01, 0x2e, 0xd4, 0x00, 0x00, 0xb2, 0xfb, 0x7f, 0x51, 0x2f, 0x01, 0xb2, 0x48, 0x2f, 0x02, 0xb2, 0x42, 0x2f, 0x03, 0x90, 0x56, 0x2f, 0xd7, 0x52, 0x79, 0x80, 0x42, 0x40, 0x81, 0x84, 0x00, 0x40, 0x42, 0x42, 0x98, 0x2e, 0x93, 0x0c, 0xd9, 0x54, 0xd7, 0x50, 0xa1, 0x40, 0x98, 0xbd, 0x82, 0x40, 0x3e, 0x82, 0xda, 0x0a, 0x44, 0x40, 0x8b, 0x16, 0xe3, 0x00, 0x53, 0x42, 0x00, 0x2e, 0x43, 0x40, 0x9a, 0x02, 0x52, 0x42, 0x00, 0x2e, 0x41, 0x40, 0x15, 0x54, 0x4a, 0x0e, 0x3a, 0x2f, 0x3a, 0x82, 0x00, 0x30, 0x41, 0x40, 0x21, 0x2e, 0x85, 0x0f, 0x40, 0xb2, 0x0a, 0x2f, 0x98, 0x2e, 0xb1, 0x0c, 0x98, 0x2e, 0x45, 0x0e, 0x98, 0x2e, 0x5b, 0x0e, 0xfb, 0x6f, 0xf0, 0x5f, 0x00, 0x30, 0x80, 0x2e, 0xce, 0xb7, 0xdd, 0x52, 0xd3, 0x54, 0x42, 0x42, 0x4f, 0x84, 0x73, 0x30, 0xdb, 0x52, 0x83, 0x42, 0x1b, 0x30, 0x6b, 0x42, 0x23, 0x30, 0x27, 0x2e, 0xd7, 0x00, 0x37, 0x2e, 0xd4, 0x00, 0x21, 0x2e, 0xd6, 0x00, 0x7a, 0x84, 0x17, 0x2c, 0x42, 0x42, 0x30, 0x30, 0x21, 0x2e, 0xd4, 0x00, 0x12, 0x2d, 0x21, 0x30, 0x00, 0x30, 0x23, 0x2e, 0xd4, 0x00, 0x21, 0x2e, 0x7b, 0xf7, 0x0b, 0x2d, 0x17, 0x30, 0x98, 0x2e, 0x51, 0x0c, 0xd5, 0x50, 0x0c, 0x82, 0x72, 0x30, 0x2f, 0x2e, 0xd4, 0x00, 0x25, 0x2e, 0x7b, 0xf7, 0x40, 0x42, 0x00, 0x2e, 0xfb, 0x6f, 0xf0, 0x5f, 0xb8, 0x2e, 0x70, 0x50, 0x0a, 0x25, 0x39, 0x86, 0xfb, 0x7f, 0xe1, 0x32, 0x62, 0x30, 0x98, 0x2e, 0xc2, 0xc4, 0xb5, 0x56, 0xa5, 0x6f, 0xab, 0x08, 0x91, 0x6f, 0x4b, 0x08, 0xdf, 0x56, 0xc4, 0x6f, 0x23, 0x09, 0x4d, 0xba, 0x93, 0xbc, 0x8c, 0x0b, 0xd1, 0x6f, 0x0b, 0x09, 0xcb, 0x52, 0xe1, 0x5e, 0x56, 0x42, 0xaf, 0x09, 0x4d, 0xba, 0x23, 0xbd, 0x94, 0x0a, 0xe5, 0x6f, 0x68, 0xbb, 0xeb, 0x08, 0xbd, 0xb9, 0x63, 0xbe, 0xfb, 0x6f, 0x52, 0x42, 0xe3, 0x0a, 0xc0, 0x2e, 0x43, 0x42, 0x90, 0x5f, 0xd1, 0x50, 0x03, 0x2e, 0x25, 0xf3, 0x13, 0x40, 0x00, 0x40, 0x9b, 0xbc, 0x9b, 0xb4, 0x08, 0xbd, 0xb8, 0xb9, 0x98, 0xbc, 0xda, 0x0a, 0x08, 0xb6, 0x89, 0x16, 0xc0, 0x2e, 0x19, 0x00, 0x62, 0x02, 0x10, 0x50, 0xfb, 0x7f, 0x98, 0x2e, 0x81, 0x0d, 0x01, 0x2e, 0xd4, 0x00, 0x31, 0x30, 0x08, 0x04, 0xfb, 0x6f, 0x01, 0x30, 0xf0, 0x5f, 0x23, 0x2e, 0xd6, 0x00, 0x21, 0x2e, 0xd7, 0x00, 0xb8, 0x2e, 0x01, 0x2e, 0xd7, 0x00, 0x03, 0x2e, 0xd6, 0x00, 0x48, 0x0e, 0x01, 0x2f, 0x80, 0x2e, 0x1f, 0x0e, 0xb8, 0x2e, 0xe3, 0x50, 0x21, 0x34, 0x01, 0x42, 0x82, 0x30, 0xc1, 0x32, 0x25, 0x2e, 0x62, 0xf5, 0x01, 0x00, 0x22, 0x30, 0x01, 0x40, 0x4a, 0x0a, 0x01, 0x42, 0xb8, 0x2e, 0xe3, 0x54, 0xf0, 0x3b, 0x83, 0x40, 0xd8, 0x08, 0xe5, 0x52, 0x83, 0x42, 0x00, 0x30, 0x83, 0x30, 0x50, 0x42, 0xc4, 0x32, 0x27, 0x2e, 0x64, 0xf5, 0x94, 0x00, 0x50, 0x42, 0x40, 0x42, 0xd3, 0x3f, 0x84, 0x40, 0x7d, 0x82, 0xe3, 0x08, 0x40, 0x42, 0x83, 0x42, 0xb8, 0x2e, 0xdd, 0x52, 0x00, 0x30, 0x40, 0x42, 0x7c, 0x86, 0xb9, 0x52, 0x09, 0x2e, 0x70, 0x0f, 0xbf, 0x54, 0xc4, 0x42, 0xd3, 0x86, 0x54, 0x40, 0x55, 0x40, 0x94, 0x42, 0x85, 0x42, 0x21, 0x2e, 0xd7, 0x00, 0x42, 0x40, 0x25, 0x2e, 0xfd, 0xf3, 0xc0, 0x42, 0x7e, 0x82, 0x05, 0x2e, 0x7d, 0x00, 0x80, 0xb2, 0x14, 0x2f, 0x05, 0x2e, 0x89, 0x00, 0x27, 0xbd, 0x2f, 0xb9, 0x80, 0x90, 0x02, 0x2f, 0x21, 0x2e, 0x6f, 0xf5, 0x0c, 0x2d, 0x07, 0x2e, 0x71, 0x0f, 0x14, 0x30, 0x1c, 0x09, 0x05, 0x2e, 0x77, 0xf7, 0xbd, 0x56, 0x47, 0xbe, 0x93, 0x08, 0x94, 0x0a, 0x25, 0x2e, 0x77, 0xf7, 0xe7, 0x54, 0x50, 0x42, 0x4a, 0x0e, 0xfc, 0x2f, 0xb8, 0x2e, 0x50, 0x50, 0x02, 0x30, 0x43, 0x86, 0xe5, 0x50, 0xfb, 0x7f, 0xe3, 0x7f, 0xd2, 0x7f, 0xc0, 0x7f, 0xb1, 0x7f, 0x00, 0x2e, 0x41, 0x40, 0x00, 0x40, 0x48, 0x04, 0x98, 0x2e, 0x74, 0xc0, 0x1e, 0xaa, 0xd3, 0x6f, 0x14, 0x30, 0xb1, 0x6f, 0xe3, 0x22, 0xc0, 0x6f, 0x52, 0x40, 0xe4, 0x6f, 0x4c, 0x0e, 0x12, 0x42, 0xd3, 0x7f, 0xeb, 0x2f, 0x03, 0x2e, 0x86, 0x0f, 0x40, 0x90, 0x11, 0x30, 0x03, 0x2f, 0x23, 0x2e, 0x86, 0x0f, 0x02, 0x2c, 0x00, 0x30, 0xd0, 0x6f, 0xfb, 0x6f, 0xb0, 0x5f, 0xb8, 0x2e, 0x40, 0x50, 0xf1, 0x7f, 0x0a, 0x25, 0x3c, 0x86, 0xeb, 0x7f, 0x41, 0x33, 0x22, 0x30, 0x98, 0x2e, 0xc2, 0xc4, 0xd3, 0x6f, 0xf4, 0x30, 0xdc, 0x09, 0x47, 0x58, 0xc2, 0x6f, 0x94, 0x09, 0xeb, 0x58, 0x6a, 0xbb, 0xdc, 0x08, 0xb4, 0xb9, 0xb1, 0xbd, 0xe9, 0x5a, 0x95, 0x08, 0x21, 0xbd, 0xf6, 0xbf, 0x77, 0x0b, 0x51, 0xbe, 0xf1, 0x6f, 0xeb, 0x6f, 0x52, 0x42, 0x54, 0x42, 0xc0, 0x2e, 0x43, 0x42, 0xc0, 0x5f, 0x50, 0x50, 0xf5, 0x50, 0x31, 0x30, 0x11, 0x42, 0xfb, 0x7f, 0x7b, 0x30, 0x0b, 0x42, 0x11, 0x30, 0x02, 0x80, 0x23, 0x33, 0x01, 0x42, 0x03, 0x00, 0x07, 0x2e, 0x80, 0x03, 0x05, 0x2e, 0xd3, 0x00, 0x23, 0x52, 0xe2, 0x7f, 0xd3, 0x7f, 0xc0, 0x7f, 0x98, 0x2e, 0xb6, 0x0e, 0xd1, 0x6f, 0x08, 0x0a, 0x1a, 0x25, 0x7b, 0x86, 0xd0, 0x7f, 0x01, 0x33, 0x12, 0x30, 0x98, 0x2e, 0xc2, 0xc4, 0xd1, 0x6f, 0x08, 0x0a, 0x00, 0xb2, 0x0d, 0x2f, 0xe3, 0x6f, 0x01, 0x2e, 0x80, 0x03, 0x51, 0x30, 0xc7, 0x86, 0x23, 0x2e, 0x21, 0xf2, 0x08, 0xbc, 0xc0, 0x42, 0x98, 0x2e, 0xa5, 0xb7, 0x00, 0x2e, 0x00, 0x2e, 0xd0, 0x2e, 0xb0, 0x6f, 0x0b, 0xb8, 0x03, 0x2e, 0x1b, 0x00, 0x08, 0x1a, 0xb0, 0x7f, 0x70, 0x30, 0x04, 0x2f, 0x21, 0x2e, 0x21, 0xf2, 0x00, 0x2e, 0x00, 0x2e, 0xd0, 0x2e, 0x98, 0x2e, 0x6d, 0xc0, 0x98, 0x2e, 0x5d, 0xc0, 0xed, 0x50, 0x98, 0x2e, 0x44, 0xcb, 0xef, 0x50, 0x98, 0x2e, 0x46, 0xc3, 0xf1, 0x50, 0x98, 0x2e, 0x53, 0xc7, 0x35, 0x50, 0x98, 0x2e, 0x64, 0xcf, 0x10, 0x30, 0x98, 0x2e, 0xdc, 0x03, 0x20, 0x26, 0xc0, 0x6f, 0x02, 0x31, 0x12, 0x42, 0xab, 0x33, 0x0b, 0x42, 0x37, 0x80, 0x01, 0x30, 0x01, 0x42, 0xf3, 0x37, 0xf7, 0x52, 0xfb, 0x50, 0x44, 0x40, 0xa2, 0x0a, 0x42, 0x42, 0x8b, 0x31, 0x09, 0x2e, 0x5e, 0xf7, 0xf9, 0x54, 0xe3, 0x08, 0x83, 0x42, 0x1b, 0x42, 0x23, 0x33, 0x4b, 0x00, 0xbc, 0x84, 0x0b, 0x40, 0x33, 0x30, 0x83, 0x42, 0x0b, 0x42, 0xe0, 0x7f, 0xd1, 0x7f, 0x98, 0x2e, 0x58, 0xb7, 0xd1, 0x6f, 0x80, 0x30, 0x40, 0x42, 0x03, 0x30, 0xe0, 0x6f, 0xf3, 0x54, 0x04, 0x30, 0x00, 0x2e, 0x00, 0x2e, 0x01, 0x89, 0x62, 0x0e, 0xfa, 0x2f, 0x43, 0x42, 0x11, 0x30, 0xfb, 0x6f, 0xc0, 0x2e, 0x01, 0x42, 0xb0, 0x5f, 0xc1, 0x4a, 0x00, 0x00, 0x6d, 0x57, 0x00, 0x00, 0x77, 0x8e, 0x00, 0x00, 0xe0, 0xff, 0xff, 0xff, 0xd3, 0xff, 0xff, 0xff, 0xe5, 0xff, 0xff, 0xff, 0xee, 0xe1, 0xff, 0xff, 0x7c, 0x13, 0x00, 0x00, 0x46, 0xe6, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1}; #define BMI270_CONFIG_FILE_SIZE sizeof(bmi270_config_file) BMI270Sensor::BMI270Sensor(ScanI2C::FoundDevice foundDevice) : MotionSensor::MotionSensor(foundDevice) { if (foundDevice.address.port == ScanI2C::I2CPort::WIRE1) { #ifdef I2C_SDA1 wire = &Wire1; #else wire = &Wire; #endif } else { wire = &Wire; } } bool BMI270Sensor::writeRegister(uint8_t reg, uint8_t value) { wire->beginTransmission(deviceAddress()); wire->write(reg); wire->write(value); return wire->endTransmission() == 0; } bool BMI270Sensor::writeRegisters(uint8_t reg, const uint8_t *data, size_t len) { wire->beginTransmission(deviceAddress()); wire->write(reg); for (size_t i = 0; i < len; i++) { wire->write(data[i]); } return wire->endTransmission() == 0; } uint8_t BMI270Sensor::readRegister(uint8_t reg) { wire->beginTransmission(deviceAddress()); wire->write(reg); wire->endTransmission(false); wire->requestFrom(deviceAddress(), (uint8_t)1); if (wire->available()) { return wire->read(); } return 0; } bool BMI270Sensor::readRegisters(uint8_t reg, uint8_t *data, size_t len) { wire->beginTransmission(deviceAddress()); wire->write(reg); wire->endTransmission(false); size_t bytesRead = wire->requestFrom(deviceAddress(), (uint8_t)len); if (bytesRead != len) { // Read any available bytes to keep the bus state clean, but report failure. for (size_t i = 0; i < bytesRead && wire->available(); i++) { data[i] = wire->read(); } return false; } for (size_t i = 0; i < len && wire->available(); i++) { data[i] = wire->read(); } return true; } bool BMI270Sensor::uploadConfigFile() { if (!writeRegister(BMI270_REG_INIT_CTRL, 0x00)) return false; const size_t chunkSize = 32; size_t bytesWritten = 0; while (bytesWritten < BMI270_CONFIG_FILE_SIZE) { size_t remaining = BMI270_CONFIG_FILE_SIZE - bytesWritten; size_t toWrite = (remaining < chunkSize) ? remaining : chunkSize; // Set address in word units before each chunk uint16_t wordAddr = bytesWritten / 2; if (!writeRegister(BMI270_REG_INIT_ADDR_0, (uint8_t)(wordAddr & 0x0F))) return false; if (!writeRegister(BMI270_REG_INIT_ADDR_1, (uint8_t)(wordAddr >> 4))) return false; wire->beginTransmission(deviceAddress()); wire->write(BMI270_REG_INIT_DATA); for (size_t i = 0; i < toWrite; i++) { wire->write(pgm_read_byte(&bmi270_config_file[bytesWritten + i])); } if (wire->endTransmission() != 0) return false; bytesWritten += toWrite; } if (!writeRegister(BMI270_REG_INIT_CTRL, 0x01)) return false; delay(50); for (int i = 0; i < 10; i++) { uint8_t status = readRegister(BMI270_REG_INTERNAL_STATUS); if ((status & 0x0F) == BMI270_INIT_OK) return true; delay(20); } LOG_WARN("BMI270 status=0x%02X", readRegister(BMI270_REG_INTERNAL_STATUS)); return false; } bool BMI270Sensor::init() { delay(10); writeRegister(BMI270_REG_CMD, BMI270_CMD_SOFTRESET); delay(50); if (!writeRegister(BMI270_REG_PWR_CONF, BMI270_PWR_CONF_ADV_POWER_SAVE_DISABLED)) return false; delay(2); if (!uploadConfigFile()) { LOG_WARN("BMI270 config failed"); return false; } uint8_t accConf = BMI270_ACC_ODR_50HZ | BMI270_ACC_BWP_NORMAL | BMI270_ACC_FILTER_PERF; if (!writeRegister(BMI270_REG_ACC_CONF, accConf) || !writeRegister(BMI270_REG_ACC_RANGE, BMI270_ACC_RANGE_2G) || !writeRegister(BMI270_REG_PWR_CTRL, BMI270_PWR_CTRL_ACC_EN)) return false; delay(50); initialized = true; return true; } int32_t BMI270Sensor::runOnce() { if (!initialized) { return MOTION_SENSOR_CHECK_INTERVAL_MS; } // Read accelerometer data (6 bytes) uint8_t data[6]; if (!readRegisters(BMI270_REG_ACC_X_LSB, data, 6)) { return MOTION_SENSOR_CHECK_INTERVAL_MS; } // Convert to 16-bit signed values int16_t x = (int16_t)((data[1] << 8) | data[0]); int16_t y = (int16_t)((data[3] << 8) | data[2]); int16_t z = (int16_t)((data[5] << 8) | data[4]); if (!hasBaseline) { prevX = x; prevY = y; prevZ = z; hasBaseline = true; return MOTION_SENSOR_CHECK_INTERVAL_MS; } // Calculate change in acceleration int16_t deltaX = abs(x - prevX); int16_t deltaY = abs(y - prevY); int16_t deltaZ = abs(z - prevZ); // Update baseline with low-pass filter prevX = (prevX * 9 + x) / 10; prevY = (prevY * 9 + y) / 10; prevZ = (prevZ * 9 + z) / 10; // Check for significant motion (~0.2g at 2g range) const int16_t threshold = 3200; if (deltaX > threshold || deltaY > threshold || deltaZ > threshold) { wakeScreen(); return 500; } return MOTION_SENSOR_CHECK_INTERVAL_MS; } #endif ================================================ FILE: src/motion/BMI270Sensor.h ================================================ #pragma once #ifndef _BMI270_SENSOR_H_ #define _BMI270_SENSOR_H_ #include "MotionSensor.h" #if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && defined(HAS_BMI270) class BMI270Sensor : public MotionSensor { private: bool initialized = false; TwoWire *wire = nullptr; // Previous readings for motion detection int16_t prevX = 0, prevY = 0, prevZ = 0; bool hasBaseline = false; // BMI270 register access bool writeRegister(uint8_t reg, uint8_t value); bool writeRegisters(uint8_t reg, const uint8_t *data, size_t len); uint8_t readRegister(uint8_t reg); bool readRegisters(uint8_t reg, uint8_t *data, size_t len); // Config file upload (BMI270 requires 8KB config blob) bool uploadConfigFile(); public: explicit BMI270Sensor(ScanI2C::FoundDevice foundDevice); virtual bool init() override; virtual int32_t runOnce() override; }; #endif #endif ================================================ FILE: src/motion/BMM150Sensor.cpp ================================================ #include "BMM150Sensor.h" #if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && __has_include() #if !defined(MESHTASTIC_EXCLUDE_SCREEN) // screen is defined in main.cpp extern graphics::Screen *screen; #endif // Flag when an interrupt has been detected volatile static bool BMM150_IRQ = false; BMM150Sensor::BMM150Sensor(ScanI2C::FoundDevice foundDevice) : MotionSensor::MotionSensor(foundDevice) {} bool BMM150Sensor::init() { // Initialise the sensor sensor = BMM150Singleton::GetInstance(device); return sensor->init(device); } int32_t BMM150Sensor::runOnce() { #if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN float heading = sensor->getCompassDegree(); switch (config.display.compass_orientation) { case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_0_INVERTED: case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_0: break; case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90: case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90_INVERTED: heading += 90; break; case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180: case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180_INVERTED: heading += 180; break; case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270: case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270_INVERTED: heading += 270; break; } if (screen) screen->setHeading(heading); #endif return MOTION_SENSOR_CHECK_INTERVAL_MS; } // ---------------------------------------------------------------------- // BMM150Singleton // ---------------------------------------------------------------------- // Get a singleton wrapper for an Sparkfun BMM_150_I2C BMM150Singleton *BMM150Singleton::GetInstance(ScanI2C::FoundDevice device) { #if defined(WIRE_INTERFACES_COUNT) && (WIRE_INTERFACES_COUNT > 1) TwoWire &bus = (device.address.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire); #else TwoWire &bus = Wire; // fallback if only one I2C interface #endif if (pinstance == nullptr) { pinstance = new BMM150Singleton(&bus, device.address.address); } return pinstance; } BMM150Singleton::~BMM150Singleton() {} BMM150Singleton *BMM150Singleton::pinstance{nullptr}; // Initialise the BMM150 Sensor // https://github.com/DFRobot/DFRobot_BMM150/blob/master/examples/getGeomagneticData/getGeomagneticData.ino bool BMM150Singleton::init(ScanI2C::FoundDevice device) { // startup LOG_DEBUG("BMM150 begin on addr 0x%02X (port=%d)", device.address.address, device.address.port); uint8_t status = begin(); if (status != 0) { LOG_DEBUG("BMM150 init error %u", status); return false; } // SW reset to make sure the device starts in a known state setOperationMode(BMM150_POWERMODE_NORMAL); setPresetMode(BMM150_PRESETMODE_LOWPOWER); setRate(BMM150_DATA_RATE_02HZ); setMeasurementXYZ(); return true; } #endif ================================================ FILE: src/motion/BMM150Sensor.h ================================================ #pragma once #ifndef _BMM_150_SENSOR_H_ #define _BMM_150_SENSOR_H_ #include "MotionSensor.h" #if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && __has_include() #include "Fusion/Fusion.h" #include // The I2C address of the Accelerometer (if found) from main.cpp extern ScanI2C::DeviceAddress accelerometer_found; // Singleton wrapper class BMM150Singleton : public DFRobot_BMM150_I2C { private: static BMM150Singleton *pinstance; protected: BMM150Singleton(TwoWire *tw, uint8_t addr) : DFRobot_BMM150_I2C(tw, addr) {} ~BMM150Singleton(); public: // Create a singleton instance (not thread safe) static BMM150Singleton *GetInstance(ScanI2C::FoundDevice device); // Singletons should not be cloneable. BMM150Singleton(BMM150Singleton &other) = delete; // Singletons should not be assignable. void operator=(const BMM150Singleton &) = delete; // Initialise the motion sensor singleton for normal operation bool init(ScanI2C::FoundDevice device); }; class BMM150Sensor : public MotionSensor { private: BMM150Singleton *sensor = nullptr; bool showingScreen = false; public: explicit BMM150Sensor(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 ================================================ FILE: src/motion/BMX160Sensor.cpp ================================================ #include "BMX160Sensor.h" #if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C BMX160Sensor::BMX160Sensor(ScanI2C::FoundDevice foundDevice) : MotionSensor::MotionSensor(foundDevice) {} #if !defined(RAK2560) && __has_include() #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("BMX160 init ok"); return true; } LOG_DEBUG("BMX160 init failed"); return false; } int32_t BMX160Sensor::runOnce() { #if !defined(MESHTASTIC_EXCLUDE_SCREEN) sBmx160SensorData_t magAccel; sBmx160SensorData_t gAccel; /* Get a new sensor event */ sensor.getAllData(&magAccel, NULL, &gAccel); if (doCalibration) { if (!showingScreen) { powerFSM.trigger(EVENT_PRESS); // keep screen alive during calibration showingScreen = true; if (screen) screen->startAlert((FrameCallback)drawFrameCalibration); } if (magAccel.x > highestX) highestX = magAccel.x; if (magAccel.x < lowestX) lowestX = magAccel.x; if (magAccel.y > highestY) highestY = magAccel.y; if (magAccel.y < lowestY) lowestY = magAccel.y; if (magAccel.z > highestZ) highestZ = magAccel.z; if (magAccel.z < lowestZ) lowestZ = magAccel.z; uint32_t now = millis(); if (now > endCalibrationAt) { doCalibration = false; endCalibrationAt = 0; showingScreen = false; if (screen) screen->endAlert(); } // LOG_DEBUG("BMX160 min_x: %.4f, max_X: %.4f, min_Y: %.4f, max_Y: %.4f, min_Z: %.4f, max_Z: %.4f", lowestX, highestX, // lowestY, highestY, lowestZ, highestZ); } int highestRealX = highestX - (highestX + lowestX) / 2; magAccel.x -= (highestX + lowestX) / 2; magAccel.y -= (highestY + lowestY) / 2; magAccel.z -= (highestZ + lowestZ) / 2; FusionVector ga, ma; ga.axis.x = -gAccel.x; // default location for the BMX160 is on the rear of the board ga.axis.y = -gAccel.y; ga.axis.z = gAccel.z; ma.axis.x = -magAccel.x; ma.axis.y = -magAccel.y; ma.axis.z = magAccel.z * 3; // If we're set to one of the inverted positions if (config.display.compass_orientation > meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270) { ma = FusionAxesSwap(ma, FusionAxesAlignmentNXNYPZ); ga = FusionAxesSwap(ga, FusionAxesAlignmentNXNYPZ); } float heading = FusionCompassCalculateHeading(FusionConventionNed, ga, ma); switch (config.display.compass_orientation) { case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_0_INVERTED: case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_0: break; case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90: case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90_INVERTED: heading += 90; break; case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180: case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180_INVERTED: heading += 180; break; case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270: case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270_INVERTED: heading += 270; break; } if (screen) screen->setHeading(heading); #endif return MOTION_SENSOR_CHECK_INTERVAL_MS; } void BMX160Sensor::calibrate(uint16_t forSeconds) { #if !defined(MESHTASTIC_EXCLUDE_SCREEN) sBmx160SensorData_t magAccel; sBmx160SensorData_t gAccel; LOG_DEBUG("BMX160 calibration started for %is", forSeconds); sensor.getAllData(&magAccel, NULL, &gAccel); highestX = magAccel.x, lowestX = magAccel.x; highestY = magAccel.y, lowestY = magAccel.y; highestZ = magAccel.z, lowestZ = magAccel.z; doCalibration = true; uint16_t calibrateFor = forSeconds * 1000; // calibrate for seconds provided endCalibrationAt = millis() + calibrateFor; if (screen) screen->setEndCalibration(endCalibrationAt); #endif } #endif #endif ================================================ FILE: src/motion/BMX160Sensor.h ================================================ #pragma once #ifndef _BMX160_SENSOR_H_ #define _BMX160_SENSOR_H_ #include "MotionSensor.h" #if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C #if !defined(RAK2560) && __has_include() #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; virtual void calibrate(uint16_t forSeconds) override; }; #else // Stub class BMX160Sensor : public MotionSensor { public: explicit BMX160Sensor(ScanI2C::FoundDevice foundDevice); }; #endif #endif #endif ================================================ FILE: src/motion/ICM20948Sensor.cpp ================================================ #include "ICM20948Sensor.h" #if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && __has_include() #if !defined(MESHTASTIC_EXCLUDE_SCREEN) // screen is defined in main.cpp extern graphics::Screen *screen; #endif // 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() { #if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN if (screen && !screen->isScreenOn() && !config.display.wake_on_tap_or_motion && !config.device.double_tap_as_button_press) { if (!isAsleep) { LOG_DEBUG("sleeping IMU"); sensor->sleep(true); isAsleep = true; } return MOTION_SENSOR_CHECK_INTERVAL_MS; } if (isAsleep) { sensor->sleep(false); isAsleep = false; } float magX = 0, magY = 0, magZ = 0; if (sensor->dataReady()) { sensor->getAGMT(); magX = sensor->agmt.mag.axes.x; magY = sensor->agmt.mag.axes.y; magZ = sensor->agmt.mag.axes.z; } if (doCalibration) { if (!showingScreen) { powerFSM.trigger(EVENT_PRESS); // keep screen alive during calibration showingScreen = true; if (screen) screen->startAlert((FrameCallback)drawFrameCalibration); } if (magX > highestX) highestX = magX; if (magX < lowestX) lowestX = magX; if (magY > highestY) highestY = magY; if (magY < lowestY) lowestY = magY; if (magZ > highestZ) highestZ = magZ; if (magZ < lowestZ) lowestZ = magZ; uint32_t now = millis(); if (now > endCalibrationAt) { doCalibration = false; endCalibrationAt = 0; showingScreen = false; if (screen) screen->endAlert(); } // LOG_DEBUG("ICM20948 min_x: %.4f, max_X: %.4f, min_Y: %.4f, max_Y: %.4f, min_Z: %.4f, max_Z: %.4f", lowestX, highestX, // lowestY, highestY, lowestZ, highestZ); } magX -= (highestX + lowestX) / 2; magY -= (highestY + lowestY) / 2; magZ -= (highestZ + lowestZ) / 2; FusionVector ga, ma; ga.axis.x = (sensor->agmt.acc.axes.x); ga.axis.y = -(sensor->agmt.acc.axes.y); ga.axis.z = -(sensor->agmt.acc.axes.z); ma.axis.x = magX; ma.axis.y = magY; ma.axis.z = magZ; // If we're set to one of the inverted positions if (config.display.compass_orientation > meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270) { ma = FusionAxesSwap(ma, FusionAxesAlignmentNXNYPZ); ga = FusionAxesSwap(ga, FusionAxesAlignmentNXNYPZ); } float heading = FusionCompassCalculateHeading(FusionConventionNed, ga, ma); switch (config.display.compass_orientation) { case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_0_INVERTED: case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_0: break; case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90: case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90_INVERTED: heading += 90; break; case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180: case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180_INVERTED: heading += 180; break; case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270: case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270_INVERTED: heading += 270; break; } if (screen) screen->setHeading(heading); #endif // Wake on motion using polling - this is not as efficient as using hardware interrupt pin (see above) auto status = sensor->setBank(0); if (sensor->status != ICM_20948_Stat_Ok) { LOG_DEBUG("ICM20948 isWakeOnMotion failed to set bank - %s", sensor->statusString()); return MOTION_SENSOR_CHECK_INTERVAL_MS; } ICM_20948_INT_STATUS_t int_stat; status = sensor->read(AGB0_REG_INT_STATUS, (uint8_t *)&int_stat, sizeof(ICM_20948_INT_STATUS_t)); if (status != ICM_20948_Stat_Ok) { LOG_DEBUG("ICM20948 isWakeOnMotion failed to read interrupts - %s", sensor->statusString()); return MOTION_SENSOR_CHECK_INTERVAL_MS; } if (int_stat.WOM_INT != 0) { // Wake up! wakeScreen(); } return MOTION_SENSOR_CHECK_INTERVAL_MS; } #endif void ICM20948Sensor::calibrate(uint16_t forSeconds) { #if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN LOG_DEBUG("Old calibration data: highestX = %f, lowestX = %f, highestY = %f, lowestY = %f, highestZ = %f, lowestZ = %f", highestX, lowestX, highestY, lowestY, highestZ, lowestZ); LOG_DEBUG("BMX160 calibration started for %is", forSeconds); if (sensor->dataReady()) { sensor->getAGMT(); highestX = sensor->agmt.mag.axes.x; lowestX = sensor->agmt.mag.axes.x; highestY = sensor->agmt.mag.axes.y; lowestY = sensor->agmt.mag.axes.y; highestZ = sensor->agmt.mag.axes.z; lowestZ = sensor->agmt.mag.axes.z; } else { highestX = 0, lowestX = 0, highestY = 0, lowestY = 0, highestZ = 0, lowestZ = 0; } doCalibration = true; uint16_t calibrateFor = forSeconds * 1000; // calibrate for seconds provided endCalibrationAt = millis() + calibrateFor; if (screen) screen->setEndCalibration(endCalibrationAt); #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 #if defined(WIRE_INTERFACES_COUNT) && (WIRE_INTERFACES_COUNT > 1) TwoWire &bus = (device.address.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire); #else TwoWire &bus = Wire; // fallback if only one I2C interface #endif bool bAddr = (device.address.address == 0x69); delay(100); LOG_DEBUG("ICM20948 begin on addr 0x%02X (port=%d, bAddr=%d)", device.address.address, device.address.port, bAddr); ICM_20948_Status_e status = begin(bus, bAddr); if (status != ICM_20948_Stat_Ok) { LOG_DEBUG("ICM20948 init begin - %s", statusString()); return false; } // SW reset to make sure the device starts in a known state if (swReset() != ICM_20948_Stat_Ok) { LOG_DEBUG("ICM20948 init reset - %s", statusString()); return false; } delay(200); // Now wake the sensor up if (sleep(false) != ICM_20948_Stat_Ok) { LOG_DEBUG("ICM20948 init wake - %s", statusString()); return false; } if (lowPower(false) != ICM_20948_Stat_Ok) { LOG_DEBUG("ICM20948 init high power - %s", statusString()); return false; } if (startupMagnetometer(false) != ICM_20948_Stat_Ok) { LOG_DEBUG("ICM20948 init magnetometer - %s", statusString()); return false; } #ifdef ICM_20948_INT_PIN // Active low cfgIntActiveLow(true); LOG_DEBUG("ICM20948 init set cfgIntActiveLow - %s", statusString()); // Push-pull cfgIntOpenDrain(false); LOG_DEBUG("ICM20948 init set cfgIntOpenDrain - %s", statusString()); // If enabled, *ANY* read will clear the INT_STATUS register. cfgIntAnyReadToClear(true); LOG_DEBUG("ICM20948 init set cfgIntAnyReadToClear - %s", statusString()); // Latch the interrupt until cleared cfgIntLatch(true); LOG_DEBUG("ICM20948 init set cfgIntLatch - %s", statusString()); // Set up an interrupt pin with an internal pullup for active low pinMode(ICM_20948_INT_PIN, INPUT_PULLUP); // Set up an interrupt 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("ICM20948 init set WOMLogic - %s", statusString()); if (status != ICM_20948_Stat_Ok) return false; // Enable interrupts on WakeOnMotion status = intEnableWOM(true); LOG_DEBUG("ICM20948 init set intEnableWOM - %s", statusString()); return status == ICM_20948_Stat_Ok; // Clear any current interrupts ICM20948_IRQ = false; clearInterrupts(); return true; } #endif ================================================ FILE: src/motion/ICM20948Sensor.h ================================================ #pragma once #ifndef _ICM_20948_SENSOR_H_ #define _ICM_20948_SENSOR_H_ #include "MotionSensor.h" #if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && __has_include() #include "Fusion/Fusion.h" #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; bool showingScreen = false; bool isAsleep = false; #ifdef MUZI_BASE float highestX = 449.000000, lowestX = -140.000000, highestY = 422.000000, lowestY = -232.000000, highestZ = 749.000000, lowestZ = 98.000000; #else float highestX = 0, lowestX = 0, highestY = 0, lowestY = 0, highestZ = 0, lowestZ = 0; #endif 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; virtual void calibrate(uint16_t forSeconds) override; }; #endif #endif ================================================ FILE: src/motion/LIS3DHSensor.cpp ================================================ #include "LIS3DHSensor.h" #include "NodeDB.h" #if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && __has_include() LIS3DHSensor::LIS3DHSensor(ScanI2C::FoundDevice foundDevice) : MotionSensor::MotionSensor(foundDevice) {} bool LIS3DHSensor::init() { if (sensor.begin(deviceAddress())) { sensor.setRange(LIS3DH_RANGE_2_G); // Adjust threshold, higher numbers are less sensitive sensor.setClick(config.device.double_tap_as_button_press ? 2 : 1, MOTION_SENSOR_CHECK_INTERVAL_MS); LOG_DEBUG("LIS3DH init ok"); return true; } LOG_DEBUG("LIS3DH init failed"); return false; } int32_t LIS3DHSensor::runOnce() { if (sensor.getClick() > 0) { uint8_t click = sensor.getClick(); if (!config.device.double_tap_as_button_press && config.display.wake_on_tap_or_motion) { wakeScreen(); } if (config.device.double_tap_as_button_press && (click & 0x20)) { buttonPress(); return 500; } } return MOTION_SENSOR_CHECK_INTERVAL_MS; } #endif ================================================ FILE: src/motion/LIS3DHSensor.h ================================================ #pragma once #ifndef _LIS3DH_SENSOR_H_ #define _LIS3DH_SENSOR_H_ #include "MotionSensor.h" #if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && __has_include() #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 ================================================ FILE: src/motion/LSM6DS3Sensor.cpp ================================================ #include "LSM6DS3Sensor.h" #include "NodeDB.h" #if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && __has_include() 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 occurrences needed to trigger, higher threshold is less sensitive sensor.enableWakeup(config.display.wake_on_tap_or_motion, 1, LSM6DS3_WAKE_THRESH); LOG_DEBUG("LSM6DS3 init ok"); return true; } LOG_DEBUG("LSM6DS3 init failed"); return false; } int32_t LSM6DS3Sensor::runOnce() { if (sensor.shake()) { wakeScreen(); return 500; } return MOTION_SENSOR_CHECK_INTERVAL_MS; } #endif ================================================ FILE: src/motion/LSM6DS3Sensor.h ================================================ #pragma once #ifndef _LSM6DS3_SENSOR_H_ #define _LSM6DS3_SENSOR_H_ #include "MotionSensor.h" #if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && __has_include() #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 ================================================ FILE: src/motion/MPU6050Sensor.cpp ================================================ #include "MPU6050Sensor.h" #if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && __has_include() MPU6050Sensor::MPU6050Sensor(ScanI2C::FoundDevice foundDevice) : MotionSensor::MotionSensor(foundDevice) {} bool MPU6050Sensor::init() { if (sensor.begin(deviceAddress())) { // setup motion detection sensor.setHighPassFilter(MPU6050_HIGHPASS_0_63_HZ); sensor.setMotionDetectionThreshold(1); sensor.setMotionDetectionDuration(20); sensor.setInterruptPinLatch(true); // Keep it latched. Will turn off when reinitialized. sensor.setInterruptPinPolarity(true); LOG_DEBUG("MPU6050 init ok"); return true; } LOG_DEBUG("MPU6050 init failed"); return false; } int32_t MPU6050Sensor::runOnce() { if (sensor.getMotionInterruptStatus()) { wakeScreen(); } return MOTION_SENSOR_CHECK_INTERVAL_MS; } #endif ================================================ FILE: src/motion/MPU6050Sensor.h ================================================ #pragma once #ifndef _MPU6050_SENSOR_H_ #define _MPU6050_SENSOR_H_ #include "MotionSensor.h" #if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && __has_include() #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 ================================================ FILE: src/motion/MotionSensor.cpp ================================================ #include "MotionSensor.h" #include "graphics/draw/CompassRenderer.h" #if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C char timeRemainingBuffer[12]; // screen is defined in main.cpp extern graphics::Screen *screen; MotionSensor::MotionSensor(ScanI2C::FoundDevice foundDevice) { device.address.address = foundDevice.address.address; device.address.port = foundDevice.address.port; device.type = foundDevice.type; LOG_DEBUG("Motion MotionSensor port: %s address: 0x%x type: %d", devicePort() == ScanI2C::I2CPort::WIRE1 ? "Wire1" : "Wire", (uint8_t)deviceAddress(), deviceType()); } 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(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN void MotionSensor::drawFrameCalibration(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { if (screen == nullptr) return; // int x_offset = display->width() / 2; // int y_offset = display->height() <= 80 ? 0 : 32; display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_MEDIUM); display->drawString(x, y, "Calibrating\nCompass"); uint8_t timeRemaining = (screen->getEndCalibration() - millis()) / 1000; sprintf(timeRemainingBuffer, "( %02d )", timeRemaining); display->setFont(FONT_SMALL); display->drawString(x, y + 40, timeRemainingBuffer); int16_t compassX = 0, compassY = 0; uint16_t compassDiam = graphics::CompassRenderer::getCompassDiam(display->getWidth(), display->getHeight()); // coordinates for the center of the compass/circle if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) { compassX = x + display->getWidth() - compassDiam / 2 - 5; compassY = y + display->getHeight() / 2; } else { compassX = x + display->getWidth() - compassDiam / 2 - 5; compassY = y + FONT_HEIGHT_SMALL + (display->getHeight() - FONT_HEIGHT_SMALL) / 2; } display->drawCircle(compassX, compassY, compassDiam / 2); graphics::CompassRenderer::drawCompassNorth(display, compassX, compassY, screen->getHeading() * PI / 180, (compassDiam / 2)); } #endif #if !MESHTASTIC_EXCLUDE_POWER_FSM void MotionSensor::wakeScreen() { if (powerFSM.getState() == &stateDARK) { LOG_DEBUG("Motion wakeScreen detected"); if (config.display.wake_on_tap_or_motion) powerFSM.trigger(EVENT_INPUT); } } void MotionSensor::buttonPress() { LOG_DEBUG("Motion buttonPress detected"); powerFSM.trigger(EVENT_PRESS); } #else void MotionSensor::wakeScreen() {} void MotionSensor::buttonPress() {} #endif #endif ================================================ FILE: src/motion/MotionSensor.h ================================================ #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_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; }; virtual void calibrate(uint16_t forSeconds){}; 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(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN // draw an OLED frame (currently only used by the RAK4631 BMX160 sensor) static void drawFrameCalibration(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); #endif ScanI2C::FoundDevice device; // Do calibration if true bool doCalibration = false; uint32_t endCalibrationAt = 0; }; #endif #endif ================================================ FILE: src/motion/QMA6100PSensor.cpp ================================================ #include "QMA6100PSensor.h" #if !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("QMA6100PS isWakeOnMotion failed to read interrupts"); return MOTION_SENSOR_CHECK_INTERVAL_MS; } if ((tempVal & 7) != 0) { // Wake up! wakeScreen(); } return MOTION_SENSOR_CHECK_INTERVAL_MS; } #endif // ---------------------------------------------------------------------- // 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("QMA6100P init begin failed"); return false; } delay(20); // SW reset to make sure the device starts in a known state if (softwareReset() != true) { LOG_WARN("QMA6100P init reset failed"); return false; } delay(20); // Set range if (!setRange(QMA_6100P_MPU_ACCEL_SCALE)) { LOG_WARN("QMA6100P init range failed"); return false; } // set active mode if (!enableAccel()) { LOG_WARN("ERROR QMA6100P active mode set failed"); } // set calibrateoffsets if (!calibrateOffsets()) { LOG_WARN("ERROR QMA6100P calibration failed"); } #ifdef QMA_6100P_INT_PIN // Active low & Open Drain uint8_t tempVal; if (!readRegisterRegion(SFE_QMA6100P_INTPINT_CONF, &tempVal, 1)) { LOG_WARN("QMA6100P init failed to read interrupt pin config"); return false; } tempVal |= 0b00000010; // Active low & Open Drain if (!writeRegisterByte(SFE_QMA6100P_INTPINT_CONF, tempVal)) { LOG_WARN("QMA6100P init failed to write interrupt pin config"); return false; } // Latch until cleared, all reads clear the latch if (!readRegisterRegion(SFE_QMA6100P_INT_CFG, &tempVal, 1)) { LOG_WARN("QMA6100P init failed to read interrupt config"); return false; } tempVal |= 0b10000001; // Latch until cleared, INT_RD_CLR1 if (!writeRegisterByte(SFE_QMA6100P_INT_CFG, tempVal)) { LOG_WARN("QMA6100P init failed to write interrupt config"); return false; } // Set up an interrupt pin with an internal pullup for active low pinMode(QMA_6100P_INT_PIN, INPUT_PULLUP); // Set up an interrupt service routine attachInterrupt(QMA_6100P_INT_PIN, QMA6100PSetInterrupt, FALLING); #endif return true; } bool QMA6100PSingleton::setWakeOnMotion() { // Enable 'Any Motion' interrupt if (!writeRegisterByte(SFE_QMA6100P_INT_EN2, 0b00000111)) { LOG_WARN("QMA6100P :setWakeOnMotion failed to write interrupt enable"); return false; } // Set 'Significant Motion' interrupt map to INT1 uint8_t tempVal; if (!readRegisterRegion(SFE_QMA6100P_INT_MAP1, &tempVal, 1)) { LOG_WARN("QMA6100P setWakeOnMotion failed to read interrupt map"); return false; } sfe_qma6100p_int_map1_bitfield_t int_map1; int_map1.all = tempVal; int_map1.bits.int1_any_mot = 1; // any motion interrupt to INT1 tempVal = int_map1.all; if (!writeRegisterByte(SFE_QMA6100P_INT_MAP1, tempVal)) { LOG_WARN("QMA6100P setWakeOnMotion failed to write interrupt map"); return false; } // Clear any current interrupts QMA6100P_IRQ = false; return true; } #endif ================================================ FILE: src/motion/QMA6100PSensor.h ================================================ #pragma once #ifndef _QMA_6100P_SENSOR_H_ #define _QMA_6100P_SENSOR_H_ #include "MotionSensor.h" #if !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 ================================================ FILE: src/motion/STK8XXXSensor.cpp ================================================ #include "STK8XXXSensor.h" #if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && defined(HAS_STK8XXX) 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("STK8XXX init ok"); return true; } LOG_DEBUG("STK8XXX init failed"); return false; } int32_t STK8XXXSensor::runOnce() { if (STK_IRQ) { STK_IRQ = false; if (config.display.wake_on_tap_or_motion) { wakeScreen(); } } return MOTION_SENSOR_CHECK_INTERVAL_MS; } #endif #endif ================================================ FILE: src/motion/STK8XXXSensor.h ================================================ #pragma once #ifndef _STK8XXX_SENSOR_H_ #define _STK8XXX_SENSOR_H_ #include "MotionSensor.h" #if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && defined(HAS_STK8XXX) #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 ================================================ FILE: src/mqtt/MQTT.cpp ================================================ #include "MQTT.h" #include "MeshService.h" #include "NodeDB.h" #include "PowerFSM.h" #include "ServiceEnvelope.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 #if HAS_ETHERNET && defined(USE_WS5500) #include #define ETH ETH2 #endif // HAS_ETHERNET #include "Default.h" #if !defined(ARCH_NRF52) || NRF52_USE_JSON #include "serialization/JSON.h" #include "serialization/MeshPacketSerializer.h" #endif #include #include #include #include #if defined(ARCH_PORTDUINO) #include #elif !defined(ntohl) #include #define ntohl __ntohl #endif #include MQTT *mqtt; namespace { constexpr int reconnectMax = 5; // 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; static bool isConnected = false; static uint32_t lastPositionUnavailableWarning = 0; static const uint32_t POSITION_UNAVAILABLE_WARNING_INTERVAL_MS = 15000; // 15 seconds inline void onReceiveProto(char *topic, byte *payload, size_t length) { const DecodedServiceEnvelope e(payload, length); if (!e.validDecode || e.channel_id == NULL || e.gateway_id == NULL || e.packet == NULL) { LOG_ERROR("Invalid MQTT service envelope, topic %s, len %u!", topic, length); return; } const meshtastic_Channel &ch = channels.getByName(e.channel_id); // Find channel by channel_id and check downlink_enabled if (!(strcmp(e.channel_id, "PKI") == 0 || (strcmp(e.channel_id, channels.getGlobalId(ch.index)) == 0 && ch.settings.downlink_enabled))) { return; } bool anyChannelHasDownlink = false; size_t numChan = channels.getNumChannels(); for (size_t i = 0; i < numChan; ++i) { const auto &c = channels.getByIndex(i); if (c.settings.downlink_enabled) { anyChannelHasDownlink = true; break; } } if (strcmp(e.channel_id, "PKI") == 0 && !anyChannelHasDownlink) { return; } // Generate node ID from nodenum for comparison std::string nodeId = nodeDB->getNodeId(); if (strcmp(e.gateway_id, nodeId.c_str()) == 0) { // Generate an implicit ACK towards ourselves (handled and processed only locally!) for this message. // We do this because packets are not rebroadcasted back into MQTT anymore and we assume that at least one node // receives it when we get our own packet back. Then we'll stop our retransmissions. if (isFromUs(e.packet)) { auto pAck = routingModule->allocAckNak(meshtastic_Routing_Error_NONE, getFrom(e.packet), e.packet->id, ch.index); pAck->transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MQTT; router->sendLocal(pAck); } else { LOG_INFO("Ignore downlink message we originally sent"); } return; } if (isFromUs(e.packet)) { LOG_INFO("Ignore downlink message we originally sent"); return; } LOG_INFO("Received MQTT topic %s, len=%u", topic, length); if (e.packet->hop_limit > HOP_MAX || e.packet->hop_start > HOP_MAX) { LOG_INFO("Invalid hop_limit(%u) or hop_start(%u)", e.packet->hop_limit, e.packet->hop_start); return; } UniquePacketPoolPacket p = packetPool.allocUniqueZeroed(); p->from = e.packet->from; p->to = e.packet->to; p->id = e.packet->id; p->channel = e.packet->channel; p->hop_limit = e.packet->hop_limit; p->hop_start = e.packet->hop_start; p->want_ack = e.packet->want_ack; p->via_mqtt = true; // Mark that the packet was received via MQTT p->transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MQTT; p->which_payload_variant = e.packet->which_payload_variant; memcpy(&p->decoded, &e.packet->decoded, std::max(sizeof(p->decoded), sizeof(p->encrypted))); if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) { if (moduleConfig.mqtt.encryption_enabled) { LOG_INFO("Ignore decoded message on MQTT, encryption is enabled"); return; } if (p->decoded.portnum == meshtastic_PortNum_ADMIN_APP) { LOG_INFO("Ignore decoded admin packet"); return; } p->channel = ch.index; } // PKI messages get accepted even if we can't decrypt if (router && p->which_payload_variant == meshtastic_MeshPacket_encrypted_tag && strcmp(e.channel_id, "PKI") == 0) { const meshtastic_NodeInfoLite *tx = nodeDB->getMeshNode(getFrom(p.get())); const meshtastic_NodeInfoLite *rx = nodeDB->getMeshNode(p->to); // Only accept PKI messages to us, or if we have both the sender and receiver in our nodeDB, as then it's // likely they discovered each other via a channel we have downlink enabled for if (isToUs(p.get()) || (tx && tx->has_user && rx && rx->has_user)) router->enqueueReceivedMessage(p.release()); } else if (router && perhapsDecode(p.get()) == DecodeState::DECODE_SUCCESS) // ignore messages if we don't have the channel key router->enqueueReceivedMessage(p.release()); } #if !defined(ARCH_NRF52) || NRF52_USE_JSON // returns true if this is a valid JSON envelope which we accept on downlink inline bool isValidJsonEnvelope(JSONObject &json) { // Generate node ID from nodenum for comparison std::string nodeId = nodeDB->getNodeId(); // if "sender" is provided, avoid processing packets we uplinked return (json.find("sender") != json.end() ? (json["sender"]->AsString().compare(nodeId) != 0) : true) && (json.find("hopLimit") != json.end() ? json["hopLimit"]->IsNumber() : true) && // hop limit should be a number (json.find("from") != json.end()) && json["from"]->IsNumber() && (json["from"]->AsNumber() == nodeDB->getNodeNum()) && // only accept message if the "from" is us (json.find("type") != json.end()) && json["type"]->IsString() && // should specify a type (json.find("payload") != json.end()); // should have a payload } inline void onReceiveJson(byte *payload, size_t length) { char payloadStr[length + 1]; memcpy(payloadStr, payload, length); payloadStr[length] = 0; // null terminated string std::unique_ptr json_value(JSON::Parse(payloadStr)); if (json_value == nullptr) { LOG_ERROR("JSON received payload on MQTT but not a valid JSON"); return; } JSONObject json; json = json_value->AsObject(); if (!isValidJsonEnvelope(json)) { LOG_ERROR("JSON received payload on MQTT but not a valid envelope"); return; } // this is a valid envelope if (json["type"]->AsString().compare("sendtext") == 0 && json["payload"]->IsString()) { std::string jsonPayloadStr = json["payload"]->AsString(); LOG_INFO("JSON payload %s, length %u", jsonPayloadStr.c_str(), jsonPayloadStr.length()); // construct protobuf data packet using TEXT_MESSAGE, send it to the mesh meshtastic_MeshPacket *p = router->allocForSending(); p->decoded.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP; if (json.find("channel") != json.end() && json["channel"]->IsNumber() && (json["channel"]->AsNumber() < channels.getNumChannels())) p->channel = json["channel"]->AsNumber(); if (json.find("to") != json.end() && json["to"]->IsNumber()) p->to = json["to"]->AsNumber(); if (json.find("hopLimit") != json.end() && json["hopLimit"]->IsNumber()) p->hop_limit = json["hopLimit"]->AsNumber(); if (jsonPayloadStr.length() <= sizeof(p->decoded.payload.bytes)) { memcpy(p->decoded.payload.bytes, jsonPayloadStr.c_str(), jsonPayloadStr.length()); p->decoded.payload.size = jsonPayloadStr.length(); service->sendToMesh(p, RX_SRC_LOCAL); } else { LOG_WARN("Received MQTT json payload too long, drop"); } } else if (json["type"]->AsString().compare("sendposition") == 0 && json["payload"]->IsObject()) { // invent the "sendposition" type for a valid envelope JSONObject posit; posit = json["payload"]->AsObject(); // get nested JSON Position meshtastic_Position pos = meshtastic_Position_init_default; if (posit.find("latitude_i") != posit.end() && posit["latitude_i"]->IsNumber()) pos.latitude_i = posit["latitude_i"]->AsNumber(); if (posit.find("longitude_i") != posit.end() && posit["longitude_i"]->IsNumber()) pos.longitude_i = posit["longitude_i"]->AsNumber(); if (posit.find("altitude") != posit.end() && posit["altitude"]->IsNumber()) pos.altitude = posit["altitude"]->AsNumber(); if (posit.find("time") != posit.end() && posit["time"]->IsNumber()) pos.time = posit["time"]->AsNumber(); // construct protobuf data packet using POSITION, send it to the mesh meshtastic_MeshPacket *p = router->allocForSending(); p->decoded.portnum = meshtastic_PortNum_POSITION_APP; if (json.find("channel") != json.end() && json["channel"]->IsNumber() && (json["channel"]->AsNumber() < channels.getNumChannels())) p->channel = json["channel"]->AsNumber(); if (json.find("to") != json.end() && json["to"]->IsNumber()) p->to = json["to"]->AsNumber(); if (json.find("hopLimit") != json.end() && json["hopLimit"]->IsNumber()) p->hop_limit = json["hopLimit"]->AsNumber(); p->decoded.payload.size = pb_encode_to_bytes(p->decoded.payload.bytes, sizeof(p->decoded.payload.bytes), &meshtastic_Position_msg, &pos); // make the Data protobuf from position service->sendToMesh(p, RX_SRC_LOCAL); } else { LOG_DEBUG("JSON ignore downlink message with unsupported type"); } } #endif /// Determines if the given IPAddress is a private IPv4 address, i.e. not routable on the public internet. bool isPrivateIpAddress(const IPAddress &ip) { constexpr struct { uint32_t network; uint32_t mask; } privateCidrRanges[] = { {.network = 192u << 24 | 168 << 16, .mask = 0xffff0000}, // 192.168.0.0/16 {.network = 172u << 24 | 16 << 16, .mask = 0xfff00000}, // 172.16.0.0/12 {.network = 169u << 24 | 254 << 16, .mask = 0xffff0000}, // 169.254.0.0/16 {.network = 10u << 24, .mask = 0xff000000}, // 10.0.0.0/8 {.network = 127u << 24 | 1, .mask = 0xffffffff}, // 127.0.0.1/32 {.network = 100u << 24 | 64 << 16, .mask = 0xffc00000}, // 100.64.0.0/10 }; const uint32_t addr = ntohl(ip); for (const auto &cidrRange : privateCidrRanges) { if (cidrRange.network == (addr & cidrRange.mask)) { LOG_INFO("MQTT server on a private IP"); return true; } } return false; } // Separate a [:] string. Returns a pair containing the parsed host and port. If the port is // not present in the input string, or is invalid, the value of the `port` argument will be returned. std::pair parseHostAndPort(String server, uint16_t port = 0) { const int delimIndex = server.indexOf(':'); if (delimIndex > 0) { const long parsedPort = server.substring(delimIndex + 1, server.length()).toInt(); if (parsedPort < 1 || parsedPort > UINT16_MAX) { LOG_WARN("Invalid MQTT port %d: %s", parsedPort, server.c_str()); } else { port = parsedPort; } server[delimIndex] = 0; } return std::make_pair(std::move(server), port); } bool isDefaultServer(const String &host) { return host.length() == 0 || host == default_mqtt_address; } bool isDefaultRootTopic(const String &root) { return root.length() == 0 || root == default_mqtt_root; } struct PubSubConfig { explicit PubSubConfig(const meshtastic_ModuleConfig_MQTTConfig &config) { if (*config.address) { serverAddr = config.address; mqttUsername = config.username; mqttPassword = config.password; } if (config.tls_enabled) { serverPort = 8883; } auto [parsedServerAddr, parsedServerPort] = parseHostAndPort(serverAddr.c_str(), serverPort); serverAddr = std::move(parsedServerAddr); serverPort = parsedServerPort; } // Defaults static constexpr uint16_t defaultPort = 1883; static constexpr uint16_t defaultPortTls = 8883; uint16_t serverPort = defaultPort; String serverAddr = default_mqtt_address; const char *mqttUsername = default_mqtt_username; const char *mqttPassword = default_mqtt_password; }; #if HAS_NETWORKING bool connectPubSub(const PubSubConfig &config, PubSubClient &pubSub, Client &client) { pubSub.setBufferSize(1024, 1024); pubSub.setClient(client); pubSub.setServer(config.serverAddr.c_str(), config.serverPort); LOG_INFO("Connecting directly to MQTT server %s, port: %d, username: %s, password: %s", config.serverAddr.c_str(), config.serverPort, config.mqttUsername, config.mqttPassword); // Generate node ID from nodenum for client identification std::string nodeId = nodeDB->getNodeId(); const bool connected = pubSub.connect(nodeId.c_str(), config.mqttUsername, config.mqttPassword); if (connected) { isConnected = true; LOG_INFO("MQTT connected"); } else { isConnected = false; LOG_WARN("Failed to connect to MQTT server"); } return connected; } #endif inline bool isConnectedToNetwork() { #ifdef USE_WS5500 if (ETH.connected()) return true; #endif #if HAS_WIFI return WiFi.isConnected(); #elif HAS_ETHERNET return Ethernet.linkStatus() == LinkON; #else return false; #endif } /** return true if we have a channel that wants uplink/downlink or map reporting is enabled */ bool wantsLink() { const bool hasChannelorMapReport = moduleConfig.mqtt.enabled && (moduleConfig.mqtt.map_reporting_enabled || channels.anyMqttEnabled()); return hasChannelorMapReport && (moduleConfig.mqtt.proxy_to_client_enabled || isConnectedToNetwork()); } } // namespace void MQTT::mqttCallback(char *topic, byte *payload, unsigned int length) { mqtt->onReceive(topic, payload, length); } void MQTT::onClientProxyReceive(meshtastic_MqttClientProxyMessage msg) { onReceive(msg.topic, msg.payload_variant.data.bytes, msg.payload_variant.data.size); } void MQTT::onReceive(char *topic, byte *payload, size_t length) { if (length == 0) { LOG_WARN("Empty MQTT payload received, topic %s!", topic); return; } // check if this is a json payload message by comparing the topic start if (moduleConfig.mqtt.json_enabled && (strncmp(topic, jsonTopic.c_str(), jsonTopic.length()) == 0)) { #if !defined(ARCH_NRF52) || NRF52_USE_JSON // parse the channel name from the topic string // the topic has been checked above for having jsonTopic prefix, so just move past it char *channelName = topic + jsonTopic.length(); // if another "/" was added, parse string up to that character channelName = strtok(channelName, "/") ? strtok(channelName, "/") : channelName; // We allow downlink JSON packets only on a channel named "mqtt" const meshtastic_Channel &sendChannel = channels.getByName(channelName); if (!(strncasecmp(channels.getGlobalId(sendChannel.index), Channels::mqttChannel, strlen(Channels::mqttChannel)) == 0 && sendChannel.settings.downlink_enabled)) { LOG_WARN("JSON downlink received on channel not called 'mqtt' or without downlink enabled"); return; } onReceiveJson(payload, length); #endif return; } onReceiveProto(topic, payload, length); } void mqttInit() { new MQTT(); } #if HAS_NETWORKING MQTT::MQTT() : MQTT(std::unique_ptr(new MQTTClient())) {} MQTT::MQTT(std::unique_ptr _mqttClient) : concurrency::OSThread("mqtt"), mqttQueue(MAX_MQTT_QUEUE), mqttClient(std::move(_mqttClient)), pubSub(*mqttClient) #else MQTT::MQTT() : concurrency::OSThread("mqtt"), mqttQueue(MAX_MQTT_QUEUE) #endif { if (moduleConfig.mqtt.enabled) { LOG_DEBUG("Init MQTT"); assert(!mqtt); mqtt = this; if (*moduleConfig.mqtt.root) { cryptTopic = moduleConfig.mqtt.root + cryptTopic; jsonTopic = moduleConfig.mqtt.root + jsonTopic; mapTopic = moduleConfig.mqtt.root + mapTopic; isConfiguredForDefaultRootTopic = isDefaultRootTopic(moduleConfig.mqtt.root); } else { cryptTopic = "msh" + cryptTopic; jsonTopic = "msh" + jsonTopic; mapTopic = "msh" + mapTopic; isConfiguredForDefaultRootTopic = true; } if (moduleConfig.mqtt.map_reporting_enabled && moduleConfig.mqtt.has_map_report_settings) { map_position_precision = Default::getConfiguredOrDefault(moduleConfig.mqtt.map_report_settings.position_precision, default_map_position_precision); map_publish_interval_msecs = Default::getConfiguredOrDefaultMs( moduleConfig.mqtt.map_report_settings.publish_interval_secs, default_map_publish_interval_secs); } auto [host, parsedPort] = parseHostAndPort(moduleConfig.mqtt.address); (void)parsedPort; isConfiguredForDefaultServer = isDefaultServer(host); IPAddress ip; isMqttServerAddressPrivate = ip.fromString(host.c_str()) && isPrivateIpAddress(ip); #if HAS_NETWORKING if (!moduleConfig.mqtt.proxy_to_client_enabled) pubSub.setCallback(mqttCallback); #endif if (moduleConfig.mqtt.proxy_to_client_enabled) { LOG_INFO("MQTT configured to use client proxy"); enabled = true; runASAP = true; reconnectCount = 0; #if !IS_RUNNING_TESTS publishNodeInfo(); #endif } // 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; strncpy(msg->topic, topic, sizeof(msg->topic)); msg->topic[sizeof(msg->topic) - 1] = '\0'; strncpy(msg->payload_variant.text, payload, sizeof(msg->payload_variant.text)); msg->payload_variant.text[sizeof(msg->payload_variant.text) - 1] = '\0'; 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; strncpy(msg->topic, topic, sizeof(msg->topic)); msg->topic[sizeof(msg->topic) - 1] = '\0'; // Ensure null termination if (length > sizeof(msg->payload_variant.data.bytes)) length = sizeof(msg->payload_variant.data.bytes); msg->payload_variant.data.size = length; memcpy(msg->payload_variant.data.bytes, payload, length); msg->retained = retained; service->sendMqttMessageToClientProxy(msg); return true; } #if HAS_NETWORKING else if (isConnectedDirectly()) { return pubSub.publish(topic, payload, length, retained); } #endif return false; } void MQTT::reconnect() { isConnected = false; if (wantsLink()) { if (moduleConfig.mqtt.proxy_to_client_enabled) { LOG_INFO("MQTT connect via client proxy instead"); enabled = true; runASAP = true; reconnectCount = 0; publishNodeInfo(); return; // Don't try to connect directly to the server } #if HAS_NETWORKING const PubSubConfig ps_config(moduleConfig.mqtt); MQTTClient *clientConnection = mqttClient.get(); #if MQTT_SUPPORTS_TLS if (moduleConfig.mqtt.tls_enabled) { mqttClientTLS.setInsecure(); LOG_INFO("Use TLS-encrypted session"); clientConnection = &mqttClientTLS; } else { LOG_INFO("Use non-TLS-encrypted session"); } #endif if (connectPubSub(ps_config, pubSub, *clientConnection)) { enabled = true; // Start running background process again runASAP = true; reconnectCount = 0; isMqttServerAddressPrivate = isPrivateIpAddress(clientConnection->remoteIP()); isConnected = true; publishNodeInfo(); sendSubscriptions(); } else { #if 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("Subscribe to %s", topic.c_str()); pubSub.subscribe(topic.c_str(), 1); // FIXME, is QOS 1 right? #if !defined(ARCH_NRF52) || \ defined(NRF52_USE_JSON) // JSON is not supported on nRF52, see issue #2804 ### Fixed by using ArduinoJSON ### if (moduleConfig.mqtt.json_enabled == true) { std::string topicDecoded = jsonTopic + channels.getGlobalId(i) + "/+"; LOG_INFO("Subscribe to %s", topicDecoded.c_str()); pubSub.subscribe(topicDecoded.c_str(), 1); // FIXME, is QOS 1 right? } #endif // ARCH_NRF52 NRF52_USE_JSON } } #if !MESHTASTIC_EXCLUDE_PKI if (hasDownlink) { std::string topic = cryptTopic + "PKI/+"; LOG_INFO("Subscribe to %s", topic.c_str()); pubSub.subscribe(topic.c_str(), 1); } #endif #endif } int32_t MQTT::runOnce() { 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; } #if HAS_NETWORKING else if (!pubSub.loop()) { if (!wantConnection) return 5000; // If we don't want connection now, check again in 5 secs else { reconnect(); // If we succeeded, empty the queue one by one and start reading rapidly, else try again in 30 seconds (TCP // connections are EXPENSIVE so try rarely) if (isConnectedDirectly()) { publishQueuedMessages(); return 200; } else return 30000; } } else { // we are connected to server, check often for new requests on the TCP port if (!wantConnection) { LOG_INFO("MQTT link not needed, drop"); pubSub.disconnect(); } powerFSM.trigger(EVENT_CONTACT_FROM_PHONE); // Suppress entering light sleep (because that would turn off bluetooth) return 20; } #else // No networking available, return default interval return 30000; #endif } bool MQTT::isValidConfig(const meshtastic_ModuleConfig_MQTTConfig &config, MQTTClient *client) { const PubSubConfig parsed(config); if (config.enabled && !config.proxy_to_client_enabled) { #if HAS_NETWORKING if (config.tls_enabled) { #if !MQTT_SUPPORTS_TLS LOG_ERROR("Invalid MQTT config: tls_enabled is not supported on this node"); return false; #endif } // Perform a lightweight TCP connectivity check without using connectPubSub(), // which mutates the module's isConnected state. This only checks if the server // is reachable — it does not establish an MQTT session. // Settings are always saved regardless of the result. if (isConnectedToNetwork()) { MQTTClient testClient; if (!testClient.connect(parsed.serverAddr.c_str(), parsed.serverPort)) { const char *warning = "Could not reach the MQTT server. Settings will be saved, but please verify the server " "address and credentials."; LOG_WARN(warning); #if !IS_RUNNING_TESTS meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); if (cn) { cn->level = meshtastic_LogRecord_Level_WARNING; cn->time = getValidTime(RTCQualityFromNet); strncpy(cn->message, warning, sizeof(cn->message) - 1); cn->message[sizeof(cn->message) - 1] = '\0'; service->sendClientNotification(cn); } #endif } testClient.stop(); } #else const char *warning = "Invalid MQTT config: proxy_to_client_enabled must be enabled on nodes that do not have a network"; LOG_ERROR(warning); #if !IS_RUNNING_TESTS meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); cn->level = meshtastic_LogRecord_Level_ERROR; cn->time = getValidTime(RTCQualityFromNet); strncpy(cn->message, warning, sizeof(cn->message) - 1); cn->message[sizeof(cn->message) - 1] = '\0'; // Ensure null termination service->sendClientNotification(cn); #endif return false; #endif } const bool defaultServer = isDefaultServer(parsed.serverAddr); if (defaultServer && !IS_ONE_OF(parsed.serverPort, PubSubConfig::defaultPort, PubSubConfig::defaultPortTls)) { const char *warning = "Invalid MQTT config: default server address must not have a port specified"; LOG_ERROR(warning); #if !IS_RUNNING_TESTS meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); cn->level = meshtastic_LogRecord_Level_ERROR; cn->time = getValidTime(RTCQualityFromNet); strncpy(cn->message, warning, sizeof(cn->message) - 1); cn->message[sizeof(cn->message) - 1] = '\0'; // Ensure null termination service->sendClientNotification(cn); #endif return false; } return true; } void MQTT::publishNodeInfo() { // TODO: NodeInfo broadcast over MQTT only (NODENUM_BROADCAST_NO_LORA) } void MQTT::publishQueuedMessages() { if (mqttQueue.isEmpty()) return; if (!moduleConfig.mqtt.proxy_to_client_enabled && !isConnected) return; LOG_DEBUG("Publish enqueued MQTT message"); const std::unique_ptr entry(mqttQueue.dequeuePtr(0)); LOG_INFO("publish %s, %u bytes from queue", entry->topic.c_str(), entry->envBytes.size()); publish(entry->topic.c_str(), entry->envBytes.data(), entry->envBytes.size(), false); #if !defined(ARCH_NRF52) || \ defined(NRF52_USE_JSON) // JSON is not supported on nRF52, see issue #2804 ### Fixed by using ArduinoJson ### if (!moduleConfig.mqtt.json_enabled) return; // handle json topic const DecodedServiceEnvelope env(entry->envBytes.data(), entry->envBytes.size()); if (!env.validDecode || env.packet == NULL || env.channel_id == NULL) return; auto jsonString = MeshPacketSerializer::JsonSerialize(env.packet); if (jsonString.length() == 0) return; // Generate node ID from nodenum for topic std::string nodeId = nodeDB->getNodeId(); std::string topicJson; if (env.packet->pki_encrypted) { topicJson = jsonTopic + "PKI/" + nodeId; } else { topicJson = jsonTopic + env.channel_id + "/" + nodeId; } LOG_INFO("JSON publish message to %s, %u bytes: %s", topicJson.c_str(), jsonString.length(), jsonString.c_str()); publish(topicJson.c_str(), jsonString.c_str(), false); #endif // ARCH_NRF52 NRF52_USE_JSON } void MQTT::onSend(const meshtastic_MeshPacket &mp_encrypted, const meshtastic_MeshPacket &mp_decoded, ChannelIndex chIndex) { if (mp_encrypted.via_mqtt) return; // Don't send messages that came from MQTT back into MQTT bool uplinkEnabled = false; for (int i = 0; i <= 7; i++) { if (channels.getByIndex(i).settings.uplink_enabled) uplinkEnabled = true; } if (!uplinkEnabled) return; // no channels have an uplink enabled auto &ch = channels.getByIndex(chIndex); // mp_decoded will not be decoded when it's PKI encrypted and not directed to us if (mp_decoded.which_payload_variant == meshtastic_MeshPacket_decoded_tag) { // For uplinking other's packets, check if it's not OK to MQTT or if it's an older packet without the bitfield bool dontUplink = !mp_decoded.decoded.has_bitfield || !(mp_decoded.decoded.bitfield & BITFIELD_OK_TO_MQTT_MASK); // Respect the DontMqttMeBro flag for other nodes' packets on public MQTT servers if (!isFromUs(&mp_decoded) && !isMqttServerAddressPrivate && dontUplink) { LOG_INFO("MQTT onSend - Not forwarding packet due to DontMqttMeBro flag"); return; } if (isConfiguredForDefaultServer && (mp_decoded.decoded.portnum == meshtastic_PortNum_RANGE_TEST_APP || mp_decoded.decoded.portnum == meshtastic_PortNum_DETECTION_SENSOR_APP)) { LOG_DEBUG("MQTT onSend - Ignoring range test or detection sensor message on public mqtt"); return; } } // Either encrypted packet (we couldn't decrypt) is marked as pki_encrypted, or we could decode the PKI encrypted packet bool isPKIEncrypted = mp_encrypted.pki_encrypted || mp_decoded.pki_encrypted; // If it was to a channel, check uplink enabled, else must be pki_encrypted if (!(ch.settings.uplink_enabled || isPKIEncrypted)) return; const char *channelId = isPKIEncrypted ? "PKI" : channels.getGlobalId(chIndex); LOG_DEBUG("MQTT onSend - Publish "); const meshtastic_MeshPacket *p; if (moduleConfig.mqtt.encryption_enabled) { p = &mp_encrypted; LOG_DEBUG("encrypted message"); } else if (mp_decoded.which_payload_variant == meshtastic_MeshPacket_decoded_tag) { p = &mp_decoded; LOG_DEBUG("portnum %i message", mp_decoded.decoded.portnum); } else { LOG_DEBUG("nothing, pkt not decrypted"); return; // Don't upload a still-encrypted PKI packet if not encryption_enabled } // Generate node ID from nodenum for service envelope std::string nodeId = nodeDB->getNodeId(); const meshtastic_ServiceEnvelope env = {.packet = const_cast(p), .channel_id = const_cast(channelId), .gateway_id = const_cast(nodeId.c_str())}; size_t numBytes = pb_encode_to_bytes(bytes, sizeof(bytes), &meshtastic_ServiceEnvelope_msg, &env); std::string topic = cryptTopic + channelId + "/" + nodeId; if (moduleConfig.mqtt.proxy_to_client_enabled || this->isConnectedDirectly()) { LOG_DEBUG("MQTT Publish %s, %u bytes", topic.c_str(), numBytes); publish(topic.c_str(), bytes, numBytes, false); #if !defined(ARCH_NRF52) || \ defined(NRF52_USE_JSON) // JSON is not supported on nRF52, see issue #2804 ### Fixed by using ArduinoJson ### if (!moduleConfig.mqtt.json_enabled) return; // handle json topic auto jsonString = MeshPacketSerializer::JsonSerialize(&mp_decoded); if (jsonString.length() == 0) return; // Generate node ID from nodenum for JSON topic std::string nodeIdForJson = nodeDB->getNodeId(); std::string topicJson = jsonTopic + channelId + "/" + nodeIdForJson; LOG_INFO("JSON publish message to %s, %u bytes: %s", topicJson.c_str(), jsonString.length(), jsonString.c_str()); publish(topicJson.c_str(), jsonString.c_str(), false); #endif // ARCH_NRF52 NRF52_USE_JSON } else { LOG_INFO("MQTT not connected, queue packet"); QueueEntry *entry; if (mqttQueue.numFree() == 0) { LOG_WARN("MQTT queue is full, discard oldest"); entry = mqttQueue.dequeuePtr(0); } else { entry = new QueueEntry; } entry->topic = std::move(topic); entry->envBytes.assign(bytes, numBytes); if (mqttQueue.enqueue(entry, 0) == false) { LOG_CRIT("Failed to add a message to mqttQueue!"); abort(); } } } void MQTT::perhapsReportToMap() { if (!moduleConfig.mqtt.map_reporting_enabled || !moduleConfig.mqtt.map_report_settings.should_report_location || !(moduleConfig.mqtt.proxy_to_client_enabled || isConnectedDirectly())) return; // Coerce the map position precision to be within the valid range // This removes obtusely large radius and privacy problematic ones from the map if (map_position_precision < 12 || map_position_precision > 15) { LOG_WARN("MQTT Map report position precision %u is out of range, using default %u", map_position_precision, default_map_position_precision); map_position_precision = default_map_position_precision; } if (Throttle::isWithinTimespanMs(last_report_to_map, map_publish_interval_msecs) && last_report_to_map != 0) return; if (localPosition.latitude_i == 0 && localPosition.longitude_i == 0) { if (Throttle::isWithinTimespanMs(lastPositionUnavailableWarning, POSITION_UNAVAILABLE_WARNING_INTERVAL_MS) == false) { LOG_WARN("MQTT Map report enabled, but no position available"); lastPositionUnavailableWarning = millis(); } return; } // Allocate MeshPacket and fill it meshtastic_MeshPacket *mp = packetPool.allocZeroed(); mp->which_payload_variant = meshtastic_MeshPacket_decoded_tag; mp->from = nodeDB->getNodeNum(); mp->to = NODENUM_BROADCAST; mp->decoded.portnum = meshtastic_PortNum_MAP_REPORT_APP; // Fill MapReport message meshtastic_MapReport mapReport = meshtastic_MapReport_init_default; memcpy(mapReport.long_name, owner.long_name, sizeof(owner.long_name)); memcpy(mapReport.short_name, owner.short_name, sizeof(owner.short_name)); mapReport.role = config.device.role; mapReport.hw_model = owner.hw_model; strncpy(mapReport.firmware_version, optstr(APP_VERSION), sizeof(mapReport.firmware_version)); mapReport.region = config.lora.region; mapReport.modem_preset = config.lora.modem_preset; mapReport.has_default_channel = channels.hasDefaultChannel(); mapReport.has_opted_report_location = true; // Set position with precision (same as in PositionModule) mapReport.latitude_i = localPosition.latitude_i & (UINT32_MAX << (32 - map_position_precision)); mapReport.longitude_i = localPosition.longitude_i & (UINT32_MAX << (32 - map_position_precision)); mapReport.latitude_i += (1 << (31 - map_position_precision)); mapReport.longitude_i += (1 << (31 - map_position_precision)); mapReport.altitude = localPosition.altitude; mapReport.position_precision = map_position_precision; mapReport.num_online_local_nodes = nodeDB->getNumOnlineMeshNodes(true); // Encode MapReport message into the MeshPacket mp->decoded.payload.size = pb_encode_to_bytes(mp->decoded.payload.bytes, sizeof(mp->decoded.payload.bytes), &meshtastic_MapReport_msg, &mapReport); // Generate node ID from nodenum for service envelope std::string nodeId = nodeDB->getNodeId(); // Encode the MeshPacket into a binary ServiceEnvelope and publish const meshtastic_ServiceEnvelope se = { .packet = mp, .channel_id = (char *)channels.getGlobalId(channels.getPrimaryIndex()), // Use primary channel as the channel_id .gateway_id = const_cast(nodeId.c_str())}; size_t numBytes = pb_encode_to_bytes(bytes, sizeof(bytes), &meshtastic_ServiceEnvelope_msg, &se); LOG_INFO("MQTT Publish map report to %s", mapTopic.c_str()); publish(mapTopic.c_str(), bytes, numBytes, false); // Release the allocated memory for MeshPacket packetPool.release(mp); // Update the last report time last_report_to_map = millis(); } ================================================ FILE: src/mqtt/MQTT.h ================================================ #pragma once #include "Default.h" #include "configuration.h" #include "concurrency/OSThread.h" #include "mesh/Channels.h" #include "mesh/generated/meshtastic/mqtt.pb.h" #if !defined(ARCH_NRF52) || NRF52_USE_JSON #include "serialization/JSON.h" #endif #if HAS_WIFI #include #if __has_include() #include #endif #endif #if HAS_ETHERNET && !defined(USE_WS5500) #include #endif #if HAS_NETWORKING #include #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 { public: MQTT(); /** * Publish a packet on the global MQTT server. * @param mp_encrypted the encrypted packet to publish * @param mp_decoded the decrypted packet to publish * @param chIndex the index of the channel for this message * * Note: for messages we are forwarding on the mesh that we can't find the channel for (because we don't have the keys), we * can not forward those messages to the cloud - because no way to find a global channel ID. */ void onSend(const meshtastic_MeshPacket &mp_encrypted, const meshtastic_MeshPacket &mp_decoded, ChannelIndex chIndex); 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); }; bool isUsingDefaultServer() { return isConfiguredForDefaultServer; } bool isUsingDefaultRootTopic() { return isConfiguredForDefaultRootTopic; } /// Validate the meshtastic_ModuleConfig_MQTTConfig. static bool isValidConfig(const meshtastic_ModuleConfig_MQTTConfig &config) { return isValidConfig(config, nullptr); } protected: struct QueueEntry { std::string topic; std::basic_string envBytes; // binary/pb_encode_to_bytes ServiceEnvelope }; PointerQueue mqttQueue; int reconnectCount = 0; bool isConfiguredForDefaultServer = true; bool isConfiguredForDefaultRootTopic = true; virtual int32_t runOnce() override; #ifndef PIO_UNIT_TESTING private: #endif #if HAS_WIFI using MQTTClient = WiFiClient; #if __has_include() using MQTTClientTLS = WiFiClientSecure; #define MQTT_SUPPORTS_TLS 1 #endif #elif HAS_ETHERNET using MQTTClient = EthernetClient; #else using MQTTClient = void; #endif #if HAS_NETWORKING std::unique_ptr mqttClient; #if MQTT_SUPPORTS_TLS MQTTClientTLS mqttClientTLS; #endif PubSubClient pubSub; explicit MQTT(std::unique_ptr mqttClient); #endif std::string cryptTopic = "/2/e/"; // msh/2/e/CHANNELID/NODEID std::string jsonTopic = "/2/json/"; // msh/2/json/CHANNELID/NODEID std::string mapTopic = "/2/map/"; // For protobuf-encoded MapReport messages // For map reporting (only applies when enabled) const uint32_t default_map_position_precision = 14; // defaults to max. offset of ~1459m uint32_t last_report_to_map = 0; uint32_t map_position_precision = default_map_position_precision; uint32_t map_publish_interval_msecs = default_map_publish_interval_secs * 1000; /** Attempt to connect to server if necessary */ void reconnect(); /** 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); static bool isValidConfig(const meshtastic_ModuleConfig_MQTTConfig &config, MQTTClient *client); /// Called when a new publish arrives from the MQTT server void onReceive(char *topic, byte *payload, size_t length); void publishQueuedMessages(); void publishNodeInfo(); // Check if we should report unencrypted information about our node for consumption by a map void perhapsReportToMap(); /// Return 0 if sleep is okay, veto sleep if we are connected to pubsub server // int preflightSleepCb(void *unused = NULL) { return pubSub.connected() ? 1 : 0; } }; void mqttInit(); extern MQTT *mqtt; ================================================ FILE: src/mqtt/ServiceEnvelope.cpp ================================================ #include "ServiceEnvelope.h" #include "mesh-pb-constants.h" #include DecodedServiceEnvelope::DecodedServiceEnvelope(const uint8_t *payload, size_t length) : meshtastic_ServiceEnvelope(meshtastic_ServiceEnvelope_init_default), validDecode(pb_decode_from_bytes(payload, length, &meshtastic_ServiceEnvelope_msg, this)) { } DecodedServiceEnvelope::DecodedServiceEnvelope(DecodedServiceEnvelope &&other) : meshtastic_ServiceEnvelope(meshtastic_ServiceEnvelope_init_zero), validDecode(other.validDecode) { std::swap(packet, other.packet); std::swap(channel_id, other.channel_id); std::swap(gateway_id, other.gateway_id); } DecodedServiceEnvelope::~DecodedServiceEnvelope() { if (validDecode) pb_release(&meshtastic_ServiceEnvelope_msg, this); } ================================================ FILE: src/mqtt/ServiceEnvelope.h ================================================ #pragma once #include "mesh/generated/meshtastic/mqtt.pb.h" // meshtastic_ServiceEnvelope that automatically releases dynamically allocated memory when it goes out of scope. struct DecodedServiceEnvelope : public meshtastic_ServiceEnvelope { DecodedServiceEnvelope(const uint8_t *payload, size_t length); DecodedServiceEnvelope(DecodedServiceEnvelope &) = delete; DecodedServiceEnvelope(DecodedServiceEnvelope &&); ~DecodedServiceEnvelope(); // Clients must check that this is true before using. const bool validDecode; }; ================================================ FILE: src/network-stubs.cpp ================================================ #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 ================================================ FILE: src/nimble/NimbleBluetooth.cpp ================================================ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_BLUETOOTH #include "BluetoothCommon.h" #include "NimbleBluetooth.h" #include "PowerFSM.h" #include "StaticPointerQueue.h" #include "concurrency/OSThread.h" #include "main.h" #include "mesh/PhoneAPI.h" #include "mesh/mesh-pb-constants.h" #include "sleep.h" #include #include #include #ifdef NIMBLE_TWO #include "NimBLEAdvertising.h" #include "NimBLEExtAdvertising.h" #include "PowerStatus.h" #endif #if defined(CONFIG_NIMBLE_CPP_IDF) #include "host/ble_gap.h" #else #include "nimble/nimble/host/include/host/ble_gap.h" #endif #if defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C6) namespace { constexpr uint16_t kPreferredBleMtu = 517; constexpr uint16_t kPreferredBleTxOctets = 251; constexpr uint16_t kPreferredBleTxTimeUs = (kPreferredBleTxOctets + 14) * 8; } // namespace #endif // Debugging options: careful, they slow things down quite a bit! // #define DEBUG_NIMBLE_ON_READ_TIMING // uncomment to time onRead duration // #define DEBUG_NIMBLE_ON_WRITE_TIMING // uncomment to time onWrite duration // #define DEBUG_NIMBLE_NOTIFY // uncomment to enable notify logging #define NIMBLE_BLUETOOTH_TO_PHONE_QUEUE_SIZE 3 #define NIMBLE_BLUETOOTH_FROM_PHONE_QUEUE_SIZE 3 NimBLECharacteristic *fromNumCharacteristic; NimBLECharacteristic *BatteryCharacteristic; NimBLECharacteristic *logRadioCharacteristic; NimBLEServer *bleServer; static bool passkeyShowing; static std::atomic nimbleBluetoothConnHandle{BLE_HS_CONN_HANDLE_NONE}; // BLE_HS_CONN_HANDLE_NONE means "no connection" static void clearPairingDisplay() { if (!passkeyShowing) { return; } passkeyShowing = false; #if HAS_SCREEN if (screen) { screen->endAlert(); } #endif } class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread { /* CAUTION: There's a lot going on here and lots of room to break things. This NimbleBluetooth.cpp file does some tricky synchronization between the NimBLE FreeRTOS task (which runs the onRead and onWrite callbacks) and the main task (which runs runOnce and the rest of PhoneAPI). The main idea is to add a little bit of synchronization here to make it so that the rest of the codebase doesn't have to know about concurrency and mutexes, and can just run happily ever after as a cooperative multitasking OSThread system, where locking isn't something that anyone has to worry about too much! :) We achieve this by having some queues and mutexes in this file only, and ensuring that all calls to getFromRadio and handleToRadio are only made from the main FreeRTOS task. This way, the rest of the codebase doesn't have to worry about being run concurrently, which would make everything else much much much more complicated. PHONE -> RADIO: - [NimBLE FreeRTOS task:] onWrite callback holds fromPhoneMutex and pushes received packets into fromPhoneQueue. - [Main task:] runOnceHandleFromPhoneQueue in main task holds fromPhoneMutex, pulls packets from fromPhoneQueue, and calls handleToRadio **in main task**. RADIO -> PHONE: - [NimBLE FreeRTOS task:] onRead callback sets onReadCallbackIsWaitingForData flag and polls in a busy loop. (unless there's already a packet waiting in toPhoneQueue) - [Main task:] runOnceHandleToPhoneQueue sees onReadCallbackIsWaitingForData flag, calls getFromRadio **in main task** to get packets from radio, holds toPhoneMutex, pushes the packet into toPhoneQueue, and clears the onReadCallbackIsWaitingForData flag. - [NimBLE FreeRTOS task:] onRead callback sees that the onReadCallbackIsWaitingForData flag cleared, holds toPhoneMutex, pops the packet from toPhoneQueue, and returns it to NimBLE. MUTEXES: - fromPhoneMutex protects fromPhoneQueue and fromPhoneQueueSize - toPhoneMutex protects toPhoneQueue, toPhoneQueueByteSizes, and toPhoneQueueSize ATOMICS: - fromPhoneQueueSize is only increased by onWrite, and only decreased by runOnceHandleFromPhoneQueue (or onDisconnect). - toPhoneQueueSize is only increased by runOnceHandleToPhoneQueue, and only decreased by onRead (or onDisconnect). - onReadCallbackIsWaitingForData is a flag. It's only set by onRead, and only cleared by runOnceHandleToPhoneQueue (or onDisconnect). PRELOADING: see comments in runOnceToPhoneCanPreloadNextPacket about when it's safe to preload packets from getFromRadio. BLE CONNECTION PARAMS: - During config, we request a high-throughput, low-latency BLE connection for speed. - After config, we switch to a lower-power BLE connection for steady-state use to extend battery life. MEMORY MANAGEMENT: - We keep packets on the stack and do not allocate heap. - We use std::array for fromPhoneQueue and toPhoneQueue to avoid mallocs and frees across FreeRTOS tasks. - Yes, we have to do some copy operations on pop because of this, but it's worth it to avoid cross-task memory management. NOTIFY IS BROKEN: - Adding NIMBLE_PROPERTY::NOTIFY to FromRadioCharacteristic appears to break things. It is NOT backwards compatible. ZERO-SIZE READS: - Returning a zero-size read from onRead breaks some clients during the config phase. So we have to block onRead until we have data. - During the STATE_SEND_PACKETS phase, it's totally OK to return zero-size reads, as clients are expected to do reads until they get a 0-byte response. CROSS-TASK WAKEUP: - If you call: bluetoothPhoneAPI->setIntervalFromNow(0); to schedule immediate processing of new data, - Then you should also call: concurrency::mainDelay.interrupt(); to wake up the main loop if it's sleeping. - Otherwise, you're going to wait ~100ms or so until the main loop wakes up from some other cause. */ public: BluetoothPhoneAPI() : concurrency::OSThread("NimbleBluetooth") { api_type = TYPE_BLE; } /* Packets from phone (BLE onWrite callback) */ std::mutex fromPhoneMutex; std::atomic fromPhoneQueueSize{0}; // We use array here (and pay the cost of memcpy) to avoid dynamic memory allocations and frees across FreeRTOS tasks. std::array fromPhoneQueue{}; /* Packets to phone (BLE onRead callback) */ std::mutex toPhoneMutex; std::atomic toPhoneQueueSize{0}; // We use array here (and pay the cost of memcpy) to avoid dynamic memory allocations and frees across FreeRTOS tasks. std::array, NIMBLE_BLUETOOTH_TO_PHONE_QUEUE_SIZE> toPhoneQueue{}; std::array toPhoneQueueByteSizes{}; // The onReadCallbackIsWaitingForData flag provides synchronization between the NimBLE task's onRead callback and our main // task's runOnce. It's only set by onRead, and only cleared by runOnce. std::atomic onReadCallbackIsWaitingForData{false}; /* Statistics/logging helpers */ std::atomic readCount{0}; std::atomic notifyCount{0}; std::atomic writeCount{0}; protected: virtual int32_t runOnce() override { while (runOnceHasWorkToDo()) { /* PROCESS fromPhoneQueue BEFORE toPhoneQueue: In normal STATE_SEND_PACKETS operation, it's unlikely that we'll have both writes and reads to process at the same time, because either onWrite or onRead will trigger this runOnce. And in STATE_SEND_PACKETS, it's generally ok to service either the reads or writes first. However, during the initial setup wantConfig packet, the clients send a write and immediately send a read, and they expect the read will respond to the write. (This also happens when a client goes from STATE_SEND_PACKETS back to another wantConfig, like the iOS client does when requesting the nodedb after requesting the main config only.) So it's safest to always service writes (fromPhoneQueue) before reads (toPhoneQueue), so that any "synchronous" write-then-read sequences from the client work as expected, even if this means we block onRead for a while: this is what the client wants! */ // PHONE -> RADIO: runOnceHandleFromPhoneQueue(); // pull data from onWrite to handleToRadio // RADIO -> PHONE: runOnceHandleToPhoneQueue(); // push data from getFromRadio to onRead } // the run is triggered via NimbleBluetoothToRadioCallback and NimbleBluetoothFromRadioCallback return INT32_MAX; } virtual void onConfigStart() override { LOG_INFO("BLE onConfigStart"); // Prefer high throughput during config/setup, at the cost of high power consumption (for a few seconds) if (bleServer && isConnected()) { uint16_t conn_handle = nimbleBluetoothConnHandle.load(); if (conn_handle != BLE_HS_CONN_HANDLE_NONE) { requestHighThroughputConnection(conn_handle); } } } virtual void onConfigComplete() override { LOG_INFO("BLE onConfigComplete"); // Switch to lower power consumption BLE connection params for steady-state use after config/setup is complete if (bleServer && isConnected()) { uint16_t conn_handle = nimbleBluetoothConnHandle.load(); if (conn_handle != BLE_HS_CONN_HANDLE_NONE) { requestLowerPowerConnection(conn_handle); } } } bool runOnceHasWorkToDo() { return runOnceHasWorkToPhone() || runOnceHasWorkFromPhone(); } bool runOnceHasWorkToPhone() { return onReadCallbackIsWaitingForData || runOnceToPhoneCanPreloadNextPacket(); } bool runOnceToPhoneCanPreloadNextPacket() { /* * PRELOADING getFromRadio RESPONSES: * * It's not safe to preload packets if we're in STATE_SEND_PACKETS, because there may be a while between the time we call * getFromRadio and when the client actually reads it. If the connection drops in that time, we might lose that packet * forever. In STATE_SEND_PACKETS, if we wait for onRead before we call getFromRadio, we minimize the time window where * the client might disconnect before completing the read. * * However, if we're in the setup states (sending config, nodeinfo, etc), it's safe and beneficial to preload packets into * toPhoneQueue because the client will just reconnect after a disconnect, losing nothing. */ if (!isConnected()) { return false; } else if (isSendingPackets()) { // If we're in STATE_SEND_PACKETS, we must wait for onRead before calling getFromRadio. return false; } else { // In other states, we can preload as long as there's space in the toPhoneQueue. return toPhoneQueueSize < NIMBLE_BLUETOOTH_TO_PHONE_QUEUE_SIZE; } } void runOnceHandleToPhoneQueue() { // Stack buffer for getFromRadio packet uint8_t fromRadioBytes[meshtastic_FromRadio_size] = {0}; size_t numBytes = 0; if (onReadCallbackIsWaitingForData || runOnceToPhoneCanPreloadNextPacket()) { numBytes = getFromRadio(fromRadioBytes); if (numBytes == 0) { /* Client expected a read, but we have nothing to send. In STATE_SEND_PACKETS, it is 100% OK to return a 0-byte response, as we expect clients to do read beyond notifies regularly, to make sure they have nothing else to read. In other states, this is fine **so long as we've already processed pending onWrites first**, because the client may requesting wantConfig and immediately doing a read. */ } else { // Push to toPhoneQueue, protected by toPhoneMutex. Hold the mutex as briefly as possible. if (toPhoneQueueSize < NIMBLE_BLUETOOTH_TO_PHONE_QUEUE_SIZE) { // Note: the comparison above is safe without a mutex because we are the only method that *increases* // toPhoneQueueSize. (It's okay if toPhoneQueueSize *decreases* in the NimBLE task meanwhile.) { // scope for toPhoneMutex mutex std::lock_guard guard(toPhoneMutex); size_t storeAtIndex = toPhoneQueueSize.load(); memcpy(toPhoneQueue[storeAtIndex].data(), fromRadioBytes, numBytes); toPhoneQueueByteSizes[storeAtIndex] = numBytes; toPhoneQueueSize++; } #ifdef DEBUG_NIMBLE_ON_READ_TIMING LOG_DEBUG("BLE getFromRadio returned numBytes=%u, pushed toPhoneQueueSize=%u", numBytes, toPhoneQueueSize.load()); #endif } else { // Shouldn't happen because the onRead callback shouldn't be waiting if the queue is full! LOG_ERROR("Shouldn't happen! Drop FromRadio packet, toPhoneQueue full (%u bytes)", numBytes); } } // Clear the onReadCallbackIsWaitingForData flag so onRead knows it can proceed. onReadCallbackIsWaitingForData = false; // only clear this flag AFTER the push } } bool runOnceHasWorkFromPhone() { return fromPhoneQueueSize > 0; } void runOnceHandleFromPhoneQueue() { // Handle packets we received from onWrite from the phone. if (fromPhoneQueueSize > 0) { // Note: the comparison above is safe without a mutex because we are the only method that *decreases* // fromPhoneQueueSize. (It's okay if fromPhoneQueueSize *increases* in the NimBLE task meanwhile.) LOG_DEBUG("NimbleBluetooth: handling ToRadio packet, fromPhoneQueueSize=%u", fromPhoneQueueSize.load()); // Pop the front of fromPhoneQueue, holding the mutex only briefly while we pop. NimBLEAttValue val; { // scope for fromPhoneMutex mutex std::lock_guard guard(fromPhoneMutex); val = fromPhoneQueue[0]; // Shift the rest of the queue down for (uint8_t i = 1; i < fromPhoneQueueSize; i++) { fromPhoneQueue[i - 1] = fromPhoneQueue[i]; } // Safe decrement due to onDisconnect if (fromPhoneQueueSize > 0) fromPhoneQueueSize--; } handleToRadio(val.data(), val.length()); } } /** * Subclasses can use this as a hook to provide custom notifications for their transport (i.e. bluetooth notifies) */ virtual void onNowHasData(uint32_t fromRadioNum) { PhoneAPI::onNowHasData(fromRadioNum); #ifdef DEBUG_NIMBLE_NOTIFY int currentNotifyCount = notifyCount.fetch_add(1); uint8_t cc = bleServer->getConnectedCount(); // This logging slows things down when there are lots of packets going to the phone, like initial connection: LOG_DEBUG("BLE notify(%d) fromNum: %d connections: %d", currentNotifyCount, fromRadioNum, cc); #endif uint8_t val[4]; put_le32(val, fromRadioNum); fromNumCharacteristic->setValue(val, sizeof(val)); #ifdef NIMBLE_TWO // NOTE: I don't have any NIMBLE_TWO devices, but this line makes me suspicious, and I suspect it needs to just be // notify(). fromNumCharacteristic->notify(val, sizeof(val), BLE_HS_CONN_HANDLE_NONE); #else fromNumCharacteristic->notify(); #endif } /// Check the current underlying physical link to see if the client is currently connected virtual bool checkIsConnected() { return bleServer && bleServer->getConnectedCount() > 0; } void requestHighThroughputConnection(uint16_t conn_handle) { /* Request a lower-latency, higher-throughput BLE connection. This comes at the cost of higher power consumption, so we may want to only use this for initial setup, and then switch to a slower mode. See https://developer.apple.com/library/archive/qa/qa1931/_index.html for formulas to calculate values, iOS/macOS constraints, and recommendations. (Android doesn't have specific constraints, but seems to be compatible with the Apple recommendations.) Selected settings: minInterval (units of 1.25ms): 7.5ms = 6 (lower than the Apple recommended minimum, but allows faster when the client supports it.) maxInterval (units of 1.25ms): 15ms = 12 latency: 0 (don't allow peripheral to skip any connection events) timeout (units of 10ms): 6 seconds = 600 (supervision timeout) These are intentionally aggressive to prioritize speed over power consumption, but are only used for a few seconds at setup. Not worth adjusting much. */ LOG_INFO("BLE requestHighThroughputConnection"); bleServer->updateConnParams(conn_handle, 6, 12, 0, 600); } void requestLowerPowerConnection(uint16_t conn_handle) { /* Request a lower power consumption (but higher latency, lower throughput) BLE connection. This is suitable for steady-state operation after initial setup is complete. See https://developer.apple.com/library/archive/qa/qa1931/_index.html for formulas to calculate values, iOS/macOS constraints, and recommendations. (Android doesn't have specific constraints, but seems to be compatible with the Apple recommendations.) Selected settings: minInterval (units of 1.25ms): 30ms = 24 maxInterval (units of 1.25ms): 50ms = 40 latency: 2 (allow peripheral to skip up to 2 consecutive connection events to save power) timeout (units of 10ms): 6 seconds = 600 (supervision timeout) There's an opportunity for tuning here if anyone wants to do some power measurements, but these should allow 10-20 packets per second. */ LOG_INFO("BLE requestLowerPowerConnection"); bleServer->updateConnParams(conn_handle, 24, 40, 2, 600); } }; 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 { #ifdef NIMBLE_TWO virtual void onWrite(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo) #else virtual void onWrite(NimBLECharacteristic *pCharacteristic) #endif { // CAUTION: This callback runs in the NimBLE task!!! Don't do anything except communicate with the main task's runOnce. // Assumption: onWrite is serialized by NimBLE, so we don't need to lock here against multiple concurrent onWrite calls. int currentWriteCount = bluetoothPhoneAPI->writeCount.fetch_add(1); #ifdef DEBUG_NIMBLE_ON_WRITE_TIMING int startMillis = millis(); LOG_DEBUG("BLE onWrite(%d): start millis=%d", currentWriteCount, startMillis); #endif auto val = pCharacteristic->getValue(); if (memcmp(lastToRadio, val.data(), val.length()) != 0) { if (bluetoothPhoneAPI->fromPhoneQueueSize < NIMBLE_BLUETOOTH_FROM_PHONE_QUEUE_SIZE) { // Note: the comparison above is safe without a mutex because we are the only method that *increases* // fromPhoneQueueSize. (It's okay if fromPhoneQueueSize *decreases* in the main task meanwhile.) memcpy(lastToRadio, val.data(), val.length()); { // scope for fromPhoneMutex mutex // Append to fromPhoneQueue, protected by fromPhoneMutex. Hold the mutex as briefly as possible. std::lock_guard guard(bluetoothPhoneAPI->fromPhoneMutex); bluetoothPhoneAPI->fromPhoneQueue.at(bluetoothPhoneAPI->fromPhoneQueueSize) = val; bluetoothPhoneAPI->fromPhoneQueueSize++; } // After releasing the mutex, schedule immediate processing of the new packet. bluetoothPhoneAPI->setIntervalFromNow(0); concurrency::mainDelay.interrupt(); // wake up main loop if sleeping #ifdef DEBUG_NIMBLE_ON_WRITE_TIMING int finishMillis = millis(); LOG_DEBUG("BLE onWrite(%d): append to fromPhoneQueue took %u ms. numBytes=%d", currentWriteCount, finishMillis - startMillis, val.length()); #endif } else { LOG_WARN("BLE onWrite(%d): Drop ToRadio packet, fromPhoneQueue full (%u bytes)", currentWriteCount, val.length()); } } else { LOG_DEBUG("BLE onWrite(%d): Drop duplicate ToRadio packet (%u bytes)", currentWriteCount, val.length()); } } }; class NimbleBluetoothFromRadioCallback : public NimBLECharacteristicCallbacks { #ifdef NIMBLE_TWO virtual void onRead(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo) #else virtual void onRead(NimBLECharacteristic *pCharacteristic) #endif { // CAUTION: This callback runs in the NimBLE task!!! Don't do anything except communicate with the main task's runOnce. int currentReadCount = bluetoothPhoneAPI->readCount.fetch_add(1); int tries = 0; int startMillis = millis(); #ifdef DEBUG_NIMBLE_ON_READ_TIMING LOG_DEBUG("BLE onRead(%d): start millis=%d", currentReadCount, startMillis); #endif // Is there a packet ready to go, or do we have to ask the main task to get one for us? if (bluetoothPhoneAPI->toPhoneQueueSize > 0) { // Note: the comparison above is safe without a mutex because we are the only method that *decreases* // toPhoneQueueSize. (It's okay if toPhoneQueueSize *increases* in the main task meanwhile.) // There's already a packet queued. Great! We don't need to wait for onReadCallbackIsWaitingForData. #ifdef DEBUG_NIMBLE_ON_READ_TIMING LOG_DEBUG("BLE onRead(%d): packet already waiting, no need to set onReadCallbackIsWaitingForData", currentReadCount); #endif } else { // Tell the main task that we'd like a packet. bluetoothPhoneAPI->onReadCallbackIsWaitingForData = true; // Wait for the main task to produce a packet for us, up to about 20 seconds. // It normally takes just a few milliseconds, but at initial startup, etc, the main task can get blocked for longer // doing various setup tasks. while (bluetoothPhoneAPI->onReadCallbackIsWaitingForData && tries < 4000) { // Schedule the main task runOnce to run ASAP. bluetoothPhoneAPI->setIntervalFromNow(0); concurrency::mainDelay.interrupt(); // wake up main loop if sleeping if (!bluetoothPhoneAPI->onReadCallbackIsWaitingForData) { // we may be able to break even before a delay, if the call to interrupt woke up the main loop and it ran // already #ifdef DEBUG_NIMBLE_ON_READ_TIMING LOG_DEBUG("BLE onRead(%d): broke before delay after %u ms, %d tries", currentReadCount, millis() - startMillis, tries); #endif break; } // This delay happens in the NimBLE FreeRTOS task, which really can't do anything until we get a value back. // No harm in polling pretty frequently. delay(tries < 20 ? 1 : 5); tries++; if (tries == 4000) { LOG_WARN( "BLE onRead(%d): timeout waiting for data after %u ms, %d tries, giving up and returning 0-size response", currentReadCount, millis() - startMillis, tries); } } } // Pop from toPhoneQueue, protected by toPhoneMutex. Hold the mutex as briefly as possible. uint8_t fromRadioBytes[meshtastic_FromRadio_size] = {0}; // Stack buffer for getFromRadio packet size_t numBytes = 0; { // scope for toPhoneMutex mutex std::lock_guard guard(bluetoothPhoneAPI->toPhoneMutex); size_t toPhoneQueueSize = bluetoothPhoneAPI->toPhoneQueueSize.load(); if (toPhoneQueueSize > 0) { // Copy from the front of the toPhoneQueue memcpy(fromRadioBytes, bluetoothPhoneAPI->toPhoneQueue[0].data(), bluetoothPhoneAPI->toPhoneQueueByteSizes[0]); numBytes = bluetoothPhoneAPI->toPhoneQueueByteSizes[0]; // Shift the rest of the queue down for (uint8_t i = 1; i < toPhoneQueueSize; i++) { memcpy(bluetoothPhoneAPI->toPhoneQueue[i - 1].data(), bluetoothPhoneAPI->toPhoneQueue[i].data(), bluetoothPhoneAPI->toPhoneQueueByteSizes[i]); // The above line is similar to: // bluetoothPhoneAPI->toPhoneQueue[i - 1] = bluetoothPhoneAPI->toPhoneQueue[i] // but is usually faster because it doesn't have to copy all the trailing bytes beyond // toPhoneQueueByteSizes[i]. // // We deliberately use an array here (and pay the CPU cost of some memcpy) to avoid synchronizing dynamic // memory allocations and frees across FreeRTOS tasks. bluetoothPhoneAPI->toPhoneQueueByteSizes[i - 1] = bluetoothPhoneAPI->toPhoneQueueByteSizes[i]; } // Safe decrement due to onDisconnect if (bluetoothPhoneAPI->toPhoneQueueSize > 0) bluetoothPhoneAPI->toPhoneQueueSize--; } else { // nothing in the toPhoneQueue; that's fine, and we'll just have numBytes=0. } } #ifdef DEBUG_NIMBLE_ON_READ_TIMING int finishMillis = millis(); LOG_DEBUG("BLE onRead(%d): onReadCallbackIsWaitingForData took %u ms, %d tries. numBytes=%d", currentReadCount, finishMillis - startMillis, tries, numBytes); #endif pCharacteristic->setValue(fromRadioBytes, numBytes); // If we sent something, wake up the main loop if it's sleeping in case there are more packets ready to enqueue. if (numBytes != 0) { bluetoothPhoneAPI->setIntervalFromNow(0); concurrency::mainDelay.interrupt(); // wake up main loop if sleeping } } }; class NimbleBluetoothServerCallback : public NimBLEServerCallbacks { #ifdef NIMBLE_TWO public: NimbleBluetoothServerCallback(NimbleBluetooth *ble) { this->ble = ble; } private: NimbleBluetooth *ble; virtual uint32_t onPassKeyDisplay() #else virtual uint32_t onPassKeyRequest() #endif { uint32_t passkey = config.bluetooth.fixed_pin; if (config.bluetooth.mode == meshtastic_Config_BluetoothConfig_PairingMode_RANDOM_PIN) { LOG_INFO("Use random passkey"); // This is the passkey to be entered on peer - we pick a number >100,000 to ensure 6 digits passkey = random(100000, 999999); } LOG_INFO("*** Enter passkey %d on the peer side ***", passkey); powerFSM.trigger(EVENT_BLUETOOTH_PAIR); meshtastic::BluetoothStatus newStatus(std::to_string(passkey)); bluetoothStatus->updateStatus(&newStatus); #if HAS_SCREEN // Todo: migrate this display code back into Screen class, and observe bluetoothStatus if (screen) { screen->startAlert([passkey](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -> void { char btPIN[16] = "888888"; snprintf(btPIN, sizeof(btPIN), "%06u", passkey); int x_offset = display->width() / 2; int y_offset = display->height() <= 80 ? 0 : 12; display->setTextAlignment(TEXT_ALIGN_CENTER); display->setFont(FONT_MEDIUM); display->drawString(x_offset + x, y_offset + y, "Bluetooth"); #if !defined(M5STACK_UNITC6L) display->setFont(FONT_SMALL); y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_MEDIUM - 4 : y_offset + FONT_HEIGHT_MEDIUM + 5; display->drawString(x_offset + x, y_offset + y, "Enter this code"); #endif display->setFont(FONT_LARGE); char pin[8]; snprintf(pin, sizeof(pin), "%.3s %.3s", btPIN, btPIN + 3); y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_SMALL - 5 : y_offset + FONT_HEIGHT_SMALL + 5; display->drawString(x_offset + x, y_offset + y, pin); display->setFont(FONT_SMALL); char deviceName[64]; snprintf(deviceName, sizeof(deviceName), "Name: %s", getDeviceName()); y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_LARGE - 6 : y_offset + FONT_HEIGHT_LARGE + 5; display->drawString(x_offset + x, y_offset + y, deviceName); }); } #endif passkeyShowing = true; return passkey; } #ifdef NIMBLE_TWO virtual void onAuthenticationComplete(NimBLEConnInfo &connInfo) #else virtual void onAuthenticationComplete(ble_gap_conn_desc *desc) #endif { LOG_INFO("BLE authentication complete"); meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED); bluetoothStatus->updateStatus(&newStatus); clearPairingDisplay(); // Store the connection handle for future use #ifdef NIMBLE_TWO nimbleBluetoothConnHandle = connInfo.getConnHandle(); #else nimbleBluetoothConnHandle = desc->conn_handle; #endif } #ifdef NIMBLE_TWO virtual void onConnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo) { LOG_INFO("BLE incoming connection %s", connInfo.getAddress().toString().c_str()); const uint16_t connHandle = connInfo.getConnHandle(); #if NIMBLE_ENABLE_2M_PHY && (defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C6)) int phyResult = ble_gap_set_prefered_le_phy(connHandle, BLE_GAP_LE_PHY_2M_MASK, BLE_GAP_LE_PHY_2M_MASK, BLE_GAP_LE_PHY_CODED_ANY); if (phyResult == 0) { LOG_INFO("BLE conn %u requested 2M PHY", connHandle); } else { LOG_WARN("Failed to prefer 2M PHY for conn %u, rc=%d", connHandle, phyResult); } #endif int dataLenResult = ble_gap_set_data_len(connHandle, kPreferredBleTxOctets, kPreferredBleTxTimeUs); if (dataLenResult == 0) { LOG_INFO("BLE conn %u requested data length %u bytes", connHandle, kPreferredBleTxOctets); } else { LOG_WARN("Failed to raise data length for conn %u, rc=%d", connHandle, dataLenResult); } LOG_INFO("BLE conn %u initial MTU %u (target %u)", connHandle, connInfo.getMTU(), kPreferredBleMtu); pServer->updateConnParams(connHandle, 6, 12, 0, 200); } #endif #ifdef NIMBLE_TWO virtual void onDisconnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo, int reason) { LOG_INFO("BLE disconnect reason: %d", reason); #else virtual void onDisconnect(NimBLEServer *pServer, ble_gap_conn_desc *desc) { LOG_INFO("BLE disconnect"); #endif #ifdef NIMBLE_TWO if (ble->isDeInit) return; #else if (nimbleBluetooth && nimbleBluetooth->isDeInit) return; #endif meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED); bluetoothStatus->updateStatus(&newStatus); clearPairingDisplay(); if (bluetoothPhoneAPI) { bluetoothPhoneAPI->close(); { // scope for fromPhoneMutex mutex std::lock_guard guard(bluetoothPhoneAPI->fromPhoneMutex); bluetoothPhoneAPI->fromPhoneQueueSize = 0; } bluetoothPhoneAPI->onReadCallbackIsWaitingForData = false; { // scope for toPhoneMutex mutex std::lock_guard guard(bluetoothPhoneAPI->toPhoneMutex); bluetoothPhoneAPI->toPhoneQueueSize = 0; } bluetoothPhoneAPI->readCount = 0; bluetoothPhoneAPI->notifyCount = 0; bluetoothPhoneAPI->writeCount = 0; } // Clear the last ToRadio packet buffer to avoid rejecting first packet from new connection memset(lastToRadio, 0, sizeof(lastToRadio)); nimbleBluetoothConnHandle = BLE_HS_CONN_HANDLE_NONE; // BLE_HS_CONN_HANDLE_NONE means "no connection" #ifdef NIMBLE_TWO // Restart Advertising ble->startAdvertising(); #else NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising(); if (!pAdvertising->start(0)) { if (pAdvertising->isAdvertising()) { LOG_DEBUG("BLE advertising already running"); } else { LOG_ERROR("BLE failed to restart advertising"); } } #endif } }; static NimbleBluetoothToRadioCallback *toRadioCallbacks; static NimbleBluetoothFromRadioCallback *fromRadioCallbacks; void NimbleBluetooth::shutdown() { // No measurable power saving for ESP32 during light-sleep(?) #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"); isDeInit = true; #ifdef BLE_LED digitalWrite(BLE_LED, LED_STATE_OFF); #endif #ifndef NIMBLE_TWO NimBLEDevice::deinit(); #endif #endif } // Has initial setup been completed bool NimbleBluetooth::isActive() { return bleServer; } bool NimbleBluetooth::isConnected() { return bleServer->getConnectedCount() > 0; } int NimbleBluetooth::getRssi() { #if defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C6) if (!bleServer || !isConnected()) { return 0; // No active BLE connection } uint16_t connHandle = nimbleBluetoothConnHandle.load(); if (connHandle == BLE_HS_CONN_HANDLE_NONE) { const auto peers = bleServer->getPeerDevices(); if (!peers.empty()) { connHandle = peers.front(); nimbleBluetoothConnHandle = connHandle; } } if (connHandle == BLE_HS_CONN_HANDLE_NONE) { return 0; // Connection handle not available yet } int8_t rssi = 0; const int rc = ble_gap_conn_rssi(connHandle, &rssi); if (rc == 0) { return rssi; } LOG_DEBUG("BLE RSSI read failed, rc=%d", rc); #endif return 0; } void NimbleBluetooth::setup() { // Uncomment for testing // NimbleBluetooth::clearBonds(); LOG_INFO("Init the NimBLE bluetooth module"); NimBLEDevice::init(getDeviceName()); NimBLEDevice::setPower(ESP_PWR_LVL_P9); #if NIMBLE_ENABLE_2M_PHY && (defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C6)) int mtuResult = NimBLEDevice::setMTU(kPreferredBleMtu); if (mtuResult == 0) { LOG_INFO("BLE MTU request set to %u", kPreferredBleMtu); } else { LOG_WARN("Unable to request MTU %u, rc=%d", kPreferredBleMtu, mtuResult); } int phyResult = ble_gap_set_prefered_default_le_phy(BLE_GAP_LE_PHY_2M_MASK, BLE_GAP_LE_PHY_2M_MASK); if (phyResult == 0) { LOG_INFO("BLE default PHY preference set to 2M"); } else { LOG_WARN("Failed to prefer 2M PHY by default, rc=%d", phyResult); } int dataLenResult = ble_gap_write_sugg_def_data_len(kPreferredBleTxOctets, kPreferredBleTxTimeUs); if (dataLenResult == 0) { LOG_INFO("BLE suggested data length set to %u bytes", kPreferredBleTxOctets); } else { LOG_WARN("Failed to raise suggested data length (%u/%u), rc=%d", kPreferredBleTxOctets, kPreferredBleTxTimeUs, dataLenResult); } #endif if (config.bluetooth.mode != meshtastic_Config_BluetoothConfig_PairingMode_NO_PIN) { NimBLEDevice::setSecurityAuth(BLE_SM_PAIR_AUTHREQ_BOND | BLE_SM_PAIR_AUTHREQ_MITM | BLE_SM_PAIR_AUTHREQ_SC); NimBLEDevice::setSecurityInitKey(BLE_SM_PAIR_KEY_DIST_ENC | BLE_SM_PAIR_KEY_DIST_ID); NimBLEDevice::setSecurityRespKey(BLE_SM_PAIR_KEY_DIST_ENC | BLE_SM_PAIR_KEY_DIST_ID); NimBLEDevice::setSecurityIOCap(BLE_HS_IO_DISPLAY_ONLY); } bleServer = NimBLEDevice::createServer(); #ifdef NIMBLE_TWO NimbleBluetoothServerCallback *serverCallbacks = new NimbleBluetoothServerCallback(this); #else NimbleBluetoothServerCallback *serverCallbacks = new NimbleBluetoothServerCallback(); #endif bleServer->setCallbacks(serverCallbacks, true); setupService(); startAdvertising(); } void NimbleBluetooth::setupService() { NimBLEService *bleService = bleServer->createService(MESH_SERVICE_UUID); NimBLECharacteristic *ToRadioCharacteristic; NimBLECharacteristic *FromRadioCharacteristic; // Define the characteristics that the app is looking for if (config.bluetooth.mode == meshtastic_Config_BluetoothConfig_PairingMode_NO_PIN) { ToRadioCharacteristic = bleService->createCharacteristic(TORADIO_UUID, NIMBLE_PROPERTY::WRITE); // Allow notifications so phones can stream FromRadio without polling. FromRadioCharacteristic = bleService->createCharacteristic(FROMRADIO_UUID, NIMBLE_PROPERTY::READ); fromNumCharacteristic = bleService->createCharacteristic(FROMNUM_UUID, NIMBLE_PROPERTY::NOTIFY | NIMBLE_PROPERTY::READ); logRadioCharacteristic = bleService->createCharacteristic(LOGRADIO_UUID, NIMBLE_PROPERTY::NOTIFY | NIMBLE_PROPERTY::READ, 512U); } else { ToRadioCharacteristic = bleService->createCharacteristic( TORADIO_UUID, NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_AUTHEN | NIMBLE_PROPERTY::WRITE_ENC); FromRadioCharacteristic = bleService->createCharacteristic( FROMRADIO_UUID, NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::READ_AUTHEN | NIMBLE_PROPERTY::READ_ENC); fromNumCharacteristic = bleService->createCharacteristic(FROMNUM_UUID, NIMBLE_PROPERTY::NOTIFY | NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::READ_AUTHEN | NIMBLE_PROPERTY::READ_ENC); logRadioCharacteristic = bleService->createCharacteristic( LOGRADIO_UUID, NIMBLE_PROPERTY::NOTIFY | NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::READ_AUTHEN | NIMBLE_PROPERTY::READ_ENC, 512U); } bluetoothPhoneAPI = new BluetoothPhoneAPI(); 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); #ifdef NIMBLE_TWO NimBLE2904 *batteryLevelDescriptor = BatteryCharacteristic->create2904(); #else NimBLE2904 *batteryLevelDescriptor = (NimBLE2904 *)BatteryCharacteristic->createDescriptor((uint16_t)0x2904); #endif batteryLevelDescriptor->setFormat(NimBLE2904::FORMAT_UINT8); batteryLevelDescriptor->setNamespace(1); batteryLevelDescriptor->setUnit(0x27ad); batteryService->start(); } void NimbleBluetooth::startAdvertising() { #ifdef NIMBLE_TWO NimBLEExtAdvertising *pAdvertising = NimBLEDevice::getAdvertising(); NimBLEExtAdvertisement legacyAdvertising; legacyAdvertising.setLegacyAdvertising(true); legacyAdvertising.setScannable(true); legacyAdvertising.setConnectable(true); legacyAdvertising.setFlags(BLE_HS_ADV_F_DISC_GEN); if (powerStatus->getHasBattery() == 1) { legacyAdvertising.setCompleteServices(NimBLEUUID((uint16_t)0x180f)); } legacyAdvertising.setCompleteServices(NimBLEUUID(MESH_SERVICE_UUID)); legacyAdvertising.setMinInterval(500); legacyAdvertising.setMaxInterval(1000); NimBLEExtAdvertisement legacyScanResponse; legacyScanResponse.setLegacyAdvertising(true); legacyScanResponse.setConnectable(true); legacyScanResponse.setName(getDeviceName()); if (!pAdvertising->setInstanceData(0, legacyAdvertising)) { LOG_ERROR("BLE failed to set legacyAdvertising"); } else if (!pAdvertising->setScanResponseData(0, legacyScanResponse)) { LOG_ERROR("BLE failed to set legacyScanResponse"); } else if (!pAdvertising->start(0, 0, 0)) { LOG_ERROR("BLE failed to start legacyAdvertising"); } #else NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising(); pAdvertising->reset(); pAdvertising->addServiceUUID(MESH_SERVICE_UUID); pAdvertising->addServiceUUID(NimBLEUUID((uint16_t)0x180f)); // 0x180F is the Battery Service pAdvertising->start(0); #endif } /// Given a level between 0-100, update the BLE attribute void updateBatteryLevel(uint8_t level) { if ((config.bluetooth.enabled == true) && bleServer && nimbleBluetooth->isConnected()) { BatteryCharacteristic->setValue(&level, 1); #ifdef NIMBLE_TWO BatteryCharacteristic->notify(&level, 1, BLE_HS_CONN_HANDLE_NONE); #else BatteryCharacteristic->notify(); #endif } } 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; } #ifdef NIMBLE_TWO logRadioCharacteristic->notify(logMessage, length, BLE_HS_CONN_HANDLE_NONE); #else logRadioCharacteristic->notify(logMessage, length, true); #endif } void clearNVS() { NimBLEDevice::deleteAllBonds(); #ifdef ARCH_ESP32 ESP.restart(); #endif } #endif ================================================ FILE: src/nimble/NimbleBluetooth.h ================================================ #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); #if defined(NIMBLE_TWO) void startAdvertising(); #endif bool isDeInit = false; private: void setupService(); #if !defined(NIMBLE_TWO) void startAdvertising(); #endif }; void setBluetoothEnable(bool enable); void clearNVS(); ================================================ FILE: src/platform/esp32/ESP32CryptoEngine.cpp ================================================ #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(); ================================================ FILE: src/platform/esp32/MeshtasticOTA.cpp ================================================ #include "MeshtasticOTA.h" #include "configuration.h" #ifdef ESP_PLATFORM #include #include #endif namespace MeshtasticOTA { static const char *nvsNamespace = "MeshtasticOTA"; static const char *combinedAppProjectName = "MeshtasticOTA"; static const char *bleOnlyAppProjectName = "MeshtasticOTA-BLE"; static const char *wifiOnlyAppProjectName = "MeshtasticOTA-WiFi"; static bool updated = false; bool isUpdated() { return updated; } void initialize() { Preferences prefs; prefs.begin(nvsNamespace); if (prefs.getBool("updated")) { LOG_INFO("First boot after OTA update"); updated = true; prefs.putBool("updated", false); } prefs.end(); } void recoverConfig(meshtastic_Config_NetworkConfig *network) { LOG_INFO("Recovering WiFi settings after OTA update"); Preferences prefs; prefs.begin(nvsNamespace, true); String ssid = prefs.getString("ssid"); String psk = prefs.getString("psk"); prefs.end(); network->wifi_enabled = true; strncpy(network->wifi_ssid, ssid.c_str(), sizeof(network->wifi_ssid)); strncpy(network->wifi_psk, psk.c_str(), sizeof(network->wifi_psk)); } void saveConfig(meshtastic_Config_NetworkConfig *network, meshtastic_OTAMode method, uint8_t *ota_hash) { LOG_INFO("Saving WiFi settings for upcoming OTA update"); Preferences prefs; prefs.begin(nvsNamespace); prefs.putUChar("method", method); prefs.putBytes("ota_hash", ota_hash, 32); prefs.putString("ssid", network->wifi_ssid); prefs.putString("psk", network->wifi_psk); prefs.putBool("updated", false); prefs.end(); } const esp_partition_t *getAppPartition() { return esp_partition_find_first(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_1, NULL); } bool getAppDesc(const esp_partition_t *part, esp_app_desc_t *app_desc) { if (esp_ota_get_partition_description(part, app_desc) != ESP_OK) { LOG_INFO("esp_ota_get_partition_description failed"); return false; } return true; } bool checkOTACapability(const esp_app_desc_t *app_desc, uint8_t method) { // Combined loader supports all (both) transports, BLE and WiFi if (strcmp(app_desc->project_name, combinedAppProjectName) == 0) { LOG_INFO("OTA partition contains combined BLE/WiFi OTA Loader"); return true; } if (method == METHOD_OTA_BLE && strcmp(app_desc->project_name, bleOnlyAppProjectName) == 0) { LOG_INFO("OTA partition contains BLE-only OTA Loader"); return true; } if (method == METHOD_OTA_WIFI && strcmp(app_desc->project_name, wifiOnlyAppProjectName) == 0) { LOG_INFO("OTA partition contains WiFi-only OTA Loader"); return true; } LOG_INFO("OTA partition does not contain a known OTA loader"); return false; } bool trySwitchToOTA() { const esp_partition_t *part = getAppPartition(); if (part == NULL) { LOG_WARN("Unable to get app partition in preparation of OTA reboot"); return false; } uint8_t result = esp_ota_set_boot_partition(part); // Partition and app checks should now be done in the AdminModule before this is called if (result != ESP_OK) { LOG_WARN("Unable to switch to OTA partiton. (Reason %d)", result); return false; } return true; } const char *getVersion() { const esp_partition_t *part = getAppPartition(); static esp_app_desc_t app_desc; if (!getAppDesc(part, &app_desc)) return ""; return app_desc.version; } } // namespace MeshtasticOTA ================================================ FILE: src/platform/esp32/MeshtasticOTA.h ================================================ #ifndef MESHTASTICOTA_H #define MESHTASTICOTA_H #include "mesh-pb-constants.h" #include #ifdef ESP_PLATFORM #include #endif #define METHOD_OTA_BLE 1 #define METHOD_OTA_WIFI 2 namespace MeshtasticOTA { void initialize(); bool isUpdated(); const esp_partition_t *getAppPartition(); bool getAppDesc(const esp_partition_t *part, esp_app_desc_t *app_desc); bool checkOTACapability(const esp_app_desc_t *app_desc, uint8_t method); void recoverConfig(meshtastic_Config_NetworkConfig *network); void saveConfig(meshtastic_Config_NetworkConfig *network, meshtastic_OTAMode method, uint8_t *ota_hash); bool trySwitchToOTA(); const char *getVersion(); } // namespace MeshtasticOTA #endif // MESHTASTICOTA_H ================================================ FILE: src/platform/esp32/SimpleAllocator.cpp ================================================ #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); } ================================================ FILE: src/platform/esp32/SimpleAllocator.h ================================================ #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(); }; ================================================ FILE: src/platform/esp32/architecture.h ================================================ #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_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 #ifndef HAS_32768HZ #define HAS_32768HZ 0 #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(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(ELECROW_ThinkNode_M2) #define HW_VENDOR meshtastic_HardwareModel_THINKNODE_M2 #elif defined(ELECROW_ThinkNode_M5) #define HW_VENDOR meshtastic_HardwareModel_THINKNODE_M5 #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(SENSECAP_INDICATOR) #define HW_VENDOR meshtastic_HardwareModel_SENSECAP_INDICATOR #elif defined(SEEED_XIAO_S3) #define HW_VENDOR meshtastic_HardwareModel_SEEED_XIAO_S3 #elif defined(MESH_TAB) #define HW_VENDOR meshtastic_HardwareModel_MESH_TAB #elif defined(T_ETH_ELITE) #define HW_VENDOR meshtastic_HardwareModel_T_ETH_ELITE #elif defined(HELTEC_SENSOR_HUB) #define HW_VENDOR meshtastic_HardwareModel_HELTEC_SENSOR_HUB #elif defined(ELECROW_PANEL) #define HW_VENDOR meshtastic_HardwareModel_CROWPANEL #elif defined(RAK3312) #define HW_VENDOR meshtastic_HardwareModel_RAK3312 #elif defined(RAK_WISMESH_TAP_V2) #define HW_VENDOR meshtastic_HardwareModel_WISMESH_TAP_V2 #elif defined(LINK_32) #define HW_VENDOR meshtastic_HardwareModel_LINK_32 #elif defined(T_DECK_PRO) #define HW_VENDOR meshtastic_HardwareModel_T_DECK_PRO #elif defined(T_BEAM_1W) #define HW_VENDOR meshtastic_HardwareModel_TBEAM_1_WATT #elif defined(T_LORA_PAGER) #define HW_VENDOR meshtastic_HardwareModel_T_LORA_PAGER #elif defined(HELTEC_V4) #define HW_VENDOR meshtastic_HardwareModel_HELTEC_V4 #elif defined(M5STACK_UNITC6L) #define HW_VENDOR meshtastic_HardwareModel_M5STACK_C6L #elif defined(HELTEC_WIRELESS_TRACKER_V2) #define HW_VENDOR meshtastic_HardwareModel_HELTEC_WIRELESS_TRACKER_V2 #elif defined(M5STACK_CARDPUTER_ADV) #define HW_VENDOR meshtastic_HardwareModel_M5STACK_CARDPUTER_ADV #else #define HW_VENDOR meshtastic_HardwareModel_PRIVATE_HW #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. // Setup flag, which indicates if our device supports power management #ifdef CONFIG_PM_ENABLE #define HAS_ESP32_PM_SUPPORT 1 #endif // Setup flag, which indicates if our device supports dynamic light sleep #if defined(HAS_ESP32_PM_SUPPORT) && defined(CONFIG_FREERTOS_USE_TICKLESS_IDLE) #define HAS_ESP32_DYNAMIC_LIGHT_SLEEP 1 #endif ================================================ FILE: src/platform/esp32/iram-quirk.c ================================================ // 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"))); ================================================ FILE: src/platform/esp32/main-esp32.cpp ================================================ #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 "nimble/NimbleBluetooth.h" #endif #include #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 // Weak empty variant shutdown prep function. // May be redefined by variant files. void variant_shutdown() __attribute__((weak)); void variant_shutdown() {} #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !MESHTASTIC_EXCLUDE_BLUETOOTH void setBluetoothEnable(bool enable) { #ifdef USE_WS5500 if ((config.bluetooth.enabled == true) && (config.network.wifi_enabled == false)) #elif 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) { #if defined(CONFIG_IDF_TARGET_ESP32C6) && defined(CONFIG_SOC_IEEE802154_SUPPORTED) auto res = esp_base_mac_addr_get(dmac); assert(res == ESP_OK); #else auto res = esp_efuse_mac_get_default(dmac); assert(res == ESP_OK); #endif } #if HAS_32768HZ #define CALIBRATE_ONE(cali_clk) calibrate_one(cali_clk, #cali_clk) static uint32_t calibrate_one(rtc_cal_sel_t cal_clk, const char *name) { const uint32_t cal_count = 1000; // const float factor = (1 << 19) * 1000.0f; unused var? uint32_t cali_val; for (int i = 0; i < 5; ++i) { cali_val = rtc_clk_cal(cal_clk, cal_count); } return cali_val; } 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("Switch RTC Source to 32.768kHz succeeded, using 32k XTAL"); CALIBRATE_ONE(RTC_CAL_RTC_MUX); CALIBRATE_ONE(RTC_CAL_32K_XTAL); } CALIBRATE_ONE(RTC_CAL_RTC_MUX); CALIBRATE_ONE(RTC_CAL_32K_XTAL); if (rtc_clk_slow_freq_get() != RTC_SLOW_FREQ_32K_XTAL) { LOG_WARN("Failed to switch 32K XTAL RTC source to 32.768kHz !!! "); return; } } #endif void esp32Setup() { /* We explicitly don't want to do call randomSeed, // as that triggers the esp32 core to use a less secure pseudorandom function. uint32_t seed = esp_random(); LOG_DEBUG("Set random seed %u", seed); randomSeed(seed); */ #ifdef ADC_V pinMode(ADC_V, INPUT); #endif LOG_DEBUG("Total heap: %d", ESP.getHeapSize()); LOG_DEBUG("Free heap: %d", ESP.getFreeHeap()); LOG_DEBUG("Total PSRAM: %d", ESP.getPsramSize()); LOG_DEBUG("Free PSRAM: %d", ESP.getFreePsram()); 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_WIFI String version = MeshtasticOTA::getVersion(); if (version.isEmpty()) { LOG_INFO("MeshtasticOTA firmware not available"); } else { LOG_INFO("MeshtasticOTA firmware version %s", version.c_str()); } MeshtasticOTA::initialize(); #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); #if 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 variant_shutdown(); // 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) } ================================================ FILE: src/platform/extra_variants/README.md ================================================ # 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 earlyInitVariant() and lateInitVariant() if your board needs them. earlyInitVariant() runs at the beginning of setup() directly after waitUntilPowerLevelSafe(); while lateInitVariant() runs after the LoRa radio is initialized. 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. ================================================ FILE: src/platform/extra_variants/heltec_wireless_tracker/variant.cpp ================================================ #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 ================================================ FILE: src/platform/extra_variants/t_deck_pro/variant.cpp ================================================ #include "configuration.h" #ifdef T_DECK_PRO #include "input/TouchScreenImpl1.h" #include #include CSE_CST328 tsPanel = CSE_CST328(EINK_WIDTH, EINK_HEIGHT, &Wire, CST328_PIN_RST, CST328_PIN_INT); bool readTouch(int16_t *x, int16_t *y) { if (tsPanel.getTouches()) { *x = tsPanel.getPoint(0).x; *y = tsPanel.getPoint(0).y; return true; } return false; } // T-Deck Pro specific init void lateInitVariant() { tsPanel.begin(); touchScreenImpl1 = new TouchScreenImpl1(EINK_WIDTH, EINK_HEIGHT, readTouch); touchScreenImpl1->init(); } #endif ================================================ FILE: src/platform/extra_variants/t_lora_pager/variant.cpp ================================================ #include "configuration.h" #ifdef T_LORA_PAGER #include "AudioBoard.h" DriverPins PinsAudioBoardES8311; AudioBoard board(AudioDriverES8311, PinsAudioBoardES8311); // TLora Pager specific init void lateInitVariant() { // AudioDriverLogger.begin(Serial, AudioDriverLogLevel::Debug); // I2C: function, scl, sda PinsAudioBoardES8311.addI2C(PinFunction::CODEC, Wire); // I2S: function, mclk, bck, ws, data_out, data_in PinsAudioBoardES8311.addI2S(PinFunction::CODEC, DAC_I2S_MCLK, DAC_I2S_BCK, DAC_I2S_WS, DAC_I2S_DOUT, DAC_I2S_DIN); // configure codec CodecConfig cfg; cfg.input_device = ADC_INPUT_LINE1; cfg.output_device = DAC_OUTPUT_ALL; cfg.i2s.bits = BIT_LENGTH_16BITS; cfg.i2s.rate = RATE_44K; board.begin(cfg); } #endif ================================================ FILE: src/platform/extra_variants/tbeam_displayshield/variant.cpp ================================================ #include "configuration.h" #ifdef HAS_CST226SE #include "TouchDrvCSTXXX.hpp" #include "input/TouchScreenImpl1.h" #include TouchDrvCSTXXX tsPanel; static constexpr uint8_t PossibleAddresses[2] = {CST328_ADDR, CST226SE_ADDR_ALT}; uint8_t i2cAddress = 0; bool readTouch(int16_t *x, int16_t *y) { int16_t x_array[1], y_array[1]; uint8_t touched = tsPanel.getPoint(x_array, y_array, 1); if (touched > 0) { *y = x_array[0]; *x = (TFT_WIDTH - y_array[0]); // Check bounds if (*x < 0 || *x >= TFT_WIDTH || *y < 0 || *y >= TFT_HEIGHT) { return false; } return true; // Valid touch detected } return false; // No valid touch data } void lateInitVariant() { tsPanel.setTouchDrvModel(TouchDrv_CST226); for (uint8_t addr : PossibleAddresses) { if (tsPanel.begin(Wire, addr, I2C_SDA, I2C_SCL)) { i2cAddress = addr; LOG_DEBUG("CST226SE init OK at address 0x%02X", addr); touchScreenImpl1 = new TouchScreenImpl1(TFT_WIDTH, TFT_HEIGHT, readTouch); touchScreenImpl1->init(); return; } } LOG_ERROR("CST226SE init failed at all known addresses"); } #endif ================================================ FILE: src/platform/nrf52/AsyncUDP.cpp ================================================ #include "AsyncUDP.h" #if HAS_ETHERNET AsyncUDP::AsyncUDP() : OSThread("AsyncUDP"), localPort(0) {} bool AsyncUDP::listenMulticast(IPAddress multicastIP, uint16_t port, uint8_t ttl) { if (!isMulticast(multicastIP)) return false; localPort = port; udp.beginMulticast(multicastIP, port); return true; } size_t AsyncUDP::write(uint8_t b) { return udp.write(&b, 1); } size_t AsyncUDP::write(const uint8_t *data, size_t len) { return udp.write(data, len); } void AsyncUDP::onPacket(const std::function &callback) { _onPacket = callback; } bool AsyncUDP::writeTo(const uint8_t *data, size_t len, IPAddress ip, uint16_t port) { if (!udp.beginPacket(ip, port)) return false; udp.write(data, len); isSending = true; bool ok = udp.endPacket(); isSending = false; return ok; } void AsyncUDP::close() { udp.stop(); localPort = 0; _onPacket = nullptr; } // AsyncUDPPacket AsyncUDPPacket::AsyncUDPPacket(EthernetUDP &source) : _udp(source), _remoteIP(source.remoteIP()), _remotePort(source.remotePort()) { if (_udp.available() > 0) { _readLength = _udp.read(_buffer, sizeof(_buffer)); } else { _readLength = 0; } } IPAddress AsyncUDPPacket::remoteIP() { return _remoteIP; } uint16_t AsyncUDPPacket::length() { return _readLength; } const uint8_t *AsyncUDPPacket::data() { return _buffer; } int32_t AsyncUDP::runOnce() { if (_onPacket && !isSending && udp.parsePacket() > 0) { AsyncUDPPacket packet(udp); _onPacket(packet); } return 5; // check every 5ms } #endif // HAS_ETHERNET ================================================ FILE: src/platform/nrf52/AsyncUDP.h ================================================ #ifndef ASYNC_UDP_H #define ASYNC_UDP_H #include "configuration.h" #if HAS_ETHERNET #include "concurrency/OSThread.h" #include #include #include #include #include class AsyncUDPPacket; class AsyncUDP : public Print, private concurrency::OSThread { public: AsyncUDP(); explicit operator bool() const { return localPort != 0; } bool listenMulticast(IPAddress multicastIP, uint16_t port, uint8_t ttl = 64); bool writeTo(const uint8_t *data, size_t len, IPAddress ip, uint16_t port); void close(); size_t write(uint8_t b) override; size_t write(const uint8_t *data, size_t len) override; void onPacket(const std::function &callback); private: EthernetUDP udp; uint16_t localPort; volatile bool isSending = false; std::function _onPacket; virtual int32_t runOnce() override; }; class AsyncUDPPacket { public: explicit AsyncUDPPacket(EthernetUDP &source); IPAddress remoteIP(); uint16_t length(); const uint8_t *data(); private: EthernetUDP &_udp; IPAddress _remoteIP; uint16_t _remotePort; size_t _readLength = 0; static constexpr size_t BUF_SIZE = 512; uint8_t _buffer[BUF_SIZE]; }; inline bool isMulticast(const IPAddress &ip) { return (ip[0] & 0xF0) == 0xE0; } #endif // HAS_ETHERNET #endif // ASYNC_UDP_H ================================================ FILE: src/platform/nrf52/BLEDfuSecure.cpp ================================================ /**************************************************************************/ /*! @file BLEDfuSecure.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; } ================================================ FILE: src/platform/nrf52/BLEDfuSecure.h ================================================ /**************************************************************************/ /*! @file BLEDfuSecure.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_ */ ================================================ FILE: src/platform/nrf52/NRF52Bluetooth.cpp ================================================ #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]; // Last ToRadio value received from the phone static uint8_t lastToRadio[MAX_TO_FROM_RADIO_SIZE]; static uint16_t connectionHandle; 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) 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 { return Bluefruit.connected(connectionHandle); } public: BluetoothPhoneAPI() { api_type = TYPE_BLE; } }; static BluetoothPhoneAPI *bluetoothPhoneAPI; void onConnect(uint16_t conn_handle) { // Get the reference to current connection BLEConnection *connection = Bluefruit.Connection(conn_handle); connectionHandle = conn_handle; char central_name[32] = {0}; connection->getPeerName(central_name, sizeof(central_name)); LOG_INFO("BLE Connected to %s", central_name); // Notify UI (or any other interested firmware components) meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED); bluetoothStatus->updateStatus(&newStatus); } /** * Callback invoked when a connection is dropped * @param conn_handle connection where this event happens * @param reason is a BLE_HCI_STATUS_CODE which can be found in ble_hci.h */ void onDisconnect(uint16_t conn_handle, uint8_t reason) { LOG_INFO("BLE Disconnected, reason = 0x%x", reason); if (bluetoothPhoneAPI) { bluetoothPhoneAPI->close(); } // Clear the last ToRadio packet buffer to avoid rejecting first packet from new connection memset(lastToRadio, 0, sizeof(lastToRadio)); // Notify UI (or any other interested firmware components) meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED); bluetoothStatus->updateStatus(&newStatus); #if HAS_SCREEN // If a pairing prompt is active, make sure we dismiss it on disconnect/cancel/failure paths. if (passkeyShowing) { passkeyShowing = false; if (screen) { screen->endAlert(); } } #endif } 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 = 417,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, 668); // 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("Ignore successor read"); } authorizeRead(conn_hdl); } void onToRadioWrite(uint16_t conn_hdl, BLECharacteristic *chr, uint8_t *data, uint16_t len) { LOG_INFO("toRadioWriteCb data %p, len %u", data, len); if (memcmp(lastToRadio, data, len) != 0) { LOG_DEBUG("New ToRadio packet"); memcpy(lastToRadio, data, len); bluetoothPhoneAPI->handleToRadio(data, len); } else { LOG_DEBUG("Drop dup ToRadio packet we just saw"); } } void 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"); Bluefruit.Security.setPairPasskeyCallback(NRF52Bluetooth::onUnwantedPairing); // Actively refuse (during factory reset) disconnect(); Bluefruit.Advertising.stop(); } void NRF52Bluetooth::startDisabled() { // Setup Bluetooth nrf52Bluetooth->setup(); // Shutdown bluetooth for minimum power draw Bluefruit.Advertising.stop(); Bluefruit.setTxPower(-40); // Minimum power LOG_INFO("Disable NRF52 Bluetooth. (Workaround: tx power min, advertise stopped)"); } bool NRF52Bluetooth::isConnected() { return Bluefruit.connected(connectionHandle); } int NRF52Bluetooth::getRssi() { return 0; // FIXME figure out where to source this } // Valid BLE TX power levels as per nRF52840 Product Specification are: "-20 to +8 dBm TX power, configurable in 4 dB steps". // See https://docs.nordicsemi.com/bundle/ps_nrf52840/page/keyfeatures_html5.html #define VALID_BLE_TX_POWER(x) \ ((x) == -20 || (x) == -16 || (x) == -12 || (x) == -8 || (x) == -4 || (x) == 0 || (x) == 4 || (x) == 8) void NRF52Bluetooth::setup() { // Initialise the Bluefruit module LOG_INFO("Init the Bluefruit nRF52 module"); Bluefruit.autoConnLed(false); Bluefruit.configPrphBandwidth(BANDWIDTH_MAX); Bluefruit.begin(); // Clear existing data. Bluefruit.Advertising.stop(); Bluefruit.Advertising.clearData(); Bluefruit.ScanResponse.clearData(); #if defined(NRF52_BLE_TX_POWER) && VALID_BLE_TX_POWER(NRF52_BLE_TX_POWER) Bluefruit.setTxPower(NRF52_BLE_TX_POWER); #endif 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); // Do not change Slave Latency to value other than 0 !!! // There is probably a bug in SoftDevice + certain Apple iOS versions being // brain damaged causing connectivity problems. // On one side it seems SoftDevice is using SlaveLatency value even // if connection parameter negotation failed and phone sees it as connectivity errors. // On the other hand Apple can randomly refuse any parameter negotiation and shutdown connection // even if you meet Apple Developer Guidelines for BLE devices. Because f* you, that's why. // While this API call sets preferred connection parameters (PPCP) - many phones ignore it (yeah) and it seems SoftDevice // will try to renegotiate connection parameters based on those values after phone connection. // So those are relatively safe values so Apple braindead firmware won't get angry and at least we may try // to negotiate some longer connection interval to save battery. // See https://github.com/meshtastic/firmware/pull/8858 for measurements. We are dealing with microamp savings anyway so not // worth dying on a hill here. Bluefruit.Periph.setConnSlaveLatency(0); // 1.25 ms units - so min, max is 15, 100 ms range. Bluefruit.Periph.setConnInterval(12, 80); #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("Init the Device Information Service"); bledis.setModel(optstr(HW_VERSION)); bledis.setFirmwareRev(optstr(APP_VERSION)); bledis.begin(); // Start the BLE Battery Service and set it to 100% LOG_INFO("Init the Battery Service"); blebas.begin(); blebas.write(0); // Unknown battery level for now // Setup the Heart Rate Monitor service using // BLEService and BLECharacteristic classes LOG_INFO("Init the Mesh bluetooth service"); setupMeshService(); // Setup the advertising packet(s) LOG_INFO("Set up the advertising payload(s)"); startAdv(); LOG_INFO("Advertise"); } void NRF52Bluetooth::resumeAdvertising() { Bluefruit.Advertising.restartOnDisconnect(true); Bluefruit.Advertising.setInterval(32, 668); // in unit of 0.625 ms Bluefruit.Advertising.setFastTimeout(30); // number of seconds in fast mode Bluefruit.Advertising.start(0); } /// Given a level between 0-100, update the BLE attribute void updateBatteryLevel(uint8_t level) { blebas.write(level); } void NRF52Bluetooth::clearBonds() { LOG_INFO("Clear bluetooth bonds!"); bond_print_list(BLE_GAP_ROLE_PERIPH); bond_print_list(BLE_GAP_ROLE_CENTRAL); Bluefruit.Periph.clearBonds(); Bluefruit.Central.clearBonds(); } void NRF52Bluetooth::onConnectionSecured(uint16_t conn_handle) { LOG_INFO("BLE connection secured"); } bool NRF52Bluetooth::onPairingPasskey(uint16_t conn_handle, uint8_t const passkey[6], bool match_request) { char passkey1[4] = {passkey[0], passkey[1], passkey[2], '\0'}; char passkey2[4] = {passkey[3], passkey[4], passkey[5], '\0'}; LOG_INFO("BLE pair process started with passkey %s %s", passkey1, passkey2); powerFSM.trigger(EVENT_BLUETOOTH_PAIR); // Get passkey as string // Note: possible leading zeros std::string textkey; for (uint8_t i = 0; i < 6; i++) textkey += (char)passkey[i]; // Notify UI (or other components) of pairing event and passkey meshtastic::BluetoothStatus newStatus(textkey); bluetoothStatus->updateStatus(&newStatus); #if HAS_SCREEN && \ !defined(MESHTASTIC_EXCLUDE_SCREEN) // Todo: migrate this display code back into Screen class, and observe bluetoothStatus if (screen) { screen->startAlert([](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -> void { char btPIN[16] = "888888"; snprintf(btPIN, sizeof(btPIN), "%06u", configuredPasskey); int x_offset = display->width() / 2; int y_offset = display->height() <= 80 ? 0 : 12; display->setTextAlignment(TEXT_ALIGN_CENTER); display->setFont(FONT_MEDIUM); display->drawString(x_offset + x, y_offset + y, "Bluetooth"); 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; if (match_request) { uint32_t start_time = millis(); while (millis() < start_time + 30000) { if (!Bluefruit.connected(conn_handle)) break; } } LOG_INFO("BLE passkey pair: match_request=%i", match_request); return true; } // Actively refuse new BLE pairings // After clearing bonds (at factory reset), clients seem initially able to attempt to re-pair, even with advertising disabled. // On NRF52Bluetooth::shutdown, we change the pairing callback to this method, to aggressively refuse any connection attempts. bool NRF52Bluetooth::onUnwantedPairing(uint16_t conn_handle, uint8_t const passkey[6], bool match_request) { NRF52Bluetooth::disconnect(); return false; } // Disconnect any BLE connections void NRF52Bluetooth::disconnect() { uint8_t connection_num = Bluefruit.connected(); if (connection_num) { // Close all connections. We're only expecting one. for (uint8_t i = 0; i < connection_num; i++) Bluefruit.disconnect(i); // Wait for disconnection while (Bluefruit.connected()) yield(); LOG_INFO("Ended BLE connection"); } } void NRF52Bluetooth::onPairingCompleted(uint16_t conn_handle, uint8_t auth_status) { if (auth_status == BLE_GAP_SEC_STATUS_SUCCESS) { LOG_INFO("BLE pair success"); meshtastic::BluetoothStatus newConnectedStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED); bluetoothStatus->updateStatus(&newConnectedStatus); } else { LOG_INFO("BLE pair failed"); // Notify UI (or any other interested firmware components) meshtastic::BluetoothStatus newDisconnectedStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED); bluetoothStatus->updateStatus(&newDisconnectedStatus); } // Todo: migrate this display code back into Screen class, and observe bluetoothStatus passkeyShowing = false; if (screen) { screen->endAlert(); } } void NRF52Bluetooth::sendLog(const uint8_t *logMessage, size_t length) { if (!isConnected() || length > 512) return; if (logRadio.indicateEnabled()) logRadio.indicate(logMessage, (uint16_t)length); else logRadio.notify(logMessage, (uint16_t)length); } ================================================ FILE: src/platform/nrf52/NRF52Bluetooth.h ================================================ #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); static bool onUnwantedPairing(uint16_t conn_handle, uint8_t const passkey[6], bool match_request); static void disconnect(); }; ================================================ FILE: src/platform/nrf52/NRF52CryptoEngine.cpp ================================================ #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(); ================================================ FILE: src/platform/nrf52/aes-256/tiny-aes.cpp ================================================ /* 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]); } } ================================================ FILE: src/platform/nrf52/aes-256/tiny-aes.h ================================================ #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_ ================================================ FILE: src/platform/nrf52/alloc.cpp ================================================ #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); } ================================================ FILE: src/platform/nrf52/architecture.h ================================================ #pragma once #define ARCH_NRF52 // // defaults for NRF52 architecture // /* * 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) * Some variants overwrite it. */ #ifndef AREF_VOLTAGE #define AREF_VOLTAGE 3.6 #endif #ifndef BATTERY_SENSE_RESOLUTION_BITS #define BATTERY_SENSE_RESOLUTION_BITS 10 #endif #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(WISMESH_TAP) #define HW_VENDOR meshtastic_HardwareModel_WISMESH_TAP #elif defined(WISMESH_TAG) #define HW_VENDOR meshtastic_HardwareModel_WISMESH_TAG #elif defined(GAT562_MESH_TRIAL_TRACKER) #define HW_VENDOR meshtastic_HardwareModel_GAT562_MESH_TRIAL_TRACKER #elif defined(NOMADSTAR_METEOR_PRO) #define HW_VENDOR meshtastic_HardwareModel_NOMADSTAR_METEOR_PRO #elif defined(R1_NEO) #define HW_VENDOR meshtastic_HardwareModel_MUZI_R1_NEO #elif defined(RAK3401) #define HW_VENDOR meshtastic_HardwareModel_RAK3401 // MAke sure all custom RAK4630 boards are defined before the generic RAK4630 #elif defined(RAK4630) #define HW_VENDOR meshtastic_HardwareModel_RAK4631 #elif defined(TTGO_T_ECHO) #define HW_VENDOR meshtastic_HardwareModel_T_ECHO #elif defined(T_ECHO_LITE) #define HW_VENDOR meshtastic_HardwareModel_T_ECHO_LITE #elif defined(TTGO_T_ECHO_PLUS) #define HW_VENDOR meshtastic_HardwareModel_T_ECHO_PLUS #elif defined(ELECROW_ThinkNode_M1) #define HW_VENDOR meshtastic_HardwareModel_THINKNODE_M1 #elif defined(ELECROW_ThinkNode_M3) #define HW_VENDOR meshtastic_HardwareModel_THINKNODE_M3 #elif defined(ELECROW_ThinkNode_M6) #define HW_VENDOR meshtastic_HardwareModel_THINKNODE_M6 #elif defined(ELECROW_ThinkNode_M4) #define HW_VENDOR meshtastic_HardwareModel_THINKNODE_M4 #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 #elif defined(MESHLINK) #define HW_VENDOR meshtastic_HardwareModel_MESHLINK #elif defined(SEEED_XIAO_NRF52840_KIT) #define HW_VENDOR meshtastic_HardwareModel_XIAO_NRF52_KIT #elif defined(SEEED_SOLAR_NODE) #define HW_VENDOR meshtastic_HardwareModel_SEEED_SOLAR_NODE #elif defined(HELTEC_MESH_POCKET) #define HW_VENDOR meshtastic_HardwareModel_HELTEC_MESH_POCKET #elif defined(SEEED_WIO_TRACKER_L1_EINK) #define HW_VENDOR meshtastic_HardwareModel_SEEED_WIO_TRACKER_L1_EINK #elif defined(SEEED_WIO_TRACKER_L1) #define HW_VENDOR meshtastic_HardwareModel_SEEED_WIO_TRACKER_L1 #elif defined(HELTEC_MESH_SOLAR) #define HW_VENDOR meshtastic_HardwareModel_HELTEC_MESH_SOLAR #elif defined(MUZI_BASE) #define HW_VENDOR meshtastic_HardwareModel_MUZI_BASE #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 #if defined(PIN_LED1) && !defined(LED_POWER) #define LED_POWER PIN_LED1 // LED1 on nrf52840-DK #endif #ifdef PIN_BUTTON1 #define BUTTON_PIN PIN_BUTTON1 #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 // Detect if running in ISR context (ARM Cortex-M4) #define xPortInIsrContext() ((SCB->ICSR & SCB_ICSR_VECTACTIVE_Msk) == 0 ? pdFALSE : pdTRUE) ================================================ FILE: src/platform/nrf52/hardfault.cpp ================================================ #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 ================================================ FILE: src/platform/nrf52/main-bare.cpp ================================================ #include "target_specific.h" ================================================ FILE: src/platform/nrf52/main-nrf52.cpp ================================================ #include "configuration.h" #include #include #include #include #include #define APP_WATCHDOG_SECS 90 #define NRFX_WDT_ENABLED 1 #define NRFX_WDT0_ENABLED 1 #define NRFX_WDT_CONFIG_NO_IRQ 1 #include "nrfx_power.h" #include #include #include #include #include #include // #include #include "NodeDB.h" #include "PowerMon.h" #include "error.h" #include "main.h" #include "meshUtils.h" #include "power.h" #include #include #ifdef BQ25703A_ADDR #include "BQ25713.h" #endif // WARNING! THRESHOLD + HYSTERESIS should be less than regulated VDD voltage - which depends on board // and is 3.0 or 3.3V. Also VDD likes to read values like 2.9999 so make sure you account for that // otherwise board will not boot at all. Before you modify this part - please triple read NRF52840 power design // section in datasheet and you understand how REG0 and REG1 regulators work together. #ifndef SAFE_VDD_VOLTAGE_THRESHOLD #define SAFE_VDD_VOLTAGE_THRESHOLD 2.7 #endif // hysteresis value #ifndef SAFE_VDD_VOLTAGE_THRESHOLD_HYST #define SAFE_VDD_VOLTAGE_THRESHOLD_HYST 0.2 #endif uint16_t getVDDVoltage(); // Weak empty variant shutdown prep function. // May be redefined by variant files. void variant_shutdown() __attribute__((weak)); void variant_shutdown() {} static nrfx_wdt_t nrfx_wdt = NRFX_WDT_INSTANCE(0); static nrfx_wdt_channel_id nrfx_wdt_channel_id_nrf52_main; // This is a public global so that the debugger can set it to false automatically from our gdbinit // @phaseloop comment: most part of codebase, including filesystem flash driver depend on softdevice // methods so disabling it may actually crash thing. Proceed with caution. bool useSoftDevice = true; // Set to false for easier debugging static inline void debugger_break(void) { __asm volatile("bkpt #0x01\n\t" "mov pc, lr\n\t"); } // PowerHAL NRF52 specific function implementations bool powerHAL_isVBUSConnected() { return NRF_POWER->USBREGSTATUS & POWER_USBREGSTATUS_VBUSDETECT_Msk; } bool powerHAL_isPowerLevelSafe() { static bool powerLevelSafe = true; uint16_t threshold = SAFE_VDD_VOLTAGE_THRESHOLD * 1000; // convert V to mV uint16_t hysteresis = SAFE_VDD_VOLTAGE_THRESHOLD_HYST * 1000; if (powerLevelSafe) { if (getVDDVoltage() < threshold) { powerLevelSafe = false; } } else { // power level is only safe again when it raises above threshold + hysteresis if (getVDDVoltage() >= (threshold + hysteresis)) { powerLevelSafe = true; } } return powerLevelSafe; } void powerHAL_platformInit() { // Enable POF power failure comparator. It will prevent writing to NVMC flash when supply voltage is too low. // Set to some low value as last resort - powerHAL_isPowerLevelSafe uses different method and should manage proper node // behaviour on its own. // POFWARN is pretty useless for node power management because it triggers only once and clearing this event will not // re-trigger it again until voltage rises to safe level and drops again. So we will use SAADC routed to VDD to read safely // voltage. // @phaseloop: I disable POFCON for now because it seems to be unreliable or buggy. Even when set at 2.0V it // triggers below 2.8V and corrupts data when pairing bluetooth - because it prevents filesystem writes and // adafruit BLE library triggers lfs_assert which reboots node and formats filesystem. // I did experiments with bench power supply and no matter what is set to POFCON, it always triggers right below // 2.8V. I compared raw registry values with datasheet. NRF_POWER->POFCON = ((POWER_POFCON_THRESHOLD_V22 << POWER_POFCON_THRESHOLD_Pos) | (POWER_POFCON_POF_Enabled << POWER_POFCON_POF_Pos)); // remember to always match VBAT_AR_INTERNAL with AREF_VALUE in variant definition file #ifdef VBAT_AR_INTERNAL analogReference(VBAT_AR_INTERNAL); #else analogReference(AR_INTERNAL); // 3.6V #endif } // get VDD voltage (in millivolts) uint16_t getVDDVoltage() { // we use the same values as regular battery read so there is no conflict on SAADC analogReadResolution(BATTERY_SENSE_RESOLUTION_BITS); // VDD range on NRF52840 is 1.8-3.3V so we need to remap analog reference to 3.6V // let's hope battery reading runs in same task and we don't have race condition analogReference(AR_INTERNAL); uint16_t vddADCRead = analogReadVDD(); float voltage = ((1000 * 3.6) / pow(2, BATTERY_SENSE_RESOLUTION_BITS)) * vddADCRead; // restore default battery reading reference #ifdef VBAT_AR_INTERNAL analogReference(VBAT_AR_INTERNAL); #endif return voltage; } 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 } #if !MESHTASTIC_EXCLUDE_BLUETOOTH void setBluetoothEnable(bool enable) { // For debugging use: don't use bluetooth if (!useSoftDevice) { if (enable) LOG_INFO("Disable NRF52 BLUETOOTH WHILE DEBUGGING"); return; } // If user disabled bluetooth: init then disable advertising & reduce power // Workaround. Avoid issue where device hangs several days after boot.. // Allegedly, no significant increase in power consumption if (!config.bluetooth.enabled) { static bool initialized = false; if (!initialized) { nrf52Bluetooth = new NRF52Bluetooth(); nrf52Bluetooth->startDisabled(); initialized = true; } return; } if (enable) { powerMon->setState(meshtastic_PowerMon_State_BT_On); // If not yet set-up if (!nrf52Bluetooth) { LOG_DEBUG("Init NRF52 Bluetooth"); nrf52Bluetooth = new NRF52Bluetooth(); nrf52Bluetooth->setup(); } // 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; } namespace { constexpr uint8_t NRF52_MAGIC_LFS_IS_CORRUPT = 0xF5; constexpr uint32_t MULTIPLE_CORRUPTION_DELAY_MILLIS = 20 * 60 * 1000; static unsigned long millis_until_formatting_again = 0; // Report the critical error from loop(), giving a chance for the screen to be initialized first. inline void reportLittleFSCorruptionOnce() { static bool report_corruption = !!millis_until_formatting_again; if (report_corruption) { report_corruption = false; RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_FLASH_CORRUPTION_UNRECOVERABLE); } } } // namespace void preFSBegin() { // The GPREGRET register keeps its value across warm boots. Check that this is a warm boot and, if GPREGRET // is set to NRF52_MAGIC_LFS_IS_CORRUPT, format LittleFS. if (!(NRF_POWER->RESETREAS == 0 && NRF_POWER->GPREGRET == NRF52_MAGIC_LFS_IS_CORRUPT)) return; NRF_POWER->GPREGRET = 0; millis_until_formatting_again = millis() + MULTIPLE_CORRUPTION_DELAY_MILLIS; InternalFS.format(); LOG_INFO("LittleFS format complete; restoring default settings"); } extern "C" void lfs_assert(const char *reason) { LOG_ERROR("LittleFS corruption detected: %s", reason); if (millis_until_formatting_again > millis()) { RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_FLASH_CORRUPTION_UNRECOVERABLE); const long millis_remain = millis_until_formatting_again - millis(); LOG_WARN("Pausing %d seconds to avoid wear on flash storage", millis_remain / 1000); delay(millis_remain); } LOG_INFO("Rebooting to format LittleFS"); delay(500); // Give the serial port a bit of time to output that last message. // Try setting GPREGRET with the SoftDevice first. If that fails (perhaps because the SD hasn't been initialize yet) then set // NRF_POWER->GPREGRET directly. // TODO: this will/can crash CPU if bluetooth stack is not compiled in or bluetooth is not initialized // (regardless if enabled or disabled) - as there is no live SoftDevice stack // implement "safe" functions detecting softdevice stack state and using proper method to set registers // do not set GPREGRET if POFWARN is triggered because it means lfs_assert reports flash undervoltage protection // and not data corruption. Reboot is fine as boot procedure will wait until power level is safe again if (!NRF_POWER->EVENTS_POFWARN) { if (!(sd_power_gpregret_clr(0, 0xFF) == NRF_SUCCESS && sd_power_gpregret_set(0, NRF52_MAGIC_LFS_IS_CORRUPT) == NRF_SUCCESS)) { NRF_POWER->GPREGRET = NRF52_MAGIC_LFS_IS_CORRUPT; } } // TODO: this should not be done when SoftDevice is enabled as device will not boot back on soft reset // as some data is retained in RAM which will prevent re-enabling bluetooth stack // Google what Nordic has to say about NVIC_* + SoftDevice NVIC_SystemReset(); } void checkSDEvents() { if (useSoftDevice) { uint32_t evt; while (NRF_SUCCESS == sd_evt_get(&evt)) { switch (evt) { case NRF_EVT_POWER_FAILURE_WARNING: RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_BROWNOUT); break; default: LOG_DEBUG("Unexpected SDevt %d", evt); break; } } } else { if (NRF_POWER->EVENTS_POFWARN) RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_BROWNOUT); } } void nrf52Loop() { { static bool watchdog_running = false; if (!watchdog_running) { nrfx_wdt_enable(&nrfx_wdt); watchdog_running = true; } } nrfx_wdt_channel_feed(&nrfx_wdt, nrfx_wdt_channel_id_nrf52_main); checkSDEvents(); reportLittleFSCorruptionOnce(); } #ifdef USE_SEMIHOSTING #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() { #ifdef ADC_V pinMode(ADC_V, INPUT); #endif uint32_t why = NRF_POWER->RESETREAS; // per // https://infocenter.nordicsemi.com/index.jsp?topic=%2Fcom.nordic.infocenter.nrf52832.ps.v1.1%2Fpower.html LOG_DEBUG("Reset reason: 0x%x", why); #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("Set random seed %u", seed.seed32); randomSeed(seed.seed32); nRFCrypto.end(); // Set up nrfx watchdog. Do not enable the watchdog yet (we do that // the first time through the main loop), so that other threads can // allocate their own wdt channel to protect themselves from hangs. nrfx_wdt_config_t wdt0_config = { .behaviour = NRF_WDT_BEHAVIOUR_PAUSE_SLEEP_HALT, .reload_value = APP_WATCHDOG_SECS * 1000, // Note: Not using wdt interrupts. // .interrupt_priority = NRFX_WDT_DEFAULT_CONFIG_IRQ_PRIORITY }; nrfx_err_t r = nrfx_wdt_init(&nrfx_wdt, &wdt0_config, nullptr // Watchdog event handler, not used, we just reset. ); assert(r == NRFX_SUCCESS); r = nrfx_wdt_channel_alloc(&nrfx_wdt, &nrfx_wdt_channel_id_nrf52_main); assert(r == NRFX_SUCCESS); } void cpuDeepSleep(uint32_t msecToWake) { // FIXME, configure RTC or button press to wake us // FIXME, power down SPI, I2C, RAMs #if HAS_WIRE Wire.end(); #endif SPI.end(); #if SPI_INTERFACES_COUNT > 1 SPI1.end(); #endif if (Serial) // Another check in case of disabled default serial, does nothing bad Serial.end(); // This may cause crashes as debug messages continue to flow. // This causes troubles with waking up on nrf52 (on pro-micro in particular): // we have no Serial1 in use on nrf52, check Serial and GPS modules. #ifdef PIN_SERIAL1_RX if (Serial1) // A straightforward solution to the wake from deepsleep problem Serial1.end(); #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 #endif // Run shutdown code if specified in variant.cpp variant_shutdown(); // Sleepy trackers or sensors can low power "sleep" // Don't enter this if we're sleeping portMAX_DELAY, since that's a shutdown event if (msecToWake != portMAX_DELAY && (IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_TRACKER, meshtastic_Config_DeviceConfig_Role_TAK_TRACKER, meshtastic_Config_DeviceConfig_Role_SENSOR) && config.power.is_power_saving == true)) { sd_power_mode_set(NRF_POWER_MODE_LOWPWR); delay(msecToWake); NVIC_SystemReset(); } else { // Resume on user button press // https://github.com/lyusupov/SoftRF/blob/81c519ca75693b696752235d559e881f2e0511ee/software/firmware/source/SoftRF/src/platform/nRF52.cpp#L1738 constexpr uint32_t DFU_MAGIC_SKIP = 0x6d; sd_power_gpregret_clr(0, 0xFF); // Clear the register before setting a new values in it for stability reasons sd_power_gpregret_set(0, DFU_MAGIC_SKIP); // Equivalent NRF_POWER->GPREGRET = DFU_MAGIC_SKIP // FIXME, use system off mode with ram retention for key state? // FIXME, use non-init RAM per // https://devzone.nordicsemi.com/f/nordic-q-a/48919/ram-retention-settings-with-softdevice-enabled #ifdef BATTERY_LPCOMP_INPUT // Wake up if power rises again nrf_lpcomp_config_t c; c.reference = BATTERY_LPCOMP_THRESHOLD; c.detection = NRF_LPCOMP_DETECT_UP; c.hyst = NRF_LPCOMP_HYST_NOHYST; nrf_lpcomp_configure(NRF_LPCOMP, &c); nrf_lpcomp_input_select(NRF_LPCOMP, BATTERY_LPCOMP_INPUT); nrf_lpcomp_enable(NRF_LPCOMP); battery_adcEnable(); nrf_lpcomp_task_trigger(NRF_LPCOMP, NRF_LPCOMP_TASK_START); while (!nrf_lpcomp_event_check(NRF_LPCOMP, NRF_LPCOMP_EVENT_READY)) ; #endif auto ok = sd_power_system_off(); if (ok != NRF_SUCCESS) { LOG_ERROR("FIXME: Ignoring soft device (EasyDMA pending?) and forcing system-off!"); NRF_POWER->SYSTEMOFF = 1; } } // 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 } ================================================ FILE: src/platform/nrf52/nrf52840_s140_v7.ld ================================================ /* 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" ================================================ FILE: src/platform/nrf52/softdevice/ble.h ================================================ /* * 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__ */ /** @} @} */ ================================================ FILE: src/platform/nrf52/softdevice/ble_err.h ================================================ /* * 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 /** @} @} */ ================================================ FILE: src/platform/nrf52/softdevice/ble_gap.h ================================================ /* * 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__ /** @} */ ================================================ FILE: src/platform/nrf52/softdevice/ble_gatt.h ================================================ /* * 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__ /** @} */ ================================================ FILE: src/platform/nrf52/softdevice/ble_gattc.h ================================================ /* * 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__ */ /** @} */ ================================================ FILE: src/platform/nrf52/softdevice/ble_gatts.h ================================================ /* * 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__ /** @} */ ================================================ FILE: src/platform/nrf52/softdevice/ble_hci.h ================================================ /* * 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__ /** @} */ ================================================ FILE: src/platform/nrf52/softdevice/ble_l2cap.h ================================================ /* * 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__ /** @} */ ================================================ FILE: src/platform/nrf52/softdevice/ble_ranges.h ================================================ /* * 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__ */ /** @} @} */ ================================================ FILE: src/platform/nrf52/softdevice/ble_types.h ================================================ /* * 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__ */ /** @} @} */ ================================================ FILE: src/platform/nrf52/softdevice/nrf52/nrf_mbr.h ================================================ /* * 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__ /** @} */ ================================================ FILE: src/platform/nrf52/softdevice/nrf_error.h ================================================ /* * 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__ /** @} */ ================================================ FILE: src/platform/nrf52/softdevice/nrf_error_sdm.h ================================================ /* * 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__ /** @} @} */ ================================================ FILE: src/platform/nrf52/softdevice/nrf_error_soc.h ================================================ /* * 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__ /** @} @} */ ================================================ FILE: src/platform/nrf52/softdevice/nrf_nvic.h ================================================ /* * 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__ /**@} */ ================================================ FILE: src/platform/nrf52/softdevice/nrf_sdm.h ================================================ /* * 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__ /** @} */ ================================================ FILE: src/platform/nrf52/softdevice/nrf_soc.h ================================================ /* * 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__ /**@} */ ================================================ FILE: src/platform/nrf52/softdevice/nrf_svc.h ================================================ /* * 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__ ================================================ FILE: src/platform/portduino/PortduinoGlue.cpp ================================================ #include "CryptoEngine.h" #include "PortduinoGPIO.h" #include "SPIChip.h" #include "mesh/RF95Interface.h" #include "sleep.h" #include "target_specific.h" #include "PortduinoGlue.h" #include "SHA256.h" #include "api/ServerAPI.h" #include "linux/gpio/LinuxGPIOPin.h" #include "meshUtils.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef PORTDUINO_LINUX_HARDWARE #include #endif #include "platform/portduino/USBHal.h" portduino_config_struct portduino_config; portduino_status_struct portduino_status; std::ofstream traceFile; std::ofstream JSONFile; Ch341Hal *ch341Hal = nullptr; char *configPath = nullptr; char *optionMac = nullptr; bool verboseEnabled = false; bool yamlOnly = false; const char *argp_program_version = optstr(APP_VERSION); char stdoutBuffer[512]; // FIXME - move setBluetoothEnable into a HALPlatform class void setBluetoothEnable(bool enable) { // not needed } void cpuDeepSleep(uint32_t msecs) { notImplemented("cpuDeepSleep"); } void updateBatteryLevel(uint8_t level) NOT_IMPLEMENTED("updateBatteryLevel"); int TCPPort = SERVER_API_DEFAULT_PORT; bool checkConfigPort = true; 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 { checkConfigPort = false; printf("Using config file %d\n", TCPPort); } break; case 'c': configPath = arg; break; case 's': portduino_config.force_simradio = true; break; case 'h': optionMac = arg; break; case 'v': verboseEnabled = true; break; case 'y': yamlOnly = true; break; case ARGP_KEY_ARG: return 0; default: return ARGP_ERR_UNKNOWN; } return 0; } void portduinoCustomInit() { static struct argp_option options[] = {{"port", 'p', "PORT", 0, "The TCP port to use."}, {"config", 'c', "CONFIG_PATH", 0, "Full path of the .yaml config file to use."}, {"hwid", 'h', "HWID", 0, "The mac address to assign to this virtual machine"}, {"sim", 's', 0, 0, "Run in Simulated radio mode"}, {"verbose", 'v', 0, 0, "Set log level to full debug"}, {"output-yaml", 'y', 0, 0, "Output config yaml and exit"}, {0}}; static void *childArguments; static char doc[] = "Meshtastic native build."; static char args_doc[] = "..."; static struct argp argp = {options, parse_opt, args_doc, doc, 0, 0, 0}; const struct argp_child child = {&argp, OPTION_ARG_OPTIONAL, 0, 0}; portduinoAddArguments(child, childArguments); } void getMacAddr(uint8_t *dmac) { // We should store this value, and short-circuit all this if it's already been set. if (optionMac != nullptr && strlen(optionMac) > 0) { if (strlen(optionMac) >= 12) { MAC_from_string(optionMac, dmac); } else { uint32_t hwId = {0}; sscanf(optionMac, "%u", &hwId); dmac[0] = 0x80; dmac[1] = 0; dmac[2] = hwId >> 24; dmac[3] = hwId >> 16; dmac[4] = hwId >> 8; dmac[5] = hwId & 0xff; } } else if (portduino_config.mac_address.length() > 11) { MAC_from_string(portduino_config.mac_address, dmac); exit; } else { struct hci_dev_info di = {0}; di.dev_id = 0; bdaddr_t bdaddr; int btsock; btsock = socket(AF_BLUETOOTH, SOCK_RAW, 1); if (btsock < 0) { // If anything fails, just return with the default value return; } if (ioctl(btsock, HCIGETDEVINFO, (void *)&di)) { return; } dmac[0] = di.bdaddr.b[5]; dmac[1] = di.bdaddr.b[4]; dmac[2] = di.bdaddr.b[3]; dmac[3] = di.bdaddr.b[2]; dmac[4] = di.bdaddr.b[1]; dmac[5] = di.bdaddr.b[0]; } } std::string cleanupNameForAutoconf(std::string name) { // Convert spaces -> dashes, lowercase std::transform(name.begin(), name.end(), name.begin(), [](unsigned char c) { if (c == ' ') { return '-'; } return (char)std::tolower(c); }); return name; } /** apps run under portduino can optionally define a portduinoSetup() to * use portduino specific init code (such as gpioBind) to setup portduino on their host machine, * before running 'arduino' code. */ void portduinoSetup() { int max_GPIO = 0; std::string gpioChipName = "gpiochip"; portduino_config.displayPanel = no_screen; // Force stdout to be line buffered setvbuf(stdout, stdoutBuffer, _IOLBF, sizeof(stdoutBuffer)); if (portduino_config.force_simradio == true) { portduino_config.lora_module = use_simradio; } else if (configPath != nullptr) { if (loadConfig(configPath)) { if (!yamlOnly) std::cout << "Using " << configPath << " as config file" << std::endl; } else { std::cout << "Unable to use " << configPath << " as config file" << std::endl; exit(EXIT_FAILURE); } } else if (access("config.yaml", R_OK) == 0) { if (loadConfig("config.yaml")) { if (!yamlOnly) std::cout << "Using local config.yaml as config file" << std::endl; } else { std::cout << "Unable to use local config.yaml as config file" << std::endl; exit(EXIT_FAILURE); } } else if (access("/etc/meshtasticd/config.yaml", R_OK) == 0) { if (loadConfig("/etc/meshtasticd/config.yaml")) { if (!yamlOnly) std::cout << "Using /etc/meshtasticd/config.yaml as config file" << std::endl; } else { std::cout << "Unable to use /etc/meshtasticd/config.yaml as config file" << std::endl; exit(EXIT_FAILURE); } } else { if (!yamlOnly) std::cout << "No 'config.yaml' found..." << std::endl; portduino_config.lora_module = use_simradio; } if (portduino_config.config_directory != "") { std::string filetype = ".yaml"; for (const std::filesystem::directory_entry &entry : std::filesystem::directory_iterator{portduino_config.config_directory}) { if (ends_with(entry.path().string(), ".yaml")) { std::cout << "Also using " << entry << " as additional config file" << std::endl; loadConfig(entry.path().c_str()); } } } if (yamlOnly) { std::cout << portduino_config.emit_yaml() << std::endl; exit(EXIT_SUCCESS); } if (portduino_config.force_simradio) { std::cout << "Running in simulated mode." << std::endl; portduino_config.MaxNodes = 200; // Default to 200 nodes // Set the random seed equal to TCPPort to have a different seed per instance randomSeed(TCPPort); return; } // If LoRa `Module: auto` (default in config.yaml), // attempt to auto config based on Product Strings if (portduino_config.lora_module == use_autoconf) { bool found_hat = false; bool found_rak_eeprom = false; bool found_ch341 = false; char hat_vendor[96] = {0}; char autoconf_product[96] = {0}; // Try CH341 try { std::cout << "autoconf: Looking for CH341 device..." << std::endl; ch341Hal = new Ch341Hal(0, portduino_config.lora_usb_serial_num, portduino_config.lora_usb_vid, portduino_config.lora_usb_pid); ch341Hal->getProductString(autoconf_product, 95); delete ch341Hal; std::cout << "autoconf: Found CH341 device " << autoconf_product << std::endl; found_ch341 = true; } catch (...) { std::cout << "autoconf: Could not locate CH341 device" << std::endl; } // Try Pi HAT+ if (strlen(autoconf_product) < 6) { std::cout << "autoconf: Looking for Pi HAT+..." << std::endl; if (access("/proc/device-tree/hat/vendor", R_OK) == 0) { std::ifstream hatVendorFile("/proc/device-tree/hat/vendor"); if (hatVendorFile.is_open()) { hatVendorFile.read(hat_vendor, 95); hatVendorFile.close(); } } if (access("/proc/device-tree/hat/product", R_OK) == 0) { std::ifstream hatProductFile("/proc/device-tree/hat/product"); if (hatProductFile.is_open()) { hatProductFile.read(autoconf_product, 95); hatProductFile.close(); } std::cout << "autoconf: Found Pi HAT+ " << hat_vendor << " " << autoconf_product << " at /proc/device-tree/hat" << std::endl; // check for custom data fields int i = 0; while (access(("/proc/device-tree/hat/custom_" + std::to_string(i)).c_str(), R_OK) == 0) { std::ifstream customFieldFile(("/proc/device-tree/hat/custom_" + std::to_string(i)).c_str()); if (customFieldFile.is_open()) { std::string customFieldName; std::string customFieldValue; getline(customFieldFile, customFieldName, ' '); getline(customFieldFile, customFieldValue, ' '); customFieldFile.close(); printf("autoconf: Found hat+ custom field %s: %s\n", customFieldName.c_str(), customFieldValue.c_str()); portduino_config.hat_plus_custom_fields[customFieldName] = customFieldValue; } i++; } // potential TODO: Validate that this is a real UUID std::ifstream hatUUID("/proc/device-tree/hat/uuid"); char uuid[38] = {0}; if (hatUUID.is_open()) { hatUUID.read(uuid, 37); hatUUID.close(); std::cout << "autoconf: UUID " << uuid << std::endl; SHA256 uuid_hash; uint8_t uuid_hash_bytes[32] = {0}; uuid_hash.reset(); uuid_hash.update(uuid, 37); uuid_hash.finalize(uuid_hash_bytes, 32); for (int j = 0; j < 16; j++) { portduino_config.device_id[j] = uuid_hash_bytes[j]; } portduino_config.has_device_id = true; uint8_t dmac[6] = {0}; dmac[0] = (uuid_hash_bytes[17] << 4) | 2; dmac[1] = uuid_hash_bytes[18]; dmac[2] = uuid_hash_bytes[19]; dmac[3] = uuid_hash_bytes[20]; dmac[4] = uuid_hash_bytes[21]; dmac[5] = uuid_hash_bytes[22]; char macBuf[13] = {0}; snprintf(macBuf, sizeof(macBuf), "%02X%02X%02X%02X%02X%02X", dmac[0], dmac[1], dmac[2], dmac[3], dmac[4], dmac[5]); portduino_config.mac_address = macBuf; found_hat = true; } } else { std::cout << "autoconf: Could not locate Pi HAT+ at /proc/device-tree/hat" << std::endl; } } // attempt to load autoconf data from an EEPROM on 0x50 // RAK6421-13300-S1:aabbcc123456:5ba85807d92138b7519cfb60460573af:3061e8d8 // :mac address :<16 random unique bytes in hexidecimal> : crc32 // crc32 is calculated on the eeprom string up to but not including the final colon if (strlen(autoconf_product) < 6 && portduino_config.i2cdev != "") { try { char *mac_start = nullptr; char *devID_start = nullptr; char *crc32_start = nullptr; Wire.begin(); Wire.beginTransmission(0x50); Wire.write(0x0); Wire.write(0x0); Wire.endTransmission(); Wire.requestFrom((uint8_t)0x50, (uint8_t)75); uint8_t i = 0; delay(100); std::string autoconf_raw; while (Wire.available() && i < sizeof(autoconf_product)) { autoconf_product[i] = Wire.read(); if (autoconf_product[i] == 0xff) { autoconf_product[i] = 0x0; break; } autoconf_raw += autoconf_product[i]; if (autoconf_product[i] == ':') { autoconf_product[i] = 0x0; if (mac_start == nullptr) { mac_start = autoconf_product + i + 1; } else if (devID_start == nullptr) { devID_start = autoconf_product + i + 1; } else if (crc32_start == nullptr) { crc32_start = autoconf_product + i + 1; } } i++; } if (crc32_start != nullptr && strlen(crc32_start) == 8) { std::string crc32_str(crc32_start); uint32_t crc32_value = 0; // convert crc32 ascii to raw uint32 for (int j = 0; j < 4; j++) { crc32_value += std::stoi(crc32_str.substr(j * 2, 2), nullptr, 16) << (3 - j) * 8; } std::cout << "autoconf: Found eeprom crc " << crc32_start << std::endl; // set the autoconf string to blank and short circuit if (crc32_value != crc32Buffer(autoconf_raw.c_str(), i - 9)) { std::cout << "autoconf: crc32 mismatch, dropping " << std::endl; autoconf_product[0] = 0x0; } else { std::cout << "autoconf: Found eeprom data " << autoconf_raw << std::endl; found_rak_eeprom = true; if (mac_start != nullptr) { std::cout << "autoconf: Found mac data " << mac_start << std::endl; if (strlen(mac_start) == 12) portduino_config.mac_address = std::string(mac_start); } if (devID_start != nullptr) { std::cout << "autoconf: Found deviceid data " << devID_start << std::endl; if (strlen(devID_start) == 32) { std::string devID_str(devID_start); for (int j = 0; j < 16; j++) { portduino_config.device_id[j] = std::stoi(devID_str.substr(j * 2, 2), nullptr, 16); } portduino_config.has_device_id = true; } } } } else { std::cout << "autoconf: crc32 missing " << std::endl; autoconf_product[0] = 0x0; } } catch (...) { std::cout << "autoconf: Could not locate EEPROM" << std::endl; } } // Load the config file based on the product string if (strlen(autoconf_product) > 0) { // From configProducts map in PortduinoGlue.h std::string product_config = ""; if (configProducts.find(autoconf_product) != configProducts.end()) { product_config = configProducts.at(autoconf_product); } else { if (found_hat) { product_config = cleanupNameForAutoconf("lora-hat-" + std::string(hat_vendor) + "-" + autoconf_product + ".yaml"); if (strncmp(hat_vendor, "RAK", strlen("RAK")) == 0 && strncmp(autoconf_product, "6421 Pi Hat", strlen("6421 Pi Hat")) == 0) { std::cout << "autoconf: Setting hardwareModel to RAK6421" << std::endl; portduino_status.hardwareModel = meshtastic_HardwareModel_RAK6421; } } else if (found_ch341) { product_config = cleanupNameForAutoconf("lora-usb-" + std::string(autoconf_product) + ".yaml"); // look for more data after the null terminator size_t len = strlen(autoconf_product); if (len < 74) { memcpy(portduino_config.device_id, autoconf_product + len + 1, 16); if (!memfll(portduino_config.device_id, '\0', 16) && !memfll(portduino_config.device_id, 0xff, 16)) { portduino_config.has_device_id = true; if (strncmp(autoconf_product, "MESHSTICK 1262", strlen("MESHSTICK 1262")) == 0) { std::cout << "autoconf: Setting hardwareModel to Meshstick 1262" << std::endl; portduino_status.hardwareModel = meshtastic_HardwareModel_MESHSTICK_1262; } } } } // Don't try to automatically find config for a device with RAK eeprom. if (found_rak_eeprom) { std::cerr << "autoconf: Found unknown RAK product " << autoconf_product << std::endl; exit(EXIT_FAILURE); } if (access((portduino_config.available_directory + product_config).c_str(), R_OK) != 0) { std::cerr << "autoconf: Unable to find config for " << autoconf_product << "(tried " << product_config << ")" << std::endl; exit(EXIT_FAILURE); } } if (loadConfig((portduino_config.available_directory + product_config).c_str())) { std::cout << "autoconf: Using " << product_config << " as config file for " << autoconf_product << std::endl; } else { std::cerr << "autoconf: Unable to use " << product_config << " as config file for " << autoconf_product << std::endl; exit(EXIT_FAILURE); } } else { std::cerr << "autoconf: Could not locate any devices" << std::endl; exit(EXIT_FAILURE); } } // if we're using a usermode driver, we need to initialize it here, to get a serial number back for mac address uint8_t dmac[6] = {0}; if (portduino_config.lora_spi_dev == "ch341") { try { ch341Hal = new Ch341Hal(0, portduino_config.lora_usb_serial_num, portduino_config.lora_usb_vid, portduino_config.lora_usb_pid); } catch (std::exception &e) { std::cerr << e.what() << std::endl; std::cerr << "Could not initialize CH341 device!" << std::endl; exit(EXIT_FAILURE); } char serial[9] = {0}; ch341Hal->getSerialString(serial, 8); std::cout << "CH341 Serial " << serial << std::endl; char product_string[96] = {0}; ch341Hal->getProductString(product_string, 95); std::cout << "CH341 Product " << product_string << std::endl; if (strlen(serial) == 8 && portduino_config.mac_address.length() < 12) { std::cout << "Deriving MAC address from Serial and Product String" << std::endl; uint8_t hash[104] = {0}; memcpy(hash, serial, 8); memcpy(hash + 8, product_string, strlen(product_string)); crypto->hash(hash, 8 + strlen(product_string)); dmac[0] = (hash[0] << 4) | 2; dmac[1] = hash[1]; dmac[2] = hash[2]; dmac[3] = hash[3]; dmac[4] = hash[4]; dmac[5] = hash[5]; char macBuf[13] = {0}; sprintf(macBuf, "%02X%02X%02X%02X%02X%02X", dmac[0], dmac[1], dmac[2], dmac[3], dmac[4], dmac[5]); portduino_config.mac_address = macBuf; } } getMacAddr(dmac); #ifndef UNIT_TEST if (dmac[0] == 0 && dmac[1] == 0 && dmac[2] == 0 && dmac[3] == 0 && dmac[4] == 0 && dmac[5] == 0) { std::cout << "*** Blank MAC Address not allowed!" << std::endl; std::cout << "Please set a MAC Address in config.yaml using either MACAddress or MACAddressSource." << std::endl; exit(EXIT_FAILURE); } #endif printf("MAC ADDRESS: %02X:%02X:%02X:%02X:%02X:%02X\n", dmac[0], dmac[1], dmac[2], dmac[3], dmac[4], dmac[5]); // Rather important to set this, if not running simulated. randomSeed(time(NULL)); std::string defaultGpioChipName = gpioChipName + std::to_string(portduino_config.lora_default_gpiochip); std::set used_pins; for (const auto *i : portduino_config.all_pins) { if (i->enabled && i->pin > max_GPIO) { max_GPIO = i->pin; } } for (auto i : portduino_config.extra_pins) { if (i.enabled && i.pin > max_GPIO) { max_GPIO = i.pin; } } gpioInit(max_GPIO + 1); // Done here so we can inform Portduino how many GPIOs we need. // Need to bind all the configured GPIO pins so they're not simulated // TODO: If one of these fails, we should log and terminate for (const auto *i : portduino_config.all_pins) { // In the case of a ch341 Lora device, we don't want to touch the system GPIO lines for Lora // Those GPIO are handled in our usermode driver instead. if (i->config_section == "Lora" && portduino_config.lora_spi_dev == "ch341") { continue; } if (i->enabled) { if (used_pins.find(i->pin) != used_pins.end()) { printf("Pin %d is in use for multiple purposes\n", i->pin); } else { if (initGPIOPin(i->pin, gpioChipName + std::to_string(i->gpiochip), i->line) != ERRNO_OK) { printf("Error setting pin number %d. It may not exist, or may already be in use.\n", i->line); exit(EXIT_FAILURE); } used_pins.insert(i->pin); } } } printf("Initializing extra pins\n"); for (auto i : portduino_config.extra_pins) { // In the case of a ch341 Lora device, we don't want to touch the system GPIO lines for Lora // Those GPIO are handled in our usermode driver instead. if (i.config_section == "Lora" && portduino_config.lora_spi_dev == "ch341") { continue; } if (i.enabled) { if (used_pins.find(i.pin) != used_pins.end()) { printf("Pin %d is in use for multiple purposes\n", i.pin); } else { if (initGPIOPin(i.pin, gpioChipName + std::to_string(i.gpiochip), i.line) != ERRNO_OK) { printf("Error setting pin number %d. It may not exist, or may already be in use.\n", i.line); exit(EXIT_FAILURE); } used_pins.insert(i.pin); } } } // In one test, this dance seemed necessary to trigger the pin to detect properly. if (portduino_config.lora_pa_detect_pin.enabled) { pinMode(portduino_config.lora_pa_detect_pin.pin, INPUT_PULLDOWN); sleep(1); if (digitalRead(portduino_config.lora_pa_detect_pin.pin) == LOW) { std::cout << "Pin " << portduino_config.lora_pa_detect_pin.pin << " PULLDOWN is LOW" << std::endl; } pinMode(portduino_config.lora_pa_detect_pin.pin, INPUT_PULLUP); sleep(1); if (digitalRead(portduino_config.lora_pa_detect_pin.pin) == HIGH) { std::cout << "Pin " << portduino_config.lora_pa_detect_pin.pin << " PULLUP is HIGH, dropping PA curve" << std::endl; portduino_config.num_pa_points = 1; portduino_config.tx_gain_lora[0] = 0; } else { std::cout << "Pin " << portduino_config.lora_pa_detect_pin.pin << " PULLUP is LOW, using PA curve" << std::endl; } // disable bias once finished pinMode(portduino_config.lora_pa_detect_pin.pin, INPUT); } else if (portduino_config.hat_plus_custom_fields.find("io_slot1") != portduino_config.hat_plus_custom_fields.end()) { printf("Hat+ io_slot1 is %s\n", portduino_config.hat_plus_custom_fields["io_slot1"].c_str()); if (portduino_config.hat_plus_custom_fields["io_slot1"] != "RAK13302") { std::cout << "Hat+ io_slot1 is not RAK13302, skipping PA curve" << std::endl; portduino_config.num_pa_points = 1; portduino_config.tx_gain_lora[0] = 0; } } for (auto i : portduino_config.extra_pins) { // In the case of a ch341 Lora device, we don't want to touch the system GPIO lines for Lora // Those GPIO are handled in our usermode driver instead. if (i.config_section == "Lora" && portduino_config.lora_spi_dev == "ch341") { continue; } if (i.enabled && i.default_high) { pinMode(i.pin, OUTPUT); digitalWrite(i.pin, HIGH); } } // Only initialize the radio pins when dealing with real, kernel controlled SPI hardware if (portduino_config.lora_spi_dev != "" && portduino_config.lora_spi_dev != "ch341") { SPI.begin(portduino_config.lora_spi_dev.c_str()); } if (portduino_config.traceFilename != "") { try { traceFile.open(portduino_config.traceFilename, std::ios::out | std::ios::app); } catch (std::ofstream::failure &e) { std::cout << "*** traceFile Exception " << e.what() << std::endl; exit(EXIT_FAILURE); } if (!traceFile.is_open()) { std::cout << "*** traceFile open failure" << std::endl; exit(EXIT_FAILURE); } } else if (portduino_config.JSONFilename != "") { try { if (portduino_config.JSONFileRotate == 0) { JSONFile.open(portduino_config.JSONFilename, std::ios::out | std::ios::app); } } catch (std::ofstream::failure &e) { std::cout << "*** JSONFile Exception " << e.what() << std::endl; exit(EXIT_FAILURE); } if (!JSONFile.is_open()) { std::cout << "*** JSONFile open failure" << std::endl; exit(EXIT_FAILURE); } } if (verboseEnabled && portduino_config.logoutputlevel != level_trace) { portduino_config.logoutputlevel = level_debug; } return; } int initGPIOPin(int pinNum, const std::string &gpioChipName, int line) { #ifdef PORTDUINO_LINUX_HARDWARE std::string gpio_name = "GPIO" + std::to_string(pinNum); std::cout << "Initializing " << gpio_name << " on chip " << gpioChipName << std::endl; try { GPIOPin *csPin; csPin = new LinuxGPIOPin(pinNum, gpioChipName.c_str(), line, gpio_name.c_str()); csPin->setSilent(); gpioBind(csPin); return ERRNO_OK; } catch (...) { const std::type_info *t = abi::__cxa_current_exception_type(); std::cout << "Warning, cannot claim pin " << gpio_name << (t ? t->name() : "null") << std::endl; return ERRNO_DISABLED; } #else return ERRNO_OK; #endif } bool loadConfig(const char *configPath) { YAML::Node yamlConfig; try { yamlConfig = YAML::LoadFile(configPath); if (yamlConfig["Logging"]) { if (yamlConfig["Logging"]["LogLevel"].as("info") == "trace") { portduino_config.logoutputlevel = level_trace; } else if (yamlConfig["Logging"]["LogLevel"].as("info") == "debug") { portduino_config.logoutputlevel = level_debug; } else if (yamlConfig["Logging"]["LogLevel"].as("info") == "info") { portduino_config.logoutputlevel = level_info; } else if (yamlConfig["Logging"]["LogLevel"].as("info") == "warn") { portduino_config.logoutputlevel = level_warn; } else if (yamlConfig["Logging"]["LogLevel"].as("info") == "error") { portduino_config.logoutputlevel = level_error; } portduino_config.traceFilename = yamlConfig["Logging"]["TraceFile"].as(""); portduino_config.JSONFilename = yamlConfig["Logging"]["JSONFile"].as(""); portduino_config.JSONFileRotate = yamlConfig["Logging"]["JSONFileRotate"].as(0); portduino_config.JSONFilter = (_meshtastic_PortNum)yamlConfig["Logging"]["JSONFilter"].as(0); if (yamlConfig["Logging"]["JSONFilter"].as("") == "textmessage") portduino_config.JSONFilter = meshtastic_PortNum_TEXT_MESSAGE_APP; else if (yamlConfig["Logging"]["JSONFilter"].as("") == "telemetry") portduino_config.JSONFilter = meshtastic_PortNum_TELEMETRY_APP; else if (yamlConfig["Logging"]["JSONFilter"].as("") == "nodeinfo") portduino_config.JSONFilter = meshtastic_PortNum_NODEINFO_APP; else if (yamlConfig["Logging"]["JSONFilter"].as("") == "position") portduino_config.JSONFilter = meshtastic_PortNum_POSITION_APP; else if (yamlConfig["Logging"]["JSONFilter"].as("") == "waypoint") portduino_config.JSONFilter = meshtastic_PortNum_WAYPOINT_APP; else if (yamlConfig["Logging"]["JSONFilter"].as("") == "neighborinfo") portduino_config.JSONFilter = meshtastic_PortNum_NEIGHBORINFO_APP; else if (yamlConfig["Logging"]["JSONFilter"].as("") == "traceroute") portduino_config.JSONFilter = meshtastic_PortNum_TRACEROUTE_APP; else if (yamlConfig["Logging"]["JSONFilter"].as("") == "detection") portduino_config.JSONFilter = meshtastic_PortNum_DETECTION_SENSOR_APP; else if (yamlConfig["Logging"]["JSONFilter"].as("") == "paxcounter") portduino_config.JSONFilter = meshtastic_PortNum_PAXCOUNTER_APP; else if (yamlConfig["Logging"]["JSONFilter"].as("") == "remotehardware") portduino_config.JSONFilter = meshtastic_PortNum_REMOTE_HARDWARE_APP; if (yamlConfig["Logging"]["AsciiLogs"]) { // Default is !isatty(1) but can be set explicitly in config.yaml portduino_config.ascii_logs = yamlConfig["Logging"]["AsciiLogs"].as(); portduino_config.ascii_logs_explicit = true; } } if (yamlConfig["Lora"]) { if (yamlConfig["Lora"]["Module"]) { for (const auto &loraModule : portduino_config.loraModules) { if (yamlConfig["Lora"]["Module"].as("") == loraModule.second) { portduino_config.lora_module = loraModule.first; break; } } } if (yamlConfig["Lora"]["SX126X_MAX_POWER"]) portduino_config.sx126x_max_power = yamlConfig["Lora"]["SX126X_MAX_POWER"].as(22); if (yamlConfig["Lora"]["SX128X_MAX_POWER"]) portduino_config.sx128x_max_power = yamlConfig["Lora"]["SX128X_MAX_POWER"].as(13); if (yamlConfig["Lora"]["LR1110_MAX_POWER"]) portduino_config.lr1110_max_power = yamlConfig["Lora"]["LR1110_MAX_POWER"].as(22); if (yamlConfig["Lora"]["LR1120_MAX_POWER"]) portduino_config.lr1120_max_power = yamlConfig["Lora"]["LR1120_MAX_POWER"].as(13); if (yamlConfig["Lora"]["RF95_MAX_POWER"]) portduino_config.rf95_max_power = yamlConfig["Lora"]["RF95_MAX_POWER"].as(20); if (yamlConfig["Lora"]["TX_GAIN_LORA"]) { YAML::Node tx_gain_node = yamlConfig["Lora"]["TX_GAIN_LORA"]; if (tx_gain_node.IsSequence() && tx_gain_node.size() != 0) { portduino_config.num_pa_points = min(tx_gain_node.size(), std::size(portduino_config.tx_gain_lora)); for (int i = 0; i < portduino_config.num_pa_points; i++) { portduino_config.tx_gain_lora[i] = tx_gain_node[i].as(); } } else { portduino_config.num_pa_points = 1; portduino_config.tx_gain_lora[0] = tx_gain_node.as(0); } } if (portduino_config.lora_module != use_autoconf && portduino_config.lora_module != use_simradio && !portduino_config.force_simradio) { portduino_config.dio2_as_rf_switch = yamlConfig["Lora"]["DIO2_AS_RF_SWITCH"].as(false); portduino_config.dio3_tcxo_voltage = yamlConfig["Lora"]["DIO3_TCXO_VOLTAGE"].as(0) * 1000; if (portduino_config.dio3_tcxo_voltage == 0 && yamlConfig["Lora"]["DIO3_TCXO_VOLTAGE"].as(false)) { portduino_config.dio3_tcxo_voltage = 1800; // default millivolts for "true" } // backwards API compatibility and to globally set gpiochip once portduino_config.lora_default_gpiochip = yamlConfig["Lora"]["gpiochip"].as(0); for (auto this_pin : portduino_config.all_pins) { if (this_pin->config_section == "Lora") { readGPIOFromYaml(yamlConfig["Lora"][this_pin->config_name], *this_pin); } } } if (yamlConfig["Lora"]["Enable_Pins"]) { for (auto extra_pin : yamlConfig["Lora"]["Enable_Pins"]) { portduino_config.extra_pins.push_back(pinMapping()); portduino_config.extra_pins.back().config_section = "Lora"; portduino_config.extra_pins.back().config_name = "Enable_Pins"; portduino_config.extra_pins.back().enabled = true; portduino_config.extra_pins.back().default_high = true; readGPIOFromYaml(extra_pin, portduino_config.extra_pins.back()); } } portduino_config.spiSpeed = yamlConfig["Lora"]["spiSpeed"].as(2000000); portduino_config.lora_usb_serial_num = yamlConfig["Lora"]["USB_Serialnum"].as(""); portduino_config.lora_usb_pid = yamlConfig["Lora"]["USB_PID"].as(0x5512); portduino_config.lora_usb_vid = yamlConfig["Lora"]["USB_VID"].as(0x1A86); portduino_config.lora_spi_dev = yamlConfig["Lora"]["spidev"].as("spidev0.0"); if (portduino_config.lora_spi_dev != "ch341") { portduino_config.lora_spi_dev = "/dev/" + portduino_config.lora_spi_dev; if (portduino_config.lora_spi_dev.length() == 14) { int x = portduino_config.lora_spi_dev.at(11) - '0'; int y = portduino_config.lora_spi_dev.at(13) - '0'; // Pretty sure this is always true if (x >= 0 && x < 10 && y >= 0 && y < 10) { // I believe this bit of weirdness is specifically for the new GUI portduino_config.lora_spi_dev_int = x + y << 4; portduino_config.display_spi_dev_int = portduino_config.lora_spi_dev_int; portduino_config.touchscreen_spi_dev_int = portduino_config.lora_spi_dev_int; } } } if (yamlConfig["Lora"]["rfswitch_table"]) { portduino_config.has_rfswitch_table = true; portduino_config.rfswitch_table[0].mode = LR11x0::MODE_STBY; portduino_config.rfswitch_table[1].mode = LR11x0::MODE_RX; portduino_config.rfswitch_table[2].mode = LR11x0::MODE_TX; portduino_config.rfswitch_table[3].mode = LR11x0::MODE_TX_HP; portduino_config.rfswitch_table[4].mode = LR11x0::MODE_TX_HF; portduino_config.rfswitch_table[5].mode = LR11x0::MODE_GNSS; portduino_config.rfswitch_table[6].mode = LR11x0::MODE_WIFI; portduino_config.rfswitch_table[7] = END_OF_MODE_TABLE; for (int i = 0; i < 5; i++) { // set up the pin array first if (yamlConfig["Lora"]["rfswitch_table"]["pins"][i].as("") == "DIO5") portduino_config.rfswitch_dio_pins[i] = RADIOLIB_LR11X0_DIO5; if (yamlConfig["Lora"]["rfswitch_table"]["pins"][i].as("") == "DIO6") portduino_config.rfswitch_dio_pins[i] = RADIOLIB_LR11X0_DIO6; if (yamlConfig["Lora"]["rfswitch_table"]["pins"][i].as("") == "DIO7") portduino_config.rfswitch_dio_pins[i] = RADIOLIB_LR11X0_DIO7; if (yamlConfig["Lora"]["rfswitch_table"]["pins"][i].as("") == "DIO8") portduino_config.rfswitch_dio_pins[i] = RADIOLIB_LR11X0_DIO8; if (yamlConfig["Lora"]["rfswitch_table"]["pins"][i].as("") == "DIO10") portduino_config.rfswitch_dio_pins[i] = RADIOLIB_LR11X0_DIO10; // now fill in the table if (yamlConfig["Lora"]["rfswitch_table"]["MODE_STBY"][i].as("") == "HIGH") portduino_config.rfswitch_table[0].values[i] = HIGH; if (yamlConfig["Lora"]["rfswitch_table"]["MODE_RX"][i].as("") == "HIGH") portduino_config.rfswitch_table[1].values[i] = HIGH; if (yamlConfig["Lora"]["rfswitch_table"]["MODE_TX"][i].as("") == "HIGH") portduino_config.rfswitch_table[2].values[i] = HIGH; if (yamlConfig["Lora"]["rfswitch_table"]["MODE_TX_HP"][i].as("") == "HIGH") portduino_config.rfswitch_table[3].values[i] = HIGH; if (yamlConfig["Lora"]["rfswitch_table"]["MODE_TX_HF"][i].as("") == "HIGH") portduino_config.rfswitch_table[4].values[i] = HIGH; if (yamlConfig["Lora"]["rfswitch_table"]["MODE_GNSS"][i].as("") == "HIGH") portduino_config.rfswitch_table[5].values[i] = HIGH; if (yamlConfig["Lora"]["rfswitch_table"]["MODE_WIFI"][i].as("") == "HIGH") portduino_config.rfswitch_table[6].values[i] = HIGH; } } } readGPIOFromYaml(yamlConfig["GPIO"]["User"], portduino_config.userButtonPin); if (yamlConfig["GPS"]) { std::string serialPath = yamlConfig["GPS"]["SerialPath"].as(""); if (serialPath != "") { Serial1.setPath(serialPath); portduino_config.has_gps = 1; } } if (yamlConfig["GPIO"]["ExtraPins"]) { for (auto extra_pin : yamlConfig["GPIO"]["ExtraPins"]) { portduino_config.extra_pins.push_back(pinMapping()); portduino_config.extra_pins.back().config_section = "GPIO"; portduino_config.extra_pins.back().config_name = "ExtraPins"; portduino_config.extra_pins.back().enabled = true; readGPIOFromYaml(extra_pin, portduino_config.extra_pins.back()); } } if (yamlConfig["I2C"]) { portduino_config.i2cdev = yamlConfig["I2C"]["I2CDevice"].as(""); } if (yamlConfig["Display"]) { for (const auto &screen_name : portduino_config.screen_names) { if (yamlConfig["Display"]["Panel"].as("") == screen_name.second) portduino_config.displayPanel = screen_name.first; } portduino_config.displayHeight = yamlConfig["Display"]["Height"].as(0); portduino_config.displayWidth = yamlConfig["Display"]["Width"].as(0); readGPIOFromYaml(yamlConfig["Display"]["DC"], portduino_config.displayDC, -1); readGPIOFromYaml(yamlConfig["Display"]["CS"], portduino_config.displayCS, -1); readGPIOFromYaml(yamlConfig["Display"]["Backlight"], portduino_config.displayBacklight, -1); readGPIOFromYaml(yamlConfig["Display"]["BacklightPWMChannel"], portduino_config.displayBacklightPWMChannel, -1); readGPIOFromYaml(yamlConfig["Display"]["Reset"], portduino_config.displayReset, -1); portduino_config.displayBacklightInvert = yamlConfig["Display"]["BacklightInvert"].as(false); portduino_config.displayRGBOrder = yamlConfig["Display"]["RGBOrder"].as(false); portduino_config.displayOffsetX = yamlConfig["Display"]["OffsetX"].as(0); portduino_config.displayOffsetY = yamlConfig["Display"]["OffsetY"].as(0); portduino_config.displayRotate = yamlConfig["Display"]["Rotate"].as(false); portduino_config.displayOffsetRotate = yamlConfig["Display"]["OffsetRotate"].as(1); portduino_config.displayInvert = yamlConfig["Display"]["Invert"].as(false); portduino_config.displayBusFrequency = yamlConfig["Display"]["BusFrequency"].as(40000000); if (yamlConfig["Display"]["spidev"]) { portduino_config.display_spi_dev = "/dev/" + yamlConfig["Display"]["spidev"].as("spidev0.1"); if (portduino_config.display_spi_dev.length() == 14) { int x = portduino_config.display_spi_dev.at(11) - '0'; int y = portduino_config.display_spi_dev.at(13) - '0'; if (x >= 0 && x < 10 && y >= 0 && y < 10) { portduino_config.display_spi_dev_int = x + y << 4; portduino_config.touchscreen_spi_dev_int = portduino_config.display_spi_dev_int; } } } } if (yamlConfig["Touchscreen"]) { if (yamlConfig["Touchscreen"]["Module"].as("") == "XPT2046") portduino_config.touchscreenModule = xpt2046; else if (yamlConfig["Touchscreen"]["Module"].as("") == "STMPE610") portduino_config.touchscreenModule = stmpe610; else if (yamlConfig["Touchscreen"]["Module"].as("") == "GT911") portduino_config.touchscreenModule = gt911; else if (yamlConfig["Touchscreen"]["Module"].as("") == "FT5x06") portduino_config.touchscreenModule = ft5x06; readGPIOFromYaml(yamlConfig["Touchscreen"]["CS"], portduino_config.touchscreenCS, -1); readGPIOFromYaml(yamlConfig["Touchscreen"]["IRQ"], portduino_config.touchscreenIRQ, -1); portduino_config.touchscreenBusFrequency = yamlConfig["Touchscreen"]["BusFrequency"].as(1000000); portduino_config.touchscreenRotate = yamlConfig["Touchscreen"]["Rotate"].as(-1); portduino_config.touchscreenI2CAddr = yamlConfig["Touchscreen"]["I2CAddr"].as(-1); if (yamlConfig["Touchscreen"]["spidev"]) { portduino_config.touchscreen_spi_dev = "/dev/" + yamlConfig["Touchscreen"]["spidev"].as(""); if (portduino_config.touchscreen_spi_dev.length() == 14) { int x = portduino_config.touchscreen_spi_dev.at(11) - '0'; int y = portduino_config.touchscreen_spi_dev.at(13) - '0'; if (x >= 0 && x < 10 && y >= 0 && y < 10) { portduino_config.touchscreen_spi_dev_int = x + y << 4; } } } } if (yamlConfig["Input"]) { portduino_config.keyboardDevice = (yamlConfig["Input"]["KeyboardDevice"]).as(""); portduino_config.pointerDevice = (yamlConfig["Input"]["PointerDevice"]).as(""); readGPIOFromYaml(yamlConfig["Input"]["User"], portduino_config.userButtonPin); readGPIOFromYaml(yamlConfig["Input"]["TrackballUp"], portduino_config.tbUpPin); readGPIOFromYaml(yamlConfig["Input"]["TrackballDown"], portduino_config.tbDownPin); readGPIOFromYaml(yamlConfig["Input"]["TrackballLeft"], portduino_config.tbLeftPin); readGPIOFromYaml(yamlConfig["Input"]["TrackballRight"], portduino_config.tbRightPin); readGPIOFromYaml(yamlConfig["Input"]["TrackballPress"], portduino_config.tbPressPin); if (yamlConfig["Input"]["TrackballDirection"].as("RISING") == "RISING") { portduino_config.tbDirection = 4; } else if (yamlConfig["Input"]["TrackballDirection"].as("RISING") == "FALLING") { portduino_config.tbDirection = 3; } } if (yamlConfig["Webserver"]) { portduino_config.webserverport = (yamlConfig["Webserver"]["Port"]).as(-1); portduino_config.webserver_root_path = (yamlConfig["Webserver"]["RootPath"]).as("/usr/share/meshtasticd/web"); portduino_config.webserver_ssl_key_path = (yamlConfig["Webserver"]["SSLKey"]).as("/etc/meshtasticd/ssl/private_key.pem"); portduino_config.webserver_ssl_cert_path = (yamlConfig["Webserver"]["SSLCert"]).as("/etc/meshtasticd/ssl/certificate.pem"); } if (yamlConfig["HostMetrics"]) { portduino_config.hostMetrics_channel = (yamlConfig["HostMetrics"]["Channel"]).as(0); portduino_config.hostMetrics_interval = (yamlConfig["HostMetrics"]["ReportInterval"]).as(0); portduino_config.hostMetrics_user_command = (yamlConfig["HostMetrics"]["UserStringCommand"]).as(""); } if (yamlConfig["Config"]) { portduino_config.has_config_overrides = true; if (yamlConfig["Config"]["DisplayMode"]) { portduino_config.has_configDisplayMode = true; if ((yamlConfig["Config"]["DisplayMode"]).as("") == "TWOCOLOR") { portduino_config.configDisplayMode = meshtastic_Config_DisplayConfig_DisplayMode_TWOCOLOR; } else if ((yamlConfig["Config"]["DisplayMode"]).as("") == "INVERTED") { portduino_config.configDisplayMode = meshtastic_Config_DisplayConfig_DisplayMode_INVERTED; } else if ((yamlConfig["Config"]["DisplayMode"]).as("") == "COLOR") { portduino_config.configDisplayMode = meshtastic_Config_DisplayConfig_DisplayMode_COLOR; } else { portduino_config.configDisplayMode = meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT; } } if (yamlConfig["Config"]["StatusMessage"]) { portduino_config.has_statusMessage = true; portduino_config.statusMessage = (yamlConfig["Config"]["StatusMessage"]).as(""); } if ((yamlConfig["Config"]["EnableUDP"]).as(false)) { portduino_config.enable_UDP = true; } } if (yamlConfig["General"]) { portduino_config.MaxNodes = (yamlConfig["General"]["MaxNodes"]).as(200); portduino_config.maxtophone = (yamlConfig["General"]["MaxMessageQueue"]).as(100); portduino_config.config_directory = (yamlConfig["General"]["ConfigDirectory"]).as(""); portduino_config.available_directory = (yamlConfig["General"]["AvailableDirectory"]).as("/etc/meshtasticd/available.d/"); if ((yamlConfig["General"]["MACAddress"]).as("") != "" && (yamlConfig["General"]["MACAddressSource"]).as("") != "") { std::cout << "Cannot set both MACAddress and MACAddressSource!" << std::endl; exit(EXIT_FAILURE); } if (checkConfigPort) { portduino_config.api_port = (yamlConfig["General"]["APIPort"]).as(-1); if (portduino_config.api_port > 1023 && portduino_config.api_port < 65536) { TCPPort = (portduino_config.api_port); } } portduino_config.mac_address = (yamlConfig["General"]["MACAddress"]).as(""); if (portduino_config.mac_address != "") { portduino_config.mac_address_explicit = true; } else if ((yamlConfig["General"]["MACAddressSource"]).as("") != "") { portduino_config.mac_address_source = (yamlConfig["General"]["MACAddressSource"]).as(""); std::ifstream infile("/sys/class/net/" + portduino_config.mac_address_source + "/address"); std::getline(infile, portduino_config.mac_address); } // https://stackoverflow.com/a/20326454 portduino_config.mac_address.erase( std::remove(portduino_config.mac_address.begin(), portduino_config.mac_address.end(), ':'), portduino_config.mac_address.end()); } } catch (YAML::Exception &e) { std::cout << "*** Exception " << e.what() << std::endl; return false; } return true; } // https://stackoverflow.com/questions/874134/find-out-if-string-ends-with-another-string-in-c static bool ends_with(std::string_view str, std::string_view suffix) { return str.size() >= suffix.size() && str.compare(str.size() - suffix.size(), suffix.size(), suffix) == 0; } bool MAC_from_string(std::string mac_str, uint8_t *dmac) { mac_str.erase(std::remove(mac_str.begin(), mac_str.end(), ':'), mac_str.end()); if (mac_str.length() == 12) { dmac[0] = std::stoi(portduino_config.mac_address.substr(0, 2), nullptr, 16); dmac[1] = std::stoi(portduino_config.mac_address.substr(2, 2), nullptr, 16); dmac[2] = std::stoi(portduino_config.mac_address.substr(4, 2), nullptr, 16); dmac[3] = std::stoi(portduino_config.mac_address.substr(6, 2), nullptr, 16); dmac[4] = std::stoi(portduino_config.mac_address.substr(8, 2), nullptr, 16); dmac[5] = std::stoi(portduino_config.mac_address.substr(10, 2), nullptr, 16); return true; } else { return false; } } std::string exec(const char *cmd) { // https://stackoverflow.com/a/478960 std::array buffer; std::string result; std::unique_ptr pipe(popen(cmd, "r"), pclose); if (!pipe) { throw std::runtime_error("popen() failed!"); } while (fgets(buffer.data(), static_cast(buffer.size()), pipe.get()) != nullptr) { result += buffer.data(); } return result; } void readGPIOFromYaml(YAML::Node sourceNode, pinMapping &destPin, int pinDefault) { if (sourceNode.IsMap()) { destPin.enabled = true; destPin.pin = sourceNode["pin"].as(pinDefault); destPin.line = sourceNode["line"].as(destPin.pin); destPin.gpiochip = sourceNode["gpiochip"].as(portduino_config.lora_default_gpiochip); } else if (sourceNode) { // backwards API compatibility destPin.enabled = true; destPin.pin = sourceNode.as(pinDefault); destPin.line = destPin.pin; destPin.gpiochip = portduino_config.lora_default_gpiochip; } } ================================================ FILE: src/platform/portduino/PortduinoGlue.h ================================================ #pragma once #include #include #include #include #include #include "LR11x0Interface.h" #include "Module.h" #include "mesh/generated/meshtastic/mesh.pb.h" #include "yaml-cpp/yaml.h" extern struct portduino_status_struct { bool LoRa_in_error = false; _meshtastic_HardwareModel hardwareModel = meshtastic_HardwareModel_PORTDUINO; } portduino_status; #include "platform/portduino/USBHal.h" // Product strings for auto-configuration // {"PRODUCT_STRING", "CONFIG.YAML"} // YAML paths are relative to `meshtastic/available.d` inline const std::unordered_map configProducts = { {"MESHTOAD", "lora-usb-meshtoad-e22.yaml"}, {"MESHSTICK", "lora-meshstick-1262.yaml"}, {"MESHADV-PI", "lora-MeshAdv-900M30S.yaml"}, {"MeshAdv Mini", "lora-MeshAdv-Mini-900M22S.yaml"}, {"POWERPI", "lora-MeshAdv-900M30S.yaml"}, {"RAK6421-13300-S1", "lora-RAK6421-13300-slot1.yaml"}, {"RAK6421-13300-S2", "lora-RAK6421-13300-slot2.yaml"}}; enum screen_modules { no_screen, x11, fb, st7789, st7735, st7735s, st7796, ili9341, ili9342, ili9486, ili9488, hx8357d }; enum touchscreen_modules { no_touchscreen, xpt2046, stmpe610, gt911, ft5x06 }; enum portduino_log_level { level_error, level_warn, level_info, level_debug, level_trace }; enum lora_module_enum { use_simradio, use_autoconf, use_rf95, use_sx1262, use_sx1268, use_sx1280, use_lr1110, use_lr1120, use_lr1121, use_llcc68 }; struct pinMapping { std::string config_section; std::string config_name; int pin = RADIOLIB_NC; int gpiochip; int line; bool enabled = false; bool default_high = false; }; extern std::ofstream traceFile; extern std::ofstream JSONFile; extern Ch341Hal *ch341Hal; int initGPIOPin(int pinNum, const std::string &gpioChipname, int line); bool loadConfig(const char *configPath); static bool ends_with(std::string_view str, std::string_view suffix); void getMacAddr(uint8_t *dmac); bool MAC_from_string(std::string mac_str, uint8_t *dmac); void readGPIOFromYaml(YAML::Node sourceNode, pinMapping &destPin, int pinDefault = RADIOLIB_NC); std::string exec(const char *cmd); extern struct portduino_config_struct { // Lora std::map loraModules = { {use_simradio, "sim"}, {use_autoconf, "auto"}, {use_rf95, "RF95"}, {use_sx1262, "sx1262"}, {use_sx1268, "sx1268"}, {use_sx1280, "sx1280"}, {use_lr1110, "lr1110"}, {use_lr1120, "lr1120"}, {use_lr1121, "lr1121"}, {use_llcc68, "LLCC68"}}; std::map screen_names = {{x11, "X11"}, {fb, "FB"}, {st7789, "ST7789"}, {st7735, "ST7735"}, {st7735s, "ST7735S"}, {st7796, "ST7796"}, {ili9341, "ILI9341"}, {ili9342, "ILI9342"}, {ili9486, "ILI9486"}, {ili9488, "ILI9488"}, {hx8357d, "HX8357D"}}; lora_module_enum lora_module; bool has_rfswitch_table = false; uint32_t rfswitch_dio_pins[5] = {RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC}; Module::RfSwitchMode_t rfswitch_table[8]; bool force_simradio = false; bool has_device_id = false; uint8_t device_id[16] = {0}; std::string lora_spi_dev = ""; std::string lora_usb_serial_num = ""; int lora_spi_dev_int = 0; int lora_default_gpiochip = 0; int sx126x_max_power = 22; int sx128x_max_power = 13; int lr1110_max_power = 22; int lr1120_max_power = 13; int rf95_max_power = 20; bool dio2_as_rf_switch = false; int dio3_tcxo_voltage = 0; int lora_usb_pid = 0x5512; int lora_usb_vid = 0x1A86; int spiSpeed = 2000000; int num_pa_points = 1; // default to 1 point, with 0 gain uint16_t tx_gain_lora[22] = {0}; pinMapping lora_cs_pin = {"Lora", "CS"}; pinMapping lora_irq_pin = {"Lora", "IRQ"}; pinMapping lora_busy_pin = {"Lora", "Busy"}; pinMapping lora_reset_pin = {"Lora", "Reset"}; pinMapping lora_txen_pin = {"Lora", "TXen"}; pinMapping lora_rxen_pin = {"Lora", "RXen"}; pinMapping lora_sx126x_ant_sw_pin = {"Lora", "SX126X_ANT_SW"}; pinMapping lora_pa_detect_pin = {"Lora", "GPIO_DETECT_PA"}; std::vector extra_pins = {}; // GPS bool has_gps = false; // I2C std::string i2cdev = ""; // Display std::string display_spi_dev = ""; int display_spi_dev_int = 0; int displayBusFrequency = 40000000; screen_modules displayPanel = no_screen; int displayWidth = 0; int displayHeight = 0; bool displayRGBOrder = false; bool displayBacklightInvert = false; bool displayRotate = false; int displayOffsetRotate = 1; bool displayInvert = false; int displayOffsetX = 0; int displayOffsetY = 0; pinMapping displayDC = {"Display", "DC"}; pinMapping displayCS = {"Display", "CS"}; pinMapping displayBacklight = {"Display", "Backlight"}; pinMapping displayBacklightPWMChannel = {"Display", "BacklightPWMChannel"}; pinMapping displayReset = {"Display", "Reset"}; // Touchscreen std::string touchscreen_spi_dev = ""; int touchscreen_spi_dev_int = 0; touchscreen_modules touchscreenModule = no_touchscreen; int touchscreenI2CAddr = -1; int touchscreenBusFrequency = 1000000; int touchscreenRotate = -1; pinMapping touchscreenCS = {"Touchscreen", "CS"}; pinMapping touchscreenIRQ = {"Touchscreen", "IRQ"}; // Input std::string keyboardDevice = ""; std::string pointerDevice = ""; int tbDirection; pinMapping userButtonPin = {"Input", "User"}; pinMapping tbUpPin = {"Input", "TrackballUp"}; pinMapping tbDownPin = {"Input", "TrackballDown"}; pinMapping tbLeftPin = {"Input", "TrackballLwft"}; pinMapping tbRightPin = {"Input", "TrackballRight"}; pinMapping tbPressPin = {"Input", "TrackballPress"}; // Logging portduino_log_level logoutputlevel = level_debug; std::string traceFilename; bool ascii_logs = !isatty(1); bool ascii_logs_explicit = false; std::string JSONFilename; int JSONFileRotate = 0; meshtastic_PortNum JSONFilter = (_meshtastic_PortNum)0; // Webserver std::string webserver_root_path = ""; std::string webserver_ssl_key_path = "/etc/meshtasticd/ssl/private_key.pem"; std::string webserver_ssl_cert_path = "/etc/meshtasticd/ssl/certificate.pem"; int webserverport = -1; // HostMetrics std::string hostMetrics_user_command = ""; int hostMetrics_interval = 0; int hostMetrics_channel = 0; // config bool has_config_overrides = false; int configDisplayMode = 0; bool has_configDisplayMode = false; std::string statusMessage = ""; bool has_statusMessage = false; bool enable_UDP = false; // General std::string mac_address = ""; bool mac_address_explicit = false; std::string mac_address_source = ""; int api_port = -1; std::string config_directory = ""; std::string available_directory = "/etc/meshtasticd/available.d/"; int maxtophone = 100; int MaxNodes = 200; std::unordered_map hat_plus_custom_fields; pinMapping *all_pins[21] = {&lora_cs_pin, &lora_irq_pin, &lora_busy_pin, &lora_reset_pin, &lora_txen_pin, &lora_rxen_pin, &lora_sx126x_ant_sw_pin, &lora_pa_detect_pin, &displayDC, &displayCS, &displayBacklight, &displayBacklightPWMChannel, &displayReset, &touchscreenCS, &touchscreenIRQ, &userButtonPin, &tbUpPin, &tbDownPin, &tbLeftPin, &tbRightPin, &tbPressPin}; std::string emit_yaml() { YAML::Emitter out; out << YAML::BeginMap; // Lora out << YAML::Key << "Lora" << YAML::Value << YAML::BeginMap; out << YAML::Key << "Module" << YAML::Value << loraModules[lora_module]; for (const auto *lora_pin : all_pins) { if (lora_pin->config_section == "Lora" && lora_pin->enabled) { out << YAML::Key << lora_pin->config_name << YAML::Value << YAML::BeginMap; out << YAML::Key << "pin" << YAML::Value << lora_pin->pin; out << YAML::Key << "line" << YAML::Value << lora_pin->line; out << YAML::Key << "gpiochip" << YAML::Value << lora_pin->gpiochip; out << YAML::EndMap; // User } } if (sx126x_max_power != 22) out << YAML::Key << "SX126X_MAX_POWER" << YAML::Value << sx126x_max_power; if (sx128x_max_power != 13) out << YAML::Key << "SX128X_MAX_POWER" << YAML::Value << sx128x_max_power; if (lr1110_max_power != 22) out << YAML::Key << "LR1110_MAX_POWER" << YAML::Value << lr1110_max_power; if (lr1120_max_power != 13) out << YAML::Key << "LR1120_MAX_POWER" << YAML::Value << lr1120_max_power; if (rf95_max_power != 20) out << YAML::Key << "RF95_MAX_POWER" << YAML::Value << rf95_max_power; if (num_pa_points > 1) { out << YAML::Key << "TX_GAIN_LORA" << YAML::Value << YAML::Flow << YAML::BeginSeq; for (int i = 0; i < num_pa_points; i++) { out << YAML::Value << tx_gain_lora[i]; } out << YAML::EndSeq; } else if (tx_gain_lora[0] != 0) { out << YAML::Key << "TX_GAIN_LORA" << YAML::Value << tx_gain_lora[0]; } if (dio2_as_rf_switch) out << YAML::Key << "DIO2_AS_RF_SWITCH" << YAML::Value << dio2_as_rf_switch; if (dio3_tcxo_voltage != 0) out << YAML::Key << "DIO3_TCXO_VOLTAGE" << YAML::Value << YAML::Precision(3) << (float)dio3_tcxo_voltage / 1000; if (lora_usb_pid != 0x5512) out << YAML::Key << "USB_PID" << YAML::Value << YAML::Hex << lora_usb_pid; if (lora_usb_vid != 0x1A86) out << YAML::Key << "USB_VID" << YAML::Value << YAML::Hex << lora_usb_vid; if (lora_spi_dev != "" && !(lora_spi_dev == "/dev/spidev0.0" && lora_module == use_autoconf)) { if (lora_spi_dev.find("/dev/") != std::string::npos) lora_spi_dev = lora_spi_dev.substr(5); out << YAML::Key << "spidev" << YAML::Value << lora_spi_dev; } if (lora_usb_serial_num != "") out << YAML::Key << "USB_Serialnum" << YAML::Value << lora_usb_serial_num; if (spiSpeed != 2000000) out << YAML::Key << "spiSpeed" << YAML::Value << spiSpeed; if (rfswitch_dio_pins[0] != RADIOLIB_NC) { out << YAML::Key << "rfswitch_table" << YAML::Value << YAML::BeginMap; out << YAML::Key << "pins"; out << YAML::Value << YAML::Flow << YAML::BeginSeq; for (int i = 0; i < 5; i++) { // set up the pin array first if (rfswitch_dio_pins[i] == RADIOLIB_LR11X0_DIO5) out << "DIO5"; if (rfswitch_dio_pins[i] == RADIOLIB_LR11X0_DIO6) out << "DIO6"; if (rfswitch_dio_pins[i] == RADIOLIB_LR11X0_DIO7) out << "DIO7"; if (rfswitch_dio_pins[i] == RADIOLIB_LR11X0_DIO8) out << "DIO8"; if (rfswitch_dio_pins[i] == RADIOLIB_LR11X0_DIO10) out << "DIO10"; } out << YAML::EndSeq; for (int i = 0; i < 7; i++) { switch (i) { case 0: out << YAML::Key << "MODE_STBY"; break; case 1: out << YAML::Key << "MODE_RX"; break; case 2: out << YAML::Key << "MODE_TX"; break; case 3: out << YAML::Key << "MODE_TX_HP"; break; case 4: out << YAML::Key << "MODE_TX_HF"; break; case 5: out << YAML::Key << "MODE_GNSS"; break; case 6: out << YAML::Key << "MODE_WIFI"; break; } out << YAML::Value << YAML::Flow << YAML::BeginSeq; for (int j = 0; j < 5; j++) { if (rfswitch_table[i].values[j] == HIGH) { out << "HIGH"; } else { out << "LOW"; } } out << YAML::EndSeq; } out << YAML::EndMap; // rfswitch_table } out << YAML::EndMap; // Lora if (!extra_pins.empty()) { out << YAML::Key << "GPIO" << YAML::Value << YAML::BeginMap; out << YAML::Key << "ExtraPins" << YAML::Value << YAML::BeginSeq; for (auto extra : extra_pins) { out << YAML::BeginMap; out << YAML::Key << "pin" << YAML::Value << extra.pin; out << YAML::Key << "line" << YAML::Value << extra.line; out << YAML::Key << "gpiochip" << YAML::Value << extra.gpiochip; out << YAML::EndMap; } out << YAML::EndSeq; out << YAML::EndMap; // GPIO } if (i2cdev != "") { out << YAML::Key << "I2C" << YAML::Value << YAML::BeginMap; out << YAML::Key << "I2CDevice" << YAML::Value << i2cdev; out << YAML::EndMap; // I2C } // Display if (displayPanel != no_screen) { out << YAML::Key << "Display" << YAML::Value << YAML::BeginMap; for (const auto &screen_name : screen_names) { if (displayPanel == screen_name.first) out << YAML::Key << "Module" << YAML::Value << screen_name.second; } for (const auto *display_pin : all_pins) { if (display_pin->config_section == "Display" && display_pin->enabled) { out << YAML::Key << display_pin->config_name << YAML::Value << YAML::BeginMap; out << YAML::Key << "pin" << YAML::Value << display_pin->pin; out << YAML::Key << "line" << YAML::Value << display_pin->line; out << YAML::Key << "gpiochip" << YAML::Value << display_pin->gpiochip; out << YAML::EndMap; } } out << YAML::Key << "spidev" << YAML::Value << display_spi_dev; out << YAML::Key << "BusFrequency" << YAML::Value << displayBusFrequency; if (displayWidth) out << YAML::Key << "Width" << YAML::Value << displayWidth; if (displayHeight) out << YAML::Key << "Height" << YAML::Value << displayHeight; if (displayRGBOrder) out << YAML::Key << "RGBOrder" << YAML::Value << true; if (displayBacklightInvert) out << YAML::Key << "BacklightInvert" << YAML::Value << true; if (displayRotate) out << YAML::Key << "Rotate" << YAML::Value << true; if (displayInvert) out << YAML::Key << "Invert" << YAML::Value << true; if (displayOffsetX) out << YAML::Key << "OffsetX" << YAML::Value << displayOffsetX; if (displayOffsetY) out << YAML::Key << "OffsetY" << YAML::Value << displayOffsetY; out << YAML::Key << "OffsetRotate" << YAML::Value << displayOffsetRotate; out << YAML::EndMap; // Display } // Touchscreen if (touchscreen_spi_dev != "") { out << YAML::Key << "Touchscreen" << YAML::Value << YAML::BeginMap; out << YAML::Key << "spidev" << YAML::Value << touchscreen_spi_dev; out << YAML::Key << "BusFrequency" << YAML::Value << touchscreenBusFrequency; switch (touchscreenModule) { case xpt2046: out << YAML::Key << "Module" << YAML::Value << "XPT2046"; case stmpe610: out << YAML::Key << "Module" << YAML::Value << "STMPE610"; case gt911: out << YAML::Key << "Module" << YAML::Value << "GT911"; case ft5x06: out << YAML::Key << "Module" << YAML::Value << "FT5x06"; } for (const auto *touchscreen_pin : all_pins) { if (touchscreen_pin->config_section == "Touchscreen" && touchscreen_pin->enabled) { out << YAML::Key << touchscreen_pin->config_name << YAML::Value << YAML::BeginMap; out << YAML::Key << "pin" << YAML::Value << touchscreen_pin->pin; out << YAML::Key << "line" << YAML::Value << touchscreen_pin->line; out << YAML::Key << "gpiochip" << YAML::Value << touchscreen_pin->gpiochip; out << YAML::EndMap; } } if (touchscreenRotate != -1) out << YAML::Key << "Rotate" << YAML::Value << touchscreenRotate; if (touchscreenI2CAddr != -1) out << YAML::Key << "I2CAddr" << YAML::Value << touchscreenI2CAddr; out << YAML::EndMap; // Touchscreen } // Input out << YAML::Key << "Input" << YAML::Value << YAML::BeginMap; if (keyboardDevice != "") out << YAML::Key << "KeyboardDevice" << YAML::Value << keyboardDevice; if (pointerDevice != "") out << YAML::Key << "PointerDevice" << YAML::Value << pointerDevice; for (const auto *input_pin : all_pins) { if (input_pin->config_section == "Input" && input_pin->enabled) { out << YAML::Key << input_pin->config_name << YAML::Value << YAML::BeginMap; out << YAML::Key << "pin" << YAML::Value << input_pin->pin; out << YAML::Key << "line" << YAML::Value << input_pin->line; out << YAML::Key << "gpiochip" << YAML::Value << input_pin->gpiochip; out << YAML::EndMap; } } if (tbDirection == 3) out << YAML::Key << "TrackballDirection" << YAML::Value << "FALLING"; out << YAML::EndMap; // Input out << YAML::Key << "Logging" << YAML::Value << YAML::BeginMap; out << YAML::Key << "LogLevel" << YAML::Value; switch (logoutputlevel) { case level_error: out << "error"; break; case level_warn: out << "warn"; break; case level_info: out << "info"; break; case level_debug: out << "debug"; break; case level_trace: out << "trace"; break; } if (traceFilename != "") out << YAML::Key << "TraceFile" << YAML::Value << traceFilename; if (JSONFilename != "") { out << YAML::Key << "JSONFile" << YAML::Value << JSONFilename; if (JSONFileRotate != 0) out << YAML::Key << "JSONFileRotate" << YAML::Value << JSONFileRotate; if (JSONFilter == meshtastic_PortNum_TEXT_MESSAGE_APP) out << YAML::Key << "JSONFilter" << YAML::Value << "textmessage"; else if (JSONFilter == meshtastic_PortNum_TELEMETRY_APP) out << YAML::Key << "JSONFilter" << YAML::Value << "telemetry"; else if (JSONFilter == meshtastic_PortNum_NODEINFO_APP) out << YAML::Key << "JSONFilter" << YAML::Value << "nodeinfo"; else if (JSONFilter == meshtastic_PortNum_POSITION_APP) out << YAML::Key << "JSONFilter" << YAML::Value << "position"; else if (JSONFilter == meshtastic_PortNum_WAYPOINT_APP) out << YAML::Key << "JSONFilter" << YAML::Value << "waypoint"; else if (JSONFilter == meshtastic_PortNum_NEIGHBORINFO_APP) out << YAML::Key << "JSONFilter" << YAML::Value << "neighborinfo"; else if (JSONFilter == meshtastic_PortNum_TRACEROUTE_APP) out << YAML::Key << "JSONFilter" << YAML::Value << "traceroute"; else if (JSONFilter == meshtastic_PortNum_DETECTION_SENSOR_APP) out << YAML::Key << "JSONFilter" << YAML::Value << "detection"; else if (JSONFilter == meshtastic_PortNum_PAXCOUNTER_APP) out << YAML::Key << "JSONFilter" << YAML::Value << "paxcounter"; else if (JSONFilter == meshtastic_PortNum_REMOTE_HARDWARE_APP) out << YAML::Key << "JSONFilter" << YAML::Value << "remotehardware"; } if (ascii_logs_explicit) { out << YAML::Key << "AsciiLogs" << YAML::Value << ascii_logs; } out << YAML::EndMap; // Logging // Webserver if (webserver_root_path != "") { out << YAML::Key << "Webserver" << YAML::Value << YAML::BeginMap; out << YAML::Key << "RootPath" << YAML::Value << webserver_root_path; out << YAML::Key << "SSLKey" << YAML::Value << webserver_ssl_key_path; out << YAML::Key << "SSLCert" << YAML::Value << webserver_ssl_cert_path; out << YAML::Key << "Port" << YAML::Value << webserverport; out << YAML::EndMap; // Webserver } // HostMetrics if (hostMetrics_user_command != "") { out << YAML::Key << "HostMetrics" << YAML::Value << YAML::BeginMap; out << YAML::Key << "UserStringCommand" << YAML::Value << hostMetrics_user_command; out << YAML::Key << "ReportInterval" << YAML::Value << hostMetrics_interval; out << YAML::Key << "Channel" << YAML::Value << hostMetrics_channel; out << YAML::EndMap; // HostMetrics } // config if (has_config_overrides) { out << YAML::Key << "Config" << YAML::Value << YAML::BeginMap; if (has_configDisplayMode) { switch (configDisplayMode) { case meshtastic_Config_DisplayConfig_DisplayMode_TWOCOLOR: out << YAML::Key << "DisplayMode" << YAML::Value << "TWOCOLOR"; break; case meshtastic_Config_DisplayConfig_DisplayMode_INVERTED: out << YAML::Key << "DisplayMode" << YAML::Value << "INVERTED"; break; case meshtastic_Config_DisplayConfig_DisplayMode_COLOR: out << YAML::Key << "DisplayMode" << YAML::Value << "COLOR"; break; case meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT: out << YAML::Key << "DisplayMode" << YAML::Value << "DEFAULT"; break; } } if (has_statusMessage) { out << YAML::Key << "StatusMessage" << YAML::Value << statusMessage; } if (enable_UDP) { out << YAML::Key << "EnableUDP" << YAML::Value << true; } out << YAML::EndMap; // Config } // General out << YAML::Key << "General" << YAML::Value << YAML::BeginMap; if (config_directory != "") out << YAML::Key << "ConfigDirectory" << YAML::Value << config_directory; if (api_port != -1) out << YAML::Key << "TCPPort" << YAML::Value << api_port; if (mac_address_explicit) out << YAML::Key << "MACAddress" << YAML::Value << mac_address; if (mac_address_source != "") out << YAML::Key << "MACAddressSource" << YAML::Value << mac_address_source; if (available_directory != "") out << YAML::Key << "AvailableDirectory" << YAML::Value << available_directory; out << YAML::Key << "MaxMessageQueue" << YAML::Value << maxtophone; out << YAML::Key << "MaxNodes" << YAML::Value << MaxNodes; out << YAML::EndMap; // General return out.c_str(); } } portduino_config; ================================================ FILE: src/platform/portduino/SimRadio.cpp ================================================ #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); bool dropped = false; ErrorCode res = txQueue.enqueue(p, &dropped) ? ERRNO_OK : ERRNO_UNKNOWN; if (dropped) { txDrop++; } if (res != ERRNO_OK) { // we weren't able to queue it, so we must drop it to prevent leaks packetPool.release(p); return res; } // set (random) transmit delay to let others reconfigure their radio, // to avoid collisions and implement timing-based flooding LOG_DEBUG("Set random delay before tx"); setTransmitDelay(); return res; } void SimRadio::setTransmitDelay() { meshtastic_MeshPacket *p = txQueue.getFront(); // We want all sending/receiving to be done by our daemon thread. // We use a delay here because this packet might have been sent in response to a packet we just received. // So we want to make sure the other side has had a chance to reconfigure its radio. /* We assume if rx_snr = 0 and rx_rssi = 0, the packet was generated locally. * This assumption is valid because of the offset generated by the radio to account for the noise * floor. */ if (p->rx_snr == 0 && p->rx_rssi == 0) { startTransmitTimer(true); } else { // If there is a SNR, start a timer scaled based on that SNR. LOG_DEBUG("rx_snr found. hop_limit:%d rx_snr:%f", p->hop_limit, p->rx_snr); startTransmitTimerRebroadcast(p); } } void SimRadio::startTransmitTimer(bool withDelay) { // If we have work to do and the timer wasn't already scheduled, schedule it now if (!txQueue.empty()) { uint32_t delayMsec = !withDelay ? 1 : getTxDelayMsec(); // LOG_DEBUG("xmit timer %d", delay); notifyLater(delayMsec, TRANSMIT_DELAY_COMPLETED, false); } } void SimRadio::startTransmitTimerRebroadcast(meshtastic_MeshPacket *p) { // If we have work to do and the timer wasn't already scheduled, schedule it now if (!txQueue.empty()) { uint32_t delayMsec = getTxDelayMsecWeighted(p); // LOG_DEBUG("xmit timer %d", delay); notifyLater(delayMsec, TRANSMIT_DELAY_COMPLETED, false); } } void SimRadio::handleTransmitInterrupt() { // This can be null if we forced the device to enter standby mode. In that case // ignore the transmit interrupt if (sendingPacket) completeSending(); isReceiving = true; if (receivingPacket) // This happens when we don't consider something a collision if we weren't sending long enough handleReceiveInterrupt(); } void SimRadio::completeSending() { // We are careful to clear sending packet before calling printPacket because // that can take a long time auto p = sendingPacket; sendingPacket = NULL; 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"); } } /** 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 receivingPacket != nullptr; } bool SimRadio::isChannelActive() { return receivingPacket != nullptr; } /** Attempt to cancel a previously sent packet. Returns true if a packet was found we could cancel */ bool SimRadio::cancelSending(NodeNum from, PacketId id) { auto p = txQueue.remove(from, id); if (p) packetPool.release(p); // free the packet we just removed bool result = (p != NULL); LOG_DEBUG("cancelSending id=0x%x, removed=%d", id, result); return result; } /** Attempt to find a packet in the TxQueue. Returns true if the packet was found. */ bool SimRadio::findInTxQueue(NodeNum from, PacketId id) { return txQueue.find(from, id); } void SimRadio::onNotify(uint32_t notification) { switch (notification) { case ISR_TX: handleTransmitInterrupt(); // LOG_DEBUG("tx complete - starting timer"); startTransmitTimer(); break; case ISR_RX: handleReceiveInterrupt(); // LOG_DEBUG("rx complete - starting timer"); startTransmitTimer(); break; case TRANSMIT_DELAY_COMPLETED: if (receivingPacket) { // This happens when we had a timer pending and we started receiving handleReceiveInterrupt(); startTransmitTimer(); break; } LOG_DEBUG("delay done"); // If we are not currently in receive mode, then restart the random delay (this can happen if the main thread // has placed the unit into standby) FIXME, how will this work if the chipset is in sleep mode? if (!txQueue.empty()) { if (!canSendImmediately()) { // LOG_DEBUG("Currently Rx/Tx-ing: set random delay"); setTransmitDelay(); // currently Rx/Tx-ing: reset random delay } else { if (isChannelActive()) { // check if there is currently a LoRa packet on the channel // LOG_DEBUG("Channel is active: set random delay"); setTransmitDelay(); // reset random delay } else { // Send any outgoing packets we have ready meshtastic_MeshPacket *txp = txQueue.dequeue(); assert(txp); startSend(txp); // Packet has been sent, count it toward our TX airtime utilization. uint32_t xmitMsec = RadioInterface::getPacketTime(txp); airTime->logAirtime(TX_LOG, xmitMsec); notifyLater(xmitMsec, ISR_TX, false); // Model the time it is busy sending } } } else { // LOG_DEBUG("done with txqueue"); } break; default: assert(0); // We expected to receive a valid notification from the ISR } } /** start an immediate transmit */ void SimRadio::startSend(meshtastic_MeshPacket *txp) { printPacket("Start low level send", txp); isReceiving = false; size_t numbytes = beginSending(txp); meshtastic_MeshPacket *p = packetPool.allocCopy(*txp); perhapsDecode(p); meshtastic_Compressed c = meshtastic_Compressed_init_default; c.portnum = p->decoded.portnum; // LOG_DEBUG("Send back to simulator with portNum %d", p->decoded.portnum); if (p->decoded.payload.size <= sizeof(c.data.bytes)) { memcpy(&c.data.bytes, p->decoded.payload.bytes, p->decoded.payload.size); c.data.size = p->decoded.payload.size; } else { LOG_WARN("Payload size larger than compressed message allows! Send empty payload"); } p->decoded.payload.size = pb_encode_to_bytes(p->decoded.payload.bytes, sizeof(p->decoded.payload.bytes), &meshtastic_Compressed_msg, &c); p->decoded.portnum = meshtastic_PortNum_SIMULATOR_APP; service->sendQueueStatusToPhone(router->getQueueStatus(), 0, p->id); service->sendToPhone(p); // Sending back to simulator service->loop(); // Process the send immediately } // Simulates device received a packet via the LoRa chip void SimRadio::unpackAndReceive(meshtastic_MeshPacket &p) { // Simulator packet (=Compressed packet) is encapsulated in a MeshPacket, so need to unwrap first meshtastic_Compressed scratch; meshtastic_Compressed *decoded = NULL; if (p.which_payload_variant == meshtastic_MeshPacket_decoded_tag) { memset(&scratch, 0, sizeof(scratch)); p.decoded.payload.size = pb_decode_from_bytes(p.decoded.payload.bytes, p.decoded.payload.size, &meshtastic_Compressed_msg, &scratch); if (p.decoded.payload.size) { decoded = &scratch; // Extract the original payload and replace memcpy(&p.decoded.payload, &decoded->data, sizeof(decoded->data)); // Switch the port from PortNum_SIMULATOR_APP back to the original PortNum p.decoded.portnum = decoded->portnum; } else LOG_ERROR("Error decoding proto for simulator message!"); } // Let SimRadio receive as if it did via its LoRa chip startReceive(&p); } void SimRadio::startReceive(meshtastic_MeshPacket *p) { #ifdef USERPREFS_SIMRADIO_EMULATE_COLLISIONS if (isActivelyReceiving()) { LOG_WARN("Collision detected, dropping current and previous packet!"); rxBad++; airTime->logAirtime(RX_ALL_LOG, getPacketTime(receivingPacket, true)); packetPool.release(receivingPacket); receivingPacket = nullptr; return; } else if (sendingPacket) { uint32_t airtimeLeft = tillRun(millis()); if (airtimeLeft <= 0) { LOG_WARN("Transmitting packet was already done"); handleTransmitInterrupt(); // Finish sending first } else if ((interval - airtimeLeft) > preambleTimeMsec) { // Only if transmitting for longer than preamble there is a collision // (channel should actually be detected as active otherwise) LOG_WARN("Collision detected during transmission!"); return; } } isReceiving = true; receivingPacket = packetPool.allocCopy(*p); uint32_t airtimeMsec = getPacketTime(p, true); notifyLater(airtimeMsec, ISR_RX, false); // Model the time it is busy receiving #else isReceiving = true; receivingPacket = packetPool.allocCopy(*p); handleReceiveInterrupt(); // Simulate receiving the packet immediately startTransmitTimer(); #endif } 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() { if (receivingPacket == nullptr) { return; } if (!isReceiving) { LOG_DEBUG("*** WAS_ASSERT *** handleReceiveInterrupt called when not in receive mode"); return; } LOG_DEBUG("HANDLE RECEIVE INTERRUPT"); rxGood++; meshtastic_MeshPacket *mp = packetPool.allocCopy(*receivingPacket); // keep a copy in packetPool packetPool.release(receivingPacket); // release the original receivingPacket = nullptr; printPacket("Lora RX", mp); airTime->logAirtime(RX_LOG, RadioInterface::getPacketTime(mp, true)); 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; } /** * 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 SimRadio::getPacketTime(uint32_t pl, bool received) { float bandwidthHz = bw * 1000.0f; bool headDisable = false; // we currently always use the header float tSym = (1 << sf) / bandwidthHz; bool lowDataOptEn = tSym > 16e-3 ? true : false; // Needed if symbol time is >16ms 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; } ================================================ FILE: src/platform/portduino/SimRadio.h ================================================ #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 }; 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; /** Attempt to find a packet in the TxQueue. Returns true if the packet was found. */ virtual bool findInTxQueue(NodeNum from, PacketId id) override; /** * Start waiting to receive a message * * External functions can call this method to wake the device from sleep. */ virtual void startReceive(meshtastic_MeshPacket *p); meshtastic_QueueStatus getQueueStatus() override; // Convert Compressed_msg to normal msg and receive it void unpackAndReceive(meshtastic_MeshPacket &p); /** * Debugging counts */ uint32_t rxBad = 0, rxGood = 0, txGood = 0, txRelay = 0; uint16_t txDrop = 0; protected: /// are _trying_ to receive a packet currently (note - we might just be waiting for one) bool isReceiving = true; 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 startTransmitTimerRebroadcast(meshtastic_MeshPacket *p); void handleTransmitInterrupt(); void handleReceiveInterrupt(); 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); meshtastic_MeshPacket *receivingPacket = nullptr; // The packet we are currently receiving protected: /** Could we send right now (i.e. either not actively receiving or transmitting)? */ virtual bool canSendImmediately(); /** * If a send was in progress finish it and return the buffer to the pool */ void completeSending(); virtual uint32_t getPacketTime(uint32_t pl, bool received = false) override; }; extern SimRadio *simRadio; ================================================ FILE: src/platform/portduino/USBHal.h ================================================ #ifndef PI_HAL_LGPIO_H #define PI_HAL_LGPIO_H // include RadioLib #include "platform/portduino/PortduinoGlue.h" #include #include #include #include #include extern uint32_t rebootAtMsec; // include the library for Raspberry GPIO pins #define PI_RISING (PINEDIO_INT_MODE_RISING) #define PI_FALLING (PINEDIO_INT_MODE_FALLING) #define PI_INPUT (0) #define PI_OUTPUT (1) #define PI_LOW (0) #define PI_HIGH (1) #define CH341_PIN_CS (101) #define CH341_PIN_IRQ (0) // the HAL must inherit from the base RadioLibHal class // and implement all of its virtual methods class Ch341Hal : public RadioLibHal { public: // default constructor - initializes the base HAL and any needed private members explicit Ch341Hal(uint8_t spiChannel, const std::string &serial = "", uint32_t vid = 0x1A86, uint32_t pid = 0x5512, uint32_t spiSpeed = 2000000, uint8_t spiDevice = 0, uint8_t gpioDevice = 0) : RadioLibHal(PI_INPUT, PI_OUTPUT, PI_LOW, PI_HIGH, PI_RISING, PI_FALLING) { if (serial != "") { strncpy(pinedio.serial_number, serial.c_str(), 8); pinedio_set_option(&pinedio, PINEDIO_OPTION_SEARCH_SERIAL, 1); } // LOG_INFO("USB Serial: %s", pinedio.serial_number); // There is no vendor with 0x0 -> so check if (vid != 0x0) { pinedio_set_option(&pinedio, PINEDIO_OPTION_VID, vid); pinedio_set_option(&pinedio, PINEDIO_OPTION_PID, pid); } int32_t ret = pinedio_init(&pinedio, NULL); if (ret != 0) { std::string s = "Could not open SPI: "; throw std::runtime_error(s + std::to_string(ret)); } pinedio_set_option(&pinedio, PINEDIO_OPTION_AUTO_CS, 0); pinedio_set_pin_mode(&pinedio, 3, true); pinedio_set_pin_mode(&pinedio, 5, true); } ~Ch341Hal() { pinedio_deinit(&pinedio); } void getSerialString(char *_serial, size_t len) { len = len > 8 ? 8 : len; strncpy(_serial, pinedio.serial_number, len); } void getProductString(char *_product_string, size_t len) { len = len > 95 ? 95 : len; memcpy(_product_string, pinedio.product_string, len); } void init() override {} void term() override {} // GPIO-related methods (pinMode, digitalWrite etc.) should check // RADIOLIB_NC as an alias for non-connected pins void pinMode(uint32_t pin, uint32_t mode) override { if (checkError()) { return; } if (pin == RADIOLIB_NC) { return; } auto res = pinedio_set_pin_mode(&pinedio, pin, mode); if (res < 0 && rebootAtMsec == 0) { LOG_ERROR("USBHal pinMode: Could not set pin %u mode to %u: %d", pin, mode, res); } } void digitalWrite(uint32_t pin, uint32_t value) override { if (checkError()) { return; } if (pin == RADIOLIB_NC) { return; } auto res = pinedio_digital_write(&pinedio, pin, value); if (res < 0 && rebootAtMsec == 0) { LOG_ERROR("USBHal digitalWrite: Could not write pin %u: %d", pin, res); portduino_status.LoRa_in_error = true; } } uint32_t digitalRead(uint32_t pin) override { if (checkError()) { return 0; } if (pin == RADIOLIB_NC) { return 0; } auto res = pinedio_digital_read(&pinedio, pin); if (res < 0 && rebootAtMsec == 0) { LOG_ERROR("USBHal digitalRead: Could not read pin %u: %d", pin, res); portduino_status.LoRa_in_error = true; return 0; } return res; } void attachInterrupt(uint32_t interruptNum, void (*interruptCb)(void), uint32_t mode) override { if (checkError()) { return; } if (interruptNum == RADIOLIB_NC) { return; } // LOG_DEBUG("Attach interrupt to pin %d", interruptNum); pinedio_attach_interrupt(&this->pinedio, (pinedio_int_pin)interruptNum, (pinedio_int_mode)mode, interruptCb); } void detachInterrupt(uint32_t interruptNum) override { if (checkError()) { return; } if (interruptNum == RADIOLIB_NC) { return; } // LOG_DEBUG("Detach interrupt from pin %d", interruptNum); pinedio_deattach_interrupt(&this->pinedio, (pinedio_int_pin)interruptNum); } void delay(unsigned long ms) override { delayMicroseconds(ms * 1000); } void delayMicroseconds(unsigned long us) override { if (us == 0) { sched_yield(); return; } usleep(us); } void yield() override { sched_yield(); } unsigned long millis() override { struct timeval tv; gettimeofday(&tv, NULL); return (tv.tv_sec * 1000ULL) + (tv.tv_usec / 1000ULL); } unsigned long micros() override { struct timeval tv; gettimeofday(&tv, NULL); return (tv.tv_sec * 1000000ULL) + tv.tv_usec; } long pulseIn(uint32_t pin, uint32_t state, unsigned long timeout) override { std::cerr << "pulseIn for pin " << pin << "is not supported!" << std::endl; return 0; } void spiBegin() {} void spiBeginTransaction() {} void spiTransfer(uint8_t *out, size_t len, uint8_t *in) { if (checkError()) { return; } int32_t ret = pinedio_transceive(&this->pinedio, out, in, len); if (ret < 0) { std::cerr << "Could not perform SPI transfer: " << ret << std::endl; } } void spiEndTransaction() {} void spiEnd() {} bool checkError() { if (pinedio.in_error) { if (!has_warned) LOG_ERROR("USBHal: libch341 in_error detected"); portduino_status.LoRa_in_error = true; has_warned = true; return true; } has_warned = false; return false; } private: pinedio_inst pinedio = {0}; bool has_warned = false; }; #endif ================================================ FILE: src/platform/portduino/architecture.h ================================================ #pragma once #define ARCH_PORTDUINO 1 // // set HW_VENDOR // #define HW_VENDOR portduino_status.hardwareModel #ifndef HAS_BUTTON #define HAS_BUTTON 1 #endif #ifndef HAS_WIFI #define HAS_WIFI 1 #endif #ifndef HAS_RADIO #define HAS_RADIO 1 #endif #ifndef HAS_TELEMETRY #define HAS_TELEMETRY 1 #endif #ifndef HAS_SENSOR #define HAS_SENSOR 1 #endif #ifndef HAS_TRACKBALL #define HAS_TRACKBALL 1 #define TB_DOWN (uint8_t) portduino_config.tbDownPin.pin #define TB_UP (uint8_t) portduino_config.tbUpPin.pin #define TB_LEFT (uint8_t) portduino_config.tbLeftPin.pin #define TB_RIGHT (uint8_t) portduino_config.tbRightPin.pin #define TB_PRESS (uint8_t) portduino_config.tbPressPin.pin #endif ================================================ FILE: src/platform/rp2xx0/architecture.h ================================================ #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 #elif defined(PRIVATE_HW) #define HW_VENDOR meshtastic_HardwareModel_PRIVATE_HW #endif // Detect if running in ISR context (ARM Cortex-M33 / RISC-V) #define xPortInIsrContext() (__get_current_exception() == 0 ? pdFALSE : pdTRUE) ================================================ FILE: src/platform/rp2xx0/hardware_rosc/include/hardware/rosc.h ================================================ /* * 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 ================================================ FILE: src/platform/rp2xx0/hardware_rosc/rosc.c ================================================ /* * 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)) ; } ================================================ FILE: src/platform/rp2xx0/main-rp2xx0.cpp ================================================ #include "configuration.h" #include "hardware/xosc.h" #include #include #include #include #ifdef __PLAT_RP2040__ #include 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(); } #else void cpuDeepSleep(uint32_t msecs) { /* Set RP2040 in dormant mode. Will not wake up. */ xosc_dormant(); } #endif void setBluetoothEnable(bool enable) { // not needed } void updateBatteryLevel(uint8_t level) { // not needed } void getMacAddr(uint8_t *dmac) { pico_unique_board_id_t src; pico_get_unique_board_id(&src); dmac[5] = src.id[7]; dmac[4] = src.id[6]; dmac[3] = src.id[5]; dmac[2] = src.id[4]; dmac[1] = src.id[3]; dmac[0] = src.id[2]; } void 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 ================================================ FILE: src/platform/rp2xx0/pico_sleep/include/pico/sleep.h ================================================ /* * 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 ================================================ FILE: src/platform/rp2xx0/pico_sleep/sleep.c ================================================ /* * 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); } ================================================ FILE: src/platform/stm32wl/LittleFS.cpp ================================================ /* * The MIT License (MIT) * * Copyright (c) 2019 hathach for Adafruit Industries * * 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 "LittleFS.h" #include "stm32wlxx_hal_flash.h" /********************************************************************************************************************** * Macro definitions **********************************************************************************************************************/ /** This macro is used to suppress compiler messages about a parameter not being used in a function. */ #define LFS_UNUSED(p) (void)((p)) #define STM32WL_PAGE_SIZE (FLASH_PAGE_SIZE) #define STM32WL_PAGE_COUNT (FLASH_PAGE_NB) #define STM32WL_FLASH_BASE (FLASH_BASE) /* * FLASH_SIZE from stm32wle5xx.h will read the actual FLASH size from the chip. * FLASH_END_ADDR is calculated from FLASH_SIZE. * Use the last 28 KiB of the FLASH */ #define LFS_FLASH_TOTAL_SIZE (14 * 2048) /* needs to be a multiple of LFS_BLOCK_SIZE */ #define LFS_BLOCK_SIZE (2048) #define LFS_FLASH_ADDR_END (FLASH_END_ADDR) #define LFS_FLASH_ADDR_BASE (LFS_FLASH_ADDR_END - LFS_FLASH_TOTAL_SIZE + 1) #if !CFG_DEBUG #define _LFS_DBG(fmt, ...) #else #define _LFS_DBG(fmt, ...) printf("%s:%d (%s): " fmt "\n", __FILE__, __LINE__, __func__, __VA_ARGS__) #endif //--------------------------------------------------------------------+ // LFS Disk IO //--------------------------------------------------------------------+ static int _internal_flash_read(const struct lfs_config *c, lfs_block_t block, lfs_off_t off, void *buffer, lfs_size_t size) { LFS_UNUSED(c); if (!buffer || !size) { _LFS_DBG("%s Invalid parameter!\r\n", __func__); return LFS_ERR_INVAL; } lfs_block_t address = LFS_FLASH_ADDR_BASE + (block * STM32WL_PAGE_SIZE + off); memcpy(buffer, (void *)address, size); return LFS_ERR_OK; } // Program a region in a block. The block must have previously // been erased. Negative error codes are propogated to the user. // May return LFS_ERR_CORRUPT if the block should be considered bad. static int _internal_flash_prog(const struct lfs_config *c, lfs_block_t block, lfs_off_t off, const void *buffer, lfs_size_t size) { lfs_block_t address = LFS_FLASH_ADDR_BASE + (block * STM32WL_PAGE_SIZE + off); HAL_StatusTypeDef hal_rc = HAL_OK; uint32_t dw_count = size / 8; uint64_t *bufp = (uint64_t *)buffer; LFS_UNUSED(c); _LFS_DBG("Programming %d bytes/%d doublewords at address 0x%08x/block %d, offset %d.", size, dw_count, address, block, off); if (HAL_FLASH_Unlock() != HAL_OK) { return LFS_ERR_IO; } for (uint32_t i = 0; i < dw_count; i++) { if ((address < LFS_FLASH_ADDR_BASE) || (address > LFS_FLASH_ADDR_END)) { _LFS_DBG("Wanted to program out of bound of FLASH: 0x%08x.\n", address); HAL_FLASH_Lock(); return LFS_ERR_INVAL; } hal_rc = HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, address, *bufp); if (hal_rc != HAL_OK) { /* Error occurred while writing data in Flash memory. * User can add here some code to deal with this error. */ _LFS_DBG("Program error at (0x%08x), 0x%X, error: 0x%08x\n", address, hal_rc, HAL_FLASH_GetError()); } address += 8; bufp += 1; } if (HAL_FLASH_Lock() != HAL_OK) { return LFS_ERR_IO; } return hal_rc == HAL_OK ? LFS_ERR_OK : LFS_ERR_IO; // If HAL_OK, return LFS_ERR_OK, else return LFS_ERR_IO } // Erase a block. A block must be erased before being programmed. // The state of an erased block is undefined. Negative error codes // are propogated to the user. // May return LFS_ERR_CORRUPT if the block should be considered bad. static int _internal_flash_erase(const struct lfs_config *c, lfs_block_t block) { lfs_block_t address = LFS_FLASH_ADDR_BASE + (block * STM32WL_PAGE_SIZE); HAL_StatusTypeDef hal_rc; FLASH_EraseInitTypeDef EraseInitStruct = {.TypeErase = FLASH_TYPEERASE_PAGES, .Page = 0, .NbPages = 1}; uint32_t PAGEError = 0; LFS_UNUSED(c); if ((address < LFS_FLASH_ADDR_BASE) || (address > LFS_FLASH_ADDR_END)) { _LFS_DBG("Wanted to erase out of bound of FLASH: 0x%08x.\n", address); return LFS_ERR_INVAL; } /* calculate the absolute page, i.e. what the ST wants */ EraseInitStruct.Page = (address - STM32WL_FLASH_BASE) / STM32WL_PAGE_SIZE; _LFS_DBG("Erasing block %d at 0x%08x... ", block, address); HAL_FLASH_Unlock(); hal_rc = HAL_FLASHEx_Erase(&EraseInitStruct, &PAGEError); HAL_FLASH_Lock(); return hal_rc == HAL_OK ? LFS_ERR_OK : LFS_ERR_IO; // If HAL_OK, return LFS_ERR_OK, else return LFS_ERR_IO } // Sync the state of the underlying block device. Negative error codes // are propogated to the user. static int _internal_flash_sync(const struct lfs_config *c) { LFS_UNUSED(c); // write function performs no caching. No need for sync. return LFS_ERR_OK; } static struct lfs_config _InternalFSConfig = {.context = NULL, .read = _internal_flash_read, .prog = _internal_flash_prog, .erase = _internal_flash_erase, .sync = _internal_flash_sync, .read_size = LFS_BLOCK_SIZE, .prog_size = LFS_BLOCK_SIZE, .block_size = LFS_BLOCK_SIZE, .block_count = LFS_FLASH_TOTAL_SIZE / LFS_BLOCK_SIZE, .lookahead = 128, .read_buffer = NULL, .prog_buffer = NULL, .lookahead_buffer = NULL, .file_buffer = NULL}; LittleFS InternalFS; //--------------------------------------------------------------------+ // //--------------------------------------------------------------------+ LittleFS::LittleFS(void) : STM32_LittleFS(&_InternalFSConfig) {} bool LittleFS::begin(void) { if (FLASH_BASE >= LFS_FLASH_ADDR_BASE) { /* There is not enough space on this device for a filesystem. */ return false; } // failed to mount, erase all pages then format and mount again if (!STM32_LittleFS::begin()) { // Erase all pages of internal flash region for Filesystem. for (uint32_t addr = LFS_FLASH_ADDR_BASE; addr < (LFS_FLASH_ADDR_END + 1); addr += STM32WL_PAGE_SIZE) { _internal_flash_erase(&_InternalFSConfig, (addr - LFS_FLASH_ADDR_BASE) / STM32WL_PAGE_SIZE); } // lfs format this->format(); // mount again if still failed, give up if (!STM32_LittleFS::begin()) return false; } return true; } ================================================ FILE: src/platform/stm32wl/LittleFS.h ================================================ /* * The MIT License (MIT) * * Copyright (c) 2019 hathach for Adafruit Industries * * 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 INTERNALFILESYSTEM_H_ #define INTERNALFILESYSTEM_H_ #include "STM32_LittleFS.h" class LittleFS : public STM32_LittleFS { public: LittleFS(void); // overwrite to also perform low level format (sector erase of whole flash region) bool begin(void); }; extern LittleFS InternalFS; #endif /* INTERNALFILESYSTEM_H_ */ ================================================ FILE: src/platform/stm32wl/STM32_LittleFS.cpp ================================================ /* * The MIT License (MIT) * * Copyright (c) 2019 Ha Thach for Adafruit Industries * * 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 "STM32_LittleFS.h" #include #include #define memclr(buffer, size) memset(buffer, 0, size) #define varclr(_var) memclr(_var, sizeof(*(_var))) using namespace STM32_LittleFS_Namespace; //--------------------------------------------------------------------+ // Implementation //--------------------------------------------------------------------+ STM32_LittleFS::STM32_LittleFS(void) : STM32_LittleFS(NULL) {} STM32_LittleFS::STM32_LittleFS(struct lfs_config *cfg) { varclr(&_lfs); _lfs_cfg = cfg; _mounted = false; } STM32_LittleFS::~STM32_LittleFS() {} // Initialize and mount the file system // Return true if mounted successfully else probably corrupted. // User should format the disk and try again bool STM32_LittleFS::begin(struct lfs_config *cfg) { _lockFS(); bool ret; // not a loop, just an quick way to short-circuit on error do { if (_mounted) { ret = true; break; } if (cfg) { _lfs_cfg = cfg; } if (nullptr == _lfs_cfg) { ret = false; break; } // actually attempt to mount, and log error if one occurs int err = lfs_mount(&_lfs, _lfs_cfg); PRINT_LFS_ERR(err); _mounted = (err == LFS_ERR_OK); ret = _mounted; } while (0); _unlockFS(); return ret; } // Tear down and unmount file system void STM32_LittleFS::end(void) { _lockFS(); if (_mounted) { _mounted = false; int err = lfs_unmount(&_lfs); PRINT_LFS_ERR(err); (void)err; } _unlockFS(); } bool STM32_LittleFS::format(void) { _lockFS(); int err = LFS_ERR_OK; bool attemptMount = _mounted; // not a loop, just an quick way to short-circuit on error do { // if already mounted: umount first -> format -> remount if (_mounted) { _mounted = false; err = lfs_unmount(&_lfs); if (LFS_ERR_OK != err) { PRINT_LFS_ERR(err); break; } } err = lfs_format(&_lfs, _lfs_cfg); if (LFS_ERR_OK != err) { PRINT_LFS_ERR(err); break; } if (attemptMount) { err = lfs_mount(&_lfs, _lfs_cfg); if (LFS_ERR_OK != err) { PRINT_LFS_ERR(err); break; } _mounted = true; } // success! } while (0); _unlockFS(); return LFS_ERR_OK == err; } // Open a file or folder STM32_LittleFS_Namespace::File STM32_LittleFS::open(char const *filepath, uint8_t mode) { // No lock is required here ... the File() object will synchronize with the mutex provided return STM32_LittleFS_Namespace::File(filepath, mode, *this); } // Check if file or folder exists bool STM32_LittleFS::exists(char const *filepath) { struct lfs_info info; _lockFS(); bool ret = (0 == lfs_stat(&_lfs, filepath, &info)); _unlockFS(); return ret; } // Create a directory, create intermediate parent if needed bool STM32_LittleFS::mkdir(char const *filepath) { bool ret = true; const char *slash = filepath; if (slash[0] == '/') slash++; // skip root '/' _lockFS(); // make intermediate parent directory(ies) while (NULL != (slash = strchr(slash, '/'))) { char parent[slash - filepath + 1] = {0}; memcpy(parent, filepath, slash - filepath); int rc = lfs_mkdir(&_lfs, parent); if (rc != LFS_ERR_OK && rc != LFS_ERR_EXIST) { PRINT_LFS_ERR(rc); ret = false; break; } slash++; } // make the final requested directory if (ret) { int rc = lfs_mkdir(&_lfs, filepath); if (rc != LFS_ERR_OK && rc != LFS_ERR_EXIST) { PRINT_LFS_ERR(rc); ret = false; } } _unlockFS(); return ret; } // Remove a file bool STM32_LittleFS::remove(char const *filepath) { _lockFS(); int err = lfs_remove(&_lfs, filepath); PRINT_LFS_ERR(err); _unlockFS(); return LFS_ERR_OK == err; } // Rename a file bool STM32_LittleFS::rename(char const *oldfilepath, char const *newfilepath) { _lockFS(); int err = lfs_rename(&_lfs, oldfilepath, newfilepath); PRINT_LFS_ERR(err); _unlockFS(); return LFS_ERR_OK == err; } // Remove a folder bool STM32_LittleFS::rmdir(char const *filepath) { _lockFS(); int err = lfs_remove(&_lfs, filepath); PRINT_LFS_ERR(err); _unlockFS(); return LFS_ERR_OK == err; } // Remove a folder recursively bool STM32_LittleFS::rmdir_r(char const *filepath) { /* lfs is modified to remove non-empty folder, According to below issue, comment these 2 line won't corrupt filesystem at least when using LFS v1. If moving to LFS v2, see tracked issue to see if issues (such as the orphans in threaded linked list) are resolved. https://github.com/ARMmbed/littlefs/issues/43 */ _lockFS(); int err = lfs_remove(&_lfs, filepath); PRINT_LFS_ERR(err); _unlockFS(); return LFS_ERR_OK == err; } //------------- Debug -------------// #if CFG_DEBUG const char *dbg_strerr_lfs(int32_t err) { switch (err) { case LFS_ERR_OK: return "LFS_ERR_OK"; case LFS_ERR_IO: return "LFS_ERR_IO"; case LFS_ERR_CORRUPT: return "LFS_ERR_CORRUPT"; case LFS_ERR_NOENT: return "LFS_ERR_NOENT"; case LFS_ERR_EXIST: return "LFS_ERR_EXIST"; case LFS_ERR_NOTDIR: return "LFS_ERR_NOTDIR"; case LFS_ERR_ISDIR: return "LFS_ERR_ISDIR"; case LFS_ERR_NOTEMPTY: return "LFS_ERR_NOTEMPTY"; case LFS_ERR_BADF: return "LFS_ERR_BADF"; case LFS_ERR_INVAL: return "LFS_ERR_INVAL"; case LFS_ERR_NOSPC: return "LFS_ERR_NOSPC"; case LFS_ERR_NOMEM: return "LFS_ERR_NOMEM"; default: static char errcode[10]; sprintf(errcode, "%ld", err); return errcode; } return NULL; } #endif ================================================ FILE: src/platform/stm32wl/STM32_LittleFS.h ================================================ /* * The MIT License (MIT) * * Copyright (c) 2019 Ha Thach for Adafruit Industries * * 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 STM32_LITTLEFS_H_ #define STM32_LITTLEFS_H_ #include // Internal Flash uses ARM Little FileSystem // https://github.com/ARMmbed/littlefs #include "../../freertosinc.h" // tied to FreeRTOS for serialization #include "STM32_LittleFS_File.h" #include "littlefs/lfs.h" class STM32_LittleFS { public: STM32_LittleFS(void); explicit STM32_LittleFS(struct lfs_config *cfg); virtual ~STM32_LittleFS(); bool begin(struct lfs_config *cfg = NULL); void end(void); // Open the specified file/directory with the supplied mode (e.g. read or // write, etc). Returns a File object for interacting with the file. // Note that currently only one file can be open at a time. STM32_LittleFS_Namespace::File open(char const *filename, uint8_t mode = STM32_LittleFS_Namespace::FILE_O_READ); // Methods to determine if the requested file path exists. bool exists(char const *filepath); // Create the requested directory hierarchy--if intermediate directories // do not exist they will be created. bool mkdir(char const *filepath); // Delete the file. bool remove(char const *filepath); // Rename the file. bool rename(char const *oldfilepath, char const *newfilepath); // Delete a folder (must be empty) bool rmdir(char const *filepath); // Delete a folder (recursively) bool rmdir_r(char const *filepath); // format file system bool format(void); /*------------------------------------------------------------------*/ /* INTERNAL USAGE ONLY * Although declare as public, it is meant to be invoked by internal * code. User should not call these directly *------------------------------------------------------------------*/ lfs_t *_getFS(void) { return &_lfs; } void _lockFS(void) { /* no-op */ } void _unlockFS(void) { /* no-op */ } protected: bool _mounted; struct lfs_config *_lfs_cfg; lfs_t _lfs; }; #if !CFG_DEBUG #define VERIFY_LFS(...) _GET_3RD_ARG(__VA_ARGS__, VERIFY_ERR_2ARGS, VERIFY_ERR_1ARGS)(__VA_ARGS__, NULL) #define PRINT_LFS_ERR(_err) #else #define VERIFY_LFS(...) _GET_3RD_ARG(__VA_ARGS__, VERIFY_ERR_2ARGS, VERIFY_ERR_1ARGS)(__VA_ARGS__, dbg_strerr_lfs) #define PRINT_LFS_ERR(_err) \ do { \ if (_err) { \ printf("%s:%d, LFS error: %d\n", __FILE__, __LINE__, _err); \ } \ } while (0) // LFS_ERR are of type int, VERIFY_MESS expects long_int const char *dbg_strerr_lfs(int32_t err); #endif #endif /* STM32_LITTLEFS_H_ */ ================================================ FILE: src/platform/stm32wl/STM32_LittleFS_File.cpp ================================================ /* * The MIT License (MIT) * * Copyright (c) 2019 Ha Thach for Adafruit Industries * * 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 "STM32_LittleFS.h" #include #define rtos_malloc malloc #define rtos_free free //--------------------------------------------------------------------+ // MACRO TYPEDEF CONSTANT ENUM DECLARATION //--------------------------------------------------------------------+ using namespace STM32_LittleFS_Namespace; File::File(STM32_LittleFS &fs) { _fs = &fs; _is_dir = false; _name[0] = 0; _name[LFS_NAME_MAX] = 0; _dir_path = NULL; _dir = NULL; _file = NULL; } File::File(char const *filename, uint8_t mode, STM32_LittleFS &fs) : File(fs) { // public constructor calls public API open(), which will obtain the mutex this->open(filename, mode); } bool File::_open_file(char const *filepath, uint8_t mode) { int flags = (mode == FILE_O_READ) ? LFS_O_RDONLY : (mode == FILE_O_WRITE) ? (LFS_O_RDWR | LFS_O_CREAT) : 0; if (flags) { _file = (lfs_file_t *)rtos_malloc(sizeof(lfs_file_t)); if (!_file) return false; int rc = lfs_file_open(_fs->_getFS(), _file, filepath, flags); if (rc) { // failed to open PRINT_LFS_ERR(rc); // free memory rtos_free(_file); _file = NULL; return false; } // move to end of file if (mode == FILE_O_WRITE) lfs_file_seek(_fs->_getFS(), _file, 0, LFS_SEEK_END); _is_dir = false; } return true; } bool File::_open_dir(char const *filepath) { _dir = (lfs_dir_t *)rtos_malloc(sizeof(lfs_dir_t)); if (!_dir) return false; int rc = lfs_dir_open(_fs->_getFS(), _dir, filepath); if (rc) { // failed to open PRINT_LFS_ERR(rc); // free memory rtos_free(_dir); _dir = NULL; return false; } _is_dir = true; _dir_path = (char *)rtos_malloc(strlen(filepath) + 1); strcpy(_dir_path, filepath); return true; } bool File::open(char const *filepath, uint8_t mode) { bool ret = false; _fs->_lockFS(); ret = this->_open(filepath, mode); _fs->_unlockFS(); return ret; } bool File::_open(char const *filepath, uint8_t mode) { bool ret = false; // close if currently opened if (this->isOpen()) _close(); struct lfs_info info; int rc = lfs_stat(_fs->_getFS(), filepath, &info); if (LFS_ERR_OK == rc) { // file existed, open file or directory accordingly ret = (info.type == LFS_TYPE_REG) ? _open_file(filepath, mode) : _open_dir(filepath); } else if (LFS_ERR_NOENT == rc) { // file not existed, only proceed with FILE_O_WRITE mode if (mode == FILE_O_WRITE) ret = _open_file(filepath, mode); } else { PRINT_LFS_ERR(rc); } // save bare file name if (ret) { char const *splash = strrchr(filepath, '/'); strncpy(_name, splash ? (splash + 1) : filepath, LFS_NAME_MAX); } return ret; } size_t File::write(uint8_t ch) { return write(&ch, 1); } size_t File::write(uint8_t const *buf, size_t size) { lfs_ssize_t wrcount = 0; _fs->_lockFS(); if (!this->_is_dir) { wrcount = lfs_file_write(_fs->_getFS(), _file, buf, size); if (wrcount < 0) { wrcount = 0; } } _fs->_unlockFS(); return wrcount; } int File::read(void) { // this thin wrapper relies on called function to synchronize int ret = -1; uint8_t ch; if (read(&ch, 1) > 0) { ret = static_cast(ch); } return ret; } int File::read(void *buf, uint16_t nbyte) { int ret = 0; _fs->_lockFS(); if (!this->_is_dir) { ret = lfs_file_read(_fs->_getFS(), _file, buf, nbyte); } _fs->_unlockFS(); return ret; } int File::peek(void) { int ret = -1; _fs->_lockFS(); if (!this->_is_dir) { uint32_t pos = lfs_file_tell(_fs->_getFS(), _file); uint8_t ch = 0; if (lfs_file_read(_fs->_getFS(), _file, &ch, 1) > 0) { ret = static_cast(ch); } (void)lfs_file_seek(_fs->_getFS(), _file, pos, LFS_SEEK_SET); } _fs->_unlockFS(); return ret; } int File::available(void) { int ret = 0; _fs->_lockFS(); if (!this->_is_dir) { uint32_t file_size = lfs_file_size(_fs->_getFS(), _file); uint32_t pos = lfs_file_tell(_fs->_getFS(), _file); ret = file_size - pos; } _fs->_unlockFS(); return ret; } bool File::seek(uint32_t pos) { bool ret = false; _fs->_lockFS(); if (!this->_is_dir) { ret = lfs_file_seek(_fs->_getFS(), _file, pos, LFS_SEEK_SET) >= 0; } _fs->_unlockFS(); return ret; } uint32_t File::position(void) { uint32_t ret = 0; _fs->_lockFS(); if (!this->_is_dir) { ret = lfs_file_tell(_fs->_getFS(), _file); } _fs->_unlockFS(); return ret; } uint32_t File::size(void) { uint32_t ret = 0; _fs->_lockFS(); if (!this->_is_dir) { ret = lfs_file_size(_fs->_getFS(), _file); } _fs->_unlockFS(); return ret; } bool File::truncate(uint32_t pos) { int32_t ret = LFS_ERR_ISDIR; _fs->_lockFS(); if (!this->_is_dir) { ret = lfs_file_truncate(_fs->_getFS(), _file, pos); } _fs->_unlockFS(); return (ret == 0); } bool File::truncate(void) { int32_t ret = LFS_ERR_ISDIR; _fs->_lockFS(); if (!this->_is_dir) { uint32_t pos = lfs_file_tell(_fs->_getFS(), _file); ret = lfs_file_truncate(_fs->_getFS(), _file, pos); } _fs->_unlockFS(); return (ret == 0); } void File::flush(void) { _fs->_lockFS(); if (!this->_is_dir) { lfs_file_sync(_fs->_getFS(), _file); } _fs->_unlockFS(); return; } void File::close(void) { _fs->_lockFS(); this->_close(); _fs->_unlockFS(); } void File::_close(void) { if (this->isOpen()) { if (this->_is_dir) { lfs_dir_close(_fs->_getFS(), _dir); rtos_free(_dir); _dir = NULL; if (this->_dir_path) rtos_free(_dir_path); _dir_path = NULL; } else { lfs_file_close(this->_fs->_getFS(), _file); rtos_free(_file); _file = NULL; } } } File::operator bool(void) { return isOpen(); } bool File::isOpen(void) { return (_file != NULL) || (_dir != NULL); } // WARNING -- although marked as `const`, the values pointed // to may change. For example, if the same File // object has `open()` called with a different // file or directory name, this same pointer will // suddenly (unexpectedly?) have different values. char const *File::name(void) { return this->_name; } bool File::isDirectory(void) { return this->_is_dir; } File File::openNextFile(uint8_t mode) { _fs->_lockFS(); File ret(*_fs); if (this->_is_dir) { struct lfs_info info; int rc; // lfs_dir_read returns 0 when reaching end of directory, 1 if found an entry // Skip the "." and ".." entries ... do { rc = lfs_dir_read(_fs->_getFS(), _dir, &info); } while (rc == 1 && (!strcmp(".", info.name) || !strcmp("..", info.name))); if (rc == 1) { // string cat name with current folder char filepath[strlen(_dir_path) + 1 + strlen(info.name) + 1]; // potential for significant stack usage strcpy(filepath, _dir_path); if (!(_dir_path[0] == '/' && _dir_path[1] == 0)) strcat(filepath, "/"); // only add '/' if cwd is not root strcat(filepath, info.name); (void)ret._open(filepath, mode); // return value is ignored ... caller is expected to check isOpened() } else if (rc < 0) { PRINT_LFS_ERR(rc); } } _fs->_unlockFS(); return ret; } void File::rewindDirectory(void) { _fs->_lockFS(); if (this->_is_dir) { lfs_dir_rewind(_fs->_getFS(), _dir); } _fs->_unlockFS(); } ================================================ FILE: src/platform/stm32wl/STM32_LittleFS_File.h ================================================ /* * The MIT License (MIT) * * Copyright (c) 2019 Ha Thach for Adafruit Industries * * 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 STM32_LITTLEFS_FILE_H_ #define STM32_LITTLEFS_FILE_H_ #include "littlefs/lfs.h" // Forward declaration class STM32_LittleFS; namespace STM32_LittleFS_Namespace { // avoid conflict with other FileSystem FILE_READ/FILE_WRITE enum { FILE_O_READ = 0, FILE_O_WRITE = 1, }; class File : public Stream { public: explicit File(STM32_LittleFS &fs); File(char const *filename, uint8_t mode, STM32_LittleFS &fs); public: bool open(char const *filename, uint8_t mode); //------------- Stream API -------------// virtual size_t write(uint8_t ch); virtual size_t write(uint8_t const *buf, size_t size); size_t write(const char *str) { if (str == NULL) return 0; return write((const uint8_t *)str, strlen(str)); } size_t write(const char *buffer, size_t size) { return write((const uint8_t *)buffer, size); } virtual int read(void); int read(void *buf, uint16_t nbyte); virtual int peek(void); virtual int available(void); virtual void flush(void); bool seek(uint32_t pos); uint32_t position(void); uint32_t size(void); bool truncate(uint32_t pos); bool truncate(void); void close(void); operator bool(void); bool isOpen(void); char const *name(void); bool isDirectory(void); File openNextFile(uint8_t mode = FILE_O_READ); void rewindDirectory(void); private: STM32_LittleFS *_fs; bool _is_dir; union { lfs_file_t *_file; lfs_dir_t *_dir; }; char *_dir_path; char _name[LFS_NAME_MAX + 1]; bool _open(char const *filepath, uint8_t mode); bool _open_file(char const *filepath, uint8_t mode); bool _open_dir(char const *filepath); void _close(void); }; } // namespace STM32_LittleFS_Namespace #endif /* STM32_LITTLEFS_FILE_H_ */ ================================================ FILE: src/platform/stm32wl/architecture.h ================================================ #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 #ifndef HAS_WIRE #define HAS_WIRE 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 #if !defined(DEBUG_MUTE) && !defined(PIO_FRAMEWORK_ARDUINO_NANOLIB_FLOAT_PRINTF) #error \ "You MUST enable PIO_FRAMEWORK_ARDUINO_NANOLIB_FLOAT_PRINTF if debug prints are enabled. printf will print uninitialized garbage instead of floats." #endif ================================================ FILE: src/platform/stm32wl/littlefs/lfs.c ================================================ /* * The little filesystem * * Copyright (c) 2017, Arm Limited. All rights reserved. * SPDX-License-Identifier: BSD-3-Clause */ #include "lfs.h" #include "lfs_util.h" #include /// Caching block device operations /// static int lfs_cache_read(lfs_t *lfs, lfs_cache_t *rcache, const lfs_cache_t *pcache, lfs_block_t block, lfs_off_t off, void *buffer, lfs_size_t size) { uint8_t *data = buffer; LFS_ASSERT(block < lfs->cfg->block_count); while (size > 0) { if (pcache && block == pcache->block && off >= pcache->off && off < pcache->off + lfs->cfg->prog_size) { // is already in pcache? lfs_size_t diff = lfs_min(size, lfs->cfg->prog_size - (off - pcache->off)); memcpy(data, &pcache->buffer[off - pcache->off], diff); data += diff; off += diff; size -= diff; continue; } if (block == rcache->block && off >= rcache->off && off < rcache->off + lfs->cfg->read_size) { // is already in rcache? lfs_size_t diff = lfs_min(size, lfs->cfg->read_size - (off - rcache->off)); memcpy(data, &rcache->buffer[off - rcache->off], diff); data += diff; off += diff; size -= diff; continue; } if (off % lfs->cfg->read_size == 0 && size >= lfs->cfg->read_size) { // bypass cache? lfs_size_t diff = size - (size % lfs->cfg->read_size); int err = lfs->cfg->read(lfs->cfg, block, off, data, diff); if (err) { return err; } data += diff; off += diff; size -= diff; continue; } // load to cache, first condition can no longer fail rcache->block = block; rcache->off = off - (off % lfs->cfg->read_size); int err = lfs->cfg->read(lfs->cfg, rcache->block, rcache->off, rcache->buffer, lfs->cfg->read_size); if (err) { return err; } } return 0; } static int lfs_cache_cmp(lfs_t *lfs, lfs_cache_t *rcache, const lfs_cache_t *pcache, lfs_block_t block, lfs_off_t off, const void *buffer, lfs_size_t size) { const uint8_t *data = buffer; for (lfs_off_t i = 0; i < size; i++) { uint8_t c; int err = lfs_cache_read(lfs, rcache, pcache, block, off + i, &c, 1); if (err) { return err; } if (c != data[i]) { return false; } } return true; } static int lfs_cache_crc(lfs_t *lfs, lfs_cache_t *rcache, const lfs_cache_t *pcache, lfs_block_t block, lfs_off_t off, lfs_size_t size, uint32_t *crc) { for (lfs_off_t i = 0; i < size; i++) { uint8_t c; int err = lfs_cache_read(lfs, rcache, pcache, block, off + i, &c, 1); if (err) { return err; } lfs_crc(crc, &c, 1); } return 0; } static inline void lfs_cache_drop(lfs_t *lfs, lfs_cache_t *rcache) { // do not zero, cheaper if cache is readonly or only going to be // written with identical data (during relocates) (void)lfs; rcache->block = 0xffffffff; } static inline void lfs_cache_zero(lfs_t *lfs, lfs_cache_t *pcache) { // zero to avoid information leak memset(pcache->buffer, 0xff, lfs->cfg->prog_size); pcache->block = 0xffffffff; } static int lfs_cache_flush(lfs_t *lfs, lfs_cache_t *pcache, lfs_cache_t *rcache) { if (pcache->block != 0xffffffff) { int err = lfs->cfg->prog(lfs->cfg, pcache->block, pcache->off, pcache->buffer, lfs->cfg->prog_size); if (err) { return err; } if (rcache) { int res = lfs_cache_cmp(lfs, rcache, NULL, pcache->block, pcache->off, pcache->buffer, lfs->cfg->prog_size); if (res < 0) { return res; } if (!res) { return LFS_ERR_CORRUPT; } } lfs_cache_zero(lfs, pcache); } return 0; } static int lfs_cache_prog(lfs_t *lfs, lfs_cache_t *pcache, lfs_cache_t *rcache, lfs_block_t block, lfs_off_t off, const void *buffer, lfs_size_t size) { const uint8_t *data = buffer; LFS_ASSERT(block < lfs->cfg->block_count); while (size > 0) { if (block == pcache->block && off >= pcache->off && off < pcache->off + lfs->cfg->prog_size) { // is already in pcache? lfs_size_t diff = lfs_min(size, lfs->cfg->prog_size - (off - pcache->off)); memcpy(&pcache->buffer[off - pcache->off], data, diff); data += diff; off += diff; size -= diff; if (off % lfs->cfg->prog_size == 0) { // eagerly flush out pcache if we fill up int err = lfs_cache_flush(lfs, pcache, rcache); if (err) { return err; } } continue; } // pcache must have been flushed, either by programming and // entire block or manually flushing the pcache LFS_ASSERT(pcache->block == 0xffffffff); if (off % lfs->cfg->prog_size == 0 && size >= lfs->cfg->prog_size) { // bypass pcache? lfs_size_t diff = size - (size % lfs->cfg->prog_size); int err = lfs->cfg->prog(lfs->cfg, block, off, data, diff); if (err) { return err; } if (rcache) { int res = lfs_cache_cmp(lfs, rcache, NULL, block, off, data, diff); if (res < 0) { return res; } if (!res) { return LFS_ERR_CORRUPT; } } data += diff; off += diff; size -= diff; continue; } // prepare pcache, first condition can no longer fail pcache->block = block; pcache->off = off - (off % lfs->cfg->prog_size); } return 0; } /// General lfs block device operations /// static int lfs_bd_read(lfs_t *lfs, lfs_block_t block, lfs_off_t off, void *buffer, lfs_size_t size) { // if we ever do more than writes to alternating pairs, // this may need to consider pcache return lfs_cache_read(lfs, &lfs->rcache, NULL, block, off, buffer, size); } static int lfs_bd_prog(lfs_t *lfs, lfs_block_t block, lfs_off_t off, const void *buffer, lfs_size_t size) { return lfs_cache_prog(lfs, &lfs->pcache, NULL, block, off, buffer, size); } static int lfs_bd_cmp(lfs_t *lfs, lfs_block_t block, lfs_off_t off, const void *buffer, lfs_size_t size) { return lfs_cache_cmp(lfs, &lfs->rcache, NULL, block, off, buffer, size); } static int lfs_bd_crc(lfs_t *lfs, lfs_block_t block, lfs_off_t off, lfs_size_t size, uint32_t *crc) { return lfs_cache_crc(lfs, &lfs->rcache, NULL, block, off, size, crc); } static int lfs_bd_erase(lfs_t *lfs, lfs_block_t block) { return lfs->cfg->erase(lfs->cfg, block); } static int lfs_bd_sync(lfs_t *lfs) { lfs_cache_drop(lfs, &lfs->rcache); int err = lfs_cache_flush(lfs, &lfs->pcache, NULL); if (err) { return err; } return lfs->cfg->sync(lfs->cfg); } /// Internal operations predeclared here /// int lfs_traverse(lfs_t *lfs, int (*cb)(void *, lfs_block_t), void *data); static int lfs_pred(lfs_t *lfs, const lfs_block_t dir[2], lfs_dir_t *pdir); static int lfs_parent(lfs_t *lfs, const lfs_block_t dir[2], lfs_dir_t *parent, lfs_entry_t *entry); static int lfs_moved(lfs_t *lfs, const void *e); static int lfs_relocate(lfs_t *lfs, const lfs_block_t oldpair[2], const lfs_block_t newpair[2]); int lfs_deorphan(lfs_t *lfs); /// Block allocator /// static int lfs_alloc_lookahead(void *p, lfs_block_t block) { lfs_t *lfs = p; lfs_block_t off = ((block - lfs->free.off) + lfs->cfg->block_count) % lfs->cfg->block_count; if (off < lfs->free.size) { lfs->free.buffer[off / 32] |= 1U << (off % 32); } return 0; } static int lfs_alloc(lfs_t *lfs, lfs_block_t *block) { while (true) { while (lfs->free.i != lfs->free.size) { lfs_block_t off = lfs->free.i; lfs->free.i += 1; lfs->free.ack -= 1; if (!(lfs->free.buffer[off / 32] & (1U << (off % 32)))) { // found a free block *block = (lfs->free.off + off) % lfs->cfg->block_count; // eagerly find next off so an alloc ack can // discredit old lookahead blocks while (lfs->free.i != lfs->free.size && (lfs->free.buffer[lfs->free.i / 32] & (1U << (lfs->free.i % 32)))) { lfs->free.i += 1; lfs->free.ack -= 1; } return 0; } } // check if we have looked at all blocks since last ack if (lfs->free.ack == 0) { LFS_WARN("No more free space %" PRIu32, lfs->free.i + lfs->free.off); return LFS_ERR_NOSPC; } lfs->free.off = (lfs->free.off + lfs->free.size) % lfs->cfg->block_count; lfs->free.size = lfs_min(lfs->cfg->lookahead, lfs->free.ack); lfs->free.i = 0; // find mask of free blocks from tree memset(lfs->free.buffer, 0, lfs->cfg->lookahead / 8); int err = lfs_traverse(lfs, lfs_alloc_lookahead, lfs); if (err) { return err; } } } static void lfs_alloc_ack(lfs_t *lfs) { lfs->free.ack = lfs->cfg->block_count; } /// Endian swapping functions /// static void lfs_dir_fromle32(struct lfs_disk_dir *d) { d->rev = lfs_fromle32(d->rev); d->size = lfs_fromle32(d->size); d->tail[0] = lfs_fromle32(d->tail[0]); d->tail[1] = lfs_fromle32(d->tail[1]); } static void lfs_dir_tole32(struct lfs_disk_dir *d) { d->rev = lfs_tole32(d->rev); d->size = lfs_tole32(d->size); d->tail[0] = lfs_tole32(d->tail[0]); d->tail[1] = lfs_tole32(d->tail[1]); } static void lfs_entry_fromle32(struct lfs_disk_entry *d) { d->u.dir[0] = lfs_fromle32(d->u.dir[0]); d->u.dir[1] = lfs_fromle32(d->u.dir[1]); } static void lfs_entry_tole32(struct lfs_disk_entry *d) { d->u.dir[0] = lfs_tole32(d->u.dir[0]); d->u.dir[1] = lfs_tole32(d->u.dir[1]); } static void lfs_superblock_fromle32(struct lfs_disk_superblock *d) { d->root[0] = lfs_fromle32(d->root[0]); d->root[1] = lfs_fromle32(d->root[1]); d->block_size = lfs_fromle32(d->block_size); d->block_count = lfs_fromle32(d->block_count); d->version = lfs_fromle32(d->version); } static void lfs_superblock_tole32(struct lfs_disk_superblock *d) { d->root[0] = lfs_tole32(d->root[0]); d->root[1] = lfs_tole32(d->root[1]); d->block_size = lfs_tole32(d->block_size); d->block_count = lfs_tole32(d->block_count); d->version = lfs_tole32(d->version); } /// Metadata pair and directory operations /// static inline void lfs_pairswap(lfs_block_t pair[2]) { lfs_block_t t = pair[0]; pair[0] = pair[1]; pair[1] = t; } static inline bool lfs_pairisnull(const lfs_block_t pair[2]) { return pair[0] == 0xffffffff || pair[1] == 0xffffffff; } static inline int lfs_paircmp(const lfs_block_t paira[2], const lfs_block_t pairb[2]) { return !(paira[0] == pairb[0] || paira[1] == pairb[1] || paira[0] == pairb[1] || paira[1] == pairb[0]); } static inline bool lfs_pairsync(const lfs_block_t paira[2], const lfs_block_t pairb[2]) { return (paira[0] == pairb[0] && paira[1] == pairb[1]) || (paira[0] == pairb[1] && paira[1] == pairb[0]); } static inline lfs_size_t lfs_entry_size(const lfs_entry_t *entry) { return 4 + entry->d.elen + entry->d.alen + entry->d.nlen; } static int lfs_dir_alloc(lfs_t *lfs, lfs_dir_t *dir) { // allocate pair of dir blocks for (int i = 0; i < 2; i++) { int err = lfs_alloc(lfs, &dir->pair[i]); if (err) { return err; } } // rather than clobbering one of the blocks we just pretend // the revision may be valid int err = lfs_bd_read(lfs, dir->pair[0], 0, &dir->d.rev, 4); if (err && err != LFS_ERR_CORRUPT) { return err; } if (err != LFS_ERR_CORRUPT) { dir->d.rev = lfs_fromle32(dir->d.rev); } // set defaults dir->d.rev += 1; dir->d.size = sizeof(dir->d) + 4; dir->d.tail[0] = 0xffffffff; dir->d.tail[1] = 0xffffffff; dir->off = sizeof(dir->d); // don't write out yet, let caller take care of that return 0; } static int lfs_dir_fetch(lfs_t *lfs, lfs_dir_t *dir, const lfs_block_t pair[2]) { // copy out pair, otherwise may be aliasing dir const lfs_block_t tpair[2] = {pair[0], pair[1]}; bool valid = false; // check both blocks for the most recent revision for (int i = 0; i < 2; i++) { struct lfs_disk_dir test; int err = lfs_bd_read(lfs, tpair[i], 0, &test, sizeof(test)); lfs_dir_fromle32(&test); if (err) { if (err == LFS_ERR_CORRUPT) { continue; } return err; } if (valid && lfs_scmp(test.rev, dir->d.rev) < 0) { continue; } if ((0x7fffffff & test.size) < sizeof(test) + 4 || (0x7fffffff & test.size) > lfs->cfg->block_size) { continue; } uint32_t crc = 0xffffffff; lfs_dir_tole32(&test); lfs_crc(&crc, &test, sizeof(test)); lfs_dir_fromle32(&test); err = lfs_bd_crc(lfs, tpair[i], sizeof(test), (0x7fffffff & test.size) - sizeof(test), &crc); if (err) { if (err == LFS_ERR_CORRUPT) { continue; } return err; } if (crc != 0) { continue; } valid = true; // setup dir in case it's valid dir->pair[0] = tpair[(i + 0) % 2]; dir->pair[1] = tpair[(i + 1) % 2]; dir->off = sizeof(dir->d); dir->d = test; } if (!valid) { LFS_ERROR("Corrupted dir pair at %" PRIu32 " %" PRIu32, tpair[0], tpair[1]); return LFS_ERR_CORRUPT; } return 0; } struct lfs_region { lfs_off_t oldoff; lfs_size_t oldlen; const void *newdata; lfs_size_t newlen; }; static int lfs_dir_commit(lfs_t *lfs, lfs_dir_t *dir, const struct lfs_region *regions, int count) { // increment revision count dir->d.rev += 1; // keep pairs in order such that pair[0] is most recent lfs_pairswap(dir->pair); for (int i = 0; i < count; i++) { dir->d.size += regions[i].newlen - regions[i].oldlen; } const lfs_block_t oldpair[2] = {dir->pair[0], dir->pair[1]}; bool relocated = false; while (true) { int err = lfs_bd_erase(lfs, dir->pair[0]); if (err) { if (err == LFS_ERR_CORRUPT) { goto relocate; } return err; } uint32_t crc = 0xffffffff; lfs_dir_tole32(&dir->d); lfs_crc(&crc, &dir->d, sizeof(dir->d)); err = lfs_bd_prog(lfs, dir->pair[0], 0, &dir->d, sizeof(dir->d)); lfs_dir_fromle32(&dir->d); if (err) { if (err == LFS_ERR_CORRUPT) { goto relocate; } return err; } int i = 0; lfs_off_t oldoff = sizeof(dir->d); lfs_off_t newoff = sizeof(dir->d); while (newoff < (0x7fffffff & dir->d.size) - 4) { if (i < count && regions[i].oldoff == oldoff) { lfs_crc(&crc, regions[i].newdata, regions[i].newlen); err = lfs_bd_prog(lfs, dir->pair[0], newoff, regions[i].newdata, regions[i].newlen); if (err) { if (err == LFS_ERR_CORRUPT) { goto relocate; } return err; } oldoff += regions[i].oldlen; newoff += regions[i].newlen; i += 1; } else { uint8_t data; err = lfs_bd_read(lfs, oldpair[1], oldoff, &data, 1); if (err) { return err; } lfs_crc(&crc, &data, 1); err = lfs_bd_prog(lfs, dir->pair[0], newoff, &data, 1); if (err) { if (err == LFS_ERR_CORRUPT) { goto relocate; } return err; } oldoff += 1; newoff += 1; } } crc = lfs_tole32(crc); err = lfs_bd_prog(lfs, dir->pair[0], newoff, &crc, 4); crc = lfs_fromle32(crc); if (err) { if (err == LFS_ERR_CORRUPT) { goto relocate; } return err; } err = lfs_bd_sync(lfs); if (err) { if (err == LFS_ERR_CORRUPT) { goto relocate; } return err; } // successful commit, check checksum to make sure uint32_t ncrc = 0xffffffff; err = lfs_bd_crc(lfs, dir->pair[0], 0, (0x7fffffff & dir->d.size) - 4, &ncrc); if (err) { return err; } if (ncrc != crc) { goto relocate; } break; relocate: // commit was corrupted LFS_DEBUG("Bad block at %" PRIu32, dir->pair[0]); // drop caches and prepare to relocate block relocated = true; lfs_cache_drop(lfs, &lfs->pcache); // can't relocate superblock, filesystem is now frozen if (lfs_paircmp(oldpair, (const lfs_block_t[2]){0, 1}) == 0) { LFS_WARN("Superblock %" PRIu32 " has become unwritable", oldpair[0]); return LFS_ERR_CORRUPT; } // relocate half of pair err = lfs_alloc(lfs, &dir->pair[0]); if (err) { return err; } } if (relocated) { // update references if we relocated LFS_DEBUG("Relocating %" PRIu32 " %" PRIu32 " to %" PRIu32 " %" PRIu32, oldpair[0], oldpair[1], dir->pair[0], dir->pair[1]); int err = lfs_relocate(lfs, oldpair, dir->pair); if (err) { return err; } } // shift over any directories that are affected for (lfs_dir_t *d = lfs->dirs; d; d = d->next) { if (lfs_paircmp(d->pair, dir->pair) == 0) { d->pair[0] = dir->pair[0]; d->pair[1] = dir->pair[1]; } } return 0; } static int lfs_dir_update(lfs_t *lfs, lfs_dir_t *dir, lfs_entry_t *entry, const void *data) { lfs_entry_tole32(&entry->d); int err = lfs_dir_commit(lfs, dir, (struct lfs_region[]){{entry->off, sizeof(entry->d), &entry->d, sizeof(entry->d)}, {entry->off + sizeof(entry->d), entry->d.nlen, data, entry->d.nlen}}, data ? 2 : 1); lfs_entry_fromle32(&entry->d); return err; } static int lfs_dir_append(lfs_t *lfs, lfs_dir_t *dir, lfs_entry_t *entry, const void *data) { // check if we fit, if top bit is set we do not and move on while (true) { if (dir->d.size + lfs_entry_size(entry) <= lfs->cfg->block_size) { entry->off = dir->d.size - 4; lfs_entry_tole32(&entry->d); int err = lfs_dir_commit( lfs, dir, (struct lfs_region[]){{entry->off, 0, &entry->d, sizeof(entry->d)}, {entry->off, 0, data, entry->d.nlen}}, 2); lfs_entry_fromle32(&entry->d); return err; } // we need to allocate a new dir block if (!(0x80000000 & dir->d.size)) { lfs_dir_t olddir = *dir; int err = lfs_dir_alloc(lfs, dir); if (err) { return err; } dir->d.tail[0] = olddir.d.tail[0]; dir->d.tail[1] = olddir.d.tail[1]; entry->off = dir->d.size - 4; lfs_entry_tole32(&entry->d); err = lfs_dir_commit( lfs, dir, (struct lfs_region[]){{entry->off, 0, &entry->d, sizeof(entry->d)}, {entry->off, 0, data, entry->d.nlen}}, 2); lfs_entry_fromle32(&entry->d); if (err) { return err; } olddir.d.size |= 0x80000000; olddir.d.tail[0] = dir->pair[0]; olddir.d.tail[1] = dir->pair[1]; return lfs_dir_commit(lfs, &olddir, NULL, 0); } int err = lfs_dir_fetch(lfs, dir, dir->d.tail); if (err) { return err; } } } static int lfs_dir_remove(lfs_t *lfs, lfs_dir_t *dir, lfs_entry_t *entry) { // check if we should just drop the directory block if ((dir->d.size & 0x7fffffff) == sizeof(dir->d) + 4 + lfs_entry_size(entry)) { lfs_dir_t pdir; int res = lfs_pred(lfs, dir->pair, &pdir); if (res < 0) { return res; } if (pdir.d.size & 0x80000000) { pdir.d.size &= dir->d.size | 0x7fffffff; pdir.d.tail[0] = dir->d.tail[0]; pdir.d.tail[1] = dir->d.tail[1]; return lfs_dir_commit(lfs, &pdir, NULL, 0); } } // shift out the entry int err = lfs_dir_commit(lfs, dir, (struct lfs_region[]){ {entry->off, lfs_entry_size(entry), NULL, 0}, }, 1); if (err) { return err; } // shift over any files/directories that are affected for (lfs_file_t *f = lfs->files; f; f = f->next) { if (lfs_paircmp(f->pair, dir->pair) == 0) { if (f->poff == entry->off) { f->pair[0] = 0xffffffff; f->pair[1] = 0xffffffff; } else if (f->poff > entry->off) { f->poff -= lfs_entry_size(entry); } } } for (lfs_dir_t *d = lfs->dirs; d; d = d->next) { if (lfs_paircmp(d->pair, dir->pair) == 0) { if (d->off > entry->off) { d->off -= lfs_entry_size(entry); d->pos -= lfs_entry_size(entry); } } } return 0; } static int lfs_dir_next(lfs_t *lfs, lfs_dir_t *dir, lfs_entry_t *entry) { while (dir->off + sizeof(entry->d) > (0x7fffffff & dir->d.size) - 4) { if (!(0x80000000 & dir->d.size)) { entry->off = dir->off; return LFS_ERR_NOENT; } int err = lfs_dir_fetch(lfs, dir, dir->d.tail); if (err) { return err; } dir->off = sizeof(dir->d); dir->pos += sizeof(dir->d) + 4; } int err = lfs_bd_read(lfs, dir->pair[0], dir->off, &entry->d, sizeof(entry->d)); lfs_entry_fromle32(&entry->d); if (err) { return err; } entry->off = dir->off; dir->off += lfs_entry_size(entry); dir->pos += lfs_entry_size(entry); return 0; } static int lfs_dir_find(lfs_t *lfs, lfs_dir_t *dir, lfs_entry_t *entry, const char **path) { const char *pathname = *path; size_t pathlen; entry->d.type = LFS_TYPE_DIR; entry->d.elen = sizeof(entry->d) - 4; entry->d.alen = 0; entry->d.nlen = 0; entry->d.u.dir[0] = lfs->root[0]; entry->d.u.dir[1] = lfs->root[1]; while (true) { nextname: // skip slashes pathname += strspn(pathname, "/"); pathlen = strcspn(pathname, "/"); // skip '.' and root '..' if ((pathlen == 1 && memcmp(pathname, ".", 1) == 0) || (pathlen == 2 && memcmp(pathname, "..", 2) == 0)) { pathname += pathlen; goto nextname; } // skip if matched by '..' in name const char *suffix = pathname + pathlen; size_t sufflen; int depth = 1; while (true) { suffix += strspn(suffix, "/"); sufflen = strcspn(suffix, "/"); if (sufflen == 0) { break; } if (sufflen == 2 && memcmp(suffix, "..", 2) == 0) { depth -= 1; if (depth == 0) { pathname = suffix + sufflen; goto nextname; } } else { depth += 1; } suffix += sufflen; } // found path if (pathname[0] == '\0') { return 0; } // update what we've found *path = pathname; // continue on if we hit a directory if (entry->d.type != LFS_TYPE_DIR) { return LFS_ERR_NOTDIR; } int err = lfs_dir_fetch(lfs, dir, entry->d.u.dir); if (err) { return err; } // find entry matching name while (true) { err = lfs_dir_next(lfs, dir, entry); if (err) { return err; } if (((0x7f & entry->d.type) != LFS_TYPE_REG && (0x7f & entry->d.type) != LFS_TYPE_DIR) || entry->d.nlen != pathlen) { continue; } int res = lfs_bd_cmp(lfs, dir->pair[0], entry->off + 4 + entry->d.elen + entry->d.alen, pathname, pathlen); if (res < 0) { return res; } // found match if (res) { break; } } // check that entry has not been moved if (entry->d.type & 0x80) { int moved = lfs_moved(lfs, &entry->d.u); if (moved) { return (moved < 0) ? moved : LFS_ERR_NOENT; } entry->d.type &= ~0x80; } // to next name pathname += pathlen; } } /// Top level directory operations /// int lfs_mkdir(lfs_t *lfs, const char *path) { // deorphan if we haven't yet, needed at most once after poweron if (!lfs->deorphaned) { int err = lfs_deorphan(lfs); if (err) { return err; } } // fetch parent directory lfs_dir_t cwd; lfs_entry_t entry; int err = lfs_dir_find(lfs, &cwd, &entry, &path); if (err != LFS_ERR_NOENT || strchr(path, '/') != NULL) { return err ? err : LFS_ERR_EXIST; } // build up new directory lfs_alloc_ack(lfs); lfs_dir_t dir; err = lfs_dir_alloc(lfs, &dir); if (err) { return err; } dir.d.tail[0] = cwd.d.tail[0]; dir.d.tail[1] = cwd.d.tail[1]; err = lfs_dir_commit(lfs, &dir, NULL, 0); if (err) { return err; } entry.d.type = LFS_TYPE_DIR; entry.d.elen = sizeof(entry.d) - 4; entry.d.alen = 0; entry.d.nlen = strlen(path); entry.d.u.dir[0] = dir.pair[0]; entry.d.u.dir[1] = dir.pair[1]; cwd.d.tail[0] = dir.pair[0]; cwd.d.tail[1] = dir.pair[1]; err = lfs_dir_append(lfs, &cwd, &entry, path); if (err) { return err; } lfs_alloc_ack(lfs); return 0; } int lfs_dir_open(lfs_t *lfs, lfs_dir_t *dir, const char *path) { dir->pair[0] = lfs->root[0]; dir->pair[1] = lfs->root[1]; lfs_entry_t entry; int err = lfs_dir_find(lfs, dir, &entry, &path); if (err) { return err; } else if (entry.d.type != LFS_TYPE_DIR) { return LFS_ERR_NOTDIR; } err = lfs_dir_fetch(lfs, dir, entry.d.u.dir); if (err) { return err; } // setup head dir // special offset for '.' and '..' dir->head[0] = dir->pair[0]; dir->head[1] = dir->pair[1]; dir->pos = sizeof(dir->d) - 2; dir->off = sizeof(dir->d); // add to list of directories dir->next = lfs->dirs; lfs->dirs = dir; return 0; } int lfs_dir_close(lfs_t *lfs, lfs_dir_t *dir) { // remove from list of directories for (lfs_dir_t **p = &lfs->dirs; *p; p = &(*p)->next) { if (*p == dir) { *p = dir->next; break; } } return 0; } int lfs_dir_read(lfs_t *lfs, lfs_dir_t *dir, struct lfs_info *info) { memset(info, 0, sizeof(*info)); // special offset for '.' and '..' if (dir->pos == sizeof(dir->d) - 2) { info->type = LFS_TYPE_DIR; strcpy(info->name, "."); dir->pos += 1; return 1; } else if (dir->pos == sizeof(dir->d) - 1) { info->type = LFS_TYPE_DIR; strcpy(info->name, ".."); dir->pos += 1; return 1; } lfs_entry_t entry; while (true) { int err = lfs_dir_next(lfs, dir, &entry); if (err) { return (err == LFS_ERR_NOENT) ? 0 : err; } if ((0x7f & entry.d.type) != LFS_TYPE_REG && (0x7f & entry.d.type) != LFS_TYPE_DIR) { continue; } // check that entry has not been moved if (entry.d.type & 0x80) { int moved = lfs_moved(lfs, &entry.d.u); if (moved < 0) { return moved; } if (moved) { continue; } entry.d.type &= ~0x80; } break; } info->type = entry.d.type; if (info->type == LFS_TYPE_REG) { info->size = entry.d.u.file.size; } int err = lfs_bd_read(lfs, dir->pair[0], entry.off + 4 + entry.d.elen + entry.d.alen, info->name, entry.d.nlen); if (err) { return err; } return 1; } int lfs_dir_seek(lfs_t *lfs, lfs_dir_t *dir, lfs_off_t off) { // simply walk from head dir int err = lfs_dir_rewind(lfs, dir); if (err) { return err; } dir->pos = off; while (off > (0x7fffffff & dir->d.size)) { off -= 0x7fffffff & dir->d.size; if (!(0x80000000 & dir->d.size)) { return LFS_ERR_INVAL; } err = lfs_dir_fetch(lfs, dir, dir->d.tail); if (err) { return err; } } dir->off = off; return 0; } int lfs_dir_rewind(lfs_t *lfs, lfs_dir_t *dir) { // reload the head dir int err = lfs_dir_fetch(lfs, dir, dir->head); if (err) { return err; } dir->pair[0] = dir->head[0]; dir->pair[1] = dir->head[1]; dir->pos = sizeof(dir->d) - 2; dir->off = sizeof(dir->d); return 0; } /// File index list operations /// static int lfs_ctz_index(lfs_t *lfs, lfs_off_t *off) { lfs_off_t size = *off; lfs_off_t b = lfs->cfg->block_size - 2 * 4; lfs_off_t i = size / b; if (i == 0) { return 0; } i = (size - 4 * (lfs_popc(i - 1) + 2)) / b; *off = size - b * i - 4 * lfs_popc(i); return i; } static int lfs_ctz_find(lfs_t *lfs, lfs_cache_t *rcache, const lfs_cache_t *pcache, lfs_block_t head, lfs_size_t size, lfs_size_t pos, lfs_block_t *block, lfs_off_t *off) { if (size == 0) { *block = 0xffffffff; *off = 0; return 0; } lfs_off_t current = lfs_ctz_index(lfs, &(lfs_off_t){size - 1}); lfs_off_t target = lfs_ctz_index(lfs, &pos); while (current > target) { lfs_size_t skip = lfs_min(lfs_npw2(current - target + 1) - 1, lfs_ctz(current)); int err = lfs_cache_read(lfs, rcache, pcache, head, 4 * skip, &head, 4); head = lfs_fromle32(head); if (err) { return err; } LFS_ASSERT(head >= 2 && head <= lfs->cfg->block_count); current -= 1 << skip; } *block = head; *off = pos; return 0; } static int lfs_ctz_extend(lfs_t *lfs, lfs_cache_t *rcache, lfs_cache_t *pcache, lfs_block_t head, lfs_size_t size, lfs_block_t *block, lfs_off_t *off) { while (true) { // go ahead and grab a block lfs_block_t nblock; int err = lfs_alloc(lfs, &nblock); if (err) { return err; } LFS_ASSERT(nblock >= 2 && nblock <= lfs->cfg->block_count); if (true) { err = lfs_bd_erase(lfs, nblock); if (err) { if (err == LFS_ERR_CORRUPT) { goto relocate; } return err; } if (size == 0) { *block = nblock; *off = 0; return 0; } size -= 1; lfs_off_t index = lfs_ctz_index(lfs, &size); size += 1; // just copy out the last block if it is incomplete if (size != lfs->cfg->block_size) { for (lfs_off_t i = 0; i < size; i++) { uint8_t data; err = lfs_cache_read(lfs, rcache, NULL, head, i, &data, 1); if (err) { return err; } err = lfs_cache_prog(lfs, pcache, rcache, nblock, i, &data, 1); if (err) { if (err == LFS_ERR_CORRUPT) { goto relocate; } return err; } } *block = nblock; *off = size; return 0; } // append block index += 1; lfs_size_t skips = lfs_ctz(index) + 1; for (lfs_off_t i = 0; i < skips; i++) { head = lfs_tole32(head); err = lfs_cache_prog(lfs, pcache, rcache, nblock, 4 * i, &head, 4); head = lfs_fromle32(head); if (err) { if (err == LFS_ERR_CORRUPT) { goto relocate; } return err; } if (i != skips - 1) { err = lfs_cache_read(lfs, rcache, NULL, head, 4 * i, &head, 4); head = lfs_fromle32(head); if (err) { return err; } } LFS_ASSERT(head >= 2 && head <= lfs->cfg->block_count); } *block = nblock; *off = 4 * skips; return 0; } relocate: LFS_DEBUG("Bad block at %" PRIu32, nblock); // just clear cache and try a new block lfs_cache_drop(lfs, &lfs->pcache); } } static int lfs_ctz_traverse(lfs_t *lfs, lfs_cache_t *rcache, const lfs_cache_t *pcache, lfs_block_t head, lfs_size_t size, int (*cb)(void *, lfs_block_t), void *data) { if (size == 0) { return 0; } lfs_off_t index = lfs_ctz_index(lfs, &(lfs_off_t){size - 1}); while (true) { int err = cb(data, head); if (err) { return err; } if (index == 0) { return 0; } lfs_block_t heads[2]; int count = 2 - (index & 1); err = lfs_cache_read(lfs, rcache, pcache, head, 0, &heads, count * 4); heads[0] = lfs_fromle32(heads[0]); heads[1] = lfs_fromle32(heads[1]); if (err) { return err; } for (int i = 0; i < count - 1; i++) { err = cb(data, heads[i]); if (err) { return err; } } head = heads[count - 1]; index -= count; } } /// Top level file operations /// int lfs_file_opencfg(lfs_t *lfs, lfs_file_t *file, const char *path, int flags, const struct lfs_file_config *cfg) { // deorphan if we haven't yet, needed at most once after poweron if ((flags & 3) != LFS_O_RDONLY && !lfs->deorphaned) { int err = lfs_deorphan(lfs); if (err) { return err; } } // allocate entry for file if it doesn't exist lfs_dir_t cwd; lfs_entry_t entry; int err = lfs_dir_find(lfs, &cwd, &entry, &path); if (err && (err != LFS_ERR_NOENT || strchr(path, '/') != NULL)) { return err; } if (err == LFS_ERR_NOENT) { if (!(flags & LFS_O_CREAT)) { return LFS_ERR_NOENT; } // create entry to remember name entry.d.type = LFS_TYPE_REG; entry.d.elen = sizeof(entry.d) - 4; entry.d.alen = 0; entry.d.nlen = strlen(path); entry.d.u.file.head = 0xffffffff; entry.d.u.file.size = 0; err = lfs_dir_append(lfs, &cwd, &entry, path); if (err) { return err; } } else if (entry.d.type == LFS_TYPE_DIR) { return LFS_ERR_ISDIR; } else if (flags & LFS_O_EXCL) { return LFS_ERR_EXIST; } // setup file struct file->cfg = cfg; file->pair[0] = cwd.pair[0]; file->pair[1] = cwd.pair[1]; file->poff = entry.off; file->head = entry.d.u.file.head; file->size = entry.d.u.file.size; file->flags = flags; file->pos = 0; if (flags & LFS_O_TRUNC) { if (file->size != 0) { file->flags |= LFS_F_DIRTY; } file->head = 0xffffffff; file->size = 0; } // allocate buffer if needed file->cache.block = 0xffffffff; if (file->cfg && file->cfg->buffer) { file->cache.buffer = file->cfg->buffer; } else if (lfs->cfg->file_buffer) { if (lfs->files) { // already in use return LFS_ERR_NOMEM; } file->cache.buffer = lfs->cfg->file_buffer; } else if ((file->flags & 3) == LFS_O_RDONLY) { file->cache.buffer = lfs_malloc(lfs->cfg->read_size); if (!file->cache.buffer) { return LFS_ERR_NOMEM; } } else { file->cache.buffer = lfs_malloc(lfs->cfg->prog_size); if (!file->cache.buffer) { return LFS_ERR_NOMEM; } } // zero to avoid information leak lfs_cache_drop(lfs, &file->cache); if ((file->flags & 3) != LFS_O_RDONLY) { lfs_cache_zero(lfs, &file->cache); } // add to list of files file->next = lfs->files; lfs->files = file; return 0; } int lfs_file_open(lfs_t *lfs, lfs_file_t *file, const char *path, int flags) { return lfs_file_opencfg(lfs, file, path, flags, NULL); } int lfs_file_close(lfs_t *lfs, lfs_file_t *file) { int err = lfs_file_sync(lfs, file); // remove from list of files for (lfs_file_t **p = &lfs->files; *p; p = &(*p)->next) { if (*p == file) { *p = file->next; break; } } // clean up memory if (!(file->cfg && file->cfg->buffer) && !lfs->cfg->file_buffer) { lfs_free(file->cache.buffer); } return err; } static int lfs_file_relocate(lfs_t *lfs, lfs_file_t *file) { relocate: LFS_DEBUG("Bad block at %" PRIu32, file->block); // just relocate what exists into new block lfs_block_t nblock; int err = lfs_alloc(lfs, &nblock); if (err) { return err; } err = lfs_bd_erase(lfs, nblock); if (err) { if (err == LFS_ERR_CORRUPT) { goto relocate; } return err; } // either read from dirty cache or disk for (lfs_off_t i = 0; i < file->off; i++) { uint8_t data; err = lfs_cache_read(lfs, &lfs->rcache, &file->cache, file->block, i, &data, 1); if (err) { return err; } err = lfs_cache_prog(lfs, &lfs->pcache, &lfs->rcache, nblock, i, &data, 1); if (err) { if (err == LFS_ERR_CORRUPT) { goto relocate; } return err; } } // copy over new state of file memcpy(file->cache.buffer, lfs->pcache.buffer, lfs->cfg->prog_size); file->cache.block = lfs->pcache.block; file->cache.off = lfs->pcache.off; lfs_cache_zero(lfs, &lfs->pcache); file->block = nblock; return 0; } static int lfs_file_flush(lfs_t *lfs, lfs_file_t *file) { if (file->flags & LFS_F_READING) { // just drop read cache lfs_cache_drop(lfs, &file->cache); file->flags &= ~LFS_F_READING; } if (file->flags & LFS_F_WRITING) { lfs_off_t pos = file->pos; // copy over anything after current branch lfs_file_t orig = { .head = file->head, .size = file->size, .flags = LFS_O_RDONLY, .pos = file->pos, .cache = lfs->rcache, }; lfs_cache_drop(lfs, &lfs->rcache); while (file->pos < file->size) { // copy over a byte at a time, leave it up to caching // to make this efficient uint8_t data; lfs_ssize_t res = lfs_file_read(lfs, &orig, &data, 1); if (res < 0) { return res; } res = lfs_file_write(lfs, file, &data, 1); if (res < 0) { return res; } // keep our reference to the rcache in sync if (lfs->rcache.block != 0xffffffff) { lfs_cache_drop(lfs, &orig.cache); lfs_cache_drop(lfs, &lfs->rcache); } } // write out what we have while (true) { int err = lfs_cache_flush(lfs, &file->cache, &lfs->rcache); if (err) { if (err == LFS_ERR_CORRUPT) { goto relocate; } return err; } break; relocate: err = lfs_file_relocate(lfs, file); if (err) { return err; } } // actual file updates file->head = file->block; file->size = file->pos; file->flags &= ~LFS_F_WRITING; file->flags |= LFS_F_DIRTY; file->pos = pos; } return 0; } int lfs_file_sync(lfs_t *lfs, lfs_file_t *file) { int err = lfs_file_flush(lfs, file); if (err) { return err; } if ((file->flags & LFS_F_DIRTY) && !(file->flags & LFS_F_ERRED) && !lfs_pairisnull(file->pair)) { // update dir entry lfs_dir_t cwd; err = lfs_dir_fetch(lfs, &cwd, file->pair); if (err) { return err; } lfs_entry_t entry = {.off = file->poff}; err = lfs_bd_read(lfs, cwd.pair[0], entry.off, &entry.d, sizeof(entry.d)); lfs_entry_fromle32(&entry.d); if (err) { return err; } LFS_ASSERT(entry.d.type == LFS_TYPE_REG); entry.d.u.file.head = file->head; entry.d.u.file.size = file->size; err = lfs_dir_update(lfs, &cwd, &entry, NULL); if (err) { return err; } file->flags &= ~LFS_F_DIRTY; } return 0; } lfs_ssize_t lfs_file_read(lfs_t *lfs, lfs_file_t *file, void *buffer, lfs_size_t size) { uint8_t *data = buffer; lfs_size_t nsize = size; if ((file->flags & 3) == LFS_O_WRONLY) { return LFS_ERR_BADF; } if (file->flags & LFS_F_WRITING) { // flush out any writes int err = lfs_file_flush(lfs, file); if (err) { return err; } } if (file->pos >= file->size) { // eof if past end return 0; } size = lfs_min(size, file->size - file->pos); nsize = size; while (nsize > 0) { // check if we need a new block if (!(file->flags & LFS_F_READING) || file->off == lfs->cfg->block_size) { int err = lfs_ctz_find(lfs, &file->cache, NULL, file->head, file->size, file->pos, &file->block, &file->off); if (err) { return err; } file->flags |= LFS_F_READING; } // read as much as we can in current block lfs_size_t diff = lfs_min(nsize, lfs->cfg->block_size - file->off); int err = lfs_cache_read(lfs, &file->cache, NULL, file->block, file->off, data, diff); if (err) { return err; } file->pos += diff; file->off += diff; data += diff; nsize -= diff; } return size; } lfs_ssize_t lfs_file_write(lfs_t *lfs, lfs_file_t *file, const void *buffer, lfs_size_t size) { const uint8_t *data = buffer; lfs_size_t nsize = size; if ((file->flags & 3) == LFS_O_RDONLY) { return LFS_ERR_BADF; } if (file->flags & LFS_F_READING) { // drop any reads int err = lfs_file_flush(lfs, file); if (err) { return err; } } if ((file->flags & LFS_O_APPEND) && file->pos < file->size) { file->pos = file->size; } if (!(file->flags & LFS_F_WRITING) && file->pos > file->size) { // fill with zeros lfs_off_t pos = file->pos; file->pos = file->size; while (file->pos < pos) { lfs_ssize_t res = lfs_file_write(lfs, file, &(uint8_t){0}, 1); if (res < 0) { return res; } } } while (nsize > 0) { // check if we need a new block if (!(file->flags & LFS_F_WRITING) || file->off == lfs->cfg->block_size) { if (!(file->flags & LFS_F_WRITING) && file->pos > 0) { // find out which block we're extending from int err = lfs_ctz_find(lfs, &file->cache, NULL, file->head, file->size, file->pos - 1, &file->block, &file->off); if (err) { file->flags |= LFS_F_ERRED; return err; } // mark cache as dirty since we may have read data into it lfs_cache_zero(lfs, &file->cache); } // extend file with new blocks lfs_alloc_ack(lfs); int err = lfs_ctz_extend(lfs, &lfs->rcache, &file->cache, file->block, file->pos, &file->block, &file->off); if (err) { file->flags |= LFS_F_ERRED; return err; } file->flags |= LFS_F_WRITING; } // program as much as we can in current block lfs_size_t diff = lfs_min(nsize, lfs->cfg->block_size - file->off); while (true) { int err = lfs_cache_prog(lfs, &file->cache, &lfs->rcache, file->block, file->off, data, diff); if (err) { if (err == LFS_ERR_CORRUPT) { goto relocate; } file->flags |= LFS_F_ERRED; return err; } break; relocate: err = lfs_file_relocate(lfs, file); if (err) { file->flags |= LFS_F_ERRED; return err; } } file->pos += diff; file->off += diff; data += diff; nsize -= diff; lfs_alloc_ack(lfs); } file->flags &= ~LFS_F_ERRED; return size; } lfs_soff_t lfs_file_seek(lfs_t *lfs, lfs_file_t *file, lfs_soff_t off, int whence) { // write out everything beforehand, may be noop if rdonly int err = lfs_file_flush(lfs, file); if (err) { return err; } // update pos if (whence == LFS_SEEK_SET) { file->pos = off; } else if (whence == LFS_SEEK_CUR) { if (off < 0 && (lfs_off_t)-off > file->pos) { return LFS_ERR_INVAL; } file->pos = file->pos + off; } else if (whence == LFS_SEEK_END) { if (off < 0 && (lfs_off_t)-off > file->size) { return LFS_ERR_INVAL; } file->pos = file->size + off; } return file->pos; } int lfs_file_truncate(lfs_t *lfs, lfs_file_t *file, lfs_off_t size) { if ((file->flags & 3) == LFS_O_RDONLY) { return LFS_ERR_BADF; } lfs_off_t oldsize = lfs_file_size(lfs, file); if (size < oldsize) { // need to flush since directly changing metadata int err = lfs_file_flush(lfs, file); if (err) { return err; } // lookup new head in ctz skip list err = lfs_ctz_find(lfs, &file->cache, NULL, file->head, file->size, size, &file->head, &(lfs_off_t){0}); if (err) { return err; } file->size = size; file->flags |= LFS_F_DIRTY; } else if (size > oldsize) { lfs_off_t pos = file->pos; // flush+seek if not already at end if (file->pos != oldsize) { int err = lfs_file_seek(lfs, file, 0, LFS_SEEK_END); if (err < 0) { return err; } } // fill with zeros while (file->pos < size) { lfs_ssize_t res = lfs_file_write(lfs, file, &(uint8_t){0}, 1); if (res < 0) { return res; } } // restore pos int err = lfs_file_seek(lfs, file, pos, LFS_SEEK_SET); if (err < 0) { return err; } } return 0; } lfs_soff_t lfs_file_tell(lfs_t *lfs, lfs_file_t const *file) { (void)lfs; return file->pos; } int lfs_file_rewind(lfs_t *lfs, lfs_file_t *file) { lfs_soff_t res = lfs_file_seek(lfs, file, 0, LFS_SEEK_SET); if (res < 0) { return res; } return 0; } lfs_soff_t lfs_file_size(lfs_t *lfs, lfs_file_t *file) { (void)lfs; if (file->flags & LFS_F_WRITING) { return lfs_max(file->pos, file->size); } else { return file->size; } } /// General fs operations /// int lfs_stat(lfs_t *lfs, const char *path, struct lfs_info *info) { lfs_dir_t cwd; lfs_entry_t entry; int err = lfs_dir_find(lfs, &cwd, &entry, &path); if (err) { return err; } memset(info, 0, sizeof(*info)); info->type = entry.d.type; if (info->type == LFS_TYPE_REG) { info->size = entry.d.u.file.size; } if (lfs_paircmp(entry.d.u.dir, lfs->root) == 0) { strcpy(info->name, "/"); } else { err = lfs_bd_read(lfs, cwd.pair[0], entry.off + 4 + entry.d.elen + entry.d.alen, info->name, entry.d.nlen); if (err) { return err; } } return 0; } int lfs_remove(lfs_t *lfs, const char *path) { // deorphan if we haven't yet, needed at most once after poweron if (!lfs->deorphaned) { int err = lfs_deorphan(lfs); if (err) { return err; } } lfs_dir_t cwd; lfs_entry_t entry; int err = lfs_dir_find(lfs, &cwd, &entry, &path); if (err) { return err; } lfs_dir_t dir; if (entry.d.type == LFS_TYPE_DIR) { // must be empty before removal, checking size // without masking top bit checks for any case where // dir is not empty err = lfs_dir_fetch(lfs, &dir, entry.d.u.dir); if (err) { return err; } /* else if (dir.d.size != sizeof(dir.d)+4) { return LFS_ERR_NOTEMPTY; } allow to remove non-empty folder, According to below issue, comment these 2 line won't corrupt filesystem https://github.com/ARMmbed/littlefs/issues/43 */ } // remove the entry err = lfs_dir_remove(lfs, &cwd, &entry); if (err) { return err; } // if we were a directory, find pred, replace tail if (entry.d.type == LFS_TYPE_DIR) { int res = lfs_pred(lfs, dir.pair, &cwd); if (res < 0) { return res; } LFS_ASSERT(res); // must have pred cwd.d.tail[0] = dir.d.tail[0]; cwd.d.tail[1] = dir.d.tail[1]; err = lfs_dir_commit(lfs, &cwd, NULL, 0); if (err) { return err; } } return 0; } int lfs_rename(lfs_t *lfs, const char *oldpath, const char *newpath) { // deorphan if we haven't yet, needed at most once after poweron if (!lfs->deorphaned) { int err = lfs_deorphan(lfs); if (err) { return err; } } // find old entry lfs_dir_t oldcwd; lfs_entry_t oldentry; int err = lfs_dir_find(lfs, &oldcwd, &oldentry, &oldpath); if (err) { return err; } // allocate new entry lfs_dir_t newcwd; lfs_entry_t preventry; err = lfs_dir_find(lfs, &newcwd, &preventry, &newpath); if (err && (err != LFS_ERR_NOENT || strchr(newpath, '/') != NULL)) { return err; } bool prevexists = (err != LFS_ERR_NOENT); bool samepair = (lfs_paircmp(oldcwd.pair, newcwd.pair) == 0); // must have same type if (prevexists && preventry.d.type != oldentry.d.type) { return LFS_ERR_ISDIR; } lfs_dir_t dir; if (prevexists && preventry.d.type == LFS_TYPE_DIR) { // must be empty before removal, checking size // without masking top bit checks for any case where // dir is not empty err = lfs_dir_fetch(lfs, &dir, preventry.d.u.dir); if (err) { return err; } else if (dir.d.size != sizeof(dir.d) + 4) { return LFS_ERR_NOTEMPTY; } } // mark as moving oldentry.d.type |= 0x80; err = lfs_dir_update(lfs, &oldcwd, &oldentry, NULL); if (err) { return err; } // update pair if newcwd == oldcwd if (samepair) { newcwd = oldcwd; } // move to new location lfs_entry_t newentry = preventry; newentry.d = oldentry.d; newentry.d.type &= ~0x80; newentry.d.nlen = strlen(newpath); if (prevexists) { err = lfs_dir_update(lfs, &newcwd, &newentry, newpath); if (err) { return err; } } else { err = lfs_dir_append(lfs, &newcwd, &newentry, newpath); if (err) { return err; } } // update pair if newcwd == oldcwd if (samepair) { oldcwd = newcwd; } // remove old entry err = lfs_dir_remove(lfs, &oldcwd, &oldentry); if (err) { return err; } // if we were a directory, find pred, replace tail if (prevexists && preventry.d.type == LFS_TYPE_DIR) { int res = lfs_pred(lfs, dir.pair, &newcwd); if (res < 0) { return res; } LFS_ASSERT(res); // must have pred newcwd.d.tail[0] = dir.d.tail[0]; newcwd.d.tail[1] = dir.d.tail[1]; err = lfs_dir_commit(lfs, &newcwd, NULL, 0); if (err) { return err; } } return 0; } /// Filesystem operations /// static void lfs_deinit(lfs_t *lfs) { // free allocated memory if (!lfs->cfg->read_buffer) { lfs_free(lfs->rcache.buffer); } if (!lfs->cfg->prog_buffer) { lfs_free(lfs->pcache.buffer); } if (!lfs->cfg->lookahead_buffer) { lfs_free(lfs->free.buffer); } } static int lfs_init(lfs_t *lfs, const struct lfs_config *cfg) { lfs->cfg = cfg; // setup read cache if (lfs->cfg->read_buffer) { lfs->rcache.buffer = lfs->cfg->read_buffer; } else { lfs->rcache.buffer = lfs_malloc(lfs->cfg->read_size); if (!lfs->rcache.buffer) { goto cleanup; } } // setup program cache if (lfs->cfg->prog_buffer) { lfs->pcache.buffer = lfs->cfg->prog_buffer; } else { lfs->pcache.buffer = lfs_malloc(lfs->cfg->prog_size); if (!lfs->pcache.buffer) { goto cleanup; } } // zero to avoid information leaks lfs_cache_zero(lfs, &lfs->pcache); lfs_cache_drop(lfs, &lfs->rcache); // setup lookahead, round down to nearest 32-bits LFS_ASSERT(lfs->cfg->lookahead % 32 == 0); LFS_ASSERT(lfs->cfg->lookahead > 0); if (lfs->cfg->lookahead_buffer) { lfs->free.buffer = lfs->cfg->lookahead_buffer; } else { lfs->free.buffer = lfs_malloc(lfs->cfg->lookahead / 8); if (!lfs->free.buffer) { goto cleanup; } } // check that program and read sizes are multiples of the block size LFS_ASSERT(lfs->cfg->prog_size % lfs->cfg->read_size == 0); LFS_ASSERT(lfs->cfg->block_size % lfs->cfg->prog_size == 0); // check that the block size is large enough to fit ctz pointers LFS_ASSERT(4 * lfs_npw2(0xffffffff / (lfs->cfg->block_size - 2 * 4)) <= lfs->cfg->block_size); // setup default state lfs->root[0] = 0xffffffff; lfs->root[1] = 0xffffffff; lfs->files = NULL; lfs->dirs = NULL; lfs->deorphaned = false; return 0; cleanup: lfs_deinit(lfs); return LFS_ERR_NOMEM; } int lfs_format(lfs_t *lfs, const struct lfs_config *cfg) { int err = 0; if (true) { err = lfs_init(lfs, cfg); if (err) { return err; } // create free lookahead memset(lfs->free.buffer, 0, lfs->cfg->lookahead / 8); lfs->free.off = 0; lfs->free.size = lfs_min(lfs->cfg->lookahead, lfs->cfg->block_count); lfs->free.i = 0; lfs_alloc_ack(lfs); // create superblock dir lfs_dir_t superdir; err = lfs_dir_alloc(lfs, &superdir); if (err) { goto cleanup; } // write root directory lfs_dir_t root; err = lfs_dir_alloc(lfs, &root); if (err) { goto cleanup; } err = lfs_dir_commit(lfs, &root, NULL, 0); if (err) { goto cleanup; } lfs->root[0] = root.pair[0]; lfs->root[1] = root.pair[1]; // write superblocks lfs_superblock_t superblock = { .off = sizeof(superdir.d), .d.type = LFS_TYPE_SUPERBLOCK, .d.elen = sizeof(superblock.d) - sizeof(superblock.d.magic) - 4, .d.nlen = sizeof(superblock.d.magic), .d.version = LFS_DISK_VERSION, .d.magic = {"littlefs"}, .d.block_size = lfs->cfg->block_size, .d.block_count = lfs->cfg->block_count, .d.root = {lfs->root[0], lfs->root[1]}, }; superdir.d.tail[0] = root.pair[0]; superdir.d.tail[1] = root.pair[1]; superdir.d.size = sizeof(superdir.d) + sizeof(superblock.d) + 4; // write both pairs to be safe lfs_superblock_tole32(&superblock.d); bool valid = false; for (int i = 0; i < 2; i++) { err = lfs_dir_commit( lfs, &superdir, (struct lfs_region[]){{sizeof(superdir.d), sizeof(superblock.d), &superblock.d, sizeof(superblock.d)}}, 1); if (err && err != LFS_ERR_CORRUPT) { goto cleanup; } valid = valid || !err; } if (!valid) { err = LFS_ERR_CORRUPT; goto cleanup; } // sanity check that fetch works err = lfs_dir_fetch(lfs, &superdir, (const lfs_block_t[2]){0, 1}); if (err) { goto cleanup; } lfs_alloc_ack(lfs); } cleanup: lfs_deinit(lfs); return err; } int lfs_mount(lfs_t *lfs, const struct lfs_config *cfg) { int err = 0; if (true) { err = lfs_init(lfs, cfg); if (err) { return err; } // setup free lookahead lfs->free.off = 0; lfs->free.size = 0; lfs->free.i = 0; lfs_alloc_ack(lfs); // load superblock lfs_dir_t dir; lfs_superblock_t superblock; err = lfs_dir_fetch(lfs, &dir, (const lfs_block_t[2]){0, 1}); if (err && err != LFS_ERR_CORRUPT) { goto cleanup; } if (!err) { err = lfs_bd_read(lfs, dir.pair[0], sizeof(dir.d), &superblock.d, sizeof(superblock.d)); lfs_superblock_fromle32(&superblock.d); if (err) { goto cleanup; } lfs->root[0] = superblock.d.root[0]; lfs->root[1] = superblock.d.root[1]; } if (err || memcmp(superblock.d.magic, "littlefs", 8) != 0) { LFS_ERROR("Invalid superblock at %d %d", 0, 1); err = LFS_ERR_CORRUPT; goto cleanup; } uint16_t major_version = (0xffff & (superblock.d.version >> 16)); uint16_t minor_version = (0xffff & (superblock.d.version >> 0)); if ((major_version != LFS_DISK_VERSION_MAJOR || minor_version > LFS_DISK_VERSION_MINOR)) { LFS_ERROR("Invalid version %d.%d", major_version, minor_version); err = LFS_ERR_INVAL; goto cleanup; } return 0; } cleanup: lfs_deinit(lfs); return err; } int lfs_unmount(lfs_t *lfs) { lfs_deinit(lfs); return 0; } /// Littlefs specific operations /// int lfs_traverse(lfs_t *lfs, int (*cb)(void *, lfs_block_t), void *data) { if (lfs_pairisnull(lfs->root)) { return 0; } // iterate over metadata pairs lfs_dir_t dir; lfs_entry_t entry; lfs_block_t cwd[2] = {0, 1}; while (true) { for (int i = 0; i < 2; i++) { int err = cb(data, cwd[i]); if (err) { return err; } } int err = lfs_dir_fetch(lfs, &dir, cwd); if (err) { return err; } // iterate over contents while (dir.off + sizeof(entry.d) <= (0x7fffffff & dir.d.size) - 4) { err = lfs_bd_read(lfs, dir.pair[0], dir.off, &entry.d, sizeof(entry.d)); lfs_entry_fromle32(&entry.d); if (err) { return err; } dir.off += lfs_entry_size(&entry); if ((0x70 & entry.d.type) == (0x70 & LFS_TYPE_REG)) { err = lfs_ctz_traverse(lfs, &lfs->rcache, NULL, entry.d.u.file.head, entry.d.u.file.size, cb, data); if (err) { return err; } } } cwd[0] = dir.d.tail[0]; cwd[1] = dir.d.tail[1]; if (lfs_pairisnull(cwd)) { break; } } // iterate over any open files for (lfs_file_t *f = lfs->files; f; f = f->next) { if (f->flags & LFS_F_DIRTY) { int err = lfs_ctz_traverse(lfs, &lfs->rcache, &f->cache, f->head, f->size, cb, data); if (err) { return err; } } if (f->flags & LFS_F_WRITING) { int err = lfs_ctz_traverse(lfs, &lfs->rcache, &f->cache, f->block, f->pos, cb, data); if (err) { return err; } } } return 0; } static int lfs_pred(lfs_t *lfs, const lfs_block_t dir[2], lfs_dir_t *pdir) { if (lfs_pairisnull(lfs->root)) { return 0; } // iterate over all directory directory entries int err = lfs_dir_fetch(lfs, pdir, (const lfs_block_t[2]){0, 1}); if (err) { return err; } while (!lfs_pairisnull(pdir->d.tail)) { if (lfs_paircmp(pdir->d.tail, dir) == 0) { return true; } err = lfs_dir_fetch(lfs, pdir, pdir->d.tail); if (err) { return err; } } return false; } static int lfs_parent(lfs_t *lfs, const lfs_block_t dir[2], lfs_dir_t *parent, lfs_entry_t *entry) { if (lfs_pairisnull(lfs->root)) { return 0; } parent->d.tail[0] = 0; parent->d.tail[1] = 1; // iterate over all directory directory entries while (!lfs_pairisnull(parent->d.tail)) { int err = lfs_dir_fetch(lfs, parent, parent->d.tail); if (err) { return err; } while (true) { err = lfs_dir_next(lfs, parent, entry); if (err && err != LFS_ERR_NOENT) { return err; } if (err == LFS_ERR_NOENT) { break; } if (((0x70 & entry->d.type) == (0x70 & LFS_TYPE_DIR)) && lfs_paircmp(entry->d.u.dir, dir) == 0) { return true; } } } return false; } static int lfs_moved(lfs_t *lfs, const void *e) { if (lfs_pairisnull(lfs->root)) { return 0; } // skip superblock lfs_dir_t cwd; int err = lfs_dir_fetch(lfs, &cwd, (const lfs_block_t[2]){0, 1}); if (err) { return err; } // iterate over all directory directory entries lfs_entry_t entry; while (!lfs_pairisnull(cwd.d.tail)) { err = lfs_dir_fetch(lfs, &cwd, cwd.d.tail); if (err) { return err; } while (true) { err = lfs_dir_next(lfs, &cwd, &entry); if (err && err != LFS_ERR_NOENT) { return err; } if (err == LFS_ERR_NOENT) { break; } if (!(0x80 & entry.d.type) && memcmp(&entry.d.u, e, sizeof(entry.d.u)) == 0) { return true; } } } return false; } static int lfs_relocate(lfs_t *lfs, const lfs_block_t oldpair[2], const lfs_block_t newpair[2]) { // find parent lfs_dir_t parent; lfs_entry_t entry; int res = lfs_parent(lfs, oldpair, &parent, &entry); if (res < 0) { return res; } if (res) { // update disk, this creates a desync entry.d.u.dir[0] = newpair[0]; entry.d.u.dir[1] = newpair[1]; int err = lfs_dir_update(lfs, &parent, &entry, NULL); if (err) { return err; } // update internal root if (lfs_paircmp(oldpair, lfs->root) == 0) { LFS_DEBUG("Relocating root %" PRIu32 " %" PRIu32, newpair[0], newpair[1]); lfs->root[0] = newpair[0]; lfs->root[1] = newpair[1]; } // clean up bad block, which should now be a desync return lfs_deorphan(lfs); } // find pred res = lfs_pred(lfs, oldpair, &parent); if (res < 0) { return res; } if (res) { // just replace bad pair, no desync can occur parent.d.tail[0] = newpair[0]; parent.d.tail[1] = newpair[1]; return lfs_dir_commit(lfs, &parent, NULL, 0); } // couldn't find dir, must be new return 0; } int lfs_deorphan(lfs_t *lfs) { lfs->deorphaned = true; if (lfs_pairisnull(lfs->root)) { return 0; } lfs_dir_t pdir = {.d.size = 0x80000000}; lfs_dir_t cwd = {.d.tail[0] = 0, .d.tail[1] = 1}; // iterate over all directory directory entries for (lfs_size_t i = 0; i < lfs->cfg->block_count; i++) { if (lfs_pairisnull(cwd.d.tail)) { return 0; } int err = lfs_dir_fetch(lfs, &cwd, cwd.d.tail); if (err) { return err; } // check head blocks for orphans if (!(0x80000000 & pdir.d.size)) { // check if we have a parent lfs_dir_t parent; lfs_entry_t entry; int res = lfs_parent(lfs, pdir.d.tail, &parent, &entry); if (res < 0) { return res; } if (!res) { // we are an orphan LFS_DEBUG("Found orphan %" PRIu32 " %" PRIu32, pdir.d.tail[0], pdir.d.tail[1]); pdir.d.tail[0] = cwd.d.tail[0]; pdir.d.tail[1] = cwd.d.tail[1]; err = lfs_dir_commit(lfs, &pdir, NULL, 0); if (err) { return err; } return 0; } if (!lfs_pairsync(entry.d.u.dir, pdir.d.tail)) { // we have desynced LFS_DEBUG("Found desync %" PRIu32 " %" PRIu32, entry.d.u.dir[0], entry.d.u.dir[1]); pdir.d.tail[0] = entry.d.u.dir[0]; pdir.d.tail[1] = entry.d.u.dir[1]; err = lfs_dir_commit(lfs, &pdir, NULL, 0); if (err) { return err; } return 0; } } // check entries for moves lfs_entry_t entry; while (true) { err = lfs_dir_next(lfs, &cwd, &entry); if (err && err != LFS_ERR_NOENT) { return err; } if (err == LFS_ERR_NOENT) { break; } // found moved entry if (entry.d.type & 0x80) { int moved = lfs_moved(lfs, &entry.d.u); if (moved < 0) { return moved; } if (moved) { LFS_DEBUG("Found move %" PRIu32 " %" PRIu32, entry.d.u.dir[0], entry.d.u.dir[1]); err = lfs_dir_remove(lfs, &cwd, &entry); if (err) { return err; } } else { LFS_DEBUG("Found partial move %" PRIu32 " %" PRIu32, entry.d.u.dir[0], entry.d.u.dir[1]); entry.d.type &= ~0x80; err = lfs_dir_update(lfs, &cwd, &entry, NULL); if (err) { return err; } } } } memcpy(&pdir, &cwd, sizeof(pdir)); } // If we reached here, we have more directory pairs than blocks in the // filesystem... So something must be horribly wrong return LFS_ERR_CORRUPT; } ================================================ FILE: src/platform/stm32wl/littlefs/lfs.h ================================================ /* * The little filesystem * * Copyright (c) 2017, Arm Limited. All rights reserved. * SPDX-License-Identifier: BSD-3-Clause */ #ifndef LFS_H #define LFS_H #include #include #ifdef __cplusplus extern "C" { #endif /// Version info /// // Software library version // Major (top-nibble), incremented on backwards incompatible changes // Minor (bottom-nibble), incremented on feature additions #define LFS_VERSION 0x00010006 #define LFS_VERSION_MAJOR (0xffff & (LFS_VERSION >> 16)) #define LFS_VERSION_MINOR (0xffff & (LFS_VERSION >> 0)) // Version of On-disk data structures // Major (top-nibble), incremented on backwards incompatible changes // Minor (bottom-nibble), incremented on feature additions #define LFS_DISK_VERSION 0x00010001 #define LFS_DISK_VERSION_MAJOR (0xffff & (LFS_DISK_VERSION >> 16)) #define LFS_DISK_VERSION_MINOR (0xffff & (LFS_DISK_VERSION >> 0)) /// Definitions /// // Type definitions typedef uint32_t lfs_size_t; typedef uint32_t lfs_off_t; typedef int32_t lfs_ssize_t; typedef int32_t lfs_soff_t; typedef uint32_t lfs_block_t; // Max name size in bytes #ifndef LFS_NAME_MAX #define LFS_NAME_MAX 255 #endif // Possible error codes, these are negative to allow // valid positive return values enum lfs_error { LFS_ERR_OK = 0, // No error LFS_ERR_IO = -5, // Error during device operation LFS_ERR_CORRUPT = -52, // Corrupted LFS_ERR_NOENT = -2, // No directory entry LFS_ERR_EXIST = -17, // Entry already exists LFS_ERR_NOTDIR = -20, // Entry is not a dir LFS_ERR_ISDIR = -21, // Entry is a dir LFS_ERR_NOTEMPTY = -39, // Dir is not empty LFS_ERR_BADF = -9, // Bad file number LFS_ERR_INVAL = -22, // Invalid parameter LFS_ERR_NOSPC = -28, // No space left on device LFS_ERR_NOMEM = -12, // No more memory available }; // File types enum lfs_type { LFS_TYPE_REG = 0x11, LFS_TYPE_DIR = 0x22, LFS_TYPE_SUPERBLOCK = 0x2e, }; // File open flags enum lfs_open_flags { // open flags LFS_O_RDONLY = 1, // Open a file as read only LFS_O_WRONLY = 2, // Open a file as write only LFS_O_RDWR = 3, // Open a file as read and write LFS_O_CREAT = 0x0100, // Create a file if it does not exist LFS_O_EXCL = 0x0200, // Fail if a file already exists LFS_O_TRUNC = 0x0400, // Truncate the existing file to zero size LFS_O_APPEND = 0x0800, // Move to end of file on every write // internally used flags LFS_F_DIRTY = 0x10000, // File does not match storage LFS_F_WRITING = 0x20000, // File has been written since last flush LFS_F_READING = 0x40000, // File has been read since last flush LFS_F_ERRED = 0x80000, // An error occured during write }; // File seek flags enum lfs_whence_flags { LFS_SEEK_SET = 0, // Seek relative to an absolute position LFS_SEEK_CUR = 1, // Seek relative to the current file position LFS_SEEK_END = 2, // Seek relative to the end of the file }; // Configuration provided during initialization of the littlefs struct lfs_config { // Opaque user provided context that can be used to pass // information to the block device operations void *context; // Read a region in a block. Negative error codes are propogated // to the user. int (*read)(const struct lfs_config *c, lfs_block_t block, lfs_off_t off, void *buffer, lfs_size_t size); // Program a region in a block. The block must have previously // been erased. Negative error codes are propogated to the user. // May return LFS_ERR_CORRUPT if the block should be considered bad. int (*prog)(const struct lfs_config *c, lfs_block_t block, lfs_off_t off, const void *buffer, lfs_size_t size); // Erase a block. A block must be erased before being programmed. // The state of an erased block is undefined. Negative error codes // are propogated to the user. // May return LFS_ERR_CORRUPT if the block should be considered bad. int (*erase)(const struct lfs_config *c, lfs_block_t block); // Sync the state of the underlying block device. Negative error codes // are propogated to the user. int (*sync)(const struct lfs_config *c); // Minimum size of a block read. This determines the size of read buffers. // This may be larger than the physical read size to improve performance // by caching more of the block device. lfs_size_t read_size; // Minimum size of a block program. This determines the size of program // buffers. This may be larger than the physical program size to improve // performance by caching more of the block device. // Must be a multiple of the read size. lfs_size_t prog_size; // Size of an erasable block. This does not impact ram consumption and // may be larger than the physical erase size. However, this should be // kept small as each file currently takes up an entire block. // Must be a multiple of the program size. lfs_size_t block_size; // Number of erasable blocks on the device. lfs_size_t block_count; // Number of blocks to lookahead during block allocation. A larger // lookahead reduces the number of passes required to allocate a block. // The lookahead buffer requires only 1 bit per block so it can be quite // large with little ram impact. Should be a multiple of 32. lfs_size_t lookahead; // Optional, statically allocated read buffer. Must be read sized. void *read_buffer; // Optional, statically allocated program buffer. Must be program sized. void *prog_buffer; // Optional, statically allocated lookahead buffer. Must be 1 bit per // lookahead block. void *lookahead_buffer; // Optional, statically allocated buffer for files. Must be program sized. // If enabled, only one file may be opened at a time. void *file_buffer; }; // Optional configuration provided during lfs_file_opencfg struct lfs_file_config { // Optional, statically allocated buffer for files. Must be program sized. // If NULL, malloc will be used by default. void *buffer; }; // File info structure struct lfs_info { // Type of the file, either LFS_TYPE_REG or LFS_TYPE_DIR uint8_t type; // Size of the file, only valid for REG files lfs_size_t size; // Name of the file stored as a null-terminated string char name[LFS_NAME_MAX + 1]; }; /// littlefs data structures /// typedef struct lfs_entry { lfs_off_t off; struct lfs_disk_entry { uint8_t type; uint8_t elen; uint8_t alen; uint8_t nlen; union { struct { lfs_block_t head; lfs_size_t size; } file; lfs_block_t dir[2]; } u; } d; } lfs_entry_t; typedef struct lfs_cache { lfs_block_t block; lfs_off_t off; uint8_t *buffer; } lfs_cache_t; typedef struct lfs_file { struct lfs_file *next; lfs_block_t pair[2]; lfs_off_t poff; lfs_block_t head; lfs_size_t size; const struct lfs_file_config *cfg; uint32_t flags; lfs_off_t pos; lfs_block_t block; lfs_off_t off; lfs_cache_t cache; } lfs_file_t; typedef struct lfs_dir { struct lfs_dir *next; lfs_block_t pair[2]; lfs_off_t off; lfs_block_t head[2]; lfs_off_t pos; struct lfs_disk_dir { uint32_t rev; lfs_size_t size; lfs_block_t tail[2]; } d; } lfs_dir_t; typedef struct lfs_superblock { lfs_off_t off; struct lfs_disk_superblock { uint8_t type; uint8_t elen; uint8_t alen; uint8_t nlen; lfs_block_t root[2]; uint32_t block_size; uint32_t block_count; uint32_t version; char magic[8]; } d; } lfs_superblock_t; typedef struct lfs_free { lfs_block_t off; lfs_block_t size; lfs_block_t i; lfs_block_t ack; uint32_t *buffer; } lfs_free_t; // The littlefs type typedef struct lfs { const struct lfs_config *cfg; lfs_block_t root[2]; lfs_file_t *files; lfs_dir_t *dirs; lfs_cache_t rcache; lfs_cache_t pcache; lfs_free_t free; bool deorphaned; } lfs_t; /// Filesystem functions /// // Format a block device with the littlefs // // Requires a littlefs object and config struct. This clobbers the littlefs // object, and does not leave the filesystem mounted. The config struct must // be zeroed for defaults and backwards compatibility. // // Returns a negative error code on failure. int lfs_format(lfs_t *lfs, const struct lfs_config *config); // Mounts a littlefs // // Requires a littlefs object and config struct. Multiple filesystems // may be mounted simultaneously with multiple littlefs objects. Both // lfs and config must be allocated while mounted. The config struct must // be zeroed for defaults and backwards compatibility. // // Returns a negative error code on failure. int lfs_mount(lfs_t *lfs, const struct lfs_config *config); // Unmounts a littlefs // // Does nothing besides releasing any allocated resources. // Returns a negative error code on failure. int lfs_unmount(lfs_t *lfs); /// General operations /// // Removes a file or directory // // If removing a directory, the directory must be empty. // Returns a negative error code on failure. int lfs_remove(lfs_t *lfs, const char *path); // Rename or move a file or directory // // If the destination exists, it must match the source in type. // If the destination is a directory, the directory must be empty. // // Returns a negative error code on failure. int lfs_rename(lfs_t *lfs, const char *oldpath, const char *newpath); // Find info about a file or directory // // Fills out the info structure, based on the specified file or directory. // Returns a negative error code on failure. int lfs_stat(lfs_t *lfs, const char *path, struct lfs_info *info); /// File operations /// // Open a file // // The mode that the file is opened in is determined by the flags, which // are values from the enum lfs_open_flags that are bitwise-ored together. // // Returns a negative error code on failure. int lfs_file_open(lfs_t *lfs, lfs_file_t *file, const char *path, int flags); // Open a file with extra configuration // // The mode that the file is opened in is determined by the flags, which // are values from the enum lfs_open_flags that are bitwise-ored together. // // The config struct provides additional config options per file as described // above. The config struct must be allocated while the file is open, and the // config struct must be zeroed for defaults and backwards compatibility. // // Returns a negative error code on failure. int lfs_file_opencfg(lfs_t *lfs, lfs_file_t *file, const char *path, int flags, const struct lfs_file_config *config); // Close a file // // Any pending writes are written out to storage as though // sync had been called and releases any allocated resources. // // Returns a negative error code on failure. int lfs_file_close(lfs_t *lfs, lfs_file_t *file); // Synchronize a file on storage // // Any pending writes are written out to storage. // Returns a negative error code on failure. int lfs_file_sync(lfs_t *lfs, lfs_file_t *file); // Read data from file // // Takes a buffer and size indicating where to store the read data. // Returns the number of bytes read, or a negative error code on failure. lfs_ssize_t lfs_file_read(lfs_t *lfs, lfs_file_t *file, void *buffer, lfs_size_t size); // Write data to file // // Takes a buffer and size indicating the data to write. The file will not // actually be updated on the storage until either sync or close is called. // // Returns the number of bytes written, or a negative error code on failure. lfs_ssize_t lfs_file_write(lfs_t *lfs, lfs_file_t *file, const void *buffer, lfs_size_t size); // Change the position of the file // // The change in position is determined by the offset and whence flag. // Returns the old position of the file, or a negative error code on failure. lfs_soff_t lfs_file_seek(lfs_t *lfs, lfs_file_t *file, lfs_soff_t off, int whence); // Truncates the size of the file to the specified size // // Returns a negative error code on failure. int lfs_file_truncate(lfs_t *lfs, lfs_file_t *file, lfs_off_t size); // Return the position of the file // // Equivalent to lfs_file_seek(lfs, file, 0, LFS_SEEK_CUR) // Returns the position of the file, or a negative error code on failure. lfs_soff_t lfs_file_tell(lfs_t *lfs, const lfs_file_t *file); // Change the position of the file to the beginning of the file // // Equivalent to lfs_file_seek(lfs, file, 0, LFS_SEEK_CUR) // Returns a negative error code on failure. int lfs_file_rewind(lfs_t *lfs, lfs_file_t *file); // Return the size of the file // // Similar to lfs_file_seek(lfs, file, 0, LFS_SEEK_END) // Returns the size of the file, or a negative error code on failure. lfs_soff_t lfs_file_size(lfs_t *lfs, lfs_file_t *file); /// Directory operations /// // Create a directory // // Returns a negative error code on failure. int lfs_mkdir(lfs_t *lfs, const char *path); // Open a directory // // Once open a directory can be used with read to iterate over files. // Returns a negative error code on failure. int lfs_dir_open(lfs_t *lfs, lfs_dir_t *dir, const char *path); // Close a directory // // Releases any allocated resources. // Returns a negative error code on failure. int lfs_dir_close(lfs_t *lfs, lfs_dir_t *dir); // Read an entry in the directory // // Fills out the info structure, based on the specified file or directory. // Returns a negative error code on failure. int lfs_dir_read(lfs_t *lfs, lfs_dir_t *dir, struct lfs_info *info); // Change the position of the directory // // The new off must be a value previous returned from tell and specifies // an absolute offset in the directory seek. // // Returns a negative error code on failure. int lfs_dir_seek(lfs_t *lfs, lfs_dir_t *dir, lfs_off_t off); // Change the position of the directory to the beginning of the directory // // Returns a negative error code on failure. int lfs_dir_rewind(lfs_t *lfs, lfs_dir_t *dir); /// Miscellaneous littlefs specific operations /// // Traverse through all blocks in use by the filesystem // // The provided callback will be called with each block address that is // currently in use by the filesystem. This can be used to determine which // blocks are in use or how much of the storage is available. // // Returns a negative error code on failure. int lfs_traverse(lfs_t *lfs, int (*cb)(void *, lfs_block_t), void *data); // Prunes any recoverable errors that may have occured in the filesystem // // Not needed to be called by user unless an operation is interrupted // but the filesystem is still mounted. This is already called on first // allocation. // // Returns a negative error code on failure. int lfs_deorphan(lfs_t *lfs); #ifdef __cplusplus } /* extern "C" */ #endif #endif ================================================ FILE: src/platform/stm32wl/littlefs/lfs_util.c ================================================ /* * lfs util functions * * Copyright (c) 2017, Arm Limited. All rights reserved. * SPDX-License-Identifier: BSD-3-Clause */ #include "lfs_util.h" // Only compile if user does not provide custom config #ifndef LFS_CONFIG // Software CRC implementation with small lookup table void lfs_crc(uint32_t *restrict crc, const void *buffer, size_t size) { static const uint32_t rtable[16] = { 0x00000000, 0x1db71064, 0x3b6e20c8, 0x26d930ac, 0x76dc4190, 0x6b6b51f4, 0x4db26158, 0x5005713c, 0xedb88320, 0xf00f9344, 0xd6d6a3e8, 0xcb61b38c, 0x9b64c2b0, 0x86d3d2d4, 0xa00ae278, 0xbdbdf21c, }; const uint8_t *data = buffer; for (size_t i = 0; i < size; i++) { *crc = (*crc >> 4) ^ rtable[(*crc ^ (data[i] >> 0)) & 0xf]; *crc = (*crc >> 4) ^ rtable[(*crc ^ (data[i] >> 4)) & 0xf]; } } #endif ================================================ FILE: src/platform/stm32wl/littlefs/lfs_util.h ================================================ /* * lfs utility functions * * Copyright (c) 2017, Arm Limited. All rights reserved. * SPDX-License-Identifier: BSD-3-Clause */ #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 !CFG_DEBUG #define LFS_NO_DEBUG #define LFS_NO_WARN #define LFS_NO_ERROR #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 #define LFS_DEBUG(fmt, ...) printf("lfs debug:%d: " fmt "\n", __LINE__, __VA_ARGS__) #else #define LFS_DEBUG(fmt, ...) #endif #ifndef LFS_NO_WARN #define LFS_WARN(fmt, ...) printf("lfs warn:%d: " fmt "\n", __LINE__, __VA_ARGS__) #else #define LFS_WARN(fmt, ...) #endif #ifndef LFS_NO_ERROR #define LFS_ERROR(fmt, ...) printf("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 #define 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 return malloc(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 free(p); #else (void)p; #endif } #ifdef __cplusplus } /* extern "C" */ #endif #endif #endif ================================================ FILE: src/platform/stm32wl/main-stm32wl.cpp ================================================ #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) {} // Hacks to force more code and data out. // By default __assert_func uses fiprintf which pulls in stdio. extern "C" void __wrap___assert_func(const char *, int, const char *, const char *) { while (true) ; return; } // By default strerror has a lot of strings we probably don't use. Make it return an empty string instead. char empty = 0; extern "C" char *__wrap_strerror(int) { return ∅ } #ifdef MESHTASTIC_EXCLUDE_TZ struct _reent; // Even if you don't use timezones, mktime will try to set the timezone anyway with _tzset_unlocked(), which pulls in scanf and // friends. The timezone is initialized to UTC by default. extern "C" void __wrap__tzset_unlocked_r(struct _reent *reent_ptr) { return; } #endif ================================================ FILE: src/power/PowerHAL.cpp ================================================ #include "PowerHAL.h" void powerHAL_init() { return powerHAL_platformInit(); } __attribute__((weak, noinline)) void powerHAL_platformInit() {} __attribute__((weak, noinline)) bool powerHAL_isPowerLevelSafe() { return true; } __attribute__((weak, noinline)) bool powerHAL_isVBUSConnected() { return false; } ================================================ FILE: src/power/PowerHAL.h ================================================ /* Power Hardware Abstraction Layer. Set of API calls to offload power management, measurements, reboots, etc to the platform and variant code to avoid #ifdef spaghetti hell and limitless device-based edge cases in the main firmware code Functions declared here (with exception of powerHAL_init) should be defined in platform specific codebase. Default function body does usually nothing. */ // Initialize HAL layer. Call it as early as possible during device boot // do not overwrite it as it's not declared with "weak" attribute. void powerHAL_init(); // platform specific init code if needed to be run early on boot void powerHAL_platformInit(); // Return true if current battery level is safe for device operation (for example flash writes). // This should be reported by power failure comparator (NRF52) or similar circuits on other platforms. // Do not use battery ADC as improper ADC configuration may prevent device from booting. bool powerHAL_isPowerLevelSafe(); // return if USB voltage is connected bool powerHAL_isVBUSConnected(); ================================================ FILE: src/power.h ================================================ #pragma once #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 // Device specific curves go in variant.h #ifndef OCV_ARRAY #define OCV_ARRAY 4190, 4050, 3990, 3890, 3800, 3720, 3630, 3530, 3420, 3300, 3100 #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 #include "modules/Telemetry/Sensor/nullSensor.h" #if __has_include() #include "modules/Telemetry/Sensor/INA219Sensor.h" extern INA219Sensor ina219Sensor; #else extern NullSensor ina219Sensor; #endif #if __has_include() #include "modules/Telemetry/Sensor/INA226Sensor.h" extern INA226Sensor ina226Sensor; #else extern NullSensor ina226Sensor; #endif #if __has_include() #include "modules/Telemetry/Sensor/INA260Sensor.h" extern INA260Sensor ina260Sensor; #else extern NullSensor ina260Sensor; #endif #if __has_include() #include "modules/Telemetry/Sensor/INA3221Sensor.h" extern INA3221Sensor ina3221Sensor; #else extern NullSensor ina3221Sensor; #endif #endif #if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR #if __has_include() #include "modules/Telemetry/Sensor/MAX17048Sensor.h" extern MAX17048Sensor max17048Sensor; #else extern NullSensor max17048Sensor; #endif #endif #if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && HAS_RAKPROT #include "modules/Telemetry/Sensor/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 powerCommandsCheck(); void readPowerStatus(); virtual bool setup(); virtual int32_t runOnce() override; void setStatusHandler(meshtastic::PowerStatus *handler) { statusHandler = handler; } const uint16_t OCV[11] = {OCV_ARRAY}; protected: meshtastic::PowerStatus *statusHandler; /// Setup a xpowers chip axp192/axp2101, return true if found bool axpChipInit(); /// Setup a simple ADC input based battery sensor bool analogInit(); /// Setup cw2015 battery level sensor bool cw2015Init(); /// Setup a 17048 battery level sensor bool max17048Init(); /// Setup a Lipo charger bool lipoChargerInit(); /// Setup a meshSolar battery sensor bool meshSolarInit(); /// Setup a serial battery sensor bool serialBatteryInit(); private: void shutdown(); void reboot(); // open circuit voltage lookup table uint8_t low_voltage_counter; uint32_t lastLogTime = 0; #ifdef DEBUG_HEAP uint32_t lastheap; #endif }; void battery_adcEnable(); extern Power *power; ================================================ FILE: src/serialization/JSON.cpp ================================================ /* * 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; } ================================================ FILE: src/serialization/JSON.h ================================================ /* * 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 ================================================ FILE: src/serialization/JSONValue.cpp ================================================ /* * 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 "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 < 0x20 || chr == 0x7F) { char buf[7]; snprintf(buf, sizeof(buf), "\\u%04x", chr); str_out += buf; } else if (chr < 0x80) { str_out += chr; } else { str_out += chr; size_t remain = str.end() - iter - 1; if ((chr & 0xE0) == 0xC0 && remain >= 1) { ++iter; str_out += *iter; } else if ((chr & 0xF0) == 0xE0 && remain >= 2) { str_out += *(++iter); str_out += *(++iter); } else if ((chr & 0xF8) == 0xF0 && remain >= 3) { str_out += *(++iter); str_out += *(++iter); str_out += *(++iter); } } ++iter; } str_out += "\""; return str_out; } /** * 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; } ================================================ FILE: src/serialization/JSONValue.h ================================================ /* * 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 ================================================ FILE: src/serialization/MeshPacketSerializer.cpp ================================================ #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" #include static const char *errStr = "Error decoding proto for %s message!"; std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp, bool shouldLog) { // the created jsonObj is immutable after creation, so // we need to do the heavy lifting before assembling it. std::string msgType; JSONObject jsonObj; 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) { // If battery is present, encode the battery level value // TODO - Add a condition to send a code for a non-present value if (decoded->variant.device_metrics.has_battery_level) { msgPayload["battery_level"] = new JSONValue((int)decoded->variant.device_metrics.battery_level); } msgPayload["voltage"] = new JSONValue(decoded->variant.device_metrics.voltage); msgPayload["channel_utilization"] = new JSONValue(decoded->variant.device_metrics.channel_utilization); msgPayload["air_util_tx"] = new JSONValue(decoded->variant.device_metrics.air_util_tx); msgPayload["uptime_seconds"] = new JSONValue((unsigned int)decoded->variant.device_metrics.uptime_seconds); } else if (decoded->which_variant == meshtastic_Telemetry_environment_metrics_tag) { // Avoid sending 0s for sensors that could be 0 if (decoded->variant.environment_metrics.has_temperature) { msgPayload["temperature"] = new JSONValue(decoded->variant.environment_metrics.temperature); } if (decoded->variant.environment_metrics.has_relative_humidity) { msgPayload["relative_humidity"] = new JSONValue(decoded->variant.environment_metrics.relative_humidity); } if (decoded->variant.environment_metrics.has_barometric_pressure) { msgPayload["barometric_pressure"] = new JSONValue(decoded->variant.environment_metrics.barometric_pressure); } if (decoded->variant.environment_metrics.has_gas_resistance) { msgPayload["gas_resistance"] = new JSONValue(decoded->variant.environment_metrics.gas_resistance); } if (decoded->variant.environment_metrics.has_voltage) { msgPayload["voltage"] = new JSONValue(decoded->variant.environment_metrics.voltage); } if (decoded->variant.environment_metrics.has_current) { msgPayload["current"] = new JSONValue(decoded->variant.environment_metrics.current); } if (decoded->variant.environment_metrics.has_lux) { msgPayload["lux"] = new JSONValue(decoded->variant.environment_metrics.lux); } if (decoded->variant.environment_metrics.has_white_lux) { msgPayload["white_lux"] = new JSONValue(decoded->variant.environment_metrics.white_lux); } if (decoded->variant.environment_metrics.has_iaq) { msgPayload["iaq"] = new JSONValue((uint)decoded->variant.environment_metrics.iaq); } if (decoded->variant.environment_metrics.has_distance) { msgPayload["distance"] = new JSONValue(decoded->variant.environment_metrics.distance); } if (decoded->variant.environment_metrics.has_wind_speed) { msgPayload["wind_speed"] = new JSONValue(decoded->variant.environment_metrics.wind_speed); } if (decoded->variant.environment_metrics.has_wind_direction) { msgPayload["wind_direction"] = new JSONValue((uint)decoded->variant.environment_metrics.wind_direction); } if (decoded->variant.environment_metrics.has_wind_gust) { msgPayload["wind_gust"] = new JSONValue(decoded->variant.environment_metrics.wind_gust); } if (decoded->variant.environment_metrics.has_wind_lull) { msgPayload["wind_lull"] = new JSONValue(decoded->variant.environment_metrics.wind_lull); } if (decoded->variant.environment_metrics.has_radiation) { msgPayload["radiation"] = new JSONValue(decoded->variant.environment_metrics.radiation); } if (decoded->variant.environment_metrics.has_ir_lux) { msgPayload["ir_lux"] = new JSONValue(decoded->variant.environment_metrics.ir_lux); } if (decoded->variant.environment_metrics.has_uv_lux) { msgPayload["uv_lux"] = new JSONValue(decoded->variant.environment_metrics.uv_lux); } if (decoded->variant.environment_metrics.has_weight) { msgPayload["weight"] = new JSONValue(decoded->variant.environment_metrics.weight); } if (decoded->variant.environment_metrics.has_rainfall_1h) { msgPayload["rainfall_1h"] = new JSONValue(decoded->variant.environment_metrics.rainfall_1h); } if (decoded->variant.environment_metrics.has_rainfall_24h) { msgPayload["rainfall_24h"] = new JSONValue(decoded->variant.environment_metrics.rainfall_24h); } if (decoded->variant.environment_metrics.has_soil_moisture) { msgPayload["soil_moisture"] = new JSONValue((uint)decoded->variant.environment_metrics.soil_moisture); } if (decoded->variant.environment_metrics.has_soil_temperature) { msgPayload["soil_temperature"] = new JSONValue(decoded->variant.environment_metrics.soil_temperature); } } else if (decoded->which_variant == meshtastic_Telemetry_air_quality_metrics_tag) { if (decoded->variant.air_quality_metrics.has_pm10_standard) { msgPayload["pm10"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm10_standard); } if (decoded->variant.air_quality_metrics.has_pm25_standard) { msgPayload["pm25"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm25_standard); } if (decoded->variant.air_quality_metrics.has_pm100_standard) { msgPayload["pm100"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm100_standard); } if (decoded->variant.air_quality_metrics.has_co2) { msgPayload["co2"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.co2); } if (decoded->variant.air_quality_metrics.has_co2_temperature) { msgPayload["co2_temperature"] = new JSONValue(decoded->variant.air_quality_metrics.co2_temperature); } if (decoded->variant.air_quality_metrics.has_co2_humidity) { msgPayload["co2_humidity"] = new JSONValue(decoded->variant.air_quality_metrics.co2_humidity); } if (decoded->variant.air_quality_metrics.has_form_formaldehyde) { msgPayload["form_formaldehyde"] = new JSONValue(decoded->variant.air_quality_metrics.form_formaldehyde); } if (decoded->variant.air_quality_metrics.has_form_temperature) { msgPayload["form_temperature"] = new JSONValue(decoded->variant.air_quality_metrics.form_temperature); } if (decoded->variant.air_quality_metrics.has_form_humidity) { msgPayload["form_humidity"] = new JSONValue(decoded->variant.air_quality_metrics.form_humidity); } } else if (decoded->which_variant == meshtastic_Telemetry_power_metrics_tag) { if (decoded->variant.power_metrics.has_ch1_voltage) { msgPayload["voltage_ch1"] = new JSONValue(decoded->variant.power_metrics.ch1_voltage); } if (decoded->variant.power_metrics.has_ch1_current) { msgPayload["current_ch1"] = new JSONValue(decoded->variant.power_metrics.ch1_current); } if (decoded->variant.power_metrics.has_ch2_voltage) { msgPayload["voltage_ch2"] = new JSONValue(decoded->variant.power_metrics.ch2_voltage); } if (decoded->variant.power_metrics.has_ch2_current) { msgPayload["current_ch2"] = new JSONValue(decoded->variant.power_metrics.ch2_current); } if (decoded->variant.power_metrics.has_ch3_voltage) { msgPayload["voltage_ch3"] = new JSONValue(decoded->variant.power_metrics.ch3_voltage); } if (decoded->variant.power_metrics.has_ch3_current) { msgPayload["current_ch3"] = new JSONValue(decoded->variant.power_metrics.ch3_current); } } jsonObj["payload"] = new JSONValue(msgPayload); } else if (shouldLog) { LOG_ERROR(errStr, msgType.c_str()); } break; } 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 JSONArray routeBack; // Route this message took back JSONArray snrTowards; // Snr for forward route JSONArray snrBack; // Snr for reverse route // Lambda function for adding a long name to the route auto addToRoute = [](JSONArray *route, NodeNum num) { char long_name[40] = "Unknown"; meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(num); bool name_known = node ? node->has_user : false; if (name_known) memcpy(long_name, node->user.long_name, sizeof(long_name)); route->push_back(new JSONValue(long_name)); }; addToRoute(&route, mp->to); // Started at the original transmitter (destination of response) for (uint8_t i = 0; i < decoded->route_count; i++) { addToRoute(&route, decoded->route[i]); } addToRoute(&route, mp->from); // Ended at the original destination (source of response) addToRoute(&routeBack, mp->from); // Started at the original destination (source of response) for (uint8_t i = 0; i < decoded->route_back_count; i++) { addToRoute(&routeBack, decoded->route_back[i]); } addToRoute(&routeBack, mp->to); // Ended at the original transmitter (destination of response) for (uint8_t i = 0; i < decoded->snr_back_count; i++) { snrBack.push_back(new JSONValue((float)decoded->snr_back[i] / 4)); } for (uint8_t i = 0; i < decoded->snr_towards_count; i++) { snrTowards.push_back(new JSONValue((float)decoded->snr_towards[i] / 4)); } msgPayload["route"] = new JSONValue(route); msgPayload["route_back"] = new JSONValue(routeBack); msgPayload["snr_back"] = new JSONValue(snrBack); msgPayload["snr_towards"] = new JSONValue(snrTowards); jsonObj["payload"] = new JSONValue(msgPayload); } else if (shouldLog) { LOG_ERROR(errStr, msgType.c_str()); } } break; } case meshtastic_PortNum_DETECTION_SENSOR_APP: { msgType = "detection"; char payloadStr[(mp->decoded.payload.size) + 1]; memcpy(payloadStr, mp->decoded.payload.bytes, mp->decoded.payload.size); payloadStr[mp->decoded.payload.size] = 0; // null terminated string msgPayload["text"] = new JSONValue(payloadStr); jsonObj["payload"] = new JSONValue(msgPayload); break; } #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(nodeDB->getNodeId().c_str()); if (mp->rx_rssi != 0) jsonObj["rssi"] = new JSONValue((int)mp->rx_rssi); if (mp->rx_snr != 0) jsonObj["snr"] = new JSONValue((float)mp->rx_snr); const int8_t hopsAway = getHopsAway(*mp); if (hopsAway >= 0) { jsonObj["hops_away"] = new JSONValue((unsigned int)(hopsAway)); jsonObj["hop_start"] = new JSONValue((unsigned int)(mp->hop_start)); } // serialize and write it to the stream JSONValue *value = new JSONValue(jsonObj); std::string jsonStr = value->Stringify(); 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); const int8_t hopsAway = getHopsAway(*mp); if (hopsAway >= 0) { jsonObj["hops_away"] = new JSONValue((unsigned int)(hopsAway)); jsonObj["hop_start"] = new JSONValue((unsigned int)(mp->hop_start)); } jsonObj["size"] = new JSONValue((unsigned int)mp->encrypted.size); auto encryptedStr = bytesToHex(mp->encrypted.bytes, mp->encrypted.size); jsonObj["bytes"] = new JSONValue(encryptedStr.c_str()); // serialize and write it to the stream JSONValue *value = new JSONValue(jsonObj); std::string jsonStr = value->Stringify(); delete value; return jsonStr; } #endif ================================================ FILE: src/serialization/MeshPacketSerializer.h ================================================ #include #include static const char hexChars[16] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; class MeshPacketSerializer { public: static std::string JsonSerialize(const meshtastic_MeshPacket *mp, bool shouldLog = true); static std::string JsonSerializeEncrypted(const meshtastic_MeshPacket *mp); 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; } }; ================================================ FILE: src/serialization/MeshPacketSerializer_nRF52.cpp ================================================ #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) { // If battery is present, encode the battery level value // TODO - Add a condition to send a code for a non-present value if (decoded->variant.device_metrics.has_battery_level) { jsonObj["payload"]["battery_level"] = (int)decoded->variant.device_metrics.battery_level; } jsonObj["payload"]["voltage"] = decoded->variant.device_metrics.voltage; jsonObj["payload"]["channel_utilization"] = decoded->variant.device_metrics.channel_utilization; jsonObj["payload"]["air_util_tx"] = decoded->variant.device_metrics.air_util_tx; jsonObj["payload"]["uptime_seconds"] = (unsigned int)decoded->variant.device_metrics.uptime_seconds; } else if (decoded->which_variant == meshtastic_Telemetry_environment_metrics_tag) { if (decoded->variant.environment_metrics.has_temperature) { jsonObj["payload"]["temperature"] = decoded->variant.environment_metrics.temperature; } if (decoded->variant.environment_metrics.has_relative_humidity) { jsonObj["payload"]["relative_humidity"] = decoded->variant.environment_metrics.relative_humidity; } if (decoded->variant.environment_metrics.has_barometric_pressure) { jsonObj["payload"]["barometric_pressure"] = decoded->variant.environment_metrics.barometric_pressure; } if (decoded->variant.environment_metrics.has_gas_resistance) { jsonObj["payload"]["gas_resistance"] = decoded->variant.environment_metrics.gas_resistance; } if (decoded->variant.environment_metrics.has_voltage) { jsonObj["payload"]["voltage"] = decoded->variant.environment_metrics.voltage; } if (decoded->variant.environment_metrics.has_current) { jsonObj["payload"]["current"] = decoded->variant.environment_metrics.current; } if (decoded->variant.environment_metrics.has_lux) { jsonObj["payload"]["lux"] = decoded->variant.environment_metrics.lux; } if (decoded->variant.environment_metrics.has_white_lux) { jsonObj["payload"]["white_lux"] = decoded->variant.environment_metrics.white_lux; } if (decoded->variant.environment_metrics.has_iaq) { jsonObj["payload"]["iaq"] = (uint)decoded->variant.environment_metrics.iaq; } if (decoded->variant.environment_metrics.has_wind_speed) { jsonObj["payload"]["wind_speed"] = decoded->variant.environment_metrics.wind_speed; } if (decoded->variant.environment_metrics.has_wind_direction) { jsonObj["payload"]["wind_direction"] = (uint)decoded->variant.environment_metrics.wind_direction; } if (decoded->variant.environment_metrics.has_wind_gust) { jsonObj["payload"]["wind_gust"] = decoded->variant.environment_metrics.wind_gust; } if (decoded->variant.environment_metrics.has_wind_lull) { jsonObj["payload"]["wind_lull"] = decoded->variant.environment_metrics.wind_lull; } if (decoded->variant.environment_metrics.has_radiation) { jsonObj["payload"]["radiation"] = decoded->variant.environment_metrics.radiation; } } else if (decoded->which_variant == meshtastic_Telemetry_air_quality_metrics_tag) { if (decoded->variant.air_quality_metrics.has_pm10_standard) { jsonObj["payload"]["pm10"] = (unsigned int)decoded->variant.air_quality_metrics.pm10_standard; } if (decoded->variant.air_quality_metrics.has_pm25_standard) { jsonObj["payload"]["pm25"] = (unsigned int)decoded->variant.air_quality_metrics.pm25_standard; } if (decoded->variant.air_quality_metrics.has_pm100_standard) { jsonObj["payload"]["pm100"] = (unsigned int)decoded->variant.air_quality_metrics.pm100_standard; } if (decoded->variant.air_quality_metrics.has_co2) { jsonObj["payload"]["co2"] = (unsigned int)decoded->variant.air_quality_metrics.co2; } if (decoded->variant.air_quality_metrics.has_co2_temperature) { jsonObj["payload"]["co2_temperature"] = decoded->variant.air_quality_metrics.co2_temperature; } if (decoded->variant.air_quality_metrics.has_co2_humidity) { jsonObj["payload"]["co2_humidity"] = decoded->variant.air_quality_metrics.co2_humidity; } if (decoded->variant.air_quality_metrics.has_form_formaldehyde) { jsonObj["payload"]["form_formaldehyde"] = decoded->variant.air_quality_metrics.form_formaldehyde; } if (decoded->variant.air_quality_metrics.has_form_temperature) { jsonObj["payload"]["form_temperature"] = decoded->variant.air_quality_metrics.form_temperature; } if (decoded->variant.air_quality_metrics.has_form_humidity) { jsonObj["payload"]["form_humidity"] = decoded->variant.air_quality_metrics.form_humidity; } } else if (decoded->which_variant == meshtastic_Telemetry_power_metrics_tag) { if (decoded->variant.power_metrics.has_ch1_voltage) { jsonObj["payload"]["voltage_ch1"] = decoded->variant.power_metrics.ch1_voltage; } if (decoded->variant.power_metrics.has_ch1_current) { jsonObj["payload"]["current_ch1"] = decoded->variant.power_metrics.ch1_current; } if (decoded->variant.power_metrics.has_ch2_voltage) { jsonObj["payload"]["voltage_ch2"] = decoded->variant.power_metrics.ch2_voltage; } if (decoded->variant.power_metrics.has_ch2_current) { jsonObj["payload"]["current_ch2"] = decoded->variant.power_metrics.ch2_current; } if (decoded->variant.power_metrics.has_ch3_voltage) { jsonObj["payload"]["voltage_ch3"] = decoded->variant.power_metrics.ch3_voltage; } if (decoded->variant.power_metrics.has_ch3_current) { jsonObj["payload"]["current_ch3"] = decoded->variant.power_metrics.ch3_current; } } } else if (shouldLog) { LOG_ERROR("Error decoding proto for telemetry message!"); return ""; } break; } case meshtastic_PortNum_NODEINFO_APP: { msgType = "nodeinfo"; meshtastic_User scratch; meshtastic_User *decoded = NULL; memset(&scratch, 0, sizeof(scratch)); if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_User_msg, &scratch)) { decoded = &scratch; jsonObj["payload"]["id"] = decoded->id; jsonObj["payload"]["longname"] = decoded->long_name; jsonObj["payload"]["shortname"] = decoded->short_name; jsonObj["payload"]["hardware"] = decoded->hw_model; jsonObj["payload"]["role"] = (int)decoded->role; } else if (shouldLog) { LOG_ERROR("Error decoding proto for nodeinfo message!"); return ""; } break; } case meshtastic_PortNum_POSITION_APP: { msgType = "position"; meshtastic_Position scratch; meshtastic_Position *decoded = NULL; memset(&scratch, 0, sizeof(scratch)); if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_Position_msg, &scratch)) { decoded = &scratch; if ((int)decoded->time) { jsonObj["payload"]["time"] = (unsigned int)decoded->time; } if ((int)decoded->timestamp) { jsonObj["payload"]["timestamp"] = (unsigned int)decoded->timestamp; } jsonObj["payload"]["latitude_i"] = (int)decoded->latitude_i; jsonObj["payload"]["longitude_i"] = (int)decoded->longitude_i; if ((int)decoded->altitude) { jsonObj["payload"]["altitude"] = (int)decoded->altitude; } if ((int)decoded->ground_speed) { jsonObj["payload"]["ground_speed"] = (unsigned int)decoded->ground_speed; } if (int(decoded->ground_track)) { jsonObj["payload"]["ground_track"] = (unsigned int)decoded->ground_track; } if (int(decoded->sats_in_view)) { jsonObj["payload"]["sats_in_view"] = (unsigned int)decoded->sats_in_view; } if ((int)decoded->PDOP) { jsonObj["payload"]["PDOP"] = (int)decoded->PDOP; } if ((int)decoded->HDOP) { jsonObj["payload"]["HDOP"] = (int)decoded->HDOP; } if ((int)decoded->VDOP) { jsonObj["payload"]["VDOP"] = (int)decoded->VDOP; } if ((int)decoded->precision_bits) { jsonObj["payload"]["precision_bits"] = (int)decoded->precision_bits; } } else if (shouldLog) { LOG_ERROR("Error decoding proto for position message!"); return ""; } break; } case meshtastic_PortNum_WAYPOINT_APP: { msgType = "position"; meshtastic_Waypoint scratch; meshtastic_Waypoint *decoded = NULL; memset(&scratch, 0, sizeof(scratch)); if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_Waypoint_msg, &scratch)) { decoded = &scratch; jsonObj["payload"]["id"] = (unsigned int)decoded->id; jsonObj["payload"]["name"] = decoded->name; jsonObj["payload"]["description"] = decoded->description; jsonObj["payload"]["expire"] = (unsigned int)decoded->expire; jsonObj["payload"]["locked_to"] = (unsigned int)decoded->locked_to; jsonObj["payload"]["latitude_i"] = (int)decoded->latitude_i; jsonObj["payload"]["longitude_i"] = (int)decoded->longitude_i; } else if (shouldLog) { LOG_ERROR("Error decoding proto for position message!"); return ""; } break; } case meshtastic_PortNum_NEIGHBORINFO_APP: { msgType = "neighborinfo"; meshtastic_NeighborInfo scratch; meshtastic_NeighborInfo *decoded = NULL; memset(&scratch, 0, sizeof(scratch)); if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_NeighborInfo_msg, &scratch)) { decoded = &scratch; jsonObj["payload"]["node_id"] = (unsigned int)decoded->node_id; jsonObj["payload"]["node_broadcast_interval_secs"] = (unsigned int)decoded->node_broadcast_interval_secs; jsonObj["payload"]["last_sent_by_id"] = (unsigned int)decoded->last_sent_by_id; jsonObj["payload"]["neighbors_count"] = decoded->neighbors_count; JsonObject neighbors_obj = arrayObj.to(); JsonArray neighbors = neighbors_obj.createNestedArray("neighbors"); JsonObject neighbors_0 = neighbors.createNestedObject(); for (uint8_t i = 0; i < decoded->neighbors_count; i++) { neighbors_0["node_id"] = (unsigned int)decoded->neighbors[i].node_id; neighbors_0["snr"] = (int)decoded->neighbors[i].snr; neighbors[i + 1] = neighbors_0; neighbors_0.clear(); } neighbors.remove(0); jsonObj["payload"]["neighbors"] = neighbors; } else if (shouldLog) { LOG_ERROR("Error decoding proto for neighborinfo message!"); return ""; } break; } case meshtastic_PortNum_TRACEROUTE_APP: { if (mp->decoded.request_id) { // Only report the traceroute response msgType = "traceroute"; meshtastic_RouteDiscovery scratch; meshtastic_RouteDiscovery *decoded = NULL; memset(&scratch, 0, sizeof(scratch)); if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_RouteDiscovery_msg, &scratch)) { decoded = &scratch; JsonArray route = arrayObj.createNestedArray("route"); auto addToRoute = [](JsonArray *route, NodeNum num) { char long_name[40] = "Unknown"; meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(num); bool name_known = node ? node->has_user : false; if (name_known) memcpy(long_name, node->user.long_name, sizeof(long_name)); route->add(long_name); }; addToRoute(&route, mp->to); // route.add(mp->to); for (uint8_t i = 0; i < decoded->route_count; i++) { addToRoute(&route, decoded->route[i]); // route.add(decoded->route[i]); } addToRoute(&route, mp->from); // route.add(mp->from); // Ended at the original destination (source of response) jsonObj["payload"]["route"] = route; } else if (shouldLog) { LOG_ERROR("Error decoding proto for traceroute message!"); return ""; } } else { LOG_WARN("Traceroute response not reported"); return ""; } break; } case meshtastic_PortNum_DETECTION_SENSOR_APP: { msgType = "detection"; char payloadStr[(mp->decoded.payload.size) + 1]; memcpy(payloadStr, mp->decoded.payload.bytes, mp->decoded.payload.size); payloadStr[mp->decoded.payload.size] = 0; // null terminated string jsonObj["payload"]["text"] = payloadStr; break; } case meshtastic_PortNum_REMOTE_HARDWARE_APP: { meshtastic_HardwareMessage scratch; meshtastic_HardwareMessage *decoded = NULL; memset(&scratch, 0, sizeof(scratch)); if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_HardwareMessage_msg, &scratch)) { decoded = &scratch; if (decoded->type == meshtastic_HardwareMessage_Type_GPIOS_CHANGED) { msgType = "gpios_changed"; jsonObj["payload"]["gpio_value"] = (unsigned int)decoded->gpio_value; } else if (decoded->type == meshtastic_HardwareMessage_Type_READ_GPIOS_REPLY) { msgType = "gpios_read_reply"; jsonObj["payload"]["gpio_value"] = (unsigned int)decoded->gpio_value; jsonObj["payload"]["gpio_mask"] = (unsigned int)decoded->gpio_mask; } } else if (shouldLog) { LOG_ERROR("Error decoding proto for RemoteHardware message!"); return ""; } break; } // add more packet types here if needed default: LOG_WARN("Unsupported packet type %d", mp->decoded.portnum); return ""; break; } } else if (shouldLog) { LOG_WARN("Couldn't convert encrypted payload of MeshPacket to JSON"); return ""; } jsonObj["id"] = (unsigned int)mp->id; jsonObj["timestamp"] = (unsigned int)mp->rx_time; jsonObj["to"] = (unsigned int)mp->to; jsonObj["from"] = (unsigned int)mp->from; jsonObj["channel"] = (unsigned int)mp->channel; jsonObj["type"] = msgType.c_str(); jsonObj["sender"] = nodeDB->getNodeId().c_str(); if (mp->rx_rssi != 0) jsonObj["rssi"] = (int)mp->rx_rssi; if (mp->rx_snr != 0) jsonObj["snr"] = (float)mp->rx_snr; const int8_t hopsAway = getHopsAway(*mp); if (hopsAway >= 0) { jsonObj["hops_away"] = (unsigned int)(hopsAway); jsonObj["hop_start"] = (unsigned int)(mp->hop_start); } // serialize and write it to the stream // Serial.printf("serialized json message: \r"); // serializeJson(jsonObj, Serial); // Serial.println(""); std::string jsonStr = ""; serializeJson(jsonObj, jsonStr); if (shouldLog) LOG_INFO("serialized json message: %s", jsonStr.c_str()); return jsonStr; } std::string MeshPacketSerializer::JsonSerializeEncrypted(const meshtastic_MeshPacket *mp) { jsonObj.clear(); jsonObj["id"] = (unsigned int)mp->id; jsonObj["time_ms"] = (double)millis(); jsonObj["timestamp"] = (unsigned int)mp->rx_time; jsonObj["to"] = (unsigned int)mp->to; jsonObj["from"] = (unsigned int)mp->from; jsonObj["channel"] = (unsigned int)mp->channel; jsonObj["want_ack"] = mp->want_ack; if (mp->rx_rssi != 0) jsonObj["rssi"] = (int)mp->rx_rssi; if (mp->rx_snr != 0) jsonObj["snr"] = (float)mp->rx_snr; const int8_t hopsAway = getHopsAway(*mp); if (hopsAway >= 0) { jsonObj["hops_away"] = (unsigned int)(hopsAway); jsonObj["hop_start"] = (unsigned int)(mp->hop_start); } jsonObj["size"] = (unsigned int)mp->encrypted.size; auto encryptedStr = bytesToHex(mp->encrypted.bytes, mp->encrypted.size); jsonObj["bytes"] = encryptedStr.c_str(); // serialize and write it to the stream std::string jsonStr = ""; serializeJson(jsonObj, jsonStr); return jsonStr; } #endif ================================================ FILE: src/serialization/cobs.cpp ================================================ #include "cobs.h" #include #ifdef SENSECAP_INDICATOR cobs_encode_result cobs_encode(uint8_t *dst_buf_ptr, size_t dst_buf_len, const uint8_t *src_ptr, size_t src_len) { cobs_encode_result result = {0, COBS_ENCODE_OK}; if (!dst_buf_ptr || !src_ptr) { result.status = COBS_ENCODE_NULL_POINTER; return result; } const uint8_t *src_read_ptr = src_ptr; const uint8_t *src_end_ptr = src_read_ptr + src_len; uint8_t *dst_buf_start_ptr = dst_buf_ptr; uint8_t *dst_buf_end_ptr = dst_buf_start_ptr + dst_buf_len; uint8_t *dst_code_write_ptr = dst_buf_ptr; uint8_t *dst_write_ptr = dst_code_write_ptr + 1; uint8_t search_len = 1; if (src_len != 0) { for (;;) { if (dst_write_ptr >= dst_buf_end_ptr) { result.status = (cobs_encode_status)(result.status | (cobs_encode_status)COBS_ENCODE_OUT_BUFFER_OVERFLOW); break; } uint8_t src_byte = *src_read_ptr++; if (src_byte == 0) { *dst_code_write_ptr = search_len; dst_code_write_ptr = dst_write_ptr++; search_len = 1; if (src_read_ptr >= src_end_ptr) { break; } } else { *dst_write_ptr++ = src_byte; search_len++; if (src_read_ptr >= src_end_ptr) { break; } if (search_len == 0xFF) { *dst_code_write_ptr = search_len; dst_code_write_ptr = dst_write_ptr++; search_len = 1; } } } } if (dst_code_write_ptr >= dst_buf_end_ptr) { result.status = (cobs_encode_status)(result.status | (cobs_encode_status)COBS_ENCODE_OUT_BUFFER_OVERFLOW); dst_write_ptr = dst_buf_end_ptr; } else { *dst_code_write_ptr = search_len; } result.out_len = dst_write_ptr - dst_buf_start_ptr; return result; } cobs_decode_result cobs_decode(uint8_t *dst_buf_ptr, size_t dst_buf_len, const uint8_t *src_ptr, size_t src_len) { cobs_decode_result result = {0, COBS_DECODE_OK}; if (!dst_buf_ptr || !src_ptr) { result.status = COBS_DECODE_NULL_POINTER; return result; } const uint8_t *src_read_ptr = src_ptr; const uint8_t *src_end_ptr = src_read_ptr + src_len; uint8_t *dst_buf_start_ptr = dst_buf_ptr; const uint8_t *dst_buf_end_ptr = dst_buf_start_ptr + dst_buf_len; uint8_t *dst_write_ptr = dst_buf_ptr; if (src_len != 0) { for (;;) { uint8_t len_code = *src_read_ptr++; if (len_code == 0) { result.status = (cobs_decode_status)(result.status | (cobs_decode_status)COBS_DECODE_ZERO_BYTE_IN_INPUT); break; } len_code--; size_t remaining_bytes = src_end_ptr - src_read_ptr; if (len_code > remaining_bytes) { result.status = (cobs_decode_status)(result.status | (cobs_decode_status)COBS_DECODE_INPUT_TOO_SHORT); len_code = remaining_bytes; } remaining_bytes = dst_buf_end_ptr - dst_write_ptr; if (len_code > remaining_bytes) { result.status = (cobs_decode_status)(result.status | (cobs_decode_status)COBS_DECODE_OUT_BUFFER_OVERFLOW); len_code = remaining_bytes; } for (uint8_t i = len_code; i != 0; i--) { uint8_t src_byte = *src_read_ptr++; if (src_byte == 0) { result.status = (cobs_decode_status)(result.status | (cobs_decode_status)COBS_DECODE_ZERO_BYTE_IN_INPUT); } *dst_write_ptr++ = src_byte; } if (src_read_ptr >= src_end_ptr) { break; } if (len_code != 0xFE) { if (dst_write_ptr >= dst_buf_end_ptr) { result.status = (cobs_decode_status)(result.status | (cobs_decode_status)COBS_DECODE_OUT_BUFFER_OVERFLOW); break; } *dst_write_ptr++ = 0; } } } result.out_len = dst_write_ptr - dst_buf_start_ptr; return result; } #endif ================================================ FILE: src/serialization/cobs.h ================================================ #ifndef COBS_H_ #define COBS_H_ #include "configuration.h" #ifdef SENSECAP_INDICATOR #include #include #define COBS_ENCODE_DST_BUF_LEN_MAX(SRC_LEN) ((SRC_LEN) + (((SRC_LEN) + 253u) / 254u)) #define COBS_DECODE_DST_BUF_LEN_MAX(SRC_LEN) (((SRC_LEN) == 0) ? 0u : ((SRC_LEN)-1u)) #define COBS_ENCODE_SRC_OFFSET(SRC_LEN) (((SRC_LEN) + 253u) / 254u) typedef enum { COBS_ENCODE_OK = 0x00, COBS_ENCODE_NULL_POINTER = 0x01, COBS_ENCODE_OUT_BUFFER_OVERFLOW = 0x02 } cobs_encode_status; typedef struct { size_t out_len; cobs_encode_status status; } cobs_encode_result; typedef enum { COBS_DECODE_OK = 0x00, COBS_DECODE_NULL_POINTER = 0x01, COBS_DECODE_OUT_BUFFER_OVERFLOW = 0x02, COBS_DECODE_ZERO_BYTE_IN_INPUT = 0x04, COBS_DECODE_INPUT_TOO_SHORT = 0x08 } cobs_decode_status; typedef struct { size_t out_len; cobs_decode_status status; } cobs_decode_result; #ifdef __cplusplus extern "C" { #endif /* COBS-encode a string of input bytes. * * dst_buf_ptr: The buffer into which the result will be written * dst_buf_len: Length of the buffer into which the result will be written * src_ptr: The byte string to be encoded * src_len Length of the byte string to be encoded * * returns: A struct containing the success status of the encoding * operation and the length of the result (that was written to * dst_buf_ptr) */ cobs_encode_result cobs_encode(uint8_t *dst_buf_ptr, size_t dst_buf_len, const uint8_t *src_ptr, size_t src_len); /* Decode a COBS byte string. * * dst_buf_ptr: The buffer into which the result will be written * dst_buf_len: Length of the buffer into which the result will be written * src_ptr: The byte string to be decoded * src_len Length of the byte string to be decoded * * returns: A struct containing the success status of the decoding * operation and the length of the result (that was written to * dst_buf_ptr) */ cobs_decode_result cobs_decode(uint8_t *dst_buf_ptr, size_t dst_buf_len, const uint8_t *src_ptr, size_t src_len); #ifdef __cplusplus } /* extern "C" */ #endif #endif /* SENSECAP_INDICATOR */ #endif /* COBS_H_ */ ================================================ FILE: src/sleep.cpp ================================================ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_GPS #include "GPS.h" #endif #include "Default.h" #include "MeshRadio.h" #include "MeshService.h" #include "NodeDB.h" #include "PowerMon.h" #include "TransmitHistory.h" #include "detect/LoRaRadioType.h" #include "error.h" #include "main.h" #include "modules/StatusLEDModule.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 #include esp_sleep_source_t wakeCause; // the reason we booted this time #endif #include "Throttle.h" #ifdef USE_XL9555 #include "ExtensionIOXL9555.hpp" extern ExtensionIOXL9555 io; #endif #ifdef HAS_PPM #include extern XPowersPPM *PPM; #endif #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 (deep) sleep and you should prepare. Must return 0 Observable notifyDeepSleep; /// Called to tell observers we are rebooting ASAP. Must return 0 Observable notifyReboot; #ifdef ARCH_ESP32 /// Called to tell observers that light sleep is about to begin Observable notifyLightSleep; /// Called to tell observers that light sleep has just ended, and why it ended Observable notifyLightSleepEnd; #endif // 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 && !HAS_TFT if (isWifiAvailable()) { /* * * There's a newly introduced bug in the espressif framework where WiFi is * unstable when the frequency is less than 240MHz. * * This mostly impacts WiFi AP mode but we'll bump the frequency for * all WiFi use cases. * (Added: Dec 23, 2021 by Jm Casler) */ #ifndef CONFIG_IDF_TARGET_ESP32C3 LOG_DEBUG("Set 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]; */ #if defined(DEBUG_PORT) && !defined(DEBUG_MUTE) // If we booted because our timer ran out or the user pressed reset, send those as fake events RESET_REASON hwReason = rtc_get_reset_reason(0); if (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("Disable any holds on RTC IO pads"); for (uint8_t i = 0; i <= GPIO_NUM_MAX; i++) { if (rtc_gpio_is_valid_gpio((gpio_num_t)i)) rtc_gpio_hold_dis((gpio_num_t)i); // ESP32 (original) else if (GPIO_IS_VALID_OUTPUT_GPIO((gpio_num_t)i)) gpio_hold_dis((gpio_num_t)i); } } #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 } void doDeepSleep(uint32_t msecToWake, bool skipPreflight = false, bool skipSaveNodeDb = false) { if (INCLUDE_vTaskSuspend && (msecToWake == portMAX_DELAY)) { LOG_INFO("Enter deep sleep forever"); } else { LOG_INFO("Enter deep sleep for %u seconds", msecToWake / 1000); } // not using wifi yet, but once we are this is needed to shutoff the radio hw // esp_wifi_stop(); waitEnterSleep(skipPreflight); #if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_BLUETOOTH // Full shutdown of bluetooth hardware if (nimbleBluetooth) nimbleBluetooth->deinit(); #endif #ifdef ARCH_ESP32 if (!shouldLoraWake(msecToWake)) notifyDeepSleep.notifyObservers(NULL); #else notifyDeepSleep.notifyObservers(NULL); #endif powerMon->setState(meshtastic_PowerMon_State_CPU_DeepSleep); if (screen) screen->doDeepSleep(); // datasheet says this will draw only 10ua if (!skipSaveNodeDb) { nodeDB->saveToDisk(); } // Persist broadcast transmit times so throttle survives reboot if (transmitHistory) transmitHistory->saveToDisk(); #ifdef PIN_POWER_EN digitalWrite(PIN_POWER_EN, LOW); pinMode(PIN_POWER_EN, INPUT); // power off peripherals #endif #ifdef RAK_WISMESH_TAP_V2 digitalWrite(SDCARD_CS, LOW); #endif #ifdef TRACKER_T1000_E #ifdef GNSS_AIROHA digitalWrite(GPS_VRTC_EN, LOW); digitalWrite(PIN_GPS_RESET, LOW); digitalWrite(GPS_SLEEP_INT, LOW); digitalWrite(GPS_RTC_INT, LOW); pinMode(GPS_RESETB_OUT, OUTPUT); digitalWrite(GPS_RESETB_OUT, LOW); #endif #ifdef BUZZER_EN_PIN digitalWrite(BUZZER_EN_PIN, LOW); #endif #ifdef PIN_3V3_EN digitalWrite(PIN_3V3_EN, LOW); #endif #ifdef PIN_WD_EN digitalWrite(PIN_WD_EN, LOW); #endif #endif statusLEDModule->setPowerLED(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 #ifdef SENSECAP_INDICATOR // Portexpander definition does not pass GPIO_IS_VALID_OUTPUT_GPIO pinMode(LORA_CS, OUTPUT); digitalWrite(LORA_CS, HIGH); gpio_hold_en((gpio_num_t)LORA_CS); #elif defined(ELECROW_PANEL) // Elecrow panels do not use LORA_CS, do nothing #else if (GPIO_IS_VALID_OUTPUT_GPIO(LORA_CS)) { // LoRa CS (RADIO_NSS) needs to stay HIGH, even during deep sleep pinMode(LORA_CS, OUTPUT); digitalWrite(LORA_CS, HIGH); gpio_hold_en((gpio_num_t)LORA_CS); } #endif #endif #ifdef HAS_PPM if (PPM) { LOG_INFO("PMM shutdown"); console->flush(); PPM->shutdown(); } #endif #ifdef HAS_PMU if (pmu_found && PMU) { // Obsolete comment: from back when we we used to receive lora packets while CPU was in deep sleep. // We no longer do that, because our light-sleep current draws are low enough and it provides fast start/low cost // wake. We currently use deep sleep only for 'we want our device to actually be off - because our battery is // critically low'. So in deep sleep we DO shut down power to LORA (and when we boot later we completely reinit it) // // No need to turn this off if the power draw in sleep mode really is just 0.2uA and turning it off would // leave floating input for the IRQ line // If we want to leave the radio receiving in would be 11.5mA current draw, but most of the time it is just waiting // in its sequencer (true?) so the average power draw should be much lower even if we were listening for packets // all the time. PMU->setChargingLedMode(XPOWERS_CHG_LED_OFF); uint8_t model = PMU->getChipModel(); if (model == XPOWERS_AXP2101) { if (HW_VENDOR == meshtastic_HardwareModel_TBEAM) { // t-beam v1.2 radio power channel PMU->disablePowerOutput(XPOWERS_ALDO2); // lora radio power channel } else if (HW_VENDOR == meshtastic_HardwareModel_LILYGO_TBEAM_S3_CORE || HW_VENDOR == meshtastic_HardwareModel_T_WATCH_S3) { PMU->disablePowerOutput(XPOWERS_ALDO3); // lora radio power channel } } else if (model == XPOWERS_AXP192) { // t-beam v1.1 radio power channel PMU->disablePowerOutput(XPOWERS_LDO2); // lora radio power channel } if (msecToWake == portMAX_DELAY) { LOG_INFO("PMU shutdown"); console->flush(); PMU->shutdown(); } } #endif #if !MESHTASTIC_EXCLUDE_I2C && defined(ARCH_ESP32) && defined(I2C_SDA) // Added by https://github.com/meshtastic/firmware/pull/4418 // Possibly to support Heltec Capsule Sensor? Wire.end(); pinMode(I2C_SDA, ANALOG); pinMode(I2C_SCL, ANALOG); #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"); // LORA_DIO1 is an extended IO pin. Setting it as a wake-up pin will cause problems, such as the indicator device not entering // LightSleep. #if defined(SENSECAP_INDICATOR) return ESP_SLEEP_WAKEUP_TIMER; #endif waitEnterSleep(false); notifyLightSleep.notifyObservers(NULL); // Button interrupts are detached here 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 low // gpio_pullup_en((gpio_num_t)SERIAL0_RX_GPIO); // alas - can only work if using the refclock, which is limited to about 9600 bps // assert(uart_set_wakeup_threshold(UART_NUM_0, 3) == ESP_OK); // assert(esp_sleep_enable_uart_wakeup(0) == ESP_OK); #endif #ifdef ROTARY_PRESS // The enableLoraInterrupt() method is using ext0_wakeup, so we are forced to use GPIO wakeup gpio_wakeup_enable((gpio_num_t)ROTARY_PRESS, GPIO_INTR_LOW_LEVEL); #endif #ifdef KB_INT gpio_wakeup_enable((gpio_num_t)KB_INT, GPIO_INTR_LOW_LEVEL); #endif #ifdef BUTTON_PIN gpio_num_t pin = (gpio_num_t)(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN); gpio_wakeup_enable(pin, GPIO_INTR_LOW_LEVEL); #endif #if defined(INPUTDRIVER_TWO_WAY_ROCKER_BTN) || defined(INPUTDRIVER_ENCODER_BTN) #if defined(INPUTDRIVER_TWO_WAY_ROCKER_BTN) #define INPUTDRIVER_WAKE_BTN_PIN INPUTDRIVER_TWO_WAY_ROCKER_BTN #else #define INPUTDRIVER_WAKE_BTN_PIN INPUTDRIVER_ENCODER_BTN #endif gpio_wakeup_enable((gpio_num_t)INPUTDRIVER_WAKE_BTN_PIN, GPIO_INTR_LOW_LEVEL); #endif #if defined(WAKE_ON_TOUCH) 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 ROTARY_PRESS gpio_wakeup_disable((gpio_num_t)ROTARY_PRESS); #endif #ifdef KB_INT gpio_wakeup_disable((gpio_num_t)KB_INT); #endif #ifdef BUTTON_PIN // Disable wake-on-button interrupt. Re-attach normal button-interrupts gpio_wakeup_disable(pin); #endif #ifdef INPUTDRIVER_WAKE_BTN_PIN gpio_wakeup_disable((gpio_num_t)INPUTDRIVER_WAKE_BTN_PIN); #undef INPUTDRIVER_WAKE_BTN_PIN #endif #if defined(WAKE_ON_TOUCH) 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(); notifyLightSleepEnd.notifyObservers(cause); // Button interrupts are reattached here #ifdef BUTTON_PIN if (cause == ESP_SLEEP_WAKEUP_GPIO) { LOG_INFO("Exit light sleep gpio: btn=%d", !digitalRead(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN)); } else #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_ROUTER_LATE); } void enableLoraInterrupt() { esp_err_t res; #if SOC_PM_SUPPORT_EXT_WAKEUP && defined(LORA_DIO1) && (LORA_DIO1 != RADIOLIB_NC) res = gpio_pulldown_en((gpio_num_t)LORA_DIO1); if (res != ESP_OK) { LOG_ERROR("gpio_pulldown_en(LORA_DIO1) result %d", res); } #if defined(LORA_RESET) && (LORA_RESET != RADIOLIB_NC) res = gpio_pullup_en((gpio_num_t)LORA_RESET); if (res != ESP_OK) { LOG_ERROR("gpio_pullup_en(LORA_RESET) result %d", res); } #endif #if defined(LORA_CS) && (LORA_CS != RADIOLIB_NC) && !defined(ELECROW_PANEL) gpio_pullup_en((gpio_num_t)LORA_CS); #endif #if HAS_LORA_FEM loraFEMInterface.setRxModeEnableWhenMCUSleep(); #endif LOG_INFO("setup LORA_DIO1 (GPIO%02d) with wakeup by gpio interrupt", LORA_DIO1); gpio_wakeup_enable((gpio_num_t)LORA_DIO1, GPIO_INTR_HIGH_LEVEL); #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 ================================================ FILE: src/sleep.h ================================================ #pragma once #include "Arduino.h" #include "Observer.h" #include "configuration.h" void doDeepSleep(uint32_t msecToWake, bool skipPreflight, bool skipSaveNodeDb), 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 (deep) sleep and you should prepare. Must return 0 extern Observable notifyDeepSleep; /// Called to tell observers we are rebooting ASAP. Must return 0 extern Observable notifyReboot; #ifdef ARCH_ESP32 /// Called to tell observers that light sleep is about to begin extern Observable notifyLightSleep; /// Called to tell observers that light sleep has just ended, and why it ended extern Observable notifyLightSleepEnd; #endif void enableModemSleep(); #ifdef ARCH_ESP32 void enableLoraInterrupt(); bool shouldLoraWake(uint32_t msecToWake); #endif ================================================ FILE: src/target_specific.h ================================================ #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); ================================================ FILE: src/watchdog/watchdogThread.cpp ================================================ #include "watchdogThread.h" #include "configuration.h" #ifdef HAS_HARDWARE_WATCHDOG WatchdogThread *watchdogThread; WatchdogThread::WatchdogThread() : OSThread("Watchdog") { setup(); } void WatchdogThread::feedDog(void) { digitalWrite(HARDWARE_WATCHDOG_DONE, HIGH); delay(1); digitalWrite(HARDWARE_WATCHDOG_DONE, LOW); } int32_t WatchdogThread::runOnce() { LOG_DEBUG("Feeding hardware watchdog"); feedDog(); return HARDWARE_WATCHDOG_TIMEOUT_MS; } bool WatchdogThread::setup() { LOG_DEBUG("init hardware watchdog"); pinMode(HARDWARE_WATCHDOG_WAKE, INPUT); pinMode(HARDWARE_WATCHDOG_DONE, OUTPUT); delay(1); digitalWrite(HARDWARE_WATCHDOG_DONE, LOW); delay(1); feedDog(); return true; } #endif ================================================ FILE: src/watchdog/watchdogThread.h ================================================ #pragma once #include "concurrency/OSThread.h" #include #ifdef HAS_HARDWARE_WATCHDOG class WatchdogThread : private concurrency::OSThread { public: WatchdogThread(); void feedDog(void); virtual bool setup(); virtual int32_t runOnce() override; }; extern WatchdogThread *watchdogThread; #endif ================================================ FILE: src/xmodem.cpp ================================================ /** * @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" #include "SPILock.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 Send 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 spiLock->lock(); file = FSCom.open(filename, FILE_O_WRITE); spiLock->unlock(); if (file) { sendControl(meshtastic_XModem_Control_ACK); isReceiving = true; packetno = 1; break; } sendControl(meshtastic_XModem_Control_NAK); isReceiving = false; break; } else { // Transmit this file from Flash LOG_INFO("XModem: Transmit file %s", filename); spiLock->lock(); file = FSCom.open(filename, FILE_O_READ); spiLock->unlock(); if (file) { packetno = 1; isTransmitting = true; xmodemStore = meshtastic_XModem_init_zero; xmodemStore.control = meshtastic_XModem_Control_SOH; xmodemStore.seq = packetno; spiLock->lock(); xmodemStore.buffer.size = file.read(xmodemStore.buffer.bytes, sizeof(meshtastic_XModem_buffer_t::bytes)); spiLock->unlock(); xmodemStore.crc16 = crc16_ccitt(xmodemStore.buffer.bytes, xmodemStore.buffer.size); LOG_DEBUG("XModem: STX Notify Send packet %d, %d Bytes", packetno, xmodemStore.buffer.size); if (xmodemStore.buffer.size < sizeof(meshtastic_XModem_buffer_t::bytes)) { isEOT = true; // send EOT on next Ack } packetReady.notifyObservers(packetno); break; } sendControl(meshtastic_XModem_Control_NAK); isTransmitting = false; break; } } else { if (isReceiving) { // normal file data packet if ((xmodemPacket.seq == packetno) && check(xmodemPacket.buffer.bytes, xmodemPacket.buffer.size, xmodemPacket.crc16)) { // valid packet spiLock->lock(); file.write(xmodemPacket.buffer.bytes, xmodemPacket.buffer.size); spiLock->unlock(); sendControl(meshtastic_XModem_Control_ACK); packetno++; break; } // invalid packet sendControl(meshtastic_XModem_Control_NAK); break; } else if (isTransmitting) { // just received something weird. sendControl(meshtastic_XModem_Control_CAN); isTransmitting = false; break; } } break; case meshtastic_XModem_Control_EOT: // End of transmission sendControl(meshtastic_XModem_Control_ACK); spiLock->lock(); file.flush(); file.close(); spiLock->unlock(); isReceiving = false; break; case meshtastic_XModem_Control_CAN: // Cancel transmission and remove file sendControl(meshtastic_XModem_Control_ACK); spiLock->lock(); file.flush(); file.close(); FSCom.remove(filename); spiLock->unlock(); isReceiving = false; break; case meshtastic_XModem_Control_ACK: // Acknowledge Send the next packet if (isTransmitting) { if (isEOT) { sendControl(meshtastic_XModem_Control_EOT); spiLock->lock(); file.close(); spiLock->unlock(); LOG_INFO("XModem: Finished send file %s", filename); isTransmitting = false; isEOT = false; break; } retrans = MAXRETRANS; // reset retransmit counter packetno++; xmodemStore = meshtastic_XModem_init_zero; xmodemStore.control = meshtastic_XModem_Control_SOH; xmodemStore.seq = packetno; spiLock->lock(); xmodemStore.buffer.size = file.read(xmodemStore.buffer.bytes, sizeof(meshtastic_XModem_buffer_t::bytes)); spiLock->unlock(); xmodemStore.crc16 = crc16_ccitt(xmodemStore.buffer.bytes, xmodemStore.buffer.size); LOG_DEBUG("XModem: ACK Notify Send packet %d, %d Bytes", packetno, xmodemStore.buffer.size); if (xmodemStore.buffer.size < sizeof(meshtastic_XModem_buffer_t::bytes)) { isEOT = true; // send EOT on next Ack } packetReady.notifyObservers(packetno); } else { // just received something weird. sendControl(meshtastic_XModem_Control_CAN); } break; case meshtastic_XModem_Control_NAK: // Negative acknowledge. Send the same buffer again if (isTransmitting) { if (--retrans <= 0) { sendControl(meshtastic_XModem_Control_CAN); spiLock->lock(); file.close(); spiLock->unlock(); LOG_INFO("XModem: Retransmit timeout, cancel file %s", filename); isTransmitting = false; break; } xmodemStore = meshtastic_XModem_init_zero; xmodemStore.control = meshtastic_XModem_Control_SOH; xmodemStore.seq = packetno; spiLock->lock(); file.seek((packetno - 1) * sizeof(meshtastic_XModem_buffer_t::bytes)); xmodemStore.buffer.size = file.read(xmodemStore.buffer.bytes, sizeof(meshtastic_XModem_buffer_t::bytes)); spiLock->unlock(); xmodemStore.crc16 = crc16_ccitt(xmodemStore.buffer.bytes, xmodemStore.buffer.size); LOG_DEBUG("XModem: NAK Notify Send packet %d, %d Bytes", packetno, xmodemStore.buffer.size); if (xmodemStore.buffer.size < sizeof(meshtastic_XModem_buffer_t::bytes)) { isEOT = true; // send EOT on next Ack } packetReady.notifyObservers(packetno); } else { // just received something weird. sendControl(meshtastic_XModem_Control_CAN); } break; default: // Unknown control character break; } } #endif ================================================ FILE: src/xmodem.h ================================================ /*********************************************************************************************************************** * 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 ================================================ FILE: suppressions.txt ================================================ // 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 useStlAlgorithm variableScope ================================================ FILE: test/TestUtil.cpp ================================================ #include "SerialConsole.h" #include "concurrency/OSThread.h" #include "gps/RTC.h" #include "TestUtil.h" #if defined(ARDUINO) #include #else #include #include #endif void initializeTestEnvironment() { concurrency::hasBeenSetup = true; consoleInit(); #if ARCH_PORTDUINO struct timeval tv; tv.tv_sec = time(NULL); tv.tv_usec = 0; perhapsSetRTC(RTCQualityNTP, &tv); #endif concurrency::OSThread::setup(); } void testDelay(unsigned long ms) { #if defined(ARDUINO) ::delay(ms); #else std::this_thread::sleep_for(std::chrono::milliseconds(ms)); #endif } ================================================ FILE: test/TestUtil.h ================================================ #pragma once // Initialize testing environment. void initializeTestEnvironment(); // Portable delay for tests (Arduino or host). void testDelay(unsigned long ms); ================================================ FILE: test/test_admin_radio/test_main.cpp ================================================ /** * Tests for the radio configuration validation and clamping functions * introduced in the radio_interface_cherrypick branch. * * Targets: * 1. getRegion() * 2. RadioInterface::validateConfigRegion() * 3. RadioInterface::validateConfigLora() * 4. RadioInterface::clampConfigLora() * 5. RegionInfo preset lists (PRESETS_STD, PRESETS_EU_868, PRESETS_UNDEF) * 6. Channel spacing calculation (placeholder for future protobuf changes) */ #include "MeshRadio.h" #include "MeshService.h" #include "NodeDB.h" #include "RadioInterface.h" #include "TestUtil.h" #include "modules/AdminModule.h" #include #include "meshtastic/config.pb.h" class MockMeshService : public MeshService { public: void sendClientNotification(meshtastic_ClientNotification *n) override { releaseClientNotificationToPool(n); } }; static MockMeshService *mockMeshService; // ----------------------------------------------------------------------- // getRegion() tests // ----------------------------------------------------------------------- extern const RegionInfo *getRegion(meshtastic_Config_LoRaConfig_RegionCode code); static void test_getRegion_returnsCorrectRegion_US() { const RegionInfo *r = getRegion(meshtastic_Config_LoRaConfig_RegionCode_US); TEST_ASSERT_NOT_NULL(r); TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_US, r->code); TEST_ASSERT_EQUAL_STRING("US", r->name); } static void test_getRegion_returnsCorrectRegion_EU868() { const RegionInfo *r = getRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868); TEST_ASSERT_NOT_NULL(r); TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_EU_868, r->code); TEST_ASSERT_EQUAL_STRING("EU_868", r->name); } static void test_getRegion_returnsCorrectRegion_LORA24() { const RegionInfo *r = getRegion(meshtastic_Config_LoRaConfig_RegionCode_LORA_24); TEST_ASSERT_NOT_NULL(r); TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_LORA_24, r->code); TEST_ASSERT_TRUE(r->wideLora); } static void test_getRegion_unsetCodeReturnsUnsetEntry() { const RegionInfo *r = getRegion(meshtastic_Config_LoRaConfig_RegionCode_UNSET); TEST_ASSERT_NOT_NULL(r); TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_UNSET, r->code); TEST_ASSERT_EQUAL_STRING("UNSET", r->name); } static void test_getRegion_unknownCodeFallsToUnset() { // A code not in the table should iterate to the UNSET sentinel const RegionInfo *r = getRegion((meshtastic_Config_LoRaConfig_RegionCode)255); TEST_ASSERT_NOT_NULL(r); TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_UNSET, r->code); } // ----------------------------------------------------------------------- // validateConfigRegion() tests // ----------------------------------------------------------------------- static void test_validateConfigRegion_validRegionReturnsTrue() { meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US; // Ensure owner is not licensed (should not matter for non-licensed-only regions) devicestate.owner.is_licensed = false; TEST_ASSERT_TRUE(RadioInterface::validateConfigRegion(cfg)); } static void test_validateConfigRegion_unsetRegionReturnsTrue() { meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; cfg.region = meshtastic_Config_LoRaConfig_RegionCode_UNSET; devicestate.owner.is_licensed = false; // UNSET region has licensedOnly=false, so should pass TEST_ASSERT_TRUE(RadioInterface::validateConfigRegion(cfg)); } // ----------------------------------------------------------------------- // Shadow tables for testing (preset lists → profiles → regions → lookup) // ----------------------------------------------------------------------- // A minimal preset list with only one entry static const meshtastic_Config_LoRaConfig_ModemPreset TEST_PRESETS_SINGLE[] = { meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, MODEM_PRESET_END, }; // A preset list that includes all turbo variants only static const meshtastic_Config_LoRaConfig_ModemPreset TEST_PRESETS_TURBO_ONLY[] = { meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO, meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO, MODEM_PRESET_END, }; // A restricted list simulating a hypothetical tight-regulation region static const meshtastic_Config_LoRaConfig_ModemPreset TEST_PRESETS_RESTRICTED[] = { meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE, MODEM_PRESET_END, }; // Mirrors PROFILE_STD but with non-zero spacing/padding for testing static const RegionProfile TEST_PROFILE_SPACED = { TEST_PRESETS_SINGLE, /* spacing */ 0.025f, /* padding */ 0.010f, /* audioPermitted */ true, /* licensedOnly */ false, /* textThrottle */ 0, /* positionThrottle */ 0, /* telemetryThrottle */ 0, /* overrideSlot */ 0, }; // A licensed-only profile for testing access control static const RegionProfile TEST_PROFILE_LICENSED = { TEST_PRESETS_RESTRICTED, /* spacing */ 0.0f, /* padding */ 0.0f, /* audioPermitted */ false, /* licensedOnly */ true, /* textThrottle */ 5, /* positionThrottle */ 10, /* telemetryThrottle */ 10, /* overrideSlot */ 3, }; // Turbo-only profile static const RegionProfile TEST_PROFILE_TURBO = { TEST_PRESETS_TURBO_ONLY, /* spacing */ 0.0f, /* padding */ 0.0f, /* audioPermitted */ true, /* licensedOnly */ false, /* textThrottle */ 0, /* positionThrottle */ 0, /* telemetryThrottle */ 0, /* overrideSlot */ 0, }; static const RegionInfo testRegions[] = { // A wide US-like region with spacing + padding {meshtastic_Config_LoRaConfig_RegionCode_US, 902.0f, 928.0f, 100, 30, false, false, &TEST_PROFILE_SPACED, "TEST_US_SPACED"}, // A narrow band simulating tight EU regulation {meshtastic_Config_LoRaConfig_RegionCode_EU_868, 869.4f, 869.65f, 10, 14, false, false, &TEST_PROFILE_LICENSED, "TEST_EU_LICENSED"}, // A wide-LoRa region with turbo-only presets {meshtastic_Config_LoRaConfig_RegionCode_LORA_24, 2400.0f, 2483.5f, 100, 10, false, true, &TEST_PROFILE_TURBO, "TEST_LORA24_TURBO"}, // Sentinel — must be last {meshtastic_Config_LoRaConfig_RegionCode_UNSET, 902.0f, 928.0f, 100, 30, false, false, &TEST_PROFILE_SPACED, "TEST_UNSET"}, }; static const RegionInfo *getTestRegion(meshtastic_Config_LoRaConfig_RegionCode code) { const RegionInfo *r = testRegions; while (r->code != meshtastic_Config_LoRaConfig_RegionCode_UNSET) { if (r->code == code) return r; r++; } return r; // Returns the UNSET sentinel } // ----------------------------------------------------------------------- // Shadow table tests // ----------------------------------------------------------------------- static void test_shadowTable_spacedProfileHasNonZeroSpacing() { const RegionInfo *r = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_US); TEST_ASSERT_EQUAL_STRING("TEST_US_SPACED", r->name); TEST_ASSERT_FLOAT_WITHIN(0.001f, 0.025f, r->profile->spacing); TEST_ASSERT_FLOAT_WITHIN(0.001f, 0.010f, r->profile->padding); } static void test_shadowTable_licensedProfileFlagsCorrect() { const RegionInfo *r = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868); TEST_ASSERT_TRUE(r->profile->licensedOnly); TEST_ASSERT_FALSE(r->profile->audioPermitted); TEST_ASSERT_EQUAL(3, r->profile->overrideSlot); } static void test_shadowTable_presetCountMatchesExpected() { const RegionInfo *spaced = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_US); TEST_ASSERT_EQUAL(1, spaced->getNumPresets()); const RegionInfo *licensed = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868); TEST_ASSERT_EQUAL(2, licensed->getNumPresets()); const RegionInfo *turbo = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_LORA_24); TEST_ASSERT_EQUAL(2, turbo->getNumPresets()); } static void test_shadowTable_defaultPresetIsFirstInList() { const RegionInfo *spaced = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_US); TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, spaced->getDefaultPreset()); const RegionInfo *licensed = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868); TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW, licensed->getDefaultPreset()); const RegionInfo *turbo = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_LORA_24); TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO, turbo->getDefaultPreset()); } static void test_shadowTable_channelSpacingWithPadding() { // Verify channel count when spacing + padding are non-zero const RegionInfo *r = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_US); float bw = modemPresetToBwKHz(r->getDefaultPreset(), r->wideLora); float channelSpacing = r->profile->spacing + (r->profile->padding * 2) + (bw / 1000.0f); // spacing=0.025, padding=0.010*2=0.020, bw=250kHz=0.250 // channelSpacing = 0.025 + 0.020 + 0.250 = 0.295 MHz TEST_ASSERT_FLOAT_WITHIN(0.001f, 0.295f, channelSpacing); uint32_t numChannels = (uint32_t)(((r->freqEnd - r->freqStart + r->profile->spacing) / channelSpacing) + 0.5f); // (928 - 902 + 0.025) / 0.295 = 88.2 → 88 TEST_ASSERT_EQUAL_UINT32(88, numChannels); } static void test_shadowTable_turboOnlyOnWideLora() { const RegionInfo *r = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_LORA_24); TEST_ASSERT_TRUE(r->wideLora); TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO, r->getDefaultPreset()); // Verify wide-LoRa bandwidth for SHORT_TURBO float bw = modemPresetToBwKHz(r->getDefaultPreset(), r->wideLora); TEST_ASSERT_FLOAT_WITHIN(0.1f, 1625.0f, bw); // 1625 kHz in wide mode } static void test_shadowTable_unknownCodeFallsToSentinel() { const RegionInfo *r = getTestRegion((meshtastic_Config_LoRaConfig_RegionCode)200); TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_UNSET, r->code); TEST_ASSERT_EQUAL_STRING("TEST_UNSET", r->name); } // ----------------------------------------------------------------------- // validateConfigLora() tests // ----------------------------------------------------------------------- static void test_validateConfigLora_validPresetForUS() { meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US; cfg.use_preset = true; cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST; TEST_ASSERT_TRUE(RadioInterface::validateConfigLora(cfg)); } static void test_validateConfigLora_allStdPresetsValidForUS() { meshtastic_Config_LoRaConfig_ModemPreset stdPresets[] = { meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST, meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO, meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO, }; for (size_t i = 0; i < sizeof(stdPresets) / sizeof(stdPresets[0]); i++) { meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US; cfg.use_preset = true; cfg.modem_preset = stdPresets[i]; TEST_ASSERT_TRUE_MESSAGE(RadioInterface::validateConfigLora(cfg), "Expected valid preset for US"); } } static void test_validateConfigLora_turboPresetsInvalidForEU868() { // EU_868 has PRESETS_EU_868 which excludes SHORT_TURBO and LONG_TURBO meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868; cfg.use_preset = true; cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO; TEST_ASSERT_FALSE_MESSAGE(RadioInterface::validateConfigLora(cfg), "SHORT_TURBO should be invalid for EU_868"); cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO; TEST_ASSERT_FALSE_MESSAGE(RadioInterface::validateConfigLora(cfg), "LONG_TURBO should be invalid for EU_868"); } static void test_validateConfigLora_validPresetsForEU868() { meshtastic_Config_LoRaConfig_ModemPreset eu868Presets[] = { meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST, meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE, }; for (size_t i = 0; i < sizeof(eu868Presets) / sizeof(eu868Presets[0]); i++) { meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868; cfg.use_preset = true; cfg.modem_preset = eu868Presets[i]; TEST_ASSERT_TRUE_MESSAGE(RadioInterface::validateConfigLora(cfg), "Expected valid preset for EU_868"); } } static void test_validateConfigLora_customBandwidthTooWideForEU868() { // EU_868 spans 869.4 - 869.65 = 0.25 MHz = 250 kHz // A 500 kHz custom BW should be rejected meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868; cfg.use_preset = false; cfg.bandwidth = 500; cfg.spread_factor = 11; cfg.coding_rate = 5; TEST_ASSERT_FALSE(RadioInterface::validateConfigLora(cfg)); } static void test_validateConfigLora_customBandwidthFitsUS() { // US spans 902 - 928 = 26 MHz, so 250 kHz BW fits easily meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US; cfg.use_preset = false; cfg.bandwidth = 250; cfg.spread_factor = 11; cfg.coding_rate = 5; TEST_ASSERT_TRUE(RadioInterface::validateConfigLora(cfg)); } static void test_validateConfigLora_customBandwidthFitsEU868() { // EU_868 spans 250 kHz, 125 kHz BW should fit meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868; cfg.use_preset = false; cfg.bandwidth = 125; cfg.spread_factor = 12; cfg.coding_rate = 8; TEST_ASSERT_TRUE(RadioInterface::validateConfigLora(cfg)); } static void test_validateConfigLora_bogusPresetRejected() { // A fabricated preset value not in any list should be rejected meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US; cfg.use_preset = true; cfg.modem_preset = (meshtastic_Config_LoRaConfig_ModemPreset)99; TEST_ASSERT_FALSE(RadioInterface::validateConfigLora(cfg)); } static void test_validateConfigLora_unsetRegionOnlyAcceptsLongFast() { // UNSET uses PROFILE_UNDEF which has only LONG_FAST meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; cfg.region = meshtastic_Config_LoRaConfig_RegionCode_UNSET; cfg.use_preset = true; cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST; TEST_ASSERT_TRUE_MESSAGE(RadioInterface::validateConfigLora(cfg), "LONG_FAST should be valid for UNSET"); cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST; TEST_ASSERT_FALSE_MESSAGE(RadioInterface::validateConfigLora(cfg), "MEDIUM_FAST should be invalid for UNSET"); cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO; TEST_ASSERT_FALSE_MESSAGE(RadioInterface::validateConfigLora(cfg), "SHORT_TURBO should be invalid for UNSET"); } static void test_validateConfigLora_allPresetsValidForLORA24() { // LORA_24 uses PROFILE_STD (9 presets) with wideLora=true meshtastic_Config_LoRaConfig_ModemPreset stdPresets[] = { meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST, meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO, meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO, }; for (size_t i = 0; i < sizeof(stdPresets) / sizeof(stdPresets[0]); i++) { meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; cfg.region = meshtastic_Config_LoRaConfig_RegionCode_LORA_24; cfg.use_preset = true; cfg.modem_preset = stdPresets[i]; TEST_ASSERT_TRUE_MESSAGE(RadioInterface::validateConfigLora(cfg), "Expected valid preset for LORA_24"); } } // ----------------------------------------------------------------------- // clampConfigLora() tests // ----------------------------------------------------------------------- static void test_clampConfigLora_invalidPresetClampedToDefault() { meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868; cfg.use_preset = true; cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO; // not in EU_868 preset list RadioInterface::clampConfigLora(cfg); const RegionInfo *eu868 = getRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868); TEST_ASSERT_EQUAL(eu868->getDefaultPreset(), cfg.modem_preset); } static void test_clampConfigLora_validPresetUnchanged() { meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US; cfg.use_preset = true; cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST; RadioInterface::clampConfigLora(cfg); TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST, cfg.modem_preset); } static void test_clampConfigLora_customBwTooWideClampedToDefaultBw() { // EU_868 span is 250kHz. A 500kHz custom BW should be clamped to default preset BW. meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868; cfg.use_preset = false; cfg.bandwidth = 500; cfg.spread_factor = 11; cfg.coding_rate = 5; RadioInterface::clampConfigLora(cfg); const RegionInfo *eu868 = getRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868); float expectedBw = modemPresetToBwKHz(eu868->getDefaultPreset(), eu868->wideLora); TEST_ASSERT_FLOAT_WITHIN(0.01f, expectedBw, (float)cfg.bandwidth); } static void test_clampConfigLora_customBwValidLeftUnchanged() { meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US; cfg.use_preset = false; cfg.bandwidth = 125; cfg.spread_factor = 12; cfg.coding_rate = 8; RadioInterface::clampConfigLora(cfg); TEST_ASSERT_EQUAL_UINT16(125, cfg.bandwidth); } static void test_clampConfigLora_bogusPresetOnUnsetClampedToLongFast() { // UNSET uses PROFILE_UNDEF with only LONG_FAST; any other preset should clamp to it meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; cfg.region = meshtastic_Config_LoRaConfig_RegionCode_UNSET; cfg.use_preset = true; cfg.modem_preset = (meshtastic_Config_LoRaConfig_ModemPreset)99; RadioInterface::clampConfigLora(cfg); TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, cfg.modem_preset); } static void test_clampConfigLora_invalidPresetOnLORA24ClampedToDefault() { // LORA_24 uses PROFILE_STD; a bogus preset should clamp to LONG_FAST (first in PRESETS_STD) meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; cfg.region = meshtastic_Config_LoRaConfig_RegionCode_LORA_24; cfg.use_preset = true; cfg.modem_preset = (meshtastic_Config_LoRaConfig_ModemPreset)99; RadioInterface::clampConfigLora(cfg); const RegionInfo *lora24 = getRegion(meshtastic_Config_LoRaConfig_RegionCode_LORA_24); TEST_ASSERT_EQUAL(lora24->getDefaultPreset(), cfg.modem_preset); } // ----------------------------------------------------------------------- // RegionInfo preset list integrity tests // ----------------------------------------------------------------------- static void test_presetsStd_hasNineEntries() { // PROFILE_STD should have exactly 9 presets const RegionInfo *us = getRegion(meshtastic_Config_LoRaConfig_RegionCode_US); TEST_ASSERT_EQUAL(9, us->getNumPresets()); TEST_ASSERT_EQUAL_PTR(PROFILE_STD.presets, us->getAvailablePresets()); } static void test_presetsEU868_hasSevenEntries() { const RegionInfo *eu = getRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868); TEST_ASSERT_EQUAL(7, eu->getNumPresets()); TEST_ASSERT_EQUAL_PTR(PROFILE_EU868.presets, eu->getAvailablePresets()); } static void test_presetsUndef_hasOneEntry() { const RegionInfo *unset = getRegion(meshtastic_Config_LoRaConfig_RegionCode_UNSET); TEST_ASSERT_EQUAL(1, unset->getNumPresets()); TEST_ASSERT_EQUAL_PTR(PROFILE_UNDEF.presets, unset->getAvailablePresets()); } static void test_defaultPresetIsInAvailablePresets() { // For every region, the defaultPreset must appear in its own availablePresets list const RegionInfo *r = regions; while (true) { bool found = false; for (size_t i = 0; i < r->getNumPresets(); i++) { if (r->getAvailablePresets()[i] == r->getDefaultPreset()) { found = true; break; } } char msg[80]; snprintf(msg, sizeof(msg), "Region %s defaultPreset not in availablePresets", r->name); TEST_ASSERT_TRUE_MESSAGE(found, msg); if (r->code == meshtastic_Config_LoRaConfig_RegionCode_UNSET) break; // UNSET is the sentinel, stop after it r++; } } static void test_regionFieldsAreSane() { // Basic sanity check: all regions have freqEnd > freqStart and a non-null name const RegionInfo *r = regions; while (true) { char msg[80]; snprintf(msg, sizeof(msg), "Region %s: freqEnd must be > freqStart", r->name); TEST_ASSERT_TRUE_MESSAGE(r->freqEnd > r->freqStart, msg); TEST_ASSERT_NOT_NULL(r->name); TEST_ASSERT_TRUE_MESSAGE(r->getNumPresets() > 0, "numPresets must be > 0"); TEST_ASSERT_NOT_NULL(r->getAvailablePresets()); if (r->code == meshtastic_Config_LoRaConfig_RegionCode_UNSET) break; r++; } } static void test_onlyLORA24HasWideLora() { // Verify that LORA_24 is the only region with wideLora=true const RegionInfo *r = regions; while (true) { char msg[80]; if (r->code == meshtastic_Config_LoRaConfig_RegionCode_LORA_24) { snprintf(msg, sizeof(msg), "Region %s should have wideLora=true", r->name); TEST_ASSERT_TRUE_MESSAGE(r->wideLora, msg); } else { snprintf(msg, sizeof(msg), "Region %s should have wideLora=false", r->name); TEST_ASSERT_FALSE_MESSAGE(r->wideLora, msg); } if (r->code == meshtastic_Config_LoRaConfig_RegionCode_UNSET) break; r++; } } // ----------------------------------------------------------------------- // Channel spacing calculation (placeholder for future protobuf updates) // ----------------------------------------------------------------------- static void test_channelSpacingCalculation_US_LONG_FAST() { // Current formula: channelSpacing = spacing + (padding * 2) + (bw / 1000) // US: spacing=0, padding=0 // LONG_FAST on non-wide region: bw=250 kHz // channelSpacing = 0 + 0 + 0.250 = 0.250 MHz // numChannels = round((928 - 902 + 0) / 0.250) = round(104) = 104 const RegionInfo *us = getRegion(meshtastic_Config_LoRaConfig_RegionCode_US); float bw = modemPresetToBwKHz(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, us->wideLora); float channelSpacing = us->profile->spacing + (us->profile->padding * 2) + (bw / 1000.0f); uint32_t numChannels = (uint32_t)(((us->freqEnd - us->freqStart + us->profile->spacing) / channelSpacing) + 0.5f); TEST_ASSERT_FLOAT_WITHIN(0.001f, 0.250f, channelSpacing); TEST_ASSERT_EQUAL_UINT32(104, numChannels); } static void test_channelSpacingCalculation_EU868_LONG_FAST() { // EU_868: freqStart=869.4, freqEnd=869.65, spacing=0, padding=0 // LONG_FAST: bw=250 kHz => channelSpacing = 0.250 MHz // numChannels = round((0.25 + 0) / 0.250) = 1 const RegionInfo *eu = getRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868); float bw = modemPresetToBwKHz(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, eu->wideLora); float channelSpacing = eu->profile->spacing + (eu->profile->padding * 2) + (bw / 1000.0f); uint32_t numChannels = (uint32_t)(((eu->freqEnd - eu->freqStart + eu->profile->spacing) / channelSpacing) + 0.5f); TEST_ASSERT_FLOAT_WITHIN(0.001f, 0.250f, channelSpacing); TEST_ASSERT_EQUAL_UINT32(1, numChannels); } // Placeholder: when protobuf region definitions include non-zero padding/spacing, // add tests here to verify the channel count and frequency calculations. static void test_channelSpacingCalculation_placeholder() { // TODO: Once protobuf RegionInfo entries have non-zero padding or spacing values, // verify: // - Channel count matches expected value for each (region, preset) pair // - First channel frequency = freqStart + (bw/2000) + padding // - Nth channel frequency = first + (n * channelSpacing) // - overrideSlot, when non-zero, forces the channel_num TEST_PASS_MESSAGE("Placeholder for future channel spacing tests with updated protobuf region fields"); } // ----------------------------------------------------------------------- // handleSetConfig fromOthers dispatch tests // ----------------------------------------------------------------------- class AdminModuleTestShim : public AdminModule { public: using AdminModule::handleSetConfig; }; static AdminModuleTestShim *testAdmin; static meshtastic_Config makeLoraSetConfig(meshtastic_Config_LoRaConfig_RegionCode region, bool usePreset, meshtastic_Config_LoRaConfig_ModemPreset preset) { meshtastic_Config c = meshtastic_Config_init_zero; c.which_payload_variant = meshtastic_Config_lora_tag; c.payload_variant.lora.region = region; c.payload_variant.lora.use_preset = usePreset; c.payload_variant.lora.modem_preset = preset; return c; } static void test_handleSetConfig_fromOthers_invalidPresetRejected() { // Set up a known-good baseline in the global config config.lora = meshtastic_Config_LoRaConfig_init_zero; config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868; config.lora.use_preset = true; config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST; initRegion(); // Build an admin set_config with an invalid preset for EU_868 meshtastic_Config c = makeLoraSetConfig(meshtastic_Config_LoRaConfig_RegionCode_EU_868, true, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO); testAdmin->handleSetConfig(c, true); // fromOthers = true // fromOthers=true: invalid preset should be rejected, old preset preserved TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, config.lora.modem_preset); } static void test_handleSetConfig_fromLocal_invalidPresetClamped() { // Set up a known-good baseline config.lora = meshtastic_Config_LoRaConfig_init_zero; config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868; config.lora.use_preset = true; config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST; initRegion(); // Build an admin set_config with an invalid preset for EU_868 meshtastic_Config c = makeLoraSetConfig(meshtastic_Config_LoRaConfig_RegionCode_EU_868, true, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO); testAdmin->handleSetConfig(c, false); // fromOthers = false (local client) // fromOthers=false: invalid preset should be clamped to the region's default const RegionInfo *eu868 = getRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868); TEST_ASSERT_EQUAL(eu868->getDefaultPreset(), config.lora.modem_preset); } static void test_handleSetConfig_fromOthers_validPresetAccepted() { // Set up baseline config.lora = meshtastic_Config_LoRaConfig_init_zero; config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868; config.lora.use_preset = true; config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST; initRegion(); // Build an admin set_config with a valid preset for EU_868 meshtastic_Config c = makeLoraSetConfig(meshtastic_Config_LoRaConfig_RegionCode_EU_868, true, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST); testAdmin->handleSetConfig(c, true); // fromOthers = true // Valid preset should be accepted regardless of fromOthers TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST, config.lora.modem_preset); } // ----------------------------------------------------------------------- // Test runner // ----------------------------------------------------------------------- void setUp(void) { mockMeshService = new MockMeshService(); service = mockMeshService; testAdmin = new AdminModuleTestShim(); } void tearDown(void) { service = nullptr; delete mockMeshService; mockMeshService = nullptr; delete testAdmin; testAdmin = nullptr; } void setup() { delay(10); delay(2000); initializeTestEnvironment(); UNITY_BEGIN(); // getRegion() RUN_TEST(test_getRegion_returnsCorrectRegion_US); RUN_TEST(test_getRegion_returnsCorrectRegion_EU868); RUN_TEST(test_getRegion_returnsCorrectRegion_LORA24); RUN_TEST(test_getRegion_unsetCodeReturnsUnsetEntry); RUN_TEST(test_getRegion_unknownCodeFallsToUnset); // validateConfigRegion() RUN_TEST(test_validateConfigRegion_validRegionReturnsTrue); RUN_TEST(test_validateConfigRegion_unsetRegionReturnsTrue); // Shadow table tests RUN_TEST(test_shadowTable_spacedProfileHasNonZeroSpacing); RUN_TEST(test_shadowTable_licensedProfileFlagsCorrect); RUN_TEST(test_shadowTable_presetCountMatchesExpected); RUN_TEST(test_shadowTable_defaultPresetIsFirstInList); RUN_TEST(test_shadowTable_channelSpacingWithPadding); RUN_TEST(test_shadowTable_turboOnlyOnWideLora); RUN_TEST(test_shadowTable_unknownCodeFallsToSentinel); // validateConfigLora() RUN_TEST(test_validateConfigLora_validPresetForUS); RUN_TEST(test_validateConfigLora_allStdPresetsValidForUS); RUN_TEST(test_validateConfigLora_turboPresetsInvalidForEU868); RUN_TEST(test_validateConfigLora_validPresetsForEU868); RUN_TEST(test_validateConfigLora_customBandwidthTooWideForEU868); RUN_TEST(test_validateConfigLora_customBandwidthFitsUS); RUN_TEST(test_validateConfigLora_customBandwidthFitsEU868); RUN_TEST(test_validateConfigLora_bogusPresetRejected); RUN_TEST(test_validateConfigLora_unsetRegionOnlyAcceptsLongFast); RUN_TEST(test_validateConfigLora_allPresetsValidForLORA24); // clampConfigLora() RUN_TEST(test_clampConfigLora_invalidPresetClampedToDefault); RUN_TEST(test_clampConfigLora_validPresetUnchanged); RUN_TEST(test_clampConfigLora_customBwTooWideClampedToDefaultBw); RUN_TEST(test_clampConfigLora_customBwValidLeftUnchanged); RUN_TEST(test_clampConfigLora_bogusPresetOnUnsetClampedToLongFast); RUN_TEST(test_clampConfigLora_invalidPresetOnLORA24ClampedToDefault); // RegionInfo preset list integrity RUN_TEST(test_presetsStd_hasNineEntries); RUN_TEST(test_presetsEU868_hasSevenEntries); RUN_TEST(test_presetsUndef_hasOneEntry); RUN_TEST(test_defaultPresetIsInAvailablePresets); RUN_TEST(test_regionFieldsAreSane); RUN_TEST(test_onlyLORA24HasWideLora); // Channel spacing (current + placeholder) RUN_TEST(test_channelSpacingCalculation_US_LONG_FAST); RUN_TEST(test_channelSpacingCalculation_EU868_LONG_FAST); RUN_TEST(test_channelSpacingCalculation_placeholder); // handleSetConfig fromOthers dispatch RUN_TEST(test_handleSetConfig_fromOthers_invalidPresetRejected); RUN_TEST(test_handleSetConfig_fromLocal_invalidPresetClamped); RUN_TEST(test_handleSetConfig_fromOthers_validPresetAccepted); exit(UNITY_END()); } void loop() {} ================================================ FILE: test/test_atak/test_main.cpp ================================================ #include #include #include "TestUtil.h" #include "meshUtils.h" void setUp(void) { // set stuff up here } void tearDown(void) { // clean stuff up here } /** * Test normal string without embedded nulls * Should behave the same as strlen() for regular strings */ void test_normal_string(void) { char test_str[32] = "Hello World"; size_t expected = 11; // strlen("Hello World") size_t result = pb_string_length(test_str, sizeof(test_str)); TEST_ASSERT_EQUAL_size_t(expected, result); } /** * Test empty string * Should return 0 for empty string */ void test_empty_string(void) { char test_str[32] = ""; size_t expected = 0; size_t result = pb_string_length(test_str, sizeof(test_str)); TEST_ASSERT_EQUAL_size_t(expected, result); } /** * Test string with only trailing nulls * Common case - string followed by null padding */ void test_trailing_nulls(void) { char test_str[32] = {0}; strcpy(test_str, "Test"); // test_str is now: "Test\0\0\0\0..." (4 chars + 28 nulls) size_t expected = 4; size_t result = pb_string_length(test_str, sizeof(test_str)); TEST_ASSERT_EQUAL_size_t(expected, result); } /** * Test string with embedded null byte * This is the critical bug case - strlen() would truncate at first null */ void test_embedded_null(void) { char test_str[32] = {0}; // Create string "ABC\0XYZ" (embedded null after C) test_str[0] = 'A'; test_str[1] = 'B'; test_str[2] = 'C'; test_str[3] = '\0'; // embedded null test_str[4] = 'X'; test_str[5] = 'Y'; test_str[6] = 'Z'; // Rest is already null from initialization // strlen would return 3, but pb_string_length should return 7 size_t strlen_result = strlen(test_str); size_t pb_result = pb_string_length(test_str, sizeof(test_str)); TEST_ASSERT_EQUAL_size_t(3, strlen_result); // strlen stops at first null TEST_ASSERT_EQUAL_size_t(7, pb_result); // pb_string_length finds last non-null } /** * Test Android UID with embedded null bytes * Real-world case from bug report: ANDROID-e7e455b40002429d * The "00" in the UID represents 0x00 bytes that were truncating the string */ void test_android_uid_pattern(void) { char test_str[32] = {0}; // Simulate "ANDROID-e7e455b4" + 0x00 + 0x00 + "2429d" const char part1[] = "ANDROID-e7e455b4"; strcpy(test_str, part1); size_t pos = strlen(part1); test_str[pos] = '\0'; // embedded null test_str[pos + 1] = '\0'; // another embedded null strcpy(test_str + pos + 2, "2429d"); // The full UID should be 24 characters size_t strlen_result = strlen(test_str); size_t pb_result = pb_string_length(test_str, sizeof(test_str)); TEST_ASSERT_EQUAL_size_t(16, strlen_result); // strlen truncates to "ANDROID-e7e455b4" TEST_ASSERT_EQUAL_size_t(23, pb_result); // pb_string_length gets full length } /** * Test string with multiple embedded nulls * Edge case with several null bytes scattered through the string */ void test_multiple_embedded_nulls(void) { char test_str[32] = {0}; // Create "A\0B\0C\0D" (3 embedded nulls) test_str[0] = 'A'; test_str[1] = '\0'; test_str[2] = 'B'; test_str[3] = '\0'; test_str[4] = 'C'; test_str[5] = '\0'; test_str[6] = 'D'; size_t strlen_result = strlen(test_str); size_t pb_result = pb_string_length(test_str, sizeof(test_str)); TEST_ASSERT_EQUAL_size_t(1, strlen_result); // strlen stops at first null TEST_ASSERT_EQUAL_size_t(7, pb_result); // pb_string_length finds all chars } /** * Test buffer completely filled with non-null characters * Edge case where string uses entire buffer */ void test_full_buffer(void) { char test_str[8]; // Fill entire buffer with 'X' memset(test_str, 'X', sizeof(test_str)); size_t result = pb_string_length(test_str, sizeof(test_str)); TEST_ASSERT_EQUAL_size_t(8, result); } /** * Test buffer with all nulls * Should return 0 */ void test_all_nulls(void) { char test_str[32] = {0}; size_t result = pb_string_length(test_str, sizeof(test_str)); TEST_ASSERT_EQUAL_size_t(0, result); } /** * Test single character followed by nulls * Minimal non-empty case */ void test_single_char(void) { char test_str[32] = {0}; test_str[0] = 'X'; size_t result = pb_string_length(test_str, sizeof(test_str)); TEST_ASSERT_EQUAL_size_t(1, result); } /** * Test callsign field typical size * Test with typical ATAK callsign field size (64 bytes) */ void test_callsign_field_size(void) { char test_str[64] = {0}; strcpy(test_str, "CALLSIGN-123"); size_t result = pb_string_length(test_str, sizeof(test_str)); TEST_ASSERT_EQUAL_size_t(12, result); } /** * Test with data at end of buffer * String with embedded null and data at very end */ void test_data_at_buffer_end(void) { char test_str[10] = {0}; test_str[0] = 'A'; test_str[1] = '\0'; test_str[8] = 'Z'; // Data near end test_str[9] = 'X'; // Data at end size_t result = pb_string_length(test_str, sizeof(test_str)); TEST_ASSERT_EQUAL_size_t(10, result); // Should find the 'X' at position 9 } void setup() { // NOTE!!! Wait for >2 secs // if board doesn't support software reset via Serial.DTR/RTS testDelay(10); testDelay(2000); UNITY_BEGIN(); RUN_TEST(test_normal_string); RUN_TEST(test_empty_string); RUN_TEST(test_trailing_nulls); RUN_TEST(test_embedded_null); RUN_TEST(test_android_uid_pattern); RUN_TEST(test_multiple_embedded_nulls); RUN_TEST(test_full_buffer); RUN_TEST(test_all_nulls); RUN_TEST(test_single_char); RUN_TEST(test_callsign_field_size); RUN_TEST(test_data_at_buffer_end); exit(UNITY_END()); } void loop() {} ================================================ FILE: test/test_crypto/test_main.cpp ================================================ // trunk-ignore-all(gitleaks): These are dummy values. Not real secrets. #include "CryptoEngine.h" #include "TestUtil.h" #include void HexToBytes(uint8_t *result, const std::string hex, size_t len = 0) { if (len) { memset(result, 0, len); } for (unsigned int i = 0; i < hex.length(); i += 2) { std::string byteString = hex.substr(i, 2); result[i / 2] = (uint8_t)strtol(byteString.c_str(), NULL, 16); } return; } void 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(void) { uint8_t private_key[32]; meshtastic_UserLite_public_key_t public_key; uint8_t expected_shared[32]; uint8_t expected_decrypted[32]; uint8_t radioBytes[128] __attribute__((__aligned__)); uint8_t decrypted[128] __attribute__((__aligned__)); uint8_t expected_nonce[16]; uint32_t fromNode = 0x0929; uint64_t packetNum = 0x13b2d662; HexToBytes(public_key.bytes, "db18fc50eea47f00251cb784819a3cf5fc361882597f589f0d7ff820e8064457"); public_key.size = 32; HexToBytes(private_key, "a00330633e63522f8a4d81ec6d9d1e6617f6c8ffd3a4c698229537d44e522277"); HexToBytes(expected_shared, "777b1545c9d6f9a2"); HexToBytes(expected_decrypted, "08011204746573744800"); HexToBytes(radioBytes, "8c646d7a2909000062d6b2136b00000040df24abfcc30a17a3d9046726099e796a1c036a792b"); HexToBytes(expected_nonce, "62d6b213036a792b2909000000"); crypto->setDHPrivateKey(private_key); TEST_ASSERT(crypto->decryptCurve25519(fromNode, public_key, packetNum, 22, radioBytes + 16, decrypted)); TEST_ASSERT_EQUAL_MEMORY(expected_shared, crypto->shared_key, 8); TEST_ASSERT_EQUAL_MEMORY(expected_nonce, crypto->nonce, 13); TEST_ASSERT_EQUAL_MEMORY(expected_decrypted, decrypted, 10); uint32_t toNode = 0; // Only impacts logging uint8_t encrypted[128] __attribute__((__aligned__)); TEST_ASSERT(crypto->encryptCurve25519(toNode, fromNode, public_key, packetNum, 10, decrypted, encrypted)); TEST_ASSERT_EQUAL_MEMORY(expected_shared, crypto->shared_key, 8); // The extraNonce is random, so skip checking the nonce and encrypted output here // Copy the nonce to check it after encryption memcpy(expected_nonce, crypto->nonce, 16); // Decrypt the re-encrypted bytes and check they are the same as what we expect TEST_ASSERT(crypto->decryptCurve25519(fromNode, public_key, packetNum, 22, encrypted, decrypted)); TEST_ASSERT_EQUAL_MEMORY(expected_shared, crypto->shared_key, 8); TEST_ASSERT_EQUAL_MEMORY(expected_nonce, crypto->nonce, 13); TEST_ASSERT_EQUAL_MEMORY(expected_decrypted, decrypted, 10); } 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); initializeTestEnvironment(); UNITY_BEGIN(); // IMPORTANT LINE! RUN_TEST(test_SHA256); RUN_TEST(test_ECB_AES256); RUN_TEST(test_DH25519); RUN_TEST(test_AES_CTR); RUN_TEST(test_PKC); exit(UNITY_END()); // stop unit testing } void loop() {} ================================================ FILE: test/test_default/test_main.cpp ================================================ // Unit tests for Default::getConfiguredOrDefaultMsScaled #include "Default.h" #include "MeshRadio.h" #include "TestUtil.h" #include "meshUtils.h" #include // Helper to compute expected ms using same logic as Default::congestionScalingCoefficient static uint32_t computeExpectedMs(uint32_t defaultSeconds, uint32_t numOnlineNodes) { uint32_t baseMs = Default::getConfiguredOrDefaultMs(0, defaultSeconds); // Routers (including ROUTER_LATE) don't scale if (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER || config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE) { return baseMs; } // Sensors and trackers don't scale if ((config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR) || (config.device.role == meshtastic_Config_DeviceConfig_Role_TRACKER)) { return baseMs; } if (numOnlineNodes <= 40) { return baseMs; } float bwKHz = config.lora.use_preset ? modemPresetToBwKHz(config.lora.modem_preset, false) : bwCodeToKHz(config.lora.bandwidth); uint8_t sf = config.lora.spread_factor; if (sf < 7) sf = 7; else if (sf > 12) sf = 12; float throttlingFactor = static_cast(pow_of_2(sf)) / (bwKHz * 100.0f); #if USERPREFS_EVENT_MODE throttlingFactor = static_cast(pow_of_2(sf)) / (bwKHz * 25.0f); #endif int nodesOverForty = (numOnlineNodes - 40); float coeff = 1.0f + (nodesOverForty * throttlingFactor); return static_cast(baseMs * coeff + 0.5f); } void test_router_no_scaling() { config.device.role = meshtastic_Config_DeviceConfig_Role_ROUTER; // set some sane lora config so bootstrap paths are deterministic config.lora.use_preset = false; config.lora.spread_factor = 9; config.lora.bandwidth = 250; uint32_t res = Default::getConfiguredOrDefaultMsScaled(0, 60, 100); uint32_t expected = computeExpectedMs(60, 100); TEST_ASSERT_EQUAL_UINT32(expected, res); } void test_client_below_threshold() { config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; config.lora.use_preset = false; config.lora.spread_factor = 9; config.lora.bandwidth = 250; uint32_t res = Default::getConfiguredOrDefaultMsScaled(0, 60, 40); uint32_t expected = computeExpectedMs(60, 40); TEST_ASSERT_EQUAL_UINT32(expected, res); } void test_client_default_preset_scaling() { config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; config.lora.use_preset = false; config.lora.spread_factor = 9; // SF9 config.lora.bandwidth = 250; // 250 kHz uint32_t res = Default::getConfiguredOrDefaultMsScaled(0, 60, 50); uint32_t expected = computeExpectedMs(60, 50); // nodesOverForty = 10 TEST_ASSERT_EQUAL_UINT32(expected, res); } void test_client_medium_fast_preset_scaling() { config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; config.lora.use_preset = true; config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST; // nodesOverForty = 30 -> test with nodes=70 uint32_t res = Default::getConfiguredOrDefaultMsScaled(0, 60, 70); uint32_t expected = computeExpectedMs(60, 70); // Allow ±1 ms tolerance for floating-point rounding TEST_ASSERT_INT_WITHIN(1, expected, res); } void test_router_uses_router_minimums() { config.device.role = meshtastic_Config_DeviceConfig_Role_ROUTER; uint32_t telemetry = Default::getConfiguredOrMinimumValue(60, min_default_telemetry_interval_secs); uint32_t position = Default::getConfiguredOrMinimumValue(60, min_default_broadcast_interval_secs); TEST_ASSERT_EQUAL_UINT32(ONE_DAY / 2, telemetry); TEST_ASSERT_EQUAL_UINT32(ONE_DAY / 2, position); } void test_router_late_uses_router_minimums() { config.device.role = meshtastic_Config_DeviceConfig_Role_ROUTER_LATE; uint32_t telemetry = Default::getConfiguredOrMinimumValue(60, min_default_telemetry_interval_secs); uint32_t position = Default::getConfiguredOrMinimumValue(60, min_default_broadcast_interval_secs); TEST_ASSERT_EQUAL_UINT32(ONE_DAY / 2, telemetry); TEST_ASSERT_EQUAL_UINT32(ONE_DAY / 2, position); } void test_client_uses_public_channel_minimums() { config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; uint32_t telemetry = Default::getConfiguredOrMinimumValue(60, min_default_telemetry_interval_secs); uint32_t position = Default::getConfiguredOrMinimumValue(60, min_default_broadcast_interval_secs); TEST_ASSERT_EQUAL_UINT32(30 * 60, telemetry); TEST_ASSERT_EQUAL_UINT32(60 * 60, position); } void setup() { // Small delay to match other test mains delay(10); initializeTestEnvironment(); UNITY_BEGIN(); RUN_TEST(test_router_no_scaling); RUN_TEST(test_client_below_threshold); RUN_TEST(test_client_default_preset_scaling); RUN_TEST(test_client_medium_fast_preset_scaling); RUN_TEST(test_router_uses_router_minimums); RUN_TEST(test_router_late_uses_router_minimums); RUN_TEST(test_client_uses_public_channel_minimums); exit(UNITY_END()); } void loop() {} ================================================ FILE: test/test_http_content_handler/test_main.cpp ================================================ #include "TestUtil.h" #include static void test_placeholder() { TEST_ASSERT_TRUE(true); } extern "C" { void setup() { initializeTestEnvironment(); UNITY_BEGIN(); RUN_TEST(test_placeholder); exit(UNITY_END()); } void loop() {} } ================================================ FILE: test/test_mesh_module/test_main.cpp ================================================ #include "MeshModule.h" #include "MeshTypes.h" #include "TestUtil.h" #include // Minimal concrete subclass for testing the base class helper class TestModule : public MeshModule { public: TestModule() : MeshModule("TestModule") {} virtual bool wantPacket(const meshtastic_MeshPacket *p) override { return true; } using MeshModule::currentRequest; using MeshModule::isMultiHopBroadcastRequest; }; static TestModule *testModule; static meshtastic_MeshPacket testPacket; void setUp(void) { testModule = new TestModule(); memset(&testPacket, 0, sizeof(testPacket)); TestModule::currentRequest = &testPacket; } void tearDown(void) { TestModule::currentRequest = NULL; delete testModule; } // Zero-hop broadcast (hop_limit == hop_start): should be allowed static void test_zeroHopBroadcast_isAllowed() { testPacket.to = NODENUM_BROADCAST; testPacket.hop_start = 3; testPacket.hop_limit = 3; // Not yet relayed TEST_ASSERT_FALSE(testModule->isMultiHopBroadcastRequest()); } // Multi-hop broadcast (hop_limit < hop_start): should be blocked static void test_multiHopBroadcast_isBlocked() { testPacket.to = NODENUM_BROADCAST; testPacket.hop_start = 7; testPacket.hop_limit = 4; // Already relayed 3 hops TEST_ASSERT_TRUE(testModule->isMultiHopBroadcastRequest()); } // Direct message (not broadcast): should always be allowed regardless of hops static void test_directMessage_isAllowed() { testPacket.to = 0x12345678; // Specific node testPacket.hop_start = 7; testPacket.hop_limit = 4; TEST_ASSERT_FALSE(testModule->isMultiHopBroadcastRequest()); } // Broadcast with hop_limit == 0 (fully relayed): should be blocked static void test_fullyRelayedBroadcast_isBlocked() { testPacket.to = NODENUM_BROADCAST; testPacket.hop_start = 3; testPacket.hop_limit = 0; TEST_ASSERT_TRUE(testModule->isMultiHopBroadcastRequest()); } // No current request: should not crash, should return false static void test_noCurrentRequest_isAllowed() { TestModule::currentRequest = NULL; TEST_ASSERT_FALSE(testModule->isMultiHopBroadcastRequest()); } // Broadcast with hop_start == 0 (legacy or local): should be allowed static void test_legacyPacket_zeroHopStart_isAllowed() { testPacket.to = NODENUM_BROADCAST; testPacket.hop_start = 0; testPacket.hop_limit = 0; // hop_limit == hop_start, so not multi-hop TEST_ASSERT_FALSE(testModule->isMultiHopBroadcastRequest()); } // Single hop relayed broadcast (hop_limit = hop_start - 1): should be blocked static void test_singleHopRelayedBroadcast_isBlocked() { testPacket.to = NODENUM_BROADCAST; testPacket.hop_start = 3; testPacket.hop_limit = 2; TEST_ASSERT_TRUE(testModule->isMultiHopBroadcastRequest()); } void setup() { initializeTestEnvironment(); UNITY_BEGIN(); RUN_TEST(test_zeroHopBroadcast_isAllowed); RUN_TEST(test_multiHopBroadcast_isBlocked); RUN_TEST(test_directMessage_isAllowed); RUN_TEST(test_fullyRelayedBroadcast_isBlocked); RUN_TEST(test_noCurrentRequest_isAllowed); RUN_TEST(test_legacyPacket_zeroHopStart_isAllowed); RUN_TEST(test_singleHopRelayedBroadcast_isBlocked); exit(UNITY_END()); } void loop() {} ================================================ FILE: test/test_meshpacket_serializer/ports/test_encrypted.cpp ================================================ #include "../test_helpers.h" // Helper function for all encrypted packet assertions void assert_encrypted_packet(const std::string &json, meshtastic_MeshPacket packet) { // Parse and validate JSON TEST_ASSERT_TRUE(json.length() > 0); JSONValue *root = JSON::Parse(json.c_str()); TEST_ASSERT_NOT_NULL(root); TEST_ASSERT_TRUE(root->IsObject()); JSONObject jsonObj = root->AsObject(); // Assert basic packet fields TEST_ASSERT_TRUE(jsonObj.find("from") != jsonObj.end()); TEST_ASSERT_EQUAL(packet.from, (uint32_t)jsonObj.at("from")->AsNumber()); TEST_ASSERT_TRUE(jsonObj.find("to") != jsonObj.end()); TEST_ASSERT_EQUAL(packet.to, (uint32_t)jsonObj.at("to")->AsNumber()); TEST_ASSERT_TRUE(jsonObj.find("id") != jsonObj.end()); TEST_ASSERT_EQUAL(packet.id, (uint32_t)jsonObj.at("id")->AsNumber()); // Assert encrypted data fields TEST_ASSERT_TRUE(jsonObj.find("bytes") != jsonObj.end()); TEST_ASSERT_TRUE(jsonObj.at("bytes")->IsString()); TEST_ASSERT_TRUE(jsonObj.find("size") != jsonObj.end()); TEST_ASSERT_EQUAL(packet.encrypted.size, (int)jsonObj.at("size")->AsNumber()); // Assert hex encoding std::string encrypted_hex = jsonObj["bytes"]->AsString(); TEST_ASSERT_EQUAL(packet.encrypted.size * 2, encrypted_hex.length()); delete root; } // Test encrypted packet serialization void test_encrypted_packet_serialization() { const char *data = "encrypted_payload_data"; meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TEXT_MESSAGE_APP, reinterpret_cast(data), strlen(data), meshtastic_MeshPacket_encrypted_tag); std::string json = MeshPacketSerializer::JsonSerializeEncrypted(&packet); assert_encrypted_packet(json, packet); } // Test empty encrypted packet void test_empty_encrypted_packet() { meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TEXT_MESSAGE_APP, nullptr, 0, meshtastic_MeshPacket_encrypted_tag); std::string json = MeshPacketSerializer::JsonSerializeEncrypted(&packet); assert_encrypted_packet(json, packet); } ================================================ FILE: test/test_meshpacket_serializer/ports/test_nodeinfo.cpp ================================================ #include "../test_helpers.h" static size_t encode_user_info(uint8_t *buffer, size_t buffer_size) { meshtastic_User user = meshtastic_User_init_zero; strcpy(user.short_name, "TEST"); strcpy(user.long_name, "Test User"); strcpy(user.id, "!12345678"); user.hw_model = meshtastic_HardwareModel_HELTEC_V3; pb_ostream_t stream = pb_ostream_from_buffer(buffer, buffer_size); pb_encode(&stream, &meshtastic_User_msg, &user); return stream.bytes_written; } // Test NODEINFO_APP port void test_nodeinfo_serialization() { uint8_t buffer[256]; size_t payload_size = encode_user_info(buffer, sizeof(buffer)); meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_NODEINFO_APP, buffer, payload_size); std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); TEST_ASSERT_TRUE(json.length() > 0); JSONValue *root = JSON::Parse(json.c_str()); TEST_ASSERT_NOT_NULL(root); TEST_ASSERT_TRUE(root->IsObject()); JSONObject jsonObj = root->AsObject(); // Check message type TEST_ASSERT_TRUE(jsonObj.find("type") != jsonObj.end()); TEST_ASSERT_EQUAL_STRING("nodeinfo", jsonObj["type"]->AsString().c_str()); // Check payload TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end()); TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject()); JSONObject payload = jsonObj["payload"]->AsObject(); // Verify user data TEST_ASSERT_TRUE(payload.find("shortname") != payload.end()); TEST_ASSERT_EQUAL_STRING("TEST", payload["shortname"]->AsString().c_str()); TEST_ASSERT_TRUE(payload.find("longname") != payload.end()); TEST_ASSERT_EQUAL_STRING("Test User", payload["longname"]->AsString().c_str()); delete root; } ================================================ FILE: test/test_meshpacket_serializer/ports/test_position.cpp ================================================ #include "../test_helpers.h" static size_t encode_position(uint8_t *buffer, size_t buffer_size) { meshtastic_Position position = meshtastic_Position_init_zero; position.latitude_i = 374208000; // 37.4208 degrees * 1e7 position.longitude_i = -1221981000; // -122.1981 degrees * 1e7 position.altitude = 123; position.time = 1609459200; position.has_altitude = true; position.has_latitude_i = true; position.has_longitude_i = true; pb_ostream_t stream = pb_ostream_from_buffer(buffer, buffer_size); pb_encode(&stream, &meshtastic_Position_msg, &position); return stream.bytes_written; } // Test POSITION_APP port void test_position_serialization() { uint8_t buffer[256]; size_t payload_size = encode_position(buffer, sizeof(buffer)); meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_POSITION_APP, buffer, payload_size); std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); TEST_ASSERT_TRUE(json.length() > 0); JSONValue *root = JSON::Parse(json.c_str()); TEST_ASSERT_NOT_NULL(root); TEST_ASSERT_TRUE(root->IsObject()); JSONObject jsonObj = root->AsObject(); // Check message type TEST_ASSERT_TRUE(jsonObj.find("type") != jsonObj.end()); TEST_ASSERT_EQUAL_STRING("position", jsonObj["type"]->AsString().c_str()); // Check payload TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end()); TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject()); JSONObject payload = jsonObj["payload"]->AsObject(); // Verify position data TEST_ASSERT_TRUE(payload.find("latitude_i") != payload.end()); TEST_ASSERT_EQUAL(374208000, (int)payload["latitude_i"]->AsNumber()); TEST_ASSERT_TRUE(payload.find("longitude_i") != payload.end()); TEST_ASSERT_EQUAL(-1221981000, (int)payload["longitude_i"]->AsNumber()); TEST_ASSERT_TRUE(payload.find("altitude") != payload.end()); TEST_ASSERT_EQUAL(123, (int)payload["altitude"]->AsNumber()); delete root; } ================================================ FILE: test/test_meshpacket_serializer/ports/test_telemetry.cpp ================================================ #include "../test_helpers.h" // Helper function to create and encode device metrics static size_t encode_telemetry_device_metrics(uint8_t *buffer, size_t buffer_size) { meshtastic_Telemetry telemetry = meshtastic_Telemetry_init_zero; telemetry.time = 1609459200; telemetry.which_variant = meshtastic_Telemetry_device_metrics_tag; telemetry.variant.device_metrics.battery_level = 85; telemetry.variant.device_metrics.has_battery_level = true; telemetry.variant.device_metrics.voltage = 3.72f; telemetry.variant.device_metrics.has_voltage = true; telemetry.variant.device_metrics.channel_utilization = 15.56f; telemetry.variant.device_metrics.has_channel_utilization = true; telemetry.variant.device_metrics.air_util_tx = 8.23f; telemetry.variant.device_metrics.has_air_util_tx = true; telemetry.variant.device_metrics.uptime_seconds = 12345; telemetry.variant.device_metrics.has_uptime_seconds = true; pb_ostream_t stream = pb_ostream_from_buffer(buffer, buffer_size); pb_encode(&stream, &meshtastic_Telemetry_msg, &telemetry); return stream.bytes_written; } // Helper function to create and encode empty environment metrics (no fields set) static size_t encode_telemetry_environment_metrics_empty(uint8_t *buffer, size_t buffer_size) { meshtastic_Telemetry telemetry = meshtastic_Telemetry_init_zero; telemetry.time = 1609459200; telemetry.which_variant = meshtastic_Telemetry_environment_metrics_tag; // NO fields are set - all has_* flags remain false // This tests that empty environment metrics don't produce any JSON fields pb_ostream_t stream = pb_ostream_from_buffer(buffer, buffer_size); pb_encode(&stream, &meshtastic_Telemetry_msg, &telemetry); return stream.bytes_written; } // Helper function to create environment metrics with ALL possible fields set // This function should be updated whenever new fields are added to the protobuf static size_t encode_telemetry_environment_metrics_all_fields(uint8_t *buffer, size_t buffer_size) { meshtastic_Telemetry telemetry = meshtastic_Telemetry_init_zero; telemetry.time = 1609459200; telemetry.which_variant = meshtastic_Telemetry_environment_metrics_tag; // Basic environment metrics telemetry.variant.environment_metrics.temperature = 23.56f; telemetry.variant.environment_metrics.has_temperature = true; telemetry.variant.environment_metrics.relative_humidity = 65.43f; telemetry.variant.environment_metrics.has_relative_humidity = true; telemetry.variant.environment_metrics.barometric_pressure = 1013.27f; telemetry.variant.environment_metrics.has_barometric_pressure = true; // Gas and air quality telemetry.variant.environment_metrics.gas_resistance = 50.58f; telemetry.variant.environment_metrics.has_gas_resistance = true; telemetry.variant.environment_metrics.iaq = 120; telemetry.variant.environment_metrics.has_iaq = true; // Power measurements telemetry.variant.environment_metrics.voltage = 3.34f; telemetry.variant.environment_metrics.has_voltage = true; telemetry.variant.environment_metrics.current = 0.53f; telemetry.variant.environment_metrics.has_current = true; // Light measurements (ALL 4 types) telemetry.variant.environment_metrics.lux = 450.12f; telemetry.variant.environment_metrics.has_lux = true; telemetry.variant.environment_metrics.white_lux = 380.95f; telemetry.variant.environment_metrics.has_white_lux = true; telemetry.variant.environment_metrics.ir_lux = 25.37f; telemetry.variant.environment_metrics.has_ir_lux = true; telemetry.variant.environment_metrics.uv_lux = 15.68f; telemetry.variant.environment_metrics.has_uv_lux = true; // Distance measurement telemetry.variant.environment_metrics.distance = 150.29f; telemetry.variant.environment_metrics.has_distance = true; // Wind measurements (ALL 4 types) telemetry.variant.environment_metrics.wind_direction = 180; telemetry.variant.environment_metrics.has_wind_direction = true; telemetry.variant.environment_metrics.wind_speed = 5.52f; telemetry.variant.environment_metrics.has_wind_speed = true; telemetry.variant.environment_metrics.wind_gust = 8.24f; telemetry.variant.environment_metrics.has_wind_gust = true; telemetry.variant.environment_metrics.wind_lull = 2.13f; telemetry.variant.environment_metrics.has_wind_lull = true; // Weight measurement telemetry.variant.environment_metrics.weight = 75.56f; telemetry.variant.environment_metrics.has_weight = true; // Radiation measurement telemetry.variant.environment_metrics.radiation = 0.13f; telemetry.variant.environment_metrics.has_radiation = true; // Rainfall measurements (BOTH types) telemetry.variant.environment_metrics.rainfall_1h = 2.57f; telemetry.variant.environment_metrics.has_rainfall_1h = true; telemetry.variant.environment_metrics.rainfall_24h = 15.89f; telemetry.variant.environment_metrics.has_rainfall_24h = true; // Soil measurements (BOTH types) telemetry.variant.environment_metrics.soil_moisture = 85; telemetry.variant.environment_metrics.has_soil_moisture = true; telemetry.variant.environment_metrics.soil_temperature = 18.54f; telemetry.variant.environment_metrics.has_soil_temperature = true; // IMPORTANT: When new environment fields are added to the protobuf, // they MUST be added here too, or the coverage test will fail! pb_ostream_t stream = pb_ostream_from_buffer(buffer, buffer_size); pb_encode(&stream, &meshtastic_Telemetry_msg, &telemetry); return stream.bytes_written; } // Helper function to create and encode environment metrics with all current fields static size_t encode_telemetry_environment_metrics(uint8_t *buffer, size_t buffer_size) { meshtastic_Telemetry telemetry = meshtastic_Telemetry_init_zero; telemetry.time = 1609459200; telemetry.which_variant = meshtastic_Telemetry_environment_metrics_tag; // Basic environment metrics telemetry.variant.environment_metrics.temperature = 23.56f; telemetry.variant.environment_metrics.has_temperature = true; telemetry.variant.environment_metrics.relative_humidity = 65.43f; telemetry.variant.environment_metrics.has_relative_humidity = true; telemetry.variant.environment_metrics.barometric_pressure = 1013.27f; telemetry.variant.environment_metrics.has_barometric_pressure = true; // Gas and air quality telemetry.variant.environment_metrics.gas_resistance = 50.58f; telemetry.variant.environment_metrics.has_gas_resistance = true; telemetry.variant.environment_metrics.iaq = 120; telemetry.variant.environment_metrics.has_iaq = true; // Power measurements telemetry.variant.environment_metrics.voltage = 3.34f; telemetry.variant.environment_metrics.has_voltage = true; telemetry.variant.environment_metrics.current = 0.53f; telemetry.variant.environment_metrics.has_current = true; // Light measurements telemetry.variant.environment_metrics.lux = 450.12f; telemetry.variant.environment_metrics.has_lux = true; telemetry.variant.environment_metrics.white_lux = 380.95f; telemetry.variant.environment_metrics.has_white_lux = true; telemetry.variant.environment_metrics.ir_lux = 25.37f; telemetry.variant.environment_metrics.has_ir_lux = true; telemetry.variant.environment_metrics.uv_lux = 15.68f; telemetry.variant.environment_metrics.has_uv_lux = true; // Distance measurement telemetry.variant.environment_metrics.distance = 150.29f; telemetry.variant.environment_metrics.has_distance = true; // Wind measurements telemetry.variant.environment_metrics.wind_direction = 180; telemetry.variant.environment_metrics.has_wind_direction = true; telemetry.variant.environment_metrics.wind_speed = 5.52f; telemetry.variant.environment_metrics.has_wind_speed = true; telemetry.variant.environment_metrics.wind_gust = 8.24f; telemetry.variant.environment_metrics.has_wind_gust = true; telemetry.variant.environment_metrics.wind_lull = 2.13f; telemetry.variant.environment_metrics.has_wind_lull = true; // Weight measurement telemetry.variant.environment_metrics.weight = 75.56f; telemetry.variant.environment_metrics.has_weight = true; // Radiation measurement telemetry.variant.environment_metrics.radiation = 0.13f; telemetry.variant.environment_metrics.has_radiation = true; // Rainfall measurements telemetry.variant.environment_metrics.rainfall_1h = 2.57f; telemetry.variant.environment_metrics.has_rainfall_1h = true; telemetry.variant.environment_metrics.rainfall_24h = 15.89f; telemetry.variant.environment_metrics.has_rainfall_24h = true; // Soil measurements telemetry.variant.environment_metrics.soil_moisture = 85; telemetry.variant.environment_metrics.has_soil_moisture = true; telemetry.variant.environment_metrics.soil_temperature = 18.54f; telemetry.variant.environment_metrics.has_soil_temperature = true; pb_ostream_t stream = pb_ostream_from_buffer(buffer, buffer_size); pb_encode(&stream, &meshtastic_Telemetry_msg, &telemetry); return stream.bytes_written; } // Test TELEMETRY_APP port with device metrics void test_telemetry_device_metrics_serialization() { uint8_t buffer[256]; size_t payload_size = encode_telemetry_device_metrics(buffer, sizeof(buffer)); meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size); std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); TEST_ASSERT_TRUE(json.length() > 0); JSONValue *root = JSON::Parse(json.c_str()); TEST_ASSERT_NOT_NULL(root); TEST_ASSERT_TRUE(root->IsObject()); JSONObject jsonObj = root->AsObject(); // Check message type TEST_ASSERT_TRUE(jsonObj.find("type") != jsonObj.end()); TEST_ASSERT_EQUAL_STRING("telemetry", jsonObj["type"]->AsString().c_str()); // Check payload TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end()); TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject()); JSONObject payload = jsonObj["payload"]->AsObject(); // Verify telemetry data TEST_ASSERT_TRUE(payload.find("battery_level") != payload.end()); TEST_ASSERT_EQUAL(85, (int)payload["battery_level"]->AsNumber()); TEST_ASSERT_TRUE(payload.find("voltage") != payload.end()); TEST_ASSERT_FLOAT_WITHIN(0.01f, 3.72f, payload["voltage"]->AsNumber()); TEST_ASSERT_TRUE(payload.find("channel_utilization") != payload.end()); TEST_ASSERT_FLOAT_WITHIN(0.01f, 15.56f, payload["channel_utilization"]->AsNumber()); TEST_ASSERT_TRUE(payload.find("uptime_seconds") != payload.end()); TEST_ASSERT_EQUAL(12345, (int)payload["uptime_seconds"]->AsNumber()); // Note: JSON serialization may not preserve exact 2-decimal formatting due to float precision // We verify the numeric values are correct within tolerance delete root; } // Test that telemetry environment metrics are properly serialized void test_telemetry_environment_metrics_serialization() { uint8_t buffer[256]; size_t payload_size = encode_telemetry_environment_metrics(buffer, sizeof(buffer)); meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size); std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); TEST_ASSERT_TRUE(json.length() > 0); JSONValue *root = JSON::Parse(json.c_str()); TEST_ASSERT_NOT_NULL(root); TEST_ASSERT_TRUE(root->IsObject()); JSONObject jsonObj = root->AsObject(); // Check payload exists TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end()); TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject()); JSONObject payload = jsonObj["payload"]->AsObject(); // Test key fields that should be present in the serializer TEST_ASSERT_TRUE(payload.find("temperature") != payload.end()); TEST_ASSERT_FLOAT_WITHIN(0.01f, 23.56f, payload["temperature"]->AsNumber()); TEST_ASSERT_TRUE(payload.find("relative_humidity") != payload.end()); TEST_ASSERT_FLOAT_WITHIN(0.01f, 65.43f, payload["relative_humidity"]->AsNumber()); TEST_ASSERT_TRUE(payload.find("distance") != payload.end()); TEST_ASSERT_FLOAT_WITHIN(0.01f, 150.29f, payload["distance"]->AsNumber()); // Note: JSON serialization may have float precision limitations // We focus on verifying numeric accuracy rather than exact string formatting delete root; } // Test comprehensive environment metrics coverage void test_telemetry_environment_metrics_comprehensive() { uint8_t buffer[256]; size_t payload_size = encode_telemetry_environment_metrics(buffer, sizeof(buffer)); meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size); std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); TEST_ASSERT_TRUE(json.length() > 0); JSONValue *root = JSON::Parse(json.c_str()); TEST_ASSERT_NOT_NULL(root); TEST_ASSERT_TRUE(root->IsObject()); JSONObject jsonObj = root->AsObject(); // Check payload exists TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end()); TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject()); JSONObject payload = jsonObj["payload"]->AsObject(); // Check all 15 originally supported fields TEST_ASSERT_TRUE(payload.find("temperature") != payload.end()); TEST_ASSERT_TRUE(payload.find("relative_humidity") != payload.end()); TEST_ASSERT_TRUE(payload.find("barometric_pressure") != payload.end()); TEST_ASSERT_TRUE(payload.find("gas_resistance") != payload.end()); TEST_ASSERT_TRUE(payload.find("voltage") != payload.end()); TEST_ASSERT_TRUE(payload.find("current") != payload.end()); TEST_ASSERT_TRUE(payload.find("iaq") != payload.end()); TEST_ASSERT_TRUE(payload.find("distance") != payload.end()); TEST_ASSERT_TRUE(payload.find("lux") != payload.end()); TEST_ASSERT_TRUE(payload.find("white_lux") != payload.end()); TEST_ASSERT_TRUE(payload.find("wind_direction") != payload.end()); TEST_ASSERT_TRUE(payload.find("wind_speed") != payload.end()); TEST_ASSERT_TRUE(payload.find("wind_gust") != payload.end()); TEST_ASSERT_TRUE(payload.find("wind_lull") != payload.end()); TEST_ASSERT_TRUE(payload.find("radiation") != payload.end()); delete root; } // Test for the 7 environment fields that were added to complete coverage void test_telemetry_environment_metrics_missing_fields() { uint8_t buffer[256]; size_t payload_size = encode_telemetry_environment_metrics(buffer, sizeof(buffer)); meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size); std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); TEST_ASSERT_TRUE(json.length() > 0); JSONValue *root = JSON::Parse(json.c_str()); TEST_ASSERT_NOT_NULL(root); TEST_ASSERT_TRUE(root->IsObject()); JSONObject jsonObj = root->AsObject(); // Check payload exists TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end()); TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject()); JSONObject payload = jsonObj["payload"]->AsObject(); // Check the 7 fields that were previously missing TEST_ASSERT_TRUE(payload.find("ir_lux") != payload.end()); TEST_ASSERT_FLOAT_WITHIN(0.01f, 25.37f, payload["ir_lux"]->AsNumber()); TEST_ASSERT_TRUE(payload.find("uv_lux") != payload.end()); TEST_ASSERT_FLOAT_WITHIN(0.01f, 15.68f, payload["uv_lux"]->AsNumber()); TEST_ASSERT_TRUE(payload.find("weight") != payload.end()); TEST_ASSERT_FLOAT_WITHIN(0.01f, 75.56f, payload["weight"]->AsNumber()); TEST_ASSERT_TRUE(payload.find("rainfall_1h") != payload.end()); TEST_ASSERT_FLOAT_WITHIN(0.01f, 2.57f, payload["rainfall_1h"]->AsNumber()); TEST_ASSERT_TRUE(payload.find("rainfall_24h") != payload.end()); TEST_ASSERT_FLOAT_WITHIN(0.01f, 15.89f, payload["rainfall_24h"]->AsNumber()); TEST_ASSERT_TRUE(payload.find("soil_moisture") != payload.end()); TEST_ASSERT_EQUAL(85, (int)payload["soil_moisture"]->AsNumber()); TEST_ASSERT_TRUE(payload.find("soil_temperature") != payload.end()); TEST_ASSERT_FLOAT_WITHIN(0.01f, 18.54f, payload["soil_temperature"]->AsNumber()); // Note: JSON float serialization may not preserve exact decimal formatting // We verify the values are numerically correct within tolerance delete root; } // Test that ALL environment fields are serialized (canary test for forgotten fields) // This test will FAIL if a new environment field is added to the protobuf but not to the serializer void test_telemetry_environment_metrics_complete_coverage() { uint8_t buffer[256]; size_t payload_size = encode_telemetry_environment_metrics_all_fields(buffer, sizeof(buffer)); meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size); std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); TEST_ASSERT_TRUE(json.length() > 0); JSONValue *root = JSON::Parse(json.c_str()); TEST_ASSERT_NOT_NULL(root); TEST_ASSERT_TRUE(root->IsObject()); JSONObject jsonObj = root->AsObject(); // Check payload exists TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end()); TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject()); JSONObject payload = jsonObj["payload"]->AsObject(); // ✅ ALL 22 environment fields MUST be present and correct // If this test fails, it means either: // 1. A new field was added to the protobuf but not to the serializer // 2. The encode_telemetry_environment_metrics_all_fields() function wasn't updated // Basic environment (3 fields) TEST_ASSERT_TRUE(payload.find("temperature") != payload.end()); TEST_ASSERT_FLOAT_WITHIN(0.01f, 23.56f, payload["temperature"]->AsNumber()); TEST_ASSERT_TRUE(payload.find("relative_humidity") != payload.end()); TEST_ASSERT_FLOAT_WITHIN(0.01f, 65.43f, payload["relative_humidity"]->AsNumber()); TEST_ASSERT_TRUE(payload.find("barometric_pressure") != payload.end()); TEST_ASSERT_FLOAT_WITHIN(0.01f, 1013.27f, payload["barometric_pressure"]->AsNumber()); // Gas and air quality (2 fields) TEST_ASSERT_TRUE(payload.find("gas_resistance") != payload.end()); TEST_ASSERT_FLOAT_WITHIN(0.01f, 50.58f, payload["gas_resistance"]->AsNumber()); TEST_ASSERT_TRUE(payload.find("iaq") != payload.end()); TEST_ASSERT_EQUAL(120, (int)payload["iaq"]->AsNumber()); // Power measurements (2 fields) TEST_ASSERT_TRUE(payload.find("voltage") != payload.end()); TEST_ASSERT_FLOAT_WITHIN(0.01f, 3.34f, payload["voltage"]->AsNumber()); TEST_ASSERT_TRUE(payload.find("current") != payload.end()); TEST_ASSERT_FLOAT_WITHIN(0.01f, 0.53f, payload["current"]->AsNumber()); // Light measurements (4 fields) TEST_ASSERT_TRUE(payload.find("lux") != payload.end()); TEST_ASSERT_FLOAT_WITHIN(0.01f, 450.12f, payload["lux"]->AsNumber()); TEST_ASSERT_TRUE(payload.find("white_lux") != payload.end()); TEST_ASSERT_FLOAT_WITHIN(0.01f, 380.95f, payload["white_lux"]->AsNumber()); TEST_ASSERT_TRUE(payload.find("ir_lux") != payload.end()); TEST_ASSERT_FLOAT_WITHIN(0.01f, 25.37f, payload["ir_lux"]->AsNumber()); TEST_ASSERT_TRUE(payload.find("uv_lux") != payload.end()); TEST_ASSERT_FLOAT_WITHIN(0.01f, 15.68f, payload["uv_lux"]->AsNumber()); // Distance measurement (1 field) TEST_ASSERT_TRUE(payload.find("distance") != payload.end()); TEST_ASSERT_FLOAT_WITHIN(0.01f, 150.29f, payload["distance"]->AsNumber()); // Wind measurements (4 fields) TEST_ASSERT_TRUE(payload.find("wind_direction") != payload.end()); TEST_ASSERT_EQUAL(180, (int)payload["wind_direction"]->AsNumber()); TEST_ASSERT_TRUE(payload.find("wind_speed") != payload.end()); TEST_ASSERT_FLOAT_WITHIN(0.01f, 5.52f, payload["wind_speed"]->AsNumber()); TEST_ASSERT_TRUE(payload.find("wind_gust") != payload.end()); TEST_ASSERT_FLOAT_WITHIN(0.01f, 8.24f, payload["wind_gust"]->AsNumber()); TEST_ASSERT_TRUE(payload.find("wind_lull") != payload.end()); TEST_ASSERT_FLOAT_WITHIN(0.01f, 2.13f, payload["wind_lull"]->AsNumber()); // Weight measurement (1 field) TEST_ASSERT_TRUE(payload.find("weight") != payload.end()); TEST_ASSERT_FLOAT_WITHIN(0.01f, 75.56f, payload["weight"]->AsNumber()); // Radiation measurement (1 field) TEST_ASSERT_TRUE(payload.find("radiation") != payload.end()); TEST_ASSERT_FLOAT_WITHIN(0.01f, 0.13f, payload["radiation"]->AsNumber()); // Rainfall measurements (2 fields) TEST_ASSERT_TRUE(payload.find("rainfall_1h") != payload.end()); TEST_ASSERT_FLOAT_WITHIN(0.01f, 2.57f, payload["rainfall_1h"]->AsNumber()); TEST_ASSERT_TRUE(payload.find("rainfall_24h") != payload.end()); TEST_ASSERT_FLOAT_WITHIN(0.01f, 15.89f, payload["rainfall_24h"]->AsNumber()); // Soil measurements (2 fields) TEST_ASSERT_TRUE(payload.find("soil_moisture") != payload.end()); TEST_ASSERT_EQUAL(85, (int)payload["soil_moisture"]->AsNumber()); TEST_ASSERT_TRUE(payload.find("soil_temperature") != payload.end()); TEST_ASSERT_FLOAT_WITHIN(0.01f, 18.54f, payload["soil_temperature"]->AsNumber()); // Total: 22 environment fields // This test ensures 100% coverage of environment metrics // Note: JSON float serialization precision may vary due to the underlying library // The important aspect is that all values are numerically accurate within tolerance delete root; } // Test that unset environment fields are not present in JSON void test_telemetry_environment_metrics_unset_fields() { uint8_t buffer[256]; size_t payload_size = encode_telemetry_environment_metrics_empty(buffer, sizeof(buffer)); meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size); std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); TEST_ASSERT_TRUE(json.length() > 0); JSONValue *root = JSON::Parse(json.c_str()); TEST_ASSERT_NOT_NULL(root); TEST_ASSERT_TRUE(root->IsObject()); JSONObject jsonObj = root->AsObject(); // Check payload exists TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end()); TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject()); JSONObject payload = jsonObj["payload"]->AsObject(); // With completely empty environment metrics, NO fields should be present // Only basic telemetry fields like "time" might be present // All 22 environment fields should be absent (none were set) TEST_ASSERT_TRUE(payload.find("temperature") == payload.end()); TEST_ASSERT_TRUE(payload.find("relative_humidity") == payload.end()); TEST_ASSERT_TRUE(payload.find("barometric_pressure") == payload.end()); TEST_ASSERT_TRUE(payload.find("gas_resistance") == payload.end()); TEST_ASSERT_TRUE(payload.find("iaq") == payload.end()); TEST_ASSERT_TRUE(payload.find("voltage") == payload.end()); TEST_ASSERT_TRUE(payload.find("current") == payload.end()); TEST_ASSERT_TRUE(payload.find("lux") == payload.end()); TEST_ASSERT_TRUE(payload.find("white_lux") == payload.end()); TEST_ASSERT_TRUE(payload.find("ir_lux") == payload.end()); TEST_ASSERT_TRUE(payload.find("uv_lux") == payload.end()); TEST_ASSERT_TRUE(payload.find("distance") == payload.end()); TEST_ASSERT_TRUE(payload.find("wind_direction") == payload.end()); TEST_ASSERT_TRUE(payload.find("wind_speed") == payload.end()); TEST_ASSERT_TRUE(payload.find("wind_gust") == payload.end()); TEST_ASSERT_TRUE(payload.find("wind_lull") == payload.end()); TEST_ASSERT_TRUE(payload.find("weight") == payload.end()); TEST_ASSERT_TRUE(payload.find("radiation") == payload.end()); TEST_ASSERT_TRUE(payload.find("rainfall_1h") == payload.end()); TEST_ASSERT_TRUE(payload.find("rainfall_24h") == payload.end()); TEST_ASSERT_TRUE(payload.find("soil_moisture") == payload.end()); TEST_ASSERT_TRUE(payload.find("soil_temperature") == payload.end()); delete root; } ================================================ FILE: test/test_meshpacket_serializer/ports/test_text_message.cpp ================================================ #include "../test_helpers.h" #include // Helper function to test common packet fields and structure void verify_text_message_packet_structure(const std::string &json, const char *expected_text) { TEST_ASSERT_TRUE(json.length() > 0); // Use smart pointer for automatic memory management std::unique_ptr root(JSON::Parse(json.c_str())); TEST_ASSERT_NOT_NULL(root.get()); TEST_ASSERT_TRUE(root->IsObject()); JSONObject jsonObj = root->AsObject(); // Check basic packet fields - use helper function to reduce duplication auto check_field = [&](const char *field, uint32_t expected_value) { auto it = jsonObj.find(field); TEST_ASSERT_TRUE(it != jsonObj.end()); TEST_ASSERT_EQUAL(expected_value, (uint32_t)it->second->AsNumber()); }; check_field("from", 0x11223344); check_field("to", 0x55667788); check_field("id", 0x9999); // Check message type auto type_it = jsonObj.find("type"); TEST_ASSERT_TRUE(type_it != jsonObj.end()); TEST_ASSERT_EQUAL_STRING("text", type_it->second->AsString().c_str()); // Check payload auto payload_it = jsonObj.find("payload"); TEST_ASSERT_TRUE(payload_it != jsonObj.end()); TEST_ASSERT_TRUE(payload_it->second->IsObject()); JSONObject payload = payload_it->second->AsObject(); auto text_it = payload.find("text"); TEST_ASSERT_TRUE(text_it != payload.end()); TEST_ASSERT_EQUAL_STRING(expected_text, text_it->second->AsString().c_str()); // No need for manual delete with smart pointer } // Test TEXT_MESSAGE_APP port void test_text_message_serialization() { const char *test_text = "Hello Meshtastic!"; meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TEXT_MESSAGE_APP, reinterpret_cast(test_text), strlen(test_text)); std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); verify_text_message_packet_structure(json, test_text); } // Test with nullptr to check robustness void test_text_message_serialization_null() { meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TEXT_MESSAGE_APP, nullptr, 0); std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); verify_text_message_packet_structure(json, ""); } // Test TEXT_MESSAGE_APP port with very long message (boundary testing) void test_text_message_serialization_long_text() { // Test with actual message size limits constexpr size_t MAX_MESSAGE_SIZE = 200; // Typical LoRa payload limit std::string long_text(MAX_MESSAGE_SIZE, 'A'); meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TEXT_MESSAGE_APP, reinterpret_cast(long_text.c_str()), long_text.length()); std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); verify_text_message_packet_structure(json, long_text.c_str()); } // Test with message over size limit (should fail) void test_text_message_serialization_oversized() { constexpr size_t OVERSIZED_MESSAGE = 250; // Over the limit std::string oversized_text(OVERSIZED_MESSAGE, 'B'); meshtastic_MeshPacket packet = create_test_packet( meshtastic_PortNum_TEXT_MESSAGE_APP, reinterpret_cast(oversized_text.c_str()), oversized_text.length()); // Should fail or return empty/error std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); // Should only verify first 234 characters for oversized messages std::string expected_text = oversized_text.substr(0, 234); verify_text_message_packet_structure(json, expected_text.c_str()); } // Add test for malformed UTF-8 sequences void test_text_message_serialization_invalid_utf8() { const uint8_t invalid_utf8[] = {0xFF, 0xFE, 0xFD, 0x00}; // Invalid UTF-8 meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TEXT_MESSAGE_APP, invalid_utf8, sizeof(invalid_utf8) - 1); // Should not crash, may produce replacement characters std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); TEST_ASSERT_TRUE(json.length() > 0); } ================================================ FILE: test/test_meshpacket_serializer/ports/test_waypoint.cpp ================================================ #include "../test_helpers.h" static size_t encode_waypoint(uint8_t *buffer, size_t buffer_size) { meshtastic_Waypoint waypoint = meshtastic_Waypoint_init_zero; waypoint.id = 12345; waypoint.latitude_i = 374208000; waypoint.longitude_i = -1221981000; waypoint.expire = 1609459200 + 3600; // 1 hour from now strcpy(waypoint.name, "Test Point"); strcpy(waypoint.description, "Test waypoint description"); pb_ostream_t stream = pb_ostream_from_buffer(buffer, buffer_size); pb_encode(&stream, &meshtastic_Waypoint_msg, &waypoint); return stream.bytes_written; } // Test WAYPOINT_APP port void test_waypoint_serialization() { uint8_t buffer[256]; size_t payload_size = encode_waypoint(buffer, sizeof(buffer)); meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_WAYPOINT_APP, buffer, payload_size); std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); TEST_ASSERT_TRUE(json.length() > 0); JSONValue *root = JSON::Parse(json.c_str()); TEST_ASSERT_NOT_NULL(root); TEST_ASSERT_TRUE(root->IsObject()); JSONObject jsonObj = root->AsObject(); // Check message type TEST_ASSERT_TRUE(jsonObj.find("type") != jsonObj.end()); TEST_ASSERT_EQUAL_STRING("waypoint", jsonObj["type"]->AsString().c_str()); // Check payload TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end()); TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject()); JSONObject payload = jsonObj["payload"]->AsObject(); // Verify waypoint data TEST_ASSERT_TRUE(payload.find("id") != payload.end()); TEST_ASSERT_EQUAL(12345, (int)payload["id"]->AsNumber()); TEST_ASSERT_TRUE(payload.find("name") != payload.end()); TEST_ASSERT_EQUAL_STRING("Test Point", payload["name"]->AsString().c_str()); delete root; } ================================================ FILE: test/test_meshpacket_serializer/test_helpers.h ================================================ #pragma once #include "serialization/JSON.h" #include "serialization/MeshPacketSerializer.h" #include #include #include #include #include #include #include // Helper function to create a test packet with the given port and payload static meshtastic_MeshPacket create_test_packet(meshtastic_PortNum port, const uint8_t *payload, size_t payload_size, int payload_variant = meshtastic_MeshPacket_decoded_tag) { meshtastic_MeshPacket packet = meshtastic_MeshPacket_init_zero; packet.id = 0x9999; packet.from = 0x11223344; packet.to = 0x55667788; packet.channel = 0; packet.hop_limit = 3; packet.want_ack = false; packet.priority = meshtastic_MeshPacket_Priority_UNSET; packet.rx_time = 1609459200; packet.rx_snr = 10.5f; packet.hop_start = 3; packet.rx_rssi = -85; packet.delayed = meshtastic_MeshPacket_Delayed_NO_DELAY; // Set decoded variant packet.which_payload_variant = payload_variant; packet.decoded.portnum = port; if (payload_variant == meshtastic_MeshPacket_encrypted_tag && payload) { packet.encrypted.size = payload_size; memcpy(packet.encrypted.bytes, payload, packet.encrypted.size); } memcpy(packet.decoded.payload.bytes, payload, payload_size); packet.decoded.payload.size = payload_size; packet.decoded.want_response = false; packet.decoded.dest = 0x55667788; packet.decoded.source = 0x11223344; packet.decoded.request_id = 0; packet.decoded.reply_id = 0; packet.decoded.emoji = 0; return packet; } ================================================ FILE: test/test_meshpacket_serializer/test_serializer.cpp ================================================ #include "test_helpers.h" #include #include // Forward declarations for test functions void test_text_message_serialization(); void test_text_message_serialization_null(); void test_text_message_serialization_long_text(); void test_text_message_serialization_oversized(); void test_text_message_serialization_invalid_utf8(); void test_position_serialization(); void test_nodeinfo_serialization(); void test_waypoint_serialization(); void test_telemetry_device_metrics_serialization(); void test_telemetry_environment_metrics_serialization(); void test_telemetry_environment_metrics_comprehensive(); void test_telemetry_environment_metrics_missing_fields(); void test_telemetry_environment_metrics_complete_coverage(); void test_telemetry_environment_metrics_unset_fields(); void test_encrypted_packet_serialization(); void test_empty_encrypted_packet(); void setup() { UNITY_BEGIN(); // Text message tests RUN_TEST(test_text_message_serialization); RUN_TEST(test_text_message_serialization_null); RUN_TEST(test_text_message_serialization_long_text); RUN_TEST(test_text_message_serialization_oversized); RUN_TEST(test_text_message_serialization_invalid_utf8); // Position tests RUN_TEST(test_position_serialization); // Nodeinfo tests RUN_TEST(test_nodeinfo_serialization); // Waypoint tests RUN_TEST(test_waypoint_serialization); // Telemetry tests RUN_TEST(test_telemetry_device_metrics_serialization); RUN_TEST(test_telemetry_environment_metrics_serialization); RUN_TEST(test_telemetry_environment_metrics_comprehensive); RUN_TEST(test_telemetry_environment_metrics_missing_fields); RUN_TEST(test_telemetry_environment_metrics_complete_coverage); RUN_TEST(test_telemetry_environment_metrics_unset_fields); // Encrypted packet test RUN_TEST(test_encrypted_packet_serialization); RUN_TEST(test_empty_encrypted_packet); UNITY_END(); } void loop() { delay(1000); } ================================================ FILE: test/test_mqtt/MQTT.cpp ================================================ #include "DebugConfiguration.h" #include "TestUtil.h" #include #ifdef ARCH_PORTDUINO #include "mesh/CryptoEngine.h" #include "mesh/Default.h" #include "mesh/MeshService.h" #include "mesh/NodeDB.h" #include "mesh/Router.h" #include "modules/RoutingModule.h" #include "mqtt/MQTT.h" #include "mqtt/ServiceEnvelope.h" #include #include #include #include #include #include #include #include #include #include #include #include #if defined(UNIT_TEST) #define IS_RUNNING_TESTS 1 #else #define IS_RUNNING_TESTS 0 #endif namespace { // Minimal router needed to receive messages from MQTT. class MockRouter : public Router { public: ~MockRouter() { // cryptLock is created in the constructor for Router. delete cryptLock; cryptLock = NULL; } void enqueueReceivedMessage(meshtastic_MeshPacket *p) override { packets_.emplace_back(*p); packetPool.release(p); } std::list packets_; // Packets received by the Router. }; // Minimal MeshService needed to receive messages from MQTT for testing PKI channel. class MockMeshService : public MeshService { public: void sendMqttMessageToClientProxy(meshtastic_MqttClientProxyMessage *m) override { messages_.emplace_back(*m); releaseMqttClientProxyMessageToPool(m); } void sendClientNotification(meshtastic_ClientNotification *n) override { notifications_.emplace_back(*n); releaseClientNotificationToPool(n); } std::list messages_; // Messages received from the MeshService. std::list notifications_; // Notifications received from the MeshService. }; // Minimal NodeDB needed to return values from getMeshNode. class MockNodeDB : public NodeDB { public: meshtastic_NodeInfoLite *getMeshNode(NodeNum n) override { return &emptyNode; } meshtastic_NodeInfoLite emptyNode = {}; }; // Minimal RoutingModule needed to return values from sendAckNak. class MockRoutingModule : public RoutingModule { public: void sendAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex, uint8_t hopLimit = 0, bool ackWantsAck = false) override { ackNacks_.emplace_back(err, to, idFrom, chIndex, hopLimit); } std::list> ackNacks_; // ackNacks received by the RoutingModule. }; // A WiFi client used by the MQTT::PubSubClient. Implements a minimal pub/sub server. // There isn't an easy way to mock PubSubClient due to it not having virtual methods, so we mock using // the WiFiClinet that PubSubClient uses. class MockPubSubServer : public WiFiClient { public: static constexpr char kTextTopic[] = "TextTopic"; uint8_t connected() override { return connected_; } void flush() override {} IPAddress remoteIP() const override { return IPAddress(htonl(ipAddress_)); } void stop() override { connected_ = false; } int connect(IPAddress ip, uint16_t port) override { port_ = port; if (refuseConnection_) return 0; connected_ = true; return 1; } int connect(const char *host, uint16_t port) override { host_ = host; port_ = port; if (refuseConnection_) return 0; connected_ = true; return 1; } int available() override { if (buffer_.empty()) return 0; return buffer_.front().size(); } int read() override { assert(available()); std::string &front = buffer_.front(); char ch = front[0]; front = front.substr(1, front.size()); if (front.empty()) buffer_.pop_front(); return ch; } size_t write(uint8_t data) override { return write(&data, 1); } size_t write(const uint8_t *buf, size_t size) override { command_ += std::string(reinterpret_cast(buf), size); if (command_.size() < 2) return size; const int len = (uint8_t)command_[1] + 2; if (command_.size() < len) return size; handleCommand(command_[0], command_.substr(2, len)); command_ = command_.substr(len, command_.size()); return size; } // The pub/sub "server". // https://public.dhe.ibm.com/software/dw/webservices/ws-mqtt/MQTT_V3.1_Protocol_Specific.pdf void handleCommand(uint8_t header, std::string_view message) { switch (header & 0xf0) { case MQTTCONNECT: LOG_DEBUG("MQTTCONNECT"); buffer_.push_back(std::string("\x20\x02\x00\x00", 4)); break; case MQTTSUBSCRIBE: { LOG_DEBUG("MQTTSUBSCRIBE"); assert(message.size() >= 5); message.remove_prefix(2); // skip messageId while (message.size() >= 3) { const uint16_t topicSize = ((uint8_t)message[0]) << 8 | (uint8_t)message[1]; message.remove_prefix(2); assert(message.size() >= topicSize + 1); std::string topic(message.data(), topicSize); message.remove_prefix(topicSize + 1); LOG_DEBUG("Subscribed to topic: %s", topic.c_str()); subscriptions_.insert(std::move(topic)); } break; } case MQTTPINGREQ: LOG_DEBUG("MQTTPINGREQ"); buffer_.push_back(std::string("\xd0\x00", 2)); break; case MQTTPUBLISH: { LOG_DEBUG("MQTTPUBLISH"); assert(message.size() >= 3); const uint16_t topicSize = ((uint8_t)message[0]) << 8 | (uint8_t)message[1]; message.remove_prefix(2); assert(message.size() >= topicSize); std::string topic(message.data(), topicSize); message.remove_prefix(topicSize); if (topic == kTextTopic) { published_.emplace_back(std::move(topic), std::string(message.data(), message.size())); } else { published_.emplace_back( std::move(topic), DecodedServiceEnvelope(reinterpret_cast(message.data()), message.size())); } break; } } } bool connected_ = false; bool refuseConnection_ = false; // Simulate a failed connection. uint32_t ipAddress_ = 0x01010101; // IP address of the MQTT server. std::string host_; // Requested host. uint16_t port_; // Requested port. std::list buffer_; // Buffer of messages for the pubSub client to receive. std::string command_; // Current command received from the pubSub client. std::set subscriptions_; // Topics that the pubSub client has subscribed to. std::list>> published_; // Messages published from the pubSub client. Each list element is a pair containing the topic name and either // a text message (if from the kTextTopic topic) or a DecodedServiceEnvelope. }; // Instances of our mocks. class MQTTUnitTest; MQTTUnitTest *unitTest; MockPubSubServer *pubsub; MockRoutingModule *mockRoutingModule; MockMeshService *mockMeshService; MockRouter *mockRouter; // Keep running the loop until either conditionMet returns true or 4 seconds elapse. // Returns true if conditionMet returns true, returns false on timeout. bool loopUntil(std::function conditionMet) { long start = millis(); while (start + 4000 > millis()) { long delayMsec = concurrency::mainController.runOrDelay(); if (conditionMet()) return true; concurrency::mainDelay.delay(std::min(delayMsec, 5L)); } return false; } // Used to access protected/private members of MQTT for unit testing. class MQTTUnitTest : public MQTT { public: MQTTUnitTest() : MQTT(std::make_unique()) { pubsub = reinterpret_cast(mqttClient.get()); } ~MQTTUnitTest() { // Needed because WiFiClient does not have a virtual destructor. mqttClient.release(); delete pubsub; } using MQTT::isValidConfig; using MQTT::reconnect; int queueSize() { return mqttQueue.numUsed(); } void reportToMap(std::optional precision = std::nullopt) { if (precision.has_value()) map_position_precision = precision.value(); map_publish_interval_msecs = 0; perhapsReportToMap(); } void publish(const meshtastic_MeshPacket *p, std::string gateway = "!87654321", std::string channel = "test") { std::stringstream topic; topic << "msh/2/e/" << channel << "/!" << gateway; const meshtastic_ServiceEnvelope env = {.packet = const_cast(p), .channel_id = const_cast(channel.c_str()), .gateway_id = const_cast(gateway.c_str())}; uint8_t bytes[256]; size_t numBytes = pb_encode_to_bytes(bytes, sizeof(bytes), &meshtastic_ServiceEnvelope_msg, &env); mqttCallback(const_cast(topic.str().c_str()), bytes, numBytes); } static void restart() { if (mqtt != NULL) { delete mqtt; mqtt = unitTest = NULL; } mqtt = unitTest = new MQTTUnitTest(); mqtt->start(); auto clearStartupOutput = []() { pubsub->published_.clear(); if (mockMeshService != nullptr) { mockMeshService->messages_.clear(); mockMeshService->notifications_.clear(); } }; if (!moduleConfig.mqtt.enabled || moduleConfig.mqtt.proxy_to_client_enabled || *moduleConfig.mqtt.root) { loopUntil([] { return true; }); // Loop once clearStartupOutput(); return; } // Wait for MQTT to subscribe to all topics. TEST_ASSERT_TRUE(loopUntil( [] { return pubsub->subscriptions_.count("msh/2/e/test/+") && pubsub->subscriptions_.count("msh/2/e/PKI/+"); })); clearStartupOutput(); } PubSubClient &getPubSub() { return pubSub; } }; // Packets used in unit tests. const meshtastic_MeshPacket decoded = { .from = 1, .to = 2, .which_payload_variant = meshtastic_MeshPacket_decoded_tag, .decoded = {.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP, .has_bitfield = true, .bitfield = BITFIELD_OK_TO_MQTT_MASK}, .id = 4, }; const meshtastic_MeshPacket encrypted = { .from = 1, .to = 2, .which_payload_variant = meshtastic_MeshPacket_encrypted_tag, .encrypted = {.size = 0}, .id = 3, }; } // namespace // Initialize mocks and configuration before running each test. void setUp(void) { moduleConfig.mqtt = meshtastic_ModuleConfig_MQTTConfig{.enabled = true, .map_reporting_enabled = true, .has_map_report_settings = true}; moduleConfig.mqtt.map_report_settings = meshtastic_ModuleConfig_MapReportSettings{ .publish_interval_secs = 0, .position_precision = 14, .should_report_location = true}; channelFile.channels[0] = meshtastic_Channel{ .index = 0, .has_settings = true, .settings = {.name = "test", .uplink_enabled = true, .downlink_enabled = true}, .role = meshtastic_Channel_Role_PRIMARY, }; channelFile.channels_count = 1; owner = meshtastic_User{.id = "!12345678"}; myNodeInfo = meshtastic_MyNodeInfo{.my_node_num = 0x12345678}; // Match the expected gateway ID in topic localPosition = meshtastic_Position{.has_latitude_i = true, .latitude_i = 700000000, .has_longitude_i = true, .longitude_i = 300000000}; router = mockRouter = new MockRouter(); service = mockMeshService = new MockMeshService(); routingModule = mockRoutingModule = new MockRoutingModule(); MQTTUnitTest::restart(); } // Deinitialize all objects created in setUp. void tearDown(void) { delete unitTest; mqtt = unitTest = NULL; delete mockRoutingModule; routingModule = mockRoutingModule = NULL; delete mockMeshService; service = mockMeshService = NULL; delete mockRouter; router = mockRouter = NULL; } // Test that the decoded MeshPacket is published when encryption_enabled = false. void test_sendDirectlyConnectedDecoded(void) { mqtt->onSend(encrypted, decoded, 0); TEST_ASSERT_EQUAL(1, pubsub->published_.size()); const auto &[topic, payload] = pubsub->published_.front(); const DecodedServiceEnvelope &env = std::get(payload); TEST_ASSERT_EQUAL_STRING("msh/2/e/test/!12345678", topic.c_str()); TEST_ASSERT_TRUE(env.validDecode); TEST_ASSERT_EQUAL(decoded.id, env.packet->id); } // Test that the encrypted MeshPacket is published when encryption_enabled = true. void test_sendDirectlyConnectedEncrypted(void) { moduleConfig.mqtt.encryption_enabled = true; mqtt->onSend(encrypted, decoded, 0); TEST_ASSERT_EQUAL(1, pubsub->published_.size()); const auto &[topic, payload] = pubsub->published_.front(); const DecodedServiceEnvelope &env = std::get(payload); TEST_ASSERT_EQUAL_STRING("msh/2/e/test/!12345678", topic.c_str()); TEST_ASSERT_TRUE(env.validDecode); TEST_ASSERT_EQUAL(encrypted.id, env.packet->id); } // Verify that the decoded MeshPacket is proxied through the MeshService when encryption_enabled = false. void test_proxyToMeshServiceDecoded(void) { moduleConfig.mqtt.proxy_to_client_enabled = true; MQTTUnitTest::restart(); mqtt->onSend(encrypted, decoded, 0); TEST_ASSERT_EQUAL(1, mockMeshService->messages_.size()); const meshtastic_MqttClientProxyMessage &message = mockMeshService->messages_.front(); TEST_ASSERT_EQUAL_STRING("msh/2/e/test/!12345678", message.topic); TEST_ASSERT_EQUAL(meshtastic_MqttClientProxyMessage_data_tag, message.which_payload_variant); const DecodedServiceEnvelope env(message.payload_variant.data.bytes, message.payload_variant.data.size); TEST_ASSERT_TRUE(env.validDecode); TEST_ASSERT_EQUAL(decoded.id, env.packet->id); } // Verify that the encrypted MeshPacket is proxied through the MeshService when encryption_enabled = true. void test_proxyToMeshServiceEncrypted(void) { moduleConfig.mqtt.proxy_to_client_enabled = true; moduleConfig.mqtt.encryption_enabled = true; MQTTUnitTest::restart(); mqtt->onSend(encrypted, decoded, 0); TEST_ASSERT_EQUAL(1, mockMeshService->messages_.size()); const meshtastic_MqttClientProxyMessage &message = mockMeshService->messages_.front(); TEST_ASSERT_EQUAL_STRING("msh/2/e/test/!12345678", message.topic); TEST_ASSERT_EQUAL(meshtastic_MqttClientProxyMessage_data_tag, message.which_payload_variant); const DecodedServiceEnvelope env(message.payload_variant.data.bytes, message.payload_variant.data.size); TEST_ASSERT_TRUE(env.validDecode); TEST_ASSERT_EQUAL(encrypted.id, env.packet->id); } // A packet without the OK to MQTT bit set should not be published to a public server. void test_dontMqttMeOnPublicServer(void) { meshtastic_MeshPacket p = decoded; p.decoded.bitfield = 0; p.decoded.has_bitfield = 0; mqtt->onSend(encrypted, p, 0); TEST_ASSERT_TRUE(pubsub->published_.empty()); } // A packet without the OK to MQTT bit set should be published to a private server. void test_okToMqttOnPrivateServer(void) { // Cause a disconnect. pubsub->connected_ = false; pubsub->refuseConnection_ = true; TEST_ASSERT_TRUE(loopUntil([] { return !unitTest->getPubSub().connected(); })); // Use 127.0.0.1 for the server's IP. pubsub->ipAddress_ = 0x7f000001; // Reconnect. pubsub->refuseConnection_ = false; TEST_ASSERT_TRUE(loopUntil([] { return unitTest->getPubSub().connected(); })); // Send the same packet as test_dontMqttMeOnPublicServer. meshtastic_MeshPacket p = decoded; p.decoded.bitfield = 0; p.decoded.has_bitfield = 0; mqtt->onSend(encrypted, p, 0); TEST_ASSERT_EQUAL(1, pubsub->published_.size()); } // Range tests messages are not uplinked to the default server. void test_noRangeTestAppOnDefaultServer(void) { meshtastic_MeshPacket p = decoded; p.decoded.portnum = meshtastic_PortNum_RANGE_TEST_APP; mqtt->onSend(encrypted, p, 0); TEST_ASSERT_TRUE(pubsub->published_.empty()); } // Detection sensor messages are not uplinked to the default server. void test_noDetectionSensorAppOnDefaultServer(void) { meshtastic_MeshPacket p = decoded; p.decoded.portnum = meshtastic_PortNum_DETECTION_SENSOR_APP; mqtt->onSend(encrypted, p, 0); TEST_ASSERT_TRUE(pubsub->published_.empty()); } // Test that a MeshPacket is queued while the MQTT server is disconnected. void test_sendQueued(void) { // Cause a disconnect. pubsub->connected_ = false; pubsub->refuseConnection_ = true; TEST_ASSERT_TRUE(loopUntil([] { return !unitTest->getPubSub().connected(); })); // Send while disconnected. mqtt->onSend(encrypted, decoded, 0); TEST_ASSERT_EQUAL(1, unitTest->queueSize()); TEST_ASSERT_TRUE(pubsub->published_.empty()); TEST_ASSERT_FALSE(unitTest->getPubSub().connected()); // Allow reconnect to happen. Expect to see the packet published now. pubsub->refuseConnection_ = false; TEST_ASSERT_TRUE(loopUntil([] { return !pubsub->published_.empty(); })); TEST_ASSERT_EQUAL(0, unitTest->queueSize()); const auto &[topic, payload] = pubsub->published_.front(); const DecodedServiceEnvelope &env = std::get(payload); TEST_ASSERT_EQUAL_STRING("msh/2/e/test/!12345678", topic.c_str()); TEST_ASSERT_TRUE(env.validDecode); TEST_ASSERT_EQUAL(decoded.id, env.packet->id); } // Verify reconnecting with the proxy enabled does not reconnect to a MQTT server. void test_reconnectProxyDoesNotReconnectMqtt(void) { moduleConfig.mqtt.proxy_to_client_enabled = true; MQTTUnitTest::restart(); unitTest->reconnect(); TEST_ASSERT_FALSE(pubsub->connected_); } // Test receiving an empty MeshPacket on a subscribed topic. void test_receiveEmptyMeshPacket(void) { unitTest->publish(NULL); TEST_ASSERT_TRUE(mockRouter->packets_.empty()); TEST_ASSERT_TRUE(mockRoutingModule->ackNacks_.empty()); } // Test receiving a decoded MeshPacket on a subscribed topic. void test_receiveDecodedProto(void) { unitTest->publish(&decoded); TEST_ASSERT_EQUAL(1, mockRouter->packets_.size()); const meshtastic_MeshPacket &p = mockRouter->packets_.front(); TEST_ASSERT_EQUAL(decoded.id, p.id); TEST_ASSERT_TRUE(p.via_mqtt); } // Test receiving a decoded MeshPacket from the phone proxy. void test_receiveDecodedProtoFromProxy(void) { const meshtastic_ServiceEnvelope env = { .packet = const_cast(&decoded), .channel_id = "test", .gateway_id = "!87654321"}; meshtastic_MqttClientProxyMessage message = meshtastic_MqttClientProxyMessage_init_default; strcat(message.topic, "msh/2/e/test/!87654321"); message.which_payload_variant = meshtastic_MqttClientProxyMessage_data_tag; message.payload_variant.data.size = pb_encode_to_bytes( message.payload_variant.data.bytes, sizeof(message.payload_variant.data.bytes), &meshtastic_ServiceEnvelope_msg, &env); mqtt->onClientProxyReceive(message); TEST_ASSERT_EQUAL(1, mockRouter->packets_.size()); const meshtastic_MeshPacket &p = mockRouter->packets_.front(); TEST_ASSERT_EQUAL(decoded.id, p.id); TEST_ASSERT_TRUE(p.via_mqtt); } // Properly handles the case where the received message is empty. void test_receiveEmptyDataFromProxy(void) { meshtastic_MqttClientProxyMessage message = meshtastic_MqttClientProxyMessage_init_default; message.which_payload_variant = meshtastic_MqttClientProxyMessage_data_tag; mqtt->onClientProxyReceive(message); TEST_ASSERT_TRUE(mockRouter->packets_.empty()); } // Packets should be ignored if downlink is not enabled. void test_receiveWithoutChannelDownlink(void) { channelFile.channels[0].settings.downlink_enabled = false; unitTest->publish(&decoded); TEST_ASSERT_TRUE(mockRouter->packets_.empty()); } // Test receiving an encrypted MeshPacket on the PKI topic. void test_receiveEncryptedPKITopicToUs(void) { meshtastic_MeshPacket e = encrypted; e.to = myNodeInfo.my_node_num; unitTest->publish(&e, "!87654321", "PKI"); TEST_ASSERT_EQUAL(1, mockRouter->packets_.size()); const meshtastic_MeshPacket &p = mockRouter->packets_.front(); TEST_ASSERT_EQUAL(encrypted.id, p.id); TEST_ASSERT_TRUE(p.via_mqtt); } // Should ignore messages published to MQTT by this gateway. void test_receiveIgnoresOwnPublishedMessages(void) { unitTest->publish(&decoded, nodeDB->getNodeId().c_str()); TEST_ASSERT_TRUE(mockRouter->packets_.empty()); TEST_ASSERT_TRUE(mockRoutingModule->ackNacks_.empty()); } // Considers receiving one of our packets an acknowledgement of it being sent. void test_receiveAcksOwnSentMessages(void) { meshtastic_MeshPacket p = decoded; p.from = myNodeInfo.my_node_num; unitTest->publish(&p, nodeDB->getNodeId().c_str()); // FIXME: Better assertion for this test // TEST_ASSERT_TRUE(mockRouter->packets_.empty()); // TEST_ASSERT_EQUAL(1, mockRoutingModule->ackNacks_.size()); // const auto &[err, to, idFrom, chIndex, hopLimit] = mockRoutingModule->ackNacks_.front(); // TEST_ASSERT_EQUAL(meshtastic_Routing_Error_NONE, err); // TEST_ASSERT_EQUAL(myNodeInfo.my_node_num, to); // TEST_ASSERT_EQUAL(p.id, idFrom); } // Should ignore our own messages from MQTT that were heard by other nodes. void test_receiveIgnoresSentMessagesFromOthers(void) { meshtastic_MeshPacket p = decoded; p.from = myNodeInfo.my_node_num; unitTest->publish(&p); TEST_ASSERT_TRUE(mockRouter->packets_.empty()); TEST_ASSERT_TRUE(mockRoutingModule->ackNacks_.empty()); } // Decoded MQTT messages should be ignored when encryption is enabled. void test_receiveIgnoresDecodedWhenEncryptionEnabled(void) { moduleConfig.mqtt.encryption_enabled = true; unitTest->publish(&decoded); TEST_ASSERT_TRUE(mockRouter->packets_.empty()); } // Non-encrypted messages for the Admin App should be ignored. void test_receiveIgnoresDecodedAdminApp(void) { meshtastic_MeshPacket p = decoded; p.decoded.portnum = meshtastic_PortNum_ADMIN_APP; unitTest->publish(&p); TEST_ASSERT_TRUE(mockRouter->packets_.empty()); } // Only the same fields that are transmitted over LoRa should be set in MQTT messages. void test_receiveIgnoresUnexpectedFields(void) { meshtastic_MeshPacket input = decoded; input.rx_snr = 10; input.rx_rssi = 20; unitTest->publish(&input); TEST_ASSERT_EQUAL(1, mockRouter->packets_.size()); const meshtastic_MeshPacket &p = mockRouter->packets_.front(); TEST_ASSERT_EQUAL(0, p.rx_snr); TEST_ASSERT_EQUAL(0, p.rx_rssi); } // Messages with an invalid hop_limit are ignored. void test_receiveIgnoresInvalidHopLimit(void) { meshtastic_MeshPacket p = decoded; p.hop_limit = 10; unitTest->publish(&p); TEST_ASSERT_TRUE(mockRouter->packets_.empty()); } // Publishing to a text channel. void test_publishTextMessageDirect(void) { TEST_ASSERT_TRUE(mqtt->publish(MockPubSubServer::kTextTopic, "payload", 0)); TEST_ASSERT_EQUAL(1, pubsub->published_.size()); const auto &[topic, payload] = pubsub->published_.front(); TEST_ASSERT_EQUAL_STRING("payload", std::get(payload).c_str()); } // Publishing to a text channel via the MQTT client proxy. void test_publishTextMessageWithProxy(void) { moduleConfig.mqtt.proxy_to_client_enabled = true; TEST_ASSERT_TRUE(mqtt->publish(MockPubSubServer::kTextTopic, "payload", 0)); TEST_ASSERT_EQUAL(1, mockMeshService->messages_.size()); const meshtastic_MqttClientProxyMessage &message = mockMeshService->messages_.front(); TEST_ASSERT_EQUAL_STRING(MockPubSubServer::kTextTopic, message.topic); TEST_ASSERT_EQUAL(meshtastic_MqttClientProxyMessage_text_tag, message.which_payload_variant); TEST_ASSERT_EQUAL_STRING("payload", message.payload_variant.text); } // Helper method to verify the expected latitude/longitude was received. void verifyLatLong(const DecodedServiceEnvelope &env, uint32_t latitude, uint32_t longitude) { TEST_ASSERT_TRUE(env.validDecode); const meshtastic_MeshPacket &p = *env.packet; TEST_ASSERT_EQUAL(NODENUM_BROADCAST, p.to); TEST_ASSERT_EQUAL(meshtastic_MeshPacket_decoded_tag, p.which_payload_variant); TEST_ASSERT_EQUAL(meshtastic_PortNum_MAP_REPORT_APP, p.decoded.portnum); meshtastic_MapReport mapReport; TEST_ASSERT_TRUE( pb_decode_from_bytes(p.decoded.payload.bytes, p.decoded.payload.size, &meshtastic_MapReport_msg, &mapReport)); TEST_ASSERT_EQUAL(latitude, mapReport.latitude_i); TEST_ASSERT_EQUAL(longitude, mapReport.longitude_i); } // Map reporting defaults to an imprecise location. void test_reportToMapDefaultImprecise(void) { unitTest->reportToMap(); TEST_ASSERT_EQUAL(1, pubsub->published_.size()); const auto &[topic, payload] = pubsub->published_.front(); TEST_ASSERT_EQUAL_STRING("msh/2/map/", topic.c_str()); } // Location is sent over the phone proxy. void test_reportToMapImpreciseProxied(void) { moduleConfig.mqtt.proxy_to_client_enabled = true; MQTTUnitTest::restart(); unitTest->reportToMap(/*precision=*/14); TEST_ASSERT_EQUAL(1, mockMeshService->messages_.size()); const meshtastic_MqttClientProxyMessage &message = mockMeshService->messages_.front(); TEST_ASSERT_EQUAL_STRING("msh/2/map/", message.topic); TEST_ASSERT_EQUAL(meshtastic_MqttClientProxyMessage_data_tag, message.which_payload_variant); const DecodedServiceEnvelope env(message.payload_variant.data.bytes, message.payload_variant.data.size); } // isUsingDefaultServer returns true when using the default server. void test_usingDefaultServer(void) { TEST_ASSERT_TRUE(mqtt->isUsingDefaultServer()); } // isUsingDefaultServer returns true when using the default server and a port. void test_usingDefaultServerWithPort(void) { std::string server = default_mqtt_address; server += ":1883"; strcpy(moduleConfig.mqtt.address, server.c_str()); MQTTUnitTest::restart(); TEST_ASSERT_TRUE(mqtt->isUsingDefaultServer()); } // isUsingDefaultServer returns true when using the default server and invalid port. void test_usingDefaultServerWithInvalidPort(void) { std::string server = default_mqtt_address; server += ":invalid"; strcpy(moduleConfig.mqtt.address, server.c_str()); MQTTUnitTest::restart(); TEST_ASSERT_TRUE(mqtt->isUsingDefaultServer()); } // isUsingDefaultServer returns false when not using the default server. void test_usingCustomServer(void) { strcpy(moduleConfig.mqtt.address, "custom"); MQTTUnitTest::restart(); TEST_ASSERT_FALSE(mqtt->isUsingDefaultServer()); } // Test that isEnabled returns true the MQTT module is enabled. void test_enabled(void) { TEST_ASSERT_TRUE(mqtt->isEnabled()); } // Test that isEnabled returns false the MQTT module not enabled. void test_disabled(void) { moduleConfig.mqtt.enabled = false; MQTTUnitTest::restart(); TEST_ASSERT_FALSE(mqtt->isEnabled()); } // Subscriptions contain the moduleConfig.mqtt.root prefix. void test_customMqttRoot(void) { strcpy(moduleConfig.mqtt.root, "custom"); MQTTUnitTest::restart(); TEST_ASSERT_TRUE(loopUntil( [] { return pubsub->subscriptions_.count("custom/2/e/test/+") && pubsub->subscriptions_.count("custom/2/e/PKI/+"); })); } // Empty configuration is valid. void test_configEmptyIsValid(void) { meshtastic_ModuleConfig_MQTTConfig config = {}; TEST_ASSERT_TRUE(MQTT::isValidConfig(config)); } // Empty 'enabled' configuration is valid. A lightweight TCP check may be performed // but does not affect the result. void test_configEnabledEmptyIsValid(void) { meshtastic_ModuleConfig_MQTTConfig config = {.enabled = true}; TEST_ASSERT_TRUE(MQTT::isValidConfig(config)); } // Configuration with the default server is valid. void test_configWithDefaultServer(void) { meshtastic_ModuleConfig_MQTTConfig config = {.address = default_mqtt_address}; TEST_ASSERT_TRUE(MQTT::isValidConfig(config)); } // Configuration with the default server and port 8888 is invalid. void test_configWithDefaultServerAndInvalidPort(void) { meshtastic_ModuleConfig_MQTTConfig config = {.address = default_mqtt_address ":8888"}; TEST_ASSERT_FALSE(MQTT::isValidConfig(config)); } // Custom host and port is valid. TCP reachability is checked but does not block saving. void test_configCustomHostAndPort(void) { meshtastic_ModuleConfig_MQTTConfig config = {.enabled = true, .address = "server:1234"}; TEST_ASSERT_TRUE(MQTT::isValidConfig(config)); } // An unreachable server is still a valid config — settings always save. // A warning notification is sent in non-test builds, but isValidConfig returns true. void test_configWithUnreachableServerIsStillValid(void) { meshtastic_ModuleConfig_MQTTConfig config = {.enabled = true, .address = "server"}; TEST_ASSERT_TRUE(MQTT::isValidConfig(config)); } // isValidConfig returns true when tls_enabled is supported, or false otherwise. void test_configWithTLSEnabled(void) { meshtastic_ModuleConfig_MQTTConfig config = {.enabled = true, .address = "server", .tls_enabled = true}; #if MQTT_SUPPORTS_TLS TEST_ASSERT_TRUE(MQTT::isValidConfig(config)); #else TEST_ASSERT_FALSE(MQTT::isValidConfig(config)); #endif } void setup() { initializeTestEnvironment(); const std::unique_ptr mockNodeDB(new MockNodeDB()); nodeDB = mockNodeDB.get(); UNITY_BEGIN(); RUN_TEST(test_sendDirectlyConnectedDecoded); RUN_TEST(test_sendDirectlyConnectedEncrypted); RUN_TEST(test_proxyToMeshServiceDecoded); RUN_TEST(test_proxyToMeshServiceEncrypted); RUN_TEST(test_dontMqttMeOnPublicServer); RUN_TEST(test_okToMqttOnPrivateServer); RUN_TEST(test_noRangeTestAppOnDefaultServer); RUN_TEST(test_noDetectionSensorAppOnDefaultServer); RUN_TEST(test_sendQueued); RUN_TEST(test_reconnectProxyDoesNotReconnectMqtt); RUN_TEST(test_receiveEmptyMeshPacket); RUN_TEST(test_receiveDecodedProto); RUN_TEST(test_receiveDecodedProtoFromProxy); RUN_TEST(test_receiveEmptyDataFromProxy); RUN_TEST(test_receiveWithoutChannelDownlink); RUN_TEST(test_receiveEncryptedPKITopicToUs); RUN_TEST(test_receiveIgnoresOwnPublishedMessages); RUN_TEST(test_receiveAcksOwnSentMessages); RUN_TEST(test_receiveIgnoresSentMessagesFromOthers); RUN_TEST(test_receiveIgnoresDecodedWhenEncryptionEnabled); RUN_TEST(test_receiveIgnoresDecodedAdminApp); RUN_TEST(test_receiveIgnoresUnexpectedFields); RUN_TEST(test_receiveIgnoresInvalidHopLimit); RUN_TEST(test_publishTextMessageDirect); RUN_TEST(test_publishTextMessageWithProxy); RUN_TEST(test_reportToMapDefaultImprecise); RUN_TEST(test_reportToMapImpreciseProxied); RUN_TEST(test_usingDefaultServer); RUN_TEST(test_usingDefaultServerWithPort); RUN_TEST(test_usingDefaultServerWithInvalidPort); RUN_TEST(test_usingCustomServer); RUN_TEST(test_enabled); RUN_TEST(test_disabled); RUN_TEST(test_customMqttRoot); RUN_TEST(test_configEmptyIsValid); RUN_TEST(test_configEnabledEmptyIsValid); RUN_TEST(test_configWithDefaultServer); RUN_TEST(test_configWithDefaultServerAndInvalidPort); RUN_TEST(test_configCustomHostAndPort); RUN_TEST(test_configWithUnreachableServerIsStillValid); RUN_TEST(test_configWithTLSEnabled); exit(UNITY_END()); } #else void setup() { initializeTestEnvironment(); LOG_WARN("This test requires the ARCH_PORTDUINO variant of WiFiClient"); UNITY_BEGIN(); UNITY_END(); } #endif void loop() {} ================================================ FILE: test/test_radio/test_main.cpp ================================================ #include "MeshRadio.h" #include "MeshService.h" #include "RadioInterface.h" #include "TestUtil.h" #include #include "meshtastic/config.pb.h" class MockMeshService : public MeshService { public: void sendClientNotification(meshtastic_ClientNotification *n) override { releaseClientNotificationToPool(n); } }; static MockMeshService *mockMeshService; static void test_bwCodeToKHz_specialMappings() { TEST_ASSERT_FLOAT_WITHIN(0.0001f, 31.25f, bwCodeToKHz(31)); TEST_ASSERT_FLOAT_WITHIN(0.0001f, 62.5f, bwCodeToKHz(62)); TEST_ASSERT_FLOAT_WITHIN(0.0001f, 203.125f, bwCodeToKHz(200)); TEST_ASSERT_FLOAT_WITHIN(0.0001f, 406.25f, bwCodeToKHz(400)); TEST_ASSERT_FLOAT_WITHIN(0.0001f, 812.5f, bwCodeToKHz(800)); TEST_ASSERT_FLOAT_WITHIN(0.0001f, 1625.0f, bwCodeToKHz(1600)); } static void test_bwCodeToKHz_passthrough() { TEST_ASSERT_FLOAT_WITHIN(0.0001f, 125.0f, bwCodeToKHz(125)); TEST_ASSERT_FLOAT_WITHIN(0.0001f, 250.0f, bwCodeToKHz(250)); } static void test_validateConfigLora_noopWhenUsePresetFalse() { meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; cfg.use_preset = false; cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US; cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST; cfg.bandwidth = 123; cfg.spread_factor = 8; RadioInterface::validateConfigLora(cfg); TEST_ASSERT_EQUAL_UINT16(123, cfg.bandwidth); TEST_ASSERT_EQUAL_UINT32(8, cfg.spread_factor); TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST, cfg.modem_preset); } static void test_validateConfigLora_validPreset_nonWideRegion() { meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; cfg.use_preset = true; cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US; cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST; TEST_ASSERT_TRUE(RadioInterface::validateConfigLora(cfg)); } static void test_validateConfigLora_validPreset_wideRegion() { meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; cfg.use_preset = true; cfg.region = meshtastic_Config_LoRaConfig_RegionCode_LORA_24; cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST; TEST_ASSERT_TRUE(RadioInterface::validateConfigLora(cfg)); } static void test_validateConfigLora_rejectsInvalidPresetForRegion() { meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; cfg.use_preset = true; cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868; cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO; TEST_ASSERT_FALSE(RadioInterface::validateConfigLora(cfg)); } static void test_clampConfigLora_invalidPresetClampedToDefault() { meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; cfg.use_preset = true; cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868; cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO; RadioInterface::clampConfigLora(cfg); TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, cfg.modem_preset); } static void test_clampConfigLora_validPresetUnchanged() { meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; cfg.use_preset = true; cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US; cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST; RadioInterface::clampConfigLora(cfg); TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST, cfg.modem_preset); } void setUp(void) { mockMeshService = new MockMeshService(); service = mockMeshService; } void tearDown(void) { service = nullptr; delete mockMeshService; mockMeshService = nullptr; } void setup() { delay(10); delay(2000); initializeTestEnvironment(); UNITY_BEGIN(); RUN_TEST(test_bwCodeToKHz_specialMappings); RUN_TEST(test_bwCodeToKHz_passthrough); RUN_TEST(test_validateConfigLora_noopWhenUsePresetFalse); RUN_TEST(test_validateConfigLora_validPreset_nonWideRegion); RUN_TEST(test_validateConfigLora_validPreset_wideRegion); RUN_TEST(test_validateConfigLora_rejectsInvalidPresetForRegion); RUN_TEST(test_clampConfigLora_invalidPresetClampedToDefault); RUN_TEST(test_clampConfigLora_validPresetUnchanged); exit(UNITY_END()); } void loop() {} ================================================ FILE: test/test_serial/SerialModule.cpp ================================================ #include "DebugConfiguration.h" #include "TestUtil.h" #include #ifdef ARCH_PORTDUINO #include "configuration.h" #if defined(UNIT_TEST) #define IS_RUNNING_TESTS 1 #else #define IS_RUNNING_TESTS 0 #endif #if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && \ !defined(CONFIG_IDF_TARGET_ESP32C3) #include "modules/SerialModule.h" #endif #if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && \ !defined(CONFIG_IDF_TARGET_ESP32C3) // Test that empty configuration is valid. void test_serialConfigEmptyIsValid(void) { meshtastic_ModuleConfig_SerialConfig config = {}; TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); } // Test that basic enabled configuration is valid. void test_serialConfigEnabledIsValid(void) { meshtastic_ModuleConfig_SerialConfig config = {.enabled = true}; TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); } // Test that configuration with override_console_serial_port and NMEA mode is valid. void test_serialConfigWithOverrideConsoleNmeaModeIsValid(void) { meshtastic_ModuleConfig_SerialConfig config = { .enabled = true, .override_console_serial_port = true, .mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_NMEA}; TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); } // Test that configuration with override_console_serial_port and CalTopo mode is valid. void test_serialConfigWithOverrideConsoleCalTopoModeIsValid(void) { meshtastic_ModuleConfig_SerialConfig config = { .enabled = true, .override_console_serial_port = true, .mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO}; TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); } // Test that configuration with override_console_serial_port and DEFAULT mode is invalid. void test_serialConfigWithOverrideConsoleDefaultModeIsInvalid(void) { meshtastic_ModuleConfig_SerialConfig config = { .enabled = true, .override_console_serial_port = true, .mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_DEFAULT}; TEST_ASSERT_FALSE(SerialModule::isValidConfig(config)); } // Test that configuration with override_console_serial_port and SIMPLE mode is invalid. void test_serialConfigWithOverrideConsoleSimpleModeIsInvalid(void) { meshtastic_ModuleConfig_SerialConfig config = { .enabled = true, .override_console_serial_port = true, .mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_SIMPLE}; TEST_ASSERT_FALSE(SerialModule::isValidConfig(config)); } // Test that configuration with override_console_serial_port and TEXTMSG mode is invalid. void test_serialConfigWithOverrideConsoleTextMsgModeIsInvalid(void) { meshtastic_ModuleConfig_SerialConfig config = { .enabled = true, .override_console_serial_port = true, .mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_TEXTMSG}; TEST_ASSERT_FALSE(SerialModule::isValidConfig(config)); } // Test that configuration with override_console_serial_port and PROTO mode is invalid. void test_serialConfigWithOverrideConsoleProtoModeIsInvalid(void) { meshtastic_ModuleConfig_SerialConfig config = { .enabled = true, .override_console_serial_port = true, .mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_PROTO}; TEST_ASSERT_FALSE(SerialModule::isValidConfig(config)); } // Test that various modes work without override_console_serial_port. void test_serialConfigVariousModesWithoutOverrideAreValid(void) { meshtastic_ModuleConfig_SerialConfig config = {.enabled = true, .override_console_serial_port = false}; // Test DEFAULT mode config.mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_DEFAULT; TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); // Test SIMPLE mode config.mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_SIMPLE; TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); // Test TEXTMSG mode config.mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_TEXTMSG; TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); // Test PROTO mode config.mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_PROTO; TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); // Test NMEA mode config.mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_NMEA; TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); // Test CALTOPO mode config.mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO; TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); } #endif // Architecture check void setup() { initializeTestEnvironment(); #if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && \ !defined(CONFIG_IDF_TARGET_ESP32C3) UNITY_BEGIN(); RUN_TEST(test_serialConfigEmptyIsValid); RUN_TEST(test_serialConfigEnabledIsValid); RUN_TEST(test_serialConfigWithOverrideConsoleNmeaModeIsValid); RUN_TEST(test_serialConfigWithOverrideConsoleCalTopoModeIsValid); RUN_TEST(test_serialConfigWithOverrideConsoleDefaultModeIsInvalid); RUN_TEST(test_serialConfigWithOverrideConsoleSimpleModeIsInvalid); RUN_TEST(test_serialConfigWithOverrideConsoleTextMsgModeIsInvalid); RUN_TEST(test_serialConfigWithOverrideConsoleProtoModeIsInvalid); RUN_TEST(test_serialConfigVariousModesWithoutOverrideAreValid); exit(UNITY_END()); #else LOG_WARN("This test requires ESP32, NRF52, or RP2040 architecture"); UNITY_BEGIN(); UNITY_END(); #endif } #else void setup() { initializeTestEnvironment(); LOG_WARN("This test requires the ARCH_PORTDUINO variant"); UNITY_BEGIN(); UNITY_END(); } #endif void loop() {} ================================================ FILE: test/test_traffic_management/test_main.cpp ================================================ #include "TestUtil.h" #include #if defined(ARCH_PORTDUINO) #define TM_TEST_ENTRY extern "C" #else #define TM_TEST_ENTRY #endif #if HAS_TRAFFIC_MANAGEMENT #include "mesh/CryptoEngine.h" #include "mesh/MeshService.h" #include "mesh/NodeDB.h" #include "mesh/Router.h" #include "modules/TrafficManagementModule.h" #include #include #include #include #include namespace { constexpr NodeNum kLocalNode = 0x11111111; constexpr NodeNum kRemoteNode = 0x22222222; constexpr NodeNum kTargetNode = 0x33333333; class MockNodeDB : public NodeDB { public: meshtastic_NodeInfoLite *getMeshNode(NodeNum n) override { if (hasCachedNode && n == cachedNodeNum) return &cachedNode; return NodeDB::getMeshNode(n); } void clearCachedNode() { hasCachedNode = false; cachedNodeNum = 0; cachedNode = meshtastic_NodeInfoLite_init_zero; } void setCachedNode(NodeNum n) { clearCachedNode(); hasCachedNode = true; cachedNodeNum = n; cachedNode.num = n; cachedNode.has_user = true; } private: bool hasCachedNode = false; NodeNum cachedNodeNum = 0; meshtastic_NodeInfoLite cachedNode = meshtastic_NodeInfoLite_init_zero; }; class MockRadioInterface : public RadioInterface { public: ErrorCode send(meshtastic_MeshPacket *p) override { packetPool.release(p); return ERRNO_OK; } uint32_t getPacketTime(uint32_t totalPacketLen, bool received = false) override { (void)totalPacketLen; (void)received; return 0; } }; class MockRouter : public Router { public: ~MockRouter() { // Router allocates a global crypt lock in its constructor. // Clean it up here so each test can build a fresh mock router. delete cryptLock; cryptLock = nullptr; } ErrorCode send(meshtastic_MeshPacket *p) override { sentPackets.push_back(*p); packetPool.release(p); return ERRNO_OK; } std::vector sentPackets; }; class TrafficManagementModuleTestShim : public TrafficManagementModule { public: using TrafficManagementModule::alterReceived; using TrafficManagementModule::handleReceived; using TrafficManagementModule::resetEpoch; using TrafficManagementModule::runOnce; bool ignoreRequestFlag() const { return ignoreRequest; } }; MockNodeDB *mockNodeDB = nullptr; static void resetTrafficConfig() { moduleConfig = meshtastic_LocalModuleConfig_init_zero; moduleConfig.has_traffic_management = true; moduleConfig.traffic_management = meshtastic_ModuleConfig_TrafficManagementConfig_init_zero; moduleConfig.traffic_management.enabled = true; config = meshtastic_LocalConfig_init_zero; config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; myNodeInfo.my_node_num = kLocalNode; router = nullptr; service = nullptr; mockNodeDB->resetNodes(); mockNodeDB->clearCachedNode(); nodeDB = mockNodeDB; } static meshtastic_MeshPacket makeDecodedPacket(meshtastic_PortNum port, NodeNum from, NodeNum to = NODENUM_BROADCAST) { meshtastic_MeshPacket packet = meshtastic_MeshPacket_init_zero; packet.from = from; packet.to = to; packet.id = 0x1001; packet.channel = 0; packet.hop_start = 3; packet.hop_limit = 3; packet.which_payload_variant = meshtastic_MeshPacket_decoded_tag; packet.decoded.portnum = port; packet.decoded.has_bitfield = true; packet.decoded.bitfield = 0; return packet; } static meshtastic_MeshPacket makeUnknownPacket(NodeNum from, NodeNum to = NODENUM_BROADCAST) { meshtastic_MeshPacket packet = meshtastic_MeshPacket_init_zero; packet.from = from; packet.to = to; packet.id = 0x2001; packet.channel = 0; packet.hop_start = 3; packet.hop_limit = 3; packet.which_payload_variant = meshtastic_MeshPacket_encrypted_tag; packet.encrypted.size = 0; return packet; } static meshtastic_MeshPacket makePositionPacket(NodeNum from, int32_t lat, int32_t lon, NodeNum to = NODENUM_BROADCAST) { meshtastic_MeshPacket packet = makeDecodedPacket(meshtastic_PortNum_POSITION_APP, from, to); meshtastic_Position pos = meshtastic_Position_init_zero; pos.has_latitude_i = true; pos.has_longitude_i = true; pos.latitude_i = lat; pos.longitude_i = lon; packet.decoded.payload.size = pb_encode_to_bytes(packet.decoded.payload.bytes, sizeof(packet.decoded.payload.bytes), &meshtastic_Position_msg, &pos); return packet; } static meshtastic_MeshPacket makeNodeInfoPacket(NodeNum from, const char *longName, const char *shortName) { meshtastic_MeshPacket packet = makeDecodedPacket(meshtastic_PortNum_NODEINFO_APP, from, NODENUM_BROADCAST); meshtastic_User user = meshtastic_User_init_zero; snprintf(user.id, sizeof(user.id), "!%08x", from); strncpy(user.long_name, longName, sizeof(user.long_name) - 1); strncpy(user.short_name, shortName, sizeof(user.short_name) - 1); packet.decoded.payload.size = pb_encode_to_bytes(packet.decoded.payload.bytes, sizeof(packet.decoded.payload.bytes), &meshtastic_User_msg, &user); return packet; } /** * Verify the module is a no-op when traffic management is disabled. * Important so config toggles cannot accidentally change routing behavior. */ static void test_tm_moduleDisabled_doesNothing(void) { moduleConfig.has_traffic_management = false; TrafficManagementModuleTestShim module; meshtastic_MeshPacket packet = makeDecodedPacket(meshtastic_PortNum_TEXT_MESSAGE_APP, kRemoteNode); ProcessMessage result = module.handleReceived(packet); meshtastic_TrafficManagementStats stats = module.getStats(); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(result)); TEST_ASSERT_EQUAL_UINT32(0, stats.packets_inspected); TEST_ASSERT_EQUAL_UINT32(0, stats.unknown_packet_drops); TEST_ASSERT_FALSE(module.ignoreRequestFlag()); } /** * Verify unknown-packet dropping uses N+1 threshold semantics. * Important to catch off-by-one regressions in drop decisions. */ static void test_tm_unknownPackets_dropOnNPlusOne(void) { moduleConfig.traffic_management.drop_unknown_enabled = true; moduleConfig.traffic_management.unknown_packet_threshold = 2; TrafficManagementModuleTestShim module; meshtastic_MeshPacket packet = makeUnknownPacket(kRemoteNode); ProcessMessage r1 = module.handleReceived(packet); ProcessMessage r2 = module.handleReceived(packet); ProcessMessage r3 = module.handleReceived(packet); meshtastic_TrafficManagementStats stats = module.getStats(); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r1)); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r2)); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::STOP), static_cast(r3)); TEST_ASSERT_EQUAL_UINT32(1, stats.unknown_packet_drops); TEST_ASSERT_EQUAL_UINT32(3, stats.packets_inspected); TEST_ASSERT_TRUE(module.ignoreRequestFlag()); } /** * Verify duplicate position broadcasts inside the dedup window are dropped. * Important because this is the primary airtime-saving behavior. */ static void test_tm_positionDedup_dropsDuplicateWithinWindow(void) { moduleConfig.traffic_management.position_dedup_enabled = true; moduleConfig.traffic_management.position_precision_bits = 16; moduleConfig.traffic_management.position_min_interval_secs = 300; TrafficManagementModuleTestShim module; meshtastic_MeshPacket first = makePositionPacket(kRemoteNode, 374221234, -1220845678); meshtastic_MeshPacket second = makePositionPacket(kRemoteNode, 374221234, -1220845678); ProcessMessage r1 = module.handleReceived(first); ProcessMessage r2 = module.handleReceived(second); meshtastic_TrafficManagementStats stats = module.getStats(); TEST_ASSERT_GREATER_THAN_UINT32(0, first.decoded.payload.size); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r1)); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::STOP), static_cast(r2)); TEST_ASSERT_EQUAL_UINT32(1, stats.position_dedup_drops); TEST_ASSERT_TRUE(module.ignoreRequestFlag()); } /** * Verify changed coordinates are forwarded even with dedup enabled. * Important so real movement updates are never suppressed as duplicates. */ static void test_tm_positionDedup_allowsMovedPosition(void) { moduleConfig.traffic_management.position_dedup_enabled = true; moduleConfig.traffic_management.position_precision_bits = 16; moduleConfig.traffic_management.position_min_interval_secs = 300; TrafficManagementModuleTestShim module; meshtastic_MeshPacket first = makePositionPacket(kRemoteNode, 374221234, -1220845678); meshtastic_MeshPacket moved = makePositionPacket(kRemoteNode, 384221234, -1210845678); ProcessMessage r1 = module.handleReceived(first); ProcessMessage r2 = module.handleReceived(moved); meshtastic_TrafficManagementStats stats = module.getStats(); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r1)); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r2)); TEST_ASSERT_EQUAL_UINT32(0, stats.position_dedup_drops); } /** * Verify rate limiting drops only after exceeding the configured threshold. * Important to protect threshold semantics from off-by-one regressions. */ static void test_tm_rateLimit_dropsOnlyAfterThreshold(void) { moduleConfig.traffic_management.rate_limit_enabled = true; moduleConfig.traffic_management.rate_limit_window_secs = 60; moduleConfig.traffic_management.rate_limit_max_packets = 3; TrafficManagementModuleTestShim module; meshtastic_MeshPacket packet = makeDecodedPacket(meshtastic_PortNum_TEXT_MESSAGE_APP, kRemoteNode); ProcessMessage r1 = module.handleReceived(packet); ProcessMessage r2 = module.handleReceived(packet); ProcessMessage r3 = module.handleReceived(packet); ProcessMessage r4 = module.handleReceived(packet); meshtastic_TrafficManagementStats stats = module.getStats(); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r1)); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r2)); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r3)); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::STOP), static_cast(r4)); TEST_ASSERT_EQUAL_UINT32(1, stats.rate_limit_drops); TEST_ASSERT_TRUE(module.ignoreRequestFlag()); } /** * Verify routing/admin traffic is exempt from rate limiting. * Important because throttling control traffic can destabilize the mesh. */ static void test_tm_rateLimit_skipsRoutingAndAdminPorts(void) { moduleConfig.traffic_management.rate_limit_enabled = true; moduleConfig.traffic_management.rate_limit_window_secs = 60; moduleConfig.traffic_management.rate_limit_max_packets = 1; TrafficManagementModuleTestShim module; meshtastic_MeshPacket routingPacket = makeDecodedPacket(meshtastic_PortNum_ROUTING_APP, kRemoteNode); meshtastic_MeshPacket adminPacket = makeDecodedPacket(meshtastic_PortNum_ADMIN_APP, kRemoteNode); for (int i = 0; i < 4; i++) { ProcessMessage rr = module.handleReceived(routingPacket); ProcessMessage ar = module.handleReceived(adminPacket); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(rr)); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(ar)); } meshtastic_TrafficManagementStats stats = module.getStats(); TEST_ASSERT_EQUAL_UINT32(0, stats.rate_limit_drops); } /** * Verify packets sourced from this node bypass dedup and rate limiting. * Important so local transmissions are not accidentally self-throttled. */ static void test_tm_fromUs_bypassesPositionAndRateFilters(void) { moduleConfig.traffic_management.position_dedup_enabled = true; moduleConfig.traffic_management.position_precision_bits = 16; moduleConfig.traffic_management.position_min_interval_secs = 300; moduleConfig.traffic_management.rate_limit_enabled = true; moduleConfig.traffic_management.rate_limit_window_secs = 60; moduleConfig.traffic_management.rate_limit_max_packets = 1; TrafficManagementModuleTestShim module; meshtastic_MeshPacket positionPacket = makePositionPacket(kLocalNode, 374221234, -1220845678); meshtastic_MeshPacket textPacket = makeDecodedPacket(meshtastic_PortNum_TEXT_MESSAGE_APP, kLocalNode); ProcessMessage p1 = module.handleReceived(positionPacket); ProcessMessage p2 = module.handleReceived(positionPacket); ProcessMessage t1 = module.handleReceived(textPacket); ProcessMessage t2 = module.handleReceived(textPacket); meshtastic_TrafficManagementStats stats = module.getStats(); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(p1)); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(p2)); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(t1)); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(t2)); TEST_ASSERT_EQUAL_UINT32(0, stats.position_dedup_drops); TEST_ASSERT_EQUAL_UINT32(0, stats.rate_limit_drops); } /** * Verify locally addressed packets are never dropped by transit shaping. * Important so dedup/rate limiting do not suppress end-user delivery. */ static void test_tm_localDestination_bypassesTransitFilters(void) { moduleConfig.traffic_management.position_dedup_enabled = true; moduleConfig.traffic_management.position_precision_bits = 16; moduleConfig.traffic_management.position_min_interval_secs = 300; moduleConfig.traffic_management.rate_limit_enabled = true; moduleConfig.traffic_management.rate_limit_window_secs = 60; moduleConfig.traffic_management.rate_limit_max_packets = 1; TrafficManagementModuleTestShim module; meshtastic_MeshPacket position1 = makePositionPacket(kRemoteNode, 374221234, -1220845678, kLocalNode); meshtastic_MeshPacket position2 = makePositionPacket(kRemoteNode, 374221234, -1220845678, kLocalNode); meshtastic_MeshPacket text1 = makeDecodedPacket(meshtastic_PortNum_TEXT_MESSAGE_APP, kRemoteNode, kLocalNode); meshtastic_MeshPacket text2 = makeDecodedPacket(meshtastic_PortNum_TEXT_MESSAGE_APP, kRemoteNode, kLocalNode); ProcessMessage p1 = module.handleReceived(position1); ProcessMessage p2 = module.handleReceived(position2); ProcessMessage t1 = module.handleReceived(text1); ProcessMessage t2 = module.handleReceived(text2); meshtastic_TrafficManagementStats stats = module.getStats(); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(p1)); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(p2)); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(t1)); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(t2)); TEST_ASSERT_EQUAL_UINT32(0, stats.position_dedup_drops); TEST_ASSERT_EQUAL_UINT32(0, stats.rate_limit_drops); } /** * Verify router role clamps NodeInfo response hops to router-safe maximum. * Important so large config values cannot widen response scope unexpectedly. */ static void test_tm_nodeinfo_routerClamp_skipsWhenTooManyHops(void) { moduleConfig.traffic_management.nodeinfo_direct_response = true; moduleConfig.traffic_management.nodeinfo_direct_response_max_hops = 10; config.device.role = meshtastic_Config_DeviceConfig_Role_ROUTER; mockNodeDB->setCachedNode(kTargetNode); TrafficManagementModuleTestShim module; meshtastic_MeshPacket request = makeDecodedPacket(meshtastic_PortNum_NODEINFO_APP, kRemoteNode, kTargetNode); request.decoded.want_response = true; request.hop_start = 5; request.hop_limit = 1; // 4 hops away; router clamp should cap max at 3 ProcessMessage result = module.handleReceived(request); meshtastic_TrafficManagementStats stats = module.getStats(); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(result)); TEST_ASSERT_EQUAL_UINT32(0, stats.nodeinfo_cache_hits); TEST_ASSERT_FALSE(module.ignoreRequestFlag()); } /** * Verify NodeInfo direct-response success path and reply packet fields. * Important because this path consumes the request and generates a spoofed cached reply. */ static void test_tm_nodeinfo_directResponse_respondsFromCache(void) { moduleConfig.traffic_management.nodeinfo_direct_response = true; moduleConfig.traffic_management.nodeinfo_direct_response_max_hops = 10; config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; config.lora.config_ok_to_mqtt = true; mockNodeDB->setCachedNode(kTargetNode); MockRouter mockRouter; mockRouter.addInterface(std::unique_ptr(new MockRadioInterface())); MeshService mockService; router = &mockRouter; service = &mockService; TrafficManagementModuleTestShim module; meshtastic_MeshPacket request = makeDecodedPacket(meshtastic_PortNum_NODEINFO_APP, kRemoteNode, kTargetNode); request.decoded.want_response = true; request.id = 0x13572468; request.hop_start = 3; request.hop_limit = 3; // direct request (0 hops away) ProcessMessage result = module.handleReceived(request); meshtastic_TrafficManagementStats stats = module.getStats(); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::STOP), static_cast(result)); TEST_ASSERT_TRUE(module.ignoreRequestFlag()); TEST_ASSERT_EQUAL_UINT32(1, stats.nodeinfo_cache_hits); TEST_ASSERT_EQUAL_UINT32(1, static_cast(mockRouter.sentPackets.size())); const meshtastic_MeshPacket &reply = mockRouter.sentPackets.front(); TEST_ASSERT_EQUAL_INT(meshtastic_PortNum_NODEINFO_APP, reply.decoded.portnum); TEST_ASSERT_EQUAL_UINT32(kTargetNode, reply.from); TEST_ASSERT_EQUAL_UINT32(kRemoteNode, reply.to); TEST_ASSERT_EQUAL_UINT32(request.id, reply.decoded.request_id); TEST_ASSERT_FALSE(reply.decoded.want_response); TEST_ASSERT_EQUAL_UINT8(0, reply.hop_limit); TEST_ASSERT_EQUAL_UINT8(0, reply.hop_start); TEST_ASSERT_EQUAL_UINT8(mockNodeDB->getLastByteOfNodeNum(kRemoteNode), reply.next_hop); TEST_ASSERT_TRUE(reply.decoded.has_bitfield); TEST_ASSERT_EQUAL_UINT8(BITFIELD_OK_TO_MQTT_MASK, reply.decoded.bitfield); } /** * Verify cached direct replies still preserve requester NodeInfo learning. * Important so consuming the request does not skip NodeDB refresh for observers. */ static void test_tm_nodeinfo_directResponse_learnsRequestorNodeInfo(void) { moduleConfig.traffic_management.nodeinfo_direct_response = true; moduleConfig.traffic_management.nodeinfo_direct_response_max_hops = 10; config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; mockNodeDB->setCachedNode(kTargetNode); MockRouter mockRouter; mockRouter.addInterface(std::unique_ptr(new MockRadioInterface())); MeshService mockService; router = &mockRouter; service = &mockService; TrafficManagementModuleTestShim module; meshtastic_MeshPacket request = makeNodeInfoPacket(kRemoteNode, "requester-long", "rq"); request.to = kTargetNode; request.decoded.want_response = true; request.id = 0x01020304; request.hop_start = 3; request.hop_limit = 3; ProcessMessage result = module.handleReceived(request); meshtastic_NodeInfoLite *requestor = mockNodeDB->getMeshNode(kRemoteNode); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::STOP), static_cast(result)); TEST_ASSERT_NOT_NULL(requestor); TEST_ASSERT_TRUE(requestor->has_user); TEST_ASSERT_EQUAL_STRING("requester-long", requestor->user.long_name); TEST_ASSERT_EQUAL_STRING("rq", requestor->user.short_name); TEST_ASSERT_EQUAL_UINT8(request.channel, requestor->channel); } /** * Verify client role only answers direct (0-hop) NodeInfo requests. * Important so clients do not answer relayed requests outside intended scope. */ static void test_tm_nodeinfo_clientClamp_skipsWhenNotDirect(void) { moduleConfig.traffic_management.nodeinfo_direct_response = true; moduleConfig.traffic_management.nodeinfo_direct_response_max_hops = 10; config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; mockNodeDB->setCachedNode(kTargetNode); TrafficManagementModuleTestShim module; meshtastic_MeshPacket request = makeDecodedPacket(meshtastic_PortNum_NODEINFO_APP, kRemoteNode, kTargetNode); request.decoded.want_response = true; request.hop_start = 2; request.hop_limit = 1; // 1 hop away; clients are clamped to max 0 ProcessMessage result = module.handleReceived(request); meshtastic_TrafficManagementStats stats = module.getStats(); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(result)); TEST_ASSERT_EQUAL_UINT32(0, stats.nodeinfo_cache_hits); TEST_ASSERT_FALSE(module.ignoreRequestFlag()); } #if !(defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM)) /** * Verify non-PSRAM builds require NodeDB for direct NodeInfo responses. * Important because fallback should only happen through node-wide data when * the dedicated PSRAM cache does not exist. */ static void test_tm_nodeinfo_directResponse_withoutNodeDbEntry_skips(void) { moduleConfig.traffic_management.nodeinfo_direct_response = true; moduleConfig.traffic_management.nodeinfo_direct_response_max_hops = 10; config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; mockNodeDB->clearCachedNode(); MockRouter mockRouter; mockRouter.addInterface(std::unique_ptr(new MockRadioInterface())); MeshService mockService; router = &mockRouter; service = &mockService; TrafficManagementModuleTestShim module; meshtastic_MeshPacket request = makeDecodedPacket(meshtastic_PortNum_NODEINFO_APP, kRemoteNode, kTargetNode); request.decoded.want_response = true; request.hop_start = 3; request.hop_limit = 3; ProcessMessage result = module.handleReceived(request); meshtastic_TrafficManagementStats stats = module.getStats(); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(result)); TEST_ASSERT_FALSE(module.ignoreRequestFlag()); TEST_ASSERT_EQUAL_UINT32(0, stats.nodeinfo_cache_hits); TEST_ASSERT_EQUAL_UINT32(0, static_cast(mockRouter.sentPackets.size())); } #endif #if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM) /** * Verify PSRAM NodeInfo cache can answer requests without NodeDB and that * shouldRespondToNodeInfo() uses cached bitfield metadata. */ static void test_tm_nodeinfo_directResponse_psramCacheRespondsAndPreservesBitfield(void) { moduleConfig.traffic_management.nodeinfo_direct_response = true; moduleConfig.traffic_management.nodeinfo_direct_response_max_hops = 10; config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; config.lora.config_ok_to_mqtt = true; mockNodeDB->clearCachedNode(); MockRouter mockRouter; mockRouter.addInterface(std::unique_ptr(new MockRadioInterface())); MeshService mockService; router = &mockRouter; service = &mockService; TrafficManagementModuleTestShim module; meshtastic_MeshPacket observed = makeNodeInfoPacket(kTargetNode, "target-long", "tg"); observed.decoded.has_bitfield = true; observed.decoded.bitfield = BITFIELD_WANT_RESPONSE_MASK; observed.channel = 2; observed.rx_time = 123456; ProcessMessage observedResult = module.handleReceived(observed); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(observedResult)); meshtastic_MeshPacket request = makeDecodedPacket(meshtastic_PortNum_NODEINFO_APP, kRemoteNode, kTargetNode); request.decoded.want_response = true; request.id = 0x24681357; request.channel = 1; request.hop_start = 3; request.hop_limit = 3; ProcessMessage result = module.handleReceived(request); meshtastic_TrafficManagementStats stats = module.getStats(); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::STOP), static_cast(result)); TEST_ASSERT_TRUE(module.ignoreRequestFlag()); TEST_ASSERT_EQUAL_UINT32(1, stats.nodeinfo_cache_hits); TEST_ASSERT_EQUAL_UINT32(1, static_cast(mockRouter.sentPackets.size())); const meshtastic_MeshPacket &reply = mockRouter.sentPackets.front(); TEST_ASSERT_TRUE(reply.decoded.has_bitfield); TEST_ASSERT_EQUAL_UINT8(static_cast(BITFIELD_WANT_RESPONSE_MASK | BITFIELD_OK_TO_MQTT_MASK), reply.decoded.bitfield); TEST_ASSERT_EQUAL_UINT32(kTargetNode, reply.from); TEST_ASSERT_EQUAL_UINT32(kRemoteNode, reply.to); TEST_ASSERT_EQUAL_UINT8(request.channel, reply.channel); TEST_ASSERT_EQUAL_UINT32(request.id, reply.decoded.request_id); } /** * Verify PSRAM cache misses do not fall back to NodeDB. * Important so the dedicated PSRAM index stays logically separate from * NodeInfoModule/NodeDB when PSRAM is available. */ static void test_tm_nodeinfo_directResponse_psramMissDoesNotFallbackToNodeDb(void) { moduleConfig.traffic_management.nodeinfo_direct_response = true; moduleConfig.traffic_management.nodeinfo_direct_response_max_hops = 10; config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; mockNodeDB->setCachedNode(kTargetNode); MockRouter mockRouter; mockRouter.addInterface(std::unique_ptr(new MockRadioInterface())); MeshService mockService; router = &mockRouter; service = &mockService; TrafficManagementModuleTestShim module; meshtastic_MeshPacket request = makeDecodedPacket(meshtastic_PortNum_NODEINFO_APP, kRemoteNode, kTargetNode); request.decoded.want_response = true; request.hop_start = 3; request.hop_limit = 3; ProcessMessage result = module.handleReceived(request); meshtastic_TrafficManagementStats stats = module.getStats(); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(result)); TEST_ASSERT_FALSE(module.ignoreRequestFlag()); TEST_ASSERT_EQUAL_UINT32(0, stats.nodeinfo_cache_hits); TEST_ASSERT_EQUAL_UINT32(0, static_cast(mockRouter.sentPackets.size())); } #endif /** * Verify relayed telemetry broadcasts are hop-exhausted when enabled. * Important to prevent further mesh propagation while still allowing one relay step. */ static void test_tm_alterReceived_exhaustsRelayedTelemetryBroadcast(void) { moduleConfig.traffic_management.exhaust_hop_telemetry = true; TrafficManagementModuleTestShim module; meshtastic_MeshPacket packet = makeDecodedPacket(meshtastic_PortNum_TELEMETRY_APP, kRemoteNode, NODENUM_BROADCAST); packet.hop_start = 5; packet.hop_limit = 3; module.alterReceived(packet); meshtastic_TrafficManagementStats stats = module.getStats(); TEST_ASSERT_EQUAL_UINT8(0, packet.hop_limit); TEST_ASSERT_EQUAL_UINT8(3, packet.hop_start); TEST_ASSERT_TRUE(module.shouldExhaustHops(packet)); TEST_ASSERT_EQUAL_UINT32(1, stats.hop_exhausted_packets); } /** * Verify hop exhaustion skips unicast and local-origin packets. * Important to avoid mutating traffic that should retain normal forwarding behavior. */ static void test_tm_alterReceived_skipsLocalAndUnicast(void) { moduleConfig.traffic_management.exhaust_hop_telemetry = true; TrafficManagementModuleTestShim module; meshtastic_MeshPacket unicast = makeDecodedPacket(meshtastic_PortNum_TELEMETRY_APP, kRemoteNode, kTargetNode); unicast.hop_start = 5; unicast.hop_limit = 3; module.alterReceived(unicast); TEST_ASSERT_EQUAL_UINT8(3, unicast.hop_limit); TEST_ASSERT_FALSE(module.shouldExhaustHops(unicast)); meshtastic_MeshPacket fromUs = makeDecodedPacket(meshtastic_PortNum_TELEMETRY_APP, kLocalNode, NODENUM_BROADCAST); fromUs.hop_start = 5; fromUs.hop_limit = 3; module.alterReceived(fromUs); TEST_ASSERT_EQUAL_UINT8(3, fromUs.hop_limit); TEST_ASSERT_FALSE(module.shouldExhaustHops(fromUs)); meshtastic_TrafficManagementStats stats = module.getStats(); TEST_ASSERT_EQUAL_UINT32(0, stats.hop_exhausted_packets); } /** * Verify position dedup window expires and later duplicates are allowed. * Important so periodic identical reports can resume after cooldown. */ static void test_tm_positionDedup_allowsDuplicateAfterIntervalExpires(void) { moduleConfig.traffic_management.position_dedup_enabled = true; moduleConfig.traffic_management.position_precision_bits = 16; moduleConfig.traffic_management.position_min_interval_secs = 1; TrafficManagementModuleTestShim module; meshtastic_MeshPacket first = makePositionPacket(kRemoteNode, 374221234, -1220845678); meshtastic_MeshPacket second = makePositionPacket(kRemoteNode, 374221234, -1220845678); meshtastic_MeshPacket third = makePositionPacket(kRemoteNode, 374221234, -1220845678); ProcessMessage r1 = module.handleReceived(first); ProcessMessage r2 = module.handleReceived(second); testDelay(1200); ProcessMessage r3 = module.handleReceived(third); meshtastic_TrafficManagementStats stats = module.getStats(); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r1)); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::STOP), static_cast(r2)); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r3)); TEST_ASSERT_EQUAL_UINT32(1, stats.position_dedup_drops); } /** * Verify interval=0 disables position deduplication. * Important because this is an explicit configuration escape hatch. */ static void test_tm_positionDedup_intervalZero_neverDrops(void) { moduleConfig.traffic_management.position_dedup_enabled = true; moduleConfig.traffic_management.position_precision_bits = 16; moduleConfig.traffic_management.position_min_interval_secs = 0; TrafficManagementModuleTestShim module; meshtastic_MeshPacket first = makePositionPacket(kRemoteNode, 374221234, -1220845678); meshtastic_MeshPacket second = makePositionPacket(kRemoteNode, 374221234, -1220845678); ProcessMessage r1 = module.handleReceived(first); ProcessMessage r2 = module.handleReceived(second); meshtastic_TrafficManagementStats stats = module.getStats(); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r1)); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r2)); TEST_ASSERT_EQUAL_UINT32(0, stats.position_dedup_drops); } /** * Verify precision values above 32 fall back to default precision. * Important so invalid config uses the documented default behavior. */ static void test_tm_positionDedup_precisionAbove32_usesDefaultPrecision(void) { moduleConfig.traffic_management.position_dedup_enabled = true; moduleConfig.traffic_management.position_precision_bits = 99; moduleConfig.traffic_management.position_min_interval_secs = 300; TrafficManagementModuleTestShim module; meshtastic_MeshPacket first = makePositionPacket(kRemoteNode, 374221234, -1220845678); meshtastic_MeshPacket second = makePositionPacket(kRemoteNode, 384221234, -1210845678); ProcessMessage r1 = module.handleReceived(first); ProcessMessage r2 = module.handleReceived(second); meshtastic_TrafficManagementStats stats = module.getStats(); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r1)); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r2)); TEST_ASSERT_EQUAL_UINT32(0, stats.position_dedup_drops); } /** * Verify precision=32 does not collapse all positions to one fingerprint. * Important to prevent false duplicate drops at the full-precision boundary. */ static void test_tm_positionDedup_precision32_allowsDistinctPositions(void) { moduleConfig.traffic_management.position_dedup_enabled = true; moduleConfig.traffic_management.position_precision_bits = 32; moduleConfig.traffic_management.position_min_interval_secs = 300; TrafficManagementModuleTestShim module; meshtastic_MeshPacket first = makePositionPacket(kRemoteNode, 374221234, -1220845678); meshtastic_MeshPacket second = makePositionPacket(kRemoteNode, 374221235, -1220845677); ProcessMessage r1 = module.handleReceived(first); ProcessMessage r2 = module.handleReceived(second); meshtastic_TrafficManagementStats stats = module.getStats(); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r1)); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r2)); TEST_ASSERT_EQUAL_UINT32(0, stats.position_dedup_drops); } /** * Verify invalid precision=0 is treated as full precision. * Important so invalid config does not collapse all positions into one fingerprint. */ static void test_tm_positionDedup_precisionZero_allowsDistinctPositions(void) { moduleConfig.traffic_management.position_dedup_enabled = true; moduleConfig.traffic_management.position_precision_bits = 0; moduleConfig.traffic_management.position_min_interval_secs = 300; TrafficManagementModuleTestShim module; meshtastic_MeshPacket first = makePositionPacket(kRemoteNode, 374221234, -1220845678); meshtastic_MeshPacket second = makePositionPacket(kRemoteNode, 374221235, -1220845677); ProcessMessage r1 = module.handleReceived(first); ProcessMessage r2 = module.handleReceived(second); meshtastic_TrafficManagementStats stats = module.getStats(); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r1)); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r2)); TEST_ASSERT_EQUAL_UINT32(0, stats.position_dedup_drops); } /** * Verify epoch reset invalidates stale position identity for dedup. * Important so reset paths cannot leak prior packet identity into new windows. */ static void test_tm_positionDedup_epochReset_doesNotDropFirstPacketAfterReset(void) { moduleConfig.traffic_management.position_dedup_enabled = true; moduleConfig.traffic_management.position_precision_bits = 16; moduleConfig.traffic_management.position_min_interval_secs = 300; TrafficManagementModuleTestShim module; meshtastic_MeshPacket first = makePositionPacket(kRemoteNode, 374221234, -1220845678); meshtastic_MeshPacket afterReset = makePositionPacket(kRemoteNode, 374221234, -1220845678); meshtastic_MeshPacket duplicate = makePositionPacket(kRemoteNode, 374221234, -1220845678); ProcessMessage r1 = module.handleReceived(first); module.resetEpoch(millis()); ProcessMessage r2 = module.handleReceived(afterReset); ProcessMessage r3 = module.handleReceived(duplicate); meshtastic_TrafficManagementStats stats = module.getStats(); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r1)); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r2)); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::STOP), static_cast(r3)); TEST_ASSERT_EQUAL_UINT32(1, stats.position_dedup_drops); } /** * Verify non-position cache state does not make the first fingerprint-0 position look duplicated. * Important so unified cache entries from other features cannot leak into dedup decisions. */ static void test_tm_positionDedup_priorRateState_doesNotDropFirstFingerprintZero(void) { moduleConfig.traffic_management.position_dedup_enabled = true; moduleConfig.traffic_management.position_precision_bits = 16; moduleConfig.traffic_management.position_min_interval_secs = 300; moduleConfig.traffic_management.rate_limit_enabled = true; moduleConfig.traffic_management.rate_limit_window_secs = 60; moduleConfig.traffic_management.rate_limit_max_packets = 10; TrafficManagementModuleTestShim module; meshtastic_MeshPacket text = makeDecodedPacket(meshtastic_PortNum_TEXT_MESSAGE_APP, kRemoteNode); meshtastic_MeshPacket first = makePositionPacket(kRemoteNode, 0x12300000, 0x45600000); meshtastic_MeshPacket duplicate = makePositionPacket(kRemoteNode, 0x12300000, 0x45600000); ProcessMessage seeded = module.handleReceived(text); ProcessMessage r1 = module.handleReceived(first); ProcessMessage r2 = module.handleReceived(duplicate); meshtastic_TrafficManagementStats stats = module.getStats(); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(seeded)); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r1)); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::STOP), static_cast(r2)); TEST_ASSERT_EQUAL_UINT32(1, stats.position_dedup_drops); } /** * Verify rate-limit counters reset after the window expires. * Important so temporary bursts do not cause persistent throttling. */ static void test_tm_rateLimit_resetsAfterWindowExpires(void) { moduleConfig.traffic_management.rate_limit_enabled = true; moduleConfig.traffic_management.rate_limit_window_secs = 1; moduleConfig.traffic_management.rate_limit_max_packets = 1; TrafficManagementModuleTestShim module; meshtastic_MeshPacket packet = makeDecodedPacket(meshtastic_PortNum_TEXT_MESSAGE_APP, kRemoteNode); ProcessMessage r1 = module.handleReceived(packet); ProcessMessage r2 = module.handleReceived(packet); testDelay(1200); ProcessMessage r3 = module.handleReceived(packet); meshtastic_TrafficManagementStats stats = module.getStats(); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r1)); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::STOP), static_cast(r2)); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r3)); TEST_ASSERT_EQUAL_UINT32(1, stats.rate_limit_drops); } /** * Verify rate-limit thresholds above 255 effectively clamp to 255. * Important because counters are uint8_t and must not overflow behavior. */ static void test_tm_rateLimit_thresholdAbove255_clamps(void) { moduleConfig.traffic_management.rate_limit_enabled = true; moduleConfig.traffic_management.rate_limit_window_secs = 60; moduleConfig.traffic_management.rate_limit_max_packets = 300; TrafficManagementModuleTestShim module; meshtastic_MeshPacket packet = makeDecodedPacket(meshtastic_PortNum_TEXT_MESSAGE_APP, kRemoteNode); for (int i = 0; i < 255; i++) { ProcessMessage result = module.handleReceived(packet); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(result)); } ProcessMessage dropped = module.handleReceived(packet); meshtastic_TrafficManagementStats stats = module.getStats(); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::STOP), static_cast(dropped)); TEST_ASSERT_EQUAL_UINT32(1, stats.rate_limit_drops); } /** * Verify unknown-packet tracking resets after its active window expires. * Important so old unknown traffic does not trigger delayed drops. */ static void test_tm_unknownPackets_resetAfterWindowExpires(void) { moduleConfig.traffic_management.drop_unknown_enabled = true; moduleConfig.traffic_management.unknown_packet_threshold = 1; moduleConfig.traffic_management.rate_limit_window_secs = 1; TrafficManagementModuleTestShim module; meshtastic_MeshPacket packet = makeUnknownPacket(kRemoteNode); ProcessMessage r1 = module.handleReceived(packet); ProcessMessage r2 = module.handleReceived(packet); testDelay(1200); ProcessMessage r3 = module.handleReceived(packet); meshtastic_TrafficManagementStats stats = module.getStats(); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r1)); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::STOP), static_cast(r2)); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r3)); TEST_ASSERT_EQUAL_UINT32(1, stats.unknown_packet_drops); } /** * Verify unknown threshold values above 255 clamp to the counter ceiling. * Important to align config semantics with saturating counter storage. */ static void test_tm_unknownPackets_thresholdAbove255_clamps(void) { moduleConfig.traffic_management.drop_unknown_enabled = true; moduleConfig.traffic_management.unknown_packet_threshold = 300; TrafficManagementModuleTestShim module; meshtastic_MeshPacket packet = makeUnknownPacket(kRemoteNode); for (int i = 0; i < 255; i++) { ProcessMessage result = module.handleReceived(packet); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(result)); } ProcessMessage dropped = module.handleReceived(packet); meshtastic_TrafficManagementStats stats = module.getStats(); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::STOP), static_cast(dropped)); TEST_ASSERT_EQUAL_UINT32(1, stats.unknown_packet_drops); } /** * Verify relayed position broadcasts can also be hop-exhausted. * Important because telemetry and position use separate exhaust flags. */ static void test_tm_alterReceived_exhaustsRelayedPositionBroadcast(void) { moduleConfig.traffic_management.exhaust_hop_position = true; TrafficManagementModuleTestShim module; meshtastic_MeshPacket packet = makePositionPacket(kRemoteNode, 374221234, -1220845678, NODENUM_BROADCAST); packet.hop_start = 5; packet.hop_limit = 2; module.alterReceived(packet); meshtastic_TrafficManagementStats stats = module.getStats(); TEST_ASSERT_EQUAL_UINT8(0, packet.hop_limit); TEST_ASSERT_EQUAL_UINT8(4, packet.hop_start); TEST_ASSERT_TRUE(module.shouldExhaustHops(packet)); TEST_ASSERT_EQUAL_UINT32(1, stats.hop_exhausted_packets); } /** * Verify hop exhaustion ignores undecoded/encrypted packets. * Important so we never mutate packets that were not decoded by this module. */ static void test_tm_alterReceived_skipsUndecodedPackets(void) { moduleConfig.traffic_management.exhaust_hop_telemetry = true; TrafficManagementModuleTestShim module; meshtastic_MeshPacket packet = makeUnknownPacket(kRemoteNode, NODENUM_BROADCAST); packet.hop_start = 5; packet.hop_limit = 3; module.alterReceived(packet); meshtastic_TrafficManagementStats stats = module.getStats(); TEST_ASSERT_EQUAL_UINT8(5, packet.hop_start); TEST_ASSERT_EQUAL_UINT8(3, packet.hop_limit); TEST_ASSERT_FALSE(module.shouldExhaustHops(packet)); TEST_ASSERT_EQUAL_UINT32(0, stats.hop_exhausted_packets); } /** * Verify exhaustRequested is per-packet and resets on next handleReceived(). * Important so a prior packet cannot leak hop-exhaust state into later packets. */ static void test_tm_alterReceived_resetExhaustFlagOnNextPacket(void) { moduleConfig.traffic_management.exhaust_hop_telemetry = true; TrafficManagementModuleTestShim module; meshtastic_MeshPacket telemetry = makeDecodedPacket(meshtastic_PortNum_TELEMETRY_APP, kRemoteNode, NODENUM_BROADCAST); telemetry.hop_start = 5; telemetry.hop_limit = 3; module.alterReceived(telemetry); TEST_ASSERT_TRUE(module.shouldExhaustHops(telemetry)); meshtastic_MeshPacket text = makeDecodedPacket(meshtastic_PortNum_TEXT_MESSAGE_APP, kRemoteNode); ProcessMessage result = module.handleReceived(text); meshtastic_TrafficManagementStats stats = module.getStats(); TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(result)); TEST_ASSERT_FALSE(module.shouldExhaustHops(telemetry)); TEST_ASSERT_EQUAL_UINT32(1, stats.hop_exhausted_packets); } /** * Verify exhaust requests are packet-scoped (from + id). * Important so stale state from one packet cannot influence unrelated packets * that pass through duplicate/rebroadcast paths before handleReceived(). */ static void test_tm_alterReceived_exhaustFlag_isPacketScoped(void) { moduleConfig.traffic_management.exhaust_hop_telemetry = true; TrafficManagementModuleTestShim module; meshtastic_MeshPacket exhausted = makeDecodedPacket(meshtastic_PortNum_TELEMETRY_APP, kRemoteNode, NODENUM_BROADCAST); exhausted.id = 0x1010; exhausted.hop_start = 5; exhausted.hop_limit = 3; module.alterReceived(exhausted); meshtastic_MeshPacket unrelated = makeDecodedPacket(meshtastic_PortNum_TELEMETRY_APP, kTargetNode, NODENUM_BROADCAST); unrelated.id = 0x2020; unrelated.hop_start = 4; unrelated.hop_limit = 0; TEST_ASSERT_TRUE(module.shouldExhaustHops(exhausted)); TEST_ASSERT_FALSE(module.shouldExhaustHops(unrelated)); } /** * Verify runOnce() returns sleep-forever interval when module is disabled. * Important to ensure the maintenance thread is effectively inert when off. */ static void test_tm_runOnce_disabledReturnsMaxInterval(void) { moduleConfig.traffic_management.enabled = false; TrafficManagementModuleTestShim module; int32_t interval = module.runOnce(); TEST_ASSERT_EQUAL_INT32(INT32_MAX, interval); } /** * Verify runOnce() returns the maintenance cadence when enabled. * Important so periodic cache housekeeping continues at expected interval. */ static void test_tm_runOnce_enabledReturnsMaintenanceInterval(void) { TrafficManagementModuleTestShim module; int32_t interval = module.runOnce(); TEST_ASSERT_EQUAL_INT32(60 * 1000, interval); } } // namespace void setUp(void) { resetTrafficConfig(); } void tearDown(void) {} TM_TEST_ENTRY void setup() { delay(10); delay(2000); initializeTestEnvironment(); mockNodeDB = new MockNodeDB(); nodeDB = mockNodeDB; UNITY_BEGIN(); RUN_TEST(test_tm_moduleDisabled_doesNothing); RUN_TEST(test_tm_unknownPackets_dropOnNPlusOne); RUN_TEST(test_tm_positionDedup_dropsDuplicateWithinWindow); RUN_TEST(test_tm_positionDedup_allowsMovedPosition); RUN_TEST(test_tm_rateLimit_dropsOnlyAfterThreshold); RUN_TEST(test_tm_rateLimit_skipsRoutingAndAdminPorts); RUN_TEST(test_tm_fromUs_bypassesPositionAndRateFilters); RUN_TEST(test_tm_localDestination_bypassesTransitFilters); RUN_TEST(test_tm_nodeinfo_routerClamp_skipsWhenTooManyHops); RUN_TEST(test_tm_nodeinfo_directResponse_respondsFromCache); RUN_TEST(test_tm_nodeinfo_directResponse_learnsRequestorNodeInfo); RUN_TEST(test_tm_nodeinfo_clientClamp_skipsWhenNotDirect); #if !(defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM)) RUN_TEST(test_tm_nodeinfo_directResponse_withoutNodeDbEntry_skips); #endif #if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM) RUN_TEST(test_tm_nodeinfo_directResponse_psramCacheRespondsAndPreservesBitfield); RUN_TEST(test_tm_nodeinfo_directResponse_psramMissDoesNotFallbackToNodeDb); #endif RUN_TEST(test_tm_alterReceived_exhaustsRelayedTelemetryBroadcast); RUN_TEST(test_tm_alterReceived_skipsLocalAndUnicast); RUN_TEST(test_tm_positionDedup_allowsDuplicateAfterIntervalExpires); RUN_TEST(test_tm_positionDedup_intervalZero_neverDrops); RUN_TEST(test_tm_positionDedup_precisionAbove32_usesDefaultPrecision); RUN_TEST(test_tm_positionDedup_precision32_allowsDistinctPositions); RUN_TEST(test_tm_positionDedup_precisionZero_allowsDistinctPositions); RUN_TEST(test_tm_positionDedup_epochReset_doesNotDropFirstPacketAfterReset); RUN_TEST(test_tm_positionDedup_priorRateState_doesNotDropFirstFingerprintZero); RUN_TEST(test_tm_rateLimit_resetsAfterWindowExpires); RUN_TEST(test_tm_rateLimit_thresholdAbove255_clamps); RUN_TEST(test_tm_unknownPackets_resetAfterWindowExpires); RUN_TEST(test_tm_unknownPackets_thresholdAbove255_clamps); RUN_TEST(test_tm_alterReceived_exhaustsRelayedPositionBroadcast); RUN_TEST(test_tm_alterReceived_skipsUndecodedPackets); RUN_TEST(test_tm_alterReceived_resetExhaustFlagOnNextPacket); RUN_TEST(test_tm_alterReceived_exhaustFlag_isPacketScoped); RUN_TEST(test_tm_runOnce_disabledReturnsMaxInterval); RUN_TEST(test_tm_runOnce_enabledReturnsMaintenanceInterval); exit(UNITY_END()); } TM_TEST_ENTRY void loop() {} #else void setUp(void) {} void tearDown(void) {} TM_TEST_ENTRY void setup() { initializeTestEnvironment(); UNITY_BEGIN(); exit(UNITY_END()); } TM_TEST_ENTRY void loop() {} #endif ================================================ FILE: test/test_transmit_history/test_main.cpp ================================================ #include "TestUtil.h" #include "TransmitHistory.h" #include #include // Reset the singleton between tests static void resetTransmitHistory() { if (transmitHistory) { delete transmitHistory; transmitHistory = nullptr; } transmitHistory = TransmitHistory::getInstance(); } void setUp(void) { resetTransmitHistory(); } void tearDown(void) {} static void test_setLastSentToMesh_stores_millis() { transmitHistory->setLastSentToMesh(meshtastic_PortNum_NODEINFO_APP); uint32_t result = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP); TEST_ASSERT_NOT_EQUAL(0, result); // The stored millis value should be very close to current millis() uint32_t diff = millis() - result; TEST_ASSERT_LESS_OR_EQUAL(100, diff); // Within 100ms } static void test_set_overwrites_previous_value() { transmitHistory->setLastSentToMesh(meshtastic_PortNum_TELEMETRY_APP); uint32_t first = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_TELEMETRY_APP); testDelay(50); transmitHistory->setLastSentToMesh(meshtastic_PortNum_TELEMETRY_APP); uint32_t second = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_TELEMETRY_APP); // The second value should be newer (larger millis) TEST_ASSERT_GREATER_THAN(first, second); } // --- Throttle integration --- static void test_throttle_blocks_within_interval() { transmitHistory->setLastSentToMesh(meshtastic_PortNum_NODEINFO_APP); uint32_t lastMs = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP); // Should be within a 10-minute interval (just set it) bool withinInterval = Throttle::isWithinTimespanMs(lastMs, 10 * 60 * 1000); TEST_ASSERT_TRUE(withinInterval); } static void test_throttle_allows_after_interval() { // Unknown key returns 0 — throttle should NOT block uint32_t lastMs = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP); TEST_ASSERT_EQUAL_UINT32(0, lastMs); // When lastMs == 0, the module check `lastMs == 0 || !isWithinTimespan` allows sending bool shouldSend = (lastMs == 0) || !Throttle::isWithinTimespanMs(lastMs, 10 * 60 * 1000); TEST_ASSERT_TRUE(shouldSend); } static void test_throttle_blocks_after_set_then_zero_does_not() { // Set it — now throttle should block transmitHistory->setLastSentToMesh(meshtastic_PortNum_TELEMETRY_APP); uint32_t lastMs = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_TELEMETRY_APP); bool shouldSend = (lastMs == 0) || !Throttle::isWithinTimespanMs(lastMs, 60 * 60 * 1000); TEST_ASSERT_FALSE(shouldSend); // Should be blocked (within 1hr interval) // Different key — should allow uint32_t otherMs = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_POSITION_APP); bool otherShouldSend = (otherMs == 0) || !Throttle::isWithinTimespanMs(otherMs, 60 * 60 * 1000); TEST_ASSERT_TRUE(otherShouldSend); } // --- Multiple keys --- static void test_multiple_keys_stored_independently() { transmitHistory->setLastSentToMesh(meshtastic_PortNum_NODEINFO_APP); uint32_t nodeInfoInitial = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP); testDelay(20); transmitHistory->setLastSentToMesh(meshtastic_PortNum_POSITION_APP); uint32_t positionInitial = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_POSITION_APP); testDelay(20); transmitHistory->setLastSentToMesh(meshtastic_PortNum_TELEMETRY_APP); uint32_t nodeInfo = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP); uint32_t position = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_POSITION_APP); uint32_t telemetry = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_TELEMETRY_APP); // All should be non-zero TEST_ASSERT_NOT_EQUAL(0, nodeInfo); TEST_ASSERT_NOT_EQUAL(0, position); TEST_ASSERT_NOT_EQUAL(0, telemetry); // Updating other keys should not overwrite earlier key timestamps TEST_ASSERT_EQUAL_UINT32(nodeInfoInitial, nodeInfo); TEST_ASSERT_EQUAL_UINT32(positionInitial, position); } // --- Singleton --- static void test_getInstance_returns_same_instance() { TransmitHistory *a = TransmitHistory::getInstance(); TransmitHistory *b = TransmitHistory::getInstance(); TEST_ASSERT_EQUAL_PTR(a, b); } static void test_getInstance_creates_global() { if (transmitHistory) { delete transmitHistory; transmitHistory = nullptr; } TEST_ASSERT_NULL(transmitHistory); TransmitHistory::getInstance(); TEST_ASSERT_NOT_NULL(transmitHistory); } // --- Persistence round-trip (loadFromDisk / saveToDisk) --- static void test_save_and_load_round_trip() { // Set some values transmitHistory->setLastSentToMesh(meshtastic_PortNum_NODEINFO_APP); testDelay(10); transmitHistory->setLastSentToMesh(meshtastic_PortNum_POSITION_APP); uint32_t nodeInfoEpoch = transmitHistory->getLastSentToMeshEpoch(meshtastic_PortNum_NODEINFO_APP); uint32_t positionEpoch = transmitHistory->getLastSentToMeshEpoch(meshtastic_PortNum_POSITION_APP); // Force save transmitHistory->saveToDisk(); // Reset and reload delete transmitHistory; transmitHistory = nullptr; transmitHistory = TransmitHistory::getInstance(); transmitHistory->loadFromDisk(); // Epoch values should be restored (if RTC was available when set) uint32_t restoredNodeInfo = transmitHistory->getLastSentToMeshEpoch(meshtastic_PortNum_NODEINFO_APP); uint32_t restoredPosition = transmitHistory->getLastSentToMeshEpoch(meshtastic_PortNum_POSITION_APP); TEST_ASSERT_EQUAL_UINT32(nodeInfoEpoch, restoredNodeInfo); TEST_ASSERT_EQUAL_UINT32(positionEpoch, restoredPosition); // After loadFromDisk, millis should be seeded (non-zero) for stored entries uint32_t restoredMillis = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP); if (restoredNodeInfo > 0) { // If epoch was stored, millis should be seeded from load TEST_ASSERT_NOT_EQUAL(0, restoredMillis); } } // --- Boot without RTC scenario --- static void test_load_seeds_millis_even_without_rtc() { // This tests the critical crash-reboot scenario: // After loadFromDisk(), even if getTime() returns 0 (no RTC), // lastMillis should be seeded so throttle blocks immediate re-broadcast. transmitHistory->setLastSentToMesh(meshtastic_PortNum_NODEINFO_APP); transmitHistory->saveToDisk(); // Simulate reboot: destroy and recreate delete transmitHistory; transmitHistory = nullptr; transmitHistory = TransmitHistory::getInstance(); transmitHistory->loadFromDisk(); // The key insight: after load, getLastSentToMeshMillis should return non-zero // because loadFromDisk seeds lastMillis[key] = millis() for every loaded entry. // This ensures throttle works even without RTC. uint32_t result = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP); uint32_t epoch = transmitHistory->getLastSentToMeshEpoch(meshtastic_PortNum_NODEINFO_APP); if (epoch > 0) { // Data was persisted — millis must be seeded TEST_ASSERT_NOT_EQUAL(0, result); // And it should cause throttle to block (treating as "just sent") bool withinInterval = Throttle::isWithinTimespanMs(result, 10 * 60 * 1000); TEST_ASSERT_TRUE(withinInterval); } // If epoch == 0, RTC wasn't available — no data was saved, so nothing to restore. // This is expected on platforms without RTC during the very first boot. } void setup() { initializeTestEnvironment(); UNITY_BEGIN(); RUN_TEST(test_setLastSentToMesh_stores_millis); RUN_TEST(test_set_overwrites_previous_value); RUN_TEST(test_throttle_blocks_within_interval); RUN_TEST(test_throttle_allows_after_interval); RUN_TEST(test_throttle_blocks_after_set_then_zero_does_not); RUN_TEST(test_multiple_keys_stored_independently); // Singleton RUN_TEST(test_getInstance_returns_same_instance); RUN_TEST(test_getInstance_creates_global); // Persistence RUN_TEST(test_save_and_load_round_trip); RUN_TEST(test_load_seeds_millis_even_without_rtc); exit(UNITY_END()); } void loop() {} ================================================ FILE: userPrefs.jsonc ================================================ { // "USERPREFS_BUTTON_PIN": "36", // "USERPREFS_CHANNELS_TO_WRITE": "3", // "USERPREFS_CHANNEL_0_DOWNLINK_ENABLED": "false", // "USERPREFS_CHANNEL_0_NAME": "REPLACEME", // "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_CHANNEL_0_UPLINK_ENABLED": "true", // "USERPREFS_CHANNEL_1_DOWNLINK_ENABLED": "false", // "USERPREFS_CHANNEL_1_NAME": "NodeChat", // "USERPREFS_CHANNEL_1_PRECISION": "14", // "USERPREFS_CHANNEL_1_PSK": "{ 0x4e, 0x22, 0x1d, 0x8b, 0xc3, 0x09, 0x1b, 0xe2, 0x11, 0x9c, 0x89, 0x12, 0xf2, 0x25, 0x19, 0x5d, 0x15, 0x3e, 0x30, 0x7b, 0x86, 0xb6, 0xec, 0xc4, 0x6a, 0xc3, 0x96, 0x5e, 0x9e, 0x10, 0x9d, 0xd5 }", // "USERPREFS_CHANNEL_1_UPLINK_ENABLED": "false", // "USERPREFS_CHANNEL_2_DOWNLINK_ENABLED": "false", // "USERPREFS_CHANNEL_2_NAME": "YardSale", // "USERPREFS_CHANNEL_2_PRECISION": "14", // "USERPREFS_CHANNEL_2_PSK": "{ 0x15, 0x6f, 0xfe, 0x46, 0xd4, 0x56, 0x63, 0x8a, 0x54, 0x43, 0x13, 0xf2, 0xef, 0x6c, 0x63, 0x89, 0xf0, 0x06, 0x30, 0x52, 0xce, 0x36, 0x5e, 0xb1, 0xe8, 0xbb, 0x86, 0xe6, 0x26, 0x5b, 0x1d, 0x58 }", // "USERPREFS_CHANNEL_2_UPLINK_ENABLED": "false", // "USERPREFS_CONFIG_GPS_MODE": "meshtastic_Config_PositionConfig_GpsMode_ENABLED", // "USERPREFS_CONFIG_LORA_IGNORE_MQTT": "true", // "USERPREFS_LORA_TX_DISABLED": "1", // If set, forces config.lora.tx_enabled=false during lora bootstrap // "USERPREFS_CONFIG_LORA_REGION": "meshtastic_Config_LoRaConfig_RegionCode_US", // "USERPREFS_CONFIG_OWNER_LONG_NAME": "My Long Name", // "USERPREFS_CONFIG_OWNER_SHORT_NAME": "MLN", // "USERPREFS_CONFIG_DEVICE_ROLE": "meshtastic_Config_DeviceConfig_Role_CLIENT", // Defaults to CLIENT. ROUTER*, and LOST AND FOUND roles are restricted. // "USERPREFS_EVENT_MODE": "1", // "USERPREFS_FIRMWARE_EDITION": "meshtastic_FirmwareEdition_BURNING_MAN", // "USERPREFS_FIXED_BLUETOOTH": "121212", // "USERPREFS_FIXED_GPS": "", // "USERPREFS_FIXED_GPS_ALT": "0", // "USERPREFS_FIXED_GPS_LAT": "48.85873920", // "USERPREFS_FIXED_GPS_LON": "2.294508368", // "USERPREFS_CONFIG_SMART_POSITION_ENABLED": "false", // "USERPREFS_CONFIG_GPS_UPDATE_INTERVAL": "600", // "USERPREFS_CONFIG_POSITION_BROADCAST_INTERVAL": "1800", // "USERPREFS_CONFIG_DEVICE_TELEM_UPDATE_INTERVAL": "900", // Device telemetry update interval in seconds // "USERPREFS_LORACONFIG_CHANNEL_NUM": "31", // "USERPREFS_LORACONFIG_MODEM_PRESET": "meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST", // "USERPREFS_USE_ADMIN_KEY_0": "{ 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 }", // "USERPREFS_USE_ADMIN_KEY_1": "{}", // "USERPREFS_USE_ADMIN_KEY_2": "{}", // "USERPREFS_OEM_TEXT": "Caterham Car Club", // "USERPREFS_OEM_FONT_SIZE": "0", // "USERPREFS_OEM_IMAGE_WIDTH": "50", // "USERPREFS_OEM_IMAGE_HEIGHT": "28", // "USERPREFS_OEM_IMAGE_DATA": "{ 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0x00, 0xC0, 0x07, 0x80, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x18, 0xFF, 0xFF, 0x61, 0x00, 0x00, 0x00, 0x0C, 0xFF, 0xFF, 0xC7, 0x00, 0x00, 0x00, 0x0C, 0xFF, 0xFF, 0xC7, 0x00, 0x00, 0x00, 0x18, 0xFF, 0xFF, 0x67, 0x00, 0x00, 0x00, 0x18, 0x1F, 0xF0, 0x67, 0x00, 0x00, 0x00, 0x30, 0x1F, 0xF8, 0x33, 0x00, 0x00, 0x00, 0x30, 0x00, 0xFC, 0x31, 0x00, 0x00, 0x00, 0x60, 0x00, 0xFE, 0x18, 0x00, 0x00, 0x00, 0x60, 0x00, 0x7E, 0x18, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x3F, 0x0C, 0x00, 0x00, 0x00, 0xC0, 0x80, 0x1F, 0x0C, 0x00, 0x00, 0x00, 0x80, 0x81, 0x1F, 0x06, 0x00, 0x00, 0x00, 0x80, 0xC1, 0x0F, 0x06, 0x00, 0x00, 0x00, 0x00, 0xC3, 0x0F, 0x03, 0x00, 0x00, 0x00, 0x00, 0xC3, 0x0F, 0x03, 0x00, 0x00, 0x00, 0x00, 0xE6, 0x8F, 0x01, 0x00, 0x00, 0x00, 0x00, 0xEE, 0xC7, 0x01, 0x00, 0x00, 0x00, 0x00, 0x0C, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00}", // "USERPREFS_NETWORK_ENABLED_PROTOCOLS": "1", // Enable UDP mesh // "USERPREFS_NETWORK_WIFI_ENABLED": "true", // "USERPREFS_NETWORK_WIFI_SSID": "wifi_ssid", // "USERPREFS_NETWORK_WIFI_PSK": "wifi_psk", // "USERPREFS_MQTT_ENABLED": "1", // "USERPREFS_MQTT_ADDRESS": "'mqtt.meshtastic.org'", // "USERPREFS_MQTT_USERNAME": "meshdev", // "USERPREFS_MQTT_PASSWORD": "large4cats", // "USERPREFS_MQTT_ENCRYPTION_ENABLED": "true", // "USERPREFS_MQTT_TLS_ENABLED": "false", // "USERPREFS_MQTT_ROOT_TOPIC": "event/REPLACEME", // "USERPREFS_RINGTONE_NAG_SECS": "60", // "USERPREFS_NODEINFO_REPLY_SUPPRESS_SECS": "43200", "USERPREFS_RINGTONE_RTTTL": "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", // "USERPREFS_NETWORK_IPV6_ENABLED": "1", "USERPREFS_TZ_STRING": "tzplaceholder " } ================================================ FILE: variants/esp32/betafpv_2400_tx_micro/platformio.ini ================================================ [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/esp32/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} # renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel adafruit/Adafruit NeoPixel@1.15.4 ================================================ FILE: variants/esp32/betafpv_2400_tx_micro/variant.h ================================================ // 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 // 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 ================================================ FILE: variants/esp32/betafpv_900_tx_nano/platformio.ini ================================================ [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/esp32/betafpv_900_tx_nano board_build.f_cpu = 240000000L upload_protocol = esptool ;upload_port = /dev/ttyUSB0 upload_speed = 460800 ================================================ FILE: variants/esp32/betafpv_900_tx_nano/variant.h ================================================ // 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_POWER 16 // green - blue is at 17 #define BUTTON_PIN 25 #define BUTTON_NEED_PULLUP #undef EXT_NOTIFY_OUT ================================================ FILE: variants/esp32/chatter2/platformio.ini ================================================ ; 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/esp32/chatter2 -DMESHTASTIC_EXCLUDE_WEBSERVER=1 -DMESHTASTIC_EXCLUDE_PAXCOUNTER=1 -ULED_BUILTIN lib_deps = ${esp32_base.lib_deps} # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX lovyan03/LovyanGFX@1.2.19 ================================================ FILE: variants/esp32/chatter2/variant.h ================================================ ////////////////////////////////////////////////////////////////////////////////// // // // Have custom connections or functionality? Configure them in this section // // // ////////////////////////////////////////////////////////////////////////////////// // Debugging // #define GPS_DEBUG // 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 // 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 FORCE_LOW_RES 1 #define SCREEN_ROTATE #define SCREEN_TRANSITION_FRAMERATE 5 // fps #define DISPLAY_FORCE_SMALL_FONTS #define TFT_BACKLIGHT_ON LOW #define USE_TFTDISPLAY 1 // 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 OCV_ARRAY 1580, 1400, 1350, 1300, 1280, 1250, 1230, 1190, 1150, 1100, 1000 #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 ///////////////////////////////////////////////////////////////////////////////// // // // 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.) ================================================ FILE: variants/esp32/diy/9m2ibr_aprs_lora_tracker/platformio.ini ================================================ ; 9M2IBR APRS LoRa Tracker: ESP32-WROOM-32 + EBYTE E22-400M30S ; https://shopee.com.my/product/1095224/21692283917 [env:9m2ibr_aprs_lora_tracker] extends = esp32_base board = esp32doit-devkit-v1 board_level = extra build_flags = ${esp32_base.build_flags} -D PRIVATE_HW -D EBYTE_E22 -D EBYTE_E22_900M30S ; Assume Tx power curve is identical to 900M30S as there is no documentation -I variants/esp32/diy/9m2ibr_aprs_lora_tracker -ULED_BUILTIN build_src_filter = ${esp32_base.build_src_filter} +<../variants/esp32/diy/9m2ibr_aprs_lora_tracker> ================================================ FILE: variants/esp32/diy/9m2ibr_aprs_lora_tracker/variant.cpp ================================================ #include "variant.h" #include "Arduino.h" void earlyInitVariant() { pinMode(USER_LED, OUTPUT); digitalWrite(USER_LED, HIGH ^ LED_STATE_ON); } ================================================ FILE: variants/esp32/diy/9m2ibr_aprs_lora_tracker/variant.h ================================================ /* 9M2IBR APRS LoRa Tracker: ESP32-WROOM-32 + EBYTE E22-400M30S https://shopee.com.my/product/1095224/21692283917 Originally developed for LoRa_APRS_iGate and GPIO is similar to https://github.com/richonguzman/LoRa_APRS_iGate/blob/main/variants/ESP32_DIY_1W_LoRa_Mesh_V1_2/board_pinout.h */ // OLED (may be different controllers depending on screen size) #define I2C_SDA 21 #define I2C_SCL 22 #define HAS_SCREEN 1 // Generates randomized BLE pin // GNSS: Ai-Thinker GP-02 BDS/GNSS module #define GPS_RX_PIN 16 #define GPS_TX_PIN 17 // Button #define BUTTON_PIN 15 // Right side button - if not available, set device.button_gpio to 0 from Meshtastic client // LEDs #define LED_POWER 13 // Tx LED #define USER_LED 2 // Rx LED // Buzzer #define PIN_BUZZER 33 // Battery sense #define BATTERY_PIN 35 #define ADC_MULTIPLIER 2.01 // 100k + 100k, and add 1% tolerance #define ADC_CHANNEL ADC1_GPIO35_CHANNEL #define BATTERY_SENSE_RESOLUTION_BITS ADC_RESOLUTION // SPI #define LORA_SCK 18 #define LORA_MISO 19 #define LORA_MOSI 23 // LoRa #define LORA_CS 5 #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 12 // IRQ for SX1262/SX1268 #define LORA_DIO2 RADIOLIB_NC // BUSY for SX1262/SX1268 #define LORA_DIO3 // NC, but used as TCXO supply by E22 module #define LORA_RXEN 32 // RF switch RX (and E22 LNA) control by ESP32 GPIO #define LORA_TXEN 25 // RF switch TX (and E22 PA) control by ESP32 GPIO // 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 // Support alternative modules if soldered in place of E22 #define USE_RF95 // RFM95/SX127x #define USE_SX1262 #define USE_SX1268 #define USE_LLCC68 // E22 TCXO support #ifdef EBYTE_E22 #define SX126X_DIO3_TCXO_VOLTAGE 1.8 #define TCXO_OPTIONAL // make it so that the firmware can try both TCXO and XTAL #endif ================================================ FILE: variants/esp32/diy/dr-dev/platformio.ini ================================================ ; Port to Disaster Radio's ESP32-v3 Dev Board [env:meshtastic-dr-dev] custom_meshtastic_hw_model = 41 custom_meshtastic_hw_model_slug = DR_DEV custom_meshtastic_architecture = esp32 custom_meshtastic_actively_supported = false custom_meshtastic_display_name = DR-DEV custom_meshtastic_tags = DIY extends = esp32_base board = esp32doit-devkit-v1 board_level = extra 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/esp32/diy/dr-dev ================================================ FILE: variants/esp32/diy/dr-dev/variant.h ================================================ // 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 ================================================ FILE: variants/esp32/diy/hydra/platformio.ini ================================================ ; Hydra - Meshtastic DIY v1 hardware with some specific changes [env:hydra] custom_meshtastic_hw_model = 39 custom_meshtastic_hw_model_slug = HYDRA custom_meshtastic_architecture = esp32 custom_meshtastic_actively_supported = true custom_meshtastic_support_level = 3 custom_meshtastic_display_name = Hydra custom_meshtastic_tags = DIY extends = esp32_base board = esp32doit-devkit-v1 build_flags = ${esp32_base.build_flags} -D DIY_V1 -I variants/esp32/diy/hydra -ULED_BUILTIN ================================================ FILE: variants/esp32/diy/hydra/variant.h ================================================ // 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 BUTTON_PIN 39 // The middle button GPIO on the T-Beam // Note: On the ESP32 base version, gpio34-39 are input-only, and do not have internal pull-ups. // If 39 is not being used for a button, it is suggested to remove the #define. #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_POWER 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 #define LORA_TXEN SX126X_TXEN // Compatibility with variant file configuration structure #define LORA_RXEN SX126X_RXEN // Compatibility with variant file configuration structure #define LORA_RESET SX126X_RESET // Compatibility with variant file configuration structure #define LORA_DIO2 SX126X_BUSY // Compatibility with variant file configuration structure ================================================ FILE: variants/esp32/diy/v1/platformio.ini ================================================ ; Meshtastic DIY v1 by Nano VHF Schematic based on ESP32-WROOM-32 (38 pins) devkit & EBYTE E22 SX1262/SX1268 module [env:meshtastic-diy-v1] custom_meshtastic_hw_model = 39 custom_meshtastic_hw_model_slug = DIY_V1 custom_meshtastic_architecture = esp32 custom_meshtastic_actively_supported = true custom_meshtastic_support_level = 3 custom_meshtastic_display_name = DIY V1 custom_meshtastic_images = diy.svg custom_meshtastic_tags = DIY extends = esp32_base board = esp32doit-devkit-v1 board_check = true build_flags = ${esp32_base.build_flags} -D DIY_V1 -D EBYTE_E22 -I variants/esp32/diy/v1 -ULED_BUILTIN ================================================ FILE: variants/esp32/diy/v1/variant.h ================================================ // 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_POWER 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 #define TCXO_OPTIONAL // make it so that the firmware can try both TCXO and XTAL #endif ================================================ FILE: variants/esp32/diy/v1_1/platformio.ini ================================================ ; 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/esp32/diy/v1_1 ================================================ FILE: variants/esp32/diy/v1_1/variant.h ================================================ // 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 #define TCXO_OPTIONAL // make it so that the firmware can try both TCXO and XTAL #endif ================================================ FILE: variants/esp32/esp32-common.ini ================================================ ; Common settings for ESP targets, mixin with extends = esp32_common [esp32_common] extends = arduino_base custom_esp32_kind = custom_mtjson_part = platform = # renovate: datasource=custom.pio depName=platformio/espressif32 packageName=platformio/platform/espressif32 platformio/espressif32@6.13.0 platform_packages = # renovate: datasource=custom.pio depName=platformio/tool-mklittlefs packageName=platformio/tool/tool-mklittlefs platformio/tool-mklittlefs@^1.203.210628 extra_scripts = ${env.extra_scripts} pre:extra_scripts/esp32_pre.py extra_scripts/esp32_extra.py 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 # Keep explicit std unflags on ESP32; base-level unflags are not sufficient # to prevent framework-injected C++11 fallback on this platform. -std=c++11 -std=gnu++11 build_flags = ${arduino_base.build_flags} -flto -Wall -Wextra -Isrc/platform/esp32 -include mbedtls/error.h -std=gnu++17 -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_BT_NIMBLE_MAX_BONDS=6 # default is 3 -DCONFIG_NIMBLE_CPP_LOG_LEVEL=2 -DCONFIG_BT_NIMBLE_MAX_CCCDS=20 -DCONFIG_BT_NIMBLE_HOST_TASK_STACK_SIZE=8192 -DESP_OPENSSL_SUPPRESS_LEGACY_WARNING -DSERIAL_BUFFER_SIZE=4096 -DSERIAL_HAS_ON_RECEIVE -DLIBPAX_ARDUINO -DLIBPAX_WIFI -DLIBPAX_BLE -DHAS_UDP_MULTICAST=1 ;-DDEBUG_HEAP -DCAN_RECLOCK_I2C lib_deps = ${arduino_base.lib_deps} ${networking_base.lib_deps} ${networking_extra.lib_deps} ${environmental_base.lib_deps} ${environmental_extra.lib_deps} ${radiolib_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-esp32_https_server packageName=https://github.com/meshtastic/esp32_https_server gitBranch=master https://github.com/meshtastic/esp32_https_server/archive/b78f12c86ea65c3ca08968840ff554ff7ed69b60.zip # renovate: datasource=custom.pio depName=NimBLE-Arduino packageName=h2zero/library/NimBLE-Arduino h2zero/NimBLE-Arduino@^1.4.3 # renovate: datasource=git-refs depName=libpax packageName=https://github.com/dbinfrago/libpax gitBranch=master https://github.com/dbinfrago/libpax/archive/3cdc0371c375676a97967547f4065607d4c53fd1.zip # renovate: datasource=github-tags depName=XPowersLib packageName=lewisxhe/XPowersLib https://github.com/lewisxhe/XPowersLib/archive/v0.3.3.zip # renovate: datasource=custom.pio depName=rweather/Crypto packageName=rweather/library/Crypto 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 ================================================ FILE: variants/esp32/esp32.ini ================================================ ; Common settings for ESP32 OG (without suffix) ; See 'esp32_common' for common ESP32-family settings [esp32_base] extends = esp32_common custom_esp32_kind = esp32 build_flags = ${esp32_common.build_flags} -DMESHTASTIC_EXCLUDE_AUDIO=1 ; Override lib_deps to use environmental_extra_no_bsec instead of environmental_extra ; BSEC library uses ~3.5KB DRAM which causes overflow on original ESP32 targets lib_deps = ${arduino_base.lib_deps} ${networking_base.lib_deps} ${networking_extra.lib_deps} ${environmental_base.lib_deps} ${environmental_extra_no_bsec.lib_deps} ${radiolib_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-esp32_https_server packageName=https://github.com/meshtastic/esp32_https_server gitBranch=master https://github.com/meshtastic/esp32_https_server/archive/b78f12c86ea65c3ca08968840ff554ff7ed69b60.zip # renovate: datasource=custom.pio depName=NimBLE-Arduino packageName=h2zero/library/NimBLE-Arduino h2zero/NimBLE-Arduino@^1.4.3 # renovate: datasource=git-refs depName=libpax packageName=https://github.com/dbinfrago/libpax gitBranch=master https://github.com/dbinfrago/libpax/archive/3cdc0371c375676a97967547f4065607d4c53fd1.zip # renovate: datasource=github-tags depName=XPowersLib packageName=lewisxhe/XPowersLib https://github.com/lewisxhe/XPowersLib/archive/v0.3.3.zip # renovate: datasource=custom.pio depName=rweather/Crypto packageName=rweather/library/Crypto rweather/Crypto@0.4.0 ================================================ FILE: variants/esp32/hackerboxes_esp32_io/platformio.ini ================================================ [env:hackerboxes-esp32-io] extends = esp32_base board = esp32dev board_level = extra build_flags = ${esp32_base.build_flags} -D PRIVATE_HW -I variants/esp32/hackerboxes_esp32_io monitor_speed = 115200 upload_protocol = esptool ;upload_port = /dev/ttyUSB0 upload_speed = 921600 ================================================ FILE: variants/esp32/hackerboxes_esp32_io/variant.h ================================================ #define BUTTON_PIN 0 // HACKBOX LoRa IO Kit // Uses a ESP-32-WROOM and a RA-01SH (SX1262) LoRa Board #define LED_POWER 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 18 #define LORA_MISO 19 #define LORA_MOSI 23 #define LORA_CS 5 #define LORA_DIO0 RADIOLIB_NC #define LORA_RESET 27 #define LORA_DIO1 33 #define LORA_DIO2 RADIOLIB_NC #define LORA_BUSY 32 #define SX126X_DIO2_AS_RF_SWITCH #define SX126X_CS LORA_CS #define SX126X_DIO1 LORA_DIO1 #define SX126X_BUSY LORA_BUSY #define SX126X_RESET LORA_RESET #define SX126X_MAX_POWER 22 // Max power of the RA-01SH is 22db ================================================ FILE: variants/esp32/heltec_v1/platformio.ini ================================================ [env:heltec-v1] custom_meshtastic_hw_model = 11 custom_meshtastic_hw_model_slug = HELTEC_V1 custom_meshtastic_architecture = esp32 custom_meshtastic_actively_supported = false custom_meshtastic_display_name = Heltec V1 custom_meshtastic_tags = Heltec ;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/esp32/heltec_v1 ================================================ FILE: variants/esp32/heltec_v1/variant.h ================================================ // 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_POWER 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 ================================================ FILE: variants/esp32/heltec_v2/platformio.ini ================================================ [env:heltec-v2_0] custom_meshtastic_hw_model = 5 custom_meshtastic_hw_model_slug = HELTEC_V2_0 custom_meshtastic_architecture = esp32 custom_meshtastic_actively_supported = false custom_meshtastic_display_name = Heltec V2.0 custom_meshtastic_tags = Heltec ;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/esp32/heltec_v2 -ULED_BUILTIN ================================================ FILE: variants/esp32/heltec_v2/variant.h ================================================ // 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_POWER 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 ================================================ FILE: variants/esp32/heltec_v2.1/platformio.ini ================================================ [env:heltec-v2_1] custom_meshtastic_hw_model = 10 custom_meshtastic_hw_model_slug = HELTEC_V2_1 custom_meshtastic_architecture = esp32 custom_meshtastic_actively_supported = false custom_meshtastic_display_name = Heltec V2.1 custom_meshtastic_tags = Heltec 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/esp32/heltec_v2.1 -ULED_BUILTIN ================================================ FILE: variants/esp32/heltec_v2.1/variant.h ================================================ // 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_POWER 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. ================================================ FILE: variants/esp32/heltec_wireless_bridge/platformio.ini ================================================ [env:heltec-wireless-bridge] ;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} -I variants/esp32/heltec_wireless_bridge -D HELTEC_WIRELESS_BRIDGE -D BOARD_HAS_PSRAM -D RADIOLIB_EXCLUDE_LR11X0=1 -D RADIOLIB_EXCLUDE_SX128X=1 -D MESHTASTIC_EXCLUDE_CANNEDMESSAGES=1 -D MESHTASTIC_EXCLUDE_DETECTIONSENSOR=1 -D MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR=1 -D MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR=1 -D MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR_EXTERNAL=1 -D MESHTASTIC_EXCLUDE_EXTERNALNOTIFICATION=1 -D MESHTASTIC_EXCLUDE_GPS=1 -D MESHTASTIC_EXCLUDE_I2C=1 -D MESHTASTIC_EXCLUDE_INPUTBROKER=1 -D MESHTASTIC_EXCLUDE_POWER_FSM=1 -D MESHTASTIC_EXCLUDE_SERIAL=1 -D MESHTASTIC_EXCLUDE_SCREEN=1 -D MESHTASTIC_EXCLUDE_WAYPOINT=1 ================================================ FILE: variants/esp32/heltec_wireless_bridge/variant.h ================================================ // updated variant 20250420 berlincount, tested with HTIT-TB // // connections in HTIT-WB // per https://www.espressif.com/sites/default/files/documentation/esp32_datasheet_en.pdf // md5: a0e6ae10ff76611aa61433366b2e4f5c esp32_datasheet_en.pdf // per https://resource.heltec.cn/download/Wireless_Bridge/Schematic_Diagram_HTIT-WB_V0.2.pdf // md5: d5c1b0219ece347dd8cee866d7d3ab0a Schematic_Diagram_HTIT-WB_V0.2.pdf #define NO_EXT_GPIO 1 #define NO_GPS 1 #define HAS_GPS 0 // GPS is not equipped #undef GPS_RX_PIN #undef GPS_TX_PIN // Green / Lora = PIN 22 / GPIO2, Yellow / Wifi = PIN 23 / GPIO0, Blue / BLE = PIN 25 / GPIO16 #define LED_POWER 22 #define WIFI_LED 23 #define BLE_LED 25 // ESP32-D0WDQ6 direct pins SX1276 #define USE_RF95 #define LORA_DIO0 26 #define LORA_DIO1 35 #define LORA_DIO2 34 #define LORA_SCK 05 #define LORA_MISO 19 #define LORA_MOSI 27 #define LORA_CS 18 // several things are not possible with JTAG enabled #ifndef USE_JTAG #define LORA_RESET 14 // LoRa Reset shares a pin with MTMS #define I2C_SDA 4 // SD_DATA1 going to W25Q64, but #define I2C_SCL 15 // SD_CMD shared a pin with MTD0 #endif // user button is present on device, but currently untested & unconfigured - couldn't figure out how it's connected // battery support is present within device, but currently untested & unconfigured - couldn't find reliable information yet ================================================ FILE: variants/esp32/heltec_wsl_v2.1/platformio.ini ================================================ [env:heltec-wsl-v2_1] extends = esp32_base board = heltec_wireless_stick_lite board_level = extra build_flags = ${esp32_base.build_flags} -D PRIVATE_HW -I variants/esp32/heltec_wsl_v2.1 ================================================ FILE: variants/esp32/heltec_wsl_v2.1/variant.h ================================================ #define I2C_SCL SCL #define I2C_SDA SDA #define LED_POWER LED // active low, powers the Battery reader, but no lora antenna boost (?) // #define VEXT_ENABLE Vext // #define VEXT_ON_VALUE LOW #define BUTTON_PIN 0 #define ADC_CTRL 21 #define ADC_CTRL_ENABLED LOW #define BATTERY_PIN 37 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage #define ADC_CHANNEL ADC1_CHANNEL_1 // ratio of voltage divider = 3.20 (R1=100k, R2=220k) #define ADC_MULTIPLIER 3.2 #define USE_RF95 // RFM95/SX127x #define LORA_DIO0 26 #define LORA_RESET 14 #define LORA_DIO1 35 #define LORA_DIO2 34 #define LORA_SCK 5 #define LORA_MISO 19 #define LORA_MOSI 27 #define LORA_CS 18 ================================================ FILE: variants/esp32/m5stack_core/pins_arduino.h ================================================ #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 */ ================================================ FILE: variants/esp32/m5stack_core/platformio.ini ================================================ [env:m5stack-core] custom_meshtastic_hw_model = 42 custom_meshtastic_hw_model_slug = M5STACK custom_meshtastic_architecture = esp32 custom_meshtastic_actively_supported = true custom_meshtastic_support_level = 3 custom_meshtastic_display_name = M5 Stack custom_meshtastic_tags = M5Stack 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/esp32/m5stack_core -DM5STACK -DMESHTASTIC_EXCLUDE_WEBSERVER=1 -DMESHTASTIC_EXCLUDE_PAXCOUNTER=1 -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 lib_ignore = m5stack-core lib_deps = ${esp32_base.lib_deps} # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX lovyan03/LovyanGFX@1.2.19 ================================================ FILE: variants/esp32/m5stack_core/variant.h ================================================ // #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 ILI9341_DRIVER #define TFT_HEIGHT 240 #define TFT_WIDTH 320 #define TFT_OFFSET_X 0 #define TFT_OFFSET_Y 0 #define TFT_BUSY -1 #define USE_TFTDISPLAY 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 ================================================ FILE: variants/esp32/m5stack_coreink/pins_arduino.h ================================================ #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 */ ================================================ FILE: variants/esp32/m5stack_coreink/platformio.ini ================================================ [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/esp32/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} # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 zinggjm/GxEPD2@1.6.8 # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib lewisxhe/SensorLib@0.3.4 lib_ignore = m5stack-coreink monitor_filters = esp32_exception_decoder board_build.f_cpu = 240000000L upload_protocol = esptool upload_port = /dev/ttyACM0 ================================================ FILE: variants/esp32/m5stack_coreink/variant.h ================================================ // 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_POWER 10 // PCF8563 RTC Module #define PCF8563_RTC 0x51 // 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/ ================================================ FILE: variants/esp32/nano-g1/platformio.ini ================================================ ; The 1.0 release of the nano-g1 board [env:nano-g1] custom_meshtastic_hw_model = 14 custom_meshtastic_hw_model_slug = NANO_G1 custom_meshtastic_architecture = esp32 custom_meshtastic_actively_supported = true custom_meshtastic_support_level = 3 custom_meshtastic_display_name = Nano G1 custom_meshtastic_tags = B&Q extends = esp32_base board = ttgo-t-beam build_flags = ${esp32_base.build_flags} -D NANO_G1 -I variants/esp32/nano-g1 -ULED_BUILTIN ================================================ FILE: variants/esp32/nano-g1/variant.h ================================================ // #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 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 ================================================ FILE: variants/esp32/nano-g1-explorer/platformio.ini ================================================ ; The 1.0 release of the nano-g1-explorer board [env:nano-g1-explorer] custom_meshtastic_hw_model = 17 custom_meshtastic_hw_model_slug = NANO_G1_EXPLORER custom_meshtastic_architecture = esp32 custom_meshtastic_actively_supported = true custom_meshtastic_support_level = 3 custom_meshtastic_display_name = Nano G1 Explorer custom_meshtastic_tags = B&Q extends = esp32_base board = ttgo-t-beam build_flags = ${esp32_base.build_flags} -D NANO_G1_EXPLORER -I variants/esp32/nano-g1-explorer -ULED_BUILTIN ================================================ FILE: variants/esp32/nano-g1-explorer/variant.h ================================================ // #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 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 ================================================ FILE: variants/esp32/radiomaster_900_bandit/platformio.ini ================================================ [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 -DHAS_STK8XXX=1 -O2 -I variants/esp32/radiomaster_900_bandit -ULED_BUILTIN board_build.f_cpu = 240000000L upload_protocol = esptool lib_deps = ${esp32_base.lib_deps} # renovate: datasource=github-tags depName=STK8xxx-Accelerometer packageName=gjelsoe/STK8xxx-Accelerometer https://github.com/gjelsoe/STK8xxx-Accelerometer/archive/v0.1.1.zip ================================================ FILE: variants/esp32/radiomaster_900_bandit/variant.h ================================================ /* 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 ================================================ FILE: variants/esp32/radiomaster_900_bandit_micro/platformio.ini ================================================ ; ; 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 -I variants/esp32/radiomaster_900_bandit_nano -ULED_BUILTIN board_build.f_cpu = 240000000L upload_protocol = esptool ================================================ FILE: variants/esp32/radiomaster_900_bandit_nano/platformio.ini ================================================ [env:radiomaster_900_bandit_nano] custom_meshtastic_hw_model = 64 custom_meshtastic_hw_model_slug = RADIOMASTER_900_BANDIT_NANO custom_meshtastic_architecture = esp32 custom_meshtastic_actively_supported = true custom_meshtastic_support_level = 2 custom_meshtastic_display_name = RadioMaster 900 Bandit Nano custom_meshtastic_tags = RadioMaster 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 -I variants/esp32/radiomaster_900_bandit_nano -ULED_BUILTIN board_build.f_cpu = 240000000L upload_protocol = esptool ================================================ FILE: variants/esp32/radiomaster_900_bandit_nano/variant.h ================================================ /* 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_POWER 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 ================================================ FILE: variants/esp32/rak11200/pins_arduino.h ================================================ #ifndef Pins_Arduino_h #define Pins_Arduino_h #include #define LED_GREEN 12 #define LED_BLUE 2 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 */ ================================================ FILE: variants/esp32/rak11200/platformio.ini ================================================ [env:rak11200] custom_meshtastic_hw_model = 13 custom_meshtastic_hw_model_slug = RAK11200 custom_meshtastic_architecture = esp32 custom_meshtastic_actively_supported = true custom_meshtastic_support_level = 3 custom_meshtastic_display_name = RAK WisBlock 11200 custom_meshtastic_images = rak11200.svg custom_meshtastic_tags = RAK extends = esp32_base board = wiscore_rak11200 board_level = pr board_check = true build_flags = ${esp32_base.build_flags} -D RAK_11200 -I variants/esp32/rak11200 -DMESHTASTIC_EXCLUDE_WEBSERVER=1 -DMESHTASTIC_EXCLUDE_PAXCOUNTER=1 -DMESHTASTIC_EXCLUDE_RANGETEST=1 -DMESHTASTIC_EXCLUDE_MQTT=1 upload_speed = 115200 ================================================ FILE: variants/esp32/rak11200/variant.h ================================================ #ifndef Pins_Arduino_h #define Pins_Arduino_h #include #define LED_GREEN 12 #define LED_BLUE 2 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_POWER 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 ================================================ FILE: variants/esp32/station-g1/platformio.ini ================================================ ; The 1.0 release of the nano-g1 board [env:station-g1] custom_meshtastic_hw_model = 25 custom_meshtastic_hw_model_slug = STATION_G1 custom_meshtastic_architecture = esp32 custom_meshtastic_actively_supported = true custom_meshtastic_support_level = 3 custom_meshtastic_display_name = Station G1 custom_meshtastic_tags = B&Q extends = esp32_base board = ttgo-t-beam build_flags = ${esp32_base.build_flags} -D STATION_G1 -I variants/esp32/station-g1 -ULED_BUILTIN ================================================ FILE: variants/esp32/station-g1/variant.h ================================================ // #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 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 ================================================ FILE: variants/esp32/tbeam/platformio.ini ================================================ ; The 1.0 release of the TBEAM board [env:tbeam] custom_meshtastic_hw_model = 4 custom_meshtastic_hw_model_slug = TBEAM custom_meshtastic_architecture = esp32 custom_meshtastic_actively_supported = true custom_meshtastic_support_level = 3 custom_meshtastic_display_name = LILYGO T-Beam custom_meshtastic_images = tbeam.svg custom_meshtastic_tags = LilyGo extends = esp32_base board = ttgo-t-beam board_check = true build_flags = ${esp32_base.build_flags} -D TBEAM_V10 -I variants/esp32/tbeam -DBOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue -ULED_BUILTIN upload_speed = 921600 [env:tbeam-displayshield] extends = env:tbeam board_level = extra build_flags = ${env:tbeam.build_flags} -D USE_ST7796 lib_deps = ${env:tbeam.lib_deps} # renovate: datasource=github-tags depName=meshtastic-st7796 packageName=meshtastic/st7796 https://github.com/meshtastic/st7796/archive/1.0.5.zip # renovate: datasource=custom.pio depName=lewisxhe-SensorLib packageName=lewisxhe/library/SensorLib lewisxhe/SensorLib@0.3.4 ================================================ FILE: variants/esp32/tbeam/variant.h ================================================ // #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_ACTIVE_LOW true #define BUTTON_ACTIVE_PULLUP true #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_POWER 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 // Used when the display shield is chosen #ifdef USE_ST7796 #undef EXT_NOTIFY_OUT #undef LED_STATE_ON #undef LED_POWER #define HAS_CST226SE 1 #define HAS_TOUCHSCREEN 1 // #define TOUCH_IRQ 35 // broken in this version of the lib 0.3.1 #ifndef TOUCH_IRQ #define TOUCH_IRQ -1 #endif #define USE_VIRTUAL_KEYBOARD 1 #define ST7796_NSS 25 #define ST7796_RS 13 // DC #define ST7796_SDA 14 // MOSI #define ST7796_SCK 15 #define ST7796_RESET 2 #define ST7796_MISO -1 #define ST7796_BUSY -1 #define VTFT_LEDA 4 #define TFT_SPI_FREQUENCY 60000000 #define TFT_HEIGHT 222 #define TFT_WIDTH 480 #define BRIGHTNESS_DEFAULT 100 // Medium Low Brightness #define SCREEN_TRANSITION_FRAMERATE 5 // fps #endif ================================================ FILE: variants/esp32/tbeam_v07/platformio.ini ================================================ ; The original TBEAM board without the AXP power chip and a few other changes [env:tbeam0_7] custom_meshtastic_hw_model = 6 custom_meshtastic_hw_model_slug = TBEAM_V0P7 custom_meshtastic_architecture = esp32 custom_meshtastic_actively_supported = false custom_meshtastic_display_name = LILYGO T-Beam V0.7 custom_meshtastic_tags = LilyGo board_level = extra extends = esp32_base board = ttgo-t-beam build_flags = ${esp32_base.build_flags} -D TBEAM_V07 -I variants/esp32/tbeam_v07 ================================================ FILE: variants/esp32/tbeam_v07/variant.h ================================================ // #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 ================================================ FILE: variants/esp32/tlora_v1/platformio.ini ================================================ [env:tlora-v1] custom_meshtastic_hw_model = 2 custom_meshtastic_hw_model_slug = TLORA_V1 custom_meshtastic_architecture = esp32 custom_meshtastic_actively_supported = false custom_meshtastic_display_name = LILYGO T-LoRa V1 custom_meshtastic_tags = LilyGo board_level = extra extends = esp32_base board = ttgo-lora32-v1 build_flags = ${esp32_base.build_flags} -D TLORA_V1 -I variants/esp32/tlora_v1 upload_speed = 115200 ================================================ FILE: variants/esp32/tlora_v1/variant.h ================================================ #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_POWER 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 ================================================ FILE: variants/esp32/tlora_v1_3/platformio.ini ================================================ [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/esp32/tlora_v1_3 upload_speed = 115200 ================================================ FILE: variants/esp32/tlora_v1_3/variant.h ================================================ #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_POWER 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 ================================================ FILE: variants/esp32/tlora_v2/platformio.ini ================================================ [env:tlora-v2] custom_meshtastic_hw_model = 1 custom_meshtastic_hw_model_slug = TLORA_V2 custom_meshtastic_architecture = esp32 custom_meshtastic_actively_supported = false custom_meshtastic_display_name = LILYGO T-LoRa V2 custom_meshtastic_tags = LilyGo board_level = extra extends = esp32_base board = ttgo-lora32-v1 build_flags = ${esp32_base.build_flags} -D TLORA_V2 -I variants/esp32/tlora_v2 ================================================ FILE: variants/esp32/tlora_v2/variant.h ================================================ #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_POWER 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 ================================================ FILE: variants/esp32/tlora_v2_1_16/platformio.ini ================================================ [env:tlora-v2-1-1_6] custom_meshtastic_hw_model = 3 custom_meshtastic_hw_model_slug = TLORA_V2_1_1P6 custom_meshtastic_architecture = esp32 custom_meshtastic_actively_supported = true custom_meshtastic_support_level = 3 custom_meshtastic_display_name = LILYGO T-LoRa V2.1-1.6 custom_meshtastic_images = tlora-v2-1-1_6.svg custom_meshtastic_tags = LilyGo extends = esp32_base board = ttgo-lora32-v21 board_check = true build_flags = ${esp32_base.build_flags} -D TLORA_V2_1_16 -I variants/esp32/tlora_v2_1_16 -ULED_BUILTIN upload_speed = 115200 [env:sugarcube] extends = env:tlora-v2-1-1_6 board_level = extra build_flags = ${env:tlora-v2-1-1_6.build_flags} -DBUTTON_PIN=0 -DPIN_BUZZER=25 -DLED_POWER=-1 ================================================ FILE: variants/esp32/tlora_v2_1_16/variant.h ================================================ #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 #if defined(LED_POWER) && LED_POWER == -1 #undef LED_POWER #else #define LED_POWER 25 // If defined we will blink this LED #endif #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 ================================================ FILE: variants/esp32/tlora_v2_1_16_tcxo/platformio.ini ================================================ [env:tlora-v2-1-1_6-tcxo] extends = esp32_base board_level = extra board = ttgo-lora32-v21 build_flags = ${esp32_base.build_flags} -D TLORA_V2_1_16 -I variants/esp32/tlora_v2_1_16 -D LORA_TCXO_GPIO=33 -ULED_BUILTIN upload_speed = 115200 ================================================ FILE: variants/esp32/tlora_v2_1_18/platformio.ini ================================================ [env:tlora-v2-1-1_8] custom_meshtastic_hw_model = 15 custom_meshtastic_hw_model_slug = TLORA_V2_1_1P8 custom_meshtastic_architecture = esp32 custom_meshtastic_actively_supported = true custom_meshtastic_support_level = 3 custom_meshtastic_display_name = LILYGO T-LoRa V2.1-1.8 custom_meshtastic_images = tlora-v2-1-1_8.svg custom_meshtastic_tags = LilyGo, 2.4GHz extends = esp32_base board_level = extra board = ttgo-lora32-v21 build_flags = ${esp32_base.build_flags} -D TLORA_V2_1_18 -I variants/esp32/tlora_v2_1_18 ================================================ FILE: variants/esp32/tlora_v2_1_18/variant.h ================================================ #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_POWER 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 ================================================ FILE: variants/esp32/tlora_v3_3_0_tcxo/platformio.ini ================================================ [env:tlora-v3-3-0-tcxo] extends = esp32_base board = ttgo-lora32-v21 build_flags = ${esp32_base.build_flags} -D TLORA_V2_1_16 -I variants/esp32/tlora_v2_1_16 -D LORA_TCXO_GPIO=12 -D BUTTON_PIN=0 -ULED_BUILTIN ================================================ FILE: variants/esp32/trackerd/platformio.ini ================================================ [env:trackerd] extends = esp32_base board_level = extra board = pico32 board_build.f_flash = 80000000L build_flags = ${esp32_base.build_flags} -D PRIVATE_HW -I variants/esp32/trackerd -D BSFILE=\"boards/dragino_lbt2.h\" ;board_build.partitions = no_ota.csv ================================================ FILE: variants/esp32/trackerd/variant.h ================================================ // 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_POWER 13 // 13 red, 2 blue, 15 red #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) ================================================ FILE: variants/esp32/wiphone/pins_arduino.h ================================================ #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 */ ================================================ FILE: variants/esp32/wiphone/platformio.ini ================================================ [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/esp32/wiphone lib_deps = ${esp32_base.lib_deps} # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX lovyan03/LovyanGFX@1.2.19 # renovate: datasource=custom.pio depName=SX1509 IO Expander packageName=sparkfun/library/SX1509 IO Expander sparkfun/SX1509 IO Expander@3.0.6 # renovate: datasource=custom.pio depName=APA102 packageName=pololu/library/APA102 pololu/APA102@3.0.0 ================================================ FILE: variants/esp32/wiphone/variant.h ================================================ #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 1 #define HAS_GPS 0 #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 #define USE_TFTDISPLAY 1 // I don't have a 'wiphone' but this I think should not be defined this way (don't set TFT_BL if we don't have a hw way to control // it) // #define ST7789_BL -1 // EXTENDER_PIN(9) #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 ================================================ FILE: variants/esp32c3/ai-c3/platformio.ini ================================================ [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/esp32c3/ai-c3 ================================================ FILE: variants/esp32c3/ai-c3/variant.h ================================================ #define SDA 0 #define SCL 1 #define I2C_SDA SDA #define I2C_SCL SCL #define BUTTON_PIN 9 // BOOT button #define LED_POWER 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 ================================================ FILE: variants/esp32c3/diy/esp32c3_super_mini/pins_arduino.h ================================================ #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 */ ================================================ FILE: variants/esp32c3/diy/esp32c3_super_mini/platformio.ini ================================================ ; ESP32 C3 Super Mini Development Board ; https://www.espboards.dev/esp32/esp32-c3-super-mini/ [env:esp32c3_super_mini] extends = esp32c3_base board = esp32-c3-devkitm-1 build_flags = ${esp32c3_base.build_flags} -D PRIVATE_HW -I variants/esp32c3/diy/esp32c3_super_mini -D ARDUINO_USB_MODE=1 -D ARDUINO_USB_CDC_ON_BOOT=1 board_level = extra ================================================ FILE: variants/esp32c3/diy/esp32c3_super_mini/variant.h ================================================ #ifndef _VARIANT_ESP32C3_SUPER_MINI_ #define _VARIANT_ESP32C3_SUPER_MINI_ /*---------------------------------------------------------------------------- * Headers *----------------------------------------------------------------------------*/ #ifdef __cplusplus extern "C" { #endif // __cplusplus // I2C (Wire) & OLED #define WIRE_INTERFACES_COUNT (1) #define I2C_SDA (1) #define I2C_SCL (0) #define USE_SSD1306 // GPS #undef GPS_RX_PIN #undef GPS_TX_PIN #define GPS_RX_PIN (20) #define GPS_TX_PIN (21) // Button #define BUTTON_PIN (9) // BOOT button // LoRa #define USE_LLCC68 #define USE_SX1262 // #define USE_RF95 #define USE_SX1268 #define LORA_DIO0 RADIOLIB_NC #define LORA_RESET (5) #define LORA_DIO1 (3) #define LORA_RXEN (2) #define LORA_BUSY (4) #define LORA_SCK (10) #define LORA_MISO (6) #define LORA_MOSI (7) #define LORA_CS (8) #define SX126X_CS LORA_CS #define SX126X_DIO1 LORA_DIO1 #define SX126X_BUSY LORA_BUSY #define SX126X_RESET LORA_RESET #define SX126X_RXEN LORA_RXEN #define SX126X_DIO3_TCXO_VOLTAGE (1.8) #define TCXO_OPTIONAL // make it so that the firmware can try both TCXO and XTAL #ifdef __cplusplus } #endif /*---------------------------------------------------------------------------- * Arduino objects - C++ only *----------------------------------------------------------------------------*/ #endif ================================================ FILE: variants/esp32c3/esp32c3.ini ================================================ [esp32c3_base] extends = esp32_common custom_esp32_kind = esp32c3 monitor_speed = 115200 monitor_filters = esp32_c3_exception_decoder lib_deps = ${esp32_common.lib_deps} # renovate: datasource=git-refs depName=meshtastic-ESP32_Codec2 packageName=https://github.com/meshtastic/ESP32_Codec2 gitBranch=master https://github.com/meshtastic/ESP32_Codec2/archive/633326c78ac251c059ab3a8c430fcdf25b41672f.zip ================================================ FILE: variants/esp32c3/hackerboxes_esp32c3_oled/platformio.ini ================================================ [env:hackerboxes-esp32c3-oled] extends = esp32c3_base board = esp32-c3-devkitm-1 board_level = extra build_flags = ${esp32c3_base.build_flags} -D PRIVATE_HW -D ARDUINO_USB_MODE=1 -D ARDUINO_USB_CDC_ON_BOOT=1 -I variants/esp32c3/hackerboxes_esp32c3_oled monitor_speed = 115200 upload_protocol = esptool ;upload_port = /dev/ttyUSB0 upload_speed = 921600 ================================================ FILE: variants/esp32c3/hackerboxes_esp32c3_oled/variant.h ================================================ #define BUTTON_PIN 9 // Hackerboxes LoRa ESP32-C3 OLED Kit // Uses a ESP32-C3 OLED Board and a RA-01SH (SX1262) LoRa Board #define LED_POWER 8 // 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_SSD1306_72_40 // #define I2C_SDA 5 // I2C pins for this board // #define I2C_SCL 6 // // #define TFT_WIDTH 72 // #define TFT_HEIGHT 40 #define USE_SX1262 #define LORA_SCK 4 #define LORA_MISO 7 #define LORA_MOSI 3 #define LORA_CS 1 #define LORA_DIO0 RADIOLIB_NC #define LORA_RESET 0 #define LORA_DIO1 20 #define LORA_DIO2 RADIOLIB_NC #define LORA_BUSY 10 #define SX126X_DIO2_AS_RF_SWITCH #define SX126X_CS LORA_CS #define SX126X_DIO1 LORA_DIO1 #define SX126X_BUSY LORA_BUSY #define SX126X_RESET LORA_RESET #define SX126X_MAX_POWER 22 // Max power of the RA-01SH is 22db ================================================ FILE: variants/esp32c3/heltec_esp32c3/pins_arduino.h ================================================ #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 */ ================================================ FILE: variants/esp32c3/heltec_esp32c3/platformio.ini ================================================ [env:heltec-ht62-esp32c3-sx1262] custom_meshtastic_hw_model = 53 custom_meshtastic_hw_model_slug = HELTEC_HT62 custom_meshtastic_architecture = esp32-c3 custom_meshtastic_actively_supported = true custom_meshtastic_support_level = 1 custom_meshtastic_display_name = Heltec HT62 custom_meshtastic_images = heltec-ht62-esp32c3-sx1262.svg custom_meshtastic_tags = Heltec extends = esp32c3_base board = esp32-c3-devkitm-1 board_level = pr build_flags = ${esp32c3_base.build_flags} -D HELTEC_HT62 -I variants/esp32c3/heltec_esp32c3 monitor_speed = 115200 upload_protocol = esptool ;upload_port = /dev/ttyUSB0 upload_speed = 921600 ================================================ FILE: variants/esp32c3/heltec_esp32c3/variant.h ================================================ #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_POWER 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 ================================================ FILE: variants/esp32c3/heltec_hru_3601/pins_arduino.h ================================================ #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 */ ================================================ FILE: variants/esp32c3/heltec_hru_3601/platformio.ini ================================================ [env:heltec-hru-3601] extends = esp32c3_base board = adafruit_qtpy_esp32c3 build_flags = ${esp32c3_base.build_flags} -D HELTEC_HRU_3601 -I variants/esp32c3/heltec_hru_3601 lib_deps = ${esp32c3_base.lib_deps} # renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel adafruit/Adafruit NeoPixel@1.15.4 ================================================ FILE: variants/esp32c3/heltec_hru_3601/variant.h ================================================ #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 ================================================ FILE: variants/esp32c3/m5stack-stamp-c3/pins_arduino.h ================================================ #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 */ ================================================ FILE: variants/esp32c3/m5stack-stamp-c3/platformio.ini ================================================ [env:m5stack-stamp-c3] extends = esp32c3_base board = esp32-c3-devkitm-1 board_level = extra build_flags = ${esp32c3_base.build_flags} -D PRIVATE_HW -I variants/esp32c3/m5stack-stamp-c3 monitor_speed = 115200 upload_protocol = esptool ;upload_port = /dev/ttyACM2 upload_speed = 921600 ================================================ FILE: variants/esp32c3/m5stack-stamp-c3/variant.h ================================================ #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 ================================================ FILE: variants/esp32c6/esp32c6.ini ================================================ [esp32c6_base] extends = esp32_common platform = # Do not renovate until we have switched to pioarduino tagged builds https://github.com/Jason2866/platform-espressif32/archive/22faa566df8c789000f8136cd8d0aca49617af55.zip build_flags = ${arduino_base.build_flags} -Wall -Wextra -Isrc/platform/esp32 -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} ${environmental_extra.lib_deps} ${radiolib_base.lib_deps} # renovate: datasource=custom.pio depName=XPowersLib packageName=lewisxhe/library/XPowersLib lewisxhe/XPowersLib@0.3.3 # renovate: datasource=git-refs depName=meshtastic-ESP32_Codec2 packageName=https://github.com/meshtastic/ESP32_Codec2 gitBranch=master https://github.com/meshtastic/ESP32_Codec2/archive/633326c78ac251c059ab3a8c430fcdf25b41672f.zip # renovate: datasource=custom.pio depName=rweather/Crypto packageName=rweather/library/Crypto rweather/Crypto@0.4.0 build_src_filter = ${esp32_common.build_src_filter} - monitor_speed = 460800 monitor_filters = esp32_c3_exception_decoder lib_ignore = ${esp32_common.lib_ignore} NonBlockingRTTTL NimBLE-Arduino libpax ================================================ FILE: variants/esp32c6/m5stack_unitc6l/pins_arduino.h ================================================ #ifndef Pins_Arduino_h #define Pins_Arduino_h #include #define USB_VID 0x2886 #define USB_PID 0x0048 static const uint8_t TX = 16; static const uint8_t RX = 17; static const uint8_t SDA = 10; static const uint8_t SCL = 8; // Default SPI will be mapped to Radio static const uint8_t MISO = 22; static const uint8_t SCK = 20; static const uint8_t MOSI = 21; static const uint8_t SS = 6; // #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 */ ================================================ FILE: variants/esp32c6/m5stack_unitc6l/platformio.ini ================================================ [env:m5stack-unitc6l] custom_meshtastic_hw_model = 111 custom_meshtastic_hw_model_slug = M5STACK_C6L custom_meshtastic_architecture = esp32-c6 custom_meshtastic_actively_supported = true custom_meshtastic_support_level = 1 custom_meshtastic_display_name = M5Stack Unit C6L custom_meshtastic_images = m5_c6l.svg custom_meshtastic_tags = M5Stack extends = esp32c6_base board = esp32-c6-devkitc-1 board_upload.flash_size = 16MB board_build.partitions = default_16MB.csv ;OpenOCD flash method ;upload_protocol = esp-builtin ;Normal method upload_protocol = esptool ;upload_port = /dev/ttyACM2 build_unflags = -D HAS_BLUETOOTH -D MESHTASTIC_EXCLUDE_BLUETOOTH -D HAS_WIFI lib_deps = ${esp32c6_base.lib_deps} # renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel adafruit/Adafruit NeoPixel@1.15.4 # renovate: datasource=custom.pio depName=NimBLE-Arduino packageName=h2zero/library/NimBLE-Arduino h2zero/NimBLE-Arduino@2.3.7 build_flags = ${esp32c6_base.build_flags} -D M5STACK_UNITC6L -I variants/esp32c6/m5stack_unitc6l -DMESHTASTIC_EXCLUDE_PAXCOUNTER=1 -DARDUINO_USB_CDC_ON_BOOT=1 -DARDUINO_USB_MODE=1 -D HAS_BLUETOOTH=1 -DCONFIG_BT_NIMBLE_EXT_ADV=1 -DCONFIG_BT_NIMBLE_MAX_EXT_ADV_INSTANCES=2 -D NIMBLE_TWO monitor_speed=115200 lib_ignore = NonBlockingRTTTL libpax build_src_filter = ${esp32c6_base.build_src_filter} +<../variants/esp32c6/m5stack_unitc6l> ================================================ FILE: variants/esp32c6/m5stack_unitc6l/variant.cpp ================================================ #include "driver/gpio.h" #include #include // I2C device addr #define PI4IO_M_ADDR 0x43 // PI4IO registers #define PI4IO_REG_CHIP_RESET 0x01 #define PI4IO_REG_IO_DIR 0x03 #define PI4IO_REG_OUT_SET 0x05 #define PI4IO_REG_OUT_H_IM 0x07 #define PI4IO_REG_IN_DEF_STA 0x09 #define PI4IO_REG_PULL_EN 0x0B #define PI4IO_REG_PULL_SEL 0x0D #define PI4IO_REG_IN_STA 0x0F #define PI4IO_REG_INT_MASK 0x11 #define PI4IO_REG_IRQ_STA 0x13 // PI4IO #define setbit(x, y) x |= (0x01 << y) #define clrbit(x, y) x &= ~(0x01 << y) #define reversebit(x, y) x ^= (0x01 << y) #define getbit(x, y) ((x) >> (y)&0x01) void i2c_read_byte(uint8_t addr, uint8_t reg, uint8_t *value) { Wire.beginTransmission(addr); Wire.write(reg); Wire.endTransmission(); Wire.requestFrom(addr, 1); *value = Wire.read(); } /*******************************************************************/ void i2c_write_byte(uint8_t addr, uint8_t reg, uint8_t value) { Wire.beginTransmission(addr); Wire.write(reg); Wire.write(value); Wire.endTransmission(); } /*******************************************************************/ void c6l_init() { // P7 LoRa Reset // P6 RF Switch // P5 LNA Enable printf("pi4io_init\n"); uint8_t in_data; i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_CHIP_RESET, 0xFF); vTaskDelay(10 / portTICK_PERIOD_MS); i2c_read_byte(PI4IO_M_ADDR, PI4IO_REG_CHIP_RESET, &in_data); vTaskDelay(10 / portTICK_PERIOD_MS); i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_IO_DIR, 0b11000000); // 0: input 1: output vTaskDelay(10 / portTICK_PERIOD_MS); i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_OUT_H_IM, 0b00111100); // 使用到的引脚关闭High-Impedance vTaskDelay(10 / portTICK_PERIOD_MS); i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_PULL_SEL, 0b11000011); // pull up/down select, 0 down, 1 up vTaskDelay(10 / portTICK_PERIOD_MS); i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_PULL_EN, 0b11000011); // pull up/down enable, 0 disable, 1 enable vTaskDelay(10 / portTICK_PERIOD_MS); i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_IN_DEF_STA, 0b00000011); // P0 P1 默认高电平, 按键按下触发中断 vTaskDelay(10 / portTICK_PERIOD_MS); i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_INT_MASK, 0b11111100); // P0 P1 中断使能 0 enable, 1 disable vTaskDelay(10 / portTICK_PERIOD_MS); i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_OUT_SET, 0b10000000); // 默认输出为0 vTaskDelay(10 / portTICK_PERIOD_MS); i2c_read_byte(PI4IO_M_ADDR, PI4IO_REG_IRQ_STA, &in_data); // 读取IRQ_STA清除标志 i2c_read_byte(PI4IO_M_ADDR, PI4IO_REG_OUT_SET, &in_data); setbit(in_data, 6); // HIGH i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_OUT_SET, in_data); } ================================================ FILE: variants/esp32c6/m5stack_unitc6l/variant.h ================================================ void c6l_init(); #define HAS_GPS 1 #define GPS_RX_PIN 4 #define GPS_TX_PIN 5 #define I2C_SDA 10 #define I2C_SCL 8 #define PIN_BUZZER 11 #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 #define ENABLE_AMBIENTLIGHTING // Turn on Ambient Lighting // #define BUTTON_PIN 9 #define BUTTON_EXTENDER #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 #define LORA_MISO 22 #define LORA_SCK 20 #define LORA_MOSI 21 #define LORA_CS 23 #define LORA_RESET RADIOLIB_NC #define LORA_DIO1 7 #define LORA_BUSY 19 #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 3.0 #define USE_SPISSD1306 #ifdef USE_SPISSD1306 #define SSD1306_NSS 6 // CS #define SSD1306_RS 18 // DC #define SSD1306_RESET 15 // #define OLED_DG 1 #endif #define SCREEN_TRANSITION_FRAMERATE 10 #define BRIGHTNESS_DEFAULT 130 // Medium Low Brightness #define SERIAL_PRINT_PORT 1 ================================================ FILE: variants/esp32c6/tlora_c6/platformio.ini ================================================ [env:tlora-c6] extends = esp32c6_base board = esp32-c6-devkitm-1 board_level = pr build_flags = ${esp32c6_base.build_flags} -D TLORA_C6 -I variants/esp32c6/tlora_c6 -DARDUINO_USB_CDC_ON_BOOT=1 -DARDUINO_USB_MODE=1 -ULED_BUILTIN ================================================ FILE: variants/esp32c6/tlora_c6/variant.h ================================================ #define I2C_SDA 8 // I2C pins for this board #define I2C_SCL 9 #define LED_POWER 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 #define SERIAL_PRINT_PORT 1 ================================================ FILE: variants/esp32s2/esp32s2.ini ================================================ [esp32s2_base] extends = esp32_common custom_esp32_kind = esp32s2 build_src_filter = ${esp32_common.build_src_filter} - - - monitor_speed = 115200 build_flags = ${esp32_common.build_flags} -DHAS_BLUETOOTH=0 -DMESHTASTIC_EXCLUDE_PAXCOUNTER -DMESHTASTIC_EXCLUDE_BLUETOOTH lib_deps = ${esp32_common.lib_deps} # renovate: datasource=git-refs depName=meshtastic-ESP32_Codec2 packageName=https://github.com/meshtastic/ESP32_Codec2 gitBranch=master https://github.com/meshtastic/ESP32_Codec2/archive/633326c78ac251c059ab3a8c430fcdf25b41672f.zip lib_ignore = ${esp32_common.lib_ignore} NimBLE-Arduino libpax ================================================ FILE: variants/esp32s2/nugget_s2_lora/platformio.ini ================================================ [env:nugget-s2-lora] extends = esp32s2_base board = lolin_s2_mini board_level = extra build_flags = ${esp32s2_base.build_flags} -D PRIVATE_HW -I variants/esp32s2/nugget_s2_lora ================================================ FILE: variants/esp32s2/nugget_s2_lora/variant.h ================================================ #define I2C_SDA 34 // I2C pins for this board #define I2C_SCL 36 #define LED_POWER 15 // If defined we will blink this LED #define HAS_NEOPIXEL // Enable the use of neopixels #define NEOPIXEL_COUNT 3 // How many neopixels are connected #define NEOPIXEL_DATA 12 // 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 // If defined, this will be used for user button presses #define BUTTON_NEED_PULLUP #define USE_RF95 #define LORA_SCK 6 #define LORA_MISO 8 #define LORA_MOSI 10 #define LORA_CS 13 #define LORA_DIO0 16 #define LORA_RESET 5 #define LORA_DIO1 RADIOLIB_NC #define LORA_DIO2 RADIOLIB_NC ================================================ FILE: variants/esp32s3/CDEBYTE_EoRa-Hub/pins_arduino.h ================================================ // 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 */ ================================================ FILE: variants/esp32s3/CDEBYTE_EoRa-Hub/platformio.ini ================================================ [env:CDEBYTE_EoRa-Hub] extends = esp32s3_base board = CDEBYTE_EoRa-Hub board_level = extra build_flags = ${esp32s3_base.build_flags} -D PRIVATE_HW -I variants/esp32s3/CDEBYTE_EoRa-Hub ================================================ FILE: variants/esp32s3/CDEBYTE_EoRa-Hub/rfswitch.h ================================================ #include "RadioLib.h" // This is rewritten to match the requirements of the E80-900M2213S // The E80 does not conform to the reference Semtech switches(!) and therefore needs a custom matrix. // See footnote #3 in "https://www.cdebyte.com/products/E80-900M2213S/2#Pin" // RF Switch Matrix SubG RFO_HP_LF / RFO_LP_LF / RFI_[NP]_LF0 // DIO5 -> RFSW0_V1 // DIO6 -> RFSW1_V2 // DIO7 -> not connected on E80 module - note that GNSS and Wifi scanning are not possible. static const uint32_t rfswitch_dio_pins[] = {RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11X0_DIO6, RADIOLIB_LR11X0_DIO7, RADIOLIB_NC, RADIOLIB_NC}; static const Module::RfSwitchMode_t rfswitch_table[] = { // mode DIO5 DIO6 DIO7 {LR11x0::MODE_STBY, {LOW, LOW, LOW}}, {LR11x0::MODE_RX, {LOW, HIGH, LOW}}, {LR11x0::MODE_TX, {HIGH, HIGH, LOW}}, {LR11x0::MODE_TX_HP, {HIGH, LOW, LOW}}, {LR11x0::MODE_TX_HF, {LOW, LOW, LOW}}, {LR11x0::MODE_GNSS, {LOW, LOW, HIGH}}, {LR11x0::MODE_WIFI, {LOW, LOW, LOW}}, END_OF_MODE_TABLE, }; ================================================ FILE: variants/esp32s3/CDEBYTE_EoRa-Hub/variant.h ================================================ // EByte EoRA-Hub // Uses E80 (LR1121) LoRa module #define LED_POWER 35 // Button - user interface #define BUTTON_PIN 0 // BOOT button #define BATTERY_PIN 1 #define ADC_CHANNEL ADC1_GPIO1_CHANNEL #define ADC_MULTIPLIER 103.0 // Calibrated value #define ADC_ATTENUATION ADC_ATTEN_DB_0 #define ADC_CTRL 37 #define ADC_CTRL_ENABLED LOW // 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_LR1121 #define LORA_SCK 9 #define LORA_MOSI 10 #define LORA_MISO 11 #define LORA_RESET 12 #define LORA_CS 8 #define LORA_DIO9 13 // LR1121 #define LR1121_IRQ_PIN 14 #define LR1121_NRESET_PIN LORA_RESET #define LR1121_BUSY_PIN LORA_DIO9 #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 ================================================ FILE: variants/esp32s3/CDEBYTE_EoRa-S3/pins_arduino.h ================================================ // 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 */ ================================================ FILE: variants/esp32s3/CDEBYTE_EoRa-S3/platformio.ini ================================================ [env:CDEBYTE_EoRa-S3] custom_meshtastic_hw_model = 61 custom_meshtastic_hw_model_slug = CDEBYTE_EORA_S3 custom_meshtastic_architecture = esp32-s3 custom_meshtastic_actively_supported = true custom_meshtastic_support_level = 3 custom_meshtastic_display_name = EBYTE EoRa-S3 custom_meshtastic_tags = EByte custom_meshtastic_requires_dfu = true extends = esp32s3_base board = CDEBYTE_EoRa-S3 build_flags = ${esp32s3_base.build_flags} -D CDEBYTE_EORA_S3 -I variants/esp32s3/CDEBYTE_EoRa-S3 ================================================ FILE: variants/esp32s3/CDEBYTE_EoRa-S3/variant.h ================================================ // LED - status indication #define LED_POWER 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 ================================================ FILE: variants/esp32s3/EBYTE_ESP32-S3/pins_arduino.h ================================================ // 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 */ ================================================ FILE: variants/esp32s3/EBYTE_ESP32-S3/platformio.ini ================================================ [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/esp32s3/EBYTE_ESP32-S3 ================================================ FILE: variants/esp32s3/EBYTE_ESP32-S3/variant.h ================================================ // 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_POWER 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.) ================================================ FILE: variants/esp32s3/ELECROW-ThinkNode-M2/pins_arduino.h ================================================ // 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 */ ================================================ FILE: variants/esp32s3/ELECROW-ThinkNode-M2/platformio.ini ================================================ [env:thinknode_m2] custom_meshtastic_hw_model = 90 custom_meshtastic_hw_model_slug = THINKNODE_M2 custom_meshtastic_architecture = esp32-s3 custom_meshtastic_actively_supported = true custom_meshtastic_support_level = 1 custom_meshtastic_display_name = ThinkNode M2 custom_meshtastic_images = thinknode_m2.svg custom_meshtastic_tags = Elecrow custom_meshtastic_requires_dfu = false extends = esp32s3_base board = ESP32-S3-WROOM-1-N4 build_flags = ${esp32s3_base.build_flags} -D ELECROW_ThinkNode_M2 -I variants/esp32s3/ELECROW-ThinkNode-M2 ================================================ FILE: variants/esp32s3/ELECROW-ThinkNode-M2/variant.h ================================================ #define PIN_BUTTON1 47 // 功能键 #define PIN_BUTTON2 4 // 电源键 #define ALT_BUTTON_PIN PIN_BUTTON2 #define ALT_BUTTON_ACTIVE_LOW false #define ALT_BUTTON_ACTIVE_PULLUP false #define LED_POWER 6 #define ADC_V 42 // USB_CHECK #define EXT_PWR_DETECT 7 #define PIN_BUZZER 5 #define I2C_SCL 15 #define I2C_SDA 16 #define UART_TX 43 #define UART_RX 44 #define VEXT_ENABLE 46 // for OLED #define VEXT_ON_VALUE HIGH #define SX126X_CS 10 #define LORA_SCK 12 #define LORA_MOSI 11 #define LORA_MISO 13 #define SX126X_RESET 21 #define SX126X_BUSY 14 #define SX126X_DIO1 3 #define SX126X_DIO2_AS_RF_SWITCH // #define SX126X_DIO3 9 #define SX126X_DIO3_TCXO_VOLTAGE 3.3 #define SX126X_MAX_POWER 22 // SX126xInterface.cpp defaults to 22 if not defined, but here we define it for good practice #define USE_SX1262 #define LORA_CS SX126X_CS // FIXME: for some reason both are used in /src #define LORA_DIO1 SX126X_DIO1 #define SX126X_POWER_EN 48 // Battery // #define BATTERY_PIN 2 #define BATTERY_PIN 17 // #define ADC_CHANNEL ADC1_GPIO2_CHANNEL #define ADC_CHANNEL ADC2_GPIO17_CHANNEL #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.548F) #define BAT_MEASURE_ADC_UNIT 2 #define HAS_SCREEN 1 #define USE_SH1106 1 #define HAS_GPS 0 #define BUTTON_PIN PIN_BUTTON1 ================================================ FILE: variants/esp32s3/ELECROW-ThinkNode-M5/pins_arduino.h ================================================ // 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 */ ================================================ FILE: variants/esp32s3/ELECROW-ThinkNode-M5/platformio.ini ================================================ [env:thinknode_m5] custom_meshtastic_hw_model = 107 custom_meshtastic_hw_model_slug = THINKNODE_M5 custom_meshtastic_architecture = esp32-s3 custom_meshtastic_actively_supported = true custom_meshtastic_support_level = 1 custom_meshtastic_display_name = ThinkNode M5 custom_meshtastic_images = thinknode_m1.svg custom_meshtastic_tags = Elecrow custom_meshtastic_requires_dfu = false extends = esp32s3_base board = ESP32-S3-WROOM-1-N4 build_src_filter = ${esp32s3_base.build_src_filter} +<../variants/esp32s3/ELECROW-ThinkNode-M5> build_flags = ${esp32s3_base.build_flags} -D ELECROW_ThinkNode_M5 -I variants/esp32s3/ELECROW-ThinkNode-M5 -DEINK_DISPLAY_MODEL=GxEPD2_154_D67 -DEINK_WIDTH=200 -DEINK_HEIGHT=200 -DUSE_EINK_DYNAMICDISPLAY ; Enable Dynamic EInk -DEINK_LIMIT_FASTREFRESH=10 ; How many consecutive fast-refreshes are permitted //20 -DEINK_LIMIT_RATE_BACKGROUND_SEC=10 ; Minimum interval between BACKGROUND updates //30 -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. lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master https://github.com/meshtastic/GxEPD2/archive/c7eb4c3c167cf396ef4f541cc5d4c6aa42f3c46b.zip # renovate: datasource=custom.pio depName=PCA9557-arduino packageName=maxpromer/library/PCA9557-arduino maxpromer/PCA9557-arduino@1.0.0 # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib lewisxhe/SensorLib@0.3.4 ================================================ FILE: variants/esp32s3/ELECROW-ThinkNode-M5/variant.cpp ================================================ #include "variant.h" #include PCA9557 io(0x18, &Wire1); void earlyInitVariant() { Wire1.begin(48, 47); io.pinMode(PCA_PIN_EINK_EN, OUTPUT); io.pinMode(PCA_PIN_POWER_EN, OUTPUT); io.pinMode(PCA_LED_POWER, OUTPUT); io.pinMode(PCA_LED_USER, OUTPUT); io.pinMode(PCA_LED_ENABLE, OUTPUT); io.digitalWrite(PCA_PIN_POWER_EN, HIGH); io.digitalWrite(PCA_LED_USER, LOW); io.digitalWrite(PCA_LED_ENABLE, LOW); } void variant_shutdown() { io.digitalWrite(PCA_PIN_POWER_EN, LOW); } ================================================ FILE: variants/esp32s3/ELECROW-ThinkNode-M5/variant.h ================================================ #ifndef ELECROW_ThinkNode_M5_VAR #define ELECROW_ThinkNode_M5_VAR #define UART_TX 43 #define UART_RX 44 #define HAS_PCA9557 // LED // Both of these are on the GPIO expander #define PCA_LED_USER 1 // the Blue LED #define PCA_LED_ENABLE 2 // the power supply to the LEDs, in an OR arrangement with VBUS power #define PCA_LED_POWER 3 // the Red LED? Seems to have hardware logic to blink when USB is plugged in. #define POWER_LED_HARDWARE_BLINKS_WHILE_CHARGING // USB_CHECK #define EXT_PWR_DETECT 12 #define BATTERY_PIN 8 #define ADC_CHANNEL ADC1_GPIO8_CHANNEL #define ADC_MULTIPLIER 2.11 // 2.0 + 10% for correction of display undervoltage. #define PIN_BUZZER 9 // Buttons #define PIN_BUTTON2 14 #define PIN_BUTTON1 21 // Wire Interfaces #define I2C_SCL 1 #define I2C_SDA 2 // PCF8563 RTC Module #define PCF8563_RTC 0x51 // GPS pins #define GPS_SWITH 10 #define HAS_GPS 1 #define GPS_L76K #define PIN_GPS_REINIT 13 // An output to reset L76K GPS. As per datasheet, low for > 100ms will reset the L76K #define PIN_GPS_STANDBY 11 // An output to wake GPS, low means allow sleep, high means force wake #define GPS_TX_PIN 20 // This is for bits going TOWARDS the CPU #define GPS_RX_PIN 19 // 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 SX126X_CS 17 #define LORA_SCK 16 #define LORA_MOSI 15 #define LORA_MISO 7 #define SX126X_RESET 6 #define SX126X_BUSY 5 #define SX126X_DIO1 4 #define SX126X_DIO2_AS_RF_SWITCH #define SX126X_DIO3_TCXO_VOLTAGE 3.3 #define SX126X_POWER_EN 46 #define SX126X_MAX_POWER 22 // SX126xInterface.cpp defaults to 22 if not defined, but here we define it for good practice #define USE_SX1262 #define LORA_CS SX126X_CS // FIXME: for some reason both are used in /src #define LORA_DIO1 SX126X_DIO1 #define USE_EINK // Note: this is really just backlight power #define PCA_PIN_EINK_EN 5 // This is the pin number on the GPIO expander #define PIN_EINK_CS 39 #define PIN_EINK_BUSY 42 #define PIN_EINK_DC 40 #define PIN_EINK_RES 41 #define PIN_EINK_SCLK 38 #define PIN_EINK_MOSI 45 // also called SDI // Controls power for all peripherals (eink + GPS + LoRa + Sensor) #define PIN_POWER_EN -1 #define PCA_PIN_POWER_EN 4 // This is the pin number on the GPIO expander #define PIN_SPI_MISO 7 #define PIN_SPI_MOSI 15 #define PIN_SPI_SCK 16 #define BUTTON_PIN PIN_BUTTON1 #define BUTTON_PIN_ALT PIN_BUTTON2 #define SERIAL_PRINT_PORT 0 #endif ================================================ FILE: variants/esp32s3/bpi_picow_esp32_s3/pins_arduino.h ================================================ #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 */ ================================================ FILE: variants/esp32s3/bpi_picow_esp32_s3/platformio.ini ================================================ [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 = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=caveman99-ESP32_Codec2 packageName=caveman99/library/ESP32 Codec2 caveman99/ESP32 Codec2@1.0.1 build_flags = ${esp32s3_base.build_flags} -D PRIVATE_HW -I variants/esp32s3/bpi_picow_esp32_s3 ================================================ FILE: variants/esp32s3/bpi_picow_esp32_s3/variant.h ================================================ #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_POWER 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 ================================================ FILE: variants/esp32s3/crowpanel-esp32s3-5-epaper/pins_arduino.h ================================================ #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 = 21; static const uint8_t SCL = 15; // Default SPI will be mapped to Radio static const uint8_t SS = 14; static const uint8_t MOSI = 8; static const uint8_t MISO = 9; static const uint8_t SCK = 3; #define SPI_MOSI (40) #define SPI_SCK (39) #define SPI_MISO (13) #define SPI_CS (10) // IO42 TF_3V3_CTL #define SDCARD_CS SPI_CS #endif /* Pins_Arduino_h */ ================================================ FILE: variants/esp32s3/crowpanel-esp32s3-5-epaper/platformio.ini ================================================ [env:crowpanel-esp32s3-5-epaper] extends = esp32s3_base board_build.arduino.memory_type = qio_opi board_build.flash_mode = qio board_build.psram_type = opi board_upload.flash_size = 8MB board_upload.maximum_size = 8388608 board_build.partitions = default_8MB.csv board = esp32-s3-devkitc-1 ;upload_port = /dev/ttyUSB0 board_level = extra upload_protocol = esptool build_flags = ${esp32s3_base.build_flags} -D CROWPANEL_ESP32S3_5_EPAPER -I variants/esp32s3/crowpanel-esp32s3-5-epaper -D PRIVATE_HW -DBOARD_HAS_PSRAM -DEINK_DISPLAY_MODEL=GxEPD2_579_GDEY0579T93 ;https://www.good-display.com/product/439.html -DEINK_WIDTH=792 -DEINK_HEIGHT=272 -DUSE_EINK_DYNAMICDISPLAY ; Enable Dynamic EInk -DEINK_LIMIT_FASTREFRESH=100 ; How many consecutive fast-refreshes are permitted ;-DEINK_LIMIT_RATE_BACKGROUND_SEC=30 ; Minimum interval between BACKGROUND updates ;-DEINK_LIMIT_RATE_RESPONSIVE_SEC=1 lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master https://github.com/meshtastic/GxEPD2/archive/c7eb4c3c167cf396ef4f541cc5d4c6aa42f3c46b.zip [env:crowpanel-esp32s3-4-epaper] extends = esp32s3_base board_build.arduino.memory_type = qio_opi board_build.flash_mode = qio board_build.psram_type = opi board_upload.flash_size = 8MB board_upload.maximum_size = 8388608 board_build.partitions = default_8MB.csv board = esp32-s3-devkitc-1 ;upload_port = /dev/ttyUSB0 board_level = extra upload_protocol = esptool build_flags = ${esp32s3_base.build_flags} -D CROWPANEL_ESP32S3_4_EPAPER -I variants/esp32s3/crowpanel-esp32s3-5-epaper -D PRIVATE_HW -DBOARD_HAS_PSRAM -DEINK_DISPLAY_MODEL=GxEPD2_420_GYE042A87 ; similar Panel: GDEY042T81 : https://www.good-display.com/product/386.html -DEINK_WIDTH=400 -DEINK_HEIGHT=300 -DUSE_EINK_DYNAMICDISPLAY ; Enable Dynamic EInk -DEINK_LIMIT_FASTREFRESH=100 ; How many consecutive fast-refreshes are permitted ;-DEINK_LIMIT_RATE_BACKGROUND_SEC=30 ; Minimum interval between BACKGROUND updates ;-DEINK_LIMIT_RATE_RESPONSIVE_SEC=1 lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master https://github.com/meshtastic/GxEPD2/archive/c7eb4c3c167cf396ef4f541cc5d4c6aa42f3c46b.zip [env:crowpanel-esp32s3-2-epaper] extends = esp32s3_base board_build.arduino.memory_type = qio_opi board_build.flash_mode = qio board_build.psram_type = opi board_upload.flash_size = 8MB board_upload.maximum_size = 8388608 board_build.partitions = default_8MB.csv board = esp32-s3-devkitc-1 ;upload_port = /dev/ttyUSB0 board_level = extra upload_protocol = esptool build_flags = ${esp32s3_base.build_flags} -D CROWPANEL_ESP32S3_2_EPAPER -I variants/esp32s3/crowpanel-esp32s3-5-epaper -D PRIVATE_HW -DBOARD_HAS_PSRAM -DEINK_DISPLAY_MODEL=GxEPD2_290_GDEY029T94 ;https://www.good-display.com/product/389.html -DEINK_WIDTH=296 -DEINK_HEIGHT=128 -DUSE_EINK_DYNAMICDISPLAY ; Enable Dynamic EInk -DEINK_LIMIT_FASTREFRESH=100 ; How many consecutive fast-refreshes are permitted ;-DEINK_LIMIT_RATE_BACKGROUND_SEC=30 ; Minimum interval between BACKGROUND updates ;-DEINK_LIMIT_RATE_RESPONSIVE_SEC=1 lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master https://github.com/meshtastic/GxEPD2/archive/c7eb4c3c167cf396ef4f541cc5d4c6aa42f3c46b.zip ================================================ FILE: variants/esp32s3/crowpanel-esp32s3-5-epaper/variant.h ================================================ #define HAS_SDCARD #define SDCARD_USE_SPI1 // Display (E-Ink) #define USE_EINK #define PIN_EINK_CS 45 #define PIN_EINK_BUSY 48 #define PIN_EINK_DC 46 #define PIN_EINK_RES 47 #define PIN_EINK_SCLK 12 #define PIN_EINK_MOSI 11 #define VEXT_ENABLE 7 // e-ink power enable pin #define VEXT_ON_VALUE HIGH #define PIN_POWER_EN 42 // TF/SD Card Power Enable Pin // #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 // 21 #define I2C_SCL SCL // 15 #define GPS_DEFAULT_NOT_PRESENT 1 // #define GPS_RX_PIN 44 // #define GPS_TX_PIN 43 #define LED_POWER 41 #define BUTTON_PIN 2 #define BUTTON_NEED_PULLUP // Buzzer - noisy ? #define PIN_BUZZER (0 + 18) // Wheel // Up 6 // Push 5 // Down 4 // MENU Top 2 // EXIT Bottom 1 // 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 3 #define LORA_MISO 9 #define LORA_MOSI 8 #define LORA_CS 14 #define LORA_RESET 38 #define LORA_DIO1 16 #define LORA_DIO2 17 // per SX1262_Receive_Interrupt/utilities.h #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 // per SX128x_Receive_Interrupt/utilities.h #ifdef USE_SX1280 #define SX128X_CS LORA_CS #define SX128X_DIO1 LORA_DIO1 #define SX128X_BUSY LORA_DIO2 #define SX128X_RESET LORA_RESET #define SX128X_RXEN 21 #define SX128X_TXEN 15 #define SX128X_MAX_POWER 3 #endif ================================================ FILE: variants/esp32s3/diy/my_esp32s3_diy_eink/pins_arduino.h ================================================ #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 */ ================================================ FILE: variants/esp32s3/diy/my_esp32s3_diy_eink/platformio.ini ================================================ [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 lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 zinggjm/GxEPD2@1.6.8 # renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel adafruit/Adafruit NeoPixel@1.15.4 build_unflags = ${esp32s3_base.build_unflags} -DARDUINO_USB_MODE=1 build_flags = ${esp32s3_base.build_flags} -D PRIVATE_HW -I variants/esp32s3/diy/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 ================================================ FILE: variants/esp32s3/diy/my_esp32s3_diy_eink/variant.h ================================================ #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_POWER 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 ================================================ FILE: variants/esp32s3/diy/my_esp32s3_diy_oled/pins_arduino.h ================================================ #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 */ ================================================ FILE: variants/esp32s3/diy/my_esp32s3_diy_oled/platformio.ini ================================================ [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 lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel adafruit/Adafruit NeoPixel@1.15.4 build_unflags = ${esp32s3_base.build_unflags} -DARDUINO_USB_MODE=1 build_flags = ${esp32s3_base.build_flags} -D PRIVATE_HW -I variants/esp32s3/diy/my_esp32s3_diy_oled -DBOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue -DARDUINO_USB_MODE=0 ================================================ FILE: variants/esp32s3/diy/my_esp32s3_diy_oled/variant.h ================================================ #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_POWER 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 ================================================ FILE: variants/esp32s3/diy/t-energy-s3_e22/platformio.ini ================================================ ; NanoVHF T-Energy-S3 + E22(0)-xxxM - DIY [env:t-energy-s3_e22] extends = esp32s3_base board = esp32-s3-devkitc-1 board_build.partitions = default_16MB.csv 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/esp32s3/diy/t-energy-s3_e22 ================================================ FILE: variants/esp32s3/diy/t-energy-s3_e22/variant.h ================================================ // 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 ================================================ FILE: variants/esp32s3/dreamcatcher/platformio.ini ================================================ [env:dreamcatcher] ; 2301, latest revision extends = esp32s3_base board = esp32s3box board_build.partitions = default_16MB.csv board_level = extra build_flags = ${esp32s3_base.build_flags} -D PRIVATE_HW -D OTHERNET_DC_REV=2301 -I variants/esp32s3/dreamcatcher -D ARDUINO_USB_CDC_ON_BOOT=1 lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=git-refs depName=ESP8266Audio packageName=https://github.com/meshtastic/ESP8266Audio gitBranch=meshtastic-2.0.0-dacfix https://github.com/meshtastic/ESP8266Audio/archive/343024632ee78d6216907b2353fc943a62422d80.zip # renovate: datasource=custom.pio depName=ESP8266SAM packageName=earlephilhower/library/ESP8266SAM earlephilhower/ESP8266SAM@1.1.0 [env:dreamcatcher-2206] extends = esp32s3_base board = esp32s3box board_build.partitions = default_16MB.csv board_level = extra build_flags = ${esp32s3_base.build_flags} -D PRIVATE_HW -D OTHERNET_DC_REV=2206 -I variants/esp32s3/dreamcatcher -D ARDUINO_USB_CDC_ON_BOOT=1 ================================================ FILE: variants/esp32s3/dreamcatcher/rfswitch.h ================================================ #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, }; ================================================ FILE: variants/esp32s3/dreamcatcher/variant.h ================================================ #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_POWER 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 DAC_I2S_MCLK 44 #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 ================================================ FILE: variants/esp32s3/elecrow_panel/pins_arduino.h ================================================ #ifndef Pins_Arduino_h #define Pins_Arduino_h #include static const uint8_t TX = 43; static const uint8_t RX = 44; static const uint8_t SDA = 15; static const uint8_t SCL = 16; // 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 SPI_MOSI = 6; static const uint8_t SPI_SCK = 5; static const uint8_t SPI_MISO = 4; 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 */ ================================================ FILE: variants/esp32s3/elecrow_panel/platformio.ini ================================================ [crowpanel_base] extends = esp32s3_base board = crowpanel board_check = true upload_protocol = esptool board_build.partitions = default_16MB.csv ; must be here for some reason, board.json is not enough !? build_flags = ${esp32s3_base.build_flags} -Os -I variants/esp32s3/elecrow_panel -D ELECROW_PANEL -D CONFIG_ARDUHAL_LOG_COLORS -D RADIOLIB_DEBUG_SPI=0 -D RADIOLIB_DEBUG_PROTOCOL=0 -D RADIOLIB_DEBUG_BASIC=0 -D RADIOLIB_VERBOSE_ASSERT=0 -D RADIOLIB_SPI_PARANOID=0 -D MESHTASTIC_EXCLUDE_CANNEDMESSAGES=1 -D MESHTASTIC_EXCLUDE_INPUTBROKER=1 -D MESHTASTIC_EXCLUDE_WEBSERVER=1 -D MESHTASTIC_EXCLUDE_SERIAL=1 -D MESHTASTIC_EXCLUDE_SOCKETAPI=1 -D MESHTASTIC_EXCLUDE_SCREEN=1 -D CONFIG_DISABLE_HAL_LOCKS=1 -D USE_PIN_BUZZER -D HAS_SCREEN=0 -D HAS_TFT=1 -D RAM_SIZE=6144 -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 USE_PACKET_API -D HAS_SDCARD -D SD_SPI_FREQUENCY=75000000 lib_deps = ${esp32s3_base.lib_deps} ${device-ui_base.lib_deps} # renovate: datasource=git-refs depName=ESP8266Audio packageName=https://github.com/meshtastic/ESP8266Audio gitBranch=meshtastic-2.0.0-dacfix https://github.com/meshtastic/ESP8266Audio/archive/343024632ee78d6216907b2353fc943a62422d80.zip # renovate: datasource=custom.pio depName=ESP8266SAM packageName=earlephilhower/library/ESP8266SAM earlephilhower/ESP8266SAM@1.1.0 # renovate: datasource=custom.pio depName=TCA9534 packageName=hideakitai/library/TCA9534 hideakitai/TCA9534@0.1.1 lovyan03/LovyanGFX@1.2.0 ; note: v1.2.7 breaks the elecrow 7" display functionality [crowpanel_small_esp32s3_base] ; 2.4, 2.8, 3.5 inch extends = crowpanel_base build_flags = ${crowpanel_base.build_flags} -D CROW_SELECT=1 -D SDCARD_USE_SOFT_SPI -D SDCARD_CS=7 -D SPI_DRIVER_SELECT=2 -D LGFX_DRIVER_TEMPLATE -D LGFX_DRIVER=LGFX_GENERIC -D GFX_DRIVER_INC=\"graphics/LGFX/LGFX_GENERIC.h\" -D VIEW_320x240 -D MAP_FULL_REDRAW [crowpanel_large_esp32s3_base] ; 4.3, 5.0, 7.0 inch extends = crowpanel_base build_flags = ${crowpanel_base.build_flags} -D CROW_SELECT=2 -D SDCARD_CS=7 -D LGFX_DRIVER=LGFX_ELECROW70 -D GFX_DRIVER_INC=\"graphics/LGFX/LGFX_ELECROW70.h\" -D DISPLAY_SET_RESOLUTION [env:elecrow-adv-24-28-tft] custom_meshtastic_hw_model = 97 custom_meshtastic_hw_model_slug = CROWPANEL custom_meshtastic_architecture = esp32-s3 custom_meshtastic_actively_supported = true custom_meshtastic_support_level = 1 custom_meshtastic_display_name = Crowpanel Adv 2.4/2.8 TFT custom_meshtastic_images = crowpanel_2_4.svg, crowpanel_2_8.svg custom_meshtastic_tags = Elecrow custom_meshtastic_requires_dfu = true custom_meshtastic_partition_scheme = 16MB extends = crowpanel_small_esp32s3_base build_flags = ${crowpanel_small_esp32s3_base.build_flags} -D SPI_FREQUENCY=80000000 -D LGFX_SCREEN_WIDTH=240 -D LGFX_SCREEN_HEIGHT=320 -D DISPLAY_SIZE=320x240 ; landscape mode -D LGFX_PANEL=ST7789 -D LGFX_ROTATION=1 -D LGFX_CFG_HOST=SPI2_HOST -D LGFX_PIN_SCK=42 -D LGFX_PIN_MOSI=39 -D LGFX_PIN_DC=41 -D LGFX_PIN_CS=40 -D LGFX_PIN_BL=38 -D LGFX_TOUCH=FT5x06 -D LGFX_TOUCH_I2C_ADDR=0x38 -D LGFX_TOUCH_I2C_SDA=15 -D LGFX_TOUCH_I2C_SCL=16 -D LGFX_TOUCH_INT=47 -D LGFX_TOUCH_RST=48 -D LGFX_TOUCH_ROTATION=0 [env:elecrow-adv-35-tft] custom_meshtastic_hw_model = 97 custom_meshtastic_hw_model_slug = CROWPANEL custom_meshtastic_architecture = esp32-s3 custom_meshtastic_actively_supported = true custom_meshtastic_support_level = 1 custom_meshtastic_display_name = Crowpanel Adv 3.5 TFT custom_meshtastic_images = crowpanel_3_5.svg custom_meshtastic_tags = Elecrow custom_meshtastic_requires_dfu = true custom_meshtastic_partition_scheme = 16MB extends = crowpanel_small_esp32s3_base board_level = pr build_flags = ${crowpanel_small_esp32s3_base.build_flags} -D LV_CACHE_DEF_SIZE=2097152 -D SPI_FREQUENCY=60000000 -D LGFX_SCREEN_WIDTH=320 -D LGFX_SCREEN_HEIGHT=480 -D DISPLAY_SIZE=320x480 ; portrait mode -D LGFX_PANEL=ILI9488 -D LGFX_ROTATION=0 -D LGFX_CFG_HOST=SPI2_HOST -D LGFX_PIN_SCK=42 -D LGFX_PIN_MOSI=39 -D LGFX_PIN_DC=41 -D LGFX_PIN_CS=40 -D LGFX_PIN_BL=38 -D LGFX_TOUCH=GT911 -D LGFX_TOUCH_I2C_ADDR=0x5D -D LGFX_TOUCH_I2C_SDA=15 -D LGFX_TOUCH_I2C_SCL=16 -D LGFX_TOUCH_INT=47 -D LGFX_TOUCH_RST=48 -D LGFX_TOUCH_ROTATION=0 -D DISPLAY_SET_RESOLUTION ; 4.3, 5.0, 7.0 inch 800x480 IPS (V1) [env:elecrow-adv1-43-50-70-tft] custom_meshtastic_hw_model = 97 custom_meshtastic_hw_model_slug = CROWPANEL custom_meshtastic_architecture = esp32-s3 custom_meshtastic_actively_supported = true custom_meshtastic_support_level = 1 custom_meshtastic_display_name = Crowpanel Adv 4.3/5.0/7.0 TFT custom_meshtastic_images = crowpanel_5_0.svg, crowpanel_7_0.svg custom_meshtastic_tags = Elecrow custom_meshtastic_requires_dfu = true custom_meshtastic_partition_scheme = 16MB extends = crowpanel_large_esp32s3_base build_flags = ${crowpanel_large_esp32s3_base.build_flags} -D VIEW_320x240 -D DISPLAY_SIZE=800x480 ; landscape mode ================================================ FILE: variants/esp32s3/elecrow_panel/variant.h ================================================ #define I2C_SDA 15 #define I2C_SCL 16 #if CROW_SELECT == 1 #define WAKE_ON_TOUCH #define SCREEN_TOUCH_INT 47 #define USE_POWERSAVE #define SLEEP_TIME 180 #endif #if CROW_SELECT == 1 // dac / amp // #define HAS_I2S // didn't get I2S sound working #define PIN_BUZZER 8 // using pwm buzzer instead (nobody will notice, lol) #define DAC_I2S_BCK 13 #define DAC_I2S_WS 11 #define DAC_I2S_DOUT 12 #define DAC_I2S_MCLK 8 // don't use GPIO0 because it's assigned to LoRa or button #else #define PIN_BUZZER 8 #endif // GPS via UART1 connector #define GPS_DEFAULT_NOT_PRESENT 1 #define HAS_GPS 1 #if CROW_SELECT == 1 #define GPS_RX_PIN 18 #define GPS_TX_PIN 17 #else // GPIOs shared with LoRa or MIC module #define GPS_RX_PIN 19 #define GPS_TX_PIN 20 #endif // Extension Slot Layout, viewed from above (2.4-3.5) // DIO1/IO1 o o IO2/NRESET // SCK/IO10 o o IO16/NC // MISO/IO9 o o IO15/NC // MOSI/IO3 o o NC/DIO2 // 3V3 o o IO46/BUSY // GND o o IO0/NSS // 5V/NC o o NC/DIO3 // J9 J8 // Extension Slot Layout, viewed from above (4.3-7.0) // !! DIO1/IO20 o o IO19/NRESET !! // !! SCK/IO5 o o IO16/NC // !! MISO/IO4 o o IO15/NC // !! MOSI/IO6 o o NC/DIO2 // 3V3 o o IO2/BUSY !! // GND o o IO0/NSS // 5V/NC o o NC/DIO3 // J9 J8 // LoRa #define USE_SX1262 #if CROW_SELECT == 1 // 2.4", 2.8, 3.5""" #define HW_SPI1_DEVICE #define LORA_CS 0 #define LORA_SCK 10 #define LORA_MISO 9 #define LORA_MOSI 3 #define LORA_RESET 2 #define LORA_DIO1 1 // SX1262 IRQ #define LORA_DIO2 46 // SX1262 BUSY // need to pull IO45 low to enable LORA and disable Microphone on 24 28 35 #define SENSOR_POWER_CTRL_PIN 45 #define SENSOR_POWER_ON LOW #else // 4.3", 5.0", 7.0" #define LORA_CS 0 #define LORA_SCK 5 #define LORA_MISO 4 #define LORA_MOSI 6 #define LORA_RESET 19 #define LORA_DIO1 20 // SX1262 IRQ #define LORA_DIO2 2 // SX1262 BUSY #endif #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 3.3 ================================================ FILE: variants/esp32s3/esp32-s3-pico/pins_arduino.h ================================================ #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 */ ================================================ FILE: variants/esp32s3/esp32-s3-pico/platformio.ini ================================================ [env:ESP32-S3-Pico] board_level = extra extends = esp32s3_base upload_protocol = esptool board = esp32-s3-pico board_build.partitions = default_16MB.csv 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/esp32s3/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} # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 zinggjm/GxEPD2@1.6.8 # renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel adafruit/Adafruit NeoPixel@1.15.4 ================================================ FILE: variants/esp32s3/esp32-s3-pico/variant.h ================================================ /* */ #define HAS_GPS 0 #undef GPS_RX_PIN #undef GPS_TX_PIN #define EXT_NOTIFY_OUT 22 #define BUTTON_PIN 0 // 17 // 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 ================================================ FILE: variants/esp32s3/esp32s3.ini ================================================ [esp32s3_base] extends = esp32_common custom_esp32_kind = esp32s3 monitor_speed = 115200 lib_deps = ${esp32_common.lib_deps} # renovate: datasource=git-refs depName=meshtastic-ESP32_Codec2 packageName=https://github.com/meshtastic/ESP32_Codec2 gitBranch=master https://github.com/meshtastic/ESP32_Codec2/archive/633326c78ac251c059ab3a8c430fcdf25b41672f.zip ================================================ FILE: variants/esp32s3/hackaday-communicator/pins_arduino.h ================================================ #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; static const uint8_t SDA = 47; static const uint8_t SCL = 14; // Default SPI will be mapped to Radio static const uint8_t SS = 17; static const uint8_t MOSI = 3; static const uint8_t MISO = 9; static const uint8_t SCK = 8; 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 */ ================================================ FILE: variants/esp32s3/hackaday-communicator/platformio.ini ================================================ ; Hackaday Communicator [env:hackaday-communicator] extends = esp32s3_base board = hackaday-communicator board_check = true board_build.partitions = default_16MB.csv upload_protocol = esptool build_src_filter = ${esp32s3_base.build_src_filter} +<../variants/esp32s3/hackaday-communicator> build_flags = ${esp32s3_base.build_flags} -D HACKADAY_COMMUNICATOR -D BOARD_HAS_PSRAM -I variants/esp32s3/hackaday-communicator lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-Arduino_GFX packageName=https://github.com/meshtastic/Arduino_GFX gitBranch=master https://github.com/meshtastic/Arduino_GFX/archive/054e81ffaf23784830a734e3c184346789349406.zip ================================================ FILE: variants/esp32s3/hackaday-communicator/variant.cpp ================================================ #include "variant.h" #include "Arduino.h" void earlyInitVariant() { pinMode(KB_INT, INPUT); } ================================================ FILE: variants/esp32s3/hackaday-communicator/variant.h ================================================ #define TFT_BL 2 #define SPI_FREQUENCY 2000000 #define SPI_READ_FREQUENCY 16000000 #define TFT_HEIGHT 142 #define TFT_WIDTH 428 #define TFT_OFFSET_X 0 #define TFT_OFFSET_Y 0 #define TFT_OFFSET_ROTATION 0 #define SCREEN_TRANSITION_FRAMERATE 5 #define HAS_SCREEN 1 #define TFT_BLACK 0 #define BRIGHTNESS_DEFAULT 130 // Medium Low Brightness #define USE_TFTDISPLAY 1 #define USE_POWERSAVE #define SLEEP_TIME 120 #define GPS_DEFAULT_NOT_PRESENT 1 // keyboard #define I2C_SDA 47 // I2C pins for this board #define I2C_SCL 14 // #define KB_BL_PIN 46 // not used for now #define KB_INT 13 #define TFT_DC 39 #define TFT_CS 41 // LoRa #define USE_SX1262 #define LORA_SCK 8 #define LORA_MISO 9 #define LORA_MOSI 3 #define LORA_CS 17 #define LORA_RESET 18 #define LORA_DIO1 16 // SX1262 IRQ #define LORA_DIO2 15 // SX1262 BUSY #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 LED_NOTIFICATION 1 #define LED_STATE_ON 0 ================================================ FILE: variants/esp32s3/heltec_capsule_sensor_v3/platformio.ini ================================================ [env:heltec_capsule_sensor_v3] extends = esp32s3_base board = heltec_wifi_lora_32_V3 board_check = true board_build.partitions = default_8MB.csv build_flags = ${esp32s3_base.build_flags} -I variants/esp32s3/heltec_capsule_sensor_v3 -D HELTEC_CAPSULE_SENSOR_V3 -ULED_BUILTIN ;-D DEBUG_DISABLED ; uncomment this line to disable DEBUG output ================================================ FILE: variants/esp32s3/heltec_capsule_sensor_v3/variant.h ================================================ #define LED_POWER 33 #define LED_POWER2 34 #define EXT_PWR_DETECT 35 #define BUTTON_PIN 18 #define BUTTON_ACTIVE_LOW false #define BUTTON_ACTIVE_PULLUP false #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 ================================================ FILE: variants/esp32s3/heltec_sensor_hub/platformio.ini ================================================ [env:heltec_sensor_hub] extends = esp32s3_base board = heltec_wifi_lora_32_V3 board_check = true build_flags = ${esp32s3_base.build_flags} -I variants/esp32s3/heltec_sensor_hub -D HELTEC_SENSOR_HUB -ULED_BUILTIN lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel adafruit/Adafruit NeoPixel@1.15.4 ================================================ FILE: variants/esp32s3/heltec_sensor_hub/variant.h ================================================ #define EXT_PWR_DETECT 20 #define BUTTON_PIN 17 #define BUTTON_ACTIVE_LOW false #define BUTTON_ACTIVE_PULLUP false #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 34 // active HIGH, powers the voltage divider. Only on 1.1 #define ADC_CTRL_ENABLED HIGH #define HAS_NEOPIXEL // Enable the use of neopixels #define NEOPIXEL_COUNT 1 // How many neopixels are connected #define NEOPIXEL_DATA 18 // gpio pin used to send data to the neopixels #define NEOPIXEL_TYPE (NEO_GRB + NEO_KHZ800) // type of neopixels in use #define USE_SX1262 #define LORA_DIO0 RADIOLIB_NC #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 #define I2C_SDA 1 #define I2C_SCL 2 #define HAS_SCREEN 0 #define SENSOR_POWER_CTRL_PIN 33 #define SENSOR_POWER_ON 1 #define PERIPHERAL_WARMUP_MS 100 #define ESP32S3_WAKE_TYPE ESP_EXT1_WAKEUP_ANY_HIGH #define ENVIRONMENTAL_TELEMETRY_MODULE_ENABLE 1 ================================================ FILE: variants/esp32s3/heltec_v3/platformio.ini ================================================ [env:heltec-v3] custom_meshtastic_hw_model = 43 custom_meshtastic_hw_model_slug = HELTEC_V3 custom_meshtastic_architecture = esp32-s3 custom_meshtastic_actively_supported = true custom_meshtastic_support_level = 1 custom_meshtastic_display_name = Heltec V3 custom_meshtastic_images = heltec-v3.svg, heltec-v3-case.svg custom_meshtastic_tags = Heltec custom_meshtastic_partition_scheme = 8MB extends = esp32s3_base board = heltec_wifi_lora_32_V3 board_level = pr board_check = true board_build.partitions = default_8MB.csv build_flags = ${esp32s3_base.build_flags} -D HELTEC_V3 -I variants/esp32s3/heltec_v3 -ULED_BUILTIN ================================================ FILE: variants/esp32s3/heltec_v3/variant.h ================================================ #define LED_POWER 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 #define HAS_32768HZ 1 ================================================ FILE: variants/esp32s3/heltec_v4/pins_arduino.h ================================================ #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; static const uint8_t SDA = 4; static const uint8_t SCL = 3; 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 = 35; static const uint8_t RST_OLED = 21; static const uint8_t SCL_OLED = 18; static const uint8_t SDA_OLED = 17; static const uint8_t RST_LoRa = 12; static const uint8_t BUSY_LoRa = 13; static const uint8_t DIO0 = 14; #endif /* Pins_Arduino_h */ ================================================ FILE: variants/esp32s3/heltec_v4/platformio.ini ================================================ [heltec_v4_base] extends = esp32s3_base board = heltec_v4 board_check = true board_build.partitions = default_16MB.csv build_flags = ${esp32s3_base.build_flags} -D HELTEC_V4 -D HAS_LORA_FEM=1 -I variants/esp32s3/heltec_v4 -ULED_BUILTIN [env:heltec-v4] custom_meshtastic_hw_model = 110 custom_meshtastic_hw_model_slug = HELTEC_V4 custom_meshtastic_architecture = esp32-s3 custom_meshtastic_actively_supported = true custom_meshtastic_support_level = 1 custom_meshtastic_display_name = Heltec V4 custom_meshtastic_images = heltec_v4.svg custom_meshtastic_tags = Heltec custom_meshtastic_requires_dfu = true custom_meshtastic_partition_scheme = 16MB extends = heltec_v4_base build_flags = ${heltec_v4_base.build_flags} -D HELTEC_V4_OLED -D USE_SSD1306 ; Heltec_v4 has an SSD1315 display (compatible with SSD1306 driver) -D LED_POWER=35 -D RESET_OLED=21 -D I2C_SDA=17 -D I2C_SCL=18 -D I2C_SDA1=4 -D I2C_SCL1=3 [env:heltec-v4-tft] custom_meshtastic_hw_model = 110 custom_meshtastic_hw_model_slug = HELTEC_V4 custom_meshtastic_architecture = esp32-s3 custom_meshtastic_actively_supported = true custom_meshtastic_support_level = 1 custom_meshtastic_display_name = Heltec V4 TFT custom_meshtastic_images = heltec_v4.svg custom_meshtastic_tags = Heltec custom_meshtastic_requires_dfu = true custom_meshtastic_partition_scheme = 16MB extends = heltec_v4_base build_flags = ${heltec_v4_base.build_flags} ;-Os -D HELTEC_V4_TFT -D I2C_SDA=4 -D I2C_SCL=3 -D I2C_SDA1=47 -D I2C_SCL1=48 -D PIN_BUTTON2=35 -D PIN_BUZZER=6 -D USE_PIN_BUZZER=PIN_BUZZER -D CONFIG_ARDUHAL_LOG_COLORS -D RADIOLIB_DEBUG_SPI=0 -D RADIOLIB_DEBUG_PROTOCOL=0 -D RADIOLIB_DEBUG_BASIC=0 -D RADIOLIB_VERBOSE_ASSERT=0 -D RADIOLIB_SPI_PARANOID=0 -D CONFIG_DISABLE_HAL_LOCKS=1 -D INPUTDRIVER_BUTTON_TYPE=0 -D HAS_SCREEN=1 -D HAS_TFT=1 -D RAM_SIZE=1860 -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 USE_PACKET_API -D LGFX_DRIVER=LGFX_HELTEC_V4_TFT -D GFX_DRIVER_INC=\"graphics/LGFX/LGFX_HELTEC_V4_TFT.h\" -D VIEW_240x320 -D DISPLAY_SET_RESOLUTION -D DISPLAY_SIZE=240x320 ; portrait mode -D LGFX_PIN_SCK=17 -D LGFX_PIN_MOSI=33 -D LGFX_PIN_DC=16 -D LGFX_PIN_CS=15 -D LGFX_PIN_BL=21 -D LGFX_PIN_RST=18 -D CUSTOM_TOUCH_DRIVER -D TOUCH_SDA_PIN=I2C_SDA1 -D TOUCH_SCL_PIN=I2C_SCL1 -D TOUCH_INT_PIN=-1 ;45 -D TOUCH_RST_PIN=44 ;base UI -D TFT_CS=LGFX_PIN_CS -D ST7789_CS=TFT_CS -D ST7789_RS=LGFX_PIN_DC -D ST7789_SDA=LGFX_PIN_MOSI -D ST7789_SCK=LGFX_PIN_SCK -D ST7789_RESET=LGFX_PIN_RST -D ST7789_MISO=-1 -D ST7789_BUSY=-1 -D ST7789_BL=LGFX_PIN_BL -D ST7789_SPI_HOST=SPI3_HOST -D TFT_BL=ST7789_BL -D SPI_FREQUENCY=40000000 -D SPI_READ_FREQUENCY=4000000 -D TFT_HEIGHT=320 -D TFT_WIDTH=240 -D TFT_OFFSET_X=0 -D TFT_OFFSET_Y=0 -D TFT_OFFSET_ROTATION=0 -D SCREEN_ROTATE -D SCREEN_TRANSITION_FRAMERATE=5 -D BRIGHTNESS_DEFAULT=130 ; Medium Low Brightness -D HAS_TOUCHSCREEN=1 -D TOUCH_I2C_PORT=0 -D TOUCH_SLAVE_ADDRESS=0x2E -D SCREEN_TOUCH_INT=TOUCH_INT_PIN -D SCREEN_TOUCH_RST=TOUCH_RST_PIN lib_deps = ${heltec_v4_base.lib_deps} ${device-ui_base.lib_deps} # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX lovyan03/LovyanGFX@1.2.19 # renovate: datasource=git-refs depName=Quency-D_chsc6x packageName=https://github.com/Quency-D/chsc6x gitBranch=master https://github.com/Quency-D/chsc6x/archive/5cbead829d6b432a8d621ed1aafd4eb474fd4f27.zip ================================================ FILE: variants/esp32s3/heltec_v4/variant.h ================================================ #define VEXT_ENABLE 36 // active low, powers the oled display and the lora antenna boost #define BUTTON_PIN 0 #define ADC_CTRL 37 #define ADC_CTRL_ENABLED HIGH #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 TCXO 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 // Enable Traffic Management Module for Heltec V4 #ifndef HAS_TRAFFIC_MANAGEMENT #define HAS_TRAFFIC_MANAGEMENT 1 #endif #ifndef TRAFFIC_MANAGEMENT_CACHE_SIZE #define TRAFFIC_MANAGEMENT_CACHE_SIZE 2048 #endif // ---- GC1109 RF FRONT END CONFIGURATION ---- // The Heltec V4.2 uses a GC1109 FEM chip with integrated PA and LNA // RF path: SX1262 -> Pi attenuator -> GC1109 PA -> Antenna // Measured net TX gain (non-linear due to PA compression): // +11dB at 0-15dBm input (e.g., 10dBm in -> 21dBm out) // +10dB at 16-17dBm input // +9dB at 18-19dBm input // +7dB at 21dBm input (e.g., 21dBm in -> 28dBm out max) // Control logic (from GC1109 datasheet): // Shutdown: CSD=0, CTX=X, CPS=X // Receive LNA: CSD=1, CTX=0, CPS=X (17dB gain, 2dB NF) // Transmit bypass: CSD=1, CTX=1, CPS=0 (~1dB loss, no PA) // Transmit PA: CSD=1, CTX=1, CPS=1 (full PA enabled) // Pin mapping: // CTX (pin 6) -> SX1262 DIO2: TX/RX path select (automatic via SX126X_DIO2_AS_RF_SWITCH) // CSD (pin 4) -> GPIO2: Chip enable (HIGH=on, LOW=shutdown) // CPS (pin 5) -> GPIO46: PA mode select (HIGH=full PA, LOW=bypass) // VCC0/VCC1 -> Vfem via U3 LDO, controlled by GPIO7 // GC1109 FEM: TX/RX path switching is handled by DIO2 -> CTX pin (via SX126X_DIO2_AS_RF_SWITCH) // Do NOT use SX126X_TXEN/RXEN as that would cause double-control of GPIO46 #define LORA_PA_POWER 7 // VFEM_Ctrl - GC1109 and KCT8103L LDO power enable #define LORA_GC1109_PA_EN 2 // CSD - GC1109 chip enable (HIGH=on) #define LORA_GC1109_PA_TX_EN 46 // CPS - GC1109 PA mode (HIGH=full PA, LOW=bypass) // ---- KCT8103L RF FRONT END CONFIGURATION ---- // The Heltec V4.3 uses a KCT8103L FEM chip with integrated PA and LNA // RF path: SX1262 -> Pi attenuator -> KCT8103L PA -> Antenna // Control logic (from KCT8103L datasheet): // Transmit PA: CSD=1, CTX=1, CPS=1 // Receive LNA: CSD=1, CTX=0, CPS=X (21dB gain, 1.9dB NF) // Receive bypass: CSD=1, CTX=1, CPS=0 // Shutdown: CSD=0, CTX=X, CPS=X // Pin mapping: // CPS (pin 5) -> SX1262 DIO2: TX/RX path select (automatic via SX126X_DIO2_AS_RF_SWITCH) // CSD (pin 4) -> GPIO2: Chip enable (HIGH=on, LOW=shutdown) // CTX (pin 6) -> GPIO5: Switch between Receive LNA Mode and Receive Bypass Mode. (HIGH=RX bypass, LOW=RX LNA) // VCC0/VCC1 -> Vfem via U3 LDO, controlled by GPIO7 // KCT8103L FEM: TX/RX path switching is handled by DIO2 -> CPS pin (via SX126X_DIO2_AS_RF_SWITCH) #define LORA_KCT8103L_PA_CSD 2 // CSD - KCT8103L chip enable (HIGH=on) #define LORA_KCT8103L_PA_CTX 5 // CTX - Switch between Receive LNA Mode and Receive Bypass Mode. (HIGH=RX bypass, LOW=RX LNA) #if HAS_TFT #define USE_TFTDISPLAY 1 #endif /* * GPS pins */ #define GPS_L76K #define PIN_GPS_RESET (42) // 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 (34) #define GPS_EN_ACTIVE LOW #define PERIPHERAL_WARMUP_MS 1000 // Make sure I2C QuickLink has stable power before continuing #define PIN_GPS_STANDBY (40) // An output to wake GPS, low means allow sleep, high means force wake #define PIN_GPS_PPS (41) // Seems to be missing on this new board #define GPS_TX_PIN (38) // This is for bits going TOWARDS the CPU #define GPS_RX_PIN (39) // This is for bits going TOWARDS the GPS #define GPS_THREAD_INTERVAL 50 ================================================ FILE: variants/esp32s3/heltec_vision_master_e213/einkDetect.h ================================================ #pragma once #include "configuration.h" enum class EInkDetectionResult : uint8_t { LCMEN213EFC1 = 0, // Initial version E0213A367 = 1, // E213 PCB marked V1.1 (Mid 2025) }; EInkDetectionResult detectEInk() { // Test 1: Logic of BUSY pin // Determines controller IC manufacturer // Fitipower: busy when LOW // Solomon Systech: busy when HIGH // Force display BUSY by holding reset pin active pinMode(PIN_EINK_RES, OUTPUT); digitalWrite(PIN_EINK_RES, LOW); delay(10); // Read whether pin is HIGH or LOW while busy pinMode(PIN_EINK_BUSY, INPUT); bool busyLogic = digitalRead(PIN_EINK_BUSY); // Test complete. Release pin pinMode(PIN_EINK_RES, INPUT); if (busyLogic == LOW) return EInkDetectionResult::LCMEN213EFC1; else // busy HIGH return EInkDetectionResult::E0213A367; } ================================================ FILE: variants/esp32s3/heltec_vision_master_e213/nicheGraphics.h ================================================ #pragma once #include "configuration.h" #ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS // InkHUD-specific components // --------------------------- #include "graphics/niche/InkHUD/InkHUD.h" // Applets #include "graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h" #include "graphics/niche/InkHUD/Applets/User/DM/DMApplet.h" #include "graphics/niche/InkHUD/Applets/User/FavoritesMap/FavoritesMapApplet.h" #include "graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.h" #include "graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h" #include "graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.h" #include "graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h" // Shared NicheGraphics components // -------------------------------- #include "graphics/niche/Drivers/EInk/E0213A367.h" #include "graphics/niche/Drivers/EInk/LCMEN2R13EFC1.h" #include "graphics/niche/Inputs/TwoButton.h" #include "buzz.h" // Button feedback #include "einkDetect.h" // Detect display model at runtime void setupNicheGraphics() { using namespace NicheGraphics; // Detect E-Ink Model // ------------------- EInkDetectionResult displayModel = detectEInk(); // SPI // ----------------------------- // Display is connected to HSPI SPIClass *hspi = new SPIClass(HSPI); hspi->begin(PIN_EINK_SCLK, -1, PIN_EINK_MOSI, PIN_EINK_CS); // E-Ink Driver // ----------------------------- Drivers::EInk *driver; if (displayModel == EInkDetectionResult::LCMEN213EFC1) // V1 (unmarked) driver = new Drivers::LCMEN213EFC1; else if (displayModel == EInkDetectionResult::E0213A367) // V1.1 driver = new Drivers::E0213A367; driver->begin(hspi, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES); // InkHUD // ---------------------------- InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance(); // Set the E-Ink driver inkhud->setDriver(driver); // Set how many FAST updates per FULL update // Set how unhealthy additional FAST updates beyond this number are if (displayModel == EInkDetectionResult::LCMEN213EFC1) // V1 (unmarked) inkhud->setDisplayResilience(10, 1.5); else if (displayModel == EInkDetectionResult::E0213A367) // V1.1 inkhud->setDisplayResilience(15, 3); // Select fonts InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1252; InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; // Customize default settings inkhud->persistence->settings.userTiles.maxCount = 2; // How many tiles can the display handle? inkhud->persistence->settings.rotation = 3; // 270 degrees clockwise inkhud->persistence->settings.userTiles.count = 1; // One tile only by default, keep things simple for new users inkhud->persistence->settings.optionalMenuItems.nextTile = false; // Behavior handled by aux button instead // Pick applets // Note: order of applets determines priority of "auto-show" feature inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); // Activated, autoshown inkhud->addApplet("DMs", new InkHUD::DMApplet); // - inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); // - inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); // - inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated inkhud->addApplet("Favorites Map", new InkHUD::FavoritesMapApplet); // - inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); // - inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, not autoshown, default on tile 0 // Start running InkHUD inkhud->begin(); // Buttons // -------------------------- Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // Shared NicheGraphics component // #0: Main User Button buttons->setWiring(0, Inputs::TwoButton::getUserButtonPin()); buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); }); buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); // #1: Aux Button buttons->setWiring(1, PIN_BUTTON2); buttons->setHandlerShortPress(1, [inkhud]() { inkhud->nextTile(); playChirp(); }); // Begin handling button events buttons->start(); } #endif ================================================ FILE: variants/esp32s3/heltec_vision_master_e213/pins_arduino.h ================================================ #ifndef Pins_Arduino_h #define Pins_Arduino_h #include 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 */ ================================================ FILE: variants/esp32s3/heltec_vision_master_e213/platformio.ini ================================================ [env:heltec-vision-master-e213] custom_meshtastic_hw_model = 67 custom_meshtastic_hw_model_slug = HELTEC_VISION_MASTER_E213 custom_meshtastic_architecture = esp32-s3 custom_meshtastic_actively_supported = true custom_meshtastic_support_level = 1 custom_meshtastic_display_name = Heltec Vision Master E213 custom_meshtastic_images = heltec-vision-master-e213.svg custom_meshtastic_tags = Heltec custom_meshtastic_requires_dfu = true custom_meshtastic_partition_scheme = 8MB extends = esp32s3_base board = heltec_vision_master_e213 board_build.partitions = default_8MB.csv build_flags = ${esp32s3_base.build_flags} -Ivariants/esp32s3/heltec_vision_master_e213 -DHELTEC_VISION_MASTER_E213 -DUSE_EINK -DGXEPD2_DRIVER_0=GxEPD2_213_FC1 -DGXEPD2_DRIVER_1=GxEPD2_213_E0213A367 -DEINK_WIDTH=250 -DEINK_HEIGHT=122 -DUSE_EINK_DYNAMICDISPLAY ; Enable Dynamic EInk -DEINK_LIMIT_FASTREFRESH=10 ; How many consecutive fast-refreshes are permitted -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" lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master https://github.com/meshtastic/GxEPD2/archive/c7eb4c3c167cf396ef4f541cc5d4c6aa42f3c46b.zip upload_speed = 115200 [env:heltec-vision-master-e213-inkhud] extends = esp32s3_base, inkhud board = heltec_vision_master_e213 board_level = pr board_build.partitions = default_8MB.csv build_src_filter = ${esp32s3_base.build_src_filter} ${inkhud.build_src_filter} build_flags = ${esp32s3_base.build_flags} ${inkhud.build_flags} -I variants/esp32s3/heltec_vision_master_e213 -D HELTEC_VISION_MASTER_E213 lib_deps = ${inkhud.lib_deps} ; InkHUD libs first, so we get GFXRoot instead of AdafruitGFX ${esp32s3_base.lib_deps} upload_speed = 921600 ================================================ FILE: variants/esp32s3/heltec_vision_master_e213/variant.h ================================================ #define LED_POWER 45 // LED is not populated on earliest board variant #define BUTTON_PIN 0 #define PIN_BUTTON2 21 // Second built-in button #define ALT_BUTTON_PIN PIN_BUTTON2 // Send the up event // I2C #define I2C_SDA SDA #define I2C_SCL SCL // Display (E-Ink) #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 // 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 ================================================ FILE: variants/esp32s3/heltec_vision_master_e290/nicheGraphics.h ================================================ /* Most of the Meshtastic firmware uses preprocessor macros throughout the code to support different hardware variants. NicheGraphics attempts a different approach: Per-device config takes place in this setupNicheGraphics() method (And a small amount in platformio.ini) This file sets up InkHUD for Heltec VM-E290. Different NicheGraphics UIs and different hardware variants will each have their own setup procedure. */ #pragma once #include "configuration.h" #ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS // InkHUD-specific components // --------------------------- #include "graphics/niche/InkHUD/InkHUD.h" // Applets #include "graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h" #include "graphics/niche/InkHUD/Applets/User/DM/DMApplet.h" #include "graphics/niche/InkHUD/Applets/User/FavoritesMap/FavoritesMapApplet.h" #include "graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.h" #include "graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h" #include "graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.h" #include "graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h" // Shared NicheGraphics components // -------------------------------- #include "graphics/niche/Drivers/EInk/DEPG0290BNS800.h" #include "graphics/niche/Inputs/TwoButton.h" // Button feedback #include "buzz.h" void setupNicheGraphics() { using namespace NicheGraphics; // SPI // ----------------------------- // Display is connected to HSPI SPIClass *hspi = new SPIClass(HSPI); hspi->begin(PIN_EINK_SCLK, -1, PIN_EINK_MOSI, PIN_EINK_CS); // E-Ink Driver // ----------------------------- Drivers::EInk *driver = new Drivers::DEPG0290BNS800; driver->begin(hspi, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY); // InkHUD // ---------------------------- InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance(); // Set the E-Ink driver inkhud->setDriver(driver); // Set how many FAST updates per FULL update // Set how unhealthy additional FAST updates beyond this number are inkhud->setDisplayResilience(7, 1.5); // Select fonts InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1252; InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; // Customize default settings inkhud->persistence->settings.userTiles.maxCount = 2; // How many tiles can the display handle? inkhud->persistence->settings.rotation = 1; // 90 degrees clockwise inkhud->persistence->settings.userTiles.count = 1; // One tile only by default, keep things simple for new users inkhud->persistence->settings.optionalMenuItems.nextTile = false; // Behavior handled by aux button instead // Pick applets // Note: order of applets determines priority of "auto-show" feature inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); // Activated, autoshown inkhud->addApplet("DMs", new InkHUD::DMApplet); // - inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); // - inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); // - inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated inkhud->addApplet("Favorites Map", new InkHUD::FavoritesMapApplet); // - inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); // - inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, not autoshown, default on tile 0 // Start running InkHUD inkhud->begin(); // Buttons // -------------------------- Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // A shared NicheGraphics component // #0: Main User Button buttons->setWiring(0, Inputs::TwoButton::getUserButtonPin()); buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); }); buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); // #1: Aux Button buttons->setWiring(1, PIN_BUTTON2); buttons->setHandlerShortPress(1, [inkhud]() { inkhud->nextTile(); playChirp(); }); // Begin handling button events buttons->start(); } #endif ================================================ FILE: variants/esp32s3/heltec_vision_master_e290/pins_arduino.h ================================================ #ifndef Pins_Arduino_h #define Pins_Arduino_h #include 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 */ ================================================ FILE: variants/esp32s3/heltec_vision_master_e290/platformio.ini ================================================ ; Using the original screen class [env:heltec-vision-master-e290] custom_meshtastic_hw_model = 68 custom_meshtastic_hw_model_slug = HELTEC_VISION_MASTER_E290 custom_meshtastic_architecture = esp32-s3 custom_meshtastic_actively_supported = true custom_meshtastic_support_level = 1 custom_meshtastic_display_name = Heltec Vision Master E290 custom_meshtastic_images = heltec-vision-master-e290.svg custom_meshtastic_tags = Heltec custom_meshtastic_requires_dfu = true custom_meshtastic_partition_scheme = 8MB extends = esp32s3_base board = heltec_vision_master_e290 board_build.partitions = default_8MB.csv build_flags = ${esp32s3_base.build_flags} -I variants/esp32s3/heltec_vision_master_e290 -D DISPLAY_FLIP_SCREEN ; Orient so the LoRa antenna faces up -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 -D USE_EINK_DYNAMICDISPLAY ; Enable Dynamic EInk -D EINK_LIMIT_FASTREFRESH=10 ; How many consecutive fast-refreshes are permitted -D EINK_HASQUIRK_GHOSTING ; Display model is identified as "prone to ghosting" lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master https://github.com/meshtastic/GxEPD2/archive/c7eb4c3c167cf396ef4f541cc5d4c6aa42f3c46b.zip upload_speed = 115200 [env:heltec-vision-master-e290-inkhud] extends = esp32s3_base, inkhud board = heltec_vision_master_e290 board_build.partitions = default_8MB.csv build_src_filter = ${esp32s3_base.build_src_filter} ${inkhud.build_src_filter} build_flags = ${esp32s3_base.build_flags} ${inkhud.build_flags} -I variants/esp32s3/heltec_vision_master_e290 -D HELTEC_VISION_MASTER_E290 lib_deps = ${inkhud.lib_deps} ; InkHUD libs first, so we get GFXRoot instead of AdafruitGFX ${esp32s3_base.lib_deps} upload_speed = 921600 ================================================ FILE: variants/esp32s3/heltec_vision_master_e290/variant.h ================================================ #define LED_POWER 45 // LED is not populated on earliest board variant #define BUTTON_PIN 0 #define PIN_BUTTON2 21 // Second built-in button #define ALT_BUTTON_PIN PIN_BUTTON2 // Send the up event // I2C #define I2C_SDA SDA #define I2C_SCL SCL // Display (E-Ink) #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 // 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 ================================================ FILE: variants/esp32s3/heltec_vision_master_t190/pins_arduino.h ================================================ #ifndef Pins_Arduino_h #define Pins_Arduino_h #include 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 */ ================================================ FILE: variants/esp32s3/heltec_vision_master_t190/platformio.ini ================================================ [env:heltec-vision-master-t190] custom_meshtastic_hw_model = 66 custom_meshtastic_hw_model_slug = HELTEC_VISION_MASTER_T190 custom_meshtastic_architecture = esp32-s3 custom_meshtastic_actively_supported = true custom_meshtastic_support_level = 1 custom_meshtastic_display_name = Heltec Vision Master T190 custom_meshtastic_images = heltec-vision-master-t190.svg custom_meshtastic_tags = Heltec custom_meshtastic_requires_dfu = true custom_meshtastic_partition_scheme = 8MB extends = esp32s3_base board = heltec_vision_master_t190 board_build.partitions = default_8MB.csv build_flags = ${esp32s3_base.build_flags} -I variants/esp32s3/heltec_vision_master_t190 -D HELTEC_VISION_MASTER_T190 lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-st7789 packageName=https://github.com/meshtastic/st7789 gitBranch=main https://github.com/meshtastic/st7789/archive/9ee76d6b18b9a8f45a2c5cae06b1134a587691eb.zip upload_speed = 921600 ================================================ FILE: variants/esp32s3/heltec_vision_master_t190/variant.h ================================================ #ifndef HAS_TFT #define BUTTON_PIN 0 #define PIN_BUTTON2 21 // Second built-in button #define ALT_BUTTON_PIN PIN_BUTTON2 // Send the up event // 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 // 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 #endif // HAS_TFT ================================================ FILE: variants/esp32s3/heltec_wireless_paper/einkDetect.h ================================================ #pragma once #include "configuration.h" enum class EInkDetectionResult : uint8_t { LCMEN213EFC1 = 0, // V1.1 E0213A367 = 1, // V1.1.1, V1.2 }; EInkDetectionResult detectEInk() { // Test 1: Logic of BUSY pin // Determines controller IC manufacturer // Fitipower: busy when LOW // Solomon Systech: busy when HIGH // Force display BUSY by holding reset pin active pinMode(PIN_EINK_RES, OUTPUT); digitalWrite(PIN_EINK_RES, LOW); delay(10); // Read whether pin is HIGH or LOW while busy pinMode(PIN_EINK_BUSY, INPUT); bool busyLogic = digitalRead(PIN_EINK_BUSY); // Test complete. Release pin pinMode(PIN_EINK_RES, INPUT); if (busyLogic == LOW) return EInkDetectionResult::LCMEN213EFC1; else // busy HIGH return EInkDetectionResult::E0213A367; } ================================================ FILE: variants/esp32s3/heltec_wireless_paper/nicheGraphics.h ================================================ #pragma once #include "configuration.h" #ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS // InkHUD-specific components // --------------------------- #include "graphics/niche/InkHUD/InkHUD.h" // Applets #include "graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h" #include "graphics/niche/InkHUD/Applets/User/DM/DMApplet.h" #include "graphics/niche/InkHUD/Applets/User/FavoritesMap/FavoritesMapApplet.h" #include "graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.h" #include "graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h" #include "graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.h" #include "graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h" // Shared NicheGraphics components // -------------------------------- #include "graphics/niche/Drivers/EInk/E0213A367.h" #include "graphics/niche/Drivers/EInk/LCMEN2R13EFC1.h" #include "graphics/niche/Inputs/TwoButton.h" #include "einkDetect.h" // Detect display model at runtime void setupNicheGraphics() { using namespace NicheGraphics; // Detect E-Ink Model // ------------------- EInkDetectionResult displayModel = detectEInk(); // SPI // ----------------------------- // Display is connected to HSPI SPIClass *hspi = new SPIClass(HSPI); hspi->begin(PIN_EINK_SCLK, -1, PIN_EINK_MOSI, PIN_EINK_CS); // E-Ink Driver // ----------------------------- Drivers::EInk *driver; if (displayModel == EInkDetectionResult::LCMEN213EFC1) // V1.1 driver = new Drivers::LCMEN213EFC1; else if (displayModel == EInkDetectionResult::E0213A367) // V1.1.1, V1.2 driver = new Drivers::E0213A367; driver->begin(hspi, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES); // InkHUD // ---------------------------- InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance(); // Set the E-Ink driver inkhud->setDriver(driver); // Set how many FAST updates per FULL update // Set how unhealthy additional FAST updates beyond this number are if (displayModel == EInkDetectionResult::LCMEN213EFC1) // V1.1 (unmarked) inkhud->setDisplayResilience(10, 1.5); else if (displayModel == EInkDetectionResult::E0213A367) // V1.1.1, V1.2 inkhud->setDisplayResilience(15, 3); // Select fonts InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1252; InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; // Customize default settings inkhud->persistence->settings.userTiles.maxCount = 2; // How many tiles can the display handle? inkhud->persistence->settings.rotation = 3; // 270 degrees clockwise inkhud->persistence->settings.userTiles.count = 1; // One tile only by default, keep things simple for new users // Pick applets // Note: order of applets determines priority of "auto-show" feature inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); // Activated, autoshown inkhud->addApplet("DMs", new InkHUD::DMApplet); // - inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); // - inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); // - inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); // - inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, not autoshown, default on tile 0 inkhud->addApplet("Favorites Map", new InkHUD::FavoritesMapApplet, false, false); // - // Start running InkHUD inkhud->begin(); // Buttons // -------------------------- Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // Shared NicheGraphics component // #0: Main User Button buttons->setWiring(0, Inputs::TwoButton::getUserButtonPin()); buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); }); buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); // No aux button on this board // Begin handling button events buttons->start(); } #endif ================================================ FILE: variants/esp32s3/heltec_wireless_paper/pins_arduino.h ================================================ #ifndef Pins_Arduino_h #define Pins_Arduino_h #include 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 */ ================================================ FILE: variants/esp32s3/heltec_wireless_paper/platformio.ini ================================================ ; Using the original screen class [env:heltec-wireless-paper] custom_meshtastic_hw_model = 49 custom_meshtastic_hw_model_slug = HELTEC_WIRELESS_PAPER custom_meshtastic_architecture = esp32-s3 custom_meshtastic_actively_supported = true custom_meshtastic_support_level = 1 custom_meshtastic_display_name = Heltec Wireless Paper custom_meshtastic_images = heltec-wireless-paper.svg custom_meshtastic_tags = Heltec custom_meshtastic_partition_scheme = 8MB extends = esp32s3_base board = heltec_wifi_lora_32_V3 board_build.partitions = default_8MB.csv build_flags = ${esp32s3_base.build_flags} -I variants/esp32s3/heltec_wireless_paper -D HELTEC_WIRELESS_PAPER -D GXEPD2_DRIVER_0=GxEPD2_213_FC1 -D GXEPD2_DRIVER_1=GxEPD2_213_E0213A367 -D EINK_WIDTH=250 -D EINK_HEIGHT=122 -D USE_EINK -D USE_EINK_DYNAMICDISPLAY ; Enable Dynamic EInk -D EINK_LIMIT_FASTREFRESH=10 ; How many consecutive fast-refreshes are permitted -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" lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master https://github.com/meshtastic/GxEPD2/archive/c7eb4c3c167cf396ef4f541cc5d4c6aa42f3c46b.zip upload_speed = 115200 [env:heltec-wireless-paper-inkhud] extends = esp32s3_base, inkhud board = heltec_wifi_lora_32_V3 board_build.partitions = default_8MB.csv build_src_filter = ${esp32s3_base.build_src_filter} ${inkhud.build_src_filter} build_flags = ${esp32s3_base.build_flags} ${inkhud.build_flags} -I variants/esp32s3/heltec_wireless_paper -D HELTEC_WIRELESS_PAPER lib_deps = ${inkhud.lib_deps} ; InkHUD libs first, so we get GFXRoot instead of AdafruitGFX ${esp32s3_base.lib_deps} upload_speed = 921600 ================================================ FILE: variants/esp32s3/heltec_wireless_paper/variant.h ================================================ #define LED_POWER 18 #define BUTTON_PIN 0 // I2C #define I2C_SDA SDA #define I2C_SCL SCL // Display (E-Ink) #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 ADC_CTRL_ENABLED LOW #define NO_EXT_GPIO 1 #define NO_GPS 1 // 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 ================================================ FILE: variants/esp32s3/heltec_wireless_paper_v1/pins_arduino.h ================================================ #ifndef Pins_Arduino_h #define Pins_Arduino_h #include 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 */ ================================================ FILE: variants/esp32s3/heltec_wireless_paper_v1/platformio.ini ================================================ [env:heltec-wireless-paper-v1_0] custom_meshtastic_hw_model = 57 custom_meshtastic_hw_model_slug = HELTEC_WIRELESS_PAPER_V1_0 custom_meshtastic_architecture = esp32-s3 custom_meshtastic_actively_supported = false custom_meshtastic_support_level = 3 custom_meshtastic_display_name = Heltec Wireless Paper V1.0 custom_meshtastic_images = heltec-wireless-paper-v1_0.svg custom_meshtastic_tags = Heltec custom_meshtastic_partition_scheme = 8MB extends = esp32s3_base board_level = extra board = heltec_wifi_lora_32_V3 board_build.partitions = default_8MB.csv build_flags = ${esp32s3_base.build_flags} -I variants/esp32s3/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_GHOSTING_PX=2000 ; (Optional) How much image ghosting is tolerated lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master https://github.com/meshtastic/GxEPD2/archive/c7eb4c3c167cf396ef4f541cc5d4c6aa42f3c46b.zip upload_speed = 115200 ================================================ FILE: variants/esp32s3/heltec_wireless_paper_v1/variant.h ================================================ #define LED_POWER 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 ADC_CTRL_ENABLED LOW #define NO_EXT_GPIO 1 #define NO_GPS 1 // 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 ================================================ FILE: variants/esp32s3/heltec_wireless_tracker/pins_arduino.h ================================================ #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 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 */ ================================================ FILE: variants/esp32s3/heltec_wireless_tracker/platformio.ini ================================================ [env:heltec-wireless-tracker] custom_meshtastic_hw_model = 48 custom_meshtastic_hw_model_slug = HELTEC_WIRELESS_TRACKER custom_meshtastic_architecture = esp32-s3 custom_meshtastic_actively_supported = true custom_meshtastic_support_level = 1 custom_meshtastic_display_name = Heltec Wireless Tracker V1.1 custom_meshtastic_images = heltec-wireless-tracker.svg custom_meshtastic_tags = Heltec custom_meshtastic_requires_dfu = true custom_meshtastic_partition_scheme = 8MB extends = esp32s3_base board = heltec_wireless_tracker board_build.partitions = default_8MB.csv upload_protocol = esptool build_flags = ${esp32s3_base.build_flags} -I variants/esp32s3/heltec_wireless_tracker -D HELTEC_TRACKER_V1_1 ;-D DEBUG_DISABLED ; uncomment this line to disable DEBUG output lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX lovyan03/LovyanGFX@1.2.19 ================================================ FILE: variants/esp32s3/heltec_wireless_tracker/variant.h ================================================ #define LED_POWER 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 #define USE_TFTDISPLAY 1 // pin 3 is Vext on v1.1 - HIGH enables LDO for Vext rail which goes to: // GPS UC6580: GPS V_DET(8), VDD_IO(7), DCDC_IN(21), pulls up RESETN(17), D_SEL(33) and BOOT_MODE(34) through 10kR // GPS LNA SW7125DE: VCC(4), pulls up SHDN(5) through 10kR // LED: VDD, LEDA (through diode) #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 ================================================ FILE: variants/esp32s3/heltec_wireless_tracker_V1_0/pins_arduino.h ================================================ #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 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 */ ================================================ FILE: variants/esp32s3/heltec_wireless_tracker_V1_0/platformio.ini ================================================ [env:heltec-wireless-tracker-V1-0] custom_meshtastic_hw_model = 58 custom_meshtastic_hw_model_slug = HELTEC_WIRELESS_TRACKER_V1_0 custom_meshtastic_architecture = esp32-s3 custom_meshtastic_actively_supported = false custom_meshtastic_support_level = 3 custom_meshtastic_display_name = Heltec Wireless Tracker V1.0 custom_meshtastic_images = heltec-wireless-tracker.svg custom_meshtastic_requires_dfu = true custom_meshtastic_partition_scheme = 8MB extends = esp32s3_base board_level = extra board = heltec_wireless_tracker board_build.partitions = default_8MB.csv upload_protocol = esptool build_flags = ${esp32s3_base.build_flags} -I variants/esp32s3/heltec_wireless_tracker_V1_0 -D HELTEC_TRACKER_V1_0 ;-D DEBUG_DISABLED ; uncomment this line to disable DEBUG output lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX lovyan03/LovyanGFX@1.2.19 ================================================ FILE: variants/esp32s3/heltec_wireless_tracker_V1_0/variant.h ================================================ #define LED_POWER 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 FORCE_LOW_RES 1 #define USE_TFTDISPLAY 1 #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 ================================================ FILE: variants/esp32s3/heltec_wireless_tracker_v2/pins_arduino.h ================================================ #ifndef Pins_Arduino_h #define Pins_Arduino_h #include "soc/soc_caps.h" #include #define DISPLAY_HEIGHT 80 #define DISPLAY_WIDTH 160 #define USB_VID 0x303a #define USB_PID 0x1001 static const uint8_t TX = 43; static const uint8_t RX = 44; static const uint8_t SDA = 6; static const uint8_t SCL = 17; 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 = 3; 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 */ ================================================ FILE: variants/esp32s3/heltec_wireless_tracker_v2/platformio.ini ================================================ [env:heltec-wireless-tracker-v2] custom_meshtastic_support_level = 1 custom_meshtastic_images = heltec_wireless_tracker_v2.svg custom_meshtastic_tags = Heltec extends = esp32s3_base board = heltec_wireless_tracker_v2 board_build.partitions = default_8MB.csv upload_protocol = esptool custom_meshtastic_hw_model = 113 custom_meshtastic_hw_model_slug = HELTEC_WIRELESS_TRACKER_V2 custom_meshtastic_architecture = esp32s3 custom_meshtastic_display_name = Heltec Wireless Tracker V2 custom_meshtastic_actively_supported = true build_flags = ${esp32s3_base.build_flags} -I variants/esp32s3/heltec_wireless_tracker_v2 -D HELTEC_WIRELESS_TRACKER_V2 -D HAS_LORA_FEM=1 lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX lovyan03/LovyanGFX@1.2.19 ================================================ FILE: variants/esp32s3/heltec_wireless_tracker_v2/variant.h ================================================ #define LED_POWER 18 #define _VARIANT_HELTEC_WIRELESS_TRACKER // 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 #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 24 #define TFT_OFFSET_Y 0 #define TFT_INVERT false #define SCREEN_TRANSITION_FRAMERATE 3 // fps #define DISPLAY_FORCE_SMALL_FONTS #define USE_TFTDISPLAY 1 #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. #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 v2, 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 TCXO 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 // ---- KCT8103L RF FRONT END CONFIGURATION ---- // The heltec_wireless_tracker_v2 uses a KCT8103L FEM chip with integrated PA and LNA // RF path: SX1262 -> Pi attenuator -> KCT8103L PA -> Antenna // Control logic (from KCT8103L datasheet): // Transmit PA: CSD=1, CTX=1, CPS=1 // Receive LNA: CSD=1, CTX=0, CPS=X (21dB gain, 1.9dB NF) // Receive bypass: CSD=1, CTX=1, CPS=0 // Shutdown: CSD=0, CTX=X, CPS=X // Pin mapping: // CPS (pin 5) -> SX1262 DIO2: TX/RX path select (automatic via SX126X_DIO2_AS_RF_SWITCH) // CSD (pin 4) -> GPIO4: Chip enable (HIGH=on, LOW=shutdown) // CTX (pin 6) -> GPIO5: Switch between Receive LNA Mode and Receive Bypass Mode. (HIGH=RX bypass, LOW=RX LNA) // VCC0/VCC1 -> Vfem via U3 LDO, controlled by GPIO7 // KCT8103L FEM: TX/RX path switching is handled by DIO2 -> CPS pin (via SX126X_DIO2_AS_RF_SWITCH) #define USE_KCT8103L_PA #define LORA_PA_POWER 7 // VFEM_Ctrl - KCT8103L LDO power enable #define LORA_KCT8103L_PA_CSD 4 // CSD - KCT8103L chip enable (HIGH=on) #define LORA_KCT8103L_PA_CTX 5 // CTX - Switch between Receive LNA Mode and Receive Bypass Mode. (HIGH=RX bypass, LOW=RX LNA) ================================================ FILE: variants/esp32s3/heltec_wsl_v3/platformio.ini ================================================ [env:heltec-wsl-v3] custom_meshtastic_hw_model = 44 custom_meshtastic_hw_model_slug = HELTEC_WSL_V3 custom_meshtastic_architecture = esp32-s3 custom_meshtastic_actively_supported = true custom_meshtastic_support_level = 1 custom_meshtastic_display_name = Heltec Wireless Stick Lite V3 custom_meshtastic_images = heltec-wsl-v3.svg custom_meshtastic_tags = Heltec custom_meshtastic_partition_scheme = 8MB extends = esp32s3_base board = heltec_wifi_lora_32_V3 board_build.partitions = default_8MB.csv # Temporary until espressif creates a release with this new target build_flags = ${esp32s3_base.build_flags} -D HELTEC_WSL_V3 -I variants/esp32s3/heltec_wsl_v3 -ULED_BUILTIN ================================================ FILE: variants/esp32s3/heltec_wsl_v3/variant.h ================================================ #define I2C_SCL SCL #define I2C_SDA SDA #define LED_POWER 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 ================================================ FILE: variants/esp32s3/icarus/pins_arduino.h ================================================ #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 */ ================================================ FILE: variants/esp32s3/icarus/platformio.ini ================================================ [env:icarus] extends = esp32s3_base board = icarus board_level = extra board_check = true board_build.mcu = esp32s3 board_build.partitions = default_8MB.csv upload_protocol = esptool upload_speed = 921600 build_unflags = ${esp32s3_base.build_unflags} -DARDUINO_USB_MODE=1 build_flags = ${esp32s3_base.build_flags} -D PRIVATE_HW -I variants/esp32s3/icarus -DBOARD_HAS_PSRAM -DARDUINO_USB_MODE=0 ================================================ FILE: variants/esp32s3/icarus/variant.h ================================================ // 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 ================================================ FILE: variants/esp32s3/link32_s3_v1/pins_arduino.h ================================================ #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 = 47; static const uint8_t SCL = 48; // Default SPI will be mapped to Radio static const uint8_t SS = 21; static const uint8_t MOSI = 34; static const uint8_t MISO = 33; static const uint8_t SCK = 16; #endif /* Pins_Arduino_h */ ================================================ FILE: variants/esp32s3/link32_s3_v1/platformio.ini ================================================ [env:link32-s3-v1] extends = esp32s3_base board = esp32-s3-devkitc-1 board_level = extra build_flags = ${esp32s3_base.build_flags} -D LINK_32 -I variants/esp32s3/link32_s3_v1 -DARDUINO_USB_CDC_ON_BOOT -DARDUINO_USB_MODE=1 -DRADIOLIB_EXCLUDE_SX128X=1 -DRADIOLIB_EXCLUDE_SX127X=1 -DRADIOLIB_EXCLUDE_LR11X0=1 ================================================ FILE: variants/esp32s3/link32_s3_v1/variant.h ================================================ #define BATTERY_PIN 15 #define ADC_CHANNEL ADC2_GPIO15_CHANNEL // ADC channel for battery voltage measurement #define BATTERY_SENSE_SAMPLES 30 #define BAT_MEASURE_ADC_UNIT 2 // Use ADC2 for battery measurement #define USE_SSD1306 #define BUTTON_PIN 0 // Button pin for this board #define CANCEL_BUTTON_PIN 36 #define CANCEL_BUTTON_ACTIVE_LOW true #define CANCEL_BUTTON_ACTIVE_PULLUP true #define HAS_NEOPIXEL // If defined, we will use the neopixel library #define NEOPIXEL_DATA 35 // Neopixel pin for this board #define NEOPIXEL_COUNT 1 // Number of neopixels on this board #define NEOPIXEL_TYPE (NEO_GRB + NEO_KHZ800) // type of neopixels in use #define ADC_MULTIPLIER 2 #define I2C_SDA 47 // I2C pins for this board #define I2C_SCL 48 #define USE_SX1262 #define LORA_SCK 16 #define LORA_MISO 33 #define LORA_MOSI 34 #define LORA_CS 21 #define LORA_RESET 18 #define LORA_DIO0 12 // a No connect on the SX1262 module #define LORA_DIO1 13 #define LORA_DIO2 14 // Not really used #define LORA_TCXO_GPIO 17 #define TCXO_OPTIONAL #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 ================================================ FILE: variants/esp32s3/m5stack_cardputer_adv/pins_arduino.h ================================================ #ifndef Pins_Arduino_h #define Pins_Arduino_h #include "soc/soc_caps.h" #include #define USB_VID 0x303a // USB JTAG/serial debug unit ID #define USB_PID 0x1001 // USB JTAG/serial debug unit ID static const uint8_t SS = 5; static const uint8_t SDA = 8; static const uint8_t SCL = 9; static const uint8_t ADC = 10; static const uint8_t TXD2 = 13; static const uint8_t MOSI = 14; static const uint8_t RXD2 = 15; static const uint8_t MISO = 39; static const uint8_t SCK = 40; #endif ================================================ FILE: variants/esp32s3/m5stack_cardputer_adv/platformio.ini ================================================ ; M5stack Cardputer Advanced [env:m5stack-cardputer-adv] extends = esp32s3_base board = m5stack-stamps3 board_check = true board_build.partitions = default_8MB.csv upload_protocol = esptool build_flags = ${esp32s3_base.build_flags} -D M5STACK_CARDPUTER_ADV -D BOARD_HAS_PSRAM -D ARDUINO_USB_CDC_ON_BOOT=1 -I variants/esp32s3/m5stack_cardputer_adv lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-st7789 packageName=https://github.com/meshtastic/st7789 gitBranch=main https://github.com/meshtastic/st7789/archive/9ee76d6b18b9a8f45a2c5cae06b1134a587691eb.zip # # renovate: datasource=github-tags depName=pschatzmann_arduino-audio-driver packageName=pschatzmann/arduino-audio-driver # https://github.com/pschatzmann/arduino-audio-driver/archive/v0.2.1.zip # renovate: datasource=git-refs depName=ESP8266Audio packageName=https://github.com/meshtastic/ESP8266Audio gitBranch=meshtastic-2.0.0-dacfix https://github.com/meshtastic/ESP8266Audio/archive/343024632ee78d6216907b2353fc943a62422d80.zip # renovate: datasource=custom.pio depName=ESP8266SAM packageName=earlephilhower/library/ESP8266SAM earlephilhower/ESP8266SAM@1.1.0 # renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel adafruit/Adafruit NeoPixel@1.15.4 ================================================ FILE: variants/esp32s3/m5stack_cardputer_adv/variant.h ================================================ #define USE_ST7789 #define ST7789_NSS 37 #define ST7789_RS 34 // DC #define ST7789_SDA 35 // MOSI #define ST7789_SCK 36 #define ST7789_RESET 33 #define ST7789_MISO -1 #define ST7789_BUSY -1 // #define VTFT_CTRL 38 #define VTFT_LEDA 38 // #define ST7789_BL (32+6) #define ST7789_SPI_HOST SPI2_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 HAS_PHYSICAL_KEYBOARD 1 // Backlight is controlled to power rail on this board, this also powers the neopixel // #define PIN_POWER_EN 38 #define BUTTON_PIN 0 #define I2C_SDA 8 #define I2C_SCL 9 #define I2C_SDA1 2 #define I2C_SCL1 1 #undef LORA_SCK #undef LORA_MISO #undef LORA_MOSI #undef LORA_CS #define LORA_SCK 40 #define LORA_MISO 39 #define LORA_MOSI 14 #define LORA_CS 5 // NSS #define USE_SX1262 #define LORA_DIO0 -1 #define LORA_RESET 3 #define LORA_RST 3 #define LORA_DIO1 4 #define LORA_DIO2 6 #define LORA_DIO3 RADIOLIB_NC #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 TCXO_OPTIONAL #undef GPS_RX_PIN #undef GPS_TX_PIN #define GPS_RX_PIN 15 #define GPS_TX_PIN 13 #define HAS_GPS 1 #define GPS_BAUDRATE 115200 // audio codec ES8311 #define HAS_I2S #define DAC_I2S_BCK 41 #define DAC_I2S_WS 43 #define DAC_I2S_DOUT 42 #define DAC_I2S_DIN 46 #define DAC_I2S_MCLK 45 // dummy // TCA8418 keyboard #define I2C_NO_RESCAN #define KB_INT 11 #define HAS_NEOPIXEL #define NEOPIXEL_COUNT 1 #define NEOPIXEL_DATA 21 #define NEOPIXEL_TYPE (NEO_GRB + NEO_KHZ800) #define BATTERY_PIN 10 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage #define ADC_CHANNEL ADC1_GPIO10_CHANNEL #define ADC_MULTIPLIER 2 * 1.02 // 100k + 100k, and add 2% to kick the voltage over the max voltage to show charging. // BMI270 6-axis IMU on internal I2C bus #define HAS_BMI270 ================================================ FILE: variants/esp32s3/m5stack_cores3/pins_arduino.h ================================================ #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) #define RGB_BUILTIN SOC_GPIO_PIN_COUNT + 48 #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 */ ================================================ FILE: variants/esp32s3/m5stack_cores3/platformio.ini ================================================ ; M5stack CoreS3 [env:m5stack-cores3] extends = esp32s3_base board = m5stack-cores3 board_check = true board_build.partitions = default_16MB.csv upload_protocol = esptool build_flags = ${esp32s3_base.build_flags} -D PRIVATE_HW -D M5STACK_CORES3 -I variants/esp32s3/m5stack_cores3 ================================================ FILE: variants/esp32s3/m5stack_cores3/variant.h ================================================ #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 ================================================ FILE: variants/esp32s3/mesh-tab/pins_arduino.h ================================================ #ifndef Pins_Arduino_h #define Pins_Arduino_h #include "soc/soc_caps.h" #include #define USB_VID 0x303A #define USB_PID 0x80D6 static const uint8_t TX = 43; static const uint8_t RX = 44; static const uint8_t SDA = 8; static const uint8_t SCL = 9; static const uint8_t SS = 5; static const uint8_t MOSI = 35; static const uint8_t MISO = 37; static const uint8_t SDO = 35; static const uint8_t SDI = 37; static const uint8_t SCK = 36; 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 T1 = 1; static const uint8_t T3 = 3; 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 T14 = 14; static const uint8_t VBAT_SENSE = 2; static const uint8_t VBUS_SENSE = 34; static const uint8_t RGB_DATA = 40; // RGB_BUILTIN and RGB_BRIGHTNESS can be used in new Arduino API neopixelWrite() #define RGB_BUILTIN (RGB_DATA + SOC_GPIO_PIN_COUNT) #define RGB_BRIGHTNESS 64 static const uint8_t RGB_PWR = 39; static const uint8_t LDO2 = 39; static const uint8_t LED = 13; #endif /* Pins_Arduino_h */ ================================================ FILE: variants/esp32s3/mesh-tab/platformio.ini ================================================ ; Base for Mesh-Tab device (esp32-s3 N16R2 + ra-sh01 lora) ; https://github.com/valzzu/Mesh-Tab [mesh_tab_base] extends = esp32s3_base board = mesh-tab 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 CONFIG_ARDUHAL_ESP_LOG -D CONFIG_ARDUHAL_LOG_COLORS=1 -D CONFIG_DISABLE_HAL_LOCKS=1 -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 HAS_SCREEN=0 -D HAS_TFT=1 -D USE_PIN_BUZZER -D RAM_SIZE=1024 -D LGFX_DRIVER_TEMPLATE -D LGFX_DRIVER=LGFX_GENERIC -D GFX_DRIVER_INC=\"graphics/LGFX/LGFX_GENERIC.h\" -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 VIEW_320x240 -D USE_PACKET_API -I variants/esp32s3/mesh-tab build_src_filter = ${esp32s3_base.build_src_filter} lib_deps = ${esp32s3_base.lib_deps} ${device-ui_base.lib_deps} # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX lovyan03/LovyanGFX@1.2.19 [mesh_tab_xpt2046] extends = mesh_tab_base build_flags = ${mesh_tab_base.build_flags} -D LGFX_TOUCH=XPT2046 -D LGFX_TOUCH_SPI_FREQ=2500000 -D LGFX_TOUCH_SPI_HOST=2 -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 [mesh_tab_ft5x06] extends = mesh_tab_base build_flags = ${mesh_tab_base.build_flags} -D LGFX_TOUCH=FT5x06 -D LGFX_TOUCH_I2C_FREQ=400000 -D LGFX_TOUCH_I2C_PORT=0 -D LGFX_TOUCH_I2C_ADDR=0x38 -D LGFX_TOUCH_I2C_SDA=8 -D LGFX_TOUCH_I2C_SCL=9 -D LGFX_TOUCH_RST=7 ; 3.2" TN TFT ST7789 / XPT2046: https://vi.aliexpress.com/item/1005005933490544.html [env:mesh-tab-3-2-TN-resistive] extends = mesh_tab_base build_flags = ${mesh_tab_xpt2046.build_flags} -D SPI_FREQUENCY=60000000 -D LGFX_SCREEN_WIDTH=240 -D LGFX_SCREEN_HEIGHT=320 -D DISPLAY_SIZE=320x240 ; landscape mode -D LGFX_PANEL=ST7789 -D LGFX_INVERT_COLOR=false -D LGFX_ROTATION=3 -D LGFX_TOUCH_ROTATION=4 ; 3.2" IPS TFT ILI9341 / XPT2046: https://www.aliexpress.com/item/1005006258575617.html [env:mesh-tab-3-2-IPS-resistive] extends = mesh_tab_base build_flags = ${mesh_tab_xpt2046.build_flags} -D SPI_FREQUENCY=60000000 ; if image is distorted then lower to 40 MHz -D LGFX_SCREEN_WIDTH=240 -D LGFX_SCREEN_HEIGHT=320 -D DISPLAY_SIZE=320x240 ; landscape mode -D LGFX_PANEL=ILI9341 -D LGFX_ROTATION=1 -D LGFX_TOUCH_ROTATION=4 ; 3.5" IPS TFT ILI9488 / XPT2046: https://vi.aliexpress.com/item/1005006333922639.html [env:mesh-tab-3-5-IPS-resistive] extends = mesh_tab_base build_flags = ${mesh_tab_xpt2046.build_flags} -D SPI_FREQUENCY=60000000 ; may go higher upto 40/60/80 MHz -D DISPLAY_SET_RESOLUTION -D LGFX_SCREEN_WIDTH=320 -D LGFX_SCREEN_HEIGHT=480 -D DISPLAY_SIZE=320x480 ; portrait mode -D LGFX_PANEL=ILI9488 -D LGFX_ROTATION=0 -D LGFX_TOUCH_ROTATION=0 ; 3.5" TN TFT ILI9488 / XPT2046: https://vi.aliexpress.com/item/32985467436.html [env:mesh-tab-3-5-TN-resistive] extends = mesh_tab_base build_flags = ${mesh_tab_xpt2046.build_flags} -D SPI_FREQUENCY=60000000 -D DISPLAY_SET_RESOLUTION -D LGFX_SCREEN_WIDTH=320 -D LGFX_SCREEN_HEIGHT=480 -D DISPLAY_SIZE=320x480 ; portrait mode -D LGFX_PANEL=HX8357B -D LGFX_INVERT_COLOR=false -D LGFX_ROTATION=4 -D LGFX_TOUCH_ROTATION=2 ; 3.2" IPS TFT ILI9341 / FT6236: https://vi.aliexpress.com/item/1005006624072350.html [env:mesh-tab-3-2-IPS-capacitive] extends = mesh_tab_base build_flags = ${mesh_tab_ft5x06.build_flags} -D SPI_FREQUENCY=75000000 ; may go higher upto 60/80 MHz -D LGFX_SCREEN_WIDTH=240 -D LGFX_SCREEN_HEIGHT=320 -D DISPLAY_SIZE=320x240 ; landscape mode -D LGFX_PANEL=ILI9341 -D LGFX_ROTATION=1 -D LGFX_TOUCH_X_MIN=0 -D LGFX_TOUCH_X_MAX=239 -D LGFX_TOUCH_Y_MIN=0 -D LGFX_TOUCH_Y_MAX=319 -D LGFX_TOUCH_ROTATION=2 ; 3.5" IPS TFT ILI9488 / FT6236: https://vi.aliexpress.com/item/1005006893699919.html [env:mesh-tab-3-5-IPS-capacitive] extends = mesh_tab_base build_flags = ${mesh_tab_ft5x06.build_flags} -D SPI_FREQUENCY=75000000 ; may go higher upto 40/60/80 MHz -D DISPLAY_SET_RESOLUTION -D LGFX_SCREEN_WIDTH=320 -D LGFX_SCREEN_HEIGHT=480 -D DISPLAY_SIZE=320x480 ; portrait mode -D LGFX_PANEL=ILI9488 -D LGFX_ROTATION=2 -D LGFX_TOUCH_X_MIN=0 -D LGFX_TOUCH_X_MAX=319 -D LGFX_TOUCH_Y_MIN=0 -D LGFX_TOUCH_Y_MAX=479 -D LGFX_TOUCH_ROTATION=0 ; 4.0" IPS TFT ILI9488 / FT6236: https://vi.aliexpress.com/item/1005007082906950.html [env:mesh-tab-4-0-IPS-capacitive] extends = mesh_tab_base build_flags = ${mesh_tab_ft5x06.build_flags} -D SPI_FREQUENCY=75000000 -D DISPLAY_SET_RESOLUTION -D LGFX_SCREEN_WIDTH=320 -D LGFX_SCREEN_HEIGHT=480 -D DISPLAY_SIZE=320x480 ; portrait mode -D LGFX_PANEL=HX8357B -D LGFX_ROTATION=4 -D LGFX_TOUCH_X_MIN=0 -D LGFX_TOUCH_X_MAX=319 -D LGFX_TOUCH_Y_MIN=0 -D LGFX_TOUCH_Y_MAX=479 -D LGFX_TOUCH_ROTATION=6 ================================================ FILE: variants/esp32s3/mesh-tab/variant.h ================================================ #ifndef _VARIANT_MESHTAB_DIY_ #define _VARIANT_MESHTAB_DIY_ #define HAS_TOUCHSCREEN 1 #define USE_POWERSAVE #define SLEEP_TIME 180 // Analog pins #define BATTERY_PIN 4 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage // ratio of voltage divider (100k, 220k) #define ADC_MULTIPLIER 1.6 // 1.45 + 10% for correction of display undervoltage. #define ADC_CHANNEL ADC1_GPIO4_CHANNEL // LED #define LED_POWER 21 // Button #define BUTTON_PIN 0 // Button #define PIN_BUZZER 5 // 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 MODULES #define USE_SX1262 // LORA SPI #define LORA_SCK 36 #define LORA_MISO 37 #define LORA_MOSI 35 #define LORA_CS 39 // LORA CONFIG #define LORA_DIO0 -1 // a No connect on the SX1262 module #define LORA_RESET 14 #define LORA_DIO1 15 // SX1262 IRQ #define LORA_DIO2 40 // SX1262 BUSY #define LORA_DIO3 // Not connected on PCB, but internally on the TTGO SX1262 #define SX126X_CS LORA_CS #define SX126X_DIO1 LORA_DIO1 #define SX126X_DIO2_AS_RF_SWITCH #define SX126X_BUSY LORA_DIO2 #define SX126X_RESET 14 #define SX126X_RXEN 47 #define SX126X_TXEN RADIOLIB_NC // Assuming that DIO2 is connected to TXEN pin #define SX126X_DIO3_TCXO_VOLTAGE 1.8 #endif ================================================ FILE: variants/esp32s3/mini-epaper-s3/nicheGraphics.h ================================================ #pragma once #include "configuration.h" #ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS // InkHUD-specific components #include "graphics/niche/InkHUD/InkHUD.h" // Applets #include "graphics/niche/InkHUD/Applet.h" #include "graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h" #include "graphics/niche/InkHUD/Applets/User/DM/DMApplet.h" #include "graphics/niche/InkHUD/Applets/User/FavoritesMap/FavoritesMapApplet.h" #include "graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.h" #include "graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h" #include "graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.h" #include "graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h" #include "graphics/niche/InkHUD/SystemApplet.h" // Shared NicheGraphics components #include "graphics/niche/Drivers/EInk/GDEW0102T4.h" #include "graphics/niche/Inputs/TwoButtonExtended.h" void setupNicheGraphics() { using namespace NicheGraphics; // Power-enable the E-Ink panel on this board before any SPI traffic. pinMode(PIN_EINK_EN, OUTPUT); digitalWrite(PIN_EINK_EN, HIGH); delay(10); // Display uses HSPI on this board SPIClass *hspi = new SPIClass(HSPI); hspi->begin(PIN_EINK_SCLK, -1, PIN_EINK_MOSI, PIN_EINK_CS); Drivers::GDEW0102T4 *displayDriver = new Drivers::GDEW0102T4; displayDriver->begin(hspi, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES); // Tuned fast-refresh values reg30 reg50 reg82 lutW2 lutB2 = 11 F2 04 11 0D displayDriver->setFastConfig({0x11, 0xF2, 0x04, 0x11, 0x0D}); Drivers::EInk *driver = displayDriver; InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance(); inkhud->setDriver(driver); // Slightly stricter FAST/FULL inkhud->setDisplayResilience(5, 1.5); inkhud->twoWayRocker = true; // Fonts InkHUD::Applet::fontLarge = FREESANS_9PT_WIN1252; InkHUD::Applet::fontMedium = FREESANS_6PT_WIN1252; InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; // Small display defaults inkhud->persistence->settings.rotation = 0; inkhud->persistence->settings.userTiles.maxCount = 1; inkhud->persistence->settings.userTiles.count = 1; inkhud->persistence->settings.joystick.enabled = true; inkhud->persistence->settings.joystick.aligned = true; inkhud->persistence->settings.optionalMenuItems.nextTile = false; // Pick applets // Note: order of applets determines priority of "auto-show" feature inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet, false, false); // - inkhud->addApplet("DMs", new InkHUD::DMApplet, true, false); // Activated, not autoshown inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0), true, true); // Activated, Autoshown inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1), false, false); // - inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated inkhud->addApplet("Favorites Map", new InkHUD::FavoritesMapApplet, false, false); // - inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet, false, false); // - inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, not autoshown, default on tile 0 // Start running InkHUD inkhud->begin(); // Enforce two-way rocker behavior regardless of persisted settings. inkhud->persistence->settings.joystick.enabled = true; inkhud->persistence->settings.joystick.aligned = true; inkhud->persistence->settings.optionalMenuItems.nextTile = false; // Inputs Inputs::TwoButtonExtended *buttons = Inputs::TwoButtonExtended::getInstance(); // Center press (boot button) buttons->setWiring(0, INPUTDRIVER_TWO_WAY_ROCKER_BTN, true); // Match baseUI encoder long-press feel. buttons->setTiming(0, 75, 300); buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); }); buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); // LEFT rocker pin is IO4; RIGHT rocker pin is IO3. buttons->setTwoWayRockerWiring(INPUTDRIVER_TWO_WAY_ROCKER_LEFT, INPUTDRIVER_TWO_WAY_ROCKER_RIGHT, true); buttons->setJoystickDebounce(50); // Two-way rocker behavior: // - when a system applet is handling input (menu, tips, etc): LEFT=up, RIGHT=down // - otherwise: LEFT=previous applet, RIGHT=next applet buttons->setTwoWayRockerPressHandlers( [inkhud]() { bool systemHandlingInput = false; for (InkHUD::SystemApplet *sa : inkhud->systemApplets) { if (sa->handleInput) { systemHandlingInput = true; break; } } if (systemHandlingInput) inkhud->navUp(); else inkhud->prevApplet(); }, [inkhud]() { bool systemHandlingInput = false; for (InkHUD::SystemApplet *sa : inkhud->systemApplets) { if (sa->handleInput) { systemHandlingInput = true; break; } } if (systemHandlingInput) inkhud->navDown(); else inkhud->nextApplet(); }); buttons->start(); } #endif ================================================ FILE: variants/esp32s3/mini-epaper-s3/pins_arduino.h ================================================ #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 = 9; // Default SPI (LoRa bus) static const uint8_t SS = -1; static const uint8_t MOSI = 17; static const uint8_t MISO = 6; static const uint8_t SCK = 8; // SD card SPI bus #define SPI_MOSI (39) #define SPI_SCK (41) #define SPI_MISO (38) #define SPI_CS (40) #endif /* Pins_Arduino_h */ ================================================ FILE: variants/esp32s3/mini-epaper-s3/platformio.ini ================================================ [env:mini-epaper-s3] ;custom_meshtastic_hw_model = custom_meshtastic_hw_model_slug = MINI_EPAPER_S3 custom_meshtastic_architecture = esp32-s3 custom_meshtastic_actively_supported = true custom_meshtastic_support_level = 1 custom_meshtastic_display_name = LILYGO Mini ePaper S3 E-Ink custom_meshtastic_images = mini-epaper-s3.svg custom_meshtastic_tags = LilyGo custom_meshtastic_requires_dfu = no extends = esp32s3_base board = mini-epaper-s3 board_check = true upload_protocol = esptool build_flags = ${esp32s3_base.build_flags} -I variants/esp32s3/mini-epaper-s3 -D MINI_EPAPER_S3 -D USE_EINK -D EINK_DISPLAY_MODEL=GxEPD2_102 -D EINK_WIDTH=128 -D EINK_HEIGHT=80 -D USE_EINK_DYNAMICDISPLAY -D EINK_LIMIT_FASTREFRESH=3 -D EINK_BACKGROUND_USES_FAST -D EINK_HASQUIRK_GHOSTING lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master https://github.com/meshtastic/GxEPD2/archive/c7eb4c3c167cf396ef4f541cc5d4c6aa42f3c46b.zip # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib lewisxhe/SensorLib@0.3.4 [env:mini-epaper-s3-inkhud] extends = esp32s3_base, inkhud board = mini-epaper-s3 board_check = true upload_protocol = esptool build_src_filter = ${esp32s3_base.build_src_filter} ${inkhud.build_src_filter} build_flags = ${esp32s3_base.build_flags} ${inkhud.build_flags} -I variants/esp32s3/mini-epaper-s3 -D MINI_EPAPER_S3 lib_deps = ${inkhud.lib_deps} ; InkHUD libs first, so we get GFXRoot instead of AdafruitGFX ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib lewisxhe/SensorLib@0.3.4 ================================================ FILE: variants/esp32s3/mini-epaper-s3/variant.h ================================================ #pragma once #define GPS_DEFAULT_NOT_PRESENT 1 // SD card (TF) #define HAS_SDCARD #define SDCARD_USE_SPI1 #define SDCARD_CS 40 #define SD_SPI_FREQUENCY 25000000U // Built-in RTC (I2C) #define PCF8563_RTC 0x51 #define HAS_RTC 1 #define I2C_SDA SDA #define I2C_SCL SCL // Battery voltage monitoring #define BATTERY_PIN 2 // 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_GPIO2_CHANNEL // Display (E-Ink) #define PIN_EINK_EN 42 #define PIN_EINK_CS 13 #define PIN_EINK_BUSY 10 #define PIN_EINK_DC 12 #define PIN_EINK_RES 11 #define PIN_EINK_SCLK 14 #define PIN_EINK_MOSI 15 #define DISPLAY_FORCE_SMALL_FONTS // Two-Way Rocker input (left/right + boot as press) #define INPUTDRIVER_TWO_WAY_ROCKER #define INPUTDRIVER_ENCODER_TYPE 2 #define INPUTDRIVER_TWO_WAY_ROCKER_RIGHT 3 #define INPUTDRIVER_TWO_WAY_ROCKER_LEFT 4 #define INPUTDRIVER_TWO_WAY_ROCKER_BTN 0 #define UPDOWN_LONG_PRESS_REPEAT_INTERVAL 150 // LoRa (SX1262) #define USE_SX1262 #define LORA_DIO1 5 #define LORA_SCK 8 #define LORA_MISO 6 #define LORA_MOSI 17 #define LORA_CS 7 // CS not connected; IO7 is free #define LORA_RESET 21 #ifdef USE_SX1262 #define SX126X_CS LORA_CS #define SX126X_DIO1 LORA_DIO1 #define SX126X_BUSY 16 #define SX126X_RESET LORA_RESET #define SX126X_DIO3_TCXO_VOLTAGE 1.8 #endif ================================================ FILE: variants/esp32s3/nibble_esp32/platformio.ini ================================================ [env:nibble-esp32] extends = esp32s3_base board = esp32-s3-zero board_level = extra build_flags = ${esp32s3_base.build_flags} -D PRIVATE_HW -I variants/esp32s3/nibble_esp32 ================================================ FILE: variants/esp32s3/nibble_esp32/variant.h ================================================ #define I2C_SDA 11 // I2C pins for this board #define I2C_SCL 10 #define LED_POWER 1 // 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 USE_RF95 #define LORA_SCK 6 #define LORA_MISO 7 #define LORA_MOSI 8 #define LORA_CS 9 #define LORA_DIO0 5 // a No connect on the SX1262 module #define LORA_RESET 4 #define LORA_DIO1 RADIOLIB_NC #define LORA_DIO2 RADIOLIB_NC ================================================ FILE: variants/esp32s3/nugget_s3_lora/platformio.ini ================================================ [env:nugget-s3-lora] extends = esp32s3_base board = lolin_s3_mini board_level = extra build_flags = ${esp32s3_base.build_flags} -D ARDUINO_USB_CDC_ON_BOOT=1 -D PRIVATE_HW -I variants/esp32s3/nugget_s3_lora ================================================ FILE: variants/esp32s3/nugget_s3_lora/variant.h ================================================ #define I2C_SDA 35 // I2C pins for this board #define I2C_SCL 36 #define USE_SSD1306 #define DISPLAY_FLIP_SCREEN #define LED_POWER 15 // If defined we will blink this LED #define HAS_NEOPIXEL // Enable the use of neopixels #define NEOPIXEL_COUNT 3 // How many neopixels are connected #define NEOPIXEL_DATA 10 // gpio pin used to send data to the neopixels #define NEOPIXEL_TYPE (NEO_GRB + NEO_KHZ800) // type of neopixels in use // Button A (44), B (43), R (12), U (13), L (11), D (18) #define BUTTON_PIN 43 // If defined, this will be used for user button presses #define BUTTON_NEED_PULLUP #define USE_RF95 #define LORA_SCK 6 #define LORA_MISO 7 #define LORA_MOSI 8 #define LORA_CS 9 #define LORA_DIO0 16 #define LORA_RESET 4 #define LORA_DIO1 RADIOLIB_NC #define LORA_DIO2 RADIOLIB_NC // jk, its not really a trackball but we're gonna pretend! #define HAS_TRACKBALL 1 #define TB_UP 13 #define TB_DOWN 18 #define TB_LEFT 11 #define TB_RIGHT 12 #define TB_PRESS 44 // BUTTON_PIN #define TB_DIRECTION FALLING #define ENABLE_AMBIENTLIGHTING ================================================ FILE: variants/esp32s3/picomputer-s3/pins_arduino.h ================================================ #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 */ ================================================ FILE: variants/esp32s3/picomputer-s3/platformio.ini ================================================ [env:picomputer-s3] custom_meshtastic_hw_model = 52 custom_meshtastic_hw_model_slug = PICOMPUTER_S3 custom_meshtastic_architecture = esp32-s3 custom_meshtastic_actively_supported = true custom_meshtastic_support_level = 3 custom_meshtastic_display_name = Pi Computer S3 custom_meshtastic_partition_scheme = 8MB extends = esp32s3_base board = bpi_picow_esp32_s3 board_check = true board_build.partitions = partition-table-8MB.csv ;OpenOCD flash method ;upload_protocol = esp-builtin ;Normal method upload_protocol = esptool build_flags = ${esp32s3_base.build_flags} -DPICOMPUTER_S3 -I variants/esp32s3/picomputer-s3 lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX lovyan03/LovyanGFX@1.2.19 build_src_filter = ${esp32s3_base.build_src_filter} [env:picomputer-s3-tft] extends = env:picomputer-s3 build_flags = ${env:picomputer-s3.build_flags} -D MESHTASTIC_EXCLUDE_WEBSERVER=1 -D INPUTDRIVER_MATRIX_TYPE=1 -D USE_PIN_BUZZER=PIN_BUZZER -D USE_SX127x -D HAS_SCREEN=1 -D HAS_TFT=1 -D RAM_SIZE=1560 -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 USE_LOG_DEBUG -D LOG_DEBUG_INC=\"DebugConfiguration.h\" -D LGFX_SCREEN_WIDTH=240 -D LGFX_SCREEN_HEIGHT=320 -D DISPLAY_SIZE=320x240 ; landscape mode -D LGFX_DRIVER=LGFX_PICOMPUTER_S3 -D GFX_DRIVER_INC=\"graphics/LGFX/LGFX_PICOMPUTER_S3.h\" -D VIEW_320x240 ; -D USE_DOUBLE_BUFFER -D USE_PACKET_API lib_deps = ${env:picomputer-s3.lib_deps} ${device-ui_base.lib_deps} ================================================ FILE: variants/esp32s3/picomputer-s3/variant.h ================================================ #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 USE_TFTDISPLAY 1 #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_OVERRIDE COLOR565(255, 255, 255) #define INPUTBROKER_MATRIX_TYPE 1 #define KEYS_COLS \ { \ 44, 47, 17, 15, 13, 41 \ } #define KEYS_ROWS \ { \ 12, 16, 42, 18, 14, 7 \ } ================================================ FILE: variants/esp32s3/rak3312/pins_arduino.h ================================================ #ifndef Pins_Arduino_h #define Pins_Arduino_h #include "variant.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 = 9; static const uint8_t SCL = 40; // Default SPI will be mapped to Radio static const uint8_t SS = 12; static const uint8_t MOSI = 11; static const uint8_t MISO = 10; static const uint8_t SCK = 13; #define SPI_INTERFACES_COUNT 1 #define SPI_MOSI (11) #define SPI_SCK (13) #define SPI_MISO (10) #define SPI_CS (12) #ifdef _VARIANT_RAK3112_ /* * Serial interfaces */ // TXD1 RXD1 on Base Board #define PIN_SERIAL1_RX (44) #define PIN_SERIAL1_TX (43) /* * Internal SPI to LoRa transceiver */ #define LORA_SX126X_SCK 5 #define LORA_SX126X_MISO 3 #define LORA_SX126X_MOSI 6 /* * Analog pins */ #define PIN_A0 (21) #define PIN_A1 (14) /* * Wire Interfaces */ #define WIRE_INTERFACES_COUNT 2 #define PIN_WIRE1_SDA (17) #define PIN_WIRE1_SCL (18) /* * GPIO's */ #define WB_IO1 21 #define WB_IO2 2 // #define WB_IO2 14 #define WB_IO3 41 #define WB_IO4 42 #define WB_IO5 38 #define WB_IO6 39 // #define WB_SW1 35 NC #define WB_A0 1 #define WB_A1 2 #define WB_CS 12 #define WB_LED1 46 #define WB_LED2 45 #endif #endif /* Pins_Arduino_h */ ================================================ FILE: variants/esp32s3/rak3312/platformio.ini ================================================ [env:rak3312] custom_meshtastic_hw_model = 106 custom_meshtastic_hw_model_slug = RAK3312 custom_meshtastic_architecture = esp32-s3 custom_meshtastic_actively_supported = true custom_meshtastic_support_level = 1 custom_meshtastic_display_name = RAK3312 custom_meshtastic_images = rak_3312.svg custom_meshtastic_tags = RAK custom_meshtastic_requires_dfu = false custom_meshtastic_partition_scheme = 16MB extends = esp32s3_base board = wiscore_rak3312 board_level = pr board_check = true upload_protocol = esptool build_flags = ${esp32s3_base.build_flags} -D RAK3312 -I variants/esp32s3/rak3312 [env:rak3112] extends = esp32s3_base board = wiscore_rak3312 board_level = extra upload_protocol = esptool build_flags = ${esp32_base.build_flags} -D RAK3312 -D _VARIANT_RAK3112_ -I variants/esp32s3/rak3312 ================================================ FILE: variants/esp32s3/rak3312/variant.h ================================================ #define I2C_SDA 9 #define I2C_SCL 40 #define USE_SX1262 #define LORA_SCK 5 #define LORA_MISO 3 #define LORA_MOSI 6 #define LORA_CS 7 #define LORA_RESET 8 #ifdef USE_SX1262 #define SX126X_CS LORA_CS #define SX126X_DIO1 47 #define SX126X_BUSY 48 #define SX126X_RESET LORA_RESET #define SX126X_DIO2_AS_RF_SWITCH #define SX126X_DIO3_TCXO_VOLTAGE 1.8 #endif #define LED_GREEN 46 #define LED_BLUE 45 #define PIN_LED1 LED_GREEN #define LED_NOTIFICATION LED_BLUE #define LED_POWER LED_GREEN #define ledOff(pin) pinMode(pin, INPUT) #define LED_STATE_ON 1 // State when LED is litted #define BATTERY_PIN 1 #define ADC_CHANNEL ADC1_GPIO1_CHANNEL #ifdef _VARIANT_RAK3112_ // Modular variant (stamp) #define ADC_MULTIPLIER 2.11 #define BUTTON_NEED_PULLUP #define HAS_SDCARD #define SDCARD_USE_SPI1 #define SDCARD_CS SPI_CS #define I2C_SDA1 PIN_WIRE1_SDA #define I2C_SCL1 PIN_WIRE1_SCL #else // Generic 3312 variant (40-pin standard connector) #define ADC_MULTIPLIER 1.667 #define SX126X_POWER_EN (4) #define PIN_POWER_EN PIN_3V3_EN #define PIN_3V3_EN (14) #define HAS_GPS 1 #define GPS_TX_PIN 43 #define GPS_RX_PIN 44 #endif ================================================ FILE: variants/esp32s3/rak_wismesh_tap_v2/pins_arduino.h ================================================ #ifndef Pins_Arduino_h #define Pins_Arduino_h #include "variant.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 = 9; static const uint8_t SCL = 40; // Default SPI will be mapped to Radio static const uint8_t SS = 12; static const uint8_t MOSI = 11; static const uint8_t MISO = 10; static const uint8_t SCK = 13; #define SPI_MOSI (11) #define SPI_SCK (13) #define SPI_MISO (10) #define SPI_CS (12) #endif /* Pins_Arduino_h */ ================================================ FILE: variants/esp32s3/rak_wismesh_tap_v2/platformio.ini ================================================ ; rak_wismeshtap2 rak3112 [ft5x06] build_flags = -D LGFX_TOUCH=FT5x06 -D LGFX_TOUCH_I2C_FREQ=100000 -D LGFX_TOUCH_I2C_PORT=0 -D LGFX_TOUCH_I2C_ADDR=0x38 -D LGFX_TOUCH_I2C_SDA=9 -D LGFX_TOUCH_I2C_SCL=40 -D LGFX_TOUCH_RST=-1 -D LGFX_TOUCH_INT=39 [env:rak_wismesh_tap_v2] custom_meshtastic_hw_model = 116 custom_meshtastic_hw_model_slug = WISMESH_TAP_V2 custom_meshtastic_architecture = esp32-s3 custom_meshtastic_actively_supported = true custom_meshtastic_support_level = 1 custom_meshtastic_display_name = RAK WisMesh Tap V2 custom_meshtastic_images = rak-wismesh-tap-v2.svg custom_meshtastic_tags = RAK custom_meshtastic_partition_scheme = 8MB extends = esp32s3_base board = wiscore_rak3312 board_check = true upload_protocol = esptool board_build.partitions = default_8MB.csv build_flags = ${esp32s3_base.build_flags} -D RAK3312 -D RAK_WISMESH_TAP_V2 -I variants/esp32s3/rak_wismesh_tap_v2 lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX lovyan03/LovyanGFX@1.2.19 [env:rak_wismesh_tap_v2-tft] extends = env:rak_wismesh_tap_v2 build_flags = ${env:rak_wismesh_tap_v2.build_flags} -D CONFIG_ARDUHAL_ESP_LOG -D CONFIG_ARDUHAL_LOG_COLORS=1 -D CONFIG_DISABLE_HAL_LOCKS=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 INPUTDRIVER_BUTTON_TYPE=0 -D HAS_SDCARD -D HAS_SCREEN=0 -D HAS_TFT=1 -D USE_PIN_BUZZER=PIN_BUZZER -D RAM_SIZE=5120 -D LGFX_DRIVER_TEMPLATE -D LGFX_DRIVER=LGFX_GENERIC -D GFX_DRIVER_INC=\"graphics/LGFX/LGFX_GENERIC.h\" -D LGFX_PIN_SCK=13 -D LGFX_PIN_MOSI=11 -D LGFX_PIN_MISO=10 -D LGFX_PIN_DC=42 -D LGFX_PIN_CS=12 -D LGFX_PIN_RST=-1 -D LGFX_PIN_BL=41 -D VIEW_320x240 -D USE_PACKET_API ${ft5x06.build_flags} -D LGFX_SCREEN_WIDTH=240 ; native panel width (portrait) -D LGFX_SCREEN_HEIGHT=320 ; native panel height (portrait) -D DISPLAY_SIZE=320x240 ; UI runs in landscape mode -D LGFX_PANEL=ST7789 -D LGFX_ROTATION=1 -D LGFX_TOUCH_X_MIN=0 -D LGFX_TOUCH_X_MAX=239 -D LGFX_TOUCH_Y_MIN=0 -D LGFX_TOUCH_Y_MAX=319 -D LGFX_TOUCH_ROTATION=2 -D LGFX_CFG_HOST=SPI3_HOST -D MAP_FULL_REDRAW=1 lib_deps = ${env:rak_wismesh_tap_v2.lib_deps} ${device-ui_base.lib_deps} ================================================ FILE: variants/esp32s3/rak_wismesh_tap_v2/variant.h ================================================ #ifndef _VARIANT_RAK_WISMESHTAP_V2_H #define _VARIANT_RAK_WISMESHTAP_V2_H #define I2C_SDA 9 #define I2C_SCL 40 #define USE_SX1262 #define LORA_SCK 5 #define LORA_MISO 3 #define LORA_MOSI 6 #define LORA_CS 7 #define LORA_RESET 8 #ifdef USE_SX1262 #define SX126X_CS LORA_CS #define SX126X_DIO1 47 #define SX126X_BUSY 48 #define SX126X_RESET LORA_RESET #define SX126X_DIO2_AS_RF_SWITCH #define SX126X_DIO3_TCXO_VOLTAGE 1.8 #endif #define SX126X_POWER_EN (4) #define PIN_POWER_EN PIN_3V3_EN #define PIN_3V3_EN (14) #define LED_GREEN 46 #define LED_BLUE 45 #define PIN_LED1 LED_GREEN #define LED_NOTIFICATION LED_BLUE #define LED_POWER LED_GREEN #define ledOff(pin) pinMode(pin, INPUT) #define LED_STATE_ON 1 // State when LED is litted #define HAS_GPS 1 #define GPS_TX_PIN 43 #define GPS_RX_PIN 44 #define SPI_MOSI (11) #define SPI_SCK (13) #define SPI_MISO (10) #define SPI_CS (12) #define BUTTON_PIN 0 #define USE_VIRTUAL_KEYBOARD 1 #define BATTERY_PIN 1 #define ADC_CHANNEL ADC1_GPIO1_CHANNEL #define ADC_MULTIPLIER 1.667 #define PIN_BUZZER 38 #define HAS_SDCARD 1 #define SDCARD_USE_SPI1 1 #define SDCARD_CS 2 #define SPI_FREQUENCY 40000000 #define SPI_READ_FREQUENCY 16000000 #define SD_SPI_FREQUENCY 50000000 #endif ================================================ FILE: variants/esp32s3/seeed-sensecap-indicator/pins_arduino.h ================================================ #ifndef Pins_Arduino_h #define Pins_Arduino_h #include // 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 */ ================================================ FILE: variants/esp32s3/seeed-sensecap-indicator/platformio.ini ================================================ ; Seeed Studio SenseCAP Indicator [env:seeed-sensecap-indicator] custom_meshtastic_hw_model = 70 custom_meshtastic_hw_model_slug = SENSECAP_INDICATOR custom_meshtastic_architecture = esp32-s3 custom_meshtastic_actively_supported = true custom_meshtastic_support_level = 1 custom_meshtastic_display_name = Seeed SenseCAP Indicator custom_meshtastic_images = seeed-sensecap-indicator.svg custom_meshtastic_tags = Seeed custom_meshtastic_partition_scheme = 8MB = true extends = esp32s3_base platform_packages = platformio/framework-arduinoespressif32 @ https://github.com/mverch67/arduino-esp32/archive/aef7fef6de3329ed6f75512d46d63bba12b09bb5.zip ; add_tca9535 (based on 2.0.16) board = seeed-sensecap-indicator board_check = true board_build.partitions = partition-table-8MB.csv upload_protocol = esptool build_flags = ${esp32s3_base.build_flags} -Ivariants/esp32s3/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} ; TODO switch back to official LovyanGFX https://github.com/mverch67/LovyanGFX/archive/4c76238c1344162a234ae917b27651af146d6fb2.zip # renovate: datasource=git-refs depName=ESP8266Audio packageName=https://github.com/meshtastic/ESP8266Audio gitBranch=meshtastic-2.0.0-dacfix https://github.com/meshtastic/ESP8266Audio/archive/343024632ee78d6216907b2353fc943a62422d80.zip # renovate: datasource=custom.pio depName=ESP8266SAM packageName=earlephilhower/library/ESP8266SAM earlephilhower/ESP8266SAM@1.1.0 [env:seeed-sensecap-indicator-tft] extends = env:seeed-sensecap-indicator board_level = pr upload_speed = 460800 build_flags = ${env:seeed-sensecap-indicator.build_flags} -D INPUTDRIVER_BUTTON_TYPE=38 -D CONFIG_DISABLE_HAL_LOCKS=1 -D HAS_SCREEN=1 -D HAS_TFT=1 -D DISPLAY_SET_RESOLUTION -D RAM_SIZE=4096 -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 USE_LOG_DEBUG -D LOG_DEBUG_INC=\"DebugConfiguration.h\" -D CUSTOM_TOUCH_DRIVER -D LGFX_SCREEN_WIDTH=480 -D LGFX_SCREEN_HEIGHT=480 -D DISPLAY_SIZE=480x480 -D LGFX_DRIVER=LGFX_INDICATOR -D GFX_DRIVER_INC=\"graphics/LGFX/LGFX_INDICATOR.h\" -D VIEW_320x240 -D USE_PACKET_API lib_deps = ${env:seeed-sensecap-indicator.lib_deps} ${device-ui_base.lib_deps} ; TODO switch back to official bb_captouch https://github.com/mverch67/bb_captouch/archive/8626412fe650d808a267791c0eae6e5860c85a5d.zip ; alternative touch library supporting FT6x36 ================================================ FILE: variants/esp32s3/seeed-sensecap-indicator/variant.h ================================================ #define I2C_SDA 39 #define I2C_SCL 40 // This board has a serial coprocessor for sensor readings #define SENSOR_RP2040_TXD 19 #define SENSOR_RP2040_RXD 20 #define SENSOR_PORT_NUM 2 #define SENSOR_BAUD_RATE 115200 #define BUTTON_PIN 38 #define BUTTON_ACTIVE_LOW true #define BUTTON_ACTIVE_PULLUP true // #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 12000000 #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 USE_TFTDISPLAY 1 #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 // in future, we may want to add a buzzer and add all sensors to the indicator via a data protocol for now only GPS is supported // // Buzzer // #define PIN_BUZZER 19 #define GPS_DEFAULT_NOT_PRESENT 1 #define GPS_RX_PIN 20 #define GPS_TX_PIN 19 #define HAS_GPS 1 #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 #define TCXO_OPTIONAL // handle Indicator V1 and V2 #define SX126X_DIO3_TCXO_VOLTAGE 2.4 #define USE_VIRTUAL_KEYBOARD 1 #define DISPLAY_CLOCK_FRAME 1 ================================================ FILE: variants/esp32s3/seeed_xiao_s3/pins_arduino.h ================================================ #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 */ ================================================ FILE: variants/esp32s3/seeed_xiao_s3/platformio.ini ================================================ [env:seeed-xiao-s3] custom_meshtastic_hw_model = 81 custom_meshtastic_hw_model_slug = SEEED_XIAO_S3 custom_meshtastic_architecture = esp32-s3 custom_meshtastic_actively_supported = true custom_meshtastic_support_level = 3 custom_meshtastic_display_name = Seeed Xiao ESP32-S3 custom_meshtastic_images = seeed-xiao-s3.svg custom_meshtastic_tags = Seeed custom_meshtastic_requires_dfu = true custom_meshtastic_partition_scheme = 8MB extends = esp32s3_base board = seeed-xiao-s3 board_level = pr board_check = true board_build.partitions = default_8MB.csv upload_protocol = esptool upload_speed = 921600 build_unflags = ${esp32s3_base.build_unflags} -DARDUINO_USB_MODE=1 build_flags = ${esp32s3_base.build_flags} -D SEEED_XIAO_S3 -I variants/esp32s3/seeed_xiao_s3 -DBOARD_HAS_PSRAM -DARDUINO_USB_MODE=0 ================================================ FILE: variants/esp32s3/seeed_xiao_s3/variant.h ================================================ /* ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄ ▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌▐░░░░░░░░░░▌ ▐░█▀▀▀▀▀▀▀▀▀ ▐░█▀▀▀▀▀▀▀▀▀ ▐░█▀▀▀▀▀▀▀▀▀ ▐░█▀▀▀▀▀▀▀▀▀ ▐░█▀▀▀▀▀▀▀█░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░█▄▄▄▄▄▄▄▄▄ ▐░█▄▄▄▄▄▄▄▄▄ ▐░█▄▄▄▄▄▄▄▄▄ ▐░█▄▄▄▄▄▄▄▄▄ ▐░▌ ▐░▌ ▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌▐░▌ ▐░▌ ▀▀▀▀▀▀▀▀▀█░▌▐░█▀▀▀▀▀▀▀▀▀ ▐░█▀▀▀▀▀▀▀▀▀ ▐░█▀▀▀▀▀▀▀▀▀ ▐░▌ ▐░▌ ▐░▌▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▄▄▄▄▄▄▄▄▄█░▌▐░█▄▄▄▄▄▄▄▄▄ ▐░█▄▄▄▄▄▄▄▄▄ ▐░█▄▄▄▄▄▄▄▄▄ ▐░█▄▄▄▄▄▄▄█░▌ ▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌▐░░░░░░░░░░▌ ▀▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀ ▄ ▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄ ▐░▌ ▐░▌▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌ ▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌ ▐░▌ ▐░▌ ▀▀▀▀█░█▀▀▀▀ ▐░█▀▀▀▀▀▀▀█░▌▐░█▀▀▀▀▀▀▀█░▌ ▐░█▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀█░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▐░▌ ▐░▌ ▐░█▄▄▄▄▄▄▄█░▌▐░▌ ▐░▌ ▐░█▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄█░▌ ▐░▌ ▐░▌ ▐░░░░░░░░░░░▌▐░▌ ▐░▌ ▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌ ▐░▌░▌ ▐░▌ ▐░█▀▀▀▀▀▀▀█░▌▐░▌ ▐░▌ ▀▀▀▀▀▀▀▀▀█░▌ ▀▀▀▀▀▀▀▀▀█░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▄▄▄▄█░█▄▄▄▄ ▐░▌ ▐░▌▐░█▄▄▄▄▄▄▄█░▌ ▄▄▄▄▄▄▄▄▄█░▌ ▄▄▄▄▄▄▄▄▄█░▌ ▐░▌ ▐░▌▐░░░░░░░░░░░▌▐░▌ ▐░▌▐░░░░░░░░░░░▌ ▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌ ▀ ▀ ▀▀▀▀▀▀▀▀▀▀▀ ▀ ▀ ▀▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀▀ */ /* 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_POWER 48 #define LED_STATE_ON 1 // State when LED is lit #define BUTTON_PIN 21 // This is the Program Button #define BUTTON_NEED_PULLUP #define BATTERY_PIN -1 #define ADC_CHANNEL ADC1_GPIO1_CHANNEL #define BATTERY_SENSE_RESOLUTION_BITS 12 /*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_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_RXEN 38 #define SX126X_TXEN RADIOLIB_NC #define SX126X_DIO3_TCXO_VOLTAGE 1.8 #endif ================================================ FILE: variants/esp32s3/station-g2/pins_arduino.h ================================================ #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 */ ================================================ FILE: variants/esp32s3/station-g2/platformio.ini ================================================ [env:station-g2] custom_meshtastic_hw_model = 31 custom_meshtastic_hw_model_slug = STATION_G2 custom_meshtastic_architecture = esp32-s3 custom_meshtastic_actively_supported = true custom_meshtastic_support_level = 2 custom_meshtastic_display_name = Station G2 custom_meshtastic_images = station-g2.svg custom_meshtastic_tags = B&Q custom_meshtastic_requires_dfu = true custom_meshtastic_partition_scheme = 16MB extends = esp32s3_base board = station-g2 board_level = pr board_check = true board_build.partitions = default_16MB.csv board_build.mcu = esp32s3 upload_protocol = esptool ;upload_port = /dev/ttyACM0 upload_speed = 921600 build_unflags = ${esp32s3_base.build_unflags} -DARDUINO_USB_MODE=0 build_flags = ${esp32s3_base.build_flags} -D STATION_G2 -I variants/esp32s3/station-g2 -DBOARD_HAS_PSRAM -DSTATION_G2 -DARDUINO_USB_MODE=1 ================================================ FILE: variants/esp32s3/station-g2/variant.h ================================================ /* 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 // Enable Traffic Management Module for Station G2 #ifndef HAS_TRAFFIC_MANAGEMENT #define HAS_TRAFFIC_MANAGEMENT 1 #endif #ifndef TRAFFIC_MANAGEMENT_CACHE_SIZE #define TRAFFIC_MANAGEMENT_CACHE_SIZE 2048 #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 */ ================================================ FILE: variants/esp32s3/t-beam-1w/pins_arduino.h ================================================ #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; // I2C for OLED and sensors static const uint8_t SDA = 8; static const uint8_t SCL = 9; // Default SPI mapped to Radio/SD static const uint8_t SS = 15; // LoRa CS static const uint8_t MOSI = 11; static const uint8_t MISO = 12; static const uint8_t SCK = 13; // SD Card CS #define SDCARD_CS 10 #endif /* Pins_Arduino_h */ ================================================ FILE: variants/esp32s3/t-beam-1w/platformio.ini ================================================ ; LilyGo T-Beam-1W (1 Watt LoRa with external PA) [env:t-beam-1w] custom_meshtastic_hw_model = 122 custom_meshtastic_hw_model_slug = TBEAM_1_WATT custom_meshtastic_architecture = esp32s3 custom_meshtastic_actively_supported = true custom_meshtastic_support_level = 1 custom_meshtastic_display_name = LILYGO T-Beam 1W custom_meshtastic_images = tbeam-1w.svg custom_meshtastic_tags = LilyGo extends = esp32s3_base board = t-beam-1w board_build.partitions = default_8MB.csv board_check = true lib_deps = ${esp32s3_base.lib_deps} build_flags = ${esp32s3_base.build_flags} -I variants/esp32s3/t-beam-1w -D T_BEAM_1W ================================================ FILE: variants/esp32s3/t-beam-1w/variant.h ================================================ // LilyGo T-Beam-1W variant.h // Configuration based on LilyGO utilities.h and RF documentation // I2C for OLED display (SH1106 at 0x3C) #define I2C_SDA 8 #define I2C_SCL 9 // GPS - Quectel L76K #define GPS_RX_PIN 5 #define GPS_TX_PIN 6 #define GPS_1PPS_PIN 7 #define GPS_WAKEUP_PIN 16 // GPS_EN_PIN in LilyGO code #define HAS_GPS 1 #define GPS_BAUDRATE 9600 // Buttons #define BUTTON_PIN 0 // BUTTON 1 #define ALT_BUTTON_PIN 17 // BUTTON 2 // SPI (shared by LoRa and SD) #define SPI_MOSI 11 #define SPI_SCK 13 #define SPI_MISO 12 #define SPI_CS 10 // SD Card #define HAS_SDCARD #define SDCARD_USE_SPI1 #define SDCARD_CS SPI_CS // LoRa Radio - SX1262 with 1W PA #define USE_SX1262 #define LORA_SCK SPI_SCK #define LORA_MISO SPI_MISO #define LORA_MOSI SPI_MOSI #define LORA_CS 15 #define LORA_RESET 3 #define LORA_DIO1 1 #define LORA_BUSY 38 // CRITICAL: Radio power enable - MUST be HIGH before lora.begin()! // GPIO 40 powers the SX1262 + PA module via LDO #define SX126X_POWER_EN 40 // TX power offset for external PA (0 = no offset, full SX1262 power) #define TX_GAIN_LORA 10 #ifdef USE_SX1262 #define SX126X_CS LORA_CS #define SX126X_DIO1 LORA_DIO1 #define SX126X_BUSY LORA_BUSY #define SX126X_RESET LORA_RESET // RF switching configuration for 1W PA module // DIO2 controls PA (via SX126X_DIO2_AS_RF_SWITCH) // CTRL PIN (GPIO 21) controls LNA - must be HIGH during RX // Truth table: DIO2=1,CTRL=0 → TX (PA on, LNA off) // DIO2=0,CTRL=1 → RX (PA off, LNA on) #define SX126X_DIO2_AS_RF_SWITCH #define SX126X_RXEN 21 // LNA enable - HIGH during RX // TCXO voltage - required for radio init #define SX126X_DIO3_TCXO_VOLTAGE 1.8 #define SX126X_MAX_POWER 22 #endif // LED #define LED_POWER 18 #define LED_STATE_ON 1 // HIGH = ON // Battery ADC #define BATTERY_PIN 4 #define ADC_CHANNEL ADC1_GPIO4_CHANNEL #define BATTERY_SENSE_SAMPLES 30 #define ADC_MULTIPLIER 2.9333 #define OCV_ARRAY 7950, 7850, 7750, 7580, 7440, 7310, 7150, 7005, 6860, 6685, 6000 // NTC temperature sensor #define NTC_PIN 14 // Fan control #define FAN_CTRL_PIN 41 // Meshtastic standard fan control pin macro #define RF95_FAN_EN FAN_CTRL_PIN // PA Ramp Time - T-Beam 1W requires >800us stabilization (default is 200us) // Value 0x05 = RADIOLIB_SX126X_PA_RAMP_800U #define SX126X_PA_RAMP_US 0x05 // Display - SH1106 OLED (128x64) #define USE_SH1106 #define OLED_WIDTH 128 #define OLED_HEIGHT 64 // 32768 Hz crystal present #define HAS_32768HZ 1 ================================================ FILE: variants/esp32s3/t-deck/pins_arduino.h ================================================ #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; 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 */ ================================================ FILE: variants/esp32s3/t-deck/platformio.ini ================================================ ; LilyGo T-Deck [env:t-deck] custom_meshtastic_hw_model = 50 custom_meshtastic_hw_model_slug = T_DECK custom_meshtastic_architecture = esp32-s3 custom_meshtastic_actively_supported = true custom_meshtastic_support_level = 1 custom_meshtastic_display_name = LILYGO T-Deck custom_meshtastic_images = t-deck.svg custom_meshtastic_tags = LilyGo custom_meshtastic_requires_dfu = true custom_meshtastic_partition_scheme = 16MB extends = esp32s3_base board = t-deck board_check = true board_build.partitions = default_16MB.csv upload_protocol = esptool build_src_filter = ${esp32s3_base.build_src_filter} +<../variants/esp32s3/t-deck> build_flags = ${esp32s3_base.build_flags} -D T_DECK -D BOARD_HAS_PSRAM -I variants/esp32s3/t-deck lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX lovyan03/LovyanGFX@1.2.19 # renovate: datasource=git-refs depName=ESP8266Audio packageName=https://github.com/meshtastic/ESP8266Audio gitBranch=meshtastic-2.0.0-dacfix https://github.com/meshtastic/ESP8266Audio/archive/343024632ee78d6216907b2353fc943a62422d80.zip # renovate: datasource=custom.pio depName=ESP8266SAM packageName=earlephilhower/library/ESP8266SAM earlephilhower/ESP8266SAM@1.1.0 [env:t-deck-tft] extends = env:t-deck board_level = pr build_flags = ${env:t-deck.build_flags} -D CONFIG_DISABLE_HAL_LOCKS=1 ; "feels" to be a bit more stable without locks -D INPUTDRIVER_I2C_KBD_TYPE=0x55 -D INPUTDRIVER_ENCODER_TYPE=3 -D INPUTDRIVER_ENCODER_LEFT=1 -D INPUTDRIVER_ENCODER_RIGHT=2 -D INPUTDRIVER_ENCODER_UP=3 -D INPUTDRIVER_ENCODER_DOWN=15 -D INPUTDRIVER_ENCODER_BTN=0 -D INPUTDRIVER_BUTTON_TYPE=0 -D HAS_SDCARD -D HAS_SCREEN=1 -D HAS_TFT=1 -D USE_I2S_BUZZER -D RAM_SIZE=5120 -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 USE_LOG_DEBUG -D LOG_DEBUG_INC=\"DebugConfiguration.h\" -D RADIOLIB_DEBUG_BASIC=0 -D RADIOLIB_DEBUG_SPI=0 -D RADIOLIB_DEBUG_PROTOCOL=0 -D RADIOLIB_SPI_PARANOID=0 ; -D CALIBRATE_TOUCH=0 -D LGFX_SCREEN_WIDTH=240 -D LGFX_SCREEN_HEIGHT=320 -D DISPLAY_SIZE=320x240 ; landscape mode -D LGFX_DRIVER=LGFX_TDECK -D GFX_DRIVER_INC=\"graphics/LGFX/LGFX_T_DECK.h\" ; -D LVGL_DRIVER=LVGL_TDECK ; -D GFX_DRIVER_INC=\"graphics/LVGL/LVGL_T_DECK.h\" ; -D LV_USE_ST7789=1 -D VIEW_320x240 ; -D USE_DOUBLE_BUFFER -D USE_PACKET_API -D MAP_FULL_REDRAW -D CUSTOM_TOUCH_DRIVER lib_deps = ${env:t-deck.lib_deps} ${device-ui_base.lib_deps} # renovate: datasource=github-tags depName=bb_captouch packageName=bitbank2/bb_captouch https://github.com/bitbank2/bb_captouch/archive/refs/tags/1.3.1.zip ================================================ FILE: variants/esp32s3/t-deck/variant.cpp ================================================ #include "variant.h" #include "Arduino.h" void earlyInitVariant() { // GPIO10 manages all peripheral power supplies // Turn on peripheral power immediately after MUC starts. // If some boards are turned on late, ESP32 will reset due to low voltage. // ESP32-C3(Keyboard) , MAX98357A(Audio Power Amplifier) , // TF Card , Display backlight(AW9364DNR) , AN48841B(Trackball) , ES7210(Decoder) pinMode(KB_POWERON, OUTPUT); digitalWrite(KB_POWERON, HIGH); // T-Deck has all three SPI peripherals (TFT, SD, LoRa) attached to the same SPI bus // We need to initialize all CS pins in advance otherwise there will be SPI communication issues // e.g. when detecting the SD card pinMode(LORA_CS, OUTPUT); digitalWrite(LORA_CS, HIGH); pinMode(SDCARD_CS, OUTPUT); digitalWrite(SDCARD_CS, HIGH); pinMode(TFT_CS, OUTPUT); digitalWrite(TFT_CS, HIGH); delay(100); } ================================================ FILE: variants/esp32s3/t-deck/variant.h ================================================ #define TFT_CS 12 // ST7789 TFT LCD #define ST7789_CS TFT_CS #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 USE_TFTDISPLAY 1 #define HAS_PHYSICAL_KEYBOARD 1 #define HAS_TOUCHSCREEN 1 #define SCREEN_TOUCH_INT 16 #define TOUCH_I2C_PORT 0 #define TOUCH_SLAVE_ADDRESS 0x5D // GT911 #define USE_POWERSAVE #define SLEEP_TIME 120 #define TB_PRESS 0 #define BUTTON_ACTIVE_LOW true #define BUTTON_ACTIVE_PULLUP true #define GPS_DEFAULT_NOT_PRESENT 1 #define GPS_RX_PIN 44 #define GPS_TX_PIN 43 // Have SPI interface SD card slot // #define HAS_SDCARD // --> needs to be in platform.ini for device-ui #define SPI_MOSI (41) #define SPI_SCK (40) #define SPI_MISO (38) #define SPI_CS (39) #define SDCARD_CS SPI_CS #define SD_SPI_FREQUENCY 75000000U #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 // trackball #define HAS_TRACKBALL 1 #define TB_UP 3 #define TB_DOWN 15 #define TB_LEFT 1 #define TB_RIGHT 2 #define TB_PRESS 0 // BUTTON_PIN #define TB_DIRECTION FALLING #define TB_THRESHOLD 3 // 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 #define DAC_I2S_MCLK 21 // GPIO lrck mic // 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) ================================================ FILE: variants/esp32s3/t-deck-pro/pins_arduino.h ================================================ #ifndef Pins_Arduino_h #define Pins_Arduino_h #include #define USB_VID 0x303a #define USB_PID 0x1001 // used for keyboard, touch controller, beam sensor, and gyroscope static const uint8_t SDA = 13; static const uint8_t SCL = 14; // Default SPI will be mapped to Radio static const uint8_t SS = 3; static const uint8_t MOSI = 33; static const uint8_t MISO = 47; static const uint8_t SCK = 36; #endif /* Pins_Arduino_h */ ================================================ FILE: variants/esp32s3/t-deck-pro/platformio.ini ================================================ [env:t-deck-pro] custom_meshtastic_hw_model = 102 custom_meshtastic_hw_model_slug = T_DECK_PRO custom_meshtastic_architecture = esp32-s3 custom_meshtastic_actively_supported = true custom_meshtastic_support_level = 1 custom_meshtastic_display_name = LILYGO T-Deck Pro custom_meshtastic_images = tdeck_pro.svg custom_meshtastic_tags = LilyGo custom_meshtastic_requires_dfu = true custom_meshtastic_partition_scheme = 16MB extends = esp32s3_base board = t-deck-pro board_check = true upload_protocol = esptool build_src_filter = ${esp32s3_base.build_src_filter} +<../variants/esp32s3/t-deck-pro> build_flags = ${esp32s3_base.build_flags} -I variants/esp32s3/t-deck-pro -D T_DECK_PRO -D USE_EINK -D EINK_DISPLAY_MODEL=GxEPD2_310_GDEQ031T10 -D EINK_WIDTH=240 -D EINK_HEIGHT=320 ;-D USE_EINK_DYNAMICDISPLAY ; Enable Dynamic EInk -D EINK_LIMIT_FASTREFRESH=10 ; How many consecutive fast-refreshes are permitted -D EINK_LIMIT_GHOSTING_PX=2000 ; (Optional) How much image ghosting is tolerated lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 zinggjm/GxEPD2@1.6.8 # renovate: datasource=git-refs depName=CSE_Touch packageName=https://github.com/CIRCUITSTATE/CSE_Touch gitBranch=main https://github.com/CIRCUITSTATE/CSE_Touch/archive/b44f23b6f870b848f1fbe453c190879bc6cfaafa.zip # renovate: datasource=github-tags depName=CSE_CST328 packageName=CIRCUITSTATE/CSE_CST328 https://github.com/CIRCUITSTATE/CSE_CST328/archive/refs/tags/v0.0.4.zip # renovate: datasource=git-refs depName=BQ27220 packageName=https://github.com/mverch67/BQ27220 gitBranch=main https://github.com/mverch67/BQ27220/archive/07d92be846abd8a0258a50c23198dac0858b22ed.zip ================================================ FILE: variants/esp32s3/t-deck-pro/variant.cpp ================================================ #include "variant.h" #include "Arduino.h" void earlyInitVariant() { pinMode(LORA_EN, OUTPUT); digitalWrite(LORA_EN, HIGH); pinMode(LORA_CS, OUTPUT); digitalWrite(LORA_CS, HIGH); pinMode(SDCARD_CS, OUTPUT); digitalWrite(SDCARD_CS, HIGH); pinMode(PIN_EINK_CS, OUTPUT); digitalWrite(PIN_EINK_CS, HIGH); } ================================================ FILE: variants/esp32s3/t-deck-pro/variant.h ================================================ // Display (E-Ink) #define PIN_EINK_CS 34 #define PIN_EINK_BUSY 37 #define PIN_EINK_DC 35 #define PIN_EINK_RES -1 #define PIN_EINK_SCLK 36 #define PIN_EINK_MOSI 47 #define I2C_SDA SDA #define I2C_SCL SCL // CST328 touch screen (implementation in src/platform/extra_variants/t_deck_pro/variant.cpp) #define HAS_TOUCHSCREEN 1 #define CST328_PIN_INT 12 #define CST328_PIN_RST 45 #define USE_POWERSAVE #define SLEEP_TIME 120 // GNNS #define HAS_GPS 1 #define GPS_BAUDRATE 38400 #define PIN_GPS_EN 15 #define GPS_EN_ACTIVE 1 #define GPS_RX_PIN 44 #define GPS_TX_PIN 43 #define PIN_GPS_PPS 1 #define BUTTON_PIN 0 // vibration motor #define PIN_VIBRATION 2 // Have SPI interface SD card slot #define HAS_SDCARD #define SDCARD_USE_SPI1 #define SPI_MOSI (33) #define SPI_SCK (36) #define SPI_MISO (47) #define SPI_CS (48) #define SDCARD_CS SPI_CS #define SD_SPI_FREQUENCY 75000000U // TCA8418 keyboard #define KB_BL_PIN 42 // microphone PCM5102A #define PCM5102A_SCK 47 #define PCM5102A_DIN 17 #define PCM5102A_LRCK 18 // LTR_553ALS light sensor #define HAS_LTR553ALS // gyroscope BHI260AP #define BOARD_1V8_EN 38 #define HAS_BHI260AP // battery charger BQ25896 #define HAS_PPM 1 #define XPOWERS_CHIP_BQ25896 // battery quality management BQ27220 #define HAS_BQ27220 1 #define BQ27220_I2C_SDA SDA #define BQ27220_I2C_SCL SCL #define BQ27220_DESIGN_CAPACITY 1400 // LoRa #define USE_SX1262 #define USE_SX1268 #define LORA_EN 46 // LoRa enable pin #define LORA_SCK 36 #define LORA_MISO 47 #define LORA_MOSI 33 #define LORA_CS 3 #define LORA_DIO0 -1 // a No connect on the SX1262 module #define LORA_RESET 4 #define LORA_DIO1 5 // SX1262 IRQ #define LORA_DIO2 6 // 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 2.4 // Internally the TTGO module hooks the SX1262-DIO2 in to control the TX/RX switch (which is the default for the sx1262interface // code) #define MODEM_POWER_EN 41 #define MODEM_PWRKEY 40 #define MODEM_RST 9 #define MODEM_RI 7 #define MODEM_DTR 8 #define MODEM_RX 10 #define MODEM_TX 11 #define HAS_PHYSICAL_KEYBOARD 1 ================================================ FILE: variants/esp32s3/t-eth-elite/pins_arduino.h ================================================ #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 = 17; static const uint8_t SCL = 18; // Default SPI will be mapped to Radio static const uint8_t SS = 40; static const uint8_t MOSI = 11; static const uint8_t MISO = 9; static const uint8_t SCK = 10; #define SPI_MOSI (11) #define SPI_SCK (10) #define SPI_MISO (9) #define SPI_CS (12) #define SDCARD_CS SPI_CS #endif /* Pins_Arduino_h */ ================================================ FILE: variants/esp32s3/t-eth-elite/platformio.ini ================================================ [env:t-eth-elite] extends = esp32s3_base board = esp32s3box board_level = pr board_check = true board_build.partitions = default_16MB.csv build_flags = ${esp32s3_base.build_flags} -D T_ETH_ELITE -D HAS_UDP_MULTICAST=1 -I variants/esp32s3/t-eth-elite lib_ignore = Ethernet lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=github-tags depName=ETHClass2 packageName=meshtastic/ETHClass2 https://github.com/meshtastic/ETHClass2/archive/v1.0.0.zip ================================================ FILE: variants/esp32s3/t-eth-elite/rfswitch.h ================================================ #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, }; ================================================ FILE: variants/esp32s3/t-eth-elite/variant.h ================================================ #define HAS_SDCARD #define SDCARD_USE_SPI1 #define HAS_GPS 1 #define GPS_RX_PIN 39 #define GPS_TX_PIN 42 #define GPS_BAUDRATE_FIXED 1 #define GPS_BAUDRATE 9600 #define I2C_SDA 17 // I2C pins for this board #define I2C_SCL 18 #define HAS_SCREEN 1 // Allow for OLED Screens on I2C Header of shield #define LED_POWER 38 // 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 10 #define LORA_MISO 9 #define LORA_MOSI 11 #define LORA_CS 40 #define LORA_RESET 46 // per SX1276_Receive_Interrupt/utilities.h #define LORA_DIO0 8 #define LORA_DIO1 16 #define LORA_DIO2 RADIOLIB_NC // per SX1262_Receive_Interrupt/utilities.h #ifdef USE_SX1262 #define SX126X_CS LORA_CS #define SX126X_DIO1 8 #define SX126X_BUSY 16 #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 8 #define SX128X_DIO2 33 #define SX128X_DIO3 34 #define SX128X_BUSY 16 #define SX128X_RESET LORA_RESET #define SX128X_RXEN 13 #define SX128X_TXEN 38 #define SX128X_MAX_POWER 3 #endif // LR1121 #ifdef USE_LR1121 #define LR1121_IRQ_PIN 8 #define LR1121_NRESET_PIN LORA_RESET #define LR1121_BUSY_PIN 16 #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_ETHERNET 1 #define USE_WS5500 1 // this driver uses the same stack as the ESP32 Wifi driver #define ETH_MISO_PIN 47 #define ETH_MOSI_PIN 21 #define ETH_SCLK_PIN 48 #define ETH_CS_PIN 45 #define ETH_INT_PIN 14 #define ETH_RST_PIN -1 #define ETH_ADDR 1 ================================================ FILE: variants/esp32s3/t-watch-s3/pins_arduino.h ================================================ #ifndef Pins_Arduino_h #define Pins_Arduino_h #include // 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 */ ================================================ FILE: variants/esp32s3/t-watch-s3/platformio.ini ================================================ ; LilyGo T-Watch S3 [env:t-watch-s3] custom_meshtastic_hw_model = 51 custom_meshtastic_hw_model_slug = T_WATCH_S3 custom_meshtastic_architecture = esp32-s3 custom_meshtastic_actively_supported = true custom_meshtastic_support_level = 3 custom_meshtastic_display_name = LILYGO T-Watch S3 custom_meshtastic_images = t-watch-s3.svg custom_meshtastic_tags = LilyGo custom_meshtastic_partition_scheme = 8MB extends = esp32s3_base board = t-watch-s3 board_check = true board_build.partitions = default_16MB.csv upload_protocol = esptool build_flags = ${esp32s3_base.build_flags} -DT_WATCH_S3 -Ivariants/esp32s3/t-watch-s3 lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX lovyan03/LovyanGFX@1.2.19 # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib lewisxhe/SensorLib@0.3.4 # renovate: datasource=custom.pio depName=Adafruit DRV2605 packageName=adafruit/library/Adafruit DRV2605 Library adafruit/Adafruit DRV2605 Library@1.2.4 # renovate: datasource=git-refs depName=ESP8266Audio packageName=https://github.com/meshtastic/ESP8266Audio gitBranch=meshtastic-2.0.0-dacfix https://github.com/meshtastic/ESP8266Audio/archive/343024632ee78d6216907b2353fc943a62422d80.zip # renovate: datasource=custom.pio depName=ESP8266SAM packageName=earlephilhower/library/ESP8266SAM earlephilhower/ESP8266SAM@1.1.0 ================================================ FILE: variants/esp32s3/t-watch-s3/variant.h ================================================ // 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 USE_TFTDISPLAY 1 #define HAS_DRV2605 1 #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 WAKE_ON_TOUCH #define USE_POWERSAVE #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 DAC_I2S_MCLK 0 #define HAS_AXP2101 // PCF8563 RTC Module #define PCF8563_RTC 0x51 #define I2C_SDA 10 // For QMC6310 sensors and screens #define I2C_SCL 11 // For QMC6310 sensors and screens #define HAS_BMA423 1 #define BMA4XX_INT 14 // Interrupt for BMA_423 axis sensor #define HAS_GPS 1 #define GPS_DEFAULT_NOT_PRESENT 1 #define GPS_BAUDRATE 38400 #define GPS_RX_PIN 41 #define GPS_TX_PIN 42 #define BUTTON_PIN 0 // only for Plus version #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) #define USE_VIRTUAL_KEYBOARD 1 #define DISPLAY_CLOCK_FRAME 1 ================================================ FILE: variants/esp32s3/tbeam-s3-core/pins_arduino.h ================================================ #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 */ ================================================ FILE: variants/esp32s3/tbeam-s3-core/platformio.ini ================================================ ; The 1.0 release of the LilyGo TBEAM-S3-Core board [env:tbeam-s3-core] custom_meshtastic_hw_model = 12 custom_meshtastic_hw_model_slug = LILYGO_TBEAM_S3_CORE custom_meshtastic_architecture = esp32-s3 custom_meshtastic_actively_supported = true custom_meshtastic_support_level = 1 custom_meshtastic_display_name = LILYGO T-Beam Supreme custom_meshtastic_images = tbeam-s3-core.svg custom_meshtastic_tags = LilyGo custom_meshtastic_requires_dfu = true custom_meshtastic_partition_scheme = 8MB extends = esp32s3_base board = tbeam-s3-core board_build.partitions = default_8MB.csv board_check = true lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib lewisxhe/SensorLib@0.3.4 build_flags = ${esp32s3_base.build_flags} -I variants/esp32s3/tbeam-s3-core ================================================ FILE: variants/esp32s3/tbeam-s3-core/rfswitch.h ================================================ #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, }; ================================================ FILE: variants/esp32s3/tbeam-s3-core/variant.h ================================================ // #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 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 USE_LR1121 #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 // LR1121 #ifdef USE_LR1121 #define LR1121_IRQ_PIN 1 #define LR1121_NRESET_PIN LORA_RESET #define LR1121_BUSY_PIN 4 #define LR1121_SPI_NSS_PIN 10 #define LR1121_SPI_SCK_PIN 12 #define LR1121_SPI_MOSI_PIN 11 #define LR1121_SPI_MISO_PIN 13 #define LR11X0_DIO3_TCXO_VOLTAGE 3.0 #define LR11X0_DIO_AS_RF_SWITCH #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 // PCF8563 RTC Module #define PCF8563_RTC 0x51 // 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 // has 32768 Hz crystal #define HAS_32768HZ 1 #define USE_SH1106 ================================================ FILE: variants/esp32s3/tlora-pager/pins_arduino.h ================================================ #ifndef Pins_Arduino_h #define Pins_Arduino_h #include #define USB_VID 0x303a #define USB_PID 0x1001 // used for keyboard, battery gauge, charger and haptic driver static const uint8_t SDA = 3; static const uint8_t SCL = 2; // Default SPI will be mapped to Radio static const uint8_t SS = 36; static const uint8_t MOSI = 34; static const uint8_t MISO = 33; static const uint8_t SCK = 35; #endif /* Pins_Arduino_h */ ================================================ FILE: variants/esp32s3/tlora-pager/platformio.ini ================================================ ; LilyGo T-Lora-Pager [env:tlora-pager] custom_meshtastic_hw_model = 103 custom_meshtastic_hw_model_slug = T_LORA_PAGER custom_meshtastic_architecture = esp32-s3 custom_meshtastic_actively_supported = true custom_meshtastic_support_level = 1 custom_meshtastic_display_name = LILYGO T-LoRa Pager custom_meshtastic_images = lilygo-tlora-pager.svg custom_meshtastic_tags = LilyGo custom_meshtastic_requires_dfu = true custom_meshtastic_partition_scheme = 16MB extends = esp32s3_base board = t-deck-pro ; same as T-Deck Pro board_check = true board_build.partitions = default_16MB.csv upload_protocol = esptool build_src_filter = ${esp32s3_base.build_src_filter} +<../variants/esp32s3/tlora-pager> build_flags = ${esp32s3_base.build_flags} -I variants/esp32s3/tlora-pager -D T_LORA_PAGER -D BOARD_HAS_PSRAM -D HAS_SDCARD -D ENABLE_ROTARY_PULLUP -D ENABLE_BUTTON_PULLUP -D ROTARY_BUXTRONICS lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX lovyan03/LovyanGFX@1.2.19 # renovate: datasource=git-refs depName=ESP8266Audio packageName=https://github.com/meshtastic/ESP8266Audio gitBranch=meshtastic-2.0.0-dacfix https://github.com/meshtastic/ESP8266Audio/archive/343024632ee78d6216907b2353fc943a62422d80.zip # renovate: datasource=custom.pio depName=ESP8266SAM packageName=earlephilhower/library/ESP8266SAM earlephilhower/ESP8266SAM@1.1.0 # renovate: datasource=custom.pio depName=Adafruit DRV2605 packageName=adafruit/library/Adafruit DRV2605 Library adafruit/Adafruit DRV2605 Library@1.2.4 # renovate: datasource=custom.pio depName=PCF8563 packageName=lewisxhe/library/PCF8563_Library lewisxhe/PCF8563_Library@1.0.1 # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib lewisxhe/SensorLib@0.3.4 # renovate: datasource=github-tags depName=pschatzmann_arduino-audio-driver packageName=pschatzmann/arduino-audio-driver https://github.com/pschatzmann/arduino-audio-driver/archive/v0.2.1.zip # TODO renovate https://github.com/mverch67/BQ27220/archive/07d92be846abd8a0258a50c23198dac0858b22ed.zip # TODO renovate https://github.com/mverch67/RotaryEncoder/archive/da958a21389cbcd485989705df602a33e092dd88.zip [env:tlora-pager-tft] board_level = extra extends = env:tlora-pager build_flags = ${env:tlora-pager.build_flags} -D CONFIG_DISABLE_HAL_LOCKS=1 -D INPUTDRIVER_ROTARY_TYPE=1 -D INPUTDRIVER_ROTARY_UP=40 -D INPUTDRIVER_ROTARY_DOWN=41 -D INPUTDRIVER_ROTARY_BTN=7 -D INPUTDRIVER_BUTTON_TYPE=0 -D HAS_SCREEN=1 -D HAS_TFT=1 -D USE_I2S_BUZZER -D RAM_SIZE=5120 -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 USE_LOG_DEBUG -D LOG_DEBUG_INC=\"DebugConfiguration.h\" -D RADIOLIB_SPI_PARANOID=0 -D LGFX_SCREEN_WIDTH=222 -D LGFX_SCREEN_HEIGHT=480 -D DISPLAY_SIZE=480x222 ; landscape mode -D DISPLAY_SET_RESOLUTION -D LGFX_DRIVER=LGFX_TLORA_PAGER -D GFX_DRIVER_INC=\"graphics/LGFX/LGFX_T_LORA_PAGER.h\" ; -D LVGL_DRIVER=LVGL_T_LORA_PAGER ; -D LV_USE_ST7796=1 -D VIEW_480x222 -D USE_PACKET_API -D MAP_FULL_REDRAW lib_deps = ${env:tlora-pager.lib_deps} ${device-ui_base.lib_deps} ================================================ FILE: variants/esp32s3/tlora-pager/rfswitch.h ================================================ #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, {LOW, HIGH}}, {LR11x0::MODE_TX, {HIGH, LOW}}, {LR11x0::MODE_TX_HP, {HIGH, LOW}}, {LR11x0::MODE_TX_HF, {LOW, LOW}}, {LR11x0::MODE_GNSS, {LOW, LOW}}, {LR11x0::MODE_WIFI, {LOW, LOW}}, END_OF_MODE_TABLE, }; ================================================ FILE: variants/esp32s3/tlora-pager/variant.cpp ================================================ #include "variant.h" #include "ExtensionIOXL9555.hpp" extern ExtensionIOXL9555 io; void earlyInitVariant() { pinMode(LORA_CS, OUTPUT); digitalWrite(LORA_CS, HIGH); pinMode(SDCARD_CS, OUTPUT); digitalWrite(SDCARD_CS, HIGH); pinMode(TFT_CS, OUTPUT); digitalWrite(TFT_CS, HIGH); pinMode(KB_INT, INPUT_PULLUP); // io expander io.begin(Wire, XL9555_SLAVE_ADDRESS0, SDA, SCL); io.pinMode(EXPANDS_DRV_EN, OUTPUT); io.digitalWrite(EXPANDS_DRV_EN, HIGH); io.pinMode(EXPANDS_AMP_EN, OUTPUT); io.digitalWrite(EXPANDS_AMP_EN, LOW); io.pinMode(EXPANDS_LORA_EN, OUTPUT); io.digitalWrite(EXPANDS_LORA_EN, HIGH); io.pinMode(EXPANDS_GPS_EN, OUTPUT); io.digitalWrite(EXPANDS_GPS_EN, HIGH); io.pinMode(EXPANDS_KB_EN, OUTPUT); io.digitalWrite(EXPANDS_KB_EN, HIGH); io.pinMode(EXPANDS_SD_EN, OUTPUT); io.digitalWrite(EXPANDS_SD_EN, HIGH); io.pinMode(EXPANDS_GPIO_EN, OUTPUT); io.digitalWrite(EXPANDS_GPIO_EN, HIGH); io.pinMode(EXPANDS_SD_PULLEN, INPUT); } ================================================ FILE: variants/esp32s3/tlora-pager/variant.h ================================================ // ST7796 TFT LCD #define TFT_CS 38 #define ST7796_CS TFT_CS #define ST7796_RS 37 // DC #define ST7796_SDA MOSI // MOSI #define ST7796_SCK SCK #define ST7796_RESET -1 #define ST7796_MISO MISO #define ST7796_BUSY -1 #define ST7796_BL 42 #define ST7796_SPI_HOST SPI2_HOST #define TFT_BL 42 #define SPI_FREQUENCY 75000000 #define SPI_READ_FREQUENCY 16000000 #define TFT_HEIGHT 480 #define TFT_WIDTH 222 #define TFT_OFFSET_X 49 #define TFT_OFFSET_Y 0 #define TFT_OFFSET_ROTATION 3 #define SCREEN_ROTATE #define SCREEN_TRANSITION_FRAMERATE 5 #define BRIGHTNESS_DEFAULT 130 // Medium Low Brightness #define USE_TFTDISPLAY 1 #define HAS_PHYSICAL_KEYBOARD 1 #define I2C_SDA SDA #define I2C_SCL SCL #define HAS_DRV2605 1 #define USE_POWERSAVE #define SLEEP_TIME 120 // GNNS #define HAS_GPS 1 #define GPS_BAUDRATE 38400 #define GPS_RX_PIN 4 #define GPS_TX_PIN 12 #define PIN_GPS_PPS 13 // PCF85063 RTC Module #define PCF85063_RTC 0x51 // Rotary #define ROTARY_A (40) #define ROTARY_B (41) #define ROTARY_PRESS (7) #define BUTTON_PIN 0 // SPI interface SD card slot #define SPI_MOSI MOSI #define SPI_SCK SCK #define SPI_MISO MISO #define SPI_CS 21 #define SDCARD_CS SPI_CS #define SD_SPI_FREQUENCY 75000000U // TCA8418 keyboard #define I2C_NO_RESCAN #define KB_BL_PIN 46 #define KB_INT 6 // audio codec ES8311 #define HAS_I2S #define DAC_I2S_BCK 11 #define DAC_I2S_WS 18 #define DAC_I2S_DOUT 45 #define DAC_I2S_DIN 17 #define DAC_I2S_MCLK 10 // gyroscope BHI260AP #define HAS_BHI260AP // battery charger BQ25896 #define HAS_PPM 1 #define XPOWERS_CHIP_BQ25896 // battery quality management BQ27220 #define HAS_BQ27220 1 #define BQ27220_I2C_SDA SDA #define BQ27220_I2C_SCL SCL #define BQ27220_DESIGN_CAPACITY 1500 // NFC ST25R3916 #define NFC_INT 5 #define NFC_CS 39 // External expansion chip XL9555 #define USE_XL9555 #define EXPANDS_DRV_EN (0) #define EXPANDS_AMP_EN (1) #define EXPANDS_KB_RST (2) #define EXPANDS_LORA_EN (3) #define EXPANDS_GPS_EN (4) #define EXPANDS_NFC_EN (5) #define EXPANDS_GPS_RST (7) #define EXPANDS_KB_EN (8) #define EXPANDS_GPIO_EN (9) #define EXPANDS_SD_DET (10) #define EXPANDS_SD_PULLEN (11) #define EXPANDS_SD_EN (12) // LoRa #define USE_SX1262 #define USE_SX1268 #define USE_SX1280 #define USE_LR1121 #define LORA_SCK 35 #define LORA_MISO 33 #define LORA_MOSI 34 #define LORA_CS 36 #define LORA_RESET 47 #define LORA_DIO0 -1 // a No connect on the SX1262 module #define LORA_DIO1 14 // SX1262 IRQ #define LORA_DIO2 48 // 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 #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 3.0 #define SX128X_CS LORA_CS #define SX128X_DIO1 LORA_DIO1 #define SX128X_BUSY LORA_DIO2 #define SX128X_RESET LORA_RESET #define LR1121_IRQ_PIN LORA_DIO1 #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 ================================================ FILE: variants/esp32s3/tlora_t3s3_epaper/nicheGraphics.h ================================================ #pragma once #include "configuration.h" #ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS // InkHUD-specific components // --------------------------- #include "graphics/niche/InkHUD/InkHUD.h" // Applets #include "graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h" #include "graphics/niche/InkHUD/Applets/User/DM/DMApplet.h" #include "graphics/niche/InkHUD/Applets/User/FavoritesMap/FavoritesMapApplet.h" #include "graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.h" #include "graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h" #include "graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.h" #include "graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h" // Shared NicheGraphics components // -------------------------------- #include "graphics/niche/Drivers/EInk/DEPG0213BNS800.h" #include "graphics/niche/Inputs/TwoButton.h" void setupNicheGraphics() { using namespace NicheGraphics; // SPI // ----------------------------- // Display is connected to HSPI SPIClass *hspi = new SPIClass(HSPI); hspi->begin(PIN_EINK_SCLK, -1, PIN_EINK_MOSI, PIN_EINK_CS); // E-Ink Driver // ----------------------------- Drivers::EInk *driver = new Drivers::DEPG0213BNS800; driver->begin(hspi, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES); // InkHUD // ---------------------------- InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance(); // Set the driver inkhud->setDriver(driver); // Set how many FAST updates per FULL update // Set how unhealthy additional FAST updates beyond this number are inkhud->setDisplayResilience(15, 1.5); // Select fonts InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1252; InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; // Customize default settings inkhud->persistence->settings.userTiles.maxCount = 2; // How many tiles can the display handle? inkhud->persistence->settings.rotation = 3; // 270 degrees clockwise inkhud->persistence->settings.userTiles.count = 1; // One tile only by default, keep things simple for new users // Pick applets // Note: order of applets determines priority of "auto-show" feature inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); // Activated, autoshown inkhud->addApplet("DMs", new InkHUD::DMApplet); // - inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); // - inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); // - inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated inkhud->addApplet("Favorites Map", new InkHUD::FavoritesMapApplet); // - inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); // - inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, not autoshown, default on tile 0 // Start running InkHUD inkhud->begin(); // Buttons // -------------------------- Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // Shared NicheGraphics component // Setup the main user button buttons->setWiring(0, Inputs::TwoButton::getUserButtonPin(), true); buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); }); buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); buttons->start(); } #endif ================================================ FILE: variants/esp32s3/tlora_t3s3_epaper/pins_arduino.h ================================================ #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 */ ================================================ FILE: variants/esp32s3/tlora_t3s3_epaper/platformio.ini ================================================ [env:tlora-t3s3-epaper] custom_meshtastic_hw_model = 16 custom_meshtastic_hw_model_slug = TLORA_T3_S3 custom_meshtastic_architecture = esp32-s3 custom_meshtastic_actively_supported = true custom_meshtastic_support_level = 1 custom_meshtastic_display_name = LILYGO T-LoRa T3-S3 E-Ink custom_meshtastic_images = tlora-t3s3-epaper.svg custom_meshtastic_tags = LilyGo custom_meshtastic_requires_dfu = true extends = esp32s3_base board = tlora-t3s3-v1 board_check = true upload_protocol = esptool build_flags = ${esp32s3_base.build_flags} -D TLORA_T3S3_EPAPER -I variants/esp32s3/tlora_t3s3_epaper -DUSE_EINK -DEINK_DISPLAY_MODEL=GxEPD2_213_BN -DEINK_WIDTH=250 -DEINK_HEIGHT=122 -DUSE_EINK_DYNAMICDISPLAY ; Enable Dynamic EInk -DEINK_LIMIT_FASTREFRESH=20 ; How many consecutive fast-refreshes are permitted //20 -DEINK_LIMIT_RATE_BACKGROUND_SEC=30 ; Minimum interval between BACKGROUND updates //30 -DEINK_LIMIT_RATE_RESPONSIVE_SEC=1 ; Minimum interval between RESPONSIVE updates -DEINK_BACKGROUND_USES_FAST ; (Optional) Use FAST refresh for both BACKGROUND and RESPONSIVE, until a limit is reached. lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master https://github.com/meshtastic/GxEPD2/archive/c7eb4c3c167cf396ef4f541cc5d4c6aa42f3c46b.zip [env:tlora-t3s3-epaper-inkhud] extends = esp32s3_base, inkhud board = tlora-t3s3-v1 board_check = true upload_protocol = esptool build_src_filter = ${esp32s3_base.build_src_filter} ${inkhud.build_src_filter} build_flags = ${esp32s3_base.build_flags} ${inkhud.build_flags} -I variants/esp32s3/tlora_t3s3_epaper -D TLORA_T3S3_EPAPER lib_deps = ${inkhud.lib_deps} ; InkHUD libs first, so we get GFXRoot instead of AdafruitGFX ${esp32s3_base.lib_deps} ================================================ FILE: variants/esp32s3/tlora_t3s3_epaper/variant.h ================================================ #define HAS_SDCARD #define SDCARD_USE_SPI1 // Display (E-Ink) #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_DEFAULT_NOT_PRESENT 1 #define GPS_RX_PIN 44 #define GPS_TX_PIN 43 #define LED_POWER 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 ================================================ FILE: variants/esp32s3/tlora_t3s3_v1/pins_arduino.h ================================================ #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 */ ================================================ FILE: variants/esp32s3/tlora_t3s3_v1/platformio.ini ================================================ [env:tlora-t3s3-v1] custom_meshtastic_hw_model = 16 custom_meshtastic_hw_model_slug = TLORA_T3_S3 custom_meshtastic_architecture = esp32-s3 custom_meshtastic_actively_supported = true custom_meshtastic_support_level = 1 custom_meshtastic_display_name = LILYGO T-LoRa T3-S3 custom_meshtastic_images = tlora-t3s3-v1.svg custom_meshtastic_tags = LilyGo custom_meshtastic_requires_dfu = true extends = esp32s3_base board = tlora-t3s3-v1 board_check = true upload_protocol = esptool build_flags = ${esp32s3_base.build_flags} -D TLORA_T3S3_V1 -I variants/esp32s3/tlora_t3s3_v1 ================================================ FILE: variants/esp32s3/tlora_t3s3_v1/rfswitch.h ================================================ #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, }; ================================================ FILE: variants/esp32s3/tlora_t3s3_v1/variant.h ================================================ #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_POWER 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 ================================================ FILE: variants/esp32s3/tracksenger/internal/pins_arduino.h ================================================ #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 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 */ ================================================ FILE: variants/esp32s3/tracksenger/internal/variant.h ================================================ #define LED_POWER 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 USE_TFTDISPLAY 1 #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_OVERRIDE COLOR565(255, 255, 255) // keyboard changes #define PIN_BUZZER 43 #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 ================================================ FILE: variants/esp32s3/tracksenger/lcd/pins_arduino.h ================================================ #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 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 */ ================================================ FILE: variants/esp32s3/tracksenger/lcd/variant.h ================================================ #define LED_POWER 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 #define USE_TFTDISPLAY 1 // 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_OVERRIDE COLOR565(255, 255, 255) // keyboard changes #define PIN_BUZZER 43 #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 ================================================ FILE: variants/esp32s3/tracksenger/oled/pins_arduino.h ================================================ #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 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 */ ================================================ FILE: variants/esp32s3/tracksenger/oled/variant.h ================================================ #define LED_POWER 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_OVERRIDE COLOR565(255, 255, 255) // keyboard changes #define PIN_BUZZER 43 #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 ================================================ FILE: variants/esp32s3/tracksenger/platformio.ini ================================================ [env:tracksenger] custom_meshtastic_hw_model = 48 custom_meshtastic_hw_model_slug = HELTEC_WIRELESS_TRACKER custom_meshtastic_architecture = esp32-s3 custom_meshtastic_actively_supported = true custom_meshtastic_support_level = 3 custom_meshtastic_display_name = TrackSenger (small TFT) custom_meshtastic_requires_dfu = true custom_meshtastic_partition_scheme = 8MB extends = esp32s3_base board = heltec_wireless_tracker board_build.partitions = default_8MB.csv upload_protocol = esp-builtin build_flags = ${esp32s3_base.build_flags} -I variants/esp32s3/tracksenger/internal -D HELTEC_TRACKER_V1_1 ;-D DEBUG_DISABLED ; uncomment this line to disable DEBUG output lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX lovyan03/LovyanGFX@1.2.19 [env:tracksenger-lcd] custom_meshtastic_hw_model = 48 custom_meshtastic_hw_model_slug = HELTEC_WIRELESS_TRACKER custom_meshtastic_architecture = esp32-s3 custom_meshtastic_actively_supported = false custom_meshtastic_support_level = 3 custom_meshtastic_display_name = TrackSenger (big TFT) custom_meshtastic_requires_dfu = true custom_meshtastic_partition_scheme = 8MB extends = esp32s3_base board = heltec_wireless_tracker board_build.partitions = default_8MB.csv upload_protocol = esp-builtin build_flags = ${esp32s3_base.build_flags} -I variants/esp32s3/tracksenger/lcd -D HELTEC_TRACKER_V1_1 ;-D DEBUG_DISABLED ; uncomment this line to disable DEBUG output lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX lovyan03/LovyanGFX@1.2.19 [env:tracksenger-oled] custom_meshtastic_hw_model = 48 custom_meshtastic_hw_model_slug = HELTEC_WIRELESS_TRACKER custom_meshtastic_architecture = esp32-s3 custom_meshtastic_actively_supported = true custom_meshtastic_support_level = 3 custom_meshtastic_display_name = TrackSenger (big OLED) custom_meshtastic_partition_scheme = 8MB extends = esp32s3_base board = heltec_wireless_tracker board_build.partitions = default_8MB.csv upload_protocol = esp-builtin build_flags = ${esp32s3_base.build_flags} -I variants/esp32s3/tracksenger/oled -D HELTEC_TRACKER_V1_1 ;-D DEBUG_DISABLED ; uncomment this line to disable DEBUG output ================================================ FILE: variants/esp32s3/unphone/pins_arduino.h ================================================ #ifndef Pins_Arduino_h #define Pins_Arduino_h #include #define USB_VID 0x16D0 #define USB_PID 0x1178 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 */ ================================================ FILE: variants/esp32s3/unphone/platformio.ini ================================================ ; platformio.ini for unphone meshtastic [env:unphone] custom_meshtastic_hw_model = 59 custom_meshtastic_hw_model_slug = UNPHONE custom_meshtastic_architecture = esp32-s3 custom_meshtastic_actively_supported = true custom_meshtastic_support_level = 3 custom_meshtastic_display_name = unPhone custom_meshtastic_requires_dfu = true custom_meshtastic_partition_scheme = 8MB extends = esp32s3_base board = unphone board_build.partitions = partition-table-8MB.csv upload_speed = 921600 monitor_speed = 115200 monitor_filters = esp32_exception_decoder build_flags = ${esp32s3_base.build_flags} -D UNPHONE -I variants/esp32s3/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 -D USE_SX127x -D SDCARD_CS=43 build_src_filter = ${esp32s3_base.build_src_filter} +<../variants/esp32s3/unphone> lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX lovyan03/LovyanGFX@1.2.19 # TODO renovate https://gitlab.com/hamishcunningham/unphonelibrary#meshtastic@9.0.0 https://gitlab.com/hamishcunningham/unphonelibrary/-/archive/meshtastic/unphonelibrary-meshtastic.zip # renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel adafruit/Adafruit NeoPixel@1.15.4 [env:unphone-tft] board_level = extra extends = env:unphone build_flags = ${env:unphone.build_flags} -D CONFIG_DISABLE_HAL_LOCKS=1 -D INPUTDRIVER_BUTTON_TYPE=21 -D HAS_SCREEN=1 -D HAS_TFT=1 -D HAS_SDCARD -D SDCARD_CS=43 -D DISPLAY_SET_RESOLUTION -D RAM_SIZE=6144 -D LV_CACHE_DEF_SIZE=2097152 -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_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 LGFX_SCREEN_WIDTH=320 -D LGFX_SCREEN_HEIGHT=480 -D DISPLAY_SIZE=320x480 ; portrait mode -D LGFX_DRIVER=LGFX_UNPHONE_V9 -D GFX_DRIVER_INC=\"graphics/LGFX/LGFX_UNPHONE.h\" -D VIEW_320x240 -D USE_PACKET_API -D MAP_FULL_REDRAW lib_deps = ${env:unphone.lib_deps} ${device-ui_base.lib_deps} ================================================ FILE: variants/esp32s3/unphone/variant.cpp ================================================ // 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); } } ================================================ FILE: variants/esp32s3/unphone/variant.h ================================================ // 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 USE_TFTDISPLAY 1 #define HAS_TOUCHSCREEN 1 #define USE_XPT2046 1 #define TOUCH_CS 38 #define USE_POWERSAVE #define SLEEP_TIME 180 #define HAS_GPS \ 0 // the unphone doesn't have a gps module by default (though // GPS featherwing -- https://www.adafruit.com/product/3133 // -- can be added) #undef GPS_RX_PIN #undef GPS_TX_PIN #define SD_SPI_FREQUENCY 25000000 #define LED_POWER 13 // the red part of the RGB LED #define LED_STATE_ON 0 // State when LED is lit #define ALT_BUTTON_PIN 21 // Button 3 - square - top button in landscape mode #define BUTTON_PIN 0 // Circle button #define BUTTON_NEED_PULLUP // we do need a helping hand up #define CANCEL_BUTTON_PIN 45 // Button 1 - triangle - bottom button in landscape mode #define CANCEL_BUTTON_ACTIVE_LOW true #define CANCEL_BUTTON_ACTIVE_PULLUP true #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 ================================================ FILE: variants/native/portduino/platformio.ini ================================================ [native_base] extends = portduino_base build_flags = ${portduino_base.build_flags} -I variants/native/portduino -I /usr/include board = cross_platform board_level = extra lib_deps = ${portduino_base.lib_deps} # renovate: datasource=custom.pio depName=Melopero RV3028 packageName=melopero/library/Melopero RV3028 melopero/Melopero RV3028@1.2.0 build_src_filter = ${portduino_base.build_src_filter} [env:native] extends = native_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 = ${native_base.build_flags} !pkg-config --libs libulfius --silence-errors || : !pkg-config --libs openssl --silence-errors || : !pkg-config --cflags --libs sdl2 --silence-errors || : !pkg-config --cflags --libs libbsd-overlay --silence-errors || : [env:native-tft] extends = native_base build_type = release lib_deps = ${native_base.lib_deps} ${device-ui_base.lib_deps} build_flags = ${native_base.build_flags} -Os -lX11 -linput -lxkbcommon -ffunction-sections -fdata-sections -Wl,--gc-sections -D RAM_SIZE=16384 -D USE_X11=1 -D HAS_TFT=1 -D HAS_SCREEN=1 -D LV_CACHE_DEF_SIZE=6291456 -D LV_BUILD_TEST=0 -D LV_USE_LIBINPUT=1 -D LV_LVGL_H_INCLUDE_SIMPLE -D LV_CONF_INCLUDE_SIMPLE -D LV_COMP_CONF_INCLUDE_SIMPLE -D USE_LOG_DEBUG -D LOG_DEBUG_INC=\"DebugConfiguration.h\" -D USE_PACKET_API -D VIEW_320x240 !pkg-config --libs libulfius --silence-errors || : !pkg-config --libs openssl --silence-errors || : !pkg-config --cflags --libs sdl2 --silence-errors || : !pkg-config --cflags --libs libbsd-overlay --silence-errors || : build_src_filter = ${native_base.build_src_filter} [env:native-fb] extends = native_base build_type = release lib_deps = ${native_base.lib_deps} ${device-ui_base.lib_deps} build_flags = ${native_base.build_flags} -Os -ffunction-sections -fdata-sections -Wl,--gc-sections -D RAM_SIZE=8192 -D USE_FRAMEBUFFER=1 -D LV_COLOR_DEPTH=32 -D HAS_TFT=1 -D HAS_SCREEN=1 -D LV_BUILD_TEST=0 -D LV_USE_LOG=0 -D LV_USE_EVDEV=1 -D LV_LVGL_H_INCLUDE_SIMPLE -D LV_CONF_INCLUDE_SIMPLE -D LV_COMP_CONF_INCLUDE_SIMPLE -D USE_LOG_DEBUG -D LOG_DEBUG_INC=\"DebugConfiguration.h\" -D USE_PACKET_API -D VIEW_320x240 -D MAP_FULL_REDRAW !pkg-config --libs libulfius --silence-errors || : !pkg-config --libs openssl --silence-errors || : !pkg-config --cflags --libs libbsd-overlay --silence-errors || : build_src_filter = ${native_base.build_src_filter} [env:native-tft-debug] extends = native_base build_type = debug lib_deps = ${native_base.lib_deps} ${device-ui_base.lib_deps} build_flags = ${native_base.build_flags} -O0 -fsanitize=address -lX11 -linput -lxkbcommon -D DEBUG_HEAP -D RAM_SIZE=16384 -D USE_X11=1 -D HAS_TFT=1 -D HAS_SCREEN=0 -D LV_CACHE_DEF_SIZE=6291456 -D LV_BUILD_TEST=0 -D LV_USE_LOG=1 -D LV_USE_SYSMON=1 -D LV_USE_PERF_MONITOR=1 -D LV_USE_MEM_MONITOR=0 -D LV_USE_PROFILER=0 -D LV_USE_LIBINPUT=1 -D LV_LVGL_H_INCLUDE_SIMPLE -D LV_CONF_INCLUDE_SIMPLE -D LV_COMP_CONF_INCLUDE_SIMPLE -D USE_LOG_DEBUG -D LOG_DEBUG_INC=\"DebugConfiguration.h\" -D USE_PACKET_API -D VIEW_320x240 !pkg-config --libs libulfius --silence-errors || : !pkg-config --libs openssl --silence-errors || : !pkg-config --cflags --libs libbsd-overlay --silence-errors || : build_src_filter = ${env:native-tft.build_src_filter} [env:coverage] extends = env:native build_flags = -lgcov --coverage -fprofile-abs-path -fsanitize=address ${env:native.build_flags} ; https://docs.platformio.org/en/latest/projectconf/sections/env/options/test/test_testing_command.html test_testing_command = ${platformio.build_dir}/${this.__env__}/meshtasticd -s ================================================ FILE: variants/native/portduino/variant.h ================================================ #ifndef HAS_SCREEN #define HAS_SCREEN 1 #endif #define USE_TFTDISPLAY 1 #define HAS_GPS 1 #define MAX_RX_TOPHONE portduino_config.maxtophone #define MAX_NUM_NODES portduino_config.MaxNodes // RAK12002 RTC Module #define RV3028_RTC (uint8_t)0b1010010 // Enable Traffic Management Module for native/portduino #ifndef HAS_TRAFFIC_MANAGEMENT #define HAS_TRAFFIC_MANAGEMENT 1 #endif #ifndef TRAFFIC_MANAGEMENT_CACHE_SIZE #define TRAFFIC_MANAGEMENT_CACHE_SIZE 2048 #endif ================================================ FILE: variants/native/portduino-buildroot/platformio.ini ================================================ [env:buildroot] extends = portduino_base ; Optional libraries should be appended to `PLATFORMIO_BUILD_FLAGS` ; environment variable in the buildroot environment. build_flags = ${portduino_base.build_flags} -O0 -I variants/native/portduino-buildroot board = buildroot board_level = extra build_src_filter = ${portduino_base.build_src_filter} ================================================ FILE: variants/native/portduino-buildroot/variant.h ================================================ #define HAS_SCREEN 1 #define USE_TFTDISPLAY 1 #define HAS_GPS 1 #define MAX_RX_TOPHONE portduino_config.maxtophone #define MAX_NUM_NODES portduino_config.MaxNodes ================================================ FILE: variants/native/portduino.ini ================================================ ; The Portduino based 'native' environment. Currently supported on Linux targets with real LoRa hardware (or simulated). [portduino_base] platform = # renovate: datasource=git-refs depName=platform-native packageName=https://github.com/meshtastic/platform-native gitBranch=develop https://github.com/meshtastic/platform-native/archive/f566d364204416cdbf298e349213f7d551f793d9.zip framework = arduino build_src_filter = ${env.build_src_filter} - - - - - - - + - - lib_deps = ${env.lib_deps} ${networking_base.lib_deps} ${networking_extra.lib_deps} ${radiolib_base.lib_deps} ${environmental_base.lib_deps} # renovate: datasource=custom.pio depName=rweather/Crypto packageName=rweather/library/Crypto rweather/Crypto@0.4.0 # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX lovyan03/LovyanGFX@1.2.19 ; # renovate: datasource=git-refs depName=libch341-spi-userspace packageName=https://github.com/pine64/libch341-spi-userspace gitBranch=main https://github.com/pine64/libch341-spi-userspace/archive/23c42319a69cffcb65868e3c72e6bed83974a393.zip # renovate: datasource=custom.pio depName=adafruit/Adafruit seesaw Library packageName=adafruit/library/Adafruit seesaw Library adafruit/Adafruit seesaw Library@1.7.9 # renovate: datasource=git-refs depName=RAK12034-BMX160 packageName=https://github.com/RAKWireless/RAK12034-BMX160 gitBranch=main https://github.com/RAKWireless/RAK12034-BMX160/archive/dcead07ffa267d3c906e9ca4a1330ab989e957e2.zip # renovate: datasource=custom.pio depName=adafruit/Adafruit BME680 Library packageName=adafruit/library/Adafruit BME680 adafruit/Adafruit BME680 Library@^2.0.5 build_flags = ${arduino_base.build_flags} -D ARCH_PORTDUINO -fPIC -D_FORTIFY_SOURCE=2 -fstack-protector-all -Wstack-protector --param ssp-buffer-size=4 -Isrc/platform/portduino -DRADIOLIB_EEPROM_UNSUPPORTED -DPORTDUINO_LINUX_HARDWARE -DHAS_UDP_MULTICAST=1 -lpthread -lstdc++fs -lbluetooth -lgpiod -lyaml-cpp -li2c -luv -std=gnu17 -std=gnu++17 lib_ignore = Adafruit NeoPixel Adafruit ST7735 and ST7789 Library SD ================================================ FILE: variants/nrf52840/Dongle_nRF52840-pca10059-v1/platformio.ini ================================================ [env:pca10059_diy_eink] board_level = extra extends = nrf52840_base board = nordic_pca10059 build_flags = ${nrf52840_base.build_flags} -Ivariants/nrf52840/Dongle_nRF52840-pca10059-v1 -D NORDIC_PCA10059 -DEINK_DISPLAY_MODEL=GxEPD2_420_M01 -DEINK_WIDTH=300 -DEINK_HEIGHT=400 build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/Dongle_nRF52840-pca10059-v1> lib_deps = ${nrf52840_base.lib_deps} # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 zinggjm/GxEPD2@1.6.8 debug_tool = jlink ================================================ FILE: variants/nrf52840/Dongle_nRF52840-pca10059-v1/variant.cpp ================================================ /* 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); } ================================================ FILE: variants/nrf52840/Dongle_nRF52840-pca10059-v1/variant.h ================================================ /* 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_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 ================================================ FILE: variants/nrf52840/ELECROW-ThinkNode-M1/nicheGraphics.h ================================================ #pragma once #include "configuration.h" #ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS // InkHUD-specific components // --------------------------- #include "graphics/niche/InkHUD/InkHUD.h" // Applets #include "graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h" #include "graphics/niche/InkHUD/Applets/User/DM/DMApplet.h" #include "graphics/niche/InkHUD/Applets/User/FavoritesMap/FavoritesMapApplet.h" #include "graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.h" #include "graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h" #include "graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.h" #include "graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h" // Shared NicheGraphics components // -------------------------------- #include "graphics/niche/Drivers/Backlight/LatchingBacklight.h" #include "graphics/niche/Drivers/EInk/GDEY0154D67.h" #include "graphics/niche/Inputs/TwoButton.h" // Button feedback #include "buzz.h" void setupNicheGraphics() { using namespace NicheGraphics; // SPI // ----------------------------- // For NRF52 platforms, SPI pins are defined in variant.h SPI1.begin(); // E-Ink Driver // ----------------------------- Drivers::EInk *driver = new Drivers::GDEY0154D67; driver->begin(&SPI1, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES); // InkHUD // ---------------------------- InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance(); // Set the E-Ink driver inkhud->setDriver(driver); // Set how many FAST updates per FULL update // Set how unhealthy additional FAST updates beyond this number are // Todo: observe the display's performance in-person and adjust accordingly. // Currently set to the values given by Elecrow for EInkDynamicDisplay. inkhud->setDisplayResilience(10, 1.5); // Select fonts InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1252; InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; // Customize default settings inkhud->persistence->settings.userTiles.maxCount = 2; // Two applets side-by-side inkhud->persistence->settings.optionalFeatures.batteryIcon = true; // Device definitely has a battery // Setup backlight controller // Note: button is attached further down Drivers::LatchingBacklight *backlight = Drivers::LatchingBacklight::getInstance(); backlight->setPin(PIN_EINK_EN); // Pick applets // Note: order of applets determines priority of "auto-show" feature inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); // Activated, autoshown inkhud->addApplet("DMs", new InkHUD::DMApplet); // - inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); // - inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); // - inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); // - inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, no autoshow, default on tile 0 inkhud->addApplet("Favorites Map", new InkHUD::FavoritesMapApplet, false, false); // - // Start running InkHUD inkhud->begin(); // Buttons // -------------------------- Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // Shared NicheGraphics component // Elecrow diagram: https://www.elecrow.com/download/product/CIL12901M/ThinkNode-M1_User_Manual.pdf // #0: Main User Button // Labeled "Page Turn Button" by manual buttons->setWiring(0, PIN_BUTTON2); buttons->setTiming(0, 50, 500); // Todo: confirm 50ms is adequate debounce buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); }); buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); // #1: Aux Button // Labeled "Function Button" by manual // Todo: additional features buttons->setWiring(1, PIN_BUTTON1); buttons->setTiming(1, 50, 500); // 500ms before latch buttons->setHandlerDown(1, [backlight]() { backlight->peek(); }); buttons->setHandlerLongPress(1, [backlight]() { backlight->latch(); playBoop(); }); buttons->setHandlerShortPress(1, [backlight]() { backlight->off(); playChirp(); }); // Begin handling button events buttons->start(); } #endif ================================================ FILE: variants/nrf52840/ELECROW-ThinkNode-M1/platformio.ini ================================================ ; First prototype eink/nrf52840/sx1262 device [env:thinknode_m1] custom_meshtastic_hw_model = 89 custom_meshtastic_hw_model_slug = THINKNODE_M1 custom_meshtastic_architecture = nrf52840 custom_meshtastic_actively_supported = true custom_meshtastic_support_level = 1 custom_meshtastic_display_name = ThinkNode M1 custom_meshtastic_images = thinknode_m1.svg custom_meshtastic_tags = Elecrow extends = nrf52840_base board = ThinkNode-M1 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/nrf52840/ELECROW-ThinkNode-M1 -DELECROW_ThinkNode_M1 -DUSE_EINK -DEINK_DISPLAY_MODEL=GxEPD2_154_D67 -DEINK_WIDTH=200 -DEINK_HEIGHT=200 -DUSE_EINK_DYNAMICDISPLAY ; Enable Dynamic EInk -DEINK_LIMIT_FASTREFRESH=10 ; How many consecutive fast-refreshes are permitted //20 -DEINK_LIMIT_RATE_BACKGROUND_SEC=10 ; Minimum interval between BACKGROUND updates //30 -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/nrf52840/ELECROW-ThinkNode-M1> lib_deps = ${nrf52840_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master https://github.com/meshtastic/GxEPD2/archive/c7eb4c3c167cf396ef4f541cc5d4c6aa42f3c46b.zip # renovate: datasource=custom.pio depName=nRF52_PWM packageName=khoih-prog/library/nRF52_PWM khoih-prog/nRF52_PWM@1.0.1 ;upload_protocol = fs [env:thinknode_m1-inkhud] extends = nrf52840_base, inkhud board = ThinkNode-M1 board_check = true debug_tool = jlink build_flags = ${nrf52840_base.build_flags} ${inkhud.build_flags} -I variants/nrf52840/ELECROW-ThinkNode-M1 -D ELECROW_ThinkNode_M1 build_src_filter = ${nrf52_base.build_src_filter} ${inkhud.build_src_filter} +<../variants/nrf52840/ELECROW-ThinkNode-M1> lib_deps = ${inkhud.lib_deps} ; InkHUD libs first, so we get GFXRoot instead of AdafruitGFX ${nrf52840_base.lib_deps} ================================================ FILE: variants/nrf52840/ELECROW-ThinkNode-M1/variant.cpp ================================================ /* 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() { // pinMode(PIN_LED1, OUTPUT); // ledOff(PIN_LED1); } void variant_shutdown() { for (int pin = 0; pin < 48; pin++) { if (pin == SX126X_BUSY || pin == PIN_SPI_SCK || pin == SX126X_DIO1 || pin == PIN_SPI_MOSI || pin == PIN_SPI_MISO || pin == SX126X_CS || pin == SX126X_RESET || pin == PIN_NFC1 || pin == PIN_NFC2 || pin == PIN_BUTTON1 || pin == PIN_BUTTON2) { continue; } pinMode(pin, OUTPUT); digitalWrite(pin, LOW); if (pin >= 32) { NRF_P1->DIRCLR = (1 << (pin - 32)); } else { NRF_GPIO->DIRCLR = (1 << pin); } } nrf_gpio_cfg_input(PIN_BUTTON1, NRF_GPIO_PIN_PULLUP); // Configure the pin to be woken up as an input nrf_gpio_pin_sense_t sense = NRF_GPIO_PIN_SENSE_LOW; nrf_gpio_cfg_sense_set(PIN_BUTTON1, sense); nrf_gpio_cfg_input(PIN_BUTTON2, NRF_GPIO_PIN_PULLUP); nrf_gpio_pin_sense_t sense1 = NRF_GPIO_PIN_SENSE_LOW; nrf_gpio_cfg_sense_set(PIN_BUTTON2, sense1); } ================================================ FILE: variants/nrf52840/ELECROW-ThinkNode-M1/variant.h ================================================ /* 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_ELECROW_EINK_V1_0_ #define _VARIANT_ELECROW_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 // Number of pins defined in PinDescription array // 在PinDescription数组中定义的引脚数 #define PINS_COUNT (48) #define NUM_DIGITAL_PINS (48) #define NUM_ANALOG_INPUTS (1) #define NUM_ANALOG_OUTPUTS (0) // LED // #define PIN_LED1 (32 + 6) #define LED_POWER (32 + 4) // red #define LED_NOTIFICATION (0 + 13) // green #define POWER_LED_HARDWARE_BLINKS_WHILE_CHARGING // USB_CHECK #define EXT_PWR_DETECT (32 + 3) #define ADC_V (0 + 8) // #define LED_BLUE PIN_LED1 #define LED_STATE_ON 1 // State when LED is lit // LED灯亮时的状态 #define PIN_BUZZER (0 + 6) /* * Buttons */ #define PIN_BUTTON1 (32 + 10) #define PIN_BUTTON2 (32 + 7) #define ALT_BUTTON_PIN PIN_BUTTON2 #define ALT_BUTTON_ACTIVE_LOW true #define ALT_BUTTON_ACTIVE_PULLUP true /* * 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 BATTERY_SENSE_SAMPLES 30 #define PIN_NFC1 (9) #define PIN_NFC2 (10) /*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) /* 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 SX126X_POWER_EN (0 + 21) #define USE_SX1262 #define SX126X_CS (0 + 24) // FIXME - we really should define LORA_CS instead #define SX126X_DIO1 (0 + 20) // Note DIO2 is attached internally to the module to an analog switch for TX/RX switching // #define SX1262_DIO3 (0 + 21) // This is used as an *output* from the sx1262 and connected internally to power the tcxo, do not // drive from the main #define SX126X_BUSY (0 + 17) #define SX126X_RESET (0 + 25) #define SX126X_DIO2_AS_RF_SWITCH #define SX126X_DIO3_TCXO_VOLTAGE 3.3 #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_SPI1_MISO (32 + 7) #define PIN_SPI1_MOSI PIN_EINK_MOSI #define PIN_SPI1_SCK PIN_EINK_SCLK /* * GPS pins */ // #define HAS_GPS 1 #define GPS_L76K #define GPS_BAUDRATE 9600 #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 + 8) // This is for bits going TOWARDS the GPS #define GPS_RX_PIN (32 + 9) // This is for bits going TOWARDS the CPU #define GPS_THREAD_INTERVAL 50 #define PIN_GPS_SWITCH (32 + 1) // GPS开关判断 #define PIN_SERIAL1_TX GPS_TX_PIN #define PIN_SERIAL1_RX GPS_RX_PIN #define SERIAL_PRINT_PORT 0 /* * 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) // 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.02F) // #define HAS_SCREEN 0 #ifdef __cplusplus } #endif #endif ================================================ FILE: variants/nrf52840/ELECROW-ThinkNode-M3/platformio.ini ================================================ [env:thinknode_m3] custom_meshtastic_support_level = 1 custom_meshtastic_images = thinknode_m3.svg custom_meshtastic_tags = Elecrow extends = nrf52840_base board = ThinkNode-M3 board_check = true debug_tool = jlink custom_meshtastic_hw_model = 115 custom_meshtastic_hw_model_slug = THINKNODE_M3 custom_meshtastic_architecture = nrf52840 custom_meshtastic_display_name = Elecrow ThinkNode M3 custom_meshtastic_actively_supported = true build_flags = ${nrf52840_base.build_flags} -Ivariants/nrf52840/ELECROW-ThinkNode-M3 -DELECROW_ThinkNode_M3 -DGPS_POWER_TOGGLE -D CONFIG_NFCT_PINS_AS_GPIOS=1 -L "${platformio.libdeps_dir}/${this.__env__}/bsec2/src/cortex-m4/fpv4-sp-d16-hard" build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/ELECROW-ThinkNode-M3> lib_deps = ${nrf52840_base.lib_deps} # renovate: datasource=custom.pio depName=nRF52_PWM packageName=khoih-prog/library/nRF52_PWM khoih-prog/nRF52_PWM@1.0.1 # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib lewisxhe/SensorLib@0.3.4 ================================================ FILE: variants/nrf52840/ELECROW-ThinkNode-M3/rfswitch.h ================================================ #include "RadioLib.h" #include "nrf.h" // set RF switch configuration for ELECROW ThinkNode M3 // ELECROW ThinkNode M3 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, }; ================================================ FILE: variants/nrf52840/ELECROW-ThinkNode-M3/variant.cpp ================================================ /* 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 "meshUtils.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(KEY_POWER, OUTPUT); digitalWrite(KEY_POWER, HIGH); pinMode(RGB_POWER, OUTPUT); digitalWrite(RGB_POWER, HIGH); pinMode(LED_GREEN, OUTPUT); digitalWrite(LED_GREEN, LED_STATE_OFF); pinMode(LED_BLUE, OUTPUT); pinMode(PIN_POWER_USB, INPUT); pinMode(PIN_POWER_DONE, INPUT); pinMode(PIN_POWER_CHRG, INPUT); pinMode(BUTTON_PIN, INPUT_PULLUP); pinMode(EEPROM_POWER, OUTPUT); digitalWrite(EEPROM_POWER, HIGH); pinMode(PIN_EN1, OUTPUT); digitalWrite(PIN_EN1, HIGH); pinMode(PIN_EN2, OUTPUT); digitalWrite(PIN_EN2, HIGH); pinMode(ACC_POWER, OUTPUT); digitalWrite(ACC_POWER, LOW); pinMode(DHT_POWER, OUTPUT); digitalWrite(DHT_POWER, HIGH); pinMode(Battery_POWER, OUTPUT); digitalWrite(Battery_POWER, HIGH); pinMode(GPS_POWER, OUTPUT); digitalWrite(GPS_POWER, HIGH); } // called from main-nrf52.cpp during the cpuDeepSleep() function void variant_shutdown() { digitalWrite(LED_RED, HIGH); digitalWrite(LED_GREEN, HIGH); digitalWrite(LED_BLUE, HIGH); digitalWrite(PIN_EN1, LOW); digitalWrite(PIN_EN2, LOW); digitalWrite(EEPROM_POWER, LOW); digitalWrite(KEY_POWER, LOW); digitalWrite(DHT_POWER, LOW); digitalWrite(ACC_POWER, LOW); digitalWrite(Battery_POWER, LOW); digitalWrite(GPS_POWER, LOW); // This sets the pin to OUTPUT and LOW for the pins *not* in the if block. for (int pin = 0; pin < 48; pin++) { if (pin == PIN_POWER_USB || pin == BUTTON_PIN || pin == PIN_EN1 || pin == PIN_EN2 || pin == DHT_POWER || pin == ACC_POWER || pin == Battery_POWER || pin == GPS_POWER || pin == LR1110_SPI_MISO_PIN || pin == LR1110_SPI_MOSI_PIN || pin == LR1110_SPI_SCK_PIN || pin == LR1110_SPI_NSS_PIN || pin == LR1110_BUSY_PIN || pin == LR1110_NRESET_PIN || pin == LR1110_IRQ_PIN || pin == GPS_TX_PIN || pin == GPS_RX_PIN || pin == LED_GREEN || pin == LED_RED || pin == LED_BLUE) { continue; } pinMode(pin, OUTPUT); digitalWrite(pin, LOW); if (pin >= 32) { NRF_P1->DIRCLR = (1 << (pin - 32)); } else { NRF_GPIO->DIRCLR = (1 << pin); } } nrf_gpio_cfg_input(BUTTON_PIN, NRF_GPIO_PIN_PULLUP); // Configure the pin to be woken up as an input nrf_gpio_pin_sense_t sense1 = NRF_GPIO_PIN_SENSE_LOW; nrf_gpio_cfg_sense_set(BUTTON_PIN, sense1); nrf_gpio_cfg_input(PIN_POWER_USB, NRF_GPIO_PIN_PULLDOWN); // Configure the pin to be woken up as an input nrf_gpio_pin_sense_t sense2 = NRF_GPIO_PIN_SENSE_HIGH; nrf_gpio_cfg_sense_set(PIN_POWER_USB, sense2); } ================================================ FILE: variants/nrf52840/ELECROW-ThinkNode-M3/variant.h ================================================ /* 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_ELECROW_EINK_V1_0_ #define _VARIANT_ELECROW_EINK_V1_0_ #ifdef __cplusplus extern "C" { #endif // __cplusplus #include "WVariant.h" #define VARIANT_MCK (64000000ul) #define USE_LFXO // Board uses 32khz crystal for LF #define ELECROW_ThinkNode_M3 1 // 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) // Power Pin #define NRF_APM #define GPS_POWER 14 #define PIN_POWER_USB 31 #define EXT_PWR_DETECT PIN_POWER_USB #define PIN_POWER_DONE 24 #define PIN_POWER_CHRG 32 #define KEY_POWER 16 #define ACC_POWER 2 #define DHT_POWER 3 #define Battery_POWER 17 #define RGB_POWER 29 #define EEPROM_POWER 7 // LED #define LED_RED 33 #define LED_POWER LED_RED #define LED_GREEN 35 #define LED_NOTIFICATION LED_GREEN #define LED_BLUE 37 #define LED_PAIRING LED_BLUE // Signals the Status LED Module to handle this LED #define LED_STATE_ON LOW #define LED_STATE_OFF HIGH // BUZZER #define PIN_BUZZER 23 #define PIN_EN1 36 #define PIN_EN2 34 /*Wire Interfaces*/ #define WIRE_INTERFACES_COUNT 1 #define PIN_WIRE_SDA 26 #define PIN_WIRE_SCL 27 // Temperature correction for sensor #define AHT10_TEMP_OFFSET -5.0 /*GPS pins*/ #define HAS_GPS 1 #define GPS_BAUDRATE 9600 #define PIN_GPS_RESET 25 #define PIN_GPS_STANDBY 21 #define GPS_TX_PIN 22 #define GPS_RX_PIN 20 #define GPS_THREAD_INTERVAL 50 #define PIN_SERIAL1_TX GPS_TX_PIN #define PIN_SERIAL1_RX GPS_RX_PIN // Button #define BUTTON_PIN 12 #define BUTTON_PIN_ALT (0 + 12) // Battery #define BATTERY_PIN 5 #define BATTERY_SENSE_RESOLUTION_BITS 12 #define BATTERY_SENSE_RESOLUTION 4096.0 #undef AREF_VOLTAGE #define AREF_VOLTAGE 2.4 #define VBAT_AR_INTERNAL AR_INTERNAL_2_4 #define ADC_MULTIPLIER (1.75) /*SPI Interfaces*/ #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 /*LORA Interfaces*/ #define USE_LR1110 #define LR1110_IRQ_PIN 40 #define LR1110_NRESET_PIN 42 #define LR1110_BUSY_PIN 43 #define LR1110_SPI_NSS_PIN 44 #define LR1110_SPI_SCK_PIN 45 #define LR1110_SPI_MOSI_PIN 46 #define LR1110_SPI_MISO_PIN 47 #define LR11X0_DIO3_TCXO_VOLTAGE 3.3 #define LR11X0_DIO_AS_RF_SWITCH #define SERIAL_PRINT_PORT 0 // PCF8563 RTC Module #define PCF8563_RTC 0x51 #ifdef __cplusplus } #endif #endif ================================================ FILE: variants/nrf52840/ELECROW-ThinkNode-M4/platformio.ini ================================================ ; ThinkNode M4 - Powerbank nrf52840/LR1110 by Elecrow [env:thinknode_m4] extends = nrf52840_base board = ThinkNode-M4 board_check = true debug_tool = jlink build_flags = ${nrf52840_base.build_flags} -Ivariants/nrf52840/ELECROW-ThinkNode-M4 -DELECROW_ThinkNode_M4 build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/ELECROW-ThinkNode-M4> lib_deps = ${nrf52840_base.lib_deps} lewisxhe/PCF8563_Library@^1.0.1 ================================================ FILE: variants/nrf52840/ELECROW-ThinkNode-M4/rfswitch.h ================================================ #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, {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, }; ================================================ FILE: variants/nrf52840/ELECROW-ThinkNode-M4/variant.cpp ================================================ /* 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() { pinMode(LED_PAIRING, OUTPUT); ledOff(LED_PAIRING); pinMode(Battery_LED_1, OUTPUT); ledOff(Battery_LED_1); pinMode(Battery_LED_2, OUTPUT); ledOff(Battery_LED_2); pinMode(Battery_LED_3, OUTPUT); ledOff(Battery_LED_3); pinMode(Battery_LED_4, OUTPUT); ledOff(Battery_LED_4); } /// called from main-nrf52.cpp during the cpuDeepSleep() function void variant_shutdown() { for (int pin = 0; pin < 48; pin++) { if (pin == PIN_GPS_EN || pin == PIN_BUTTON1) { continue; } pinMode(pin, OUTPUT); digitalWrite(pin, LOW); if (pin >= 32) { NRF_P1->DIRCLR = (1 << (pin - 32)); } else { NRF_GPIO->DIRCLR = (1 << pin); } } digitalWrite(PIN_GPS_EN, HIGH); } ================================================ FILE: variants/nrf52840/ELECROW-ThinkNode-M4/variant.h ================================================ /* 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_ELECROW_THINKNODE_M4_ #define _VARIANT_ELECROW_THINKNODE_M4_ /** Master clock frequency */ #define VARIANT_MCK (64000000ul) #define USE_LFXO /*---------------------------------------------------------------------------- * Headers *----------------------------------------------------------------------------*/ #include "WVariant.h" #ifdef __cplusplus extern "C" { #endif // __cplusplus #define PINS_COUNT (48) #define NUM_DIGITAL_PINS (48) #define NUM_ANALOG_INPUTS (1) #define NUM_ANALOG_OUTPUTS (0) // LEDs #define LED_BLUE -1 #define LED_NOTIFICATION (32 + 9) #define LED_PAIRING (13) #define Battery_LED_1 (15) #define Battery_LED_2 (17) #define Battery_LED_3 (32 + 2) #define Battery_LED_4 (32 + 4) #define LED_STATE_ON 1 // Button #define PIN_BUTTON1 (4) // Battery ADC #define PIN_A0 (2) #define BATTERY_PIN PIN_A0 #define BATTERY_SENSE_SAMPLES 30 #define ADC_RESOLUTION 14 #define BATTERY_SENSE_RESOLUTION_BITS 12 #define BATTERY_SENSE_RESOLUTION 4096.0 #define ADC_MULTIPLIER (2.00F) #undef AREF_VOLTAGE #define AREF_VOLTAGE 3.0 #define VBAT_AR_INTERNAL AR_INTERNAL_3_0 #define HAS_SERIAL_BATTERY_LEVEL 1 #define SERIAL_BATTERY_RX 30 #define SERIAL_BATTERY_TX 5 static const uint8_t A0 = PIN_A0; #define PIN_NFC1 (9) #define PIN_NFC2 (10) // I2C #define WIRE_INTERFACES_COUNT 1 #define PIN_WIRE_SDA (23) #define PIN_WIRE_SCL (25) // actually the LORA Radio #define PIN_POWER_EN (11) // charger status #define EXT_CHRG_DETECT (32 + 6) #define EXT_CHRG_DETECT_VALUE HIGH // SPI #define SPI_INTERFACES_COUNT 1 #define PIN_SPI_MISO (8) #define PIN_SPI_MOSI (7) #define PIN_SPI_SCK (6) #define LORA_RESET (32 + 8) #define LORA_DIO1 (12) #define LORA_DIO2 (26) #define LORA_SCK PIN_SPI_SCK #define LORA_MISO PIN_SPI_MISO #define LORA_MOSI PIN_SPI_MOSI #define LORA_CS (27) #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 // Peripherals on I2C bus. Active Low #define VEXT_ENABLE (32) #define VEXT_ON_VALUE LOW // GPS L76K #define HAS_GPS 1 #define GPS_L76K #define GPS_BAUDRATE 9600 #define PIN_GPS_EN (32 + 11) #define GPS_EN_ACTIVE LOW #define PIN_GPS_RESET (3) #define GPS_RESET_MODE HIGH #define PIN_GPS_STANDBY (28) #define GPS_STANDBY_ACTIVE HIGH #define GPS_TX_PIN (32 + 12) #define GPS_RX_PIN (32 + 14) #define GPS_THREAD_INTERVAL 50 #define PIN_SERIAL1_RX GPS_RX_PIN #define PIN_SERIAL1_TX GPS_TX_PIN #define SERIAL_PRINT_PORT 0 #ifdef __cplusplus } #endif #endif ================================================ FILE: variants/nrf52840/ELECROW-ThinkNode-M6/platformio.ini ================================================ ; ThinkNode M6 - Outdoor Solar Power nrf52840/sx1262 device [env:thinknode_m6] custom_meshtastic_hw_model = 120 custom_meshtastic_hw_model_slug = THINKNODE_M6 custom_meshtastic_architecture = nrf52840 custom_meshtastic_actively_supported = false custom_meshtastic_support_level = 1 custom_meshtastic_display_name = ThinkNode M6 custom_meshtastic_images = thinknode_m6.svg custom_meshtastic_tags = Elecrow extends = nrf52840_base board = ThinkNode-M6 board_check = true debug_tool = jlink build_flags = ${nrf52840_base.build_flags} -Ivariants/nrf52840/ELECROW-ThinkNode-M6 -DELECROW_ThinkNode_M6 build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/ELECROW-ThinkNode-M6> lib_deps = ${nrf52840_base.lib_deps} # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib lewisxhe/SensorLib@0.3.4 ================================================ FILE: variants/nrf52840/ELECROW-ThinkNode-M6/variant.cpp ================================================ /* 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() { pinMode(LED_PAIRING, OUTPUT); ledOff(LED_PAIRING); pinMode(VDD_FLASH_EN, OUTPUT); digitalWrite(VDD_FLASH_EN, HIGH); } // called from main-nrf52.cpp during the cpuDeepSleep() function void variant_shutdown() { // This sets the pin to OUTPUT and LOW for the pins *not* in the if block. for (int pin = 0; pin < 48; pin++) { if (pin == PIN_GPS_EN || pin == ADC_CTRL || pin == PIN_BUTTON1 || pin == PIN_SPI_MISO || pin == PIN_SPI_MOSI || pin == PIN_SPI_SCK || pin == SX126X_CS || pin == SX126X_RESET || pin == SX126X_BUSY || pin == SX126X_DIO1) { continue; } pinMode(pin, OUTPUT); digitalWrite(pin, LOW); if (pin >= 32) { NRF_P1->DIRCLR = (1 << (pin - 32)); } else { NRF_GPIO->DIRCLR = (1 << pin); } } digitalWrite(PIN_GPS_EN, LOW); digitalWrite(ADC_CTRL, LOW); // digitalWrite(RTC_POWER, LOW); nrf_gpio_cfg_input(PIN_BUTTON1, NRF_GPIO_PIN_PULLUP); // Configure the pin to be woken up as an input nrf_gpio_pin_sense_t sense1 = NRF_GPIO_PIN_SENSE_LOW; nrf_gpio_cfg_sense_set(PIN_BUTTON1, sense1); nrf_gpio_cfg_input(EXT_CHRG_DETECT, NRF_GPIO_PIN_PULLUP); // Configure the pin to be woken up as an input nrf_gpio_pin_sense_t sense2 = NRF_GPIO_PIN_SENSE_LOW; nrf_gpio_cfg_sense_set(EXT_CHRG_DETECT, sense2); } ================================================ FILE: variants/nrf52840/ELECROW-ThinkNode-M6/variant.h ================================================ /* 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_ELECROW_THINKNODE_M6_ #define _VARIANT_ELECROW_THINKNODE_M6_ /** Master clock frequency */ #define VARIANT_MCK (64000000ul) #define USE_LFXO /*---------------------------------------------------------------------------- * Headers *----------------------------------------------------------------------------*/ #include "WVariant.h" #ifdef __cplusplus extern "C" { #endif // __cplusplus #define PINS_COUNT (48) #define NUM_DIGITAL_PINS (48) #define NUM_ANALOG_INPUTS (1) #define NUM_ANALOG_OUTPUTS (0) // LEDs #define LED_BLUE -1 #define LED_POWER (12) #define LED_PAIRING (7) #define LED_NOTIFICATION LED_PAIRING #define LED_STATE_ON HIGH #define LED_STATE_OFF LOW // USB power detection #define EXT_PWR_DETECT (13) // Button #define PIN_BUTTON1 (17) // Battery ADC #define PIN_A0 (28) #define BATTERY_PIN PIN_A0 #define ADC_CTRL (11) #define ADC_CTRL_ENABLED 1 static const uint8_t A0 = PIN_A0; #define ADC_RESOLUTION 14 #define BATTERY_SENSE_SAMPLES 30 #define PIN_NFC1 (9) #define PIN_NFC2 (10) // I2C #define WIRE_INTERFACES_COUNT 1 #define PIN_WIRE_SDA (32 + 9) #define PIN_WIRE_SCL (8) // Peripheral power enable #define PIN_POWER_EN (27) // Solar charger status #define EXT_CHRG_DETECT (15) #define EXT_CHRG_DETECT_VALUE LOW // QSPI Flash #define PIN_QSPI_SCK (32 + 3) #define PIN_QSPI_CS (23) #define PIN_QSPI_IO0 (32 + 1) #define PIN_QSPI_IO1 (32 + 2) #define PIN_QSPI_IO2 (32 + 4) #define PIN_QSPI_IO3 (32 + 5) #define EXTERNAL_FLASH_DEVICES MX25R1635F #define EXTERNAL_FLASH_USE_QSPI #define VDD_FLASH_EN (21) // LoRa SX1262 #define USE_SX1262 #define SX126X_CS (32 + 12) #define SX126X_DIO1 (32 + 6) #define SX126X_BUSY (32 + 11) #define SX126X_RESET (32 + 10) #define SX126X_DIO2_AS_RF_SWITCH #define SX126X_DIO3_TCXO_VOLTAGE 3.3 // GPS L76K #define GPS_L76K #define GPS_BAUDRATE 9600 #define PIN_GPS_EN (6) #define PIN_GPS_REINIT (29) #define PIN_GPS_STANDBY (30) #define PIN_GPS_PPS (31) #define GPS_TX_PIN (2) #define GPS_RX_PIN (3) #define GPS_THREAD_INTERVAL 50 #define PIN_SERIAL1_TX GPS_TX_PIN #define PIN_SERIAL1_RX GPS_RX_PIN // Secondary UART #define PIN_SERIAL2_RX (22) #define PIN_SERIAL2_TX (24) // PCF8563 RTC Module #define PCF8563_RTC 0x51 // SPI #define SPI_INTERFACES_COUNT 1 #define PIN_SPI_MISO (32 + 15) #define PIN_SPI_MOSI (32 + 14) #define PIN_SPI_SCK (32 + 13) // Battery #define BATTERY_SENSE_RESOLUTION_BITS 12 #define BATTERY_SENSE_RESOLUTION 4096.0 #undef AREF_VOLTAGE #define AREF_VOLTAGE 2.4 #define VBAT_AR_INTERNAL AR_INTERNAL_2_4 #define ADC_MULTIPLIER (1.75F) #define HAS_SOLAR #define OCV_ARRAY 4080, 3990, 3935, 3880, 3825, 3770, 3715, 3660, 3605, 3550, 3450 #ifdef __cplusplus } #endif #endif ================================================ FILE: variants/nrf52840/ME25LS01-4Y10TD/platformio.ini ================================================ [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/nrf52840/ME25LS01-4Y10TD -Isrc/platform/nrf52/softdevice -Isrc/platform/nrf52/softdevice/nrf52 -DME25LS01_4Y10TD board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/ME25LS01-4Y10TD> ; 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 ================================================ FILE: variants/nrf52840/ME25LS01-4Y10TD/rfswitch.h ================================================ #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, }; ================================================ FILE: variants/nrf52840/ME25LS01-4Y10TD/variant.cpp ================================================ /* 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(PIN_3V3_EN, OUTPUT); digitalWrite(PIN_3V3_EN, HIGH); } ================================================ FILE: variants/nrf52840/ME25LS01-4Y10TD/variant.h ================================================ /* 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_POWER PIN_LED1 #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_ ================================================ FILE: variants/nrf52840/ME25LS01-4Y10TD_e-ink/platformio.ini ================================================ [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/nrf52840/ME25LS01-4Y10TD_e-ink -Isrc/platform/nrf52/softdevice -Isrc/platform/nrf52/softdevice/nrf52 -DME25LS01_4Y10TD -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/nrf52840/ME25LS01-4Y10TD_e-ink> lib_deps = ${nrf52840_base.lib_deps} # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 zinggjm/GxEPD2@1.6.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 ================================================ FILE: variants/nrf52840/ME25LS01-4Y10TD_e-ink/rfswitch.h ================================================ #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, }; ================================================ FILE: variants/nrf52840/ME25LS01-4Y10TD_e-ink/variant.cpp ================================================ /* 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(PIN_3V3_EN, OUTPUT); digitalWrite(PIN_3V3_EN, HIGH); } ================================================ FILE: variants/nrf52840/ME25LS01-4Y10TD_e-ink/variant.h ================================================ /* 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_POWER PIN_LED1 #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__ ================================================ FILE: variants/nrf52840/MS24SF1/platformio.ini ================================================ [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/nrf52840/MS24SF1 -Isrc/platform/nrf52/softdevice -Isrc/platform/nrf52/softdevice/nrf52 board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/MS24SF1> ; 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 ================================================ FILE: variants/nrf52840/MS24SF1/variant.cpp ================================================ /* 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() {} ================================================ FILE: variants/nrf52840/MS24SF1/variant.h ================================================ /* 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_POWER PIN_LED1 #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_ ================================================ FILE: variants/nrf52840/MakePython_nRF52840_eink/platformio.ini ================================================ [env:makerpython_nrf52840_sx1280_eink] board_level = extra extends = nrf52840_base board = nordic_pca10059 build_flags = ${nrf52840_base.build_flags} -Ivariants/nrf52840/MakePython_nRF52840_eink -D PRIVATE_HW -D PIN_EINK_EN -DEINK_DISPLAY_MODEL=GxEPD2_290_T5D -DEINK_WIDTH=296 -DEINK_HEIGHT=128 build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/MakePython_nRF52840_eink> lib_deps = ${nrf52840_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-ESP32_Codec2 packageName=https://github.com/meshtastic/ESP32_Codec2 gitBranch=master https://github.com/meshtastic/ESP32_Codec2/archive/633326c78ac251c059ab3a8c430fcdf25b41672f.zip # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 zinggjm/GxEPD2@1.6.8 debug_tool = jlink ;upload_port = /dev/ttyACM4 ================================================ FILE: variants/nrf52840/MakePython_nRF52840_eink/variant.cpp ================================================ /* 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); } ================================================ FILE: variants/nrf52840/MakePython_nRF52840_eink/variant.h ================================================ #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 LED_GREEN PIN_LED1 #define LED_STATE_ON 0 // State when LED is lit /* * 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 ================================================ FILE: variants/nrf52840/MakePython_nRF52840_oled/platformio.ini ================================================ [env:makerpython_nrf52840_sx1280_oled] board_level = extra extends = nrf52840_base board = nordic_pca10059 build_flags = ${nrf52840_base.build_flags} -I variants/nrf52840/MakePython_nRF52840_oled -D PRIVATE_HW build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/MakePython_nRF52840_oled> lib_deps = ${nrf52840_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-ESP32_Codec2 packageName=https://github.com/meshtastic/ESP32_Codec2 gitBranch=master https://github.com/meshtastic/ESP32_Codec2/archive/633326c78ac251c059ab3a8c430fcdf25b41672f.zip debug_tool = jlink ================================================ FILE: variants/nrf52840/MakePython_nRF52840_oled/variant.cpp ================================================ /* 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); } ================================================ FILE: variants/nrf52840/MakePython_nRF52840_oled/variant.h ================================================ #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 LED_GREEN PIN_LED1 #define LED_STATE_ON 0 // State when LED is lit /* * 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 ================================================ FILE: variants/nrf52840/TWC_mesh_v4/platformio.ini ================================================ [env:TWC_mesh_v4] extends = nrf52840_base board = nordic_pca10059 board_level = extra build_flags = ${nrf52840_base.build_flags} -I variants/nrf52840/TWC_mesh_v4 -D TWC_mesh_v4 build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/TWC_mesh_v4> lib_deps = ${nrf52840_base.lib_deps} # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 zinggjm/GxEPD2@1.6.8 debug_tool = jlink ================================================ FILE: variants/nrf52840/TWC_mesh_v4/variant.cpp ================================================ /* 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); } ================================================ FILE: variants/nrf52840/TWC_mesh_v4/variant.h ================================================ #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_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 ================================================ FILE: variants/nrf52840/canaryone/platformio.ini ================================================ ; Public Beta oled/nrf52840/sx1262 device [env:canaryone] custom_meshtastic_hw_model = 29 custom_meshtastic_hw_model_slug = CANARYONE custom_meshtastic_architecture = nrf52840 custom_meshtastic_actively_supported = true custom_meshtastic_support_level = 3 custom_meshtastic_display_name = Canary One custom_meshtastic_tags = Canary 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} -I variants/nrf52840/canaryone build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/canaryone> lib_deps = ${nrf52840_base.lib_deps} ;upload_protocol = fs ================================================ FILE: variants/nrf52840/canaryone/variant.cpp ================================================ /* 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); } ================================================ FILE: variants/nrf52840/canaryone/variant.h ================================================ /* 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_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 PERIPHERAL_WARMUP_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 9600 // #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 + 8) // This is for bits going TOWARDS the GPS #define GPS_RX_PIN (GPIO_PORT1 + 9) // This is for bits going TOWARDS the CPU #define GPS_THREAD_INTERVAL 50 #define PIN_SERIAL1_RX GPS_RX_PIN #define PIN_SERIAL1_TX GPS_TX_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) #define SERIAL_PRINT_PORT 0 #ifdef __cplusplus } #endif /*---------------------------------------------------------------------------- * Arduino objects - C++ only *----------------------------------------------------------------------------*/ #endif ================================================ FILE: variants/nrf52840/cpp_overrides/lfs_util.h ================================================ /* * 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 ================================================ FILE: variants/nrf52840/diy/WashTastic/platformio.ini ================================================ ; Promicro + E22900M30S [env:WashTastic] extends = nrf52840_base board = promicro-nrf52840 board_level = extra build_flags = ${nrf52840_base.build_flags} -I variants/nrf52840/diy/nrf52_promicro_diy_tcxo -D PRIVATE_HW -D EBYTE_E22_900M30S build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/diy/nrf52_promicro_diy_tcxo> debug_tool = jlink ================================================ FILE: variants/nrf52840/diy/nrf52_promicro_diy_tcxo/custom_build_tasks.py ================================================ # Simplifies DIY InkHUD builds, with presets for several common E-Ink displays # - build using custom task in Platformio's "Project Tasks" panel # - build with `pio run -e -t build_weact_154` (or similar) # Silence trunk's objections to the import statements # trunk-ignore-all(ruff/F821) # trunk-ignore-all(flake8/F821) from SCons.Script import COMMAND_LINE_TARGETS Import("env") Import("projenv") # Custom targets # These wrappers just run the normal build task under a different target name # We intercept the build later on, based on the target name env.AddTarget( name="build_weact_154", dependencies=["buildprog"], actions=None, title='Build (WeAct 1.54")', ) env.AddTarget( name="build_weact_213", dependencies=["buildprog"], actions=None, title='Build (WeAct 2.13")', ) env.AddTarget( name="build_weact_290", dependencies=["buildprog"], actions=None, title='Build (WeAct 2.9")', ) env.AddTarget( name="build_weact_420", dependencies=["buildprog"], actions=None, title='Build (WeAct 4.2")', ) # Check whether a build was started via one of our custom targets above if "build_weact_154" in COMMAND_LINE_TARGETS: print('Building for WeAct 1.54" Display') projenv["CPPDEFINES"].append(("INKHUD_BUILDCONF_DRIVER", "ZJY200200_0154DAAMFGN")) projenv["CPPDEFINES"].append(("INKHUD_BUILDCONF_DISPLAYRESILIENCE", "15")) elif "build_weact_213" in COMMAND_LINE_TARGETS: print('Building for WeAct 2.13" Display') projenv["CPPDEFINES"].append(("INKHUD_BUILDCONF_DRIVER", "HINK_E0213A289")) projenv["CPPDEFINES"].append(("INKHUD_BUILDCONF_DISPLAYRESILIENCE", "10")) elif "build_weact_290" in COMMAND_LINE_TARGETS: print('Building for WeAct 2.9" Display') projenv["CPPDEFINES"].append(("INKHUD_BUILDCONF_DRIVER", "ZJY128296_029EAAMFGN")) projenv["CPPDEFINES"].append(("INKHUD_BUILDCONF_DISPLAYRESILIENCE", "15")) elif "build_weact_420" in COMMAND_LINE_TARGETS: print('Building for WeAct 4.2" Display') projenv["CPPDEFINES"].append(("INKHUD_BUILDCONF_DRIVER", "HINK_E042A87")) projenv["CPPDEFINES"].append(("INKHUD_BUILDCONF_DISPLAYRESILIENCE", "15")) ================================================ FILE: variants/nrf52840/diy/nrf52_promicro_diy_tcxo/nicheGraphics.h ================================================ #pragma once #include "configuration.h" #ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS // InkHUD-specific components // --------------------------- #include "graphics/niche/InkHUD/InkHUD.h" // Applets #include "graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h" #include "graphics/niche/InkHUD/Applets/User/DM/DMApplet.h" #include "graphics/niche/InkHUD/Applets/User/FavoritesMap/FavoritesMapApplet.h" #include "graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.h" #include "graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h" #include "graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.h" #include "graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h" // Shared NicheGraphics components // -------------------------------- #include "graphics/niche/Drivers/EInk/HINK_E0213A289.h" // WeAct 2.13" #include "graphics/niche/Drivers/EInk/HINK_E042A87.h" // WeAct 4.2" #include "graphics/niche/Drivers/EInk/ZJY128296_029EAAMFGN.h" // WeAct 2.9" #include "graphics/niche/Drivers/EInk/ZJY200200_0154DAAMFGN.h" // WeACt 1.54" #include "graphics/niche/Inputs/TwoButton.h" #if !defined(INKHUD_BUILDCONF_DRIVER) || !defined(INKHUD_BUILDCONF_DISPLAYRESILIENCE) #error If not using a DIY preset, display model and resilience must be set manually #endif void setupNicheGraphics() { using namespace NicheGraphics; // SPI // ----------------------------- SPI.begin(); // Driver // ----------------------------- // Use E-Ink driver Drivers::EInk *driver = new Drivers::INKHUD_BUILDCONF_DRIVER; driver->begin(&SPI, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES); // InkHUD // ---------------------------- InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance(); // Set the driver inkhud->setDriver(driver); // Set how many FAST updates per FULL update. inkhud->setDisplayResilience(INKHUD_BUILDCONF_DISPLAYRESILIENCE); // Suggest roughly ten // Select fonts InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1252; InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; // Init settings, and customize defaults // Values ignored individually if found saved to flash inkhud->persistence->settings.rotation = (driver->height > driver->width ? 1 : 0); // Rotate 90deg to landscape, if needed inkhud->persistence->settings.userTiles.maxCount = 4; inkhud->persistence->settings.optionalFeatures.batteryIcon = true; // Pick applets // Note: order of applets determines priority of "auto-show" feature inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet); inkhud->addApplet("DMs", new InkHUD::DMApplet, true, false, 3); // Default on tile 3 inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0), true, false, 2); // Default on tile 2 inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true, false, 1); // Default on tile 1 inkhud->addApplet("Favorites Map", new InkHUD::FavoritesMapApplet); inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet, true, false, 0); // Default on tile 0 inkhud->addApplet("Heard", new InkHUD::HeardApplet, true); // Background // Start running InkHUD inkhud->begin(); // Buttons // -------------------------- Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // Shared NicheGraphics component // Setup the main user button buttons->setWiring(0, Inputs::TwoButton::getUserButtonPin(), true); // Internal pull up buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); }); buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); buttons->start(); } #endif ================================================ FILE: variants/nrf52840/diy/nrf52_promicro_diy_tcxo/platformio.ini ================================================ ; Promicro + E22(0)-xxxM / HT-RA62 modules board variant - DIY - with TCXO [env:nrf52_promicro_diy_tcxo] custom_meshtastic_hw_model = 63 custom_meshtastic_hw_model_slug = NRF52_PROMICRO_DIY custom_meshtastic_architecture = nrf52840 custom_meshtastic_actively_supported = true custom_meshtastic_support_level = 3 custom_meshtastic_display_name = NRF52 Pro-micro DIY custom_meshtastic_images = promicro.svg custom_meshtastic_tags = DIY custom_meshtastic_requires_dfu = true extends = nrf52840_base board = promicro-nrf52840 build_flags = ${nrf52840_base.build_flags} -I variants/nrf52840/diy/nrf52_promicro_diy_tcxo -D NRF52_PROMICRO_DIY build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/diy/nrf52_promicro_diy_tcxo> debug_tool = jlink ; NRF52 ProMicro w/ E-Ink display [env:nrf52_promicro_diy-inkhud] board_level = extra extends = nrf52840_base, inkhud board = promicro-nrf52840 build_flags = ${nrf52840_base.build_flags} ${inkhud.build_flags} -I variants/nrf52840/diy/nrf52_promicro_diy_tcxo -D NRF52_PROMICRO_DIY build_src_filter = ${nrf52_base.build_src_filter} ${inkhud.build_src_filter} +<../variants/nrf52840/diy/nrf52_promicro_diy_tcxo> lib_deps = ${inkhud.lib_deps} ; InkHUD libs first, so we get GFXRoot instead of AdafruitGFX ${nrf52840_base.lib_deps} extra_scripts = ${nrf52840_base.extra_scripts} variants/nrf52840/diy/nrf52_promicro_diy_tcxo/custom_build_tasks.py ; Add to PIO's Project Tasks pane: preset builds for common displays ================================================ FILE: variants/nrf52840/diy/nrf52_promicro_diy_tcxo/readme.md ================================================ # Notes News 2025-12-04 - The GPS pin definitions have been changed!!! This has no material effect on current builds, but future builders may wish to review how they are using the wires. ## General The pinout is contained in the variant.h file, and a [generic schematic](./Schematic_Pro-Micro_Pinouts_2025-12-04.pdf) is located in this directory. This variant is suitable for both TCXO and XTAL types of modules. The old XTAL variant has been removed to reduce confusion. ### Note on DIO2, RXEN, TXEN, and RF switching Several modules require external switching between transmit (Tx) and receive (Rx). This can be achieved using several methods: 1. Link the TXEN pin on the radio module to DIO2 on the same module, and then connect RXEN on the radio module to pin 0.17 on the Pro-Micro. 2. Use DIO2 to drive a logic inverter, so that when DIO2 is `high`, RXEN is `low`, and vice versa. 3. Use DIO2 to drive a pair of MOSFETs or transistors to supply the same function. RXEN is not required to be connected if the selected module already has internal RF switching, or if external RF switching logic is already applied. Also worth noting that the Seeed WIO SX1262 in particular only has RXEN exposed (marked RF_SW) and has the DIO2-TXEN link internally. ## Making a node based on this variant Making your own node based on this design is straightforward. There are various open source and free to use PCB design files available, or you can solder wires directly from a module to the pro-micro.
< Click to expand > The table of known modules is at the bottom of the variant.h, and reproduced here for convenience. | 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 | Ext | Cute! DIO2/TXEN are not exposed | | Seeed | Wio-LR1121 | yes | Int | LR1121, needs alternate rfswitch.h | | AI-Thinker | RA-02 | No | Int | SX1278 **433mhz band only** | | RF Solutions | RFM95 | No | Int | Untested | | Ebyte | E80-900M2213S | Yes | Int | LR1121 radio |
## LR1121 modules - E80 is the default The E80 from CDEbyte is the most obtainable module at present, and has been selected as the default option. Naturally, CDEbyte have chosen to ignore the generic Semtech implementation of the RF switching logic and have supplied confusing and contradictory documentation, which is explained below. tl;dr: The E80 is chosen as the default. **If you wish to use another module, the table in `rfswitch.h` must be adjusted accordingly.** ### E80 switching - the saga The CDEbyte implementation of the LR1121 is contained in their E80 module. As stated above, CDEbyte have chosen to ignore the generic Semtech implementation of the RF switching logic and have their own table, which is located at the bottom of the page [here](https://www.cdebyte.com/products/E80-900M2213S/2#Pin), and reflected on page 6 of their user manual, and reproduced below: | DIO5/RFSW0 | DIO6/RFSW1 | RF status | | ---------- | ---------- | ----------------------------- | | 0 | 0 | RX | | 0 | 1 | TX (Sub-1GHz low power mode) | | 1 | 0 | TX (Sub-1GHz high power mode) | | 1 | 1 | TX(2.4GHz) | However, looking at the sample code they provide on page 9, the values would be: | DIO5/RFSW0 | DIO6/RFSW1 | RF status | | ---------- | ---------- | ----------------------------- | | 0 | 1 | RX | | 1 | 1 | TX (Sub-1GHz low power mode) | | 1 | 0 | TX (Sub-1GHz high power mode) | | 0 | 0 | TX(2.4GHz) | The Semtech default, the values are (taken from [here](https://github.com/Lora-net/SWSD006/blob/v2.6.1/lib/app_subGHz_config_lr11xx.c#L145-L154)):
< Click to expand > ```cpp .rfswitch = { .enable = LR11XX_SYSTEM_RFSW0_HIGH | LR11XX_SYSTEM_RFSW1_HIGH | LR11XX_SYSTEM_RFSW2_HIGH, .standby = 0, .rx = LR11XX_SYSTEM_RFSW0_HIGH, .tx = LR11XX_SYSTEM_RFSW0_HIGH | LR11XX_SYSTEM_RFSW1_HIGH, .tx_hp = LR11XX_SYSTEM_RFSW1_HIGH, .tx_hf = 0, .gnss = LR11XX_SYSTEM_RFSW2_HIGH, .wifi = 0, }, ```
| DIO5/RFSW0 | DIO6/RFSW1 | RF status | | ---------- | ---------- | ----------------------------- | | 1 | 0 | RX | | 1 | 1 | TX (Sub-1GHz low power mode) | | 0 | 1 | TX (Sub-1GHz high power mode) | | 0 | 0 | TX(2.4GHz) | It is evident from the tables above that there is no real consistency to those provided by Ebyte. #### An experiment Tests were conducted in each of the three configurations between a known-good SX1262 and an E80, passing packets in both directions and recording the reported RSSI. The E80 was set at 22db and 14db to activate the high and low power settings respectively. The results are shown in the chart below. ![Chart showing RSSI readings in each configuration and setting](./E80_RSSI_per_case.webp) ## Conclusion The RF switching is based on the code example given. Logically, this shows the DIO5 and DIO6 are swapped compared to the reference design. If future DIYers wish to use an alternative module, the table in `rfswitch.h` must be adjusted accordingly. ================================================ FILE: variants/nrf52840/diy/nrf52_promicro_diy_tcxo/rfswitch.h ================================================ #include "RadioLib.h" // This is rewritten to match the requirements of the E80-900M2213S // The E80 does not conform to the reference Semtech switches(!) and therefore needs a custom matrix. // See footnote #3 in "https://www.cdebyte.com/products/E80-900M2213S/2#Pin" // RF Switch Matrix SubG RFO_HP_LF / RFO_LP_LF / RFI_[NP]_LF0 // DIO5 -> RFSW0_V1 // DIO6 -> RFSW1_V2 // DIO7 -> not connected on E80 module - note that GNSS and Wifi scanning are not possible. static const uint32_t rfswitch_dio_pins[] = {RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11X0_DIO6, RADIOLIB_LR11X0_DIO7, RADIOLIB_NC, RADIOLIB_NC}; static const Module::RfSwitchMode_t rfswitch_table[] = { // mode DIO5 DIO6 DIO7 {LR11x0::MODE_STBY, {LOW, LOW, LOW}}, {LR11x0::MODE_RX, {LOW, HIGH, LOW}}, {LR11x0::MODE_TX, {HIGH, HIGH, LOW}}, {LR11x0::MODE_TX_HP, {HIGH, LOW, LOW}}, {LR11x0::MODE_TX_HF, {LOW, LOW, LOW}}, {LR11x0::MODE_GNSS, {LOW, LOW, HIGH}}, {LR11x0::MODE_WIFI, {LOW, LOW, LOW}}, END_OF_MODE_TABLE, }; ================================================ FILE: variants/nrf52840/diy/nrf52_promicro_diy_tcxo/variant.cpp ================================================ /* 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); } void variant_shutdown() { nrf_gpio_cfg_input(BUTTON_PIN, NRF_GPIO_PIN_PULLUP); // Enable internal pull-up on the button pin nrf_gpio_pin_sense_t sense = NRF_GPIO_PIN_SENSE_LOW; // Configure SENSE signal on low edge nrf_gpio_cfg_sense_set(BUTTON_PIN, sense); // Apply SENSE to wake up the device from the deep sleep } ================================================ FILE: variants/nrf52840/diy/nrf52_promicro_diy_tcxo/variant.h ================================================ #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_TX     |   | P0.29   | BUSY         | DIO0 | | P0.22 | GPS_RX     |   | 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 // 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 GPS_TX_PIN (0 + 20) // P0.20 - This is data from the MCU #define GPS_RX_PIN (0 + 22) // P0.22 - This is data from the GNSS #define PIN_GPS_EN (0 + 24) // P0.24 #define GPS_UBLOX // define GPS_DEBUG // UART interfaces #define PIN_SERIAL1_TX GPS_TX_PIN #define PIN_SERIAL1_RX GPS_RX_PIN #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 #define USE_LR1121 // RF95 CONFIG #define LORA_DIO0 (0 + 29) // P0.29 BUSY #define LORA_DIO1 (0 + 10) // P0.10 IRQ #define LORA_RESET (0 + 9) // P0.09 NRST // 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. // LR1121 #ifdef USE_LR1121 #define LR1121_IRQ_PIN (0 + 10) // P0.10 IRQ #define LR1121_NRESET_PIN LORA_RESET // P0.09 NRST #define LR1121_BUSY_PIN (0 + 29) // P0.29 BUSY #define LR1121_SPI_NSS_PIN LORA_CS // P1.13 #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 #endif // #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 | Ext | Cute! DIO2/TXEN are not exposed | | Seeed | Wio-LR1121 | yes | Int | LR1121, needs alternate rfswitch.h | | AI-Thinker | RA-02 | No | Int | SX1278 **433mhz band only** | | RF Solutions | RFM95 | No | Int | Untested | | Ebyte | E80-900M2213S | Yes | Int | LR1121 radio | */ #define SX126X_DIO3_TCXO_VOLTAGE 1.8 #define TCXO_OPTIONAL // make it so that the firmware can try both TCXO and XTAL // E-Ink DIY #define PIN_EINK_CS (32 + 7) #define PIN_EINK_DC (32 + 2) #define PIN_EINK_RES (32 + 1) #define PIN_EINK_BUSY (32 + 6) #ifdef __cplusplus } #endif /*---------------------------------------------------------------------------- * Arduino objects - C++ only *----------------------------------------------------------------------------*/ #endif ================================================ FILE: variants/nrf52840/diy/seeed-xiao-nrf52840-wio-sx1262/README.md ================================================ # XIAO nRF52840 + XIAO Wio SX1262 For a mere doubling in price you too can swap out the XIAO ESP32S3 for a XIAO nRF52840, stack the Wio SX1262 radio board either above or underneath the nRF52840, solder the pins, and achieve a massive improvement in battery life! I'm not really sure why else you would want to as the ESP32S3 is perfectly cromulent, easily connects to the Wio SX1262 via the B2B connector and has an onboard IPEX connector for the included Bluetooth antenna. So you'll also lose BT range, but you will also have working ADC for the battery in Meshtastic and also have an ESP32S3 to use for something else! If you're still reading you are clearly gonna do it anyway, so...mount the Wio SX1262 either on top or underneath depending on your preference. The `variant.h` will work with either configuration though it does map the Wio SX1262's button to nRF52840 Pin `D5` as it can still be used as a user button and it's nice to be able to gracefully shutdown a node by holding it down for 5 seconds. If you do decide to wire up the button, orient it so looking straight-down at the Wio SX1262 the radio chip is at the bottom, button in the middle and the hole is at the top - the **left** side of the button should be soldered to `GND` (e.g. the 2nd pin down the top on the **right** row of pins) and the **right** side of the button should be soldered to `D5` (e.g. the 2nd pin up from the button on the **left** row of pins.). This mirrors the original wiring and wiring it in reverse could end up connecting GND to voltage and that's no beuno. Serial Pins remain available on `D6` (TX) and `D7` (RX) should you want to use them, and I2C has been mapped to NFC1 (SDA, D30) and NFC2 (SCL, D31) The same pins could be reordered if you would like to have a different arrangement, in `variant.h` you would just need to change the relevant lines: ```cpp #define GPS_TX_PIN D6 // This is data from the MCU #define GPS_RX_PIN D7 // This is data from the GNSS module #define PIN_WIRE_SDA D6 #define PIN_WIRE_SCL D7 ``` ================================================ FILE: variants/nrf52840/diy/seeed-xiao-nrf52840-wio-sx1262/platformio.ini ================================================ ; Seeed Xiao BLE but using the B2B from ESP32S3 variant [env:seeed_xiao_nrf52840_btb] extends = env:seeed_xiao_nrf52840_kit board_level = extra build_flags = ${nrf52840_base.build_flags} -Ivariants/nrf52840/seeed_xiao_nrf52840_kit -Isrc/platform/nrf52/softdevice -Isrc/platform/nrf52/softdevice/nrf52 -DPRIVATE_HW ; Define private hardware -DSEEED_XIAO_NRF_WIO_BTB ; Define Seeed XIAO nRF Wio B2B -USEEED_XIAO_NRF52840_KIT ; Remove default HWID -USEEED_XIAO_NRF_KIT_DEFAULT ; Remove default define board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/seeed_xiao_nrf52840_kit> 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 ================================================ FILE: variants/nrf52840/diy/seeed_xiao_nrf52840_e22/platformio.ini ================================================ ; Seeed XIAO nRF52840 + EBYTE E22-900M30S - Pinout matching Wio-SX1262 (SKU 113010003) [env:seeed_xiao_nrf52840_e22_900m30s] extends = env:seeed_xiao_nrf52840_kit board_level = extra build_flags = ${env:seeed_xiao_nrf52840_kit.build_flags} -D PRIVATE_HW -DEBYTE_E22 -DEBYTE_E22_900M30S -USEEED_XIAO_NRF52840_KIT ; remove default HWID -USEEED_XIAO_NRF_KIT_DEFAULT ; remove default define ; Seeed XIAO nRF52840 + EBYTE E22-900M33S - Pinout matching Wio-SX1262 (SKU 113010003) [env:seeed_xiao_nrf52840_e22_900m33s] extends = env:seeed_xiao_nrf52840_kit board_level = extra build_flags = ${env:seeed_xiao_nrf52840_kit.build_flags} -D PRIVATE_HW -DEBYTE_E22 -DEBYTE_E22_900M33S -USEEED_XIAO_NRF52840_KIT ; remove default HWID -USEEED_XIAO_NRF_KIT_DEFAULT ; remove default define ================================================ FILE: variants/nrf52840/diy/xiao_ble/README.md ================================================ # XIAO nrf52840/nrf52840 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 nrf52840 (or XIAO nrf52840 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 nrf52840 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. ## 1. Wire the board Connecting the E22 to the XIAO nrf52840 is straightforward, but there are a few gotchas to be mindful of. ### On the XIAO nrf52840 - 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 nrf52840 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 Tx/Rx Switching Mode, `SX126X_TXEN` must be defined (`#define #define SX126X_TXEN D6`) in `variants/seeed_xiao_nrf52840_kit/variant.h` in the code block following: ```c #ifdef XIAO_BLE_LEGACY_PINOUT // Legacy xiao_ble variant pinout for third-party SX126x modules e.g. EBYTE E22 ``` ### Example Wiring for Automatic Tx/Rx Switching Mode #### MCU -> E22 Connections | XIAO nrf52840 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 nrf52840 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)_ ## 2. Build Meshtastic 1. Follow the [Building Meshtastic Firmware](https://meshtastic.org/docs/development/firmware/build/) documentation, stop after **Build** → **Step 2** 2. For **Build** → **Step 3**, select `xiao_ble` as your target 3. Adjust source code if you: - Wired your board for Manual Tx/Rx Switching Mode: see [Wire the Board](#1-wire-the-board) - Used an E22-900M33S module (this step is important to avoid **damaging the power amplifier** in the M33S module and **transmitting power above legal limits**!): 1. Open `variants/diy/platformio.ini` 2. Search for `[env:xiao_ble]` 3. In the line starting with `build_flags` within this section, change `-DEBYTE_E22_900M30S` to `-DEBYTE_E22_900M33S` 4. Follow **Build** → **Step 4** to build the firmware 5. Stop here, because the **PlatformIO: Upload** step does not work for factory-fresh XIAO nrf52840 (the automatic reset to bootloader only works if Meshtastic firmware is already running) 6. The built `firmware.uf2` binary can be found in the folder `.pio/build/xiao_ble/firmware.uf2` (relative to where you cloned the Git repository to), we will need it for [flashing the firmware](#3-flash-the-firmware-to-the-xiao-nrf52840) (manually) ## 3. Flash the Firmware to the XIAO nrf52840 1. Double press the XIAO nrf52840's `reset` button to put it in bootloader mode, and a USB volume named `XIAO SENSE` will appear 2. Copy the `firmware.uf2` file to the `XIAO SENSE` volume (refer to the last step of [Build Meshtastic](#2-build-meshtastic)) 3. The XIAO nrf52840's red LED will flash for several seconds as the firmware is copied 4. Once Meshtastic firmware successfully boots, the: 1. Green LED will turn on 2. Red LED will flash several times to indicate flash memory writes during initial settings file creation 3. Green LED will blink every second once the firmware is running normally 5. If you do not see the above LED patters, proceed to [Troubleshooting](#4-troubleshooting) ## 4. 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. Open the [Meshtastic web flasher](https://flasher.meshtastic.org/) 2. Select the **_Seeed XIAO NRF52840 Kit_** 3. Click the **_trash can icon_** to the right of **_Flash_** 4. Follow the instructions on the screen **Do not flash the Seeed XIAO NRF52840 Kit firmware** if you have wired the LoRa module according to this variant, as the Seeed XIAO NRF52840 Kit uses different wiring for the SX1262 LoRa chip - 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 inadvertently high (usually due to a pin mapping conflict). ## 5. Notes - **Transmit Power** - There is a power amplifier after the SX1262's Tx, so the actual Tx power is just over 7 dB greater than the SX1262's set Tx power (the E22-900M30S actually tops out just over 29dB at 5V according to the datasheet) - Meshtastic firmware is aware of the gain of the E22-900M30S module, so the Meshtastic clients' Tx power setting reflects the actual output power, i.e. setting 30 dBm in the Meshtastic app programs the E22 module to correctly output 30 dBm, setting 24 dBm will output 24 dBm, etc. - **Adequate 5V Power Supply to the E22 Module** - Have a bypass capacitor from its 5V supply to ground; 100 µF works well - Voltage must be between 5V–5.5V, lower supply voltage results in less output power; for example, with a fully charged LiPo at 4.2V, Tx power appears to max out around 26-27 dBm ### Additional Reading - [S5NC/CDEBYTE_Modules](https://github.com/S5NC/CDEBYTE_Modules) has additional information about EBYTE E22 modules' internal workings, including photographs - [RadioLib High power Radio Modules Guide](https://github.com/jgromes/RadioLib/wiki/High-power-Radio-Modules-Guide) ## 6. 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 nrf52840 + E22 coupled with an [Abracon ARRKP4065-S915A](https://www.digikey.com/en/products/detail/abracon-llc/ARRKP4065-S915A/8593263") ceramic patch antenna 2. A RAK 5005/4631 coupled with a [Laird MA9-5N](https://www.streakwave.com/laird-technologies-ma9-5n-55dbi-900mhz-mobile-omni-select-mount) 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 nrf52840 + E22 and recording the received RSSI. - The opposite was done to observe the E22's Rx sensitivity: messages were sent from the XIAO nrf52840 + 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. ================================================ FILE: variants/nrf52840/diy/xiao_ble/platformio.ini ================================================ ; Seeed Xiao BLE: https://www.digikey.com/en/products/detail/seeed-technology-co-ltd/102010448/16652893 [env:xiao_ble] extends = env:seeed_xiao_nrf52840_kit board_level = extra build_flags = ${nrf52840_base.build_flags} -Ivariants/nrf52840/seeed_xiao_nrf52840_kit -Isrc/platform/nrf52/softdevice -Isrc/platform/nrf52/softdevice/nrf52 -D PRIVATE_HW -DXIAO_BLE_LEGACY_PINOUT -DEBYTE_E22 -USEEED_XIAO_NRF52840_KIT ; remove default HWID -USEEED_XIAO_NRF_KIT_DEFAULT ; remove default define board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/seeed_xiao_nrf52840_kit> 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 ; Seeed Xiao BLE: https://www.digikey.com/en/products/detail/seeed-technology-co-ltd/102010448/16652893 [env:xiao_ble_30db] extends = env:seeed_xiao_nrf52840_kit board_level = extra build_flags = ${nrf52840_base.build_flags} -Ivariants/nrf52840/seeed_xiao_nrf52840_kit -Isrc/platform/nrf52/softdevice -Isrc/platform/nrf52/softdevice/nrf52 -DPRIVATE_HW ; Define private hardware -DXIAO_BLE_LEGACY_PINOUT ; Set legacy pinout -DEBYTE_E22_900M30S ; Set 30db module -USEEED_XIAO_NRF52840_KIT ; Remove default HWID -USEEED_XIAO_NRF_KIT_DEFAULT ; Remove default define board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/seeed_xiao_nrf52840_kit> 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 ; Seeed Xiao BLE: https://www.digikey.com/en/products/detail/seeed-technology-co-ltd/102010448/16652893 [env:xiao_ble_33db] extends = env:seeed_xiao_nrf52840_kit board_level = extra build_flags = ${nrf52840_base.build_flags} -Ivariants/nrf52840/seeed_xiao_nrf52840_kit -Isrc/platform/nrf52/softdevice -Isrc/platform/nrf52/softdevice/nrf52 -DPRIVATE_HW ; Define private hardware -DXIAO_BLE_LEGACY_PINOUT ; Set legacy pinout -DEBYTE_E22_900M33S ; Set 33db module -USEEED_XIAO_NRF52840_KIT ; Remove default HWID -USEEED_XIAO_NRF_KIT_DEFAULT ; Remove default define board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/seeed_xiao_nrf52840_kit> 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 ================================================ FILE: variants/nrf52840/dls_Minimesh_Lite/platformio.ini ================================================ [env:minimesh_lite] extends = nrf52840_base board = minimesh_lite board_level = extra build_flags = ${nrf52840_base.build_flags} -Ivariants/nrf52840/dls_Minimesh_Lite -DPRIVATE_HW build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/dls_Minimesh_Lite> debug_tool = jlink ================================================ FILE: variants/nrf52840/dls_Minimesh_Lite/variant.cpp ================================================ /* 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); } ================================================ FILE: variants/nrf52840/dls_Minimesh_Lite/variant.h ================================================ #ifndef _VARIANT_MINIMESH_LITE_ #define _VARIANT_MINIMESH_LITE_ #define VARIANT_MCK (64000000ul) #define USE_LFRC #include "WVariant.h" #ifdef __cplusplus extern "C" { #endif #define MINIMESH_LITE // 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) #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 #define VBAT_MV_PER_LSB (0.73242188F) #define VBAT_DIVIDER (0.6F) #define VBAT_DIVIDER_COMP (1.73) #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 #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) #define PIN_WIRE_SCL (0 + 11) // LED #define PIN_LED1 (0 + 15) // Actually red #define LED_BLUE PIN_LED1 #define LED_STATE_ON 1 // Button #define BUTTON_PIN (32 + 0) // GPS #define GPS_TX_PIN (0 + 20) #define GPS_RX_PIN (0 + 22) #define PIN_GPS_EN (0 + 24) #define GPS_UBLOX // define GPS_DEBUG // UART interfaces #define PIN_SERIAL1_TX GPS_TX_PIN #define PIN_SERIAL1_RX GPS_RX_PIN #define PIN_SERIAL2_RX (0 + 6) #define PIN_SERIAL2_TX (0 + 8) // Serial interfaces #define SPI_INTERFACES_COUNT 1 #define PIN_SPI_MISO (0 + 2) #define PIN_SPI_MOSI (32 + 15) #define PIN_SPI_SCK (32 + 11) #define LORA_MISO PIN_SPI_MISO #define LORA_MOSI PIN_SPI_MOSI #define LORA_SCK PIN_SPI_SCK #define LORA_CS (32 + 13) // LORA MODULES #define USE_LLCC68 #define USE_SX1262 #define USE_SX1268 // SX126X CONFIG #define SX126X_CS (32 + 13) #define SX126X_DIO1 (0 + 10) #define SX126X_DIO2_AS_RF_SWITCH #define SX126X_BUSY (0 + 29) #define SX126X_RESET (0 + 9) #define SX126X_RXEN (0 + 17) #define SX126X_TXEN RADIOLIB_NC #define SX126X_DIO3_TCXO_VOLTAGE 1.8 #define TCXO_OPTIONAL // make it so that the firmware can try both TCXO and XTAL #ifdef __cplusplus } #endif #endif // _VARIANT_MINIMESH_LITE_ ================================================ FILE: variants/nrf52840/feather_diy/platformio.ini ================================================ ; 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/nrf52840/feather_diy -Dfeather_diy build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/feather_diy> 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 ================================================ FILE: variants/nrf52840/feather_diy/variant.cpp ================================================ /* 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" ================================================ FILE: variants/nrf52840/feather_diy/variant.h ================================================ /* 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_GREEN PIN_LED2 // Actually red #define LED_BLUE PIN_LED1 #define LED_STATE_ON 1 // State when LED is lit #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 ================================================ FILE: variants/nrf52840/gat562_mesh_trial_tracker/platformio.ini ================================================ ; The very slick RAK wireless RAK 4631 / 4630 board - Unified firmware for 5005/19003, with or without OLED RAK 1921 [env:gat562_mesh_trial_tracker] extends = nrf52840_base board_level = extra board = gat562_mesh_trial_tracker board_check = true build_flags = ${nrf52840_base.build_flags} -I variants/nrf52840/gat562_mesh_trial_tracker ;-D GAT562_MESH_TRIAL_TRACKER -D PRIVATE_HW -DRADIOLIB_EXCLUDE_SX128X=1 -DRADIOLIB_EXCLUDE_SX127X=1 -DRADIOLIB_EXCLUDE_LR11X0=1 build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/gat562_mesh_trial_tracker> ================================================ FILE: variants/nrf52840/gat562_mesh_trial_tracker/variant.cpp ================================================ /* 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); // 3V3 Power Rail pinMode(PIN_3V3_EN, OUTPUT); digitalWrite(PIN_3V3_EN, HIGH); } ================================================ FILE: variants/nrf52840/gat562_mesh_trial_tracker/variant.h ================================================ /* 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_GAT562_MESH_TRIAL_TRACKER_ #define _VARIANT_GAT562_MESH_TRIAL_TRACKER_ // led pin 2 (blue), see https://github.com/meshtastic/firmware/blob/master/src/mesh/NodeDB.cpp#L723 #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 LED_BLUE (36) #define LED_GREEN PIN_LED1 #define LED_NOTIFICATION LED_BLUE #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 (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 // Display - OLED connected via I2C #define HAS_SCREEN 1 #define USE_SSD1306 // 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 */ // configure the SET pin on the RAK12039 sensor board to disable the sensor while not reading // air quality telemetry. PIN_NFC2 doesn't seem to be used anywhere else in the codebase, but if // you're having problems with your node behaving weirdly when a RAK12039 board isn't connected, // try disabling this. // #define PMSA003I_ENABLE_PIN PIN_NFC2 // #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_BAUDRATE 9600 #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_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 ================================================ FILE: variants/nrf52840/heltec_mesh_node_t114/platformio.ini ================================================ ; First prototype nrf52840/sx1262 device [env:heltec-mesh-node-t114] custom_meshtastic_hw_model = 69 custom_meshtastic_hw_model_slug = HELTEC_MESH_NODE_T114 custom_meshtastic_architecture = nrf52840 custom_meshtastic_actively_supported = true custom_meshtastic_support_level = 1 custom_meshtastic_display_name = Heltec Mesh Node T114 custom_meshtastic_images = heltec-mesh-node-t114.svg, heltec-mesh-node-t114-case.svg custom_meshtastic_tags = Heltec extends = nrf52840_base board = heltec_mesh_node_t114 board_level = pr 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/nrf52840/heltec_mesh_node_t114 -DHELTEC_T114 build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/heltec_mesh_node_t114> lib_deps = ${nrf52840_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-st7789 packageName=https://github.com/meshtastic/st7789 gitBranch=main https://github.com/meshtastic/st7789/archive/9ee76d6b18b9a8f45a2c5cae06b1134a587691eb.zip ================================================ FILE: variants/nrf52840/heltec_mesh_node_t114/variant.cpp ================================================ /* 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 "Arduino.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); } void variant_shutdown() { nrf_gpio_cfg_default(PIN_GPS_PPS); detachInterrupt(PIN_GPS_PPS); detachInterrupt(PIN_BUTTON1); } ================================================ FILE: variants/nrf52840/heltec_mesh_node_t114/variant.h ================================================ /* 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 // T114 gets a muted yellow on black display #define TFT_MESH_OVERRIDE COLOR565(255, 255, 128) // #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_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 /* * Bluetooth */ // The bluetooth transmit power on the nRF52840 is adjustable from -20dB to +8dB in steps of 4dB // so NRF52_BLE_TX_POWER can be set to -20, -16, -12, -8, -4, 0 (default), 4, and 8. // #define NRF52_BLE_TX_POWER 8 /* * 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 + 7) // This is for bits going TOWARDS the CPU #define GPS_RX_PIN (32 + 5) // This is for bits going TOWARDS the GPS #define GPS_THREAD_INTERVAL 50 #define PIN_SERIAL1_RX GPS_RX_PIN #define PIN_SERIAL1_TX GPS_TX_PIN /* * 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) // 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.916F) // rf52840 AIN2 = Pin 4 // commented out due to power leakage of 2.9mA in shutdown state see reported issue #8801 // #define BATTERY_LPCOMP_INPUT NRF_LPCOMP_INPUT_2 //UNSAFE // We have AIN2 with a VBAT divider so AIN2 = VBAT * (100/490) // We have the device going deep sleep under 3.1V, which is AIN2 = 0.63V // So we can wake up when VBAT>=VDD is restored to 3.3V, where AIN2 = 0.67V // Ratio 0.67/3.3 = 0.20, so we can pick a bit higher, 2/8 VDD, which means // VBAT=4.04V #define BATTERY_LPCOMP_THRESHOLD NRF_LPCOMP_REF_SUPPLY_2_8 #ifdef __cplusplus } #endif /*---------------------------------------------------------------------------- * Arduino objects - C++ only *----------------------------------------------------------------------------*/ #endif ================================================ FILE: variants/nrf52840/heltec_mesh_node_t114-inkhud/custom_build_tasks.py ================================================ # Simplifies DIY InkHUD builds, with presets for several common E-Ink displays # - build using custom task in Platformio's "Project Tasks" panel # - build with `pio run -e -t build_weact_154` (or similar) # Silence trunk's objections to the import statements # trunk-ignore-all(ruff/F821) # trunk-ignore-all(flake8/F821) from SCons.Script import COMMAND_LINE_TARGETS Import("env") Import("projenv") # Custom targets # These wrappers just run the normal build task under a different target name # We intercept the build later on, based on the target name env.AddTarget( name="build_weact_154", dependencies=["buildprog"], actions=None, title='Build (WeAct 1.54")', ) env.AddTarget( name="build_weact_213", dependencies=["buildprog"], actions=None, title='Build (WeAct 2.13")', ) env.AddTarget( name="build_weact_290", dependencies=["buildprog"], actions=None, title='Build (WeAct 2.9")', ) env.AddTarget( name="build_weact_420", dependencies=["buildprog"], actions=None, title='Build (WeAct 4.2")', ) # Check whether a build was started via one of our custom targets above if "build_weact_154" in COMMAND_LINE_TARGETS: print('Building for WeAct 1.54" Display') projenv["CPPDEFINES"].append(("INKHUD_BUILDCONF_DRIVER", "ZJY200200_0154DAAMFGN")) projenv["CPPDEFINES"].append(("INKHUD_BUILDCONF_DISPLAYRESILIENCE", "15")) elif "build_weact_213" in COMMAND_LINE_TARGETS: print('Building for WeAct 2.13" Display') projenv["CPPDEFINES"].append(("INKHUD_BUILDCONF_DRIVER", "HINK_E0213A289")) projenv["CPPDEFINES"].append(("INKHUD_BUILDCONF_DISPLAYRESILIENCE", "10")) elif "build_weact_290" in COMMAND_LINE_TARGETS: print('Building for WeAct 2.9" Display') projenv["CPPDEFINES"].append(("INKHUD_BUILDCONF_DRIVER", "ZJY128296_029EAAMFGN")) projenv["CPPDEFINES"].append(("INKHUD_BUILDCONF_DISPLAYRESILIENCE", "15")) elif "build_weact_420" in COMMAND_LINE_TARGETS: print('Building for WeAct 4.2" Display') projenv["CPPDEFINES"].append(("INKHUD_BUILDCONF_DRIVER", "HINK_E042A87")) projenv["CPPDEFINES"].append(("INKHUD_BUILDCONF_DISPLAYRESILIENCE", "15")) ================================================ FILE: variants/nrf52840/heltec_mesh_node_t114-inkhud/nicheGraphics.h ================================================ #pragma once #include "configuration.h" #ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS // InkHUD-specific components // --------------------------- #include "graphics/niche/InkHUD/InkHUD.h" // Applets #include "graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h" #include "graphics/niche/InkHUD/Applets/User/DM/DMApplet.h" #include "graphics/niche/InkHUD/Applets/User/FavoritesMap/FavoritesMapApplet.h" #include "graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.h" #include "graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h" #include "graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.h" #include "graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h" // Shared NicheGraphics components // -------------------------------- #include "graphics/niche/Drivers/EInk/HINK_E0213A289.h" // WeAct 2.13" #include "graphics/niche/Drivers/EInk/HINK_E042A87.h" // WeAct 4.2" #include "graphics/niche/Drivers/EInk/ZJY128296_029EAAMFGN.h" // WeAct 2.9" #include "graphics/niche/Drivers/EInk/ZJY200200_0154DAAMFGN.h" // WeACt 1.54" #include "graphics/niche/Inputs/TwoButton.h" #if !defined(INKHUD_BUILDCONF_DRIVER) || !defined(INKHUD_BUILDCONF_DISPLAYRESILIENCE) // cppcheck-suppress preprocessorErrorDirective #error If not using a DIY preset, display model and resilience must be set manually #endif void setupNicheGraphics() { using namespace NicheGraphics; // SPI // ----------------------------- SPI1.begin(); // Driver // ----------------------------- // Use E-Ink driver Drivers::EInk *driver = new Drivers::INKHUD_BUILDCONF_DRIVER; driver->begin(&SPI1, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES); // InkHUD // ---------------------------- InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance(); // Set the driver inkhud->setDriver(driver); // Set how many FAST updates per FULL update. inkhud->setDisplayResilience(INKHUD_BUILDCONF_DISPLAYRESILIENCE); // Suggest roughly ten // Select fonts InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1252; InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; // Init settings, and customize defaults // Values ignored individually if found saved to flash inkhud->persistence->settings.rotation = (driver->height > driver->width ? 1 : 0); // Rotate 90deg to landscape, if needed inkhud->persistence->settings.userTiles.maxCount = 4; inkhud->persistence->settings.optionalFeatures.batteryIcon = true; // Pick applets // Note: order of applets determines priority of "auto-show" feature inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet); inkhud->addApplet("DMs", new InkHUD::DMApplet, true, false, 3); // Default on tile 3 inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0), true, false, 2); // Default on tile 2 inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true, false, 1); // Default on tile 1 inkhud->addApplet("Favorites Map", new InkHUD::FavoritesMapApplet); inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet, true, false, 0); // Default on tile 0 inkhud->addApplet("Heard", new InkHUD::HeardApplet, true); // Background // Start running InkHUD inkhud->begin(); // Buttons // -------------------------- Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // Shared NicheGraphics component // #0: Main User Button buttons->setWiring(0, Inputs::TwoButton::getUserButtonPin()); buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); }); buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); buttons->start(); } #endif ================================================ FILE: variants/nrf52840/heltec_mesh_node_t114-inkhud/platformio.ini ================================================ [env:heltec-mesh-node-t114-inkhud] board_level = extra extends = nrf52840_base, inkhud board = heltec_mesh_node_t114 board_check = true build_flags = ${nrf52840_base.build_flags} ${inkhud.build_flags} -I variants/nrf52840/heltec_mesh_node_t114-inkhud build_src_filter = ${nrf52_base.build_src_filter} ${inkhud.build_src_filter} +<../variants/nrf52840/heltec_mesh_node_t114-inkhud> lib_deps = ${inkhud.lib_deps} ; InkHUD libs first, so we get GFXRoot instead of AdafruitGFX ${nrf52840_base.lib_deps} extra_scripts = ${env.extra_scripts} variants/nrf52840/diy/nrf52_promicro_diy_tcxo/custom_build_tasks.py ; Add to PIO's Project Tasks pane: preset builds for common displays ================================================ FILE: variants/nrf52840/heltec_mesh_node_t114-inkhud/variant.cpp ================================================ /* 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 "Arduino.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); } void variant_shutdown() { nrf_gpio_cfg_default(PIN_GPS_PPS); detachInterrupt(PIN_GPS_PPS); detachInterrupt(PIN_BUTTON1); } ================================================ FILE: variants/nrf52840/heltec_mesh_node_t114-inkhud/variant.h ================================================ // Unlike many other InkHUD variants, this environment does require its own variant.h file // This is because the default T114 variant maps SPI1 pins to the optional TFT display, and those pins are not broken out #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 // 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_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 /* * 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 /* * E-Ink DIY */ #define PIN_EINK_MOSI (0 + 8) // also called SDA #define PIN_EINK_SCLK (0 + 7) #define PIN_EINK_CS (32 + 12) #define PIN_EINK_DC (32 + 14) #define PIN_EINK_RES (0 + 5) #define PIN_EINK_BUSY (32 + 15) /* * 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 + 7) // This is for bits going TOWARDS the CPU #define GPS_RX_PIN (32 + 5) // This is for bits going TOWARDS the GPS #define GPS_THREAD_INTERVAL 50 #define PIN_SERIAL1_RX GPS_RX_PIN #define PIN_SERIAL1_TX GPS_TX_PIN /* * 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_SPI1_MISO -1 #define PIN_SPI1_MOSI PIN_EINK_MOSI #define PIN_SPI1_SCK PIN_EINK_SCLK // 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) #ifdef __cplusplus } #endif /*---------------------------------------------------------------------------- * Arduino objects - C++ only *----------------------------------------------------------------------------*/ #endif ================================================ FILE: variants/nrf52840/heltec_mesh_pocket/nicheGraphics.h ================================================ #pragma once #include "configuration.h" #ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS // InkHUD-specific components // --------------------------- #include "graphics/niche/InkHUD/InkHUD.h" // Applets #include "graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h" #include "graphics/niche/InkHUD/Applets/User/DM/DMApplet.h" #include "graphics/niche/InkHUD/Applets/User/FavoritesMap/FavoritesMapApplet.h" #include "graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.h" #include "graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h" #include "graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.h" #include "graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h" // Shared NicheGraphics components // -------------------------------- #include "graphics/niche/Drivers/EInk/LCMEN2R13ECC1.h" #include "graphics/niche/Inputs/TwoButton.h" void setupNicheGraphics() { using namespace NicheGraphics; // SPI // ----------------------------- // For NRF52 platforms, SPI pins are defined in variant.h SPI1.begin(); // E-Ink Driver // ----------------------------- Drivers::EInk *driver = new Drivers::LCMEN2R13ECC1; driver->begin(&SPI1, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES); // InkHUD // ---------------------------- InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance(); // Set the E-Ink driver inkhud->setDriver(driver); // Set how many FAST updates per FULL update // Set how unhealthy additional FAST updates beyond this number are inkhud->setDisplayResilience(10, 1.5); // Select fonts InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1253; InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1253; InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1253; // Customize default settings inkhud->persistence->settings.userTiles.maxCount = 2; // How many tiles can the display handle? inkhud->persistence->settings.rotation = 3; // 270 degrees clockwise inkhud->persistence->settings.userTiles.count = 1; // One tile only by default, keep things simple for new users inkhud->persistence->settings.optionalMenuItems.nextTile = true; // Pick applets // Note: order of applets determines priority of "auto-show" feature inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); // Activated, autoshown inkhud->addApplet("DMs", new InkHUD::DMApplet); // - inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); // - inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); // - inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated inkhud->addApplet("Favorites Map", new InkHUD::FavoritesMapApplet); // - inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); // - inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, no autoshow, default on tile 0 // Start running InkHUD inkhud->begin(); // Buttons // -------------------------- Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // Shared NicheGraphics component // #0: Main User Button buttons->setWiring(0, Inputs::TwoButton::getUserButtonPin()); buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); }); buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); // Begin handling button events buttons->start(); } #endif ================================================ FILE: variants/nrf52840/heltec_mesh_pocket/platformio.ini ================================================ ; First prototype nrf52840/sx1262 device [env:heltec-mesh-pocket-5000] custom_meshtastic_support_level = 1 custom_meshtastic_images = heltec_mesh_pocket.svg custom_meshtastic_tags = Heltec extends = nrf52840_base board = heltec_mesh_pocket debug_tool = jlink custom_device_hw_model = 94 custom_meshtastic_hw_model = 94 custom_meshtastic_hw_model_slug = HELTEC_MESH_POCKET custom_meshtastic_architecture = nrf52840 custom_meshtastic_display_name = Heltec Mesh Pocket custom_meshtastic_actively_supported = true custom_meshtastic_variant = 5000mAh custom_meshtastic_key = heltec_mesh_pocket # add -DCFG_SYSVIEW if you want to use the Segger systemview tool for OS profiling. build_flags = ${nrf52840_base.build_flags} -Ivariants/nrf52840/heltec_mesh_pocket -DHELTEC_MESH_POCKET -DHELTEC_MESH_POCKET_BATTERY_5000 -DUSE_EINK -DEINK_DISPLAY_MODEL=GxEPD2_213_B74 -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 build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/heltec_mesh_pocket> lib_deps = ${nrf52840_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master https://github.com/meshtastic/GxEPD2/archive/c7eb4c3c167cf396ef4f541cc5d4c6aa42f3c46b.zip [env:heltec-mesh-pocket-5000-inkhud] extends = nrf52840_base, inkhud board = heltec_mesh_pocket custom_meshtastic_hw_model = 94 custom_meshtastic_hw_model_slug = HELTEC_MESH_POCKET custom_meshtastic_architecture = nrf52840 custom_meshtastic_display_name = Heltec Mesh Pocket custom_meshtastic_actively_supported = true custom_meshtastic_variant = 5000mAh InkHUD custom_meshtastic_key = heltec_mesh_pocket build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/heltec_mesh_pocket> ${inkhud.build_src_filter} build_flags = ${inkhud.build_flags} ${nrf52840_base.build_flags} -I variants/nrf52840/heltec_mesh_pocket -D HELTEC_MESH_POCKET -D HELTEC_MESH_POCKET_BATTERY_5000 lib_deps = ${inkhud.lib_deps} ; InkHUD libs first, so we get GFXRoot instead of AdafruitGFX ${nrf52840_base.lib_deps} ; First prototype nrf52840/sx1262 device [env:heltec-mesh-pocket-10000] custom_meshtastic_support_level = 1 custom_meshtastic_images = heltec_mesh_pocket.svg custom_meshtastic_tags = Heltec extends = nrf52840_base board = heltec_mesh_pocket debug_tool = jlink custom_meshtastic_hw_model = 94 custom_meshtastic_hw_model_slug = HELTEC_MESH_POCKET custom_meshtastic_architecture = nrf52840 custom_meshtastic_display_name = Heltec Mesh Pocket custom_meshtastic_actively_supported = true custom_meshtastic_variant = 10000mAh custom_meshtastic_key = heltec_mesh_pocket # add -DCFG_SYSVIEW if you want to use the Segger systemview tool for OS profiling. build_flags = ${nrf52840_base.build_flags} -Ivariants/nrf52840/heltec_mesh_pocket -DHELTEC_MESH_POCKET -DHELTEC_MESH_POCKET_BATTERY_10000 -DUSE_EINK -DEINK_DISPLAY_MODEL=GxEPD2_213_B74 -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 build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/heltec_mesh_pocket> lib_deps = ${nrf52840_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master https://github.com/meshtastic/GxEPD2/archive/c7eb4c3c167cf396ef4f541cc5d4c6aa42f3c46b.zip [env:heltec-mesh-pocket-10000-inkhud] extends = nrf52840_base, inkhud board = heltec_mesh_pocket custom_meshtastic_hw_model = 94 custom_meshtastic_hw_model_slug = HELTEC_MESH_POCKET custom_meshtastic_architecture = nrf52840 custom_meshtastic_display_name = Heltec Mesh Pocket custom_meshtastic_actively_supported = true custom_meshtastic_variant = 10000mAh InkHUD custom_meshtastic_key = heltec_mesh_pocket build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/heltec_mesh_pocket> ${inkhud.build_src_filter} build_flags = ${inkhud.build_flags} ${nrf52840_base.build_flags} -I variants/nrf52840/heltec_mesh_pocket -D HELTEC_MESH_POCKET -D HELTEC_MESH_POCKET_BATTERY_10000 lib_deps = ${inkhud.lib_deps} ; InkHUD libs first, so we get GFXRoot instead of AdafruitGFX ${nrf52840_base.lib_deps} ================================================ FILE: variants/nrf52840/heltec_mesh_pocket/variant.cpp ================================================ #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}; ================================================ FILE: variants/nrf52840/heltec_mesh_pocket/variant.h ================================================ #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 // 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 (13) // 13 red (confirmed on 1.0 board) #define LED_RED PIN_LED1 #define LED_BLUE PIN_LED1 #define LED_GREEN PIN_LED1 #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 /* No longer populated on PCB */ #define PIN_SERIAL2_RX (0 + 7) #define PIN_SERIAL2_TX (0 + 8) // #define PIN_SERIAL2_EN (0 + 17) /** Wire Interfaces */ #define WIRE_INTERFACES_COUNT 1 #define PIN_WIRE_SDA (32 + 15) #define PIN_WIRE_SCL (32 + 13) /* * Lora radio */ #define USE_SX1262 #define SX126X_CS (0 + 26) // FIXME - we really should define LORA_CS instead #define LORA_CS (0 + 26) #define SX126X_DIO1 (0 + 16) // 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 + 15) #define SX126X_RESET (0 + 12) // 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 // Display (E-Ink) #define PIN_EINK_CS 24 #define PIN_EINK_BUSY 32 + 6 #define PIN_EINK_DC 31 #define PIN_EINK_RES 32 + 4 #define PIN_EINK_SCLK 22 #define PIN_EINK_MOSI 20 #define PIN_SPI1_MISO -1 #define PIN_SPI1_MOSI PIN_EINK_MOSI #define PIN_SPI1_SCK PIN_EINK_SCLK /* * GPS pins */ #define PIN_SERIAL1_RX 32 + 5 #define PIN_SERIAL1_TX 32 + 7 /* * SPI Interfaces */ #define SPI_INTERFACES_COUNT 2 // For LORA, spi 0 #define PIN_SPI_MISO (32 + 9) #define PIN_SPI_MOSI (0 + 5) #define PIN_SPI_SCK (0 + 4) // 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 32 + 2 #define ADC_CTRL_ENABLED HIGH #define BATTERY_PIN 29 #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.6425F) #if defined(HELTEC_MESH_POCKET_BATTERY_5000) #define OCV_ARRAY 4300, 4240, 4120, 4000, 3888, 3800, 3740, 3698, 3655, 3580, 3400 #elif defined(HELTEC_MESH_POCKET_BATTERY_10000) #define OCV_ARRAY 4100, 4060, 3960, 3840, 3729, 3625, 3550, 3500, 3420, 3345, 3100 #endif #undef HAS_GPS #define HAS_GPS 0 #ifdef __cplusplus } #endif #endif ================================================ FILE: variants/nrf52840/heltec_mesh_solar/nicheGraphics.h ================================================ #pragma once #include "configuration.h" #ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS // InkHUD-specific components // --------------------------- #include "graphics/niche/InkHUD/InkHUD.h" // Applets #include "graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h" #include "graphics/niche/InkHUD/Applets/User/DM/DMApplet.h" #include "graphics/niche/InkHUD/Applets/User/FavoritesMap/FavoritesMapApplet.h" #include "graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.h" #include "graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h" #include "graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.h" #include "graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h" // Shared NicheGraphics components // -------------------------------- #include "graphics/niche/Drivers/EInk/E0213A367.h" #include "graphics/niche/Inputs/TwoButton.h" void setupNicheGraphics() { using namespace NicheGraphics; // SPI // ----------------------------- // For NRF52 platforms, SPI pins are defined in variant.h SPI1.begin(); // E-Ink Driver // ----------------------------- Drivers::EInk *driver = new Drivers::E0213A367; driver->begin(&SPI1, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES); // InkHUD // ---------------------------- InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance(); // Set the E-Ink driver inkhud->setDriver(driver); // Set how many FAST updates per FULL update // Set how unhealthy additional FAST updates beyond this number are inkhud->setDisplayResilience(10, 1.5); // Select fonts InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1252; InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; // Customize default settings inkhud->persistence->settings.userTiles.maxCount = 2; // How many tiles can the display handle? inkhud->persistence->settings.rotation = 3; // 270 degrees clockwise inkhud->persistence->settings.userTiles.count = 1; // One tile only by default, keep things simple for new users inkhud->persistence->settings.optionalMenuItems.nextTile = true; // Pick applets // Note: order of applets determines priority of "auto-show" feature inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); // Activated, autoshown inkhud->addApplet("DMs", new InkHUD::DMApplet); // - inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); // - inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); // - inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated inkhud->addApplet("Favorites Map", new InkHUD::FavoritesMapApplet); // - inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); // - inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, no autoshow, default on tile 0 // Start running InkHUD inkhud->begin(); // Buttons // -------------------------- Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // Shared NicheGraphics component // #0: Main User Button buttons->setWiring(0, Inputs::TwoButton::getUserButtonPin()); buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); }); buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); // Begin handling button events buttons->start(); } #endif ================================================ FILE: variants/nrf52840/heltec_mesh_solar/platformio.ini ================================================ ; First prototype nrf52840/sx1262 device [heltec_mesh_solar_base] extends = nrf52840_base board = heltec_mesh_solar board_level = pr 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/nrf52840/heltec_mesh_solar -DHELTEC_MESH_SOLAR build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/heltec_mesh_solar> lib_deps = ${nrf52840_base.lib_deps} # renovate: datasource=git-refs depName=NMIoT-meshsolar packageName=https://github.com/NMIoT/meshsolar gitBranch=main https://github.com/NMIoT/meshsolar/archive/dfc5330dad443982e6cdd37a61d33fc7252f468b.zip # renovate: datasource=custom.pio depName=ArduinoJson packageName=bblanchon/library/ArduinoJson bblanchon/ArduinoJson@6.21.6 [env:heltec-mesh-solar] custom_meshtastic_hw_model = 108 custom_meshtastic_hw_model_slug = HELTEC_MESH_SOLAR custom_meshtastic_architecture = nrf52840 custom_meshtastic_actively_supported = false custom_meshtastic_support_level = 1 custom_meshtastic_display_name = Heltec MeshSolar custom_meshtastic_images = heltec-mesh-solar.svg custom_meshtastic_tags = Heltec extends = heltec_mesh_solar_base build_flags = ${heltec_mesh_solar_base.build_flags} -DSPI_INTERFACES_COUNT=1 [env:heltec-mesh-solar-eink] extends = heltec_mesh_solar_base build_flags = ${heltec_mesh_solar_base.build_flags} -DHELTEC_MESH_SOLAR_EINK -DSPI_INTERFACES_COUNT=2 -DUSE_EINK -DPIN_SCREEN_VDD_CTL=3 -DPIN_EINK_CS=41 -DPIN_EINK_BUSY=11 -DPIN_EINK_DC=13 -DPIN_EINK_RES=40 -DPIN_EINK_SCLK=12 -DPIN_EINK_MOSI=2 -DPIN_SPI1_MISO=-1 -DPIN_SPI1_MOSI=PIN_EINK_MOSI -DPIN_SPI1_SCK=PIN_EINK_SCLK -DEINK_DISPLAY_MODEL=GxEPD2_213_E0213A367 -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_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" -DENABLE_MESSAGE_PERSISTENCE=0 ; Disable flash persistence for space-limited build -DMESHTASTIC_EXCLUDE_WIFI=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 lib_deps = ${heltec_mesh_solar_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master https://github.com/meshtastic/GxEPD2/archive/c7eb4c3c167cf396ef4f541cc5d4c6aa42f3c46b.zip [env:heltec-mesh-solar-inkhud] extends = heltec_mesh_solar_base, inkhud build_src_filter = ${heltec_mesh_solar_base.build_src_filter} ${inkhud.build_src_filter} build_flags = ${heltec_mesh_solar_base.build_flags} ${inkhud.build_flags} -DHELTEC_MESH_SOLAR_INKHUD -DSPI_INTERFACES_COUNT=2 -DPIN_SCREEN_VDD_CTL=3 -DPIN_EINK_CS=41 -DPIN_EINK_BUSY=11 -DPIN_EINK_DC=13 -DPIN_EINK_RES=40 -DPIN_EINK_SCLK=12 -DPIN_EINK_MOSI=2 -DPIN_SPI1_MISO=-1 -DPIN_SPI1_MOSI=PIN_EINK_MOSI -DPIN_SPI1_SCK=PIN_EINK_SCLK lib_deps = ${inkhud.lib_deps} ; InkHUD libs first, so we get GFXRoot instead of AdafruitGFX ${heltec_mesh_solar_base.lib_deps} [env:heltec-mesh-solar-oled] extends = heltec_mesh_solar_base build_flags = ${heltec_mesh_solar_base.build_flags} -DHELTEC_MESH_SOLAR_OLED -DSPI_INTERFACES_COUNT=1 -DPIN_SCREEN_VDD_CTL=3 -DHAS_SCREEN=1 -DRESET_OLED=40 -DPIN_WIRE_SDA=2 -DPIN_WIRE_SCL=12 [env:heltec-mesh-solar-tft] extends = heltec_mesh_solar_base build_flags = ${heltec_mesh_solar_base.build_flags} -DHELTEC_MESH_SOLAR_TFT -DSPI_INTERFACES_COUNT=2 -DUSE_ST7789 -DST7789_NSS=41 -DST7789_RS=13 -DST7789_SDA=2 -DST7789_SCK=12 -DST7789_RESET=40 -DST7789_MISO=-1 -DST7789_BUSY=-1 -DVTFT_CTRL=3 -DVTFT_LEDA=11 -DTFT_BACKLIGHT_ON=HIGH -DST7789_SPI_HOST=SPI2_HOST -DSPI_FREQUENCY=10000000 -DSPI_READ_FREQUENCY=10000000 -DTFT_HEIGHT=170 -DTFT_WIDTH=320 -DTFT_OFFSET_X=0 -DTFT_OFFSET_Y=0 -DBRIGHTNESS_DEFAULT=100 -DPIN_SPI1_MISO=ST7789_MISO -DPIN_SPI1_MOSI=ST7789_SDA -DPIN_SPI1_SCK=ST7789_SCK lib_deps = ${heltec_mesh_solar_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-st7789 packageName=https://github.com/meshtastic/st7789 gitBranch=main https://github.com/meshtastic/st7789/archive/9ee76d6b18b9a8f45a2c5cae06b1134a587691eb.zip ================================================ FILE: variants/nrf52840/heltec_mesh_solar/variant.cpp ================================================ /* 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 "Arduino.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() { pinMode(BQ4050_EMERGENCY_SHUTDOWN_PIN, INPUT); #if defined(PIN_SCREEN_VDD_CTL) pinMode(PIN_SCREEN_VDD_CTL, OUTPUT); digitalWrite(PIN_SCREEN_VDD_CTL, LOW); // Start with power on #endif } void variant_shutdown() { nrf_gpio_cfg_default(PIN_GPS_PPS); detachInterrupt(PIN_GPS_PPS); detachInterrupt(PIN_BUTTON1); } ================================================ FILE: variants/nrf52840/heltec_mesh_solar/variant.h ================================================ /* 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 // 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) #define PIN_LED1 (32 + 15) // green (confirmed on 1.0 board) #define LED_BLUE PIN_LED1 // fake for bluefruit library #define LED_GREEN PIN_LED1 #define LED_STATE_ON 0 // State when LED is lit // #define HAS_NEOPIXEL // Enable the use of neopixels // #define NEOPIXEL_COUNT 1 // How many neopixels are connected // #define NEOPIXEL_DATA (32 + 15) // 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 (-1) #define PIN_SERIAL2_TX (-1) /* * I2C */ #define WIRE_INTERFACES_COUNT 2 #ifndef HELTEC_MESH_SOLAR_OLED // I2C bus 0 #define PIN_WIRE_SDA (0 + 6) #define PIN_WIRE_SCL (0 + 26) #endif // I2C bus 1 // Available on header pins, for general use #define PIN_WIRE1_SDA (0 + 30) #define PIN_WIRE1_SCL (0 + 5) /* * 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 /* * 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 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 + 7) // This is for bits going TOWARDS the CPU #define GPS_RX_PIN (32 + 5) // This is for bits going TOWARDS the GPS #define GPS_THREAD_INTERVAL 50 #define PIN_SERIAL1_RX GPS_RX_PIN #define PIN_SERIAL1_TX GPS_TX_PIN /* * SPI Interfaces */ // For LORA, spi 0 #define PIN_SPI_MISO (0 + 23) #define PIN_SPI_MOSI (0 + 22) #define PIN_SPI_SCK (0 + 19) // To debug via the segger JLINK console rather than the CDC-ACM serial device // #define USE_SEGGER // Hardware watchdog #define HAS_HARDWARE_WATCHDOG #define HARDWARE_WATCHDOG_DONE (0 + 9) #define HARDWARE_WATCHDOG_WAKE (0 + 10) #define HARDWARE_WATCHDOG_TIMEOUT_MS (6 * 60 * 1000) // 6 minute watchdog #define BQ4050_SDA_PIN (32 + 1) // I2C data line pin #define BQ4050_SCL_PIN (32 + 0) // I2C clock line pin #define BQ4050_EMERGENCY_SHUTDOWN_PIN (32 + 3) // Emergency shutdown pin #define SERIAL_PRINT_PORT 0 #ifdef __cplusplus } #endif /*---------------------------------------------------------------------------- * Arduino objects - C++ only *----------------------------------------------------------------------------*/ #endif ================================================ FILE: variants/nrf52840/meshlink/platformio.ini ================================================ ; MeshLink board developed by LoraItalia. NRF52840, eByte E22900M22S (Will also come with other frequencies), 25w MPPT solar charger (5v,12v,18v selectable), support for gps, buzzer, oled or e-ink display, 10 gpios, hardware watchdog ; https://www.loraitalia.it ; firmware for boards with or without oled display [env:meshlink] extends = nrf52840_base board = meshlink board_level = extra ;board_check = true build_flags = ${nrf52840_base.build_flags} -I variants/nrf52840/meshlink -D MESHLINK -DRADIOLIB_EXCLUDE_SX128X=1 -DRADIOLIB_EXCLUDE_SX127X=1 -DRADIOLIB_EXCLUDE_LR11X0=1 build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/meshlink> debug_tool = jlink ; 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 [env:meshlink_eink] extends = nrf52840_base board = meshlink board_level = extra ;board_check = true build_flags = ${nrf52840_base.build_flags} -I variants/nrf52840/meshlink -D MESHLINK -DRADIOLIB_EXCLUDE_SX128X=1 -DRADIOLIB_EXCLUDE_SX127X=1 -DRADIOLIB_EXCLUDE_LR11X0=1 -D USE_EINK -D EINK_DISPLAY_MODEL=GxEPD2_213_B74 -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 build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/meshlink> lib_deps = ${nrf52840_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master https://github.com/meshtastic/GxEPD2/archive/c7eb4c3c167cf396ef4f541cc5d4c6aa42f3c46b.zip debug_tool = jlink ; 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 ================================================ FILE: variants/nrf52840/meshlink/variant.cpp ================================================ #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(PIN_LED1, OUTPUT); digitalWrite(PIN_LED1, HIGH); // turn off the white led while booting // otherwise it will stay lit for several seconds (could be annoying) #ifdef PIN_WD_EN pinMode(PIN_WD_EN, OUTPUT); digitalWrite(PIN_WD_EN, HIGH); // Enable the Watchdog at boot #endif } void variant_shutdown() { #ifdef PIN_WD_EN digitalWrite(PIN_WD_EN, LOW); #endif } ================================================ FILE: variants/nrf52840/meshlink/variant.h ================================================ #ifndef _VARIANT_MESHLINK_ #define _VARIANT_MESHLINK_ #ifndef MESHLINK #define MESHLINK #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 (2) #define NUM_ANALOG_OUTPUTS (0) #define BUTTON_PIN (-1) // If defined, this will be used for user button presses, #define BUTTON_NEED_PULLUP // LEDs #define PIN_LED1 (24) // Built in white led for status #define LED_BLUE PIN_LED1 #define LED_STATE_ON 0 // State when LED is lit // Testing USB detection // #define NRF_APM /* * Analog pins */ #define PIN_A1 (3) // P0.03/AIN1 #define ADC_RESOLUTION 14 // Other pins // #define PIN_AREF (2) // static const uint8_t AREF = PIN_AREF; /* * Serial interfaces */ #define PIN_SERIAL1_RX (32 + 8) #define PIN_SERIAL1_TX (7) #define SERIAL_PRINT_PORT 0 /* * SPI Interfaces */ #define SPI_INTERFACES_COUNT 2 #define PIN_SPI_MISO (8) #define PIN_SPI_MOSI (32 + 9) #define PIN_SPI_SCK (11) #define PIN_SPI1_MISO (23) #define PIN_SPI1_MOSI (21) #define PIN_SPI1_SCK (19) static const uint8_t SS = 12; 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 (15) #define PIN_EINK_BUSY (16) #define PIN_EINK_DC (14) #define PIN_EINK_RES (17) #define PIN_EINK_SCLK (19) #define PIN_EINK_MOSI (21) // also called SDI /* * Wire Interfaces */ #define WIRE_INTERFACES_COUNT 1 #define PIN_WIRE_SDA (1) #define PIN_WIRE_SCL (27) // QSPI Pins #define PIN_QSPI_SCK 19 #define PIN_QSPI_CS 22 #define PIN_QSPI_IO0 21 #define PIN_QSPI_IO1 23 #define PIN_QSPI_IO2 32 #define PIN_QSPI_IO3 20 // On-board QSPI Flash #define EXTERNAL_FLASH_DEVICES W25Q16JVUXIQ #define EXTERNAL_FLASH_USE_QSPI #define USE_SX1262 #define SX126X_CS (12) #define SX126X_DIO1 (32 + 1) #define SX126X_BUSY (32 + 3) #define SX126X_RESET (6) // #define SX126X_RXEN (13) // 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 // pin 25 is used to enable or disable the watchdog. This pin has to be disabled when cpu is put to sleep // otherwise the timer will expire and wd will reboot the cpu #define PIN_WD_EN (25) #define PIN_GPS_PPS (26) // Pulse per second input from the GPS #define GPS_TX_PIN PIN_SERIAL1_TX // This is for bits going TOWARDS the CPU #define GPS_RX_PIN PIN_SERIAL1_RX // This is for bits going TOWARDS the GPS // #define GPS_THREAD_INTERVAL 50 // Define pin to enable GPS toggle (set GPIO to LOW) via user button triple press #define PIN_GPS_EN (0) #define GPS_EN_ACTIVE LOW #define PIN_BUZZER (31) // P0.31/AIN7 // Battery // The battery sense is hooked to pin A0 (2) #define BATTERY_PIN (2) // 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.42 // fine tuning of voltage #ifdef __cplusplus } #endif /*---------------------------------------------------------------------------- * Arduino objects - C++ only *----------------------------------------------------------------------------*/ #endif ================================================ FILE: variants/nrf52840/meshtiny/platformio.ini ================================================ ; MeshTiny - Custom device based on GAT562 with encoder and buzzer support [env:meshtiny] extends = nrf52840_base board = meshtiny board_level = extra build_flags = ${nrf52840_base.build_flags} -Ivariants/nrf52840/meshtiny -D MESHTINY -D USE_PIN_BUZZER -D MESHTASTIC_EXCLUDE_GPS=1 build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/meshtiny> ================================================ FILE: variants/nrf52840/meshtiny/variant.cpp ================================================ /* 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 & LED_BLUE pinMode(PIN_LED1, OUTPUT); ledOff(PIN_LED1); pinMode(LED_BLUE, OUTPUT); ledOff(LED_BLUE); // 3V3 Power Rail pinMode(PIN_3V3_EN, OUTPUT); digitalWrite(PIN_3V3_EN, HIGH); // Initialize Encoder pins pinMode(INPUTDRIVER_ENCODER_UP, INPUT_PULLUP); pinMode(INPUTDRIVER_ENCODER_DOWN, INPUT_PULLUP); pinMode(INPUTDRIVER_ENCODER_BTN, INPUT_PULLUP); // Initialize Buzzer pin pinMode(PIN_BUZZER, OUTPUT); digitalWrite(PIN_BUZZER, LOW); } ================================================ FILE: variants/nrf52840/meshtiny/variant.h ================================================ /* 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_MESHTINY_ #define _VARIANT_MESHTINY_ #ifndef MESHTINY #define MESHTINY #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 LED_BLUE (36) #define LED_GREEN PIN_LED1 #define LED_STATE_ON 1 // State when LED is litted /* * Encoder */ #define INPUTDRIVER_ENCODER_TYPE 2 #define INPUTDRIVER_ENCODER_UP 26 #define INPUTDRIVER_ENCODER_DOWN 4 #define INPUTDRIVER_ENCODER_BTN 28 #define UPDOWN_LONG_PRESS_REPEAT_INTERVAL 150 /* * Buzzer - PWM */ #define PIN_BUZZER 30 /* * Buttons */ #define CANCEL_BUTTON_PIN 9 #define BUTTON_NEED_PULLUP #define CANCEL_BUTTON_ACTIVE_LOW true #define CANCEL_BUTTON_ACTIVE_PULLUP false /* * 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 HAS_SCREEN 1 #define USE_SSD1306 /* * 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 22 // Changed from 26 to avoid conflict with encoder #define PIN_QSPI_IO0 27 // Changed from 30 to avoid conflict with buzzer #define PIN_QSPI_IO1 29 #define PIN_QSPI_IO2 21 // Changed from 28 to avoid conflict with encoder button #define PIN_QSPI_IO3 2 // On-board QSPI Flash #define EXTERNAL_FLASH_DEVICES IS25LP080D #define EXTERNAL_FLASH_USE_QSPI #define USE_SX1262 #define SX126X_CS (42) #define SX126X_DIO1 (47) #define SX126X_BUSY (46) #define SX126X_RESET (38) #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 #define PIN_3V3_EN (34) // 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 #ifdef __cplusplus } #endif /*---------------------------------------------------------------------------- * Arduino objects - C++ only *----------------------------------------------------------------------------*/ #endif ================================================ FILE: variants/nrf52840/monteops_hw1/platformio.ini ================================================ ; 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} -I variants/nrf52840/monteops_hw1 -D MONTEOPS_HW1 build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/monteops_hw1> + + + lib_deps = ${nrf52840_base.lib_deps} ${networking_base.lib_deps} # renovate: datasource=github-tags depName=RAK13800-W5100S packageName=RAKWireless/RAK13800-W5100S https://github.com/RAKWireless/RAK13800-W5100S/archive/1.0.2.zip 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 ================================================ FILE: variants/nrf52840/monteops_hw1/variant.cpp ================================================ /* 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); } ================================================ FILE: variants/nrf52840/monteops_hw1/variant.h ================================================ /* 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 LED_BLUE (36) // Connected to WWAN host LED (if present) #define LED_GREEN PIN_LED1 #define LED_NOTIFICATION LED_BLUE #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 (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_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 ================================================ FILE: variants/nrf52840/muzi_base/platformio.ini ================================================ [env:muzi-base] custom_meshtastic_hw_model = 93 custom_meshtastic_hw_model_slug = MUZI_BASE custom_meshtastic_architecture = nrf52840 custom_meshtastic_actively_supported = true custom_meshtastic_support_level = 1 custom_meshtastic_display_name = muzi BASE custom_meshtastic_images = muzi_base.svg custom_meshtastic_tags = muzi extends = nrf52840_base board = muzi-base build_flags = ${nrf52840_base.build_flags} -I variants/nrf52840/muzi_base -D MUZI_BASE -D CONFIG_NFCT_PINS_AS_GPIOS=1 -L "${platformio.libdeps_dir}/${this.__env__}/bsec2/src/cortex-m4/fpv4-sp-d16-hard" build_src_filter = ${nrf52840_base.build_src_filter} +<../variants/nrf52840/muzi_base> lib_deps = ${nrf52840_base.lib_deps} # renovate: datasource=custom.pio depName=ArtronShop_RX8130CE packageName=artronshop/library/ArtronShop_RX8130CE artronshop/ArtronShop_RX8130CE@1.0.0 ================================================ FILE: variants/nrf52840/muzi_base/rfswitch.h ================================================ #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, }; ================================================ FILE: variants/nrf52840/muzi_base/variant.cpp ================================================ #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() { // Initialize the digital pins as inputs or outputs pinMode(PIN_LED1, OUTPUT); digitalWrite(PIN_LED1, HIGH); pinMode(LED_BLUE, OUTPUT); digitalWrite(LED_BLUE, HIGH); // Initialize LoRa pins pinMode(SX126X_RESET, OUTPUT); digitalWrite(SX126X_RESET, HIGH); pinMode(SX126X_CS, OUTPUT); digitalWrite(SX126X_CS, HIGH); pinMode(GPS_EN_GPIO, OUTPUT); digitalWrite(GPS_EN_GPIO, HIGH); // GPS on initially pinMode(SCREEN_12V_ENABLE, OUTPUT); digitalWrite(SCREEN_12V_ENABLE, LOW); // pinMode(BATTERY_CHARGING_INV, INPUT); } ================================================ FILE: variants/nrf52840/muzi_base/variant.h ================================================ #pragma once #ifndef _VARIANT_MUZI_BASE_ #define _VARIANT_MUZI_BASE_ /** 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) // Define I2C Peripherals #define WIRE_INTERFACES_COUNT 2 // this is the OLED bus #define PIN_WIRE_SDA (0 + 24) // P0.24 #define PIN_WIRE_SCL (0 + 25) // P0.25 // IMU bus #define PIN_WIRE1_SDA (0 + 04) // P0.04 #define PIN_WIRE1_SCL (0 + 06) // P0.06 #define COMPASS_ORIENTATION meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270 #define HAS_ICM20948 // forces the i2c address to be seen as this sensor #define RX8130CE_RTC 0x32 // LEDs #define PIN_LED1 (32 + 3) // P1.03, Green #define LED_BLUE (32 + 4) // P1.04, Blue #define LED_NOTIFICATION LED_BLUE #define LED_STATE_ON 0 // State when LED is lit // Buttons #define HAS_TRACKBALL 1 #define TB_UP (0 + 21) #define TB_DOWN (0 + 17) #define TB_LEFT (32 + 05) #define TB_RIGHT (0 + 16) #define TB_PRESS (0 + 10) #define TB_DIRECTION FALLING #define CANCEL_BUTTON_PIN (0 + 15) // P0.15 #define CANCEL_BUTTON_ACTIVE_LOW true #define CANCEL_BUTTON_ACTIVE_PULLUP false // Switch #define SWITCH_MODE1 (32 + 9) // P1.09, Top Position #define SWITCH_MODE2 (0 + 12) // P0.12, Middle Position #define PIN_GPS_SWITCH SWITCH_MODE2 /* * SPI Interfaces */ #define SPI_INTERFACES_COUNT 1 // For LORA, spi 0 #define PIN_SPI_MISO (32 + 15) // P1.15 #define PIN_SPI_MOSI (32 + 14) // P1.14 #define PIN_SPI_SCK (32 + 13) // P1.13 #define LORA_SCK PIN_SPI_SCK #define LORA_MISO PIN_SPI_MISO #define LORA_MOSI PIN_SPI_MOSI #define LORA_CS (32 + 12) // P1.12 #define USE_SX1262 #define SX126X_CS LORA_CS #define SX126X_DIO1 (32 + 6) // P1.06 #define SX126X_BUSY (32 + 11) // P1.11 #define SX126X_RESET (32 + 10) // P1.10 #define SX126X_DIO2_AS_RF_SWITCH #define SX126X_DIO3_TCXO_VOLTAGE 3.3 #define USE_LR1121 #define LR1121_IRQ_PIN (32 + 8) // P1.08 #define LR1121_NRESET_PIN (32 + 10) // P1.10 #define LR1121_BUSY_PIN (32 + 11) // P1.11 #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 // GPS #define GPS_RX_PIN (0 + 20) // P0.20 #define GPS_TX_PIN (0 + 19) // P0.19 #define GPS_EN_GPIO (32 + 1) // P1.01 #define PIN_SERIAL1_RX GPS_RX_PIN #define PIN_SERIAL1_TX GPS_TX_PIN #define PIN_BUZZER (0 + 22) // P0.22 // Battery monitoring #define BATTERY_PIN (0 + 31) // P0.31 // #define CHARGER_FAULT (0 + 27) // P0.27 #define BATTERY_CHARGING_INV (32 + 02) // P1.02 #define BATTERY_SENSE_RESOLUTION_BITS 12 #define BATTERY_SENSE_RESOLUTION 4096.0 #define ADC_MULTIPLIER 1.537 #define OCV_ARRAY 4050, 4010, 3990, 3930, 3870, 3820, 3740, 3630, 3550, 3450, 3100 // Display - I2C display #define HAS_SCREEN 1 #define SCREEN_12V_ENABLE (0 + 23) // P0.23 #define USE_SH1107 #define USERPREFS_OEM_TEXT "muzi_works_logo" #define USERPREFS_OEM_FONT_SIZE 0 #define USERPREFS_OEM_IMAGE_WIDTH 88 // 11 bytes wide #define USERPREFS_OEM_IMAGE_HEIGHT 47 // 517 bytes total #define USERPREFS_OEM_IMAGE_DATA \ { \ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, \ 0xFF, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x03, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ 0xF7, 0x0F, 0xFF, 0x00, 0xF0, 0xFF, 0x0F, 0xC0, 0xFF, 0x07, 0x78, 0xFF, 0x9F, 0xFF, 0x01, 0xF0, 0xFF, 0x0F, 0xC0, \ 0xFF, 0x03, 0x78, 0x3F, 0xFE, 0xF3, 0x01, 0xF0, 0xFF, 0x0F, 0x00, 0xE0, 0x03, 0x78, 0x1F, 0xFC, 0xC0, 0x03, 0xF0, \ 0xFF, 0x0F, 0x00, 0xE0, 0x01, 0x78, 0x0F, 0xF8, 0x80, 0x03, 0xF0, 0xFF, 0x0F, 0x00, 0xF0, 0x00, 0x78, 0x0F, 0x78, \ 0x80, 0x03, 0xF0, 0xFF, 0x0F, 0x00, 0x70, 0x00, 0x78, 0x07, 0x70, 0x80, 0x03, 0xF0, 0xFF, 0x0F, 0x00, 0x78, 0x00, \ 0x78, 0x07, 0x70, 0x80, 0x03, 0xF0, 0xFF, 0x0F, 0x00, 0x3C, 0x00, 0x78, 0x07, 0x70, 0x80, 0x03, 0xF0, 0xFF, 0x0F, \ 0x00, 0x1C, 0x00, 0x78, 0x07, 0x70, 0x80, 0x03, 0xF0, 0xFF, 0x0F, 0x00, 0x1E, 0x00, 0x78, 0x07, 0x70, 0x80, 0x03, \ 0xE0, 0xFF, 0x0F, 0x00, 0x0F, 0x00, 0x78, 0x07, 0x70, 0x80, 0x03, 0xE0, 0xFF, 0x07, 0x80, 0x07, 0x00, 0x78, 0x07, \ 0x70, 0x80, 0x03, 0xC0, 0xFF, 0x07, 0x80, 0x07, 0x00, 0x78, 0x07, 0x70, 0x80, 0x03, 0xC0, 0xFF, 0x03, 0xC0, 0xFF, \ 0x07, 0x78, 0x07, 0x70, 0x80, 0x03, 0x00, 0xFF, 0x01, 0xE0, 0xFF, 0x07, 0x78, 0x07, 0x70, 0x80, 0x03, 0x00, 0x7C, \ 0x00, 0xF0, 0xFF, 0x07, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, \ 0xFF, 0xFF, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, \ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0xE3, 0xE7, 0xC7, 0x1F, 0xF8, 0x0F, 0xF0, 0xE7, 0xE3, 0x07, 0x7C, 0xC7, 0xE7, \ 0xC3, 0x0F, 0xE0, 0x07, 0xE0, 0xC7, 0xE1, 0x03, 0x70, 0xC7, 0xC3, 0xE3, 0x87, 0xC1, 0x07, 0xC0, 0xC7, 0xF8, 0xE3, \ 0x71, 0xC7, 0xC3, 0xE3, 0xE3, 0xC7, 0xC7, 0xC7, 0x47, 0xF8, 0xF3, 0x7F, 0x8F, 0xC3, 0xF1, 0xE3, 0x8F, 0xC7, 0x8F, \ 0x27, 0xFC, 0xE3, 0x7F, 0x8F, 0x81, 0xF1, 0xF1, 0x8F, 0xC7, 0xCF, 0x07, 0xFE, 0x03, 0x7E, 0x8F, 0x99, 0xF1, 0xF1, \ 0x8F, 0x07, 0xC0, 0x07, 0xFF, 0x07, 0x78, 0x9F, 0x99, 0xF9, 0xF1, 0x8F, 0x07, 0xE0, 0x07, 0xFE, 0x3F, 0x70, 0x1F, \ 0x18, 0xF8, 0xF3, 0x8F, 0x07, 0xF0, 0x27, 0xFC, 0xFF, 0x71, 0x3F, 0x18, 0xF8, 0xE3, 0xC7, 0xC7, 0xF1, 0x47, 0xF8, \ 0xF3, 0x63, 0x3F, 0x3C, 0xFC, 0xC3, 0xC3, 0xC7, 0xE3, 0xC7, 0xF0, 0xE1, 0x71, 0x3F, 0x3C, 0xFC, 0x07, 0xE0, 0xC7, \ 0xC7, 0xC7, 0xE1, 0x03, 0x70, 0x7F, 0x7E, 0xFE, 0x0F, 0xF0, 0xC7, 0x87, 0xC7, 0xC3, 0x07, 0x78, 0xFF, 0xFF, 0xFF, \ 0x7F, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0x1F, 0x7E, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, \ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, \ 0xFF, 0xFF, 0x7F \ } // QSPI Pins #define PIN_QSPI_SCK (0 + 3) #define PIN_QSPI_CS (0 + 26) #define PIN_QSPI_IO0 (0 + 30) #define PIN_QSPI_IO1 (0 + 29) #define PIN_QSPI_IO2 (0 + 28) #define PIN_QSPI_IO3 (0 + 2) // On-board QSPI Flash #define EXTERNAL_FLASH_DEVICES W25Q32JVSS #define EXTERNAL_FLASH_USE_QSPI #define SERIAL_PRINT_PORT 0 // NFC is disabled via CONFIG_NFCT_PINS_AS_GPIOS=1 build flag // This configures P0.09 and P0.10 as regular GPIO pins instead of NFC pins #ifdef __cplusplus } #endif /*---------------------------------------------------------------------------- * Arduino objects - C++ only *----------------------------------------------------------------------------*/ #ifdef __cplusplus #endif #endif // _VARIANT_MUZI_BASE_ ================================================ FILE: variants/nrf52840/nano-g2-ultra/platformio.ini ================================================ ; First prototype eink/nrf52840/sx1262 device [env:nano-g2-ultra] custom_meshtastic_hw_model = 18 custom_meshtastic_hw_model_slug = NANO_G2_ULTRA custom_meshtastic_architecture = nrf52840 custom_meshtastic_actively_supported = true custom_meshtastic_support_level = 2 custom_meshtastic_display_name = Nano G2 Ultra custom_meshtastic_images = nano-g2-ultra.svg custom_meshtastic_tags = B&Q extends = nrf52840_base board = nano-g2-ultra debug_tool = jlink build_flags = ${nrf52840_base.build_flags} -I variants/nrf52840/nano-g2-ultra -D NANO_G2_ULTRA build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/nano-g2-ultra> lib_deps = ${nrf52840_base.lib_deps} # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib lewisxhe/SensorLib@0.3.4 ;upload_protocol = fs ================================================ FILE: variants/nrf52840/nano-g2-ultra/variant.cpp ================================================ /* 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 } ================================================ FILE: variants/nrf52840/nano-g2-ultra/variant.h ================================================ /* 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) #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) /* 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 GPS_TX_PIN (0 + 10) // This is for bits going FROM the CPU #define GPS_RX_PIN (0 + 9) // This is for bits going FROM the GPS // #define GPS_THREAD_INTERVAL 50 #define PIN_SERIAL1_TX GPS_TX_PIN #define PIN_SERIAL1_RX GPS_RX_PIN // PCF8563 RTC Module #define PIN_RTC_INT (0 + 14) // Interrupt from the PCF8563 RTC #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) // 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) /** OLED Screen Model */ #define ARDUINO_ARCH_AVR #define USE_SH1107_128_64 #ifdef __cplusplus } #endif /*---------------------------------------------------------------------------- * Arduino objects - C++ only *----------------------------------------------------------------------------*/ #endif ================================================ FILE: variants/nrf52840/nrf52.ini ================================================ [nrf52_base] ; Instead of the standard nordicnrf52 platform, we use our fork which has our added variant files platform = # renovate: datasource=custom.pio depName=platformio/nordicnrf52 packageName=platformio/platform/nordicnrf52 platformio/nordicnrf52@10.11.0 extends = arduino_base platform_packages = ; our custom Git version with C++17 support in platform.txt # TODO renovate platformio/framework-arduinoadafruitnrf52 @ https://github.com/meshtastic/Adafruit_nRF52_Arduino#cpp17-platform ; Don't renovate toolchain-gccarmnoneeabi platformio/toolchain-gccarmnoneeabi@~1.90301.0 extra_scripts = ${env.extra_scripts} extra_scripts/nrf52_extra.py build_type = release build_flags = -include variants/nrf52840/cpp_overrides/lfs_util.h ${arduino_base.build_flags} -Wno-unused-variable -Isrc/platform/nrf52 -DLFS_NO_ASSERT ; Disable LFS assertions , see https://github.com/meshtastic/firmware/pull/3818 -DMESHTASTIC_EXCLUDE_AUDIO=1 -DMESHTASTIC_EXCLUDE_PAXCOUNTER=1 -Os build_unflags = -Ofast -Og -ggdb3 -ggdb2 -g3 -g2 -g -g1 -g0 -std=c++11 -std=gnu++11 build_src_filter = ${arduino_base.build_src_filter} - - - - - - - - - - - lib_deps= ${arduino_base.lib_deps} ${radiolib_base.lib_deps} # renovate: datasource=custom.pio depName=rweather/Crypto packageName=rweather/library/Crypto rweather/Crypto@0.4.0 lib_ignore = BluetoothOTA lvgl ================================================ FILE: variants/nrf52840/nrf52832.ini ================================================ [nrf52832_base] extends = nrf52_base build_flags = ${nrf52_base.build_flags} -DSERIAL_BUFFER_SIZE=1024 ================================================ FILE: variants/nrf52840/nrf52840.ini ================================================ [nrf52840_base] extends = nrf52_base build_flags = ${nrf52_base.build_flags} -DSERIAL_BUFFER_SIZE=4096 lib_deps = ${nrf52_base.lib_deps} ${environmental_base.lib_deps} ${environmental_extra.lib_deps} # renovate: datasource=git-refs depName=Kongduino-Adafruit_nRFCrypto packageName=https://github.com/Kongduino/Adafruit_nRFCrypto gitBranch=master https://github.com/Kongduino/Adafruit_nRFCrypto/archive/8cde7189b5ead9dcd49f72601b43b969c0bbc06e.zip ; 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 ================================================ FILE: variants/nrf52840/r1-neo/platformio.ini ================================================ ; The R1 Neo board [env:r1-neo] custom_meshtastic_hw_model = 101 custom_meshtastic_hw_model_slug = MUZI_R1_NEO custom_meshtastic_architecture = nrf52840 custom_meshtastic_actively_supported = true custom_meshtastic_support_level = 1 custom_meshtastic_display_name = muzi R1 Neo custom_meshtastic_images = muzi_r1_neo.svg custom_meshtastic_tags = muzi extends = nrf52840_base board = r1-neo board_check = true build_flags = ${nrf52840_base.build_flags} -Ivariants/nrf52840/r1-neo -D R1_NEO -DRADIOLIB_EXCLUDE_SX128X=1 -DRADIOLIB_EXCLUDE_SX127X=1 -DRADIOLIB_EXCLUDE_LR11X0=1 build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/r1-neo> + + lib_deps = ${nrf52840_base.lib_deps} ${networking_base.lib_deps} # renovate: datasource=github-tags depName=RAK13800-W5100S packageName=RAKWireless/RAK13800-W5100S https://github.com/RAKWireless/RAK13800-W5100S/archive/1.0.2.zip # renovate: datasource=custom.pio depName=RAK NCP5623 RGB LED packageName=rakwireless/library/RAKwireless NCP5623 RGB LED library rakwireless/RAKwireless NCP5623 RGB LED library@1.0.3 # renovate: datasource=custom.pio depName=ArtronShop_RX8130CE packageName=artronshop/library/ArtronShop_RX8130CE artronshop/ArtronShop_RX8130CE@1.0.0 ================================================ FILE: variants/nrf52840/r1-neo/variant.cpp ================================================ /* 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); // 3V3 Power Rail // pinMode(PIN_3V3_EN, OUTPUT); // digitalWrite(PIN_3V3_EN, HIGH); } void earlyInitVariant() { pinMode(DCDC_EN_HOLD, OUTPUT); digitalWrite(DCDC_EN_HOLD, HIGH); pinMode(NRF_ON, OUTPUT); digitalWrite(NRF_ON, HIGH); } ================================================ FILE: variants/nrf52840/r1-neo/variant.h ================================================ /* 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_R1NEO_ #define _VARIANT_R1NEO_ #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 (1) #define NUM_ANALOG_OUTPUTS (0) // LEDs #define PIN_LED1 (32 + 4) // P1.04 Controls Green LED #define LED_BLUE (28) // P0.28 Controls Blue LED #define LED_GREEN PIN_LED1 #define LED_NOTIFICATION LED_BLUE #define LED_STATE_ON 1 // State when LED is litted // Button #define PIN_BUTTON1 (26) #define BUTTON_ACTIVE_LOW 0 #define BUTTON_ACTIVE_PULLUP 0 #define BUTTON_SENSE_TYPE INPUT_SENSE_HIGH #define ADC_RESOLUTION 14 // Serial for GPS #define PIN_SERIAL1_RX (25) #define PIN_SERIAL1_TX (24) // Connected to Jlink CDC #define PIN_SERIAL2_RX (8) #define PIN_SERIAL2_TX (6) /* * SPI Interfaces */ #define SPI_INTERFACES_COUNT 1 #define PIN_SPI_MISO (45) #define PIN_SPI_MOSI (44) #define PIN_SPI_SCK (43) 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; // R1 Neo Extras #define DCDC_EN_HOLD (13) // P0.13 Keeps DCDC alive after user button is pressed #define NRF_ON (29) // P0.29 Tells IO controller device is on // RAKRGB #define HAS_NCP5623 #define HAS_SCREEN 0 /* * Wire Interfaces */ #define WIRE_INTERFACES_COUNT 1 #define PIN_WIRE_SDA (19) // P0.19 RTC_SDA #define PIN_WIRE_SCL (20) // P0.20 RTC_SCL #define PIN_BUZZER (0 + 3) // P0.03 #define USE_SX1262 #define SX126X_CS (42) #define SX126X_DIO1 (47) #define SX126X_BUSY (46) #define SX126X_RESET (38) #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 #define PIN_GPS_EN (32 + 1) // P1.01 #define PIN_GPS_PPS (2) // P0.02 Pulse per second input from the GPS #define GPS_RX_PIN PIN_SERIAL1_RX #define GPS_TX_PIN PIN_SERIAL1_TX // Battery #define BATTERY_PIN (0 + 31) // P0.31 ADC_VBAT // 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.667 #define OCV_ARRAY 4120, 4020, 4000, 3940, 3870, 3820, 3750, 3630, 3550, 3450, 3100 #define RX8130CE_RTC 0x32 #ifdef __cplusplus } #endif /*---------------------------------------------------------------------------- * Arduino objects - C++ only *----------------------------------------------------------------------------*/ #endif ================================================ FILE: variants/nrf52840/rak2560/platformio.ini ================================================ ; Firmware for the WisMesh HUB RAK2560, including a onewire module to talk to the RAK 9154 solar battery. [env:rak2560] custom_meshtastic_hw_model = 22 custom_meshtastic_hw_model_slug = WISMESH_HUB custom_meshtastic_architecture = nrf52840 custom_meshtastic_actively_supported = true custom_meshtastic_support_level = 1 custom_meshtastic_display_name = RAK WisMesh Repeater custom_meshtastic_images = rak2560.svg custom_meshtastic_tags = RAK extends = nrf52840_base board = wiscore_rak4631 board_check = true build_flags = ${nrf52840_base.build_flags} -I variants/nrf52840/rak2560 -D RAK_4631 -DRADIOLIB_EXCLUDE_SX128X=1 -DRADIOLIB_EXCLUDE_SX127X=1 -DRADIOLIB_EXCLUDE_LR11X0=1 -DHAS_RAKPROT=1 ; Define if RAk OneWireSerial is used (disables GPS) build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/rak2560> + + + lib_deps = ${nrf52840_base.lib_deps} ${networking_base.lib_deps} # renovate: datasource=custom.pio depName=Melopero RV3028 packageName=melopero/library/Melopero RV3028 melopero/Melopero RV3028@1.2.0 # renovate: datasource=github-tags depName=RAK-OneWireSerial packageName=beegee-tokyo/RAK-OneWireSerial https://github.com/beegee-tokyo/RAK-OneWireSerial/archive/0.0.2.zip 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 ================================================ FILE: variants/nrf52840/rak2560/variant.cpp ================================================ /* 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); // 3V3 Power Rail pinMode(PIN_3V3_EN, OUTPUT); digitalWrite(PIN_3V3_EN, HIGH); } ================================================ FILE: variants/nrf52840/rak2560/variant.h ================================================ /* 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 LED_BLUE (36) #define LED_GREEN PIN_LED1 #define LED_NOTIFICATION LED_BLUE #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 (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 #define GPS_SERIAL_PORT Serial2 // 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 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 AQ_SET_PIN 10 #ifdef __cplusplus } #endif /*---------------------------------------------------------------------------- * Arduino objects - C++ only *----------------------------------------------------------------------------*/ #endif ================================================ FILE: variants/nrf52840/rak3401_1watt/platformio.ini ================================================ ; The very slick RAK wireless RAK 4631 / 4630 board - Unified firmware for 5005/19003, with or without OLED RAK 1921 [env:rak3401-1watt] custom_meshtastic_hw_model = 117 custom_meshtastic_hw_model_slug = RAK3401 custom_meshtastic_architecture = nrf52840 custom_meshtastic_actively_supported = true custom_meshtastic_support_level = 1 custom_meshtastic_display_name = RAK3401 1W custom_meshtastic_images = rak3401.svg custom_meshtastic_tags = RAK custom_meshtastic_requires_dfu = true extends = nrf52840_base board = wiscore_rak4631 board_check = true build_flags = ${nrf52840_base.build_flags} -Ivariants/nrf52840/rak3401_1watt -D RAK_4631 ; -DGPS_POWER_TOGGLE ; comment this line to disable triple press function on the user button to turn off gps entirely. -D RAK3401 -D RAK13302 ; RAK 1Watt Power Amplifier -DRADIOLIB_EXCLUDE_SX128X=1 -DRADIOLIB_EXCLUDE_SX127X=1 -DRADIOLIB_EXCLUDE_LR11X0=1 build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/rak3401_1watt> + lib_deps = ${nrf52840_base.lib_deps} ${networking_base.lib_deps} # renovate: datasource=custom.pio depName=Melopero RV3028 packageName=melopero/library/Melopero RV3028 melopero/Melopero RV3028@1.2.0 # renovate: datasource=custom.pio depName=RAK NCP5623 RGB LED packageName=rakwireless/library/RAKwireless NCP5623 RGB LED library rakwireless/RAKwireless NCP5623 RGB LED library@1.0.3 # renovate: datasource=custom.pio depName=RAK12035_SoilMoisture packageName=beegee-tokyo/library/RAK12035_SoilMoisture beegee-tokyo/RAK12035_SoilMoisture@1.0.4 # renovate: datasource=git-refs depName=RAK12034-BMX160 packageName=https://github.com/RAKWireless/RAK12034-BMX160 gitBranch=main https://github.com/RAKWireless/RAK12034-BMX160/archive/dcead07ffa267d3c906e9ca4a1330ab989e957e2.zip ; 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" ================================================ FILE: variants/nrf52840/rak3401_1watt/variant.cpp ================================================ /* 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); // 3V3 Power Rail pinMode(PIN_3V3_EN, OUTPUT); digitalWrite(PIN_3V3_EN, HIGH); } ================================================ FILE: variants/nrf52840/rak3401_1watt/variant.h ================================================ /* 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_RAK3401_ #define _VARIANT_RAK3401_ #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 LED_BLUE (36) #define LED_GREEN PIN_LED1 #define LED_NOTIFICATION LED_BLUE #define LED_STATE_ON 1 // State when LED is litted /* * 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 WB_I2C1_SDA (13) // SENSOR_SLOT IO_SLOT #define WB_I2C1_SCL (14) // SENSOR_SLOT IO_SLOT #define PIN_AREF (2) #define PIN_NFC1 (9) #define WB_IO5 PIN_NFC1 #define WB_IO4 (4) #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 /* * Wire Interfaces */ #define WIRE_INTERFACES_COUNT 1 #define PIN_WIRE_SDA (WB_I2C1_SDA) #define PIN_WIRE_SCL (WB_I2C1_SCL) // 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 // 1watt sx1262 RAK13302 #define HW_SPI1_DEVICE 1 #define LORA_SCK PIN_SPI1_SCK #define LORA_MISO PIN_SPI1_MISO #define LORA_MOSI PIN_SPI1_MOSI #define LORA_CS 26 #define USE_SX1262 #define SX126X_CS (26) #define SX126X_DIO1 (10) #define SX126X_BUSY (9) #define SX126X_RESET (4) #define SX126X_POWER_EN (21) // 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 // If using a power chip like the INA3221 you can override the default battery voltage channel below // and comment out NRF_APM to use the INA3221 instead of the USB detection for charging // #define INA3221_BAT_CH INA3221_CH2 // #define INA3221_ENV_CH INA3221_CH1 // enables 3.3V periphery like GPS or IO Module // Do not toggle this for GPS power savings #define PIN_3V3_EN (34) #define WB_IO2 PIN_3V3_EN // 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 RAK_4631 1 #ifdef __cplusplus } #endif /*---------------------------------------------------------------------------- * Arduino objects - C++ only *----------------------------------------------------------------------------*/ #endif ================================================ FILE: variants/nrf52840/rak4631/platformio.ini ================================================ ; The very slick RAK wireless RAK 4631 / 4630 board - Unified firmware for 5005/19003, with or without OLED RAK 1921 [env:rak4631] custom_meshtastic_hw_model = 9 custom_meshtastic_hw_model_slug = RAK4631 custom_meshtastic_architecture = nrf52840 custom_meshtastic_actively_supported = true custom_meshtastic_support_level = 1 custom_meshtastic_display_name = RAK WisBlock 4631 custom_meshtastic_images = rak4631.svg, rak4631_case.svg custom_meshtastic_tags = RAK extends = nrf52840_base board = wiscore_rak4631 board_level = pr board_check = true build_type = release build_flags = ${nrf52840_base.build_flags} -I variants/nrf52840/rak4631 -D RAK_4631 -DMESHTASTIC_USE_EINK_UI=0 -DRADIOLIB_EXCLUDE_SX128X=1 -DRADIOLIB_EXCLUDE_SX127X=1 -DRADIOLIB_EXCLUDE_LR11X0=1 build_src_filter = ${nrf52_base.build_src_filter} \ +<../variants/nrf52840/rak4631> \ + \ + \ + \ - \ - \ - lib_deps = ${nrf52840_base.lib_deps} ${networking_base.lib_deps} # renovate: datasource=custom.pio depName=Melopero RV3028 packageName=melopero/library/Melopero RV3028 melopero/Melopero RV3028@1.2.0 # renovate: datasource=github-tags depName=RAK13800-W5100S packageName=RAKWireless/RAK13800-W5100S https://github.com/RAKWireless/RAK13800-W5100S/archive/1.0.2.zip # renovate: datasource=custom.pio depName=RAK NCP5623 RGB LED packageName=rakwireless/library/RAKwireless NCP5623 RGB LED library rakwireless/RAKwireless NCP5623 RGB LED library@1.0.3 # renovate: datasource=custom.pio depName=RAK12035_SoilMoisture packageName=beegee-tokyo/library/RAK12035_SoilMoisture beegee-tokyo/RAK12035_SoilMoisture@1.0.4 # renovate: datasource=git-refs depName=RAK12034-BMX160 packageName=https://github.com/RAKWireless/RAK12034-BMX160 gitBranch=main https://github.com/RAKWireless/RAK12034-BMX160/archive/dcead07ffa267d3c906e9ca4a1330ab989e957e2.zip ; 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} # TODO renovate https://github.com/geeksville/Armduino-Semihosting/archive/35b538fdf208c3530c1434cd099a08e486672ee4.zip ; 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 ================================================ FILE: variants/nrf52840/rak4631/variant.cpp ================================================ /* 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); // 3V3 Power Rail pinMode(PIN_3V3_EN, OUTPUT); digitalWrite(PIN_3V3_EN, HIGH); } ================================================ FILE: variants/nrf52840/rak4631/variant.h ================================================ /* 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 LED_BLUE (36) #define LED_GREEN PIN_LED1 #define LED_NOTIFICATION LED_BLUE #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 (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 WB_I2C1_SDA (13) // SENSOR_SLOT IO_SLOT #define WB_I2C1_SCL (14) // SENSOR_SLOT IO_SLOT #define PIN_AREF (2) #define PIN_NFC1 (9) #define WB_IO5 PIN_NFC1 #define WB_IO4 (4) #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 (WB_I2C1_SDA) #define PIN_WIRE_SCL (WB_I2C1_SCL) // 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 */ // configure the SET pin on the RAK12039 sensor board to disable the sensor while not reading // air quality telemetry. PIN_NFC2 doesn't seem to be used anywhere else in the codebase, but if // you're having problems with your node behaving weirdly when a RAK12039 board isn't connected, // try disabling this. #define PMSA003I_ENABLE_PIN PIN_NFC2 #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 // If using a power chip like the INA3221 you can override the default battery voltage channel below // and comment out NRF_APM to use the INA3221 instead of the USB detection for charging // #define INA3221_BAT_CH INA3221_CH2 // #define INA3221_ENV_CH INA3221_CH1 // enables 3.3V periphery like GPS or IO Module // Do not toggle this for GPS power savings #define PIN_3V3_EN (34) #define WB_IO2 PIN_3V3_EN // 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! // RAK4631 custom ringtone #undef USERPREFS_RINGTONE_RTTTL #define USERPREFS_RINGTONE_RTTTL "Rak:d=32,o=5,b=200:b7,p,b7,4p,p" // 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 // RAK4630 AIN0 = nrf52840 AIN3 = Pin 5 #define BATTERY_LPCOMP_INPUT NRF_LPCOMP_INPUT_3 // We have AIN3 with a VBAT divider so AIN3 = VBAT * (1.5/2.5) // We have the device going deep sleep under 3.1V, which is AIN3 = 1.86V // So we can wake up when VBAT>=VDD is restored to 3.3V, where AIN3 = 1.98V // 1.98/3.3 = 6/10, but that's close to the VBAT divider, so we // pick 6/8VDD, which means VBAT=4.1V. // Reference: // VDD=3.3V AIN3=5/8*VDD=2.06V VBAT=1.66*AIN3=3.41V // VDD=3.3V AIN3=11/16*VDD=2.26V VBAT=1.66*AIN3=3.76V // VDD=3.3V AIN3=6/8*VDD=2.47V VBAT=1.66*AIN3=4.1V #define BATTERY_LPCOMP_THRESHOLD NRF_LPCOMP_REF_SUPPLY_11_16 #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 ================================================ FILE: variants/nrf52840/rak4631_epaper/platformio.ini ================================================ ; 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} -I variants/nrf52840/rak4631_epaper -D RAK_4631 -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/nrf52840/rak4631_epaper> lib_deps = ${nrf52840_base.lib_deps} # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 zinggjm/GxEPD2@1.6.8 # renovate: datasource=custom.pio depName=Melopero RV3028 packageName=melopero/library/Melopero RV3028 melopero/Melopero RV3028@1.2.0 # renovate: datasource=custom.pio depName=RAK NCP5623 RGB LED packageName=rakwireless/library/RAKwireless NCP5623 RGB LED library rakwireless/RAKwireless NCP5623 RGB LED library@1.0.3 # renovate: datasource=custom.pio depName=RAK12034 packageName=beegee-tokyo/library/RAKwireless RAK12034 beegee-tokyo/RAKwireless RAK12034@1.0.0 # renovate: datasource=custom.pio depName=RAK12035_SoilMoisture packageName=beegee-tokyo/library/RAK12035_SoilMoisture beegee-tokyo/RAK12035_SoilMoisture@1.0.4 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 ================================================ FILE: variants/nrf52840/rak4631_epaper/variant.cpp ================================================ /* 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); // 3V3 Power Rail pinMode(PIN_3V3_EN, OUTPUT); digitalWrite(PIN_3V3_EN, HIGH); } ================================================ FILE: variants/nrf52840/rak4631_epaper/variant.h ================================================ /* 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 LED_BLUE (36) #define LED_GREEN PIN_LED1 #define LED_NOTIFICATION LED_BLUE #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 (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 WB_IO5 PIN_NFC1 #define WB_IO4 (4) #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) #define WB_IO2 PIN_3V3_EN // 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 RAK_4631 1 #ifdef __cplusplus } #endif /*---------------------------------------------------------------------------- * Arduino objects - C++ only *----------------------------------------------------------------------------*/ #endif ================================================ FILE: variants/nrf52840/rak4631_epaper_onrxtx/platformio.ini ================================================ ; 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} -I variants/nrf52840/rak4631_epaper -D RAK_4631 -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/nrf52840/rak4631_epaper_onrxtx> lib_deps = ${nrf52840_base.lib_deps} # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 zinggjm/GxEPD2@1.6.8 # renovate: datasource=custom.pio depName=Melopero RV3028 packageName=melopero/library/Melopero RV3028 melopero/Melopero RV3028@1.2.0 # renovate: datasource=custom.pio depName=RAK NCP5623 RGB LED packageName=rakwireless/library/RAKwireless NCP5623 RGB LED library rakwireless/RAKwireless NCP5623 RGB LED library@1.0.3 # renovate: datasource=custom.pio depName=RAK12034 packageName=beegee-tokyo/library/RAKwireless RAK12034 beegee-tokyo/RAKwireless RAK12034@1.0.0 # renovate: datasource=custom.pio depName=RAK12035_SoilMoisture packageName=beegee-tokyo/library/RAK12035_SoilMoisture beegee-tokyo/RAK12035_SoilMoisture@1.0.4 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 ================================================ FILE: variants/nrf52840/rak4631_epaper_onrxtx/variant.cpp ================================================ /* 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); // 3V3 Power Rail pinMode(PIN_3V3_EN, OUTPUT); digitalWrite(PIN_3V3_EN, HIGH); } ================================================ FILE: variants/nrf52840/rak4631_epaper_onrxtx/variant.h ================================================ #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 LED_BLUE (36) #define LED_GREEN PIN_LED1 #define LED_NOTIFICATION LED_BLUE #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 WB_IO5 PIN_NFC1 #define WB_IO4 (4) // #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) #define WB_IO2 PIN_3V3_EN // 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 #ifdef __cplusplus } #endif /*---------------------------------------------------------------------------- * Arduino objects - C++ only *----------------------------------------------------------------------------*/ #endif ================================================ FILE: variants/nrf52840/rak4631_eth_gw/platformio.ini ================================================ ; 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} -I variants/nrf52840/rak4631_eth_gw -D RAK_4631 -DHAS_UDP_MULTICAST=1 -DEINK_DISPLAY_MODEL=GxEPD2_213_BN -DEINK_WIDTH=250 -DEINK_HEIGHT=122 -DNRF52_USE_JSON=1 -DMESHTASTIC_EXCLUDE_WIFI=1 -DMESHTASTIC_EXCLUDE_SCREEN=1 ; -DMESHTASTIC_EXCLUDE_PKI=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/nrf52840/rak4631_eth_gw> + + + + lib_deps = ${nrf52840_base.lib_deps} ${networking_base.lib_deps} # renovate: datasource=custom.pio depName=Melopero RV3028 packageName=melopero/library/Melopero RV3028 melopero/Melopero RV3028@1.2.0 # renovate: datasource=github-tags depName=RAK13800-W5100S packageName=RAKWireless/RAK13800-W5100S https://github.com/RAKWireless/RAK13800-W5100S/archive/1.0.2.zip # renovate: datasource=custom.pio depName=RAK NCP5623 RGB LED packageName=rakwireless/library/RAKwireless NCP5623 RGB LED library rakwireless/RAKwireless NCP5623 RGB LED library@1.0.3 # renovate: datasource=git-refs depName=RAK12034-BMX160 packageName=https://github.com/RAKWireless/RAK12034-BMX160 gitBranch=main https://github.com/RAKWireless/RAK12034-BMX160/archive/dcead07ffa267d3c906e9ca4a1330ab989e957e2.zip # renovate: datasource=custom.pio depName=ArduinoJson packageName=bblanchon/library/ArduinoJson bblanchon/ArduinoJson@6.21.6 ; 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} # TODO renovate https://github.com/geeksville/Armduino-Semihosting/archive/35b538fdf208c3530c1434cd099a08e486672ee4.zip ; 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 ================================================ FILE: variants/nrf52840/rak4631_eth_gw/variant.cpp ================================================ /* 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); // 3V3 Power Rail pinMode(PIN_3V3_EN, OUTPUT); digitalWrite(PIN_3V3_EN, HIGH); } ================================================ FILE: variants/nrf52840/rak4631_eth_gw/variant.h ================================================ /* 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 LED_BLUE (36) #define LED_GREEN PIN_LED1 #define LED_NOTIFICATION LED_BLUE #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 (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 WB_IO5 PIN_NFC1 #define WB_IO4 (4) #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) #define WB_IO2 PIN_3V3_EN // 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_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 ================================================ FILE: variants/nrf52840/rak4631_nomadstar_meteor_pro/platformio.ini ================================================ ; NomadStar Meteor Pro based on RAK4631 with RGBW LED LP5562 support [env:rak4631_nomadstar_meteor_pro] custom_meshtastic_hw_model = 96 custom_meshtastic_hw_model_slug = NOMADSTAR_METEOR_PRO custom_meshtastic_architecture = nrf52840 custom_meshtastic_actively_supported = true custom_meshtastic_support_level = 1 custom_meshtastic_display_name = NomadStar Meteor Pro custom_meshtastic_images = meteor_pro.svg custom_meshtastic_tags = NomadStar extends = nrf52840_base board = wiscore_rak4631 board_check = true build_flags = ${nrf52840_base.build_flags} -I variants/nrf52840/rak4631_nomadstar_meteor_pro -D NOMADSTAR_METEOR_PRO -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/nrf52840/rak4631_nomadstar_meteor_pro> + + lib_deps = ${nrf52840_base.lib_deps} # renovate: datasource=git-refs depName=IOBoard-RGB-LP5562-Library packageName=NomadStar-outdoor/IOBoard-RGB-LP5562-Library gitBranch=master https://github.com/NomadStar-outdoor/IOBoard-RGB-LP5562-Library/archive/9c366c875e1e8103ed97b5d4c09f3878345da80a.zip ; 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_nomadstar_meteor_pro_dbg] extends = env:rak4631_nomadstar_meteor_pro 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} # TODO renovate https://github.com/geeksville/Armduino-Semihosting/archive/35b538fdf208c3530c1434cd099a08e486672ee4.zip ; 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 ================================================ FILE: variants/nrf52840/rak4631_nomadstar_meteor_pro/variant.cpp ================================================ /* 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); // 3V3 Power Rail pinMode(PIN_3V3_EN, OUTPUT); digitalWrite(PIN_3V3_EN, HIGH); } ================================================ FILE: variants/nrf52840/rak4631_nomadstar_meteor_pro/variant.h ================================================ /* 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 LED_BLUE (36) #define LED_GREEN PIN_LED1 #define LED_NOTIFICATION LED_BLUE #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 (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 // Texas Instrument LP5562 #define HAS_LP5562 #define ENABLE_AMBIENTLIGHTING /* * 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 // 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_ETHERNET 0 #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 ================================================ FILE: variants/nrf52840/rak_wismeshtag/platformio.ini ================================================ ; The very slick RAK wireless RAK 4631 / 4630 board - Unified firmware for 5005/19003, with or without OLED RAK 1921 [env:rak_wismeshtag] custom_meshtastic_support_level = 1 custom_meshtastic_images = rak_wismesh_tag.svg custom_meshtastic_tags = RAK extends = nrf52840_base board = wiscore_rak4631 board_check = true custom_meshtastic_hw_model = 105 custom_meshtastic_hw_model_slug = WISMESH_TAG custom_meshtastic_architecture = nrf52840 custom_meshtastic_display_name = RAK WisMesh Tag custom_meshtastic_actively_supported = true build_flags = ${nrf52840_base.build_flags} -I variants/nrf52840/rak_wismeshtag -D WISMESH_TAG -D RAK_4631 -DRADIOLIB_EXCLUDE_SX128X=1 -DRADIOLIB_EXCLUDE_SX127X=1 -DRADIOLIB_EXCLUDE_LR11X0=1 -DMESHTASTIC_EXCLUDE_WIFI=1 build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/rak_wismeshtag> ================================================ FILE: variants/nrf52840/rak_wismeshtag/variant.cpp ================================================ /* 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); // 3V3 Power Rail pinMode(PIN_3V3_EN, OUTPUT); digitalWrite(PIN_3V3_EN, HIGH); } ================================================ FILE: variants/nrf52840/rak_wismeshtag/variant.h ================================================ /* 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 LED_BLUE (36) #define LED_GREEN PIN_LED1 #define LED_NOTIFICATION LED_BLUE #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 (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 /* * Wire Interfaces */ #define WIRE_INTERFACES_COUNT 1 // RAK WISMESHTAG #define PIN_WIRE_SDA (25) #define PIN_WIRE_SCL (24) // 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) // RAK WISMESHTAG #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 // RAK WISMESHTAG #define PIN_BUZZER 21 // 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 OCV_ARRAY 4240, 4112, 4029, 3970, 3906, 3846, 3824, 3802, 3776, 3650, 3072 #define RAK_4631 1 #define HAS_SCREEN 0 #ifdef __cplusplus } #endif /*---------------------------------------------------------------------------- * Arduino objects - C++ only *----------------------------------------------------------------------------*/ #endif ================================================ FILE: variants/nrf52840/rak_wismeshtap/platformio.ini ================================================ ; 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:rak_wismeshtap] custom_meshtastic_hw_model = 84 custom_meshtastic_hw_model_slug = WISMESH_TAP custom_meshtastic_architecture = nrf52840 custom_meshtastic_actively_supported = true custom_meshtastic_support_level = 1 custom_meshtastic_display_name = RAK WisMesh Tap custom_meshtastic_images = rak-wismeshtap.svg custom_meshtastic_tags = RAK extends = nrf52840_base board = wiscore_rak4631 build_flags = ${nrf52840_base.build_flags} -Ivariants/nrf52840/rak_wismeshtap -DWISMESH_TAP -DRAK_4631 -DEINK_DISPLAY_MODEL=GxEPD2_213_BN -DEINK_WIDTH=250 -DEINK_HEIGHT=122 -DMESHTASTIC_EXCLUDE_WIFI=1 -DMESHTASTIC_EXCLUDE_DETECTIONSENSOR=1 -DMESHTASTIC_EXCLUDE_STOREFORWARD=1 -DMESHTASTIC_EXCLUDE_POWER_TELEMETRY=1 -DMESHTASTIC_EXCLUDE_ATAK=1 build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/rak_wismeshtap> + + + lib_deps = ${nrf52840_base.lib_deps} ${networking_base.lib_deps} # renovate: datasource=custom.pio depName=Melopero RV3028 packageName=melopero/library/Melopero RV3028 melopero/Melopero RV3028@1.2.0 # renovate: datasource=github-tags depName=RAK13800-W5100S packageName=RAKWireless/RAK13800-W5100S https://github.com/RAKWireless/RAK13800-W5100S/archive/1.0.2.zip # renovate: datasource=custom.pio depName=RAK NCP5623 RGB LED packageName=rakwireless/library/RAKwireless NCP5623 RGB LED library rakwireless/RAKwireless NCP5623 RGB LED library@1.0.3 # renovate: datasource=custom.pio depName=TFT_eSPI packageName=bodmer/library/TFT_eSPI bodmer/TFT_eSPI@2.5.43 # renovate: datasource=custom.pio depName=RAK12034 packageName=beegee-tokyo/library/RAKwireless RAK12034 beegee-tokyo/RAKwireless RAK12034@1.0.0 # renovate: datasource=custom.pio depName=RAK14014-FT6336U packageName=beegee-tokyo/library/RAK14014-FT6336U beegee-tokyo/RAK14014-FT6336U@1.0.1 # renovate: datasource=custom.pio depName=RAK12035_SoilMoisture packageName=beegee-tokyo/library/RAK12035_SoilMoisture beegee-tokyo/RAK12035_SoilMoisture@1.0.4 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 ================================================ FILE: variants/nrf52840/rak_wismeshtap/variant.cpp ================================================ /* 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); // 3V3 Power Rail pinMode(PIN_3V3_EN, OUTPUT); digitalWrite(PIN_3V3_EN, HIGH); } void variant_shutdown() { // GPIO restores input status, otherwise there will be leakage current nrf_gpio_cfg_default(TFT_BL); nrf_gpio_cfg_default(TFT_DC); nrf_gpio_cfg_default(TFT_CS); nrf_gpio_cfg_default(TFT_SCLK); nrf_gpio_cfg_default(TFT_MOSI); nrf_gpio_cfg_default(TFT_MISO); nrf_gpio_cfg_default(SCREEN_TOUCH_INT); nrf_gpio_cfg_default(WB_I2C1_SCL); nrf_gpio_cfg_default(WB_I2C1_SDA); // nrf_gpio_cfg_default(WB_I2C2_SCL); // nrf_gpio_cfg_default(WB_I2C2_SDA); } ================================================ FILE: variants/nrf52840/rak_wismeshtap/variant.h ================================================ /* 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 LED_BLUE (36) #define LED_GREEN PIN_LED1 #define LED_NOTIFICATION LED_BLUE #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 /* * 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 WB_IO5 PIN_NFC1 #define WB_IO4 (4) #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 <- already defined above (ln. 94) // static const uint8_t WB_IO5 = 9; // SLOT_D <- already defined above (ln. 93) 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 RAK_4631 1 #define AQ_SET_PIN 10 #ifdef __cplusplus } #endif #define RAK14014 // Tell it we have a RAK14014 #define USER_SETUP_LOADED 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 USE_TFTDISPLAY 1 #define SCREEN_ROTATE #define SCREEN_TRANSITION_FRAMERATE 5 #define HAS_TOUCHSCREEN 1 #define SCREEN_TOUCH_INT WB_IO6 #define USE_POWERSAVE #define SLEEP_TIME 120 #define USE_VIRTUAL_KEYBOARD 1 /*---------------------------------------------------------------------------- * Arduino objects - C++ only *----------------------------------------------------------------------------*/ #endif ================================================ FILE: variants/nrf52840/seeed_solar_node/platformio.ini ================================================ [env:seeed_solar_node] custom_meshtastic_hw_model = 95 custom_meshtastic_hw_model_slug = SEEED_SOLAR_NODE custom_meshtastic_architecture = nrf52840 custom_meshtastic_actively_supported = true custom_meshtastic_support_level = 1 custom_meshtastic_display_name = Seeed SenseCAP Solar Node custom_meshtastic_images = seeed_solar.svg custom_meshtastic_tags = Seeed board = seeed_solar_node extends = nrf52840_base ;board_level = extra build_flags = ${nrf52840_base.build_flags} -I variants/nrf52840/seeed_solar_node -D SEEED_SOLAR_NODE -I src/platform/nrf52/softdevice -I src/platform/nrf52/softdevice/nrf52 board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/seeed_solar_node> debug_tool = jlink ================================================ FILE: variants/nrf52840/seeed_solar_node/variant.cpp ================================================ /* * variant.cpp - Digital pin mapping for nRF52-based development board * * This file defines the pin mapping array that maps logical digital pins (D0-D17) * to physical GPIO ports/pins on the Nordic nRF52 series microcontroller. * * Board: [Seeed Studio XIAO nRF52840 Sense (Seeed Solar Node)] * Hardware Features: * - LoRa module (CS/SCK/MISO/MOSI control pins) * - GNSS module (TX/RX/Reset/Wakeup) * - User LEDs (D11-D12) * - User button (D13) * - Grove/NFC interface (D14-D15) * - Battery voltage monitoring (D16) * * Created [20250225] * By [Dylan] * Version 1.0 * License: [MIT] */ #include "variant.h" #include "nrf.h" #include "wiring_constants.h" #include "wiring_digital.h" /** * @brief Digital pin to GPIO port/pin mapping table * * Format: Logical Pin (Dx) -> nRF Port.Pin (Px.xx) * * Pin Groupings: * [D0-D10] - Peripheral control (LoRa, GNSS) * [D11-D12] - LED outputs * [D13] - User button * [D14-D15] - Grove/NFC interface * [D16] - Battery voltage ADC input * [D17] - GNSS module reset */ extern "C" { const uint32_t g_ADigitalPinMap[] = { // D0 .. D10 - Peripheral control pins 2, // D0 P0.02 (A0) GNSS_WAKEUP 3, // D1 P0.03 (A1) LORA_DIO1 28, // D2 P0.28 (A2) LORA_RESET 29, // D3 P0.29 (A3) LORA_BUSY 4, // D4 P0.04 (A4/SDA) LORA_CS 5, // D5 P0.05 (A5/SCL) LORA_SW 43, // D6 P1.11 (UART_TX) GNSS_TX 44, // D7 P1.12 (UART_RX) GNSS_RX 45, // D8 P1.13 (SPI_SCK) LORA_SCK 46, // D9 P1.14 (SPI_MISO) LORA_MISO 47, // D10 P1.15 (SPI_MOSI) LORA_MOSI // D11-D12 - LED outputs 15, // D11 P0.15 User LED 19, // D12 P0.19 Breathing LED // D13 - User input 33, // D13 P1.01 User Button // D14-D15 - Grove/NFC interface 9, // D14 P0.09 NFC1/GROVE_D1 10, // D15 P0.10 NFC2/GROVE_D0 // D16 - Power management // 31, // D16 P0.31 VBAT_ADC (Battery voltage) 31, // D16 P0.31 VBAT_ADC (Battery voltage) // D17 - GNSS control 35, // D17 P1.03 GNSS_RESET 37, // D18 P1.05 GNSS_ENABLE 14, // D19 P0.14 BAT_READ 39, // D20 P1.07 USER_BUTTON // 21, // D21 P0.21 (QSPI_SCK) 25, // D22 P0.25 (QSPI_CSN) 20, // D23 P0.20 (QSPI_SIO_0 DI) 24, // D24 P0.24 (QSPI_SIO_1 DO) 22, // D25 P0.22 (QSPI_SIO_2 WP) 23, // D26 P0.23 (QSPI_SIO_3 HOLD) }; } void initVariant() { pinMode(PIN_QSPI_CS, OUTPUT); digitalWrite(PIN_QSPI_CS, HIGH); // This setup is crucial for ensuring low power consumption and proper initialization of the hardware components. pinMode(GPS_EN, OUTPUT); digitalWrite(GPS_EN, LOW); // VBAT_ENABLE pinMode(BAT_READ, OUTPUT); digitalWrite(BAT_READ, LOW); pinMode(PIN_LED1, OUTPUT); digitalWrite(PIN_LED1, LOW); pinMode(PIN_LED2, OUTPUT); digitalWrite(PIN_LED2, LOW); pinMode(PIN_LED2, OUTPUT); pinMode(GPS_EN, OUTPUT); digitalWrite(GPS_EN, HIGH); } ================================================ FILE: variants/nrf52840/seeed_solar_node/variant.h ================================================ #ifndef _SEEED_SOLAR_NODE_H_ #define _SEEED_SOLAR_NODE_H_ #include "WVariant.h" // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // Clock Configuration // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ #define VARIANT_MCK (64000000ul) // Master clock frequency #define USE_LFXO // 32.768kHz crystal for LFCLK // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // Pin Capacity Definitions // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ #define PINS_COUNT (33u) // Total GPIO pins #define NUM_DIGITAL_PINS (33u) // Digital I/O pins #define NUM_ANALOG_INPUTS (8u) // Analog inputs (A0-A5 + VBAT + AREF) #define NUM_ANALOG_OUTPUTS (0u) // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // LED Configuration // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // LEDs // LEDs #define PIN_LED1 (12) // LED P1.15 #define PIN_LED2 (11) // #define LED_GREEN PIN_LED1 #define LED_BLUE PIN_LED2 #define LED_STATE_ON 1 // State when LED is litted // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // Button Configuration // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ #define BUTTON_PIN D13 // This is the Program Button // #define BUTTON_NEED_PULLUP 1 #define BUTTON_ACTIVE_LOW true #define BUTTON_ACTIVE_PULLUP false #define BUTTON_PIN_TOUCH 20 // Touch button // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // Digital Pin Mapping (D0-D10) // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ #define D0 0 // P0.02 GNSS_WAKEUP/IO0 #define D1 1 // P0.03 LORA_DIO1 #define D2 2 // P0.28 LORA_RESET #define D3 3 // P0.29 LORA_BUSY #define D4 4 // P0.04 LORA_CS/I2C_SDA #define D5 5 // P0.05 LORA_SW/I2C_SCL #define D6 6 // P1.11 GNSS_TX #define D7 7 // P1.12 GNSS_RX #define D8 8 // P1.13 SPI_SCK #define D9 9 // P1.14 SPI_MISO #define D10 10 // P1.15 SPI_MOSI #define D13 13 // P1.01 User Button #define D14 14 // P0.09 NFC1/GROVE_D1 #define D15 15 // P0.10 NFC2/GROVE_D0 #define D16 16 // P0.31 VBAT_ADC (Battery voltage) #define D17 17 // P1.03 GNSS_RESET #define D18 18 // P1.05 GNSS_ENABLE #define D19 19 // P0.14 BAT_READ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // Analog Pin Definitions // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ #define PIN_A0 0 // P0.02 Analog Input 0 #define PIN_A1 1 // P0.03 Analog Input 1 #define PIN_A2 2 // P0.28 Analog Input 2 #define PIN_A3 3 // P0.29 Analog Input 3 #define PIN_A4 4 // P0.04 Analog Input 4 #define PIN_A5 5 // P0.05 Analog Input 5 #define PIN_VBAT D16 // P0.31 Battery voltage sense // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // Communication Interfaces // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // I2C Configuration #define HAS_WIRE 1 #define PIN_WIRE_SDA D14 // P0.09 #define PIN_WIRE_SCL D15 // P0.10 #define WIRE_INTERFACES_COUNT 1 #define I2C_NO_RESCAN static const uint8_t SDA = PIN_WIRE_SDA; static const uint8_t SCL = PIN_WIRE_SCL; // SPI Configuration (SX1262) #define SPI_INTERFACES_COUNT 1 #define PIN_SPI_MISO 9 // P1.14 (D9) #define PIN_SPI_MOSI 10 // P1.15 (D10) #define PIN_SPI_SCK 8 // P1.13 (D8) // SX1262 LoRa Module Pins #define USE_SX1262 #define SX126X_CS D4 // Chip select #define SX126X_DIO1 D1 // Digital IO 1 (Interrupt) #define SX126X_BUSY D3 // Busy status #define SX126X_RESET D2 // Reset control #define SX126X_DIO3_TCXO_VOLTAGE 1.8 // TCXO supply voltage #define SX126X_RXEN D5 // RX enable control #define SX126X_TXEN RADIOLIB_NC #define SX126X_DIO2_AS_RF_SWITCH // This Line is really necessary for SX1262 to work with RF switch or will loss TX power // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // Power Management // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ #define BAT_READ \ D19 // P0_14 = 14 Reads battery voltage from divider on signal board. (PIN_VBAT is reading voltage divider on XIAO and is // program pin 32 / or P0.31) #define BATTERY_SENSE_RESOLUTION_BITS 12 #define ADC_MULTIPLIER 3.3 #define BATTERY_PIN PIN_VBAT // PIN_A7 #define AREF_VOLTAGE 3.3 #define OCV_ARRAY 4200, 3986, 3922, 3812, 3734, 3645, 3527, 3420, 3281, 3087, 2786 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // GPS L76KB // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ #define GPS_L76K #ifdef GPS_L76K #define GPS_TX_PIN D6 // 44 #define GPS_RX_PIN D7 // 43 #define HAS_GPS 1 #define GPS_BAUDRATE 9600 #define GPS_THREAD_INTERVAL 50 #define PIN_SERIAL1_TX GPS_TX_PIN #define PIN_SERIAL1_RX GPS_RX_PIN #define PIN_GPS_STANDBY D0 #define GPS_EN D18 // P1.05 #endif // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // On-board QSPI Flash // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // On-board QSPI Flash #define PIN_QSPI_SCK (21) #define PIN_QSPI_CS (22) #define PIN_QSPI_IO0 (23) #define PIN_QSPI_IO1 (24) #define PIN_QSPI_IO2 (25) #define PIN_QSPI_IO3 (26) #define EXTERNAL_FLASH_DEVICES P25Q16H #define EXTERNAL_FLASH_USE_QSPI // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // Compatibility Definitions // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ #ifdef __cplusplus extern "C" { #endif // Serial port placeholders #define PIN_SERIAL2_RX (-1) #define PIN_SERIAL2_TX (-1) #ifdef __cplusplus } #endif #endif // _SEEED_SOLAR_NODE_H_ ================================================ FILE: variants/nrf52840/seeed_wio_tracker_L1/platformio.ini ================================================ [env:seeed_wio_tracker_L1] custom_meshtastic_hw_model = 99 custom_meshtastic_hw_model_slug = SEEED_WIO_TRACKER_L1 custom_meshtastic_architecture = nrf52840 custom_meshtastic_actively_supported = true custom_meshtastic_support_level = 1 custom_meshtastic_display_name = Seeed Wio Tracker L1 custom_meshtastic_images = wio_tracker_l1_case.svg custom_meshtastic_tags = Seeed custom_meshtastic_requires_dfu = true board = seeed_wio_tracker_L1 extends = nrf52840_base build_flags = ${nrf52840_base.build_flags} -I variants/nrf52840/seeed_wio_tracker_L1 -D SEEED_WIO_TRACKER_L1 -I src/platform/nrf52/softdevice -I src/platform/nrf52/softdevice/nrf52 board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/seeed_wio_tracker_L1> debug_tool = jlink ================================================ FILE: variants/nrf52840/seeed_wio_tracker_L1/variant.cpp ================================================ /* * variant.cpp - Digital pin mapping for TRACKER L1 * * This file defines the pin mapping array that maps logical digital pins (D0-D17) * to physical GPIO ports/pins on the Nordic nRF52 series microcontroller. * * Board: [Seeed Studio WIO TRACKER L1] * Hardware Features: * - LoRa module (CS/SCK/MISO/MOSI control pins) * - GNSS module (TX/RX/Reset/Wakeup) * - User LEDs (D11-D12) * - User button (D13) * - Grove/NFC interface (D14-D15) * - Battery voltage monitoring (D16) * * Created [20250521] * By [Dylan] */ #include "variant.h" #include "nrf.h" #include "wiring_constants.h" #include "wiring_digital.h" /** * @brief Digital pin to GPIO port/pin mapping table * * Format: Logical Pin (Dx) -> nRF Port.Pin (Px.xx) * */ extern "C" { const uint32_t g_ADigitalPinMap[] = { // D0 .. D10 - Peripheral control pins 41, // D0 P1.09 GNSS_WAKEUP 7, // D1 P0.07 LORA_DIO1 39, // D2 P1,07 LORA_RESET 42, // D3 P1.10 LORA_BUSY 46, // D4 P1.14 (A4/SDA) LORA_CS 40, // D5 P1.08 (A5/SCL) LORA_SW 27, // D6 P0.27 (UART_TX) GNSS_TX 26, // D7 P0.26 (UART_RX) GNSS_RX 30, // D8 P0.30 (SPI_SCK) LORA_SCK 3, // D9 P0.3 (SPI_MISO) LORA_MISO 28, // D10 P0.28 (SPI_MOSI) LORA_MOSI // D11-D12 - LED outputs 33, // D11 P1.1 User LED // Buzzzer 32, // D12 P1.0 Buzzer // D13 - User input 8, // D13 P0.08 User Button // D14-D15 - Grove interface 6, // D14 P0.06 OLED SDA 5, // D15 P0.05 OLED SCL // D16 - Battery voltage ADC input 31, // D16 P0.31 VBAT_ADC // GROVE 43, // D17 P0.00 GROVESDA 44, // D18 P0.01 GROVESCL // FLASH 21, // D19 P0.21 (QSPI_SCK) 25, // D20 P0.25 (QSPI_CSN) 20, // D21 P0.20 (QSPI_SIO_0 DI) 24, // D22 P0.24 (QSPI_SIO_1 DO) 22, // D23 P0.22 (QSPI_SIO_2 WP) 23, // D24 P0.23 (QSPI_SIO_3 HOLD) 36, // D25 TB_UP 12, // D26 TB_DOWN 11, // D27 TB_LEFT 35, // D28 TB_RIGHT 37, // D29 TB_PRESS 4, // D30 BAT_CTL }; } void initVariant() { pinMode(PIN_QSPI_CS, OUTPUT); digitalWrite(PIN_QSPI_CS, HIGH); // This setup is crucial for ensuring low power consumption and proper initialization of the hardware components. // VBAT_ENABLE pinMode(BAT_READ, OUTPUT); digitalWrite(BAT_READ, HIGH); pinMode(PIN_LED1, OUTPUT); digitalWrite(PIN_LED1, LOW); pinMode(PIN_LED2, OUTPUT); digitalWrite(PIN_LED2, LOW); pinMode(PIN_LED2, OUTPUT); } ================================================ FILE: variants/nrf52840/seeed_wio_tracker_L1/variant.h ================================================ #ifndef _SEEED_TRACKER_L1_H_ #define _SEEED_TRACKER_L1_H_ #include "WVariant.h" // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // Clock Configuration // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ #define VARIANT_MCK (64000000ul) // Master clock frequency #define USE_LFXO // 32.768kHz crystal for LFCLK // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // Pin Capacity Definitions // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ #define PINS_COUNT (33u) // Total GPIO pins #define NUM_DIGITAL_PINS (33u) // Digital I/O pins #define NUM_ANALOG_INPUTS (8u) // Analog inputs (A0-A5 + VBAT + AREF) #define NUM_ANALOG_OUTPUTS (0u) // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // LED Configuration // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // LEDs // LEDs #define PIN_LED1 (11) // LED P1.15 #define PIN_LED2 (12) // #define LED_GREEN PIN_LED1 #define LED_BLUE PIN_LED2 #define LED_STATE_ON 1 // State when LED is lit // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // Button Configuration // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ #define CANCEL_BUTTON_PIN D13 // This is the Program Button // #define BUTTON_NEED_PULLUP 1 #define CANCEL_BUTTON_ACTIVE_LOW true #define CANCEL_BUTTON_ACTIVE_PULLUP false // #define BUTTON_PIN_TOUCH 13 // Touch button // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // Digital Pin Mapping (D0-D10) // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ #define D0 0 // P1.06 GNSS_WAKEUP/IO0 #define D1 1 // P0.07 LORA_DIO1 #define D2 2 // P1.07 LORA_RESET #define D3 3 // P1.10 LORA_BUSY #define D4 4 // P1.14 LORA_CS #define D5 5 // P1.08 LORA_SW #define D6 6 // P0.27 GNSS_TX #define D7 7 // P0.26 GNSS_RX #define D8 8 // P0.30 SPI_SCK #define D9 9 // P0.03 SPI_MISO #define D10 10 // P0.28 SPI_MOSI #define D12 12 // P1.00 Buzzer #define D13 13 // P0.08 User Button #define D14 14 // P0.05 OLED SCL #define D15 15 // P0.06 OLED SDA #define D16 16 // P0.31 VBAT_ADC #define D17 17 // P0.00 GROVE SDA #define D18 18 // P0.01 GROVE_SCL // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // Analog Pin Definitions // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ #define PIN_A0 0 // P0.02 Analog Input 0 #define PIN_A1 1 // P0.03 Analog Input 1 #define PIN_A2 2 // P0.28 Analog Input 2 #define PIN_A3 3 // P0.29 Analog Input 3 #define PIN_A4 4 // P0.04 Analog Input 4 #define PIN_A5 5 // P0.05 Analog Input 5 #define PIN_VBAT D16 // P0.31 Battery voltage sense // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // Communication Interfaces // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // I2C Configuration // #define HAS_WIRE 1 #define PIN_WIRE_SDA D14 // P0.09 #define PIN_WIRE_SCL D15 // P0.10 #define WIRE_INTERFACES_COUNT 2 #define PIN_WIRE1_SDA D18 #define PIN_WIRE1_SCL D17 #define I2C_NO_RESCAN static const uint8_t SDA = PIN_WIRE_SDA; static const uint8_t SCL = PIN_WIRE_SCL; #define HAS_SCREEN 1 #define USE_SSD1306 1 // SPI Configuration (SX1262) #define SPI_INTERFACES_COUNT 1 #define PIN_SPI_MISO 9 // P0.03 (D9) #define PIN_SPI_MOSI 10 // P0.28 (D10) #define PIN_SPI_SCK 8 // P0.30 (D8) // SX1262 LoRa Module Pins #define USE_SX1262 #define SX126X_CS D4 // Chip select #define SX126X_DIO1 D1 // Digital IO 1 (Interrupt) #define SX126X_BUSY D3 // Busy status #define SX126X_RESET D2 // Reset control #define SX126X_DIO3_TCXO_VOLTAGE 1.8 // TCXO supply voltage #define SX126X_RXEN D5 // RX enable control #define SX126X_TXEN RADIOLIB_NC #define SX126X_DIO2_AS_RF_SWITCH // This Line is really necessary for SX1262 to work with RF switch or will loss TX power // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // Power Management // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ #define BAT_READ 30 // D30 = P0.04 Reads battery voltage from divider on signal board. #define BATTERY_SENSE_RESOLUTION_BITS 12 #define ADC_MULTIPLIER 2.0 #define BATTERY_PIN PIN_VBAT // PIN_A7 #define AREF_VOLTAGE 3.6 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // GPS L76KB // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ #define GPS_L76K #ifdef GPS_L76K #define GPS_TX_PIN D6 // P0.26 - This is data from the MCU #define GPS_RX_PIN D7 // P0.27 - This is data from the GNSS #define HAS_GPS 1 #define GPS_BAUDRATE 9600 #define GPS_THREAD_INTERVAL 50 #define PIN_SERIAL1_RX GPS_RX_PIN #define PIN_SERIAL1_TX GPS_TX_PIN #define PIN_GPS_STANDBY D0 // #define GPS_DEBUG // #define GPS_EN D18 // P1.05 #endif // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // On-board QSPI Flash // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // On-board QSPI Flash #define PIN_QSPI_SCK (21) #define PIN_QSPI_CS (22) #define PIN_QSPI_IO0 (23) #define PIN_QSPI_IO1 (24) #define PIN_QSPI_IO2 (25) #define PIN_QSPI_IO3 (26) #define EXTERNAL_FLASH_DEVICES P25Q16H #define EXTERNAL_FLASH_USE_QSPI // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // Buzzer // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // Buzzer #define PIN_BUZZER D12 // P1.00, pwm output // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // joystick // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ #define CANNED_MESSAGE_ADD_CONFIRMATION 1 // trackball #define HAS_TRACKBALL 1 #define TB_UP 25 #define TB_DOWN 26 #define TB_LEFT 27 #define TB_RIGHT 28 #define TB_PRESS 29 #define TB_DIRECTION FALLING // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // Compatibility Definitions // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ #ifdef __cplusplus extern "C" { #endif // Serial port placeholders #define PIN_SERIAL2_RX (-1) #define PIN_SERIAL2_TX (-1) #ifdef __cplusplus } #endif #endif // _SEEED_SOLAR_NODE_H_ ================================================ FILE: variants/nrf52840/seeed_wio_tracker_L1_eink/nicheGraphics.h ================================================ #pragma once #include "configuration.h" #ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS // InkHUD-specific components // --------------------------- #include "graphics/niche/InkHUD/InkHUD.h" // Applets #include "graphics/niche/InkHUD/Applets/Examples/UserAppletInputExample/UserAppletInputExample.h" #include "graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h" #include "graphics/niche/InkHUD/Applets/User/DM/DMApplet.h" #include "graphics/niche/InkHUD/Applets/User/FavoritesMap/FavoritesMapApplet.h" #include "graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.h" #include "graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h" #include "graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.h" #include "graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h" // Shared NicheGraphics components // -------------------------------- #include "graphics/niche/Drivers/EInk/ZJY122250_0213BAAMFGN.h" #include "graphics/niche/Inputs/TwoButtonExtended.h" void setupNicheGraphics() { using namespace NicheGraphics; // SPI // ----------------------------- // For NRF52 platforms, SPI pins are defined in variant.h SPI1.begin(); // E-Ink Driver // ----------------------------- Drivers::EInk *driver = new Drivers::ZJY122250_0213BAAMFGN; driver->begin(&SPI1, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES); // InkHUD // ---------------------------- InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance(); // Set the E-Ink driver inkhud->setDriver(driver); // Set how many FAST updates per FULL update inkhud->setDisplayResilience(15); // Select fonts InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1252; InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; // Customize default settings inkhud->persistence->settings.rotation = 1; // 90 degrees clockwise #if HAS_TRACKBALL inkhud->persistence->settings.joystick.enabled = true; // Device uses a joystick inkhud->persistence->settings.joystick.alignment = 3; // 270 degrees inkhud->persistence->settings.optionalMenuItems.nextTile = false; // Use joystick instead #endif inkhud->persistence->settings.optionalFeatures.batteryIcon = true; // Device definitely has a battery inkhud->persistence->settings.userTiles.count = 1; // One tile only by default, keep things simple for new users inkhud->persistence->settings.userTiles.maxCount = 2; // Two applets side-by-side inkhud->persistence->settings.optionalFeatures.batteryIcon = true; // Pick applets // Note: order of applets determines priority of "auto-show" feature inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); // Activated, autoshown inkhud->addApplet("DMs", new InkHUD::DMApplet); // - inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); // - inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); // - inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); // - inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, no autoshow, default on tile 0 inkhud->addApplet("Favorites Map", new InkHUD::FavoritesMapApplet, false, false); // - // Start running InkHUD inkhud->begin(); // Buttons // -------------------------- Inputs::TwoButtonExtended *buttons = Inputs::TwoButtonExtended::getInstance(); // Shared NicheGraphics component #if HAS_TRACKBALL // #0: Exit Button buttons->setWiring(0, Inputs::TwoButtonExtended::getUserButtonPin()); buttons->setTiming(0, 75, 500); buttons->setHandlerShortPress(0, [inkhud]() { inkhud->exitShort(); }); buttons->setHandlerLongPress(0, [inkhud]() { inkhud->exitLong(); }); // #1: Joystick Center buttons->setWiring(1, TB_PRESS); buttons->setTiming(1, 75, 500); buttons->setHandlerShortPress(1, [inkhud]() { inkhud->shortpress(); }); buttons->setHandlerLongPress(1, [inkhud]() { inkhud->longpress(); }); // Joystick Directions buttons->setJoystickWiring(TB_UP, TB_DOWN, TB_LEFT, TB_RIGHT); buttons->setJoystickDebounce(50); buttons->setJoystickPressHandlers([inkhud]() { inkhud->navUp(); }, [inkhud]() { inkhud->navDown(); }, [inkhud]() { inkhud->navLeft(); }, [inkhud]() { inkhud->navRight(); }); #else // #0: User Button buttons->setWiring(0, Inputs::TwoButtonExtended::getUserButtonPin()); buttons->setTiming(0, 75, 500); buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); }); buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); #endif // Begin handling button events buttons->start(); } #endif ================================================ FILE: variants/nrf52840/seeed_wio_tracker_L1_eink/platformio.ini ================================================ [env:seeed_wio_tracker_L1_eink] custom_meshtastic_hw_model = 100 custom_meshtastic_hw_model_slug = SEEED_WIO_TRACKER_L1_EINK custom_meshtastic_architecture = nrf52840 custom_meshtastic_actively_supported = true custom_meshtastic_support_level = 1 custom_meshtastic_display_name = Seeed Wio Tracker L1 E-Ink custom_meshtastic_images = wio_tracker_l1_eink.svg custom_meshtastic_tags = Seeed board = seeed_wio_tracker_L1 extends = nrf52840_base ;board_level = extra build_flags = ${nrf52840_base.build_flags} -I variants/nrf52840/seeed_wio_tracker_L1_eink -D SEEED_WIO_TRACKER_L1_EINK -D SEEED_WIO_TRACKER_L1 -I src/platform/nrf52/softdevice -I src/platform/nrf52/softdevice/nrf52 -DUSE_EINK -DEINK_DISPLAY_MODEL=GxEPD2_213_B74 -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 board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/seeed_wio_tracker_L1_eink> lib_deps = ${nrf52840_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master https://github.com/meshtastic/GxEPD2/archive/c7eb4c3c167cf396ef4f541cc5d4c6aa42f3c46b.zip debug_tool = jlink [env:seeed_wio_tracker_L1_eink-inkhud] board = seeed_wio_tracker_L1 extends = nrf52840_base, inkhud build_flags = ${nrf52840_base.build_flags} ${inkhud.build_flags} -I variants/nrf52840/seeed_wio_tracker_L1_eink -D SEEED_WIO_TRACKER_L1 -D BUTTON_PIN=D13 board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld build_src_filter = ${nrf52_base.build_src_filter} ${inkhud.build_src_filter} +<../variants/nrf52840/seeed_wio_tracker_L1_eink> lib_deps = ${inkhud.lib_deps} ; Before base libs_deps, so we use ZinggJM/GFXRoot instead of AdafruitGFX (saves space) ${nrf52840_base.lib_deps} debug_tool = jlink ================================================ FILE: variants/nrf52840/seeed_wio_tracker_L1_eink/variant.cpp ================================================ /* * variant.cpp - Digital pin mapping for TRACKER L1 * * This file defines the pin mapping array that maps logical digital pins (D0-D17) * to physical GPIO ports/pins on the Nordic nRF52 series microcontroller. * * Board: [Seeed Studio WIO TRACKER L1] * Hardware Features: * - LoRa module (CS/SCK/MISO/MOSI control pins) * - GNSS module (TX/RX/Reset/Wakeup) * - User LEDs (D11-D12) * - User button (D13) * - Grove/NFC interface (D14-D15) * - Battery voltage monitoring (D16) * * Created [20250521] * By [Dylan] */ #include "variant.h" #include "nrf.h" #include "wiring_constants.h" #include "wiring_digital.h" /** * @brief Digital pin to GPIO port/pin mapping table * * Format: Logical Pin (Dx) -> nRF Port.Pin (Px.xx) * */ extern "C" { const uint32_t g_ADigitalPinMap[] = { // D0 .. D10 - Peripheral control pins 41, // D0 P1.09 GNSS_WAKEUP 7, // D1 P0.07 LORA_DIO1 39, // D2 P1.07 LORA_RESET 42, // D3 P1.10 LORA_BUSY 46, // D4 P1.14 (A4/SDA) LORA_CS 40, // D5 P1.08 (A5/SCL) LORA_SW 27, // D6 P0.27 (UART_TX) GNSS_TX 26, // D7 P0.26 (UART_RX) GNSS_RX 30, // D8 P0.30 (SPI_SCK) LORA_SCK 3, // D9 P0.3 (SPI_MISO) LORA_MISO 28, // D10 P0.28 (SPI_MOSI) LORA_MOSI // D11-D12 - LED outputs 33, // D11 P1.1 User LED // Buzzer 32, // D12 P1.0 Buzzer // D13 - User input 8, // D13 P0.08 User Button // D14-D15 - Grove interface 6, // D14 P0.06 OLED SDA 5, // D15 P0.05 OLED SCL // D16 - Battery voltage ADC input 31, // D16 P0.31 VBAT_ADC // GROVE 43, // D17 P0.00 GROVESDA 44, // D18 P0.01 GROVESCL // FLASH 21, // D19 P0.21 (QSPI_SCK) 25, // D20 P0.25 (QSPI_CSN) 20, // D21 P0.20 (QSPI_SIO_0 DI) 24, // D22 P0.24 (QSPI_SIO_1 DO) 22, // D23 P0.22 (QSPI_SIO_2 WP) 23, // D24 P0.23 (QSPI_SIO_3 HOLD) 36, // D25 TB_UP 12, // D26 TB_DOWN 11, // D27 TB_LEFT 35, // D28 TB_RIGHT 37, // D29 TB_PRESS 4, // D30 BAT_CTL 13, // D31 EINK_SCK 14, // D32 EINK_RST 15, // D33 EINK_MOSI 16, // D34 EINK_DC 17, // D35 EINK_BUSY 19, // D36 EINK_CS }; } void initVariant() { pinMode(PIN_QSPI_CS, OUTPUT); digitalWrite(PIN_QSPI_CS, HIGH); // This setup is crucial for ensuring low power consumption and proper initialization of the hardware components. // VBAT_ENABLE pinMode(BAT_READ, OUTPUT); digitalWrite(BAT_READ, HIGH); pinMode(PIN_LED1, OUTPUT); digitalWrite(PIN_LED1, LOW); pinMode(PIN_LED2, OUTPUT); digitalWrite(PIN_LED2, LOW); } ================================================ FILE: variants/nrf52840/seeed_wio_tracker_L1_eink/variant.h ================================================ #ifndef _SEEED_TRACKER_L1_H_ #define _SEEED_TRACKER_L1_H_ #include "WVariant.h" // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // Clock Configuration // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ #define VARIANT_MCK (64000000ul) // Master clock frequency #define USE_LFXO // 32.768kHz crystal for LFCLK // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // Pin Capacity Definitions // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ #define PINS_COUNT (38u) // Total GPIO pins #define NUM_DIGITAL_PINS (38u) // Digital I/O pins #define NUM_ANALOG_INPUTS (8u) // Analog inputs (A0-A5 + VBAT + AREF) #define NUM_ANALOG_OUTPUTS (0u) // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // LED Configuration // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // LEDs // LEDs #define PIN_LED1 (11) // LED P1.15 #define PIN_LED2 (12) // #define LED_GREEN PIN_LED1 #define LED_BLUE PIN_LED2 #define LED_STATE_ON 1 // State when LED is lit // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // Button Configuration // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ #define CANCEL_BUTTON_PIN D13 // This is the Program Button // #define BUTTON_NEED_PULLUP 1 #define CANCEL_BUTTON_ACTIVE_LOW true #define CANCEL_BUTTON_ACTIVE_PULLUP false // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // Digital Pin Mapping (D0-D10) // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ #define D0 0 // P1.06 GNSS_WAKEUP/IO0 #define D1 1 // P0.07 LORA_DIO1 #define D2 2 // P1.07 LORA_RESET #define D3 3 // P1.10 LORA_BUSY #define D4 4 // P1.14 LORA_CS #define D5 5 // P1.08 LORA_SW #define D6 6 // P0.27 GNSS_TX #define D7 7 // P0.26 GNSS_RX #define D8 8 // P0.30 SPI_SCK #define D9 9 // P0.03 SPI_MISO #define D10 10 // P0.28 SPI_MOSI #define D12 12 // P1.00 Buzzer #define D13 13 // P0.08 User Button #define D14 14 // P0.05 OLED SCL #define D15 15 // P0.06 OLED SDA #define D16 16 // P0.31 VBAT_ADC #define D17 17 // P0.00 GROVE SDA #define D18 18 // P0.01 GROVE_SCL // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // Analog Pin Definitions // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ #define PIN_A0 0 // P0.02 Analog Input 0 #define PIN_A1 1 // P0.03 Analog Input 1 #define PIN_A2 2 // P0.28 Analog Input 2 #define PIN_A3 3 // P0.29 Analog Input 3 #define PIN_A4 4 // P0.04 Analog Input 4 #define PIN_A5 5 // P0.05 Analog Input 5 #define PIN_VBAT D16 // P0.31 Battery voltage sense // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // Communication Interfaces // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // I2C Configuration #define HAS_WIRE 1 #define PIN_WIRE_SDA D18 // P0.09 #define PIN_WIRE_SCL D17 // P0.10 #define WIRE_INTERFACES_COUNT 1 static const uint8_t SDA = PIN_WIRE_SDA; static const uint8_t SCL = PIN_WIRE_SCL; // SPI Configuration (SX1262) // #define SPI_INTERFACES_COUNT 1 #define PIN_SPI_MISO 9 // P0.03 (D9) #define PIN_SPI_MOSI 10 // P0.28 (D10) #define PIN_SPI_SCK 8 // P0.30 (D8) // SX1262 LoRa Module Pins #define USE_SX1262 #define SX126X_CS D4 // Chip select #define SX126X_DIO1 D1 // Digital IO 1 (Interrupt) #define SX126X_BUSY D3 // Busy status #define SX126X_RESET D2 // Reset control #define SX126X_DIO3_TCXO_VOLTAGE 1.8 // TCXO supply voltage #define SX126X_RXEN D5 // RX enable control #define SX126X_TXEN RADIOLIB_NC #define SX126X_DIO2_AS_RF_SWITCH // This Line is really necessary for SX1262 to work with RF switch or will loss TX power // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // EINK // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ #define SPI_INTERFACES_COUNT 2 #define PIN_EINK_CS 36 #define PIN_EINK_BUSY 35 #define PIN_EINK_DC 34 #define PIN_EINK_RES 32 #define PIN_EINK_SCLK 31 #define PIN_EINK_MOSI 33 #define PIN_EINK_EN 14 // unused #define PIN_SPI1_MISO -1 // 15 unused #define PIN_SPI1_MOSI PIN_EINK_MOSI #define PIN_SPI1_SCK PIN_EINK_SCLK // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // Power Management // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ #define BAT_READ 30 // D30 = P0.04 Reads battery voltage from divider on signal board. #define BATTERY_SENSE_RESOLUTION_BITS 12 #define ADC_MULTIPLIER 2.0 #define BATTERY_PIN PIN_VBAT // PIN_A7 #define AREF_VOLTAGE 3.6 #define OCV_ARRAY 4200, 3876, 3826, 3763, 3713, 3660, 3573, 3485, 3422, 3359, 3300 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // GPS L76KB // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ #define GPS_L76K #ifdef GPS_L76K #define GPS_TX_PIN D6 // P0.26 - This is data from the MCU #define GPS_RX_PIN D7 // P0.27 - This is data from the GNSS #define HAS_GPS 1 #define GPS_BAUDRATE 9600 #define GPS_THREAD_INTERVAL 50 #define PIN_SERIAL1_RX GPS_RX_PIN #define PIN_SERIAL1_TX GPS_TX_PIN #define PIN_GPS_STANDBY D0 // #define GPS_DEBUG // #define GPS_EN D18 // P1.05 #endif // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // On-board QSPI Flash // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // On-board QSPI Flash #define PIN_QSPI_SCK (21) #define PIN_QSPI_CS (22) #define PIN_QSPI_IO0 (23) #define PIN_QSPI_IO1 (24) #define PIN_QSPI_IO2 (25) #define PIN_QSPI_IO3 (26) #define EXTERNAL_FLASH_DEVICES P25Q16H #define EXTERNAL_FLASH_USE_QSPI // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // Buzzer // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // Buzzer #define PIN_BUZZER D12 // P1.00, pwm output // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // joystick // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // trackball #define HAS_TRACKBALL 1 #define TB_UP 25 #define TB_DOWN 26 #define TB_LEFT 27 #define TB_RIGHT 28 #define TB_PRESS 29 #define TB_DIRECTION FALLING #define CANNED_MESSAGE_ADD_CONFIRMATION 1 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // Compatibility Definitions // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ #ifdef __cplusplus extern "C" { #endif // Serial port placeholders #define PIN_SERIAL2_RX (-1) #define PIN_SERIAL2_TX (-1) #ifdef __cplusplus } #endif #endif // _SEEED_TRACKER_L1_H_ ================================================ FILE: variants/nrf52840/seeed_xiao_nrf52840_kit/platformio.ini ================================================ ; Seeed Xiao BLE: https://wiki.seeedstudio.com/XIAO_BLE/ [env:seeed_xiao_nrf52840_kit] custom_meshtastic_hw_model = 88 custom_meshtastic_hw_model_slug = XIAO_NRF52_KIT custom_meshtastic_architecture = nrf52840 custom_meshtastic_actively_supported = true custom_meshtastic_support_level = 1 custom_meshtastic_display_name = Seeed Xiao NRF52840 Kit custom_meshtastic_images = seeed_xiao_nrf52_kit.svg custom_meshtastic_tags = Seeed extends = nrf52840_base board = xiao_ble_sense board_level = pr build_flags = ${nrf52840_base.build_flags} -I variants/nrf52840/seeed_xiao_nrf52840_kit -I src/platform/nrf52/softdevice -I src/platform/nrf52/softdevice/nrf52 -DSEEED_XIAO_NRF52840_KIT -DSEEED_XIAO_NRF_KIT_DEFAULT board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/seeed_xiao_nrf52840_kit> 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 ; Seeed Xiao BLE but with GPS moved to NFC pins, and therefore i2c active [env:seeed_xiao_nrf52840_kit_i2c] extends = env:seeed_xiao_nrf52840_kit board_level = extra build_flags = ${env:seeed_xiao_nrf52840_kit.build_flags} -DSEEED_XIAO_NRF52840_KIT -DSEEED_XIAO_NRF_KIT_I2C ; Define I2C variant -USEEED_XIAO_NRF_KIT_DEFAULT ; Remove default define ================================================ FILE: variants/nrf52840/seeed_xiao_nrf52840_kit/variant.cpp ================================================ #include "variant.h" #include "configuration.h" #include "nrf.h" #include "wiring_constants.h" #include "wiring_digital.h" #include #include #include #include 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, // D19 is P1.10 (MIC_PWR) 32, // D20 is P1.00 (PDM_CLK) 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) }; /* 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 */ void initVariant() { // Set BQ25101 ISET to 100mA instead of 50mA pinMode(HICHG, OUTPUT); digitalWrite(HICHG, LOW); // LEDs pinMode(PIN_LED1, OUTPUT); ledOff(PIN_LED1); pinMode(PIN_LED2, OUTPUT); ledOff(PIN_LED2); pinMode(PIN_LED3, OUTPUT); ledOff(PIN_LED3); } ================================================ FILE: variants/nrf52840/seeed_xiao_nrf52840_kit/variant.h ================================================ #ifndef _SEEED_XIAO_NRF52840_KIT_H_ #define _SEEED_XIAO_NRF52840_KIT_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 /* Xiao pin assignments | Pin | Default | I2C | BTB | BLE-L | | Pin | Default | I2C | BTB | BLE-L | | ----- | -------- | ---- | ---- | ----- | --- | ----- | ------- | ---- | ---- | ----- | | | | | | | | | | | | | | D0 | G_STBY | UBTN | DIO1 | CS | | 5v | | | | | | D1 | DIO1 | DIO1 | Busy | DIO1 | | GND | | | | | | D2 | NRST | NRST | NRST | Busy | | 3v3 | | | | | | D3 | Busy | Busy | CS | NRST | | D10 | MOSI | MOSI | MOSI | MOSI | | D4 | CS | CS | RXEN | SDA | | D9 | MISO | MISO | MISO | MISO | | D5 | RXEN | RXEN | | SCL | | D8 | SCK | SCK | SCK | SCK | | D6 | G_TX | SDA | G_TX | | | D7 | G_RX | SCL | G_RX | RXEN | | | | | | | | | | | | | | | End | | | | | | | | | | | NFC1/ | SDA | G_TX | SDA | G_TX | | NFC2/ | SCL | G_RX | SCL | G_RX | | D30 | | | | | | D31 | | | | | | | | | | | | | | | | | | | Internal | | | | | | | | | | | D16 | SCL1 | SCL1 | SCL1 | SCL1 | | | | | | | | D17 | SDA1 | SDA1 | SDA1 | SDA1 | | | | | | | The default column shows the pin assignments for the Wio-SX1262 for XIAO (standalone SKU 113010003 or nRF52840 kit SKU 102010710). The I2C column shows an alternative pin assignment using I2C on D6/D7 in place of the GNSS. The BTB column shows the pin assignment for the Wio-SX1262 -30-pin board-to-board connector version from the ESP32S3 kit. The BLE-L column shows the pin assignment for the original DIY xiao_ble, and which is retained for legacy users. Note that the in addition to the difference between the default and the I2C pinouts in placing the pins on NFC or D6/D7, the user button is activated on D0. The button conflicts with the official GNSS module, so caution is advised. */ #define PINS_COUNT (33) #define NUM_DIGITAL_PINS (33) #define NUM_ANALOG_INPUTS (8) #define NUM_ANALOG_OUTPUTS (0) /* * 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 (32) #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 /* * LEDs */ #define LED_STATE_ON (0) // RGB LED is common anode #define LED_RED (11) #define LED_GREEN (13) #define LED_BLUE (12) #define PIN_LED1 LED_GREEN // PIN_LED1 is used in src/platform/nrf52/architecture.h to define LED_POWER #define PIN_LED2 LED_BLUE #define PIN_LED3 LED_RED /* * Buttons */ /* * D0 is shared with PIN_GPS_STANDBY on the L76K GNSS Module, so refer to * GPS_L76K definition preventing this conflict */ // #define BUTTON_PIN D0 /* * Serial Interfaces */ #define PIN_SERIAL2_RX (-1) #define PIN_SERIAL2_TX (-1) /* * Pinout for SX126x */ #define USE_SX1262 #if defined(XIAO_BLE_LEGACY_PINOUT) // Legacy xiao_ble variant pinout for third-party SX126x modules e.g. EBYTE E22 #define SX126X_CS D0 #define SX126X_DIO1 D1 #define SX126X_BUSY D2 #define SX126X_RESET D3 #define SX126X_RXEN D7 #else #if defined(SEEED_XIAO_NRF_WIO_BTB) // Wio-SX1262 for XIAO with 30-pin board-to-board connector // https://files.seeedstudio.com/products/SenseCAP/Wio_SX1262/Schematic_Diagram_Wio-SX1262_for_XIAO.pdf #define SX126X_CS D3 #define SX126X_DIO1 D0 #define SX126X_BUSY D1 #define SX126X_RESET D2 #define SX126X_RXEN D4 #else // Wio-SX1262 for XIAO (standalone SKU 113010003 or nRF52840 kit SKU 102010710) // Same for both default and I2C pinouts // https://files.seeedstudio.com/products/SenseCAP/Wio_SX1262/Wio-SX1262%20for%20XIAO%20V1.0_SCH.pdf #define SX126X_CS D4 #define SX126X_DIO1 D1 #define SX126X_BUSY D3 #define SX126X_RESET D2 #define SX126X_RXEN D5 #endif // defined(SEEED_XIAO_NRF_WIO_BTB) #endif // defined(XIAO_BLE_LEGACY_PINOUT) // Common pinouts for all SX126x pinouts above #define SX126X_TXEN RADIOLIB_NC #define SX126X_DIO2_AS_RF_SWITCH // DIO2 is used to control the TX side of the RF switch #define SX126X_DIO3_TCXO_VOLTAGE 1.8 /* * SPI Interfaces * Defined after pinout for SX1262x to factor in CS pinout variations */ #define SPI_INTERFACES_COUNT 1 #define PIN_SPI_MISO D9 #define PIN_SPI_MOSI D10 #define PIN_SPI_SCK D8 static const uint8_t SS = SX126X_CS; static const uint8_t MOSI = PIN_SPI_MOSI; static const uint8_t MISO = PIN_SPI_MISO; static const uint8_t SCK = PIN_SPI_SCK; /* * GPS */ // GPS L76K // Default GPS L76K #if defined(SEEED_XIAO_NRF_KIT_DEFAULT) || defined(SEEED_XIAO_NRF_WIO_BTB) #define GPS_L76K #define GPS_TX_PIN D6 // This is data from the MCU #define GPS_RX_PIN D7 // This is data from the GNSS module #if defined(SEEED_XIAO_NRF_KIT_DEFAULT) #define PIN_GPS_STANDBY D0 // this is where the conflicting pinouts come from #endif // I2C and BLE-Legacy put them on the NFC pins #else #define GPS_TX_PIN (30) #define GPS_RX_PIN (31) #endif #define HAS_GPS 1 #define GPS_BAUDRATE 9600 #define GPS_THREAD_INTERVAL 50 #define PIN_SERIAL1_TX GPS_TX_PIN #define PIN_SERIAL1_RX GPS_RX_PIN /* * Battery */ #define BATTERY_PIN PIN_VBAT // P0.31: VBAT voltage divider #define ADC_MULTIPLIER (3) // ... R17=1M, R18=510k #define ADC_CTRL VBAT_ENABLE // P0.14: VBAT voltage divider #define ADC_CTRL_ENABLED LOW // ... sink #define EXT_CHRG_DETECT (23) // P0.17: Charge LED #define EXT_CHRG_DETECT_VALUE LOW // ... BQ25101 ~CHG indicates charging #define HICHG (22) // P0.13: BQ25101 ISET 100mA instead of 50mA #define BATTERY_SENSE_RESOLUTION_BITS (10) /* * Wire Interfaces * Keep this section after potentially conflicting pin definitions */ #define I2C_NO_RESCAN // I2C is a bit finicky, don't scan too much #define WIRE_INTERFACES_COUNT 1 // changed to 1 for now, as LSM6DS3TR has issues. #if defined(XIAO_BLE_LEGACY_PINOUT) // Used for I2C by DIY xiao_ble variant #define PIN_WIRE_SDA D4 #define PIN_WIRE_SCL D5 #else // Put the I2C pins on the NFC pins by default #if defined(SEEED_XIAO_NRF_KIT_DEFAULT) || defined(SEEED_XIAO_NRF_WIO_BTB) #define PIN_WIRE_SDA 30 #define PIN_WIRE_SCL 31 #else // If not on legacy or defauly, we're wanting I2C on the back pins #define PIN_WIRE_SDA D6 #define PIN_WIRE_SCL D7 #endif // defined(SEEED_XIAO_NRF_KIT_DEFAULT) || defined(SEEED_XIAO_NRF_WIO_BTB) #endif // defined(XIAO_BLE_LEGACY_PINOUT) // // Internal LSM6DS3TR on XIAO nRF52840 Series - put it on wire1 // // Note: disabled for now, as there are some issues with the LSM. // #define PIN_WIRE1_SDA (17) // #define PIN_WIRE1_SCL (16) static const uint8_t SDA = PIN_WIRE_SDA; // Not sure if this is needed static const uint8_t SCL = PIN_WIRE_SCL; // Not sure if this is needed // // 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 /* * Buttons * Keep this section after potentially conflicting pin definitions * because D0 has multiple possible conflicts with various XIAO modules: */ #if defined(SEEED_XIAO_NRF_KIT_I2C) #define BUTTON_PIN D0 #endif #if defined(SEEED_XIAO_NRF_WIO_BTB) #define BUTTON_PIN D5 #endif #ifdef __cplusplus } #endif /*---------------------------------------------------------------------------- * Arduino objects - C++ only *----------------------------------------------------------------------------*/ #endif ================================================ FILE: variants/nrf52840/t-echo/nicheGraphics.h ================================================ #pragma once #include "configuration.h" #ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS // InkHUD-specific components // --------------------------- #include "graphics/niche/InkHUD/InkHUD.h" // Applets #include "graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h" #include "graphics/niche/InkHUD/Applets/User/DM/DMApplet.h" #include "graphics/niche/InkHUD/Applets/User/FavoritesMap/FavoritesMapApplet.h" #include "graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.h" #include "graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h" #include "graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.h" #include "graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h" // Shared NicheGraphics components // -------------------------------- #include "graphics/niche/Drivers/Backlight/LatchingBacklight.h" #include "graphics/niche/Drivers/EInk/GDEY0154D67.h" #include "graphics/niche/Inputs/TwoButton.h" // Special case - fix T-Echo's touch button // ---------------------------------------- // On a handful of T-Echos, LoRa TX triggers the capacitive touch // To avoid this, we lockout the button during TX #include "mesh/RadioLibInterface.h" void setupNicheGraphics() { using namespace NicheGraphics; // SPI // ----------------------------- // For NRF52 platforms, SPI pins are defined in variant.h SPI1.begin(); // E-Ink Driver // ----------------------------- Drivers::EInk *driver = new Drivers::GDEY0154D67; driver->begin(&SPI1, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES); // InkHUD // ---------------------------- InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance(); // Set the E-Ink driver inkhud->setDriver(driver); // Set how many FAST updates per FULL update // Set how unhealthy additional FAST updates beyond this number are inkhud->setDisplayResilience(20, 1.5); // Select fonts InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1252; InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; // Customize default settings inkhud->persistence->settings.userTiles.maxCount = 2; // Two applets side-by-side inkhud->persistence->settings.rotation = 3; // 270 degrees clockwise inkhud->persistence->settings.optionalFeatures.batteryIcon = true; // Device definitely has a battery inkhud->persistence->settings.optionalMenuItems.backlight = true; // Until proves capacitive button works by touching it // Setup backlight controller // Note: AUX button attached further down Drivers::LatchingBacklight *backlight = Drivers::LatchingBacklight::getInstance(); backlight->setPin(PIN_EINK_EN); // Pick applets // Note: order of applets determines priority of "auto-show" feature inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); // Activated, autoshown inkhud->addApplet("DMs", new InkHUD::DMApplet); // - inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); // - inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); // - inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated inkhud->addApplet("Favorites Map", new InkHUD::FavoritesMapApplet); // - inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); // - inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, no autoshow, default on tile 0 // Start running InkHUD inkhud->begin(); // Buttons // -------------------------- Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // Shared NicheGraphics component // #0: Main User Button buttons->setWiring(0, Inputs::TwoButton::getUserButtonPin()); buttons->setTiming(0, 75, 500); buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); }); buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); // #1: Aux Button (Capacitive Touch Button) // - short: momentary backlight // - long: latch backlight on buttons->setWiring(1, PIN_BUTTON_TOUCH); buttons->setTiming(1, 50, 5000); // 5 seconds before latch - limited by T-Echo's capacitive touch IC buttons->setHandlerDown(1, [inkhud, backlight]() { // Discard the button press if radio is active // Rare hardware fault: LoRa activity triggers touch button if (!RadioLibInterface::instance || RadioLibInterface::instance->isSending()) return; // Backlight on (while held) backlight->peek(); // Handler has run, which confirms touch button wasn't removed as part of DIY build. // No longer need the fallback backlight toggle in menu. inkhud->persistence->settings.optionalMenuItems.backlight = false; }); buttons->setHandlerLongPress(1, [backlight]() { backlight->latch(); }); buttons->setHandlerShortPress(1, [backlight]() { backlight->off(); }); // Begin handling button events buttons->start(); } #endif ================================================ FILE: variants/nrf52840/t-echo/platformio.ini ================================================ ; Using original screen class [env:t-echo] custom_meshtastic_hw_model = 7 custom_meshtastic_hw_model_slug = T_ECHO custom_meshtastic_architecture = nrf52840 custom_meshtastic_actively_supported = true custom_meshtastic_support_level = 1 custom_meshtastic_display_name = LILYGO T-Echo custom_meshtastic_images = t-echo.svg custom_meshtastic_tags = LilyGo extends = nrf52840_base board = t-echo board_level = pr 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/nrf52840/t-echo -DEINK_DISPLAY_MODEL=GxEPD2_154_D67 -DEINK_WIDTH=200 -DEINK_HEIGHT=200 -DUSE_EINK -DUSE_EINK_DYNAMICDISPLAY ; Enable Dynamic EInk -DEINK_LIMIT_FASTREFRESH=20 ; How many consecutive fast-refreshes are permitted -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/nrf52840/t-echo> lib_deps = ${nrf52840_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master https://github.com/meshtastic/GxEPD2/archive/c7eb4c3c167cf396ef4f541cc5d4c6aa42f3c46b.zip # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib lewisxhe/SensorLib@0.3.4 ;upload_protocol = fs [env:t-echo-inkhud] extends = nrf52840_base, inkhud board = t-echo board_level = pr board_check = true debug_tool = jlink build_flags = ${nrf52840_base.build_flags} ${inkhud.build_flags} -I variants/nrf52840/t-echo build_src_filter = ${nrf52_base.build_src_filter} ${inkhud.build_src_filter} +<../variants/nrf52840/t-echo> lib_deps = ${inkhud.lib_deps} ; InkHUD libs first, so we get GFXRoot instead of AdafruitGFX ${nrf52840_base.lib_deps} # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib lewisxhe/SensorLib@0.3.4 ================================================ FILE: variants/nrf52840/t-echo/variant.cpp ================================================ /* 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); } void variant_shutdown() { // To power off the T-Echo, the display must be set // as an input pin; otherwise, there will be leakage current. pinMode(PIN_EINK_CS, INPUT); pinMode(PIN_EINK_DC, INPUT); pinMode(PIN_EINK_RES, INPUT); pinMode(PIN_EINK_BUSY, INPUT); } ================================================ FILE: variants/nrf52840/t-echo/variant.h ================================================ /* 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_STATE_ON 0 // State when LED is lit /* * Buttons */ #define PIN_BUTTON1 (32 + 10) #define BUTTON_ACTIVE_LOW true #define BUTTON_ACTIVE_PULLUP true #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_TOUCH_ACTIVE_LOW true #define BUTTON_TOUCH_ACTIVE_PULLUP true #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 */ #define SERIAL_PRINT_PORT 0 /* 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) /* 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 #define TCXO_OPTIONAL // Internally the TTGO module hooks the SX1262-DIO2 in to control the TX/RX switch (which is the default for the sx1262interface // code) // #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_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 + 8) // This is for bits going TOWARDS the CPU #define GPS_RX_PIN (32 + 9) // This is for bits going TOWARDS the GPS #define GPS_THREAD_INTERVAL 50 #define PIN_SERIAL1_RX GPS_RX_PIN #define PIN_SERIAL1_TX GPS_TX_PIN // PCF8563 RTC Module #define PIN_RTC_INT (0 + 16) // Interrupt from the PCF8563 RTC #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) // 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 NO_EXT_GPIO 1 #ifdef __cplusplus } #endif /*---------------------------------------------------------------------------- * Arduino objects - C++ only *----------------------------------------------------------------------------*/ #endif ================================================ FILE: variants/nrf52840/t-echo-lite/platformio.ini ================================================ ; Using original screen class [env:t-echo-lite] custom_meshtastic_hw_model = 109 custom_meshtastic_hw_model_slug = T_ECHO_LITE custom_meshtastic_architecture = nrf52840 custom_meshtastic_actively_supported = false custom_meshtastic_support_level = 1 custom_meshtastic_display_name = LILYGO T-Echo Lite custom_meshtastic_images = techo_lite.svg custom_meshtastic_tags = LilyGo 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/nrf52840/t-echo-lite -D T_ECHO_LITE -D EINK_DISPLAY_MODEL=GxEPD2_122_T61 -D EINK_WIDTH=192 -D EINK_HEIGHT=176 -D USE_EINK -D USE_EINK_DYNAMICDISPLAY ; Enable Dynamic EInk -D EINK_LIMIT_FASTREFRESH=20 ; How many consecutive fast-refreshes are permitted -D EINK_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/nrf52840/t-echo-lite> lib_deps = ${nrf52840_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master https://github.com/meshtastic/GxEPD2/archive/c7eb4c3c167cf396ef4f541cc5d4c6aa42f3c46b.zip ;upload_protocol = fs ================================================ FILE: variants/nrf52840/t-echo-lite/variant.cpp ================================================ /* 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); } ================================================ FILE: variants/nrf52840/t-echo-lite/variant.h ================================================ /* 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 // 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 + 7) // Green LED #define PIN_LED2 (32 + 5) // Blue LED // Unused(by firmware) LEDs: #define PIN_LED3 (32 + 14) // Red LED inside, under the display. #define LED_RED PIN_LED3 #define LED_BLUE PIN_LED2 #define LED_GREEN PIN_LED1 #define BLE_LED LED_BLUE #define LED_STATE_ON 0 // State when LED is lit // Buttons #define PIN_BUTTON1 (0 + 24) #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 BUTTON_CLICK_MS 400 // Analog pins #define PIN_A0 (0 + 2) // Battery ADC #define BATTERY_PIN PIN_A0 static const uint8_t A0 = PIN_A0; #define ADC_RESOLUTION 14 #define ADC_CTRL (0 + 31) #define ADC_CTRL_ENABLED HIGH // NFC #define PIN_NFC1 (9) #define PIN_NFC2 (10) // Wire Interfaces #define WIRE_INTERFACES_COUNT 1 #define PIN_WIRE_SDA (32 + 4) #define PIN_WIRE_SCL (32 + 2) /* Internal, PCB PAD interrupt PIN. Currently not used. (Not built in my device) */ // #define PIN_IMU_INT (0 + 16) // Interrupt from the IMU, macro name correct?! // External serial flash ZD25WQ32CEIGR // QSPI Pins #define PIN_QSPI_SCK (0 + 4) #define PIN_QSPI_CS (0 + 12) #define PIN_QSPI_IO0 (0 + 6) // MOSI if using two bit interface #define PIN_QSPI_IO1 (0 + 8) // MISO if using two bit interface #define PIN_QSPI_IO2 (32 + 9) // WP if using two bit interface (i.e. not used) #define PIN_QSPI_IO3 (0 + 26) // HOLD if using two bit interface (i.e. not used) // On-board QSPI Flash #define EXTERNAL_FLASH_DEVICES ZD25WQ32CEIGR #define EXTERNAL_FLASH_USE_QSPI // Lora radio #define USE_SX1262 // #define USE_SX1268 // currently only available with XS1262. #define SX126X_CS (0 + 11) #define SX126X_DIO1 (32 + 8) #define SX126X_DIO2 (0 + 5) #define SX126X_BUSY (0 + 14) #define SX126X_RESET (0 + 7) #define SX126X_RXEN (32 + 1) #define SX126X_TXEN (0 + 27) // #define TCXO_OPTIONAL #define SX126X_DIO3_TCXO_VOLTAGE 1.8 // eink display pins #define VEXT_ENABLE (32 + 12) #define VEXT_ON_VALUE LOW #define PIN_EINK_CS (0 + 22) #define PIN_EINK_BUSY (0 + 3) #define PIN_EINK_DC (0 + 21) #define PIN_EINK_RES (0 + 28) #define PIN_EINK_SCLK (0 + 19) #define PIN_EINK_MOSI (0 + 20) // Controls power 3V3 for all peripherals (eink + GPS + LoRa + Sensor) #define PIN_POWER_EN (0 + 30) // 3V3 POWER Enable #define PIN_SPI1_MISO (-1) // The display does not use MISO. #define PIN_SPI1_MOSI PIN_EINK_MOSI #define PIN_SPI1_SCK PIN_EINK_SCLK // GPS pins // #define GPS_DEBUG #define GPS_L76K #define GPS_BAUDRATE 9600 #define HAS_GPS 1 // #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_EN (32 + 11) // GPS power #define GPS_EN_ACTIVE 1 #define PIN_GPS_STANDBY (32 + 13) // wakeup pin #define PIN_GPS_PPS (32 + 15) #define GPS_TX_PIN (32 + 10) // L76K module RX PIN #define GPS_RX_PIN (0 + 29) // L76K module TX PIN #define GPS_THREAD_INTERVAL 50 #define PIN_SERIAL1_RX GPS_TX_PIN #define PIN_SERIAL1_TX GPS_RX_PIN // SPI Interfaces #define SPI_INTERFACES_COUNT 2 // For LORA, SPI 0 #define PIN_SPI_MISO (0 + 17) #define PIN_SPI_MOSI (0 + 15) #define PIN_SPI_SCK (0 + 13) // Battery // The battery sense is hooked to pin A0 (2) // it is defined in the analogue 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 SERIAL_PRINT_PORT 0 // #define NO_EXT_GPIO 1 // PINs back side // Batt & solar connector left up corner /* ------------------------------- | VDDH, VBAT, 0.23, SCL , 1.06 | | GND , SDA , 0.09, 0.10, 0.25 | ------------------------------- -------- | VDDH | | GND | | 1.13 | - Wake Up/standby | 1.15 | - PPS | 0.29 | - TX | 1.10 | - RX | 1.11 | - EN -------- ------------------------------- | 3V3 , GND , 0.16, 1.03, G_WU | 0.16 internal solder pad interrupt PIN, | G_EN, G_RX, G_TX, GND , PPS | ------------------------------- */ // To debug via the segger JLINK console rather than the CDC-ACM serial device // #define USE_SEGGER #ifdef __cplusplus } #endif /*---------------------------------------------------------------------------- * Arduino objects - C++ only *----------------------------------------------------------------------------*/ #endif ================================================ FILE: variants/nrf52840/t-echo-plus/nicheGraphics.h ================================================ #pragma once #include "configuration.h" #ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS #include "graphics/niche/Drivers/Backlight/LatchingBacklight.h" #include "graphics/niche/Drivers/EInk/GDEY0154D67.h" #include "graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h" #include "graphics/niche/InkHUD/Applets/User/DM/DMApplet.h" #include "graphics/niche/InkHUD/Applets/User/FavoritesMap/FavoritesMapApplet.h" #include "graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.h" #include "graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h" #include "graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.h" #include "graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h" #include "graphics/niche/InkHUD/InkHUD.h" #include "graphics/niche/Inputs/TwoButton.h" void setupNicheGraphics() { using namespace NicheGraphics; SPI1.begin(); Drivers::EInk *driver = new Drivers::GDEY0154D67; driver->begin(&SPI1, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES); InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance(); inkhud->setDriver(driver); inkhud->setDisplayResilience(20, 1.5); InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1252; InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; inkhud->persistence->settings.userTiles.maxCount = 2; inkhud->persistence->settings.rotation = 3; inkhud->persistence->settings.optionalFeatures.batteryIcon = true; inkhud->persistence->settings.optionalMenuItems.backlight = true; Drivers::LatchingBacklight *backlight = Drivers::LatchingBacklight::getInstance(); backlight->setPin(PIN_EINK_BL); inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); inkhud->addApplet("DMs", new InkHUD::DMApplet); inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); inkhud->addApplet("Favorites Map", new InkHUD::FavoritesMapApplet); inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); inkhud->begin(); Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); buttons->setWiring(0, Inputs::TwoButton::getUserButtonPin()); buttons->setTiming(0, 75, 500); buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); }); buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); buttons->setWiring(1, PIN_BUTTON_TOUCH); buttons->setTiming(1, 50, 5000); buttons->setHandlerDown(1, [inkhud, backlight]() { backlight->peek(); inkhud->persistence->settings.optionalMenuItems.backlight = false; }); buttons->setHandlerLongPress(1, [backlight]() { backlight->latch(); }); buttons->setHandlerShortPress(1, [backlight]() { backlight->off(); }); buttons->start(); } #endif ================================================ FILE: variants/nrf52840/t-echo-plus/platformio.ini ================================================ [env:t-echo-plus] extends = nrf52840_base board = t-echo board_level = pr board_check = true debug_tool = jlink build_flags = ${nrf52840_base.build_flags} -DTTGO_T_ECHO_PLUS -Ivariants/nrf52840/t-echo-plus -DEINK_DISPLAY_MODEL=GxEPD2_154_D67 -DEINK_WIDTH=200 -DEINK_HEIGHT=200 -DUSE_EINK -DUSE_EINK_DYNAMICDISPLAY ; Enable Dynamic EInk -DEINK_LIMIT_FASTREFRESH=20 ; How many consecutive fast-refreshes are permitted -DEINK_BACKGROUND_USES_FAST ; (Optional) Use FAST refresh for both BACKGROUND and RESPONSIVE, until a limit is reached. -DI2C_NO_RESCAN build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/t-echo-plus> lib_deps = ${nrf52840_base.lib_deps} https://github.com/meshtastic/GxEPD2/archive/55f618961db45a23eff0233546430f1e5a80f63a.zip lewisxhe/PCF8563_Library@^1.0.1 adafruit/Adafruit DRV2605 Library@1.2.4 ================================================ FILE: variants/nrf52840/t-echo-plus/variant.cpp ================================================ #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 (if populated) pinMode(PIN_LED1, OUTPUT); ledOff(PIN_LED1); pinMode(PIN_LED2, OUTPUT); ledOff(PIN_LED2); pinMode(PIN_LED3, OUTPUT); ledOff(PIN_LED3); } ================================================ FILE: variants/nrf52840/t-echo-plus/variant.h ================================================ #ifndef _VARIANT_T_ECHO_PLUS_ #define _VARIANT_T_ECHO_PLUS_ #define VARIANT_MCK (64000000ul) #define USE_LFXO #include "WVariant.h" #ifdef __cplusplus extern "C" { #endif // Pin counts #define PINS_COUNT (48) #define NUM_DIGITAL_PINS (48) #define NUM_ANALOG_INPUTS (1) #define NUM_ANALOG_OUTPUTS (0) // LEDs (not documented on pinmap; keep defaults for compatibility) #define PIN_LED1 (0 + 14) #define PIN_LED2 (0 + 15) #define PIN_LED3 (0 + 13) #define LED_RED PIN_LED3 #define LED_BLUE PIN_LED1 #define LED_GREEN PIN_LED2 #define LED_STATE_ON 0 // Buttons / touch #define PIN_BUTTON1 (32 + 10) #define BUTTON_ACTIVE_LOW true #define BUTTON_ACTIVE_PULLUP true #define PIN_BUTTON2 (0 + 18) // reset-labelled but usable as GPIO #define PIN_BUTTON_TOUCH (0 + 11) // capacitive touch #define BUTTON_TOUCH_ACTIVE_LOW true #define BUTTON_TOUCH_ACTIVE_PULLUP true #define BUTTON_CLICK_MS 400 #define BUTTON_TOUCH_MS 200 // Analog #define PIN_A0 (4) #define BATTERY_PIN PIN_A0 static const uint8_t A0 = PIN_A0; #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 (2.0F) // NFC #define PIN_NFC1 (9) #define PIN_NFC2 (10) // I2C (IMU BHI260AP, RTC, etc.) #define WIRE_INTERFACES_COUNT 1 #define PIN_WIRE_SDA (0 + 26) #define PIN_WIRE_SCL (0 + 27) #define HAS_BHI260AP #define TP_SER_IO (0 + 11) // RTC interrupt #define PIN_RTC_INT (0 + 16) // QSPI flash #define PIN_QSPI_SCK (32 + 14) #define PIN_QSPI_CS (32 + 15) #define PIN_QSPI_IO0 (32 + 12) #define PIN_QSPI_IO1 (32 + 13) #define PIN_QSPI_IO2 (0 + 7) #define PIN_QSPI_IO3 (0 + 5) // On-board QSPI Flash #define EXTERNAL_FLASH_DEVICES MX25R1635F #define EXTERNAL_FLASH_USE_QSPI // LoRa SX1262 #define USE_SX1262 #define USE_SX1268 #define SX126X_CS (0 + 24) #define SX126X_DIO1 (0 + 20) #define SX1262_DIO3 (0 + 21) #define SX126X_BUSY (0 + 17) #define SX126X_RESET (0 + 25) #define SX126X_DIO2_AS_RF_SWITCH #define SX126X_DIO3_TCXO_VOLTAGE 1.8 #define TCXO_OPTIONAL #define SPI_INTERFACES_COUNT 2 #define PIN_SPI_MISO (0 + 23) #define PIN_SPI_MOSI (0 + 22) #define PIN_SPI_SCK (0 + 19) // E-paper (1.54" per pinmap) // Alias PIN_EINK_EN to keep common eink power control code working #define PIN_EINK_BL (32 + 11) // backlight / panel power switch #define PIN_EINK_EN PIN_EINK_BL #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 // Power control #define PIN_POWER_EN (0 + 12) #define PIN_SPI1_MISO (32 + 7) // Placeholder MISO; keep off QSPI pins to avoid contention #define PIN_SPI1_MOSI PIN_EINK_MOSI #define PIN_SPI1_SCK PIN_EINK_SCLK // GPS (TX/RX/Wake/Reset/PPS per pinmap) #define GPS_L76K #define PIN_GPS_REINIT (32 + 5) // Reset #define PIN_GPS_STANDBY (32 + 2) // Wake #define PIN_GPS_PPS (32 + 4) #define GPS_TX_PIN (32 + 8) #define GPS_RX_PIN (32 + 9) #define GPS_THREAD_INTERVAL 50 #define PIN_SERIAL1_RX GPS_RX_PIN #define PIN_SERIAL1_TX GPS_TX_PIN // Sensors / accessories #define PIN_BUZZER (0 + 6) #define PIN_DRV_EN (0 + 8) #define HAS_DRV2605 1 #define SERIAL_PRINT_PORT 0 #ifdef __cplusplus } #endif #endif ================================================ FILE: variants/nrf52840/tracker-t1000-e/platformio.ini ================================================ [env:tracker-t1000-e] custom_meshtastic_support_level = 1 custom_meshtastic_images = tracker-t1000-e.svg custom_meshtastic_tags = Seeed extends = nrf52840_base board = tracker-t1000-e board_level = pr custom_meshtastic_hw_model = 71 custom_meshtastic_hw_model_slug = TRACKER_T1000_E custom_meshtastic_architecture = nrf52840 custom_meshtastic_display_name = Seeed SenseCAP T1000-E custom_meshtastic_actively_supported = true build_flags = ${nrf52840_base.build_flags} -Ivariants/nrf52840/tracker-t1000-e -Isrc/platform/nrf52/softdevice -Isrc/platform/nrf52/softdevice/nrf52 -DTRACKER_T1000_E -DMESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR_EXTERNAL=1 -DMESHTASTIC_EXCLUDE_CANNEDMESSAGES=1 -DMESHTASTIC_EXCLUDE_SCREEN=1 -DMESHTASTIC_EXCLUDE_DETECTIONSENSOR=1 -DMESHTASTIC_EXCLUDE_WIFI=1 board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/tracker-t1000-e> lib_deps = ${nrf52840_base.lib_deps} # TODO renovate https://github.com/meshtastic/QMA6100P_Arduino_Library/archive/14c900b8b2e4feaac5007a7e41e0c1b7f0841136.zip 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 ================================================ FILE: variants/nrf52840/tracker-t1000-e/rfswitch.h ================================================ #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, }; ================================================ FILE: variants/nrf52840/tracker-t1000-e/variant.cpp ================================================ /* 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(PIN_3V3_EN, OUTPUT); digitalWrite(PIN_3V3_EN, HIGH); pinMode(PIN_3V3_ACC_EN, OUTPUT); digitalWrite(PIN_3V3_ACC_EN, HIGH); pinMode(T1000X_SENSOR_EN_PIN, OUTPUT); digitalWrite(T1000X_SENSOR_EN_PIN, 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); } ================================================ FILE: variants/nrf52840/tracker-t1000-e/variant.h ================================================ /* 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_POWER PIN_LED1 #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 0x5 // enable input pull-down #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 GPS_PROBETRIES 5 #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 BATTERY_PIN 2 // P0.02/AIN0, BAT_ADC #define BATTERY_IMMUTABLE #define ADC_MULTIPLIER (2.0F) // P0.04 is sensor power enable, 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 #define OCV_ARRAY 4190, 4042, 3957, 3885, 3820, 3776, 3746, 3725, 3696, 3644, 3100 // 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_SENSOR_EN_PIN (0 + 4) // P0.4, Power to Sensor (GPIO, not ADC) #define T1000X_NTC_PIN (0 + 31) // P0.31/AIN7 #define T1000X_LUX_PIN (0 + 29) // P0.29/AIN5 #define HAS_SCREEN 0 // Enable Traffic Management Module for testing on T1000-E // NRF52840 has 256KB RAM - 1024 entries uses ~10KB #ifndef HAS_TRAFFIC_MANAGEMENT #define HAS_TRAFFIC_MANAGEMENT 1 #endif #ifndef TRAFFIC_MANAGEMENT_CACHE_SIZE #define TRAFFIC_MANAGEMENT_CACHE_SIZE 1024 #endif #ifdef __cplusplus } #endif /*---------------------------------------------------------------------------- * Arduino objects - C++ only *----------------------------------------------------------------------------*/ #endif // _VARIANT_TRACKER_T1000_E_ ================================================ FILE: variants/nrf52840/wio-sdk-wm1110/platformio.ini ================================================ ; 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 = ${nrf52840_base.extra_scripts} 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 = -Ofast -Og -ggdb3 -ggdb2 -g3 -g2 -g -g1 -g0 -DUSBCON -DUSE_TINYUSB build_flags = ${nrf52840_base.build_flags} -Ivariants/nrf52840/wio-sdk-wm1110 -Isrc/platform/nrf52/softdevice -Isrc/platform/nrf52/softdevice/nrf52 -DWIO_WM1110 -DCFG_TUD_CDC=0 board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/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 ================================================ FILE: variants/nrf52840/wio-sdk-wm1110/rfswitch.h ================================================ #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, }; ================================================ FILE: variants/nrf52840/wio-sdk-wm1110/variant.cpp ================================================ /* 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); } ================================================ FILE: variants/nrf52840/wio-sdk-wm1110/variant.h ================================================ /* 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_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_ ================================================ FILE: variants/nrf52840/wio-t1000-s/platformio.ini ================================================ ; 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/nrf52840/wio-t1000-s -Isrc/platform/nrf52/softdevice -Isrc/platform/nrf52/softdevice/nrf52 -DWIO_WM1110 board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/wio-t1000-s> 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 ================================================ FILE: variants/nrf52840/wio-t1000-s/rfswitch.h ================================================ #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, }; ================================================ FILE: variants/nrf52840/wio-t1000-s/variant.cpp ================================================ /* 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(PIN_3V3_EN, OUTPUT); digitalWrite(PIN_3V3_EN, HIGH); pinMode(PIN_3V3_ACC_EN, OUTPUT); digitalWrite(PIN_3V3_ACC_EN, LOW); pinMode(T1000X_SENSOR_EN_PIN, OUTPUT); digitalWrite(T1000X_SENSOR_EN_PIN, 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); } ================================================ FILE: variants/nrf52840/wio-t1000-s/variant.h ================================================ /* 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_POWER PIN_LED1 #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 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_SENSOR_EN_PIN (0 + 4) // P0.4, Power to Sensor (GPIO, not ADC) #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_ ================================================ FILE: variants/nrf52840/wio-tracker-wm1110/platformio.ini ================================================ ; The red tracker Dev Board with the WM1110 module [env:wio-tracker-wm1110] custom_meshtastic_hw_model = 21 custom_meshtastic_hw_model_slug = WIO_WM1110 custom_meshtastic_architecture = nrf52840 custom_meshtastic_actively_supported = true custom_meshtastic_support_level = 3 custom_meshtastic_display_name = Seeed Wio WM1110 Tracker custom_meshtastic_images = wio-tracker-wm1110.svg custom_meshtastic_tags = Seeed custom_meshtastic_requires_dfu = true extends = nrf52840_base board = wio-tracker-wm1110 build_flags = ${nrf52840_base.build_flags} -Ivariants/nrf52840/wio-tracker-wm1110 -Isrc/platform/nrf52/softdevice -Isrc/platform/nrf52/softdevice/nrf52 -DWIO_WM1110 board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/wio-tracker-wm1110> ; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) ;upload_protocol = jlink ================================================ FILE: variants/nrf52840/wio-tracker-wm1110/rfswitch.h ================================================ #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, }; ================================================ FILE: variants/nrf52840/wio-tracker-wm1110/variant.cpp ================================================ /* 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); // 3V3 Power Rail pinMode(PIN_3V3_EN, OUTPUT); digitalWrite(PIN_3V3_EN, HIGH); } ================================================ FILE: variants/nrf52840/wio-tracker-wm1110/variant.h ================================================ /* 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 LED_GREEN PIN_LED1 #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 #define GPS_RX_PIN PIN_SERIAL1_RX #define GPS_TX_PIN PIN_SERIAL1_TX #define HAS_GPS 1 #ifdef __cplusplus } #endif /*---------------------------------------------------------------------------- * Arduino objects - C++ only *----------------------------------------------------------------------------*/ #endif // _VARIANT_WIO_TRACKER_WM1110_ ================================================ FILE: variants/rp2040/challenger_2040_lora/pins_arduino.h ================================================ #pragma once #define PINS_COUNT (25u) #define NUM_DIGITAL_PINS (25u) #define NUM_ANALOG_INPUTS (4u) #define NUM_ANALOG_OUTPUTS (0u) #define ADC_RESOLUTION (12u) // LEDs #define LED_POWER (24u) // Serial #define PIN_SERIAL1_TX (16u) #define PIN_SERIAL1_RX (17u) // SPI #define PIN_SPI0_MISO (20u) #define PIN_SPI0_MOSI (23u) #define PIN_SPI0_SCK (22u) #define PIN_SPI0_SS (21u) // Connected to LoRa module #define PIN_SPI1_MISO (12u) #define PIN_SPI1_MOSI (11u) #define PIN_SPI1_SCK (10u) #define PIN_SPI1_SS (9u) #define RFM95W_SS (9u) #define RFM95W_DIO0 (14u) #define RFM95W_DIO1 (15u) #define RFM95W_DIO2 (18u) #define RFM95W_RST (13u) #define RFM95W_SPI SPI1 // Wire #define PIN_WIRE0_SDA (0u) #define PIN_WIRE0_SCL (1u) // Not pinned out #define PIN_WIRE1_SDA (31u) #define PIN_WIRE1_SCL (31u) #define PIN_SERIAL2_RX (31u) #define PIN_SERIAL2_TX (31u) #define SERIAL_HOWMANY (1u) #define SPI_HOWMANY (2u) #define WIRE_HOWMANY (1u) static const uint8_t D0 = (16u); static const uint8_t D1 = (17u); static const uint8_t D2 = (20u); static const uint8_t D3 = (23u); static const uint8_t D4 = (22u); static const uint8_t D5 = (2u); static const uint8_t D6 = (3u); static const uint8_t D7 = (0u); static const uint8_t D8 = (1u); static const uint8_t D9 = (4u); static const uint8_t D10 = (5u); static const uint8_t D11 = (6u); static const uint8_t D12 = (7u); static const uint8_t D13 = (8u); static const uint8_t D14 = (13u); static const uint8_t D15 = (14u); static const uint8_t D16 = (15u); static const uint8_t D17 = (18u); static const uint8_t D18 = (24u); static const uint8_t A0 = (26u); static const uint8_t A1 = (27u); static const uint8_t A2 = (28u); static const uint8_t A3 = (29u); static const uint8_t A4 = (19u); static const uint8_t A5 = (21u); #ifndef SS #define SS PIN_SPI1_SS #endif ================================================ FILE: variants/rp2040/challenger_2040_lora/platformio.ini ================================================ [env:challenger_2040_lora] extends = rp2040_base board = challenger_2040_lora board_level = extra upload_protocol = picotool # add our variants files to the include and src paths build_flags = ${rp2040_base.build_flags} -D PRIVATE_HW -I variants/rp2040/challenger_2040_lora -D DEBUG_RP2040_PORT=Serial -D HW_SPI1_DEVICE debug_build_flags = ${rp2040_base.build_flags} debug_tool = cmsis-dap ; for e.g. Picotool ================================================ FILE: variants/rp2040/challenger_2040_lora/variant.h ================================================ // Define SS for compatibility with libraries expecting a default SPI chip select pin #define ARDUINO_ARCH_AVR #define EXT_NOTIFY_OUT 0xFFFFFFFF #define BUTTON_PIN 0xFFFFFFFF #define USE_RF95 // RFM95/SX127x #undef LORA_SCK #undef LORA_MISO #undef LORA_MOSI #undef LORA_CS // https://gitlab.com/invectorlabs/hw/challenger_rp2040_lora #define LORA_SCK 10 // Clock #define LORA_CS 9 // Chip Select #define LORA_MOSI 11 // Serial Data Out #define LORA_MISO 12 // Serial Data In #define LORA_RESET 13 // Reset #define LORA_DIO0 14 // DIO0 #define LORA_DIO1 15 // DIO1 #define LORA_DIO2 18 // DIO2 #define LORA_DIO3 0xFFFFFFFF // Not connected #define LORA_DIO4 0xFFFFFFFF // Not connected #define LORA_DIO5 0xFFFFFFFF // Not connected #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 ================================================ FILE: variants/rp2040/ec_catsniffer/platformio.ini ================================================ [env:catsniffer] extends = rp2040_base board = rpipico board_level = extra upload_protocol = picotool build_flags = ${rp2040_base.build_flags} -D RPI_PICO -I variants/rp2040/ec_catsniffer -D DEBUG_RP2040_PORT=Serial ; -D HW_SPI1_DEVICE debug_build_flags = ${rp2040_base.build_flags}, -g debug_tool = cmsis-dap ================================================ FILE: variants/rp2040/ec_catsniffer/variant.cpp ================================================ /* 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 "wiring_constants.h" #include "wiring_digital.h" #define CTF1 8 #define CTF2 9 #define CTF3 10 void initVariant() { // Config the LoRa Switch pinMode(CTF1, OUTPUT); pinMode(CTF2, OUTPUT); pinMode(CTF3, OUTPUT); digitalWrite(CTF1, HIGH); digitalWrite(CTF2, LOW); digitalWrite(CTF3, LOW); } ================================================ FILE: variants/rp2040/ec_catsniffer/variant.h ================================================ // #define RADIOLIB_CUSTOM_ARDUINO 1 // #define RADIOLIB_TONE_UNSUPPORTED 1 // #define RADIOLIB_SOFTWARE_SERIAL_UNSUPPORTED 1 #define ARDUINO_ARCH_AVR #define HAS_SCREEN 0 #define HAS_GPS 0 #undef GPS_RX_PIN #undef GPS_TX_PIN #define LED_POWER 27 #define USE_SX1262 #undef LORA_SCK #undef LORA_MISO #undef LORA_MOSI #undef LORA_CS #define LORA_SCK 18 #define LORA_MISO 16 #define LORA_MOSI 19 #define LORA_CS 17 // NSS #define LORA_DIO0 5 #define LORA_RESET 24 #define LORA_DIO1 4 #define LORA_DIO2 23 #define LORA_DIO3 25 #define SX126X_RXEN 21 #define SX126X_TXEN 20 #ifdef USE_SX1262 #define SX126X_CS LORA_CS #define SX126X_DIO1 LORA_DIO0 #define SX126X_BUSY LORA_DIO1 #define SX126X_RESET LORA_RESET // #define SX126X_DIO2_AS_RF_SWITCH #define SX126X_DIO3_TCXO_VOLTAGE 0 #endif ================================================ FILE: variants/rp2040/feather_rp2040_rfm95/platformio.ini ================================================ [env:feather_rp2040_rfm95] extends = rp2040_base board = adafruit_feather board_level = extra upload_protocol = picotool # add our variants files to the include and src paths build_flags = ${rp2040_base.build_flags} -D RP2040_FEATHER_RFM95 -I variants/rp2040/feather_rp2040_rfm95 -D DEBUG_RP2040_PORT=Serial -D HW_SPI1_DEVICE debug_build_flags = ${rp2040_base.build_flags} debug_tool = cmsis-dap ; for e.g. Picotool ================================================ FILE: variants/rp2040/feather_rp2040_rfm95/variant.h ================================================ // #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_POWER 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 ================================================ FILE: variants/rp2040/nibble_rp2040/platformio.ini ================================================ [env:nibble-rp2040] extends = rp2040_base board = rpipico board_level = extra upload_protocol = picotool # add our variants files to the include and src paths build_flags = ${rp2040_base.build_flags} -D PRIVATE_HW -I variants/rp2040/nibble_rp2040 -D DEBUG_RP2040_PORT=Serial -D HW_SPI1_DEVICE debug_build_flags = ${rp2040_base.build_flags}, -g debug_tool = cmsis-dap ; for e.g. Picotool ================================================ FILE: variants/rp2040/nibble_rp2040/variant.h ================================================ #define ARDUINO_ARCH_AVR #define BUTTON_PIN -1 // Pin 17 used for antenna switching via DIO4 #define LED_POWER 1 #define HAS_CPU_SHUTDOWN 1 #define USE_RF95 #define LORA_SCK 10 #define LORA_MISO 12 #define LORA_MOSI 11 #define LORA_CS 13 #define LORA_DIO0 14 #define LORA_RESET 15 #define LORA_DIO1 RADIOLIB_NC #define LORA_DIO2 RADIOLIB_NC ================================================ FILE: variants/rp2040/rak11310/pins_arduino.h ================================================ #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_LED1 (23u) #define LED_NOTIFICATION (24u) #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_SPI1_MISO (12u) #define PIN_SPI1_MOSI (11u) #define PIN_SPI1_SCK (10u) #define PIN_SPI1_SS (13u) #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 (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; ================================================ FILE: variants/rp2040/rak11310/platformio.ini ================================================ [env:rak11310] custom_meshtastic_hw_model = 26 custom_meshtastic_hw_model_slug = RAK11310 custom_meshtastic_architecture = rp2040 custom_meshtastic_actively_supported = true custom_meshtastic_support_level = 2 custom_meshtastic_display_name = RAK WisBlock 11310 custom_meshtastic_images = rak11310.svg custom_meshtastic_tags = RAK custom_meshtastic_requires_dfu = true extends = rp2040_base board = rakwireless_rak11300 board_level = pr upload_protocol = picotool # add our variants files to the include and src paths build_flags = ${rp2040_base.build_flags} -D RAK11310 -I variants/rp2040/rak11310 -D DEBUG_RP2040_PORT=Serial -D RV3028_RTC=0x52 build_src_filter = ${rp2040_base.build_src_filter} +<../variants/rp2040/rak11310> + + + lib_deps = ${rp2040_base.lib_deps} ${networking_base.lib_deps} ${networking_extra.lib_deps} # renovate: datasource=custom.pio depName=Melopero RV3028 packageName=melopero/library/Melopero RV3028 melopero/Melopero RV3028@1.2.0 # renovate: datasource=github-tags depName=RAK13800-W5100S packageName=RAKWireless/RAK13800-W5100S https://github.com/RAKWireless/RAK13800-W5100S/archive/1.0.2.zip debug_build_flags = ${rp2040_base.build_flags}, -g debug_tool = cmsis-dap ; for e.g. Picotool ================================================ FILE: variants/rp2040/rak11310/variant.h ================================================ // #define RADIOLIB_CUSTOM_ARDUINO 1 // #define RADIOLIB_TONE_UNSUPPORTED 1 // #define RADIOLIB_SOFTWARE_SERIAL_UNSUPPORTED 1 #define ARDUINO_ARCH_AVR // Define I2C pins to ensure correct usage of both ports #define I2C_SDA 20 #define I2C_SCL 21 #define I2C_SDA1 2 #define I2C_SCL1 3 #define LED_POWER PIN_LED1 #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 (10u) #define LORA_MOSI (11u) #define LORA_MISO (12u) #define LORA_CS (13u) #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 #define HAS_ETHERNET 1 #define PIN_ETHERNET_RESET 7 // IO3 #define PIN_ETHERNET_SS 17 #define ETH_SPI_PORT SPI #define PIN_ETH_POWER_EN 22 ================================================ FILE: variants/rp2040/rp2040-lora/platformio.ini ================================================ [env:rp2040-lora] custom_meshtastic_hw_model = 30 custom_meshtastic_hw_model_slug = RP2040_LORA custom_meshtastic_architecture = rp2040 custom_meshtastic_actively_supported = true custom_meshtastic_support_level = 2 custom_meshtastic_display_name = RP2040 LoRa custom_meshtastic_tags = Waveshare custom_meshtastic_requires_dfu = true extends = rp2040_base board = rpipico upload_protocol = picotool # add our variants files to the include and src paths build_flags = ${rp2040_base.build_flags} -D RP2040_LORA -I variants/rp2040/rp2040-lora -D DEBUG_RP2040_PORT=Serial -D HW_SPI1_DEVICE debug_build_flags = ${rp2040_base.build_flags}, -g debug_tool = cmsis-dap ; for e.g. Picotool ================================================ FILE: variants/rp2040/rp2040-lora/variant.h ================================================ // #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 #define BUTTON_PIN -1 // Pin 17 used for antenna switching via DIO4 #define LED_POWER 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 ================================================ FILE: variants/rp2040/rp2040.ini ================================================ ; Common settings for rp2040 Processor based targets [rp2040_base] platform = # TODO renovate https://github.com/maxgerhardt/platform-raspberrypi#cc24cfef37ed22ca9f2a6aead28c2deb76c39f24 ; For arduino-pico >= 5.4.4 extends = arduino_base platform_packages = # TODO renovate arduino-pico@https://github.com/earlephilhower/arduino-pico/releases/download/5.4.4/rp2040-5.4.4.zip 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__FREERTOS=1 # -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} ${environmental_extra.lib_deps} ${radiolib_base.lib_deps} # renovate: datasource=custom.pio depName=rweather/Crypto packageName=rweather/library/Crypto rweather/Crypto@0.4.0 ================================================ FILE: variants/rp2040/rpipico/platformio.ini ================================================ [env:pico] custom_meshtastic_hw_model = 47 custom_meshtastic_hw_model_slug = RPI_PICO custom_meshtastic_architecture = rp2040 custom_meshtastic_actively_supported = true custom_meshtastic_support_level = 3 custom_meshtastic_display_name = Raspberry Pi Pico custom_meshtastic_images = pico.svg custom_meshtastic_tags = RPi, DIY custom_meshtastic_requires_dfu = true extends = rp2040_base board = rpipico board_level = pr upload_protocol = picotool # add our variants files to the include and src paths build_flags = ${rp2040_base.build_flags} -D RPI_PICO -I variants/rp2040/rpipico -D DEBUG_RP2040_PORT=Serial -D HW_SPI1_DEVICE debug_build_flags = ${rp2040_base.build_flags}, -g debug_tool = cmsis-dap ; for e.g. Picotool ================================================ FILE: variants/rp2040/rpipico/variant.h ================================================ // #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_POWER 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 ================================================ FILE: variants/rp2040/rpipico-slowclock/platformio.ini ================================================ [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/rp2040/rpipico-slowclock -DDEBUG_RP2040_PORT=Serial2 -DHW_SPI1_DEVICE -g -DNO_USB debug_build_flags = ${rp2040_base.build_flags} -g -DNO_USB ================================================ FILE: variants/rp2040/rpipico-slowclock/variant.h ================================================ #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_POWER 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 ================================================ FILE: variants/rp2040/rpipicow/platformio.ini ================================================ [env:picow] custom_meshtastic_hw_model = 47 custom_meshtastic_hw_model_slug = RPI_PICO custom_meshtastic_architecture = rp2040 custom_meshtastic_actively_supported = true custom_meshtastic_support_level = 3 custom_meshtastic_display_name = Raspberry Pi Pico W custom_meshtastic_images = rpipicow.svg custom_meshtastic_tags = RPi, DIY custom_meshtastic_requires_dfu = true extends = rp2040_base board = rpipicow board_level = pr board_check = true upload_protocol = picotool # add our variants files to the include and src paths build_flags = ${rp2040_base.build_flags} -D RPI_PICO -I variants/rp2040/rpipicow -D HW_SPI1_DEVICE -D HAS_UDP_MULTICAST=1 -fexceptions # for exception handling in MQTT -ULED_BUILTIN build_src_filter = ${rp2040_base.build_src_filter} + lib_deps = ${rp2040_base.lib_deps} ${networking_base.lib_deps} ${networking_extra.lib_deps} debug_build_flags = ${rp2040_base.build_flags}, -g debug_tool = cmsis-dap ; for e.g. Picotool ================================================ FILE: variants/rp2040/rpipicow/variant.h ================================================ // #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_POWER 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 ================================================ FILE: variants/rp2040/senselora_rp2040/pins_arduino.h ================================================ #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_LED1 (23u) #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; ================================================ FILE: variants/rp2040/senselora_rp2040/platformio.ini ================================================ [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} -D SENSELORA_RP2040 -I variants/rp2040/senselora_rp2040 -D DEBUG_RP2040_PORT=Serial ================================================ FILE: variants/rp2040/senselora_rp2040/variant.h ================================================ #define ARDUINO_ARCH_AVR #define USE_SSD1306 #define BUTTON_PIN 2 #define BUTTON_NEED_PULLUP #define LED_POWER PIN_LED1 #define ledOff(pin) pinMode(pin, INPUT) #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 ================================================ FILE: variants/rp2350/rp2350.ini ================================================ ; Common settings for rp2350 Processor based targets [rp2350_base] platform = # TODO renovate https://github.com/maxgerhardt/platform-raspberrypi#cc24cfef37ed22ca9f2a6aead28c2deb76c39f24 ; For arduino-pico >= 5.4.4 extends = arduino_base platform_packages = # TODO renovate arduino-pico@https://github.com/earlephilhower/arduino-pico/releases/download/5.4.4/rp2040-5.4.4.zip board_build.core = earlephilhower board_build.filesystem_size = 0.5m build_flags = ${arduino_base.build_flags} -Wno-unused-variable -Wcast-align -Isrc/platform/rp2xx0 -D__PLAT_RP2350__ -D__FREERTOS=1 build_src_filter = ${arduino_base.build_src_filter} - - - - - - - - - - - lib_ignore = BluetoothOTA lvgl lib_deps = ${arduino_base.lib_deps} ${environmental_base.lib_deps} ${environmental_extra.lib_deps} ${radiolib_base.lib_deps} # renovate: datasource=custom.pio depName=rweather/Crypto packageName=rweather/library/Crypto rweather/Crypto@0.4.0 ================================================ FILE: variants/rp2350/rpipico2/platformio.ini ================================================ [env:pico2] extends = rp2350_base board = rpipico2 board_level = pr upload_protocol = picotool # add our variants files to the include and src paths build_flags = ${rp2350_base.build_flags} -D RPI_PICO2 -I variants/rp2350/rpipico2 -D DEBUG_RP2040_PORT=Serial -D HW_SPI1_DEVICE debug_build_flags = ${rp2350_base.build_flags}, -g debug_tool = cmsis-dap ; for e.g. Picotool ================================================ FILE: variants/rp2350/rpipico2/variant.h ================================================ // #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_POWER 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 ================================================ FILE: variants/rp2350/rpipico2w/platformio.ini ================================================ [env:pico2w] extends = rp2350_base board = rpipico2w board_level = pr board_check = true 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 = ${rp2350_base.build_flags} -DRPI_PICO2 -Ivariants/rp2350/rpipico2w # -DDEBUG_RP2040_PORT=Serial -DHW_SPI1_DEVICE -DARDUINO_RASPBERRY_PI_PICO_2W -DARDUINO_ARCH_RP2040 -DHAS_WIFI=1 -fexceptions # for exception handling in MQTT -DHAS_UDP_MULTICAST=1 build_src_filter = ${rp2350_base.build_src_filter} + lib_deps = ${rp2350_base.lib_deps} ${networking_base.lib_deps} ${networking_extra.lib_deps} debug_build_flags = ${rp2350_base.build_flags}, -g ================================================ FILE: variants/rp2350/rpipico2w/variant.h ================================================ // #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 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 ================================================ FILE: variants/stm32/CDEBYTE_E77-MBL/platformio.ini ================================================ [env:CDEBYTE_E77-MBL] extends = stm32_base board = ebyte_e77_dev board_upload.maximum_size = 233472 ; reserve the last 28KB for filesystem board_level = extra build_flags = ${stm32_base.build_flags} -Ivariants/stm32/CDEBYTE_E77-MBL -DSERIAL_UART_INSTANCE=2 -DPIN_SERIAL_RX=PA3 -DPIN_SERIAL_TX=PA2 -DENABLE_HWSERIAL1 -DPIN_SERIAL1_RX=PB7 -DPIN_SERIAL1_TX=PB6 -DMESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR=1 -DMESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR=1 -DMESHTASTIC_EXCLUDE_I2C=1 -DMESHTASTIC_EXCLUDE_GPS=1 upload_port = stlink ================================================ FILE: variants/stm32/CDEBYTE_E77-MBL/rfswitch.h ================================================ // From E77-900M22S Product Specification // https://www.cdebyte.com/pdf-down.aspx?id=2963 // Note 1: PA6 and PA7 pins are used as internal control RF switches of the module, PA6 = RF_TXEN, PA7 = RF_RXEN, RF_TXEN=1 // RF_RXEN=0 is the transmit channel, and RF_TXEN=0 RF_RXEN=1 is the receiving channel static const RADIOLIB_PIN_TYPE rfswitch_pins[5] = {PA7, PA6, 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}; ================================================ FILE: variants/stm32/CDEBYTE_E77-MBL/variant.h ================================================ /* EByte E77-MBL series https://www.cdebyte.com/products/E77-900MBL-01 https://www.cdebyte.com/products/E77-400MBL-01 https://github.com/olliw42/mLRS-docu/blob/master/docs/EBYTE_E77_MBL.md */ /* This variant is a work in progress. Do not expect a working Meshtastic device with this target. */ #ifndef _VARIANT_EBYTE_E77_ #define _VARIANT_EBYTE_E77_ #define USE_STM32WLx #define LED_POWER PB4 // LED1 // #define LED_POWER PB3 // LED2 #define LED_STATE_ON 1 #define SERIAL_PRINT_PORT 1 #define EBYTE_E77_MBL #endif ================================================ FILE: variants/stm32/milesight_gs301/platformio.ini ================================================ ; Milesight GS301 Bathroom Odor Detector ; https://www.milesight.com/iot/product/lorawan-sensor/gs301 [env:milesight_gs301] extends = stm32_base board = wiscore_rak3172 ; Convenient choice as the same USART is used for programming/debug board_level = extra board_upload.maximum_size = 233472 ; reserve the last 28KB for filesystem build_flags = ${stm32_base.build_flags} -Ivariants/stm32/milesight_gs301 -DPRIVATE_HW -DMESHTASTIC_EXCLUDE_GPS=1 -DMESHTASTIC_EXCLUDE_I2C=1 # Analog ADuCM355 (unsupported) so no point building support for I2C in -DMESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR=1 -DMESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR=1 build_unflags = -DDEBUG_MUTE # We have space for debug output until sensor support is added build_src_filter = ${stm32_base.build_src_filter} +<../variants/stm32/milesight_gs301> lib_deps = ${stm32_base.lib_deps} upload_port = stlink ================================================ FILE: variants/stm32/milesight_gs301/rfswitch.h ================================================ // Seems to use the same RF switch pins as RAK3172… getting Tx/Rx SNR +11dB with a nearby node // PB8, PC13 static const RADIOLIB_PIN_TYPE rfswitch_pins[5] = {PB8, PC13, 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}; ================================================ FILE: variants/stm32/milesight_gs301/variant.cpp ================================================ #include "variant.h" #include "Arduino.h" void earlyInitVariant() { pinMode(USER_LED, OUTPUT); digitalWrite(USER_LED, HIGH ^ LED_STATE_ON); } ================================================ FILE: variants/stm32/milesight_gs301/variant.h ================================================ #ifndef _VARIANT_MILESIGHT_GS301_ #define _VARIANT_MILESIGHT_GS301_ #define USE_STM32WLx // I/O #define LED_STATE_ON 1 #define PIN_LED1 PA0 // Green LED #define LED_POWER PIN_LED1 #define PIN_LED2 PA0 // Red LED #define USER_LED PIN_LED2 #define BUTTON_PIN PC13 #define BUTTON_ACTIVE_LOW true #define BUTTON_ACTIVE_PULLUP false #define PIN_BUZZER PA6 // EC Sense DGM10 Double Gas Module // Analog ADuCM355 (unsupported); SHTC3 is connected to ADuCM355 and not directly accessible #define PIN_WIRE_SDA PB7 #define PIN_WIRE_SCL PB8 // Commented out to keep sensor powered down due to lack of support /* #define VEXT_ENABLE PB12 // TI TPS61291DRV VSEL - set LOW before ENable for Vout = 3.3V #define VEXT_ON_VALUE LOW #define SENSOR_POWER_CTRL_PIN PB2 // TI TPS61291DRV EN pin #define SENSOR_POWER_ON HIGH #define HAS_SENSOR 1 */ #define ENABLE_HWSERIAL1 #define PIN_SERIAL1_RX NC #define PIN_SERIAL1_TX PB6 // LoRa #define SX126X_DIO3_TCXO_VOLTAGE 3.0 // Required to avoid Serial1 conflicts due to board definition here: // https://github.com/stm32duino/Arduino_Core_STM32/blob/main/variants/STM32WLxx/WL54CCU_WL55CCU_WLE4C(8-B-C)U_WLE5C(8-B-C)U/variant_RAK3172_MODULE.h #define RAK3172 #endif ================================================ FILE: variants/stm32/rak3172/platformio.ini ================================================ [env:rak3172] extends = stm32_base board = wiscore_rak3172 board_level = pr board_upload.maximum_size = 233472 ; reserve the last 28KB for filesystem build_flags = ${stm32_base.build_flags} -Ivariants/stm32/rak3172 -DENABLE_HWSERIAL1 -DPIN_SERIAL1_RX=PB7 -DPIN_SERIAL1_TX=PB6 -DPIN_WIRE_SDA=PA11 -DPIN_WIRE_SCL=PA12 -DMESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR=1 -DMESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR=1 -DMESHTASTIC_EXCLUDE_I2C=1 -DMESHTASTIC_EXCLUDE_GPS=1 upload_port = stlink ================================================ FILE: variants/stm32/rak3172/rfswitch.h ================================================ // Pins from https://forum.rakwireless.com/t/rak3172-internal-schematic/4557/2 // PB8, PC13 static const RADIOLIB_PIN_TYPE rfswitch_pins[5] = {PB8, PC13, 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}; ================================================ FILE: variants/stm32/rak3172/variant.h ================================================ /* STM32WLE5 Core Module for LoRaWAN® RAK3372 https://store.rakwireless.com/products/wisblock-core-module-rak3372 */ /* 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 LED_POWER PA0 // Green LED #define LED_STATE_ON 1 #define RAK3172 #define SERIAL_PRINT_PORT 1 #endif ================================================ FILE: variants/stm32/russell/platformio.ini ================================================ ; Russell is a board designed to mount on an ER34615/IFR32700 cell and go Up! on a balloon ; Hardware repository: https://github.com/Meshtastic-Malaysia/russell ; - RAK3172 STM32WLE5CCU6 MCU + integrated SX1262 LoRa ; - CDtop CD-PA1010D GPS ; - Bosch Sensortec BME280 sensor ; - Consonance CN3158 LiFePO4 solar charger [env:russell] extends = stm32_base board = wiscore_rak3172 board_level = extra board_upload.maximum_size = 233472 ; reserve the last 28KB for filesystem build_flags = ${stm32_base.build_flags} -Ivariants/stm32/russell -DPRIVATE_HW lib_deps = ${stm32_base.lib_deps} # renovate: datasource=custom.pio depName=Adafruit BME280 packageName=adafruit/library/Adafruit BME280 Library adafruit/Adafruit BME280 Library@2.3.0 upload_port = stlink ================================================ FILE: variants/stm32/russell/rfswitch.h ================================================ // Pins from https://forum.rakwireless.com/t/rak3172-internal-schematic/4557/2 // PB8, PC13 static const RADIOLIB_PIN_TYPE rfswitch_pins[5] = {PB8, PC13, 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}; ================================================ FILE: variants/stm32/russell/variant.h ================================================ #ifndef _VARIANT_RUSSELL_ #define _VARIANT_RUSSELL_ #define USE_STM32WLx // I/O #define LED_POWER PA0 // Red LED #define LED_STATE_ON 1 #define BUTTON_PIN PH3 // Shared with BOOT0 #define BUTTON_NEED_PULLUP // Charger IC charge/standby pins are open-drain with no hardware pull-up: // Internal pull-up is needed on STM32 (TODO) // #define EXT_CHRG_DETECT PA5 // #define EXT_PWR_DETECT PA4 // Bosch Sensortec BME280 #define HAS_SENSOR 1 // CDtop CD-PA1010D #define ENABLE_HWSERIAL1 #define PIN_SERIAL1_RX PB7 #define PIN_SERIAL1_TX PB6 #define HAS_GPS 1 #define PIN_GPS_STANDBY PA15 #define GPS_RX_PIN PB7 #define GPS_TX_PIN PB6 // LoRa /* * RAK3172 (-20–85°C) -> No TCXO * RAK3172-T (-40–85°C) -> 3.0V TCXO * https://github.com/RAKWireless/RAK-STM32-RUI/blob/e5a28be8fab1a492bd9223dd425ca33a8a297d90/variants/WisDuo_RAK3172-T_Board/radio_conf.h#L91 */ #define TCXO_OPTIONAL #define SX126X_DIO3_TCXO_VOLTAGE 3.0 // Required to avoid Serial1 conflicts due to board definition here: // https://github.com/stm32duino/Arduino_Core_STM32/blob/main/variants/STM32WLxx/WL54CCU_WL55CCU_WLE4C(8-B-C)U_WLE5C(8-B-C)U/variant_RAK3172_MODULE.h #define RAK3172 #endif ================================================ FILE: variants/stm32/stm32.ini ================================================ [stm32_base] extends = arduino_base platform = # renovate: datasource=custom.pio depName=platformio/ststm32 packageName=platformio/platform/ststm32 platformio/ststm32@19.5.0 platform_packages = # renovate: datasource=github-tags depName=Arduino_Core_STM32 packageName=stm32duino/Arduino_Core_STM32 platformio/framework-arduinoststm32@https://github.com/stm32duino/Arduino_Core_STM32/archive/2.10.1.zip extra_scripts = ${env.extra_scripts} extra_scripts/stm32_extra.py build_type = release build_flags = ${arduino_base.build_flags} -flto -Isrc/platform/stm32wl -g -DMESHTASTIC_EXCLUDE_AUDIO=1 -DMESHTASTIC_EXCLUDE_ATAK=1 ; ATAK is quite big, disable it for big flash savings. -DMESHTASTIC_EXCLUDE_INPUTBROKER=1 -DMESHTASTIC_EXCLUDE_POWERMON=1 -DMESHTASTIC_EXCLUDE_SCREEN=1 -DMESHTASTIC_EXCLUDE_MQTT=1 -DMESHTASTIC_EXCLUDE_BLUETOOTH=1 -DMESHTASTIC_EXCLUDE_WIFI=1 -DMESHTASTIC_EXCLUDE_TZ=1 ; Exclude TZ to save some flash space. -DSERIAL_RX_BUFFER_SIZE=256 ; For GPS - the default of 64 is too small. -DHAS_SCREEN=0 ; Always disable screen for STM32, it is not supported. -DPIO_FRAMEWORK_ARDUINO_NANOLIB_FLOAT_PRINTF ; This is REQUIRED for at least traceroute debug prints - without it the length ends up uninitialized. -DDEBUG_MUTE ; You can #undef DEBUG_MUTE in certain source files if you need the logs. -fmerge-all-constants -ffunction-sections -fdata-sections -DRADIOLIB_EXCLUDE_SX128X=1 -DRADIOLIB_EXCLUDE_SX127X=1 -DRADIOLIB_EXCLUDE_LR11X0=1 -DHAL_DAC_MODULE_ONLY -DHAL_RNG_MODULE_ENABLED -Wl,--wrap=__assert_func -Wl,--wrap=strerror -Wl,--wrap=_tzset_unlocked_r build_src_filter = ${arduino_base.build_src_filter} - - - - - - - - - - - - - - board_upload.offset_address = 0x08000000 upload_protocol = stlink debug_tool = stlink lib_deps = ${env.lib_deps} ${radiolib_base.lib_deps} # renovate: datasource=git-refs depName=caveman99-stm32-Crypto packageName=https://github.com/caveman99/Crypto gitBranch=main https://github.com/caveman99/Crypto/archive/1aa30eb536bd52a576fde6dfa393bf7349cf102d.zip lib_ignore = OneButton ================================================ FILE: variants/stm32/wio-e5/platformio.ini ================================================ [env:wio-e5] extends = stm32_base board = lora_e5_dev_board board_level = pr board_upload.maximum_size = 233472 ; reserve the last 28KB for filesystem build_flags = ${stm32_base.build_flags} -Ivariants/stm32/wio-e5 -DSERIAL_UART_INSTANCE=1 -DPIN_SERIAL_RX=PB7 -DPIN_SERIAL_TX=PB6 -DPIN_WIRE_SDA=PA15 -DPIN_WIRE_SCL=PB15 -DHAS_SENSOR=1 -DENABLE_HWSERIAL2 -DPIN_SERIAL2_TX=PA2 -DPIN_SERIAL2_RX=PA3 -DHAS_GPS=1 -DGPS_SERIAL_PORT=Serial2 -DMESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR=1 upload_port = stlink ================================================ FILE: variants/stm32/wio-e5/rfswitch.h ================================================ /* 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}; ================================================ FILE: variants/stm32/wio-e5/variant.h ================================================ /* 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 LED_POWER PB5 #define LED_STATE_ON 0 #define WIO_E5 #endif ================================================ FILE: version.properties ================================================ [VERSION] major = 2 minor = 7 build = 21