meerk40t 0.9.3001__py2.py3-none-any.whl → 0.9.7010__py2.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.
Files changed (445) hide show
  1. meerk40t/__init__.py +1 -1
  2. meerk40t/balormk/balor_params.py +167 -167
  3. meerk40t/balormk/clone_loader.py +457 -457
  4. meerk40t/balormk/controller.py +1566 -1512
  5. meerk40t/balormk/cylindermod.py +64 -0
  6. meerk40t/balormk/device.py +966 -1959
  7. meerk40t/balormk/driver.py +778 -591
  8. meerk40t/balormk/galvo_commands.py +1195 -0
  9. meerk40t/balormk/gui/balorconfig.py +237 -111
  10. meerk40t/balormk/gui/balorcontroller.py +191 -184
  11. meerk40t/balormk/gui/baloroperationproperties.py +116 -115
  12. meerk40t/balormk/gui/corscene.py +845 -0
  13. meerk40t/balormk/gui/gui.py +179 -147
  14. meerk40t/balormk/livelightjob.py +466 -382
  15. meerk40t/balormk/mock_connection.py +131 -109
  16. meerk40t/balormk/plugin.py +133 -135
  17. meerk40t/balormk/usb_connection.py +306 -301
  18. meerk40t/camera/__init__.py +1 -1
  19. meerk40t/camera/camera.py +514 -397
  20. meerk40t/camera/gui/camerapanel.py +1241 -1095
  21. meerk40t/camera/gui/gui.py +58 -58
  22. meerk40t/camera/plugin.py +441 -399
  23. meerk40t/ch341/__init__.py +27 -27
  24. meerk40t/ch341/ch341device.py +628 -628
  25. meerk40t/ch341/libusb.py +595 -589
  26. meerk40t/ch341/mock.py +171 -171
  27. meerk40t/ch341/windriver.py +157 -157
  28. meerk40t/constants.py +13 -0
  29. meerk40t/core/__init__.py +1 -1
  30. meerk40t/core/bindalias.py +550 -539
  31. meerk40t/core/core.py +47 -47
  32. meerk40t/core/cutcode/cubiccut.py +73 -73
  33. meerk40t/core/cutcode/cutcode.py +315 -312
  34. meerk40t/core/cutcode/cutgroup.py +141 -137
  35. meerk40t/core/cutcode/cutobject.py +192 -185
  36. meerk40t/core/cutcode/dwellcut.py +37 -37
  37. meerk40t/core/cutcode/gotocut.py +29 -29
  38. meerk40t/core/cutcode/homecut.py +29 -29
  39. meerk40t/core/cutcode/inputcut.py +34 -34
  40. meerk40t/core/cutcode/linecut.py +33 -33
  41. meerk40t/core/cutcode/outputcut.py +34 -34
  42. meerk40t/core/cutcode/plotcut.py +335 -335
  43. meerk40t/core/cutcode/quadcut.py +61 -61
  44. meerk40t/core/cutcode/rastercut.py +168 -148
  45. meerk40t/core/cutcode/waitcut.py +34 -34
  46. meerk40t/core/cutplan.py +1843 -1316
  47. meerk40t/core/drivers.py +330 -329
  48. meerk40t/core/elements/align.py +801 -669
  49. meerk40t/core/elements/branches.py +1844 -1507
  50. meerk40t/core/elements/clipboard.py +229 -219
  51. meerk40t/core/elements/element_treeops.py +4561 -2837
  52. meerk40t/core/elements/element_types.py +125 -105
  53. meerk40t/core/elements/elements.py +4329 -3617
  54. meerk40t/core/elements/files.py +117 -64
  55. meerk40t/core/elements/geometry.py +473 -224
  56. meerk40t/core/elements/grid.py +467 -316
  57. meerk40t/core/elements/materials.py +158 -94
  58. meerk40t/core/elements/notes.py +50 -38
  59. meerk40t/core/elements/offset_clpr.py +933 -912
  60. meerk40t/core/elements/offset_mk.py +963 -955
  61. meerk40t/core/elements/penbox.py +339 -267
  62. meerk40t/core/elements/placements.py +300 -83
  63. meerk40t/core/elements/render.py +785 -687
  64. meerk40t/core/elements/shapes.py +2618 -2092
  65. meerk40t/core/elements/trace.py +651 -563
  66. meerk40t/core/elements/tree_commands.py +415 -409
  67. meerk40t/core/elements/undo_redo.py +116 -58
  68. meerk40t/core/elements/wordlist.py +319 -200
  69. meerk40t/core/exceptions.py +9 -9
  70. meerk40t/core/laserjob.py +220 -220
  71. meerk40t/core/logging.py +63 -63
  72. meerk40t/core/node/blobnode.py +83 -86
  73. meerk40t/core/node/bootstrap.py +105 -103
  74. meerk40t/core/node/branch_elems.py +40 -31
  75. meerk40t/core/node/branch_ops.py +45 -38
  76. meerk40t/core/node/branch_regmark.py +48 -41
  77. meerk40t/core/node/cutnode.py +29 -32
  78. meerk40t/core/node/effect_hatch.py +375 -257
  79. meerk40t/core/node/effect_warp.py +398 -0
  80. meerk40t/core/node/effect_wobble.py +441 -309
  81. meerk40t/core/node/elem_ellipse.py +404 -309
  82. meerk40t/core/node/elem_image.py +1082 -801
  83. meerk40t/core/node/elem_line.py +358 -292
  84. meerk40t/core/node/elem_path.py +259 -201
  85. meerk40t/core/node/elem_point.py +129 -102
  86. meerk40t/core/node/elem_polyline.py +310 -246
  87. meerk40t/core/node/elem_rect.py +376 -286
  88. meerk40t/core/node/elem_text.py +445 -418
  89. meerk40t/core/node/filenode.py +59 -40
  90. meerk40t/core/node/groupnode.py +138 -74
  91. meerk40t/core/node/image_processed.py +777 -766
  92. meerk40t/core/node/image_raster.py +156 -113
  93. meerk40t/core/node/layernode.py +31 -31
  94. meerk40t/core/node/mixins.py +135 -107
  95. meerk40t/core/node/node.py +1427 -1304
  96. meerk40t/core/node/nutils.py +117 -114
  97. meerk40t/core/node/op_cut.py +462 -335
  98. meerk40t/core/node/op_dots.py +296 -251
  99. meerk40t/core/node/op_engrave.py +414 -311
  100. meerk40t/core/node/op_image.py +755 -369
  101. meerk40t/core/node/op_raster.py +787 -522
  102. meerk40t/core/node/place_current.py +37 -40
  103. meerk40t/core/node/place_point.py +329 -126
  104. meerk40t/core/node/refnode.py +58 -47
  105. meerk40t/core/node/rootnode.py +225 -219
  106. meerk40t/core/node/util_console.py +48 -48
  107. meerk40t/core/node/util_goto.py +84 -65
  108. meerk40t/core/node/util_home.py +61 -61
  109. meerk40t/core/node/util_input.py +102 -102
  110. meerk40t/core/node/util_output.py +102 -102
  111. meerk40t/core/node/util_wait.py +65 -65
  112. meerk40t/core/parameters.py +709 -707
  113. meerk40t/core/planner.py +875 -785
  114. meerk40t/core/plotplanner.py +656 -652
  115. meerk40t/core/space.py +120 -113
  116. meerk40t/core/spoolers.py +706 -705
  117. meerk40t/core/svg_io.py +1836 -1549
  118. meerk40t/core/treeop.py +534 -445
  119. meerk40t/core/undos.py +278 -124
  120. meerk40t/core/units.py +784 -680
  121. meerk40t/core/view.py +393 -322
  122. meerk40t/core/webhelp.py +62 -62
  123. meerk40t/core/wordlist.py +513 -504
  124. meerk40t/cylinder/cylinder.py +247 -0
  125. meerk40t/cylinder/gui/cylindersettings.py +41 -0
  126. meerk40t/cylinder/gui/gui.py +24 -0
  127. meerk40t/device/__init__.py +1 -1
  128. meerk40t/device/basedevice.py +322 -123
  129. meerk40t/device/devicechoices.py +50 -0
  130. meerk40t/device/dummydevice.py +163 -128
  131. meerk40t/device/gui/defaultactions.py +618 -602
  132. meerk40t/device/gui/effectspanel.py +114 -0
  133. meerk40t/device/gui/formatterpanel.py +253 -290
  134. meerk40t/device/gui/warningpanel.py +337 -260
  135. meerk40t/device/mixins.py +13 -13
  136. meerk40t/dxf/__init__.py +1 -1
  137. meerk40t/dxf/dxf_io.py +766 -554
  138. meerk40t/dxf/plugin.py +47 -35
  139. meerk40t/external_plugins.py +79 -79
  140. meerk40t/external_plugins_build.py +28 -28
  141. meerk40t/extra/cag.py +112 -116
  142. meerk40t/extra/coolant.py +403 -0
  143. meerk40t/extra/encode_detect.py +198 -0
  144. meerk40t/extra/ezd.py +1165 -1165
  145. meerk40t/extra/hershey.py +835 -340
  146. meerk40t/extra/imageactions.py +322 -316
  147. meerk40t/extra/inkscape.py +630 -622
  148. meerk40t/extra/lbrn.py +424 -424
  149. meerk40t/extra/outerworld.py +284 -0
  150. meerk40t/extra/param_functions.py +1542 -1556
  151. meerk40t/extra/potrace.py +257 -253
  152. meerk40t/extra/serial_exchange.py +118 -0
  153. meerk40t/extra/updater.py +602 -453
  154. meerk40t/extra/vectrace.py +147 -146
  155. meerk40t/extra/winsleep.py +83 -83
  156. meerk40t/extra/xcs_reader.py +597 -0
  157. meerk40t/fill/fills.py +781 -335
  158. meerk40t/fill/patternfill.py +1061 -1061
  159. meerk40t/fill/patterns.py +614 -567
  160. meerk40t/grbl/control.py +87 -87
  161. meerk40t/grbl/controller.py +990 -903
  162. meerk40t/grbl/device.py +1081 -768
  163. meerk40t/grbl/driver.py +989 -771
  164. meerk40t/grbl/emulator.py +532 -497
  165. meerk40t/grbl/gcodejob.py +783 -767
  166. meerk40t/grbl/gui/grblconfiguration.py +373 -298
  167. meerk40t/grbl/gui/grblcontroller.py +485 -271
  168. meerk40t/grbl/gui/grblhardwareconfig.py +269 -153
  169. meerk40t/grbl/gui/grbloperationconfig.py +105 -0
  170. meerk40t/grbl/gui/gui.py +147 -116
  171. meerk40t/grbl/interpreter.py +44 -44
  172. meerk40t/grbl/loader.py +22 -22
  173. meerk40t/grbl/mock_connection.py +56 -56
  174. meerk40t/grbl/plugin.py +294 -264
  175. meerk40t/grbl/serial_connection.py +93 -88
  176. meerk40t/grbl/tcp_connection.py +81 -79
  177. meerk40t/grbl/ws_connection.py +112 -0
  178. meerk40t/gui/__init__.py +1 -1
  179. meerk40t/gui/about.py +2042 -296
  180. meerk40t/gui/alignment.py +1644 -1608
  181. meerk40t/gui/autoexec.py +199 -0
  182. meerk40t/gui/basicops.py +791 -670
  183. meerk40t/gui/bufferview.py +77 -71
  184. meerk40t/gui/busy.py +170 -133
  185. meerk40t/gui/choicepropertypanel.py +1673 -1469
  186. meerk40t/gui/consolepanel.py +706 -542
  187. meerk40t/gui/devicepanel.py +687 -581
  188. meerk40t/gui/dialogoptions.py +110 -107
  189. meerk40t/gui/executejob.py +316 -306
  190. meerk40t/gui/fonts.py +90 -90
  191. meerk40t/gui/functionwrapper.py +252 -0
  192. meerk40t/gui/gui_mixins.py +729 -0
  193. meerk40t/gui/guicolors.py +205 -182
  194. meerk40t/gui/help_assets/help_assets.py +218 -201
  195. meerk40t/gui/helper.py +154 -0
  196. meerk40t/gui/hersheymanager.py +1430 -846
  197. meerk40t/gui/icons.py +3422 -2747
  198. meerk40t/gui/imagesplitter.py +555 -508
  199. meerk40t/gui/keymap.py +354 -344
  200. meerk40t/gui/laserpanel.py +892 -806
  201. meerk40t/gui/laserrender.py +1470 -1232
  202. meerk40t/gui/lasertoolpanel.py +805 -793
  203. meerk40t/gui/magnetoptions.py +436 -0
  204. meerk40t/gui/materialmanager.py +2917 -0
  205. meerk40t/gui/materialtest.py +1722 -1694
  206. meerk40t/gui/mkdebug.py +646 -359
  207. meerk40t/gui/mwindow.py +163 -140
  208. meerk40t/gui/navigationpanels.py +2605 -2467
  209. meerk40t/gui/notes.py +143 -142
  210. meerk40t/gui/opassignment.py +414 -410
  211. meerk40t/gui/operation_info.py +310 -299
  212. meerk40t/gui/plugin.py +494 -328
  213. meerk40t/gui/position.py +714 -669
  214. meerk40t/gui/preferences.py +901 -650
  215. meerk40t/gui/propertypanels/attributes.py +1461 -1131
  216. meerk40t/gui/propertypanels/blobproperty.py +117 -114
  217. meerk40t/gui/propertypanels/consoleproperty.py +83 -80
  218. meerk40t/gui/propertypanels/gotoproperty.py +77 -0
  219. meerk40t/gui/propertypanels/groupproperties.py +223 -217
  220. meerk40t/gui/propertypanels/hatchproperty.py +489 -469
  221. meerk40t/gui/propertypanels/imageproperty.py +2244 -1384
  222. meerk40t/gui/propertypanels/inputproperty.py +59 -58
  223. meerk40t/gui/propertypanels/opbranchproperties.py +82 -80
  224. meerk40t/gui/propertypanels/operationpropertymain.py +1890 -1638
  225. meerk40t/gui/propertypanels/outputproperty.py +59 -58
  226. meerk40t/gui/propertypanels/pathproperty.py +389 -380
  227. meerk40t/gui/propertypanels/placementproperty.py +1214 -383
  228. meerk40t/gui/propertypanels/pointproperty.py +140 -136
  229. meerk40t/gui/propertypanels/propertywindow.py +313 -181
  230. meerk40t/gui/propertypanels/rasterwizardpanels.py +996 -912
  231. meerk40t/gui/propertypanels/regbranchproperties.py +76 -0
  232. meerk40t/gui/propertypanels/textproperty.py +770 -755
  233. meerk40t/gui/propertypanels/waitproperty.py +56 -55
  234. meerk40t/gui/propertypanels/warpproperty.py +121 -0
  235. meerk40t/gui/propertypanels/wobbleproperty.py +255 -204
  236. meerk40t/gui/ribbon.py +2468 -2210
  237. meerk40t/gui/scene/scene.py +1100 -1051
  238. meerk40t/gui/scene/sceneconst.py +22 -22
  239. meerk40t/gui/scene/scenepanel.py +439 -349
  240. meerk40t/gui/scene/scenespacewidget.py +365 -365
  241. meerk40t/gui/scene/widget.py +518 -505
  242. meerk40t/gui/scenewidgets/affinemover.py +215 -215
  243. meerk40t/gui/scenewidgets/attractionwidget.py +315 -309
  244. meerk40t/gui/scenewidgets/bedwidget.py +120 -97
  245. meerk40t/gui/scenewidgets/elementswidget.py +137 -107
  246. meerk40t/gui/scenewidgets/gridwidget.py +785 -745
  247. meerk40t/gui/scenewidgets/guidewidget.py +765 -765
  248. meerk40t/gui/scenewidgets/laserpathwidget.py +66 -66
  249. meerk40t/gui/scenewidgets/machineoriginwidget.py +86 -86
  250. meerk40t/gui/scenewidgets/nodeselector.py +28 -28
  251. meerk40t/gui/scenewidgets/rectselectwidget.py +589 -346
  252. meerk40t/gui/scenewidgets/relocatewidget.py +33 -33
  253. meerk40t/gui/scenewidgets/reticlewidget.py +83 -83
  254. meerk40t/gui/scenewidgets/selectionwidget.py +2952 -2756
  255. meerk40t/gui/simpleui.py +357 -333
  256. meerk40t/gui/simulation.py +2431 -2094
  257. meerk40t/gui/snapoptions.py +208 -203
  258. meerk40t/gui/spoolerpanel.py +1227 -1180
  259. meerk40t/gui/statusbarwidgets/defaultoperations.py +480 -353
  260. meerk40t/gui/statusbarwidgets/infowidget.py +520 -483
  261. meerk40t/gui/statusbarwidgets/opassignwidget.py +356 -355
  262. meerk40t/gui/statusbarwidgets/selectionwidget.py +172 -171
  263. meerk40t/gui/statusbarwidgets/shapepropwidget.py +754 -236
  264. meerk40t/gui/statusbarwidgets/statusbar.py +272 -260
  265. meerk40t/gui/statusbarwidgets/statusbarwidget.py +268 -270
  266. meerk40t/gui/statusbarwidgets/strokewidget.py +267 -251
  267. meerk40t/gui/themes.py +200 -78
  268. meerk40t/gui/tips.py +591 -0
  269. meerk40t/gui/toolwidgets/circlebrush.py +35 -35
  270. meerk40t/gui/toolwidgets/toolcircle.py +248 -242
  271. meerk40t/gui/toolwidgets/toolcontainer.py +82 -77
  272. meerk40t/gui/toolwidgets/tooldraw.py +97 -90
  273. meerk40t/gui/toolwidgets/toolellipse.py +219 -212
  274. meerk40t/gui/toolwidgets/toolimagecut.py +25 -132
  275. meerk40t/gui/toolwidgets/toolline.py +39 -144
  276. meerk40t/gui/toolwidgets/toollinetext.py +79 -236
  277. meerk40t/gui/toolwidgets/toollinetext_inline.py +296 -0
  278. meerk40t/gui/toolwidgets/toolmeasure.py +160 -216
  279. meerk40t/gui/toolwidgets/toolnodeedit.py +2088 -2074
  280. meerk40t/gui/toolwidgets/toolnodemove.py +92 -94
  281. meerk40t/gui/toolwidgets/toolparameter.py +754 -668
  282. meerk40t/gui/toolwidgets/toolplacement.py +108 -108
  283. meerk40t/gui/toolwidgets/toolpoint.py +68 -59
  284. meerk40t/gui/toolwidgets/toolpointlistbuilder.py +294 -0
  285. meerk40t/gui/toolwidgets/toolpointmove.py +183 -0
  286. meerk40t/gui/toolwidgets/toolpolygon.py +288 -403
  287. meerk40t/gui/toolwidgets/toolpolyline.py +38 -196
  288. meerk40t/gui/toolwidgets/toolrect.py +211 -207
  289. meerk40t/gui/toolwidgets/toolrelocate.py +72 -72
  290. meerk40t/gui/toolwidgets/toolribbon.py +598 -113
  291. meerk40t/gui/toolwidgets/tooltabedit.py +546 -0
  292. meerk40t/gui/toolwidgets/tooltext.py +98 -89
  293. meerk40t/gui/toolwidgets/toolvector.py +213 -204
  294. meerk40t/gui/toolwidgets/toolwidget.py +39 -39
  295. meerk40t/gui/usbconnect.py +98 -91
  296. meerk40t/gui/utilitywidgets/buttonwidget.py +18 -18
  297. meerk40t/gui/utilitywidgets/checkboxwidget.py +90 -90
  298. meerk40t/gui/utilitywidgets/controlwidget.py +14 -14
  299. meerk40t/gui/utilitywidgets/cyclocycloidwidget.py +343 -340
  300. meerk40t/gui/utilitywidgets/debugwidgets.py +148 -0
  301. meerk40t/gui/utilitywidgets/handlewidget.py +27 -27
  302. meerk40t/gui/utilitywidgets/harmonograph.py +450 -447
  303. meerk40t/gui/utilitywidgets/openclosewidget.py +40 -40
  304. meerk40t/gui/utilitywidgets/rotationwidget.py +54 -54
  305. meerk40t/gui/utilitywidgets/scalewidget.py +75 -75
  306. meerk40t/gui/utilitywidgets/seekbarwidget.py +183 -183
  307. meerk40t/gui/utilitywidgets/togglewidget.py +142 -142
  308. meerk40t/gui/utilitywidgets/toolbarwidget.py +8 -8
  309. meerk40t/gui/wordlisteditor.py +985 -931
  310. meerk40t/gui/wxmeerk40t.py +1444 -1169
  311. meerk40t/gui/wxmmain.py +5578 -4112
  312. meerk40t/gui/wxmribbon.py +1591 -1076
  313. meerk40t/gui/wxmscene.py +1635 -1453
  314. meerk40t/gui/wxmtree.py +2410 -2089
  315. meerk40t/gui/wxutils.py +1769 -1099
  316. meerk40t/gui/zmatrix.py +102 -102
  317. meerk40t/image/__init__.py +1 -1
  318. meerk40t/image/dither.py +429 -0
  319. meerk40t/image/imagetools.py +2778 -2269
  320. meerk40t/internal_plugins.py +150 -130
  321. meerk40t/kernel/__init__.py +63 -12
  322. meerk40t/kernel/channel.py +259 -212
  323. meerk40t/kernel/context.py +538 -538
  324. meerk40t/kernel/exceptions.py +41 -41
  325. meerk40t/kernel/functions.py +463 -414
  326. meerk40t/kernel/jobs.py +100 -100
  327. meerk40t/kernel/kernel.py +3809 -3571
  328. meerk40t/kernel/lifecycles.py +71 -71
  329. meerk40t/kernel/module.py +49 -49
  330. meerk40t/kernel/service.py +147 -147
  331. meerk40t/kernel/settings.py +383 -343
  332. meerk40t/lihuiyu/controller.py +883 -876
  333. meerk40t/lihuiyu/device.py +1181 -1069
  334. meerk40t/lihuiyu/driver.py +1466 -1372
  335. meerk40t/lihuiyu/gui/gui.py +127 -106
  336. meerk40t/lihuiyu/gui/lhyaccelgui.py +377 -363
  337. meerk40t/lihuiyu/gui/lhycontrollergui.py +741 -651
  338. meerk40t/lihuiyu/gui/lhydrivergui.py +470 -446
  339. meerk40t/lihuiyu/gui/lhyoperationproperties.py +238 -237
  340. meerk40t/lihuiyu/gui/tcpcontroller.py +226 -190
  341. meerk40t/lihuiyu/interpreter.py +53 -53
  342. meerk40t/lihuiyu/laserspeed.py +450 -450
  343. meerk40t/lihuiyu/loader.py +90 -90
  344. meerk40t/lihuiyu/parser.py +404 -404
  345. meerk40t/lihuiyu/plugin.py +101 -102
  346. meerk40t/lihuiyu/tcp_connection.py +111 -109
  347. meerk40t/main.py +231 -165
  348. meerk40t/moshi/builder.py +788 -781
  349. meerk40t/moshi/controller.py +505 -499
  350. meerk40t/moshi/device.py +495 -442
  351. meerk40t/moshi/driver.py +862 -696
  352. meerk40t/moshi/gui/gui.py +78 -76
  353. meerk40t/moshi/gui/moshicontrollergui.py +538 -522
  354. meerk40t/moshi/gui/moshidrivergui.py +87 -75
  355. meerk40t/moshi/plugin.py +43 -43
  356. meerk40t/network/console_server.py +102 -57
  357. meerk40t/network/kernelserver.py +10 -9
  358. meerk40t/network/tcp_server.py +142 -140
  359. meerk40t/network/udp_server.py +103 -77
  360. meerk40t/network/web_server.py +390 -0
  361. meerk40t/newly/controller.py +1158 -1144
  362. meerk40t/newly/device.py +874 -732
  363. meerk40t/newly/driver.py +540 -412
  364. meerk40t/newly/gui/gui.py +219 -188
  365. meerk40t/newly/gui/newlyconfig.py +116 -101
  366. meerk40t/newly/gui/newlycontroller.py +193 -186
  367. meerk40t/newly/gui/operationproperties.py +51 -51
  368. meerk40t/newly/mock_connection.py +82 -82
  369. meerk40t/newly/newly_params.py +56 -56
  370. meerk40t/newly/plugin.py +1214 -1246
  371. meerk40t/newly/usb_connection.py +322 -322
  372. meerk40t/rotary/gui/gui.py +52 -46
  373. meerk40t/rotary/gui/rotarysettings.py +240 -232
  374. meerk40t/rotary/rotary.py +202 -98
  375. meerk40t/ruida/control.py +291 -91
  376. meerk40t/ruida/controller.py +138 -1088
  377. meerk40t/ruida/device.py +672 -231
  378. meerk40t/ruida/driver.py +534 -472
  379. meerk40t/ruida/emulator.py +1494 -1491
  380. meerk40t/ruida/exceptions.py +4 -4
  381. meerk40t/ruida/gui/gui.py +71 -76
  382. meerk40t/ruida/gui/ruidaconfig.py +239 -72
  383. meerk40t/ruida/gui/ruidacontroller.py +187 -184
  384. meerk40t/ruida/gui/ruidaoperationproperties.py +48 -47
  385. meerk40t/ruida/loader.py +54 -52
  386. meerk40t/ruida/mock_connection.py +57 -109
  387. meerk40t/ruida/plugin.py +124 -87
  388. meerk40t/ruida/rdjob.py +2084 -945
  389. meerk40t/ruida/serial_connection.py +116 -0
  390. meerk40t/ruida/tcp_connection.py +146 -0
  391. meerk40t/ruida/udp_connection.py +73 -0
  392. meerk40t/svgelements.py +9671 -9669
  393. meerk40t/tools/driver_to_path.py +584 -579
  394. meerk40t/tools/geomstr.py +5583 -4680
  395. meerk40t/tools/jhfparser.py +357 -292
  396. meerk40t/tools/kerftest.py +904 -890
  397. meerk40t/tools/livinghinges.py +1168 -1033
  398. meerk40t/tools/pathtools.py +987 -949
  399. meerk40t/tools/pmatrix.py +234 -0
  400. meerk40t/tools/pointfinder.py +942 -942
  401. meerk40t/tools/polybool.py +940 -940
  402. meerk40t/tools/rasterplotter.py +1660 -547
  403. meerk40t/tools/shxparser.py +989 -901
  404. meerk40t/tools/ttfparser.py +726 -446
  405. meerk40t/tools/zinglplotter.py +595 -593
  406. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7010.dist-info}/LICENSE +21 -21
  407. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7010.dist-info}/METADATA +150 -139
  408. meerk40t-0.9.7010.dist-info/RECORD +445 -0
  409. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7010.dist-info}/WHEEL +1 -1
  410. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7010.dist-info}/top_level.txt +0 -1
  411. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7010.dist-info}/zip-safe +1 -1
  412. meerk40t/balormk/elementlightjob.py +0 -159
  413. meerk40t-0.9.3001.dist-info/RECORD +0 -437
  414. test/bootstrap.py +0 -63
  415. test/test_cli.py +0 -12
  416. test/test_core_cutcode.py +0 -418
  417. test/test_core_elements.py +0 -144
  418. test/test_core_plotplanner.py +0 -397
  419. test/test_core_viewports.py +0 -312
  420. test/test_drivers_grbl.py +0 -108
  421. test/test_drivers_lihuiyu.py +0 -443
  422. test/test_drivers_newly.py +0 -113
  423. test/test_element_degenerate_points.py +0 -43
  424. test/test_elements_classify.py +0 -97
  425. test/test_elements_penbox.py +0 -22
  426. test/test_file_svg.py +0 -176
  427. test/test_fill.py +0 -155
  428. test/test_geomstr.py +0 -1523
  429. test/test_geomstr_nodes.py +0 -18
  430. test/test_imagetools_actualize.py +0 -306
  431. test/test_imagetools_wizard.py +0 -258
  432. test/test_kernel.py +0 -200
  433. test/test_laser_speeds.py +0 -3303
  434. test/test_length.py +0 -57
  435. test/test_lifecycle.py +0 -66
  436. test/test_operations.py +0 -251
  437. test/test_operations_hatch.py +0 -57
  438. test/test_ruida.py +0 -19
  439. test/test_spooler.py +0 -22
  440. test/test_tools_rasterplotter.py +0 -29
  441. test/test_wobble.py +0 -133
  442. test/test_zingl.py +0 -124
  443. {test → meerk40t/cylinder}/__init__.py +0 -0
  444. /meerk40t/{core/element_commands.py → cylinder/gui/__init__.py} +0 -0
  445. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7010.dist-info}/entry_points.txt +0 -0
