/** * Proxi AI — BLE UART Command Interface * ===================================== * * Firmware für den micro:bit im KOSMOS-Proxi-Roboter (baugleich Tobbie II). * Nimmt Textbefehle über Bluetooth-LE-UART entgegen und antwortet auf demselben Weg. * * Protokoll: CMD:PARAM:PARAM\n -> OK:...\n | ERR:...\n * Vollständige Referenz: docs/PROTOCOL.md * * Wichtig: Die Motoren sind reine An/Aus-H-Brücken — es gibt KEINE * Geschwindigkeitsregelung. Gesteuert wird ausschließlich über die Dauer. * Die Hardware-Zugriffe laufen komplett über die Kosmos-Extension in proxi.ts. */ // ============================================================ // KONFIGURATION // ============================================================ /** Obergrenze für eine Bewegung ohne explizite Dauer (ms). Sicherheitsnetz. */ const SAFETY_MS = 10000 /** Obergrenze für jede einzelne Bewegung (ms). */ const MAX_MOVE_MS = 60000 const FW_VERSION = "proxi-ai/1.0" /** * Kurzkennung des Quellstands, erzeugt von build.sh in version.ts. * Geflasht wird von Hand — ohne die Kennung lässt sich nicht feststellen, * welcher Stand gerade auf dem Roboter liegt, und man testet gegen die * falsche Firmware. STATUS gibt sie deshalb mit zurück. */ function fwId(): string { return FW_VERSION + "+" + FW_BUILD } // ============================================================ // ZUSTAND // ============================================================ let connected = false /** control.millis(), zu dem der Laufmotor abgeschaltet wird. 0 = kein Timer. */ let walkUntil = 0 /** control.millis(), zu dem der Drehmotor abgeschaltet wird. 0 = kein Timer. */ let turnUntil = 0 /** * Warteschlange für den Aux-Fiber (alles was länger blockiert). * Ein einzelner Slot reichte nicht: während ein langer Job lief, hat der * nächste den übernächsten überschrieben — quittiert war er da aber * längst mit OK. Lieber eine kurze Schlange und ein ehrliches "busy". */ const AUX_QUEUE_MAX = 8 let auxKinds: string[] = [] let auxTexts: string[] = [] let auxNums: number[] = [] let auxNums2: number[] = [] /** Bricht einen laufenden Job ab. Setzt stopAll(). */ let auxAbort = false /** * Sicherung gegen einen hängenden Lautsprecher. * Reißt die Verbindung mitten in einem Ton ab, kommt der Aux-Fiber unter * Umständen nicht mehr dazu, ihn abzuschalten — dann tönt der Pin weiter, * bis jemand den Roboter ausmacht. Vor jedem Ton wird deshalb notiert, wann * er spätestens vorbei sein muss; der Watchdog-Fiber legt ihn danach still. * 0 = gerade kein Ton erwartet. */ let audioUntil = 0 /** Ton anmelden. ms = erwartete Dauer. */ function audioBegin(ms: number): void { audioUntil = control.millis() + ms + 300 } /** * Lautsprecher stilllegen. * * Bewusst NICHT music.stopAllSounds(): das ruft intern stopMelody() auf und * zieht damit den kompletten Melodie-Player wieder mit ins Programm — also * genau die rund 3 KB, wegen derer die Firmware nicht mehr auf den micro:bit * V1 passt. playTone mit Frequenz 0 legt den Pin genauso still. */ function audioSilence(): void { music.playTone(0, 1) } /** Ton abmelden — der Aux-Fiber hat regulär zu Ende gespielt. */ function audioEnd(): void { audioUntil = 0 audioSilence() } /** false = Schlange voll. */ function queueAux(kind: string, text: string, n1: number, n2: number): boolean { if (auxKinds.length >= AUX_QUEUE_MAX) return false auxKinds.push(kind) auxTexts.push(text) auxNums.push(n1) auxNums2.push(n2) return true } function clearAuxQueue(): void { auxKinds = [] auxTexts = [] auxNums = [] auxNums2 = [] } // ============================================================ // STRING-HILFEN // (MakeCode kennt weder split(), noch toUpperCase(), noch trim()) // ============================================================ function trimLine(s: string): string { let a = 0 let b = s.length while (a < b) { const c = s.charAt(a) if (c == " " || c == "\t" || c == "\r" || c == "\n") a++ else break } while (b > a) { const c = s.charAt(b - 1) if (c == " " || c == "\t" || c == "\r" || c == "\n") b-- else break } return s.substr(a, b - a) } function upper(s: string): string { let out = "" for (let i = 0; i < s.length; i++) { let c = s.charCodeAt(i) if (c >= 97 && c <= 122) c = c - 32 out = out + String.fromCharCode(c) } return out } function splitColon(s: string): string[] { const out: string[] = [] let cur = "" for (let i = 0; i < s.length; i++) { const c = s.charAt(i) if (c == ":") { out.push(cur) cur = "" } else { cur = cur + c } } out.push(cur) return out } function arg(p: string[], i: number): string { return i < p.length ? p[i] : "" } /** Parameter als Zahl, mit Default bei fehlend/unlesbar. */ function argNum(p: string[], i: number, dflt: number): number { const s = arg(p, i) if (s == "") return dflt const v = parseInt(s) // NaN-Test ohne isNaN(): NaN ist der einzige Wert, der sich selbst ungleich ist. if (v != v) return dflt return v } function clamp(v: number, lo: number, hi: number): number { if (v < lo) return lo if (v > hi) return hi return v } /** Alles ab Index i wieder mit ":" zusammensetzen — für Freitext. */ function joinFrom(p: string[], i: number): string { let out = "" for (let k = i; k < p.length; k++) { if (k > i) out = out + ":" out = out + p[k] } return out } // ============================================================ // ANTWORTEN // ============================================================ function reply(line: string): void { if (connected) bluetooth.uartWriteLine(line) } // ============================================================ // BEWEGUNG // ============================================================ /** * Dauer eines Bewegungsbefehls auflösen. * fehlt / leer -> SAFETY_MS (Sicherheitsnetz) * 0 -> 0 = dauerhaft, bis STOP (bewusste Entscheidung des Aufrufers) */ function moveDuration(p: string[], i: number): number { const s = arg(p, i) if (s == "") return SAFETY_MS const v = argNum(p, i, SAFETY_MS) if (v == 0) return 0 if (v < 0) return SAFETY_MS return clamp(v, 1, MAX_MOVE_MS) } function stopWalk(): void { walkUntil = 0 Proxi.stehenbleiben() } function stopTurn(): void { turnUntil = 0 Proxi.drehungsstopp() } function stopAll(): void { clearAuxQueue() auxAbort = true stopWalk() stopTurn() // Der Ton muss hier weg, auch wenn stopAll() aus einem fremden Fiber // kommt: ohne das tönt der Lautsprecher nach einem Verbindungsabbruch // einfach weiter. Die kurze Frist des Watchdogs fängt den Rest ab. audioUntil = control.millis() + 50 } // ============================================================ // BEWEGUNGSFIGUREN // // Die Extension bringt tanz()/stampfen()/kopf_schuetteln() schon mit, die // laufen aber am Stück durch — ein STOP mittendrin käme erst danach an. // Deshalb hier nachgebaut, Schritt für Schritt und mit Abbruchprüfung. // Die Pins fasst weiterhin ausschließlich die Extension an. // ============================================================ /** Pause in kleinen Scheiben. false = abgebrochen. */ function figurePause(ms: number): boolean { let left = ms while (left > 0) { if (auxAbort) return false const slice = left > 50 ? 50 : left basic.pause(slice) left = left - slice } return !auxAbort } function doDance(times: number): void { for (let i = 0; i < times; i++) { Proxi.rückwärts() Proxi.rechtsdrehung() if (!figurePause(250)) break Proxi.vorwärts() Proxi.linksdrehung() if (!figurePause(250)) break } Proxi.stehenbleiben() Proxi.drehungsstopp() } function doStamp(times: number): void { for (let i = 0; i < times; i++) { Proxi.vorwärts() if (!figurePause(150)) break Proxi.rückwärts() if (!figurePause(150)) break } Proxi.stehenbleiben() } function doShake(times: number): void { for (let i = 0; i < times; i++) { Proxi.linksdrehung() if (!figurePause(250)) break Proxi.rechtsdrehung() if (!figurePause(250)) break } Proxi.drehungsstopp() } function cmdMove(p: string[]): string { const dir = upper(arg(p, 1)) if (dir == "STOP") { stopWalk() return "OK:MOVE:STOP" } if (dir != "FWD" && dir != "BWD") return "ERR:MOVE:bad_dir" const ms = moveDuration(p, 2) if (dir == "FWD") Proxi.vorwärts() else Proxi.rückwärts() walkUntil = ms > 0 ? control.millis() + ms : 0 return "OK:MOVE:" + dir + ":" + ms } function cmdTurn(p: string[]): string { const dir = upper(arg(p, 1)) if (dir == "STOP") { stopTurn() return "OK:TURN:STOP" } if (dir != "LEFT" && dir != "RIGHT") return "ERR:TURN:bad_dir" const ms = moveDuration(p, 2) if (dir == "LEFT") Proxi.linksdrehung() else Proxi.rechtsdrehung() turnUntil = ms > 0 ? control.millis() + ms : 0 return "OK:TURN:" + dir + ":" + ms } // ============================================================ // DISPLAY // ============================================================ /** Icon anzeigen, ohne zu blockieren (Intervall 0 = sofort zurück). */ function showIconNow(icon: IconNames): void { basic.showIcon(icon, 0) } function cmdFace(p: string[]): string { const name = upper(arg(p, 1)) if (name == "RAW") { // FACE:RAW:r0:r1:r2:r3:r4 — je Zeile eine Bitmaske 0..31, Bit 0 = links basic.clearScreen() for (let y = 0; y < 5; y++) { let bits = clamp(argNum(p, 2 + y, 0), 0, 31) for (let x = 0; x < 5; x++) { if (bits % 2 == 1) led.plot(x, y) bits = Math.idiv(bits, 2) } } return "OK:FACE:RAW" } if (name == "HAPPY") showIconNow(IconNames.Happy) else if (name == "SAD") showIconNow(IconNames.Sad) else if (name == "ANGRY") showIconNow(IconNames.Angry) else if (name == "CONFUSED") showIconNow(IconNames.Confused) else if (name == "ASLEEP") showIconNow(IconNames.Asleep) else if (name == "SURPRISED") showIconNow(IconNames.Surprised) else if (name == "SILLY") showIconNow(IconNames.Silly) else if (name == "FABULOUS") showIconNow(IconNames.Fabulous) else if (name == "MEH") showIconNow(IconNames.Meh) else if (name == "YES") showIconNow(IconNames.Yes) else if (name == "NO") showIconNow(IconNames.No) else if (name == "HEART") showIconNow(IconNames.Heart) else if (name == "SMALLHEART") showIconNow(IconNames.SmallHeart) else if (name == "SKULL") showIconNow(IconNames.Skull) else if (name == "GHOST") showIconNow(IconNames.Ghost) else if (name == "DUCK") showIconNow(IconNames.Duck) else if (name == "HOUSE") showIconNow(IconNames.House) else if (name == "GIRAFFE") showIconNow(IconNames.Giraffe) else if (name == "COW") showIconNow(IconNames.Cow) else if (name == "RABBIT") showIconNow(IconNames.Rabbit) else if (name == "SNAKE") showIconNow(IconNames.Snake) else if (name == "BUTTERFLY") showIconNow(IconNames.Butterfly) else if (name == "TORTOISE") showIconNow(IconNames.Tortoise) else if (name == "STICKFIGURE") showIconNow(IconNames.StickFigure) else if (name == "TARGET") showIconNow(IconNames.Target) else if (name == "DIAMOND") showIconNow(IconNames.Diamond) else if (name == "SQUARE") showIconNow(IconNames.Square) else if (name == "TRIANGLE") showIconNow(IconNames.Triangle) else if (name == "UMBRELLA") showIconNow(IconNames.Umbrella) else if (name == "SWORD") showIconNow(IconNames.Sword) else if (name == "NORTH") basic.showArrow(ArrowNames.North, 0) else if (name == "EAST") basic.showArrow(ArrowNames.East, 0) else if (name == "SOUTH") basic.showArrow(ArrowNames.South, 0) else if (name == "WEST") basic.showArrow(ArrowNames.West, 0) else return "ERR:FACE:unknown" return "OK:FACE:" + name } // ============================================================ // MUSIK // // Der eingebaute Melodie-Player von MakeCode spielt in einem EIGENEN Fiber ab, // auf den man von außen keinen Zugriff hat. Ruft irgendein anderer Fiber // gleichzeitig playTone() oder stopAllSounds() auf, greifen zwei Fibers // parallel auf den Audio-Pin zu und der micro:bit stürzt ab (Panic). // Ausgelöst hat das hier zuverlässig die Folge MELODY -> TONE -> MELODY. // // Deshalb wird er nicht benutzt. Die Notendaten sind dieselben wie in der // MakeCode-Bibliothek (je zwei Bytes: MIDI-Notennummer und Dauer in // Viertel-Schlägen, Notennummer 0 = Pause), abgespielt werden sie hier Note // für Note — ausschließlich aus dem Aux-Fiber. Damit fasst genau ein Fiber // jemals den Lautsprecher an, und ein Not-Stopp greift zwischen zwei Noten. // ============================================================ const MELODY_BPM = 120 function melodyBuffer(name: string): Buffer { if (name == "DADADUM") return hex`00024f024f024f024b0800024d024d024d024a08` if (name == "ENTERTAINER") return hex`4a014b014c0154024c0154024c0154035401560157015801540156015802530156025404` if (name == "PRELUDE") return hex`48014c014f01540158014f015401580148014c014f01540158014f015401580148014a014f01560159014f015601590148014a014f01560159014f015601590147014a014f01560159014f015601590147014a014f01560159014f015601590148014c014f01540158014f015401580148014c014f01540158014f0154015801` if (name == "ODE") return hex`4c044c044d044f044f044d044c044a04480448044a044c044c064a024a084c044c044d044f044f044d044c044a04480448044a044c044a0648024808` if (name == "NYAN") return hex`5a025c02550157025301560155015302530255025602560155015301550157015a015c0157015a015501560153015501530157025a025c0157015a0155015701530156015701560155015301550156025301550157015a01550156015501530155025302550253024e01500153024e01500153015501570153015801570158015a01530253024e01500153014e0158015701550153014e014b014c014e0153024e01500153024e015001530153015501570153014e0150014e0153025301520153014e01500153015801570158015a0153025502` if (name == "RINGTONE") return hex`48014a014c024f024a014c014d0251024c014d014f0253025404` if (name == "FUNK") return hex`300230023302300135023001350236023702300230023702300136023001360235023302` if (name == "BLUES") return hex`30023402370239023a0239023702340230023402370239023a02390237023402350239023c023e023f023e023c02390230023402370239023a0239023702340237023b023e024102350239023c023f0230023402370234023702350234023202` if (name == "BIRTHDAY") return hex`480348014a0448044d044c08480348014a0448044f044d0848034801540451044d044c044a045203520151044d044f044d08` if (name == "WEDDING") return hex`48044d034d014d0848044f034c014d0848044d035101540451034d014d044c034d014f08` if (name == "FUNERAL") return hex`3c043c033c013c043f033e013e033c013c033b013c04` if (name == "PUNCHLINE") return hex`480343014201430144034303000347034803` if (name == "BADDY") return hex`3c0300033e023f0200023c0200024208` if (name == "CHASE") return hex`5101530154015301510200025101530154015301510200025102580257025802590258025702580253015401560154015302000253015401560154015302000253025802570258025902580257025802` if (name == "BADING") return hex`5f016403` if (name == "WAWAWAWAA") return hex`400300013f0300013e0400013d08` if (name == "JUMPUP") return hex`54015601580159015b01` if (name == "JUMPDOWN") return hex`5b015901580156015401` if (name == "POWERUP") return hex`4f01540158015b0258015b03` if (name == "POWERDOWN") return hex`5b01570154014f0253015403` return null } /** * Halbtöne der Oktave ab C4 (MIDI 60), mit 16 multipliziert. * Geteilt wird erst ganz am Schluss, damit beim Oktavieren nach unten * nichts wegläuft — sonst liegen tiefe Töne bis zu 11 Cent daneben. */ const NOTE_HZ16 = [4186, 4435, 4699, 4978, 5274, 5588, 5920, 6272, 6645, 7040, 7459, 7902] /** * MIDI-Notennummer -> Frequenz in Hz. 69 = A4 = 440 Hz. * * Bewusst reine Ganzzahl-Arithmetik, KEIN Math.pow und kein Math.round. * Das Ergebnis einer Kommazahl-Rechnung ist zwar rechnerisch richtig, kommt * aber in music.playTone() falsch an: der Tonausgang macht daraus eine * unsinnige Schwingungsdauer, kracht laut und bleibt hängen — und riss dabei * zuverlässig die BLE-Verbindung ab. Ein Ton mit ganzzahliger Frequenz * (z.B. direkt aus TONE:784:200) lief dagegen immer sauber durch. * * Also: hier nie wieder Fließkomma einbauen. */ function midiToFreq(midi: number): number { let hz16 = NOTE_HZ16[midi % 12] let oct = Math.idiv(midi, 12) - 5 while (oct > 0) { hz16 = hz16 * 2 oct = oct - 1 } while (oct < 0) { hz16 = Math.idiv(hz16, 2) oct = oct + 1 } return Math.idiv(hz16 + 8, 16) | 0 } /** * Spielt eine Notenfolge ab. Nur aus dem Aux-Fiber aufrufen. * Gibt zurück, wie viele Noten tatsächlich gespielt wurden — steht in der * DONE-Meldung und zeigt, wo es hängt, falls es hängt. */ function playMelodyBuffer(buf: Buffer): number { // wie im Original: eine Viertel-Schlag-Einheit bei 120 bpm = 125 ms const beat = Math.idiv(Math.idiv(60000, MELODY_BPM), 4) let played = 0 for (let i = 0; i + 1 < buf.length; i += 2) { if (auxAbort) break const midi = buf[i] const ms = buf[i + 1] * beat if (midi == 0) { basic.pause(ms) } else { audioBegin(ms) music.playTone(midiToFreq(midi), ms) audioUntil = 0 } played++ } audioEnd() return played } function cmdMelody(p: string[]): string { const name = upper(arg(p, 1)) if (!melodyBuffer(name)) return "ERR:MELODY:unknown" if (!queueAux("MELODY", name, 0, 0)) return "ERR:MELODY:busy" return "OK:MELODY:" + name } // ============================================================ // SENSOREN // ============================================================ /** * Es gibt bewusst kein SENSOR:LIGHT. * * Der Helligkeitssensor des micro:bit misst über die LED-Matrix, und die * sitzt bei Proxi hinter einer getönten Scheibe. Ausgebaut misst der Sensor * einwandfrei, eingebaut liefert er 0 — auch mit einer Taschenlampe direkt * davor. Das ist kein Softwareproblem und mit Software auch nicht zu lösen. */ function cmdSensor(p: string[]): string { const what = upper(arg(p, 1)) if (what == "" || what == "ALL") { return "SENSOR:ALL:IR_L=" + Proxi.Lese_LBlock() + ",IR_R=" + Proxi.Lese_RBlock() + ",TEMP=" + input.temperature() + ",PWR=" + pins.digitalReadPin(DigitalPin.P8) } if (what == "IR_L" || what == "IR_LEFT") return "SENSOR:IR_L:" + Proxi.Lese_LBlock() if (what == "IR_R" || what == "IR_RIGHT") return "SENSOR:IR_R:" + Proxi.Lese_RBlock() if (what == "TEMP") return "SENSOR:TEMP:" + input.temperature() if (what == "PWR") return "SENSOR:PWR:" + pins.digitalReadPin(DigitalPin.P8) return "ERR:SENSOR:bad_type" } // ============================================================ // DISPATCHER // ============================================================ function execute(raw: string): string { const line = trimLine(raw) if (line == "") return "" const p = splitColon(line) const cmd = upper(p[0]) if (cmd == "PING") return "PONG" if (cmd == "STATUS") return "STATUS:" + fwId() + ":PWR=" + pins.digitalReadPin(DigitalPin.P8) if (cmd == "STOP") { stopAll() basic.clearScreen() return "OK:STOP" } if (cmd == "MOVE") return cmdMove(p) if (cmd == "TURN") return cmdTurn(p) if (cmd == "FACE") return cmdFace(p) if (cmd == "MELODY") return cmdMelody(p) if (cmd == "SENSOR") return cmdSensor(p) if (cmd == "CLEAR") { basic.clearScreen() return "OK:CLEAR" } // --- Jobs, die blockieren: an den Aux-Fiber übergeben --- if (cmd == "TEXT") { const t = joinFrom(p, 1) if (t == "") return "ERR:TEXT:empty" if (!queueAux("TEXT", t, 0, 0)) return "ERR:TEXT:busy" return "OK:TEXT" } if (cmd == "TONE") { const hz = clamp(argNum(p, 1, 440), 50, 5000) const ms = clamp(argNum(p, 2, 400), 10, 5000) if (!queueAux("TONE", "", hz, ms)) return "ERR:TONE:busy" return "OK:TONE:" + hz } if (cmd == "DANCE") { const n = clamp(argNum(p, 1, 2), 1, 20) if (!queueAux("DANCE", "", n, 0)) return "ERR:DANCE:busy" return "OK:DANCE:" + n } if (cmd == "STAMP") { const n = clamp(argNum(p, 1, 3), 1, 20) if (!queueAux("STAMP", "", n, 0)) return "ERR:STAMP:busy" return "OK:STAMP:" + n } if (cmd == "SHAKE") { const n = clamp(argNum(p, 1, 3), 1, 20) if (!queueAux("SHAKE", "", n, 0)) return "ERR:SHAKE:busy" return "OK:SHAKE:" + n } return "ERR:unknown:" + cmd } // ============================================================ // FIBER 1 — Bewegungs-Watchdog // Hält den BLE-Handler frei: Bewegungen laufen mit Deadline, // STOP kommt dadurch auch mitten in einer Fahrt noch durch. // ============================================================ control.inBackground(function () { while (true) { const now = control.millis() if (walkUntil > 0 && now >= walkUntil) { walkUntil = 0 Proxi.stehenbleiben() } if (turnUntil > 0 && now >= turnUntil) { turnUntil = 0 Proxi.drehungsstopp() } if (audioUntil > 0 && now >= audioUntil) { // Notbremse — hier darf der Lautsprecher ausnahmsweise aus einem // fremden Fiber abgeschaltet werden, weil der Aux-Fiber es // offensichtlich nicht mehr selbst tut. audioUntil = 0 audioSilence() } basic.pause(10) } }) // ============================================================ // FIBER 2 — Aux-Worker für alles Blockierende // ============================================================ control.inBackground(function () { while (true) { if (auxKinds.length > 0) { // Die vier shift() laufen ohne Yield dazwischen und bleiben // dadurch gegenüber den Event-Handlern konsistent. const job = auxKinds.shift() const text = auxTexts.shift() const n1 = auxNums.shift() const n2 = auxNums2.shift() auxAbort = false if (job == "TEXT") { basic.showString(text) basic.clearScreen() } else if (job == "TONE") { audioBegin(n2) music.playTone(n1, n2) audioEnd() } else if (job == "MELODY") { const buf = melodyBuffer(text) if (buf) playMelodyBuffer(buf) } else if (job == "DANCE") { doDance(n1) } else if (job == "STAMP") { doStamp(n1) } else if (job == "SHAKE") { doShake(n1) } reply(auxAbort ? "DONE:" + job + ":aborted" : "DONE:" + job) } basic.pause(20) } }) // ============================================================ // BLUETOOTH // ============================================================ bluetooth.onBluetoothConnected(function () { connected = true showIconNow(IconNames.Yes) bluetooth.uartWriteLine("READY:" + fwId()) }) bluetooth.onBluetoothDisconnected(function () { connected = false stopAll() showIconNow(IconNames.Asleep) }) bluetooth.onUartDataReceived(serial.delimiters(Delimiters.NewLine), function () { const raw = bluetooth.uartReadUntil(serial.delimiters(Delimiters.NewLine)) // Ein Absturz im Handler würde den micro:bit anhalten — dann lieber // einen Fehler zurückmelden und weitermachen. let response = "" try { response = execute(raw) } catch (e) { response = "ERR:exception" } if (response != "") reply(response) }) // ============================================================ // TASTEN — funktionieren auch ohne Verbindung // ============================================================ input.onButtonPressed(Button.A, function () { stopAll() showIconNow(IconNames.No) reply("EVENT:ESTOP") }) input.onButtonPressed(Button.B, function () { reply("STATUS:" + fwId() + ":PWR=" + pins.digitalReadPin(DigitalPin.P8)) showIconNow(IconNames.Target) }) // ============================================================ // START // ============================================================ Proxi.stehenbleiben() Proxi.drehungsstopp() // Ton fest auf P0 legen — dort sitzt Proxis Summer, und darüber klingen auch // alle Kosmos-Programme. // // Das ist kein Schönheitsfix, sondern zwingend: der micro:bit V2 gibt Töne // sonst über seinen internen Audio-Mixer aus, und der lässt während des // Tönens den Funk nicht mehr zum Zug kommen. Die BLE-Verbindung stirbt dann // nach wenigen Sekunden an Connection Timeout (0x08) — reproduzierbar mit // jeder längeren Melodie. Auf P0 ist der Mixer aus dem Spiel und die // Verbindung hält auch über die 16 Sekunden von NYAN. // // Der eingebaute Lautsprecher des V2 darf dabei anbleiben; getestet mit // music.setBuiltInSpeakerEnabled(true) und voller Melodie ohne Abbruch. // Entscheidend ist allein der Pitch-Pin. pins.analogSetPitchPin(AnalogPin.P0) bluetooth.startUartService() showIconNow(IconNames.Asleep)