Skip to content

Commit 408039c

Browse files
author
sidey79
committed
fix: vereinheitliche die JSON-Antwortstruktur für CC1101-Parameter und verbessere Timeout-Konstanten
1 parent 5e32994 commit 408039c

6 files changed

Lines changed: 180 additions & 98 deletions

File tree

.roo/mcp.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"mcpServers":{"filesystem":{"command":"npx","args":["-y","@modelcontextprotocol/server-filesystem","/workspaces/PySignalduino"],"alwaysAllow":["edit_file","read_text_file","search_files","read_multiple_files"]},"git":{"command":"uvx","args":["mcp-server-git","--repository","/workspaces/PySignalduino"],"alwaysAllow":["git_diff_unstaged","git_checkout"]}}}
1+
{"mcpServers":{"filesystem":{"command":"npx","args":["-y","@modelcontextprotocol/server-filesystem","/workspaces/PySignalduino"],"alwaysAllow":["edit_file","read_text_file","search_files","read_multiple_files","create_directory","list_directory","directory_tree"]},"git":{"command":"uvx","args":["mcp-server-git","--repository","/workspaces/PySignalduino"],"alwaysAllow":["git_diff_unstaged","git_checkout"]}}}

docs/01_user_guide/mqtt_api.adoc

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -136,35 +136,35 @@ GET-Befehle benötigen eine leere Payload (`{}`) oder nur eine `req_id`.
136136
| CC1101 Konfigurationsregister-Dump als gekapselter String.
137137

138138
| `get/cc1101/patable`
139-
| `"C3E = C0 C1 C2 C3 C4 C5 C6 C7"`
139+
| `{"pa_table_hex": "C3E = C0 C1 C2 C3 C4 C5 C6 C7"}`
140140
| CC1101 PA-Tabelle.
141141

142142
| `get/cc1101/register`
143-
| `"C00 = 29"`
143+
| `{"register_value": "C00 = 29"}`
144144
| Liest den Wert eines einzelnen CC1101-Registers (Adresse 0x00). Der Befehl nimmt keinen Wert in der Payload entgegen und liest standardmäßig Register 0x00.
145145

146146
| `get/cc1101/frequency`
147-
| `{"frequency_mhz": 868.3500}`
147+
| `{"frequency": 868.3500}`
148148
| Aktuelle RF-Frequenz in MHz.
149149

150150
| `get/cc1101/bandwidth`
151-
| `102.0`
151+
| `{"bandwidth": 102.0}`
152152
| Aktuelle IF-Bandbreite in kHz.
153153

154154
| `get/cc1101/rampl`
155-
| `30`
155+
| `{"rampl": 30}`
156156
| Aktuelle Empfängerverstärkung (LNA Gain) in dB. Mögliche Werte: `24, 27, 30, 33, 36, 38, 40, 42`.
157157

158158
| `get/cc1101/sensitivity`
159-
| `12`
159+
| `{"sensitivity": 12}`
160160
| Aktuelle Empfindlichkeit in dB. Mögliche Werte: `4, 8, 12, 16`.
161161

162162
| `get/cc1101/datarate`
163-
| `4.8`
163+
| `{"datarate": 4.8}`
164164
| Aktuelle Datenrate in kBaud.
165165

