@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,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 @@
|
|
|
1
|
+
machine-logic-sdk==3.1.0
|
|
@@ -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())
|