37 Commits
v1.0 ... v1.2

Author SHA1 Message Date
UA1ZBE
7e978fcc9c update docs: relay3/4 pins, inverted logic, all relays independent 2026-07-22 15:51:39 +03:00
UA1ZBE
9ca5530c73 add relay3 (pin5), relay4 (pin6); invert logic: ON=HIGH, OFF=LOW 2026-07-22 15:49:23 +03:00
UA1ZBE
1430bba3f4 relay1 independent from ohrana, both relays saved 2026-07-22 14:09:15 +03:00
UA1ZBE
e68a762e88 docs: add TZ+3 mention, remove stale line ref 2026-07-22 14:03:44 +03:00
UA1ZBE
9b1c8f3497 Revert "add name set, beacon commands"
This reverts commit acc36cf3ad.
2026-07-22 14:00:37 +03:00
UA1ZBE
0b01526a39 Revert "docs: add name, name set, beacon"
This reverts commit 62cf8fc3f4.
2026-07-22 14:00:37 +03:00
UA1ZBE
fb30065edd Revert "fix beacon: init Ed25519 identity for valid advert signature"
This reverts commit d487b84926.
2026-07-22 14:00:37 +03:00
UA1ZBE
87ac515063 Revert "fix beacon: add onAdvertRecv to display received beacons"
This reverts commit 8b21bccac2.
2026-07-22 14:00:37 +03:00
UA1ZBE
9deb046ae3 Revert "auto beacon every hour + on startup via AdvertDataBuilder"
This reverts commit d49443f66c.
2026-07-22 14:00:37 +03:00
UA1ZBE
d49443f66c auto beacon every hour + on startup via AdvertDataBuilder 2026-07-22 13:55:05 +03:00
UA1ZBE
8b21bccac2 fix beacon: add onAdvertRecv to display received beacons 2026-07-22 13:44:50 +03:00
UA1ZBE
d487b84926 fix beacon: init Ed25519 identity for valid advert signature 2026-07-22 13:39:33 +03:00
UA1ZBE
62cf8fc3f4 docs: add name, name set, beacon 2026-07-22 13:32:59 +03:00
UA1ZBE
acc36cf3ad add name set, beacon commands 2026-07-22 13:32:35 +03:00
UA1ZBE
d3990516b5 add TZ offset +3h (MSK) for time display/set 2026-07-22 13:00:55 +03:00
UA1ZBE
dfdd6ab343 update README: add snr, remove canal 2026-07-22 12:56:42 +03:00
UA1ZBE
cb2a33c97e add snr command, update docs 2026-07-22 12:54:44 +03:00
UA1ZBE
c754bfb7f8 Add time command (query), update docs 2026-07-22 11:12:54 +03:00
UA1ZBE
99e684dd4f Update docs: time command 2026-07-22 11:03:10 +03:00
UA1ZBE
6e77419828 Add time s command to set RTC date/time 2026-07-22 11:02:49 +03:00
UA1ZBE
166fffa2ce Update docs: uptime, kontrol, reboot 2026-07-22 10:54:17 +03:00
UA1ZBE
4a05d81585 Add kontrol command for auto status interval 2026-07-22 10:46:47 +03:00
UA1ZBE
d215c12038 Add uptime command 2026-07-22 10:41:45 +03:00
UA1ZBE
966ef26c5e Add reboot command 2026-07-22 10:36:59 +03:00
UA1ZBE
7886291759 Revert BLE: restore working state without BLE 2026-07-22 10:34:18 +03:00
UA1ZBE
aea9860bbf Add BLE UART for channel configuration 2026-07-22 10:23:16 +03:00
UA1ZBE
92427da26a Fix canal parsing: replace sscanf with strchr 2026-07-22 10:12:24 +03:00
UA1ZBE
b7857a029b Update docs: canal command replaces security_conf 2026-07-22 10:04:35 +03:00
UA1ZBE
e94dbcfb55 Add canal command: save channel to flash and reboot 2026-07-22 10:04:14 +03:00
UA1ZBE
0cbb75853d Update README: add relay command 2026-07-22 09:53:15 +03:00
UA1ZBE
287da12a83 Update docs: relay command, battery status, 5s delay 2026-07-22 09:45:47 +03:00
UA1ZBE
db8a358573 Add relay command to show both relay states 2026-07-22 09:39:05 +03:00
UA1ZBE
c576c571e5 Add battery voltage to status response 2026-07-22 09:29:35 +03:00
UA1ZBE
307e27de60 Add 5s deferred response for commands 2026-07-22 09:20:13 +03:00
UA1ZBE
2909d069d0 Fix WIRING.md: use correct nRF GPIO port names (P0.xx, P1.xx) 2026-07-22 08:16:36 +03:00
UA1ZBE
6978394307 Add wiring diagram 2026-07-22 08:03:59 +03:00
UA1ZBE
ade6f3d433 Add README.md with project description 2026-07-22 08:02:01 +03:00
6 changed files with 507 additions and 61 deletions