166166
| `get/cc1101/settings`
167-
| `{"frequency_mhz": 868.35, "bandwidth": 102.0, "rampl": 30, "sens": 12, "datarate": 4.8}`
167+
| `{"frequency": 868.35, "bandwidth": 102.0, "rampl": 30, "sensitivity": 12, "datarate": 4.8}`
168168
| Aggregierte Abfrage aller CC1101-Haupteinstellungen.
169169
|===
170170

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
= ADR 005: Vereinheitlichung der MQTT-Antwortstruktur für CC1101-Parameter
2+
:doctype: article :encoding: utf-8 :lang: de :status: Proposed :decided-at: 2026-01-07 :decided-by: Roo
3+
:toc: left
4+
5+
[[kontext]]
6+
== Kontext
7+
8+
Aktuell weichen die JSON-Antwortstrukturen für die Abfrage einzelner CC1101-Parameter via MQTT (z.B. Topic `get/cc1101/bandwidth`) von der Struktur der Gesamt-Abfrage (Topic `get/cc1101/settings`) ab.
9+
10+
* **Aktuelle Einzelabfrage (angenommen):** `get/cc1101/bandwidth` -> `{"bandwidth": "X kHz"}`
11+
* **Aktuelle Gesamtabfrage (angenommen):** `get/cc1101/settings` -> `{"cc1101": {"bandwidth": "X kHz", "rampl": "Y dbm", ...}}`
12+
13+
Diese Inkonsistenz erschwert die automatisierte Verarbeitung der Antworten, da Clients je nach Abfragetyp unterschiedliche JSON-Pfade parsen müssen. Ziel ist eine konsistente Struktur, bei der die JSON-Knotennamen für die einzelnen Parameter in beiden Abfragetypen identisch sind.
14+
15+
[[entscheidung]]
16+
== Entscheidung
17+
18+
Die JSON-Antwortstruktur für alle CC1101-Parameter-Abfragen wird vereinheitlicht. Die Schlüsselnamen der einzelnen Parameter in der JSON-Antwort werden in beiden Abfragetypen (Einzelparameter und Gesamt-Settings) identisch verwendet. Es wird entschieden, die Schlüssel der Einzelparameter ohne umschließendes Wrapper-Objekt zu verwenden.
19+
20+
* **Antwort auf `get/cc1101/parameter` (z.B. `get/cc1101/bandwidth`):**
21+
```json
22+
{"bandwidth": "X kHz"}
23+
```
24+
* **Antwort auf `get/cc1101/settings`:**
25+
```json
26+
{
27+
"bandwidth": "X kHz",
28+
"rampl": "Y dbm",
29+
"sensitivity": "Z",
30+
"datarate": "A kbps"
31+
}
32+
```
33+
Die `settings`-Antwort ist somit eine direkte Aggregation der Einzelparameter-Antworten.
34+
35+
[[konsequenzen]]
36+
== Konsequenzen
37+
38+
=== Positive Konsequenzen
39+
* **Konsistenz:** Vereinfacht das Parsen für MQTT-Clients, da die logischen Parameternamen (z.B. `bandwidth`) immer als JSON-Schlüssel auf der obersten Ebene der jeweiligen Antwort verwendet werden.
40+
* **Wartbarkeit:** Reduziert die Komplexität in der Implementierung, da die Logik zur Generierung der Parameterdaten wiederverwendet werden kann.
41+
42+
=== Negative Konsequenzen
43+
* **Breaking Change:** Bestehende Clients, die sich auf eine Wrapper-Struktur wie `{"cc1101": {...}}` bei der Gesamt-Abfrage (`get/cc1101/settings`) verlassen, müssen angepasst werden.
44+
* **Migration:** Die Server-Logik für die MQTT-Antworten in der PySignalduino-Implementierung muss entsprechend geändert werden.
45+
46+
[[alternativen]]
47+
== Alternativen
48+
* **Alternative A: Wrapper in Einzelabfragen beibehalten:** Man könnte die Einzelabfrage um den CC1101-Wrapper erweitern (z.B. `get/cc1101/bandwidth` -> `{"cc1101": {"bandwidth": "X kHz"}}`). Dies wurde abgelehnt, da es unnötige Verschachtelung für Einzelwerte einführt und die Lesbarkeit des Payloads verschlechtert.
49+
* **Alternative B: Einzelabfragen als reiner Wert:** Die Antwort könnte nur den reinen Wert zurückgeben (z.B. `get/cc1101/bandwidth` -> `"X kHz"`). Dies wurde abgelehnt, da es das JSON-Format verlässt und der Parametername im Payload verloren ginge, was die Eindeutigkeit erschwert.

signalduino/commands.py

Lines changed: 56 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
from jsonschema import validate, ValidationError
1010
from signalduino.exceptions import CommandValidationError, SignalduinoCommandTimeout
11+
from .constants import SDUINO_CMD_TIMEOUT
1112

1213
if TYPE_CHECKING:
1314
# Importiere SignalduinoController nur für Type Hinting zur Kompilierzeit
@@ -39,11 +40,11 @@ def _parse_decoder_config(self, response: str) -> Dict[str, int]:
3940
pass
4041
return config
4142

