From 965c3e9e3041118cda7f8cab193009f2b5842603 Mon Sep 17 00:00:00 2001 From: UA1ZBE Date: Wed, 22 Jul 2026 07:56:45 +0300 Subject: [PATCH] Initial commit: Security Mesh Node --- .gitignore | 10 + INSTRUCTION.md | 236 ++++++++++++++++++++++ USER_MANUAL.md | 121 ++++++++++++ boards/heltec_t114.json | 61 ++++++ boards/nrf52840_s140_v6_extrafs.ld | 28 +++ boards/promicro_nrf52840.json | 79 ++++++++ platformio.ini | 93 +++++++++ scripts/add_external_sources.py | 33 ++++ scripts/create_uf2.py | 31 +++ src/main.cpp | 251 ++++++++++++++++++++++++ src/target.h | 146 ++++++++++++++ src/variants/T114Board.cpp | 59 ++++++ src/variants/T114Board.h | 59 ++++++ src/variants/promicro/PromicroBoard.cpp | 25 +++ src/variants/promicro/PromicroBoard.h | 79 ++++++++ src/variants/promicro/variant.cpp | 15 ++ src/variants/promicro/variant.h | 82 ++++++++ src/variants/variant.cpp | 15 ++ src/variants/variant.h | 142 ++++++++++++++ 19 files changed, 1565 insertions(+) create mode 100644 .gitignore create mode 100644 INSTRUCTION.md create mode 100644 USER_MANUAL.md create mode 100644 boards/heltec_t114.json create mode 100644 boards/nrf52840_s140_v6_extrafs.ld create mode 100644 boards/promicro_nrf52840.json create mode 100644 platformio.ini create mode 100644 scripts/add_external_sources.py create mode 100644 scripts/create_uf2.py create mode 100644 src/main.cpp create mode 100644 src/target.h create mode 100644 src/variants/T114Board.cpp create mode 100644 src/variants/T114Board.h create mode 100644 src/variants/promicro/PromicroBoard.cpp create mode 100644 src/variants/promicro/PromicroBoard.h create mode 100644 src/variants/promicro/variant.cpp create mode 100644 src/variants/promicro/variant.h create mode 100644 src/variants/variant.cpp create mode 100644 src/variants/variant.h diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d6b8ef9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +.pio/ +.pioenvs/ +.piolibdeps/ +__pycache__/ +*.o +*.d +*.elf +*.bin +*.hex +*.zip diff --git a/INSTRUCTION.md b/INSTRUCTION.md new file mode 100644 index 0000000..751f60f --- /dev/null +++ b/INSTRUCTION.md @@ -0,0 +1,236 @@ +# Security Mesh Node — Architecture & Development Guide + +## Project Structure + +``` +security-new/ +├── platformio.ini # PlatformIO config (targets, libs, flags) +├── src/ +│ ├── main.cpp # Main firmware — SecurityMesh class + loop +│ ├── target.h # Pin mappings, radio params, platform defs +│ └── variants/ +│ └── promicro/ # ProMicro-specific files +├── boards/ # Custom linker scripts +├── scripts/ # Build helper scripts +├── .pio/ # Build output (ignored) +``` + +All MeshCore library lives at `../MeshCore/`. + +--- + +## How Radio Mesh Works + +### Low-level Layer (RadioLib) +- `radio_module` (SX1262) — raw LoRa send/receive. +- `radio_driver` (CustomSX1262Wrapper) — RadioLib wrapper. Provides `getCurrentRSSI()`, `setParams()`, `setTxPower()`. + +### Mesh Layer (MeshCore) +- `mesh::Mesh` — main class that manages flooding, routing, encryption, channel storage. +- `mesh::Dispatcher` — base of Mesh (packet scheduling, radio control). +- `mesh::GroupChannel` — channel with `hash[1]` and `secret[32]`. +- `mesh::Packet` — a mesh packet. Obtained from `PacketManager` (pool of 16 slots). + +### Lifecycle + +``` +1. radio_module.begin() — init SX1262 +2. radio_driver.setParams(...) — freq, BW, SF, CR +3. Mesh.begin() — starts listening +4. Mesh.loop() loop() — pumps radio ISR, processes incoming +5. onGroupDataRecv() callback — called when encrypted group msg arrives +``` + +--- + +## SecurityMesh Class + +### Channel Derivation (`setup_channel()`) + +``` +SHA256("1234") → 32 bytes key +mesh_channel.secret = key[0..15] (first 16 bytes) +mesh_channel.hash = SHA256(secret)[0..0] (first 1 byte, PATH_HASH_SIZE=1) +``` + +### Critical Virtual Overrides + +#### `searchChannelsByHash()` +**WHY IT'S NEEDED:** `Mesh::loop()` calls `searchChannelsByHash()` to match incoming packet hashes against stored channels. The base implementation returns 0 (no matches). Without this override, **every incoming group message is silently dropped** and `onGroupDataRecv` is never called. + +```cpp +int searchChannelsByHash(const uint8_t* hash, mesh::GroupChannel channels[], int max_matches) override { + if (max_matches > 0 && channel_ready + && memcmp(hash, mesh_channel.hash, sizeof(mesh_channel.hash)) == 0) { + channels[0] = mesh_channel; + return 1; // found 1 match + } + return 0; +} +``` + +`sizeof(mesh_channel.hash)` = `PATH_HASH_SIZE` = 1 byte. + +#### `onGroupDataRecv()` +Called for each decrypted group message that matched a channel. Format: `[4 bytes ts][1 byte type][text]`. Type byte must be 0 for text. + +### Sending (`send_text()`) + +```cpp +void send_text(const char* text) { + uint8_t buf[256]; + buf[0..3] = timestamp (4 bytes) + buf[4] = 0 (type: text) + buf[5..] = text + Packet* pkt = createGroupDatagram(PAYLOAD_TYPE_GRP_TXT, mesh_channel, buf, 5 + text_len); + sendFlood(pkt, 0, 1); // 1 hop +} +``` + +`sendFlood` with `path_hash_size=1` means 0-hop (immediate neighbors only). Increase to flood across the mesh. + +--- + +## Command Processing + +All commands are processed in `onGroupDataRecv()`: + +### Preprocessing +1. **Trim trailing spaces** — `"status "` → `"status"` +2. **Lowercase** — `"Status"` → `"status"` +3. **Strip prefix** — `"Security: status"` → `"status"` (anything before `": "`) +4. **Trim leading spaces** + +### Command Table + +| Command | Action | Response | +|---------|--------|----------| +| `help` | list commands | `help:...` | +| `ohrana on` | arm, relay1 ON | `ohrana on ok` | +| `ohrana off` | disarm, relay1 OFF | `ohrana off ok` | +| `relay1 on` | relay1 ON (direct) | `relay1 on ok` | +| `relay1 off` | relay1 OFF (direct) | `relay1 off ok` | +| `relay2 on` | relay2 ON | `relay2 on ok` | +| `relay2 off` | relay2 OFF | `relay2 off ok` | +| `status` | report state | `status:armed/off temp:±X.XC hall:trig/ok motion:trig/ok` | +| `ver` | build date | `build:Mon DD YYYY HH:MM:SS` | +| `scan` | noise floor | `noise:-XXXdBm` | +| `security_conf ` | change channel | `channel:` | + +### Auto-Sent Messages +- **Startup:** `poweron Mon DD YYYY HH:MM:SS` — sent 10s after boot. +- **Hall alert:** `alert:hall` — when armed and reed opens. +- **Motion alert:** `alert:motion` — when armed and PIR triggers. + +Cooldown: 10 minutes between alerts (ALERT_COOLDOWN_MS). + +--- + +## Hardware + +| Pin (nRF52) | Pin (ProMicro) | Function | +|-------------|----------------|----------| +| 30 | 13 | NSS (LoRa SPI CS) | +| 29 | 12 | SCK | +| 31 | 14 | MOSI | +| 2 | 15 | MISO | +| 7 | 11 | DIO1 (LoRa IRQ) | +| 6 | 10 | RST | +| 10 | 16 | BUSY | +| — | 21 | POWER_EN (ProMicro only) | +| 12 | 0 | REED (hall sensor) | +| 11 | 1 | PIR (motion) | +| 3 | 2 | DS18B20 (temp) | +| 4 | 3 | RELAY_1 | +| 5 | 4 | RELAY_2 | +| 13 | 22 | LED_STATUS | + +**Reference:** `target.h` lines 25–114. + +--- + +## Radio Config + +| Param | Value | +|-------|-------| +| Frequency | 868.731018 MHz | +| Bandwidth | 62.5 kHz | +| Spreading Factor | 7 | +| Coding Rate | 7 (4/7) | +| TX Power | 22 dBm | +| TCXO voltage | 1.8V | +| Current limit | 140 mA | +| RX Boosted Gain | on | +| DIO2 as RF switch | on | + +`PATH_HASH_MODE=0` → `PATH_HASH_SIZE=1` (minimal hash, maximum throughput). + +--- + +## Build & Upload + +```bash +# Build +pio run -e promicro_security + +# Upload (via nrfutil) +pio run -e promicro_security -t upload + +# Monitor +pio device monitor -b 115200 +``` + +Current resource usage: RAM 8%, Flash 36.5%. + +--- + +## Common Pitfalls & Debugging + +### `onGroupDataRecv` not called despite LED blink +The LED blink (3 fast flashes) is the first thing in `onGroupDataRecv`. If LED doesn't blink: +- Check `searchChannelsByHash()` override — it's required. +- Check hash match: both sides must derive the same `secret` and `hash` from passphrase. +- Check radio config matches (freq, BW, SF, CR). + +### No response despite LED blinking +- Command was received (LED blinked) but processing or send failed. +- **Test with simple response** — if `send_text("ok")` fails but `send_text("relay2 on ok")` works, check response length/SNPRINTF. +- `sendFlood` with `path_hash_size=1` sends only to neighbors. If response doesn't arrive at the remote app, check app's mesh routing or increase path_hash_size. + +### `send_text()` doesn't actually send +- `createGroupDatagram` may return NULL if packet pool is full (16 slots). +- `rtc_clock.getCurrentTime()` returning bogus data (VolatileRTCClock starts from 0, fine). + +### Adding a new command +1. Add `else if` block in `onGroupDataRecv()` after line 168. +2. Use `strcmp(cmd, "command") == 0` for exact match. +3. Call `send_text("response")` to reply. +4. Update the `help` string. + +### Config persistent storage +- `InternalFS` (LittleFS) on nRF52 internal flash. +- `SecurityConfig`: armed + relay2 state saved as binary struct. +- File: `/security.cfg`, magic `0x53454355`. + +--- + +## Key Files in MeshCore Library + +| File | What it provides | +|------|-----------------| +| `MeshCore/src/Mesh.h` | `Mesh`, `GroupChannel`, virtual methods | +| `MeshCore/src/MeshCore.h` | Constants (`PUB_KEY_SIZE`, `PATH_HASH_SIZE`, etc.) | +| `MeshCore/src/Dispatcher.h` | `Dispatcher`, packet scheduling | +| `MeshCore/src/helpers/SimpleMeshTables.h` | Duplicate packet detection | +| `MeshCore/src/helpers/StaticPoolPacketManager.h` | Packet memory pool | +| `MeshCore/src/helpers/ChannelDetails.h` | Channel helper structs | +| `MeshCore/src/helpers/NRF52Board.h` | nRF52 board abstraction | + +--- + +## Things That Won't Work + +- **BLE** — not included. No `BLESerialInterface`, no `BaseChatMesh`. +- **`BaseChatMesh`** — not used. SecurityMesh inherits directly from `mesh::Mesh`. +- **Serial commands** — not processed. Serial is for debug output only. +- **`onPeerDataRecv`** — not used. Group messaging only, no direct peer messages. diff --git a/USER_MANUAL.md b/USER_MANUAL.md new file mode 100644 index 0000000..89972e8 --- /dev/null +++ b/USER_MANUAL.md @@ -0,0 +1,121 @@ +# Security Mesh Node — Инструкция пользователя + +## Обзор + +Security Node — LoRa mesh-устройство, которое следит за датчиками (геркон, PIR движение, температура) и управляет 2 реле. Работает по радиоканалу через MeshCore — без интернета, без Bluetooth, без проводов. + +Команды и оповещения передаются как текстовые сообщения по mesh-сети. + +--- + +## Быстрый старт + +1. Подайте питание (USB или батарея). +2. Подождите ~10 секунд — устройство отправит `poweron`. +3. Отправляйте команды с любого другого узла mesh. + +Все команды **не чувствительны к регистру**, лишние пробелы в конце игнорируются. +**Префикс не обязателен** — работают и `"status"`, и `"Security: status"`. + +--- + +## Команды + +| Команда | Что делает | Пример ответа | +|---------|-----------|---------------| +| `help` | Список всех команд | `help:ohrana on,ohrana off,...` | +| `ohrana on` | **Включить охрану** — датчики активны, реле 1 замкнуто | `ohrana on ok` | +| `ohrana off` | **Выключить охрану** — датчики не активны, реле 1 разомкнуто | `ohrana off ok` | +| `relay1 on` | Включить реле 1 (независимо от охраны) | `relay1 on ok` | +| `relay1 off` | Выключить реле 1 | `relay1 off ok` | +| `relay2 on` | Включить реле 2 | `relay2 on ok` | +| `relay2 off` | Выключить реле 2 | `relay2 off ok` | +| `status` | Показать состояние устройства | `status:armed temp:+23.5C hall:ok motion:trig` | +| `ver` | Версия прошивки (дата сборки) | `build:Jul 21 2026 15:30:00` | +| `scan` | Измерить уровень шума в эфире | `noise:-112dBm` | +| `security_conf <имя>` | Сменить канал (пароль из `<имя>`) | `channel:mysecret` | + +--- + +## Поля статуса + +Ответ: `status:armed/off temp:+XX.XC/N/A hall:trig/ok motion:trig/ok` + +| Поле | Значение | +|------|----------| +| `status:armed` | Охрана включена (датчики активны) | +| `status:off` | Охрана выключена | +| `temp:+23.5C` | Температура с датчика DS18B20 | +| `temp:N/A` | Датчик температуры не подключён | +| `hall:ok` | Геркон замкнут (норма) | +| `hall:trig` | Геркон разомкнулся (дверь/окно открыли) | +| `motion:ok` | Движения нет | +| `motion:trig` | Движение обнаружено | + +--- + +## Оповещения + +Когда охрана **включена** (`ohrana on`), устройство автоматически отправляет: + +| Оповещение | Что значит | +|------------|-----------| +| `alert:hall` | Геркон разомкнулся — дверь/окно открыли | +| `alert:motion` | PIR датчик сработал — движение | + +Одно и то же оповещение не повторится раньше чем через **10 минут** (защита от дребезга). +Оповещение сбрасывается автоматически, когда датчик возвращается в нормальное состояние. + +--- + +## Сообщение при запуске + +Через ~10 секунд после подачи питания устройство отправляет: +`poweron Jul 21 2026 15:30:00` + +Это подтверждает, что устройство включено и работает на текущем канале. + +--- + +## Светодиод (LED на плате) + +| Режим | Что значит | +|-------|-----------| +| Не горит | Охрана выключена, нет активности | +| Медленно мигает (1 сек) | **Охрана включена** | +| 3 быстрых вспышки | Получена команда (любая) | +| Часто мигает (100 мс) | **Ошибка радио** — аппаратная проблема | + +--- + +## Реле + +- **Реле 1** — управляется `ohrana on/off` и `relay1 on/off`. + Охрана включена (`ohrana on`): **замкнуто** (NC разомкнуты, NO замкнуты). + Охрана выключена (`ohrana off`): **разомкнуто**. + +- **Реле 2** — независимое, только `relay2 on/off`. + Состояние сохраняется при перезагрузке. + +--- + +## Смена канала (`security_conf`) + +По умолчанию канал формируется из пароля `"1234"`. +Чтобы сменить: + +``` +security_conf мойпароль +``` + +Ответ: `channel:мойпароль` + +**Важно:** Все устройства должны быть на одном канале. После смены канала связь будет только с теми узлами, у которых такой же пароль. + +--- + +## Советы + +- Если команда не получила ответа: проверьте канал, питание, дальность связи. LED должен мигнуть 3 раза при получении команды — если не мигнул, сообщение не дошло. +- Все ответы тоже передаются по mesh — они появятся в ленте сообщений вашего приложения. +- `scan` выполняется около 1 секунды (20 замеров RSSI). diff --git a/boards/heltec_t114.json b/boards/heltec_t114.json new file mode 100644 index 0000000..a4a61d0 --- /dev/null +++ b/boards/heltec_t114.json @@ -0,0 +1,61 @@ +{ + "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": "HT-n5262", + "mcu": "nrf52840", + "variant": "Heltec_T114_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": "nrf52.cfg" + }, + "frameworks": [ + "arduino" + ], + "name": "Heltec T114 Board", + "upload": { + "maximum_ram_size": 235520, + "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/", + "vendor": "Heltec" +} \ No newline at end of file diff --git a/boards/nrf52840_s140_v6_extrafs.ld b/boards/nrf52840_s140_v6_extrafs.ld new file mode 100644 index 0000000..88771bc --- /dev/null +++ b/boards/nrf52840_s140_v6_extrafs.ld @@ -0,0 +1,28 @@ +SEARCH_DIR(.) +GROUP(-lgcc -lc -lnosys) + +MEMORY +{ + FLASH (rx) : ORIGIN = 0x26000, LENGTH = 0xD4000 - 0x26000 + RAM (rwx) : ORIGIN = 0x20006000, LENGTH = 0x20040000 - 0x20006000 +} + +SECTIONS +{ + . = ALIGN(4); + .svc_data : + { + PROVIDE(__start_svc_data = .); + KEEP(*(.svc_data)) + PROVIDE(__stop_svc_data = .); + } > RAM + + .fs_data : + { + PROVIDE(__start_fs_data = .); + KEEP(*(.fs_data)) + PROVIDE(__stop_fs_data = .); + } > RAM +} INSERT AFTER .data; + +INCLUDE "nrf52_common.ld" diff --git a/boards/promicro_nrf52840.json b/boards/promicro_nrf52840.json new file mode 100644 index 0000000..a4460f3 --- /dev/null +++ b/boards/promicro_nrf52840.json @@ -0,0 +1,79 @@ +{ + "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 NRF52840", + "mcu": "nrf52840", + "variant": "promicro_nrf52840", + "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", + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52.cfg" + }, + "frameworks": [ + "arduino", + "zephyr" + ], + "name": "ProMicro NRF52840", + "upload": { + "maximum_ram_size": 235520, + "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.nologo.tech/en/product/otherboard/NRF52840.html", + "vendor": "Nologo" + } \ No newline at end of file diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..4f233cd --- /dev/null +++ b/platformio.ini @@ -0,0 +1,93 @@ +[platformio] +default_envs = promicro_security + +[arduino_base] +framework = arduino +monitor_speed = 115200 +lib_deps = + SPI + Wire + jgromes/RadioLib @ ^7.6.0 + rweather/Crypto @ ^0.4.0 + adafruit/RTClib @ ^2.1.3 + electroniccats/CayenneLPP @ 1.6.1 + paulstoffregen/OneWire @ ^2.3.8 + milesburton/DallasTemperature @ ^3.11.0 +build_flags = + -w -DNDEBUG + -DRADIOLIB_STATIC_ONLY=1 + -DRADIOLIB_GODMODE=1 + -D ENABLE_ADVERT_ON_BOOT=1 + -D ENABLE_PRIVATE_KEY_IMPORT=1 + -D ENABLE_PRIVATE_KEY_EXPORT=1 + -D RADIOLIB_EXCLUDE_CC1101=1 + -D RADIOLIB_EXCLUDE_RF69=1 + -D RADIOLIB_EXCLUDE_SX1231=1 + -D RADIOLIB_EXCLUDE_SI443X=1 + -D RADIOLIB_EXCLUDE_RFM2X=1 + -D RADIOLIB_EXCLUDE_SX128X=1 + -D RADIOLIB_EXCLUDE_AFSK=1 + -D RADIOLIB_EXCLUDE_AX25=1 + -D RADIOLIB_EXCLUDE_HELLSCHREIBER=1 + -D RADIOLIB_EXCLUDE_MORSE=1 + -D RADIOLIB_EXCLUDE_APRS=1 + -D RADIOLIB_EXCLUDE_BELL=1 + -D RADIOLIB_EXCLUDE_RTTY=1 + -D RADIOLIB_EXCLUDE_SSTV=1 + +[nrf52_base] +extends = arduino_base +platform = nordicnrf52 +platform_packages = + framework-arduinoadafruitnrf52 @ https://github.com/meshcore-dev/Adafruit_nRF52_Arduino#d541301 +extra_scripts = scripts/create_uf2.py +build_flags = ${arduino_base.build_flags} + -D NRF52_PLATFORM + -D LFS_NO_ASSERT=1 + -D EXTRAFS=1 +lib_deps = + ${arduino_base.lib_deps} + https://github.com/oltaco/CustomLFS#0.2.2 + +[security_base] +extends = nrf52_base +build_flags = + ${nrf52_base.build_flags} + -I src + -I ../MeshCore/src + -I ../MeshCore/lib/ed25519 + -D USE_SX1262 + -D RADIO_CLASS=CustomSX1262 + -D WRAPPER_CLASS=CustomSX1262Wrapper + -D MAX_CONTACTS=50 + -D MAX_GROUP_CHANNELS=8 + -D OFFLINE_QUEUE_SIZE=32 + -D PATH_HASH_MODE=0 + -D NRF52_PLATFORM +lib_deps = + ${nrf52_base.lib_deps} + file://../MeshCore + densaugeo/base64 @ ~1.4.0 + Bluefruit52Lib + +[env:promicro_security] +extends = security_base +board = promicro_nrf52840 +board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld +board_upload.maximum_size = 712704 +build_flags = + ${security_base.build_flags} + -D PROMICRO + -I src/variants/promicro + -D LORA_FREQ=868.731018 + -D LORA_BW=62.5 + -D LORA_SF=7 + -D LORA_CR=7 + -I $PROJECT_PACKAGES_DIR/framework-arduinoadafruitnrf52/libraries/Bluefruit52Lib/src + -I $PROJECT_PACKAGES_DIR/framework-arduinoadafruitnrf52/libraries/Adafruit_nRFCrypto/src +build_src_filter = + +<*.cpp> + + + +<../../MeshCore/lib/ed25519/*.c> +upload_protocol = nrfutil +debug_tool = jlink diff --git a/scripts/add_external_sources.py b/scripts/add_external_sources.py new file mode 100644 index 0000000..b7c8736 --- /dev/null +++ b/scripts/add_external_sources.py @@ -0,0 +1,33 @@ +#!/usr/bin/python3 + +import glob +import os +from os.path import join, normpath + +Import("env") + +project_dir = env.subst("$PROJECT_DIR") +meshcore_dir = normpath(join(project_dir, "..", "MeshCore")) + +# Add MeshCore source files via build_src_filter override +# We need to add them before the build starts +meshcore_glob_patterns = [ + "src/*.cpp", + "src/helpers/*.cpp", + "src/helpers/radiolib/*.cpp", + "src/helpers/bridges/BridgeBase.cpp", + "src/helpers/nrf52/SerialBLEInterface.cpp", +] + +# Add source files to the build environment +for pattern in meshcore_glob_patterns: + full_pattern = normpath(join(meshcore_dir, pattern)) + files = glob.glob(full_pattern) + for f in sorted(files): + env.Append(SOURCES=[f]) + +# Also add T114 variant files if they exist +t114_pattern = normpath(join(meshcore_dir, "variants/heltec_t114/*.cpp")) +t114_files = glob.glob(t114_pattern) +for f in sorted(t114_files): + env.Append(SOURCES=[f]) diff --git a/scripts/create_uf2.py b/scripts/create_uf2.py new file mode 100644 index 0000000..314d635 --- /dev/null +++ b/scripts/create_uf2.py @@ -0,0 +1,31 @@ +#!/usr/bin/python3 + +import os + +Import("env") + +firmware_hex = "${BUILD_DIR}/${PROGNAME}.hex" +uf2_file = os.environ.get("UF2_FILE_PATH", "${BUILD_DIR}/${PROGNAME}.uf2") +meshcore_dir = os.path.normpath(os.path.join(env.subst("$PROJECT_DIR"), "..", "MeshCore")) +uf2conv = os.path.join(meshcore_dir, "bin", "uf2conv", "uf2conv.py") + +def create_uf2_action(source, target, env): + uf2_cmd = " ".join( + [ + '"$PYTHONEXE"', + '"' + uf2conv + '"', + '-f', '0xADA52840', + '-c', firmware_hex, + '-o', uf2_file, + ] + ) + env.Execute(uf2_cmd) + +env.AddCustomTarget( + name="create_uf2", + dependencies=firmware_hex, + actions=create_uf2_action, + title="Create UF2 file", + description="Use uf2conv to convert hex binary into uf2", + always_build=True, +) diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..cd7d191 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,251 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "target.h" + +using namespace Adafruit_LittleFS_Namespace; + +#ifdef PROMICRO +PromicroBoard board; +Module radio_module_obj(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI); +RADIO_CLASS radio_module(&radio_module_obj); +WRAPPER_CLASS radio_driver(radio_module, board); +VolatileRTCClock fallback_clock; +AutoDiscoverRTCClock rtc_clock(fallback_clock); +#else +NRF52Board board("Security"); +Module radio_module_obj(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI); +RADIO_CLASS radio_module(&radio_module_obj); +WRAPPER_CLASS radio_driver(radio_module, board); +AutoDiscoverRTCClock rtc_clock; +#endif + +StdRNG fast_rng; +SimpleMeshTables tables; + +OneWire oneWire(PIN_DS18B20); +DallasTemperature ds18b20(&oneWire); + +static bool g_armed = false; +static bool g_hall_alert = false; +static bool g_motion_alert = false; +static bool g_relay2_state = false; +static bool g_temp_requested = false; +static unsigned long g_temp_last_request = 0; +static float g_last_temperature = -127.0f; +static unsigned long g_last_hall_alert = 0; +static unsigned long g_last_motion_alert = 0; +static const unsigned long ALERT_COOLDOWN_MS = 600000; +static unsigned long g_boot_time = 0; +static bool g_startup_msg_sent = false; + +struct SecurityConfig { uint32_t magic; uint8_t armed; uint8_t relay2; }; +static const uint32_t CFG_MAGIC = 0x53454355; + +bool saveSecurityConfig() { + SecurityConfig cfg = { CFG_MAGIC, g_armed ? 1 : 0, g_relay2_state ? 1 : 0 }; + File f = InternalFS.open(CONFIG_FILE, FILE_O_WRITE); + if (!f) return false; + bool ok = f.write((const uint8_t*)&cfg, sizeof(cfg)) == sizeof(cfg); + f.close(); return ok; +} + +bool loadSecurityConfig() { + File f = InternalFS.open(CONFIG_FILE, FILE_O_READ); + if (!f) return false; + SecurityConfig cfg; + if (f.read(&cfg, sizeof(cfg)) != sizeof(cfg)) { f.close(); return false; } + f.close(); + if (cfg.magic != CFG_MAGIC) return false; + g_armed = cfg.armed; g_relay2_state = cfg.relay2; + return true; +} + +class SecurityMesh : public mesh::Mesh { +public: + mesh::GroupChannel mesh_channel; + bool channel_ready = false; + + SecurityMesh(mesh::Radio& r, mesh::RNG& rng, mesh::RTCClock& clock, SimpleMeshTables& t) + : Mesh(r, *new ArduinoMillis(), rng, clock, *new StaticPoolPacketManager(16), t) {} + + void setup_channel() { + uint8_t key[32]; + SHA256 sha; sha.reset(); sha.update((const uint8_t*)"1234", 4); sha.finalize(key, 32); + memset(mesh_channel.secret, 0, 32); + memcpy(mesh_channel.secret, key, 16); + mesh::Utils::sha256(mesh_channel.hash, sizeof(mesh_channel.hash), mesh_channel.secret, 16); + channel_ready = true; + } + + int searchChannelsByHash(const uint8_t* hash, mesh::GroupChannel channels[], int max_matches) override { + if (max_matches > 0 && channel_ready && memcmp(hash, mesh_channel.hash, sizeof(mesh_channel.hash)) == 0) { + channels[0] = mesh_channel; + return 1; + } + return 0; + } + + void send_text(const char* text) { + if (!channel_ready) return; + uint32_t ts = rtc_clock.getCurrentTime(); + uint8_t buf[256]; + memcpy(buf, &ts, 4); + buf[4] = 0; + int tlen = strlen(text); + if (tlen > 200) tlen = 200; + memcpy(buf + 5, text, tlen); + auto pkt = createGroupDatagram(PAYLOAD_TYPE_GRP_TXT, mesh_channel, buf, 5 + tlen); + if (pkt) sendFlood(pkt, (uint32_t)0, (uint8_t)1); + } + + void onGroupDataRecv(mesh::Packet* pkt, uint8_t type, const mesh::GroupChannel& channel, uint8_t* data, size_t len) override { + if (type != PAYLOAD_TYPE_GRP_TXT) return; + if (len < 5 || data[4] != 0) return; + + for (int i = 0; i < 3; i++) { digitalWrite(PIN_LED_STATUS, HIGH); delay(50); digitalWrite(PIN_LED_STATUS, LOW); delay(50); } + + int tlen = len - 5; + if (tlen > 250) tlen = 250; + char text[256]; + memcpy(text, data + 5, tlen); + text[tlen] = 0; + + int i = tlen; while (i > 0 && text[i-1] == ' ') { text[i-1] = 0; i--; } + for (int i = 0; text[i]; i++) { if (text[i] >= 'A' && text[i] <= 'Z') text[i] += 32; } + + const char* cmd = text; + const char* colon = strchr(text, ':'); + if (colon && colon[1] == ' ') cmd = colon + 2; + while (*cmd == ' ') cmd++; + + if (strcmp(cmd, "help") == 0) { + send_text("help:ohrana on,ohrana off,relay1 on,relay1 off,relay2 on,relay2 off,status,ver,scan,security_conf"); + } else if (strcmp(cmd, "ohrana on") == 0) { + g_armed = true; digitalWrite(PIN_RELAY_1, LOW); saveSecurityConfig(); send_text("ohrana on ok"); + } else if (strcmp(cmd, "ohrana off") == 0) { + g_armed = false; digitalWrite(PIN_RELAY_1, HIGH); saveSecurityConfig(); send_text("ohrana off ok"); + } else if (strcmp(cmd, "relay1 on") == 0) { + digitalWrite(PIN_RELAY_1, LOW); send_text("relay1 on ok"); + } else if (strcmp(cmd, "relay1 off") == 0) { + digitalWrite(PIN_RELAY_1, HIGH); send_text("relay1 off ok"); + } else if (strcmp(cmd, "relay2 on") == 0) { + g_relay2_state = true; digitalWrite(PIN_RELAY_2, LOW); send_text("relay2 on ok"); + } else if (strcmp(cmd, "relay2 off") == 0) { + g_relay2_state = false; digitalWrite(PIN_RELAY_2, HIGH); send_text("relay2 off ok"); + } else if (strcmp(cmd, "status") == 0) { + char r[128]; char tb[16]; float t = g_last_temperature; + if (t == -127.0f) strcpy(tb, "N/A"); else snprintf(tb, sizeof(tb), "%+.1fC", t); + snprintf(r, sizeof(r), "status:%s temp:%s hall:%s motion:%s", + g_armed ? "armed" : "off", tb, g_hall_alert ? "trig" : "ok", g_motion_alert ? "trig" : "ok"); + send_text(r); + } else if (strcmp(cmd, "ver") == 0) { + char r[64]; snprintf(r, sizeof(r), "build:%s %s", __DATE__, __TIME__); send_text(r); + } else if (strcmp(cmd, "scan") == 0) { + float sum = 0; + for (int j = 0; j < 20; j++) { sum += radio_driver.getCurrentRSSI(); delay(50); } + char r[32]; snprintf(r, sizeof(r), "noise:%.0fdBm", sum / 20); send_text(r); + } else if (strncmp(cmd, "security_conf", 13) == 0) { + const char* nc = cmd + 13; while (*nc == ' ') nc++; + if (strlen(nc) > 0 && strlen(nc) < 32) { + uint8_t key[32]; + SHA256 sha; sha.reset(); sha.update((const uint8_t*)nc, strlen(nc)); sha.finalize(key, 32); + memset(mesh_channel.secret, 0, 32); + memcpy(mesh_channel.secret, key, 16); + mesh::Utils::sha256(mesh_channel.hash, sizeof(mesh_channel.hash), mesh_channel.secret, 16); + char r[64]; snprintf(r, sizeof(r), "channel:%s", nc); send_text(r); + } else send_text("bad name"); + } + } +}; + +SecurityMesh the_mesh(radio_driver, fast_rng, rtc_clock, tables); + +bool radio_init() { +#ifdef PROMICRO + board.begin(); +#endif + SPI.setPins(P_LORA_MISO, P_LORA_SCLK, P_LORA_MOSI); + SPI.begin(); + if (radio_module.begin() != RADIOLIB_ERR_NONE) return false; + radio_module.setDio2AsRfSwitch(true); + radio_module.setTCXO(SX126X_DIO3_TCXO_VOLTAGE); + radio_module.setCurrentLimit(SX126X_CURRENT_LIMIT); + radio_driver.setParams(LORA_FREQ, LORA_BW, LORA_SF, LORA_CR); + radio_driver.setTxPower(LORA_TX_POWER); + radio_driver.setRxBoostedGainMode(true); + return true; +} + +void setup() { + Serial.begin(115200); + g_boot_time = millis(); + + NRF_WDT->CONFIG = (WDT_CONFIG_SLEEP_Run << WDT_CONFIG_SLEEP_Pos) | (WDT_CONFIG_HALT_Pause << WDT_CONFIG_HALT_Pos); + NRF_WDT->CRV = (WDT_TIMEOUT_MS * 32768) / 1000; + NRF_WDT->RREN = WDT_RREN_RR0_Enabled << WDT_RREN_RR0_Pos; + NRF_WDT->TASKS_START = 1; + + pinMode(PIN_REED, INPUT_PULLUP); pinMode(PIN_PIR, INPUT); ds18b20.begin(); + pinMode(PIN_RELAY_1, OUTPUT); pinMode(PIN_RELAY_2, OUTPUT); pinMode(PIN_LED_STATUS, OUTPUT); + digitalWrite(PIN_RELAY_1, HIGH); digitalWrite(PIN_RELAY_2, HIGH); digitalWrite(PIN_LED_STATUS, LOW); + + InternalFS.begin(); + loadSecurityConfig(); + if (g_armed) digitalWrite(PIN_RELAY_1, LOW); else digitalWrite(PIN_RELAY_1, HIGH); + digitalWrite(PIN_RELAY_2, g_relay2_state ? LOW : HIGH); + + if (!radio_init()) { + Serial.println("RADIO FAIL"); + while (1) { digitalWrite(PIN_LED_STATUS, HIGH); delay(100); digitalWrite(PIN_LED_STATUS, LOW); delay(100); } + } + + fast_rng.begin(radio_driver.getRngSeed()); + + the_mesh.setup_channel(); + the_mesh.begin(); + + Serial.println("OK"); +} + +void loop() { + NRF_WDT->RR[0] = WDT_RR_RR_Reload; + the_mesh.loop(); + rtc_clock.tick(); + + if (!g_startup_msg_sent && millis() - g_boot_time >= 10000) { + g_startup_msg_sent = true; + char m[80]; snprintf(m, sizeof(m), "poweron %s %s", __DATE__, __TIME__); + the_mesh.send_text(m); + } + + unsigned long now = millis(); + int reed = digitalRead(PIN_REED); + if (g_armed && reed == HIGH && !g_hall_alert && now - g_last_hall_alert > ALERT_COOLDOWN_MS) { + g_hall_alert = true; g_last_hall_alert = now; the_mesh.send_text("alert:hall"); + } + int pir = digitalRead(PIN_PIR); + if (g_armed && pir == HIGH && !g_motion_alert && now - g_last_motion_alert > ALERT_COOLDOWN_MS) { + g_motion_alert = true; g_last_motion_alert = now; the_mesh.send_text("alert:motion"); + } + if (g_hall_alert && reed == LOW) g_hall_alert = false; + if (g_motion_alert && pir == LOW) g_motion_alert = false; + + if (!g_temp_requested) { ds18b20.requestTemperatures(); g_temp_requested = true; g_temp_last_request = now; } + else if (now - g_temp_last_request > 750) { g_last_temperature = ds18b20.getTempCByIndex(0); g_temp_requested = false; } + + if (g_armed) { + static unsigned long lt = 0; static bool ls = false; + if (now - lt > 1000) { lt = now; ls = !ls; digitalWrite(PIN_LED_STATUS, ls); } + } else digitalWrite(PIN_LED_STATUS, LOW); +} diff --git a/src/target.h b/src/target.h new file mode 100644 index 0000000..4120f50 --- /dev/null +++ b/src/target.h @@ -0,0 +1,146 @@ +#pragma once + +#define RADIOLIB_STATIC_ONLY 1 + +#define USE_SX1262 +#define RADIO_CLASS CustomSX1262 +#define WRAPPER_CLASS CustomSX1262Wrapper + +#define SX126X_DIO2_AS_RF_SWITCH true +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 +#define SX126X_CURRENT_LIMIT 140 +#define SX126X_RX_BOOSTED_GAIN 1 + +#define LORA_FREQ 868.731018 +#define LORA_BW 62.5 +#define LORA_SF 7 +#define LORA_CR 7 +#ifndef LORA_TX_POWER +#define LORA_TX_POWER 22 +#endif +#define MAX_LORA_TX_POWER 22 + +#define PATH_HASH_MODE 0 + +#ifdef PROMICRO + #ifndef P_LORA_NSS + #define P_LORA_NSS 13 + #endif + #ifndef P_LORA_SCLK + #define P_LORA_SCLK 12 + #endif + #ifndef P_LORA_MOSI + #define P_LORA_MOSI 14 + #endif + #ifndef P_LORA_MISO + #define P_LORA_MISO 15 + #endif + #ifndef P_LORA_DIO_1 + #define P_LORA_DIO_1 11 + #endif + #ifndef P_LORA_RESET + #define P_LORA_RESET 10 + #endif + #ifndef P_LORA_BUSY + #define P_LORA_BUSY 16 + #endif + #define SX126X_POWER_EN 21 +#else + #ifndef P_LORA_NSS + #define P_LORA_NSS 30 + #endif + #ifndef P_LORA_SCLK + #define P_LORA_SCLK 29 + #endif + #ifndef P_LORA_MOSI + #define P_LORA_MOSI 31 + #endif + #ifndef P_LORA_MISO + #define P_LORA_MISO 2 + #endif + #ifndef P_LORA_DIO_1 + #define P_LORA_DIO_1 7 + #endif + #ifndef P_LORA_RESET + #define P_LORA_RESET 6 + #endif + #ifndef P_LORA_BUSY + #define P_LORA_BUSY 10 + #endif +#endif + +#ifdef PROMICRO + #ifndef PIN_REED + #define PIN_REED 0 + #endif + #ifndef PIN_PIR + #define PIN_PIR 1 + #endif + #ifndef PIN_DS18B20 + #define PIN_DS18B20 2 + #endif +#else + #ifndef PIN_REED + #define PIN_REED 12 + #endif + #ifndef PIN_PIR + #define PIN_PIR 11 + #endif + #ifndef PIN_DS18B20 + #define PIN_DS18B20 3 + #endif +#endif + +#ifdef PROMICRO + #ifndef PIN_RELAY_1 + #define PIN_RELAY_1 3 + #endif + #ifndef PIN_RELAY_2 + #define PIN_RELAY_2 4 + #endif + #ifndef PIN_LED_STATUS + #define PIN_LED_STATUS 22 + #endif +#else + #ifndef PIN_RELAY_1 + #define PIN_RELAY_1 4 + #endif + #ifndef PIN_RELAY_2 + #define PIN_RELAY_2 5 + #endif + #ifndef PIN_LED_STATUS + #define PIN_LED_STATUS 13 + #endif +#endif + +#define CONFIG_FILE "/security.cfg" +#define CHANNEL_FILE "/channel.cfg" +#define WDT_TIMEOUT_MS 10000 + +#include +#include +#include + +#ifdef PROMICRO +#include +#elif defined(HELTEC_T114) +#include +#endif + +#ifdef PROMICRO +extern PromicroBoard board; +extern RADIO_CLASS radio_module; +extern WRAPPER_CLASS radio_driver; +#elif defined(HELTEC_T114) +extern T114Board board; +extern RADIO_CLASS radio_module; +extern WRAPPER_CLASS radio_driver; +#else +extern RADIO_CLASS radio_module; +extern WRAPPER_CLASS radio_driver; +#endif + +extern AutoDiscoverRTCClock rtc_clock; + +bool radio_init(); +mesh::LocalIdentity radio_new_identity(); diff --git a/src/variants/T114Board.cpp b/src/variants/T114Board.cpp new file mode 100644 index 0000000..c03d39a --- /dev/null +++ b/src/variants/T114Board.cpp @@ -0,0 +1,59 @@ +#include "T114Board.h" + +#include +#include + +#ifdef NRF52_POWER_MANAGEMENT +// Static configuration for power management +// Values come from variant.h defines +const PowerMgtConfig power_config = { + .lpcomp_ain_channel = PWRMGT_LPCOMP_AIN, + .lpcomp_refsel = PWRMGT_LPCOMP_REFSEL, + .voltage_bootlock = PWRMGT_VOLTAGE_BOOTLOCK +}; + +void T114Board::initiateShutdown(uint8_t reason) { +#if ENV_INCLUDE_GPS == 1 + pinMode(GPS_EN, OUTPUT); + digitalWrite(GPS_EN, LOW); +#endif + digitalWrite(SX126X_POWER_EN, LOW); + + bool enable_lpcomp = (reason == SHUTDOWN_REASON_LOW_VOLTAGE || + reason == SHUTDOWN_REASON_BOOT_PROTECT); + pinMode(PIN_BAT_CTL, OUTPUT); + digitalWrite(PIN_BAT_CTL, enable_lpcomp ? HIGH : LOW); + + if (enable_lpcomp) { + configureVoltageWake(power_config.lpcomp_ain_channel, power_config.lpcomp_refsel); + } + + enterSystemOff(reason); +} +#endif // NRF52_POWER_MANAGEMENT + +void T114Board::begin() { + NRF52Board::begin(); + + pinMode(PIN_VBAT_READ, INPUT); + +#if defined(PIN_BOARD_SDA) && defined(PIN_BOARD_SCL) + Wire.setPins(PIN_BOARD_SDA, PIN_BOARD_SCL); +#endif + + Wire.begin(); + +#ifdef P_LORA_TX_LED + pinMode(P_LORA_TX_LED, OUTPUT); + digitalWrite(P_LORA_TX_LED, HIGH); +#endif + + pinMode(SX126X_POWER_EN, OUTPUT); +#ifdef NRF52_POWER_MANAGEMENT + // Boot voltage protection check (may not return if voltage too low) + // We need to call this after we configure SX126X_POWER_EN as output but before we pull high + checkBootVoltage(&power_config); +#endif + digitalWrite(SX126X_POWER_EN, HIGH); + delay(10); // give sx1262 some time to power up +} \ No newline at end of file diff --git a/src/variants/T114Board.h b/src/variants/T114Board.h new file mode 100644 index 0000000..f27dc29 --- /dev/null +++ b/src/variants/T114Board.h @@ -0,0 +1,59 @@ +#pragma once + +#include +#include +#include + +// built-ins +#define PIN_VBAT_READ 4 +#define PIN_BAT_CTL 6 +#define MV_LSB (3000.0F / 4096.0F) // 12-bit ADC with 3.0V input range + +class T114Board : public NRF52BoardDCDC { +protected: +#ifdef NRF52_POWER_MANAGEMENT + void initiateShutdown(uint8_t reason) override; +#endif + +public: + T114Board() : NRF52Board("T114_OTA") {} + void begin(); + +#if defined(P_LORA_TX_LED) + void onBeforeTransmit() override { + digitalWrite(P_LORA_TX_LED, LOW); // turn TX LED on + } + void onAfterTransmit() override { + digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED off + } +#endif + + uint16_t getBattMilliVolts() override { + int adcvalue = 0; + analogReadResolution(12); + analogReference(AR_INTERNAL_3_0); + pinMode(PIN_BAT_CTL, OUTPUT); // battery adc can be read only ctrl pin 6 set to high + digitalWrite(PIN_BAT_CTL, 1); + + delay(10); + adcvalue = analogRead(PIN_VBAT_READ); + digitalWrite(6, 0); + + return (uint16_t)((float)adcvalue * MV_LSB * 4.9); + } + + const char* getManufacturerName() const override { + return "Heltec T114"; + } + + void powerOff() override { +#ifdef LED_PIN + digitalWrite(LED_PIN, HIGH); +#endif +#if ENV_INCLUDE_GPS == 1 + pinMode(GPS_EN, OUTPUT); + digitalWrite(GPS_EN, LOW); +#endif + sd_power_system_off(); + } +}; diff --git a/src/variants/promicro/PromicroBoard.cpp b/src/variants/promicro/PromicroBoard.cpp new file mode 100644 index 0000000..7011521 --- /dev/null +++ b/src/variants/promicro/PromicroBoard.cpp @@ -0,0 +1,25 @@ +#include +#include + +#include "PromicroBoard.h" + +void PromicroBoard::begin() { + NRF52Board::begin(); + btn_prev_state = HIGH; + + pinMode(PIN_VBAT_READ, INPUT); + + #ifdef BUTTON_PIN + pinMode(BUTTON_PIN, INPUT_PULLUP); + #endif + + #if defined(PIN_BOARD_SDA) && defined(PIN_BOARD_SCL) + Wire.setPins(PIN_BOARD_SDA, PIN_BOARD_SCL); + #endif + + Wire.begin(); + + pinMode(SX126X_POWER_EN, OUTPUT); + digitalWrite(SX126X_POWER_EN, HIGH); + delay(10); // give sx1262 some time to power up +} \ No newline at end of file diff --git a/src/variants/promicro/PromicroBoard.h b/src/variants/promicro/PromicroBoard.h new file mode 100644 index 0000000..7b6afb1 --- /dev/null +++ b/src/variants/promicro/PromicroBoard.h @@ -0,0 +1,79 @@ +#pragma once + +#include +#include +#include + +#define P_LORA_NSS 13 //P1.13 45 +#define P_LORA_DIO_1 11 //P0.10 10 +#define P_LORA_RESET 10 //P0.09 9 +#define P_LORA_BUSY 16 //P0.29 29 +#define P_LORA_MISO 15 //P0.02 2 +#define P_LORA_SCLK 12 //P1.11 43 +#define P_LORA_MOSI 14 //P1.15 47 +#define SX126X_POWER_EN 21 //P0.13 13 +#define SX126X_RXEN 2 //P0.17 +#define SX126X_TXEN RADIOLIB_NC +#define SX126X_DIO2_AS_RF_SWITCH true +#define SX126X_DIO3_TCXO_VOLTAGE (1.8f) + +#define PIN_VBAT_READ 17 +#define ADC_MULTIPLIER (1.815f) // dependent on voltage divider resistors. TODO: more accurate battery tracking + +class PromicroBoard : public NRF52BoardDCDC { +protected: + uint8_t btn_prev_state; + float adc_mult = ADC_MULTIPLIER; + +public: + PromicroBoard() : NRF52Board("ProMicro_OTA") {} + void begin(); + + #define BATTERY_SAMPLES 8 + + uint16_t getBattMilliVolts() override { + analogReadResolution(12); + + uint32_t raw = 0; + for (int i = 0; i < BATTERY_SAMPLES; i++) { + raw += analogRead(PIN_VBAT_READ); + } + raw = raw / BATTERY_SAMPLES; + return (adc_mult * raw); + } + + bool setAdcMultiplier(float multiplier) override { + if (multiplier == 0.0f) { + adc_mult = ADC_MULTIPLIER;} + else { + adc_mult = multiplier; + } + return true; + } + float getAdcMultiplier() const override { + if (adc_mult == 0.0f) { + return ADC_MULTIPLIER; + } else { + return adc_mult; + } + } + + const char* getManufacturerName() const override { + return "ProMicro DIY"; + } + + int buttonStateChanged() { + #ifdef BUTTON_PIN + uint8_t v = digitalRead(BUTTON_PIN); + if (v != btn_prev_state) { + btn_prev_state = v; + return (v == LOW) ? 1 : -1; + } + #endif + return 0; + } + + void powerOff() override { + sd_power_system_off(); + } +}; diff --git a/src/variants/promicro/variant.cpp b/src/variants/promicro/variant.cpp new file mode 100644 index 0000000..0a4c3aa --- /dev/null +++ b/src/variants/promicro/variant.cpp @@ -0,0 +1,15 @@ +#include "variant.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +const uint32_t g_ADigitalPinMap[] = { + 8, 6, 17, 20, 22, 24, 32, 11, 36, 38, + 9, 10, 43, 45, 47, 2, 29, 31, + 33, 34, 37, + 13, 15 +}; + +void initVariant() +{ +} + diff --git a/src/variants/promicro/variant.h b/src/variants/promicro/variant.h new file mode 100644 index 0000000..98489da --- /dev/null +++ b/src/variants/promicro/variant.h @@ -0,0 +1,82 @@ +/* + * variant.h + * Copyright (C) 2023 Seeed K.K. + * MIT License + */ + + #pragma once + + #include "WVariant.h" + + //////////////////////////////////////////////////////////////////////////////// + // Low frequency clock source + +#define VARIANT_MCK (64000000ul) + +//#define USE_LFXO // 32.768 kHz crystal oscillator +#define USE_LFRC // 32.768 kHz RC oscillator + +//////////////////////////////////////////////////////////////////////////////// +// Power + +#define PIN_EXT_VCC (21) +#define EXT_VCC (PIN_EXT_VCC) + +#define BATTERY_PIN (17) +#define ADC_RESOLUTION 12 + +//////////////////////////////////////////////////////////////////////////////// +// Number of pins + +#define PINS_COUNT (23) +#define NUM_DIGITAL_PINS (23) +#define NUM_ANALOG_INPUTS (3) +#define NUM_ANALOG_OUTPUTS (0) + +//////////////////////////////////////////////////////////////////////////////// +// UART pin definition + +#define PIN_SERIAL1_TX (1) +#define PIN_SERIAL1_RX (0) + +//////////////////////////////////////////////////////////////////////////////// +// I2C pin definition + +#define WIRE_INTERFACES_COUNT 2 + +#define PIN_WIRE_SDA (6) +#define PIN_WIRE_SCL (7) +#define PIN_WIRE1_SDA (13) +#define PIN_WIRE1_SCL (14) + +//////////////////////////////////////////////////////////////////////////////// +// SPI pin definition + +#define SPI_INTERFACES_COUNT 2 + +#define PIN_SPI_SCK (2) +#define PIN_SPI_MISO (3) +#define PIN_SPI_MOSI (4) + +#define PIN_SPI_NSS (5) + +#define PIN_SPI1_SCK (18) +#define PIN_SPI1_MISO (19) +#define PIN_SPI1_MOSI (20) + +//////////////////////////////////////////////////////////////////////////////// +// Builtin LEDs + +#define PIN_LED (22) +#define LED_PIN PIN_LED +#define LED_BLUE PIN_LED +#define LED_BUILTIN PIN_LED +#define LED_STATE_ON 1 + +//////////////////////////////////////////////////////////////////////////////// +// Builtin buttons + +#define PIN_BUTTON1 (6) +#define BUTTON_PIN PIN_BUTTON1 + + diff --git a/src/variants/variant.cpp b/src/variants/variant.cpp new file mode 100644 index 0000000..2bca56a --- /dev/null +++ b/src/variants/variant.cpp @@ -0,0 +1,15 @@ +#include "variant.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +const uint32_t g_ADigitalPinMap[] = { + 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, 32, 33, 34, 35, 36, 37, 38, 39, + 40, 41, 42, 43, 44, 45, 46, 47 +}; + +void initVariant() +{ + pinMode(PIN_USER_BTN, INPUT); +} diff --git a/src/variants/variant.h b/src/variants/variant.h new file mode 100644 index 0000000..bfb4484 --- /dev/null +++ b/src/variants/variant.h @@ -0,0 +1,142 @@ +/* + * variant.h + * Copyright (C) 2023 Seeed K.K. + * MIT License + */ + +#pragma once + +#include "WVariant.h" + +//////////////////////////////////////////////////////////////////////////////// +// Low frequency clock source + +#define USE_LFXO // 32.768 kHz crystal oscillator +#define VARIANT_MCK (64000000ul) + +#define WIRE_INTERFACES_COUNT (2) + +//////////////////////////////////////////////////////////////////////////////// +// Power + +#define NRF_APM +#define PIN_3V3_EN (38) + +#define BATTERY_PIN (4) +#define ADC_MULTIPLIER (4.90F) + +#define ADC_RESOLUTION (14) +#define BATTERY_SENSE_RES (12) + +#define AREF_VOLTAGE (3.0) + +// Power management boot protection threshold (millivolts) +// Set to 0 to disable boot protection +#define PWRMGT_VOLTAGE_BOOTLOCK 3300 // Won't boot below this voltage (mV) +// LPCOMP wake configuration (voltage recovery from SYSTEMOFF) +// AIN2 = P0.04 = BATTERY_PIN / PIN_VBAT_READ +#define PWRMGT_LPCOMP_AIN 2 +#define PWRMGT_LPCOMP_REFSEL 1 // 2/8 VDD (~3.68-4.04V) + +//////////////////////////////////////////////////////////////////////////////// +// Number of pins + +#define PINS_COUNT (48) +#define NUM_DIGITAL_PINS (48) +#define NUM_ANALOG_INPUTS (1) +#define NUM_ANALOG_OUTPUTS (0) + +//////////////////////////////////////////////////////////////////////////////// +// UART pin definition + +#define PIN_SERIAL1_RX (37) +#define PIN_SERIAL1_TX (39) + +#define PIN_SERIAL2_RX (9) +#define PIN_SERIAL2_TX (10) + +//////////////////////////////////////////////////////////////////////////////// +// I2C pin definition + +#define PIN_WIRE_SDA (26) // P0.26 +#define PIN_WIRE_SCL (27) // P0.27 + +#define PIN_WIRE1_SDA (7) // P0.8 +#define PIN_WIRE1_SCL (8) // P0.7 + +//////////////////////////////////////////////////////////////////////////////// +// SPI pin definition + +#define SPI_INTERFACES_COUNT (2) + +#define PIN_SPI_MISO (23) +#define PIN_SPI_MOSI (22) +#define PIN_SPI_SCK (19) +#define PIN_SPI_NSS (24) + +//////////////////////////////////////////////////////////////////////////////// +// Builtin LEDs + +#define LED_BUILTIN (35) +#define PIN_LED LED_BUILTIN +#define LED_RED LED_BUILTIN +#define LED_BLUE (-1) // No blue led, prevents Bluefruit flashing the green LED during advertising +#define LED_PIN LED_BUILTIN + +#define LED_STATE_ON LOW + +#define PIN_NEOPIXEL (14) +#define NEOPIXEL_NUM (2) + +//////////////////////////////////////////////////////////////////////////////// +// Builtin buttons + +#define PIN_BUTTON1 (42) +#define BUTTON_PIN PIN_BUTTON1 + +// #define PIN_BUTTON2 (11) +// #define BUTTON_PIN2 PIN_BUTTON2 + +#define PIN_USER_BTN BUTTON_PIN + +#define EXTERNAL_FLASH_DEVICES MX25R1635F +#define EXTERNAL_FLASH_USE_QSPI + +//////////////////////////////////////////////////////////////////////////////// +// Lora + +#define USE_SX1262 +#define LORA_CS (24) +#define SX126X_DIO1 (20) +#define SX126X_BUSY (17) +#define SX126X_RESET (25) +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 + +#define PIN_SPI1_MISO (43) +#define PIN_SPI1_MOSI (41) +#define PIN_SPI1_SCK (40) + +//////////////////////////////////////////////////////////////////////////////// +// Buzzer + +// #define PIN_BUZZER (46) + + +//////////////////////////////////////////////////////////////////////////////// +// GPS + +#define GPS_EN (21) +#define GPS_RESET (38) +#define PIN_GPS_RX (39) // This is for bits going TOWARDS the GPS +#define PIN_GPS_TX (37) // This is for bits going TOWARDS the CPU + +//////////////////////////////////////////////////////////////////////////////// +// TFT +#define PIN_TFT_SCL (40) +#define PIN_TFT_SDA (41) +#define PIN_TFT_RST (2) +#define PIN_TFT_VDD_CTL (3) +#define PIN_TFT_LEDA_CTL (15) +#define PIN_TFT_CS (11) +#define PIN_TFT_DC (12)