Atelier.

atelier · pagină internă · pentru tata

Tot codul firmware

Toate sketch-urile Arduino pentru VIA, gata de copy-paste în Arduino IDE. Layout LED-uri: [0]=roșu pieton · [1]=verde pieton · [2]=verde maș · [3]=galben · [4]=roșu maș.

Cod final — gata de flashat

Selectează nod (N1/N2/N3) sau mașinuță (M1/M2/M3), apasă Copy, lipești în Arduino IDE și flashezi. Board pentru N1/N2/N3: ESP32 Dev Module. Pentru M1/M2/M3: ESP32C3 Dev Module.

·
// VIA — Nod N1 (firmware v2 — ciclu 4 stari + adaptive + ambulanta)
// LED + OLED mare + buton + microfon I2S + ESP-NOW mesh + sniff tag-uri WiFi (cu RSSI)
// Board: ESP32 Dev Module

#include <Adafruit_NeoPixel.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <driver/i2s.h>
#include <WiFi.h>
#include <esp_now.h>
#include <esp_wifi.h>

#define NODE_ID "N1"

const int PIN_LED = 5;
const int NUM_LEDS = 5;
const int PIN_BUTON = 4;
#define I2S_PORT     I2S_NUM_0
#define SCK_PIN      14
#define WS_PIN       15
#define SD_PIN       32
#define SAMPLE_RATE  16000
#define BUFFER_SIZE  512
#define PRAG_AMP     200           // gate scăzut — sirena la distanță ajunge la 300-500 amp
#define PRAG_TON     5.0f          // sirena tonal-curată la distanță — vorbire <1, palme <2
#define PRAG_AMP_HUGE 3000         // override pentru sirena APROAPE și tare (orice tonalitate)
#define N_CONSEC     8             // 8 frame-uri (~256ms sustained) — RAMÂNE strict ca să filtreze palme
#define LONG_PRESS_MS  1500        // long-press >=1.5s pe buton = AMBULANTA manual; <1.5s = pieton
#define CASCADA_OFFSET_MS 400      // offset val intre noduri (N1=0, N2=400, N3=800ms)
#define WIFI_CANAL   1             // ESP-NOW + sniff pe ACELAȘI canal
#define RSSI_APROAPE -65           // dBm: peste asta, tag e "la mine" (mai permisiv decât -55)
#define TIMEOUT_TAG  4000          // ms

// Goertzel: target 750Hz si 600Hz (sirena europeana two-tone).
// k = N * f_target / fs    fs=16000, N=512 → k=24 → 750Hz exact; k=19 → 593.75Hz
const int K_HI = 24;
const int K_LO = 19;
const float COEFF_HI = 2.0f * cosf(2.0f * 3.14159265f * K_HI / (float)BUFFER_SIZE);
const float COEFF_LO = 2.0f * cosf(2.0f * 3.14159265f * K_LO / (float)BUFFER_SIZE);

uint8_t BROADCAST_MAC[] = { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF };

typedef struct {
  char node_id[4];
  uint8_t tip;          // 0=ALIVE, 1=SIRENA, 2=CORIDOR_REQ (green wave masini)
  uint32_t scor;
  int8_t rssi_tags[3];  // RSSI per M1/M2/M3 (asa cum vede expeditorul); -127 = nevazut
  uint8_t stare;        // 0-3 (VERDE_MASINI, GALBEN_SPRE_R, ROSU_MASINI, GALBEN_SPRE_V)
  uint16_t ramas_ms;    // câte ms până la următoarea tranziție (clip la 65535)
  uint8_t telefoane;    // T = telefoane detectate
  uint8_t pietoni;      // P = pietoni count
  uint8_t flags;        // bit0=ambulanta_activa, bit1=wave_activ
} mesaj_t;

typedef struct {
  int8_t rssi;
  unsigned long ultim_vazut;
} tag_t;

typedef struct {
  uint8_t mac[6];
  char node_id[4];
  unsigned long ultim_vazut;
  int8_t rssi_tags[3];  // ultimul raport de RSSI per tag de la acest vecin
  uint8_t stare;
  uint16_t ramas_ms;
  uint8_t telefoane;
  uint8_t pietoni;
  uint8_t flags;
} vecin_t;

Adafruit_NeoPixel leduri(NUM_LEDS, PIN_LED, NEO_GRB + NEO_KHZ800);
Adafruit_SSD1306 oled(128, 64, &Wire, -1);

// Layout LED fizic: [0]=roșu pieton, [1]=verde pieton, [2]=verde maș, [3]=galben, [4]=roșu maș
// State machine: 4 stari (galben INAINTE si DUPA verde mașini)
const int VERDE_MASINI    = 0;   // V maș + R pieton
const int GALBEN_SPRE_R   = 1;   // Galben + R pieton  (transition V→R)
const int ROSU_MASINI     = 2;   // R maș + V pieton
const int GALBEN_SPRE_V   = 3;   // Galben + R pieton  (transition R→V)
const int NUM_STARI       = 4;

// NUME = culoarea PIETONULUI/UTILIZATORULUI dominant pe stare. SUBT = pe cine.
// Fix de la versiunea precedenta (era "ROSU/PIETONI" — incorrect, pietonii au VERDE cand masinile au ROSU).
const char* NUME[NUM_STARI]  = { "VERDE",   "GALBEN",   "VERDE",    "GALBEN"   };
const char* SUBT[NUM_STARI]  = { "MASINI",  "STOP",     "PIETONI",  "PORNESC"  };

int stare = VERDE_MASINI;
unsigned long timp_intrat = 0;
unsigned long durata_stare = 5000;   // setata la fiecare intrare in stare

int pietoni = 0;
unsigned long ultima_apasare = 0;
const unsigned long ANTI_BOUNCE = 1500;
unsigned long apasari_recente[10] = {0};   // timestamp ultimele 10 apasari

int32_t buffer_i2s[BUFFER_SIZE];
bool sirena_locala = false;
bool sirena_de_la_vecin = false;
unsigned long timp_sirena = 0;
const unsigned long DURATA_SIRENA = 10000;   // ambulanță 10s (era 6s)
int frames_tonal_seq = 0;          // contor frame-uri consecutive cu tonal=true

// Cascadă val: programez intrarea în ambulanța la timpul X
unsigned long intra_ambulanta_la = 0;
char nod_sirena[4] = "?";          // cine a detectat (afișat pe OLED)

// Green wave: când local M>=2 mașinuțe → trimit CORIDOR_REQ → toate trec pe VERDE
unsigned long wave_pana_la = 0;
char nod_wave[4] = "?";
unsigned long ultim_wave_trimis = 0;
#define WAVE_DURATA 8000
#define WAVE_COOLDOWN 5000

// Direction tracking per tag (origine→destinație cu histeresis)
typedef struct {
  char origine[4];       // unde a fost INAINTE (gol = necunoscut)
  char destinatie[4];    // unde e ACUM (gol = nedetectat)
  char candidat[4];      // candidat pentru schimbare (gol = niciun candidat)
  unsigned long candidat_de_la;
  unsigned long ultim_aproape;
} tag_dir_t;

tag_dir_t tag_dir[3] = {0};
#define RSSI_LA_INTERSECTIE -55    // peste asta consider tag-ul ca fiind la nodul respectiv
#define SUSTAINED_MS         2000  // 2s la rang ca să schimbe destinația
#define UITARE_DIR_MS        8000  // dacă tag-ul nu a fost aproape de nimeni 8s, șterg directie

// Telefoane detectate prin probe requests (alte device-uri WiFi în zonă)
#define MAX_TELEFOANE 16
#define TIMEOUT_TELEFON 10000      // 10s fără probe → uitat
typedef struct {
  uint8_t mac[6];
  unsigned long ultim_vazut;
  int8_t rssi;
} telefon_t;
volatile telefon_t telefoane[MAX_TELEFOANE] = {0};
volatile int telefoane_n_global = 0;

// Long-press buton (pentru AMBULANȚĂ manual)
bool buton_apasat = false;
unsigned long buton_apasat_la = 0;
bool long_press_consumat = false;

volatile tag_t taguri[3] = { {0, 0}, {0, 0}, {0, 0} };

// Vecini ESP-NOW — array cu MAC complet, max 8 vecini
vecin_t vecini_arr[8] = {0};
const unsigned long TIMP_VECIN_VECHI = 10000;  // 10s (era 6s — prea agresiv)

unsigned long ultim_broadcast = 0;
const unsigned long INTERVAL_ALIVE = 1000;   // 1s (era 2s — prea rar)
unsigned long ultim_print = 0;
const unsigned long INTERVAL_PRINT = 500;

// Helper: cauta/adauga vecin in array
int idx_vecin(const uint8_t* mac) {
  for (int i = 0; i < 8; i++) {
    if (vecini_arr[i].ultim_vazut > 0 && memcmp(vecini_arr[i].mac, mac, 6) == 0) return i;
  }
  // slot liber sau cel mai vechi
  int idx_liber = -1;
  unsigned long cel_mai_vechi = 0xFFFFFFFF;
  int idx_inlocuit = 0;
  for (int i = 0; i < 8; i++) {
    if (vecini_arr[i].ultim_vazut == 0) { idx_liber = i; break; }
    if (vecini_arr[i].ultim_vazut < cel_mai_vechi) {
      cel_mai_vechi = vecini_arr[i].ultim_vazut;
      idx_inlocuit = i;
    }
  }
  return (idx_liber >= 0) ? idx_liber : idx_inlocuit;
}