meerk40t/core/spoolers.py CHANGED
@@ -1,705 +1,706 @@
1
- import time
2
- from math import isinf
3
- from threading import Condition
4
-
5
- from meerk40t.core.laserjob import LaserJob
6
- from meerk40t.core.units import Length
7
- from meerk40t.kernel import CommandSyntaxError
8
-
9
- """
10
- This module defines a set of commands that usually send a single easy command to the spooler. Basic jogging, home,
11
- unlock rail commands. And it provides the the spooler class which should be provided by each driver.
12
-
13
- Spoolers process different jobs in order. A spooler job can be anything, but usually is a LaserJob which is a simple
14
- list of commands.
15
- """
16
-
17
-
18
- def plugin(kernel, lifecycle):
19
- if lifecycle == "register":
20
- _ = kernel.translation
21
-
22
- @kernel.console_command(
23
- "spool",
24
- help=_("spool <command>"),
25
- regex=True,
26
- input_type=(None, "plan"),
27
- output_type="spooler",
28
- )
29
- def spool(command, channel, _, data=None, remainder=None, **kwgs):
30
- device = kernel.device
31
-
32
- spooler = device.spooler
33
- # Do we have a filename to use as label?
34
- label = kernel.elements.basename
35
-
36
- if data is not None:
37
- # If plan data is in data, then we copy that and move on to next step.
38
- data.final()
39
- loops = 1
40
- elements = kernel.elements
41
- e = elements.op_branch
42
-
43
- if e.loop_continuous:
44
- loops = float("inf")
45
- else:
46
- if e.loop_enabled:
47
- loops = e.loop_n
48
- spooler.laserjob(
49
- data.plan, loops=loops, label=label, outline=data.outline
50
- )
51
- channel(_("Spooled Plan."))
52
- kernel.root.signal("plan", data.name, 6)
53
-
54
- if remainder is None:
55
- channel(_("----------"))
56
- channel(_("Spoolers:"))
57
- for d, d_name in enumerate(device.match("device", suffix=True)):
58
- channel(f"{d}: {d_name}")
59
- channel(_("----------"))
60
- channel(_("Spooler on device {name}:").format(name=str(device.label)))
61
- for s, op_name in enumerate(spooler.queue):
62
- channel(f"{s}: {op_name}")
63
- channel(_("----------"))
64
- return "spooler", spooler
65
-
66
- @kernel.console_argument("op", type=str, help=_("unlock, origin, home, etc"))
67
- @kernel.console_command(
68
- "send",
69
- help=_("send a plan-command to the spooler"),
70
- input_type="spooler",
71
- output_type="spooler",
72
- )
73
- def spooler_send(
74
- command, channel, _, data_type=None, op=None, data=None, **kwgs
75
- ):
76
- spooler = data
77
- if op is None:
78
- raise CommandSyntaxError
79
- try:
80
- for plan_command, command_name, suffix in kernel.find("plan", op):
81
- label = f"Send {op}"
82
- spooler.laserjob(plan_command, label=label)
83
- return data_type, spooler
84
- except (KeyError, IndexError):
85
- pass
86
- channel(_("No plan command found."))
87
- return data_type, spooler
88
-
89
- @kernel.console_command(
90
- "list",
91
- help=_("spool<?> list"),
92
- input_type="spooler",
93
- output_type="spooler",
94
- )
95
- def spooler_list(command, channel, _, data_type=None, data=None, **kwgs):
96
- spooler = data
97
- channel(_("----------"))
98
- channel(_("Spoolers:"))
99
- for d, d_name in enumerate(kernel.match("device", suffix=True)):
100
- channel(f"{d}: {d_name}")
101
- channel(_("----------"))
102
- channel(
103
- _("Spooler on device {name}:").format(name=str(kernel.device.label))
104
- )
105
- for s, op_name in enumerate(spooler.queue):
106
- channel(f"{s}: {op_name}")
107
- channel(_("----------"))
108
- return data_type, spooler
109
-
110
- @kernel.console_command(
111
- "clear",
112
- help=_("spooler<?> clear"),
113
- input_type="spooler",
114
- output_type="spooler",
115
- )
116
- def spooler_clear(command, channel, _, data_type=None, data=None, **kwgs):
117
- spooler = data
118
- spooler.clear_queue()
119
- return data_type, spooler
120
-
121
- @kernel.console_command(
122
- "+laser",
123
- hidden=True,
124
- input_type=("spooler", None),
125
- output_type="spooler",
126
- help=_("turn laser on in place"),
127
- )
128
- def plus_laser(data, **kwgs):
129
- if data is None:
130
- data = kernel.device.spooler
131
- spooler = data
132
- spooler.command("laser_on")
133
- return "spooler", spooler
134
-
135
- @kernel.console_command(
136
- "-laser",
137
- hidden=True,
138
- input_type=("spooler", None),
139
- output_type="spooler",
140
- help=_("turn laser off in place"),
141
- )
142
- def minus_laser(data, **kwgs):
143
- if data is None:
144
- data = kernel.device.spooler
145
- spooler = data
146
- spooler.command("laser_off")
147
- return "spooler", spooler
148
-
149
- @kernel.console_argument(
150
- "amount", type=Length, help=_("amount to move in the set direction.")
151
- )
152
- @kernel.console_command(
153
- ("left", "right", "up", "down"),
154
- input_type=("spooler", None),
155
- output_type="spooler",
156
- help=_("cmd <amount>"),
157
- )
158
- def direction(command, channel, _, data=None, amount=None, **kwgs):
159
- if data is None:
160
- data = kernel.device.spooler
161
- spooler = data
162
- if amount is None:
163
- amount = Length("1mm")
164
- if not hasattr(spooler, "_dx"):
165
- spooler._dx = Length(0)
166
- if not hasattr(spooler, "_dy"):
167
- spooler._dy = Length(0)
168
- if command.endswith("right"):
169
- spooler._dx += amount
170
- elif command.endswith("left"):
171
- spooler._dx -= amount
172
- elif command.endswith("up"):
173
- spooler._dy -= amount
174
- elif command.endswith("down"):
175
- spooler._dy += amount
176
- kernel.console(".timer 1 0 spool jog\n")
177
- return "spooler", spooler
178
-
179
- @kernel.console_option("force", "f", type=bool, action="store_true")
180
- @kernel.console_command(
181
- "jog",
182
- hidden=True,
183
- input_type=("spooler", None),
184
- output_type="spooler",
185
- help=_("executes outstanding jog buffer"),
186
- )
187
- def jog(command, channel, _, data, force=False, **kwgs):
188
- if data is None:
189
- data = kernel.device.spooler
190
- spooler = data
191
- try:
192
- idx = spooler._dx
193
- idy = spooler._dy
194
- except AttributeError:
195
- return
196
- if force:
197
- spooler.command("move_rel", idx, idy)
198
- spooler._dx = Length(0)
199
- spooler._dy = Length(0)
200
- else:
201
- if spooler.is_idle:
202
- spooler.command("move_rel", float(idx), float(idy))
203
- channel(_("Position moved: {x} {y}").format(x=idx, y=idy))
204
- spooler._dx = Length(0)
205
- spooler._dy = Length(0)
206
- else:
207
- channel(_("Busy Error"))
208
- return "spooler", spooler
209
-
210
- @kernel.console_option("force", "f", type=bool, action="store_true")
211
- @kernel.console_argument("x", type=Length, help=_("change in x"))
212
- @kernel.console_argument("y", type=Length, help=_("change in y"))
213
- @kernel.console_command(
214
- ("move", "move_absolute"),
215
- input_type=("spooler", None),
216
- output_type="spooler",
217
- help=_("move <x> <y>: move to position."),
218
- )
219
- def move_absolute(channel, _, x, y, data=None, force=False, **kwgs):
220
- if data is None:
221
- data = kernel.device.spooler
222
- spooler = data
223
- if y is None:
224
- raise CommandSyntaxError
225
- if force:
226
- spooler.command("move_abs", x, y)
227
- else:
228
- if spooler.is_idle:
229
- spooler.command("move_abs", x, y)
230
- else:
231
- channel(_("Busy Error"))
232
- return "spooler", spooler
233
-
234
- @kernel.console_option("force", "f", type=bool, action="store_true")
235
- @kernel.console_argument("dx", type=Length, help=_("change in x"))
236
- @kernel.console_argument("dy", type=Length, help=_("change in y"))
237
- @kernel.console_command(
238
- "move_relative",
239
- input_type=("spooler", None),
240
- output_type="spooler",
241
- help=_("move_relative <dx> <dy>"),
242
- )
243
- def move_relative(channel, _, dx, dy, data=None, force=False, **kwgs):
244
- if data is None:
245
- data = kernel.device.spooler
246
- spooler = data
247
- if dy is None:
248
- raise CommandSyntaxError
249
- if force:
250
- spooler.command("move_rel", dx, dy)
251
- else:
252
- if spooler.is_idle:
253
- spooler.command("move_rel", dx, dy)
254
- else:
255
- channel(_("Busy Error"))
256
- return "spooler", spooler
257
-
258
- @kernel.console_command(
259
- "home",
260
- input_type=("spooler", None),
261
- output_type="spooler",
262
- help=_("home the laser"),
263
- )
264
- def home(data=None, **kwgs):
265
- if data is None:
266
- data = kernel.device.spooler
267
- spooler = data
268
- spooler.command("home")
269
- return "spooler", spooler
270
-
271
- @kernel.console_command(
272
- "physical_home",
273
- input_type=("spooler", None),
274
- output_type="spooler",
275
- help=_("home the laser (goto endstops)"),
276
- )
277
- def physical_home(data=None, **kwgs):
278
- if data is None:
279
- data = kernel.device.spooler
280
- spooler = data
281
- spooler.command("physical_home")
282
- return "spooler", spooler
283
-
284
- @kernel.console_command(
285
- "unlock",
286
- input_type=("spooler", None),
287
- output_type="spooler",
288
- help=_("unlock the rail"),
289
- )
290
- def unlock(data=None, **kwgs):
291
- if data is None:
292
- data = kernel.device.spooler
293
- spooler = data
294
- spooler.command("unlock_rail")
295
- return "spooler", spooler
296
-
297
- @kernel.console_command(
298
- "lock",
299
- input_type=("spooler", None),
300
- output_type="spooler",
301
- help=_("lock the rail"),
302
- )
303
- def lock(data, **kwgs):
304
- if data is None:
305
- data = kernel.device.spooler
306
- spooler = data
307
- spooler.command("lock_rail")
308
- return "spooler", spooler
309
-
310
- @kernel.console_command(
311
- "test_dot_and_home",
312
- input_type=("spooler", None),
313
- hidden=True,
314
- )
315
- def run_home_and_dot_test(data, **kwgs):
316
- if data is None:
317
- data = kernel.device.spooler
318
- spooler = data
319
-
320
- def home_dot_test():
321
- for i in range(25):
322
- yield "rapid_mode"
323
- yield "home"
324
- yield "laser_off"
325
- yield "wait_finish"
326
- yield "move_abs", "3in", "3in"
327
- yield "wait_finish"
328
- yield "laser_on"
329
- yield "wait", 50
330
- yield "laser_off"
331
- yield "wait_finish"
332
- yield "home"
333
- yield "wait_finish"
334
-
335
- spooler.laserjob(
336
- list(home_dot_test()), label=f"Dot and Home Test", helper=True
337
- )
338
- return "spooler", spooler
339
-
340
-
341
- class SpoolerJob:
342
- """
343
- Example of Spooler Job.
344
-
345
- The primary methodology of a spoolerjob is that it has an `execute` function that takes the driver as an argument
346
- it should then perform driver-like commands on the given driver to perform whatever actions the job should
347
- execute.
348
-
349
- The `priority` attribute is required.
350
-
351
- The job should be permitted to `stop()` and respond to `is_running()`, and other checks as to elapsed_time(),
352
- estimate_time(), and status.
353
- """
354
-
355
- def __init__(
356
- self,
357
- service,
358
- ):
359
- self.stopped = False
360
- self.service = service
361
- self.runtime = 0
362
- self.priority = 0
363
-
364
- @property
365
- def status(self):
366
- """
367
- Status is simply a status as to the job. This will be relayed by things that check the job status of jobs
368
- in the spooler.
369
-
370
- @return:
371
- """
372
- if self.is_running:
373
- return "Running"
374
- else:
375
- return "Queued"
376
-
377
- def execute(self, driver):
378
- """
379
- This is the primary method of the SpoolerJob. In this example we call the "home()" function.
380
- @param driver:
381
- @return:
382
- """
383
- try:
384
- driver.home()
385
- except AttributeError:
386
- pass
387
-
388
- return True
389
-
390
- def stop(self):
391
- self.stopped = True
392
-
393
- def is_running(self):
394
- return not self.stopped
395
-
396
- def elapsed_time(self):
397
- """
398
- How long is this job already running...
399
- """
400
- result = 0
401
- return result
402
-
403
- def estimate_time(self):
404
- return 0
405
-
406
-
407
- class Spooler:
408
- """
409
- Spoolers are threaded job processors. A series of jobs is added to the spooler and these jobs are
410
- processed in order. The driver is checked for any holds it may have preventing new commands from being
411
- executed. If that isn't the case, the highest priority job is executed by calling the job's required
412
- `execute()` function passing the relevant driver as the one variable. The job itself is agnostic, and will
413
- execute whatever it wants calling the driver-like functions that may or may not exist on the driver.
414
-
415
- If execute() returns true then it is fully executed and will be removed. Otherwise, it will be repeatedly
416
- called until whatever work it is doing is finished. This also means the driver itself is checked for holds
417
- (usually pausing or busy) each cycle.
418
- """
419
-
420
- def __init__(self, context, driver=None, **kwargs):
421
- self.context = context
422
- self.driver = driver
423
- self._current = None
424
-
425
- self._lock = Condition()
426
- self._queue = []
427
-
428
- self._shutdown = False
429
- self._thread = None
430
-
431
- def __repr__(self):
432
- return f"Spooler({str(self.context)})"
433
-
434
- def __del__(self):
435
- self.name = None
436
- self._lock = None
437
- self._queue = None
438
-
439
- def __len__(self):
440
- return len(self._queue)
441
-
442
- def added(self, *args, **kwargs):
443
- """
444
- Device service is added to the kernel.
445
-
446
- @param args:
447
- @param kwargs:
448
- @return:
449
- """
450
- self.restart()
451
-
452
- def shutdown(self, *args, **kwargs):
453
- """
454
- device service is shutdown during the shutdown of the kernel or destruction of the service
455
-
456
- @param args:
457
- @param kwargs:
458
- @return:
459
- """
460
- self._shutdown = True
461
- with self._lock:
462
- self._lock.notify_all()
463
-
464
- def restart(self):
465
- """
466
- Start or restart the spooler thread.
467
-
468
- @return:
469
- """
470
- self._shutdown = False
471
- if self._thread is None:
472
-
473
- def clear_thread(*a):
474
- self._shutdown = True
475
- self._thread = None
476
- try:
477
- # If something is currently processing stop it.
478
- self._current.stop()
479
- except AttributeError:
480
- pass
481
- with self._lock:
482
- self._lock.notify_all()
483
-
484
- self._thread = self.context.threaded(
485
- self.run,
486
- result=clear_thread,
487
- thread_name=f"Spooler({self.context.path})",
488
- )
489
- self._thread.stop = clear_thread
490
-
491
- def run(self):
492
- """
493
- Run thread for the spooler.
494
-
495
- The thread runs while the spooler is not shutdown. This executes in the spooler thread. It waits, while
496
- the queue is empty and is notified when items are added to the queue. Each job in the spooler is called
497
- with execute(). If the function returns True, the job is finished and removed. We then move on to the next
498
- spooler item. If execute() returns False the job is not finished and will be reattempted. Each spooler
499
- cycle checks the priority and whether there's a wait/hold for jobs at that priority level.
500
-
501
- @return:
502
- """
503
- while not self._shutdown:
504
- if self.context.kernel.is_shutdown:
505
- return # Kernel shutdown spooler threads should die off.
506
- is_valid = False
507
- with self._lock:
508
- idx = 0
509
- while not is_valid:
510
- try:
511
- program = self._queue[idx]
512
- # Not all type of jobs are regular jobs,
513
- # so that needs to be checked...
514
- if hasattr(program, "enabled"):
515
- if program.enabled:
516
- is_valid = True
517
- break
518
- else:
519
- idx += 1
520
- else:
521
- is_valid = True
522
- break
523
- except IndexError:
524
- # There is no work to do.
525
- self._lock.wait()
526
- break
527
- if not is_valid:
528
- continue
529
-
530
- priority = program.priority
531
-
532
- # Check if the driver holds work at this priority level.
533
- if self.driver.hold_work(priority):
534
- time.sleep(0.01)
535
- continue
536
- if program != self._current:
537
- # A different job is loaded. If it has a job_start, we call that.
538
- if hasattr(self.driver, "job_start"):
539
- function = getattr(self.driver, "job_start")
540
- function(program)
541
- self._current = program
542
- try:
543
- fully_executed = program.execute(self.driver)
544
- except ConnectionAbortedError:
545
- # Driver could no longer connect to where it was told to send the data.
546
- return
547
- except ConnectionRefusedError:
548
- # Driver connection failed but, we are not aborting the spooler thread
549
- if self._shutdown:
550
- return
551
- with self._lock:
552
- self._lock.wait()
553
- continue
554
- if fully_executed:
555
- # all work finished
556
- self.remove(program)
557
-
558
- # If we finished this work we call job_finished.
559
- if hasattr(self.driver, "job_finish"):
560
- function = getattr(self.driver, "job_finish")
561
- function(program)
562
-
563
- @property
564
- def is_idle(self):
565
- return len(self._queue) == 0 or self._queue[0].priority < 0
566
-
567
- @property
568
- def current(self):
569
- return self._current
570
-
571
- @property
572
- def queue(self):
573
- return self._queue
574
-
575
- def laserjob(
576
- self, job, priority=0, loops=1, label=None, helper=False, outline=None
577
- ):
578
- """
579
- send a wrapped laser job to the spooler.
580
- """
581
- if label is None:
582
- label = f"{self.__class__.__name__}:{len(job)} items"
583
- # label = str(job)
584
- ljob = LaserJob(
585
- label,
586
- list(job),
587
- driver=self.driver,
588
- priority=priority,
589
- loops=loops,
590
- outline=outline,
591
- )
592
- ljob.helper = helper
593
- ljob.uid = self.context.logging.uid("job")
594
- with self._lock:
595
- self._stop_lower_priority_running_jobs(priority)
596
- self._queue.append(ljob)
597
- self._queue.sort(key=lambda e: e.priority, reverse=True)
598
- self._lock.notify()
599
- self.context.signal("spooler;queue", len(self._queue))
600
-
601
- def command(self, *job, priority=0, helper=True, outline=None):
602
- ljob = LaserJob(
603
- str(job), [job], driver=self.driver, priority=priority, outline=outline
604
- )
605
- ljob.helper = helper
606
- ljob.uid = self.context.logging.uid("job")
607
- with self._lock:
608
- self._stop_lower_priority_running_jobs(priority)
609
- self._queue.append(ljob)
610
- self._queue.sort(key=lambda e: e.priority, reverse=True)
611
- self._lock.notify()
612
- self.context.signal("spooler;queue", len(self._queue))
613
-
614
- def send(self, job, prevent_duplicate=False):
615
- """
616
- Send a job to the spooler queue
617
-
618
- @param job: job to send to the spooler.
619
- @param prevent_duplicate: prevents the same job from being added again.
620
- @return:
621
- """
622
- job.uid = self.context.logging.uid("job")
623
- with self._lock:
624
- if prevent_duplicate:
625
- for q in self._queue:
626
- if q is job:
627
- return
628
- self._stop_lower_priority_running_jobs(job.priority)
629
- self._queue.append(job)
630
- self._queue.sort(key=lambda e: e.priority, reverse=True)
631
- self._lock.notify()
632
- self.context.signal("spooler;queue", len(self._queue))
633
-
634
- def _stop_lower_priority_running_jobs(self, priority):
635
- for e in self._queue:
636
- if e.is_running() and e.priority < priority:
637
- e.stop()
638
-
639
- def clear_queue(self):
640
- with self._lock:
641
- for element in self._queue:
642
- loop = getattr(element, "loops_executed", 0)
643
- total = getattr(element, "loops", 0)
644
- if isinf(total):
645
- status = "stopped"
646
- elif loop < total:
647
- status = "stopped"
648
- else:
649
- status = "completed"
650
- self.context.logging.event(
651
- {
652
- "uid": getattr(element, "uid"),
653
- "status": status,
654
- "loop": getattr(element, "loops_executed", None),
655
- "total": getattr(element, "loops", None),
656
- "label": getattr(element, "label", None),
657
- "start_time": getattr(element, "time_started", None),
658
- "duration": getattr(element, "runtime", None),
659
- "device": self.context.label,
660
- "important": not getattr(element, "helper", False),
661
- "estimate": element.estimate_time()
662
- if hasattr(element, "estimate_time")
663
- else None,
664
- "steps_done": getattr(element, "steps_done", None),
665
- "steps_total": getattr(element, "steps_total", None),
666
- }
667
- )
668
- self.context.signal("spooler;completed")
669
- element.stop()
670
- self._queue.clear()
671
- self._lock.notify()
672
- self.context.signal("spooler;queue", len(self._queue))
673
-
674
- def remove(self, element):
675
- with self._lock:
676
- status = "completed"
677
- if element.status == "Running":
678
- element.stop()
679
- status = "Stopped"
680
- self.context.logging.event(
681
- {
682
- "uid": getattr(element, "uid"),
683
- "status": status,
684
- "loop": getattr(element, "loops_executed", None),
685
- "total": getattr(element, "loops", None),
686
- "label": getattr(element, "label", None),
687
- "start_time": getattr(element, "time_started", None),
688
- "duration": getattr(element, "runtime", None),
689
- "device": self.context.label,
690
- "important": not getattr(element, "helper", False),
691
- "estimate": element.estimate_time()
692
- if hasattr(element, "estimate_time")
693
- else None,
694
- "steps_done": getattr(element, "steps_done", None),
695
- "steps_total": getattr(element, "steps_total", None),
696
- }
697
- )
698
- self.context.signal("spooler;completed")
699
- element.stop()
700
- for i in range(len(self._queue) - 1, -1, -1):
701
- e = self._queue[i]
702
- if e is element:
703
- del self._queue[i]
704
- self._lock.notify()
705
- self.context.signal("spooler;queue", len(self._queue))
1
+ import time
2
+ from math import isinf
3
+ from threading import Condition
4
+
5
+ from meerk40t.core.laserjob import LaserJob
6
+ from meerk40t.core.units import Length
7
+ from meerk40t.kernel import CommandSyntaxError
8
+
9
+ """
10
+ This module defines a set of commands that usually send a single easy command to the spooler. Basic jogging, home,
11
+ unlock rail commands. And it provides the the spooler class which should be provided by each driver.
12
+
13
+ Spoolers process different jobs in order. A spooler job can be anything, but usually is a LaserJob which is a simple
14
+ list of commands.
15
+ """
16
+
17
+
18
+ def plugin(kernel, lifecycle):
19
+ if lifecycle == "register":
20
+ _ = kernel.translation
21
+
22
+ @kernel.console_command(
23
+ "spool",
24
+ help=_("spool <command>"),
25
+ regex=True,
26
+ input_type=(None, "plan"),
27
+ output_type="spooler",
28
+ )
29
+ def spool(command, channel, _, data=None, remainder=None, **kwgs):
30
+ device = kernel.device
31
+
32
+ spooler = device.spooler
33
+ # Do we have a filename to use as label?
34
+ label = kernel.elements.basename
35
+
36
+ if data is not None:
37
+ # If plan data is in data, then we copy that and move on to next step.
38
+ data.final()
39
+ loops = 1
40
+ elements = kernel.elements
41
+ elements("wordlist advance\n")
42
+ e = elements.op_branch
43
+
44
+ if e.loop_continuous:
45
+ loops = float("inf")
46
+ else:
47
+ if e.loop_enabled:
48
+ loops = e.loop_n
49
+ spooler.laserjob(
50
+ data.plan, loops=loops, label=label, outline=data.outline
51
+ )
52
+ channel(_("Spooled Plan."))
53
+ kernel.root.signal("plan", data.name, 6)
54
+
55
+ if remainder is None:
56
+ channel(_("----------"))
57
+ channel(_("Spoolers:"))
58
+ for d, d_name in enumerate(device.match("device", suffix=True)):
59
+ channel(f"{d}: {d_name}")
60
+ channel(_("----------"))
61
+ channel(_("Spooler on device {name}:").format(name=str(device.label)))
62
+ for s, op_name in enumerate(spooler.queue):
63
+ channel(f"{s}: {op_name}")
64
+ channel(_("----------"))
65
+ return "spooler", spooler
66
+
67
+ @kernel.console_argument("op", type=str, help=_("unlock, origin, home, etc"))
68
+ @kernel.console_command(
69
+ "send",
70
+ help=_("send a plan-command to the spooler"),
71
+ input_type="spooler",
72
+ output_type="spooler",
73
+ )
74
+ def spooler_send(
75
+ command, channel, _, data_type=None, op=None, data=None, **kwgs
76
+ ):
77
+ spooler = data
78
+ if op is None:
79
+ raise CommandSyntaxError
80
+ try:
81
+ for plan_command, command_name, suffix in kernel.find("plan", op):
82
+ label = f"Send {op}"
83
+ spooler.laserjob(plan_command, label=label)
84
+ return data_type, spooler
85
+ except (KeyError, IndexError):
86
+ pass
87
+ channel(_("No plan command found."))
88
+ return data_type, spooler
89
+
90
+ @kernel.console_command(
91
+ "list",
92
+ help=_("spool<?> list"),
93
+ input_type="spooler",
94
+ output_type="spooler",
95
+ )
96
+ def spooler_list(command, channel, _, data_type=None, data=None, **kwgs):
97
+ spooler = data
98
+ channel(_("----------"))
99
+ channel(_("Spoolers:"))
100
+ for d, d_name in enumerate(kernel.match("device", suffix=True)):
101
+ channel(f"{d}: {d_name}")
102
+ channel(_("----------"))
103
+ channel(
104
+ _("Spooler on device {name}:").format(name=str(kernel.device.label))
105
+ )
106
+ for s, op_name in enumerate(spooler.queue):
107
+ channel(f"{s}: {op_name}")
108
+ channel(_("----------"))
109
+ return data_type, spooler
110
+
111
+ @kernel.console_command(
112
+ "clear",
113
+ help=_("spooler<?> clear"),
114
+ input_type="spooler",
115
+ output_type="spooler",
116
+ )
117
+ def spooler_clear(command, channel, _, data_type=None, data=None, **kwgs):
118
+ spooler = data
119
+ spooler.clear_queue()
120
+ return data_type, spooler
121
+
122
+ @kernel.console_command(
123
+ "+laser",
124
+ hidden=True,
125
+ input_type=("spooler", None),
126
+ output_type="spooler",
127
+ help=_("turn laser on in place"),
128
+ )
129
+ def plus_laser(data, **kwgs):
130
+ if data is None:
131
+ data = kernel.device.spooler
132
+ spooler = data
133
+ spooler.command("laser_on")
134
+ return "spooler", spooler
135
+
136
+ @kernel.console_command(
137
+ "-laser",
138
+ hidden=True,
139
+ input_type=("spooler", None),
140
+ output_type="spooler",
141
+ help=_("turn laser off in place"),
142
+ )
143
+ def minus_laser(data, **kwgs):
144
+ if data is None:
145
+ data = kernel.device.spooler
146
+ spooler = data
147
+ spooler.command("laser_off")
148
+ return "spooler", spooler
149
+
150
+ @kernel.console_argument(
151
+ "amount", type=Length, help=_("amount to move in the set direction.")
152
+ )
153
+ @kernel.console_command(
154
+ ("left", "right", "up", "down"),
155
+ input_type=("spooler", None),
156
+ output_type="spooler",
157
+ help=_("cmd <amount>"),
158
+ )
159
+ def direction(command, channel, _, data=None, amount=None, **kwgs):
160
+ if data is None:
161
+ data = kernel.device.spooler
162
+ spooler = data
163
+ if amount is None:
164
+ amount = Length("1mm")
165
+ if not hasattr(spooler, "_dx"):
166
+ spooler._dx = Length(0)
167
+ if not hasattr(spooler, "_dy"):
168
+ spooler._dy = Length(0)
169
+ if command.endswith("right"):
170
+ spooler._dx += amount
171
+ elif command.endswith("left"):
172
+ spooler._dx -= amount
173
+ elif command.endswith("up"):
174
+ spooler._dy -= amount
175
+ elif command.endswith("down"):
176
+ spooler._dy += amount
177
+ kernel.console(".timer 1 0 spool jog\n")
178
+ return "spooler", spooler
179
+
180
+ @kernel.console_option("force", "f", type=bool, action="store_true")
181
+ @kernel.console_command(
182
+ "jog",
183
+ hidden=True,
184
+ input_type=("spooler", None),
185
+ output_type="spooler",
186
+ help=_("executes outstanding jog buffer"),
187
+ )
188
+ def jog(command, channel, _, data, force=False, **kwgs):
189
+ if data is None:
190
+ data = kernel.device.spooler
191
+ spooler = data
192
+ try:
193
+ idx = spooler._dx
194
+ idy = spooler._dy
195
+ except AttributeError:
196
+ return
197
+ if force:
198
+ spooler.command("move_rel", idx, idy)
199
+ spooler._dx = Length(0)
200
+ spooler._dy = Length(0)
201
+ else:
202
+ if spooler.is_idle:
203
+ spooler.command("move_rel", float(idx), float(idy))
204
+ channel(_("Position moved: {x} {y}").format(x=idx, y=idy))
205
+ spooler._dx = Length(0)
206
+ spooler._dy = Length(0)
207
+ else:
208
+ channel(_("Busy Error"))
209
+ return "spooler", spooler
210
+
211
+ @kernel.console_option("force", "f", type=bool, action="store_true")
212
+ @kernel.console_argument("x", type=Length, help=_("change in x"))
213
+ @kernel.console_argument("y", type=Length, help=_("change in y"))
214
+ @kernel.console_command(
215
+ ("move", "move_absolute"),
216
+ input_type=("spooler", None),
217
+ output_type="spooler",
218
+ help=_("move <x> <y>: move to position."),
219
+ )
220
+ def move_absolute(channel, _, x, y, data=None, force=False, **kwgs):
221
+ if data is None:
222
+ data = kernel.device.spooler
223
+ spooler = data
224
+ if y is None:
225
+ raise CommandSyntaxError
226
+ if force:
227
+ spooler.command("move_abs", x, y)
228
+ else:
229
+ if spooler.is_idle:
230
+ spooler.command("move_abs", x, y)
231
+ else:
232
+ channel(_("Busy Error"))
233
+ return "spooler", spooler
234
+
235
+ @kernel.console_option("force", "f", type=bool, action="store_true")
236
+ @kernel.console_argument("dx", type=Length, help=_("change in x"))
237
+ @kernel.console_argument("dy", type=Length, help=_("change in y"))
238
+ @kernel.console_command(
239
+ "move_relative",
240
+ input_type=("spooler", None),
241
+ output_type="spooler",
242
+ help=_("move_relative <dx> <dy>"),
243
+ )
244
+ def move_relative(channel, _, dx, dy, data=None, force=False, **kwgs):
245
+ if data is None:
246
+ data = kernel.device.spooler
247
+ spooler = data
248
+ if dy is None:
249
+ raise CommandSyntaxError
250
+ if force:
251
+ spooler.command("move_rel", dx, dy)
252
+ else:
253
+ if spooler.is_idle:
254
+ spooler.command("move_rel", dx, dy)
255
+ else:
256
+ channel(_("Busy Error"))
257
+ return "spooler", spooler
258
+
259
+ @kernel.console_command(
260
+ "home",
261
+ input_type=("spooler", None),
262
+ output_type="spooler",
263
+ help=_("home the laser"),
264
+ )
265
+ def home(data=None, **kwgs):
266
+ if data is None:
267
+ data = kernel.device.spooler
268
+ spooler = data
269
+ spooler.command("home")
270
+ return "spooler", spooler
271
+
272
+ @kernel.console_command(
273
+ "physical_home",
274
+ input_type=("spooler", None),
275
+ output_type="spooler",
276
+ help=_("home the laser (goto endstops)"),
277
+ )
278
+ def physical_home(data=None, **kwgs):
279
+ if data is None:
280
+ data = kernel.device.spooler
281
+ spooler = data
282
+ spooler.command("physical_home")
283
+ return "spooler", spooler
284
+
285
+ @kernel.console_command(
286
+ "unlock",
287
+ input_type=("spooler", None),
288
+ output_type="spooler",
289
+ help=_("unlock the rail"),
290
+ )
291
+ def unlock(data=None, **kwgs):
292
+ if data is None:
293
+ data = kernel.device.spooler
294
+ spooler = data
295
+ spooler.command("unlock_rail")
296
+ return "spooler", spooler
297
+
298
+ @kernel.console_command(
299
+ "lock",
300
+ input_type=("spooler", None),
301
+ output_type="spooler",
302
+ help=_("lock the rail"),
303
+ )
304
+ def lock(data, **kwgs):
305
+ if data is None:
306
+ data = kernel.device.spooler
307
+ spooler = data
308
+ spooler.command("lock_rail")
309
+ return "spooler", spooler
310
+
311
+ @kernel.console_command(
312
+ "test_dot_and_home",
313
+ input_type=("spooler", None),
314
+ hidden=True,
315
+ )
316
+ def run_home_and_dot_test(data, **kwgs):
317
+ if data is None:
318
+ data = kernel.device.spooler
319
+ spooler = data
320
+
321
+ def home_dot_test():
322
+ for i in range(25):
323
+ yield "rapid_mode"
324
+ yield "home"
325
+ yield "laser_off"
326
+ yield "wait_finish"
327
+ yield "move_abs", "3in", "3in"
328
+ yield "wait_finish"
329
+ yield "laser_on"
330
+ yield "wait", 50
331
+ yield "laser_off"
332
+ yield "wait_finish"
333
+ yield "home"
334
+ yield "wait_finish"
335
+
336
+ spooler.laserjob(
337
+ list(home_dot_test()), label="Dot and Home Test", helper=True
338
+ )
339
+ return "spooler", spooler
340
+
341
+
342
+ class SpoolerJob:
343
+ """
344
+ Example of Spooler Job.
345
+
346
+ The primary methodology of a spoolerjob is that it has an `execute` function that takes the driver as an argument
347
+ it should then perform driver-like commands on the given driver to perform whatever actions the job should
348
+ execute.
349
+
350
+ The `priority` attribute is required.
351
+
352
+ The job should be permitted to `stop()` and respond to `is_running()`, and other checks as to elapsed_time(),
353
+ estimate_time(), and status.
354
+ """
355
+
356
+ def __init__(
357
+ self,
358
+ service,
359
+ ):
360
+ self.stopped = False
361
+ self.service = service
362
+ self.runtime = 0
363
+ self.priority = 0
364
+
365
+ @property
366
+ def status(self):
367
+ """
368
+ Status is simply a status as to the job. This will be relayed by things that check the job status of jobs
369
+ in the spooler.
370
+
371
+ @return:
372
+ """
373
+ if self.is_running:
374
+ return "Running"
375
+ else:
376
+ return "Queued"
377
+
378
+ def execute(self, driver):
379
+ """
380
+ This is the primary method of the SpoolerJob. In this example we call the "home()" function.
381
+ @param driver:
382
+ @return:
383
+ """
384
+ try:
385
+ driver.home()
386
+ except AttributeError:
387
+ pass
388
+
389
+ return True
390
+
391
+ def stop(self):
392
+ self.stopped = True
393
+
394
+ def is_running(self):
395
+ return not self.stopped
396
+
397
+ def elapsed_time(self):
398
+ """
399
+ How long is this job already running...
400
+ """
401
+ result = 0
402
+ return result
403
+
404
+ def estimate_time(self):
405
+ return 0
406
+
407
+
408
+ class Spooler:
409
+ """
410
+ Spoolers are threaded job processors. A series of jobs is added to the spooler and these jobs are
411
+ processed in order. The driver is checked for any holds it may have preventing new commands from being
412
+ executed. If that isn't the case, the highest priority job is executed by calling the job's required
413
+ `execute()` function passing the relevant driver as the one variable. The job itself is agnostic, and will
414
+ execute whatever it wants calling the driver-like functions that may or may not exist on the driver.
415
+
416
+ If execute() returns true then it is fully executed and will be removed. Otherwise, it will be repeatedly
417
+ called until whatever work it is doing is finished. This also means the driver itself is checked for holds
418
+ (usually pausing or busy) each cycle.
419
+ """
420
+
421
+ def __init__(self, context, driver=None, **kwargs):
422
+ self.context = context
423
+ self.driver = driver
424
+ self._current = None
425
+
426
+ self._lock = Condition()
427
+ self._queue = []
428
+
429
+ self._shutdown = False
430
+ self._thread = None
431
+
432
+ def __repr__(self):
433
+ return f"Spooler({str(self.context)})"
434
+
435
+ def __del__(self):
436
+ self.name = None
437
+ self._lock = None
438
+ self._queue = None
439
+
440
+ def __len__(self):
441
+ return len(self._queue)
442
+
443
+ def added(self, *args, **kwargs):
444
+ """
445
+ Device service is added to the kernel.
446
+
447
+ @param args:
448
+ @param kwargs:
449
+ @return:
450
+ """
451
+ self.restart()
452
+
453
+ def shutdown(self, *args, **kwargs):
454
+ """
455
+ device service is shutdown during the shutdown of the kernel or destruction of the service
456
+
457
+ @param args:
458
+ @param kwargs:
459
+ @return:
460
+ """
461
+ self._shutdown = True
462
+ with self._lock:
463
+ self._lock.notify_all()
464
+
465
+ def restart(self):
466
+ """
467
+ Start or restart the spooler thread.
468
+
469
+ @return:
470
+ """
471
+ self._shutdown = False
472
+ if self._thread is None:
473
+
474
+ def clear_thread(*a):
475
+ self._shutdown = True
476
+ self._thread = None
477
+ try:
478
+ # If something is currently processing stop it.
479
+ self._current.stop()
480
+ except AttributeError:
481
+ pass
482
+ with self._lock:
483
+ self._lock.notify_all()
484
+
485
+ self._thread = self.context.threaded(
486
+ self.run,
487
+ result=clear_thread,
488
+ thread_name=f"Spooler({self.context.path})",
489
+ )
490
+ self._thread.stop = clear_thread
491
+
492
+ def run(self):
493
+ """
494
+ Run thread for the spooler.
495
+
496
+ The thread runs while the spooler is not shutdown. This executes in the spooler thread. It waits, while
497
+ the queue is empty and is notified when items are added to the queue. Each job in the spooler is called
498
+ with execute(). If the function returns True, the job is finished and removed. We then move on to the next
499
+ spooler item. If execute() returns False the job is not finished and will be reattempted. Each spooler
500
+ cycle checks the priority and whether there's a wait/hold for jobs at that priority level.
501
+
502
+ @return:
503
+ """
504
+ while not self._shutdown:
505
+ if self.context.kernel.is_shutdown:
506
+ return # Kernel shutdown spooler threads should die off.
507
+ is_valid = False
508
+ with self._lock:
509
+ idx = 0
510
+ while not is_valid:
511
+ try:
512
+ program = self._queue[idx]
513
+ # Not all type of jobs are regular jobs,
514
+ # so that needs to be checked...
515
+ if hasattr(program, "enabled"):
516
+ if program.enabled:
517
+ is_valid = True
518
+ break
519
+ else:
520
+ idx += 1
521
+ else:
522
+ is_valid = True
523
+ break
524
+ except IndexError:
525
+ # There is no work to do.
526
+ self._lock.wait()
527
+ break
528
+ if not is_valid:
529
+ continue
530
+
531
+ priority = program.priority
532
+
533
+ # Check if the driver holds work at this priority level.
534
+ if self.driver.hold_work(priority):
535
+ time.sleep(0.01)
536
+ continue
537
+ if program != self._current:
538
+ # A different job is loaded. If it has a job_start, we call that.
539
+ if hasattr(self.driver, "job_start"):
540
+ function = getattr(self.driver, "job_start")
541
+ function(program)
542
+ self._current = program
543
+ try:
544
+ fully_executed = program.execute(self.driver)
545
+ except ConnectionAbortedError:
546
+ # Driver could no longer connect to where it was told to send the data.
547
+ return
548
+ except ConnectionRefusedError:
549
+ # Driver connection failed but, we are not aborting the spooler thread
550
+ if self._shutdown:
551
+ return
552
+ with self._lock:
553
+ self._lock.wait()
554
+ continue
555
+ if fully_executed:
556
+ # all work finished
557
+ self.remove(program)
558
+
559
+ # If we finished this work we call job_finished.
560
+ if hasattr(self.driver, "job_finish"):
561
+ function = getattr(self.driver, "job_finish")
562
+ function(program)
563
+
564
+ @property
565
+ def is_idle(self):
566
+ return len(self._queue) == 0 or self._queue[0].priority < 0
567
+
568
+ @property
569
+ def current(self):
570
+ return self._current
571
+
572
+ @property
573
+ def queue(self):
574
+ return self._queue
575
+
576
+ def laserjob(
577
+ self, job, priority=0, loops=1, label=None, helper=False, outline=None
578
+ ):
579
+ """
580
+ send a wrapped laser job to the spooler.
581
+ """
582
+ if label is None:
583
+ label = f"{self.__class__.__name__}:{len(job)} items"
584
+ # label = str(job)
585
+ ljob = LaserJob(
586
+ label,
587
+ list(job),
588
+ driver=self.driver,
589
+ priority=priority,
590
+ loops=loops,
591
+ outline=outline,
592
+ )
593
+ ljob.helper = helper
594
+ ljob.uid = self.context.logging.uid("job")
595
+ with self._lock:
596
+ self._stop_lower_priority_running_jobs(priority)
597
+ self._queue.append(ljob)
598
+ self._queue.sort(key=lambda e: e.priority, reverse=True)
599
+ self._lock.notify()
600
+ self.context.signal("spooler;queue", len(self._queue))
601
+
602
+ def command(self, *job, priority=0, helper=True, outline=None):
603
+ ljob = LaserJob(
604
+ str(job), [job], driver=self.driver, priority=priority, outline=outline
605
+ )
606
+ ljob.helper = helper
607
+ ljob.uid = self.context.logging.uid("job")
608
+ with self._lock:
609
+ self._stop_lower_priority_running_jobs(priority)
610
+ self._queue.append(ljob)
611
+ self._queue.sort(key=lambda e: e.priority, reverse=True)
612
+ self._lock.notify()
613
+ self.context.signal("spooler;queue", len(self._queue))
614
+
615
+ def send(self, job, prevent_duplicate=False):
616
+ """
617
+ Send a job to the spooler queue
618
+
619
+ @param job: job to send to the spooler.
620
+ @param prevent_duplicate: prevents the same job from being added again.
621
+ @return:
622
+ """
623
+ job.uid = self.context.logging.uid("job")
624
+ with self._lock:
625
+ if prevent_duplicate:
626
+ for q in self._queue:
627
+ if q is job:
628
+ return
629
+ self._stop_lower_priority_running_jobs(job.priority)
630
+ self._queue.append(job)
631
+ self._queue.sort(key=lambda e: e.priority, reverse=True)
632
+ self._lock.notify()
633
+ self.context.signal("spooler;queue", len(self._queue))
634
+
635
+ def _stop_lower_priority_running_jobs(self, priority):
636
+ for e in self._queue:
637
+ if e.is_running() and e.priority < priority:
638
+ e.stop()
639
+
640
+ def clear_queue(self):
641
+ with self._lock:
642
+ for element in self._queue:
643
+ loop = getattr(element, "loops_executed", 0)
644
+ total = getattr(element, "loops", 0)
645
+ if isinf(total):
646
+ status = "stopped"
647
+ elif loop < total:
648
+ status = "stopped"
649
+ else:
650
+ status = "completed"
651
+ self.context.logging.event(
652
+ {
653
+ "uid": getattr(element, "uid"),
654
+ "status": status,
655
+ "loop": getattr(element, "loops_executed", None),
656
+ "total": getattr(element, "loops", None),
657
+ "label": getattr(element, "label", None),
658
+ "start_time": getattr(element, "time_started", None),
659
+ "duration": getattr(element, "runtime", None),
660
+ "device": self.context.label,
661
+ "important": not getattr(element, "helper", False),
662
+ "estimate": element.estimate_time()
663
+ if hasattr(element, "estimate_time")
664
+ else None,
665
+ "steps_done": getattr(element, "steps_done", None),
666
+ "steps_total": getattr(element, "steps_total", None),
667
+ }
668
+ )
669
+ self.context.signal("spooler;completed")
670
+ element.stop()
671
+ self._queue.clear()
672
+ self._lock.notify()
673
+ self.context.signal("spooler;queue", len(self._queue))
674
+
675
+ def remove(self, element):
676
+ with self._lock:
677
+ status = "completed"
678
+ if element.status == "Running":
679
+ element.stop()
680
+ status = "Stopped"
681
+ self.context.logging.event(
682
+ {
683
+ "uid": getattr(element, "uid"),
684
+ "status": status,
685
+ "loop": getattr(element, "loops_executed", None),
686
+ "total": getattr(element, "loops", None),
687
+ "label": getattr(element, "label", None),
688
+ "start_time": getattr(element, "time_started", None),
689
+ "duration": getattr(element, "runtime", None),
690
+ "device": self.context.label,
691
+ "important": not getattr(element, "helper", False),
692
+ "estimate": element.estimate_time()
693
+ if hasattr(element, "estimate_time")
694
+ else None,
695
+ "steps_done": getattr(element, "steps_done", None),
696
+ "steps_total": getattr(element, "steps_total", None),
697
+ }
698
+ )
699
+ self.context.signal("spooler;completed")
700
+ element.stop()
701
+ for i in range(len(self._queue) - 1, -1, -1):
702
+ e = self._queue[i]
703
+ if e is element:
704
+ del self._queue[i]
705
+ self._lock.notify()
706
+ self.context.signal("spooler;queue", len(self._queue))