moons-motor 0.1.5__py3-none-any.whl → 1.0.0__py3-none-any.whl
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.
- moons_motor/motor.py +550 -520
- moons_motor/observer.py +8 -7
- moons_motor/subject.py +17 -15
- {moons_motor-0.1.5.dist-info → moons_motor-1.0.0.dist-info}/METADATA +2 -1
- moons_motor-1.0.0.dist-info/RECORD +11 -0
- moons_motor-0.1.5.dist-info/RECORD +0 -11
- {moons_motor-0.1.5.dist-info → moons_motor-1.0.0.dist-info}/WHEEL +0 -0
- {moons_motor-0.1.5.dist-info → moons_motor-1.0.0.dist-info}/licenses/LICENSE +0 -0
- {moons_motor-0.1.5.dist-info → moons_motor-1.0.0.dist-info}/top_level.txt +0 -0
moons_motor/motor.py
CHANGED
@@ -1,520 +1,550 @@
|
|
1
|
-
import serial
|
2
|
-
import serial.rs485
|
3
|
-
from serial.tools import list_ports
|
4
|
-
import re
|
5
|
-
import
|
6
|
-
|
7
|
-
from rich
|
8
|
-
from rich.
|
9
|
-
import
|
10
|
-
from moons_motor.subject import Subject
|
11
|
-
import time
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
+
|
41
|
-
+
|
42
|
-
+
|
43
|
-
+
|
44
|
-
|
45
|
-
|
46
|
-
+
|
47
|
-
+
|
48
|
-
+
|
49
|
-
+
|
50
|
-
|
51
|
-
|
52
|
-
+
|
53
|
-
+
|
54
|
-
+
|
55
|
-
+
|
56
|
-
|
57
|
-
|
58
|
-
+
|
59
|
-
+
|
60
|
-
+
|
61
|
-
+
|
62
|
-
|
63
|
-
|
64
|
-
+
|
65
|
-
+
|
66
|
-
+
|
67
|
-
+
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
formatter
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
"
|
124
|
-
"
|
125
|
-
"
|
126
|
-
"
|
127
|
-
"
|
128
|
-
"
|
129
|
-
"
|
130
|
-
"
|
131
|
-
"
|
132
|
-
"
|
133
|
-
"
|
134
|
-
|
135
|
-
"
|
136
|
-
"
|
137
|
-
"
|
138
|
-
"
|
139
|
-
"
|
140
|
-
"
|
141
|
-
"
|
142
|
-
"
|
143
|
-
"
|
144
|
-
"
|
145
|
-
"
|
146
|
-
"
|
147
|
-
"
|
148
|
-
"
|
149
|
-
"
|
150
|
-
"
|
151
|
-
"
|
152
|
-
"
|
153
|
-
"
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
logger
|
158
|
-
|
159
|
-
|
160
|
-
ch
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
self.
|
175
|
-
self.
|
176
|
-
self.
|
177
|
-
self.
|
178
|
-
self.
|
179
|
-
self.
|
180
|
-
|
181
|
-
self.
|
182
|
-
self.
|
183
|
-
self.
|
184
|
-
self.
|
185
|
-
self.
|
186
|
-
self.
|
187
|
-
self.
|
188
|
-
self.readBuffer = ""
|
189
|
-
|
190
|
-
self.console = Console()
|
191
|
-
|
192
|
-
self.is_log_message = True
|
193
|
-
|
194
|
-
self.microstep = {
|
195
|
-
0: 200,
|
196
|
-
1: 400,
|
197
|
-
3: 2000,
|
198
|
-
4: 5000,
|
199
|
-
5: 10000,
|
200
|
-
6: 12800,
|
201
|
-
7: 18000,
|
202
|
-
8: 20000,
|
203
|
-
9: 21600,
|
204
|
-
10: 25000,
|
205
|
-
11: 25400,
|
206
|
-
12: 25600,
|
207
|
-
13: 36000,
|
208
|
-
14: 50000,
|
209
|
-
15: 50800,
|
210
|
-
}
|
211
|
-
|
212
|
-
# region connection & main functions
|
213
|
-
@staticmethod
|
214
|
-
def list_all_ports():
|
215
|
-
ports = list(list_ports.comports())
|
216
|
-
simple_ports = []
|
217
|
-
port_info = ""
|
218
|
-
for p in ports:
|
219
|
-
port_info += f"■ {p.device} {p.description} [blue]{p.usb_info()}[/blue]"
|
220
|
-
if p != ports[-1]:
|
221
|
-
port_info += "\n"
|
222
|
-
simple_ports.append(p.description)
|
223
|
-
print(Panel(port_info, title="All COMPorts"))
|
224
|
-
return simple_ports
|
225
|
-
|
226
|
-
@staticmethod
|
227
|
-
def process_response(response):
|
228
|
-
equal_sign_index = response.index("=")
|
229
|
-
address = response[0]
|
230
|
-
command = response[1:equal_sign_index]
|
231
|
-
value = response[equal_sign_index + 1 :]
|
232
|
-
|
233
|
-
if command == "IT" or command == "IU":
|
234
|
-
# Handle temperature response
|
235
|
-
value = int(value) / 10.0
|
236
|
-
return {
|
237
|
-
"address": address,
|
238
|
-
"command": command,
|
239
|
-
"value": value,
|
240
|
-
}
|
241
|
-
|
242
|
-
def
|
243
|
-
|
244
|
-
self.
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
self.
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
self.
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
self.
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
self.send_command(
|
461
|
-
address=motor_address, command=StepperCommand.
|
462
|
-
)
|
463
|
-
self.send_command(
|
464
|
-
address=motor_address, command=StepperCommand.
|
465
|
-
)
|
466
|
-
self.send_command(
|
467
|
-
address=motor_address, command=StepperCommand.
|
468
|
-
)
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
command
|
474
|
-
|
475
|
-
|
476
|
-
self.
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
"
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
1
|
+
import serial
|
2
|
+
import serial.rs485
|
3
|
+
from serial.tools import list_ports
|
4
|
+
import re
|
5
|
+
import asyncio
|
6
|
+
import serial_asyncio
|
7
|
+
from rich import print
|
8
|
+
from rich.console import Console
|
9
|
+
from rich.panel import Panel
|
10
|
+
from moons_motor.subject import Subject
|
11
|
+
import time
|
12
|
+
|
13
|
+
from dataclasses import dataclass
|
14
|
+
|
15
|
+
import logging
|
16
|
+
|
17
|
+
|
18
|
+
class ColoredFormatter(logging.Formatter):
|
19
|
+
# Define ANSI escape codes for colors and reset
|
20
|
+
grey = "\x1b[38;21m"
|
21
|
+
yellow = "\x1b[33;21m"
|
22
|
+
red = "\x1b[31;21m"
|
23
|
+
bold_red = "\x1b[31;1m"
|
24
|
+
bold_yellow = "\x1b[33;1m"
|
25
|
+
bold_green = "\x1b[32;1m"
|
26
|
+
bold_blue = "\x1b[34;1m"
|
27
|
+
bold_cyan = "\x1b[36;1m"
|
28
|
+
bold_magenta = "\x1b[35;1m"
|
29
|
+
reset = "\x1b[0m"
|
30
|
+
|
31
|
+
# Define the base format string
|
32
|
+
format_time = "%(asctime)s"
|
33
|
+
format_header = " [%(levelname)s]"
|
34
|
+
format_base = " %(message)s"
|
35
|
+
|
36
|
+
# Map log levels to colored format strings
|
37
|
+
FORMATS = {
|
38
|
+
logging.DEBUG: format_time
|
39
|
+
+ bold_cyan
|
40
|
+
+ format_header
|
41
|
+
+ reset
|
42
|
+
+ format_base
|
43
|
+
+ reset,
|
44
|
+
logging.INFO: format_time
|
45
|
+
+ bold_green
|
46
|
+
+ format_header
|
47
|
+
+ reset
|
48
|
+
+ format_base
|
49
|
+
+ reset,
|
50
|
+
logging.WARNING: format_time
|
51
|
+
+ bold_yellow
|
52
|
+
+ format_header
|
53
|
+
+ reset
|
54
|
+
+ format_base
|
55
|
+
+ reset,
|
56
|
+
logging.ERROR: format_time
|
57
|
+
+ bold_red
|
58
|
+
+ format_header
|
59
|
+
+ reset
|
60
|
+
+ format_base
|
61
|
+
+ reset,
|
62
|
+
logging.CRITICAL: format_time
|
63
|
+
+ bold_magenta
|
64
|
+
+ format_header
|
65
|
+
+ reset
|
66
|
+
+ format_base
|
67
|
+
+ reset,
|
68
|
+
}
|
69
|
+
|
70
|
+
def format(self, record):
|
71
|
+
log_fmt = self.FORMATS.get(record.levelno)
|
72
|
+
formatter = logging.Formatter(log_fmt)
|
73
|
+
return formatter.format(record)
|
74
|
+
|
75
|
+
|
76
|
+
class StepperModules:
|
77
|
+
STM17S_3RN = "STM17S-3RN"
|
78
|
+
|
79
|
+
|
80
|
+
@dataclass(frozen=True)
|
81
|
+
class StepperCommand:
|
82
|
+
JOG: str = "CJ" # Start jogging
|
83
|
+
JOG_SPEED: str = "JS" # Jogging speed (Need to set before start jogging)
|
84
|
+
JOG_ACCELERATION: str = (
|
85
|
+
"JA" # Jogging acceleration (Need to set before start jogging)
|
86
|
+
)
|
87
|
+
CHANGE_JOG_SPEED: str = "CS" # Change jogging speed while jogging
|
88
|
+
STOP_JOG: str = "SJ" # Stop jogging with deceleration
|
89
|
+
STOP: str = "ST" # Stop immediately (No deceleration)
|
90
|
+
STOP_DECEL: str = "STD" # Stop with deceleration
|
91
|
+
STOP_KILL: str = (
|
92
|
+
"SK" # Stop with deceleration(Control by AM) and kill all unexecuted commands
|
93
|
+
)
|
94
|
+
STOP_KILL_DECEL: str = (
|
95
|
+
"SKD" # Stop and kill all unexecuted commands with deceleration(Control by DE)
|
96
|
+
)
|
97
|
+
ENABLE: str = "ME" # Enable motor
|
98
|
+
DISABLE: str = "MD" # Disable motor
|
99
|
+
MOVE_ABSOLUTE: str = "FP" # Move to absolute position
|
100
|
+
MOVE_FIXED_DISTANCE: str = "FL" # Move to fixed distance
|
101
|
+
POSITION: str = "IP" # Motor absolute position(Calculated trajectory position)
|
102
|
+
TEMPERATURE: str = "IT" # Motor temperature
|
103
|
+
VOLTAGE: str = "IU" # Motor voltage
|
104
|
+
|
105
|
+
ENCODER_POSITION: str = "EP" # Encoder position
|
106
|
+
SET_POSITION: str = "SP" # Set encoder position
|
107
|
+
|
108
|
+
HOME: str = "SH" # Home position
|
109
|
+
VELOCITY: str = "VE" # Set velocity
|
110
|
+
|
111
|
+
ALARM_RESET: str = "AR" # Reset alarm
|
112
|
+
|
113
|
+
SET_RETURN_FORMAT_DECIMAL: str = "IFD" # Set return format to decimal
|
114
|
+
SET_RETURN_FORMAT_HEXADECIMAL: str = "IFH" # Set return format to hexadecimal
|
115
|
+
|
116
|
+
SET_TRANSMIT_DELAY: str = "TD" # Set transmit delay
|
117
|
+
REQUEST_STATUS: str = "RS" # Request status
|
118
|
+
|
119
|
+
|
120
|
+
class MoonsStepper(Subject):
|
121
|
+
motorAdress = [
|
122
|
+
"0",
|
123
|
+
"1",
|
124
|
+
"2",
|
125
|
+
"3",
|
126
|
+
"4",
|
127
|
+
"5",
|
128
|
+
"6",
|
129
|
+
"7",
|
130
|
+
"8",
|
131
|
+
"9",
|
132
|
+
"!",
|
133
|
+
'"',
|
134
|
+
"#",
|
135
|
+
"$",
|
136
|
+
"%",
|
137
|
+
"&",
|
138
|
+
"'",
|
139
|
+
"(",
|
140
|
+
")",
|
141
|
+
"*",
|
142
|
+
"+",
|
143
|
+
",",
|
144
|
+
"-",
|
145
|
+
".",
|
146
|
+
"/",
|
147
|
+
":",
|
148
|
+
";",
|
149
|
+
"<",
|
150
|
+
"=",
|
151
|
+
">",
|
152
|
+
"?",
|
153
|
+
"@",
|
154
|
+
]
|
155
|
+
# Configure logging
|
156
|
+
logger = logging.getLogger(__name__)
|
157
|
+
logger.setLevel(logging.INFO)
|
158
|
+
|
159
|
+
ch = logging.StreamHandler()
|
160
|
+
ch.setFormatter(ColoredFormatter())
|
161
|
+
logger.addHandler(ch)
|
162
|
+
|
163
|
+
def __init__(
|
164
|
+
self,
|
165
|
+
model: StepperModules,
|
166
|
+
VID,
|
167
|
+
PID,
|
168
|
+
SERIAL_NUM,
|
169
|
+
only_simlate=False,
|
170
|
+
universe=0,
|
171
|
+
):
|
172
|
+
super().__init__()
|
173
|
+
self.universe = universe
|
174
|
+
self.model = model # Motor model
|
175
|
+
self.only_simulate = only_simlate
|
176
|
+
self.device = "" # COM port description
|
177
|
+
self.VID = VID
|
178
|
+
self.PID = PID
|
179
|
+
self.SERIAL_NUM = SERIAL_NUM # ID for determent the deivice had same VID and PID, can be config using chips manufacturer tool
|
180
|
+
|
181
|
+
self.is_connected = False
|
182
|
+
self.ser_reader = None
|
183
|
+
self.ser_writer = None
|
184
|
+
self.send_queue = asyncio.Queue()
|
185
|
+
self.recv_queue = asyncio.Queue()
|
186
|
+
self.pending_futures = {}
|
187
|
+
self._io_tasks = []
|
188
|
+
self.readBuffer = ""
|
189
|
+
|
190
|
+
self.console = Console()
|
191
|
+
|
192
|
+
self.is_log_message = True
|
193
|
+
|
194
|
+
self.microstep = {
|
195
|
+
0: 200,
|
196
|
+
1: 400,
|
197
|
+
3: 2000,
|
198
|
+
4: 5000,
|
199
|
+
5: 10000,
|
200
|
+
6: 12800,
|
201
|
+
7: 18000,
|
202
|
+
8: 20000,
|
203
|
+
9: 21600,
|
204
|
+
10: 25000,
|
205
|
+
11: 25400,
|
206
|
+
12: 25600,
|
207
|
+
13: 36000,
|
208
|
+
14: 50000,
|
209
|
+
15: 50800,
|
210
|
+
}
|
211
|
+
|
212
|
+
# region connection & main functions
|
213
|
+
@staticmethod
|
214
|
+
def list_all_ports():
|
215
|
+
ports = list(list_ports.comports())
|
216
|
+
simple_ports = []
|
217
|
+
port_info = ""
|
218
|
+
for p in ports:
|
219
|
+
port_info += f"■ {p.device} {p.description} [blue]{p.usb_info()}[/blue]"
|
220
|
+
if p != ports[-1]:
|
221
|
+
port_info += "\n"
|
222
|
+
simple_ports.append(p.description)
|
223
|
+
print(Panel(port_info, title="All COMPorts"))
|
224
|
+
return simple_ports
|
225
|
+
|
226
|
+
@staticmethod
|
227
|
+
def process_response(response):
|
228
|
+
equal_sign_index = response.index("=")
|
229
|
+
address = response[0]
|
230
|
+
command = response[1:equal_sign_index]
|
231
|
+
value = response[equal_sign_index + 1 :]
|
232
|
+
|
233
|
+
if command == "IT" or command == "IU":
|
234
|
+
# Handle temperature response
|
235
|
+
value = int(value) / 10.0
|
236
|
+
return {
|
237
|
+
"address": address,
|
238
|
+
"command": command,
|
239
|
+
"value": value,
|
240
|
+
}
|
241
|
+
|
242
|
+
async def connect(self, COM=None, baudrate=9600, callback=None):
|
243
|
+
# Simulate mode
|
244
|
+
if self.only_simulate:
|
245
|
+
self.is_connected = True
|
246
|
+
self.device = f"Simulate-{self.universe}"
|
247
|
+
MoonsStepper.logger.info(f"{self.device} connected")
|
248
|
+
if callback:
|
249
|
+
callback(self.device, self.is_connected)
|
250
|
+
return
|
251
|
+
|
252
|
+
# Find port
|
253
|
+
ports = list(list_ports.comports())
|
254
|
+
found_port = None
|
255
|
+
if COM is not None:
|
256
|
+
for p in ports:
|
257
|
+
if p.device == COM:
|
258
|
+
found_port = p
|
259
|
+
self.device = p.description
|
260
|
+
break
|
261
|
+
if not found_port:
|
262
|
+
MoonsStepper.logger.error(f"Specified COM port {COM} not found.")
|
263
|
+
if callback:
|
264
|
+
callback(self.device, False)
|
265
|
+
return
|
266
|
+
else:
|
267
|
+
# Auto-detect port by VID/PID
|
268
|
+
for p in ports:
|
269
|
+
m = re.match(
|
270
|
+
r"USB\s*VID:PID=(\w+):(\w+)\s*SER=([A-Za-z0-9]*)", p.usb_info()
|
271
|
+
)
|
272
|
+
if (
|
273
|
+
m
|
274
|
+
and m.group(1) == self.VID
|
275
|
+
and m.group(2) == self.PID
|
276
|
+
and (m.group(3) == self.SERIAL_NUM or self.SERIAL_NUM == "")
|
277
|
+
):
|
278
|
+
MoonsStepper.logger.info(
|
279
|
+
f"Device found: {p.description} | VID: {m.group(1)} | PID: {m.group(2)} | SER: {m.group(3)}"
|
280
|
+
)
|
281
|
+
self.device = p.description
|
282
|
+
found_port = p
|
283
|
+
break
|
284
|
+
|
285
|
+
if not found_port:
|
286
|
+
MoonsStepper.logger.error(
|
287
|
+
f"Device with VID={self.VID}, PID={self.PID} not found."
|
288
|
+
)
|
289
|
+
if callback:
|
290
|
+
callback(self.device, self.is_connected)
|
291
|
+
return
|
292
|
+
|
293
|
+
# Attempt to connect
|
294
|
+
try:
|
295
|
+
# --- THIS IS THE CRITICAL RS485 PART ---
|
296
|
+
rs485_settings = serial.rs485.RS485Settings(
|
297
|
+
rts_level_for_tx=True,
|
298
|
+
rts_level_for_rx=False,
|
299
|
+
loopback=False,
|
300
|
+
delay_before_tx=0.02,
|
301
|
+
delay_before_rx=0.02,
|
302
|
+
)
|
303
|
+
self.ser_reader, self.ser_writer = (
|
304
|
+
await serial_asyncio.open_serial_connection(
|
305
|
+
url=found_port.device,
|
306
|
+
baudrate=baudrate,
|
307
|
+
# rs485_mode=rs485_settings, # Pass the settings here
|
308
|
+
)
|
309
|
+
)
|
310
|
+
# ----------------------------------------
|
311
|
+
|
312
|
+
self.is_connected = True
|
313
|
+
MoonsStepper.logger.info(f"Device connected: {self.device}")
|
314
|
+
|
315
|
+
# Start I/O handlers
|
316
|
+
read_task = asyncio.create_task(self._read_handler())
|
317
|
+
write_task = asyncio.create_task(self._write_handler())
|
318
|
+
self._io_tasks = [read_task, write_task]
|
319
|
+
|
320
|
+
except Exception as e:
|
321
|
+
MoonsStepper.logger.error(f"Device connection error: {e}")
|
322
|
+
self.is_connected = False
|
323
|
+
|
324
|
+
await asyncio.sleep(0.1)
|
325
|
+
|
326
|
+
if callback:
|
327
|
+
callback(self.device, self.is_connected)
|
328
|
+
|
329
|
+
async def _read_handler(self):
|
330
|
+
MoonsStepper.logger.info("Read handler started.")
|
331
|
+
while self.is_connected:
|
332
|
+
try:
|
333
|
+
response_bytes = await self.ser_reader.readuntil(b"\r")
|
334
|
+
response = response_bytes.decode("ascii", errors="ignore").strip()
|
335
|
+
if response:
|
336
|
+
self.handle_recv(response)
|
337
|
+
except asyncio.CancelledError:
|
338
|
+
MoonsStepper.logger.info("Read handler cancelled.")
|
339
|
+
break
|
340
|
+
except Exception as e:
|
341
|
+
MoonsStepper.logger.error(f"Read error: {e}")
|
342
|
+
self.is_connected = False
|
343
|
+
break
|
344
|
+
|
345
|
+
async def _write_handler(self):
|
346
|
+
MoonsStepper.logger.info("Write handler started.")
|
347
|
+
while self.is_connected:
|
348
|
+
try:
|
349
|
+
command = await self.send_queue.get()
|
350
|
+
|
351
|
+
full_command = (command + "\r").encode("ascii")
|
352
|
+
self.ser_writer.write(full_command)
|
353
|
+
await self.ser_writer.drain()
|
354
|
+
|
355
|
+
if self.is_log_message:
|
356
|
+
MoonsStepper.logger.debug(f"Sent to {self.device}: {command}")
|
357
|
+
|
358
|
+
self.send_queue.task_done()
|
359
|
+
await asyncio.sleep(0.05)
|
360
|
+
except asyncio.CancelledError:
|
361
|
+
MoonsStepper.logger.info("Write handler cancelled.")
|
362
|
+
break
|
363
|
+
except Exception as e:
|
364
|
+
MoonsStepper.logger.error(f"Write error: {e}")
|
365
|
+
self.is_connected = False
|
366
|
+
break
|
367
|
+
|
368
|
+
async def disconnect(self):
|
369
|
+
if not self.is_connected and not self.only_simulate:
|
370
|
+
return
|
371
|
+
|
372
|
+
# For simulation
|
373
|
+
if self.only_simulate:
|
374
|
+
self.is_connected = False
|
375
|
+
MoonsStepper.logger.info(f"Simulate-{self.universe} disconnected")
|
376
|
+
return
|
377
|
+
|
378
|
+
# Stop I/O tasks
|
379
|
+
self.is_connected = False
|
380
|
+
for task in self._io_tasks:
|
381
|
+
task.cancel()
|
382
|
+
|
383
|
+
# Wait for tasks to finish cancellation
|
384
|
+
await asyncio.gather(*self._io_tasks, return_exceptions=True)
|
385
|
+
|
386
|
+
# Clear queues
|
387
|
+
while not self.send_queue.empty():
|
388
|
+
self.send_queue.get_nowait()
|
389
|
+
while not self.recv_queue.empty():
|
390
|
+
self.recv_queue.get_nowait()
|
391
|
+
|
392
|
+
# Close serial connection
|
393
|
+
if self.ser_writer:
|
394
|
+
self.ser_writer.close()
|
395
|
+
await self.ser_writer.wait_closed()
|
396
|
+
|
397
|
+
MoonsStepper.logger.info(f"Device disconnected: {self.device}")
|
398
|
+
|
399
|
+
async def send_command(self, address="", command="", value=None):
|
400
|
+
if not self.is_connected and not self.only_simulate:
|
401
|
+
MoonsStepper.logger.warning("Not connected. Cannot send command.")
|
402
|
+
return
|
403
|
+
|
404
|
+
if command == "":
|
405
|
+
MoonsStepper.logger.warning("Command can't be empty")
|
406
|
+
return
|
407
|
+
|
408
|
+
if value is not None:
|
409
|
+
command_str = self.addressed_cmd(address, command + str(value))
|
410
|
+
else:
|
411
|
+
command_str = self.addressed_cmd(address, command)
|
412
|
+
|
413
|
+
await self.send_queue.put(command_str)
|
414
|
+
await super().notify_observers(f"{self.universe}-{command_str}")
|
415
|
+
|
416
|
+
def handle_recv(self, response):
|
417
|
+
# First, process the response to extract key information
|
418
|
+
try:
|
419
|
+
processed = MoonsStepper.process_response(response)
|
420
|
+
address = processed.get("address")
|
421
|
+
command = processed.get("command")
|
422
|
+
# Create a unique key for the request-response pair
|
423
|
+
future_key = (address, command)
|
424
|
+
|
425
|
+
# Check if a future is waiting for this specific response
|
426
|
+
if future_key in self.pending_futures:
|
427
|
+
future = self.pending_futures.pop(future_key)
|
428
|
+
future.set_result(processed) # Set the future's result
|
429
|
+
MoonsStepper.logger.debug(f"Future for {future_key} resolved.")
|
430
|
+
return # Stop further processing
|
431
|
+
|
432
|
+
except Exception as e:
|
433
|
+
# This can happen for simple ACK/NACK responses that don't have an '='
|
434
|
+
MoonsStepper.logger.debug(f"Received non-standard response: {response}. Error: {e}")
|
435
|
+
|
436
|
+
# Handle general ACKs or unexpected messages
|
437
|
+
if "*" in response:
|
438
|
+
MoonsStepper.logger.info(f"(o)buffered_ack")
|
439
|
+
elif "%" in response:
|
440
|
+
MoonsStepper.logger.info(f"(v)success_ack")
|
441
|
+
elif "?" in response:
|
442
|
+
MoonsStepper.logger.info(f"(x)fail_ack")
|
443
|
+
else:
|
444
|
+
MoonsStepper.logger.info(f"Received unhandled message from {self.device}: {response}")
|
445
|
+
# Optionally, put unhandled messages into the general queue
|
446
|
+
self.recv_queue.put_nowait(response)
|
447
|
+
|
448
|
+
# endregion
|
449
|
+
|
450
|
+
# region motor motion functions
|
451
|
+
|
452
|
+
# def setup_motor(self, motor_address="", kill=False):
|
453
|
+
# if kill:
|
454
|
+
# self.stop_and_kill(motor_address)
|
455
|
+
# self.set_transmit_delay(motor_address, 25)
|
456
|
+
# self.set_return_format_dexcimal(motor_address)
|
457
|
+
|
458
|
+
async def home(self, motor_address="", speed=0.3):
|
459
|
+
# Send initial homing commands
|
460
|
+
await self.send_command(
|
461
|
+
address=motor_address, command=StepperCommand.VELOCITY, value=speed
|
462
|
+
)
|
463
|
+
await self.send_command(
|
464
|
+
address=motor_address, command=StepperCommand.HOME, value="3F"
|
465
|
+
)
|
466
|
+
await self.send_command(
|
467
|
+
address=motor_address, command=StepperCommand.ENCODER_POSITION, value=0
|
468
|
+
)
|
469
|
+
await self.send_command(
|
470
|
+
address=motor_address, command=StepperCommand.SET_POSITION, value=0
|
471
|
+
)
|
472
|
+
|
473
|
+
MoonsStepper.logger.info(f"Homing command sent to address {motor_address}. Polling for completion...")
|
474
|
+
|
475
|
+
# Loop until the motor is no longer in the homing state
|
476
|
+
while self.is_connected:
|
477
|
+
result = await self.get_status(
|
478
|
+
motor_address=motor_address,
|
479
|
+
command=StepperCommand.REQUEST_STATUS,
|
480
|
+
)
|
481
|
+
|
482
|
+
if result and "H" not in result.get("value", ""):
|
483
|
+
MoonsStepper.logger.info(f"Motor at address {motor_address} is homed.")
|
484
|
+
return # Homing is complete
|
485
|
+
|
486
|
+
MoonsStepper.logger.info(f"Motor at address {motor_address} is not homed yet. Waiting...")
|
487
|
+
# Wait a bit before polling again
|
488
|
+
await asyncio.sleep(0.5)
|
489
|
+
|
490
|
+
# endregion
|
491
|
+
async def get_status(self, motor_address, command: StepperCommand):
|
492
|
+
future = asyncio.get_running_loop().create_future()
|
493
|
+
future_key = (motor_address, command)
|
494
|
+
self.pending_futures[future_key] = future
|
495
|
+
|
496
|
+
await self.send_command(motor_address, command)
|
497
|
+
|
498
|
+
try:
|
499
|
+
# Wait for the future to be resolved by handle_recv
|
500
|
+
result = await asyncio.wait_for(future, timeout=2.0) # 2-second timeout
|
501
|
+
return result
|
502
|
+
except asyncio.TimeoutError:
|
503
|
+
MoonsStepper.logger.error(f"Timeout waiting for response for {future_key}")
|
504
|
+
# Clean up the pending future on timeout
|
505
|
+
self.pending_futures.pop(future_key, None)
|
506
|
+
return None
|
507
|
+
|
508
|
+
def decode_status(status_code):
|
509
|
+
"""
|
510
|
+
Decode the status code from the motor.
|
511
|
+
"""
|
512
|
+
status = {
|
513
|
+
"A": "An Alarm code is present (use AL command to see code, AR command to clear code)",
|
514
|
+
"D": "Disabled (the drive is disabled)",
|
515
|
+
"E": "Drive Fault (drive must be reset by AR command to clear this fault)",
|
516
|
+
"F": "Motor moving",
|
517
|
+
"H": "Homing (SH in progress)",
|
518
|
+
"J": "Jogging (CJ in progress)",
|
519
|
+
"M": "Motion in progress (Feed & Jog Commands)",
|
520
|
+
"P": "In position",
|
521
|
+
"R": "Ready (Drive is enabled and ready)",
|
522
|
+
"S": "Stopping a motion (ST or SK command executing)",
|
523
|
+
"T": "Wait Time (WT command executing)",
|
524
|
+
"W": "Wait Input (WI command executing)",
|
525
|
+
}
|
526
|
+
status_string = ""
|
527
|
+
for char in status_code:
|
528
|
+
if char in status:
|
529
|
+
status_string += status[char]
|
530
|
+
status_string += "\n"
|
531
|
+
else:
|
532
|
+
status_string += f"Unknown status code: {char}"
|
533
|
+
return status_string
|
534
|
+
|
535
|
+
# endregion
|
536
|
+
|
537
|
+
# region utility functions
|
538
|
+
|
539
|
+
def addressed_cmd(self, motor_address, command):
|
540
|
+
return f"{motor_address}{command}"
|
541
|
+
|
542
|
+
|
543
|
+
# endregion
|
544
|
+
|
545
|
+
# SERIAL => 上次已知父系(尾巴+A) 或是事件分頁
|
546
|
+
# reg USB\s*VID:PID=(\w+):(\w+)\s*SER=([A-Za-z0-9]+)
|
547
|
+
|
548
|
+
|
549
|
+
# serial_num 裝置例項路徑
|
550
|
+
# TD(Tramsmit Delay) = 15
|
moons_motor/observer.py
CHANGED
@@ -1,7 +1,8 @@
|
|
1
|
-
from abc import ABC, abstractmethod
|
2
|
-
|
3
|
-
|
4
|
-
@abstractmethod
|
5
|
-
class Observer(ABC):
|
6
|
-
|
7
|
-
|
1
|
+
from abc import ABC, abstractmethod
|
2
|
+
|
3
|
+
|
4
|
+
@abstractmethod
|
5
|
+
class Observer(ABC):
|
6
|
+
@abstractmethod
|
7
|
+
async def update(self, event):
|
8
|
+
pass
|
moons_motor/subject.py
CHANGED
@@ -1,15 +1,17 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
1
|
+
import asyncio
|
2
|
+
|
3
|
+
class Subject:
|
4
|
+
def __init__(self):
|
5
|
+
self._observers = []
|
6
|
+
|
7
|
+
def register(self, observer):
|
8
|
+
if observer not in self._observers:
|
9
|
+
self._observers.append(observer)
|
10
|
+
|
11
|
+
def unregister(self, observer):
|
12
|
+
if observer in self._observers:
|
13
|
+
self._observers.remove(observer)
|
14
|
+
|
15
|
+
async def notify_observers(self, event):
|
16
|
+
tasks = [observer.update(event) for observer in self._observers]
|
17
|
+
await asyncio.gather(*tasks)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: moons_motor
|
3
|
-
Version: 0.
|
3
|
+
Version: 1.0.0
|
4
4
|
Summary: This is a python library for controlling the Moons' motor through the serial port.
|
5
5
|
Author-email: miroc <mike8503111@gmail.com>
|
6
6
|
Project-URL: Repository, https://github.com/miroc99/moons_motor.git
|
@@ -16,6 +16,7 @@ Requires-Dist: pyserial
|
|
16
16
|
Requires-Dist: rich
|
17
17
|
Requires-Dist: python-socketio
|
18
18
|
Requires-Dist: requests
|
19
|
+
Requires-Dist: pyserial_asyncio
|
19
20
|
Dynamic: license-file
|
20
21
|
|
21
22
|
# Moons Motor
|
@@ -0,0 +1,11 @@
|
|
1
|
+
moons_motor/__init__.py,sha256=qOpsRwizV-DpKSvNzyvj8ju3cs6vwgIICur1Oe6sxOA,27
|
2
|
+
moons_motor/motor.py,sha256=ACqfttTr9IntPC4AnX351CuxQx1AQLqBXuM46wppagU,17972
|
3
|
+
moons_motor/observer.py,sha256=uAZtAxYU0nziR71TFsO-ZtXQGtD4lwmOCMBEWgG7t60,143
|
4
|
+
moons_motor/simulate.py,sha256=J0y1fZhoOim9i-BAkprxnPern1SAdkDfKPqT2MWyDwU,2561
|
5
|
+
moons_motor/status.py,sha256=jXQZFZTt9ugHktkWKLII8MpEQQaeO-UjlwTrrP4LJNE,2872
|
6
|
+
moons_motor/subject.py,sha256=4ev1qB6r62LHjcMB7rK7sXUUWQEjyUhOItHhOnY1aig,486
|
7
|
+
moons_motor-1.0.0.dist-info/licenses/LICENSE,sha256=nsYjO800SjIjI85y2kVHR5mC3tca2vs4kK_BhNe89bM,1074
|
8
|
+
moons_motor-1.0.0.dist-info/METADATA,sha256=eAsit-qZW6kKtVRipQi_GuWH747S1dg0c6xUl0S-P3w,1436
|
9
|
+
moons_motor-1.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
10
|
+
moons_motor-1.0.0.dist-info/top_level.txt,sha256=0dE-CR5_NYBw34jHIDGQNWpMllzO6mtUIuKyRv_rJLg,12
|
11
|
+
moons_motor-1.0.0.dist-info/RECORD,,
|
@@ -1,11 +0,0 @@
|
|
1
|
-
moons_motor/__init__.py,sha256=qOpsRwizV-DpKSvNzyvj8ju3cs6vwgIICur1Oe6sxOA,27
|
2
|
-
moons_motor/motor.py,sha256=LujQRwyndbeowiGcjvfPt_L8j3B2582hDJLYMD-8suM,17381
|
3
|
-
moons_motor/observer.py,sha256=PXzuPYKRb2HpjArJcD8HakYIPfFGAs1uBDIL8PSizgA,124
|
4
|
-
moons_motor/simulate.py,sha256=J0y1fZhoOim9i-BAkprxnPern1SAdkDfKPqT2MWyDwU,2561
|
5
|
-
moons_motor/status.py,sha256=jXQZFZTt9ugHktkWKLII8MpEQQaeO-UjlwTrrP4LJNE,2872
|
6
|
-
moons_motor/subject.py,sha256=L_GS6fvJTeX7X23o3T92oiZ4rtLVKA2OEd9GpHn_Dz4,445
|
7
|
-
moons_motor-0.1.5.dist-info/licenses/LICENSE,sha256=nsYjO800SjIjI85y2kVHR5mC3tca2vs4kK_BhNe89bM,1074
|
8
|
-
moons_motor-0.1.5.dist-info/METADATA,sha256=enjd1RbUxi1c2mzwIAXenUEu-KegcywTe70SoIXiwQs,1403
|
9
|
-
moons_motor-0.1.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
10
|
-
moons_motor-0.1.5.dist-info/top_level.txt,sha256=0dE-CR5_NYBw34jHIDGQNWpMllzO6mtUIuKyRv_rJLg,12
|
11
|
-
moons_motor-0.1.5.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|