void on_recv(const esp_now_recv_info* info, const uint8_t* data, int len) {
  if (len != sizeof(mesaj_t)) return;
  mesaj_t* m = (mesaj_t*)data;
  int idx = idx_vecin(info->src_addr);
  memcpy(vecini_arr[idx].mac, info->src_addr, 6);
  memcpy(vecini_arr[idx].node_id, m->node_id, 4);
  vecini_arr[idx].ultim_vazut = millis();
  // Salvez RSSI per tag raportat de acest vecin (pentru triangulare)
  for (int t = 0; t < 3; t++) vecini_arr[idx].rssi_tags[t] = m->rssi_tags[t];
  // Salvez extended state info (pentru retransmit Serial fără 3 USB)
  vecini_arr[idx].stare = m->stare;
  vecini_arr[idx].ramas_ms = m->ramas_ms;
  vecini_arr[idx].telefoane = m->telefoane;
  vecini_arr[idx].pietoni = m->pietoni;
  vecini_arr[idx].flags = m->flags;

  if (m->tip == 1) {
    // Cascadă val: nodul transmitter intră primul, eu cu offset bazat pe distanță ID
    int my_num = NODE_ID[1] - '0';     // N1→1, N2→2, N3→3
    int sender_num = m->node_id[1] - '0';
    int diff = abs(my_num - sender_num);
    intra_ambulanta_la = millis() + (unsigned long)diff * CASCADA_OFFSET_MS;
    memcpy(nod_sirena, m->node_id, 4);
    nod_sirena[3] = 0;
    Serial.printf("[ESP-NOW] SIRENA de la %s, intru val in %dms\n", m->node_id, (int)(diff * CASCADA_OFFSET_MS));
  } else if (m->tip == 2) {
    // CORIDOR_REQ: green wave declanșat de aglomerație mașinuțe la alt nod
    wave_pana_la = millis() + WAVE_DURATA;
    memcpy(nod_wave, m->node_id, 4);
    nod_wave[3] = 0;
    // Forțez countdown corect (la fel ca trigger local — fixat în loop secțiunea 5c)
    Serial.printf("[WAVE] Coridor verde primit de la %s (durata %dms)\n", m->node_id, (int)WAVE_DURATA);
  }
}

void IRAM_ATTR sniff_cb(void* buf, wifi_promiscuous_pkt_type_t type) {
  if (type != WIFI_PKT_MGMT) return;
  wifi_promiscuous_pkt_t* pkt = (wifi_promiscuous_pkt_t*)buf;
  const uint8_t* payload = pkt->payload;
  if ((payload[0] & 0xFC) != 0x40) return;          // nu-i probe request
  const uint8_t* src = payload + 10;
  // 1. Tag-uri mașinuțe (OUI C0:49:EF:00:00:0X)
  if (src[0] == 0xC0 && src[1] == 0x49 && src[2] == 0xEF
      && src[3] == 0x00 && src[4] == 0x00) {
    uint8_t last = src[5];
    if (last >= 1 && last <= 3) {
      uint8_t idx = last - 1;
      taguri[idx].rssi = pkt->rx_ctrl.rssi;
      taguri[idx].ultim_vazut = millis();
    }
    return;
  }
  // 2. Alt probe request = telefon real / device WiFi în zonă
  // Cache circular: caut MAC existent, sau slot liber, sau înlocuiesc cel mai vechi
  unsigned long acum = millis();
  int slot_existent = -1;
  int slot_liber = -1;
  int slot_vechi = 0;
  unsigned long cel_mai_vechi = telefoane[0].ultim_vazut;
  for (int i = 0; i < MAX_TELEFOANE; i++) {
    if (telefoane[i].ultim_vazut > 0 && memcmp((const void*)telefoane[i].mac, src, 6) == 0) {
      slot_existent = i; break;
    }
    if (telefoane[i].ultim_vazut == 0 && slot_liber < 0) slot_liber = i;
    if (telefoane[i].ultim_vazut > 0 && telefoane[i].ultim_vazut < cel_mai_vechi) {
      cel_mai_vechi = telefoane[i].ultim_vazut;
      slot_vechi = i;
    }
  }
  int slot = (slot_existent >= 0) ? slot_existent : (slot_liber >= 0 ? slot_liber : slot_vechi);
  for (int b = 0; b < 6; b++) telefoane[slot].mac[b] = src[b];
  telefoane[slot].ultim_vazut = acum;
  telefoane[slot].rssi = pkt->rx_ctrl.rssi;
}

void setup_i2s() {
  i2s_config_t cfg = {
    .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX),
    .sample_rate = SAMPLE_RATE,
    .bits_per_sample = I2S_BITS_PER_SAMPLE_32BIT,
    .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
    .communication_format = I2S_COMM_FORMAT_STAND_I2S,
    .intr_alloc_flags = 0,
    .dma_buf_count = 4,
    .dma_buf_len = 512,
    .use_apll = false,
  };
  i2s_pin_config_t pins = {
    .mck_io_num = I2S_PIN_NO_CHANGE,
    .bck_io_num = SCK_PIN,
    .ws_io_num = WS_PIN,
    .data_out_num = I2S_PIN_NO_CHANGE,
    .data_in_num = SD_PIN
  };
  i2s_driver_install(I2S_PORT, &cfg, 0, NULL);
  i2s_set_pin(I2S_PORT, &pins);
}

// Cate masini aproape (RSSI > prag, văzute recent)
int conteaza_masini_aproape(unsigned long acum) {
  int n = 0;
  for (int i = 0; i < 3; i++) {
    if (taguri[i].ultim_vazut > 0 && acum - taguri[i].ultim_vazut < TIMEOUT_TAG
        && taguri[i].rssi > RSSI_APROAPE) n++;
  }
  return n;
}

// Cate apasari pieton in ultimele 30s
int conteaza_pietoni_recenti(unsigned long acum) {
  int n = 0;
  for (int i = 0; i < 10; i++) {
    if (apasari_recente[i] > 0 && acum - apasari_recente[i] < 30000) n++;
  }
  return n;
}

// Update direction tracking pentru un tag — origine→destinație cu histeresis
void update_directie(int t, unsigned long acum, int8_t rssi_glob, const char* best_node) {
  // Reset complet dacă tag-ul a dispărut de mult
  if (acum - tag_dir[t].ultim_aproape > UITARE_DIR_MS) {
    tag_dir[t].origine[0] = 0;
    tag_dir[t].destinatie[0] = 0;
    tag_dir[t].candidat[0] = 0;
  }
  // Daca rssi prea slab, nu fac update activ (păstrez ultima destinație ca „inertia")
  if (rssi_glob < RSSI_LA_INTERSECTIE) return;
  // RSSI suficient → tag-ul e aproape de cineva, marchez timestamp
  tag_dir[t].ultim_aproape = acum;

  // Cazul: nicio destinație înca → o setez direct (fără histeresis pentru prima detection)
  if (tag_dir[t].destinatie[0] == 0) {
    strncpy(tag_dir[t].destinatie, best_node, 4);
    tag_dir[t].destinatie[3] = 0;
    tag_dir[t].candidat[0] = 0;
    return;
  }
  // best_node == destinație curentă → nimic de schimbat, reset candidat
  if (strncmp(tag_dir[t].destinatie, best_node, 4) == 0) {
    tag_dir[t].candidat[0] = 0;
    return;
  }
  // best_node diferit de destinație — verifică/start candidat
  if (strncmp(tag_dir[t].candidat, best_node, 4) != 0) {
    strncpy(tag_dir[t].candidat, best_node, 4);
    tag_dir[t].candidat[3] = 0;
    tag_dir[t].candidat_de_la = acum;
    return;
  }
  // Candidatul s-a stabilizat 2s+ → promovează
  if (acum - tag_dir[t].candidat_de_la >= SUSTAINED_MS) {
    strncpy(tag_dir[t].origine, tag_dir[t].destinatie, 4);
    tag_dir[t].origine[3] = 0;
    strncpy(tag_dir[t].destinatie, best_node, 4);
    tag_dir[t].destinatie[3] = 0;
    tag_dir[t].candidat[0] = 0;
    Serial.printf("[DIR] M%d: %.4s → %.4s\n", t + 1, tag_dir[t].origine, tag_dir[t].destinatie);
  }
}

// Decide durata la INTRAREA in stare (adaptive, lent demonstrativ)
unsigned long durata_pentru(int s, unsigned long acum) {
  if (s == VERDE_MASINI) {
    int m = conteaza_masini_aproape(acum);
    return (m >= 3) ? 12000 : 8000;      // trafic mare (3+ mașini) → 12s, normal 8s
  }
  if (s == ROSU_MASINI) {
    int p = conteaza_pietoni_recenti(acum);
    // Pietoni virtuali (telefoane în zonă) cresc nevoia
    int t = telefoane_n_global;
    if (p >= 3 || t >= 5) return 9000;   // multi pietoni reali SAU multe telefoane → 9s
    if (t >= 2) return 7000;             // câteva telefoane → 7s
    return 6000;                          // normal
  }
  return 2500;                            // galben (ambele tranziții) → 2.5s
}

// Centreaza text pe OLED la y dat (size 1/2/3)
void print_centrat(const char* s, int y, int size) {
  oled.setTextSize(size);
  int16_t x1, y1; uint16_t w, h;
  oled.getTextBounds(s, 0, 0, &x1, &y1, &w, &h);
  int x = (128 - w) / 2;
  oled.setCursor(x < 0 ? 0 : x, y);
  oled.print(s);
}

