From d80b7cecd2b7b5f03ef6efa3ed3a5752b1664499 Mon Sep 17 00:00:00 2001 From: whitehml Date: Wed, 9 Sep 2026 04:25:30 -0400 Subject: [PATCH 1/5] Steer SDL off the macOS GameController framework so XInput pads enumerate --- godot/export_presets.cfg | 7 +++++++ rust/xtask/src/main.rs | 16 ++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/godot/export_presets.cfg b/godot/export_presets.cfg index a23ec9e..fbf3ed8 100644 --- a/godot/export_presets.cfg +++ b/godot/export_presets.cfg @@ -137,3 +137,10 @@ application/version="1.0" ; rather than leaving it to the exporter. codesign/codesign=0 notarization/notarization=0 +application/additional_plist_content="LSEnvironment + + SDL_JOYSTICK_MFI + 0 + SDL_HIDAPI_LIBUSB_WHITELIST + 0 +" diff --git a/rust/xtask/src/main.rs b/rust/xtask/src/main.rs index 986fefa..fdc62c2 100644 --- a/rust/xtask/src/main.rs +++ b/rust/xtask/src/main.rs @@ -249,9 +249,25 @@ fn clean() -> Result { fn godot(extra: &[&str]) -> Command { let mut cmd = Command::new("godot4"); cmd.arg("--path").arg(repo_root().join("godot")).args(extra); + apply_input_hints(&mut cmd); cmd } +#[cfg(target_os = "macos")] +fn apply_input_hints(cmd: &mut Command) { + for (key, value) in [ + ("SDL_JOYSTICK_MFI", "0"), + ("SDL_HIDAPI_LIBUSB_WHITELIST", "0"), + ] { + if std::env::var_os(key).is_none() { + cmd.env(key, value); + } + } +} + +#[cfg(not(target_os = "macos"))] +fn apply_input_hints(_cmd: &mut Command) {} + fn run(mut cmd: Command, label: &str) -> Result { let status = cmd.status().map_err(|e| format!("{label}: {e}"))?; if !status.success() { From d9bfdb08f3e92fb659b87e30398c1f345cd1ceb8 Mon Sep 17 00:00:00 2001 From: whitehml Date: Wed, 9 Sep 2026 04:25:30 -0400 Subject: [PATCH 2/5] Show camera frames unrotated and bump the version to 1.4.2 --- VERSION | 2 +- godot/scripts/pages/drive/drive_page.gd | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/VERSION b/VERSION index 88c5fb8..9df886c 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.4.0 +1.4.2 diff --git a/godot/scripts/pages/drive/drive_page.gd b/godot/scripts/pages/drive/drive_page.gd index 4bf0961..47e3c0e 100644 --- a/godot/scripts/pages/drive/drive_page.gd +++ b/godot/scripts/pages/drive/drive_page.gd @@ -104,7 +104,6 @@ func _apply_source(slot: int, source: int) -> void: var header: String = SOURCE_NAMES[source] if CAM_FEEDS.has(source): - cam.flip_v = source == Source.WEBCAM cam.texture = _cam_textures.get(CAM_FEEDS[source]) if cam.texture == null: header += " — no feed" From f25eb23a6e906ec1b28b31cc9edd32189e11ec36 Mon Sep 17 00:00:00 2001 From: whitehml Date: Mon, 21 Sep 2026 16:09:56 -0400 Subject: [PATCH 3/5] Add BIOBUZZ field --- godot/assets/fields/biobuzz.webp | Bin 0 -> 15476 bytes godot/assets/fields/biobuzz.webp.import | 40 ++++++++++++++++++++++ godot/scripts/pages/field/field_images.gd | 1 + 3 files changed, 41 insertions(+) create mode 100644 godot/assets/fields/biobuzz.webp create mode 100644 godot/assets/fields/biobuzz.webp.import diff --git a/godot/assets/fields/biobuzz.webp b/godot/assets/fields/biobuzz.webp new file mode 100644 index 0000000000000000000000000000000000000000..29b8e3589b5af0beb8a5c8dc96c263f6f25ce4b0 GIT binary patch literal 15476 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 literal 0 HcmV?d00001 diff --git a/godot/assets/fields/biobuzz.webp.import b/godot/assets/fields/biobuzz.webp.import new file mode 100644 index 0000000..05a5da1 --- /dev/null +++ b/godot/assets/fields/biobuzz.webp.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://8fyldmx3i7ir" +path="res://.godot/imported/biobuzz.webp-7e07c77e7e1540149c6b5c1dc566eb8c.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/fields/biobuzz.webp" +dest_files=["res://.godot/imported/biobuzz.webp-7e07c77e7e1540149c6b5c1dc566eb8c.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/godot/scripts/pages/field/field_images.gd b/godot/scripts/pages/field/field_images.gd index e0ee7ec..5108dfb 100644 --- a/godot/scripts/pages/field/field_images.gd +++ b/godot/scripts/pages/field/field_images.gd @@ -1,6 +1,7 @@ class_name FieldImages const FIELDS := [ + {"name": "2026-27 BIOBUZZ", "path": "res://assets/fields/biobuzz.webp"}, {"name": "2025-26 DECODE", "path": "res://assets/fields/decode.webp"}, {"name": "2024-25 INTO THE DEEP", "path": "res://assets/fields/into_the_deep.webp"}, {"name": "Field (no game elements)", "path": "res://assets/fields/base.webp"}, From 12e4953ae0a7e816a73d2f715eb0cd6475a053e7 Mon Sep 17 00:00:00 2001 From: whitehml Date: Mon, 21 Sep 2026 16:39:00 -0400 Subject: [PATCH 4/5] Warn when the RC has another DS and make the link label a button --- external/robocol | 2 +- godot/autoloads/robot_client.gd | 37 ++++++++++++++++++++ godot/scenes/main.tscn | 26 +++++++++++++- godot/scripts/shell/status_bar.gd | 57 +++++++++++++++++++++++++++---- rust/robocol_godot/src/lib.rs | 17 +++++++++ 5 files changed, 130 insertions(+), 9 deletions(-) diff --git a/external/robocol b/external/robocol index 0fe3d2c..3f4fe1c 160000 --- a/external/robocol +++ b/external/robocol @@ -1 +1 @@ -Subproject commit 0fe3d2c911d59f14b3a568e995b9e18a6b0adeaa +Subproject commit 3f4fe1c44a059da9e20686e14e1d83b0fb0264ce diff --git a/godot/autoloads/robot_client.gd b/godot/autoloads/robot_client.gd index 51fdbd0..67ba546 100644 --- a/godot/autoloads/robot_client.gd +++ b/godot/autoloads/robot_client.gd @@ -1,6 +1,8 @@ extends Node signal connection_changed(connected: bool) +signal link_changed(link: int) +signal connection_notice(text: String) signal opmode_list_changed(opmodes: Array) signal phase_changed(phase: int, opmode_name: String) signal selected_opmode_changed(opmode_name: String) @@ -18,8 +20,13 @@ signal video_frame(source: String, texture: Texture2D) signal video_stream_ended(source: String) enum Phase { DISCONNECTED, IDLE, INIT, RUNNING } +enum Link { DISCONNECTED, CONNECTING, CONNECTED } const GAMEPAD_HZ := 50.0 +const BUSY_NOTICE := ( + "The Robot Controller at %s is already connected to another Driver Station. " + + "Disconnect it there, then select DISCONNECTED to try again." +) const LOCATION_RESOURCE := "RESOURCE" const LOCATION_LOCAL := "LOCAL_STORAGE" @@ -32,6 +39,7 @@ const LOCATION_NONE := "NONE" const DEFAULT_LIMELIGHT_STREAM := "http://192.168.43.1:5800/" var phase: int = Phase.DISCONNECTED +var link: int = Link.DISCONNECTED var selected_opmode := "" var opmodes: Array = [] var configurations: Array = [] @@ -58,6 +66,7 @@ func _ready() -> void: _bridge = ClassDB.instantiate("RobocolBridge") add_child(_bridge) _bridge.connection_changed.connect(_on_bridge_connection_changed) + _bridge.peer_busy.connect(_on_bridge_peer_busy) _bridge.opmode_list_changed.connect(_on_bridge_opmode_list) _bridge.phase_changed.connect(_on_bridge_phase_changed) _bridge.telemetry_received.connect(_on_bridge_telemetry) @@ -87,6 +96,7 @@ func _ready() -> void: -1 if bind_env.is_empty() else int(bind_env), -1 if peer_port_env.is_empty() else int(peer_port_env) ) + _set_link(Link.CONNECTING) _setup_video_streams() @@ -113,6 +123,23 @@ func _setup_video_streams() -> void: _bridge.add_video_stream("limelight", limelight) +func set_link_enabled(enabled: bool) -> void: + if enabled: + connection_notice.emit("") + _set_link(Link.CONNECTING) + _bridge.connect_rc() + else: + _set_link(Link.DISCONNECTED) + _bridge.disconnect_rc() + + +func _set_link(new_link: int) -> void: + if new_link == link: + return + link = new_link + link_changed.emit(new_link) + + func select_opmode(opmode_name: String) -> void: if phase == Phase.IDLE: selected_opmode = opmode_name @@ -249,9 +276,19 @@ func _on_slot_changed(new_slot: int) -> void: func _on_bridge_connection_changed(connected: bool) -> void: + if connected: + connection_notice.emit("") + _set_link(Link.CONNECTED) + elif link == Link.CONNECTED: + _set_link(Link.CONNECTING) connection_changed.emit(connected) +func _on_bridge_peer_busy(peer: String) -> void: + _set_link(Link.DISCONNECTED) + connection_notice.emit(BUSY_NOTICE % peer) + + func _on_bridge_opmode_list(list: Array) -> void: opmodes = list opmode_list_changed.emit(list) diff --git a/godot/scenes/main.tscn b/godot/scenes/main.tscn index a431dab..9e7126e 100644 --- a/godot/scenes/main.tscn +++ b/godot/scenes/main.tscn @@ -65,9 +65,10 @@ theme_type_variation = &"StatusBarCard" layout_mode = 2 theme_override_constants/separation = 16 -[node name="ConnectionLabel" type="Label" parent="Layout/StatusBar/StatusRow/StatusCard/StatusGroup"] +[node name="ConnectionButton" type="Button" parent="Layout/StatusBar/StatusRow/StatusCard/StatusGroup"] unique_name_in_owner = true layout_mode = 2 +flat = true text = "DISCONNECTED" [node name="PhaseLabel" type="Label" parent="Layout/StatusBar/StatusRow/StatusCard/StatusGroup"] @@ -191,6 +192,29 @@ layout_mode = 2 size_flags_vertical = 0 text = "Dismiss" +[node name="ConnectionBanner" type="PanelContainer" parent="Layout"] +unique_name_in_owner = true +visible = false +layout_mode = 2 +theme_override_styles/panel = SubResource("StyleBoxFlat_error") + +[node name="ConnectionRow" type="HBoxContainer" parent="Layout/ConnectionBanner"] +layout_mode = 2 +theme_override_constants/separation = 12 + +[node name="ConnectionText" type="Label" parent="Layout/ConnectionBanner/ConnectionRow"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_colors/font_color = Color(1, 0.85, 0.85, 1) +autowrap_mode = 3 + +[node name="ConnectionDismiss" type="Button" parent="Layout/ConnectionBanner/ConnectionRow"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_vertical = 0 +text = "Dismiss" + [node name="Pages" type="Control" parent="Layout"] layout_mode = 2 size_flags_vertical = 3 diff --git a/godot/scripts/shell/status_bar.gd b/godot/scripts/shell/status_bar.gd index 5e011a0..c851cde 100644 --- a/godot/scripts/shell/status_bar.gd +++ b/godot/scripts/shell/status_bar.gd @@ -10,15 +10,32 @@ const PHASE_NAMES := { const MATCH_SECONDS := 120.0 const ENDGAME_SECONDS := 30.0 const SLOT_STATUS := {1: &"slot1", 2: &"slot2"} +const LINK_LABELS := { + RobotClient.Link.DISCONNECTED: ["DISCONNECTED", &"disconnected"], + RobotClient.Link.CONNECTING: ["CONNECTING", &"warn"], + RobotClient.Link.CONNECTED: ["CONNECTED", &"connected"], +} +const LINK_FONT_COLORS := [ + &"font_color", + &"font_hover_color", + &"font_focus_color", + &"font_pressed_color", + &"font_hover_pressed_color", +] var _clock_idle := false -var _connected := false +var _disconnect_dialog: ConfirmationDialog func _ready() -> void: %StopButton.pressed.connect(_on_action_pressed) _size_action_button() - RobotClient.connection_changed.connect(_on_connection_changed) + _build_disconnect_dialog() + %ConnectionButton.pressed.connect(_on_connection_pressed) + RobotClient.link_changed.connect(_on_link_changed) + RobotClient.connection_notice.connect(_on_connection_notice) + %ConnectionDismiss.pressed.connect(_on_connection_notice.bind("")) + _on_link_changed(RobotClient.link) RobotClient.phase_changed.connect(_on_phase_changed) RobotClient.selected_opmode_changed.connect(_on_selected_opmode_changed) RobotClient.battery_voltage_changed.connect(_on_battery_voltage) @@ -45,15 +62,41 @@ func _untint(label: Label) -> void: func refresh_tints() -> void: - _on_connection_changed(_connected) + _on_link_changed(RobotClient.link) _update_slot_badge() _clock_idle = false -func _on_connection_changed(connected: bool) -> void: - _connected = connected - %ConnectionLabel.text = "CONNECTED" if connected else "DISCONNECTED" - _tint(%ConnectionLabel, &"connected" if connected else &"disconnected") +func _build_disconnect_dialog() -> void: + _disconnect_dialog = ConfirmationDialog.new() + _disconnect_dialog.title = "Disconnect" + _disconnect_dialog.dialog_text = "Disconnect from the Robot Controller?" + _disconnect_dialog.ok_button_text = "Disconnect" + _disconnect_dialog.confirmed.connect(RobotClient.set_link_enabled.bind(false)) + add_child(_disconnect_dialog) + DialogCancel.install(_disconnect_dialog) + + +func _on_connection_pressed() -> void: + if RobotClient.link == RobotClient.Link.CONNECTED: + _disconnect_dialog.popup_centered() + else: + RobotClient.set_link_enabled(true) + + +func _on_link_changed(link: int) -> void: + var button: Button = %ConnectionButton + button.text = LINK_LABELS[link][0] + var color := get_theme_color(LINK_LABELS[link][1], ThemeTokens.STATUS_TYPE) + for font_color: StringName in LINK_FONT_COLORS: + button.add_theme_color_override(font_color, color) + if link != RobotClient.Link.CONNECTED: + _disconnect_dialog.hide() + + +func _on_connection_notice(text: String) -> void: + %ConnectionText.text = text + %ConnectionBanner.visible = not text.is_empty() func _on_phase_changed(phase: int, opmode_name: String) -> void: diff --git a/rust/robocol_godot/src/lib.rs b/rust/robocol_godot/src/lib.rs index 8491d90..7d1c20c 100644 --- a/rust/robocol_godot/src/lib.rs +++ b/rust/robocol_godot/src/lib.rs @@ -138,6 +138,8 @@ impl RobocolBridge { #[signal] fn connection_changed(connected: bool); #[signal] + fn peer_busy(peer: GString); + #[signal] fn opmode_list_changed(opmodes: VarArray); #[signal] fn phase_changed(phase: i64, opmode_name: GString); @@ -214,6 +216,16 @@ impl RobocolBridge { } } + #[func] + fn connect_rc(&self) { + self.with_client(RobocolClient::connect); + } + + #[func] + fn disconnect_rc(&self) { + self.with_client(RobocolClient::disconnect); + } + #[func] fn stop_client(&mut self) { self.client = None; @@ -490,6 +502,11 @@ impl RobocolBridge { self.emit_stream_ended("webcam"); self.telemetry_by_tag.clear(); } + Event::PeerBusy { peer } => { + self.signals() + .peer_busy() + .emit(&GString::from(&peer.ip().to_string())); + } Event::OpModeList(list) => { let mut opmodes = VarArray::new(); for meta in list { From 034e7b3870a2eaf1fec72411119b397533290b02 Mon Sep 17 00:00:00 2001 From: whitehml Date: Mon, 21 Sep 2026 17:00:30 -0400 Subject: [PATCH 5/5] Track config selection by location so same-named configs stay separate --- external/robocol | 2 +- godot/scripts/pages/config/config_screen.gd | 32 ++++++++++++--------- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/external/robocol b/external/robocol index 3f4fe1c..5cedf32 160000 --- a/external/robocol +++ b/external/robocol @@ -1 +1 @@ -Subproject commit 3f4fe1c44a059da9e20686e14e1d83b0fb0264ce +Subproject commit 5cedf3290390febb38f094fe4864e2f0f44d88a9 diff --git a/godot/scripts/pages/config/config_screen.gd b/godot/scripts/pages/config/config_screen.gd index 52692a0..ec2f7e1 100644 --- a/godot/scripts/pages/config/config_screen.gd +++ b/godot/scripts/pages/config/config_screen.gd @@ -8,7 +8,7 @@ var _configs: Array = [] var _stale_key := "" var _activated_key := "" var _group := ButtonGroup.new() -var _selected_name := "" +var _selected_key := "" var _pending_metas: Array[Dictionary] = [] var _ambiguity_prompted := false var _ambiguity_candidates: Array[Dictionary] = [] @@ -67,7 +67,7 @@ func _refresh_list() -> void: row.toggle_mode = true row.button_group = _group row.alignment = HORIZONTAL_ALIGNMENT_LEFT - row.button_pressed = meta.get("name") == _selected_name + row.button_pressed = _config_key(meta) == _selected_key row.pressed.connect(_on_config_selected.bind(meta)) %ConfigList.add_child(row) var status := _row_status(meta) @@ -141,7 +141,7 @@ func _row_status(meta: Dictionary) -> StringName: func _on_config_selected(meta: Dictionary) -> void: - _selected_name = meta.get("name", "") + _selected_key = _config_key(meta) _pending_metas.append(meta.duplicate()) RobotClient.request_particular_configuration(meta) @@ -161,7 +161,7 @@ func grip_tap(grip: StringName) -> void: func _selected_index() -> int: for i in _configs.size(): - if _configs[i].get("name") == _selected_name: + if _config_key(_configs[i]) == _selected_key: return i return -1 @@ -174,7 +174,7 @@ func _on_configuration_received(xml: String) -> void: func _on_new() -> void: - _selected_name = "" + _selected_key = "" _editor.load_new( { "isDirty": true, @@ -190,21 +190,21 @@ func _on_new() -> void: func _on_editor_saved(meta: Dictionary, active_out_of_date: bool) -> void: - _selected_name = meta.get("name", "") + _selected_key = _config_key(meta) if active_out_of_date: _stale_key = _config_key(meta) if _activated_key == _stale_key: _activated_key = "" _save_state() _refresh_list() - _poll_until_listed(_selected_name) + _poll_until_listed(_selected_key) -func _poll_until_listed(name: String) -> void: +func _poll_until_listed(key: String) -> void: for _i in LIST_POLL_TRIES: RobotClient.request_configurations() await get_tree().create_timer(LIST_POLL_INTERVAL).timeout - if _configs.any(func(c: Dictionary) -> bool: return c.get("name") == name): + if _listed(key): return @@ -220,18 +220,18 @@ func _on_editor_deleted(meta: Dictionary) -> void: _activated_key = "" _ambiguous_keys.erase(key) _save_state() - if _selected_name == meta.get("name", ""): - _selected_name = "" + if _selected_key == key: + _selected_key = "" _pending_metas.clear() _refresh_list() - _poll_until_unlisted(meta.get("name", "")) + _poll_until_unlisted(key) -func _poll_until_unlisted(name: String) -> void: +func _poll_until_unlisted(key: String) -> void: for _i in LIST_POLL_TRIES: RobotClient.request_configurations() await get_tree().create_timer(LIST_POLL_INTERVAL).timeout - if not _configs.any(func(c: Dictionary) -> bool: return c.get("name") == name): + if not _listed(key): return @@ -326,6 +326,10 @@ func _on_ambiguity_dismissed() -> void: _refresh_list() +func _listed(key: String) -> bool: + return not _listed_config(key).is_empty() + + func _listed_config(key: String) -> Dictionary: for meta: Dictionary in _configs: if _config_key(meta) == key: