variograf_ekg.tc¶
variograf_ekg.tc — EKG eines Variograf-Rekorders ueber BLE, 256 Hz.
// variograf_ekg.tc — EKG eines Variograf-Rekorders ueber BLE, 256 Hz.
//
// ⭐ LAEUFT (2026-08-06) -- auf C09 (Beschleunigung) UND auf C02 (EKG) nachgewiesen:
// 250 Saetze/s bei nominal 256, null Synchronfehler. Ohne Elektroden steht der
// EKG-Eingang an beiden Anschlaegen (+-23608 uV) -- das ist der rechnerische
// Vollausschlag 32768*5/6940 auf ein Mikrovolt genau, bestaetigt also die Skalierung.
// Zuvor kam kein einziger Messwert, und die Ursache lag NICHT
// im Protokoll: der 29 Byte lange Startbefehl passte nicht in EINEN BLE-Schreibvorgang
// (ATT-MTU 23 -> hoechstens 20 Byte Nutzlast). Er verliess den ESP nie. Kurze Befehle
// gingen deshalb immer durch -- VS wurde beantwortet, ein nacktes CR liess das Geraet
// PIEPEN --, nur der Start nicht. Behoben in der Firmware: bleSppWrite zerlegt jetzt
// an der MTU.
// ⚠️ Sichtbar wurde es erst, als sende() den Rueckgabewert PRUEFTE. Vorher sah ein
// fehlgeschlagener Schreibvorgang aus wie ein Geraet, das den Befehl empfaengt und
// nicht reagiert -- und ich habe daraufhin die Befehlsfolge verdaechtigt statt die
// Uebertragung. Ein ungeprueftes Schreiben verwandelt einen Transportfehler in ein
// scheinbares Protokollraetsel.
//
// Baut auf ble_spp_probe.tc auf (BRSP-Verbindung) und setzt den MESSBETRIEB obendrauf.
// Alle Zahlen hier stammen aus oeko4.def, mit dem geprueften Feldschema aus
// uVariotest26/src/DefParser.cpp aufgeloest — nicht geraten:
//
// R: $012c = 300, F: erstes Byte $48 -> Modus 0 -> 300 * 0,0130208 ms = 3,90625 ms
// ⭐ GLOBALE ABTASTRATE 256 Hz. C02 (EKG) hat Teiler 1, laeuft also voll mit.
// C02: phys[mV] = (roh - 32768) * 5 / 6940
// SAV: 0252-56-24 -> Version 252, Subtyp 24
//
// ⚠️ C16 "HR" ist KEIN Geraetekanal: alias() = (size>>2)&0x1f ergibt bei $89 die 2,
// der Kanal wird also aus C02 BERECHNET (typ $80 = QRS-Detektor). Das Geraet sendet
// keine Herzfrequenz — die muessen wir selbst aus dem EKG gewinnen.
//
// ⚠️⚠️ DAS GERAET SCHALTET SICH NACH EINER MINUTE OHNE VERBINDUNG AB (gemu, 2026-08-06).
// Das sieht von aussen exakt wie ein kaputter Verbindungsaufbau aus: "verbinde ..." und
// sechsmal fehlgeschlagen, obwohl kurz zuvor alles lief. Wer zwischen zwei Versuchen
// laenger nachdenkt, flasht oder Logs liest, faellt darauf herein. Vor dem Suchen auf
// der ESP-Seite ALSO IMMER ZUERST: laeuft das Geraet ueberhaupt noch?
//
// ⭐ LIVE-KURVE auf der Hauptseite des Geraets (Canvas), Daten inkrementell ueber
// /ekg?seit=<folge>. Muster wie in max30102.tc: holen und zeichnen sind entkoppelt,
// gezeichnet wird auf requestAnimationFrame mit dem NACHKOMMAANTEIL des Lesezeigers,
// der naechste Abruf haengt per setTimeout AN DER ANTWORT (nie setInterval).
// ⚠️ Ohne Elektroden zeigt die Kurve Rauschen zwischen beiden Anschlaegen -- das ist
// der offene Eingang, kein Fehler.
//
// Bedienung:
// VGCON verbinden + Messbetrieb starten
// VGOFF Messbetrieb beenden + trennen
// VGST Zahlen: Saetze/s, Synchronfehler, Spanne in mV
// ── BRSP (BlueRadios), am Geraet ausgelesen + gegen gemus iPad-App geprueft ──
#define ZIEL_TYP 0
#define ZIEL_SVC "da2b84f1-6279-48de-bdc0-afbea0226079"
#define BRSP_TX "18cda784-4bd3-4370-85bb-bfed91ec86af" // Geraet meldet hier
#define BRSP_RX "bf03260c-7205-4c25-af43-93b1c299d159" // wir schreiben hier
#define BRSP_MODE "a87988b9-694c-479c-900e-95dfa6c00a24"
// ── Welcher Kanal? ──
// ⭐ ZUM AUSPROBIEREN DIE BESCHLEUNIGUNG NEHMEN, NICHT DAS EKG. Ein EKG ohne
// angelegte Elektroden ist Rauschen — daran sieht man NICHT, ob die Uebertragung
// stimmt. Ein Beschleunigungswert dagegen folgt dem KIPPEN des Geraets: flach ~0,
// auf die Kante gestellt ~±1000 mg. Ein Signal, das man selbst steuert, ist der
// einzige ehrliche Beweis, dass Rahmen, Reihenfolge und Skalierung stimmen.
// (Idee von gemu, 2026-08-05.)
//
// Werte aus oeko4.def, Spalten $MULF $OFFS $DIVF:
// C09 X mg offs 2048 1/1 <- Testkanal, kippen
// C02 EKG mV offs 32768 5/6940 <- das eigentliche Ziel
// ⭐ JETZT AUF DEM EKG-KANAL. Zum Nachpruefen der Uebertragung wieder auf C09
// umstellen (Werte in der Tabelle darueber) -- die Kanalmaske rechnet sich selbst.
#define KANAL_NR 2
#define KANAL_NAME "EKG"
#define KANAL_EINH "uV"
#define KANAL_OFFS 32768
#define KANAL_MULF 5
#define KANAL_DIVF 6940
// ⚠️ 1000 ist hier PFLICHT, nicht Kosmetik: der Faktor ist 5/6940 = 0,00072, und
// ganzzahlig waere (roh-32768)*5/6940 fuer fast jeden Wert schlicht 0. Mit SKAL 1000
// rechnen wir in Tausendstel mV, also MIKROVOLT -- daher auch die Einheit oben.
// Ueberlauf ist geprueft: (+-32768)*5*1000 = +-164 Mio, passt in int32.
#define KANAL_SKAL 1000
// ⚠️ Satzlaenge = 2*Kanaele+1, und sie ist FEST — unabhaengig von den Scan-Teilern.
// Ein Kanal mit Teiler 8 kommt trotzdem in JEDEM Takt ueber den Draht, er wird nur
// nicht jedes Mal abgelegt. Wer hier einen Kanal dazunimmt, muss KANAELE erhoehen.
#define KANAELE 1
#define SATZ_LEN 3
// Version 252 >= 212 -> Ratenwort = gscan|0x8000; Subtyp 24 >= 7 -> br = $ff
#define GSCAN 300
#define BR_BYTE 0xff
// ── Live-Kurve ──
// ⚠️ MUSS IN SLOT 0 LAUFEN: webOn() traegt die Adresse zwar slot-blind ein, der
// Verteiler ruft aber immer slots[0]. Aus Slot 1 waere /ekg erreichbar und wuerde
// trotzdem das falsche Programm aufrufen -- ohne Fehlermeldung.
#define KURVE_N 500 // Punkte im Ring = 2 s bei 250 Hz
#define AR_FENSTER 750 // Werte, nach denen der Anzeigebereich neu bestimmt wird
#define ANZ_RATE 250 // gemessene Satzrate, fuer den Lesezeiger im Browser
#define LEBEN_MS 5000 // XON-Takt (siehe unten -- NICHT optional)
int ziel_mac[6];
char zeile[160];
char rx[256];
char roh[600]; // Sammelpuffer fuer angebrochene Saetze
int roh_n;
int verbunden;
int laeuft; // 1 = Messbetrieb an
int auftrag; // 1=start 2=stop 3=Zahlen
int kurve[KURVE_N]; // auf 0..99 skaliert
int kpos; int kfolge;
int ar_mn; int ar_mx; int ar_mn_n; int ar_mx_n; int ar_fz; int ar_init;
char blk[128];
char argb[16];
char startcmd[40];
int efa[5];
int saetze; // seit dem Start empfangene Saetze
int syncfehler;
int mn_mv; int mx_mv;
int letzter_wert;
int t_start; int t_leben; int t_melde;
char txbuf[40];
int rohmodus; // 1 = eingehende Bytes als Hex zeigen, nicht zerlegen
void zahlen_melden() {
int dt = millis() - t_start;
int rate = 0;
if (dt > 0) { rate = (saetze * 1000) / dt; }
sprintf(zeile, "VG: %s = %d %s | %d Saetze, %d/s, Sync-Fehler %d, Spanne %d..%d",
KANAL_NAME, letzter_wert, KANAL_EINH, saetze, rate, syncfehler, mn_mv, mx_mv);
addLog(zeile);
}
// Startbefehl zusammensetzen, statt eine Hexkette von Hand hinzuschreiben —
// so stimmt die Maske automatisch, wenn oben ein anderer Kanal eingetragen wird.
// Maske: efa[c>>4] |= 1 << (15-(c&15)) mit c = Kanalnummer MINUS EINS.
void startcmd_bauen() {
int i = 0;
while (i < 5) { efa[i] = 0; i = i + 1; }
int c = KANAL_NR - 1;
efa[c >> 4] = efa[c >> 4] | (1 << (15 - (c & 15)));
sprintf(startcmd, "!W%02X%04x%04x%04x%04x%04x%04x", BR_BYTE,
efa[0], efa[1], efa[2], efa[3], efa[4], GSCAN | 0x8000);
}
// EIN Satz: [hi][lo][0x0D]
void satz(int hi, int lo) {
int r = ((hi & 0xFF) << 8) | (lo & 0xFF);
// ⚠️ Bei kleinen Faktoren in Ganzzahlarithmetik zuerst MULTIPLIZIEREN. Beim EKG
// ist der Faktor 5/6940 — (roh-32768)*5/6940 waere fuer fast jeden Wert 0.
// KANAL_SKAL zieht die Aufloesung vorher hoch.
int mv = ((r - KANAL_OFFS) * KANAL_MULF * KANAL_SKAL) / KANAL_DIVF;
letzter_wert = mv;
// ⚠️ Selbst-Einmessung: was ein EKG liefert, haengt an Elektroden und Lage. Der
// Bereich wird ueber ein Fenster neu bestimmt. Untergrenze fuer die Spanne, sonst
// wird bei stillem Eingang das letzte Bit Rauschen auf volle Hoehe verstaerkt.
if (!ar_init) { ar_mn = mv; ar_mx = mv; ar_mn_n = mv; ar_mx_n = mv; ar_init = 1; }
if (mv < ar_mn_n) { ar_mn_n = mv; }
if (mv > ar_mx_n) { ar_mx_n = mv; }
ar_fz = ar_fz + 1;
if (ar_fz >= AR_FENSTER) {
ar_mn = ar_mn_n; ar_mx = ar_mx_n;
ar_mn_n = mv; ar_mx_n = mv; ar_fz = 0;
}
int sp = ar_mx - ar_mn;
if (sp < 200) { sp = 200; }
int y = ((mv - ar_mn) * 99) / sp;
if (y < 0) { y = 0; }
if (y > 99) { y = 99; }
kurve[kpos] = y;
kpos = kpos + 1;
if (kpos >= KURVE_N) { kpos = 0; }
kfolge = kfolge + 1;
if (saetze == 0) { mn_mv = mv; mx_mv = mv; }
if (mv < mn_mv) { mn_mv = mv; }
if (mv > mx_mv) { mx_mv = mv; }
saetze = saetze + 1;
}
// Sammelpuffer zerlegen. Steht an der erwarteten Stelle kein 0x0D, ist der Strom
// verrutscht -- dann bis zum naechsten 0x0D verwerfen und dort neu aufsetzen.
void zerlegen() {
int p = 0;
while (roh_n - p >= SATZ_LEN) {
if ((roh[p + SATZ_LEN - 1] & 0xFF) == 0x0D) {
satz(roh[p], roh[p + 1]);
p = p + SATZ_LEN;
} else {
syncfehler = syncfehler + 1;
int q = p;
while (q < roh_n && (roh[q] & 0xFF) != 0x0D) { q = q + 1; }
if (q >= roh_n) { p = roh_n; break; }
p = q + 1;
}
}
// Rest nach vorne schieben
int k = 0;
while (p + k < roh_n) { roh[k] = roh[p + k]; k = k + 1; }
roh_n = k;
}
// ⚠️ IMMER den Rueckgabewert pruefen und melden. Genau dieses ungeprueft gelassene
// bleSppWrite hat die Fehlersuche um einen Tag verlaengert (siehe Kopf).
int sende(char s[]) {
char b[40];
strcpy(b, s);
strcat(b, "\r");
int ok = bleSppWrite(BRSP_RX, b, strlen(b));
if (!ok) {
char w[64];
sprintf(w, "VG: SCHREIBEN FEHLGESCHLAGEN (%d Byte)", strlen(b));
addLog(w);
}
return ok;
}
// Befehl senden und die Antwort ABWARTEN UND VERWERFEN.
// ⚠️ Beides ist Pflicht. Das Warten, weil das Geraet die Schritte der Reihe nach
// erwartet; das Verwerfen, weil eine liegengebliebene Antwort nach dem Start als
// Messrahmen gedeutet wuerde und lauter Synchronfehler produzierte.
void frage(char c[], int wartems) {
sende(c);
int bis = millis() + wartems;
while (millis() < bis) {
int n = bleSppAvailable();
if (n > 0) {
if (n > 255) { n = 255; }
bleSppRead(rx, n);
}
delay(20);
}
}
void Command(char cmd[]) {
if (strcmp(cmd, "CON") == 0) { auftrag = 1; responseCmnd("starte ..."); }
else if (strcmp(cmd, "OFF") == 0) { auftrag = 2; responseCmnd("beende"); }
else if (strcmp(cmd, "ST") == 0) { auftrag = 3; responseCmnd("siehe Log"); }
else if (strStartsWith(cmd, "TX ")) {
// Zum Eingrenzen: beliebigen ASCII-Befehl schicken und die Antwort als Hex sehen.
strSub(txbuf, cmd, 3, 0);
auftrag = 4;
responseCmnd("sende ...");
}
else { responseCmnd("VGCON | VGOFF | VGST"); }
}
// Nur der Zuwachs seit der mitgeschickten Folgenummer -- siehe TinyC_Reference/CLAUDE.md
// "A live curve in the web UI that does not stutter".
void WebOn() {
if (webHandler() != 1) { return; }
int seit = 0;
if (webArg("seit", argb) > 0) { seit = strToInt(argb); }
int neu = kfolge - seit;
if (neu < 0) { neu = KURVE_N; }
if (neu > KURVE_N) { neu = KURVE_N; }
sprintf(zeile, "%d,%d,%d,%d,%d|", kfolge, neu, letzter_wert, laeuft, syncfehler);
webSend(zeile);
int p = kpos - neu;
if (p < 0) { p = p + KURVE_N; }
int i = 0;
while (i < neu) {
int o = 0;
blk[0] = 0;
while (o < 48 && i < neu) {
sprintf(zeile, "%02d", kurve[p]);
strcat(blk, zeile);
p = p + 1;
if (p >= KURVE_N) { p = 0; }
o = o + 1; i = i + 1;
}
webSend(blk);
}
}
void WebPage() {
webSend("<div style='margin:8px 0'><canvas id='eg' width='500' height='150' ");
webSend("style='width:100%;height:160px;background:#111;border-radius:4px'></canvas>");
webSend("<div id='ev' style='text-align:center;font-size:0.9em;padding:4px'>--</div></div>");
webSend("<script>(function(){var c=document.getElementById('eg');if(!c)return;");
webSend("var x=c.getContext('2d'),b=[],sq=0,rp=0,tl=0,W=500,H=150,N=500,R=250;");
webSend("function pull(){fetch('/ekg?seit='+sq).then(r=>r.text()).then(t=>{");
webSend("var p=t.split('|');if(p.length<2)return;var h=p[0].split(',');sq=+h[0];");
webSend("var s=p[1],k=s.length>>1;for(var i=0;i<k;i++)b.push(+s.substr(i*2,2));");
webSend("if(b.length>1500){var d=b.length-1500;b.splice(0,d);rp-=d;}if(rp<0)rp=0;");
webSend("document.getElementById('ev').textContent=(h[3]=='1'?");
webSend("('EKG '+h[2]+' uV Sync-Fehler '+h[4]):'Messbetrieb aus');");
// ⚠️ setTimeout NACH der Antwort, nie setInterval -- sonst sind zwei Abrufe mit
// derselben Folgenummer offen und die Kurve springt zurueck.
webSend("}).catch(e=>{}).finally(()=>setTimeout(pull,200));}");
webSend("function draw(now){if(!tl)tl=now;var dt=now-tl;tl=now;if(dt>200)dt=200;");
webSend("var lag=b.length-rp,e=(lag-N)/N;if(e>1)e=1;if(e<-1)e=-1;");
webSend("rp+=dt*R*(1+0.02*e)/1000;if(rp>b.length)rp=b.length;");
// ⚠️ MIT dem Nachkommaanteil zeichnen, sonst steht die Kurve abwechselnd und springt.
webSend("var fi=Math.floor(rp),fr=rp-fi;");
webSend("x.fillStyle='#111';x.fillRect(0,0,W,H);");
webSend("var st=fi-N;if(st<0){st=0;fr=0;}var n=Math.min(N,b.length-st);");
webSend("if(n>1){x.strokeStyle='#3ad13a';x.lineWidth=1.5;x.beginPath();");
webSend("for(var i=0;i<n;i++){var v=b[st+i],px=i*W/N-fr*W/N,py=H-1-v*(H-2)/99;");
webSend("if(i==0)x.moveTo(px,py);else x.lineTo(px,py);}x.stroke();}");
webSend("requestAnimationFrame(draw);}");
webSend("pull();requestAnimationFrame(draw);})();</script>");
}
int main() {
addCommand("VG");
webOn(1, "/ekg");
ziel_mac[0] = 0xEC; ziel_mac[1] = 0xFE; ziel_mac[2] = 0x7E;
ziel_mac[3] = 0x10; ziel_mac[4] = 0xE1; ziel_mac[5] = 0xEF;
bleSppTarget(ziel_mac, ZIEL_TYP, ZIEL_SVC);
return 0;
}
void TaskLoop() {
if (auftrag == 1) {
auftrag = 0;
// ⚠️ ERST DEN FUNKSTAPEL HOCHFAHREN. NimBLEClient::connect() steigt sofort
// und OHNE Fehlercode aus, wenn der Host noch nicht synchronisiert ist
// (NimBLEClient.cpp: `if (!NimBLEDevice::m_synced) return false`). Ein kurzer
// Scan bringt ihn hoch. Genau daran lag es, dass dieselbe Verbindung vorher
// klappte und nach einem Neustart nicht mehr: vorher lief der Scanner.
bleScan(600);
delay(900);
bleScanStop();
delay(200);
// ⚠️ MEHRFACH VERSUCHEN. Der Verbindungsaufbau ist nicht deterministisch:
// NimBLEClient::connect() steigt ohne Fehlercode aus, wenn der Host nicht
// synchronisiert ist ODER der laufende Scan sich nicht anhalten laesst —
// und BLE_ESP32 startet seinen Hintergrund-Scan immer wieder neu. Das Geraet
// selbst ist nachweislich da (sendet bei -62 dBm). Also nicht beim ersten
// Nein aufgeben.
int ok = 0;
int v = 0;
while (v < 6 && !ok) {
v = v + 1;
addLog("VG: verbinde ...");
ok = bleSppConnect();
if (!ok) { delay(1200); }
}
if (!ok) { addLog("VG: Verbindung nach 6 Versuchen fehlgeschlagen"); return; }
sprintf(zeile, "VG: verbunden im %d. Versuch", v);
addLog(zeile);
if (!bleSppSub(BRSP_TX)) { addLog("VG: BRSP_TX nicht abonniert"); return; }
char m[2];
m[0] = 1; // BrspModeData
bleSppWrite(BRSP_MODE, m, 1);
verbunden = 1;
delay(200);
// ⚠️⚠️ DIESE REIHENFOLGE IST NICHT VERHANDELBAR. Beim ersten Versuch hatte ich
// die fuenf Abfragen uebersprungen und gleich %9220/%9401/!W geschickt — das
// Geraet blieb daraufhin im Befehlsmodus und sendete NICHTS, antwortete aber
// weiter brav auf VS. Kein Fehler, keine Meldung, nur Stille.
// Genau diese Folge steht in uVariotest26/src/AcquisitionWorker.cpp.
frage("VS", 400); // 1. Kennung (der Port bricht ohne Antwort ab)
frage("%00", 400); // 2. Rekorderstatus
frage("%13", 400); // 3. Quarz/Subtyp
frage("%08", 400); // 4. Batteriespannung
frage("%09", 400); // 5. Geraetename
// ⚠️ %9220 schaltet im GERAET einen Zeitueberlauf scharf. Ab jetzt MUSS
// regelmaessig ein Lebenszeichen kommen, sonst bricht das Geraet die
// Uebertragung von selbst ab -- ohne Fehler, sie hoert einfach auf.
// ⚠️⚠️ HIER STAND EINMAL ZUSAETZLICH frage("%9401") — "Befehlsmodus" — aus
// uVariotest26/AcquisitionWorker.cpp uebernommen. Damit blieb das Geraet
// stumm: !W wurde ignoriert, VS aber weiter brav beantwortet. Die
// ORIGINAL-App (VpOnline/MainViewController.m StartOnline) schickt vor dem
// !W ausschliesslich %9220 — und wer ein Geraet in den Befehlsmodus
// schaltet, darf sich nicht wundern, dass es nicht sendet.
frage("%9220", 200); // 6. Zeitueberlauf scharf machen -- und sonst nichts
roh_n = 0; saetze = 0; syncfehler = 0;
t_start = millis(); t_leben = t_start; t_melde = t_start;
startcmd_bauen();
sprintf(zeile, "VG: Startbefehl %s (%d Byte)", startcmd, strlen(startcmd) + 1);
addLog(zeile);
sende(startcmd);
laeuft = 1;
sprintf(zeile, "VG: laeuft — C%02d %s, Satzlaenge %d Byte", KANAL_NR, KANAL_NAME, SATZ_LEN);
addLog(zeile);
return;
}
if (auftrag == 2) {
auftrag = 0;
if (laeuft) { sende(""); delay(200); } // "\r" allein beendet
laeuft = 0;
bleSppClose();
verbunden = 0;
zahlen_melden();
addLog("VG: beendet");
return;
}
if (auftrag == 3) { auftrag = 0; zahlen_melden(); return; }
if (auftrag == 4) {
auftrag = 0;
rohmodus = 1; laeuft = 0; roh_n = 0;
sende(txbuf);
sprintf(zeile, "VG: gesendet \"%s\"", txbuf);
addLog(zeile);
return;
}
if (!verbunden) { delay(200); return; }
if (!bleSppState()) {
sprintf(zeile, "VG: Verbindung verloren nach %d ms, %d Saetze", millis() - t_start, saetze);
addLog(zeile);
verbunden = 0; laeuft = 0;
return;
}
int jetzt = millis();
// ⚠️ Lebenszeichen VOR dem Lesen senden, nicht danach: kommt gerade nichts
// herein, verpasst man sonst den Takt und das Geraet steigt aus.
if (laeuft && jetzt - t_leben >= LEBEN_MS) {
t_leben = jetzt;
char x[2];
x[0] = 0x11; // XON
int xo = bleSppWrite(BRSP_RX, x, 1);
sprintf(zeile, "VG: XON nach %d ms %s", jetzt - t_start, xo ? "ok" : "FEHLGESCHLAGEN");
addLog(zeile);
}
int n = bleSppAvailable();
if (n > 0) {
if (n > 255) { n = 255; }
int got = bleSppRead(rx, n);
int i = 0;
while (i < got) {
if (roh_n < 590) { roh[roh_n] = rx[i]; roh_n = roh_n + 1; }
i = i + 1;
}
if (rohmodus) {
int k = 0;
while (k < got) {
char hz[80]; hz[0] = 0;
int j = 0;
while (j < 24 && k < got) {
char b2[4]; sprintf(b2, "%02X ", rx[k] & 0xFF);
strcat(hz, b2); k = k + 1; j = j + 1;
}
addLog(hz);
}
} else {
zerlegen();
}
}
if (laeuft && jetzt - t_melde >= 1000) { t_melde = jetzt; zahlen_melden(); }
delay(10);
}
void CleanUp() {
if (laeuft) { sende(""); }
bleSppClose();
}