void deseneaza_oled(unsigned long acum, bool sirena, bool wave_activ, int masini, int idx_aproape,
                    int8_t rssi_aproape, int vecini_n, int telefoane_n,
                    char dir_per_tag[3][4], int8_t rssi_global_tag[3]) {
  oled.clearDisplay();

  if (sirena) {
    // Mod CORIDOR VERDE — afișeaza traseul + ambulanță animată
    oled.setTextColor(SSD1306_WHITE);
    // Header cu sursa
    char src[16];
    snprintf(src, sizeof(src), "AMBULANTA %s", nod_sirena);
    oled.setTextSize(1);
    oled.setCursor(0, 0);
    oled.print(src);
    oled.drawFastHLine(0, 10, 128, SSD1306_WHITE);

    // Text mare CORIDOR / VERDE
    print_centrat("CORIDOR", 14, 2);
    print_centrat("VERDE", 32, 2);

    // Săgeată mare jos, animată spre dreapta (spre spital)
    int sage_offset = (int)((acum / 80) % 16);   // 0..15 cicluri 80ms
    int ay = 54;
    // 3 chevron-uri „» » »" care se mută la dreapta
    for (int i = 0; i < 4; i++) {
      int cx = 16 + i * 28 + sage_offset;
      if (cx > 128) cx -= 96;
      oled.drawLine(cx - 5, ay - 4, cx, ay, SSD1306_WHITE);
      oled.drawLine(cx, ay, cx - 5, ay + 4, SSD1306_WHITE);
      oled.drawLine(cx - 4, ay - 4, cx + 1, ay, SSD1306_WHITE);
      oled.drawLine(cx + 1, ay, cx - 4, ay + 4, SSD1306_WHITE);
    }
    oled.display();
    return;
  }

  // Header sus, mic (size 1) — include telefoane (T) ca pietoni virtuali + marker WAVE
  char hdr[32];
  if (wave_activ) {
    snprintf(hdr, sizeof(hdr), "%s WAVE-%.2s M%d T%d", NODE_ID, nod_wave, masini, telefoane_n);
  } else {
    snprintf(hdr, sizeof(hdr), "%s M%d V%d T%d P%d", NODE_ID, masini, vecini_n, telefoane_n, pietoni);
  }
  oled.setTextSize(1);
  oled.setCursor(0, 0);
  oled.print(hdr);
  oled.drawFastHLine(0, 10, 128, wave_activ ? SSD1306_WHITE : SSD1306_WHITE);
  if (wave_activ) {
    // Marker subțire deasupra liniei pentru a evidenția stare specială
    oled.drawFastHLine(0, 11, 128, SSD1306_WHITE);
  }

  // Stare mare (size 2) centrat
  print_centrat(NUME[stare], 14, 2);
  print_centrat(SUBT[stare], 32, 1);

  // Countdown jos dreapta — secunde mari (size 2)
  unsigned long ramas = (durata_stare > acum - timp_intrat) ? (durata_stare - (acum - timp_intrat)) : 0;
  unsigned long sec = (ramas + 999) / 1000;
  char buf[8];
  snprintf(buf, sizeof(buf), "%lus", sec);
  oled.setTextSize(2);
  int16_t x1, y1; uint16_t w, h;
  oled.getTextBounds(buf, 0, 0, &x1, &y1, &w, &h);
  oled.setCursor(128 - w - 2, 44);
  oled.print(buf);

  // Progress bar countdown jos (umple invers — scade pe măsură ce timpul trece)
  if (durata_stare > 0) {
    int progBarY = 62;
    int progLen = (int)((128 * ramas) / durata_stare);
    if (progLen < 0) progLen = 0;
    if (progLen > 128) progLen = 128;
    oled.drawFastHLine(0, progBarY, 128, SSD1306_WHITE);
    oled.drawFastHLine(0, progBarY + 1, progLen, SSD1306_WHITE);
  }

  // Direction tracking jos stânga: "M1:N1>N2 M2@N2" (origine→destinație cu histeresis)
  oled.setTextSize(1);
  oled.setCursor(0, 50);
  int print_count = 0;
  for (int t = 0; t < 3 && print_count < 2; t++) {
    if (tag_dir[t].destinatie[0]) {
      oled.print("M"); oled.print(t + 1);
      // Doar 2 chars din NODE_ID ("N1", "N2", "N3")
      char destbuf[3] = { tag_dir[t].destinatie[0], tag_dir[t].destinatie[1], 0 };
      if (tag_dir[t].origine[0]) {
        oled.print(":");
        char origbuf[3] = { tag_dir[t].origine[0], tag_dir[t].origine[1], 0 };
        oled.print(origbuf);
        oled.print(">");
        oled.print(destbuf);
      } else {
        oled.print("@");
        oled.print(destbuf);
      }
      oled.print(" ");
      print_count++;
    }
  }

  oled.display();
}

void deseneaza_led(int s, unsigned long acum, bool sirena) {
  leduri.clear();
  if (sirena) {
    // CORIDOR VERDE — verde mașini steady (drum liber pentru ambulanță) + LED4 roșu pulsing
    // ca să distinge de ciclul normal (LED roșu mașini clipește alert)
    leduri.setPixelColor(2, leduri.Color(0, 100, 0));     // verde mașini, intens
    bool blink = ((acum / 200) % 2) == 0;
    if (blink) leduri.setPixelColor(4, leduri.Color(120, 0, 0));  // alert roșu pulsing
    // Pieton rămâne roșu (nu treci când e ambulanță)
    leduri.setPixelColor(0, leduri.Color(80, 0, 0));
  } else if (s == VERDE_MASINI) {
    leduri.setPixelColor(2, leduri.Color(0, 80, 0));
    leduri.setPixelColor(0, leduri.Color(80, 0, 0));
  } else if (s == GALBEN_SPRE_R || s == GALBEN_SPRE_V) {
    leduri.setPixelColor(3, leduri.Color(120, 90, 0));
    leduri.setPixelColor(0, leduri.Color(80, 0, 0));
  } else if (s == ROSU_MASINI) {
    leduri.setPixelColor(4, leduri.Color(80, 0, 0));
    leduri.setPixelColor(1, leduri.Color(0, 80, 0));
  }
  leduri.show();
}

void setup() {
  Serial.begin(115200);
  delay(200);
  Serial.println();
  Serial.print("=== VIA Nod ");
  Serial.print(NODE_ID);
  Serial.println(" v2 booting ===");

  leduri.begin();
  pinMode(PIN_BUTON, INPUT_PULLUP);

  if (!oled.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
    Serial.println("EROARE: OLED nu raspunde la I2C 0x3C");
    while (true) { delay(1000); }
  }
  oled.clearDisplay();
  oled.setTextColor(SSD1306_WHITE);
  oled.setTextSize(1);
  oled.setCursor(20, 24);
  oled.print("Pornesc ");
  oled.print(NODE_ID);
  oled.display();
  Serial.println("OK: OLED");

  setup_i2s();
  Serial.println("OK: microfon I2S");

  WiFi.mode(WIFI_STA);
  esp_wifi_set_channel(WIFI_CANAL, WIFI_SECOND_CHAN_NONE);
  if (esp_now_init() != ESP_OK) {
    Serial.println("EROARE: ESP-NOW init"); while (true) delay(1000);
  }
  esp_now_peer_info_t peer = {};
  memcpy(peer.peer_addr, BROADCAST_MAC, 6);
  peer.channel = WIFI_CANAL;
  peer.encrypt = false;
  esp_now_add_peer(&peer);
  esp_now_register_recv_cb(on_recv);
  Serial.print("OK: ESP-NOW pe canal ");
  Serial.println(WIFI_CANAL);

  // MAC propriu (util la debug)
  uint8_t my_mac[6];
  esp_wifi_get_mac(WIFI_IF_STA, my_mac);
  Serial.printf("MAC nod: %02X:%02X:%02X:%02X:%02X:%02X\n",
    my_mac[0], my_mac[1], my_mac[2], my_mac[3], my_mac[4], my_mac[5]);

  esp_wifi_set_promiscuous_rx_cb(&sniff_cb);
  wifi_promiscuous_filter_t filt = { .filter_mask = WIFI_PROMIS_FILTER_MASK_MGMT };
  esp_wifi_set_promiscuous_filter(&filt);
  esp_wifi_set_promiscuous(true);
  Serial.println("OK: sniff probe requests activ (OUI C0:49:EF:00:00:XX)");

  timp_intrat = millis();
  durata_stare = durata_pentru(stare, timp_intrat);
}

