-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdevice.py
More file actions
285 lines (217 loc) · 10.8 KB
/
device.py
File metadata and controls
285 lines (217 loc) · 10.8 KB
1
2
3
4
5
6
7
8
9
10
11
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
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
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
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
277
278
279
280
281
282
283
284
285
import serialUtils
import grblUtils
import config
import threading
from notifiers.baseNotifier import Job
from werkzeug.serving import is_running_from_reloader
import logging
from enum import Enum
import threading
import time
class DeviceStatus(Enum):
IDLE = 1
BUSY = 2
ERROR = 3
NOT_FOUND = 4
HOLD = 5
class WellKnownCommands(Enum):
CMD_STATUS = "?"
CMD_GOTO_ORIGIN = "G0 X0Y0"
CMD_RESUME = "~" #to resume after a HOLD state
# Methods are sync unless stated otherwise
class Device:
__CHECKER_THREAD_LOCK = threading.Lock()
#constructor
def __init__(self, port : str):
self.port = port
self.status = DeviceStatus.NOT_FOUND
self.emergency_stop_requested = False
self.checker_thread = None
self.checker_thread_killed = False
#destructor
def __del__(self) -> None:
if self.checker_thread is not None:
logging.warning(f"Checker thread to be killed for thread {self.checker_thread}")
self.checker_thread_killed = True
self.checker_thread.join()
logging.warning(f"Checker thread killed")
def __notifyAll (self, job : Job):
if job is not None:
#notify
for x in config.myconfig["notifiers"]:
try:
x.NotifyJobCompletion(job)
except Exception as ex:
#fail silently
logging.warning(f"Failed to notify job {job} completion {x} with message {ex}")
def __completeJob (self, job : Job):
self.status = DeviceStatus.IDLE
if job is not None:
job.finish()
self.__notifyAll(job)
#-------------------------------------------------------------
def simulate (self, fileOnDisk, asynchronous = False, job : Job = None, jobParams: dict = None):
self.status = DeviceStatus.BUSY
if asynchronous:
threading.Thread(target=self.__simulateAsync, args=(fileOnDisk, job, jobParams, )).start()
else:
self.__simulateAsync(fileOnDisk, job, jobParams)
def __simulateAsync (self, fileOnDisk, job : Job = None, jobParams: dict = None):
serialUtils.simulateFile(self.port, fileOnDisk, jobParams=jobParams)
self.__completeJob(job)
#-------------------------------------------------------------
def burn (self, fileOnDisk, asynchronous = False, job : Job = None, jobParams: dict = None):
self.status = DeviceStatus.BUSY
self.emergency_stop_requested = False
if asynchronous:
threading.Thread(target=self.__burnAsync, args=(fileOnDisk, job, jobParams, )).start()
else:
self.__burnAsync(fileOnDisk, job, jobParams)
def __burnAsync (self, fileOnDisk, job : Job = None, jobParams: dict = None):
#the modifiers to use when burning a file
linemodif = [serialUtils.linemodifier_skipComments]
if not config.myconfig.get("G4 delays handled by device", True):
#if your device doesn't handle G4 delays, you can use this to handle them on the software "sending" side
linemodif.append(serialUtils.linemodifier_delayCommands)
if "laserPowerAdjust" in jobParams:
#if the job parameters contain a laser power adjust, we need to modify the line
linemodif.append(serialUtils.linemodifier_laserAdjustPower)
if "laserSpeedAdjust" in jobParams:
#if the job parameters contain a laser Speed adjust, we need to modify the line
linemodif.append(serialUtils.linemodifier_laserAdjustSpeed)
logging.debug(f"Device.burn() : fileOnDisk {fileOnDisk} Job {job} jobParams {jobParams}")
logging.debug(f"Device.burn() : linemodif x{len(linemodif)} : {linemodif}")
serialUtils.processFile(self.port, fileOnDisk, lineModifiers=linemodif, forceStopCheck=self.__checkForJobCancellation, jobParams=jobParams)
if self.emergency_stop_requested:
#send outro
grblUtils.sendOutroOnly(self.port)
self.emergency_stop_requested = False
self.__completeJob(job)
#called by the serial utils to check if a job cancellation was requested
def __checkForJobCancellation (self):
#change status in the __burnAsync method
return self.emergency_stop_requested
#-------------------------------------------------------------
def frame(self, fileOnDisk, asynchronous = False, job : Job = None, jobParams: dict = None):
self.status = DeviceStatus.BUSY
if asynchronous:
threading.Thread(target=self.__frameAsync, args=(fileOnDisk,job, jobParams, )).start()
else:
self.__frameAsync(fileOnDisk, job, jobParams)
def __frameAsync(self, fileOnDisk, job : Job = None, jobParams: dict = None):
grblUtils.generateFrame(self.port, fileOnDisk, framingSpeendInMMPerSec=25, jobParams=jobParams)
self.__completeJob(job)
#-------------------------------------------------------------
def frameWithCornerPause(self, fileOnDisk, asynchronous = False, job : Job = None, jobParams: dict = None):
self.status = DeviceStatus.BUSY
if asynchronous:
threading.Thread(target=self.__frameWithCornerPauseAsync, args=(fileOnDisk,job, jobParams, )).start()
else:
self.__frameWithCornerPauseAsync(fileOnDisk, job, jobParams)
def __frameWithCornerPauseAsync(self, fileOnDisk, job : Job = None, jobParams: dict = None):
grblUtils.generateFrame(self.port, fileOnDisk, pauseAtCornersInSec=4, framingSpeendInMMPerSec=50, jobParams=jobParams)
self.__completeJob(job)
#-------------------------------------------------------------
# send command to device and returns result
def sendCommand(self, command) -> str:
self.status = DeviceStatus.BUSY
res = serialUtils.sendCommand(self.port, command)
if res is not None:
self.status = DeviceStatus.IDLE
else:
self.status = DeviceStatus.ERROR
return res
#-------------------------------------------------------------
#check for device status HOLD or IDLE (returns but doesn't change the status)
def check_device_status(self) -> DeviceStatus:
#ignore BUSY or ERROR states
if self.status not in [DeviceStatus.IDLE, DeviceStatus.HOLD]:
return self.status
res = self.sendCommand(WellKnownCommands.CMD_STATUS.value)
if res is None:
status = DeviceStatus.ERROR
else:
#not perfect but should be flexible enough
if "hold" in res.lower():
status = DeviceStatus.HOLD
elif "idle" in res.lower():
status = DeviceStatus.IDLE
else:
#what the hell is this status?
logging.warning(f"Device.check_device_status() : unknown status '{res}', returning ERROR")
status = DeviceStatus.ERROR
logging.warning(f"Device.check_device_status() : status {status}")
return status
#-------------------------------------------------------------
#attempt ONCE to reset the status to IDLE if HOLD state (changes the status)
def try_to_resume(self) -> DeviceStatus:
original_status = self.check_device_status()
logging.warning(f"Device.try_to_resume() : original status {original_status}")
if original_status == DeviceStatus.HOLD:
res = self.sendCommand(WellKnownCommands.CMD_RESUME.value)
if res is not None:
new_status = self.check_device_status()
else:
new_status = DeviceStatus.ERROR
logging.warning(f"Device.try_to_resume() : new status {new_status}")
self.status = new_status
#in any case, return the current status
return self.status
#-------------------------------------------------------------
def disconnect(self):
self.status = DeviceStatus.BUSY
serialUtils.disconnect()
self.status = DeviceStatus.NOT_FOUND
#-------------------------------------------------------------
def connect(self, port : str = None) -> DeviceStatus:
if port is None:
port = self.port
if self.status not in [DeviceStatus.NOT_FOUND, DeviceStatus.ERROR]:
logging.warning(f"Device.connect() : wrong status {self.status}")
#no change
return self.status
#not need to try on ports that aren't available
if port not in serialUtils.listAvailableSerialPorts(portname_only=True):
logging.warning(f"Device.connect() : port not available {port}")
self.status = DeviceStatus.NOT_FOUND
return self.status
try:
logging.warning(f"Device.connect() : Connecting to device on port {port}")
self.port = port
serialUtils.connect(self.port)
self.status = DeviceStatus.IDLE
except:
self.status = DeviceStatus.ERROR
return self.status
#-------------------------------------------------------------
def reconnect(self, port) -> DeviceStatus:
self.disconnect()
return self.connect(port)
#-------------------------------------------------------------
def check_for_device(self):
#will terminate once the device is found
while not self.checker_thread_killed and self.status in [DeviceStatus.NOT_FOUND]:
logging.warning(f"Checking for device on port {self.port}")
self.connect()
logging.warning(f"Trying to connect to device on port {self.port} with status {self.status}")
if self.status == DeviceStatus.IDLE:
#if the device is connected BUT in HOLD state, try to resume
status = self.check_device_status()
if status in [DeviceStatus.HOLD]:
self.try_to_resume()
#wait a few seconds before trying again
time.sleep(3)
def start_thread_check_for_device(self):
#https://stackoverflow.com/questions/25504149/why-does-running-the-flask-dev-server-run-itself-twice
#in dev mode, the server is started twice, so we need to check if we are in the reloader (and ignore it): we want to run in child subprocess only
if not is_running_from_reloader():
return
with Device.__CHECKER_THREAD_LOCK:
if self.checker_thread != None:
logging.warning(f"ABORT Checker thread already started for port {self.port}")
return
logging.warning(f"Starting thread to check for device on port {self.port}")
self.checker_thread = threading.Thread(target=self.check_for_device)
#stop when main thread stops
self.checker_thread.daemon = True
self.checker_thread.start()