BLE Beacon Radar / Antifurto
XIAO nRF52840 + Expansion · prossimità beacon
Radar di prossimità per beacon BLE + demo arm/disarm antifurto. In RADAR il buzzer becca più fitto quanto sei vicino (come un metal-detector); in ARM disarma sotto soglia e riarma se il beacon si perde per 10 s. Il tasto (long-press) associa un singolo MAC, così segue solo quel beacon.
Seeed XIAO nRF52840 + Expansion Arduino · Seeed nRF52 core
Componenti usati
nRF52840
MCU · BLE Cortex-M4F
Nordic Semiconductor
SSD1306
OLED 128×64 (Expansion)
—
Buzzer
Feedback acustico (Expansion)
—
Pulsante
Modo / associazione (Expansion)
—
ble_beacon_radar.ino
216 righe GitHub
/*
* xiao-ble-beacon-radar — XIAO nRF52840 + Expansion Board (OLED + buzzer + tasto)
*
* Radar di prossimità per beacon BLE + demo arm/disarm antifurto Visla.
*
* PULSANTE (D1):
* click corto → cambia modo: RADAR ⇄ ARM
* pressione 1.5s → ASSOCIA il beacon più vicino (o annulla l'associazione)
*
* RADAR: beep tanto più fitti/acuti quanto più sei vicino (metal detector).
* ARM: sotto soglia per N letture → DISARMA. Beacon perso 10s → RIARMA.
*
* ⚠️ Una volta associato segue SOLO quel MAC: un antifurto che disarma
* davanti al beacon di chiunque non serve a niente.
*/
#include <bluefruit.h>
#include <U8x8lib.h>
U8X8_SSD1306_128X64_NONAME_SW_I2C oled(D5, D4, U8X8_PIN_NONE);
#define BUZZER A3
#define BUTTON D1
#define RSSI_NEAR -45 // "addosso"
#define RSSI_FAR -85 // "quasi perso"
#define ARM_ON -60 // sotto → disarma
#define ARM_OFF -75 // sopra → riarma (isteresi)
#define ARM_HITS 3 // letture consecutive prima di disarmare
#define LOST_MS 10000
#define EMA_A 0.30f
enum Mode { RADAR, ARM };
static Mode mode = RADAR;
static uint8_t target[6];
static bool paired = false, haveTgt = false, armed = true;
static float ema = -100;
static uint32_t lastSeen = 0, lastBeep = 0, lastUi = 0, btnDown = 0;
static int hits = 0;
static uint8_t lastCount = 0;
static bool countInit = false;
static volatile uint8_t ringPending = 0;
static bool longDone = false;
void beep(int f, int ms) {
long half = 500000L / f, cyc = (long)ms * 1000L / (half * 2);
for (long i = 0; i < cyc; i++) {
digitalWrite(BUZZER, HIGH); delayMicroseconds(half);
digitalWrite(BUZZER, LOW); delayMicroseconds(half);
}
}
void chirpUp() { beep(1400,70); delay(40); beep(2000,70); delay(40); beep(2600,110); }
void chirpDown() { beep(2600,70); delay(40); beep(1400,140); }
/* DIN-DON da campanello */
void dingDong() { beep(1319,180); delay(30); beep(1047,320); }
static uint8_t vislaFlags = 0, vislaCount = 0, vislaBatt = 0;
static bool isVisla = false;
/* iBeacon (02 15) oppure beacon VISLA (FF FF 'V' ver ...) */
static bool isBeacon(ble_gap_evt_adv_report_t* r, bool* visla) {
uint8_t b[31];
uint8_t n = Bluefruit.Scanner.parseReportByType(r, BLE_GAP_AD_TYPE_MANUFACTURER_SPECIFIC_DATA, b, sizeof(b));
*visla = false;
if (n >= 9 && b[0] == 'V') { // Bluefruit toglie i 2 byte di company ID
*visla = true;
vislaFlags = b[6]; vislaCount = b[7]; vislaBatt = b[8];
return true;
}
for (int i = 0; i + 1 < n; i++) if (b[i] == 0x02 && b[i+1] == 0x15) return true;
return false;
}
void scan_cb(ble_gap_evt_adv_report_t* rep) {
bool v = false;
if (rep->type.scan_response || !isBeacon(rep, &v)) { Bluefruit.Scanner.resume(); return; }
bool isTgt = haveTgt && memcmp(target, rep->peer_addr.addr, 6) == 0;
// ASSOCIATO: solo il suo MAC. LIBERO: insegue il più forte (+6dB di isteresi).
if (!paired && (!haveTgt || (!isTgt && rep->rssi > ema + 6))) {
memcpy(target, rep->peer_addr.addr, 6);
haveTgt = true; ema = rep->rssi; isTgt = true;
Serial.printf("\n>>> target: %02X:%02X:%02X:%02X:%02X:%02X\n",
target[5],target[4],target[3],target[2],target[1],target[0]);
}
if (isTgt) {
ema = EMA_A * rep->rssi + (1 - EMA_A) * ema; lastSeen = millis(); isVisla = v;
if (v) {
if (!countInit) { lastCount = vislaCount; countInit = true; }
else if (vislaCount != lastCount) {
ringPending += (uint8_t)(vislaCount - lastCount); // recupera i click persi
lastCount = vislaCount;
}
}
}
Bluefruit.Scanner.resume();
}
void macStr(char* s) {
sprintf(s, "%02X:%02X:%02X:%02X", target[3], target[2], target[1], target[0]);
}
void draw(float r, bool lost) {
oled.clearLine(0);
oled.drawString(0, 0, mode == RADAR ? "RADAR " : "ARMING");
oled.drawString(9, 0, paired ? "[LOCK]" : "[auto]");
char b[20];
if (lost) {
oled.clearLine(1); oled.clearLine(2);
oled.draw2x2String(0, 1, " -- ");
oled.clearLine(3); oled.clearLine(4);
oled.draw2x2String(0, 3, "assente ");
} else {
sprintf(b, "%4d dBm", (int)r);
oled.draw2x2String(0, 1, b);
float d = pow(10.0, (-55.0 - r) / 20.0);
sprintf(b, "~%4.1f m ", d);
oled.draw2x2String(0, 3, b);
}
int bars = lost ? 0 : constrain(map((int)r, RSSI_FAR, RSSI_NEAR, 0, 14), 0, 14);
char bar[17]; bar[0] = '[';
for (int i = 0; i < 14; i++) bar[i+1] = i < bars ? '#' : ' ';
bar[15] = ']'; bar[16] = 0;
oled.drawString(0, 5, bar);
oled.clearLine(6);
if (isVisla && !lost) {
sprintf(b, "VISLA %s b%d%%", (vislaFlags & 1) ? "PRESS" : " . ", vislaBatt);
oled.drawString(0, 6, b);
} else if (haveTgt) { macStr(b); oled.drawString(0, 6, b); }
else oled.drawString(0, 6, "--:--:--:--");
oled.clearLine(7);
if (mode == ARM) oled.drawString(0, 7, armed ? "** ARMATO **" : ">> DISARMATO");
}
void setup() {
pinMode(BUZZER, OUTPUT);
pinMode(BUTTON, INPUT_PULLUP);
Serial.begin(115200);
oled.begin(); oled.setFont(u8x8_font_chroma48medium8_r);
oled.clear(); oled.drawString(0, 0, "VISLA RADAR");
oled.drawString(0, 2, "click: modo");
oled.drawString(0, 3, "hold : associa");
Bluefruit.begin(0, 1);
Bluefruit.setName("VislaRadar");
Bluefruit.Scanner.setRxCallback(scan_cb);
Bluefruit.Scanner.restartOnDisconnect(false);
Bluefruit.Scanner.setInterval(160, 160);
Bluefruit.Scanner.useActiveScan(false);
Bluefruit.Scanner.start(0);
beep(1200, 60); delay(60); beep(1800, 60);
delay(1200); oled.clear();
}
void loop() {
// --- pulsante: click = modo, hold = associa ---
bool down = digitalRead(BUTTON) == LOW;
if (down && !btnDown) { btnDown = millis(); longDone = false; }
if (down && !longDone && millis() - btnDown > 1500) {
longDone = true;
if (paired) { paired = false; haveTgt = false; ema = -100;
Serial.println("### associazione ANNULLATA"); beep(800, 150); }
else if (haveTgt) { paired = true; armed = true; hits = 0;
char m[20]; macStr(m);
Serial.printf("### ASSOCIATO a %s — seguo solo lui\n", m);
beep(2000,80); delay(50); beep(2600,80); delay(50); beep(3000,150); }
else beep(500, 200); // niente da associare
}
if (!down && btnDown) {
if (!longDone && millis() - btnDown > 40) { // click corto
mode = (mode == RADAR) ? ARM : RADAR; hits = 0; armed = true;
Serial.printf("\n### MODO: %s\n", mode == RADAR ? "RADAR" : "ARM");
beep(1500, 80);
}
btnDown = 0;
}
uint32_t now = millis();
bool lost = !haveTgt || (now - lastSeen > 2500);
float r = lost ? -100 : ema;
if (now - lastUi > 250) { lastUi = now; draw(r, lost); }
// 🔔 CAMPANELLO: il beacon Visla ha annunciato una pressione
while (ringPending) {
ringPending--;
Serial.printf("\n🔔 DING-DONG! (count %u, batt %u%%)\n", vislaCount, vislaBatt);
oled.clearLine(7); oled.drawString(0, 7, ">>> DING-DONG! ");
dingDong();
lastUi = 0;
}
if (mode == RADAR) {
if (lost) return;
int gap = map(constrain((int)r, RSSI_FAR, RSSI_NEAR), RSSI_FAR, RSSI_NEAR, 900, 60);
if (now - lastBeep > (uint32_t)gap) {
lastBeep = now;
beep(map(constrain((int)r, RSSI_FAR, RSSI_NEAR), RSSI_FAR, RSSI_NEAR, 900, 2600), 18);
}
} else {
if (!lost && r > ARM_ON) { if (hits < ARM_HITS) hits++; }
else if (lost || r < ARM_OFF) hits = 0;
if (armed && hits >= ARM_HITS) {
armed = false; Serial.println("\n>> DISARMATO (proprietario vicino)"); chirpUp();
}
if (!armed && lost && now - lastSeen > LOST_MS) {
armed = true; hits = 0; Serial.println("\n** RIARMATO (proprietario lontano)"); chirpDown();
}
}
}