void loop() {
  unsigned long acum = millis();

  // 1. Microfon — citește frame
  size_t citit = 0;
  i2s_read(I2S_PORT, buffer_i2s, sizeof(buffer_i2s), &citit, 0);
  int32_t maxim = -2147483648, minim = 2147483647;
  for (int i = 0; i < BUFFER_SIZE; i++) {
    if (buffer_i2s[i] > maxim) maxim = buffer_i2s[i];
    if (buffer_i2s[i] < minim) minim = buffer_i2s[i];
  }
  int32_t amplitudine = (maxim - minim) >> 16;

  // 1b. Goertzel pentru 750Hz si 600Hz + energie totala
  // Calculam doar daca amplitudine > prag minim (skip processing pe liniste)
  float ratio = 0.0f;
  float g_hi = 0.0f, g_lo = 0.0f, e_total = 0.0f;
  if (amplitudine > PRAG_AMP) {
    float q1_hi = 0, q2_hi = 0, q1_lo = 0, q2_lo = 0;
    for (int i = 0; i < BUFFER_SIZE; i++) {
      float s = (float)(buffer_i2s[i] >> 16);   // scale la int16 range
      e_total += s * s;
      float q0_hi = COEFF_HI * q1_hi - q2_hi + s;
      q2_hi = q1_hi; q1_hi = q0_hi;
      float q0_lo = COEFF_LO * q1_lo - q2_lo + s;
      q2_lo = q1_lo; q1_lo = q0_lo;
    }
    g_hi = q1_hi*q1_hi + q2_hi*q2_hi - q1_hi*q2_hi*COEFF_HI;
    g_lo = q1_lo*q1_lo + q2_lo*q2_lo - q1_lo*q2_lo*COEFF_LO;
    if (e_total > 1.0f) ratio = (g_hi + g_lo) / e_total;
  }

  // 2. Detect sirena: SAU (amp + ratio tonal) SAU (amp foarte mare orice tonalitate)
  bool tonal = (amplitudine > PRAG_AMP) && (ratio > PRAG_TON);
  bool huge = amplitudine > PRAG_AMP_HUGE;
  if (tonal || huge) frames_tonal_seq++; else frames_tonal_seq = 0;

  if (frames_tonal_seq >= N_CONSEC && !sirena_locala) {
    sirena_locala = true;
    timp_sirena = acum;
    timp_intrat = acum;             // reset countdown — afișează cât a rămas din DURATA_SIRENA
    stare = VERDE_MASINI;
    durata_stare = DURATA_SIRENA;
    strncpy(nod_sirena, NODE_ID, 4); nod_sirena[3] = 0;
    mesaj_t m;
    strncpy(m.node_id, NODE_ID, 4);
    m.tip = 1;
    m.scor = (uint32_t)(ratio * 1000);
    for (int t = 0; t < 3; t++) {
      m.rssi_tags[t] = (taguri[t].ultim_vazut > 0 && acum - taguri[t].ultim_vazut < TIMEOUT_TAG)
                        ? taguri[t].rssi : -127;
    }
    for (int i = 0; i < 5; i++) {
      esp_now_send(BROADCAST_MAC, (uint8_t*)&m, sizeof(m));
      delay(20);
    }
    Serial.printf("[MIC] SIRENA TONALA local @ amp=%ld ratio=%.2f\n", (long)amplitudine, ratio);
  }

  // 2b. Cascadă val — vecinii intră cu offset
  if (intra_ambulanta_la > 0 && acum >= intra_ambulanta_la && !sirena_de_la_vecin) {
    sirena_de_la_vecin = true;
    timp_sirena = acum;
    timp_intrat = acum;             // reset countdown pentru afișare corectă
    stare = VERDE_MASINI;
    durata_stare = DURATA_SIRENA;
    Serial.printf("[VAL] intru in AMBULANTA (dupa offset)\n");
  }

  bool sirena = sirena_locala || sirena_de_la_vecin;
  if (sirena && (acum - timp_sirena > DURATA_SIRENA)) {
    sirena_locala = false;
    sirena_de_la_vecin = false;
    intra_ambulanta_la = 0;
    nod_sirena[0] = '?'; nod_sirena[1] = 0;
    timp_intrat = acum;
    stare = VERDE_MASINI;
    durata_stare = durata_pentru(stare, acum);
  }

  // 3. ALIVE broadcast cu rssi_tags + extended state (pentru retransmit pe N1 Serial)
  if (acum - ultim_broadcast >= INTERVAL_ALIVE) {
    mesaj_t m;
    strncpy(m.node_id, NODE_ID, 4);
    m.tip = 0;
    m.scor = (uint32_t)pietoni;
    for (int t = 0; t < 3; t++) {
      m.rssi_tags[t] = (taguri[t].ultim_vazut > 0 && acum - taguri[t].ultim_vazut < TIMEOUT_TAG)
                        ? taguri[t].rssi : -127;
    }
    m.stare = (uint8_t)stare;
    unsigned long ramas_calc = (durata_stare > acum - timp_intrat) ? (durata_stare - (acum - timp_intrat)) : 0;
    m.ramas_ms = (ramas_calc > 65535) ? 65535 : (uint16_t)ramas_calc;
    m.telefoane = (telefoane_n_global > 255) ? 255 : (uint8_t)telefoane_n_global;
    m.pietoni = (pietoni > 255) ? 255 : (uint8_t)pietoni;
    m.flags = 0;
    if (sirena_locala || sirena_de_la_vecin) m.flags |= 0x01;
    if (wave_pana_la > acum) m.flags |= 0x02;
    esp_now_send(BROADCAST_MAC, (uint8_t*)&m, sizeof(m));
    delay(15);
    esp_now_send(BROADCAST_MAC, (uint8_t*)&m, sizeof(m));
    ultim_broadcast = acum;
  }

  // 4. Vecini activi
  int vecini_n = 0;
  for (int i = 0; i < 8; i++) {
    if (vecini_arr[i].ultim_vazut > 0 && acum - vecini_arr[i].ultim_vazut < TIMP_VECIN_VECHI) {
      vecini_n++;
    }
  }

  // 4b. Telefoane (probe requests externe) — pietoni virtuali în zonă
  int telefoane_n = 0;
  for (int i = 0; i < MAX_TELEFOANE; i++) {
    if (telefoane[i].ultim_vazut > 0 && acum - telefoane[i].ultim_vazut < TIMEOUT_TELEFON) {
      telefoane_n++;
    }
  }
  telefoane_n_global = telefoane_n;   // util în durata_pentru()

  // 5. Tag-uri vazute local + triangulare (global)
  int masini = 0;
  int cel_mai_aproape = -1;
  int8_t cel_mai_bun_rssi = -127;
  for (int i = 0; i < 3; i++) {
    if (taguri[i].ultim_vazut > 0 && acum - taguri[i].ultim_vazut < TIMEOUT_TAG) {
      masini++;
      if (taguri[i].rssi > cel_mai_bun_rssi) {
        cel_mai_bun_rssi = taguri[i].rssi;
        cel_mai_aproape = i + 1;
      }
    }
  }

  // Triangulare: pentru fiecare tag, găsește nodul (local sau vecin) cu RSSI maxim
  char dir_per_tag[3][4] = { {0}, {0}, {0} };   // node_id al celui mai aproape (instant)
  int8_t rssi_global_tag[3] = { -127, -127, -127 };
  for (int t = 0; t < 3; t++) {
    if (taguri[t].ultim_vazut > 0 && acum - taguri[t].ultim_vazut < TIMEOUT_TAG) {
      rssi_global_tag[t] = taguri[t].rssi;
      strncpy(dir_per_tag[t], NODE_ID, 4);
    }
    for (int v = 0; v < 8; v++) {
      if (vecini_arr[v].ultim_vazut > 0 && acum - vecini_arr[v].ultim_vazut < TIMP_VECIN_VECHI) {
        if (vecini_arr[v].rssi_tags[t] != -127 && vecini_arr[v].rssi_tags[t] > rssi_global_tag[t]) {
          rssi_global_tag[t] = vecini_arr[v].rssi_tags[t];
          strncpy(dir_per_tag[t], vecini_arr[v].node_id, 4);
        }
      }
    }
    // Update direction tracking cu histeresis
    update_directie(t, acum, rssi_global_tag[t], dir_per_tag[t]);
  }

  // 5b. Green wave: dacă local văd 2+ mașinuțe aproape, trimit CORIDOR_REQ
  int m_aproape = conteaza_masini_aproape(acum);
  if (m_aproape >= 3 && acum - ultim_wave_trimis > WAVE_COOLDOWN && !sirena) {
    mesaj_t m;
    strncpy(m.node_id, NODE_ID, 4);
    m.tip = 2;
    m.scor = (uint32_t)m_aproape;
    for (int t = 0; t < 3; t++) {
      m.rssi_tags[t] = (taguri[t].ultim_vazut > 0 && acum - taguri[t].ultim_vazut < TIMEOUT_TAG)
                        ? taguri[t].rssi : -127;
    }
    for (int i = 0; i < 3; i++) {
      esp_now_send(BROADCAST_MAC, (uint8_t*)&m, sizeof(m));
      delay(15);
    }
    wave_pana_la = acum + WAVE_DURATA;
    strncpy(nod_wave, NODE_ID, 4); nod_wave[3] = 0;
    ultim_wave_trimis = acum;
    // Forțez countdown afișat la WAVE_DURATA și state VERDE
    stare = VERDE_MASINI;
    timp_intrat = acum;
    durata_stare = WAVE_DURATA;
    Serial.printf("[WAVE] Trimit CORIDOR (%d masini aproape local)\n", m_aproape);
  }

  // 5c. Wave activ → forțez VERDE_MASINI (NU în sirena — ambulanța are prioritate)
  bool wave_activ = (wave_pana_la > acum) && !sirena;
  if (wave_activ) {
    unsigned long wave_remaining = wave_pana_la - acum;
    // Forțez doar dacă stare != VERDE sau countdown nu acoperă wave-ul
    bool need_force = (stare != VERDE_MASINI) ||
                      ((durata_stare > (acum - timp_intrat)) ? (durata_stare - (acum - timp_intrat)) : 0) < wave_remaining;
    if (need_force) {
      stare = VERDE_MASINI;
      timp_intrat = acum;
      durata_stare = wave_remaining;
    }
  }

  // 6. Tranzitie stare (NU in sirena, NU in wave)
  if (!sirena && !wave_activ && acum - timp_intrat >= durata_stare) {
    stare = (stare + 1) % NUM_STARI;
    timp_intrat = acum;
    durata_stare = durata_pentru(stare, acum);
    Serial.printf("[STARE] -> %s/%s durata=%lums\n", NUME[stare], SUBT[stare], durata_stare);
  }

  // 7. Buton: long-press = AMBULANTA manual, short = pieton
  bool buton_jos = (digitalRead(PIN_BUTON) == LOW);
  if (buton_jos && !buton_apasat) {
    buton_apasat = true;
    buton_apasat_la = acum;
    long_press_consumat = false;
  }
  if (buton_jos && buton_apasat && !long_press_consumat && acum - buton_apasat_la >= LONG_PRESS_MS) {
    long_press_consumat = true;
    if (!sirena_locala) {
      sirena_locala = true;
      timp_sirena = acum;
      timp_intrat = acum;             // reset countdown — durata afișată = DURATA_SIRENA
      stare = VERDE_MASINI;
      durata_stare = DURATA_SIRENA;
      strncpy(nod_sirena, NODE_ID, 4); nod_sirena[3] = 0;
      mesaj_t m;
      strncpy(m.node_id, NODE_ID, 4);
      m.tip = 1;
      m.scor = 9999;
      for (int t = 0; t < 3; t++) {
        m.rssi_tags[t] = (taguri[t].ultim_vazut > 0 && acum - taguri[t].ultim_vazut < TIMEOUT_TAG)
                          ? taguri[t].rssi : -127;
      }
      for (int i = 0; i < 5; i++) {
        esp_now_send(BROADCAST_MAC, (uint8_t*)&m, sizeof(m));
        delay(20);
      }
      Serial.println("[BUTON] LONG-PRESS → AMBULANTA manual");
    }
  }
  if (!buton_jos && buton_apasat) {
    if (!long_press_consumat && acum - ultima_apasare >= ANTI_BOUNCE) {
      pietoni++;
      ultima_apasare = acum;
      int idx_min = 0;
      for (int i = 1; i < 10; i++) if (apasari_recente[i] < apasari_recente[idx_min]) idx_min = i;
      apasari_recente[idx_min] = acum;
      Serial.println("[BUTON] pieton apasat (short)");
    }
    buton_apasat = false;
  }

  // 8. LED + OLED
  deseneaza_led(stare, acum, sirena);
  deseneaza_oled(acum, sirena, wave_activ, masini, cel_mai_aproape, cel_mai_bun_rssi, vecini_n, telefoane_n,
                 dir_per_tag, rssi_global_tag);

  // 9. Serial debug la 500ms
  if (acum - ultim_print >= INTERVAL_PRINT) {
    unsigned long ramas_ms = (durata_stare > acum - timp_intrat) ? (durata_stare - (acum - timp_intrat)) : 0;
    Serial.printf("[%s] amp=%ld r=%.2f %s/%s ramas=%lums M=%d V=%d T=%d P=%d",
      NODE_ID, (long)amplitudine, ratio, NUME[stare], SUBT[stare], ramas_ms, masini, vecini_n, telefoane_n, pietoni);
    if (sirena) Serial.printf(" !AMBULANTA-de-la-%s!", nod_sirena);
    if (wave_activ) Serial.printf(" !WAVE-de-la-%s(%lums)!", nod_wave, wave_pana_la - acum);
    for (int t = 0; t < 3; t++) {
      if (tag_dir[t].destinatie[0]) {
        if (tag_dir[t].origine[0]) {
          Serial.printf(" M%d:%.4s>%.4s(%d)", t + 1, tag_dir[t].origine, tag_dir[t].destinatie, rssi_global_tag[t]);
        } else {
          Serial.printf(" M%d@%.4s(%d)", t + 1, tag_dir[t].destinatie, rssi_global_tag[t]);
        }
      }
    }
    for (int i = 0; i < 8; i++) {
      if (vecini_arr[i].ultim_vazut > 0 && acum - vecini_arr[i].ultim_vazut < TIMP_VECIN_VECHI) {
        Serial.printf(" vec=%.4s", vecini_arr[i].node_id);
      }
    }
    Serial.println();

    // RETRANSMIT: pentru fiecare vecin activ, printez o linie cu status-ul lui
    // ca radar/debug să vadă toate 3 noduri fără să le conectăm pe toate la USB
    for (int i = 0; i < 8; i++) {
      vecin_t& v = vecini_arr[i];
      if (v.ultim_vazut == 0 || acum - v.ultim_vazut >= TIMP_VECIN_VECHI) continue;
      if (v.stare >= NUM_STARI) continue;   // stare invalida = nu retransmit
      Serial.printf("[%.4s_r] %s/%s ramas=%ums T=%u P=%u",
        v.node_id, NUME[v.stare], SUBT[v.stare], v.ramas_ms, v.telefoane, v.pietoni);
      if (v.flags & 0x01) Serial.printf(" !AMBULANTA-de-la-%.4s!", v.node_id);
      if (v.flags & 0x02) Serial.printf(" !WAVE-de-la-%.4s!", v.node_id);
      // tag-uri din perspectiva acestui vecin
      for (int t = 0; t < 3; t++) {
        if (v.rssi_tags[t] != -127) {
          Serial.printf(" M%d_vecin=%d", t + 1, v.rssi_tags[t]);
        }
      }
      Serial.println();
    }

    ultim_print = acum;
  }

  delay(20);
}

