#include #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; static char g_channel_pass[32] = ""; bool saveChannelPass(const char* pass) { File f = InternalFS.open(CHANNEL_FILE, FILE_O_WRITE); if (!f) return false; size_t len = strlen(pass); if (len > 31) len = 31; bool ok = f.write((const uint8_t*)pass, len) == len; f.close(); return ok; } bool loadChannelPass() { File f = InternalFS.open(CHANNEL_FILE, FILE_O_READ); if (!f) return false; int len = f.read((uint8_t*)g_channel_pass, sizeof(g_channel_pass) - 1); f.close(); if (len > 0) { g_channel_pass[len] = 0; return true; } return false; } BLEUart bleuart; void ble_send(const char* text) { bleuart.println(text); } void setup_ble() { Bluefruit.begin(1, 0); Bluefruit.setName("Security-Mesh"); Bluefruit.setTxPower(4); bleuart.begin(); Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE); Bluefruit.Advertising.addTxPower(); Bluefruit.Advertising.addService(bleuart); Bluefruit.ScanResponse.addName(); Bluefruit.Advertising.restartOnDisconnect(true); Bluefruit.Advertising.setInterval(32, 244); Bluefruit.Advertising.start(0); } void process_ble_line(const char* line) { if (strncmp(line, "channel ", 8) == 0) { const char* pass = line + 8; int len = strlen(pass); if (len < 1 || len > 31) { ble_send("bad password length"); return; } int digits = 0; for (int i = 0; pass[i]; i++) if (pass[i] >= '0' && pass[i] <= '9') digits++; if (digits != len) { ble_send("use digits only"); return; } strncpy(g_channel_pass, pass, sizeof(g_channel_pass) - 1); saveChannelPass(pass); char r[64]; snprintf(r, sizeof(r), "channel %s saved, rebooting...", pass); ble_send(r); delay(100); NVIC_SystemReset(); } else if (strcmp(line, "status") == 0) { char r[64]; snprintf(r, sizeof(r), "channel: %s", g_channel_pass[0] ? g_channel_pass : "1234"); ble_send(r); } else if (strcmp(line, "help") == 0) { ble_send("commands: channel , status, help"); } else { ble_send("unknown. try help"); } } 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() { const char* pass = g_channel_pass[0] ? g_channel_pass : "1234"; uint8_t key[32]; SHA256 sha; sha.reset(); sha.update((const uint8_t*)pass, strlen(pass)); 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; } 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; 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"); } else if (strcmp(cmd, "ohrana on") == 0) { g_armed = true; digitalWrite(PIN_RELAY_1, LOW); saveSecurityConfig(); defer_response("ohrana on ok"); } else if (strcmp(cmd, "ohrana off") == 0) { g_armed = false; digitalWrite(PIN_RELAY_1, HIGH); saveSecurityConfig(); defer_response("ohrana off ok"); } else if (strcmp(cmd, "relay1 on") == 0) { digitalWrite(PIN_RELAY_1, LOW); defer_response("relay1 on ok"); } else if (strcmp(cmd, "relay1 off") == 0) { digitalWrite(PIN_RELAY_1, HIGH); defer_response("relay1 off ok"); } else if (strcmp(cmd, "relay2 on") == 0) { g_relay2_state = true; digitalWrite(PIN_RELAY_2, LOW); defer_response("relay2 on ok"); } else if (strcmp(cmd, "relay2 off") == 0) { g_relay2_state = false; digitalWrite(PIN_RELAY_2, HIGH); defer_response("relay2 off ok"); } else if (strcmp(cmd, "relay") == 0) { char r[32]; snprintf(r, sizeof(r), "relay:1%s 2%s", digitalRead(PIN_RELAY_1) == LOW ? "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); } } }; 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(); loadChannelPass(); if (g_armed) digitalWrite(PIN_RELAY_1, LOW); else digitalWrite(PIN_RELAY_1, HIGH); digitalWrite(PIN_RELAY_2, g_relay2_state ? LOW : HIGH); setup_ble(); 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(); static char ble_line[64]; static int ble_pos = 0; while (bleuart.available()) { char c = bleuart.read(); if (c == '\n' || c == '\r') { ble_line[ble_pos] = 0; if (ble_pos > 0) process_ble_line(ble_line); ble_pos = 0; } else if (ble_pos < (int)sizeof(ble_line) - 1) { ble_line[ble_pos++] = c; } } 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); }