gini-toolkit 6.0.1.dev0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gini/__init__.py +12 -0
- gini/__main__.py +107 -0
- gini/_version.py +24 -0
- gini/agent/__init__.py +17 -0
- gini/agent/agent_gamemaster.py +140 -0
- gini/agent/api.py +291 -0
- gini/agent/ask.py +123 -0
- gini/agent/authoring.py +72 -0
- gini/agent/blackboard.py +114 -0
- gini/agent/contracts.py +142 -0
- gini/agent/domains.py +91 -0
- gini/agent/embed.py +123 -0
- gini/agent/gamemaster.py +256 -0
- gini/agent/kb.py +148 -0
- gini/agent/lesson_resolver.py +261 -0
- gini/agent/llm/__init__.py +5 -0
- gini/agent/llm/backend.py +43 -0
- gini/agent/llm/fake.py +25 -0
- gini/agent/llm/ollama.py +206 -0
- gini/agent/loop.py +258 -0
- gini/agent/mcp_server.py +86 -0
- gini/agent/meaning.py +225 -0
- gini/agent/mission.py +210 -0
- gini/agent/mission_controller.py +208 -0
- gini/agent/narration.py +116 -0
- gini/agent/notifier.py +86 -0
- gini/agent/personas.py +79 -0
- gini/agent/reasoning.py +172 -0
- gini/agent/recall.py +248 -0
- gini/agent/session.py +79 -0
- gini/agent/teaching_center.py +482 -0
- gini/agent/tools/__init__.py +3 -0
- gini/agent/tools/registry.py +193 -0
- gini/agent/twin/__init__.py +28 -0
- gini/agent/twin/authoring.py +71 -0
- gini/agent/twin/contracts.py +54 -0
- gini/agent/twin/dialectic.py +189 -0
- gini/agent/twin/harness.py +93 -0
- gini/agent/twin/justify.py +156 -0
- gini/agent/twin/learner.py +64 -0
- gini/agent/twin/mission.py +60 -0
- gini/agent/twin/os_coach.py +79 -0
- gini/agent/twin/salience.py +30 -0
- gini/agent/understand.py +250 -0
- gini/agent/verifiers.py +106 -0
- gini/agent/wizard.py +178 -0
- gini/agent/xv6_pack.py +74 -0
- gini/app/__init__.py +3 -0
- gini/app/context.py +368 -0
- gini/app/paths.py +121 -0
- gini/data/README.md +21 -0
- gini/domain/__init__.py +9 -0
- gini/domain/assembly.py +209 -0
- gini/domain/authoring.py +353 -0
- gini/domain/blueprints.py +5 -0
- gini/domain/capabilities.py +177 -0
- gini/domain/catalog.py +85 -0
- gini/domain/certify.py +201 -0
- gini/domain/compose.py +413 -0
- gini/domain/composition.py +88 -0
- gini/domain/concepts.py +383 -0
- gini/domain/connection_rules.py +269 -0
- gini/domain/constraints.py +153 -0
- gini/domain/content.py +59 -0
- gini/domain/cpu_journey.py +89 -0
- gini/domain/devices.py +747 -0
- gini/domain/diagnose.py +201 -0
- gini/domain/element_guide.py +327 -0
- gini/domain/explain.py +90 -0
- gini/domain/fingerprint.py +201 -0
- gini/domain/firewall.py +34 -0
- gini/domain/flowlog.py +61 -0
- gini/domain/flowtable.py +179 -0
- gini/domain/fragment_yaml.py +230 -0
- gini/domain/fragments.py +169 -0
- gini/domain/games/__init__.py +2 -0
- gini/domain/games/paging_games.py +119 -0
- gini/domain/games/policy_game.py +86 -0
- gini/domain/games/process_game.py +48 -0
- gini/domain/games/thrash_game.py +75 -0
- gini/domain/games/translate_game.py +60 -0
- gini/domain/games/trap_game.py +86 -0
- gini/domain/grader.py +155 -0
- gini/domain/grouping.py +67 -0
- gini/domain/legality.py +103 -0
- gini/domain/lesson.py +241 -0
- gini/domain/lexicon.py +150 -0
- gini/domain/machine_state.py +410 -0
- gini/domain/missions/networking/basic-lan.yaml +32 -0
- gini/domain/missions/networking/cache-in-front.yaml +23 -0
- gini/domain/missions/networking/decouple-with-queue.yaml +31 -0
- gini/domain/missions/networking/drive-load.yaml +20 -0
- gini/domain/missions/networking/fix-the-address.yaml +75 -0
- gini/domain/missions/networking/fix-the-lan.yaml +43 -0
- gini/domain/missions/networking/inspect-flows.yaml +16 -0
- gini/domain/missions/networking/k8s-autoscale.yaml +27 -0
- gini/domain/missions/networking/least-privilege.yaml +21 -0
- gini/domain/missions/networking/load-balanced-web.yaml +29 -0
- gini/domain/missions/networking/observe-it.yaml +24 -0
- gini/domain/missions/networking/put-in-vpc.yaml +30 -0
- gini/domain/missions/networking/reachability-boundary.yaml +56 -0
- gini/domain/missions/networking/sdn-reactive.yaml +35 -0
- gini/domain/missions/networking/send-request.yaml +19 -0
- gini/domain/missions/networking/serverless-api.yaml +25 -0
- gini/domain/missions/networking/service-chain.yaml +33 -0
- gini/domain/missions/os/lottery-fix.yaml +19 -0
- gini/domain/missions/os/priority-fix.yaml +24 -0
- gini/domain/missions.py +111 -0
- gini/domain/modulechain.py +36 -0
- gini/domain/objectives.py +488 -0
- gini/domain/os_zoo.py +79 -0
- gini/domain/paging_sim.py +141 -0
- gini/domain/pricing.py +199 -0
- gini/domain/probes.py +226 -0
- gini/domain/profile.py +142 -0
- gini/domain/recipes.py +738 -0
- gini/domain/riders.py +309 -0
- gini/domain/router_modules.py +224 -0
- gini/domain/routetable.py +67 -0
- gini/domain/scoring.py +76 -0
- gini/domain/staging.py +122 -0
- gini/domain/syscall_builder.py +144 -0
- gini/domain/topic_cloud.py +62 -0
- gini/domain/topology.py +213 -0
- gini/domain/vocabulary.py +51 -0
- gini/domain/xv6.py +808 -0
- gini/domain/xv6_fs.py +250 -0
- gini/domain/xv6_runner.py +113 -0
- gini/domain/xv6_vm.py +385 -0
- gini/gloader.py +17 -0
- gini/runtime/__init__.py +18 -0
- gini/runtime/cloudfabric_agent.py +370 -0
- gini/runtime/console.py +68 -0
- gini/runtime/control.py +70 -0
- gini/runtime/frame.py +138 -0
- gini/runtime/gbridge.py +638 -0
- gini/runtime/grouter.py +223 -0
- gini/runtime/hostsim.py +90 -0
- gini/runtime/shuttle.py +348 -0
- gini/runtime/switch.py +109 -0
- gini/runtime/transport.py +77 -0
- gini/runtime/xv6_bridge.py +312 -0
- gini/server/__init__.py +22 -0
- gini/server/__main__.py +74 -0
- gini/server/app.py +140 -0
- gini/server/auth.py +82 -0
- gini/server/policy.py +57 -0
- gini/server/session.py +23 -0
- gini/services/__init__.py +15 -0
- gini/services/boardflash.py +248 -0
- gini/services/boardsetup.py +374 -0
- gini/services/cloud_catalog.py +143 -0
- gini/services/compiler.py +1858 -0
- gini/services/discovery.py +324 -0
- gini/services/gloader.py +183 -0
- gini/services/orchestrator.py +1460 -0
- gini/services/persistence.py +28 -0
- gini/services/probe_runner.py +149 -0
- gini/services/project.py +217 -0
- gini/services/remote.py +93 -0
- gini/services/rider_runner.py +96 -0
- gini/services/rider_session.py +171 -0
- gini/services/shadow_store.py +52 -0
- gini/services/terminal.py +45 -0
- gini/setup/__init__.py +17 -0
- gini/setup/cli.py +109 -0
- gini/setup/images.py +33 -0
- gini/setup/marker.py +43 -0
- gini/setup/runtime.py +69 -0
- gini/ui/__init__.py +3 -0
- gini/ui/assets/app_icon.icns +0 -0
- gini/ui/assets/app_icon.ico +0 -0
- gini/ui/assets/app_icon.png +0 -0
- gini/ui/assets/app_icon_1024.png +0 -0
- gini/ui/assets/cue/_w.txt +1 -0
- gini/ui/assets/cue/ai.png +0 -0
- gini/ui/assets/cue/canvas.png +0 -0
- gini/ui/assets/cue/cloud.png +0 -0
- gini/ui/assets/cue/cost.png +0 -0
- gini/ui/assets/cue/dark/ai.png +0 -0
- gini/ui/assets/cue/dark/canvas.png +0 -0
- gini/ui/assets/cue/dark/cloud.png +0 -0
- gini/ui/assets/cue/dark/cost.png +0 -0
- gini/ui/assets/cue/dark/metrics.png +0 -0
- gini/ui/assets/cue/dark/router.png +0 -0
- gini/ui/assets/cue/dark/run.png +0 -0
- gini/ui/assets/cue/dark/serverless.png +0 -0
- gini/ui/assets/cue/dark/settings.png +0 -0
- gini/ui/assets/cue/dark/welcome.png +0 -0
- gini/ui/assets/cue/dark/wizard.png +0 -0
- gini/ui/assets/cue/ginibrand/ai.png +0 -0
- gini/ui/assets/cue/ginibrand/canvas.png +0 -0
- gini/ui/assets/cue/ginibrand/cloud.png +0 -0
- gini/ui/assets/cue/ginibrand/cost.png +0 -0
- gini/ui/assets/cue/ginibrand/metrics.png +0 -0
- gini/ui/assets/cue/ginibrand/router.png +0 -0
- gini/ui/assets/cue/ginibrand/run.png +0 -0
- gini/ui/assets/cue/ginibrand/serverless.png +0 -0
- gini/ui/assets/cue/ginibrand/settings.png +0 -0
- gini/ui/assets/cue/ginibrand/welcome.png +0 -0
- gini/ui/assets/cue/ginibrand/wizard.png +0 -0
- gini/ui/assets/cue/highcontrast/ai.png +0 -0
- gini/ui/assets/cue/highcontrast/canvas.png +0 -0
- gini/ui/assets/cue/highcontrast/cloud.png +0 -0
- gini/ui/assets/cue/highcontrast/cost.png +0 -0
- gini/ui/assets/cue/highcontrast/metrics.png +0 -0
- gini/ui/assets/cue/highcontrast/router.png +0 -0
- gini/ui/assets/cue/highcontrast/run.png +0 -0
- gini/ui/assets/cue/highcontrast/serverless.png +0 -0
- gini/ui/assets/cue/highcontrast/settings.png +0 -0
- gini/ui/assets/cue/highcontrast/welcome.png +0 -0
- gini/ui/assets/cue/highcontrast/wizard.png +0 -0
- gini/ui/assets/cue/light/ai.png +0 -0
- gini/ui/assets/cue/light/canvas.png +0 -0
- gini/ui/assets/cue/light/cloud.png +0 -0
- gini/ui/assets/cue/light/cost.png +0 -0
- gini/ui/assets/cue/light/metrics.png +0 -0
- gini/ui/assets/cue/light/router.png +0 -0
- gini/ui/assets/cue/light/run.png +0 -0
- gini/ui/assets/cue/light/serverless.png +0 -0
- gini/ui/assets/cue/light/settings.png +0 -0
- gini/ui/assets/cue/light/welcome.png +0 -0
- gini/ui/assets/cue/light/wizard.png +0 -0
- gini/ui/assets/cue/metrics.png +0 -0
- gini/ui/assets/cue/router.png +0 -0
- gini/ui/assets/cue/run.png +0 -0
- gini/ui/assets/cue/serverless.png +0 -0
- gini/ui/assets/cue/settings.png +0 -0
- gini/ui/assets/cue/welcome.png +0 -0
- gini/ui/assets/cue/wizard.png +0 -0
- gini/ui/assistant.py +2111 -0
- gini/ui/author_dialog.py +184 -0
- gini/ui/board_dialog.py +247 -0
- gini/ui/branding.py +21 -0
- gini/ui/canvas.py +2007 -0
- gini/ui/chat_panel.py +7 -0
- gini/ui/cpu_journey.py +212 -0
- gini/ui/cpu_lab.py +306 -0
- gini/ui/cue_cards.py +214 -0
- gini/ui/dashboard.py +222 -0
- gini/ui/diagnose_game.py +336 -0
- gini/ui/fingerprint_lab.py +219 -0
- gini/ui/flash_dialog.py +244 -0
- gini/ui/flow_layout.py +63 -0
- gini/ui/fragment_manager.py +1415 -0
- gini/ui/game_catalog.py +184 -0
- gini/ui/game_renderers.py +340 -0
- gini/ui/games_lab.py +90 -0
- gini/ui/inspector.py +1055 -0
- gini/ui/live_metrics.py +130 -0
- gini/ui/machine_lab.py +1412 -0
- gini/ui/main_window.py +3153 -0
- gini/ui/memory_lab.py +371 -0
- gini/ui/mission_panel.py +302 -0
- gini/ui/mode_indicator.py +227 -0
- gini/ui/palette.py +112 -0
- gini/ui/peripherals.py +218 -0
- gini/ui/process_tree.py +130 -0
- gini/ui/reset_dialog.py +179 -0
- gini/ui/router_lab.py +776 -0
- gini/ui/run_button.py +183 -0
- gini/ui/settings_dialog.py +234 -0
- gini/ui/signin_dialog.py +111 -0
- gini/ui/storage_lab.py +219 -0
- gini/ui/syscall_builder.py +235 -0
- gini/ui/syscall_lab.py +152 -0
- gini/ui/theme/__init__.py +5 -0
- gini/ui/theme/icons.py +145 -0
- gini/ui/theme/manager.py +291 -0
- gini/ui/theme/tokens.py +194 -0
- gini/ui/trap_lab.py +270 -0
- gini/ui/worker_host.py +102 -0
- gini/ui/zoo_lab.py +112 -0
- gini_toolkit-6.0.1.dev0.dist-info/METADATA +77 -0
- gini_toolkit-6.0.1.dev0.dist-info/RECORD +278 -0
- gini_toolkit-6.0.1.dev0.dist-info/WHEEL +5 -0
- gini_toolkit-6.0.1.dev0.dist-info/entry_points.txt +3 -0
- gini_toolkit-6.0.1.dev0.dist-info/top_level.txt +1 -0
gini/ui/process_tree.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Live process tree for the Machine Lab — the process hierarchy as a tree, not a flat table.
|
|
2
|
+
|
|
3
|
+
Built from each process's parent link (ppid, from gini_dump). fork() adds a child, exec() relabels,
|
|
4
|
+
exit() shows a zombie, wait() reaps it, and an exiting parent's children re-parent to init — all
|
|
5
|
+
visible as the tree changes. State is colour-coded; the running process(es) are highlighted; user
|
|
6
|
+
processes get a kill affordance. Driven entirely by the live serial dump (no gdb).
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from PySide6.QtCore import Qt, Signal
|
|
11
|
+
from PySide6.QtGui import QColor, QFont
|
|
12
|
+
from PySide6.QtWidgets import QHeaderView, QPushButton, QTreeWidget, QTreeWidgetItem
|
|
13
|
+
|
|
14
|
+
from ..domain.xv6 import build_process_tree
|
|
15
|
+
from .theme import ThemeManager
|
|
16
|
+
from .theme.manager import scale_css as _scss
|
|
17
|
+
|
|
18
|
+
_STATE_ACCENT = {"running": "green", "runnable": "amber", "sleeping": "slate",
|
|
19
|
+
"zombie": "red", "used": "slate", "unused": "slate"}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ProcessTree(QTreeWidget):
|
|
23
|
+
"""A QTreeWidget rendering of the process hierarchy. `kill_requested(pid)` fires when the
|
|
24
|
+
user clicks a process's kill affordance (never shown for init/sh)."""
|
|
25
|
+
|
|
26
|
+
kill_requested = Signal(int)
|
|
27
|
+
|
|
28
|
+
def __init__(self, theme: ThemeManager) -> None:
|
|
29
|
+
super().__init__()
|
|
30
|
+
self.theme = theme
|
|
31
|
+
self._live = False
|
|
32
|
+
self._killing: set = set() # pids with a kill in flight -> show a pending state
|
|
33
|
+
self.setColumnCount(4)
|
|
34
|
+
self.setHeaderLabels(["process", "pid", "state", ""])
|
|
35
|
+
self.setRootIsDecorated(True)
|
|
36
|
+
self.setIndentation(16)
|
|
37
|
+
self.setSelectionMode(QTreeWidget.NoSelection)
|
|
38
|
+
self.setFocusPolicy(Qt.NoFocus)
|
|
39
|
+
self.setColumnWidth(1, 48)
|
|
40
|
+
self.setColumnWidth(3, 84)
|
|
41
|
+
self.header().setSectionResizeMode(0, QHeaderView.Stretch)
|
|
42
|
+
t = theme.theme
|
|
43
|
+
self.setStyleSheet(
|
|
44
|
+
f"QTreeWidget{{background:{t.panel};color:{t.text};border:none;font-size:12px;"
|
|
45
|
+
f"outline:none;}}"
|
|
46
|
+
f"QHeaderView::section{{background:{t.panel2};color:{t.muted};border:none;"
|
|
47
|
+
"padding:4px;}"
|
|
48
|
+
f"QTreeWidget::item{{padding:2px 0;}}")
|
|
49
|
+
|
|
50
|
+
def set_live(self, live: bool) -> None:
|
|
51
|
+
self._live = live
|
|
52
|
+
|
|
53
|
+
def set_procs(self, procs, running_pids=(), flags=None) -> None:
|
|
54
|
+
"""Rebuild the tree from the current process list. `running_pids` are highlighted; `flags`
|
|
55
|
+
maps pid -> a short reason string (e.g. 'starving') that badges the process."""
|
|
56
|
+
t = self.theme.theme
|
|
57
|
+
running = set(running_pids or [])
|
|
58
|
+
flags = flags or {}
|
|
59
|
+
self._killing &= {p.pid for p in procs} # drop pending marks for procs that are now gone
|
|
60
|
+
self.clear()
|
|
61
|
+
items: dict = {} # pid -> QTreeWidgetItem
|
|
62
|
+
for node in build_process_tree(procs):
|
|
63
|
+
p = node.proc
|
|
64
|
+
parent_item = items.get(p.parent)
|
|
65
|
+
it = QTreeWidgetItem(parent_item if parent_item is not None else self)
|
|
66
|
+
it.setText(0, p.name)
|
|
67
|
+
it.setText(1, str(p.pid))
|
|
68
|
+
it.setText(2, p.state)
|
|
69
|
+
color = QColor(t.accent_for(_STATE_ACCENT.get(p.state, "slate")))
|
|
70
|
+
it.setForeground(2, color)
|
|
71
|
+
if p.pid in running:
|
|
72
|
+
it.setForeground(0, color)
|
|
73
|
+
f = it.font(0); f.setBold(True); it.setFont(0, f)
|
|
74
|
+
elif p.state == "zombie":
|
|
75
|
+
for c in (0, 1, 2):
|
|
76
|
+
it.setForeground(c, QColor(t.faint))
|
|
77
|
+
if p.pid in flags: # scheduling badge (e.g. starvation)
|
|
78
|
+
reason = flags[p.pid]
|
|
79
|
+
it.setText(2, f"{p.state} ⚠ {reason}")
|
|
80
|
+
it.setForeground(2, QColor(t.accent_for("red")))
|
|
81
|
+
it.setToolTip(2, reason)
|
|
82
|
+
items[p.pid] = it
|
|
83
|
+
if self._live and p.pid > 2: # kill affordance (never init/sh)
|
|
84
|
+
self.setItemWidget(it, 3, self._kill_button(p.pid))
|
|
85
|
+
self.expandAll()
|
|
86
|
+
|
|
87
|
+
def _kill_button(self, pid: int) -> QPushButton:
|
|
88
|
+
"""A proper labelled Kill button (the old bare ✕ was easy to miss), with a persistent
|
|
89
|
+
'killing…' pending state so the click visibly registers even while the kill lands."""
|
|
90
|
+
t = self.theme.theme
|
|
91
|
+
pending = pid in self._killing
|
|
92
|
+
red = t.accent_for("red")
|
|
93
|
+
b = QPushButton("killing…" if pending else "Kill")
|
|
94
|
+
b.setFixedHeight(20)
|
|
95
|
+
b.setToolTip(f"kill pid {pid}")
|
|
96
|
+
b.setCursor(Qt.PointingHandCursor)
|
|
97
|
+
if pending:
|
|
98
|
+
b.setEnabled(False)
|
|
99
|
+
b.setStyleSheet(_scss(
|
|
100
|
+
f"QPushButton{{color:{t.faint};background:transparent;border:1px solid {t.line};"
|
|
101
|
+
"border-radius:5px;padding:0 8px;font-size:11px;}"))
|
|
102
|
+
else:
|
|
103
|
+
b.setStyleSheet(_scss(
|
|
104
|
+
f"QPushButton{{color:{red};background:transparent;border:1px solid {red};"
|
|
105
|
+
"border-radius:5px;padding:0 8px;font-size:11px;font-weight:600;}"
|
|
106
|
+
f"QPushButton:hover{{color:#ffffff;background:{red};}}"))
|
|
107
|
+
b.clicked.connect(lambda _c=False, p=pid: self._on_kill_clicked(p))
|
|
108
|
+
return b
|
|
109
|
+
|
|
110
|
+
def _on_kill_clicked(self, pid: int) -> None:
|
|
111
|
+
self._killing.add(pid) # mark pending; survives the ~0.5s tree rebuilds
|
|
112
|
+
it = self._find_item(pid)
|
|
113
|
+
if it is not None: # flip THIS button to the pending state immediately
|
|
114
|
+
self.setItemWidget(it, 3, self._kill_button(pid))
|
|
115
|
+
self.kill_requested.emit(pid)
|
|
116
|
+
|
|
117
|
+
def _find_item(self, pid: int):
|
|
118
|
+
def walk(it):
|
|
119
|
+
if it.text(1) == str(pid):
|
|
120
|
+
return it
|
|
121
|
+
for i in range(it.childCount()):
|
|
122
|
+
r = walk(it.child(i))
|
|
123
|
+
if r is not None:
|
|
124
|
+
return r
|
|
125
|
+
return None
|
|
126
|
+
for i in range(self.topLevelItemCount()):
|
|
127
|
+
r = walk(self.topLevelItem(i))
|
|
128
|
+
if r is not None:
|
|
129
|
+
return r
|
|
130
|
+
return None
|
gini/ui/reset_dialog.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""Hardware → Reset a Board: release a board's pairing, over USB.
|
|
2
|
+
|
|
3
|
+
A claimed board answers only the laptop that owns it and is invisible to every other
|
|
4
|
+
gBuilder. That is what stops thirty laptops in a room from stealing each other's
|
|
5
|
+
hardware — but it means a board claimed by the wrong laptop looks, from the student's
|
|
6
|
+
side, exactly like a board that is simply broken: it powers up, joins the Wi-Fi, and
|
|
7
|
+
never appears on anyone's canvas.
|
|
8
|
+
|
|
9
|
+
There is deliberately no timed auto-release: a board must never re-open itself while its
|
|
10
|
+
owner is at lunch. Physical possession is the authority instead — and USB *is* what
|
|
11
|
+
physical possession means in software. Hence this dialog: whoever is holding the board
|
|
12
|
+
can always take it back, and nobody else can.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from PySide6.QtCore import QObject, Qt, Signal
|
|
17
|
+
from PySide6.QtWidgets import (
|
|
18
|
+
QComboBox, QDialog, QDialogButtonBox, QFormLayout, QHBoxLayout, QLabel,
|
|
19
|
+
QMessageBox, QPushButton, QVBoxLayout,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
from ..services import boardsetup as bs
|
|
23
|
+
from .worker_host import WorkerHost
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _note(text: str) -> QLabel:
|
|
27
|
+
lab = QLabel(text)
|
|
28
|
+
lab.setWordWrap(True)
|
|
29
|
+
lab.setStyleSheet("color: palette(mid);")
|
|
30
|
+
return lab
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class _Scan(QObject):
|
|
34
|
+
done = Signal(list, list)
|
|
35
|
+
failed = Signal(str)
|
|
36
|
+
|
|
37
|
+
def run(self) -> None:
|
|
38
|
+
try:
|
|
39
|
+
boards, others = bs.detect_boards()
|
|
40
|
+
self.done.emit(boards, others)
|
|
41
|
+
except Exception as e:
|
|
42
|
+
self.failed.emit(str(e))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class _Unpair(QObject):
|
|
46
|
+
done = Signal(bool, str)
|
|
47
|
+
|
|
48
|
+
def __init__(self, port: str) -> None:
|
|
49
|
+
super().__init__()
|
|
50
|
+
self._port = port
|
|
51
|
+
|
|
52
|
+
def run(self) -> None:
|
|
53
|
+
try:
|
|
54
|
+
with bs.BoardConsole(self._port) as con:
|
|
55
|
+
if not con.wait_for_prompt():
|
|
56
|
+
self.done.emit(False, "the board never reached its prompt — unplug "
|
|
57
|
+
"it, plug it back in, and try again")
|
|
58
|
+
return
|
|
59
|
+
ok, msg = con.unpair()
|
|
60
|
+
self.done.emit(ok, msg)
|
|
61
|
+
except Exception as e:
|
|
62
|
+
self.done.emit(False, str(e))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class ResetBoardDialog(WorkerHost, QDialog):
|
|
66
|
+
"""Pick a plugged-in board and release whoever owns it."""
|
|
67
|
+
|
|
68
|
+
def __init__(self, parent=None) -> None:
|
|
69
|
+
super().__init__(parent)
|
|
70
|
+
self.setWindowTitle("Reset a Board")
|
|
71
|
+
self.setMinimumWidth(560)
|
|
72
|
+
self._alive = True
|
|
73
|
+
self._thread = None
|
|
74
|
+
self._worker = None
|
|
75
|
+
self._connections = []
|
|
76
|
+
|
|
77
|
+
root = QVBoxLayout(self)
|
|
78
|
+
root.addWidget(_note(
|
|
79
|
+
"Releases a board's pairing, so any gBuilder can claim it again.<br><br>"
|
|
80
|
+
"Use this when a board is online and healthy but never appears on the "
|
|
81
|
+
"canvas — that usually means another laptop claimed it. Settings, id and "
|
|
82
|
+
"Wi-Fi are kept; only the pairing is cleared."))
|
|
83
|
+
|
|
84
|
+
form = QFormLayout()
|
|
85
|
+
self.board = QComboBox()
|
|
86
|
+
row = QHBoxLayout()
|
|
87
|
+
row.addWidget(self.board, 1)
|
|
88
|
+
self.rescan = QPushButton("Rescan")
|
|
89
|
+
self.rescan.clicked.connect(self.scan)
|
|
90
|
+
row.addWidget(self.rescan)
|
|
91
|
+
form.addRow("Board", row)
|
|
92
|
+
self.owner = QLabel("—")
|
|
93
|
+
form.addRow("Claimed by", self.owner)
|
|
94
|
+
root.addLayout(form)
|
|
95
|
+
|
|
96
|
+
self.status = _note("")
|
|
97
|
+
root.addWidget(self.status)
|
|
98
|
+
|
|
99
|
+
self.buttons = QDialogButtonBox(QDialogButtonBox.Cancel)
|
|
100
|
+
self.reset_btn = QPushButton("Release")
|
|
101
|
+
self.reset_btn.setDefault(True)
|
|
102
|
+
self.buttons.addButton(self.reset_btn, QDialogButtonBox.AcceptRole)
|
|
103
|
+
self.reset_btn.clicked.connect(self._reset)
|
|
104
|
+
self.buttons.rejected.connect(self.reject)
|
|
105
|
+
root.addWidget(self.buttons)
|
|
106
|
+
|
|
107
|
+
self.board.currentIndexChanged.connect(self._board_changed)
|
|
108
|
+
self.scan()
|
|
109
|
+
|
|
110
|
+
def scan(self) -> None:
|
|
111
|
+
self._busy(True, "looking for boards…")
|
|
112
|
+
self._run(_Scan(), self._scanned)
|
|
113
|
+
|
|
114
|
+
def _scanned(self, boards: list, others: list) -> None:
|
|
115
|
+
if not self._alive:
|
|
116
|
+
return
|
|
117
|
+
self._boards = boards
|
|
118
|
+
self.board.blockSignals(True)
|
|
119
|
+
self.board.clear()
|
|
120
|
+
for b in boards:
|
|
121
|
+
self.board.addItem(f"{b.board_id or '(no id)'} — {b.port}", b.port)
|
|
122
|
+
self.board.blockSignals(False)
|
|
123
|
+
self._busy(False, "")
|
|
124
|
+
if not boards:
|
|
125
|
+
self.owner.setText("—")
|
|
126
|
+
self.reset_btn.setEnabled(False)
|
|
127
|
+
self.status.setText(
|
|
128
|
+
"No board found. Plug one in with a <b>data</b> USB cable."
|
|
129
|
+
+ (f" ({len(others)} other serial device(s) seen.)" if others else ""))
|
|
130
|
+
return
|
|
131
|
+
self._board_changed()
|
|
132
|
+
|
|
133
|
+
def _board_changed(self) -> None:
|
|
134
|
+
if not self._alive or self.board.currentIndex() < 0:
|
|
135
|
+
return
|
|
136
|
+
b = self._boards[self.board.currentIndex()]
|
|
137
|
+
owner = (getattr(b, "owner", "") or "").strip()
|
|
138
|
+
self.owner.setText(owner or "not claimed")
|
|
139
|
+
# Releasing an unclaimed board is harmless, so it stays enabled: the point is to
|
|
140
|
+
# let someone who suspects a stuck claim just try it and see.
|
|
141
|
+
self.reset_btn.setEnabled(True)
|
|
142
|
+
|
|
143
|
+
def _reset(self) -> None:
|
|
144
|
+
if self.board.currentIndex() < 0:
|
|
145
|
+
return
|
|
146
|
+
self._busy(True, "releasing…")
|
|
147
|
+
self._run(_Unpair(self.board.currentData()), self._done)
|
|
148
|
+
|
|
149
|
+
def _done(self, ok: bool, msg: str) -> None:
|
|
150
|
+
if not self._alive:
|
|
151
|
+
return
|
|
152
|
+
self._busy(False, "")
|
|
153
|
+
if not ok:
|
|
154
|
+
QMessageBox.warning(self, "Could not release the board", msg)
|
|
155
|
+
self.status.setText(msg)
|
|
156
|
+
return
|
|
157
|
+
QMessageBox.information(self, "Board released", msg)
|
|
158
|
+
self.accept()
|
|
159
|
+
|
|
160
|
+
def _worker_failed(self, message: str) -> None:
|
|
161
|
+
if self._alive:
|
|
162
|
+
self._busy(False, "")
|
|
163
|
+
self.status.setText(message)
|
|
164
|
+
|
|
165
|
+
def closeEvent(self, event) -> None:
|
|
166
|
+
self._detach()
|
|
167
|
+
super().closeEvent(event)
|
|
168
|
+
|
|
169
|
+
def reject(self) -> None:
|
|
170
|
+
self._detach()
|
|
171
|
+
super().reject()
|
|
172
|
+
|
|
173
|
+
def _busy(self, busy: bool, message: str) -> None:
|
|
174
|
+
if not self._alive:
|
|
175
|
+
return
|
|
176
|
+
self.status.setText(message)
|
|
177
|
+
self.rescan.setEnabled(not busy)
|
|
178
|
+
self.reset_btn.setEnabled(not busy)
|
|
179
|
+
self.setCursor(Qt.WaitCursor if busy else Qt.ArrowCursor)
|