Toți N1/N2/N3 au cod IDENTIC, doar NODE_ID diferă. Toți M1/M2/M3 la fel, doar ultimul byte MAC. Asta înseamnă că poți edita o singură dată N1, copia, schimbi NODE_ID și salvezi sub alt nume pentru N2.

Schimbări v2 față de versiunea inițială:

  • 4 stări în loc de 3 — galben între VERDE→ROȘU și galben între ROȘU→VERDE (cum e în realitate, nu doar într-o direcție).
  • Durată adaptivă: VERDE_MASINI = 5s normal, dar 8s dacă văd 2+ mașini aproape (RSSI > -65 dBm); ROSU_MASINI = 4s normal, dar 7s dacă au fost 3+ apăsări pe buton în ultimele 30s.
  • Mod AMBULANȚĂ: la sirenă, toate cele 5 LED-uri clipesc galben (mod ALERT, stop universal) iar OLED-ul devine alb invertat cu „AMBULANTA trece!" pulsing. Durează 6s, apoi ciclul reia de la VERDE_MASINI. Sirena se propagă pe ESP-NOW spre toate celelalte noduri (5x repeat).
  • Tracking vecini cu MAC complet + node_id — array de 8 sloturi, fiecare cu MAC ID. Serial print arată vec=N1 vec=N3 (clar care vecini se văd, nu un număr abstract).
  • ESP-NOW fiabil: ALIVE la 1s (era 2s), trimis 2x consecutiv per cycle. Timeout vecin 10s (era 6s — prea agresiv pentru broadcast loss). Codul vechi pierdea cca 30% din ALIVE → vecini = 0 sau 1; v2 fixează asta.
  • OLED redesign mare: header subțire sus (NODE_ID, mașini, vecini, pietoni), stare MARE centrată (size 2), subtitlu (size 1), countdown secunde mare jos-dreapta. La sirenă, totul invertat + pulse-bar.
  • Tag scan la 800ms (era 2s) — nodurile văd mașinuțele mult mai fiabil în timpul demo-ului.

Protocol de test (înainte de concurs)

Ordine clară de testare. NU sări peste pași — fiecare confirmă o piesă din sistem. Bifează pe măsură ce merge.

  1. PAS 1 · Tuning PRAG_SIRENA pe N1 (15 min)
    1. Flashezi pe N1 sketch-ul tuning de mai jos.
    2. Deschizi Arduino IDE → Tools → Serial Plotter (115200).
    3. Cu liniște în cameră → vezi un grafic plat (zgomot ~ 100-500).
    4. Pornești sirena MP3 lângă microfon → grafic urcă brusc → notează valoarea de vârf.
    5. Pune PRAG_SIRENA în codul final la 60% din vârf (ex: vârf 8000 → PRAG = 4800).
  2. PAS 2 · Flashezi N1 cu codul final (5 min)
    1. Selectezi tab N1 sus, Copy, paste în Arduino IDE, schimbi PRAG_SIRENA dacă-i nevoie, Upload.
    2. Deschizi Serial Monitor 115200 → ar trebui să vezi:
    === VIA Nod N1 v2 booting ===
    OK: OLED
    OK: microfon I2S
    OK: ESP-NOW pe canal 1
    MAC nod: XX:XX:XX:XX:XX:XX
    OK: sniff probe requests activ (OUI C0:49:EF:00:00:XX)
    [STARE] -> GALBEN durata=1500ms
    [N1] amp=152 VERDE/MASINI ramas=4750ms M=0 V=0 P=0
    [N1] amp=148 VERDE/MASINI ramas=4250ms M=0 V=0 P=0
    ...

    Pe OLED: header subțire sus „N1 M0 V0 P0", apoi „VERDE / MASINI" mare centrat, countdown „5s" jos dreapta. Ciclu 4 stări: VERDE(5s) → GALBEN(1.5s) → ROȘU(4s) → GALBEN(1.5s) → repeat.

  3. PAS 3 · Verifici un tag mașinuță (M1) (5 min)
    1. Flashezi tag-ul M1 cu tab M1 (board ESP32C3 Dev Module).
    2. Pornești switch-ul tag-ului → LED roșu pe TP4056 stins, ESP32-C3 activ.
    3. Apropii tag-ul de N1 (≤30 cm).
    4. Serial Monitor N1 → după 2-4 secunde apare M1=-45 (sau valoare similară) la sfârșit:
    [N1] amp=130 VERDE/MASINI ramas=3200ms M=1 V=0 P=0 M1=-45
    [N1] amp=128 GALBEN/ATENTIE ramas=1100ms M=1 V=0 P=0 M1=-47

    Pe OLED: header arată „M1" (1 mașinuță), iar jos stânga „M1:-45dB". Mută tag-ul la 2m → RSSI scade spre -70/-80. În cealaltă cameră → după 4s, „M0" (dispare).

    Dacă NU apare M1=...: vezi troubleshoot tag-uri mai jos.

  4. PAS 4 · 3 noduri + 3 tag-uri toate odată (10 min)
    1. Flashezi N2 și N3 (cu tab-urile corespunzătoare, doar NODE_ID diferă).
    2. Pornești toate 3 noduri și toate 3 tag-uri.
    3. Pe oricare nod, Serial Monitor → trebuie să vezi:
    [N1] amp=120 VERDE/MASINI ramas=4800ms M=3 V=2 P=0 M1=-52 M2=-68 M3=-45 vec=N2 vec=N3

    V=2 = celelalte 2 noduri (cu vec=N2 vec=N3 la sfârșit ne spune EXACT cine se vede). M=3 = toate 3 tag-uri văzute.

    Dacă V=1 sau 0: ESP-NOW broadcast loss. Verifică pe Serial al fiecărui nod ce vec=... apare. Dacă N2 vede N3 dar nu N1, e probabil interferență fizică (carcasă, distanță). Mută plăcile mai aproape, repetă.

  5. PAS 5 · Test AMBULANȚĂ + propagare ESP-NOW (5 min)
    1. 3 noduri pornite, Serial Monitor pe N2.
    2. Pornești sirena MP3 lângă N1 (vezi mai jos link YouTube).
    3. Pe N1 Serial: [MIC] SIRENA local @ amp=XXXX. Toate 5 LED-uri pe N1 clipesc galben (mod ALERT). OLED-ul lui N1 invertat alb cu „AMBULANTA trece!" pulsing + bar.
    4. Pe N2 și N3 Serial: [ESP-NOW] SIRENA primita de la N1 (în maxim 200ms după N1). Aceleași LED-uri clipind galben + OLED invertat.
    5. După 6s, toate 3 revin la VERDE_MASINI și încep ciclul normal.

    De ce mod ALERT (galben pulsat) și nu „toate verzi"? În realitate când vine ambulanța intersecția trece în „atenție universală" (analog cu semafor în avarie) — ambulanța trece cu prioritate iar mașinile/pietonii se opresc. Vizual mai impresionant pentru pitch.

  6. Troubleshoot tag-uri (dacă PAS 3 eșuează):
    SimptomCauzăFix
    Niciun M1= delocTag nu emite (oprit/baterie)LED pe tag aprins? Switch ON?
    Niciun M1=, dar tag aprinsMAC nu s-a setatPe tag în Serial Monitor → trebuie să apară „MAC tag M1: C0:49:EF:00:00:01"
    RSSI sare brusc 30 dBmAntenă acoperită sau interferență BluetoothDeparte de telefon/router. Țineți tag-ul în mână (nu pe metal).
    M1=-90 chiar lipit de nodESP32-C3 SuperMini antenă slabă pe PCBAcceptabil — pragul de „aproape" e -55 dBm. Mută tag-ul mai aproape.
    Tag dispare după 4s deși-i aproapeTag scan-ul WiFi prea rar (2s/scan e marginal)Scade delay(2000) la delay(800) pe sketch tag — emite probe mai des.

