meerk40t 0.9.3001__py2.py3-none-any.whl → 0.9.7020__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 (446) 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 +1194 -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 +1858 -1507
  50. meerk40t/core/elements/clipboard.py +229 -219
  51. meerk40t/core/elements/element_treeops.py +4595 -2837
  52. meerk40t/core/elements/element_types.py +125 -105
  53. meerk40t/core/elements/elements.py +4315 -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 +934 -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/testcases.py +105 -0
  66. meerk40t/core/elements/trace.py +651 -563
  67. meerk40t/core/elements/tree_commands.py +415 -409
  68. meerk40t/core/elements/undo_redo.py +116 -58
  69. meerk40t/core/elements/wordlist.py +319 -200
  70. meerk40t/core/exceptions.py +9 -9
  71. meerk40t/core/laserjob.py +220 -220
  72. meerk40t/core/logging.py +63 -63
  73. meerk40t/core/node/blobnode.py +83 -86
  74. meerk40t/core/node/bootstrap.py +105 -103
  75. meerk40t/core/node/branch_elems.py +40 -31
  76. meerk40t/core/node/branch_ops.py +45 -38
  77. meerk40t/core/node/branch_regmark.py +48 -41
  78. meerk40t/core/node/cutnode.py +29 -32
  79. meerk40t/core/node/effect_hatch.py +375 -257
  80. meerk40t/core/node/effect_warp.py +398 -0
  81. meerk40t/core/node/effect_wobble.py +441 -309
  82. meerk40t/core/node/elem_ellipse.py +404 -309
  83. meerk40t/core/node/elem_image.py +1082 -801
  84. meerk40t/core/node/elem_line.py +358 -292
  85. meerk40t/core/node/elem_path.py +259 -201
  86. meerk40t/core/node/elem_point.py +129 -102
  87. meerk40t/core/node/elem_polyline.py +310 -246
  88. meerk40t/core/node/elem_rect.py +376 -286
  89. meerk40t/core/node/elem_text.py +445 -418
  90. meerk40t/core/node/filenode.py +59 -40
  91. meerk40t/core/node/groupnode.py +138 -74
  92. meerk40t/core/node/image_processed.py +777 -766
  93. meerk40t/core/node/image_raster.py +156 -113
  94. meerk40t/core/node/layernode.py +31 -31
  95. meerk40t/core/node/mixins.py +135 -107
  96. meerk40t/core/node/node.py +1427 -1304
  97. meerk40t/core/node/nutils.py +117 -114
  98. meerk40t/core/node/op_cut.py +463 -335
  99. meerk40t/core/node/op_dots.py +296 -251
  100. meerk40t/core/node/op_engrave.py +414 -311
  101. meerk40t/core/node/op_image.py +755 -369
  102. meerk40t/core/node/op_raster.py +787 -522
  103. meerk40t/core/node/place_current.py +37 -40
  104. meerk40t/core/node/place_point.py +329 -126
  105. meerk40t/core/node/refnode.py +58 -47
  106. meerk40t/core/node/rootnode.py +225 -219
  107. meerk40t/core/node/util_console.py +48 -48
  108. meerk40t/core/node/util_goto.py +84 -65
  109. meerk40t/core/node/util_home.py +61 -61
  110. meerk40t/core/node/util_input.py +102 -102
  111. meerk40t/core/node/util_output.py +102 -102
  112. meerk40t/core/node/util_wait.py +65 -65
  113. meerk40t/core/parameters.py +709 -707
  114. meerk40t/core/planner.py +875 -785
  115. meerk40t/core/plotplanner.py +656 -652
  116. meerk40t/core/space.py +120 -113
  117. meerk40t/core/spoolers.py +706 -705
  118. meerk40t/core/svg_io.py +1836 -1549
  119. meerk40t/core/treeop.py +534 -445
  120. meerk40t/core/undos.py +278 -124
  121. meerk40t/core/units.py +784 -680
  122. meerk40t/core/view.py +393 -322
  123. meerk40t/core/webhelp.py +62 -62
  124. meerk40t/core/wordlist.py +513 -504
  125. meerk40t/cylinder/cylinder.py +247 -0
  126. meerk40t/cylinder/gui/cylindersettings.py +41 -0
  127. meerk40t/cylinder/gui/gui.py +24 -0
  128. meerk40t/device/__init__.py +1 -1
  129. meerk40t/device/basedevice.py +322 -123
  130. meerk40t/device/devicechoices.py +50 -0
  131. meerk40t/device/dummydevice.py +163 -128
  132. meerk40t/device/gui/defaultactions.py +618 -602
  133. meerk40t/device/gui/effectspanel.py +114 -0
  134. meerk40t/device/gui/formatterpanel.py +253 -290
  135. meerk40t/device/gui/warningpanel.py +337 -260
  136. meerk40t/device/mixins.py +13 -13
  137. meerk40t/dxf/__init__.py +1 -1
  138. meerk40t/dxf/dxf_io.py +766 -554
  139. meerk40t/dxf/plugin.py +47 -35
  140. meerk40t/external_plugins.py +79 -79
  141. meerk40t/external_plugins_build.py +28 -28
  142. meerk40t/extra/cag.py +112 -116
  143. meerk40t/extra/coolant.py +403 -0
  144. meerk40t/extra/encode_detect.py +204 -0
  145. meerk40t/extra/ezd.py +1165 -1165
  146. meerk40t/extra/hershey.py +834 -340
  147. meerk40t/extra/imageactions.py +322 -316
  148. meerk40t/extra/inkscape.py +628 -622
  149. meerk40t/extra/lbrn.py +424 -424
  150. meerk40t/extra/outerworld.py +283 -0
  151. meerk40t/extra/param_functions.py +1542 -1556
  152. meerk40t/extra/potrace.py +257 -253
  153. meerk40t/extra/serial_exchange.py +118 -0
  154. meerk40t/extra/updater.py +602 -453
  155. meerk40t/extra/vectrace.py +147 -146
  156. meerk40t/extra/winsleep.py +83 -83
  157. meerk40t/extra/xcs_reader.py +597 -0
  158. meerk40t/fill/fills.py +781 -335
  159. meerk40t/fill/patternfill.py +1061 -1061
  160. meerk40t/fill/patterns.py +614 -567
  161. meerk40t/grbl/control.py +87 -87
  162. meerk40t/grbl/controller.py +990 -903
  163. meerk40t/grbl/device.py +1084 -768
  164. meerk40t/grbl/driver.py +989 -771
  165. meerk40t/grbl/emulator.py +532 -497
  166. meerk40t/grbl/gcodejob.py +783 -767
  167. meerk40t/grbl/gui/grblconfiguration.py +373 -298
  168. meerk40t/grbl/gui/grblcontroller.py +485 -271
  169. meerk40t/grbl/gui/grblhardwareconfig.py +269 -153
  170. meerk40t/grbl/gui/grbloperationconfig.py +105 -0
  171. meerk40t/grbl/gui/gui.py +147 -116
  172. meerk40t/grbl/interpreter.py +44 -44
  173. meerk40t/grbl/loader.py +22 -22
  174. meerk40t/grbl/mock_connection.py +56 -56
  175. meerk40t/grbl/plugin.py +294 -264
  176. meerk40t/grbl/serial_connection.py +93 -88
  177. meerk40t/grbl/tcp_connection.py +81 -79
  178. meerk40t/grbl/ws_connection.py +112 -0
  179. meerk40t/gui/__init__.py +1 -1
  180. meerk40t/gui/about.py +2042 -296
  181. meerk40t/gui/alignment.py +1644 -1608
  182. meerk40t/gui/autoexec.py +199 -0
  183. meerk40t/gui/basicops.py +791 -670
  184. meerk40t/gui/bufferview.py +77 -71
  185. meerk40t/gui/busy.py +232 -133
  186. meerk40t/gui/choicepropertypanel.py +1662 -1469
  187. meerk40t/gui/consolepanel.py +706 -542
  188. meerk40t/gui/devicepanel.py +687 -581
  189. meerk40t/gui/dialogoptions.py +110 -107
  190. meerk40t/gui/executejob.py +316 -306
  191. meerk40t/gui/fonts.py +90 -90
  192. meerk40t/gui/functionwrapper.py +252 -0
  193. meerk40t/gui/gui_mixins.py +729 -0
  194. meerk40t/gui/guicolors.py +205 -182
  195. meerk40t/gui/help_assets/help_assets.py +218 -201
  196. meerk40t/gui/helper.py +154 -0
  197. meerk40t/gui/hersheymanager.py +1440 -846
  198. meerk40t/gui/icons.py +3422 -2747
  199. meerk40t/gui/imagesplitter.py +555 -508
  200. meerk40t/gui/keymap.py +354 -344
  201. meerk40t/gui/laserpanel.py +897 -806
  202. meerk40t/gui/laserrender.py +1470 -1232
  203. meerk40t/gui/lasertoolpanel.py +805 -793
  204. meerk40t/gui/magnetoptions.py +436 -0
  205. meerk40t/gui/materialmanager.py +2944 -0
  206. meerk40t/gui/materialtest.py +1722 -1694
  207. meerk40t/gui/mkdebug.py +646 -359
  208. meerk40t/gui/mwindow.py +163 -140
  209. meerk40t/gui/navigationpanels.py +2605 -2467
  210. meerk40t/gui/notes.py +143 -142
  211. meerk40t/gui/opassignment.py +414 -410
  212. meerk40t/gui/operation_info.py +310 -299
  213. meerk40t/gui/plugin.py +500 -328
  214. meerk40t/gui/position.py +714 -669
  215. meerk40t/gui/preferences.py +901 -650
  216. meerk40t/gui/propertypanels/attributes.py +1461 -1131
  217. meerk40t/gui/propertypanels/blobproperty.py +117 -114
  218. meerk40t/gui/propertypanels/consoleproperty.py +83 -80
  219. meerk40t/gui/propertypanels/gotoproperty.py +77 -0
  220. meerk40t/gui/propertypanels/groupproperties.py +223 -217
  221. meerk40t/gui/propertypanels/hatchproperty.py +489 -469
  222. meerk40t/gui/propertypanels/imageproperty.py +2244 -1384
  223. meerk40t/gui/propertypanels/inputproperty.py +59 -58
  224. meerk40t/gui/propertypanels/opbranchproperties.py +82 -80
  225. meerk40t/gui/propertypanels/operationpropertymain.py +1890 -1638
  226. meerk40t/gui/propertypanels/outputproperty.py +59 -58
  227. meerk40t/gui/propertypanels/pathproperty.py +389 -380
  228. meerk40t/gui/propertypanels/placementproperty.py +1214 -383
  229. meerk40t/gui/propertypanels/pointproperty.py +140 -136
  230. meerk40t/gui/propertypanels/propertywindow.py +313 -181
  231. meerk40t/gui/propertypanels/rasterwizardpanels.py +996 -912
  232. meerk40t/gui/propertypanels/regbranchproperties.py +76 -0
  233. meerk40t/gui/propertypanels/textproperty.py +770 -755
  234. meerk40t/gui/propertypanels/waitproperty.py +56 -55
  235. meerk40t/gui/propertypanels/warpproperty.py +121 -0
  236. meerk40t/gui/propertypanels/wobbleproperty.py +255 -204
  237. meerk40t/gui/ribbon.py +2471 -2210
  238. meerk40t/gui/scene/scene.py +1100 -1051
  239. meerk40t/gui/scene/sceneconst.py +22 -22
  240. meerk40t/gui/scene/scenepanel.py +439 -349
  241. meerk40t/gui/scene/scenespacewidget.py +365 -365
  242. meerk40t/gui/scene/widget.py +518 -505
  243. meerk40t/gui/scenewidgets/affinemover.py +215 -215
  244. meerk40t/gui/scenewidgets/attractionwidget.py +315 -309
  245. meerk40t/gui/scenewidgets/bedwidget.py +120 -97
  246. meerk40t/gui/scenewidgets/elementswidget.py +137 -107
  247. meerk40t/gui/scenewidgets/gridwidget.py +785 -745
  248. meerk40t/gui/scenewidgets/guidewidget.py +765 -765
  249. meerk40t/gui/scenewidgets/laserpathwidget.py +66 -66
  250. meerk40t/gui/scenewidgets/machineoriginwidget.py +86 -86
  251. meerk40t/gui/scenewidgets/nodeselector.py +28 -28
  252. meerk40t/gui/scenewidgets/rectselectwidget.py +592 -346
  253. meerk40t/gui/scenewidgets/relocatewidget.py +33 -33
  254. meerk40t/gui/scenewidgets/reticlewidget.py +83 -83
  255. meerk40t/gui/scenewidgets/selectionwidget.py +2958 -2756
  256. meerk40t/gui/simpleui.py +362 -333
  257. meerk40t/gui/simulation.py +2451 -2094
  258. meerk40t/gui/snapoptions.py +208 -203
  259. meerk40t/gui/spoolerpanel.py +1227 -1180
  260. meerk40t/gui/statusbarwidgets/defaultoperations.py +480 -353
  261. meerk40t/gui/statusbarwidgets/infowidget.py +520 -483
  262. meerk40t/gui/statusbarwidgets/opassignwidget.py +356 -355
  263. meerk40t/gui/statusbarwidgets/selectionwidget.py +172 -171
  264. meerk40t/gui/statusbarwidgets/shapepropwidget.py +754 -236
  265. meerk40t/gui/statusbarwidgets/statusbar.py +272 -260
  266. meerk40t/gui/statusbarwidgets/statusbarwidget.py +268 -270
  267. meerk40t/gui/statusbarwidgets/strokewidget.py +267 -251
  268. meerk40t/gui/themes.py +200 -78
  269. meerk40t/gui/tips.py +590 -0
  270. meerk40t/gui/toolwidgets/circlebrush.py +35 -35
  271. meerk40t/gui/toolwidgets/toolcircle.py +248 -242
  272. meerk40t/gui/toolwidgets/toolcontainer.py +82 -77
  273. meerk40t/gui/toolwidgets/tooldraw.py +97 -90
  274. meerk40t/gui/toolwidgets/toolellipse.py +219 -212
  275. meerk40t/gui/toolwidgets/toolimagecut.py +25 -132
  276. meerk40t/gui/toolwidgets/toolline.py +39 -144
  277. meerk40t/gui/toolwidgets/toollinetext.py +79 -236
  278. meerk40t/gui/toolwidgets/toollinetext_inline.py +296 -0
  279. meerk40t/gui/toolwidgets/toolmeasure.py +163 -216
  280. meerk40t/gui/toolwidgets/toolnodeedit.py +2088 -2074
  281. meerk40t/gui/toolwidgets/toolnodemove.py +92 -94
  282. meerk40t/gui/toolwidgets/toolparameter.py +754 -668
  283. meerk40t/gui/toolwidgets/toolplacement.py +108 -108
  284. meerk40t/gui/toolwidgets/toolpoint.py +68 -59
  285. meerk40t/gui/toolwidgets/toolpointlistbuilder.py +294 -0
  286. meerk40t/gui/toolwidgets/toolpointmove.py +183 -0
  287. meerk40t/gui/toolwidgets/toolpolygon.py +288 -403
  288. meerk40t/gui/toolwidgets/toolpolyline.py +38 -196
  289. meerk40t/gui/toolwidgets/toolrect.py +211 -207
  290. meerk40t/gui/toolwidgets/toolrelocate.py +72 -72
  291. meerk40t/gui/toolwidgets/toolribbon.py +598 -113
  292. meerk40t/gui/toolwidgets/tooltabedit.py +546 -0
  293. meerk40t/gui/toolwidgets/tooltext.py +98 -89
  294. meerk40t/gui/toolwidgets/toolvector.py +213 -204
  295. meerk40t/gui/toolwidgets/toolwidget.py +39 -39
  296. meerk40t/gui/usbconnect.py +98 -91
  297. meerk40t/gui/utilitywidgets/buttonwidget.py +18 -18
  298. meerk40t/gui/utilitywidgets/checkboxwidget.py +90 -90
  299. meerk40t/gui/utilitywidgets/controlwidget.py +14 -14
  300. meerk40t/gui/utilitywidgets/cyclocycloidwidget.py +343 -340
  301. meerk40t/gui/utilitywidgets/debugwidgets.py +148 -0
  302. meerk40t/gui/utilitywidgets/handlewidget.py +27 -27
  303. meerk40t/gui/utilitywidgets/harmonograph.py +450 -447
  304. meerk40t/gui/utilitywidgets/openclosewidget.py +40 -40
  305. meerk40t/gui/utilitywidgets/rotationwidget.py +54 -54
  306. meerk40t/gui/utilitywidgets/scalewidget.py +75 -75
  307. meerk40t/gui/utilitywidgets/seekbarwidget.py +183 -183
  308. meerk40t/gui/utilitywidgets/togglewidget.py +142 -142
  309. meerk40t/gui/utilitywidgets/toolbarwidget.py +8 -8
  310. meerk40t/gui/wordlisteditor.py +985 -931
  311. meerk40t/gui/wxmeerk40t.py +1447 -1169
  312. meerk40t/gui/wxmmain.py +5644 -4112
  313. meerk40t/gui/wxmribbon.py +1591 -1076
  314. meerk40t/gui/wxmscene.py +1631 -1453
  315. meerk40t/gui/wxmtree.py +2416 -2089
  316. meerk40t/gui/wxutils.py +1769 -1099
  317. meerk40t/gui/zmatrix.py +102 -102
  318. meerk40t/image/__init__.py +1 -1
  319. meerk40t/image/dither.py +429 -0
  320. meerk40t/image/imagetools.py +2793 -2269
  321. meerk40t/internal_plugins.py +150 -130
  322. meerk40t/kernel/__init__.py +63 -12
  323. meerk40t/kernel/channel.py +259 -212
  324. meerk40t/kernel/context.py +538 -538
  325. meerk40t/kernel/exceptions.py +41 -41
  326. meerk40t/kernel/functions.py +463 -414
  327. meerk40t/kernel/jobs.py +100 -100
  328. meerk40t/kernel/kernel.py +3828 -3571
  329. meerk40t/kernel/lifecycles.py +71 -71
  330. meerk40t/kernel/module.py +49 -49
  331. meerk40t/kernel/service.py +147 -147
  332. meerk40t/kernel/settings.py +383 -343
  333. meerk40t/lihuiyu/controller.py +883 -876
  334. meerk40t/lihuiyu/device.py +1181 -1069
  335. meerk40t/lihuiyu/driver.py +1466 -1372
  336. meerk40t/lihuiyu/gui/gui.py +127 -106
  337. meerk40t/lihuiyu/gui/lhyaccelgui.py +377 -363
  338. meerk40t/lihuiyu/gui/lhycontrollergui.py +741 -651
  339. meerk40t/lihuiyu/gui/lhydrivergui.py +470 -446
  340. meerk40t/lihuiyu/gui/lhyoperationproperties.py +238 -237
  341. meerk40t/lihuiyu/gui/tcpcontroller.py +226 -190
  342. meerk40t/lihuiyu/interpreter.py +53 -53
  343. meerk40t/lihuiyu/laserspeed.py +450 -450
  344. meerk40t/lihuiyu/loader.py +90 -90
  345. meerk40t/lihuiyu/parser.py +404 -404
  346. meerk40t/lihuiyu/plugin.py +101 -102
  347. meerk40t/lihuiyu/tcp_connection.py +111 -109
  348. meerk40t/main.py +231 -165
  349. meerk40t/moshi/builder.py +788 -781
  350. meerk40t/moshi/controller.py +505 -499
  351. meerk40t/moshi/device.py +495 -442
  352. meerk40t/moshi/driver.py +862 -696
  353. meerk40t/moshi/gui/gui.py +78 -76
  354. meerk40t/moshi/gui/moshicontrollergui.py +538 -522
  355. meerk40t/moshi/gui/moshidrivergui.py +87 -75
  356. meerk40t/moshi/plugin.py +43 -43
  357. meerk40t/network/console_server.py +140 -57
  358. meerk40t/network/kernelserver.py +10 -9
  359. meerk40t/network/tcp_server.py +142 -140
  360. meerk40t/network/udp_server.py +103 -77
  361. meerk40t/network/web_server.py +404 -0
  362. meerk40t/newly/controller.py +1158 -1144
  363. meerk40t/newly/device.py +874 -732
  364. meerk40t/newly/driver.py +540 -412
  365. meerk40t/newly/gui/gui.py +219 -188
  366. meerk40t/newly/gui/newlyconfig.py +116 -101
  367. meerk40t/newly/gui/newlycontroller.py +193 -186
  368. meerk40t/newly/gui/operationproperties.py +51 -51
  369. meerk40t/newly/mock_connection.py +82 -82
  370. meerk40t/newly/newly_params.py +56 -56
  371. meerk40t/newly/plugin.py +1214 -1246
  372. meerk40t/newly/usb_connection.py +322 -322
  373. meerk40t/rotary/gui/gui.py +52 -46
  374. meerk40t/rotary/gui/rotarysettings.py +240 -232
  375. meerk40t/rotary/rotary.py +202 -98
  376. meerk40t/ruida/control.py +291 -91
  377. meerk40t/ruida/controller.py +138 -1088
  378. meerk40t/ruida/device.py +676 -231
  379. meerk40t/ruida/driver.py +534 -472
  380. meerk40t/ruida/emulator.py +1494 -1491
  381. meerk40t/ruida/exceptions.py +4 -4
  382. meerk40t/ruida/gui/gui.py +71 -76
  383. meerk40t/ruida/gui/ruidaconfig.py +239 -72
  384. meerk40t/ruida/gui/ruidacontroller.py +187 -184
  385. meerk40t/ruida/gui/ruidaoperationproperties.py +48 -47
  386. meerk40t/ruida/loader.py +54 -52
  387. meerk40t/ruida/mock_connection.py +57 -109
  388. meerk40t/ruida/plugin.py +124 -87
  389. meerk40t/ruida/rdjob.py +2084 -945
  390. meerk40t/ruida/serial_connection.py +116 -0
  391. meerk40t/ruida/tcp_connection.py +146 -0
  392. meerk40t/ruida/udp_connection.py +73 -0
  393. meerk40t/svgelements.py +9671 -9669
  394. meerk40t/tools/driver_to_path.py +584 -579
  395. meerk40t/tools/geomstr.py +5583 -4680
  396. meerk40t/tools/jhfparser.py +357 -292
  397. meerk40t/tools/kerftest.py +904 -890
  398. meerk40t/tools/livinghinges.py +1168 -1033
  399. meerk40t/tools/pathtools.py +987 -949
  400. meerk40t/tools/pmatrix.py +234 -0
  401. meerk40t/tools/pointfinder.py +942 -942
  402. meerk40t/tools/polybool.py +941 -940
  403. meerk40t/tools/rasterplotter.py +1660 -547
  404. meerk40t/tools/shxparser.py +1047 -901
  405. meerk40t/tools/ttfparser.py +726 -446
  406. meerk40t/tools/zinglplotter.py +595 -593
  407. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7020.dist-info}/LICENSE +21 -21
  408. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7020.dist-info}/METADATA +150 -139
  409. meerk40t-0.9.7020.dist-info/RECORD +446 -0
  410. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7020.dist-info}/WHEEL +1 -1
  411. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7020.dist-info}/top_level.txt +0 -1
  412. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7020.dist-info}/zip-safe +1 -1
  413. meerk40t/balormk/elementlightjob.py +0 -159
  414. meerk40t-0.9.3001.dist-info/RECORD +0 -437
  415. test/bootstrap.py +0 -63
  416. test/test_cli.py +0 -12
  417. test/test_core_cutcode.py +0 -418
  418. test/test_core_elements.py +0 -144
  419. test/test_core_plotplanner.py +0 -397
  420. test/test_core_viewports.py +0 -312
  421. test/test_drivers_grbl.py +0 -108
  422. test/test_drivers_lihuiyu.py +0 -443
  423. test/test_drivers_newly.py +0 -113
  424. test/test_element_degenerate_points.py +0 -43
  425. test/test_elements_classify.py +0 -97
  426. test/test_elements_penbox.py +0 -22
  427. test/test_file_svg.py +0 -176
  428. test/test_fill.py +0 -155
  429. test/test_geomstr.py +0 -1523
  430. test/test_geomstr_nodes.py +0 -18
  431. test/test_imagetools_actualize.py +0 -306
  432. test/test_imagetools_wizard.py +0 -258
  433. test/test_kernel.py +0 -200
  434. test/test_laser_speeds.py +0 -3303
  435. test/test_length.py +0 -57
  436. test/test_lifecycle.py +0 -66
  437. test/test_operations.py +0 -251
  438. test/test_operations_hatch.py +0 -57
  439. test/test_ruida.py +0 -19
  440. test/test_spooler.py +0 -22
  441. test/test_tools_rasterplotter.py +0 -29
  442. test/test_wobble.py +0 -133
  443. test/test_zingl.py +0 -124
  444. {test → meerk40t/cylinder}/__init__.py +0 -0
  445. /meerk40t/{core/element_commands.py → cylinder/gui/__init__.py} +0 -0
  446. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7020.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))