View File

@@ -106,16 +106,41 @@ All commands are processed in `onGroupDataRecv()`:
| 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` |
| `ohrana on` | arm | `ohrana on ok` |
| `ohrana off` | disarm | `ohrana off ok` |
| `relay1 on` | relay1 ON (HIGH) | `relay1 on ok` |
| `relay1 off` | relay1 OFF (LOW) | `relay1 off ok` |
| `relay2 on` | relay2 ON (HIGH) | `relay2 on ok` |
| `relay2 off` | relay2 OFF (LOW) | `relay2 off ok` |
| `relay3 on` | relay3 ON (HIGH) | `relay3 on ok` |
| `relay3 off` | relay3 OFF (LOW) | `relay3 off ok` |
| `relay4 on` | relay4 ON (HIGH) | `relay4 on ok` |
| `relay4 off` | relay4 OFF (LOW) | `relay4 off ok` |
| `relay` | state of all relays | `relay:1on 2off 3on 4off` |
| `status` | report state | `status:armed/off temp:±X.XC hall:trig/ok motion:trig/ok bat:X.XXV/USB` |
| `ver` | build date | `build:Mon DD YYYY HH:MM:SS` |
| `scan` | noise floor | `noise:-XXXdBm` |
| `security_conf <name>` | change channel | `channel:<name>` |
| `snr` | RSSI и SNR последнего пакета | `rssi:-78dBm snr:+6.5dB` |
| `uptime` | device uptime | `uptime:1d 3h 15m` |
| `kontrol <min>` | auto-status every N min | `kontrol 60 min ok` |
| `kontrol off` | disable auto-status | `kontrol off` |
| `reboot` | reboot device | `rebooting` |
| `time` | show RTC date/time (MSK, UTC+3) | `time:22.07.2026 11:00` |
| `time s DD.MM.YYYY HH.MM` | set RTC date/time | `time set 22.07.2026 11.00` |
### Response Delay
All command responses are sent with a **5-second delay** (non-blocking, via `defer_response`).
The action (relay toggle, armed state change) happens immediately — only the reply text is deferred.
Alerts (`alert:hall`, `alert:motion`) and startup (`poweron`) are sent immediately without delay.
### Special Commands
- **`snr`** — returns `rssi:-78dBm snr:+6.5dB` based on the last received packet. Uses `radio_driver.getLastRSSI()` and `pkt->getSNR()`. Stored in `last_rssi` / `last_snr` fields, updated on every `onGroupDataRecv`.
- **`reboot`** — sends `rebooting` immediately (not deferred), blinks LED 5 times, then `NVIC_SystemReset()`.
- **`uptime`** — calculates uptime from `g_boot_time` (set at boot). Format: `uptime:1d 3h 15m` (days/hours/minutes).
- **`kontrol <min>`** — sets `g_auto_status_interval = min * 60000`. In `loop()`, when `millis() - g_last_auto_status >= interval`, sends status. Range: 11440 minutes.
### Auto-Sent Messages
- **Startup:** `poweron Mon DD YYYY HH:MM:SS` — sent 10s after boot.
@@ -180,7 +205,7 @@ pio run -e promicro_security -t upload
pio device monitor -b 115200
```
Current resource usage: RAM 8%, Flash 36.5%.
Current resource usage: RAM 8.1%, Flash 36.8%.
---
@@ -202,14 +227,14 @@ The LED blink (3 fast flashes) is the first thing in `onGroupDataRecv`. If LED d
- `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.
1. Add `else if` block in `onGroupDataRecv()`.
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.
- `SecurityConfig`: armed + relay1/2/3/4 state saved as binary struct.
- File: `/security.cfg`, magic `0x53454355`.
---

54
README.md Normal file
View File

@@ -0,0 +1,54 @@
# Security Mesh Node
LoRa mesh-устройство охраны на базе nRF52840 (ProMicro) и SX1262.
Работает по радиоканалу через MeshCore — без интернета, Bluetooth и проводов.
## Возможности
- Управление четырьмя реле
- Датчик Холла (геркон) — оповещение об открытии двери/окна
- PIR датчик движения
- DS18B20 температура
- Светодиодная индикация состояния
- Сохранение конфигурации в энергонезависимую память
- SNR — RSSI и SNR последнего принятого пакета
## Аппарат
- **SoC:** nRF52840 (Adafruit ProMicro nRF52840)
- **Радио:** SX1262 (868 МГц, SF7, 62.5 кГц)
- **Память:** 256 КБ ОЗУ, 712 КБ Flash (занято 37%)
## Команды
| Команда | Действие |
|---------|---------|
| `ohrana on` | Включить охрану |
| `ohrana off` | Выключить охрану |
| `relay1 on/off` | Управление реле 1 |
| `relay2 on/off` | Управление реле 2 |
| `relay3 on/off` | Управление реле 3 |
| `relay4 on/off` | Управление реле 4 |
| `relay` | Состояние всех реле |
| `status` | Состояние устройства (включая bat) |
| `ver` | Версия прошивки |
| `scan` | Уровень шума в эфире |
| `snr` | RSSI и SNR последнего пакета |
| `uptime` | Время работы |
| `kontrol <min>` | Автоотправка статуса каждые N мин |
| `kontrol off` | Отключить автоотправку |
| `reboot` | Перезагрузить устройство |
| `time` | Показать дату/время |
| `time s DD.MM.YYYY HH.MM` | Установить дату/время |
| `help` | Список команд |
## Релизы
Готовые прошивки (.hex, .zip, .uf2) — в [Releases](https://git2.ua1zbe.ru/ua1zbe/security-new/releases).
## Сборка
```bash
pio run -e promicro_security
pio run -e promicro_security -t upload
```

View File

@@ -24,22 +24,33 @@ Security Node — LoRa mesh-устройство, которое следит з
| Команда | Что делает | Пример ответа |
|---------|-----------|---------------|
| `help` | Список всех команд | `help:ohrana on,ohrana off,...` |
| `ohrana on` | **Включить охрану** — датчики активны, реле 1 замкнуто | `ohrana on ok` |
| `ohrana off` | **Выключить охрану** — датчики не активны, реле 1 разомкнуто | `ohrana off ok` |
| `relay1 on` | Включить реле 1 (независимо от охраны) | `relay1 on ok` |
| `ohrana on` | **Включить охрану** — датчики активны | `ohrana on ok` |
| `ohrana off` | **Выключить охрану** — датчики не активны | `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` |
| `relay3 on` | Включить реле 3 | `relay3 on ok` |
| `relay3 off` | Выключить реле 3 | `relay3 off ok` |
| `relay4 on` | Включить реле 4 | `relay4 on ok` |
| `relay4 off` | Выключить реле 4 | `relay4 off ok` |
| `relay` | Состояние всех реле | `relay:1on 2off 3on 4off` |
| `status` | Показать состояние устройства | `status:armed temp:+23.5C hall:ok motion:trig bat:4.12V` |
| `ver` | Версия прошивки (дата сборки) | `build:Jul 21 2026 15:30:00` |
| `scan` | Измерить уровень шума в эфире | `noise:-112dBm` |
| `security_conf <имя>` | Сменить канал (пароль из `<имя>`) | `channel:mysecret` |
| `snr` | RSSI и SNR последнего принятого пакета | `rssi:-78dBm snr:+6.5dB` |
| `uptime` | Время работы устройства | `uptime:1d 3h 15m` |
| `kontrol <мин>` | Автоотправка статуса каждые N мин | `kontrol 60 min ok` |
| `kontrol off` | Отключить автоотправку статуса | `kontrol off` |
| `reboot` | Перезагрузить устройство | `rebooting` |
| `time` | Показать дату и время | `time:22.07.2026 11:00` |
| `time s DD.MM.YYYY HH.MM` | Установить дату и время | `time set 22.07.2026 11.00` |
---
## Поля статуса
Ответ: `status:armed/off temp:+XX.XC/N/A hall:trig/ok motion:trig/ok`
Ответ: `status:armed/off temp:+XX.XC/N/A hall:trig/ok motion:trig/ok bat:X.XXV/USB`
| Поле | Значение |
|------|----------|
@@ -51,6 +62,8 @@ Security Node — LoRa mesh-устройство, которое следит з
| `hall:trig` | Геркон разомкнулся (дверь/окно открыли) |
| `motion:ok` | Движения нет |
| `motion:trig` | Движение обнаружено |
| `bat:4.12V` | Напряжение батареи |
| `bat:USB` | Питание от USB (нет батареи) |
---
@@ -90,30 +103,46 @@ Security Node — LoRa mesh-устройство, которое следит з
## Реле
- **Реле 1** — управляется `ohrana on/off` и `relay1 on/off`.
Охрана включена (`ohrana on`): **замкнуто** (NC разомкнуты, NO замкнуты).
Охрана выключена (`ohrana off`): **разомкнуто**.
Все реле независимы, состояние каждого сохраняется при перезагрузке.
Логика: ON = высокий уровень (HIGH), OFF = низкий (LOW).
- **Реле 2** — независимое, только `relay2 on/off`.
Состояние сохраняется при перезагрузке.
- **Реле 1** (P0.20) — `relay1 on/off`
- **Реле 2** (P0.22) — `relay2 on/off`
- **Реле 3** (P0.24) — `relay3 on/off`
- **Реле 4** (P1.00) — `relay4 on/off`
---
## Смена канала (`security_conf`)
## Автоотправка статуса (`kontrol`)
По умолчанию канал формируется из пароля `"1234"`.
Чтобы сменить:
`kontrol 60` — устройство будет само отправлять `status` каждые 60 минут.
`kontrol off` — отключить. Интервал от 1 до 1440 минут (1 минута — 24 часа).
```
security_conf мойпароль
```
## Качество сигнала (`snr`)
Ответ: `channel:мойпароль`
`snr` — показывает RSSI (уровень сигнала в dBm) и SNR (соотношение сигнал/шум в dB) последнего принятого пакета. Полезно для оценки качества связи и при настройке антенны. Чем ближе RSSI к 0 и чем выше SNR — тем лучше.
**Важно:** Все устройства должны быть на одном канале. После смены канала связь будет только с теми узлами, у которых такой же пароль.
## Синхронизация времени (`time`)
`time` — показать текущее время устройства (МСК, UTC+3).
`time s 22.07.2026 11.00` — установить дату и время. Формат: `time s DD.MM.YYYY HH.MM`
Время используется для отметок в сообщениях. После перезагрузки сбрасывается (если нет внешнего RTC).
## Перезагрузка (`reboot`)
`reboot` — устройство отправит `rebooting` и перезагрузится.
## Время работы (`uptime`)
`uptime` — показывает, сколько времени устройство работает без перезагрузки.
Пример: `uptime:1d 3h 15m` (1 день, 3 часа, 15 минут).
---
## Задержка ответа
Ответ на любую команду приходит **через 5 секунд**. Само действие (включение реле, смена охраны) выполняется мгновенно, но ответ отправляется с задержкой. Оповещения и `poweron` отправляются без задержки.
## Советы
- Если команда не получила ответа: проверьте канал, питание, дальность связи. LED должен мигнуть 3 раза при получении команды — если не мигнул, сообщение не дошло.

192
WIRING.md Normal file
View File

@@ -0,0 +1,192 @@
# Схема подключения — Security Mesh Node (ProMicro nRF52840)
Все пины ниже указаны по **маркировке на плате** (nRF52840 GPiO).
---
## Сопоставление пинов
| В коде (Arduino pin) | На плате (nRF) | Куда подключено |
|----------------------|----------------|-----------------|
| 0 | **P0.08** | Геркон (reed switch) |
| 1 | **P0.06** | PIR датчик движения |
| 2 | **P0.17** | DS18B20 температура |
| 3 | **P0.20** | Реле 1 |
| 4 | **P0.22** | Реле 2 |
| 5 | **P0.24** | Реле 3 |
| 6 | **P1.00** | Реле 4 |
| 10 | **P0.09** | SX1262 RST |
| 11 | **P0.10** | SX1262 DIO1 |
| 12 | **P1.11** | SX1262 SCK |
| 13 | **P1.13** | SX1262 NSS |
| 14 | **P1.15** | SX1262 MOSI |
| 15 | **P0.02** | SX1262 MISO |
| 16 | **P0.29** | SX1262 BUSY |
| 21 | **P0.13** | SX1262 POWER_EN |
| 22 | **P0.15** | LED статуса (встроенный) |
---
## SX1262 LoRa модуль
```
Плата (nRF) ───── SX1262
────────────────────────────────
P1.13 ────────── NSS
P1.11 ────────── SCK
P1.15 ────────── MOSI
P0.02 ────────── MISO
P0.10 ────────── DIO1
P0.09 ────────── RST
P0.29 ────────── BUSY
P0.13 ────────── POWER_EN (если есть, иначе VCC)
3.3V ────────── VCC
GND ────────── GND
```
> **Важно:** SX1262 питается от 3.3В. Не подключайте к 5В!
---
## Датчики
### Геркон (Reed) — P0.08
```
P0.08 ──┬──────────── GND
╲ 10kΩ (подтяжка к 3.3В, либо INPUT_PULLUP)
3.3V
Геркон: один конец на P0.08, второй на GND.
При замыкании — P0.08 соединяется с GND.
```
### PIR датчик движения — P0.06
```
PIR датчик:
VCC ── 3.3V или 5V (по питанию датчика)
GND ── GND
OUT ── P0.06
```
### DS18B20 — P0.17
```
DS18B20:
VCC ── 3.3В
GND ── GND
DQ ── P0.17
┌─ 4.7kΩ
P0.17 ────┤
└── 3.3В
```
---
## Реле
Логика: ON = HIGH, OFF = LOW.
### Реле 1 — P0.20
```
P0.20 ── IN1 релейного модуля
Релейный модуль:
VCC ── 5V (внешний источник)
GND ── GND
IN1 ── P0.20
```
> Реле 1 управляется `relay1 on/off`. Состояние сохраняется.
### Реле 2 — P0.22
```
P0.22 ── IN2 релейного модуля
Релейный модуль:
VCC ── 5V
GND ── GND
IN2 ── P0.22
```
> Реле 2 управляется `relay2 on/off`. Состояние сохраняется.
### Реле 3 — P0.24
```
P0.24 ── IN3 релейного модуля
Релейный модуль:
VCC ── 5V
GND ── GND
IN3 ── P0.24
```
> Реле 3 управляется `relay3 on/off`. Состояние сохраняется.
### Реле 4 — P1.00
```
P1.00 ── IN4 релейного модуля
Релейный модуль:
VCC ── 5V
GND ── GND
IN4 ── P1.00
```
> Реле 4 управляется `relay4 on/off`. Состояние сохраняется.
---
## LED статуса — P0.15
Встроенный красный светодиод на плате (P0.15).
Активный уровень — HIGH.
---
## Общая схема
```
┌──────────────────────┐
│ ProMicro nRF52840 │
│ │
SX1262 ◄──SPI──┤ P1.13 NSS │
│ │ P1.11 SCK │
│ │ P1.15 MOSI │
│ │ P0.02 MISO │
├──────┤ P0.10 DIO1 │
├──────┤ P0.09 RST │
├──────┤ P0.29 BUSY │
├──────┤ P0.13 POWER_EN │
│ │ │
Геркон ──┤ P0.08│ P0.20 ─── Реле 1 │
│ │ P0.22 ─── Реле 2 │
│ │ P0.24 ─── Реле 3 │
│ │ P1.00 ─── Реле 4 │
PIR ────┤ P0.06│ │
│ │ P0.15 ─── LED статуса│
DS18B20 ───┤ P0.17│ │
└──────┴───────────────────────┘
```
---
## Питание
- **USB** (5В) — через встроенный регулятор
- **Релейные модули** обычно требуют 5В — используйте внешний источник
- SX1262 питается от 3.3В (pin VCC на плате)
**Потребление:**
- TX (~22 dBm): ~120-140 мА
- RX: ~15-20 мА

View File

@@ -39,21 +39,40 @@ DallasTemperature ds18b20(&oneWire);
static bool g_armed = false;
static bool g_hall_alert = false;
static bool g_motion_alert = false;
static bool g_relay1_state = false;
static bool g_relay2_state = false;
static bool g_relay3_state = false;
static bool g_relay4_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 const uint32_t TZ_OFFSET = 10800; // +3h (MSK)
static uint32_t date_to_epoch(int y, int m, int d, int h, int mi) {
const uint8_t mdays[] = {31,28,31,30,31,30,31,31,30,31,30,31};
uint32_t days = (y - 1970) * 365;
for (int i = 1970; i < y; i++) {
if ((i % 4 == 0 && i % 100 != 0) || i % 400 == 0) days++;
}
for (int i = 0; i < m - 1; i++) days += mdays[i];
if (m > 2 && ((y % 4 == 0 && y % 100 != 0) || y % 400 == 0)) days++;
days += d - 1;
return days * 86400 + h * 3600 + mi * 60;
}
static unsigned long g_boot_time = 0;
static bool g_startup_msg_sent = false;
static unsigned long g_auto_status_interval = 0;
static unsigned long g_last_auto_status = 0;
struct SecurityConfig { uint32_t magic; uint8_t armed; uint8_t relay2; };
static const uint32_t CFG_MAGIC = 0x53454355;
struct SecurityConfig { uint32_t magic; uint8_t armed; uint8_t relay1; uint8_t relay2; uint8_t relay3; uint8_t relay4; };
static const uint32_t CFG_MAGIC = 0x53454357;
bool saveSecurityConfig() {
SecurityConfig cfg = { CFG_MAGIC, g_armed ? 1 : 0, g_relay2_state ? 1 : 0 };
SecurityConfig cfg = { CFG_MAGIC, g_armed ? 1 : 0, g_relay1_state ? 1 : 0, g_relay2_state ? 1 : 0, g_relay3_state ? 1 : 0, g_relay4_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);
@@ -67,7 +86,7 @@ bool loadSecurityConfig() {
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;
g_armed = cfg.armed; g_relay1_state = cfg.relay1; g_relay2_state = cfg.relay2; g_relay3_state = cfg.relay3; g_relay4_state = cfg.relay4;
return true;
}
@@ -96,6 +115,27 @@ public:
return 0;
}
float last_rssi = -127.0f;
float last_snr = 0.0f;
char pending_response[256];
unsigned long pending_at = 0;
bool pending = false;
void flush_response() {
if (pending && millis() >= pending_at) {
send_text(pending_response);
pending = false;
}
}
void defer_response(const char* text) {
strncpy(pending_response, text, sizeof(pending_response) - 1);
pending_response[sizeof(pending_response) - 1] = 0;
pending_at = millis() + 5000;
pending = true;
}
void send_text(const char* text) {
if (!channel_ready) return;
uint32_t ts = rtc_clock.getCurrentTime();
@@ -113,6 +153,9 @@ public:
if (type != PAYLOAD_TYPE_GRP_TXT) return;
if (len < 5 || data[4] != 0) return;
last_rssi = radio_driver.getLastRSSI();
last_snr = pkt->getSNR();
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;
@@ -130,41 +173,118 @@ public:
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");
defer_response("help:ohrana on,ohrana off,relay1 on,relay1 off,relay2 on,relay2 off,relay3 on,relay3 off,relay4 on,relay4 off,relay,status,ver,scan,snr,uptime,kontrol,time,reboot");
} else if (strcmp(cmd, "ohrana on") == 0) {
g_armed = true; digitalWrite(PIN_RELAY_1, LOW); saveSecurityConfig(); send_text("ohrana on ok");
g_armed = true; saveSecurityConfig(); defer_response("ohrana on ok");
} else if (strcmp(cmd, "ohrana off") == 0) {
g_armed = false; digitalWrite(PIN_RELAY_1, HIGH); saveSecurityConfig(); send_text("ohrana off ok");
g_armed = false; saveSecurityConfig(); defer_response("ohrana off ok");
} else if (strcmp(cmd, "relay1 on") == 0) {
digitalWrite(PIN_RELAY_1, LOW); send_text("relay1 on ok");
g_relay1_state = true; digitalWrite(PIN_RELAY_1, HIGH); saveSecurityConfig(); defer_response("relay1 on ok");
} else if (strcmp(cmd, "relay1 off") == 0) {
digitalWrite(PIN_RELAY_1, HIGH); send_text("relay1 off ok");
g_relay1_state = false; digitalWrite(PIN_RELAY_1, LOW); saveSecurityConfig(); defer_response("relay1 off ok");
} else if (strcmp(cmd, "relay2 on") == 0) {
g_relay2_state = true; digitalWrite(PIN_RELAY_2, LOW); send_text("relay2 on ok");
g_relay2_state = true; digitalWrite(PIN_RELAY_2, HIGH); saveSecurityConfig(); defer_response("relay2 on ok");
} else if (strcmp(cmd, "relay2 off") == 0) {
g_relay2_state = false; digitalWrite(PIN_RELAY_2, HIGH); send_text("relay2 off ok");
g_relay2_state = false; digitalWrite(PIN_RELAY_2, LOW); saveSecurityConfig(); defer_response("relay2 off ok");
} else if (strcmp(cmd, "relay3 on") == 0) {
g_relay3_state = true; digitalWrite(PIN_RELAY_3, HIGH); saveSecurityConfig(); defer_response("relay3 on ok");
} else if (strcmp(cmd, "relay3 off") == 0) {
g_relay3_state = false; digitalWrite(PIN_RELAY_3, LOW); saveSecurityConfig(); defer_response("relay3 off ok");
} else if (strcmp(cmd, "relay4 on") == 0) {
g_relay4_state = true; digitalWrite(PIN_RELAY_4, HIGH); saveSecurityConfig(); defer_response("relay4 on ok");
} else if (strcmp(cmd, "relay4 off") == 0) {
g_relay4_state = false; digitalWrite(PIN_RELAY_4, LOW); saveSecurityConfig(); defer_response("relay4 off ok");
} else if (strcmp(cmd, "relay") == 0) {
char r[48]; snprintf(r, sizeof(r), "relay:1%s 2%s 3%s 4%s",
g_relay1_state ? "on" : "off",
g_relay2_state ? "on" : "off",
g_relay3_state ? "on" : "off",
g_relay4_state ? "on" : "off");
defer_response(r);
} else if (strcmp(cmd, "status") == 0) {
char r[128]; char tb[16]; float t = g_last_temperature;
char r[128]; char tb[16]; char vb[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);
uint16_t mv = board.getBattMilliVolts();
if (mv < 100) strcpy(vb, "USB"); else snprintf(vb, sizeof(vb), "%.2fV", mv / 1000.0f);
snprintf(r, sizeof(r), "status:%s temp:%s hall:%s motion:%s bat:%s",
g_armed ? "armed" : "off", tb, g_hall_alert ? "trig" : "ok", g_motion_alert ? "trig" : "ok", vb);
defer_response(r);
} else if (strcmp(cmd, "ver") == 0) {
char r[64]; snprintf(r, sizeof(r), "build:%s %s", __DATE__, __TIME__); send_text(r);
char r[64]; snprintf(r, sizeof(r), "build:%s %s", __DATE__, __TIME__); defer_response(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");
char r[32]; snprintf(r, sizeof(r), "noise:%.0fdBm", sum / 20); defer_response(r);
} else if (strcmp(cmd, "uptime") == 0) {
unsigned long sec = (millis() - g_boot_time) / 1000;
int d = sec / 86400; sec %= 86400;
int h = sec / 3600; sec %= 3600;
int m = sec / 60;
char r[64];
if (d > 0) snprintf(r, sizeof(r), "uptime:%dd %dh %dm", d, h, m);
else if (h > 0) snprintf(r, sizeof(r), "uptime:%dh %dm", h, m);
else snprintf(r, sizeof(r), "uptime:%dm", m);
defer_response(r);
} else if (strcmp(cmd, "kontrol") == 0 || strncmp(cmd, "kontrol ", 8) == 0) {
const char* arg = cmd + 7; while (*arg == ' ') arg++;
if (strcmp(arg, "off") == 0) {
g_auto_status_interval = 0;
defer_response("kontrol off");
} else {
int val = atoi(arg);
if (val >= 1 && val <= 1440) {
g_auto_status_interval = (unsigned long)val * 60000;
g_last_auto_status = millis();
char r[48]; snprintf(r, sizeof(r), "kontrol %d min ok", val);
defer_response(r);
} else defer_response("kontrol 1-1440 or off");
}
} else if (strncmp(cmd, "time", 4) == 0) {
const char* rest = cmd + 4; while (*rest == ' ') rest++;
if (*rest == 0) {
uint32_t ts = rtc_clock.getCurrentTime() + TZ_OFFSET;
if (ts < 100000) { defer_response("time not set"); return; }
uint32_t days = ts / 86400;
int h = (ts % 86400) / 3600;
int mi = (ts % 3600) / 60;
int y = 1970;
while (true) {
int leap = ((y % 4 == 0 && y % 100 != 0) || y % 400 == 0) ? 366 : 365;
if (days < (uint32_t)leap) break;
days -= leap; y++;
}
const uint8_t mdays[] = {31,28,31,30,31,30,31,31,30,31,30,31};
int m;
for (m = 1; m <= 12; m++) {
int dim = mdays[m-1];
if (m == 2 && ((y % 4 == 0 && y % 100 != 0) || y % 400 == 0)) dim = 29;
if (days < (uint32_t)dim) break;
days -= dim;
}
char r[64];
snprintf(r, sizeof(r), "time:%02d.%02d.%04d %02d:%02d", (int)(days + 1), m, y, h, mi);
defer_response(r);
return;
}
if (strncmp(rest, "s ", 2) != 0) { defer_response("time s DD.MM.YYYY HH.MM"); return; }
rest += 2;
int d, m, y, h, mi;
if (sscanf(rest, "%d.%d.%d %d.%d", &d, &m, &y, &h, &mi) == 5) {
if (y < 2020 || y > 2099 || m < 1 || m > 12 || d < 1 || d > 31 || h > 23 || mi > 59) {
defer_response("bad date"); return;
}
rtc_clock.setCurrentTime(date_to_epoch(y, m, d, h, mi) - TZ_OFFSET);
char r[64]; snprintf(r, sizeof(r), "time set %02d.%02d.%04d %02d.%02d", d, m, y, h, mi);
defer_response(r);
} else defer_response("time s DD.MM.YYYY HH.MM");
} else if (strcmp(cmd, "snr") == 0) {
char r[48]; snprintf(r, sizeof(r), "rssi:%.0fdBm snr:%.1fdB", last_rssi, last_snr);
defer_response(r);
} else if (strcmp(cmd, "reboot") == 0) {
send_text("rebooting");
delay(100);
for (int i = 0; i < 5; i++) { digitalWrite(PIN_LED_STATUS, HIGH); delay(80); digitalWrite(PIN_LED_STATUS, LOW); delay(80); }
NVIC_SystemReset();
}
}
};
@@ -197,13 +317,15 @@ void setup() {
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);
pinMode(PIN_RELAY_1, OUTPUT); pinMode(PIN_RELAY_2, OUTPUT); pinMode(PIN_RELAY_3, OUTPUT); pinMode(PIN_RELAY_4, OUTPUT); pinMode(PIN_LED_STATUS, OUTPUT);
digitalWrite(PIN_RELAY_1, LOW); digitalWrite(PIN_RELAY_2, LOW); digitalWrite(PIN_RELAY_3, LOW); digitalWrite(PIN_RELAY_4, LOW); 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);
digitalWrite(PIN_RELAY_1, g_relay1_state ? HIGH : LOW);
digitalWrite(PIN_RELAY_2, g_relay2_state ? HIGH : LOW);
digitalWrite(PIN_RELAY_3, g_relay3_state ? HIGH : LOW);
digitalWrite(PIN_RELAY_4, g_relay4_state ? HIGH : LOW);
if (!radio_init()) {
Serial.println("RADIO FAIL");
@@ -221,6 +343,7 @@ void setup() {
void loop() {
NRF_WDT->RR[0] = WDT_RR_RR_Reload;
the_mesh.loop();
the_mesh.flush_response();
rtc_clock.tick();
if (!g_startup_msg_sent && millis() - g_boot_time >= 10000) {
@@ -244,6 +367,17 @@ void loop() {
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_auto_status_interval > 0 && now - g_last_auto_status >= g_auto_status_interval) {
g_last_auto_status = now;
char r[128]; char tb[16]; char vb[16]; float t = g_last_temperature;
if (t == -127.0f) strcpy(tb, "N/A"); else snprintf(tb, sizeof(tb), "%+.1fC", t);
uint16_t mv = board.getBattMilliVolts();
if (mv < 100) strcpy(vb, "USB"); else snprintf(vb, sizeof(vb), "%.2fV", mv / 1000.0f);
snprintf(r, sizeof(r), "status:%s temp:%s hall:%s motion:%s bat:%s",
g_armed ? "armed" : "off", tb, g_hall_alert ? "trig" : "ok", g_motion_alert ? "trig" : "ok", vb);
the_mesh.send_text(r);
}
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); }

View File

@@ -98,6 +98,12 @@
#ifndef PIN_RELAY_2
#define PIN_RELAY_2 4
#endif
#ifndef PIN_RELAY_3
#define PIN_RELAY_3 5
#endif
#ifndef PIN_RELAY_4
#define PIN_RELAY_4 6
#endif
#ifndef PIN_LED_STATUS
#define PIN_LED_STATUS 22
#endif
@@ -108,6 +114,12 @@
#ifndef PIN_RELAY_2
#define PIN_RELAY_2 5
#endif
#ifndef PIN_RELAY_3
#define PIN_RELAY_3 6
#endif
#ifndef PIN_RELAY_4
#define PIN_RELAY_4 7
#endif
#ifndef PIN_LED_STATUS
#define PIN_LED_STATUS 13
#endif