@vention/vention-skills 0.10.7 → 0.10.9
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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +1 -1
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/skills/vention-design/SKILL.md +1 -1
- package/skills/vention-machine-logic/SKILL.md +5 -5
- package/skills/vention-machine-logic-hmi/SKILL.md +4 -4
- package/skills/vention-machine-logic-hmi/examples/indexing-station-hmi/customui/src/pages/operate-page.tsx +107 -53
- package/skills/vention-machine-logic-hmi/examples/indexing-station-hmi/proto/app.proto +5 -4
- package/skills/vention-machine-logic-hmi/examples/indexing-station-hmi/server.py +5 -0
- package/skills/vention-monitoring/SKILL.md +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Vention skills
|
|
2
2
|
|
|
3
|
-
Version 0.10.
|
|
3
|
+
Version 0.10.9. 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
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.
|
|
4
|
+
"version": "0.10.9",
|
|
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.
|
|
6
|
+
version: "0.10.9"
|
|
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.
|
|
6
|
+
version: "0.10.9"
|
|
7
7
|
machine-logic-sdk: ">=3.0,<4"
|
|
8
8
|
author: Vention
|
|
9
9
|
---
|
|
@@ -147,7 +147,7 @@ print(f"cycle {cycle} took {elapsed:.1f} s") # the work that was worth not b
|
|
|
147
147
|
robot.wait_for_motion_completion() # robots take no arguments
|
|
148
148
|
```
|
|
149
149
|
|
|
150
|
-
Poll `state.move_in_progress` only when something has to be serviced mid-travel, and still finish on the wait. `move_continuous_async` is the exception to all of it: it has no target to arrive at, so it ends at `stop()`, never at a wait.
|
|
150
|
+
Poll `state.move_in_progress` only when something has to be serviced mid-travel, such as a Stop or a timeout, and still finish on the wait: the flag can read False before the move has begun, so a polling loop alone may return before the axis arrives. `move_continuous_async` is the exception to all of it: it has no target to arrive at, so it ends at `stop()`, never at a wait.
|
|
151
151
|
|
|
152
152
|
## Reacting to an input edge
|
|
153
153
|
|
|
@@ -243,8 +243,8 @@ Each example is a standalone `main.py` whose module docstring names the device n
|
|
|
243
243
|
2. Run `uv venv --python 3.10 && uv pip install -r requirements-dev.txt`, then `.venv/bin/pyright`. The venv runs the target's Python and the SDK the application pins, so a wrong name or signature shows up against what the target runs. The first pyright run may download Node if none is on the PATH, so a slow or offline first run is not a type error. Fix every error and run it again until it is clean.
|
|
244
244
|
3. pyright cannot check a name passed as a string. Check every name passed to `get_*` and `move_to_target*` with a case-sensitive `grep -F '"<name>"'` against `.vention-design-configuration.json` or `.vention-design-scene-assets.json`, and fix any that does not match exactly.
|
|
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
|
-
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
|
-
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.
|
|
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
|
+
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. 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
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.
|
|
249
249
|
8. 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.
|
|
250
250
|
9. Show the user the diff before pushing over an application they did not write in this session.
|
|
@@ -263,7 +263,7 @@ The controller keeps its own scene. Positions re-taught on the pendant and the c
|
|
|
263
263
|
This code drives physical machinery that can injure someone.
|
|
264
264
|
|
|
265
265
|
- Before a run on real hardware, tell the user plainly what will move and how far.
|
|
266
|
-
- Give an application that will run on hardware a speed setting that scales every move and goes up to 100 %, even when its steps don't divide 100 (15, 30 … 90, 100),
|
|
266
|
+
- Give an application that will run on hardware a speed setting that scales every move and goes up to 100 %, even when its steps don't divide 100 (15, 30 … 90, 100). After deploying to hardware, tell the user to run the first cycles on the machine at a low speed setting, with a hand on the emergency stop, before raising it.
|
|
267
267
|
- Never remove, bypass, or work around an emergency stop, safety interlock, light curtain, door switch, or speed limit, and never suggest doing so, even temporarily, even for testing. If a safety device is blocking a program, say so and stop.
|
|
268
268
|
- Never write code that starts motion automatically on power-up unless the user has explicitly asked for it and confirmed the cell is guarded.
|
|
269
269
|
|
|
@@ -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.
|
|
6
|
+
version: "0.10.9"
|
|
7
7
|
author: Vention
|
|
8
8
|
---
|
|
9
9
|
|
|
@@ -152,7 +152,7 @@ The machine-apps-components README is behind its code: `NavigationBar` takes `Na
|
|
|
152
152
|
|
|
153
153
|
- Wrap the app in `machineUiHmiTheme`.
|
|
154
154
|
- Give the app an Operate page at `/`, any setup pages it needs (such as Recipes, and a Settings page holding the speed setting of an app that runs on hardware), and a Logs page: a `LogsPanel` fed by the Events stream, as the example's `logs-page.tsx` does, where the operator finds out why a cycle faulted. Nav labels name the page and never echo a top-bar action, and their icons don't suggest one. Never "Run", "Start" or a play icon: beside the top bar's Start, the operator reads it as a second Start button.
|
|
155
|
-
- The Operate page is what the operator watches during a run, so it reads from across the cell. Copy the example's `operate-page.tsx` and adapt it rather than starting from a blank page.
|
|
155
|
+
- The Operate page is what the operator watches during a run, so it reads from across the cell. Copy the example's `operate-page.tsx` and adapt it rather than starting from a blank page. In every state (idle, starting, running, stopping, after a run, fault, disconnected) show the same regions from top to bottom, never swapped for a placeholder when no cycle runs: the step in progress named inside a ring that fills against the last cycle's time, or by steps done while no cycle has been timed yet, with the elapsed time counting up and no indeterminate spinner, beside the recipe and the cycles done and rate; the cycle's steps as a row of tiles, the current one lit; and the run's progress bar. Before a run they show the next run's plan, after one its result until the next Start, and on a fault the step that faulted. The example is part of this skill, so open and copy it even when the user says not to copy code from their computer: copy its StepRing, StepRail and CycleProgress as they are, and publish the step, the cycle's elapsed time and `cycles_done` on the status stream as its `enter_step` and `run_cycles` do. A single `VentionProgressBar` cannot mark the cycles a Stop cancels. Below them go the settings for the next run (disabled while one runs) and one short paragraph on what Start and Stop do. Leave out what the operator can neither change nor act on, such as axis stops, taught positions or device names, here and in the Start confirmation: they are the program's constants, not the run's. Lay it out with `Box` or `Stack` gaps, each readout a label above its value in theme text variants. Never set `fontSize` or `lineHeight`: a value sized by hand overlaps its label.
|
|
156
156
|
- Show each piece of information in one place only. The `StatusTopBar` already shows the state, so a page never repeats it, not even as a status indicator or an alert titled with the same word. A page adds only what the bar cannot say: the cause of a fault, what happens next, what to do.
|
|
157
157
|
- The screen fits the pendant without scrolling: keep `body { margin: 0 }` from the example's `index.html`, or the full-viewport root overflows it. Only the page area between the two bars scrolls.
|
|
158
158
|
- Exactly one way back to Control Center: the `NavigationBar`'s own button, which confirms before leaving. Keep `ui.showDefaultBackToControlCenterButton` `false` in `project.json`, or the pendant lays a second one over the screen.
|
|
@@ -171,7 +171,7 @@ The machine-apps-components README is behind its code: `NavigationBar` takes `Na
|
|
|
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
|
-
3. `vention push`, then open the HMI from the MachineLogic tab in MachineBuilder and run it on the digital twin before any hardware run.
|
|
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
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.
|
|
176
176
|
|
|
177
177
|
## Safety
|
|
@@ -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.
|
|
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.
|
|
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.
|
|
@@ -3,7 +3,7 @@ import { COLORS, VentionAlert, VentionSelect } from "@vention/machine-ui"
|
|
|
3
3
|
import { useEffect, useState } from "react"
|
|
4
4
|
import { client } from "../client"
|
|
5
5
|
import type { Recipe, StationStatus } from "../gen/app_pb"
|
|
6
|
-
import { STEPS, canStart, errorText
|
|
6
|
+
import { STEPS, canStart, errorText } from "../station"
|
|
7
7
|
|
|
8
8
|
interface OperatePageProps {
|
|
9
9
|
status: StationStatus | undefined
|
|
@@ -13,31 +13,66 @@ interface OperatePageProps {
|
|
|
13
13
|
onError: (message: string) => void
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
/** What the page shows. The same regions render in every phase; only their content changes. */
|
|
17
|
+
type Phase = "idle" | "starting" | "running" | "stopping" | "done" | "fault"
|
|
18
|
+
|
|
16
19
|
const HATCHED = `repeating-linear-gradient(45deg, ${COLORS.slate[100]} 0 8px, ${COLORS.slate[200]} 8px 16px)`
|
|
17
20
|
const RING_RADIUS = 52
|
|
18
21
|
const RING_LENGTH = 2 * Math.PI * RING_RADIUS
|
|
19
22
|
const TICK_MS = 250
|
|
23
|
+
const LIT: Partial<Record<Phase, string>> = { running: COLORS.blue[500], stopping: COLORS.amber[500], fault: COLORS.red[600] }
|
|
24
|
+
|
|
25
|
+
function phaseOf(status: StationStatus | undefined): Phase {
|
|
26
|
+
if (status === undefined) return "idle"
|
|
27
|
+
if (status.state === "fault") return "fault"
|
|
28
|
+
if (status.state === "Station_stopping") return "stopping"
|
|
29
|
+
if (status.state === "Station_running") return status.cycle > 0 ? "running" : "starting"
|
|
30
|
+
return status.recipeName === "" ? "idle" : "done"
|
|
31
|
+
}
|
|
20
32
|
|
|
21
|
-
/**
|
|
22
|
-
function
|
|
33
|
+
/** The ring's text when it names no step of a cycle: the next run's first step, the start, or how the last run ended. */
|
|
34
|
+
function ringLabel(phase: Phase, status: StationStatus | undefined): string | undefined {
|
|
35
|
+
if (phase === "idle") return `Next: ${STEPS[0].label}`
|
|
36
|
+
if (phase === "starting" && status?.step === "") return "Starting"
|
|
37
|
+
if (phase !== "done" || status === undefined) return undefined
|
|
38
|
+
const { cyclesDone, cycleCount } = status
|
|
39
|
+
return cyclesDone === cycleCount ? `${cycleCount} of ${cycleCount} done` : `Stopped after ${cyclesDone} of ${cycleCount}`
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Seconds into the cycle, counted on from the last status so the ring moves between heartbeats; frozen while `ticking` is false. */
|
|
43
|
+
function useCycleElapsed(status: StationStatus | undefined, ticking: boolean): number {
|
|
23
44
|
const [received, setReceived] = useState({ seconds: 0, at: 0 })
|
|
24
45
|
const [now, setNow] = useState(() => performance.now())
|
|
25
|
-
const running = status !== undefined && status.step !== ""
|
|
26
46
|
useEffect(() => setReceived({ seconds: status?.cycleElapsedSeconds ?? 0, at: performance.now() }), [status])
|
|
27
47
|
useEffect(() => {
|
|
28
|
-
if (!
|
|
48
|
+
if (!ticking) return
|
|
29
49
|
const timer = setInterval(() => setNow(performance.now()), TICK_MS)
|
|
30
50
|
return () => clearInterval(timer)
|
|
31
|
-
}, [
|
|
32
|
-
return
|
|
51
|
+
}, [ticking])
|
|
52
|
+
return received.seconds + Math.max(0, now - received.at) / 1000
|
|
33
53
|
}
|
|
34
54
|
|
|
35
|
-
/** The step in progress, inside a ring that fills
|
|
36
|
-
function StepRing({
|
|
55
|
+
/** The step in progress, inside a ring that fills against the last cycle's time, or by steps done until a cycle has been timed. `label` replaces the step's name. */
|
|
56
|
+
function StepRing({
|
|
57
|
+
step,
|
|
58
|
+
elapsed,
|
|
59
|
+
expected,
|
|
60
|
+
phase,
|
|
61
|
+
label,
|
|
62
|
+
}: {
|
|
63
|
+
step: string
|
|
64
|
+
elapsed: number
|
|
65
|
+
expected: number
|
|
66
|
+
phase: Phase
|
|
67
|
+
label?: string
|
|
68
|
+
}) {
|
|
69
|
+
const currentIndex = STEPS.findIndex(candidate => candidate.name === step)
|
|
37
70
|
const current = STEPS.find(candidate => candidate.name === step)
|
|
38
71
|
const StepIcon = current?.icon
|
|
39
|
-
const
|
|
40
|
-
const
|
|
72
|
+
const cycling = phase === "running" || phase === "stopping"
|
|
73
|
+
const timedFill = expected > 0 ? Math.min(elapsed / expected, 1) : Math.max(currentIndex, 0) / STEPS.length
|
|
74
|
+
const filled = phase === "done" || phase === "fault" ? 1 : cycling ? timedFill : 0
|
|
75
|
+
const color = LIT[phase] ?? COLORS.slate[400]
|
|
41
76
|
return (
|
|
42
77
|
<Box sx={{ position: "relative", width: 220, height: 220, flexShrink: 0 }}>
|
|
43
78
|
<svg viewBox="0 0 120 120" width={220} height={220} aria-hidden="true">
|
|
@@ -62,28 +97,33 @@ function StepRing({ step, elapsed, expected, stopping }: { step: string; elapsed
|
|
|
62
97
|
flexDirection: "column",
|
|
63
98
|
alignItems: "center",
|
|
64
99
|
justifyContent: "center",
|
|
100
|
+
textAlign: "center",
|
|
65
101
|
gap: 1,
|
|
66
102
|
}}
|
|
67
103
|
>
|
|
68
104
|
{StepIcon && <StepIcon size={36} color={color} />}
|
|
69
|
-
<Typography variant="heading18SemiBold">{current?.label ?? step}</Typography>
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
105
|
+
<Typography variant="heading18SemiBold">{label ?? current?.label ?? step}</Typography>
|
|
106
|
+
{cycling && (
|
|
107
|
+
<Typography variant="paragraph18Medium">
|
|
108
|
+
{Math.floor(elapsed)} s{expected > 0 && ` of ~${Math.round(expected)} s`}
|
|
109
|
+
</Typography>
|
|
110
|
+
)}
|
|
111
|
+
{cycling && expected === 0 && <Typography variant="paragraph18Medium">Timing this cycle</Typography>}
|
|
112
|
+
{phase === "fault" && <Typography variant="paragraph18Medium">Faulted</Typography>}
|
|
73
113
|
</Box>
|
|
74
114
|
</Box>
|
|
75
115
|
)
|
|
76
116
|
}
|
|
77
117
|
|
|
78
|
-
/** The cycle's steps in order: done ones green, the current one lit, the rest grey. */
|
|
79
|
-
function StepRail({ step,
|
|
118
|
+
/** The cycle's steps in order: done ones green, the current one lit in the phase's color, the rest grey. */
|
|
119
|
+
function StepRail({ step, phase }: { step: string; phase: Phase }) {
|
|
80
120
|
const currentIndex = STEPS.findIndex(candidate => candidate.name === step)
|
|
81
121
|
return (
|
|
82
122
|
<Box sx={{ display: "grid", gridTemplateColumns: `repeat(${STEPS.length}, minmax(0, 1fr))`, gap: 4 }}>
|
|
83
123
|
{STEPS.map(({ name, icon: StepIcon }, index) => {
|
|
84
124
|
const done = index < currentIndex
|
|
85
125
|
const current = index === currentIndex
|
|
86
|
-
const background = current ? (
|
|
126
|
+
const background = current ? (LIT[phase] ?? COLORS.slate[400]) : done ? COLORS.green[100] : COLORS.slate[100]
|
|
87
127
|
const iconColor = current ? "white" : done ? COLORS.green[700] : COLORS.slate[400]
|
|
88
128
|
return (
|
|
89
129
|
<Box key={name} sx={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 2 }}>
|
|
@@ -117,20 +157,31 @@ function Counter({ label, value }: { label: string; value: string }) {
|
|
|
117
157
|
)
|
|
118
158
|
}
|
|
119
159
|
|
|
120
|
-
/** Done cycles green, the
|
|
121
|
-
function CycleProgress({ cycle, cycleCount,
|
|
122
|
-
const
|
|
160
|
+
/** Done cycles green, the one in progress lit in the phase's color, and the cycles Stop cancels hatched. */
|
|
161
|
+
function CycleProgress({ cycle, done, cycleCount, phase }: { cycle: number; done: number; cycleCount: number; phase: Phase }) {
|
|
162
|
+
const inCycle = (phase === "running" || phase === "stopping" || phase === "fault") && cycle > 0
|
|
163
|
+
const finished = inCycle ? cycle - 1 : done
|
|
164
|
+
const rest = cycleCount - finished - (inCycle ? 1 : 0)
|
|
165
|
+
const cancelled = (phase === "stopping" || phase === "done") && rest > 0
|
|
123
166
|
const segment = (count: number, background: string) => <Box sx={{ flex: count, height: 16, borderRadius: 1, background }} />
|
|
167
|
+
const total = phase === "stopping" && cancelled ? <s>{cycleCount}</s> : cycleCount
|
|
124
168
|
return (
|
|
125
169
|
<Box sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
|
|
126
170
|
<Box sx={{ display: "flex", gap: 1 }}>
|
|
127
|
-
{
|
|
128
|
-
{segment(1,
|
|
129
|
-
{
|
|
171
|
+
{finished > 0 && segment(finished, COLORS.green[500])}
|
|
172
|
+
{inCycle && segment(1, phase === "running" ? COLORS.green[200] : (LIT[phase] ?? COLORS.slate[400]))}
|
|
173
|
+
{(rest > 0 || cycleCount === 0) && segment(Math.max(rest, 1), cancelled ? HATCHED : COLORS.slate[100])}
|
|
130
174
|
</Box>
|
|
131
175
|
<Typography variant="heading18SemiBold">
|
|
132
|
-
|
|
133
|
-
{
|
|
176
|
+
{phase === "idle" && (cycleCount > 0 ? `Next run: ${cycleCount} cycle${cycleCount === 1 ? "" : "s"}` : "Next run: choose a recipe")}
|
|
177
|
+
{phase === "done" && "Last run, until the next Start"}
|
|
178
|
+
{phase === "fault" && `Cycle ${cycle} of ${cycleCount} faulted`}
|
|
179
|
+
{(phase === "starting" || phase === "running" || phase === "stopping") && (
|
|
180
|
+
<>
|
|
181
|
+
{cycle > 0 ? `Cycle ${cycle}` : "0"} of {total}
|
|
182
|
+
{phase === "stopping" && ", stopping after the move in progress"}
|
|
183
|
+
</>
|
|
184
|
+
)}
|
|
134
185
|
</Typography>
|
|
135
186
|
</Box>
|
|
136
187
|
)
|
|
@@ -138,9 +189,10 @@ function CycleProgress({ cycle, cycleCount, stopping }: { cycle: number; cycleCo
|
|
|
138
189
|
|
|
139
190
|
export function OperatePage({ status, connected, recipe, onRecipeChange, onError }: OperatePageProps) {
|
|
140
191
|
const [recipes, setRecipes] = useState<Recipe[]>([])
|
|
141
|
-
const
|
|
142
|
-
const elapsed = useCycleElapsed(status)
|
|
143
|
-
const
|
|
192
|
+
const phase = phaseOf(status)
|
|
193
|
+
const elapsed = useCycleElapsed(status, connected && (phase === "running" || phase === "stopping"))
|
|
194
|
+
const idle = phase === "idle" || status === undefined
|
|
195
|
+
const lastCycleSeconds = status?.lastCycleSeconds ?? 0
|
|
144
196
|
|
|
145
197
|
useEffect(() => {
|
|
146
198
|
if (!connected) return
|
|
@@ -165,33 +217,35 @@ export function OperatePage({ status, connected, recipe, onRecipeChange, onError
|
|
|
165
217
|
onPrimaryButtonClick={reset}
|
|
166
218
|
/>
|
|
167
219
|
)}
|
|
168
|
-
{
|
|
169
|
-
<Box sx={{ display: "flex",
|
|
170
|
-
<
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
220
|
+
<Box sx={{ display: "flex", flexDirection: "column", gap: 5, opacity: connected ? 1 : 0.5 }}>
|
|
221
|
+
<Box sx={{ display: "flex", alignItems: "center", gap: 5 }}>
|
|
222
|
+
<StepRing
|
|
223
|
+
step={idle ? STEPS[0].name : status.step}
|
|
224
|
+
elapsed={elapsed}
|
|
225
|
+
expected={lastCycleSeconds}
|
|
226
|
+
phase={phase}
|
|
227
|
+
label={ringLabel(phase, status)}
|
|
228
|
+
/>
|
|
229
|
+
<Box sx={{ flex: 1, minWidth: 0, display: "flex", flexDirection: "column", gap: 4 }}>
|
|
230
|
+
<Typography variant="heading24SemiBold">{idle ? (recipe?.name ?? "No recipe chosen") : status.recipeName}</Typography>
|
|
231
|
+
{phase === "stopping" && <Typography variant="paragraph18Medium">The emergency stop halts at once.</Typography>}
|
|
232
|
+
<Box sx={{ display: "grid", gridTemplateColumns: "repeat(2, minmax(0, 1fr))", gap: 4 }}>
|
|
233
|
+
<Counter label="Cycles done" value={idle ? "–" : `${status.cyclesDone} of ${status.cycleCount}`} />
|
|
234
|
+
<Counter
|
|
235
|
+
label="Rate"
|
|
236
|
+
value={lastCycleSeconds > 0 ? `${Math.round(3600 / lastCycleSeconds)} parts/h` : idle ? "–" : "After cycle 1"}
|
|
237
|
+
/>
|
|
182
238
|
</Box>
|
|
183
239
|
</Box>
|
|
184
|
-
<StepRail step={status.step} stopping={stopping} />
|
|
185
|
-
<CycleProgress cycle={status.cycle} cycleCount={status.cycleCount} stopping={stopping} />
|
|
186
|
-
</Box>
|
|
187
|
-
) : (
|
|
188
|
-
<Box sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
|
|
189
|
-
<Typography variant="heading24SemiBold">{status?.recipeName || "No recipe running"}</Typography>
|
|
190
|
-
<Typography variant="paragraph18Medium">
|
|
191
|
-
Last cycle: {status && status.lastCycleSeconds > 0 ? `${status.lastCycleSeconds.toFixed(1)} s` : "none yet"}
|
|
192
|
-
</Typography>
|
|
193
240
|
</Box>
|
|
194
|
-
|
|
241
|
+
<StepRail step={idle ? "" : status.step} phase={phase} />
|
|
242
|
+
<CycleProgress
|
|
243
|
+
cycle={status?.cycle ?? 0}
|
|
244
|
+
done={status?.cyclesDone ?? 0}
|
|
245
|
+
cycleCount={idle ? (recipe?.cycleCount ?? 0) : status.cycleCount}
|
|
246
|
+
phase={phase}
|
|
247
|
+
/>
|
|
248
|
+
</Box>
|
|
195
249
|
<VentionSelect
|
|
196
250
|
size="xx-large"
|
|
197
251
|
variant="outlined"
|
|
@@ -158,10 +158,11 @@ message StationStatus {
|
|
|
158
158
|
string recipe_name = 2;
|
|
159
159
|
int32 cycle = 3;
|
|
160
160
|
int32 cycle_count = 4;
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
double
|
|
164
|
-
|
|
161
|
+
int32 cycles_done = 5;
|
|
162
|
+
string step = 6;
|
|
163
|
+
double cycle_elapsed_seconds = 7;
|
|
164
|
+
double last_cycle_seconds = 8;
|
|
165
|
+
string fault = 9;
|
|
165
166
|
}
|
|
166
167
|
|
|
167
168
|
message Event {
|
|
@@ -63,6 +63,7 @@ class StationStatus(BaseModel):
|
|
|
63
63
|
recipe_name: str
|
|
64
64
|
cycle: int
|
|
65
65
|
cycle_count: int
|
|
66
|
+
cycles_done: int = 0
|
|
66
67
|
step: str
|
|
67
68
|
cycle_elapsed_seconds: float
|
|
68
69
|
last_cycle_seconds: float
|
|
@@ -91,6 +92,7 @@ class IndexingStation(StateMachine):
|
|
|
91
92
|
self.machine: Optional[Machine] = None
|
|
92
93
|
self.recipe: Optional[Recipe] = None
|
|
93
94
|
self.cycle = 0
|
|
95
|
+
self.cycles_done = 0
|
|
94
96
|
self.step = ""
|
|
95
97
|
self.cycle_started = 0.0
|
|
96
98
|
self.last_cycle_seconds = 0.0
|
|
@@ -104,6 +106,7 @@ class IndexingStation(StateMachine):
|
|
|
104
106
|
recipe_name=recipe.name if recipe else "",
|
|
105
107
|
cycle=self.cycle,
|
|
106
108
|
cycle_count=recipe.cycle_count if recipe else 0,
|
|
109
|
+
cycles_done=self.cycles_done,
|
|
107
110
|
step=self.step,
|
|
108
111
|
cycle_elapsed_seconds=time.monotonic() - self.cycle_started if self.step else 0.0,
|
|
109
112
|
last_cycle_seconds=self.last_cycle_seconds,
|
|
@@ -186,6 +189,7 @@ class IndexingStation(StateMachine):
|
|
|
186
189
|
await self.enter_step("Release")
|
|
187
190
|
await asyncio.to_thread(self.release)
|
|
188
191
|
self.last_cycle_seconds = time.monotonic() - self.cycle_started
|
|
192
|
+
self.cycles_done = cycle
|
|
189
193
|
await publish_status()
|
|
190
194
|
await publish_event("info", f"Cycle {cycle}/{recipe.cycle_count} done", f"Took {self.last_cycle_seconds:.1f} s.")
|
|
191
195
|
await asyncio.to_thread(self.release)
|
|
@@ -248,6 +252,7 @@ async def start(request: StartRequest) -> CommandResponse:
|
|
|
248
252
|
return CommandResponse(accepted=False, message=f"no recipe {request.recipe_id}")
|
|
249
253
|
station.recipe = recipe
|
|
250
254
|
station.cycle = 0
|
|
255
|
+
station.cycles_done = 0
|
|
251
256
|
station.trigger(Triggers.run.name)
|
|
252
257
|
return CommandResponse(accepted=True, message=f"running {recipe.name}")
|
|
253
258
|
|
|
@@ -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.
|
|
6
|
+
version: "0.10.9"
|
|
7
7
|
author: Vention
|
|
8
8
|
---
|
|
9
9
|
|