Zum Inhalt

ble_spp_probe.tc

ble_spp_probe.tc — Verbindungsprobe fuer eine DAUERHAFTE BLE-Verbindung ("BLE SPP").

Source on GitHub

// ble_spp_probe.tc — Verbindungsprobe fuer eine DAUERHAFTE BLE-Verbindung ("BLE SPP").
//
// Anders als der Einzelvorgangs-Client (bleTarget/bleReadStart/bleWriteStart: verbinden,
// EINEN Lese-/Schreib-/Notify-Vorgang, trennen) haelt bleSppConnect() die Verbindung
// offen, bis das Skript sie mit bleSppClose() beendet — der richtige Zugang fuer ein
// Geraet, das laufend meldet (ein BlueRadios-Modul im BLE-Modus, ein Nordic-UART-artiger
// Sensor), statt einmal aufzuwachen und wieder zu schlafen.
//
// ⭐ VERIFIZIERT an echter Hardware 2026-08-05 auf .39 (ESP32-S3) gegen gemus EKG-Geraet mit BlueRadios-Dualmodul (MAC EC:FE:7E:10:E1:EF, BRSP-Profil): verbinden, BRSP_TX abonnieren, Datenmodus setzen, "VS\r" schreiben -- Antwort kam als 48 Byte in vier Haeppchen zurueck ("0252-56-2097151:AA000000:def0.def:..."), und bleSppState() meldete DANACH weiter 1. Genau das kann der Einzelvorgangs-Client nicht: er haette nach der Antwort getrennt.
// Bei einem UNBEKANNTEN Geraet trotzdem zuerst ble_gatt_explore.tc — erst schauen,
// dann verbinden, dann dem Protokoll vertrauen (so lief es auch bei Classic SPP).
//
// VORAUSSETZUNG: MAC-Adresse, Adresstyp, Dienst-UUID und Merkmals-UUIDs des Zielgeraets
// muessen bekannt sein — bei einem proprietaeren Geraet stehen die NICHT im Datenblatt.
// Zuerst ble_gatt_explore.tc laufen lassen und die Ausgabe hier eintragen.
//
// Bedienung in der Konsole:
//   SPPBCON      Verbindung aufbauen + auf NOTIFY_CHR abonnieren
//   SPPBOFF      trennen
//   SPPBST       Zustand abfragen
//   SPPBTX xy    Text xy auf WRITE_CHR schreiben (zum Testen einer Quittung)

// ⭐ ECHTE WERTE, am Geraet ausgelesen (ble_gatt_explore.tc, 2026-08-05) und gegen
// gemus eigene iPad-App gegengeprueft (VpOnline/Brsp.h + BRSP/.../Brsp.m): das ist
// das BRSP-Profil (BlueRadios Serial Port), Zeile fuer Zeile deckungsgleich.
//
//   BRSP_SERVICE  da2b84f1-...   der Dienst
//   BRSP_INFO     99564a02-...   R    Modulkennung
//   BRSP_MODE     a87988b9-...   RW   0=Idle, 1=DATEN, 2=Fernbefehl
//   BRSP_RX       bf03260c-...   W    hier schreibt der RECHNER hinein
//   BRSP_TX       18cda784-...   N    hier meldet das GERAET  <- abonnieren
//   BRSP_CTS      0a1934f5-...   RWw  Flusssteuerung, Rechner -> Modul
//   BRSP_RTS      fdd6b4d3-...   RN   Flusssteuerung, Modul -> Rechner
//
// ⚠️ RX/TX sind aus Sicht des MODULS benannt. Wir schreiben auf RX und lesen von TX.
#define ZIEL_TYP   0                                         // public (Scan meldete typ=0)
#define ZIEL_SVC   "da2b84f1-6279-48de-bdc0-afbea0226079"
#define NOTIFY_CHR "18cda784-4bd3-4370-85bb-bfed91ec86af"    // BRSP_TX
#define WRITE_CHR  "bf03260c-7205-4c25-af43-93b1c299d159"    // BRSP_RX
#define MODE_CHR   "a87988b9-694c-479c-900e-95dfa6c00a24"    // BRSP_MODE
#define MODE_DATEN 1                                          // BrspModeData
#define BRSP_CTS   "0a1934f5-24b8-4f13-9842-37bb167c6aff"    // Flusssteuerung Rechner->Modul

int  ziel_mac[6];     // in main() gesetzt -- MAC aus ble_gatt_explore.tc eintragen
int  verbunden;       // 0 = nicht (mehr) verbunden, 1 = verbunden
int  auftrag;         // 1=verbinden, 2=trennen, 3=Zustand melden

char rx[256];
char zeile[160];
char txbuf[64];
int  tx_len;
int  tx_auftrag;      // 1 = txbuf senden

persist int laeufe;
persist int treffer;

void zustand_melden() {
    int st = bleSppState();
    sprintf(zeile, "SPPB: Zustand %d (%s)", st, st ? "VERBUNDEN" : "getrennt");
    addLog(zeile);
}

void Command(char cmd[]) {
    if (strcmp(cmd, "CON") == 0) {
        auftrag = 1;
        responseCmnd("verbinde ...");
    } else if (strcmp(cmd, "OFF") == 0) {
        auftrag = 2;
        responseCmnd("trenne");
    } else if (strcmp(cmd, "ST") == 0) {
        zustand_melden();
        responseCmnd("siehe Log");
    } else if (strStartsWith(cmd, "TX ")) {
        // ⚠️ Wagenruecklauf anhaengen: JEDER Variograf-Befehl endet auf \r
        // (siehe [[variograf-protokoll-vponline]]), ohne ihn antwortet das Geraet nie.
        strSub(txbuf, cmd, 3, 0);
        strcat(txbuf, "\r");
        tx_len = strlen(txbuf);
        tx_auftrag = 1;
        responseCmnd("sende ...");
    } else if (strcmp(cmd, "BEEP") == 0) {
        // ⭐ gemus Test: ein NACKTES CR loest am Geraet immer einen Piep aus. Das ist
        // der direkteste Nachweis, dass ein Schreibvorgang den REKORDER erreicht --
        // unabhaengig von jeder Antwort, die auch aus dem Modulpuffer stammen koennte.
        // Dreimal mit Abstand, damit ein ueberhoerter Einzelpiep ausgeschlossen ist.
        auftrag = 6;
        responseCmnd("sende 3x CR -- bitte zuhoeren");
    } else if (strcmp(cmd, "VS2") == 0) {
        // ⭐ Der Test, der Modulpuffer von echter Antwort trennt: ZWEIMAL fragen.
        // Aus einem Puffer kommt die Antwort einmal, vom Rekorder zweimal.
        auftrag = 5;
        responseCmnd("sende VS, warte, sende VS erneut");
    } else {
        responseCmnd("SPPBCON | SPPBOFF | SPPBST | SPPBTX <text> | SPPBVS2");
    }
}

