/****************************************************
* STALKER "Emission" Mesh — NODE (ESP8266 / ESP-01)
* Працює з головним ESP32 (комунікація ESPNOW, flood+TTL)
*
* - Зміна власного SSID (SoftAP) за командою CMD_SET_SSID
* - "Деактивація" (CMD_SLEEP): виставляє нейтральний SSID і
* ігнорує інші команди (крім CMD_WAKE). Залишається на ESPNOW,
* щоб отримати пробудження.
* - Пробудження (CMD_WAKE): повертається до останнього "активного" SSID
* або очікує новий CMD_SET_SSID.
* - Відповідь на опитування (CMD_STATUS_REQ) — ACK_STATUS з поточним SSID.
* - Ретрансляція усіх корисних пакетів з TTL та дедуплікацією (origin_id+seq).
*
* Піни ESP-01: живлення 3.3В, CH_PD=HIGH, GPIO0/2 — як зазвичай.
****************************************************/
#include <Arduino.h>
#include <ESP8266WiFi.h>
#include <espnow.h>
// ========== ПАРАМЕТРИ СІТКИ ==========
#define NODE_ID 7 // <<< Унікальний ID цього вузла (1..150). ЗМІНИ!
#define NET_CH 6 // <<< Канал Wi-Fi/ESPNOW (має збігатися з головним)
#define NET_VER 1
// Ліміти/константи як у головного
#define MAX_NODES 150
#define BASE_SSID_MAX 16
#define FULL_SSID_MAX 31
#define MSG_TTL_DEFAULT 20
// ========== СТРУКТУРИ ПАКЕТІВ (ідентичні головному) ==========
#pragma pack(push,1)
enum MsgType : uint8_t {
CMD_SET_SSID = 1, CMD_SLEEP = 2, CMD_STATUS_REQ = 3, ACK_STATUS = 4, CMD_WAKE = 5
};
struct MsgHdr {
uint8_t ver; // NET_VER
uint8_t type; // MsgType
uint16_t origin_id; // хто створив пакет
uint16_t target_id; // цільовий вузол (або спец. значення)
uint32_t seq; // послідовність у джерела
uint8_t ttl; // time-to-live
uint8_t hop; // лічильник хопів
uint16_t len; // довжина payload
uint32_t crc; // CRC32 header+payload (без цього поля)
};
struct PayloadSetSsid {
char ssid[FULL_SSID_MAX+1]; // \0-terminated
uint8_t channel; // очікуваний канал
};
struct PayloadSleep {
char neutral[FULL_SSID_MAX+1];
uint32_t sleepMs; // 0 = "до пробудження"
};
struct PayloadStatusReq {
uint16_t want_id_from;
uint16_t want_id_to;
};
struct PayloadAckStatus {
uint16_t node_id;
char ssid[FULL_SSID_MAX+1];
int8_t rssi; // для ESP8266 поставимо 0 (нема з чого брати)
};
#pragma pack(pop)
// ========== ГЛОБАЛЬНІ ==========
static const uint8_t BCAST[6] = {0xFF,0xFF,0xFF,0xFF,0xFF,0xFF};
char g_currentSsid[FULL_SSID_MAX+1] = "GRV"; // поточний SSID AP
char g_lastActiveSsid[FULL_SSID_MAX+1] = "GRV";// останній "робочий" SSID
char g_neutralSsid[FULL_SSID_MAX+1] = "OFF";// нейтральний для деактивації
bool g_sleeping = false; // прапор "деактивації"
uint32_t g_seq = 0; // локальний лічильник для ACK
// Невеликий буфер бачених пакетів для дедуплікації
struct SeenKey { uint16_t origin; uint32_t seq; };
#define SEEN_BUF 64
SeenKey seen[SEEN_BUF];
uint8_t seen_head = 0;
// ========== УТИЛІТИ ==========
static inline uint32_t crc32_update(uint32_t crc, uint8_t data) {
crc ^= data;
for (uint8_t i=0;i<8;i++) crc = (crc >> 1) ^ (0xEDB88320 & (-(int)(crc & 1)));
return crc;
}
static uint32_t crc32_buf(const uint8_t* p, size_t n) {
uint32_t crc = 0xFFFFFFFF;
for (size_t i=0;i<n;i++) crc = crc32_update(crc, p[i]);
return ~crc;
}
static void safeCopy(char* dst, const char* src, size_t cap) {
if (!cap) return;
strncpy(dst, src ? src : "", cap-1);
dst[cap-1] = '\0';
}
static bool seenHas(uint16_t origin, uint32_t seq) {
for (uint8_t i=0;i<SEEN_BUF;i++) {
if (seen[i].origin == origin && seen[i].seq == seq) return true;
}
return false;
}
static void seenPut(uint16_t origin, uint32_t seq) {
seen[seen_head].origin = origin;
seen[seen_head].seq = seq;
seen_head = (seen_head + 1) % SEEN_BUF;
}
static void logHexMac(const uint8_t* mac) {
#ifdef DEBUG
if (!mac) return;
for (int i=0;i<6;i++){ if (mac[i]<16) Serial.print('0'); Serial.print(mac[i],HEX); if (i<5) Serial.print(':'); }
#endif
}
// Перевиставити SoftAP SSID на льоту
static bool setMyAP(const char* ssid) {
// Уникаємо зайвих реконфігурацій
if (strcmp(g_currentSsid, ssid) == 0) return true;
safeCopy(g_currentSsid, ssid, sizeof(g_currentSsid));
// Швидка реконфігурація AP на фіксованому каналі
WiFi.softAPdisconnect(true);
delay(20);
bool ok = WiFi.softAP(g_currentSsid, "", NET_CH, 0, 1);
#ifdef DEBUG
Serial.print("AP->"); Serial.print(g_currentSsid); Serial.print(" ch="); Serial.println(NET_CH);
#endif
return ok;
}
// Надіслати пакет (широкомовно)
static void sendPacket(const uint8_t* buf, uint8_t len) {
// На ESP8266 broadcast можна робити NULL-адресою
esp_now_send(nullptr, (uint8_t*)buf, len);
}
// Відправити ACK_STATUS
static void sendAckStatus() {
uint8_t pkt[sizeof(MsgHdr) + sizeof(PayloadAckStatus)];
MsgHdr* h = (MsgHdr*)pkt;
PayloadAckStatus* p = (PayloadAckStatus*)(pkt + sizeof(MsgHdr));
h->ver = NET_VER;
h->type = ACK_STATUS;
h->origin_id = NODE_ID; // я — джерело
h->target_id = 0; // не суттєво для ACK
h->seq = ++g_seq;
h->ttl = MSG_TTL_DEFAULT;
h->hop = 0;
h->len = sizeof(PayloadAckStatus);
h->crc = 0;
p->node_id = NODE_ID;
safeCopy(p->ssid, g_currentSsid, sizeof(p->ssid));
p->rssi = 0; // для ESP8266 поставимо 0 (немає валідного RSSI контексту)
h->crc = crc32_buf(pkt, sizeof(MsgHdr)-sizeof(uint32_t) + h->len);
sendPacket(pkt, sizeof(pkt));
}
// Форвард будь-якого отриманого пакета (зменшити TTL, ++hop, перерахувати CRC)
static void forwardPacket(uint8_t* data, int len) {
if (len < (int)sizeof(MsgHdr)) return;
MsgHdr* h = (MsgHdr*)data;
if (h->ttl == 0) return;
h->ttl--;
h->hop++;
h->crc = 0;
h->crc = crc32_buf(data, len);
sendPacket(data, len);
}
// ========== ESPNOW CALLBACKS ==========
void onDataSent(uint8_t* mac, uint8_t status) {
#ifdef DEBUG
Serial.print("ESPNOW sent to "); logHexMac(mac); Serial.print(" -> "); Serial.println(status);
#endif
}
void onDataRecv(uint8_t *mac, uint8_t *data, uint8_t len) {
if (len < sizeof(MsgHdr)) return;
// Копіюємо заголовок, щоб безпечно його читати
MsgHdr h; memcpy(&h, data, sizeof(MsgHdr));
if (h.ver != NET_VER) return;
if ((sizeof(MsgHdr) - sizeof(uint32_t) + h.len) != len) return;
// Перевірка CRC
uint32_t got = ((MsgHdr*)data)->crc;
((MsgHdr*)data)->crc = 0;
if (crc32_buf(data, len) != got) return;
// Дедуплікація (до будь-яких дій)
if (seenHas(h.origin_id, h.seq)) return;
seenPut(h.origin_id, h.seq);
#ifdef DEBUG
Serial.print("RX type="); Serial.print(h.type);
Serial.print(" tgt="); Serial.print(h.target_id);
Serial.print(" ttl="); Serial.print(h.ttl);
Serial.print(" hop="); Serial.print(h.hop);
Serial.print(" from "); logHexMac(mac); Serial.println();
#endif
// Вказівник на payload
const uint8_t* pl = data + sizeof(MsgHdr);
// --- Обробка по типах ---
switch (h.type) {
case CMD_SET_SSID:
if (h.len == sizeof(PayloadSetSsid)) {
const PayloadSetSsid* p = (const PayloadSetSsid*)pl;
// Якщо пакет адресований мені — застосувати
if (h.target_id == NODE_ID || h.target_id == 0xFFFF) {
// Переконуємось, що на правильному каналі
if (p->channel == NET_CH) {
if (!g_sleeping) {
// Змінюємо SSID
setMyAP(p->ssid);
// Запам'ятати як "робочий" для потенційного пробудження
safeCopy(g_lastActiveSsid, p->ssid, sizeof(g_lastActiveSsid));
}
}
}
}
break;
case CMD_SLEEP:
if (h.len == sizeof(PayloadSleep)) {
const PayloadSleep* p = (const PayloadSleep*)pl;
if (h.target_id == NODE_ID || h.target_id == 0xFFFF) {
// Перейти у "деактивацію": нейтральний SSID і ігнор інших команд
safeCopy(g_neutralSsid, p->neutral, sizeof(g_neutralSsid));
setMyAP(g_neutralSsid);
g_sleeping = true;
}
}
break;
case CMD_WAKE:
if (h.target_id == NODE_ID || h.target_id == 0xFFFF) {
// Вийти з "деактивації"
g_sleeping = false;
// Повернути попередній "робочий" SSID (або чекати наступний CMD_SET_SSID)
setMyAP(g_lastActiveSsid);
}
break;
case CMD_STATUS_REQ:
if (h.len == sizeof(PayloadStatusReq)) {
const PayloadStatusReq* p = (const PayloadStatusReq*)pl;
// Відповідаємо лише якщо наш ID у зазначеному діапазоні
if (NODE_ID >= p->want_id_from && NODE_ID <= p->want_id_to) {
sendAckStatus();
}
}
break;
case ACK_STATUS:
// Нічого: ці пакети йдуть "вгору" — ми лише ретранслюємо
break;
default:
break;
}
// --- Ретрансляція (flood) ---
// Спрощено: форвардимо всі валідні типи, якщо TTL>0.
// Завдяки дедуплікації по (origin_id, seq) не буде циклів.
if (h.ttl > 0) {
forwardPacket(data, len);
}
}
// ========== ІНІЦІАЛІЗАЦІЯ ==========
bool espnowInit() {
// Режим AP+STA на фіксованому каналі
WiFi.mode(WIFI_AP_STA);
WiFi.persistent(false);
WiFi.setOutputPower(20.5f); // ~100 мВт (зважай на локальні обмеження)
delay(10);
// Стартуемо SoftAP із початковим SSID (поки що — "GRV")
WiFi.softAP(g_currentSsid, "", NET_CH, 0, 1);
delay(10);
if (esp_now_init() != 0) return false;
// Комбо-роль (прийом+передача) і зворотні виклики
esp_now_set_self_role(ESP_NOW_ROLE_COMBO);
esp_now_register_send_cb(onDataSent);
esp_now_register_recv_cb(onDataRecv);
// Додаємо широкомовного "peer" (на ESP8266 не обов'язково, але не завадить)
esp_now_add_peer((uint8_t*)BCAST, ESP_NOW_ROLE_COMBO, NET_CH, NULL, 0);
return true;
}
// ========== SETUP/LOOP ==========
void setup() {
#ifdef DEBUG
Serial.begin(115200);
Serial.println();
Serial.println("NODE boot...");
#endif
// Для наочності зробимо SSID у старті з власним ID
char bootSsid[FULL_SSID_MAX+1];
snprintf(bootSsid, sizeof(bootSsid), "GRV_%u", (unsigned)NODE_ID);
safeCopy(g_currentSsid, bootSsid, sizeof(g_currentSsid));
safeCopy(g_lastActiveSsid, bootSsid, sizeof(g_lastActiveSsid));
if (!espnowInit()) {
#ifdef DEBUG
Serial.println("ESPNOW init FAILED");
#endif
// Блимати вічно не можемо — просто зависнемо
while (true) { delay(1000); }
}
#ifdef DEBUG
Serial.print("Ready. NODE_ID="); Serial.print(NODE_ID);
Serial.print(" CH="); Serial.print(NET_CH);
Serial.print(" SSID="); Serial.println(g_currentSsid);
#endif
}
void loop() {
// Немає потреби в активній логіці — все на callback'ах.
// За бажання можна періодично "оживляти" AP (на випадок якщо щось збило канал)
static uint32_t t0 = 0;
uint32_t now = millis();
if (now - t0 > 30000) {
t0 = now;
// Переконаємось, що AP все ще на нашому каналі та з вірним SSID
setMyAP(g_currentSsid);
}
delay(10);
}
MISO → GPIO19
SCK → GPIO18
SS/SDA (Chip Select) → GPIO5
MOSI → GPIO23
VCC → 3.3 В, GND → GND
RST → GPIO4