@tool extends EditorProperty var open_button: Button var dialog: ConfirmationDialog var draft: CabinetPCBConfig var chosen := "J1" var buttons: Dictionary = {} var type_select: OptionButton var input_pull: CheckBox var device_name: LineEdit var device_id: LineEdit var binding: LineEdit var notes: TextEdit var pin_label: Label var warning_label: Label var summary: Label var loading := false func _init() -> void: open_button = Button.new() open_button.text = "Open PCB connections..." open_button.pressed.connect(_open) add_child(open_button) add_focusable(open_button) func _update_property() -> void: var config: CabinetPCBConfig = get_edited_object().get(get_edited_property()) open_button.text = "PCB connections (%d)..." % config.connections.size() if config else "Set up PCB connections..." func _open() -> void: var source: CabinetPCBConfig = get_edited_object().get(get_edited_property()) draft = source.duplicate(true) if source else CabinetPCBConfig.new() # Rebuild static labels after editor script reloads as well as dynamic fields. if is_instance_valid(dialog): dialog.hide() dialog.queue_free() buttons.clear() _build_dialog() dialog.title = "%s - Custom GPIO routing PCB" % get_edited_object().display_name _select(chosen) dialog.popup_centered_clamped(Vector2i(1120, 760), 0.9) func _label(text: String, parent: Node) -> Label: var label := Label.new() label.text = text label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART parent.add_child(label) return label func _build_dialog() -> void: dialog = ConfirmationDialog.new() dialog.ok_button_text = "Apply connections" dialog.min_size = Vector2i(700, 550) dialog.confirmed.connect(_apply) add_child(dialog) var scroll := ScrollContainer.new() dialog.add_child(scroll) var content := VBoxContainer.new() content.size_flags_horizontal = Control.SIZE_EXPAND_FILL scroll.add_child(content) _label("Select a socket, then describe the device. Apply supports Undo/Redo; save the cabinet resource to keep changes.", content) var panel := PanelContainer.new() var style := StyleBoxFlat.new() style.bg_color = Color("164d3a") style.content_margin_left = 16 style.content_margin_right = 16 style.content_margin_top = 16 style.content_margin_bottom = 16 panel.add_theme_stylebox_override("panel", style) content.add_child(panel) var board := VBoxContainer.new() board.add_theme_constant_override("separation", 18) panel.add_child(board) var header := Button.new() header.text = "J9 | Raspberry Pi GPIO ribbon | verify pin 1 orientation" header.disabled = true board.add_child(header) var middle := HBoxContainer.new() board.add_child(middle) var group_index := 0 for group in [["J15", "J14", "J13", "J12", "J11", "J10"], ["J22", "J21", "J20", "J19", "J18", "J17"], ["J16"]]: var column := VBoxContainer.new() column.size_flags_horizontal = Control.SIZE_EXPAND_FILL middle.add_child(column) _label(["SIGNAL + 3V3", "SIGNAL / GND", "I2C"][group_index], column) group_index += 1 var grid := GridContainer.new() grid.columns = group.size() grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL column.add_child(grid) for id in group: _port_button(id, grid) _label("READER CONNECTORS | selector BCM22 / BCM27 / BCM17 | shared reset BCM20", board) var readers := GridContainer.new() readers.columns = 8 board.add_child(readers) for index in range(1, 9): _port_button("J%d" % index, readers) summary = _label("", content) var details := HBoxContainer.new() details.add_theme_constant_override("separation", 24) content.add_child(details) var left := VBoxContainer.new() left.size_flags_horizontal = Control.SIZE_EXPAND_FILL details.add_child(left) pin_label = _label("", left) _label("GPIO is a control signal. Motors, relays and lighting need an appropriate driver and supply. This editor does not write GPIO or install reader drivers.", left) var form := VBoxContainer.new() form.size_flags_horizontal = Control.SIZE_EXPAND_FILL details.add_child(form) _label("Connected device", form) type_select = OptionButton.new() type_select.item_selected.connect(_type_changed) form.add_child(type_select) input_pull = CheckBox.new() input_pull.text = "Input pull-up (active when connected to GND)" input_pull.toggled.connect(func(_value: bool): _store()) form.add_child(input_pull) device_name = _field("Device name", form) device_id = _field("Unique device ID (within this cabinet)", form) binding = _field("Hardware action ID (outputs) / event ID (inputs)", form) _label("Wiring notes / driver / supply / I2C address", form) notes = TextEdit.new() notes.custom_minimum_size.y = 80 notes.text_changed.connect(_store) form.add_child(notes) warning_label = _label("", content) warning_label.modulate = Color("ffd07a") func _field(title: String, parent: Node) -> LineEdit: _label(title, parent) var field := LineEdit.new() field.text_changed.connect(func(_text: String): _store()) parent.add_child(field) return field func _port_button(id: String, parent: Node) -> void: var button := Button.new() button.size_flags_horizontal = Control.SIZE_EXPAND_FILL button.custom_minimum_size = Vector2(64, 82) button.toggle_mode = true button.pressed.connect(_select.bind(id)) parent.add_child(button) buttons[id] = button func _select(id: String) -> void: chosen = id loading = true var definition := CabinetPCBConfig.port(id) var item := draft.connection(id) pin_label.text = "%s | %s\n\n%s" % [id, definition.label, definition.pins] type_select.clear() type_select.add_item("Empty / unplugged") type_select.set_item_metadata(0, "") for kind in definition.types: type_select.add_item(CabinetPCBConfig.TYPES[kind]) type_select.set_item_metadata(type_select.item_count - 1, kind) type_select.select(0) if item: for index in range(type_select.item_count): if type_select.get_item_metadata(index) == item.device_type: type_select.select(index) device_name.text = item.display_name if item else "" device_id.text = item.device_id if item else "" binding.text = item.binding_id if item else "" notes.text = item.notes if item else "" input_pull.set_pressed_no_signal(item.input_pull_up if item else true) loading = false _refresh_board() func _type_changed(_index: int) -> void: _store() func _store() -> void: if loading: return var item := PCBConnection.new() item.port_id = chosen item.device_type = str(type_select.get_selected_metadata()) item.display_name = device_name.text.strip_edges() item.device_id = device_id.text.strip_edges() item.binding_id = binding.text.strip_edges() item.notes = notes.text item.input_pull_up = input_pull.button_pressed draft.set_connection(item) _refresh_board() func _refresh_board() -> void: for id in buttons: var definition := CabinetPCBConfig.port(id) var item := draft.connection(id) var button: Button = buttons[id] button.text = "%s\n%s\n%s" % [id, definition.label, item.device_type.to_upper() if item else "Empty"] button.set_pressed_no_signal(id == chosen) button.tooltip_text = "%s\n%s" % [definition.pins, item.display_name if item else "Unassigned"] var disabled := str(type_select.get_selected_metadata()).is_empty() device_name.editable = not disabled device_id.editable = not disabled binding.editable = not disabled notes.editable = not disabled input_pull.visible = str(type_select.get_selected_metadata()) in ["button", "sensor"] summary.text = "%d connections configured | %s selected" % [draft.connections.size(), chosen] var issues := draft.issues(get_edited_object().hardware_config) warning_label.text = "Review wiring:\n- " + "\n- ".join(issues) if not issues.is_empty() else "Inventory is consistent. Runtime wiring and drivers are configured separately." func _apply() -> void: emit_changed(get_edited_property(), draft.duplicate(true)) _update_property()