int main() {
    addCommand("SPPB");
    // MAC des EKG-Geraets, am 2026-08-05 mit GEXSCAN gefunden (-63 dBm, das naechste):
    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;
        laeufe = laeufe + 1;
        saveVars();
        addLog("SPPB: verbinde (blockiert ein paar Sekunden) ...");
        int ok = bleSppConnect();
        if (!ok) {
            addLog("SPPB: Verbindung fehlgeschlagen (ausser Reichweite, Dienst nicht gefunden, oder schon verbunden anderswo)");
            verbunden = 0;
        } else {
            int sub = bleSppSub(NOTIFY_CHR);
            if (!sub) {
                addLog("SPPB: verbunden, aber Abonnieren von BRSP_TX fehlgeschlagen -- UUID pruefen");
            } else {
                // ⭐ OHNE DAS FLIESST NICHTS. Das Modul liefert erst im Datenmodus;
                // gemus App macht genau das in changeBrspMode(): EIN Byte auf
                // BRSP_MODE. Der Kopfkommentar der App sagt zwar "Default ist
                // BrspModeData", aber darauf zu bauen heisst raten -- also setzen.
                // ⚠️ RUECKGABEWERT PRUEFEN. Bisher stand hier ein ungeprueftes
                // bleSppWrite -- und ob der Datenmodus ueberhaupt ankam, war damit
                // reine Annahme. Genau das ist der Verdacht: dass unsere Schreib-
                // vorgaenge das Modul nicht erreichen (ueber Classic PIEPT das
                // Geraet bei VS, ueber BLE nicht -- gemu, 2026-08-05).
                char m[2];
                m[0] = MODE_DATEN;
                int mw = bleSppWrite(MODE_CHR, m, 1);
                // Flusssteuerung freigeben: CTS=0 heisst "du darfst senden".
                // gemus App schaltet das fuer Dual-Module ausdruecklich um.
                char cts[2];
                cts[0] = 0;
                int cw = bleSppWrite(BRSP_CTS, cts, 1);
                sprintf(zeile, "SPPB: Datenmodus %s, CTS %s",
                        mw ? "ok" : "FEHLGESCHLAGEN", cw ? "ok" : "FEHLGESCHLAGEN");
                addLog(zeile);
                treffer = treffer + 1;
                saveVars();
            }
            verbunden = 1;
        }
        return;
    }
    if (auftrag == 2) {
        auftrag = 0;
        bleSppClose();
        verbunden = 0;
        addLog("SPPB: getrennt");
        return;
    }
    if (auftrag == 3) {
        auftrag = 0;
        zustand_melden();
        return;
    }
    if (auftrag == 6) {
        auftrag = 0;
        char cr[2];
        cr[0] = 0x0D;
        int i = 0;
        while (i < 3) {
            i = i + 1;
            int w = bleSppWrite(WRITE_CHR, cr, 1);
            sprintf(zeile, "SPPB: --- CR Nr. %d gesendet (Schreiben %s) ---", i, w ? "ok" : "FEHLER");
            addLog(zeile);
            delay(3000);
        }
        addLog("SPPB: --- drei CR raus. Wie oft hat es gepiept? ---");
        return;
    }
    if (auftrag == 5) {
        auftrag = 0;
        addLog("SPPB: --- VS Nr. 1 (jetzt auf den Piep hoeren) ---");
        strcpy(txbuf, "VS\r");
        bleSppWrite(WRITE_CHR, txbuf, 3);
        delay(4000);
        addLog("SPPB: --- VS Nr. 2 ---");
        bleSppWrite(WRITE_CHR, txbuf, 3);
        return;
    }
    if (tx_auftrag) {
        tx_auftrag = 0;
        int wok = bleSppWrite(WRITE_CHR, txbuf, tx_len);
        sprintf(zeile, "SPPB: Schreiben %s", wok ? "ok" : "fehlgeschlagen");
        addLog(zeile);
        return;
    }

    if (!verbunden) { delay(200); return; }

    // ⚠️ bleSppState() spiegelt einen Verbindungsabbruch, den das Geraet selbst ausloest
    // (Reichweite, Batterie) -- ohne diese Pruefung wuerde das Skript ewig auf einer toten
    // Verbindung auf Daten warten, ohne es zu merken.
    if (!bleSppState()) {
        addLog("SPPB: Verbindung verloren");
        verbunden = 0;
        return;
    }

    int n = bleSppAvailable();
    if (n > 0) {
        if (n > 255) { n = 255; }
        int got = bleSppRead(rx, n);
        sprintf(zeile, "SPPB: %d Byte:", got);
        addLog(zeile);
        // Als Hex ausgeben -- Biosignal-Rohdaten sind selten druckbarer Text.
        int i = 0;
        while (i < got) {
            char hexz[64];
            hexz[0] = 0;
            int j = 0;
            while (j < 16 && i < got) {
                char b2[4];
                sprintf(b2, "%02X ", rx[i] & 0xFF);
                strcat(hexz, b2);
                i = i + 1;
                j = j + 1;
            }
            addLog(hexz);
        }
    }
    delay(20);
}

void CleanUp() {
    bleSppClose();
}