Files
christian 3905647b7f Proxi AI — Spielzeug-Roboter mit natürlicher Sprache steuern
Firmware, Bluetooth-Brücke und Agent-Skill für den KOSMOS-Proxi: einmal
flashen, danach hört der Roboter über Bluetooth LE auf Textbefehle. Was die
Befehle schickt, ist ihm egal — ein Skript, ein curl, oder ein KI-Agent, der
aus "mach mal was Lustiges" eine Choreografie baut.

  firmware/   MakeCode-Projekt (TypeScript) und fertige .hex für V1 und V2
  bridge/     BLE-Brücke und HTTP-API, einzige Abhängigkeit ist bleak
  agent/      Skill-Definition: Alltagssprache -> Befehle
  tools/      Quelltext aus .hex extrahieren, Musik-Tabellen prüfen
  docs/       Protokoll, Einrichtung, Hardware, Lizenzen

Zwei Dinge, die sonst nirgends stehen und in docs/HARDWARE.md dokumentiert
sind: die Pin-Belegung des Roboters ist nicht öffentlich, sie wurde aus den
mitgelieferten .hex-Dateien rekonstruiert (tools/hex_source.py holt sie
heraus). Und der Ton braucht zwingend eine ganzzahlige Frequenz sowie Pin P0
— sonst kracht der Lautsprecher, der Aufruf hängt und die Bluetooth-
Verbindung stirbt.

Am echten Roboter durchgetestet: Verbindung, Sensoren, Display, 41 Sekunden
Musik am Stück, alle Bewegungsrichtungen, Musik und Fahren gleichzeitig,
Tanzfiguren, Not-Stopp mitten in der Bewegung, Autostart nach Neustart.

MIT-Lizenz. Die Hardware-Extension stammt von kaku111 (TobbieII) und die
Notendaten aus pxt-microbit, beide ebenfalls MIT — siehe THIRD-PARTY-NOTICES.
2026-08-22 11:41:00 +00:00

366 lines
13 KiB
TypeScript

