vincibot 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- vincibot/__init__.py +10 -0
- vincibot/_protokoll.py +109 -0
- vincibot/_typen.py +45 -0
- vincibot/_verbindung.py +160 -0
- vincibot/_woerter.py +87 -0
- vincibot/asynchron.py +476 -0
- vincibot/py.typed +0 -0
- vincibot/roboter.py +451 -0
- vincibot/scratch.py +1325 -0
- vincibot-0.2.0.dist-info/METADATA +273 -0
- vincibot-0.2.0.dist-info/RECORD +15 -0
- vincibot-0.2.0.dist-info/WHEEL +5 -0
- vincibot-0.2.0.dist-info/entry_points.txt +2 -0
- vincibot-0.2.0.dist-info/licenses/LICENSE +21 -0
- vincibot-0.2.0.dist-info/top_level.txt +1 -0
vincibot/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""vincibot – den VinciBot (MatataStudio) per Bluetooth mit Python steuern."""
|
|
2
|
+
from .roboter import VinciBot
|
|
3
|
+
from .asynchron import VinciBotAsync
|
|
4
|
+
from ._protokoll import Sensorwerte
|
|
5
|
+
from ._typen import Farbe
|
|
6
|
+
from ._woerter import RoboterFehler, FARBEN
|
|
7
|
+
from ._verbindung import VerbindungsFehler
|
|
8
|
+
|
|
9
|
+
__all__ = ["VinciBot", "VinciBotAsync", "RoboterFehler", "VerbindungsFehler", "Sensorwerte", "Farbe", "FARBEN"]
|
|
10
|
+
__version__ = "0.2.0"
|
vincibot/_protokoll.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Internes: Aufbau der Bluetooth-Pakete (für Anfänger nicht nötig)."""
|
|
2
|
+
from collections.abc import Iterable
|
|
3
|
+
from typing import TypedDict
|
|
4
|
+
|
|
5
|
+
class Sensorwerte(TypedDict):
|
|
6
|
+
"""Die aktuellen Messwerte des Roboters."""
|
|
7
|
+
hell_links: int
|
|
8
|
+
hell_rechts: int
|
|
9
|
+
linie: list[int]
|
|
10
|
+
geraeusch: int
|
|
11
|
+
farbe: tuple[int, int, int]
|
|
12
|
+
abstand_cm: float
|
|
13
|
+
lautstaerke: int
|
|
14
|
+
tempo: int
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# Sequenznummern: Der Bot nummeriert Befehlsarten durch und antwortet mit derselben Nummer.
|
|
18
|
+
SEQUENZ: dict[str, int] = {
|
|
19
|
+
"strecke": 0, "winkel": 1, "fahrt_start": 2, "fahrt_stopp": 3,
|
|
20
|
+
"strecke_tempo": 4, "winkel_tempo": 5, "motor_lauf": 6, "motor_tempo": 8, "motor_stopp": 9,
|
|
21
|
+
"tanz": 10, "emotion": 11, "verhalten": 12, "bild": 16, "text": 17, "anzeige_aus": 18,
|
|
22
|
+
"helligkeit": 19, "pixel": 20, "ausrichtung": 21, "led_feld": 22, "anzeige_drehen": 23,
|
|
23
|
+
"led_alle": 26, "led_einzeln": 27, "led_aus": 28,
|
|
24
|
+
"effekt": 31, "lied": 33, "sprich": 35, "datei": 37, "ton_stopp": 38,
|
|
25
|
+
"lautstaerke": 39, "trommel": 43, "pause": 44, "note": 45, "instrument": 46, "tempo": 47,
|
|
26
|
+
"ir_senden": 52, "abfrage": 0,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
# Begrüßung nach dem Verbinden (wie in der offiziellen Web-Oberfläche)
|
|
30
|
+
BEGRUESSUNG: list[list[int]] = [[126, 2, 2, 0, 0], [1, 1]]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def pruefsumme(daten: Iterable[int], startwert: int = 0xFFFF) -> int:
|
|
34
|
+
ergebnis = startwert
|
|
35
|
+
for byte in daten:
|
|
36
|
+
ergebnis = ((ergebnis >> 8) | (ergebnis << 8)) & 0xFFFF
|
|
37
|
+
ergebnis ^= byte
|
|
38
|
+
ergebnis ^= (ergebnis & 0xFF) >> 4
|
|
39
|
+
ergebnis ^= (ergebnis << 12) & 0xFFFF
|
|
40
|
+
ergebnis ^= ((ergebnis & 0xFF) << 5) & 0xFFFF
|
|
41
|
+
return ergebnis
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _maskiere(byte: int) -> list[int]:
|
|
45
|
+
if byte == 0xFE:
|
|
46
|
+
return [0xFD, 0xDE]
|
|
47
|
+
if byte == 0xFD:
|
|
48
|
+
return [0xFD, 0xDD]
|
|
49
|
+
return [byte]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def baue_paket(nutzdaten: list[int]) -> bytes:
|
|
53
|
+
"""Startbyte 0xFE, Länge, Nutzdaten, Prüfsumme (Sonderbytes werden maskiert)."""
|
|
54
|
+
laenge = len(nutzdaten) + 2
|
|
55
|
+
summe = pruefsumme(nutzdaten, pruefsumme([laenge]))
|
|
56
|
+
paket: list[int] = [0xFE] + _maskiere(laenge)
|
|
57
|
+
for byte in nutzdaten:
|
|
58
|
+
paket += _maskiere(byte)
|
|
59
|
+
paket += _maskiere(summe >> 8) + _maskiere(summe & 0xFF)
|
|
60
|
+
return bytes(paket)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def baue_befehlspaket(code: str, sequenz: int) -> bytes:
|
|
64
|
+
"""Paket für einen Python-Befehl, den der Bot ausführt."""
|
|
65
|
+
return baue_paket([0x70, 0x00, sequenz] + list(code.encode("ascii", "replace")))
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def text_literal(text: object) -> str:
|
|
69
|
+
"""Macht aus beliebigem Text ein Python-Textstück für den Bot (Umlaute als \\uXXXX)."""
|
|
70
|
+
teile: list[str] = []
|
|
71
|
+
for zeichen in str(text):
|
|
72
|
+
if zeichen in "\\'\"":
|
|
73
|
+
teile.append("\\" + zeichen)
|
|
74
|
+
elif ord(zeichen) > 127:
|
|
75
|
+
teile.append("\\u%04x" % ord(zeichen))
|
|
76
|
+
else:
|
|
77
|
+
teile.append(zeichen)
|
|
78
|
+
return "'" + "".join(teile) + "'"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def entpacke(puffer: list[int]) -> list[list[int]]:
|
|
82
|
+
"""Holt vollständige Pakete aus einem Byte-Puffer. Gibt (liste_von_nutzdaten) zurück
|
|
83
|
+
und entfernt Verarbeitetes aus dem Puffer."""
|
|
84
|
+
pakete: list[list[int]] = []
|
|
85
|
+
while True:
|
|
86
|
+
if 0xFE not in puffer:
|
|
87
|
+
puffer.clear()
|
|
88
|
+
return pakete
|
|
89
|
+
del puffer[:puffer.index(0xFE)]
|
|
90
|
+
roh: list[int] = []
|
|
91
|
+
i = 1
|
|
92
|
+
ende = None
|
|
93
|
+
while i < len(puffer):
|
|
94
|
+
b = puffer[i]
|
|
95
|
+
if b == 0xFD and i + 1 < len(puffer):
|
|
96
|
+
roh.append(0xFE if puffer[i + 1] == 0xDE else 0xFD)
|
|
97
|
+
i += 2
|
|
98
|
+
elif b == 0xFD:
|
|
99
|
+
break
|
|
100
|
+
else:
|
|
101
|
+
roh.append(b)
|
|
102
|
+
i += 1
|
|
103
|
+
if roh and len(roh) >= roh[0] + 1:
|
|
104
|
+
ende = i
|
|
105
|
+
break
|
|
106
|
+
if ende is None:
|
|
107
|
+
return pakete # Paket noch unvollständig
|
|
108
|
+
del puffer[:ende]
|
|
109
|
+
pakete.append(roh[1:roh[0] - 1])
|
vincibot/_typen.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Erlaubte Texte für die Befehle, als `Literal`-Typen.
|
|
2
|
+
|
|
3
|
+
Dadurch erkennt der Editor (Linter) Tippfehler sofort, zum Beispiel `roboter.gefuehl("froelich")`.
|
|
4
|
+
Ein Test (`tests/test_typen.py`) prüft, dass diese Listen zu den Wörterbüchern passen.
|
|
5
|
+
"""
|
|
6
|
+
from typing import Literal
|
|
7
|
+
|
|
8
|
+
Farbname = Literal["rot", "gruen", "grün", "blau", "gelb", "orange", "lila", "tuerkis", "türkis",
|
|
9
|
+
"rosa", "weiss", "weiß", "schwarz", "aus"]
|
|
10
|
+
Farbe = Farbname | tuple[int, int, int]
|
|
11
|
+
|
|
12
|
+
Position = Literal["links oben", "links mitte", "links unten",
|
|
13
|
+
"rechts oben", "rechts mitte", "rechts unten"]
|
|
14
|
+
|
|
15
|
+
Gefuehl = Literal["froehlich", "fröhlich", "wuetend", "wütend", "traurig", "ueberrascht",
|
|
16
|
+
"überrascht", "aengstlich", "ängstlich", "angeekelt", "begeistert", "schwindelig",
|
|
17
|
+
"muede", "müde", "schlaefrig", "schläfrig", "wach", "stolz"]
|
|
18
|
+
Tanz = Literal["tango", "walzer", "steppen", "samba", "polonaise", "jazz", "rock", "polka",
|
|
19
|
+
"rumba", "disco", "mazurka"]
|
|
20
|
+
Bewegung = Literal["umschauen", "verletzt", "sprinten", "kreisen", "schuetteln", "schütteln",
|
|
21
|
+
"frech", "stolz"]
|
|
22
|
+
Geraeusch = Literal["krankenwagen", "polizei", "feuerwehr", "zug", "zugpfeife", "schiffshorn",
|
|
23
|
+
"flugzeug", "auto", "automotor", "magnetbahn"]
|
|
24
|
+
Lied = Literal["funkel", "jingle bells", "geburtstag", "freude", "holzsoldaten", "jagdpolka",
|
|
25
|
+
"elise", "forelle", "peter und der wolf"]
|
|
26
|
+
Instrument = Literal["klavier", "e-klavier", "orgel", "gitarre", "e-gitarre", "bass", "zupfbass",
|
|
27
|
+
"cello", "posaune", "klarinette", "saxophon", "floete", "holzfloete", "fagott",
|
|
28
|
+
"chor", "vibraphon", "spieluhr", "steeldrum", "marimba", "synthesizer",
|
|
29
|
+
"weicher synthesizer"]
|
|
30
|
+
Note = Literal["c", "d", "e", "f", "g", "a", "h"]
|
|
31
|
+
|
|
32
|
+
FahrRichtung = Literal["vorwaerts", "vorwärts", "rueckwaerts", "rückwärts", "links", "rechts"]
|
|
33
|
+
Rad = Literal["links", "rechts", "beide"]
|
|
34
|
+
Einheit = Literal["grad", "umdrehungen", "sekunden"]
|
|
35
|
+
Drehrichtung = Literal["uhrzeigersinn", "gegen"]
|
|
36
|
+
DisplayRichtung = Literal["hoch", "runter", "links", "rechts"]
|
|
37
|
+
|
|
38
|
+
Seite = Literal["links", "rechts"]
|
|
39
|
+
SensorFarbe = Literal["weiss", "weiß", "rot", "gelb", "gruen", "grün", "blau", "lila", "schwarz"]
|
|
40
|
+
Linienfarbe = Literal["schwarz", "weiss", "weiß"]
|
|
41
|
+
Farbkanal = Literal["rot", "gruen", "grün", "blau"]
|
|
42
|
+
Bodensensor = Literal[1, 2, 3, 4, 5]
|
|
43
|
+
Taste = Literal["dreieck", "quadrat", "kreis"]
|
|
44
|
+
Fernbedienungstaste = Literal["volume", "mode", "speed", "forward", "backward", "left", "right",
|
|
45
|
+
"play", "dance", "led", "music", "1", "2", "3"]
|
vincibot/_verbindung.py
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""Internes: Bluetooth-Verbindung (asynchron). Anfänger nutzen stattdessen `VinciBot`."""
|
|
2
|
+
import asyncio
|
|
3
|
+
import shutil
|
|
4
|
+
import sys
|
|
5
|
+
import time
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
from bleak import BleakClient, BleakScanner
|
|
9
|
+
from bleak.backends.device import BLEDevice
|
|
10
|
+
from bleak.backends.characteristic import BleakGATTCharacteristic
|
|
11
|
+
|
|
12
|
+
from ._protokoll import BEGRUESSUNG, Sensorwerte, baue_befehlspaket, baue_paket, entpacke
|
|
13
|
+
|
|
14
|
+
SCHREIB_UUID = "6e400002-b5a3-f393-e0a9-e50e24dcca9e"
|
|
15
|
+
MELDUNG_UUID = "6e400003-b5a3-f393-e0a9-e50e24dcca9e"
|
|
16
|
+
PAKETGROESSE = 20
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class VerbindungsFehler(Exception):
|
|
20
|
+
"""Der Roboter konnte nicht gefunden oder verbunden werden."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class Antwort:
|
|
25
|
+
"""Die Antwort des Roboters auf einen Befehl."""
|
|
26
|
+
sequenz: int
|
|
27
|
+
status: int
|
|
28
|
+
text: str
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def fehler(self) -> bool:
|
|
32
|
+
return self.status != 0
|
|
33
|
+
|
|
34
|
+
class Verbindung:
|
|
35
|
+
def __init__(self, adresse: str | None = None, namensanfang: str = "VinciBot",
|
|
36
|
+
suchdauer: float = 15) -> None:
|
|
37
|
+
self.adresse: str | None = adresse
|
|
38
|
+
self.namensanfang: str = namensanfang
|
|
39
|
+
self.suchdauer: float = suchdauer
|
|
40
|
+
self.sensoren: Sensorwerte | None = None
|
|
41
|
+
self._client: BleakClient | None = None
|
|
42
|
+
self._puffer: list[int] = []
|
|
43
|
+
self._erwartet: dict[int, asyncio.Future[Antwort]] = {}
|
|
44
|
+
self._begruessung: asyncio.Future[list[int]] | None = None
|
|
45
|
+
self._letzte_meldung: tuple[bytes, float] = (b"", 0.0)
|
|
46
|
+
self.firmware: str | None = None
|
|
47
|
+
|
|
48
|
+
async def _suche(self) -> BLEDevice | None:
|
|
49
|
+
if self.adresse:
|
|
50
|
+
return await BleakScanner.find_device_by_address(self.adresse, timeout=self.suchdauer)
|
|
51
|
+
return await BleakScanner.find_device_by_filter(
|
|
52
|
+
lambda g, _: bool(g.name) and g.name.startswith(self.namensanfang),
|
|
53
|
+
timeout=self.suchdauer)
|
|
54
|
+
|
|
55
|
+
async def _haengende_verbindung_loesen(self) -> bool:
|
|
56
|
+
"""Linux: Ein Roboter, der im System noch als 'verbunden' gilt (z. B. nach einem
|
|
57
|
+
abgebrochenen Programm), wird nicht mehr gefunden. Dann trennen wir ihn hier."""
|
|
58
|
+
if not sys.platform.startswith("linux") or shutil.which("bluetoothctl") is None:
|
|
59
|
+
return False
|
|
60
|
+
prozess = await asyncio.create_subprocess_exec(
|
|
61
|
+
"bluetoothctl", "devices", "Connected",
|
|
62
|
+
stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.DEVNULL)
|
|
63
|
+
ausgabe, _ = await prozess.communicate()
|
|
64
|
+
gefunden = False
|
|
65
|
+
for zeile in ausgabe.decode("utf-8", "replace").splitlines():
|
|
66
|
+
teile = zeile.split(maxsplit=2)
|
|
67
|
+
if len(teile) == 3 and teile[0] == "Device" and (
|
|
68
|
+
teile[1].upper() == (self.adresse or "").upper()
|
|
69
|
+
or (not self.adresse and teile[2].startswith(self.namensanfang))):
|
|
70
|
+
trennen = await asyncio.create_subprocess_exec(
|
|
71
|
+
"bluetoothctl", "disconnect", teile[1],
|
|
72
|
+
stdout=asyncio.subprocess.DEVNULL, stderr=asyncio.subprocess.DEVNULL)
|
|
73
|
+
_ = await trennen.wait()
|
|
74
|
+
gefunden = True
|
|
75
|
+
if gefunden:
|
|
76
|
+
await asyncio.sleep(3)
|
|
77
|
+
return gefunden
|
|
78
|
+
|
|
79
|
+
async def verbinden(self) -> None:
|
|
80
|
+
# Erst prüfen, ob der Roboter im System noch als "verbunden" hängt (spart die Wartezeit der Suche).
|
|
81
|
+
_ = await self._haengende_verbindung_loesen()
|
|
82
|
+
geraet = await self._suche()
|
|
83
|
+
if geraet is None:
|
|
84
|
+
raise VerbindungsFehler(
|
|
85
|
+
"Kein VinciBot gefunden. Ist er eingeschaltet und nicht schon mit einem anderen"
|
|
86
|
+
+ " Programm oder Browser verbunden?")
|
|
87
|
+
self._client = BleakClient(geraet)
|
|
88
|
+
await self._client.connect()
|
|
89
|
+
await self._client.start_notify(MELDUNG_UUID, self._bei_meldung)
|
|
90
|
+
for nutzdaten in BEGRUESSUNG:
|
|
91
|
+
self._begruessung = asyncio.get_running_loop().create_future()
|
|
92
|
+
await self._sende(baue_paket(nutzdaten))
|
|
93
|
+
try:
|
|
94
|
+
_ = await asyncio.wait_for(self._begruessung, 3)
|
|
95
|
+
except asyncio.TimeoutError:
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
async def trennen(self) -> None:
|
|
99
|
+
if self._client is not None:
|
|
100
|
+
try:
|
|
101
|
+
await self._client.disconnect()
|
|
102
|
+
finally:
|
|
103
|
+
self._client = None
|
|
104
|
+
|
|
105
|
+
async def _sende(self, paket: bytes) -> None:
|
|
106
|
+
assert self._client is not None, "Nicht verbunden"
|
|
107
|
+
for anfang in range(0, len(paket), PAKETGROESSE):
|
|
108
|
+
await self._client.write_gatt_char(
|
|
109
|
+
SCHREIB_UUID, paket[anfang:anfang + PAKETGROESSE], response=True)
|
|
110
|
+
|
|
111
|
+
def _bei_meldung(self, _: BleakGATTCharacteristic, daten: bytearray) -> None:
|
|
112
|
+
# Der Roboter schickt jede Meldung doppelt; die zweite Kopie wird ignoriert.
|
|
113
|
+
jetzt = time.monotonic()
|
|
114
|
+
if bytes(daten) == self._letzte_meldung[0] and jetzt - self._letzte_meldung[1] < 0.2:
|
|
115
|
+
return
|
|
116
|
+
self._letzte_meldung = (bytes(daten), jetzt)
|
|
117
|
+
self._puffer.extend(daten)
|
|
118
|
+
for nutzdaten in entpacke(self._puffer):
|
|
119
|
+
self._verarbeite(nutzdaten)
|
|
120
|
+
|
|
121
|
+
def _verarbeite(self, n: list[int]) -> None:
|
|
122
|
+
if not n:
|
|
123
|
+
return
|
|
124
|
+
if n[0] == 0x74 and len(n) >= 16:
|
|
125
|
+
self.sensoren = Sensorwerte(
|
|
126
|
+
hell_links=n[1], hell_rechts=n[2], linie=[n[3], n[4], n[5], n[6], n[7]],
|
|
127
|
+
geraeusch=n[8], farbe=(n[9], n[10], n[11]),
|
|
128
|
+
abstand_cm=((n[12] << 8) | n[13]) / 10, lautstaerke=n[14], tempo=n[15])
|
|
129
|
+
elif n[0] == 0x70 and len(n) >= 4:
|
|
130
|
+
antwort = Antwort(n[2], n[3], bytes(n[4:]).decode("utf-8", "replace"))
|
|
131
|
+
erwartet = self._erwartet.pop(antwort.sequenz, None)
|
|
132
|
+
if erwartet and not erwartet.done():
|
|
133
|
+
erwartet.set_result(antwort)
|
|
134
|
+
elif n[0] == 1 and len(n) >= 5:
|
|
135
|
+
self.firmware = f"{n[2]}.{n[3]}.{n[4]}"
|
|
136
|
+
if self._begruessung and not self._begruessung.done():
|
|
137
|
+
self._begruessung.set_result(n)
|
|
138
|
+
elif self._begruessung and not self._begruessung.done():
|
|
139
|
+
self._begruessung.set_result(n)
|
|
140
|
+
|
|
141
|
+
async def firmware_abfragen(self) -> str | None:
|
|
142
|
+
"""Fragt die Firmware-Version des Roboters ab, z. B. "1.2.0"."""
|
|
143
|
+
self._begruessung = asyncio.get_running_loop().create_future()
|
|
144
|
+
await self._sende(baue_paket([1, 1]))
|
|
145
|
+
try:
|
|
146
|
+
_ = await asyncio.wait_for(self._begruessung, 3)
|
|
147
|
+
except asyncio.TimeoutError:
|
|
148
|
+
pass
|
|
149
|
+
return self.firmware
|
|
150
|
+
|
|
151
|
+
async def befehl(self, code: str, sequenz: int = 0, zeitlimit: float = 15) -> Antwort | None:
|
|
152
|
+
"""Führt einen Python-Befehl auf dem Bot aus und wartet auf seine Antwort."""
|
|
153
|
+
zukunft: asyncio.Future[Antwort] = asyncio.get_running_loop().create_future()
|
|
154
|
+
self._erwartet[sequenz] = zukunft
|
|
155
|
+
await self._sende(baue_befehlspaket(code, sequenz))
|
|
156
|
+
try:
|
|
157
|
+
return await asyncio.wait_for(zukunft, zeitlimit)
|
|
158
|
+
except asyncio.TimeoutError:
|
|
159
|
+
_ = self._erwartet.pop(sequenz, None)
|
|
160
|
+
return None
|
vincibot/_woerter.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Internes: Wörterbücher (Deutsch -> Befehle des Roboters) und Hilfsfunktionen."""
|
|
2
|
+
from collections.abc import Mapping
|
|
3
|
+
from typing import TypeVar
|
|
4
|
+
|
|
5
|
+
from ._typen import Farbe
|
|
6
|
+
|
|
7
|
+
_T = TypeVar("_T")
|
|
8
|
+
|
|
9
|
+
# ---------------------------------------------------------------- Wörterbücher (Deutsch -> Roboter)
|
|
10
|
+
FARBEN: dict[str, tuple[int, int, int]] = {
|
|
11
|
+
"rot": (255, 0, 0), "gruen": (0, 255, 0), "grün": (0, 255, 0), "blau": (0, 0, 255),
|
|
12
|
+
"gelb": (255, 200, 0), "orange": (255, 100, 0), "lila": (160, 0, 255),
|
|
13
|
+
"tuerkis": (0, 220, 220), "türkis": (0, 220, 220), "rosa": (255, 80, 160),
|
|
14
|
+
"weiss": (255, 255, 255), "weiß": (255, 255, 255), "schwarz": (0, 0, 0), "aus": (0, 0, 0),
|
|
15
|
+
}
|
|
16
|
+
FARBEN_SENSOR: dict[str, str] = {"weiss": "white", "weiß": "white", "rot": "red", "gelb": "yellow",
|
|
17
|
+
"gruen": "green", "grün": "green", "blau": "blue", "lila": "purple",
|
|
18
|
+
"schwarz": "black"}
|
|
19
|
+
GEFUEHLE: dict[str, str] = {
|
|
20
|
+
"froehlich": "happy", "fröhlich": "happy", "wuetend": "angry", "wütend": "angry",
|
|
21
|
+
"traurig": "sad", "ueberrascht": "surprised", "überrascht": "surprised",
|
|
22
|
+
"aengstlich": "feared", "ängstlich": "feared", "angeekelt": "disgusted",
|
|
23
|
+
"begeistert": "joyful", "schwindelig": "dizzy", "muede": "fatigued", "müde": "fatigued",
|
|
24
|
+
"schlaefrig": "sleepy", "schläfrig": "sleepy", "wach": "awake", "stolz": "proud",
|
|
25
|
+
}
|
|
26
|
+
TAENZE: dict[str, str] = {"tango": "tango", "walzer": "waltz", "steppen": "tap", "samba": "samba",
|
|
27
|
+
"polonaise": "polonaise", "jazz": "jazz", "rock": "rock", "polka": "polka",
|
|
28
|
+
"rumba": "rumba", "disco": "disco", "mazurka": "mazurka"}
|
|
29
|
+
BEWEGUNGEN: dict[str, str] = {"umschauen": "look around", "verletzt": "hurt", "sprinten": "sprint",
|
|
30
|
+
"kreisen": "circling", "schuetteln": "shake", "schütteln": "shake",
|
|
31
|
+
"frech": "naughty", "stolz": "proud"}
|
|
32
|
+
GERAEUSCHE: dict[str, str] = {
|
|
33
|
+
"krankenwagen": "ambulance", "polizei": "police car", "feuerwehr": "fire engine",
|
|
34
|
+
"zug": "train", "zugpfeife": "train whistle", "schiffshorn": "boat whistle",
|
|
35
|
+
"flugzeug": "aircraft", "auto": "automobile", "automotor": "automobile engine",
|
|
36
|
+
"magnetbahn": "magnetically levitated train",
|
|
37
|
+
}
|
|
38
|
+
LIEDER: dict[str, str] = {
|
|
39
|
+
"funkel": "twinkle", "jingle bells": "jingle bells", "geburtstag": "happy birthday to you",
|
|
40
|
+
"freude": "ode to joy", "holzsoldaten": "parade of the wooden soldiers",
|
|
41
|
+
"jagdpolka": "hunting polka", "elise": "to alice", "forelle": "die forelle",
|
|
42
|
+
"peter und der wolf": "peter and the wolf",
|
|
43
|
+
}
|
|
44
|
+
INSTRUMENTE: list[str] = ["klavier", "e-klavier", "orgel", "gitarre", "e-gitarre", "bass", "zupfbass",
|
|
45
|
+
"cello", "posaune", "klarinette", "saxophon", "floete", "holzfloete", "fagott",
|
|
46
|
+
"chor", "vibraphon", "spieluhr", "steeldrum", "marimba", "synthesizer",
|
|
47
|
+
"weicher synthesizer"]
|
|
48
|
+
# Tonhöhen in Hertz für die vierte Oktave (deutsche Notennamen: h statt b/si)
|
|
49
|
+
NOTEN: dict[str, int] = {"c": 262, "d": 294, "e": 330, "f": 349, "g": 392, "a": 440, "h": 494}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# Die sechs Lichter von vorne gesehen: 1 6 / 2 5 / 3 4 (Nummer = Platz in der Befehlsliste des Roboters)
|
|
53
|
+
POSITIONEN: dict[str, int] = {
|
|
54
|
+
"links oben": 1, "links mitte": 2, "links unten": 3,
|
|
55
|
+
"rechts unten": 4, "rechts mitte": 5, "rechts oben": 6,
|
|
56
|
+
}
|
|
57
|
+
POSITIONEN.update({name.replace(" ", "_"): nr for name, nr in list(POSITIONEN.items())})
|
|
58
|
+
POSITION_NAMEN: dict[int, str] = {nr: name for name, nr in POSITIONEN.items() if " " in name}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class RoboterFehler(Exception):
|
|
62
|
+
"""Etwas ist beim Steuern des Roboters schiefgegangen."""
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def farbe_umrechnen(farbe: Farbe) -> tuple[int, int, int]:
|
|
66
|
+
"""Nimmt einen Farbnamen ('rot') oder drei Zahlen (255, 0, 0) und gibt (r, g, b) zurück."""
|
|
67
|
+
if isinstance(farbe, str):
|
|
68
|
+
name = farbe.lower().strip()
|
|
69
|
+
if name not in FARBEN:
|
|
70
|
+
raise RoboterFehler(f"Die Farbe '{farbe}' kenne ich nicht. Mögliche Farben: "
|
|
71
|
+
+ ", ".join(sorted({k for k in FARBEN if k.isascii()})))
|
|
72
|
+
return FARBEN[name]
|
|
73
|
+
r, g, b = farbe
|
|
74
|
+
for wert in (r, g, b):
|
|
75
|
+
if not 0 <= wert <= 255:
|
|
76
|
+
raise RoboterFehler("Farbwerte müssen zwischen 0 und 255 liegen.")
|
|
77
|
+
return (int(r), int(g), int(b))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def waehle(name: object, tabelle: Mapping[str, _T], art: str) -> _T:
|
|
81
|
+
schluessel = str(name).lower().strip()
|
|
82
|
+
if schluessel not in tabelle:
|
|
83
|
+
raise RoboterFehler(f"Ich kenne {art} '{name}' nicht. Mögliche Werte: "
|
|
84
|
+
+ ", ".join(sorted({k for k in tabelle if k.isascii()})))
|
|
85
|
+
return tabelle[schluessel]
|
|
86
|
+
|
|
87
|
+
|