Sketch de tuning PRAG_SIRENA

Sketch minimal cu doar microfonul I2S. Output curat pentru Serial Plotter. Rulează 30s în liniște (notează vârful zgomot), apoi 30s cu sirena MP3 (notează vârful sirenă). Setezi PRAG_SIRENA la jumătatea distanței dintre cele 2.

Board: ESP32 Dev Module. Hardware: doar INMP441 (VDD→3.3V, GND→GND, L/R→GND, WS→GPIO 15, SCK→GPIO 14, SD→GPIO 32).

// Tuning PRAG_SIRENA — printează amplitudine pentru Serial Plotter
#include <driver/i2s.h>

#define I2S_PORT     I2S_NUM_0
#define SCK_PIN      14
#define WS_PIN       15
#define SD_PIN       32
#define SAMPLE_RATE  16000
#define BUFFER_SIZE  512

int32_t buffer_i2s[BUFFER_SIZE];

void setup() {
  Serial.begin(115200);
  delay(200);
  Serial.println("Amplitudine");   // header Serial Plotter

  i2s_config_t cfg = {
    .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX),
    .sample_rate = SAMPLE_RATE,
    .bits_per_sample = I2S_BITS_PER_SAMPLE_32BIT,
    .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
    .communication_format = I2S_COMM_FORMAT_STAND_I2S,
    .intr_alloc_flags = 0,
    .dma_buf_count = 4,
    .dma_buf_len = 512,
    .use_apll = false,
  };
  i2s_pin_config_t pins = {
    .mck_io_num = I2S_PIN_NO_CHANGE,
    .bck_io_num = SCK_PIN,
    .ws_io_num = WS_PIN,
    .data_out_num = I2S_PIN_NO_CHANGE,
    .data_in_num = SD_PIN
  };
  i2s_driver_install(I2S_PORT, &cfg, 0, NULL);
  i2s_set_pin(I2S_PORT, &pins);
}

void loop() {
  size_t citit = 0;
  i2s_read(I2S_PORT, buffer_i2s, sizeof(buffer_i2s), &citit, 0);
  int32_t maxim = -2147483648, minim = 2147483647;
  for (int i = 0; i < BUFFER_SIZE; i++) {
    if (buffer_i2s[i] > maxim) maxim = buffer_i2s[i];
    if (buffer_i2s[i] < minim) minim = buffer_i2s[i];
  }
  int32_t amplitudine = (maxim - minim) >> 16;
  Serial.println(amplitudine);
  delay(50);
}

Cum interpretezi graficul:

  • Liniște (cameră goală, fără vorbit) → 50-300
  • Vorbit normal → 500-1500
  • Strigăt/bătaie din palme → 2000-4000
  • Sirenă MP3 lângă mic → 6000-15000 (depinde volum)
  • Sirenă MP3 la 1m → 3000-6000

Regula: PRAG_SIRENA = jumătatea distanței dintre vârful „strigăt" și vârful „sirenă". Pentru sirenă la ~1m, prag bun e 2500-3500.

🔊 Sursă sirenă pentru test (YouTube)

Recomand un clip loop de minim 10 minute (ca să nu se oprească mid-test). Caută pe YouTube:

→ caută „ambulance siren 10 hours" pe YouTube

Sirena europeană (two-tone) merge cel mai bine cu INMP441 — are vârfuri spectrale curate la ~600Hz și ~800Hz. Volumul telefonului pe ~70%, ține-l la ~30 cm de microfon pentru tuning, apoi mută-l la ~1m pentru testul de demo.


⚠ ÎNAINTE DE PRIMUL COMPILE — Librării de instalat

Dacă vezi fatal error: Adafruit_NeoPixel.h: No such file or directory înseamnă că librăria nu-i instalată.

Soluția: instalează cele 3 librării de mai jos. Durează 1-2 minute total. Apoi rămân salvate permanent pe disc.

Pași:

  1. Arduino IDE → Sketch → Include Library → Manage Libraries... (sau Ctrl+Shift+I)
  2. Caută pe rând și apasă Install pe fiecare:
Caută Autor Pentru
Adafruit NeoPixelAdafruitLED-uri WS2812 (Pas B+)
Adafruit GFX LibraryAdafruitgrafică OLED (Pas D+)
Adafruit SSD1306Adafruitdriver OLED (Pas D+). La instalare va întreba „Install missing dependencies?" → Install all.

După ce le vezi marcate „INSTALLED" verde, închide Library Manager. Re-compilează → ar trebui să meargă. Dacă tot dă eroare: Restart Arduino IDE (uneori cache-ul nu se reîncarcă automat).

Suport ESP32 (dacă nu-l ai deja):

  1. File → Preferences → Additional Boards Manager URLs:
https://espressif.github.io/arduino-esp32/package_esp32_index.json

Apoi Tools → Board → Boards Manager → caută „esp32" → Install. Selectează ESP32 Dev Module pentru plăcile N1/N2/N3, sau ESP32C3 Dev Module pentru tag-uri.


Test 1 — Pas A · Blink intern

Board: ESP32 Dev Module. Hardware: doar placa + cablu USB.

void setup() {
  pinMode(2, OUTPUT);              // pin 2 = LED intern pe majoritatea ESP32 DevKit
}

void loop() {
  digitalWrite(2, HIGH);           // pune 3.3V pe pin 2 → LED aprins
  delay(500);
  digitalWrite(2, LOW);            // pune 0V pe pin 2 → LED stins
  delay(500);
}

Test 1 — Pas B · Primul LED roșu pieton (LED[0])

Hardware: LED[0] cablat (VCC→5V, GND→GND, DI→GPIO 5).

#include <Adafruit_NeoPixel.h>

const int PIN_LED = 5;
const int NUM_LEDS = 1;

Adafruit_NeoPixel leduri(NUM_LEDS, PIN_LED, NEO_GRB + NEO_KHZ800);

void setup() {
  leduri.begin();
  leduri.setPixelColor(0, leduri.Color(80, 0, 0));  // LED[0] roșu (verifică LED fizic = roșu pieton)
  leduri.show();
}

void loop() {
  // nimic
}

Dacă LED-ul se aprinde verde sau albastru în loc de roșu → schimbă NEO_GRB cu NEO_RGB și reflashează.

Test 1 — Pas C · Ciclu semafor REAL

Hardware: toate 5 LED-uri în lanț. 3 stări de semafor, 2 LED-uri aprinse simultan per stare:

#include <Adafruit_NeoPixel.h>

const int PIN_LED = 5;
const int NUM_LEDS = 5;

Adafruit_NeoPixel leduri(NUM_LEDS, PIN_LED, NEO_GRB + NEO_KHZ800);

// Layout fizic: [0]=roșu pieton, [1]=verde pieton, [2]=verde maș, [3]=galben, [4]=roșu maș

const int VERDE_MASINI = 0, GALBEN = 1, ROSU_MASINI = 2;
const unsigned long DURATA[] = { 5000, 1500, 4000 };

int stare = VERDE_MASINI;
unsigned long timp_intrat = 0;

void setup() {
  leduri.begin();
  timp_intrat = millis();
}

void loop() {
  if (millis() - timp_intrat >= DURATA[stare]) {
    stare = (stare + 1) % 3;
    timp_intrat = millis();
  }

  leduri.clear();
  if (stare == VERDE_MASINI) {
    leduri.setPixelColor(2, leduri.Color(0, 80, 0));   // verde mașini
    leduri.setPixelColor(0, leduri.Color(80, 0, 0));   // roșu pieton
  }
  if (stare == GALBEN) {
    leduri.setPixelColor(3, leduri.Color(80, 80, 0));  // galben
    leduri.setPixelColor(0, leduri.Color(80, 0, 0));   // roșu pieton (continuă)
  }
  if (stare == ROSU_MASINI) {
    leduri.setPixelColor(4, leduri.Color(80, 0, 0));   // roșu mașini
    leduri.setPixelColor(1, leduri.Color(0, 80, 0));   // verde pieton
  }
  leduri.show();
}

Test 1 — Pas D · + OLED

Hardware: Pas C + OLED cablat (SDA→GPIO 21, SCL→GPIO 22, VCC→3.3V, GND→GND).

#include <Adafruit_NeoPixel.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

const int PIN_LED = 5;
const int NUM_LEDS = 5;

Adafruit_NeoPixel leduri(NUM_LEDS, PIN_LED, NEO_GRB + NEO_KHZ800);
Adafruit_SSD1306 oled(128, 64, &Wire, -1);

const int VERDE_MASINI = 0, GALBEN = 1, ROSU_MASINI = 2;
const unsigned long DURATA[] = { 5000, 1500, 4000 };
const char* NUME[] = { "VERDE MASINI", "GALBEN", "VERDE PIETON" };

