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.
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
node_modules/
|
||||
pxt_modules/
|
||||
built/
|
||||
package.json.npm
|
||||
package-lock.json
|
||||
Executable
+53
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env bash
|
||||
# Baut die Proxi-AI-Firmware zu einer flashbaren .hex.
|
||||
#
|
||||
# ./build.sh -> proxi-ai.hex
|
||||
#
|
||||
# Das Ergebnis ist ein "Universal Hex": es enthaelt den Code für micro:bit V1
|
||||
# und V2 und läuft auf beiden.
|
||||
#
|
||||
# Erstmalige Einrichtung (einmalig, braucht Internet — der C++-Teil wird vom
|
||||
# MakeCode-Build-Service kompiliert):
|
||||
# npm install pxt
|
||||
# ./node_modules/.bin/pxt target microbit
|
||||
# ./node_modules/.bin/pxt install
|
||||
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
OUT="built/binary.hex"
|
||||
|
||||
if [[ ! -d node_modules/pxt-microbit ]]; then
|
||||
echo "pxt-microbit fehlt. Erst einrichten:" >&2
|
||||
echo " npm install pxt && ./node_modules/.bin/pxt target microbit && ./node_modules/.bin/pxt install" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Build-Kennung aus dem Quellstand erzeugen. Geflasht wird von Hand — ohne
|
||||
# diese Kennung sieht man dem Roboter nicht an, welcher Stand auf ihm liegt.
|
||||
# Der Hash läuft nur über die Quellen, version.ts selbst zählt nicht mit;
|
||||
# gleicher Code ergibt also immer dieselbe Kennung.
|
||||
BUILD_ID=$(cat main.ts proxi.ts pxt.json | sha256sum | cut -c1-7)
|
||||
cat > version.ts <<EOF
|
||||
// Erzeugt von build.sh — nicht von Hand ändern.
|
||||
const FW_BUILD = "$BUILD_ID"
|
||||
EOF
|
||||
echo "Build-Kennung: $BUILD_ID"
|
||||
|
||||
# Musik-Tabellen prüfen, bevor überhaupt gebaut wird. Der Fehler, der das
|
||||
# hier nötig gemacht hat, war am Schreibtisch unsichtbar und nur am Gerät zu
|
||||
# sehen — dieser Test fängt ihn vorher ab.
|
||||
python3 ../tools/check_melodies.py
|
||||
|
||||
rm -f "$OUT"
|
||||
./node_modules/.bin/pxt build
|
||||
|
||||
cp "$OUT" proxi-ai.hex
|
||||
|
||||
# Gegenprobe: die eingebetteten Quellen aus der .hex zurückholen und vergleichen.
|
||||
python3 ../tools/hex_source.py --check proxi-ai.hex main.ts
|
||||
python3 ../tools/hex_source.py --check proxi-ai.hex proxi.ts
|
||||
|
||||
echo
|
||||
echo "Fertig: $(pwd)/proxi-ai.hex"
|
||||
echo "Auf das MICROBIT-Laufwerk kopieren — fertig."
|
||||
@@ -0,0 +1,718 @@
|
||||
/**
|
||||
* 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)
|
||||
+19116
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,365 @@
|
||||
// ============================================================
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "proxi-ai",
|
||||
"description": "BLE-Kommando-Interface fuer den KOSMOS-Proxi-Roboter",
|
||||
"dependencies": {
|
||||
"core": "*",
|
||||
"bluetooth": "*",
|
||||
"microphone": "*"
|
||||
},
|
||||
"files": [
|
||||
"version.ts",
|
||||
"main.ts",
|
||||
"proxi.ts"
|
||||
],
|
||||
"preferredEditor": "tsprj",
|
||||
"yotta": {
|
||||
"config": {
|
||||
"microbit-dal": {
|
||||
"bluetooth": {
|
||||
"open": 1,
|
||||
"whitelist": 0,
|
||||
"event_service": 0,
|
||||
"dfu_service": 0,
|
||||
"device_info_service": 0,
|
||||
"eddystone_url": 0,
|
||||
"eddystone_uid": 0,
|
||||
"partial_flashing": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES5",
|
||||
"noImplicitAny": true,
|
||||
"outDir": "built",
|
||||
"rootDir": "."
|
||||
},
|
||||
"exclude": ["pxt_modules/**/*test.ts"]
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
// Erzeugt von build.sh — nicht von Hand ändern.
|
||||
const FW_BUILD = "0fae7b1"
|
||||
Reference in New Issue
Block a user