@vention/vention-skills 0.1.0

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.
Files changed (35) hide show
  1. package/.claude-plugin/marketplace.json +19 -0
  2. package/.claude-plugin/plugin.json +15 -0
  3. package/README.md +61 -0
  4. package/mcp.json +9 -0
  5. package/package.json +19 -0
  6. package/plugin.json +12 -0
  7. package/skills/vention-design/SKILL.md +522 -0
  8. package/skills/vention-machine-logic/SKILL.md +256 -0
  9. package/skills/vention-machine-logic/examples/conveyor-with-sensor/main.py +58 -0
  10. package/skills/vention-machine-logic/examples/homing-and-indexing/main.py +51 -0
  11. package/skills/vention-machine-logic/examples/parallel-conveyor-with-recipe/main.py +118 -0
  12. package/skills/vention-machine-logic/examples/parallel-conveyor-with-recipe/requirements.txt +1 -0
  13. package/skills/vention-machine-logic/examples/pick-and-place-state-machine/main.py +159 -0
  14. package/skills/vention-machine-logic/examples/pick-and-place-state-machine/requirements.txt +2 -0
  15. package/skills/vention-machine-logic/scripts/library-readme.py +233 -0
  16. package/skills/vention-machine-logic/scripts/vention-docs.py +176 -0
  17. package/skills/vention-machine-logic-hmi/SKILL.md +210 -0
  18. package/skills/vention-machine-logic-hmi/examples/indexing-station-hmi/customui/buf.gen.yaml +5 -0
  19. package/skills/vention-machine-logic-hmi/examples/indexing-station-hmi/customui/index.html +11 -0
  20. package/skills/vention-machine-logic-hmi/examples/indexing-station-hmi/customui/package.json +34 -0
  21. package/skills/vention-machine-logic-hmi/examples/indexing-station-hmi/customui/src/app.tsx +152 -0
  22. package/skills/vention-machine-logic-hmi/examples/indexing-station-hmi/customui/src/client.ts +17 -0
  23. package/skills/vention-machine-logic-hmi/examples/indexing-station-hmi/customui/src/main.tsx +16 -0
  24. package/skills/vention-machine-logic-hmi/examples/indexing-station-hmi/customui/src/pages/logs-page.tsx +52 -0
  25. package/skills/vention-machine-logic-hmi/examples/indexing-station-hmi/customui/src/pages/recipes-page.tsx +139 -0
  26. package/skills/vention-machine-logic-hmi/examples/indexing-station-hmi/customui/src/pages/run-page.tsx +65 -0
  27. package/skills/vention-machine-logic-hmi/examples/indexing-station-hmi/customui/src/station.ts +34 -0
  28. package/skills/vention-machine-logic-hmi/examples/indexing-station-hmi/customui/src/use-stream.ts +54 -0
  29. package/skills/vention-machine-logic-hmi/examples/indexing-station-hmi/customui/tsconfig.json +24 -0
  30. package/skills/vention-machine-logic-hmi/examples/indexing-station-hmi/models.py +13 -0
  31. package/skills/vention-machine-logic-hmi/examples/indexing-station-hmi/project.json +6 -0
  32. package/skills/vention-machine-logic-hmi/examples/indexing-station-hmi/proto/app.proto +192 -0
  33. package/skills/vention-machine-logic-hmi/examples/indexing-station-hmi/requirements.txt +5 -0
  34. package/skills/vention-machine-logic-hmi/examples/indexing-station-hmi/server.py +242 -0
  35. package/skills/vention-monitoring/SKILL.md +82 -0
