Add per-client LED controls aligned with the REST API schema.

Each client panel gets mode-specific LED inputs (color, intensity, progress, digit, blink) and the Makefile now copies API_REST.md alongside the WebSocket docs.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-29 21:55:18 +02:00
co-authored by Cursor
parent 84827c9782
commit 4f3435ba37
3 changed files with 525 additions and 1 deletions
+239
View File
@@ -10,6 +10,9 @@ const PANEL_BORDER_NORMAL := Color(0.22, 0.25, 0.32, 1.0)
const PANEL_STYLE_FLASH := Color(0.32, 0.28, 0.06, 1.0)
const PANEL_BORDER_FLASH := Color(1.0, 0.82, 0.15, 1.0)
const LED_MODES: PackedStringArray = ["color", "clear", "progress", "digit", "blink", "find-me"]
const LED_MODES_COLOR_INTENSITY: PackedStringArray = ["color", "progress", "digit", "blink"]
@onready var status_label: Label = $UiLayer/TopBar/StatusLabel
@onready var log_output: TextEdit = $UiLayer/MainSplit/LogPanel/LogOutput
@onready var stream_output: Label = $UiLayer/MainSplit/LogPanel/StreamOutput
@@ -210,6 +213,122 @@ func _populate_clients(clients: Array) -> void:
tap_row.add_child(tap_double)
tap_row.add_child(tap_triple)
var led_label := Label.new()
led_label.text = "LED Ring:"
led_label.add_theme_font_size_override("font_size", 12)
led_label.add_theme_color_override("font_color", Color(0.7, 0.75, 0.85))
vbox.add_child(led_label)
var led_row := HBoxContainer.new()
led_row.add_theme_constant_override("separation", 8)
vbox.add_child(led_row)
var led_mode := OptionButton.new()
for mode in LED_MODES:
led_mode.add_item(mode)
led_mode.disabled = not available
led_row.add_child(led_mode)
var led_apply := Button.new()
led_apply.text = "Anwenden"
led_apply.disabled = not available
led_apply.pressed.connect(_apply_led_for_client.bind(cid))
led_row.add_child(led_apply)
var led_params := VBoxContainer.new()
led_params.add_theme_constant_override("separation", 4)
vbox.add_child(led_params)
var led_color_row := HBoxContainer.new()
led_color_row.add_theme_constant_override("separation", 8)
led_params.add_child(led_color_row)
var led_color_lbl := Label.new()
led_color_lbl.text = "Farbe:"
led_color_row.add_child(led_color_lbl)
var led_color := ColorPickerButton.new()
led_color.color = Color(1.0, 0.0, 0.0)
led_color.custom_minimum_size = Vector2(36, 28)
led_color.disabled = not available
led_color_row.add_child(led_color)
var led_intensity_row := HBoxContainer.new()
led_intensity_row.add_theme_constant_override("separation", 8)
led_params.add_child(led_intensity_row)
var led_intensity_lbl := Label.new()
led_intensity_lbl.text = "Intensität:"
led_intensity_row.add_child(led_intensity_lbl)
var led_intensity := SpinBox.new()
led_intensity.min_value = 0
led_intensity.max_value = 255
led_intensity.value = 128
led_intensity.custom_minimum_size = Vector2(80, 0)
_set_spinbox_enabled(led_intensity, available)
led_intensity_row.add_child(led_intensity)
var led_progress_row := HBoxContainer.new()
led_progress_row.add_theme_constant_override("separation", 8)
led_params.add_child(led_progress_row)
var led_progress_lbl := Label.new()
led_progress_lbl.text = "Fortschritt %:"
led_progress_row.add_child(led_progress_lbl)
var led_progress := SpinBox.new()
led_progress.min_value = 0
led_progress.max_value = 100
led_progress.value = 50
led_progress.custom_minimum_size = Vector2(80, 0)
_set_spinbox_enabled(led_progress, available)
led_progress_row.add_child(led_progress)
var led_digit_row := HBoxContainer.new()
led_digit_row.add_theme_constant_override("separation", 8)
led_params.add_child(led_digit_row)
var led_digit_lbl := Label.new()
led_digit_lbl.text = "Ziffer:"
led_digit_row.add_child(led_digit_lbl)
var led_digit := SpinBox.new()
led_digit.min_value = 0
led_digit.max_value = 10
led_digit.value = 3
led_digit.custom_minimum_size = Vector2(80, 0)
_set_spinbox_enabled(led_digit, available)
led_digit_row.add_child(led_digit)
var led_blink_row := HBoxContainer.new()
led_blink_row.add_theme_constant_override("separation", 8)
led_params.add_child(led_blink_row)
var led_blink_ms_lbl := Label.new()
led_blink_ms_lbl.text = "Blink ms:"
led_blink_row.add_child(led_blink_ms_lbl)
var led_blink_ms := SpinBox.new()
led_blink_ms.min_value = 1
led_blink_ms.max_value = 10000
led_blink_ms.value = 500
led_blink_ms.custom_minimum_size = Vector2(80, 0)
_set_spinbox_enabled(led_blink_ms, available)
led_blink_row.add_child(led_blink_ms)
var led_blink_count_lbl := Label.new()
led_blink_count_lbl.text = "Anzahl:"
led_blink_row.add_child(led_blink_count_lbl)
var led_blink_count := SpinBox.new()
led_blink_count.min_value = 1
led_blink_count.max_value = 100
led_blink_count.value = 3
led_blink_count.custom_minimum_size = Vector2(60, 0)
_set_spinbox_enabled(led_blink_count, available)
led_blink_row.add_child(led_blink_count)
led_mode.item_selected.connect(func(_idx: int) -> void: _update_led_fields(cid))
var led_presets := HBoxContainer.new()
led_presets.add_theme_constant_override("separation", 6)
vbox.add_child(led_presets)
_add_led_preset_btn(led_presets, "Aus", cid, available, func() -> void: _led_apply_preset(cid, "clear"))
_add_led_preset_btn(led_presets, "Rot", cid, available, func() -> void: _led_apply_preset(cid, "color", Color(1, 0.2, 0.2), 128))
_add_led_preset_btn(led_presets, "Grün", cid, available, func() -> void: _led_apply_preset(cid, "color", Color(0.2, 1, 0.3), 128))
_add_led_preset_btn(led_presets, "Blau", cid, available, func() -> void: _led_apply_preset(cid, "color", Color(0.2, 0.4, 1), 128))
_add_led_preset_btn(led_presets, "Find", cid, available, func() -> void: _led_apply_preset(cid, "find-me"))
var values_label := Label.new()
values_label.text = ""
values_label.add_theme_font_size_override("font_size", 12)
@@ -229,9 +348,22 @@ func _populate_clients(clients: Array) -> void:
"tap_single": tap_single,
"tap_double": tap_double,
"tap_triple": tap_triple,
"led_color": led_color,
"led_mode": led_mode,
"led_intensity": led_intensity,
"led_progress": led_progress,
"led_digit": led_digit,
"led_blink_ms": led_blink_ms,
"led_blink_count": led_blink_count,
"led_color_row": led_color_row,
"led_intensity_row": led_intensity_row,
"led_progress_row": led_progress_row,
"led_digit_row": led_digit_row,
"led_blink_row": led_blink_row,
"values_label": values_label,
"last_tap": "",
}
_update_led_fields(cid)
empty_hint.visible = _client_ui.is_empty()
start_stream_btn.disabled = _client_ui.is_empty() or not has_available or _streaming
@@ -278,6 +410,8 @@ func _handle_message(text: String) -> void:
_on_accel(data)
"tap":
_on_tap(data)
"led_ring_status":
_log("← LED #%s: %s" % [data.get("client_id", "?"), "OK" if data.get("success", false) else data.get("error", "?")])
_:
if not _streaming or not msg_type.ends_with("_status"):
_log("%s" % _format_response(data))
@@ -361,6 +495,111 @@ func _on_tap(data: Dictionary) -> void:
stream_output.text = "tap\n" + "\n".join(summary)
func _set_spinbox_enabled(spin: SpinBox, enabled: bool) -> void:
spin.editable = enabled
spin.get_line_edit().editable = enabled
func _add_led_preset_btn(
row: HBoxContainer,
text: String,
cid: int,
available: bool,
on_pressed: Callable
) -> void:
var btn := Button.new()
btn.text = text
btn.disabled = not available
btn.pressed.connect(on_pressed)
row.add_child(btn)
func _update_led_fields(cid: int) -> void:
if not _client_ui.has(cid):
return
var ui: Dictionary = _client_ui[cid]
var mode: String = ui["led_mode"].get_item_text(ui["led_mode"].selected)
ui["led_color_row"].visible = mode in LED_MODES_COLOR_INTENSITY
ui["led_intensity_row"].visible = mode in LED_MODES_COLOR_INTENSITY
ui["led_progress_row"].visible = mode == "progress"
ui["led_digit_row"].visible = mode == "digit"
ui["led_blink_row"].visible = mode == "blink"
func _select_led_mode(ui: Dictionary, mode: String) -> void:
for i in ui["led_mode"].item_count:
if ui["led_mode"].get_item_text(i) == mode:
ui["led_mode"].select(i)
return
func _led_apply_preset(
cid: int,
mode: String,
color: Color = Color.WHITE,
intensity: int = 128,
param: int = 50
) -> void:
if not _client_ui.has(cid):
return
var ui: Dictionary = _client_ui[cid]
_select_led_mode(ui, mode)
if mode in LED_MODES_COLOR_INTENSITY:
ui["led_color"].color = color
ui["led_intensity"].value = intensity
if mode == "progress":
ui["led_progress"].value = param
if mode == "digit":
ui["led_digit"].value = param
if mode == "blink":
ui["led_blink_ms"].value = param
ui["led_blink_count"].value = 3
_update_led_fields(cid)
_apply_led_for_client(cid)
func _apply_led_for_client(cid: int) -> void:
if not _client_ui.has(cid):
return
_send(_build_led_payload(cid))
func _build_led_payload(cid: int) -> Dictionary:
var ui: Dictionary = _client_ui[cid]
var mode: String = ui["led_mode"].get_item_text(ui["led_mode"].selected)
var payload := {"type": "set_led_ring", "client_id": cid, "mode": mode}
match mode:
"clear", "find-me":
pass
"color":
_payload_add_color_intensity(payload, ui)
"progress":
payload["progress"] = int(ui["led_progress"].value)
_payload_add_color_intensity(payload, ui)
"digit":
payload["digit"] = int(ui["led_digit"].value)
_payload_add_color_intensity(payload, ui)
"blink":
payload["blink_ms"] = int(ui["led_blink_ms"].value)
payload["blink_count"] = int(ui["led_blink_count"].value)
_payload_add_color_intensity(payload, ui)
return payload
func _payload_add_color_intensity(payload: Dictionary, ui: Dictionary) -> void:
_payload_add_rgb(payload, ui["led_color"].color)
payload["intensity"] = int(ui["led_intensity"].value)
func _payload_add_rgb(payload: Dictionary, color: Color) -> void:
payload["r"] = int(color.r * 255)
payload["g"] = int(color.g * 255)
payload["b"] = int(color.b * 255)
func _make_panel_style(bg: Color, border: Color, border_width: int) -> StyleBoxFlat:
var style := StyleBoxFlat.new()
style.bg_color = bg