@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,256 @@
1
+ ---
2
+ name: vention-machine-logic
3
+ description: Write, edit, validate, and deploy MachineLogic Python applications for Vention machines with the machine-logic-sdk and the Vention CLI. Use when the user wants to program a Vention machine or MachineMotion controller, drive actuators, conveyors, robots, pneumatics, or digital I/O from Python, build a state machine for a cell, debug a failing application from its logs, or pull, push, or link an application with the Vention CLI (vention or vn). Trigger on MachineLogic, machine-logic-sdk, MachineMotion, MachineBuilder applications, or the Vention CLI, including inside a directory that holds .machine-code-app-directory-info.json. Do not use when the user has asked for no program at all and only wants an existing machine, its parts, or its configuration explained back to them in words; a read-only script that prints what the machine reports is still a program.
4
+ license: MIT
5
+ metadata:
6
+ version: "0.1.0"
7
+ machine-logic-sdk: ">=3.0,<4"
8
+ author: Vention
9
+ ---
10
+
11
+ # Vention MachineLogic applications
12
+
13
+ A MachineLogic application is a Python program that drives a Vention machine through the `machinelogic` package (installed as `machine-logic-sdk`). It runs on the machine's MachineMotion AI controller or on the design's digital twin, and it travels between the user's disk and the design through the Vention CLI (`vention`).
14
+
15
+ ## Before you start
16
+
17
+ Work only inside a directory the Vention CLI has linked to an application. The marker is `.machine-code-app-directory-info.json` in the directory root.
18
+
19
+ 1. If the marker is missing, ask the user to run `vention pull` (downloads the application and its configuration) or `vention link` (attaches an existing folder) in that directory, or to give you the path of a directory that already has it. Never create the marker or an application layout by hand: `vention push` cannot round-trip a directory the CLI did not link.
20
+ 2. If `vention` says it is not logged in, the user runs `vention login`.
21
+ 3. The design must be open in MachineBuilder with its MachineLogic tab active while pulling, pushing, or reading logs; that is what keeps the design's twin reachable.
22
+
23
+ ## Read the configuration first
24
+
25
+ `vention pull` writes `.vention-design-configuration.json` next to the code. Read it before writing any device call. Every actuator, robot, input, output, pneumatic, and AC motor is addressed by the `friendlyName` in that file, character for character. A wrong name fails at run time with a `MachineException`, not at write time.
26
+
27
+ - Never guess or tidy a name. `"Gripper Vaccum"` in the configuration means `machine.get_output("Gripper Vaccum")` in the code.
28
+ - Inputs and outputs use the pin's friendly name (`"Input A1"`), not the name of the device wired to it.
29
+ - If the file is missing, ask the user to run `vention pull` again rather than inventing names.
30
+
31
+ ## The CLI at use time
32
+
33
+ Do not rely on a memorised command list. At the start of a session run `vention --version` and `vention --help`, and run `vention <command> --help` before using a command for the first time. Describe only what the installed version prints.
34
+
35
+ - If `vention` is not on the PATH, tell the user to install it with `npm install -g @vention/vention-cli`, then stop describing commands they do not have.
36
+ - Older versions lack commands newer ones have. If `--help` does not list a command you expected, say so instead of using it, and offer the upgrade: `npm install -g @vention/vention-cli@latest`.
37
+ - `vention` and `vn` are the same binary; show the user the long form.
38
+
39
+ ## The SDK version comes from the application
40
+
41
+ Every version the target installs is declared by the application: `requirements.txt` for Python, `customui/package.json` for the HMI bundle. The SDK is one of those dependencies, not a property of the machine. When a `requirements.txt` exists, the execution engine builds the application a dedicated environment from exactly that file, so the pin decides the version rather than describing it.
42
+
43
+ Read `machine-logic-sdk==3.1.0` from `requirements.txt`. That is the answer. A `uv.lock` answers too when the project is uv-managed on the authoring side, but the target never installs from it: the engine compiles `requirements.txt` into a lock of its own inside the venv. A range such as `>=3.0` is a constraint, not a version, so treat it as no pin. Never infer a version from the code you are reading: three majors are documented at once, and a signature taken from the wrong one compiles against nothing.
44
+
45
+ An application with no pin inherits whatever the target already has, and nothing on disk says what that is. Tell the user the version is undeclared and offer to add the pin. Until it is there, read the target and name the signal alongside the version:
46
+
47
+ - The startup line `vention logs` prints while the application runs.
48
+ - On a controller only, `softwareVersion` in `.vention-design-configuration.json`, read through the compatibility table on the version's documentation page. A digital twin hardcodes that field, so skip this signal there.
49
+ - `pip show machine-logic-sdk` on the user's laptop. That is the authoring environment, not the target, so label it unverified.
50
+
51
+ Say `unknown` when none of them answers.
52
+
53
+ Pin the version the target can install rather than the newest one: a controller has the SDK that came with its MMAI release, an existing application runs against the version it was written for, and pinned dependencies are fetched on the target, so an isolated cell resolves only what it already has. A `requirements.txt` that lists no `machine-logic-sdk` removes the SDK the application would otherwise inherit, so once that file exists the SDK belongs in it.
54
+
55
+ With the version in hand, fetch the reference published for it:
56
+
57
+ ```bash
58
+ python3 scripts/vention-docs.py --version 3.1.0 --print-page
59
+ python3 scripts/vention-docs.py --from-requirements requirements.txt --print-url
60
+ ```
61
+
62
+ It matches the published page by title, checks that the page body names that version before printing anything from it, and refuses to answer from a different version. On `unavailable: <reason>` (exit 2) the documentation is unreachable or publishes no page for that version: continue from this file alone and tell the user the reference was unavailable.
63
+
64
+ ## Library documentation at the pinned version
65
+
66
+ Vention's application libraries publish their documentation as their package README. Read the README of the version the application pins rather than recalling an API that moved between releases.
67
+
68
+ ```bash
69
+ python3 scripts/library-readme.py --from-app .
70
+ ```
71
+
72
+ It reads `requirements.txt` for `vention-state-machine`, `vention-storage`, and `vention-communication`, and `customui/package.json` (the HMI source folder; `ui/` next to it holds only the built bundle) for `@vention/machine-ui`, `@vention/machine-apps-components`, and `@vention/machine-logic-ui-sdk`, and prints each README under a `## <name>@<version>` heading. A dependency declared as a range is reported `unavailable` instead of answered from the newest release. `--pypi <name> <version>` and `--npm <name> <version>` read a single package.
73
+
74
+ ## The shape of an application
75
+
76
+ Write the code to `main.py` before you answer, every time the user asks for a program, a script or a readout — even a one-off, even a read-only one. Code that only appears in the reply cannot be type-checked, pushed, or run.
77
+
78
+ `main.py` is the entry point named in `project.json`. Resolve devices once from the configuration, keep speeds in named constants at the top, and make the shutdown path unconditional.
79
+
80
+ ```python
81
+ from machinelogic import Machine, MotionProfile, VentionException
82
+ from machinelogic.types import MachineSafetyState
83
+
84
+ TRAVEL_SPEED_MM_S = 100.0
85
+ TRAVEL_ACCELERATION_MM_S2 = 200.0
86
+
87
+ machine = Machine() # one per process; connects to the controller this application runs on
88
+ axis = machine.get_actuator("x") # friendlyName from .vention-design-configuration.json
89
+ part_present = machine.get_input("Input A1")
90
+ profile = MotionProfile(velocity=TRAVEL_SPEED_MM_S, acceleration=TRAVEL_ACCELERATION_MM_S2)
91
+
92
+ try:
93
+ axis.home(timeout=30) # absolute positions mean nothing before homing
94
+ for station_mm in (50.0, 150.0, 250.0):
95
+ # check the emergency stop once per pass in any loop that commands motion
96
+ if machine.state.safety_state == MachineSafetyState.EMERGENCY_STOP:
97
+ print("emergency stop active, leaving the cycle")
98
+ break
99
+ if not part_present.state.value:
100
+ axis.move_absolute(station_mm, profile)
101
+ except VentionException as error: # base class of every SDK exception
102
+ print(f"machine fault: {error}")
103
+ finally:
104
+ axis.stop() # the shutdown path runs whatever happened above
105
+ ```
106
+
107
+ `Machine()` is one per process. Once it fails, a retry in the same process fails too: the user restarts the application (stop, then play) to reconnect.
108
+
109
+ The SDK documentation for the target's version is fetched at use time (`scripts/vention-docs.py`, above) and the complete examples below show the rest of the shape; when a call is in neither, say the SDK does not expose it rather than inventing one.
110
+
111
+ An operator screen for the application is the `vention-machine-logic-hmi` skill's job. When the request asks for one, load that skill before writing the RPC surface and build the screen in the same task: the request is not done until both exist, so never stop between them to ask whether to go on.
112
+
113
+ ## Reaching a device
114
+
115
+ The getter follows the element's type in `.vention-design-configuration.json`, not the word the user reached for.
116
+
117
+ ```python
118
+ machine.get_actuator("x") # a linear or rotary axis
119
+ machine.get_robot("UR3e") # an arm
120
+ machine.get_pneumatic("Outfeed Stopper - Air") # a two-position air cylinder
121
+ machine.get_input("Input A1") # a digital input pin
122
+ machine.get_output("Gripper Vaccum") # a digital output pin
123
+ machine.get_ac_motor("Belt Drive")
124
+ machine.get_scene() # the robot's taught positions and frames
125
+ machine.get_machine_motion("MachineMotion AI").get_actuator("x") # scoped to one controller
126
+ ```
127
+
128
+ An air cylinder the configuration lists as a pneumatic is `get_pneumatic`, moved with `push_async()` and `pull_async()`; `IPneumatic` has no `extend()` or `retract()`. Drop to `get_output` on its solenoid pins only when the user asks for the raw signals rather than the cylinder. Go through `get_machine_motion` whenever the design carries more than one controller, or the user names the controller a device hangs off: the machine-wide lookup searches them all and is ambiguous there.
129
+
130
+ ## Moving, and when not to block
131
+
132
+ A move blocks. Reach for the `_async` half of one only when there is work to do while the machine travels, and finish it on the SDK's completion signal rather than a guessed sleep.
133
+
134
+ ```python
135
+ axis.move_absolute(180.0, profile) # also move_relative; returns on arrival
136
+ robot.movel(pose, 50.0, 100.0) # also movej, move_to_target
137
+ print(axis.state.position) # live position, readable at any time
138
+
139
+ axis.move_absolute_async(180.0, profile) # also move_relative_async
140
+ print(axis.state.move_in_progress) # True while it travels
141
+ axis.wait_for_move_completion(timeout=20) # actuators take a timeout
142
+
143
+ robot.movej_async(joint_angles_deg, 40.0, 80.0) # also movel_async, move_to_target_async
144
+ print(f"cycle {cycle} took {elapsed:.1f} s") # the work that was worth not blocking for
145
+ robot.wait_for_motion_completion() # robots take no arguments
146
+ ```
147
+
148
+ 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.
149
+
150
+ ## Reacting to an input edge
151
+
152
+ Polling can straddle a pulse that opens and closes between two reads. `on_state_change` fires on every transition, on a background thread, so the process has to stay alive for the edges to land in:
153
+
154
+ ```python
155
+ from machinelogic.ivention.idigital_input import IDigitalInput
156
+
157
+ def on_edge(value: bool, digital_input: IDigitalInput) -> None:
158
+ print(f"{digital_input.configuration.name} went {'high' if value else 'low'}")
159
+
160
+ part_present.on_state_change(on_edge)
161
+ ```
162
+
163
+ ## Doing several things at once
164
+
165
+ A plain sequence stays a plain synchronous script. When two or more things have to happen at the same time (two conveyors, a counter beside a cycle, a cycle that also listens for MQTT), run each as a task on one asyncio loop. The SDK has no coroutines: an `_async` method only sends the command and returns, and the waits block. Keep every blocking call off the loop, or it stalls every other task for as long as it blocks.
166
+
167
+ ```python
168
+ async def feed_board(conveyor: IActuator, line: LineState, travel_mm: float) -> None:
169
+ recipe = line.recipe
170
+ profile = MotionProfile(velocity=recipe.conveyor_speed_mm_s, acceleration=recipe.conveyor_acceleration_mm_s2)
171
+ conveyor.move_relative_async(travel_mm, profile) # quick: sends the move
172
+ await asyncio.to_thread(conveyor.wait_for_move_completion, 20) # the wait blocks a worker thread, not the loop
173
+ line.boards_out += 1 # quick calls and shared state stay on the loop
174
+ ```
175
+
176
+ A blocking call with no `_async` version, such as `home`, goes through `asyncio.to_thread` too; home once, before the tasks start.
177
+
178
+ - Start the tasks with `asyncio.gather` or `asyncio.wait`; the target runs Python 3.10, which has no `TaskGroup`.
179
+ - SDK callbacks (`on_state_change`, `machine.on_system_state_change`, MQTT) run on SDK threads. Hand their work to the loop with `loop.call_soon_threadsafe`, never by touching shared state from the callback.
180
+ - Cancelling a task does not stop a move a worker thread is waiting on. The shutdown path stops every device it started, in a `finally`.
181
+ - Keep shared state in one dataclass created in `main` and passed to each task. Only the loop's thread changes it, so it needs no lock.
182
+
183
+ **Recipes.** No Vention library defines a recipe. Write one frozen dataclass per parameter set in a `dict[str, Recipe]`, pick one at start or on an operator event, and read it through the shared state. When recipes must survive a restart, store them with `vention-storage` and read its README first (`library-readme.py`, above).
184
+
185
+ **State machines.** When the cell has modes (idle, running, paused, faulted) or reacts to events rather than running one sequence, use `vention-state-machine`: pin it in `requirements.txt`, read its README at that pin, and build on its `ready` and `fault` states. A machine has one active state, so concurrent machines are separate instances on the same loop. `examples/pick-and-place-state-machine` shows the shape; `examples/parallel-conveyor-with-recipe` shows parallel tasks with shared state and a recipe. If the README at that pin has no "State naming rules" section, give every member of a `StateGroup` a single word with no underscore: those versions split a nested state's name on `_`, so a member like `interlock_check` raises `KeyError: 'interlock'` when the machine is built.
186
+
187
+ ## Taught robot positions
188
+
189
+ `vention pull` also writes `.vention-design-scene-assets.json` when the design has taught positions. It is the list `machine.get_scene()` loads, one record per position or frame: its `type` names the getter that reads it (`RobotWaypoint` is `get_cartesian_target`, `RobotJointPosition` is `get_joint_target`, `RobotCalibrationFrame` is `get_calibration_frame`, `ReferenceFrame` is `get_reference_frame`) and `parameters.name` is the name to pass, character for character. Read it before naming a target. When a step needs a position the file does not hold, ask the user to teach it on the design; never map a missing step onto a target that happens to exist.
190
+
191
+ Positions taught on the design live in the scene under their friendly names. Drive off those names rather than copying pose numbers out of a pendant into the source. `movel` holds a straight line in Cartesian space; `movej` interpolates the joints and can swing the tool sideways on the way, so a lift off a part or an approach that has to come straight down is `movel`.
192
+
193
+ ```python
194
+ scene = machine.get_scene()
195
+ print(scene.get_cartesian_target("Pick").get_position(relative_to="robot_base")) # [x, y, z, rx, ry, rz]
196
+ print(scene.get_joint_target("home").get_joint_angles())
197
+ frame = scene.get_calibration_frame("Pick Frame")
198
+ print(frame.get_calibrated_value() or frame.get_default_value()) # None until the frame is calibrated
199
+ robot.move_to_target("Pick", "l", 150.0, 300.0) # "l" straight line, "j" joint space
200
+ pose, _timestamp = robot.state.cartesian_position_data # live pose, to compute a target from where it is
201
+ pose[2] += 20.0
202
+ robot.movel(pose, 50.0, 100.0)
203
+ ```
204
+
205
+ ## Complete examples
206
+
207
+ Each example is a standalone `main.py` whose module docstring names the device names it assumes. Copy the structure, then replace the names with the ones in the user's configuration.
208
+
209
+ - `examples/homing-and-indexing/main.py`: home one actuator, then index through three absolute positions, reading a sensor before each move, with a `finally` that stops the axis.
210
+ - `examples/pick-and-place-state-machine/main.py`: a `vention-state-machine` machine (ready, picking, placing, fault) driving one robot, one gripper output, and one part-present input on an asyncio loop, with the emergency stop routed to `fault` and the robot stopped on every way out.
211
+ - `examples/parallel-conveyor-with-recipe/main.py`: two conveyors and a pop-up transfer as parallel asyncio tasks, sharing counters through one dataclass, with speeds and batch size taken from a recipe.
212
+ - `examples/conveyor-with-sensor/main.py`: run a conveyor actuator continuously until a sensor sees a part, count parts, and stop after a target count.
213
+
214
+ ## Validate before you push
215
+
216
+ 1. Keep a `requirements-dev.txt` and a `pyrightconfig.json` next to `requirements.txt`. The engine installs only `requirements.txt`, so neither reaches the machine.
217
+
218
+ ```text
219
+ -r requirements.txt
220
+ pyright==1.1.411
221
+ ```
222
+
223
+ ```json
224
+ { "typeCheckingMode": "basic", "pythonVersion": "3.10", "reportMissingImports": true, "enableTypeIgnoreComments": false, "venvPath": ".", "venv": ".venv" }
225
+ ```
226
+
227
+ 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.
228
+ 3. 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.
229
+ 4. `vention push`, then run the application on the digital twin from MachineBuilder before any hardware run.
230
+ 5. Read the output with `vention logs`. MachineLogic failures name the device and the reason; read the log before proposing a change.
231
+ 6. Show the user the diff before pushing over an application they did not write in this session.
232
+
233
+ ## Safety
234
+
235
+ This code drives physical machinery that can injure someone.
236
+
237
+ - Before a run on real hardware, tell the user plainly what will move and how far.
238
+ - Recommend a low-speed dry run before production speeds.
239
+ - 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.
240
+ - Never write code that starts motion automatically on power-up unless the user has explicitly asked for it and confirmed the cell is guarded.
241
+
242
+ ## Common mistakes
243
+
244
+ - Device names that do not match `.vention-design-configuration.json`.
245
+ - Answering with code in the reply instead of writing `main.py` to disk.
246
+ - Commanding absolute moves before homing.
247
+ - Calling a move without a `MotionProfile`, or passing jerk to a continuous move (it is ignored with a warning).
248
+ - Using an `_async` move where a blocking one would do, leaving one without its `wait_for_move_completion` / `wait_for_motion_completion`, or sleeping a guessed travel time in place of that wait.
249
+ - Driving a pneumatic through `get_output`, or inventing `extend()` / `retract()` on it.
250
+ - Reaching an actuator machine-wide on a rig with more than one controller instead of through `get_machine_motion`.
251
+ - Hard-coding a pose the scene already holds under a taught name.
252
+ - Creating `Machine()` at import time, or twice in one process.
253
+ - Treating a simulation pass as evidence the program is safe on hardware.
254
+ - `pip install machinelogic`: the package is `machine-logic-sdk`; only the import is `machinelogic`.
255
+ - Calling a blocking SDK method (a wait, `home`, a blocking move) directly inside `async def`, or using `asyncio.TaskGroup` (Python 3.11) on a 3.10 target.
256
+ - Assuming a cancelled task stopped its move.
@@ -0,0 +1,58 @@
1
+ """Run a conveyor until a sensor sees a part, count parts, stop after a target count.
2
+
3
+ Assumes these friendly names from .vention-design-configuration.json:
4
+ - actuator "x": the conveyor belt axis, configured in mm
5
+ - input "Input A2": a photo-eye at the end of the belt, high while a part blocks it
6
+
7
+ A part is counted on the sensor's rising edge. The belt stops when the target count
8
+ is reached, when no part shows up for SENSOR_TIMEOUT_S, or on any fault.
9
+ """
10
+
11
+ import time
12
+
13
+ from machinelogic import ActuatorException, Machine, MachineException, MotionProfile
14
+ from machinelogic.types import MachineSafetyState
15
+
16
+ BELT_SPEED_MM_S = 150.0
17
+ BELT_ACCELERATION_MM_S2 = 300.0
18
+ PARTS_TO_COUNT = 5
19
+ SENSOR_TIMEOUT_S = 60.0
20
+ POLL_INTERVAL_S = 0.05
21
+
22
+
23
+ def main() -> None:
24
+ machine = Machine()
25
+ belt = machine.get_actuator("x")
26
+ photo_eye = machine.get_input("Input A2")
27
+ # Continuous moves use velocity and acceleration only; jerk would be ignored.
28
+ belt_profile = MotionProfile(velocity=BELT_SPEED_MM_S, acceleration=BELT_ACCELERATION_MM_S2)
29
+
30
+ parts_seen = 0
31
+ try:
32
+ # A continuous move has no target position, so the belt does not need homing.
33
+ belt.move_continuous_async(belt_profile)
34
+ deadline = time.monotonic() + SENSOR_TIMEOUT_S
35
+ part_in_front = photo_eye.state.value
36
+
37
+ while parts_seen < PARTS_TO_COUNT and time.monotonic() < deadline:
38
+ if machine.state.safety_state == MachineSafetyState.EMERGENCY_STOP:
39
+ print("emergency stop active, stopping the belt")
40
+ break
41
+ sensor_now = photo_eye.state.value
42
+ if sensor_now and not part_in_front:
43
+ parts_seen += 1
44
+ deadline = time.monotonic() + SENSOR_TIMEOUT_S
45
+ print(f"part {parts_seen} of {PARTS_TO_COUNT}")
46
+ part_in_front = sensor_now
47
+ time.sleep(POLL_INTERVAL_S)
48
+
49
+ if parts_seen < PARTS_TO_COUNT:
50
+ print(f"no part for {SENSOR_TIMEOUT_S:.0f} s, stopping with {parts_seen} counted")
51
+ except (ActuatorException, MachineException) as error:
52
+ print(f"machine fault: {error}")
53
+ finally:
54
+ belt.stop()
55
+
56
+
57
+ if __name__ == "__main__":
58
+ main()
@@ -0,0 +1,51 @@
1
+ """Home one actuator, then index through three absolute positions.
2
+
3
+ Assumes these friendly names from .vention-design-configuration.json:
4
+ - actuator "x": a linear axis configured in mm
5
+ - input "Input A1": a station-occupied sensor, high while a part sits at the station
6
+
7
+ Before each move the sensor is read; an occupied station is skipped. Run on the
8
+ digital twin before any hardware run.
9
+ """
10
+
11
+ import time
12
+
13
+ from machinelogic import ActuatorException, Machine, MachineException, MotionProfile
14
+ from machinelogic.types import MachineSafetyState
15
+
16
+ HOMING_TIMEOUT_S = 30.0
17
+ STATION_POSITIONS_MM = [50.0, 150.0, 250.0]
18
+ INDEX_SPEED_MM_S = 100.0
19
+ INDEX_ACCELERATION_MM_S2 = 200.0
20
+ DWELL_AT_STATION_S = 0.5
21
+
22
+
23
+ def main() -> None:
24
+ machine = Machine()
25
+ axis = machine.get_actuator("x")
26
+ station_occupied = machine.get_input("Input A1")
27
+ index_profile = MotionProfile(velocity=INDEX_SPEED_MM_S, acceleration=INDEX_ACCELERATION_MM_S2)
28
+
29
+ try:
30
+ # Absolute positions are only meaningful after homing.
31
+ axis.home(timeout=HOMING_TIMEOUT_S)
32
+ print("homed")
33
+
34
+ for station_position_mm in STATION_POSITIONS_MM:
35
+ if machine.state.safety_state == MachineSafetyState.EMERGENCY_STOP:
36
+ print("emergency stop active, leaving the cycle")
37
+ break
38
+ if station_occupied.state.value:
39
+ print(f"station at {station_position_mm:.0f} mm is occupied, skipping")
40
+ continue
41
+ axis.move_absolute(station_position_mm, index_profile)
42
+ print(f"indexed to {station_position_mm:.0f} mm")
43
+ time.sleep(DWELL_AT_STATION_S)
44
+ except (ActuatorException, MachineException) as error:
45
+ print(f"machine fault: {error}")
46
+ finally:
47
+ axis.stop()
48
+
49
+
50
+ if __name__ == "__main__":
51
+ main()
@@ -0,0 +1,118 @@
1
+ """Two conveyors and a pop-up transfer running at the same time, driven by a recipe.
2
+
3
+ Assumes these friendly names from .vention-design-configuration.json:
4
+ - actuators "Infeed" and "Outfeed": timing-belt conveyors
5
+ - pneumatic "InfeedPopUP-Air": lifts a board off the infeed onto the cross conveyor
6
+ - input "End Infeed": beam break at the end of the infeed, high while a board is there
7
+ - input "End Outfeed": beam break at the end of the outfeed, high while a board is there
8
+
9
+ Each sequence is a task on one asyncio loop. SDK callbacks arrive on SDK threads and are handed to
10
+ the loop with call_soon_threadsafe; the shared LineState is only changed on the loop's thread, so it
11
+ needs no lock. The batch ends when the recipe's board count leaves the outfeed or the emergency stop
12
+ trips, and the finally stops both conveyors and drops the pop-up whatever happened.
13
+ Run it as `python main.py fragile` to pick a recipe; the default is "standard".
14
+ """
15
+
16
+ import asyncio
17
+ import sys
18
+ from dataclasses import dataclass
19
+
20
+ from machinelogic import Machine, MotionProfile, VentionException
21
+ from machinelogic.ivention.iactuator import IActuator
22
+ from machinelogic.ivention.idigital_input import IDigitalInput
23
+ from machinelogic.ivention.imachine import MachineOperationalState, MachineSafetyState
24
+ from machinelogic.ivention.ipneumatic import IPneumatic
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class Recipe:
29
+ conveyor_speed_mm_s: float
30
+ conveyor_acceleration_mm_s2: float
31
+ boards_per_batch: int
32
+
33
+
34
+ RECIPES = {
35
+ "standard": Recipe(conveyor_speed_mm_s=200.0, conveyor_acceleration_mm_s2=400.0, boards_per_batch=20),
36
+ "fragile": Recipe(conveyor_speed_mm_s=80.0, conveyor_acceleration_mm_s2=150.0, boards_per_batch=10),
37
+ }
38
+ POPUP_HOLD_S = 1.0 # the cylinder reports no completion, so the transfer holds for a fixed time
39
+
40
+
41
+ @dataclass
42
+ class LineState:
43
+ recipe: Recipe
44
+ boards_transferred: int = 0
45
+ boards_out: int = 0
46
+
47
+
48
+ async def transfer_boards(popup: IPneumatic, arrivals: "asyncio.Queue[None]", line: LineState) -> None:
49
+ while True:
50
+ await arrivals.get()
51
+ popup.push_async()
52
+ await asyncio.sleep(POPUP_HOLD_S)
53
+ popup.pull_async()
54
+ line.boards_transferred += 1
55
+
56
+
57
+ async def count_boards(departures: "asyncio.Queue[None]", line: LineState, done: asyncio.Event) -> None:
58
+ while line.boards_out < line.recipe.boards_per_batch:
59
+ await departures.get()
60
+ line.boards_out += 1
61
+ done.set()
62
+
63
+
64
+ async def run_line(recipe_name: str) -> None:
65
+ recipe = RECIPES[recipe_name]
66
+ machine = Machine()
67
+ conveyors: list[IActuator] = [machine.get_actuator("Infeed"), machine.get_actuator("Outfeed")]
68
+ popup = machine.get_pneumatic("InfeedPopUP-Air")
69
+ end_infeed = machine.get_input("End Infeed")
70
+ end_outfeed = machine.get_input("End Outfeed")
71
+
72
+ loop = asyncio.get_running_loop()
73
+ line = LineState(recipe=recipe)
74
+ done = asyncio.Event()
75
+ arrivals: "asyncio.Queue[None]" = asyncio.Queue()
76
+ departures: "asyncio.Queue[None]" = asyncio.Queue()
77
+
78
+ def on_rising_edge(queue: "asyncio.Queue[None]"):
79
+ def callback(value: bool, _input: IDigitalInput) -> None:
80
+ if value:
81
+ loop.call_soon_threadsafe(queue.put_nowait, None)
82
+
83
+ return callback
84
+
85
+ def on_safety(_operational: MachineOperationalState, safety: MachineSafetyState) -> None:
86
+ if safety == MachineSafetyState.EMERGENCY_STOP:
87
+ print("emergency stop, ending the batch")
88
+ loop.call_soon_threadsafe(done.set)
89
+
90
+ profile = MotionProfile(velocity=recipe.conveyor_speed_mm_s, acceleration=recipe.conveyor_acceleration_mm_s2)
91
+ workers = [
92
+ asyncio.create_task(transfer_boards(popup, arrivals, line)),
93
+ asyncio.create_task(count_boards(departures, line, done)),
94
+ ]
95
+ finished = asyncio.create_task(done.wait())
96
+ try:
97
+ end_infeed.on_state_change(on_rising_edge(arrivals))
98
+ end_outfeed.on_state_change(on_rising_edge(departures))
99
+ machine.on_system_state_change(on_safety)
100
+ for conveyor in conveyors:
101
+ conveyor.move_continuous_async(profile)
102
+ completed, _pending = await asyncio.wait([*workers, finished], return_when=asyncio.FIRST_COMPLETED)
103
+ for task in completed:
104
+ task.result() # re-raises a worker's exception here rather than losing it
105
+ except VentionException as error:
106
+ print(f"machine fault: {error}")
107
+ finally:
108
+ for task in [*workers, finished]:
109
+ task.cancel()
110
+ await asyncio.gather(*workers, return_exceptions=True)
111
+ for conveyor in conveyors:
112
+ conveyor.stop()
113
+ popup.pull_async()
114
+ print(f"{line.boards_out} of {recipe.boards_per_batch} boards out, {line.boards_transferred} transferred ({recipe_name})")
115
+
116
+
117
+ if __name__ == "__main__":
118
+ asyncio.run(run_line(sys.argv[1] if len(sys.argv) > 1 else "standard"))
@@ -0,0 +1,159 @@
1
+ """Pick-and-place cycle on vention-state-machine, driven from one asyncio loop.
2
+
3
+ Assumes these friendly names from .vention-design-configuration.json:
4
+ - robot "UR3e"
5
+ - output "Gripper Vaccum": vacuum gripper, True switches suction on
6
+ - input "Input A1": part-present sensor at the pick position, high while a part is there
7
+
8
+ The machine starts in `ready`; a part arriving, or one already waiting when a cycle ends, triggers
9
+ `start` into picking. Robot moves start with movej_async and finish on the SDK's completion wait in a
10
+ worker thread, so the loop keeps serving the part sensor and the safety callback. An emergency stop or
11
+ any exception in a step routes to `fault`, which cancels the running step; the `finally` stops the arm
12
+ and releases the gripper on every way out, and an error that is not a VentionException is re-raised after it.
13
+ The joint targets are placeholders in degrees; teach real ones on the digital twin first.
14
+ """
15
+
16
+ import asyncio
17
+ from typing import Awaitable, Callable, Optional
18
+
19
+ from machinelogic import Machine, VentionException
20
+ from machinelogic.ivention.idigital_input import IDigitalInput
21
+ from machinelogic.ivention.idigital_output import IDigitalOutput
22
+ from machinelogic.ivention.imachine import MachineOperationalState, MachineSafetyState
23
+ from machinelogic.ivention.irobot import IRobot
24
+ from state_machine.core import BaseStates, BaseTriggers, StateMachine
25
+ from state_machine.decorators import on_enter_state
26
+ from state_machine.defs import State, StateGroup, Trigger
27
+
28
+ JOINT_SPEED_DEG_S = 30.0
29
+ JOINT_ACCELERATION_DEG_S2 = 60.0
30
+ HOME_JOINTS_DEG = [0.0, -90.0, 90.0, -90.0, -90.0, 0.0]
31
+ PICK_JOINTS_DEG = [20.0, -80.0, 100.0, -110.0, -90.0, 0.0]
32
+ PLACE_JOINTS_DEG = [-40.0, -80.0, 100.0, -110.0, -90.0, 0.0]
33
+ GRIP_SETTLE_S = 0.3
34
+ CYCLES_TO_RUN = 3
35
+
36
+
37
+ class Cycle(StateGroup):
38
+ picking: State = State()
39
+ placing: State = State()
40
+
41
+
42
+ class States:
43
+ cycle = Cycle()
44
+
45
+
46
+ class Triggers:
47
+ start = Trigger(BaseTriggers.START.value)
48
+ picked = Trigger("picked")
49
+ placed = Trigger("placed")
50
+
51
+
52
+ TRANSITIONS = [
53
+ Triggers.start.transition(BaseStates.READY.value, States.cycle.picking),
54
+ Triggers.picked.transition(States.cycle.picking, States.cycle.placing),
55
+ Triggers.placed.transition(States.cycle.placing, BaseStates.READY.value),
56
+ ]
57
+
58
+
59
+ class PickAndPlace(StateMachine):
60
+ def __init__(self, robot: IRobot, vacuum: IDigitalOutput, part_present: IDigitalInput, finished: asyncio.Event) -> None:
61
+ super().__init__(states=States, transitions=TRANSITIONS, enable_last_state_recovery=False)
62
+ self.robot = robot
63
+ self.vacuum = vacuum
64
+ self.part_present = part_present
65
+ self.finished = finished
66
+ self.cycles_done = 0
67
+ self.step: Optional["asyncio.Task[None]"] = None
68
+ self.error: Optional[Exception] = None
69
+
70
+ def start_if_ready(self) -> None:
71
+ if self.state == BaseStates.READY.value:
72
+ self.trigger(Triggers.start.name)
73
+
74
+ async def move_joints(self, target_deg: list[float]) -> None:
75
+ self.robot.movej_async(target_deg, JOINT_SPEED_DEG_S, JOINT_ACCELERATION_DEG_S2)
76
+ await asyncio.to_thread(self.robot.wait_for_motion_completion)
77
+
78
+ async def run_step(self, step: Callable[[], Awaitable[None]], trigger_when_done: str) -> None:
79
+ try:
80
+ await step()
81
+ self.trigger(trigger_when_done)
82
+ except VentionException as error:
83
+ print(f"fault in {self.state}: {error}")
84
+ self.trigger(BaseTriggers.TO_FAULT.value)
85
+ except Exception as error: # anything else would end this task silently and leave the cell waiting
86
+ self.error = error
87
+ print(f"unexpected error in {self.state}: {error!r}")
88
+ self.trigger(BaseTriggers.TO_FAULT.value)
89
+
90
+ async def pick(self) -> None:
91
+ await self.move_joints(PICK_JOINTS_DEG)
92
+ self.vacuum.write(True)
93
+ await asyncio.sleep(GRIP_SETTLE_S)
94
+
95
+ async def place(self) -> None:
96
+ await self.move_joints(PLACE_JOINTS_DEG)
97
+ self.vacuum.write(False)
98
+ await asyncio.sleep(GRIP_SETTLE_S)
99
+ await self.move_joints(HOME_JOINTS_DEG)
100
+ self.cycles_done += 1
101
+
102
+ # Enter hooks are called synchronously, so each one starts its step as a task and returns.
103
+ @on_enter_state(States.cycle.picking)
104
+ def enter_picking(self, _: object) -> None:
105
+ self.step = self.spawn(self.run_step(self.pick, Triggers.picked.name))
106
+
107
+ @on_enter_state(States.cycle.placing)
108
+ def enter_placing(self, _: object) -> None:
109
+ self.step = self.spawn(self.run_step(self.place, Triggers.placed.name))
110
+
111
+ @on_enter_state(BaseStates.READY.value)
112
+ def enter_ready(self, _: object) -> None:
113
+ if self.cycles_done >= CYCLES_TO_RUN:
114
+ self.finished.set()
115
+ elif self.part_present.state.value:
116
+ # the part arrived mid-cycle, so no edge will start it; call_soon keeps the transition out of this hook
117
+ asyncio.get_running_loop().call_soon(self.start_if_ready)
118
+
119
+ @on_enter_state(BaseStates.FAULT.value)
120
+ def enter_fault(self, _: object) -> None:
121
+ if self.step is not None:
122
+ self.step.cancel()
123
+ self.finished.set()
124
+
125
+
126
+ async def run_cell() -> None:
127
+ machine = Machine()
128
+ robot = machine.get_robot("UR3e")
129
+ vacuum = machine.get_output("Gripper Vaccum")
130
+ part_present = machine.get_input("Input A1")
131
+ loop = asyncio.get_running_loop()
132
+ finished = asyncio.Event()
133
+ cell = PickAndPlace(robot, vacuum, part_present, finished)
134
+
135
+ def on_part(value: bool, _input: IDigitalInput) -> None:
136
+ if value:
137
+ loop.call_soon_threadsafe(cell.start_if_ready)
138
+
139
+ def on_safety(_operational: MachineOperationalState, safety: MachineSafetyState) -> None:
140
+ if safety == MachineSafetyState.EMERGENCY_STOP:
141
+ loop.call_soon_threadsafe(cell.trigger, BaseTriggers.TO_FAULT.value)
142
+
143
+ try:
144
+ await asyncio.to_thread(robot.movej, HOME_JOINTS_DEG, JOINT_SPEED_DEG_S, JOINT_ACCELERATION_DEG_S2)
145
+ part_present.on_state_change(on_part)
146
+ machine.on_system_state_change(on_safety)
147
+ if part_present.state.value:
148
+ cell.start_if_ready()
149
+ await finished.wait()
150
+ finally:
151
+ robot.move_stop()
152
+ vacuum.write(False)
153
+ print(f"finished in state {cell.state} after {cell.cycles_done} cycle(s)")
154
+ if cell.error is not None:
155
+ raise cell.error
156
+
157
+
158
+ if __name__ == "__main__":
159
+ asyncio.run(run_cell())
@@ -0,0 +1,2 @@
1
+ machine-logic-sdk==3.1.0
2
+ vention-state-machine==0.4.55