@vention/vention-skills 0.10.14 → 0.10.15

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vention",
3
- "version": "0.10.14",
3
+ "version": "0.10.15",
4
4
  "description": "Vention skills for coding agents: write MachineLogic applications, build machine designs, and diagnose deployed machines.",
5
5
  "author": {
6
6
  "name": "Vention"
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Vention skills
2
2
 
3
- Version 0.10.14. Skills for coding agents that work with Vention machines, packaged as one plugin, `vention`, in the `vention-skills` marketplace.
3
+ Version 0.10.15. Skills for coding agents that work with Vention machines, packaged as one plugin, `vention`, in the `vention-skills` marketplace.
4
4
 
5
5
  | Skill | What it does |
6
6
  | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vention/vention-skills",
3
- "version": "0.10.14",
3
+ "version": "0.10.15",
4
4
  "description": "Vention skills for coding agents: write MachineLogic applications, build machine designs, and diagnose deployed machines.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
3
  "name": "vention",
4
- "version": "0.10.14",
4
+ "version": "0.10.15",
5
5
  "description": "Vention skills for coding agents: write MachineLogic applications, build machine designs, and diagnose deployed machines.",
6
6
  "author": {
7
7
  "name": "Vention"
@@ -3,7 +3,7 @@ name: vention-design
3
3
  description: Create, build and edit Vention machine designs and design graphs for agentic layout. Use when the user wants to start a new design, lay out an automation cell, add or reposition components in a Vention design, generate a machine layout from a description, add or place a floor plan image under a design, list a design's versions, or inspect an existing design graph. Trigger on MachineBuilder designs, design graphs, assembly graphs, extrusions, actuators, robot mounting, cell footprints, floor plans, or a machine-builder URL.
4
4
  license: MIT
5
5
  metadata:
6
- version: "0.10.14"
6
+ version: "0.10.15"
7
7
  author: Vention
8
8
  ---
9
9
 
@@ -3,7 +3,7 @@ name: vention-machine-logic
3
3
  description: Write, edit, validate, and deploy MachineLogic Python applications for Vention machines with the machine-logic-sdk and the Vention CLI. Load it before writing or changing any Python that talks to a Vention machine, however short, since device names and version-specific SDK calls come from here. Use to program a Vention machine or MachineMotion controller, drive actuators, conveyors, robots, pneumatics, or digital I/O, change an existing application, build a state machine, debug from logs, or pull, push, or link with the Vention CLI (vention or vn). Trigger on MachineLogic, machine-logic-sdk, MachineMotion, MachineBuilder applications, the Vention CLI, or a directory holding .machine-code-app-directory-info.json. Printing what the machine or robot holds now (positions, taught targets, calibration, I/O) is a program: files on disk hold only as-designed values. Not for explaining an existing machine or its configuration in words when no program is asked for, nor for Python that never touches the machine.
4
4
  license: MIT
5
5
  metadata:
6
- version: "0.10.14"
6
+ version: "0.10.15"
7
7
  machine-logic-sdk: ">=3.0,<4"
8
8
  author: Vention
9
9
  ---
@@ -245,7 +245,7 @@ Each example is a standalone `main.py` whose module docstring names the device n
245
245
  4. If `uv` is missing or the install fails, tell the user the code is unvalidated. Never report a check you could not run as passed.
246
246
  5. `vention push`, then run the application on the digital twin from MachineBuilder before any hardware run. One or two full cycles prove the cycle; each extra one costs a cycle time and shows nothing the first two did not. Run them at 100 % of the speed setting, chosen for that run: nothing on the twin can be hurt.
247
247
  6. Read the output with `vention logs` once the run ends, rather than following it while the cycles play. `vention logs` prints only the most recent lines, so a count of matching lines can stop rising before the run ends. Print each cycle's measured time. When the cycle time is unknown, sleep once for one cycle's estimate, read `vention logs` once for the measured time, then sleep once for the cycles left: a rounded-up guess for the whole run wastes the difference. Before that first sleep, read `vention logs` once a few seconds after starting the run, to see that the first cycle began: a run stuck before its first move would otherwise cost the whole sleep. Print a line before every call that can block for long, such as a home or a long move, not only after it, so a log that stops shows which call it stopped in. Never loop over `vention logs` waiting for a line to appear, and never end your turn while a background wait is running. MachineLogic failures name the device and the reason; read the log before proposing a change.
248
- 7. Check a motion from the log, not by watching the twin: `print` the robot's joint angles or the axis position at each step, then compare them with what the step should reach. For a repeated or oscillating motion, such as a wave, print the joint angles at each end of the swing, so the log alone shows how far it moved and which joints moved.
248
+ 7. Check a motion from the log, not by watching the twin: `print` the robot's joint angles (`angles, _timestamp = robot.state.joint_angles_data`) or the axis position at each step, then compare them with what the step should reach. For a repeated or oscillating motion, such as a wave, print the joint angles at each end of the swing, so the log alone shows how far it moved and which joints moved.
249
249
  8. On the digital twin, `home()` can hang: it does not return, even past its timeout, because the SDK keeps retrying the request. The twin also reports an axis's end sensors as off at every position, so their state cannot tell you where the axis is. Never delete the homing to get past a hang: the hardware needs it. Stop the run, tell the user the home did not finish on the twin, and ask whether to skip the home on the twin only.
250
250
  9. Pressing the emergency stop on the digital twin's pendant does nothing, so skip that test there. Tell the user the e-stop path is untested until the first run on the real cell.
251
251
  10. Show the user the diff before pushing over an application they did not write in this session.
@@ -257,6 +257,8 @@ The controller keeps its own scene. Positions re-taught on the pendant and the c
257
257
  - Deploy over the LAN from the application's Deploy menu in MachineBuilder with only the application ticked. "Design-level Scene Assets" replaces the controller's positions with the design's, including every one re-taught on the pendant, and "Configuration" replaces its configuration.
258
258
  - When the application needs a position the controller lacks, ask the user to teach it on the pendant under the same name it has on the design.
259
259
  - Deploying restarts the application, so deploy only while the cell is idle.
260
+ - Every start installs `requirements.txt` from PyPI, so the controller needs internet access; without it the install fails and the application never starts.
261
+ - After a deploy, confirm the application started before telling the user it runs. `curl -s http://<controller>/execution-engine/v1/application` returns its `state` and, under `logging`, the install's and the application's logs (parse it with `json.loads(text, strict=False)`: the log text holds raw control characters). A `completed` state with an install error in the logs means it never ran; tell the user what failed.
260
262
  - `print` what someone reading the controller's log needs, such as a step starting, a part finished or a fault. An HMI's event stream does not reach the log.
261
263
 
262
264
  ## Safety
@@ -3,7 +3,7 @@ name: vention-machine-logic-hmi
3
3
  description: Build or edit the custom operator HMI of a MachineLogic application, the React touch screen in customui/ that runs in MachineBuilder and on the MachineMotion pendant, wired to the application's Python process through vention-communication. Use when the user asks for an operator screen, a custom HMI or UI, a touch panel, start and stop buttons, live machine state on screen, or recipes an operator picks, for a Vention machine or inside a directory holding .machine-code-app-directory-info.json. Uses @vention/machine-ui, @vention/machine-apps-components and @vention/machine-logic-ui-sdk. Do not use for the no-code HMI Builder, or for the Python motion logic alone (that is vention-machine-logic).
4
4
  license: MIT
5
5
  metadata:
6
- version: "0.10.14"
6
+ version: "0.10.15"
7
7
  author: Vention
8
8
  ---
9
9
 
@@ -84,7 +84,7 @@ Blocking SDK calls, cancellation and the shutdown path follow the `vention-machi
84
84
 
85
85
  - Start the cycle from an `@on_enter_state` handler with `self.spawn(...)`.
86
86
  - Never publish a stream or fire a trigger from inside a worker thread. Return to the loop first.
87
- - Make one thread call per move and check a stop flag between them, so Stop finishes the move in progress. Release what the cycle holds (clamp open, outputs off) before you fire `BaseTriggers.TO_FAULT` or re-raise.
87
+ - Make one thread call per move and check a stop flag between them, so Stop finishes the move in progress. Release what the cycle holds (clamp open, outputs off) before you fire `BaseTriggers.TO_FAULT` or re-raise. `TO_FAULT` does not cancel what `spawn` started. Route the emergency stop from `machine.on_system_state_change` to fault after `await station.cancel_tasks()`, and stop every device in the `lifespan`'s shutdown, as the example's `fault_on_estop` and `stop_devices` do.
88
88
 
89
89
  ## Live data is a stream
90
90
 
@@ -165,14 +165,14 @@ The machine-apps-components README is behind its code: `NavigationBar` takes `Na
165
165
  - Confirm anything that starts motion or deletes data in a `VentionModal` with `isTouchDevice`, and say in it what will move.
166
166
  - `VentionButton` variants are `filled-brand`, `filled`, `outline`, `text-only`, `shaded` and `destructive`. MUI's `contained` and `outlined` do not exist on it, and neither does a `color` prop.
167
167
  - Start is green and Stop is red, both set through the `StatusTopBar` button's `backgroundColor`, `backgroundColorHover` and `textColor`.
168
- - A `LogsPanel` row cuts `message` and `description` to one line and shows the rest only on hover. Keep `message` to a few words saying what happened, put the cause and what to do next in `description`, and open the whole entry in a `VentionModal` from `onLogClick`.
168
+ - A `LogsPanel` row cuts `message` and `description` to one line and shows the rest only on hover. Keep `message` to a few words saying what happened, put the cause and what to do next in `description`, and open the whole entry in a `VentionModal` from `onLogClick`. Raise every error event as a banner on whatever page is open, whose View button opens that entry on the Logs page, as the example's `app.tsx` does.
169
169
 
170
170
  ## Validate before you push
171
171
 
172
172
  1. In the application directory, with the `requirements.txt` packages installed locally: `python3 -c "import server"` to re-emit `proto/app.proto`, and commit it if `git status` shows a change.
173
173
  2. In `customui/`: `npm install`, `npm run build`, then `npx tsc --noEmit`, and check that `ui/index.html` exists and loads its script from `./assets/`, a relative path.
174
174
  3. `vention push`, then open the HMI from the MachineLogic tab in MachineBuilder and run it on the digital twin before any hardware run. Inside the MachineBuilder tab the browser's element search can't see the HMI's controls, so take a screenshot and tap by coordinates, and take a fresh screenshot after each tap since the layout moves.
175
- 4. Read `vention logs` when a command fails; the server's errors land there. On a real controller, read them as the `vention-machine-logic` skill's "Deploying to a real cell" says.
175
+ 4. Read `vention logs` when a command fails; the server's errors land there. On a real controller, confirm the application started and read its logs as the `vention-machine-logic` skill's "Deploying to a real cell" says.
176
176
 
177
177
  ## Safety
178
178
 
@@ -180,7 +180,7 @@ The HMI drives physical machinery that can injure someone.
180
180
 
181
181
  - Start never fires on page load, on reconnect, or from a stream message. Only an operator's tap, confirmed in a modal, starts motion.
182
182
  - Start and Stop live in the `StatusTopBar`, so they are on every page. Do not repeat them on a page. Raise the bar above `theme.zIndex.modal`, as the example's `app.tsx` does, or an open `VentionModal` covers Stop.
183
- - Stop stays enabled unless the station is known to be ready or already stopping, as the example's `canStop` does. A fault is neither, so Stop stays enabled there, and so it does while the stream is disconnected: a dropped stream may hide a moving machine. Every other command is disabled while disconnected.
183
+ - Stop is disabled until this page load has received its first status, since no Stop can reach the app before then; show why next to it and say to use the emergency stop if the machine moves. After that, Stop stays enabled unless the station is known to be ready or already stopping, as the example's `canStop` does. A fault is neither, so Stop stays enabled there, and so it does while the stream is disconnected: a dropped stream may hide a moving machine. When Stop fails, say it did not reach the station and to use the emergency stop. Every other command is disabled while disconnected.
184
184
  - A tap on Stop shows at once. While the station stops, the Stop button reads "Stopping…" (derive that label from the streamed state and `connected`, as the example's `isStopping(status, connected)` does, never from a tapped flag alone: once the stream drops it reads "Stop" again and stays pressable), the Operate page's progress bar marks where the run now ends (the cycle in progress amber, the cancelled ones hatched and struck through), and a line by the heading says the emergency stop halts at once. Say it on the screen the operator already watches rather than in a separate alert.
185
185
  - Send Start with a `timeoutMs` and an abort signal, and abort it when the stream drops. A Start the operator can no longer see must not land later; say it was not confirmed and to check the station.
186
186
  - The HMI never offers a way around an emergency stop, safety interlock, light curtain or guard, not even a hidden one for testing.
@@ -190,7 +190,7 @@ The HMI drives physical machinery that can injure someone.
190
190
 
191
191
  `examples/indexing-station-hmi/` indexes a part on a conveyor, clamps it, proves the clamp closed, dwells and releases, for the cycle count of the recipe the operator picked.
192
192
 
193
- - `server.py`: the state machine, the streams, the Start action, both bundles, and `Machine()` in the `lifespan`.
193
+ - `server.py`: the state machine, the streams, the Start action, both bundles, `Machine()` in the `lifespan`, the emergency stop routed to fault, and every device stopped on shutdown.
194
194
  - `models.py`: the `Recipe` storage model, every field with a zero default.
195
195
  - `project.json`, `requirements.txt`: the one process and the exact pins.
196
196
  - `proto/app.proto`: what `finalize()` emits.
@@ -216,7 +216,7 @@ The HMI drives physical machinery that can injure someone.
216
216
  - `size` left at the `"small"` default on a touch screen.
217
217
  - `BrowserRouter`, whose routes miss under `.../index.html`.
218
218
  - Reading versions from `ui/package.json`; the manifest is `customui/package.json`.
219
- - Disabling Stop whenever the stream drops.
219
+ - Disabling Stop whenever the stream drops, or enabling it before the page has ever connected.
220
220
  - A Stop that looks the same before and after the tap while the station finishes its move.
221
221
  - The state shown in the `StatusTopBar` and again on a page.
222
222
  - `severity="warning"` on a `VentionAlert`. It is `"warn"`.
@@ -4,7 +4,7 @@ import { NavigationBar, StatusTopBar } from "@vention/machine-apps-components"
4
4
  import type { LogEntry, LogType } from "@vention/machine-apps-components"
5
5
  import { COLORS, VentionAlert, VentionModal } from "@vention/machine-ui"
6
6
  import { useEffect, useRef, useState } from "react"
7
- import { Navigate, Route, Routes } from "react-router-dom"
7
+ import { Navigate, Route, Routes, useNavigate } from "react-router-dom"
8
8
  import { client, openEvents, openStatus, STATUS_STALL_MS } from "./client"
9
9
  import type { Event, Recipe } from "./gen/app_pb"
10
10
  import { LogsPage } from "./pages/logs-page"
@@ -52,6 +52,11 @@ export function App() {
52
52
  const { value: status, connected } = useStream(openStatus, STATUS_STALL_MS)
53
53
  const logEntries = useEventLog()
54
54
  const [commandError, setCommandError] = useState("")
55
+ const navigate = useNavigate()
56
+ // Every error event raises a banner on whatever page is open; View opens the whole entry on the Logs page.
57
+ const [errorEntry, setErrorEntry] = useState<LogEntry | undefined>(undefined)
58
+ const [openLogEntry, setOpenLogEntry] = useState<LogEntry | undefined>(undefined)
59
+ const latestEntry = logEntries[0]
55
60
  const state = describeState(status, connected)
56
61
 
57
62
  const [recipe, setRecipe] = useState<Recipe | undefined>(undefined)
@@ -63,6 +68,16 @@ export function App() {
63
68
  if (!connected) startRequest.current?.abort()
64
69
  }, [connected])
65
70
 
71
+ useEffect(() => {
72
+ if (latestEntry?.level === "error") setErrorEntry(latestEntry)
73
+ }, [latestEntry])
74
+
75
+ const viewError = () => {
76
+ setOpenLogEntry(errorEntry)
77
+ setErrorEntry(undefined)
78
+ navigate("/logs")
79
+ }
80
+
66
81
  const start = async () => {
67
82
  setConfirmingStart(false)
68
83
  if (recipe === undefined) return
@@ -77,7 +92,9 @@ export function App() {
77
92
  }
78
93
 
79
94
  const stop = () => {
80
- client.trigger_Stop({}).catch((error: unknown) => setCommandError(`Stop failed: ${errorText(error)}`))
95
+ client
96
+ .trigger_Stop({})
97
+ .catch((error: unknown) => setCommandError(`Stop did not reach the station (${errorText(error)}). Use the emergency stop.`))
81
98
  }
82
99
 
83
100
  return (
@@ -111,6 +128,27 @@ export function App() {
111
128
  ]}
112
129
  />
113
130
  </Box>
131
+ {status === undefined && (
132
+ <VentionAlert
133
+ severity="warn"
134
+ size="xx-large"
135
+ title="Not connected to the app yet"
136
+ descriptionText="Stop is available once the app answers. Use the emergency stop if the machine moves."
137
+ isFullWidth
138
+ />
139
+ )}
140
+ {errorEntry !== undefined && (
141
+ <VentionAlert
142
+ severity="error"
143
+ size="xx-large"
144
+ title={errorEntry.message}
145
+ primaryButtonText="View"
146
+ onPrimaryButtonClick={viewError}
147
+ isFullWidth
148
+ isCloseable
149
+ onCloseButtonClick={() => setErrorEntry(undefined)}
150
+ />
151
+ )}
114
152
  {commandError !== "" && (
115
153
  <VentionAlert
116
154
  severity="error"
@@ -130,7 +168,7 @@ export function App() {
130
168
  }
131
169
  />
132
170
  <Route path="/recipes" element={<RecipesPage connected={connected} onError={setCommandError} />} />
133
- <Route path="/logs" element={<LogsPage entries={logEntries} />} />
171
+ <Route path="/logs" element={<LogsPage entries={logEntries} openEntry={openLogEntry} onOpenEntry={setOpenLogEntry} />} />
134
172
  <Route path="*" element={<Navigate to="/" replace />} />
135
173
  </Routes>
136
174
  </Box>
@@ -2,7 +2,7 @@ import { LogsPanel } from "@vention/machine-apps-components"
2
2
  import type { FetchParams, FetchResult, LogEntry, LogType } from "@vention/machine-apps-components"
3
3
  import { VentionModal } from "@vention/machine-ui"
4
4
  import type { VentionModalProps } from "@vention/machine-ui"
5
- import { useCallback, useState } from "react"
5
+ import { useCallback } from "react"
6
6
 
7
7
  const PAGE_SIZE = 10
8
8
  const MODAL_TYPE: Record<LogType, VentionModalProps["type"]> = { error: "negative", warning: "warning", info: "info" }
@@ -15,8 +15,14 @@ function matchesFilters(entry: LogEntry, { filters }: FetchParams): boolean {
15
15
  return true
16
16
  }
17
17
 
18
- export function LogsPage({ entries }: { entries: LogEntry[] }) {
19
- const [openEntry, setOpenEntry] = useState<LogEntry | undefined>(undefined)
18
+ type LogsPageProps = {
19
+ entries: LogEntry[]
20
+ // Held by the app, so the error banner can open its entry here.
21
+ openEntry: LogEntry | undefined
22
+ onOpenEntry: (entry: LogEntry | undefined) => void
23
+ }
24
+
25
+ export function LogsPage({ entries, openEntry, onOpenEntry: setOpenEntry }: LogsPageProps) {
20
26
  const dataFetcher = useCallback(
21
27
  async (params: FetchParams): Promise<FetchResult> => {
22
28
  const matching = entries.filter(entry => matchesFilters(entry, params))
@@ -22,10 +22,13 @@ const STATE_DISPLAY: Record<string, StateDisplay> = {
22
22
  Station_stopping: { label: "Stopping", color: COLORS.amber[500] },
23
23
  fault: { label: "Fault", color: COLORS.red[600] },
24
24
  }
25
+ const CONNECTING: StateDisplay = { label: "Connecting…", color: COLORS.slate[400] }
25
26
  const DISCONNECTED: StateDisplay = { label: "Disconnected", color: COLORS.slate[400] }
26
27
 
28
+ // `status` stays set once the stream has delivered, so undefined means this page has never connected.
27
29
  export function describeState(status: StationStatus | undefined, connected: boolean): StateDisplay {
28
- if (!connected || status === undefined) return DISCONNECTED
30
+ if (status === undefined) return CONNECTING
31
+ if (!connected) return DISCONNECTED
29
32
  return STATE_DISPLAY[status.state] ?? { label: status.state, color: COLORS.slate[500] }
30
33
  }
31
34
 
@@ -37,9 +40,12 @@ export function isStopping(status: StationStatus | undefined, connected: boolean
37
40
  return connected && status?.state === "Station_stopping"
38
41
  }
39
42
 
40
- /** Stop is disabled only when the station is known to be ready or already stopping: a dropped stream may hide a moving conveyor. */
43
+ /**
44
+ * Stop waits for the first connection, since before it no Stop can reach the app. After that it is disabled only
45
+ * when the station is known to be ready or already stopping: a dropped stream may hide a moving conveyor.
46
+ */
41
47
  export function canStop(status: StationStatus | undefined, connected: boolean): boolean {
42
- return !(connected && status?.state === "ready") && !isStopping(status, connected)
48
+ return status !== undefined && !(connected && status.state === "ready") && !isStopping(status, connected)
43
49
  }
44
50
 
45
51
  export function errorText(error: unknown): string {
@@ -1,6 +1,7 @@
1
1
  """Indexing station with an operator HMI: index, clamp, prove the clamp, dwell, release, per recipe cycle.
2
2
 
3
3
  Needs actuator "Conveyor", output "Clamp" and input "Clamp Closed"; customui/ calls it only through the generated client.
4
+ An emergency stop sends the station to fault, and shutting the server down stops the conveyor and opens the clamp.
4
5
  """
5
6
 
6
7
  import asyncio
@@ -16,6 +17,7 @@ from pydantic import BaseModel
16
17
  from communication.app import VentionApp
17
18
  from communication.decorators import action, stream
18
19
  from machinelogic import Machine, MotionProfile
20
+ from machinelogic.ivention.imachine import MachineOperationalState, MachineSafetyState
19
21
  from state_machine.core import BaseStates, BaseTriggers, StateMachine
20
22
  from state_machine.decorators import on_enter_state, on_state_change
21
23
  from state_machine.defs import State, StateGroup, Trigger
@@ -155,6 +157,20 @@ class IndexingStation(StateMachine):
155
157
  if self.machine is not None:
156
158
  self.machine.get_output("Clamp").write(False)
157
159
 
160
+ def stop_devices(self) -> None:
161
+ if self.machine is not None:
162
+ self.machine.get_actuator("Conveyor").stop()
163
+ self.release()
164
+
165
+ async def fault_on_estop(self) -> None:
166
+ # The to_fault trigger does not cancel the cycle task, so cancel it first.
167
+ await self.cancel_tasks()
168
+ self.fault_message = "emergency stop pressed"
169
+ self.trigger(BaseTriggers.TO_FAULT.value)
170
+ await publish_event(
171
+ "error", "Emergency stop", "Release the emergency stop and check the station, then press Reset on the Operate page."
172
+ )
173
+
158
174
  async def release_safely(self) -> None:
159
175
  try:
160
176
  await asyncio.to_thread(self.release)
@@ -218,14 +234,26 @@ async def repeat_status() -> None:
218
234
 
219
235
  @asynccontextmanager
220
236
  async def lifespan(_: FastAPI) -> AsyncIterator[None]:
237
+ loop = asyncio.get_running_loop()
238
+
239
+ def on_safety(_operational: MachineOperationalState, safety: MachineSafetyState) -> None:
240
+ if safety == MachineSafetyState.EMERGENCY_STOP:
241
+ asyncio.run_coroutine_threadsafe(station.fault_on_estop(), loop)
242
+
221
243
  try:
222
244
  station.machine = await asyncio.to_thread(Machine)
245
+ station.machine.on_system_state_change(on_safety)
223
246
  except Exception as error:
224
247
  station.fault_message = f"cannot reach the controller: {error}"
225
248
  await publish_status()
226
249
  heartbeat = asyncio.create_task(repeat_status())
227
- yield
228
- heartbeat.cancel()
250
+ try:
251
+ yield
252
+ finally:
253
+ heartbeat.cancel()
254
+ # Cancelling the cycle does not stop a move already running in its thread.
255
+ await station.cancel_tasks()
256
+ await asyncio.to_thread(station.stop_devices)
229
257
 
230
258
 
231
259
  app = VentionApp(name="IndexingStation", emit_proto=True, proto_path="proto/app.proto", lifespan=lifespan)
@@ -3,7 +3,7 @@ name: vention-monitoring
3
3
  description: Diagnose deployed Vention machines from controller state, history, health, and activity data. Use when the user reports a machine that is down, faulted, offline, slow, or underperforming, asks why a cycle time regressed, wants to interpret MachineMotion event data or drive and brake status, asks about OEE or uptime, or wants to know which machine in a fleet needs attention. Trigger on a MachineMotion serial number plus any complaint.
4
4
  license: MIT
5
5
  metadata:
6
- version: "0.10.14"
6
+ version: "0.10.15"
7
7
  author: Vention
8
8
  ---
9
9