int stare = VERDE_MASINI;
unsigned long timp_intrat = 0;

void setup() {
  leduri.begin();

  if (!oled.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
    while (true) { delay(1000); }    // OLED nu pornește — blocat aici
  }
  oled.clearDisplay();
  oled.setTextColor(SSD1306_WHITE);
  oled.setTextSize(1);

  timp_intrat = millis();
}

void loop() {
  if (millis() - timp_intrat >= DURATA[stare]) {
    stare = (stare + 1) % 3;
    timp_intrat = millis();
  }

  leduri.clear();
  if (stare == VERDE_MASINI) {
    leduri.setPixelColor(2, leduri.Color(0, 80, 0));
    leduri.setPixelColor(0, leduri.Color(80, 0, 0));
  }
  if (stare == GALBEN) {
    leduri.setPixelColor(3, leduri.Color(80, 80, 0));
    leduri.setPixelColor(0, leduri.Color(80, 0, 0));
  }
  if (stare == ROSU_MASINI) {
    leduri.setPixelColor(4, leduri.Color(80, 0, 0));
    leduri.setPixelColor(1, leduri.Color(0, 80, 0));
  }
  leduri.show();

  oled.clearDisplay();
  oled.setCursor(0, 0);
  oled.println("Nod N1");
  oled.println();
  oled.println(NUME[stare]);
  oled.display();
}

Test 1 — Pas E · + Buton pieton

Hardware: Pas D + Buton (S→GPIO 4, GND→GND, VCC neconectat).

#include <Adafruit_NeoPixel.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

const int PIN_LED = 5;
const int NUM_LEDS = 5;
const int PIN_BUTON = 4;

Adafruit_NeoPixel leduri(NUM_LEDS, PIN_LED, NEO_GRB + NEO_KHZ800);
Adafruit_SSD1306 oled(128, 64, &Wire, -1);

const int VERDE_MASINI = 0, GALBEN = 1, ROSU_MASINI = 2;
const unsigned long DURATA[] = { 5000, 1500, 4000 };
const char* NUME[] = { "VERDE MASINI", "GALBEN", "VERDE PIETON" };

int stare = VERDE_MASINI;
unsigned long timp_intrat = 0;

int pietoni = 0;
unsigned long ultima_apasare = 0;
const unsigned long ANTI_BOUNCE = 2000;

void setup() {
  leduri.begin();
  pinMode(PIN_BUTON, INPUT_PULLUP);

  if (!oled.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
    while (true) { delay(1000); }
  }
  oled.clearDisplay();
  oled.setTextColor(SSD1306_WHITE);
  oled.setTextSize(1);

  timp_intrat = millis();
}

void loop() {
  if (millis() - timp_intrat >= DURATA[stare]) {
    stare = (stare + 1) % 3;
    timp_intrat = millis();
  }

  // Citește buton
  if (digitalRead(PIN_BUTON) == LOW) {
    if (millis() - ultima_apasare >= ANTI_BOUNCE) {
      pietoni++;
      ultima_apasare = millis();
    }
  }

  leduri.clear();
  if (stare == VERDE_MASINI) {
    leduri.setPixelColor(2, leduri.Color(0, 80, 0));
    leduri.setPixelColor(0, leduri.Color(80, 0, 0));
  }
  if (stare == GALBEN) {
    leduri.setPixelColor(3, leduri.Color(80, 80, 0));
    leduri.setPixelColor(0, leduri.Color(80, 0, 0));
  }
  if (stare == ROSU_MASINI) {
    leduri.setPixelColor(4, leduri.Color(80, 0, 0));
    leduri.setPixelColor(1, leduri.Color(0, 80, 0));
  }
  leduri.show();

  oled.clearDisplay();
  oled.setCursor(0, 0);
  oled.println("Nod N1");
  oled.println();
  oled.println(NUME[stare]);
  oled.print("Pietoni: ");
  oled.println(pietoni);
  oled.display();
}

Test 2 — Microfon I2S (neverificat pe placă)

Hardware: Pas E + INMP441 (VDD→3.3V, GND→GND, L/R→GND, WS→GPIO 15, SCK→GPIO 14, SD→GPIO 32).

Codul de mai jos înlocuiește complet Pas E. PRAG_SIRENA reglat prin observație pe Serial Monitor.

#include <Adafruit_NeoPixel.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <driver/i2s.h>

const int PIN_LED = 5;
const int NUM_LEDS = 5;
const int PIN_BUTON = 4;

#define I2S_PORT     I2S_NUM_0
#define SCK_PIN      14
#define WS_PIN       15
#define SD_PIN       32
#define SAMPLE_RATE  16000
#define BUFFER_SIZE  512
#define PRAG_SIRENA  3500   // reglează prin observație pe Serial

Adafruit_NeoPixel leduri(NUM_LEDS, PIN_LED, NEO_GRB + NEO_KHZ800);
Adafruit_SSD1306 oled(128, 64, &Wire, -1);

const int VERDE_MASINI = 0, GALBEN = 1, ROSU_MASINI = 2;
const unsigned long DURATA[] = { 5000, 1500, 4000 };
const char* NUME[] = { "VERDE MASINI", "GALBEN", "VERDE PIETON" };

int stare = VERDE_MASINI;
unsigned long timp_intrat = 0;

int pietoni = 0;
unsigned long ultima_apasare = 0;
const unsigned long ANTI_BOUNCE = 2000;

int32_t buffer[BUFFER_SIZE];
bool sirena_detectata = false;
unsigned long timp_sirena = 0;
const unsigned long DURATA_ALERTA_SIRENA = 5000;

void setup_i2s() {
  i2s_config_t cfg = {
    .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX),
    .sample_rate = SAMPLE_RATE,
    .bits_per_sample = I2S_BITS_PER_SAMPLE_32BIT,
    .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
    .communication_format = I2S_COMM_FORMAT_STAND_I2S,
    .intr_alloc_flags = 0,
    .dma_buf_count = 4,
    .dma_buf_len = 512,
    .use_apll = false,
  };
  i2s_pin_config_t pins = {
    .bck_io_num = SCK_PIN,
    .ws_io_num = WS_PIN,
    .data_out_num = -1,
    .data_in_num = SD_PIN,
  };
  i2s_driver_install(I2S_PORT, &cfg, 0, NULL);
  i2s_set_pin(I2S_PORT, &pins);
}

void setup() {
  Serial.begin(115200);
  leduri.begin();
  pinMode(PIN_BUTON, INPUT_PULLUP);

  if (!oled.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
    while (true) { delay(1000); }
  }
  oled.clearDisplay();
  oled.setTextColor(SSD1306_WHITE);
  oled.setTextSize(1);

  setup_i2s();
  Serial.println("OK: Microfon I2S");

  timp_intrat = millis();
}

void loop() {
  // 1. Citește microfon
  size_t citit = 0;
  i2s_read(I2S_PORT, buffer, sizeof(buffer), &citit, 0);
  int32_t maxim = -2147483648, minim = 2147483647;
  for (int i = 0; i < BUFFER_SIZE; i++) {
    if (buffer[i] > maxim) maxim = buffer[i];
    if (buffer[i] < minim) minim = buffer[i];
  }
  int32_t amplitudine = (maxim - minim) >> 16;

  // 2. Detectează sirenă
  if (amplitudine > PRAG_SIRENA) {
    sirena_detectata = true;
    timp_sirena = millis();
    stare = VERDE_MASINI;       // forțează trecerea instant la verde mașini
    timp_intrat = millis();
  }
  if (sirena_detectata && (millis() - timp_sirena > DURATA_ALERTA_SIRENA)) {
    sirena_detectata = false;
  }

  // 3. Tranziție normală stare (dacă nu-i sirenă)
  if (!sirena_detectata && millis() - timp_intrat >= DURATA[stare]) {
    stare = (stare + 1) % 3;
    timp_intrat = millis();
  }

  // 4. Citește buton pieton
  if (digitalRead(PIN_BUTON) == LOW) {
    if (millis() - ultima_apasare >= ANTI_BOUNCE) {
      pietoni++;
      ultima_apasare = millis();
    }
  }

  // 5. LED-uri
  leduri.clear();
  if (stare == VERDE_MASINI) {
    leduri.setPixelColor(2, leduri.Color(0, 80, 0));
    leduri.setPixelColor(0, leduri.Color(80, 0, 0));
  }
  if (stare == GALBEN) {
    leduri.setPixelColor(3, leduri.Color(80, 80, 0));
    leduri.setPixelColor(0, leduri.Color(80, 0, 0));
  }
  if (stare == ROSU_MASINI) {
    leduri.setPixelColor(4, leduri.Color(80, 0, 0));
    leduri.setPixelColor(1, leduri.Color(0, 80, 0));
  }
  leduri.show();

  // 6. OLED
  oled.clearDisplay();
  oled.setCursor(0, 0);
  oled.println("Nod N1");
  if (sirena_detectata) {
    oled.println("!! SIRENA !!");
  } else {
    oled.println(NUME[stare]);
  }
  oled.print("Pietoni: ");
  oled.println(pietoni);
  oled.print("Amp: ");
  oled.println(amplitudine);
  oled.display();

  Serial.print("Amplitudine: ");
  Serial.println(amplitudine);

  delay(30);
}

Test 3 — ESP-NOW mesh (neverificat pe placă)

Hardware: 2 plăci identice cu Test 2. Schimbă NODE_ID între cele 2 reflasheri.

⚠ Despre versiunea ESP32 Arduino core

Codul de mai jos folosește semnătura nouă pentru callback-ul ESP-NOW (ESP32 Arduino core v3.x):

void on_recv(const esp_now_recv_info* info, const uint8_t* data, int len)

Dacă vezi eroarea invalid conversion from 'void (*)(const uint8_t*, ...)' to 'esp_now_recv_cb_t', înseamnă că ai core v3.x dar codul folosea signatura veche.

Pentru core v2.x (mai vechi), foloseste varianta:

