@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.
- package/.claude-plugin/marketplace.json +19 -0
- package/.claude-plugin/plugin.json +15 -0
- package/README.md +61 -0
- package/mcp.json +9 -0
- package/package.json +19 -0
- package/plugin.json +12 -0
- package/skills/vention-design/SKILL.md +522 -0
- package/skills/vention-machine-logic/SKILL.md +256 -0
- package/skills/vention-machine-logic/examples/conveyor-with-sensor/main.py +58 -0
- package/skills/vention-machine-logic/examples/homing-and-indexing/main.py +51 -0
- package/skills/vention-machine-logic/examples/parallel-conveyor-with-recipe/main.py +118 -0
- package/skills/vention-machine-logic/examples/parallel-conveyor-with-recipe/requirements.txt +1 -0
- package/skills/vention-machine-logic/examples/pick-and-place-state-machine/main.py +159 -0
- package/skills/vention-machine-logic/examples/pick-and-place-state-machine/requirements.txt +2 -0
- package/skills/vention-machine-logic/scripts/library-readme.py +233 -0
- package/skills/vention-machine-logic/scripts/vention-docs.py +176 -0
- package/skills/vention-machine-logic-hmi/SKILL.md +210 -0
- package/skills/vention-machine-logic-hmi/examples/indexing-station-hmi/customui/buf.gen.yaml +5 -0
- package/skills/vention-machine-logic-hmi/examples/indexing-station-hmi/customui/index.html +11 -0
- package/skills/vention-machine-logic-hmi/examples/indexing-station-hmi/customui/package.json +34 -0
- package/skills/vention-machine-logic-hmi/examples/indexing-station-hmi/customui/src/app.tsx +152 -0
- package/skills/vention-machine-logic-hmi/examples/indexing-station-hmi/customui/src/client.ts +17 -0
- package/skills/vention-machine-logic-hmi/examples/indexing-station-hmi/customui/src/main.tsx +16 -0
- package/skills/vention-machine-logic-hmi/examples/indexing-station-hmi/customui/src/pages/logs-page.tsx +52 -0
- package/skills/vention-machine-logic-hmi/examples/indexing-station-hmi/customui/src/pages/recipes-page.tsx +139 -0
- package/skills/vention-machine-logic-hmi/examples/indexing-station-hmi/customui/src/pages/run-page.tsx +65 -0
- package/skills/vention-machine-logic-hmi/examples/indexing-station-hmi/customui/src/station.ts +34 -0
- package/skills/vention-machine-logic-hmi/examples/indexing-station-hmi/customui/src/use-stream.ts +54 -0
- package/skills/vention-machine-logic-hmi/examples/indexing-station-hmi/customui/tsconfig.json +24 -0
- package/skills/vention-machine-logic-hmi/examples/indexing-station-hmi/models.py +13 -0
- package/skills/vention-machine-logic-hmi/examples/indexing-station-hmi/project.json +6 -0
- package/skills/vention-machine-logic-hmi/examples/indexing-station-hmi/proto/app.proto +192 -0
- package/skills/vention-machine-logic-hmi/examples/indexing-station-hmi/requirements.txt +5 -0
- package/skills/vention-machine-logic-hmi/examples/indexing-station-hmi/server.py +242 -0
- 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,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.
|