-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathCurio.py
1579 lines (1371 loc) · 59 KB
/
Curio.py
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
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
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
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
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
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
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# (c) 2013,2014 [email protected]
# (c) 2016 [email protected]
# (c) 2016 Alexander Wenger
# (c) 2017 Johann Gail
#
# Distribute under GPLv2 or ask.
#
# Driver for a Graphtec Silhouette Cameo plotter.
# modeled after https://github.com/nosliwneb/robocut.git
# https://github.com/pmonta/gerber2graphtec/blob/master/file2graphtec
#
# Native resolution of the plotter is 0.05mm -- All movements are integer multiples of this.
#
# 2015-06-04, [email protected] using print_function. added wait_for_ready().
# plot(bboxonly=None) is now the special case for not doing anything. False is normal plot.
# 2015-06-05 Renamed cut_bbox() to find_bbox(). It does not cut anything.
# 2015-06-06 refactored plot_cmds() from plot().
# 2016-05-16 no reset per default, this helps usbip.
# 2016-05-21 detect python-usb < 1.0 and give instructions.
# 2017-04-20 Adding Cameo3 USB IDs
# 2020-06- Adding Cameo4 and refactor code
from __future__ import print_function
import os
import re
import sys
import time
import serial, serial.tools.list_ports
usb_reset_needed = False # https://github.com/fablabnbg/inkscape-silhouette/issues/10
sys.path.append(os.path.dirname(os.path.abspath(__file__)) + '/pyusb-1.0.2') # have a pyusb fallback
sys_platform = sys.platform.lower()
if sys_platform.startswith('win'):
import usb.core
import usb.backend.libusb1
backend = usb.backend.libusb1.get_backend(find_library=lambda x: ".\libusb-1.0.dll")
dev = usb.core.find(backend=backend)
elif sys_platform.startswith('darwin'):
import usb1, usb.core
usb1ctx = usb1.USBContext()
else: # if sys_platform.startswith('linux'):
try:
import usb.core # where???
except Exception as e:
try:
import libusb1 as usb
except Exception as e1:
try:
import usb
except Exception as e2:
print("The python usb module could not be found. Try", file=sys.stderr)
print("\t sudo zypper in python-usb \t\t# if you run SUSE", file=sys.stderr)
print("\t sudo apt-get install python-usb \t\t# if you run Ubuntu", file=sys.stderr)
print("\n\n\n", file=sys.stderr)
raise e2
try:
try:
usb_vi = usb.version_info[0]
usb_vi_str = str(usb.version_info)
except AttributeError:
usb_vi = 0
if sys_platform.startswith('win'):
usb_vi = 1
pass # windows does not seem to detect the usb.version , gives attribute error. Other tests of pyusb work, pyusb is installed.
usb_vi_str = 'unknown'
if usb_vi < 1:
print("Your python usb module appears to be "+usb_vi_str+" -- We need version 1.x", file=sys.stderr)
print("For Debian 8 try:\n echo > /etc/apt/sources.list.d/backports.list 'deb http://ftp.debian.org debian jessie-backports main\n apt-get update\n apt-get -t jessie-backports install python-usb", file=sys.stderr)
print("\n\n\n", file=sys.stderr)
print("For Ubuntu 14.04try:\n pip install pyusb --upgrade", file=sys.stderr)
print("\n\n\n", file=sys.stderr)
sys.exit(0)
except NameError:
pass # on OS X usb.version_info[0] will always fail as libusb1 is being used
# taken from
# robocut/CutDialog.ui
# robocut/CutDialog.cpp
MEDIA = [
# CAUTION: keep in sync with sendto_silhouette.inx
# media, pressure, speed, depth, cap-color, name
( 100, 27, 10, 1, "yellow", "Card without Craft Paper Backing"),
( 101, 27, 10, 1, "yellow", "Card with Craft Paper Backing"),
( 102, 10, 5, 1, "blue", "Vinyl Sticker"),
( 106, 14, 10, 1, "blue", "Film Labels"),
( 111, 27, 10, 1, "yellow", "Thick Media"),
( 112, 2, 10, 1, "blue", "Thin Media"),
( 113, 18, 10,None, "pen", "Pen"),
( 120, 30, 10, 1, "blue", "Bond Paper 13-28 lbs (105g)"),
( 121, 30, 10, 1, "yellow", "Bristol Paper 57-67 lbs (145g)"),
( 122, 30, 10, 1, "yellow", "Cardstock 40-60 lbs (90g)"),
( 123, 30, 10, 1, "yellow", "Cover 40-60 lbs (170g)"),
( 124, 1, 10, 1, "blue", "Film, Double Matte Translucent"),
( 125, 1, 10, 1, "blue", "Film, Vinyl With Adhesive Back"),
( 126, 1, 10, 1, "blue", "Film, Window With Kling Adhesive"),
( 127, 30, 10, 1, "red", "Index 90 lbs (165g)"),
( 128, 20, 10, 1, "yellow", "Inkjet Photo Paper 28-44 lbs (70g)"),
( 129, 27, 10, 1, "red", "Inkjet Photo Paper 45-75 lbs (110g)"),
( 130, 30, 3, 1, "red", "Magnetic Sheet"),
( 131, 30, 10, 1, "blue", "Offset 24-60 lbs (90g)"),
( 132, 5, 10, 1, "blue", "Print Paper Light Weight"),
( 133, 25, 10, 1, "yellow", "Print Paper Medium Weight"),
( 134, 20, 10, 1, "blue", "Sticker Sheet"),
( 135, 20, 10, 1, "red", "Tag 100 lbs (275g)"),
( 136, 30, 10, 1, "blue", "Text Paper 24-70 lbs (105g)"),
( 137, 30, 10, 1, "yellow", "Vellum Bristol 57-67 lbs (145g)"),
( 138, 30, 10, 1, "blue", "Writing Paper 24-70 lbs (105g)"),
( 139, 10, 10, 1, "red", "Pick and Place"),
( 300, None, None,None, "custom", "Custom"),
]
# robocut/Plotter.h:53 ff
VENDOR_ID_GRAPHTEC = 0x0b4d
PRODUCT_ID_CC200_20 = 0x110a
PRODUCT_ID_CC300_20 = 0x111a
PRODUCT_ID_SILHOUETTE_SD_1 = 0x111c
PRODUCT_ID_SILHOUETTE_SD_2 = 0x111d
PRODUCT_ID_SILHOUETTE_CAMEO = 0x1121
PRODUCT_ID_SILHOUETTE_CAMEO2 = 0x112b
PRODUCT_ID_SILHOUETTE_CURIO = 0x112c
PRODUCT_ID_SILHOUETTE_CAMEO3 = 0x112f
PRODUCT_ID_SILHOUETTE_CAMEO4 = 0x1137
PRODUCT_ID_SILHOUETTE_PORTRAIT = 0x1123
PRODUCT_ID_SILHOUETTE_PORTRAIT2 = 0x1132
# End Of Text - marks the end of a command
CMD_ETX = '\x03'
# Escape - send escape command
CMD_ESC = '\x1b'
### Escape Commands
# End Of Transmission - will initialize the device,
CMD_EOT = '\x04'
# Enquiry - Returns device status
CMD_ENQ = '\x05'
# Negative Acnoledge - Returns device tool setup
CMD_NAK = '\x15'
SILHOUETTE_CAMEO4_TOOL_EMPTY = 0
SILHOUETTE_CAMEO4_TOOL_RATCHETBLADE = 1
SILHOUETTE_CAMEO4_TOOL_AUTOBLADE = 2
SILHOUETTE_CAMEO4_TOOL_DEEPCUTBLADE = 3
SILHOUETTE_CAMEO4_TOOL_KRAFTBLADE = 4
SILHOUETTE_CAMEO4_TOOL_ROTARYBLADE = 5
SILHOUETTE_CAMEO4_TOOL_PEN = 7
SILHOUETTE_CAMEO4_TOOL_ERROR = 255
DEVICE = [
{ 'vendor_id': VENDOR_ID_GRAPHTEC, 'product_id': PRODUCT_ID_SILHOUETTE_PORTRAIT, 'name': 'Silhouette Portrait',
'width_mm': 206, 'length_mm': 3000, 'regmark': True },
{ 'vendor_id': VENDOR_ID_GRAPHTEC, 'product_id': PRODUCT_ID_SILHOUETTE_PORTRAIT2, 'name': 'Silhouette Portrait2',
'width_mm': 203, 'length_mm': 3000, 'regmark': True },
{ 'vendor_id': VENDOR_ID_GRAPHTEC, 'product_id': PRODUCT_ID_SILHOUETTE_CAMEO, 'name': 'Silhouette Cameo',
# margin_top_mm is just for safety when moving backwards with thin media
# margin_left_mm is a physical limit, but is relative to width_mm!
'width_mm': 304, 'length_mm': 3000, 'margin_left_mm':9.0, 'margin_top_mm':1.0, 'regmark': True },
{ 'vendor_id': VENDOR_ID_GRAPHTEC, 'product_id': PRODUCT_ID_SILHOUETTE_CAMEO2, 'name': 'Silhouette Cameo2',
# margin_top_mm is just for safety when moving backwards with thin media
# margin_left_mm is a physical limit, but is relative to width_mm!
'width_mm': 304, 'length_mm': 3000, 'margin_left_mm':9.0, 'margin_top_mm':1.0, 'regmark': True },
{ 'vendor_id': VENDOR_ID_GRAPHTEC, 'product_id': PRODUCT_ID_SILHOUETTE_CURIO, 'name': 'Silhouette Curio',
'width_mm': 216, 'length_mm': 152, 'regmark': True },
{ 'vendor_id': VENDOR_ID_GRAPHTEC, 'product_id': PRODUCT_ID_SILHOUETTE_CAMEO3, 'name': 'Silhouette Cameo3',
# margin_top_mm is just for safety when moving backwards with thin media
# margin_left_mm is a physical limit, but is relative to width_mm!
'width_mm': 304.8, 'length_mm': 3000, 'margin_left_mm':0.0, 'margin_top_mm':0.0, 'regmark': True },
{ 'vendor_id': VENDOR_ID_GRAPHTEC, 'product_id': PRODUCT_ID_SILHOUETTE_CAMEO4, 'name': 'Silhouette Cameo4',
# margin_top_mm is just for safety when moving backwards with thin media
# margin_left_mm is a physical limit, but is relative to width_mm!
'width_mm': 304.8, 'length_mm': 3000, 'margin_left_mm':0.0, 'margin_top_mm':0.0, 'regmark': True },
{ 'vendor_id': VENDOR_ID_GRAPHTEC, 'product_id': PRODUCT_ID_CC200_20, 'name': 'Craft Robo CC200-20',
'width_mm': 200, 'length_mm': 1000, 'regmark': True },
{ 'vendor_id': VENDOR_ID_GRAPHTEC, 'product_id': PRODUCT_ID_CC300_20, 'name': 'Craft Robo CC300-20' },
{ 'vendor_id': VENDOR_ID_GRAPHTEC, 'product_id': PRODUCT_ID_SILHOUETTE_SD_1, 'name': 'Silhouette SD 1' },
{ 'vendor_id': VENDOR_ID_GRAPHTEC, 'product_id': PRODUCT_ID_SILHOUETTE_SD_2, 'name': 'Silhouette SD 2' },
]
def _bbox_extend(bb, x, y):
# The coordinate system origin is in the top lefthand corner.
# Downwards and rightwards we count positive. Just like SVG or HPGL.
# Thus lly is a higher number than ury
if not 'llx' in bb or x < bb['llx']: bb['llx'] = x
if not 'urx' in bb or x > bb['urx']: bb['urx'] = x
if not 'lly' in bb or y > bb['lly']: bb['lly'] = y
if not 'ury' in bb or y < bb['ury']: bb['ury'] = y
# 1 mm = 20 SU
# 1 in = 508 SU
# 8.5 in = 4318 SU
# 11 in = 5588 SU
def _mm_2_SU(mm):
"""Convert mm to SU (SilhuetteUnit) using round
Parameters
----------
mm : int, float
input millimetre
Returns
-------
int
output SU
"""
return int(round(mm * 20.0))
def _inch_2_SU(inch):
"""Convert inch to SU (SilhuetteUnit) using round
Parameters
----------
inch : int, float
input inch
Returns
-------
int
output SU
"""
return int(round(inch * 508.0))
class SilhouetteCameoTool:
def __init__(self, toolholder=1):
if toolholder is None:
toolholder = 1
self.toolholder = toolholder
def select(self):
""" select tool command """
return "J%d" % self.toolholder
def pressure(self, pressure):
""" set pressure command """
return "FX%d,%d" % (pressure, self.toolholder)
def speed(self, speed):
""" set speed command """
return "!%d,%d" % (speed, self.toolholder)
def depth(self, depth):
""" set depth command """
return "TF%d,%d" % (depth, self.toolholder)
def cutter_offset(self, xmm, ymm):
""" set cutter offset command using mm """
return "FC%d,%d,%d" % (_mm_2_SU(xmm), _mm_2_SU(ymm), self.toolholder)
def lift(self, lift):
""" set lift command """
if lift:
return "FE1,%d" % self.toolholder
else:
return "FE0,%d" % self.toolholder
def sharpen_corners(self, start, end):
return [
"FF%d,0,%d" % (start, self.toolholder),
"FF%d,%d,%d" % (start, end, self.toolholder)]
class SilhouetteCameo:
def __init__(self, log=sys.stderr, no_device=False, progress_cb=None):
""" This initializer simply finds the first known device.
The default paper alignment is left hand side for devices with known width
(currently Cameo and Portrait). Otherwise it is right hand side.
Use setup() to specify your needs.
If no_device is True, the usb device is not actually opened, and all
generated data is discarded.
The progress_cb is called with the following parameters:
int(strokes_done), int(strikes_total), str(status_flags)
The status_flags contain 't' when there was a (non-fatal) write timeout
on the device.
"""
self.leftaligned = False # True: only works for DEVICE with known hardware.width_mm
self.log = log
self.progress_cb = progress_cb
dev = None
self.margins_printed = None
self.listofcmds=[]
if no_device is True:
self.hardware = { 'name': 'Crashtest Dummy Device' }
else:
for hardware in DEVICE:
if sys_platform.startswith('win'):
#print("device lookup under windows not tested. Help adding code!", file=self.log)
#print("device lookup under windows not tested. Help adding code!")
dev = usb.core.find(idVendor=hardware['vendor_id'], idProduct=hardware['product_id'])
elif sys_platform.startswith('darwin'):
dev = usb1ctx.openByVendorIDAndProductID(hardware['vendor_id'], hardware['product_id'])
else: # linux
dev = usb.core.find(idVendor=hardware['vendor_id'], idProduct=hardware['product_id'])
if dev:
self.hardware = hardware
break
if dev is None:
if sys_platform.startswith('win'):
#print("device fallback under windows not tested. Help adding code!", file=self.log)
dev = usb.core.find(idVendor=VENDOR_ID_GRAPHTEC)
self.hardware = { 'name': 'Unknown Graphtec device' }
if dev:
self.hardware['name'] += " 0x%04x" % dev.idProduct
self.hardware['product_id'] = dev.idProduct
self.hardware['vendor_id'] = dev.idVendor
elif sys_platform.startswith('darwin'):
print("device fallback under macosx not implemented. Help adding code!", file=self.log)
else: # linux
dev = usb.core.find(idVendor=VENDOR_ID_GRAPHTEC)
self.hardware = { 'name': 'Unknown Graphtec device ' }
if dev:
self.hardware['name'] += " 0x%04x" % dev.idProduct
self.hardware['product_id'] = dev.idProduct
self.hardware['vendor_id'] = dev.idVendor
if dev is None:
msg = ''
try:
for dev in usb.core.find(find_all=True):
msg += "(%04x,%04x) " % (dev.idVendor, dev.idProduct)
except NameError:
msg += "unable to list devices on OS X"
raise ValueError('No Graphtec Silhouette devices found.\nCheck USB and Power.\nDevices: '+msg)
try:
dev_bus = dev.bus
except:
dev_bus = -1
try:
dev_addr = dev.address
except:
dev_addr = -1
print("%s found on usb bus=%d addr=%d" % (self.hardware['name'], dev_bus, dev_addr), file=self.log)
if sys_platform.startswith('win'):
#print("device init under windows not implemented. Help adding code!", file=self.log)
pass
elif sys_platform.startswith('darwin'):
dev.claimInterface(0)
# usb_enpoint = 1
# dev.bulkWrite(usb_endpoint, data)
else: # linux
try:
if dev.is_kernel_driver_active(0):
print("is_kernel_driver_active(0) returned nonzero", file=self.log)
if dev.detach_kernel_driver(0):
print("detach_kernel_driver(0) returned nonzero", file=self.log)
except usb.core.USBError as e:
print("usb.core.USBError:", e, file=self.log)
if e.errno == 13:
msg = """
If you are not running as root, this might be a udev issue.
Try a file /etc/udev/rules.d/99-graphtec-silhouette.rules
with the following example syntax:
SUBSYSTEM=="usb", ATTR{idVendor}=="%04x", ATTR{idProduct}=="%04x", MODE="666"
Then run 'sudo udevadm trigger' to load this file.
Alternatively, you can add yourself to group 'lp' and logout/login.""" % (self.hardware['vendor_id'], self.hardware['product_id'])
print(msg, file=self.log)
print(msg, file=sys.stderr)
sys.exit(0)
if usb_reset_needed:
for i in range(5):
try:
dev.reset()
break
except usb.core.USBError as e:
print("reset failed: ", e, file=self.log)
print("retrying reset in 5 sec", file=self.log)
time.sleep(5)
try:
dev.set_configuration()
dev.set_interface_altsetting() # Probably not really necessary.
except usb.core.USBError:
pass
self.dev = dev
self.need_interface = False # probably never needed, but harmful on some versions of usb.core
self.regmark = False # not yet implemented. See robocut/Plotter.cpp:446
if self.dev is None or 'width_mm' in self.hardware:
self.leftaligned = True
self.enable_sw_clipping = True
def product_id(self):
return self.hardware['product_id'] if 'product_id' in self.hardware else None
def write(self, data, timeout=10000):
"""Send a command to the device. Long commands are sent in chunks of 4096 bytes.
A nonblocking read() is attempted before write(), to find spurious diagnostics."""
if self.dev is None: return None
# convert string to bytes if required
if isinstance(data, str):
data = data.encode()
# robocut/Plotter.cpp:73 says: Send in 4096 byte chunks. Not sure where I got this from, I'm not sure it is actually necessary.
try:
resp = self.read(timeout=10) # poll the inbound buffer
if resp:
print("response before write('%s'): '%s'" % (data, resp), file=self.log)
except:
pass
endpoint = 0x01
chunksz = 4096
r = 0
o = 0
msg=''
retry = 0
while o < len(data):
if o:
if self.progress_cb:
self.progress_cb(o,len(data),msg)
elif self.log:
self.log.write(" %d%% %s\r" % (100.*o/len(data),msg))
self.log.flush()
chunk = data[o:o+chunksz]
try:
if self.need_interface:
try:
r = self.dev.write(endpoint, chunk, interface=0, timeout=timeout)
except AttributeError:
r = self.dev.bulkWrite(endpoint, chunk, interface=0, timeout=timeout)
else:
try:
r = self.dev.write(endpoint, chunk, timeout=timeout)
except AttributeError:
r = self.dev.bulkWrite(endpoint, chunk, timeout=timeout)
except TypeError as te:
# write() got an unexpected keyword argument 'interface'
raise TypeError("Write Exception: %s, %s dev=%s" % (type(te), te, type(self.dev)))
except AttributeError as ae:
# write() got an unexpected keyword argument 'interface'
raise TypeError("Write Exception: %s, %s dev=%s" % (type(ae), ae, type(self.dev)))
except Exception as e:
# raise USBError(_str_error[ret], ret, _libusb_errno[ret])
# usb.core.USBError: [Errno 110] Operation timed
#print("Write Exception: %s, %s errno=%s" % (type(e), e, e.errno), file=s.log)
import errno
try:
if e.errno == errno.ETIMEDOUT:
time.sleep(1)
msg += 't'
continue
except Exception as ee:
msg += "s.dev.write Error: {}".format(ee)
else:
if len(msg):
msg = ''
self.log.write("\n")
# print("write([%d:%d], len=%d) = %d" % (o,o+chunksz, len(chunk), r), file=s.log)
if r == 0 and retry < 5:
time.sleep(1)
retry += 1
msg += 'r'
elif r <= 0:
raise ValueError('write %d bytes failed: r=%d' % (len(chunk), r))
else:
retry = 0
o += r
if o != len(data):
raise ValueError('write all %d bytes failed: o=%d' % (len(data), o))
def safe_write(self, data):
"""wrapper for write with special emphasis on not to over-load the cutter with long commands."""
if self.dev is None: return None
# convert string to bytes if required
if isinstance(data, str):
data = data.encode()
# Silhouette Studio uses packet size of maximal 3k, 1k is default
safemaxchunksz = 1024
so = 0
delimiter = CMD_ETX.encode()
while so < len(data):
safechunksz = min(safemaxchunksz, len(data)-so)
candidate = data[so:so+safechunksz]
# strip string candidate of unfinished command at its end
safechunk = candidate[0:(candidate.rfind(delimiter) + 1)]
self.write(data = safechunk)
# wait for cutter to finish current chunk, otherwise blocking might occur
while not self.status() == "ready":
time.sleep(0.05)
so += len(safechunk)
def send_command(self, cmd, timeout=10000):
""" Sends a command or a list of commands of type string """
if isinstance(cmd, str):
data = cmd
elif isinstance(cmd, list) and isinstance(cmd[0], str):
data = CMD_ETX.join(cmd)
else:
raise TypeError("Send Command Exception: %s " % type(cmd))
self.write(data + CMD_ETX, timeout)
def safe_send_command(self, cmd):
""" Sends a command or a list of commands of type string """
if isinstance(cmd, str):
data = cmd
elif isinstance(cmd, list):
if not cmd:
# if list of commands is empty this function shall do nothing
return
else:
# list must not contain anything but strings
for c in cmd:
if not isinstance(c, str):
raise TypeError("Send Command Exception: %s " % type(cmd))
data = CMD_ETX.join(cmd)
else:
raise TypeError("Send Command Exception: %s " % type(cmd))
self.safe_write(data + CMD_ETX)
def send_escape(self, esc):
""" Sends a Escape Command """
if isinstance(esc, str):
self.write(CMD_ESC + esc)
else:
raise TypeError("Send Escape Exception: %s " % type(esc))
def read(self, size=64, timeout=5000):
"""Low level read method"""
if self.dev is None: return None
endpoint = 0x82
if self.need_interface:
try:
data = self.dev.read(endpoint, size, timeout=timeout, interface=0)
except AttributeError:
data = self.dev.bulkRead(endpoint, size, timeout=timeout, interface=0)
else:
try:
data = self.dev.read(endpoint, size, timeout=timeout)
except AttributeError:
data = self.dev.bulkRead(endpoint, size, timeout=timeout)
if data is None:
raise ValueError('read failed: none')
if isinstance(data, (str, bytes, bytearray)):
return data.decode()
else:
try:
return data.tobytes().decode() # with py3
except:
return data.tostring().decode() # with py2/3 - dropped in py39
def try_read(self, size=64, timeout=1000):
ret=None
try:
ret = self.read(size=size,timeout=timeout)
print("try_read got: '%s'" % ret)
except:
pass
return ret
def send_receive_command(self, cmd, tx_timeout=10000, rx_timeout=1000):
self.send_command(cmd, tx_timeout)
try:
resp = self.read(timeout=rx_timeout)
if len(resp) > 1:
return resp[:-1]
except:
pass
return None
def status(self):
"""Query the device status. This can return one of the three strings
'ready', 'moving', 'unloaded' or a raw (unknown) byte sequence."""
if self.dev is None: return 'none'
# Status request.
self.send_escape(CMD_ENQ)
resp = b"None\x03"
try:
resp = self.read(timeout=5000)
except usb.core.USBError as e:
print("usb.core.USBError:", e, file=self.log)
pass
if resp[-1] != CMD_ETX: raise ValueError('status response not terminated with 0x03: %s' % (resp[-1]))
if resp[:-1] == '0': return "ready"
if resp[:-1] == '1': return "moving"
if resp[:-1] == '2': return "unloaded"
return resp[:-1]
def get_tool_setup(self):
""" gets the type of the tools installed in Cameo 4 """
if self.dev is None:
return 'none'
if self.product_id() != PRODUCT_ID_SILHOUETTE_CAMEO4:
return 'none'
# tool setup request.
self.send_escape(CMD_NAK)
try:
resp = self.read(timeout=1000)
if len(resp) > 1:
return resp[:-1]
except:
pass
return 'none'
def wait_for_ready(self, timeout=30, verbose=True):
# get_version() is likely to timeout here...
# if verbose: print("device version: '%s'" % s.get_version(), file=sys.stderr)
state = self.status()
for i in range(1, int(timeout*.5)):
if (state == 'ready'):
break
if verbose == True:
print(" %d/%d: status=%s\r" % (i, int(timeout*.5), state), end='', file=sys.stderr)
if verbose == False:
if state == 'unloaded':
print(" %d/%d: please load media ...\r" % (i, int(timeout*.5)), end='', file=sys.stderr)
elif i > 50:
print(" %d/%d: status=%s\r" % (i, int(timeout*.5), state), end='', file=sys.stderr)
time.sleep(0.5)
state = self.status()
if verbose: print("",file=sys.stderr)
return state
def initialize(self):
"""Send the init command. Called by setup()."""
# taken from robocut/Plotter.cpp:331 ff
# Initialize plotter.
try:
self.send_escape(CMD_EOT)
except Exception as e:
raise ValueError("Write Exception: %s, %s errno=%s\n\nFailed to write the first 3 bytes. Permissions? inf-wizard?" % (type(e), e, e.errno))
# Initial palaver
print("Device Version: '%s'" % self.get_version(), file=self.log)
# Additional commands seen in init by Silhouette Studio
"""
# Get Upper Left Coords: 2 six digit numbers.
resp = self.send_receive_command("[")
if resp:
# response '0,0'
print("[: '%s'" % resp, file=self.log)
# Get Lower Right Coordinates: 2 six digit numbers
resp = self.send_receive_command("U")
if resp:
# response '20320,4120' max. usable print range?
# response ' 20320, 3840' on Portrait
print("U: '%s'" % resp, file=self.log)
# Unknown: 1 five digit number. Maybe last speed set?
resp = self.send_receive_command("FQ0")
if resp:
# response '10'
# response ' 5' on portrait
print("FQ0: '%s'" % resp, file=self.log)
# Unknown: 1 five digit number. Maybe last blade offset or last pressure?
resp = self.send_receive_command("FQ2")
if resp:
# response '18'
# response ' 17' on portrait
print("FQ2: '%s'" % resp, file=self.log)
"""
if self.product_id() in [PRODUCT_ID_SILHOUETTE_CAMEO3, PRODUCT_ID_SILHOUETTE_CAMEO4]:
# Unknown: 2 five digit numbers. Probably machine stored calibration offset of the regmark sensor optics
resp = self.send_receive_command("TB71")
if resp:
# response ' 0, 0' on portrait
print("TB71: '%s'" % resp, file=self.log)
# Unknown: 2 five digit numbers. Probably machine stored calibration factors of carriage and roller (carriage, roller / unit 1/100% i.e. 0.0001)
resp = self.send_receive_command("FA")
if resp:
# response ' 0, 0' on portrait
print("FA: '%s'" % resp, file=self.log)
# Silhouette Studio does not appear to issue this command when using a cameo 4
if self.product_id() == PRODUCT_ID_SILHOUETTE_CAMEO3:
resp = self.send_receive_command("TC")
if resp:
# response '0,0'
print("TC: '%s'" % resp, file=self.log)
def get_version(self):
"""Retrieve the firmware version string from the device."""
if self.dev is None: return None
return self.send_receive_command("FG", rx_timeout = 10000)
def set_boundary(self, top, left, bottom, right):
""" Sets boundary box """
self.send_command(["\\%d,%d" % (top, left), "Z%d,%d" % (bottom, right)])
def set_cutting_mat(self, cuttingmat, mediawidth, mediaheight):
"""Setting Cutting mat only for Cameo 3 and 4
Parameters
----------
cuttingmat : {'cameo_12x12', 'cameo_12x24'. None}
type of the cutting mat
mediawidth : float
width of the media
mediaheight : float
height of the media
"""
if self.product_id() not in [PRODUCT_ID_SILHOUETTE_CAMEO3, PRODUCT_ID_SILHOUETTE_CAMEO4]:
return
if cuttingmat == 'cameo_12x12':
self.send_command("TG1")
elif cuttingmat == 'cameo_12x24':
self.send_command("TG2")
else:
self.send_command("TG0")
#FNx, x = 0 seem to be some kind of reset, x = 1: plotter head moves to other
# side of media (boundary check?), but next cut run will stall
#TB50,x: x = 1 landscape mode, x = 0 portrait mode
self.send_command(["FN0", "TB50,0"])
if cuttingmat == 'cameo_12x12':
self.set_boundary(0, 0, _inch_2_SU(12), _inch_2_SU(12))
elif cuttingmat == 'cameo_12x24':
self.set_boundary(0, 0, _inch_2_SU(24), _inch_2_SU(12))
else:
bottom = _mm_2_SU(self.hardware['length_mm'] if 'length_mm' in self.hardware else mediaheight)
right = _mm_2_SU(self.hardware['width_mm'] if 'width_mm' in self.hardware else mediawidth)
self.set_boundary(0, 0, bottom, right)
def setup(self, media=139, speed=None, pressure=None, toolholder=None, pen=None,
cuttingmat=None, sharpencorners=False, sharpencorners_start=0.1, sharpencorners_end=0.1,
autoblade=False, depth=None, sw_clipping=True, trackenhancing=False, bladediameter=0.9,
landscape=False, leftaligned=None, mediawidth=210.0, mediaheight=297.0, verbose=True):
"""Setup the Silhouette Device
Parameters
----------
media : int, optional
range is [100..300], "Print Paper Light Weight". Defaults to 132.
speed : int, optional
range is [1..10] for Cameo3 and older,
range is [1..30] for Cameo4. Defaults to None, from paper (132 -> 10).
pressure : int, optional
range is [1..33], Notice: Cameo runs trackenhancing if you select a pressure of 19 or more. Defaults to None, from paper (132 -> 5).
toolholder : int, optional
range is [1..2]. Defaults to 1.
pen : bool, optional
media dependent. Defaults to None.
cuttingmat : {'cameo_12x12', 'cameo_12x24'}, optional
setting the cutting mat. Defaults to None.
sharpencorners : bool, optional
Defaults to False.
sharpencorners_start : float, optional
Defaults to 0.1.
sharpencorners_end : float, optional
Defaults to 0.1.
autoblade : bool, optional
Defaults to False.
depth : int, optional
range is [0..10] Defaults to None.
sw_clipping : bool, optional
Defaults to True.
trackenhancing : bool, optional
Defaults to False.
bladediameter : float, optional
Defaults to 0.9.
landscape : bool, optional
Defaults to False.
leftaligned : bool, optional
Loaded media is aligned left(=True) or right(=False). Defaults to device dependant.
mediawidth : float, optional
Defaults to 210.0.
mediaheight : float, optional
Defaults to 297.0.
"""
if leftaligned is not None:
self.leftaligned = leftaligned
if self.dev is None: return None
self.initialize()
self.set_cutting_mat(cuttingmat, mediawidth, mediaheight)
if media is not None:
if media < 100 or media > 300: media = 300
# Silhouette Studio does not appear to issue this command
if self.product_id() not in [PRODUCT_ID_SILHOUETTE_CAMEO3, PRODUCT_ID_SILHOUETTE_CAMEO4]:
self.send_command("FW%d" % media)
if pen is None:
if media == 113:
pen = True
else:
pen = False
for i in MEDIA:
if i[0] == media:
if verbose:
print("Media=%d, cap='%s', name='%s'" % (media, i[4], i[5]), file=self.log)
if pressure is None: pressure = i[1]
if speed is None: speed = i[2]
if depth is None: depth = i[3]
break
tool = SilhouetteCameoTool(toolholder)
if toolholder is None:
toolholder = 1
if self.product_id() in [PRODUCT_ID_SILHOUETTE_CAMEO3, PRODUCT_ID_SILHOUETTE_CAMEO4]:
self.send_command(tool.select())
if verbose:
print("toolholder: %d" % toolholder, file=self.log)
# cameo 4 sets some parameters two times (force, acceleration, Cutter offset)
if self.product_id() == PRODUCT_ID_SILHOUETTE_CAMEO4:
if pressure is not None:
if pressure < 1: pressure = 1
if pressure > 33: pressure = 33
self.send_command(tool.pressure(pressure))
if verbose:
print("pressure: %d" % pressure, file=self.log)
# on first connection acceleration is always set to 0
self.send_command(self.acceleration_cmd(0))
if speed is not None:
if speed < 1: speed = 1
if speed > 30: speed = 30
self.send_command(tool.speed(speed))
if verbose:
print("speed: %d" % speed, file=self.log)
# set cutter offset a first time (seems to always be 0mm x 0.05mm)
self.send_command(tool.cutter_offset(0, 0.05))
# lift tool between paths
self.send_command(tool.lift(sharpencorners))
if pen:
self.send_command(tool.sharpen_corners(0, 0))
else:
# start and end for sharpen corners is transmitted in tenth of a millimeter NOT in SUs
sharpencorners_start = int((sharpencorners_start + 0.05) * 10.0)
sharpencorners_end = int((sharpencorners_end + 0.05) * 10.0)
self.send_command(tool.sharpen_corners(sharpencorners_start, sharpencorners_end))
# set pressure a second time (don't know why, just reproducing)
if pressure is not None:
if pressure < 1: pressure = 1
if pressure > 33: pressure = 33
self.send_command(tool.pressure(pressure))
if verbose:
print("pressure: %d" % pressure, file=self.log)
self.send_command(self.acceleration_cmd(3))
# set cutter offset a second time (this time with blade specific parameters)
if pen:
self.send_command(tool.cutter_offset(0, 0.05))
else:
self.send_command(tool.cutter_offset(bladediameter, 0.05))
else:
if speed is not None:
if speed < 1: speed = 1
if speed > 10: speed = 10
if self.product_id() == PRODUCT_ID_SILHOUETTE_CAMEO3:
self.send_command(tool.speed(speed))
else:
self.send_command("!%d" % speed)
if verbose:
print("speed: %d" % speed, file=self.log)
if pressure is not None:
if pressure < 1: pressure = 1
if pressure > 33: pressure = 33
if self.product_id() == PRODUCT_ID_SILHOUETTE_CAMEO3:
self.send_command(tool.pressure(pressure))
else:
self.send_command("FX%d" % pressure)
# s.write(b"FX%d,0\x03" % pressure); # oops, graphtecprint does it like this
if verbose:
print("pressure: %d" % pressure, file=self.log)
if self.product_id() == PRODUCT_ID_SILHOUETTE_CAMEO3:
if pen:
self.send_command(tool.cutter_offset(0, 0.05))
if self.leftaligned:
if verbose:
print("Loaded media is expected left-aligned.", file=self.log)
else:
if verbose:
print("Loaded media is expected right-aligned.", file=self.log)
# Lift plotter head at sharp corners
if self.product_id() == PRODUCT_ID_SILHOUETTE_CAMEO3:
self.send_command(tool.lift(sharpencorners))
if pen:
self.send_command(tool.sharpen_corners(0, 0))
else:
# TODO: shouldn't be this also SU? why * 10 ?
sharpencorners_start = int((sharpencorners_start + 0.05) * 10.0)
sharpencorners_end = int((sharpencorners_end + 0.05) * 10.0)
self.send_command(tool.sharpen_corners(sharpencorners_start, sharpencorners_end))
# robocut/Plotter.cpp:393 says:
# It is 0 for the pen, 18 for cutting. Default diameter of a blade is 0.9mm
# C possible stands for curvature. Not that any of the other letters make sense...
# C possible stands for circle.
# This value is the circle diameter which is executed on direction changes on corners to adjust the blade.
# Seems to be limited to 46 or 47. Values above does keep the last setting on the device.
if self.product_id() == PRODUCT_ID_SILHOUETTE_CAMEO3:
if not pen:
self.send_command([
tool.cutter_offset(0, 0.05),
tool.cutter_offset(bladediameter, 0.05)])
elif self.product_id() == PRODUCT_ID_SILHOUETTE_CURIO:
if pen:
#print("Curio is using pen");
self.send_command("FC0") # Silhouette Studio issues this command twice in a row; 0 for pen
self.send_command("FC0")
else:
#print("Curio is using blade");
self.send_command("FC18") # Silhouette Studio issues this command twice in a row; 18 for blade
self.send_command("FC18")
else:
if pen:
self.send_command("FC0")
else:
self.send_command("FC%d" % _mm_2_SU(bladediameter))
if self.product_id() in [PRODUCT_ID_SILHOUETTE_CAMEO3, PRODUCT_ID_SILHOUETTE_CAMEO4]:
if autoblade and depth is not None:
if toolholder == 1:
if depth < 0: depth = 0
if depth > 10: depth = 10
self.send_command(tool.depth(depth))
if verbose:
print("depth: %d" % depth, file=self.log)
self.enable_sw_clipping = sw_clipping
# if enabled, rollers three times forward and back.
# needs a pressure of 19 or more, else nothing will happen
if trackenhancing is not None:
if trackenhancing:
self.send_command("FY0")
else:
if self.product_id() in [PRODUCT_ID_SILHOUETTE_CAMEO3, PRODUCT_ID_SILHOUETTE_CAMEO4]:
pass
else:
self.send_command("FY1")
#FNx, x = 0 seem to be some kind of reset, x = 1: plotter head moves to other
# side of media (boundary check?), but next cut run will stall
#TB50,x: x = 1 landscape mode, x = 0 portrait mode
if self.product_id() in [PRODUCT_ID_SILHOUETTE_CAMEO3, PRODUCT_ID_SILHOUETTE_CAMEO4]:
pass
else:
if landscape is not None:
if landscape:
self.send_command(["FN0", "TB50,1"])
else:
self.send_command(["FN0", "TB50,0"])
# Don't lift plotter head between paths
self.send_command("FE0,0")