42-
async def get_version(self, timeout: float = 2.0) -> str:
43+
async def get_version(self, timeout: float = SDUINO_CMD_TIMEOUT) -> str:
4344
"""Firmware version (V)"""
4445
return await self._send_command(command="V", expect_response=True, timeout=timeout)
4546

46-
async def get_free_ram(self, timeout: float = 2.0) -> int:
47+
async def get_free_ram(self, timeout: float = SDUINO_CMD_TIMEOUT) -> int:
4748
"""Free RAM (R)"""
4849
# Firmware typically responds with a numeric value (e.g., "1234")
4950
response_pattern = re.compile(r'^(\d+)$')
@@ -54,7 +55,7 @@ async def get_free_ram(self, timeout: float = 2.0) -> int:
5455
return int(match.group(1))
5556
raise ValueError(f"Unexpected response format for Free RAM: {response}")
5657

57-
async def get_uptime(self, timeout: float = 2.0) -> int:
58+
async def get_uptime(self, timeout: float = SDUINO_CMD_TIMEOUT) -> int:
5859
"""System uptime (t)"""
5960
# Firmware typically responds with a numeric value (e.g., "1234")
6061
response_pattern = re.compile(r'^(\d+)$')
@@ -65,17 +66,17 @@ async def get_uptime(self, timeout: float = 2.0) -> int:
6566
return int(match.group(1))
6667
raise ValueError(f"Unexpected response format for Uptime: {response}")
6768

68-
async def get_cmds(self, timeout: float = 2.0) -> str:
69+
async def get_cmds(self, timeout: float = SDUINO_CMD_TIMEOUT) -> str:
6970
"""Available commands (?)"""
7071
return await self._send_command(command="?", expect_response=True, timeout=timeout)
7172

72-
async def ping(self, timeout: float = 2.0) -> str:
73+
async def ping(self, timeout: float = SDUINO_CMD_TIMEOUT) -> str:
7374
"""Ping (P)"""
7475
return await self._send_command(command="P", expect_response=True, timeout=timeout)
7576

76-
async def get_config(self, timeout: float = 2.0) -> Dict[str, int]:
77+
async def get_config(self, timeout: float = SDUINO_CMD_TIMEOUT) -> Dict[str, int]:
7778
"""Decoder configuration (CG) - Returns parsed dictionary."""
78-
config_pattern = re.compile(r'^MS=[01];MU=[01];MC=[01];Mred=[01](;M[A-Za-z0-9]+=[01])*$')
79+
config_pattern = re.compile(r'^\s*([A-Za-z0-9]+=\d+;?)+\s*$', re.IGNORECASE)
7980
response = await self._send_command(
8081
command="CG",
8182
expect_response=True,
@@ -84,19 +85,20 @@ async def get_config(self, timeout: float = 2.0) -> Dict[str, int]:
8485
)
8586
return self._parse_decoder_config(response)
8687

87-
async def get_ccconf(self, timeout: float = 2.0) -> Dict[str, str]:
88+
async def get_ccconf(self, timeout: float = SDUINO_CMD_TIMEOUT) -> Dict[str, str]:
8889
"""CC1101 configuration registers (C0DnF). Returns a dictionary with the raw string."""
8990
# Response-Pattern aus 00_SIGNALduino.pm, Zeile 86, angepasst an Python regex
90-
response = await self._send_command(command="C0DnF", expect_response=True, timeout=timeout, response_pattern=re.compile(r'C0Dn11\s*=\s*[a-f0-9]+', re.IGNORECASE))
91+
response = await self._send_command(command="C0DnF", expect_response=True, timeout=timeout, response_pattern=re.compile(r'^\s*C0D\w*\s*=\s*.*$', re.IGNORECASE))
9192
# Kapselt den rohen String, um die MQTT-Antwort konsistent als Dict zurückzugeben
9293
return {"cc1101_config_string": response}
9394

