1
+ import configparser
1
2
import glob
2
3
import json
3
4
import math
5
+ import os
6
+ import pkg_resources
4
7
import sys
5
8
import time
6
9
from threading import Event
7
10
8
11
import serial
9
12
10
- from opentrons .drivers .virtual_smoothie import VirtualSmoothie
11
13
from opentrons .util .log import get_logger
12
14
from opentrons .util .vector import Vector
13
15
14
16
from opentrons .util import trace
15
17
16
18
19
+ DEFAULTS_DIR_PATH = pkg_resources .resource_filename (
20
+ 'opentrons.config' , 'smoothie' )
21
+ DEFAULTS_FILE_PATH = os .path .join (DEFAULTS_DIR_PATH , 'smoothie-defaults.ini' )
22
+ CONFIG_DIR_PATH = os .environ .get ('APP_DATA_DIR' , os .getcwd ())
23
+ CONFIG_DIR_PATH = os .path .join (CONFIG_DIR_PATH , 'smoothie' )
24
+ CONFIG_FILE_PATH = os .path .join (CONFIG_DIR_PATH , 'smoothie-config.ini' )
25
+
17
26
JSON_ERROR = None
18
27
if sys .version_info > (3 , 4 ):
19
28
JSON_ERROR = ValueError
@@ -41,23 +50,22 @@ class CNCDriver(object):
41
50
ACCELERATION = 'M204'
42
51
MOTORS_ON = 'M17'
43
52
MOTORS_OFF = 'M18'
53
+ STEPS_PER_MM = 'M92'
44
54
45
55
DISENGAGE_FEEDBACK = 'M63'
46
56
47
57
ABSOLUTE_POSITIONING = 'G90'
48
58
RELATIVE_POSITIONING = 'G91'
49
59
50
- GET_OT_VERSION = 'config-get sd ot_version'
60
+ CONFIG_GET = 'config-get sd'
61
+ CONFIG_SET = 'config-set sd'
62
+ OT_VERSION = 'ot_version'
51
63
GET_FIRMWARE_VERSION = 'version'
52
- GET_CONFIG_VERSION = 'config-get sd version'
53
- GET_STEPS_PER_MM = {
54
- 'x' : 'config-get sd alpha_steps_per_mm' ,
55
- 'y' : 'config-get sd beta_steps_per_mm'
56
- }
57
-
58
- SET_STEPS_PER_MM = {
59
- 'x' : 'config-set sd alpha_steps_per_mm ' ,
60
- 'y' : 'config-set sd beta_steps_per_mm '
64
+ CONFIG_VERSION = 'version'
65
+ CONFIG_STEPS_PER_MM = {
66
+ 'x' : 'alpha_steps_per_mm' ,
67
+ 'y' : 'beta_steps_per_mm' ,
68
+ 'z' : 'gamma_steps_per_mm'
61
69
}
62
70
63
71
MOSFET = [
@@ -74,32 +82,68 @@ class CNCDriver(object):
74
82
"""
75
83
connection = None
76
84
77
- serial_timeout = 0.1
85
+ serial_timeout = None
86
+ serial_baudrate = None
78
87
79
- # TODO: move to config
80
- COMPATIBLE_FIRMARE = ['v1.0.5' ]
81
- COMPATIBLE_CONFIG = ['v1.2.0' ]
82
88
firmware_version = None
83
89
config_version = None
84
-
85
90
ot_version = None
86
- ot_one_dimensions = {
87
- 'hood' : Vector (400 , 250 , 100 ),
88
- 'one_pro' : Vector (400 , 400 , 100 ),
89
- 'one_standard' : Vector (400 , 400 , 100 )
90
- }
91
91
92
92
def __init__ (self ):
93
+
93
94
self .stopped = Event ()
94
95
self .can_move = Event ()
95
96
self .resume ()
96
- self .head_speed = 3000 # smoothie's default speed in mm/minute
97
97
self .current_commands = []
98
98
99
- self .SMOOTHIE_SUCCESS = 'Succes '
99
+ self .SMOOTHIE_SUCCESS = 'Success '
100
100
self .SMOOTHIE_ERROR = 'Received unexpected response from Smoothie'
101
101
self .STOPPED = 'Received a STOP signal and exited from movements'
102
102
103
+ self .ignore_smoothie_sd = False
104
+
105
+ self .defaults = configparser .ConfigParser ()
106
+ self .defaults .read (DEFAULTS_FILE_PATH )
107
+ self ._create_saved_settings_file ()
108
+ self .saved_settings = configparser .ConfigParser ()
109
+ self .saved_settings .read (CONFIG_FILE_PATH )
110
+ self ._copy_defaults_to_settings ()
111
+ self ._apply_settings ()
112
+
113
+ def _create_saved_settings_file (self ):
114
+ if not os .path .isdir (CONFIG_DIR_PATH ):
115
+ os .mkdir (CONFIG_DIR_PATH )
116
+ if not os .path .isfile (CONFIG_FILE_PATH ):
117
+ with open (CONFIG_FILE_PATH , 'w' ) as configfile :
118
+ configfile .write ('' )
119
+
120
+ def _copy_defaults_to_settings (self ):
121
+ for n in self .defaults .sections ():
122
+ if n not in self .saved_settings :
123
+ self .saved_settings [n ] = self .defaults [n ]
124
+ for key , val in self .defaults [n ].items ():
125
+ if key not in self .saved_settings [n ]:
126
+ self .saved_settings [n ][key ] = val
127
+
128
+ def _apply_settings (self ):
129
+ self .serial_timeout = float (
130
+ self .saved_settings ['serial' ].get ('timeout' , 0.1 ))
131
+ self .serial_baudrate = int (
132
+ self .saved_settings ['serial' ].get ('baudrate' , 115200 ))
133
+
134
+ self .head_speed = int (
135
+ self .saved_settings ['state' ].get ('head_speed' , 3000 ))
136
+
137
+ self .COMPATIBLE_FIRMARE = json .loads (
138
+ self .saved_settings ['versions' ].get ('firmware' , '[]' ))
139
+ self .COMPATIBLE_CONFIG = json .loads (
140
+ self .saved_settings ['versions' ].get ('config' , '[]' ))
141
+ self .ot_one_dimensions = json .loads (
142
+ self .saved_settings ['versions' ].get ('ot_versions' , '{}' ))
143
+ for key in self .ot_one_dimensions .keys ():
144
+ axis_size = Vector (self .ot_one_dimensions [key ])
145
+ self .ot_one_dimensions [key ] = axis_size
146
+
103
147
def get_connected_port (self ):
104
148
"""
105
149
Returns the port the driver is currently connected to
@@ -128,6 +172,8 @@ def get_serial_ports_list(self):
128
172
sys .platform .startswith ('cygwin' )):
129
173
# this excludes your current terminal "/dev/tty"
130
174
ports = glob .glob ('/dev/tty*' )
175
+ # ignore Smoothie's local storage if linux (temporary work-around)
176
+ self .ignore_smoothie_sd = True
131
177
elif sys .platform .startswith ('darwin' ):
132
178
ports = glob .glob ('/dev/tty.*' )
133
179
else :
@@ -157,6 +203,12 @@ def connect(self, device):
157
203
self .reset_port ()
158
204
log .debug ("Connected to {}" .format (device ))
159
205
self .versions_compatible ()
206
+ # set the previously saved steps_per_mm values for X and Y
207
+ if self .ignore_smoothie_sd :
208
+ for axis in 'xyz' :
209
+ self .set_steps_per_mm (
210
+ axis , self .saved_settings ['config' ].get (
211
+ self .CONFIG_STEPS_PER_MM [axis ]))
160
212
return self .calm_down ()
161
213
162
214
def is_connected (self ):
@@ -184,7 +236,7 @@ def check_paused_stopped(self):
184
236
self .can_move .wait ()
185
237
if self .stopped .is_set ():
186
238
self .resume ()
187
- raise RuntimeWarning ('Stop signal received' )
239
+ raise RuntimeWarning (self . STOPPED )
188
240
189
241
def send_command (self , command , ** kwargs ):
190
242
"""
@@ -194,7 +246,7 @@ def send_command(self, command, **kwargs):
194
246
Returns a string with the Smoothie board's response
195
247
Empty string if no response from Smoothie
196
248
197
- >>> send_command(self.MOVE, x=100 y=100)
249
+ send_command(self.MOVE, x=100 y=100)
198
250
G0 X100 Y100
199
251
"""
200
252
@@ -243,14 +295,8 @@ def wait_for_response(self, timeout=20.0):
243
295
raise RuntimeWarning ('no response after {} seconds' .format (timeout ))
244
296
245
297
def flush_port (self ):
246
- # if we are running a virtual smoothie
247
- # we don't need a timeout for flush
248
- if isinstance (self .connection , VirtualSmoothie ):
249
- self .readline_from_serial ()
250
- else :
298
+ while self .readline_from_serial ():
251
299
time .sleep (self .serial_timeout )
252
- while self .readline_from_serial ():
253
- time .sleep (self .serial_timeout )
254
300
255
301
def readline_from_serial (self ):
256
302
msg = b''
@@ -477,6 +523,9 @@ def calibrate_steps_per_mm(self, axis, expected_travel, actual_travel):
477
523
def set_head_speed (self , rate = None ):
478
524
if rate :
479
525
self .head_speed = rate
526
+ self .saved_settings ['state' ]['head_speed' ] = str (self .head_speed )
527
+ with open (CONFIG_FILE_PATH , 'w' ) as configfile :
528
+ self .saved_settings .write (configfile )
480
529
kwargs = {"F" : self .head_speed }
481
530
res = self .send_command (self .SET_SPEED , ** kwargs )
482
531
return res == b'ok'
@@ -499,7 +548,7 @@ def versions_compatible(self):
499
548
}
500
549
if self .firmware_version not in self .COMPATIBLE_FIRMARE :
501
550
res ['firmware' ] = False
502
- if self .config_version not in self .COMPATIBLE_CONFIG :
551
+ if self .config_file_version not in self .COMPATIBLE_CONFIG :
503
552
res ['config' ] = False
504
553
if not self .ot_version :
505
554
res ['ot_version' ] = False
@@ -511,15 +560,14 @@ def versions_compatible(self):
511
560
'config={config}, '
512
561
'ot_version={ot_version}' .format (
513
562
firmware = self .firmware_version ,
514
- config = self .config_version ,
563
+ config = self .config_file_version ,
515
564
ot_version = self .ot_version
516
565
)
517
566
)
518
567
return res
519
568
520
569
def get_ot_version (self ):
521
- res = self .send_command (self .GET_OT_VERSION )
522
- res = res .decode ().split (' ' )[- 1 ]
570
+ res = self .get_config_value (self .OT_VERSION )
523
571
self .ot_version = None
524
572
if res not in self .ot_one_dimensions :
525
573
log .debug ('{} is not an ot_version' .format (res ))
@@ -538,24 +586,57 @@ def get_firmware_version(self):
538
586
return self .firmware_version
539
587
540
588
def get_config_version (self ):
541
- res = self .send_command (self .GET_CONFIG_VERSION )
542
- res = res .decode ().split (' ' )[- 1 ]
543
- self .config_version = res
544
- return self .config_version
589
+ res = self .get_config_value (self .CONFIG_VERSION )
590
+ self .config_file_version = res
591
+ return self .config_file_version
545
592
546
593
def get_steps_per_mm (self , axis ):
547
- if axis not in self . GET_STEPS_PER_MM :
594
+ if axis . lower () not in 'xyz' :
548
595
raise ValueError ('Axis {} not supported' .format (axis ))
549
- res = self .send_command (self .GET_STEPS_PER_MM [axis ])
550
- return float (res .decode ().split (' ' )[- 1 ])
596
+
597
+ res = self .send_command (self .STEPS_PER_MM )
598
+ self .wait_for_response () # extra b'ok' sent from smoothie after M92
599
+ try :
600
+ value = json .loads (res .decode ())[self .STEPS_PER_MM ][axis .upper ()]
601
+ return float (value )
602
+ except Exception :
603
+ raise RuntimeError (
604
+ '{0}: {1}' .format (self .SMOOTHIE_ERROR , res ))
551
605
552
606
def set_steps_per_mm (self , axis , value ):
553
- if axis not in self . SET_STEPS_PER_MM :
607
+ if axis . lower () not in 'xyz' :
554
608
raise ValueError ('Axis {} not supported' .format (axis ))
555
- command = self .SET_STEPS_PER_MM [axis ]
556
- command += str (value )
557
- res = self .send_command (command )
558
- return res .decode ().split (' ' )[- 1 ] == str (value )
609
+
610
+ res = self .send_command (self .STEPS_PER_MM , ** {axis .upper (): value })
611
+ self .wait_for_response () # extra b'ok' sent from smoothie after M92
612
+
613
+ key = self .CONFIG_STEPS_PER_MM [axis .lower ()]
614
+ try :
615
+ response_dict = json .loads (res .decode ())
616
+ returned_value = response_dict [self .STEPS_PER_MM ][axis .upper ()]
617
+ self .set_config_value (key , str (returned_value ))
618
+ return float (returned_value ) == value
619
+ except Exception :
620
+ raise RuntimeError (
621
+ '{0}: {1}' .format (self .SMOOTHIE_ERROR , res ))
622
+
623
+ def get_config_value (self , key ):
624
+ res = self .saved_settings ['config' ].get (key )
625
+ if not self .ignore_smoothie_sd :
626
+ command = '{0} {1}' .format (self .CONFIG_GET , key )
627
+ res = self .send_command (command ).decode ().split (' ' )[- 1 ]
628
+ return res
629
+
630
+ def set_config_value (self , key , value ):
631
+ success = True
632
+ if not self .ignore_smoothie_sd :
633
+ command = '{0} {1} {2}' .format (self .CONFIG_SET , key , value )
634
+ res = self .send_command (command )
635
+ success = res .decode ().split (' ' )[- 1 ] == str (value )
636
+ self .saved_settings ['config' ][key ] = value
637
+ with open (CONFIG_FILE_PATH , 'w' ) as configfile :
638
+ self .saved_settings .write (configfile )
639
+ return success
559
640
560
641
def get_endstop_switches (self ):
561
642
first_line = self .send_command (self .GET_ENDSTOPS )
0 commit comments