grafito-canstepper 0.1.0__tar.gz
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.
- grafito_canstepper-0.1.0/PKG-INFO +122 -0
- grafito_canstepper-0.1.0/README.md +94 -0
- grafito_canstepper-0.1.0/canstepper/__init__.py +95 -0
- grafito_canstepper-0.1.0/canstepper/axis.py +198 -0
- grafito_canstepper-0.1.0/canstepper/bus.py +367 -0
- grafito_canstepper-0.1.0/canstepper/config.py +240 -0
- grafito_canstepper-0.1.0/canstepper/exceptions.py +60 -0
- grafito_canstepper-0.1.0/canstepper/group.py +113 -0
- grafito_canstepper-0.1.0/canstepper/kinematics.py +428 -0
- grafito_canstepper-0.1.0/canstepper/node.py +626 -0
- grafito_canstepper-0.1.0/canstepper/protocol.py +317 -0
- grafito_canstepper-0.1.0/canstepper/py.typed +0 -0
- grafito_canstepper-0.1.0/canstepper/sim.py +363 -0
- grafito_canstepper-0.1.0/canstepper/telemetry.py +190 -0
- grafito_canstepper-0.1.0/canstepper/transport/__init__.py +4 -0
- grafito_canstepper-0.1.0/canstepper/transport/base.py +43 -0
- grafito_canstepper-0.1.0/canstepper/transport/serial_bridge.py +81 -0
- grafito_canstepper-0.1.0/grafito_canstepper.egg-info/PKG-INFO +122 -0
- grafito_canstepper-0.1.0/grafito_canstepper.egg-info/SOURCES.txt +29 -0
- grafito_canstepper-0.1.0/grafito_canstepper.egg-info/dependency_links.txt +1 -0
- grafito_canstepper-0.1.0/grafito_canstepper.egg-info/requires.txt +14 -0
- grafito_canstepper-0.1.0/grafito_canstepper.egg-info/top_level.txt +1 -0
- grafito_canstepper-0.1.0/pyproject.toml +44 -0
- grafito_canstepper-0.1.0/setup.cfg +4 -0
- grafito_canstepper-0.1.0/setup.py +19 -0
- grafito_canstepper-0.1.0/tests/test_axis.py +51 -0
- grafito_canstepper-0.1.0/tests/test_bus_node.py +153 -0
- grafito_canstepper-0.1.0/tests/test_config.py +90 -0
- grafito_canstepper-0.1.0/tests/test_group.py +37 -0
- grafito_canstepper-0.1.0/tests/test_kinematics.py +121 -0
- grafito_canstepper-0.1.0/tests/test_protocol.py +72 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: grafito-canstepper
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Host library for Grafito CANStepper boards (GCSP v1): multi-node CAN stepper control with encoders, homing, groups and coordinated motion
|
|
5
|
+
Author-email: Grafito Innovations <grafito.innovations@gmail.com>
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Documentation, https://docs.grafito.in
|
|
8
|
+
Project-URL: Homepage, https://docs.grafito.in
|
|
9
|
+
Project-URL: Source, https://github.com/Grafito-Innovations/Grafito-Edge-Services/tree/main/can_stepper
|
|
10
|
+
Project-URL: Issues, https://github.com/Grafito-Innovations/Grafito-Edge-Services/issues
|
|
11
|
+
Keywords: stepper,CAN,motion-control,esp32,tmc2209,encoder
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Manufacturing
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Topic :: System :: Hardware :: Hardware Drivers
|
|
17
|
+
Requires-Python: >=3.9
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
Requires-Dist: pyserial>=3.4
|
|
20
|
+
Provides-Extra: toml
|
|
21
|
+
Requires-Dist: tomli>=2.0; python_version < "3.11" and extra == "toml"
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
24
|
+
Requires-Dist: ruff; extra == "dev"
|
|
25
|
+
Requires-Dist: mypy; extra == "dev"
|
|
26
|
+
Requires-Dist: tomli>=2.0; python_version < "3.11" and extra == "dev"
|
|
27
|
+
Dynamic: requires-python
|
|
28
|
+
|
|
29
|
+
# Grafito CANStepper
|
|
30
|
+
|
|
31
|
+
Closed-loop stepper motor control over CAN. Each board carries an ESP32-C3,
|
|
32
|
+
a TMC2209 driver, an MT6701 14-bit magnetic encoder and a CAN transceiver;
|
|
33
|
+
up to 31 boards daisy-chain on one 1 Mbps bus and are driven from Python
|
|
34
|
+
through the USB port of any board on the chain.
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
can_stepper/
|
|
38
|
+
├── firmware/GrafitoCANStepper_C3/ # node firmware (GCSP v1)
|
|
39
|
+
├── canstepper/ # Python package (grafito-canstepper)
|
|
40
|
+
├── examples/ # runnable examples + machine.toml
|
|
41
|
+
├── tests/ # pytest suite (runs on a software sim)
|
|
42
|
+
└── docs/
|
|
43
|
+
├── PRD.md # product requirements
|
|
44
|
+
├── protocol.md # GCSP v1 wire protocol reference
|
|
45
|
+
└── quickstart.md # unboxing -> moving motor
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Highlights
|
|
49
|
+
|
|
50
|
+
- **Layered Python API** — raw frames → `StepperNode` → `NodeGroup` →
|
|
51
|
+
`Axis` (mm units via `rotation_distance`) → `DualMotorAxis` / `CoreXY` /
|
|
52
|
+
`MotionGroup` → declarative `machine.toml`.
|
|
53
|
+
- **Safety scopes** — `node.estop()`, `group.estop()`, `bus.estop_all()`
|
|
54
|
+
(single broadcast frame); e-stop is latched until re-enabled.
|
|
55
|
+
- **Homing** — physical endstop on IO8, sensorless (StallGuard), or
|
|
56
|
+
set-zero; configurable current, backoff and timeout.
|
|
57
|
+
- **Closed loop** — firmware ≥1.2 plans a rest-to-rest **trapezoidal**
|
|
58
|
+
trajectory with **velocity feedforward** and a light tracking PID on the
|
|
59
|
+
MT6701 encoder (settle + stall watchdog). All motion limits are
|
|
60
|
+
*configurable defaults*, never hard clamps.
|
|
61
|
+
- **On-bus leader/follower** — dual-motor gantries stay coupled with no
|
|
62
|
+
host in the loop.
|
|
63
|
+
- **Simulator included** — `canstepper.sim.SimNetwork` implements the whole
|
|
64
|
+
protocol in software; the test suite and the examples run without hardware.
|
|
65
|
+
|
|
66
|
+
## Install & first spin
|
|
67
|
+
|
|
68
|
+
The package is **`grafito-canstepper`** on the index; you `import canstepper`.
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
# Works today (GitHub monorepo subdirectory — not yet on PyPI):
|
|
72
|
+
pip install "git+https://github.com/Grafito-Innovations/Grafito-Edge-Services.git#subdirectory=can_stepper"
|
|
73
|
+
|
|
74
|
+
# Development (from this directory):
|
|
75
|
+
pip install -U pip setuptools && pip install -e ".[dev]"
|
|
76
|
+
|
|
77
|
+
# After publishing to PyPI:
|
|
78
|
+
# pip install grafito-canstepper
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Docs: **https://docs.grafito.in**
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from canstepper import CANStepperBus
|
|
85
|
+
|
|
86
|
+
with CANStepperBus.serial("/dev/ttyACM0") as bus:
|
|
87
|
+
print(bus.discover())
|
|
88
|
+
node = bus.node(1)
|
|
89
|
+
node.set_run_current(40).enable()
|
|
90
|
+
node.move_to(360.0, blocking=True)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Continue with:
|
|
94
|
+
|
|
95
|
+
- [docs/quickstart.md](docs/quickstart.md) — first motion
|
|
96
|
+
- [docs/closed_loop_tuning.md](docs/closed_loop_tuning.md) — trapezoid + v_ff,
|
|
97
|
+
hardware matrix + 10 min soak on **PR42HS40-1204AF-02** (NEMA 17 1.2 A)
|
|
98
|
+
- [docs/protocol.md](docs/protocol.md) — GCSP v1 wire protocol
|
|
99
|
+
|
|
100
|
+
## Development
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
cd can_stepper
|
|
104
|
+
python3 -m pytest # simulator suite
|
|
105
|
+
python3 tools/cl_speed_validate.py /dev/ttyACM0 1 70 8 # hardware OL vs CL
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Firmware builds with Arduino IDE or `arduino-cli` (ESP32C3 Dev Module, USB
|
|
109
|
+
CDC On Boot = Enabled; libraries: FastAccelStepper, TMC2209 janelia-arduino):
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
arduino-cli compile --fqbn esp32:esp32:esp32c3:CDCOnBoot=cdc \
|
|
113
|
+
firmware/GrafitoCANStepper_C3
|
|
114
|
+
arduino-cli upload -p /dev/ttyACM0 --fqbn esp32:esp32:esp32c3:CDCOnBoot=cdc \
|
|
115
|
+
firmware/GrafitoCANStepper_C3
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Legacy note
|
|
119
|
+
|
|
120
|
+
`CANStepperNode_C3/` and `canbus.py` are the earlier bring-up firmware and
|
|
121
|
+
host script based on a third-party protocol. They are superseded by the
|
|
122
|
+
clean-room GCSP v1 stack above and kept only for reference during migration.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Grafito CANStepper
|
|
2
|
+
|
|
3
|
+
Closed-loop stepper motor control over CAN. Each board carries an ESP32-C3,
|
|
4
|
+
a TMC2209 driver, an MT6701 14-bit magnetic encoder and a CAN transceiver;
|
|
5
|
+
up to 31 boards daisy-chain on one 1 Mbps bus and are driven from Python
|
|
6
|
+
through the USB port of any board on the chain.
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
can_stepper/
|
|
10
|
+
├── firmware/GrafitoCANStepper_C3/ # node firmware (GCSP v1)
|
|
11
|
+
├── canstepper/ # Python package (grafito-canstepper)
|
|
12
|
+
├── examples/ # runnable examples + machine.toml
|
|
13
|
+
├── tests/ # pytest suite (runs on a software sim)
|
|
14
|
+
└── docs/
|
|
15
|
+
├── PRD.md # product requirements
|
|
16
|
+
├── protocol.md # GCSP v1 wire protocol reference
|
|
17
|
+
└── quickstart.md # unboxing -> moving motor
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Highlights
|
|
21
|
+
|
|
22
|
+
- **Layered Python API** — raw frames → `StepperNode` → `NodeGroup` →
|
|
23
|
+
`Axis` (mm units via `rotation_distance`) → `DualMotorAxis` / `CoreXY` /
|
|
24
|
+
`MotionGroup` → declarative `machine.toml`.
|
|
25
|
+
- **Safety scopes** — `node.estop()`, `group.estop()`, `bus.estop_all()`
|
|
26
|
+
(single broadcast frame); e-stop is latched until re-enabled.
|
|
27
|
+
- **Homing** — physical endstop on IO8, sensorless (StallGuard), or
|
|
28
|
+
set-zero; configurable current, backoff and timeout.
|
|
29
|
+
- **Closed loop** — firmware ≥1.2 plans a rest-to-rest **trapezoidal**
|
|
30
|
+
trajectory with **velocity feedforward** and a light tracking PID on the
|
|
31
|
+
MT6701 encoder (settle + stall watchdog). All motion limits are
|
|
32
|
+
*configurable defaults*, never hard clamps.
|
|
33
|
+
- **On-bus leader/follower** — dual-motor gantries stay coupled with no
|
|
34
|
+
host in the loop.
|
|
35
|
+
- **Simulator included** — `canstepper.sim.SimNetwork` implements the whole
|
|
36
|
+
protocol in software; the test suite and the examples run without hardware.
|
|
37
|
+
|
|
38
|
+
## Install & first spin
|
|
39
|
+
|
|
40
|
+
The package is **`grafito-canstepper`** on the index; you `import canstepper`.
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# Works today (GitHub monorepo subdirectory — not yet on PyPI):
|
|
44
|
+
pip install "git+https://github.com/Grafito-Innovations/Grafito-Edge-Services.git#subdirectory=can_stepper"
|
|
45
|
+
|
|
46
|
+
# Development (from this directory):
|
|
47
|
+
pip install -U pip setuptools && pip install -e ".[dev]"
|
|
48
|
+
|
|
49
|
+
# After publishing to PyPI:
|
|
50
|
+
# pip install grafito-canstepper
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Docs: **https://docs.grafito.in**
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from canstepper import CANStepperBus
|
|
57
|
+
|
|
58
|
+
with CANStepperBus.serial("/dev/ttyACM0") as bus:
|
|
59
|
+
print(bus.discover())
|
|
60
|
+
node = bus.node(1)
|
|
61
|
+
node.set_run_current(40).enable()
|
|
62
|
+
node.move_to(360.0, blocking=True)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Continue with:
|
|
66
|
+
|
|
67
|
+
- [docs/quickstart.md](docs/quickstart.md) — first motion
|
|
68
|
+
- [docs/closed_loop_tuning.md](docs/closed_loop_tuning.md) — trapezoid + v_ff,
|
|
69
|
+
hardware matrix + 10 min soak on **PR42HS40-1204AF-02** (NEMA 17 1.2 A)
|
|
70
|
+
- [docs/protocol.md](docs/protocol.md) — GCSP v1 wire protocol
|
|
71
|
+
|
|
72
|
+
## Development
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
cd can_stepper
|
|
76
|
+
python3 -m pytest # simulator suite
|
|
77
|
+
python3 tools/cl_speed_validate.py /dev/ttyACM0 1 70 8 # hardware OL vs CL
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Firmware builds with Arduino IDE or `arduino-cli` (ESP32C3 Dev Module, USB
|
|
81
|
+
CDC On Boot = Enabled; libraries: FastAccelStepper, TMC2209 janelia-arduino):
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
arduino-cli compile --fqbn esp32:esp32:esp32c3:CDCOnBoot=cdc \
|
|
85
|
+
firmware/GrafitoCANStepper_C3
|
|
86
|
+
arduino-cli upload -p /dev/ttyACM0 --fqbn esp32:esp32:esp32c3:CDCOnBoot=cdc \
|
|
87
|
+
firmware/GrafitoCANStepper_C3
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Legacy note
|
|
91
|
+
|
|
92
|
+
`CANStepperNode_C3/` and `canbus.py` are the earlier bring-up firmware and
|
|
93
|
+
host script based on a third-party protocol. They are superseded by the
|
|
94
|
+
clean-room GCSP v1 stack above and kept only for reference during migration.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""grafito-canstepper — host library for Grafito CANStepper boards (GCSP v1).
|
|
2
|
+
|
|
3
|
+
Quick start::
|
|
4
|
+
|
|
5
|
+
from canstepper import CANStepperBus
|
|
6
|
+
|
|
7
|
+
bus = CANStepperBus.serial("/dev/ttyACM0")
|
|
8
|
+
print(bus.discover())
|
|
9
|
+
|
|
10
|
+
node = bus.node(1)
|
|
11
|
+
node.set_run_current(40).set_microsteps(16).enable()
|
|
12
|
+
node.move_to(180.0, blocking=True)
|
|
13
|
+
|
|
14
|
+
bus.estop_all() # single broadcast frame stops every node
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from .axis import Axis
|
|
18
|
+
from .bus import CANStepperBus
|
|
19
|
+
from .config import Machine
|
|
20
|
+
from .exceptions import (
|
|
21
|
+
CANStepperError,
|
|
22
|
+
ConfigError,
|
|
23
|
+
EStopActive,
|
|
24
|
+
HomingFailed,
|
|
25
|
+
LimitViolation,
|
|
26
|
+
NodeFault,
|
|
27
|
+
NotHomed,
|
|
28
|
+
ParamRejected,
|
|
29
|
+
RequestTimeout,
|
|
30
|
+
TransportError,
|
|
31
|
+
UnknownParam,
|
|
32
|
+
)
|
|
33
|
+
from .group import NodeGroup
|
|
34
|
+
from .kinematics import CoreXY, DualMotorAxis, MotionGroup
|
|
35
|
+
from .node import SpeedLimits, StepperNode
|
|
36
|
+
from .protocol import (
|
|
37
|
+
BROADCAST,
|
|
38
|
+
PROTOCOL_VERSION,
|
|
39
|
+
Cmd,
|
|
40
|
+
EndstopAction,
|
|
41
|
+
Event,
|
|
42
|
+
Fault,
|
|
43
|
+
Frame,
|
|
44
|
+
HomeMethod,
|
|
45
|
+
Mode,
|
|
46
|
+
Param,
|
|
47
|
+
StandstillMode,
|
|
48
|
+
Tel,
|
|
49
|
+
)
|
|
50
|
+
from .telemetry import CanHealth, FollowStatus, NodeState, NodeStatus, PidStatus
|
|
51
|
+
from .transport import SerialBridgeTransport, Transport
|
|
52
|
+
|
|
53
|
+
__version__ = "0.1.0"
|
|
54
|
+
|
|
55
|
+
__all__ = [
|
|
56
|
+
"Axis",
|
|
57
|
+
"BROADCAST",
|
|
58
|
+
"CANStepperBus",
|
|
59
|
+
"CANStepperError",
|
|
60
|
+
"CanHealth",
|
|
61
|
+
"Cmd",
|
|
62
|
+
"ConfigError",
|
|
63
|
+
"CoreXY",
|
|
64
|
+
"DualMotorAxis",
|
|
65
|
+
"EndstopAction",
|
|
66
|
+
"EStopActive",
|
|
67
|
+
"Event",
|
|
68
|
+
"Fault",
|
|
69
|
+
"FollowStatus",
|
|
70
|
+
"Frame",
|
|
71
|
+
"HomeMethod",
|
|
72
|
+
"HomingFailed",
|
|
73
|
+
"LimitViolation",
|
|
74
|
+
"Machine",
|
|
75
|
+
"Mode",
|
|
76
|
+
"MotionGroup",
|
|
77
|
+
"NodeFault",
|
|
78
|
+
"NodeGroup",
|
|
79
|
+
"NodeState",
|
|
80
|
+
"NodeStatus",
|
|
81
|
+
"NotHomed",
|
|
82
|
+
"Param",
|
|
83
|
+
"ParamRejected",
|
|
84
|
+
"PidStatus",
|
|
85
|
+
"PROTOCOL_VERSION",
|
|
86
|
+
"RequestTimeout",
|
|
87
|
+
"SerialBridgeTransport",
|
|
88
|
+
"SpeedLimits",
|
|
89
|
+
"StandstillMode",
|
|
90
|
+
"StepperNode",
|
|
91
|
+
"Tel",
|
|
92
|
+
"Transport",
|
|
93
|
+
"TransportError",
|
|
94
|
+
"UnknownParam",
|
|
95
|
+
]
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""Axis — a stepper node dressed in real-world linear units.
|
|
2
|
+
|
|
3
|
+
Klipper-style ``rotation_distance``: the distance the axis travels for one
|
|
4
|
+
full motor revolution (e.g. an 8 mm-lead leadscrew -> 8.0, a GT2-20T belt
|
|
5
|
+
-> 40.0). All Axis positions/speeds are in those units ("mm" below).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Optional, Union
|
|
11
|
+
|
|
12
|
+
from .exceptions import LimitViolation, NotHomed
|
|
13
|
+
from .node import StepperNode
|
|
14
|
+
from .protocol import HomeMethod, Param
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Axis:
|
|
18
|
+
"""Linear (or geared rotary) axis on one node.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
node: the StepperNode driving the axis
|
|
22
|
+
rotation_distance: units of travel per motor revolution
|
|
23
|
+
gear_ratio: motor revs per output rev (e.g. 5.0 for a 5:1 box)
|
|
24
|
+
min_pos / max_pos: optional host-side soft travel limits (units)
|
|
25
|
+
max_speed: optional host-side speed limit (units/s)
|
|
26
|
+
require_homing: refuse moves until the axis was homed
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
node: StepperNode,
|
|
32
|
+
rotation_distance: float,
|
|
33
|
+
gear_ratio: float = 1.0,
|
|
34
|
+
min_pos: Optional[float] = None,
|
|
35
|
+
max_pos: Optional[float] = None,
|
|
36
|
+
max_speed: Optional[float] = None,
|
|
37
|
+
require_homing: bool = False,
|
|
38
|
+
name: str = "",
|
|
39
|
+
):
|
|
40
|
+
if rotation_distance <= 0:
|
|
41
|
+
raise ValueError("rotation_distance must be positive")
|
|
42
|
+
if gear_ratio <= 0:
|
|
43
|
+
raise ValueError("gear_ratio must be positive")
|
|
44
|
+
self.node = node
|
|
45
|
+
self.rotation_distance = float(rotation_distance)
|
|
46
|
+
self.gear_ratio = float(gear_ratio)
|
|
47
|
+
self.min_pos = min_pos
|
|
48
|
+
self.max_pos = max_pos
|
|
49
|
+
self.max_speed = max_speed
|
|
50
|
+
self.require_homing = require_homing
|
|
51
|
+
self.name = name or f"axis{node.node_id}"
|
|
52
|
+
self._homed = False
|
|
53
|
+
|
|
54
|
+
# -- unit conversion ---------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
def units_to_deg(self, units: float) -> float:
|
|
57
|
+
return units / self.rotation_distance * 360.0 * self.gear_ratio
|
|
58
|
+
|
|
59
|
+
def deg_to_units(self, deg: float) -> float:
|
|
60
|
+
return deg / 360.0 / self.gear_ratio * self.rotation_distance
|
|
61
|
+
|
|
62
|
+
# -- state ---------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def position(self) -> Optional[float]:
|
|
66
|
+
"""Last-known position in axis units (cached telemetry)."""
|
|
67
|
+
deg = self.node.state.position_deg
|
|
68
|
+
return None if deg is None else self.deg_to_units(deg)
|
|
69
|
+
|
|
70
|
+
def get_position(self) -> float:
|
|
71
|
+
"""Round-trip read of the position in axis units."""
|
|
72
|
+
return self.deg_to_units(self.node.get_position())
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def homed(self) -> bool:
|
|
76
|
+
status = self.node.state.status
|
|
77
|
+
if status is not None:
|
|
78
|
+
return status.homed or self._homed
|
|
79
|
+
return self._homed
|
|
80
|
+
|
|
81
|
+
# -- motion ---------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
def move_to(
|
|
84
|
+
self,
|
|
85
|
+
pos: float,
|
|
86
|
+
speed: Optional[float] = None,
|
|
87
|
+
blocking: bool = False,
|
|
88
|
+
timeout: float = 60.0,
|
|
89
|
+
) -> "Axis":
|
|
90
|
+
"""Absolute move in axis units; ``speed`` (units/s) sets the node's
|
|
91
|
+
move speed for this and subsequent moves."""
|
|
92
|
+
self._check_motion_allowed(pos, speed)
|
|
93
|
+
if speed is not None:
|
|
94
|
+
self.node.set_max_speed(self.units_to_deg(speed))
|
|
95
|
+
self.node.move_to(self.units_to_deg(pos), blocking=blocking, timeout=timeout)
|
|
96
|
+
return self
|
|
97
|
+
|
|
98
|
+
def move_by(
|
|
99
|
+
self,
|
|
100
|
+
delta: float,
|
|
101
|
+
speed: Optional[float] = None,
|
|
102
|
+
blocking: bool = False,
|
|
103
|
+
timeout: float = 60.0,
|
|
104
|
+
) -> "Axis":
|
|
105
|
+
target = None
|
|
106
|
+
if self.min_pos is not None or self.max_pos is not None:
|
|
107
|
+
current = self.position
|
|
108
|
+
if current is not None:
|
|
109
|
+
target = current + delta
|
|
110
|
+
self._check_motion_allowed(target, speed)
|
|
111
|
+
if speed is not None:
|
|
112
|
+
self.node.set_max_speed(self.units_to_deg(speed))
|
|
113
|
+
self.node.move_by(self.units_to_deg(delta), blocking=blocking, timeout=timeout)
|
|
114
|
+
return self
|
|
115
|
+
|
|
116
|
+
def run(self, speed: float) -> "Axis":
|
|
117
|
+
"""Continuous motion at signed ``speed`` (units/s)."""
|
|
118
|
+
if self.max_speed is not None and abs(speed) > self.max_speed:
|
|
119
|
+
raise LimitViolation(
|
|
120
|
+
f"{self.name}: {abs(speed):.3f} exceeds max_speed {self.max_speed:.3f}"
|
|
121
|
+
)
|
|
122
|
+
self.node.run(self.units_to_deg(speed))
|
|
123
|
+
return self
|
|
124
|
+
|
|
125
|
+
def stop(self) -> "Axis":
|
|
126
|
+
self.node.stop()
|
|
127
|
+
return self
|
|
128
|
+
|
|
129
|
+
def estop(self) -> "Axis":
|
|
130
|
+
self.node.estop()
|
|
131
|
+
return self
|
|
132
|
+
|
|
133
|
+
# -- homing ---------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
def home(
|
|
136
|
+
self,
|
|
137
|
+
method: Union[str, HomeMethod] = HomeMethod.ENDSTOP,
|
|
138
|
+
direction: int = -1,
|
|
139
|
+
speed: float = 10.0,
|
|
140
|
+
current_percent: Optional[int] = None,
|
|
141
|
+
backoff: Optional[float] = None,
|
|
142
|
+
timeout: float = 60.0,
|
|
143
|
+
) -> "Axis":
|
|
144
|
+
"""Home the axis. ``speed`` and ``backoff`` are in axis units.
|
|
145
|
+
|
|
146
|
+
For ``"stallguard"`` homing, tune ``node.set_stall_threshold()`` and
|
|
147
|
+
use a reduced ``current_percent`` so the crash into the hard stop
|
|
148
|
+
stays gentle.
|
|
149
|
+
"""
|
|
150
|
+
if current_percent is not None:
|
|
151
|
+
self.node.set_param(Param.HOMING_CURRENT, current_percent)
|
|
152
|
+
if backoff is not None:
|
|
153
|
+
self.node.set_param(Param.HOMING_BACKOFF, self.units_to_deg(backoff))
|
|
154
|
+
self.node.home(
|
|
155
|
+
method=method,
|
|
156
|
+
direction=direction,
|
|
157
|
+
speed_deg_s=self.units_to_deg(speed),
|
|
158
|
+
blocking=True,
|
|
159
|
+
timeout=timeout,
|
|
160
|
+
)
|
|
161
|
+
self._homed = True
|
|
162
|
+
return self
|
|
163
|
+
|
|
164
|
+
def set_zero(self) -> "Axis":
|
|
165
|
+
self.node.set_zero()
|
|
166
|
+
self._homed = True
|
|
167
|
+
return self
|
|
168
|
+
|
|
169
|
+
def wait_settled(self, timeout: float = 30.0) -> "Axis":
|
|
170
|
+
self.node.wait_settled(timeout=timeout)
|
|
171
|
+
return self
|
|
172
|
+
|
|
173
|
+
# -- internals ---------------------------------------------------------------------
|
|
174
|
+
|
|
175
|
+
def _check_motion_allowed(
|
|
176
|
+
self, target: Optional[float], speed: Optional[float]
|
|
177
|
+
) -> None:
|
|
178
|
+
if self.require_homing and not self.homed:
|
|
179
|
+
raise NotHomed(f"{self.name}: home the axis before moving it")
|
|
180
|
+
if speed is not None and self.max_speed is not None and abs(speed) > self.max_speed:
|
|
181
|
+
raise LimitViolation(
|
|
182
|
+
f"{self.name}: {abs(speed):.3f} exceeds max_speed {self.max_speed:.3f}"
|
|
183
|
+
)
|
|
184
|
+
if target is not None:
|
|
185
|
+
if self.min_pos is not None and target < self.min_pos:
|
|
186
|
+
raise LimitViolation(
|
|
187
|
+
f"{self.name}: target {target:.3f} below min_pos {self.min_pos:.3f}"
|
|
188
|
+
)
|
|
189
|
+
if self.max_pos is not None and target > self.max_pos:
|
|
190
|
+
raise LimitViolation(
|
|
191
|
+
f"{self.name}: target {target:.3f} above max_pos {self.max_pos:.3f}"
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
def __repr__(self) -> str:
|
|
195
|
+
return (
|
|
196
|
+
f"<Axis {self.name!r} node={self.node.node_id} "
|
|
197
|
+
f"rotation_distance={self.rotation_distance}>"
|
|
198
|
+
)
|