Expand Glitch operations hub, cabinet authoring, deployment, and RFID support
Operator hub - Introduce Escape Gallery branding and Glitch: the Dark Machine room views - Add cabinet illustrations, Pac-Man marquee/gameplay artwork, and waiting states - Add device feedback panels with RFID readings, input/output states, and stale status - Add logged operator hardware interventions with request deduplication Room and cabinet authoring - Add a visual custom-PCB connection editor with per-cabinet device inventory - Support device IDs, bindings, wiring notes, input polarity, and validation - Add physical-room label/dimension toggles and editable doorway annotations - Preserve doorway changes through undo, save, and revert - Expand cabinet profiles, room layout, and Glitch scenario resources Scenarios and gameplay - Add per-assignment game configuration and runtime feature validation - Add timed test wins and Frogger individual-home objectives - Add the three-cabinet timed/score/Frogger test scenario - Add Cabinet 1 Pac-Man RFID credits, credit sound, and CREDIT 0/1 display - Require physical Start after an accepted scan and prevent duplicate credits Pi hardware and deployment - Add GPIO feedback sampling and a separate multiplexed RFID helper - Validate reader pin ownership and input/output conflicts - Add SSH cabinet startup and Pi desktop-session service support - Add master packaging, site setup, SSH enrollment, and staged cabinet releases - Verify deployment manifests and support explicit activation/status/log commands Documentation and verification - Consolidate the root README around the current architecture and setup workflow - Add the complete master/Pi/RFID export and deployment walkthrough - Document network isolation, credentials, card identification, and rollback - Add PCB, room-editor, feedback, credit, deployment, and controller tests - Isolate protocol tests from designer-edited cabinet configurations Known limitations - RFID helper compilation and physical Pi commissioning remain unverified - Fleet provisioning does not yet configure RFID-only installations completely - Broader Pac-Man tests retain a ghost-capture assertion failure - Staged exports must be rebuilt and checked before deploying runtime 2.3
This commit is contained in:
1 parent
705ec31e0b
commit
c1b2195834
90 files changed
+4969
-456
No files matched your search
@@ -0,0 +1,209 @@
|
||||
@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()
|
||||
@@ -0,0 +1 @@
|
||||
uid://pqhm21y6t7c6
|
||||
@@ -231,7 +231,12 @@ var simulation_driver := EditorSimulationDriver.new()
|
||||
|
||||
var editor_selection := EscapeRoomEditorSelection.new()
|
||||
|
||||
var doorway_tool: Button
|
||||
var doorway_check: CheckBox
|
||||
var swing_check: CheckBox
|
||||
|
||||
func _ready() -> void:
|
||||
_setup_view_controls()
|
||||
|
||||
select_tool_button.button_pressed = true
|
||||
|
||||
@@ -1503,6 +1508,9 @@ func _on_show_connections_check_box_toggled(
|
||||
)
|
||||
|
||||
func _clear_property_panel() -> void:
|
||||
if doorway_check != null:
|
||||
doorway_check.disabled = true
|
||||
swing_check.disabled = true
|
||||
|
||||
selected_label.text = (
|
||||
"Nothing selected"
|
||||
@@ -1668,6 +1676,7 @@ func _on_wall_creation_requested(
|
||||
wall.start = start
|
||||
wall.end = end
|
||||
wall.thickness = 0.10
|
||||
wall.is_doorway = doorway_tool.button_pressed
|
||||
|
||||
if undo_redo == null:
|
||||
_add_wall(
|
||||
@@ -1828,10 +1837,15 @@ func _refresh_wall_property_panel(
|
||||
return
|
||||
|
||||
selected_label.text = (
|
||||
"Wall: %s"
|
||||
("Doorway: %s" if wall.is_doorway else "Wall: %s")
|
||||
% wall.wall_id
|
||||
)
|
||||
|
||||
doorway_check.disabled = false
|
||||
doorway_check.set_pressed_no_signal(wall.is_doorway)
|
||||
swing_check.set_pressed_no_signal(wall.swing_reversed)
|
||||
swing_check.disabled = not wall.is_doorway
|
||||
|
||||
wall_start_x_spin_box.set_value_no_signal(
|
||||
wall.start.x * METERS_TO_FEET
|
||||
)
|
||||
@@ -3001,3 +3015,68 @@ func _on_room_layout_refresh_requested() -> void:
|
||||
status_label.text = (
|
||||
"Physical room refreshed."
|
||||
)
|
||||
|
||||
|
||||
func _setup_view_controls() -> void:
|
||||
var toolbar := draw_wall_button.get_parent()
|
||||
doorway_tool = Button.new()
|
||||
doorway_tool.text = "DRAW DOORWAY"
|
||||
doorway_tool.toggle_mode = true
|
||||
doorway_tool.button_group = draw_wall_button.button_group
|
||||
doorway_tool.tooltip_text = "Drag from hinge to opposite jamb. Align with a wall to create an opening."
|
||||
toolbar.add_child(doorway_tool)
|
||||
toolbar.move_child(doorway_tool, draw_wall_button.get_index() + 1)
|
||||
doorway_tool.pressed.connect(func(): room_layout.set_edit_mode(RoomLayout.EditMode.DRAW_WALL))
|
||||
var view_bar := HBoxContainer.new()
|
||||
toolbar.get_parent().add_child(view_bar)
|
||||
toolbar.get_parent().move_child(view_bar, toolbar.get_index() + 1)
|
||||
for title in ["Labels", "Dimensions"]:
|
||||
var toggle := CheckBox.new()
|
||||
toggle.text = title
|
||||
toggle.button_pressed = true
|
||||
view_bar.add_child(toggle)
|
||||
if title == "Labels":
|
||||
toggle.toggled.connect(room_layout.set_labels_visible)
|
||||
else:
|
||||
toggle.toggled.connect(room_layout.set_dimensions_visible)
|
||||
doorway_check = CheckBox.new()
|
||||
doorway_check.text = "Doorway opening"
|
||||
doorway_check.disabled = true
|
||||
doorway_check.toggled.connect(func(value: bool): _commit_door_setting("is_doorway", value))
|
||||
swing_check = CheckBox.new()
|
||||
swing_check.text = "Reverse door swing"
|
||||
swing_check.disabled = true
|
||||
swing_check.toggled.connect(func(value: bool): _commit_door_setting("swing_reversed", value))
|
||||
var properties := delete_wall_button.get_parent()
|
||||
properties.add_child(doorway_check)
|
||||
properties.add_child(swing_check)
|
||||
properties.move_child(doorway_check, delete_wall_button.get_index())
|
||||
properties.move_child(swing_check, delete_wall_button.get_index())
|
||||
|
||||
func _commit_door_setting(property: String, value: bool) -> void:
|
||||
if context == null:
|
||||
return
|
||||
var target := context.get_room()
|
||||
var wall := target.get_wall(room_layout.selected_wall_id)
|
||||
if wall == null:
|
||||
doorway_check.set_pressed_no_signal(false)
|
||||
swing_check.set_pressed_no_signal(false)
|
||||
return
|
||||
var old: bool = wall.get(property)
|
||||
if old == value:
|
||||
return
|
||||
if undo_redo == null:
|
||||
_apply_door_setting(target, wall, property, value)
|
||||
return
|
||||
undo_redo.create_action("Edit doorway", UndoRedo.MERGE_DISABLE, target)
|
||||
undo_redo.add_do_method(self, "_apply_door_setting", target, wall, property, value)
|
||||
undo_redo.add_undo_method(self, "_apply_door_setting", target, wall, property, old)
|
||||
undo_redo.commit_action()
|
||||
|
||||
func _apply_door_setting(target: RoomDefinition, wall: RoomWallDefinition, property: String, value: bool) -> void:
|
||||
if not target.walls.has(wall):
|
||||
return
|
||||
wall.set(property, value)
|
||||
context.room_edit_session.mark_dirty()
|
||||
room_layout.queue_redraw()
|
||||
_refresh_wall_property_panel(wall.wall_id)
|
||||
Reference in new issue
Block a user