94-
async def get_ccpatable(self, timeout: float = 2.0) -> str:
95+
async def get_ccpatable(self, timeout: float = SDUINO_CMD_TIMEOUT) -> Dict[str, str]:
9596
"""CC1101 PA table (C3E)"""
9697
# Response-Pattern aus 00_SIGNALduino.pm, Zeile 88
97-
return await self._send_command(command="C3E", expect_response=True, timeout=timeout, response_pattern=re.compile(r'C3E\s*=\s*.*'))
98+
response = await self._send_command(command="C3E", expect_response=True, timeout=timeout, response_pattern=re.compile(r'^\s*C3E\s*=\s*.*\s*$', re.IGNORECASE))
99+
return {"pa_table_hex": response}
98100

99-
async def factory_reset(self, timeout: float = 5.0) -> Dict[str, str]:
101+
async def factory_reset(self, timeout: float = SDUINO_CMD_TIMEOUT) -> Dict[str, str]:
100102
"""Sets EEPROM defaults, effectively a factory reset (e).
101103
102104
This command does not send a response unless debug mode is active. We treat the command
@@ -113,33 +115,35 @@ async def get_cc1101_settings(self, payload: Optional[Dict[str, Any]] = None) ->
113115
"""
114116
# Alle benötigten Getter existieren bereits in SignalduinoCommands
115117
freq_result = await self.get_frequency(payload)
116-
bandwidth = await self.get_bandwidth(payload)
117-
rampl = await self.get_rampl(payload)
118-
sens = await self.get_sensitivity(payload)
119-
datarate = await self.get_data_rate(payload)
118+
bandwidth_result = await self.get_bandwidth(payload)
119+
rampl_result = await self.get_rampl(payload)
120+
sens_result = await self.get_sensitivity(payload)
121+
datarate_result = await self.get_data_rate(payload)
120122

121123
return {
122-
# Flatten the frequency structure
123-
"frequency_mhz": freq_result["frequency_mhz"],
124-
"bandwidth": bandwidth,
125-
"rampl": rampl,
126-
"sens": sens,
127-
"datarate": datarate,
124+
"frequency_mhz": freq_result["frequency"],
125+
"bandwidth": bandwidth_result["bandwidth"],
126+
"rampl": rampl_result["rampl"],
127+
"sensitivity": sens_result["sensitivity"],
128+
"datarate": datarate_result["datarate"],
128129
}
129130

130131
# --- CC1101 Hardware Status GET-Methoden (Basierend auf 00_SIGNALduino.pm) ---
131132

132133
async def _read_register_value(self, register_address: int) -> int:
133134
"""Liest einen CC1101-Registerwert und gibt ihn als Integer zurück."""
134-
response = await self.read_cc1101_register(register_address)
135-
# Stellt sicher, dass wir nur den Wert nach 'C[A-Fa-f0-9]{2} = ' extrahieren
136-
match = re.search(r'C[A-Fa-f0-9]{2}\s*=\s*([0-9A-Fa-f]+)', response, re.IGNORECASE)
135+
response_dict = await self.read_cc1101_register(register_address)
136+
response = response_dict["register_value"]
137+
138+
# Stellt sicher, dass wir nur den Wert nach 'C[A-Fa-f0-9]{2}\s*=\s*([0-9A-Fa-f]+)' extrahieren
139+
# Hinzufügen von \s* um die Werte herum, um Whitespace-Toleranz zu erhöhen.
140+
match = re.search(r'C[A-Fa-f0-9]{2}\s*=\s*([0-9A-Fa-f]+)\s*', response, re.IGNORECASE)
137141
if match:
138142
return int(match.group(1), 16)
139143
# Fängt auch den Fall 'ccreg 00:' (default-Antwort) oder andere unerwartete Antworten ab
140144
raise ValueError(f"Unexpected response format for CC1101 register read: {response}")
141145

142-
async def get_bandwidth(self, payload: Optional[Dict[str, Any]] = None) -> float:
146+
async def get_bandwidth(self, payload: Optional[Dict[str, Any]] = None) -> Dict[str, float]:
143147
"""Liest die CC1101 Bandbreitenregister (MDMCFG4/0x10) und berechnet die Bandbreite in kHz."""
144148
r10 = await self._read_register_value(0x10) # MDMCFG4
145149

@@ -150,9 +154,9 @@ async def get_bandwidth(self, payload: Optional[Dict[str, Any]] = None) -> float
150154
# Frequenz (FXOSC) ist 26 MHz (26000 kHz)
151155
bandwidth_khz = 26000.0 / (8.0 * (4.0 + mant_b) * (1 << exp_b))
152156