@@ -0,0 +1,192 @@
1
+ syntax = "proto3";
2
+ package vention.app.v1;
3
+
4
+ import "google/protobuf/empty.proto";
5
+
6
+
7
+ message StartRequest {
8
+ int32 recipe_id = 1;
9
+ }
10
+
11
+ message CommandResponse {
12
+ bool accepted = 1;
13
+ string message = 2;
14
+ }
15
+
16
+ message StateResponse {
17
+ string state = 1;
18
+ string last_state = 2;
19
+ }
20
+
21
+ message HistoryEntry {
22
+ string timestamp = 1;
23
+ string state = 2;
24
+ int32 duration_ms = 3;
25
+ }
26
+
27
+ message HistoryResponse {
28
+ repeated HistoryEntry history = 1;
29
+ int32 buffer_size = 2;
30
+ }
31
+
32
+ message TriggerResponse {
33
+ string result = 1;
34
+ string previous_state = 2;
35
+ string new_state = 3;
36
+ }
37
+
38
+ message CrudListRequest {
39
+ bool include_deleted = 1;
40
+ int32 limit = 2;
41
+ int32 offset = 3;
42
+ string order_by = 4;
43
+ bool order_desc = 5;
44
+ }
45
+
46
+ message Recipe {
47
+ int32 id = 1;
48
+ string name = 2;
49
+ double index_distance_mm = 3;
50
+ double speed_mm_s = 4;
51
+ int32 dwell_ms = 5;
52
+ int32 cycle_count = 6;
53
+ }
54
+
55
+ message RecipeListResponse {
56
+ repeated Recipe records = 1;
57
+ }
58
+
59
+ message FilterCondition {
60
+ string field = 1;
61
+ string operation = 2;
62
+ string value = 3;
63
+ }
64
+
65
+ message CrudFindRequest {
66
+ repeated FilterCondition filters = 1;
67
+ bool include_deleted = 2;
68
+ int32 limit = 3;
69
+ int32 offset = 4;
70
+ string order_by = 5;
71
+ bool order_desc = 6;
72
+ }
73
+
74
+ message CrudGetRequest {
75
+ int32 record_id = 1;
76
+ bool include_deleted = 2;
77
+ }
78
+
79
+ message RecipeGetResponse {
80
+ Recipe record = 1;
81
+ }
82
+
83
+ message RecipeCreateRequest {
84
+ Recipe record = 1;
85
+ string actor = 2;
86
+ }
87
+
88
+ message RecipeCreateResponse {
89
+ Recipe record = 1;
90
+ }
91
+
92
+ message RecipeUpdateRequest {
93
+ int32 record_id = 1;
94
+ Recipe record = 2;
95
+ string actor = 3;
96
+ }
97
+
98
+ message RecipeUpdateResponse {
99
+ Recipe record = 1;
100
+ }
101
+
102
+ message CrudDeleteRequest {
103
+ int32 record_id = 1;
104
+ string actor = 2;
105
+ }
106
+
107
+ message RecipeStatusResponse {
108
+ string status = 1;
109
+ }
110
+
111
+ message CrudRestoreRequest {
112
+ int32 record_id = 1;
113
+ string actor = 2;
114
+ }
115
+
116
+ message RecipeRestoreResponse {
117
+ Recipe record = 1;
118
+ }
119
+
120
+ message HealthResponse {
121
+ string status = 1;
122
+ }
123
+
124
+ message AuditQueryRequest {
125
+ string component = 1;
126
+ int32 record_id = 2;
127
+ string actor = 3;
128
+ string operation = 4;
129
+ string since = 5;
130
+ string until = 6;
131
+ int32 limit = 7;
132
+ int32 offset = 8;
133
+ }
134
+
135
+ message AuditListResponse {
136
+ repeated string records = 1;
137
+ }
138
+
139
+ message FileResponse {
140
+ string filename = 1;
141
+ bytes bytes = 2;
142
+ }
143
+
144
+ message DatabaseRestoreRequest {
145
+ bytes bytes = 1;
146
+ string filename = 2;
147
+ bool integrity_check = 3;
148
+ bool dry_run = 4;
149
+ }
150
+
151
+ message DatabaseRestoreResponse {
152
+ bool ok = 1;
153
+ string message = 2;
154
+ }
155
+
156
+ message StationStatus {
157
+ string state = 1;
158
+ string recipe_name = 2;
159
+ int32 cycle = 3;
160
+ int32 cycle_count = 4;
161
+ double last_cycle_seconds = 5;
162
+ string fault = 6;
163
+ }
164
+
165
+ message Event {
166
+ string timestamp = 1;
167
+ string level = 2;
168
+ string message = 3;
169
+ string detail = 4;
170
+ }
171
+
172
+ service IndexingStationService {
173
+ rpc Start (StartRequest) returns (CommandResponse);
174
+ rpc GetState (google.protobuf.Empty) returns (StateResponse);
175
+ rpc GetHistory (google.protobuf.Empty) returns (HistoryResponse);
176
+ rpc Trigger_Stop (google.protobuf.Empty) returns (TriggerResponse);
177
+ rpc Trigger_Reset (google.protobuf.Empty) returns (TriggerResponse);
178
+ rpc Recipe_ListRecords (CrudListRequest) returns (RecipeListResponse);
179
+ rpc Recipe_FindRecords (CrudFindRequest) returns (RecipeListResponse);
180
+ rpc Recipe_GetRecord (CrudGetRequest) returns (RecipeGetResponse);
181
+ rpc Recipe_CreateRecord (RecipeCreateRequest) returns (RecipeCreateResponse);
182
+ rpc Recipe_UpdateRecord (RecipeUpdateRequest) returns (RecipeUpdateResponse);
183
+ rpc Recipe_DeleteRecord (CrudDeleteRequest) returns (RecipeStatusResponse);
184
+ rpc Recipe_RestoreRecord (CrudRestoreRequest) returns (RecipeRestoreResponse);
185
+ rpc Database_Health (google.protobuf.Empty) returns (HealthResponse);
186
+ rpc Database_ReadAudit (AuditQueryRequest) returns (AuditListResponse);
187
+ rpc Database_ExportZip (google.protobuf.Empty) returns (FileResponse);
188
+ rpc Database_BackupSqlite (google.protobuf.Empty) returns (FileResponse);
189
+ rpc Database_RestoreSqlite (DatabaseRestoreRequest) returns (DatabaseRestoreResponse);
190
+ rpc Status (google.protobuf.Empty) returns (stream StationStatus);
191
+ rpc Events (google.protobuf.Empty) returns (stream Event);
192
+ }
@@ -0,0 +1,5 @@
1
+ machine-logic-sdk==3.1.0
2
+ vention-communication==0.15.6
3
+ vention-state-machine==0.15.6
4
+ vention-storage==0.18.6
5
+ uvicorn==0.35.0
@@ -0,0 +1,242 @@
1
+ """Indexing station with an operator HMI: index, clamp, prove the clamp, dwell, release, per recipe cycle.
2
+
3
+ Needs actuator "Conveyor", output "Clamp" and input "Clamp Closed"; customui/ calls it only through the generated client.
4
+ """
5
+
6
+ import asyncio
7
+ import time
8
+ from contextlib import asynccontextmanager
9
+ from datetime import datetime, timezone
10
+ from pathlib import Path
11
+ from typing import AsyncIterator, Optional
12
+
13
+ from fastapi import FastAPI
14
+ from pydantic import BaseModel
15
+
16
+ from communication.app import VentionApp
17
+ from communication.decorators import action, stream
18
+ from machinelogic import Machine, MotionProfile
19
+ from state_machine.core import BaseStates, BaseTriggers, StateMachine
20
+ from state_machine.decorators import on_enter_state, on_state_change
21
+ from state_machine.defs import State, StateGroup, Trigger
22
+ from state_machine.vention_communication import build_state_machine_rpc_bundle
23
+ from storage.accessor import ModelAccessor
24
+ from storage.bootstrap import bootstrap
25
+ from storage.vention_communication import build_storage_rpc_bundle
26
+
27
+ from models import Recipe
28
+
29
+ CLAMP_SETTLE_S = 0.3
30
+ CLAMP_PROOF_TIMEOUT_S = 2.0
31
+ CLAMP_POLL_S = 0.05
32
+ # The HMI treats a Status stream that stays silent for three heartbeats as dropped.
33
+ STATUS_HEARTBEAT_S = 2.0
34
+ ACCELERATION_MM_S2 = 400.0
35
+ DATA_DIR = Path(__file__).parent / "data"
36
+
37
+
38
+ class Station(StateGroup):
39
+ running: State = State()
40
+ stopping: State = State()
41
+
42
+
43
+ class States:
44
+ station = Station()
45
+
46
+
47
+ class Triggers:
48
+ run = Trigger("run")
49
+ stop = Trigger("stop")
50
+ finished = Trigger("finished")
51
+
52
+
53
+ TRANSITIONS = [
54
+ Triggers.run.transition(BaseStates.READY.value, States.station.running),
55
+ Triggers.stop.transition(States.station.running, States.station.stopping),
56
+ Triggers.finished.transition(States.station.running, BaseStates.READY.value),
57
+ Triggers.finished.transition(States.station.stopping, BaseStates.READY.value),
58
+ ]
59
+
60
+
61
+ class StationStatus(BaseModel):
62
+ state: str
63
+ recipe_name: str
64
+ cycle: int
65
+ cycle_count: int
66
+ last_cycle_seconds: float
67
+ fault: str
68
+
69
+
70
+ class Event(BaseModel):
71
+ timestamp: str
72
+ level: str
73
+ message: str
74
+ detail: str
75
+
76
+
77
+ class StartRequest(BaseModel):
78
+ recipe_id: int
79
+
80
+
81
+ class CommandResponse(BaseModel):
82
+ accepted: bool
83
+ message: str
84
+
85
+
86
+ class IndexingStation(StateMachine):
87
+ def __init__(self) -> None:
88
+ super().__init__(states=States, transitions=TRANSITIONS)
89
+ self.machine: Optional[Machine] = None
90
+ self.recipe: Optional[Recipe] = None
91
+ self.cycle = 0
92
+ self.last_cycle_seconds = 0.0
93
+ self.fault_message = ""
94
+ self.stop_requested = False
95
+
96
+ def snapshot(self) -> StationStatus:
97
+ recipe = self.recipe
98
+ return StationStatus(
99
+ state=str(self.state),
100
+ recipe_name=recipe.name if recipe else "",
101
+ cycle=self.cycle,
102
+ cycle_count=recipe.cycle_count if recipe else 0,
103
+ last_cycle_seconds=self.last_cycle_seconds,
104
+ fault=self.fault_message,
105
+ )
106
+
107
+ @on_state_change
108
+ def publish_state_change(self, _old_state: str, _new_state: str, _trigger: str) -> None:
109
+ self.spawn(publish_status())
110
+
111
+ @on_enter_state(BaseStates.READY.value)
112
+ def enter_ready(self, _: object) -> None:
113
+ self.fault_message = ""
114
+
115
+ @on_enter_state(States.station.running)
116
+ def enter_running(self, _: object) -> None:
117
+ self.stop_requested = False
118
+ self.spawn(self.run_cycles())
119
+
120
+ @on_enter_state(States.station.stopping)
121
+ def enter_stopping(self, _: object) -> None:
122
+ self.stop_requested = True
123
+
124
+ def connected_machine(self) -> Machine:
125
+ if self.machine is None:
126
+ raise RuntimeError("the machine is not connected")
127
+ return self.machine
128
+
129
+ def index_part(self, recipe: Recipe) -> None:
130
+ conveyor = self.connected_machine().get_actuator("Conveyor")
131
+ conveyor.move_relative(recipe.index_distance_mm, MotionProfile(velocity=recipe.speed_mm_s, acceleration=ACCELERATION_MM_S2))
132
+
133
+ def clamp_part(self) -> None:
134
+ machine = self.connected_machine()
135
+ clamp_closed = machine.get_input("Clamp Closed")
136
+ machine.get_output("Clamp").write(True)
137
+ time.sleep(CLAMP_SETTLE_S)
138
+ deadline = time.monotonic() + CLAMP_PROOF_TIMEOUT_S
139
+ while not clamp_closed.state.value:
140
+ if time.monotonic() > deadline:
141
+ raise RuntimeError(f"clamp never proved closed within {CLAMP_PROOF_TIMEOUT_S} s")
142
+ time.sleep(CLAMP_POLL_S)
143
+
144
+ def release(self) -> None:
145
+ if self.machine is not None:
146
+ self.machine.get_output("Clamp").write(False)
147
+
148
+ async def release_safely(self) -> None:
149
+ try:
150
+ await asyncio.to_thread(self.release)
151
+ except Exception as error:
152
+ await publish_event("error", "Clamp release failed", f"{error}. Check the clamp is open before the next run.")
153
+
154
+ async def run_cycles(self) -> None:
155
+ recipe = self.recipe
156
+ if recipe is None:
157
+ return
158
+ try:
159
+ for cycle in range(1, recipe.cycle_count + 1):
160
+ if self.stop_requested:
161
+ break
162
+ started = time.monotonic()
163
+ self.cycle = cycle
164
+ await publish_status()
165
+ await asyncio.to_thread(self.index_part, recipe)
166
+ if self.stop_requested:
167
+ break
168
+ await asyncio.to_thread(self.clamp_part)
169
+ if self.stop_requested:
170
+ break
171
+ await asyncio.sleep(recipe.dwell_ms / 1000)
172
+ await asyncio.to_thread(self.release)
173
+ self.last_cycle_seconds = time.monotonic() - started
174
+ await publish_status()
175
+ await publish_event("info", f"Cycle {cycle}/{recipe.cycle_count} done", f"Took {self.last_cycle_seconds:.1f} s.")
176
+ await asyncio.to_thread(self.release)
177
+ self.trigger(Triggers.finished.name)
178
+ except asyncio.CancelledError:
179
+ await self.release_safely()
180
+ raise
181
+ except Exception as error:
182
+ await self.release_safely()
183
+ self.fault_message = f"cycle {self.cycle}: {error}"
184
+ await publish_event("error", f"Cycle {self.cycle} faulted", f"{error}. Clear the cause, then press Reset on the Run page.")
185
+ self.trigger(BaseTriggers.TO_FAULT.value)
186
+
187
+
188
+ DATA_DIR.mkdir(exist_ok=True)
189
+ bootstrap(database_url=f"sqlite:///{DATA_DIR / 'recipes.db'}")
190
+ recipe_accessor = ModelAccessor(Recipe, "recipe")
191
+ station = IndexingStation()
192
+
193
+
194
+ async def repeat_status() -> None:
195
+ while True:
196
+ await asyncio.sleep(STATUS_HEARTBEAT_S)
197
+ await publish_status()
198
+
199
+
200
+ @asynccontextmanager
201
+ async def lifespan(_: FastAPI) -> AsyncIterator[None]:
202
+ try:
203
+ station.machine = await asyncio.to_thread(Machine)
204
+ except Exception as error:
205
+ station.fault_message = f"cannot reach the controller: {error}"
206
+ await publish_status()
207
+ heartbeat = asyncio.create_task(repeat_status())
208
+ yield
209
+ heartbeat.cancel()
210
+
211
+
212
+ app = VentionApp(name="IndexingStation", emit_proto=True, proto_path="proto/app.proto", lifespan=lifespan)
213
+
214
+
215
+ @stream(name="Status", payload=StationStatus, replay=True, queue_maxsize=1, policy="latest")
216
+ async def publish_status() -> StationStatus:
217
+ return station.snapshot()
218
+
219
+
220
+ @stream(name="Events", payload=Event, replay=False, queue_maxsize=100, policy="fifo")
221
+ async def publish_event(level: str, message: str, detail: str) -> Event:
222
+ return Event(timestamp=datetime.now(timezone.utc).isoformat(), level=level, message=message, detail=detail)
223
+
224
+
225
+ @action("Start")
226
+ async def start(request: StartRequest) -> CommandResponse:
227
+ if station.machine is None:
228
+ return CommandResponse(accepted=False, message="the machine is not connected")
229
+ if station.state != BaseStates.READY.value:
230
+ return CommandResponse(accepted=False, message=f"cannot start from {station.state}")
231
+ recipe = recipe_accessor.get(request.recipe_id)
232
+ if recipe is None:
233
+ return CommandResponse(accepted=False, message=f"no recipe {request.recipe_id}")
234
+ station.recipe = recipe
235
+ station.cycle = 0
236
+ station.trigger(Triggers.run.name)
237
+ return CommandResponse(accepted=True, message=f"running {recipe.name}")
238
+
239
+
240
+ app.register_rpc_plugin(build_state_machine_rpc_bundle(station, triggers=[Triggers.stop.name, BaseTriggers.RESET.value]))
241
+ app.register_rpc_plugin(build_storage_rpc_bundle(accessors=[recipe_accessor], max_records_per_model=None))
242
+ app.finalize()
@@ -0,0 +1,82 @@
1
+ ---
2
+ name: vention-monitoring
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
+ license: MIT
5
+ metadata:
6
+ version: "0.1.0"
7
+ author: Vention
8
+ ---
9
+
10
+ # Troubleshooting Deployed Vention Machines
11
+
12
+ Diagnose machines in the field through the Vention MCP server.
13
+
14
+ ## Get the serial number first
15
+
16
+ Every tool here needs a MachineMotion serial number, for example `6060009`. Ask for it
17
+ before doing anything else. Do not diagnose from a description alone and do not guess a
18
+ serial number.
19
+
20
+ ## Tools
21
+
22
+ | Tool | Use it for |
23
+ | --- | --- |
24
+ | `machine_status` | Current state — always start here, it is cheap |
25
+ | `machine_history` | State changes, run time, cycle data, counter deltas over a window |
26
+ | `machine_health` | CPU load and temperature, memory, disk, per-drive temperature |
27
+ | `machine_activity_log` | Readable event feed — brake released, drive ready, state changed |
28
+ | `machine_metric` | Raw series for one numeric metric key, including custom counters |
29
+ | `machine_logs_request` / `machine_logs_ready` | Full log archive when the above is not enough |
30
+
31
+ Time windows are 24h, 7d, 30d, 90d, or 1y, in UTC. `machine_activity_log` is the
32
+ exception: it supports 15 minutes to 2 days only, and longer windows return nothing.
33
+ `machine_metric` accepts numeric keys only — a state key will fail.
34
+
35
+ ## Read timestamps before concluding anything
36
+
37
+ `machine_status` is a snapshot, not a history. **Every key carries its own timestamp;
38
+ there is no shared "as of" moment.** A value can be hours or months old.
39
+
40
+ - An old timestamp means the controller stopped reporting that key — not that the state
41
+ just changed.
42
+ - If machine state is `offline`, the controller is not connected right now and every
43
+ other value is simply the last one it sent. Say this explicitly rather than reporting
44
+ stale values as current.
45
+
46
+ ## Triage order
47
+
48
+ 1. `machine_status` — is it online, are drives ready, what is the application doing?
49
+ 2. If offline → check comms route, then network and power at the site.
50
+ 3. If drives not ready → check E-stop state, then drive faults.
51
+ 4. If the application is not running → `machine_activity_log` over a short window.
52
+ 5. If it runs but underperforms → `machine_history` for cycle and run-time trends, then
53
+ `machine_metric` for the specific number in question.
54
+ 6. If nothing explains it → `machine_logs_request`.
55
+
56
+ ## Rules
57
+
58
+ - Distinguish "the controller reports X" from "the cause is X". Never state a cause the
59
+ data does not support.
60
+ - A snapshot cannot tell you when something changed, how often, or how the machine
61
+ behaved over time. Do not infer any of those from `machine_status`.
62
+ - Report what the data shows, what it does not show, and what you would check next.
63
+ - Known discrepancy: run-time e-stop duration and the machine-state stream do not always
64
+ reconcile, and either can be wrong. Treat stream-derived e-stop time as a floor, and
65
+ say so if it matters to the conclusion.
66
+
67
+ ## Safety
68
+
69
+ - Never instruct anyone to bypass a safety device or clear a fault they cannot see.
70
+ - Never suggest re-enabling or restarting a machine remotely. Someone may be inside the
71
+ cell. If remediation needs physical access, say that a qualified person must confirm
72
+ the cell is clear first.
73
+ - When the data is ambiguous and the machine is down, escalate to Vention support rather
74
+ than guessing.
75
+
76
+ ## Common mistakes
77
+
78
+ - Reading stale timestamps as current state.
79
+ - Diagnosing an offline machine from its last-known values.
80
+ - Confusing application state with machine state.
81
+ - Requesting a 7-day `machine_activity_log` window and reporting "no events" when the
82
+ call simply exceeded the supported range.