Skip to content

Commit b9049a2

Browse files
committed
add motor configuration
1 parent c30c65a commit b9049a2

1 file changed

Lines changed: 315 additions & 0 deletions

File tree

uc2rest/motor_config.py

Lines changed: 315 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,315 @@
1+
"""
2+
Motor configuration data structures for unified motor parameter management.
3+
This module provides dataclasses for storing motor settings per axis,
4+
including motion parameters, homing configuration, and TMC driver settings.
5+
"""
6+
7+
import json
8+
from dataclasses import dataclass, field, asdict
9+
from typing import Optional, Dict, List
10+
11+
12+
@dataclass
13+
class TMCSettings:
14+
"""TMC stepper driver settings for a single axis."""
15+
msteps: int = 16 # Microsteps (1, 2, 4, 8, 16, 32, 64, 128, 256)
16+
rms_current: int = 500 # RMS current in mA
17+
sgthrs: int = 10 # StallGuard threshold
18+
semin: int = 5 # Minimum coolstep current
19+
semax: int = 2 # Maximum coolstep current
20+
blank_time: int = 24 # Comparator blank time
21+
toff: int = 3 # Off time
22+
23+
def to_dict(self) -> dict:
24+
return asdict(self)
25+
26+
@classmethod
27+
def from_dict(cls, data: dict) -> 'TMCSettings':
28+
if data is None:
29+
return cls()
30+
return cls(**{k: v for k, v in data.items() if k in cls.__dataclass_fields__})
31+
32+
33+
@dataclass
34+
class HomingSettings:
35+
"""Homing configuration for a single axis."""
36+
enabled: bool = False # Is homing enabled for this axis
37+
speed: int = 15000 # Homing speed in steps/s
38+
direction: int = -1 # Homing direction (-1 or 1)
39+
endstop_polarity: int = 1 # Endstop polarity (0=NO, 1=NC)
40+
endpos_release: int = 3000 # Steps to back off after hitting endstop
41+
timeout: int = 20000 # Homing timeout in ms
42+
home_on_start: bool = False # Home this axis on startup
43+
home_steps: int = 0 # Steps to move if no endstop (open-loop homing)
44+
45+
def to_dict(self) -> dict:
46+
return asdict(self)
47+
48+
@classmethod
49+
def from_dict(cls, data: dict) -> 'HomingSettings':
50+
if data is None:
51+
return cls()
52+
return cls(**{k: v for k, v in data.items() if k in cls.__dataclass_fields__})
53+
54+
55+
@dataclass
56+
class MotionSettings:
57+
"""Motion parameters for a single axis."""
58+
step_size: float = 1.0 # Steps per µm (calibrated stepsize)
59+
min_pos: float = float('-inf') # Minimum position limit
60+
max_pos: float = float('inf') # Maximum position limit
61+
backlash: int = 0 # Backlash compensation in steps
62+
max_speed: int = 10000 # Maximum speed in steps/s
63+
initial_speed: int = 10000 # Initial/default speed in steps/s
64+
acceleration: int = 1000000 # Acceleration in steps/s²
65+
66+
def to_dict(self) -> dict:
67+
# Handle inf values for JSON serialization
68+
d = asdict(self)
69+
if d['min_pos'] == float('-inf'):
70+
d['min_pos'] = None
71+
if d['max_pos'] == float('inf'):
72+
d['max_pos'] = None
73+
return d
74+
75+
@classmethod
76+
def from_dict(cls, data: dict) -> 'MotionSettings':
77+
if data is None:
78+
return cls()
79+
# Handle None values from JSON
80+
if data.get('min_pos') is None:
81+
data['min_pos'] = float('-inf')
82+
if data.get('max_pos') is None:
83+
data['max_pos'] = float('inf')
84+
return cls(**{k: v for k, v in data.items() if k in cls.__dataclass_fields__})
85+
86+
87+
@dataclass
88+
class SoftLimitSettings:
89+
"""Soft limit configuration for a single axis."""
90+
enabled: bool = False
91+
min_pos: int = 0
92+
max_pos: int = 0
93+
94+
def to_dict(self) -> dict:
95+
return asdict(self)
96+
97+
@classmethod
98+
def from_dict(cls, data: dict) -> 'SoftLimitSettings':
99+
if data is None:
100+
return cls()
101+
return cls(**{k: v for k, v in data.items() if k in cls.__dataclass_fields__})
102+
103+
104+
@dataclass
105+
class HardLimitSettings:
106+
"""Hard limit (emergency stop) configuration for a single axis."""
107+
enabled: bool = True # Hard limits enabled by default
108+
polarity: int = 0 # 0=NO (Normally Open), 1=NC (Normally Closed)
109+
110+
def to_dict(self) -> dict:
111+
return asdict(self)
112+
113+
@classmethod
114+
def from_dict(cls, data: dict) -> 'HardLimitSettings':
115+
if data is None:
116+
return cls()
117+
return cls(**{k: v for k, v in data.items() if k in cls.__dataclass_fields__})
118+
119+
120+
@dataclass
121+
class AxisConfig:
122+
"""Complete configuration for a single motor axis."""
123+
axis: str # Axis name: "X", "Y", "Z", or "A"
124+
motion: MotionSettings = field(default_factory=MotionSettings)
125+
homing: HomingSettings = field(default_factory=HomingSettings)
126+
tmc: TMCSettings = field(default_factory=TMCSettings)
127+
soft_limits: SoftLimitSettings = field(default_factory=SoftLimitSettings)
128+
hard_limits: HardLimitSettings = field(default_factory=HardLimitSettings)
129+
joystick_inverted: bool = False
130+
131+
def to_dict(self) -> dict:
132+
return {
133+
'axis': self.axis,
134+
'motion': self.motion.to_dict(),
135+
'homing': self.homing.to_dict(),
136+
'tmc': self.tmc.to_dict(),
137+
'soft_limits': self.soft_limits.to_dict(),
138+
'hard_limits': self.hard_limits.to_dict(),
139+
'joystick_inverted': self.joystick_inverted
140+
}
141+
142+
@classmethod
143+
def from_dict(cls, data: dict) -> 'AxisConfig':
144+
if data is None:
145+
raise ValueError("Axis data cannot be None")
146+
return cls(
147+
axis=data.get('axis', 'X'),
148+
motion=MotionSettings.from_dict(data.get('motion')),
149+
homing=HomingSettings.from_dict(data.get('homing')),
150+
tmc=TMCSettings.from_dict(data.get('tmc')),
151+
soft_limits=SoftLimitSettings.from_dict(data.get('soft_limits')),
152+
hard_limits=HardLimitSettings.from_dict(data.get('hard_limits')),
153+
joystick_inverted=data.get('joystick_inverted', False)
154+
)
155+
156+
157+
@dataclass
158+
class MotorSystemConfig:
159+
"""Complete motor system configuration for all axes."""
160+
axes: Dict[str, AxisConfig] = field(default_factory=dict)
161+
axis_order: List[int] = field(default_factory=lambda: [0, 1, 2, 3]) # A, X, Y, Z mapping
162+
is_corexy: bool = False
163+
is_enabled: bool = True
164+
enable_auto: bool = True
165+
is_dual_axis: bool = False # Dual axis mode (A and Z linked)
166+
167+
def __post_init__(self):
168+
# Initialize default axes if not provided
169+
if not self.axes:
170+
for axis_name in ['A', 'X', 'Y', 'Z']:
171+
self.axes[axis_name] = AxisConfig(axis=axis_name)
172+
173+
def get_axis(self, axis: str) -> AxisConfig:
174+
"""Get configuration for a specific axis."""
175+
axis = axis.upper()
176+
if axis not in self.axes:
177+
self.axes[axis] = AxisConfig(axis=axis)
178+
return self.axes[axis]
179+
180+
def set_axis(self, axis: str, config: AxisConfig):
181+
"""Set configuration for a specific axis."""
182+
axis = axis.upper()
183+
self.axes[axis] = config
184+
185+
def to_dict(self) -> dict:
186+
return {
187+
'axes': {k: v.to_dict() for k, v in self.axes.items()},
188+
'axis_order': self.axis_order,
189+
'is_corexy': self.is_corexy,
190+
'is_enabled': self.is_enabled,
191+
'enable_auto': self.enable_auto,
192+
'is_dual_axis': self.is_dual_axis
193+
}
194+
195+
@classmethod
196+
def from_dict(cls, data: dict) -> 'MotorSystemConfig':
197+
if data is None:
198+
return cls()
199+
axes = {}
200+
if 'axes' in data:
201+
for axis_name, axis_data in data['axes'].items():
202+
axes[axis_name] = AxisConfig.from_dict(axis_data)
203+
return cls(
204+
axes=axes,
205+
axis_order=data.get('axis_order', [0, 1, 2, 3]),
206+
is_corexy=data.get('is_corexy', False),
207+
is_enabled=data.get('is_enabled', True),
208+
enable_auto=data.get('enable_auto', True),
209+
is_dual_axis=data.get('is_dual_axis', False)
210+
)
211+
212+
def to_json(self) -> str:
213+
"""Serialize to JSON string."""
214+
return json.dumps(self.to_dict(), indent=2)
215+
216+
@classmethod
217+
def from_json(cls, json_str: str) -> 'MotorSystemConfig':
218+
"""Deserialize from JSON string."""
219+
return cls.from_dict(json.loads(json_str))
220+
221+
@classmethod
222+
def from_imswitch_config(cls, manager_properties: dict, stage_offsets: dict = None) -> 'MotorSystemConfig':
223+
"""
224+
Create MotorSystemConfig from ImSwitch positionerInfo.managerProperties format.
225+
226+
This converts the existing ImSwitch configuration format to the unified format.
227+
"""
228+
config = cls()
229+
230+
# Global settings
231+
config.axis_order = manager_properties.get('axisOrder', [0, 1, 2, 3])
232+
config.is_corexy = manager_properties.get('isCoreXY', False)
233+
config.is_enabled = manager_properties.get('isEnable', True)
234+
config.enable_auto = manager_properties.get('enableauto', True)
235+
config.is_dual_axis = manager_properties.get('isDualaxis', False)
236+
237+
# Per-axis settings
238+
for axis in ['X', 'Y', 'Z', 'A']:
239+
axis_config = config.get_axis(axis)
240+
241+
# Motion settings
242+
axis_config.motion.step_size = manager_properties.get(f'stepsize{axis}', 1)
243+
axis_config.motion.min_pos = manager_properties.get(f'min{axis}', float('-inf'))
244+
axis_config.motion.max_pos = manager_properties.get(f'max{axis}', float('inf'))
245+
axis_config.motion.backlash = manager_properties.get(f'backlash{axis}', 0)
246+
axis_config.motion.max_speed = manager_properties.get(f'maxSpeed{axis}', 10000)
247+
axis_config.motion.initial_speed = manager_properties.get(f'initialSpeed{axis}', 10000)
248+
249+
# Homing settings
250+
axis_config.homing.enabled = manager_properties.get(f'home{axis}enabled', False)
251+
axis_config.homing.speed = manager_properties.get(f'homeSpeed{axis}', 15000)
252+
axis_config.homing.direction = manager_properties.get(f'homeDirection{axis}', -1)
253+
axis_config.homing.endstop_polarity = manager_properties.get(f'homeEndstoppolarity{axis}', 1)
254+
axis_config.homing.endpos_release = manager_properties.get(f'homeEndposRelease{axis}', 3000)
255+
axis_config.homing.timeout = manager_properties.get(f'homeTimeout{axis}', 20000)
256+
axis_config.homing.home_on_start = manager_properties.get(f'homeOnStart{axis}', False)
257+
axis_config.homing.home_steps = manager_properties.get(f'homeSteps{axis}', 0)
258+
259+
# TMC settings (if available)
260+
if manager_properties.get(f'msteps{axis}') is not None:
261+
axis_config.tmc.msteps = manager_properties.get(f'msteps{axis}', 16)
262+
axis_config.tmc.rms_current = manager_properties.get(f'rms_current{axis}', 500)
263+
axis_config.tmc.sgthrs = manager_properties.get(f'sgthrs{axis}', 10)
264+
axis_config.tmc.semin = manager_properties.get(f'semin{axis}', 5)
265+
axis_config.tmc.semax = manager_properties.get(f'semax{axis}', 2)
266+
axis_config.tmc.blank_time = manager_properties.get(f'blank_time{axis}', 24)
267+
axis_config.tmc.toff = manager_properties.get(f'toff{axis}', 3)
268+
269+
config.set_axis(axis, axis_config)
270+
271+
return config
272+
273+
def to_imswitch_properties(self) -> dict:
274+
"""
275+
Convert back to ImSwitch positionerInfo.managerProperties format.
276+
"""
277+
props = {
278+
'axisOrder': self.axis_order,
279+
'isCoreXY': self.is_corexy,
280+
'isEnable': self.is_enabled,
281+
'enableauto': self.enable_auto,
282+
'isDualaxis': self.is_dual_axis,
283+
}
284+
285+
for axis_name, axis_config in self.axes.items():
286+
# Motion settings
287+
props[f'stepsize{axis_name}'] = axis_config.motion.step_size
288+
if axis_config.motion.min_pos != float('-inf'):
289+
props[f'min{axis_name}'] = axis_config.motion.min_pos
290+
if axis_config.motion.max_pos != float('inf'):
291+
props[f'max{axis_name}'] = axis_config.motion.max_pos
292+
props[f'backlash{axis_name}'] = axis_config.motion.backlash
293+
props[f'maxSpeed{axis_name}'] = axis_config.motion.max_speed
294+
props[f'initialSpeed{axis_name}'] = axis_config.motion.initial_speed
295+
296+
# Homing settings
297+
props[f'home{axis_name}enabled'] = axis_config.homing.enabled
298+
props[f'homeSpeed{axis_name}'] = axis_config.homing.speed
299+
props[f'homeDirection{axis_name}'] = axis_config.homing.direction
300+
props[f'homeEndstoppolarity{axis_name}'] = axis_config.homing.endstop_polarity
301+
props[f'homeEndposRelease{axis_name}'] = axis_config.homing.endpos_release
302+
props[f'homeTimeout{axis_name}'] = axis_config.homing.timeout
303+
props[f'homeOnStart{axis_name}'] = axis_config.homing.home_on_start
304+
props[f'homeSteps{axis_name}'] = axis_config.homing.home_steps
305+
306+
# TMC settings
307+
props[f'msteps{axis_name}'] = axis_config.tmc.msteps
308+
props[f'rms_current{axis_name}'] = axis_config.tmc.rms_current
309+
props[f'sgthrs{axis_name}'] = axis_config.tmc.sgthrs
310+
props[f'semin{axis_name}'] = axis_config.tmc.semin
311+
props[f'semax{axis_name}'] = axis_config.tmc.semax
312+
props[f'blank_time{axis_name}'] = axis_config.tmc.blank_time
313+
props[f'toff{axis_name}'] = axis_config.tmc.toff
314+
315+
return props

0 commit comments

Comments
 (0)