153-
return round(bandwidth_khz, 3)
157+
return {"bandwidth": round(bandwidth_khz, 3)}
154158

155-
async def get_rampl(self, payload: Optional[Dict[str, Any]] = None) -> int:
159+
async def get_rampl(self, payload: Optional[Dict[str, Any]] = None) -> Dict[str, int]:
156160
"""Liest die CC1101 Verstärkungsregister (AGCCTRL0/0x1B) und gibt die Verstärkung in dB zurück."""
157161
r1b = await self._read_register_value(0x1B) # AGCCTRL0
158162

@@ -164,23 +168,25 @@ async def get_rampl(self, payload: Optional[Dict[str, Any]] = None) -> int:
164168
index = r1b & 7
165169

166170
if index < len(ampllist):
167-
return ampllist[index]
171+
rampl_db = ampllist[index]
168172
else:
169173
# Dies sollte nicht passieren, wenn die CC1101-Registerwerte korrekt sind
170174
logger.warning("Invalid AGC_LNA_GAIN setting found in 0x1B: %s", index)
171-
return -1 # Fehlerwert
175+
rampl_db = -1 # Fehlerwert
176+
177+
return {"rampl": rampl_db}
172178

173-
async def get_sensitivity(self, payload: Optional[Dict[str, Any]] = None) -> int:
179+
async def get_sensitivity(self, payload: Optional[Dict[str, Any]] = None) -> Dict[str, int]:
174180
"""Liest die CC1101 Empfindlichkeitsregister (RSSIAGC/0x1D) und gibt die Empfindlichkeit in dB zurück."""
175181
r1d = await self._read_register_value(0x1D) # RSSIAGC (0x1D)
176182

177183
# Sens (dB) = 4 + 4 * (r1d & 3)
178184
# Die unteren 2 Bits enthalten den LNA-Modus (LNA_PD_BUF)
179185
sens_db = 4 + 4 * (r1d & 3)
180186

181-
return sens_db
187+
return {"sensitivity": sens_db}
182188

183-
async def get_data_rate(self, payload: Optional[Dict[str, Any]] = None) -> float:
189+
async def get_data_rate(self, payload: Optional[Dict[str, Any]] = None) -> Dict[str, float]:
184190
"""Liest die CC1101 Datenratenregister (MDMCFG4/0x10 und MDMCFG3/0x11) und berechnet die Datenrate in kBaud."""
185191
r10 = await self._read_register_value(0x10) # MDMCFG4
186192
r11 = await self._read_register_value(0x11) # MDMCFG3
@@ -201,7 +207,7 @@ async def get_data_rate(self, payload: Optional[Dict[str, Any]] = None) -> float
201207
# Umrechnung in kBaud (kiloBaud = kiloBits pro Sekunde)
202208
data_rate_kbaud = data_rate_hz / 1000.0
203209

204-
return round(data_rate_kbaud, 2)
210+
return {"datarate": round(data_rate_kbaud, 2)}
205211

