372 lines
16 KiB
C++
372 lines
16 KiB
C++
#include <Arduino.h>
|
|
#include <Mesh.h>
|
|
#include <InternalFileSystem.h>
|
|
#include <OneWire.h>
|
|
#include <DallasTemperature.h>
|
|
#include <helpers/ArduinoHelpers.h>
|
|
#include <helpers/NRF52Board.h>
|
|
#include <helpers/SimpleMeshTables.h>
|
|
#include <helpers/StaticPoolPacketManager.h>
|
|
#include <helpers/ChannelDetails.h>
|
|
#include <helpers/ArduinoSerialInterface.h>
|
|
#include <Crypto.h>
|
|
#include <SHA256.h>
|
|
#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_relay1_state = 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 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 relay1; uint8_t relay2; };
|
|
static const uint32_t CFG_MAGIC = 0x53454356;
|
|
|
|
bool saveSecurityConfig() {
|
|
SecurityConfig cfg = { CFG_MAGIC, g_armed ? 1 : 0, g_relay1_state ? 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_relay1_state = cfg.relay1; 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;
|
|
}
|
|
|
|
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();
|
|
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;
|
|
|
|
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;
|
|
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) {
|
|
defer_response("help:ohrana on,ohrana off,relay1 on,relay1 off,relay2 on,relay2 off,relay,status,ver,scan,snr,uptime,kontrol,time,reboot");
|
|
} else if (strcmp(cmd, "ohrana on") == 0) {
|
|
g_armed = true; saveSecurityConfig(); defer_response("ohrana on ok");
|
|
} else if (strcmp(cmd, "ohrana off") == 0) {
|
|
g_armed = false; saveSecurityConfig(); defer_response("ohrana off ok");
|
|
} else if (strcmp(cmd, "relay1 on") == 0) {
|
|
g_relay1_state = true; digitalWrite(PIN_RELAY_1, LOW); saveSecurityConfig(); defer_response("relay1 on ok");
|
|
} else if (strcmp(cmd, "relay1 off") == 0) {
|
|
g_relay1_state = false; digitalWrite(PIN_RELAY_1, HIGH); saveSecurityConfig(); defer_response("relay1 off ok");
|
|
} else if (strcmp(cmd, "relay2 on") == 0) {
|
|
g_relay2_state = true; digitalWrite(PIN_RELAY_2, LOW); saveSecurityConfig(); defer_response("relay2 on ok");
|
|
} else if (strcmp(cmd, "relay2 off") == 0) {
|
|
g_relay2_state = false; digitalWrite(PIN_RELAY_2, HIGH); saveSecurityConfig(); defer_response("relay2 off ok");
|
|
} else if (strcmp(cmd, "relay") == 0) {
|
|
char r[32]; snprintf(r, sizeof(r), "relay:1%s 2%s",
|
|
g_relay1_state ? "on" : "off",
|
|
g_relay2_state ? "on" : "off");
|
|
defer_response(r);
|
|
} else if (strcmp(cmd, "status") == 0) {
|
|
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);
|
|
defer_response(r);
|
|
} else if (strcmp(cmd, "ver") == 0) {
|
|
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); 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();
|
|
}
|
|
}
|
|
};
|
|
|
|
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();
|
|
digitalWrite(PIN_RELAY_1, g_relay1_state ? LOW : 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();
|
|
the_mesh.flush_response();
|
|
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_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); }
|
|
} else digitalWrite(PIN_LED_STATUS, LOW);
|
|
}
|