// ============================================================
// Proxi \u2014 Hardware-Extension
//
// FREMDER CODE. Unveraendert uebernommen bis auf diesen Kopf.
// Hier bitte nichts aendern: solange das die Original-Funktionen sind,
// verhaelt sich diese Firmware bei den Motoren exakt wie die Programme,
// die Kosmos mitliefert \u2014 inklusive der P8-Pruefung, deren genaue
// Bedeutung nicht dokumentiert ist (siehe docs/HARDWARE.md).
//
// Herkunft: aus der .hex von Kosmos' Beispielprogrammen extrahiert
// (dort als custom.ts), siehe tools/hex_source.py.
// Eingedeutschte Fassung der Extension "TobbieII" von kaku111,
// https://github.com/kaku111/20190209 \u2014 MIT.
// Lizenz: MIT, siehe docs/LICENSES.md
// ============================================================
//% weight=0 color=#0033E6 icon="\uf1b9" block="KOSMOS - Proxi"
//uf1b9
namespace Proxi {
let ADL_R: number = 0;
let ADH_R: number = 0;
let ADL_L: number = 0;
let ADH_L: number = 0;
let Read_LIR: number = 0;
let Read_RIR: number = 0;
let event_src_ir = 12;
let event_ir_sensor = 1;
let Motor_R: boolean = false;
let Motor_L: boolean = false;
let PX: number = 0;
let PY: number = 0;
let Force: number = 10;
function IR_sensorL(irdataL: number) { //此為中斷觸發方塊
control.inBackground(() => {
let flag = false
let last_flag = false
while (true) {
let ob: boolean = LBlock();
if (ob) { flag = true } else { flag = false }
if (flag != last_flag) {
if (flag) {
control.raiseEvent(event_src_ir, event_ir_sensor)
basic.pause(300) //300ms
}
last_flag = flag
}
basic.pause(1)
}
}
)
}
//
// 背景執行紅外線測距
// @param irdata_Set ; eg: 512
//
// //% blockId="IR_EVENTL" block="ON obstacles on the left: |%irdata_Set"
// //% irdata_Set.min=0 irdata_Set.max=1023
// //% blockGap=10 weight=99 //代表其重要性,越重放越高
// export function onIRL(irdata_Set: number = 512, handler: Action) {
// IR_sensorL(irdata_Set);
// control.onEvent(event_src_ir, event_ir_sensor, handler);
// }
// function IR_sensorR(irdata: number) {
// control.inBackground(() => {
// let flag = false
// let last_flag = false
// while (true) {
// let ob: boolean = LBlock();
// if(ob){flag=true}else{flag=false}
// if (flag != last_flag) {
// if (flag) {
// control.raiseEvent(event_src_ir, event_ir_sensor)
// basic.pause(3)
// }
// last_flag = flag
// }
// basic.pause(1)
// }
// }
// )
// }
/** Read the value sensed by the right side of the infrared sensor.
*/
//% blockId="Lese_RBlock" block="Lese rechten IR-Wert aus (Ergebnis 0 - 1024)"
//% blockGap=5 weight=65 //與下一個方塊的間隙及排重
export function Lese_RBlock(): number {
ADL_R = pins.analogReadPin(AnalogPin.P2)
pins.digitalWritePin(DigitalPin.P12, 1)
control.waitMicros(250)
ADH_R = pins.analogReadPin(AnalogPin.P2)
pins.digitalWritePin(DigitalPin.P12, 0)
if (pins.digitalReadPin(DigitalPin.P8) == 1) Read_RIR = ADH_R - ADL_R;
return (Read_RIR)
}
/** Read the value sensed by the left side of the infrared sensor.
*/
//% blockId="Lese_LBlock" block="Lese linken IR-Wert aus (Ergebnis 0 - 1024)"
//% blockGap=15 weight=60 //與下一個方塊的間隙及排重
export function Lese_LBlock(): number {
ADL_L = pins.analogReadPin(AnalogPin.P1)
pins.digitalWritePin(DigitalPin.P12, 1)
control.waitMicros(250)
ADH_L = pins.analogReadPin(AnalogPin.P1)
pins.digitalWritePin(DigitalPin.P12, 0)
Read_LIR = ADH_L - ADL_L;
return (Read_LIR)
}
/**
*Determine if there are obstacles on the right side.
*@param thresholdR ; eg: 512
*/
//% blockId="RBlock" block="wenn der rechte IR-Wert über %thresholdR liegt"
//% thresholdR.min=0 thresholdR.max=1023
//% blockGap=5 weight=58
export function RBlock(thresholdR: number = 512): boolean {
ADL_R = pins.analogReadPin(AnalogPin.P2)
pins.digitalWritePin(DigitalPin.P12, 1)
control.waitMicros(250)
ADH_R = pins.analogReadPin(AnalogPin.P2)
pins.digitalWritePin(DigitalPin.P12, 0)
if (((ADH_R - ADL_R) > thresholdR) && (pins.digitalReadPin(DigitalPin.P8) == 1)) {
//basic.showIcon(IconNames.House)
return (true)
} else {
//basic.showIcon(IconNames.Cow)
return (false)
}
}
/**
*Determine if there are obstacles on the left side.
*@param thresholdL ; eg: 512
*/
//% blockId="LBlock" block="wenn der linke IR-Wert über %thresholdL liegt"
//% thresholdL.min=0 thresholdL.max=1023
//% blockGap=10 weight=57
export function LBlock(thresholdL: number = 512): boolean {
ADL_L = pins.analogReadPin(AnalogPin.P1)
pins.digitalWritePin(DigitalPin.P12, 1)
control.waitMicros(250)
ADH_L = 0
if (pins.digitalReadPin(DigitalPin.P8) == 1) {
ADH_L = pins.analogReadPin(AnalogPin.P1)
pins.digitalWritePin(DigitalPin.P12, 0)
}
if ((ADH_L - ADL_L) > thresholdL) {//512) {
//basic.showIcon(IconNames.House)
return (true)
} else {
//basic.showIcon(IconNames.Cow)
return (false)
}
}
//輸出脈波
//% blockId="IRbolck" block="Out pulse & show-04"
//% blockGap=10 weight=55
//export function IRblock() {
// ADL_L = pins.analogReadPin(AnalogPin.P1)
// ADL_R = pins.analogReadPin(AnalogPin.P2)
// pins.digitalWritePin(DigitalPin.P12, 1)
// control.waitMicros(250)
// ADH_L = pins.analogReadPin(AnalogPin.P1)
// ADH_R = pins.analogReadPin(AnalogPin.P2)
// pins.digitalWritePin(DigitalPin.P12, 0)
// if ((ADH_L-ADL_L) > 512) {
//basic.showIcon(IconNames.House)
// led.plot(0, 0)
// led.unplot(0,4)
// } else {
//basic.showIcon(IconNames.Cow)
// led.plot(0, 4)
// led.unplot(0,0)
// }
// if ((ADH_R-ADL_R) > 512) {
//basic.showIcon(IconNames.House)
// led.plot(4, 0)
// led.unplot(4, 4)
// } else {
//basic.showIcon(IconNames.Cow)
// led.plot(4, 4)
// led.unplot(4,0)
// }
//return(true)
//}
/**
*Proxi läuft vorwaerts.
*/
//% blockId="vorwärts" block="Proxi läuft vorwärts"
//% blockGap=3 weight=35
export function vorwärts() {
if (pins.digitalReadPin(DigitalPin.P8) == 1) {
pins.digitalWritePin(DigitalPin.P13, 1)
pins.digitalWritePin(DigitalPin.P14, 0)
}
}
/**
*Proxi läuft rückwärts.
*/
//% blockId="rückwärts" block="Proxi läuft rückwärts"
//% blockGap=3 weight=34
export function rückwärts() {
if (Force != 0) {
pins.digitalWritePin(DigitalPin.P13, 0)
pins.digitalWritePin(DigitalPin.P14, 1)
Force = Force - 1;
}
if (pins.digitalReadPin(DigitalPin.P8) == 1) { Force = 10 }
}
/**
*Proxi bleibt stehen.
*/
//% blockId="stehenbleiben" block="Proxi bleibt stehen"
//% blockGap=10 weight=33
export function stehenbleiben() {
pins.digitalWritePin(DigitalPin.P13, 0)
pins.digitalWritePin(DigitalPin.P14, 0)
}
/**
*Proxi Rechtsdrehung.
*/
//% blockId="rechtsdrehung" block="Proxi Rechtsdrehung"
//% blockGap=3 weight=32
export function rechtsdrehung() {
pins.digitalWritePin(DigitalPin.P15, 0)
pins.digitalWritePin(DigitalPin.P16, 1)
Motor_L = false
Motor_R = true
}
/**
*Proxi Linksdrehung.
*/
//% blockId="linksdrehung" block="Proxi Linksdrehung"
//% blockGap=3 weight=31
export function linksdrehung() {
pins.digitalWritePin(DigitalPin.P15, 1)
pins.digitalWritePin(DigitalPin.P16, 0)
Motor_L = true
Motor_R = false
}
/**
*Proxi stoppt Drehung
*/
//% blockId="drehungstopp" block="Proxi stoppt Drehung"
//% blockGap=10 weight=30
export function drehungsstopp() {
if (Motor_L || Motor_R) {
if (Motor_R) {
pins.digitalWritePin(DigitalPin.P15, 1)
pins.digitalWritePin(DigitalPin.P16, 0)
} else {
pins.digitalWritePin(DigitalPin.P15, 0)
pins.digitalWritePin(DigitalPin.P16, 1)
}
basic.pause(50)
}
if (pins.digitalReadPin(DigitalPin.P8) == 1) {
pins.digitalWritePin(DigitalPin.P15, 0)
pins.digitalWritePin(DigitalPin.P16, 0)
Motor_L = false
Motor_R = false
}
}
/**
*Proxi stampft eine bestimmte Anzahl auf.
*@param time describe parameter here, eg:5
*/
//% blockId="stampfen" block="Proxi stampft %time mal auf"
//% time.min=1 time.max=100
//% blockGap=5 weight=25
//% advanced=true
export function stampfen(time: number): void {
for (let i = 0; i < time; i++) {
pins.digitalWritePin(DigitalPin.P13, 1) //向前
pins.digitalWritePin(DigitalPin.P14, 0)
basic.pause(150)
pins.digitalWritePin(DigitalPin.P13, 0) //向後
pins.digitalWritePin(DigitalPin.P14, 1)
basic.pause(150)
}
pins.digitalWritePin(DigitalPin.P13, 0) //停止
pins.digitalWritePin(DigitalPin.P14, 0)
}
/**
*Proxi schüttelt seinen Kopf eine bestimmte Anzahl.
*@param time describe parameter here, eg:5
*/
//% blockId="kopf_schuetteln" block="Proxi %time mal Kopfschütteln"
//% time.min=1 time.max=100
//% blockGap=5 weight=26
//% advanced=true
export function kopf_schuetteln(time: number): void {
for (let i = 0; i < time; i++) {
pins.digitalWritePin(DigitalPin.P15, 1) //左轉
pins.digitalWritePin(DigitalPin.P16, 0)
basic.pause(250)
pins.digitalWritePin(DigitalPin.P15, 0) //右轉
pins.digitalWritePin(DigitalPin.P16, 1)
basic.pause(250)
}
pins.digitalWritePin(DigitalPin.P15, 0) //停止行走
pins.digitalWritePin(DigitalPin.P16, 0)
}
/**
*Proxi tanzt eine bestimmte Anzahl.
*@param time describe parameter here, eg:5
*/
//% blockId="tanz" block="Proxi tanzt %time mal"
//% time.min=1 time.max=100
//% blockGap=5 weight=24
//% advanced=true
export function tanz(time: number): void {
for (let i = 0; i < time; i++) {
pins.digitalWritePin(DigitalPin.P13, 0) //向後
pins.digitalWritePin(DigitalPin.P14, 1)
pins.digitalWritePin(DigitalPin.P15, 0) //右轉
pins.digitalWritePin(DigitalPin.P16, 1)
basic.pause(250)
pins.digitalWritePin(DigitalPin.P13, 1) //向前
pins.digitalWritePin(DigitalPin.P14, 0)
pins.digitalWritePin(DigitalPin.P15, 1) //左轉
pins.digitalWritePin(DigitalPin.P16, 0)
basic.pause(250)
}
pins.digitalWritePin(DigitalPin.P13, 0)
pins.digitalWritePin(DigitalPin.P14, 0)
pins.digitalWritePin(DigitalPin.P15, 0)
pins.digitalWritePin(DigitalPin.P16, 0)
}
/**
*Proxi zeigt Stimmung auf Gesicht an (nur APP).
*@param RX_Data describe parameter here
*/
//% blockId="BLE_DOT" block="Proxi zeigt Stimmung auf Gesicht an (nur APP) %RX_Data"
//% blockGap=5 weight=23
//% advanced=true
export function zeige_Gesicht(RX_Data: string): void {
basic.clearScreen()
for (let PY = 0; PY <= 4; PY++) {
let PLOT_DATA: number = parseInt(RX_Data.substr(PY * 2 + 1, 2))
for (let PX = 0; PX <= 4; PX++) {
if (PLOT_DATA % 2 == 1) {
led.plot(PX, PY)
PLOT_DATA = PLOT_DATA - 1
}
PLOT_DATA = PLOT_DATA / 2
}
}
}
}