206212
def _calculate_datarate_registers(self, datarate_kbaud: float) -> tuple[int, int]:
207213
"""
@@ -260,11 +266,17 @@ def _calculate_datarate_registers(self, datarate_kbaud: float) -> tuple[int, int
260266

261267
return best_drate_e, best_drate_m
262268

263-
async def read_cc1101_register(self, register_address: int, timeout: float = 2.0) -> str:
269+
async def read_cc1101_register(self, register_address: int, timeout: float = SDUINO_CMD_TIMEOUT) -> Dict[str, str]:
264270
"""Read CC1101 register (C<reg>)"""
265271
hex_addr = f"{register_address:02X}"
266272
# Response-Pattern: ccreg 00: oder Cxx = yy (aus 00_SIGNALduino.pm, Zeile 87)
267-
return await self._send_command(command=f"C{hex_addr}", expect_response=True, timeout=timeout, response_pattern=re.compile(r'C[a-f0-9]{2}\s*=\s*[a-f0-9]+|ccreg 00:', re.IGNORECASE))
273+
# Die Regex muss an den Anfang und das Ende der Zeile gebunden werden (re.match wird verwendet)
274+
# ^(C[a-f0-9]{2}\s*=\s*[a-f0-9]+|ccreg 00:.*)\s*$
275+
# Hinweis: *Der Controller verwendet re.match*, was implizit ^ bindet.
276+
# Wir müssen den Regex also an das Ende binden, um Leerzeichen zu erlauben.
277+
response_pattern = re.compile(r'^\s*(C[a-f0-9]{2}\s*=\s*[a-f0-9]+|ccreg [a-f0-9]{2}:.*)\s*$', re.IGNORECASE)
278+
response = await self._send_command(command=f"C{hex_addr}", expect_response=True, timeout=timeout, response_pattern=response_pattern)
279+
return {"register_value": response}
268280

269281
async def _get_frequency_registers(self) -> int:
270282
"""Liest die CC1101 Frequenzregister (FREQ2, FREQ1, FREQ0) und kombiniert sie zu einem 24-Bit-Wert (F_REG)."""
@@ -276,24 +288,24 @@ async def _get_frequency_registers(self) -> int:
276288

277289
# Funktion zum Extrahieren des Hex-Werts aus der Antwort: Cxx = <hex>
278290
def extract_hex_value(response: str) -> int:
279-
# Stellt sicher, dass wir nur den Wert nach 'C[A-Fa-f0-9]{2} = ' extrahieren
280-
match = re.search(r'C[A-Fa-f0-9]{2}\s=\s([0-9A-Fa-f]+)$', response)
291+
# Stellt sicher, dass wir nur den Wert nach 'C[A-Fa-f0-9]{2}\s=\s([0-9A-Fa-f]+)$' extrahieren
292+
match = re.search(r'C[A-Fa-f0-9]{2}\s*=\s*([0-9A-Fa-f]+)\s*$', response)
281293
if match:
282294
return int(match.group(1), 16)
283295
# Fängt auch den Fall 'ccreg 00:' (default-Antwort) oder andere unerwartete Antworten ab
284296
raise ValueError(f"Unexpected response format for CC1101 register read: {response}")
285297

286298
# FREQ2 (0D)
287299
response2 = await self.read_cc1101_register(FREQ2)
288-
freq2 = extract_hex_value(response2)
300+
freq2 = extract_hex_value(response2["register_value"])
289301

290302
# FREQ1 (0E)
291303
response1 = await self.read_cc1101_register(FREQ1)
292-
freq1 = extract_hex_value(response1)
304+
freq1 = extract_hex_value(response1["register_value"])
293305

294306
# FREQ0 (0F)
295307
response0 = await self.read_cc1101_register(FREQ0)
296-
freq0 = extract_hex_value(response0)
308+
freq0 = extract_hex_value(response0["register_value"])
297309

298310
# Die Register bilden eine 24-Bit-Zahl: (FREQ2 << 16) | (FREQ1 << 8) | FREQ0
299311
f_reg = (freq2 << 16) | (freq1 << 8) | freq0
@@ -317,14 +329,14 @@ async def get_frequency(self, payload: Optional[Dict[str, Any]] = None) -> Dict[
317329

318330
# Rückgabe des gekapselten und auf 4 Dezimalstellen gerundeten Wertes, wie in tests/test_mqtt.py erwartet.
319331
return {
320-
"frequency_mhz": round(frequency_mhz, 4)
332+
"frequency": round(frequency_mhz, 4)
321333
}
322334

323-
async def send_raw_message(self, command: str, timeout: float = 2.0) -> str:
335+
async def send_raw_message(self, command: str, timeout: float = SDUINO_CMD_TIMEOUT) -> str:
324336
"""Send raw message (M...)"""
325337
return await self._send_command(command=command, expect_response=True, timeout=timeout)
326338

327-
async def send_message(self, message: str, timeout: float = 2.0) -> None:
339+
async def send_message(self, message: str, timeout: float = SDUINO_CMD_TIMEOUT) -> None:
328340
"""Send a pre-encoded message (P...#R...). This is typically used for 'set raw' commands where the message is already fully formatted.
329341
330342
NOTE: This sends the message AS IS, without any wrapping like 'set raw '.

0 commit comments

Comments
 (0)