void on_recv(const uint8_t* mac, const uint8_t* data, int len)

Verifică versiunea în Tools → Board → Boards Manager → caută „esp32" → vezi versiunea curentă instalată.

#include <Adafruit_NeoPixel.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <driver/i2s.h>
#include <WiFi.h>
#include <esp_now.h>
#include <esp_wifi.h>

#define NODE_ID "N1"   // ← schimbă la "N2" pentru a doua placă

const int PIN_LED = 5;
const int NUM_LEDS = 5;
const int PIN_BUTON = 4;

#define I2S_PORT     I2S_NUM_0
#define SCK_PIN      14
#define WS_PIN       15
#define SD_PIN       32
#define SAMPLE_RATE  16000
#define BUFFER_SIZE  512
#define PRAG_SIRENA  3500

uint8_t BROADCAST_MAC[] = { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF };

typedef struct {
  char node_id[4];
  uint8_t tip;       // 0=ALIVE, 1=SIRENA
  uint32_t scor;
} mesaj_t;

mesaj_t outgoing = { NODE_ID, 0, 0 };

Adafruit_NeoPixel leduri(NUM_LEDS, PIN_LED, NEO_GRB + NEO_KHZ800);
Adafruit_SSD1306 oled(128, 64, &Wire, -1);

const int VERDE_MASINI = 0, GALBEN = 1, ROSU_MASINI = 2;
const unsigned long DURATA[] = { 5000, 1500, 4000 };
const char* NUME[] = { "VERDE MASINI", "GALBEN", "VERDE PIETON" };

int stare = VERDE_MASINI;
unsigned long timp_intrat = 0;
int pietoni = 0;
unsigned long ultima_apasare = 0;
const unsigned long ANTI_BOUNCE = 2000;

int32_t buffer[BUFFER_SIZE];
bool sirena_locala = false;
bool sirena_de_la_vecin = false;
unsigned long timp_sirena = 0;
const unsigned long DURATA_SIRENA = 5000;

int vecini = 0;
unsigned long ultim_alive[5] = {0};
const unsigned long TIMP_VECIN_VECHI = 6000;

unsigned long ultim_broadcast = 0;
const unsigned long INTERVAL_ALIVE = 2000;

// ESP32 Arduino core v3.x: primul parametru e esp_now_recv_info*, nu uint8_t* mac
// Pentru core v2.x: foloseste signatura veche (const uint8_t* mac, ...)
void on_recv(const esp_now_recv_info* info, const uint8_t* data, int len) {
  if (len != sizeof(mesaj_t)) return;
  mesaj_t* m = (mesaj_t*)data;
  if (m->tip == 1) {
    sirena_de_la_vecin = true;
    timp_sirena = millis();
    stare = VERDE_MASINI;
    timp_intrat = millis();
  }
  // simplificat: numără cât timp am primit ALIVE de la cineva
  ultim_alive[0] = millis();
  // MAC-ul expeditorului e disponibil în info->src_addr dacă vrei să-l folosești
}

void setup_i2s() {
  i2s_config_t cfg = {
    .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX),
    .sample_rate = SAMPLE_RATE,
    .bits_per_sample = I2S_BITS_PER_SAMPLE_32BIT,
    .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
    .communication_format = I2S_COMM_FORMAT_STAND_I2S,
    .intr_alloc_flags = 0,
    .dma_buf_count = 4,
    .dma_buf_len = 512,
    .use_apll = false,
  };
  i2s_pin_config_t pins = {
    .mck_io_num = I2S_PIN_NO_CHANGE,
    .bck_io_num = SCK_PIN,
    .ws_io_num = WS_PIN,
    .data_out_num = I2S_PIN_NO_CHANGE,
    .data_in_num = SD_PIN
  };
  i2s_driver_install(I2S_PORT, &cfg, 0, NULL);
  i2s_set_pin(I2S_PORT, &pins);
}

void setup() {
  Serial.begin(115200);
  leduri.begin();
  pinMode(PIN_BUTON, INPUT_PULLUP);

  if (!oled.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
    while (true) { delay(1000); }
  }
  oled.clearDisplay();
  oled.setTextColor(SSD1306_WHITE);
  oled.setTextSize(1);

  setup_i2s();

  WiFi.mode(WIFI_STA);
  esp_wifi_set_channel(1, WIFI_SECOND_CHAN_NONE);
  if (esp_now_init() != ESP_OK) {
    Serial.println("EROARE: ESP-NOW init");
    while (true) delay(1000);
  }
  esp_now_peer_info_t peer = {};
  memcpy(peer.peer_addr, BROADCAST_MAC, 6);
  peer.channel = 1;
  peer.encrypt = false;
  esp_now_add_peer(&peer);
  esp_now_register_recv_cb(on_recv);

  Serial.print("OK ");
  Serial.println(NODE_ID);

  timp_intrat = millis();
}

void loop() {
  // 1. Citește microfon
  size_t citit = 0;
  i2s_read(I2S_PORT, buffer, sizeof(buffer), &citit, 0);
  int32_t maxim = -2147483648, minim = 2147483647;
  for (int i = 0; i < BUFFER_SIZE; i++) {
    if (buffer[i] > maxim) maxim = buffer[i];
    if (buffer[i] < minim) minim = buffer[i];
  }
  int32_t amplitudine = (maxim - minim) >> 16;

  // 2. Detectează sirenă local
  if (amplitudine > PRAG_SIRENA && !sirena_locala) {
    sirena_locala = true;
    timp_sirena = millis();
    stare = VERDE_MASINI;
    timp_intrat = millis();
    // trimit SIRENA prin ESP-NOW (de 3 ori pentru fiabilitate)
    mesaj_t m = { NODE_ID, 1, 0 };
    for (int i = 0; i < 3; i++) {
      esp_now_send(BROADCAST_MAC, (uint8_t*)&m, sizeof(m));
      delay(50);
    }
  }

  // 3. Reset alertă sirenă
  bool sirena = sirena_locala || sirena_de_la_vecin;
  if (sirena && (millis() - timp_sirena > DURATA_SIRENA)) {
    sirena_locala = false;
    sirena_de_la_vecin = false;
  }

  // 4. ALIVE broadcast la fiecare 2s
  if (millis() - ultim_broadcast >= INTERVAL_ALIVE) {
    mesaj_t m = { NODE_ID, 0, (uint32_t)pietoni };
    esp_now_send(BROADCAST_MAC, (uint8_t*)&m, sizeof(m));
    ultim_broadcast = millis();
  }

  // 5. Numără vecini activi
  vecini = 0;
  for (int i = 0; i < 5; i++) {
    if (ultim_alive[i] > 0 && millis() - ultim_alive[i] < TIMP_VECIN_VECHI) {
      vecini++;
    }
  }

  // 6. Tranziție stare (dacă nu sirenă)
  if (!sirena && millis() - timp_intrat >= DURATA[stare]) {
    stare = (stare + 1) % 3;
    timp_intrat = millis();
  }

  // 7. Buton
  if (digitalRead(PIN_BUTON) == LOW) {
    if (millis() - ultima_apasare >= ANTI_BOUNCE) {
      pietoni++;
      ultima_apasare = millis();
    }
  }

  // 8. LED-uri
  leduri.clear();
  if (stare == VERDE_MASINI) {
    leduri.setPixelColor(2, leduri.Color(0, 80, 0));
    leduri.setPixelColor(0, leduri.Color(80, 0, 0));
  }
  if (stare == GALBEN) {
    leduri.setPixelColor(3, leduri.Color(80, 80, 0));
    leduri.setPixelColor(0, leduri.Color(80, 0, 0));
  }
  if (stare == ROSU_MASINI) {
    leduri.setPixelColor(4, leduri.Color(80, 0, 0));
    leduri.setPixelColor(1, leduri.Color(0, 80, 0));
  }
  leduri.show();

  // 9. OLED
  oled.clearDisplay();
  oled.setCursor(0, 0);
  oled.print("Nod ");
  oled.println(NODE_ID);
  if (sirena) {
    oled.println("!! SIRENA !!");
  } else {
    oled.println(NUME[stare]);
  }
  oled.print("Pietoni: ");
  oled.println(pietoni);
  oled.print("Vecini: ");
  oled.println(vecini);
  oled.display();

  delay(30);
}

Tag mașinuță — ESP32-C3 SuperMini + MAC unic

Board: ESP32C3 Dev Module. Hardware: LiPo + TP4056 + switch + ESP32-C3 SuperMini (vezi schema în lecția 08).

Schimbă ultimul byte din MAC_TAG între cele 3 tag-uri: 0x01 pentru tag 1, 0x02 pentru tag 2, 0x03 pentru tag 3.

#include <WiFi.h>
#include <esp_wifi.h>

// MAC unic per tag (schimbă ultimul byte între tag-uri)
uint8_t MAC_TAG[] = { 0xC0, 0x49, 0xEF, 0x00, 0x00, 0x01 };

void setup() {
  Serial.begin(115200);

  WiFi.mode(WIFI_STA);
  esp_wifi_set_mac(WIFI_IF_STA, MAC_TAG);
  WiFi.disconnect();
  delay(100);

  Serial.print("MAC tag: ");
  Serial.println(WiFi.macAddress());
}

void loop() {
  // Scanare WiFi pe canal 1 — emite probe requests
  WiFi.scanNetworks(true, true, false, 100, 1);
  delay(2000);  // sleep 2 secunde
}

Note importante:

  • Codul Test 2 și Test 3 e SCRIS dar nu-i testat pe placă reală. La prima compilare pot apărea warnings sau errors mici de adaptat.
  • Pentru ESP32-C3 SuperMini (tag), folosește board ESP32C3 Dev Module, nu ESP32 Dev Module.
  • Toate sketch-urile pornesc cu un Serial.begin(115200) — deschide Serial Monitor la 115200 ca să vezi mesajele.
  • Dacă upload-ul eșuează cu „Connecting...........": apasă și ține apăsat BOOT pe placă în timp ce dai Upload, eliberează după ce începe transferul.