ChangeAtZ.py 56 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474
  1. # ChangeAtZ script - Change printing parameters at a given height
  2. # This script is the successor of the TweakAtZ plugin for legacy Cura.
  3. # It contains code from the TweakAtZ plugin V1.0-V4.x and from the ExampleScript by Jaime van Kessel, Ultimaker B.V.
  4. # It runs with the PostProcessingPlugin which is released under the terms of the AGPLv3 or higher.
  5. # This script is licensed under the Creative Commons - Attribution - Share Alike (CC BY-SA) terms
  6. # Authors of the ChangeAtZ plugin / script:
  7. # Written by Steven Morlock, smorloc@gmail.com
  8. # Modified by Ricardo Gomez, ricardoga@otulook.com, to add Bed Temperature and make it work with Cura_13.06.04+
  9. # Modified by Stefan Heule, Dim3nsioneer@gmx.ch since V3.0 (see changelog below)
  10. # Modified by Jaime van Kessel (Ultimaker), j.vankessel@ultimaker.com to make it work for 15.10 / 2.x
  11. # Modified by Ghostkeeper (Ultimaker), rubend@tutanota.com, to debug.
  12. # Modified by Wes Hanney, https://github.com/novamxd, Retract Length + Speed, Clean up
  13. # Modified by Alex Jaxon, https://github.com/legend069, Added option to modify Build Volume Temperature
  14. # history / changelog:
  15. # V3.0.1: TweakAtZ-state default 1 (i.e. the plugin works without any TweakAtZ comment)
  16. # V3.1: Recognizes UltiGCode and deactivates value reset, fan speed added, alternatively layer no. to tweak at,
  17. # extruder three temperature disabled by "#Ex3"
  18. # V3.1.1: Bugfix reset flow rate
  19. # V3.1.2: Bugfix disable TweakAtZ on Cool Head Lift
  20. # V3.2: Flow rate for specific extruder added (only for 2 extruders), bugfix parser,
  21. # added speed reset at the end of the print
  22. # V4.0: Progress bar, tweaking over multiple layers, M605&M606 implemented, reset after one layer option,
  23. # extruder three code removed, tweaking print speed, save call of Publisher class,
  24. # uses previous value from other plugins also on UltiGCode
  25. # V4.0.1: Bugfix for doubled G1 commands
  26. # V4.0.2: Uses Cura progress bar instead of its own
  27. # V4.0.3: Bugfix for cool head lift (contributed by luisonoff)
  28. # V4.9.91: First version for Cura 15.06.x and PostProcessingPlugin
  29. # V4.9.92: Modifications for Cura 15.10
  30. # V4.9.93: Minor bugfixes (input settings) / documentation
  31. # V4.9.94: Bugfix Combobox-selection; remove logger
  32. # V5.0: Bugfix for fall back after one layer and doubled G0 commands when using print speed tweak, Initial version for Cura 2.x
  33. # V5.0.1: Bugfix for calling unknown property 'bedTemp' of previous settings storage and unknown variable 'speed'
  34. # V5.1: API Changes included for use with Cura 2.2
  35. # V5.2.0: Wes Hanney. Added support for changing Retract Length and Speed. Removed layer spread option. Fixed issue of cumulative ChangeAtZ
  36. # mods so they can now properly be stacked on top of each other. Applied code refactoring to clean up various coding styles. Added comments.
  37. # Broke up functions for clarity. Split up class so it can be debugged outside of Cura.
  38. # V5.2.1: Wes Hanney. Added support for firmware based retractions. Fixed issue of properly restoring previous values in single layer option.
  39. # Added support for outputting changes to LCD (untested). Added type hints to most functions and variables. Added more comments. Created GCodeCommand
  40. # class for better detection of G1 vs G10 or G11 commands, and accessing arguments. Moved most GCode methods to GCodeCommand class. Improved wording
  41. # of Single Layer vs Keep Layer to better reflect what was happening.
  42. # V5.3.0 Alex Jaxon, Added option to modify Build Volume Temperature keeping current format
  43. #
  44. # Uses -
  45. # M220 S<factor in percent> - set speed factor override percentage
  46. # M221 S<factor in percent> - set flow factor override percentage
  47. # M221 S<factor in percent> T<0-#toolheads> - set flow factor override percentage for single extruder
  48. # M104 S<temp> T<0-#toolheads> - set extruder <T> to target temperature <S>
  49. # M140 S<temp> - set bed target temperature
  50. # M106 S<PWM> - set fan speed to target speed <S>
  51. # M207 S<mm> F<mm/m> - set the retract length <S> or feed rate <F>
  52. # M117 - output the current changes
  53. from typing import List, Dict
  54. from ..Script import Script
  55. import re
  56. # this was broken up into a separate class so the main ChangeAtZ script could be debugged outside of Cura
  57. class ChangeAtZ(Script):
  58. version = "5.3.0"
  59. def getSettingDataString(self):
  60. return """{
  61. "name": "ChangeAtZ """ + self.version + """(Experimental)",
  62. "key": "ChangeAtZ",
  63. "metadata": {},
  64. "version": 2,
  65. "settings": {
  66. "caz_enabled": {
  67. "label": "Enabled",
  68. "description": "Allows adding multiple ChangeAtZ mods and disabling them as needed.",
  69. "type": "bool",
  70. "default_value": true
  71. },
  72. "a_trigger": {
  73. "label": "Trigger",
  74. "description": "Trigger at height or at layer no.",
  75. "type": "enum",
  76. "options": {
  77. "height": "Height",
  78. "layer_no": "Layer No."
  79. },
  80. "default_value": "height"
  81. },
  82. "b_targetZ": {
  83. "label": "Change Height",
  84. "description": "Z height to change at",
  85. "unit": "mm",
  86. "type": "float",
  87. "default_value": 5.0,
  88. "minimum_value": "0",
  89. "minimum_value_warning": "0.1",
  90. "maximum_value_warning": "230",
  91. "enabled": "a_trigger == 'height'"
  92. },
  93. "b_targetL": {
  94. "label": "Change Layer",
  95. "description": "Layer no. to change at",
  96. "unit": "",
  97. "type": "int",
  98. "default_value": 1,
  99. "minimum_value": "-100",
  100. "minimum_value_warning": "-1",
  101. "enabled": "a_trigger == 'layer_no'"
  102. },
  103. "c_behavior": {
  104. "label": "Apply To",
  105. "description": "Target Layer + Subsequent Layers is good for testing changes between ranges of layers, ex: Layer 0 to 10 or 0mm to 5mm. Single layer is good for testing changes at a single layer, ex: at Layer 10 or 5mm only.",
  106. "type": "enum",
  107. "options": {
  108. "keep_value": "Target Layer + Subsequent Layers",
  109. "single_layer": "Target Layer Only"
  110. },
  111. "default_value": "keep_value"
  112. },
  113. "caz_output_to_display": {
  114. "label": "Output to Display",
  115. "description": "Displays the current changes to the LCD",
  116. "type": "bool",
  117. "default_value": false
  118. },
  119. "e1_Change_speed": {
  120. "label": "Change Speed",
  121. "description": "Select if total speed (print and travel) has to be changed",
  122. "type": "bool",
  123. "default_value": false
  124. },
  125. "e2_speed": {
  126. "label": "Speed",
  127. "description": "New total speed (print and travel)",
  128. "unit": "%",
  129. "type": "int",
  130. "default_value": 100,
  131. "minimum_value": "1",
  132. "minimum_value_warning": "10",
  133. "maximum_value_warning": "200",
  134. "enabled": "e1_Change_speed"
  135. },
  136. "f1_Change_printspeed": {
  137. "label": "Change Print Speed",
  138. "description": "Select if print speed has to be changed",
  139. "type": "bool",
  140. "default_value": false
  141. },
  142. "f2_printspeed": {
  143. "label": "Print Speed",
  144. "description": "New print speed",
  145. "unit": "%",
  146. "type": "int",
  147. "default_value": 100,
  148. "minimum_value": "1",
  149. "minimum_value_warning": "10",
  150. "maximum_value_warning": "200",
  151. "enabled": "f1_Change_printspeed"
  152. },
  153. "g1_Change_flowrate": {
  154. "label": "Change Flow Rate",
  155. "description": "Select if flow rate has to be changed",
  156. "type": "bool",
  157. "default_value": false
  158. },
  159. "g2_flowrate": {
  160. "label": "Flow Rate",
  161. "description": "New Flow rate",
  162. "unit": "%",
  163. "type": "int",
  164. "default_value": 100,
  165. "minimum_value": "1",
  166. "minimum_value_warning": "10",
  167. "maximum_value_warning": "200",
  168. "enabled": "g1_Change_flowrate"
  169. },
  170. "g3_Change_flowrateOne": {
  171. "label": "Change Flow Rate 1",
  172. "description": "Select if first extruder flow rate has to be changed",
  173. "type": "bool",
  174. "default_value": false
  175. },
  176. "g4_flowrateOne": {
  177. "label": "Flow Rate One",
  178. "description": "New Flow rate Extruder 1",
  179. "unit": "%",
  180. "type": "int",
  181. "default_value": 100,
  182. "minimum_value": "1",
  183. "minimum_value_warning": "10",
  184. "maximum_value_warning": "200",
  185. "enabled": "g3_Change_flowrateOne"
  186. },
  187. "g5_Change_flowrateTwo": {
  188. "label": "Change Flow Rate 2",
  189. "description": "Select if second extruder flow rate has to be changed",
  190. "type": "bool",
  191. "default_value": false
  192. },
  193. "g6_flowrateTwo": {
  194. "label": "Flow Rate two",
  195. "description": "New Flow rate Extruder 2",
  196. "unit": "%",
  197. "type": "int",
  198. "default_value": 100,
  199. "minimum_value": "1",
  200. "minimum_value_warning": "10",
  201. "maximum_value_warning": "200",
  202. "enabled": "g5_Change_flowrateTwo"
  203. },
  204. "h1_Change_bedTemp": {
  205. "label": "Change Bed Temp",
  206. "description": "Select if Bed Temperature has to be changed",
  207. "type": "bool",
  208. "default_value": false
  209. },
  210. "h2_bedTemp": {
  211. "label": "Bed Temp",
  212. "description": "New Bed Temperature",
  213. "unit": "C",
  214. "type": "float",
  215. "default_value": 60,
  216. "minimum_value": "0",
  217. "minimum_value_warning": "30",
  218. "maximum_value_warning": "120",
  219. "enabled": "h1_Change_bedTemp"
  220. },
  221. "h1_Change_buildVolumeTemperature": {
  222. "label": "Change Build Volume Temperature",
  223. "description": "Select if Build Volume Temperature has to be changed",
  224. "type": "bool",
  225. "default_value": false
  226. },
  227. "h2_buildVolumeTemperature": {
  228. "label": "Build Volume Temperature",
  229. "description": "New Build Volume Temperature",
  230. "unit": "C",
  231. "type": "float",
  232. "default_value": 20,
  233. "minimum_value": "0",
  234. "minimum_value_warning": "10",
  235. "maximum_value_warning": "50",
  236. "enabled": "h1_Change_buildVolumeTemperature"
  237. },
  238. "i1_Change_extruderOne": {
  239. "label": "Change Extruder 1 Temp",
  240. "description": "Select if First Extruder Temperature has to be changed",
  241. "type": "bool",
  242. "default_value": false
  243. },
  244. "i2_extruderOne": {
  245. "label": "Extruder 1 Temp",
  246. "description": "New First Extruder Temperature",
  247. "unit": "C",
  248. "type": "float",
  249. "default_value": 190,
  250. "minimum_value": "0",
  251. "minimum_value_warning": "160",
  252. "maximum_value_warning": "250",
  253. "enabled": "i1_Change_extruderOne"
  254. },
  255. "i3_Change_extruderTwo": {
  256. "label": "Change Extruder 2 Temp",
  257. "description": "Select if Second Extruder Temperature has to be changed",
  258. "type": "bool",
  259. "default_value": false
  260. },
  261. "i4_extruderTwo": {
  262. "label": "Extruder 2 Temp",
  263. "description": "New Second Extruder Temperature",
  264. "unit": "C",
  265. "type": "float",
  266. "default_value": 190,
  267. "minimum_value": "0",
  268. "minimum_value_warning": "160",
  269. "maximum_value_warning": "250",
  270. "enabled": "i3_Change_extruderTwo"
  271. },
  272. "j1_Change_fanSpeed": {
  273. "label": "Change Fan Speed",
  274. "description": "Select if Fan Speed has to be changed",
  275. "type": "bool",
  276. "default_value": false
  277. },
  278. "j2_fanSpeed": {
  279. "label": "Fan Speed",
  280. "description": "New Fan Speed (0-100)",
  281. "unit": "%",
  282. "type": "int",
  283. "default_value": 100,
  284. "minimum_value": "0",
  285. "minimum_value_warning": "0",
  286. "maximum_value_warning": "100",
  287. "enabled": "j1_Change_fanSpeed"
  288. },
  289. "caz_change_retract": {
  290. "label": "Change Retraction",
  291. "description": "Indicates you would like to modify retraction properties.",
  292. "type": "bool",
  293. "default_value": false
  294. },
  295. "caz_retractstyle": {
  296. "label": "Retract Style",
  297. "description": "Specify if you're using firmware retraction or linear move based retractions. Check your printer settings to see which you're using.",
  298. "type": "enum",
  299. "options": {
  300. "linear": "Linear Move",
  301. "firmware": "Firmware"
  302. },
  303. "default_value": "linear",
  304. "enabled": "caz_change_retract"
  305. },
  306. "caz_change_retractfeedrate": {
  307. "label": "Change Retract Feed Rate",
  308. "description": "Changes the retraction feed rate during print",
  309. "type": "bool",
  310. "default_value": false,
  311. "enabled": "caz_change_retract"
  312. },
  313. "caz_retractfeedrate": {
  314. "label": "Retract Feed Rate",
  315. "description": "New Retract Feed Rate (mm/s)",
  316. "unit": "mm/s",
  317. "type": "float",
  318. "default_value": 40,
  319. "minimum_value": "0",
  320. "minimum_value_warning": "0",
  321. "maximum_value_warning": "100",
  322. "enabled": "caz_change_retractfeedrate"
  323. },
  324. "caz_change_retractlength": {
  325. "label": "Change Retract Length",
  326. "description": "Changes the retraction length during print",
  327. "type": "bool",
  328. "default_value": false,
  329. "enabled": "caz_change_retract"
  330. },
  331. "caz_retractlength": {
  332. "label": "Retract Length",
  333. "description": "New Retract Length (mm)",
  334. "unit": "mm",
  335. "type": "float",
  336. "default_value": 6,
  337. "minimum_value": "0",
  338. "minimum_value_warning": "0",
  339. "maximum_value_warning": "20",
  340. "enabled": "caz_change_retractlength"
  341. }
  342. }
  343. }"""
  344. def __init__(self):
  345. super().__init__()
  346. def execute(self, data):
  347. caz_instance = ChangeAtZProcessor()
  348. caz_instance.targetValues = {}
  349. # copy over our settings to our change z class
  350. self.setIntSettingIfEnabled(caz_instance, "e1_Change_speed", "speed", "e2_speed")
  351. self.setIntSettingIfEnabled(caz_instance, "f1_Change_printspeed", "printspeed", "f2_printspeed")
  352. self.setIntSettingIfEnabled(caz_instance, "g1_Change_flowrate", "flowrate", "g2_flowrate")
  353. self.setIntSettingIfEnabled(caz_instance, "g3_Change_flowrateOne", "flowrateOne", "g4_flowrateOne")
  354. self.setIntSettingIfEnabled(caz_instance, "g5_Change_flowrateTwo", "flowrateTwo", "g6_flowrateTwo")
  355. self.setFloatSettingIfEnabled(caz_instance, "h1_Change_bedTemp", "bedTemp", "h2_bedTemp")
  356. self.setFloatSettingIfEnabled(caz_instance, "h1_Change_buildVolumeTemperature", "buildVolumeTemperature", "h2_buildVolumeTemperature")
  357. self.setFloatSettingIfEnabled(caz_instance, "i1_Change_extruderOne", "extruderOne", "i2_extruderOne")
  358. self.setFloatSettingIfEnabled(caz_instance, "i3_Change_extruderTwo", "extruderTwo", "i4_extruderTwo")
  359. self.setIntSettingIfEnabled(caz_instance, "j1_Change_fanSpeed", "fanSpeed", "j2_fanSpeed")
  360. self.setFloatSettingIfEnabled(caz_instance, "caz_change_retractfeedrate", "retractfeedrate", "caz_retractfeedrate")
  361. self.setFloatSettingIfEnabled(caz_instance, "caz_change_retractlength", "retractlength", "caz_retractlength")
  362. # is this mod enabled?
  363. caz_instance.enabled = self.getSettingValueByKey("caz_enabled")
  364. # are we emitting data to the LCD?
  365. caz_instance.displayChangesToLcd = self.getSettingValueByKey("caz_output_to_display")
  366. # are we doing linear move retractions?
  367. caz_instance.linearRetraction = self.getSettingValueByKey("caz_retractstyle") == "linear"
  368. # see if we're applying to a single layer or to all layers hence forth
  369. caz_instance.applyToSingleLayer = self.getSettingValueByKey("c_behavior") == "single_layer"
  370. # used for easy reference of layer or height targeting
  371. caz_instance.targetByLayer = self.getSettingValueByKey("a_trigger") == "layer_no"
  372. # change our target based on what we're targeting
  373. caz_instance.targetLayer = self.getIntSettingByKey("b_targetL", None)
  374. caz_instance.targetZ = self.getFloatSettingByKey("b_targetZ", None)
  375. # run our script
  376. return caz_instance.execute(data)
  377. # Sets the given TargetValue in the ChangeAtZ instance if the trigger is specified
  378. def setIntSettingIfEnabled(self, caz_instance, trigger, target, setting):
  379. # stop here if our trigger isn't enabled
  380. if not self.getSettingValueByKey(trigger):
  381. return
  382. # get our value from the settings
  383. value = self.getIntSettingByKey(setting, None)
  384. # skip if there's no value or we can't interpret it
  385. if value is None:
  386. return
  387. # set our value in the target settings
  388. caz_instance.targetValues[target] = value
  389. # Sets the given TargetValue in the ChangeAtZ instance if the trigger is specified
  390. def setFloatSettingIfEnabled(self, caz_instance, trigger, target, setting):
  391. # stop here if our trigger isn't enabled
  392. if not self.getSettingValueByKey(trigger):
  393. return
  394. # get our value from the settings
  395. value = self.getFloatSettingByKey(setting, None)
  396. # skip if there's no value or we can't interpret it
  397. if value is None:
  398. return
  399. # set our value in the target settings
  400. caz_instance.targetValues[target] = value
  401. # Returns the given settings value as an integer or the default if it cannot parse it
  402. def getIntSettingByKey(self, key, default):
  403. # change our target based on what we're targeting
  404. try:
  405. return int(self.getSettingValueByKey(key))
  406. except:
  407. return default
  408. # Returns the given settings value as an integer or the default if it cannot parse it
  409. def getFloatSettingByKey(self, key, default):
  410. # change our target based on what we're targeting
  411. try:
  412. return float(self.getSettingValueByKey(key))
  413. except:
  414. return default
  415. # This is a utility class for getting details of gcodes from a given line
  416. class GCodeCommand:
  417. # The GCode command itself (ex: G10)
  418. command = None,
  419. # Contains any arguments passed to the command. The key is the argument name, the value is the value of the argument.
  420. arguments = {}
  421. # Contains the components of the command broken into pieces
  422. components = []
  423. # Constructor. Sets up defaults
  424. def __init__(self):
  425. self.reset()
  426. # Gets a GCode Command from the given single line of GCode
  427. @staticmethod
  428. def getFromLine(line: str):
  429. # obviously if we don't have a command, we can't return anything
  430. if line is None or len(line) == 0:
  431. return None
  432. # we only support G or M commands
  433. if line[0] != "G" and line[0] != "M":
  434. return None
  435. # remove any comments
  436. line = re.sub(r";.*$", "", line)
  437. # break into the individual components
  438. command_pieces = line.strip().split(" ")
  439. # our return command details
  440. command = GCodeCommand()
  441. # stop here if we don't even have something to interpret
  442. if len(command_pieces) == 0:
  443. return None
  444. # stores all the components of the command within the class for later
  445. command.components = command_pieces
  446. # set the actual command
  447. command.command = command_pieces[0]
  448. # stop here if we don't have any parameters
  449. if len(command_pieces) == 1:
  450. return None
  451. # return our indexed command
  452. return command
  453. # Handy function for reading a linear move command
  454. @staticmethod
  455. def getLinearMoveCommand(line: str):
  456. # get our command from the line
  457. linear_command = GCodeCommand.getFromLine(line)
  458. # if it's not a linear move, we don't care
  459. if linear_command is None or (linear_command.command != "G0" and linear_command.command != "G1"):
  460. return None
  461. # convert our values to floats (or defaults)
  462. linear_command.arguments["F"] = linear_command.getArgumentAsFloat("F", None)
  463. linear_command.arguments["X"] = linear_command.getArgumentAsFloat("X", None)
  464. linear_command.arguments["Y"] = linear_command.getArgumentAsFloat("Y", None)
  465. linear_command.arguments["Z"] = linear_command.getArgumentAsFloat("Z", None)
  466. linear_command.arguments["E"] = linear_command.getArgumentAsFloat("E", None)
  467. # return our new command
  468. return linear_command
  469. # Gets the value of a parameter or returns the default if there is none
  470. def getArgument(self, name: str, default: str = None) -> str:
  471. # parse our arguments (only happens once)
  472. self.parseArguments()
  473. # if we don't have the parameter, return the default
  474. if name not in self.arguments:
  475. return default
  476. # otherwise return the value
  477. return self.arguments[name]
  478. # Gets the value of a parameter as a float or returns the default
  479. def getArgumentAsFloat(self, name: str, default: float = None) -> float:
  480. # try to parse as a float, otherwise return the default
  481. try:
  482. return float(self.getArgument(name, default))
  483. except:
  484. return default
  485. # Gets the value of a parameter as an integer or returns the default
  486. def getArgumentAsInt(self, name: str, default: int = None) -> int:
  487. # try to parse as a integer, otherwise return the default
  488. try:
  489. return int(self.getArgument(name, default))
  490. except:
  491. return default
  492. # Allows retrieving values from the given GCODE line
  493. @staticmethod
  494. def getDirectArgument(line: str, key: str, default: str = None) -> str:
  495. if key not in line or (";" in line and line.find(key) > line.find(";") and ";ChangeAtZ" not in key and ";LAYER:" not in key):
  496. return default
  497. # allows for string lengths larger than 1
  498. sub_part = line[line.find(key) + len(key):]
  499. if ";ChangeAtZ" in key:
  500. m = re.search("^[0-4]", sub_part)
  501. elif ";LAYER:" in key:
  502. m = re.search("^[+-]?[0-9]*", sub_part)
  503. else:
  504. # the minus at the beginning allows for negative values, e.g. for delta printers
  505. m = re.search(r"^[-]?[0-9]*\.?[0-9]*", sub_part)
  506. if m is None:
  507. return default
  508. try:
  509. return m.group(0)
  510. except:
  511. return default
  512. # Converts the command parameter to a int or returns the default
  513. @staticmethod
  514. def getDirectArgumentAsFloat(line: str, key: str, default: float = None) -> float:
  515. # get the value from the command
  516. value = GCodeCommand.getDirectArgument(line, key, default)
  517. # stop here if it's the default
  518. if value == default:
  519. return value
  520. try:
  521. return float(value)
  522. except:
  523. return default
  524. # Converts the command parameter to a int or returns the default
  525. @staticmethod
  526. def getDirectArgumentAsInt(line: str, key: str, default: int = None) -> int:
  527. # get the value from the command
  528. value = GCodeCommand.getDirectArgument(line, key, default)
  529. # stop here if it's the default
  530. if value == default:
  531. return value
  532. try:
  533. return int(value)
  534. except:
  535. return default
  536. # Parses the arguments of the command on demand, only once
  537. def parseArguments(self):
  538. # stop here if we don't have any remaining components
  539. if len(self.components) <= 1:
  540. return None
  541. # iterate and index all of our parameters, skip the first component as it's the command
  542. for i in range(1, len(self.components)):
  543. # get our component
  544. component = self.components[i]
  545. # get the first character of the parameter, which is the name
  546. component_name = component[0]
  547. # get the value of the parameter (the rest of the string
  548. component_value = None
  549. # get our value if we have one
  550. if len(component) > 1:
  551. component_value = component[1:]
  552. # index the argument
  553. self.arguments[component_name] = component_value
  554. # clear the components to we don't process again
  555. self.components = []
  556. # Easy function for replacing any GCODE parameter variable in a given GCODE command
  557. @staticmethod
  558. def replaceDirectArgument(line: str, key: str, value: str) -> str:
  559. return re.sub(r"(^|\s)" + key + r"[\d\.]+(\s|$)", r"\1" + key + str(value) + r"\2", line)
  560. # Resets the model back to defaults
  561. def reset(self):
  562. self.command = None
  563. self.arguments = {}
  564. # The primary ChangeAtZ class that does all the gcode editing. This was broken out into an
  565. # independent class so it could be debugged using a standard IDE
  566. class ChangeAtZProcessor:
  567. # Holds our current height
  568. currentZ = None
  569. # Holds our current layer number
  570. currentLayer = None
  571. # Indicates if we're only supposed to apply our settings to a single layer or multiple layers
  572. applyToSingleLayer = False
  573. # Indicates if this should emit the changes as they happen to the LCD
  574. displayChangesToLcd = False
  575. # Indicates that this mod is still enabled (or not)
  576. enabled = True
  577. # Indicates if we're processing inside the target layer or not
  578. insideTargetLayer = False
  579. # Indicates if we have restored the previous values from before we started our pass
  580. lastValuesRestored = False
  581. # Indicates if the user has opted for linear move retractions or firmware retractions
  582. linearRetraction = True
  583. # Indicates if we're targeting by layer or height value
  584. targetByLayer = True
  585. # Indicates if we have injected our changed values for the given layer yet
  586. targetValuesInjected = False
  587. # Holds the last extrusion value, used with detecting when a retraction is made
  588. lastE = None
  589. # An index of our gcodes which we're monitoring
  590. lastValues = {}
  591. # The detected layer height from the gcode
  592. layerHeight = None
  593. # The target layer
  594. targetLayer = None
  595. # Holds the values the user has requested to change
  596. targetValues = {}
  597. # The target height in mm
  598. targetZ = None
  599. # Used to track if we've been inside our target layer yet
  600. wasInsideTargetLayer = False
  601. # boots up the class with defaults
  602. def __init__(self):
  603. self.reset()
  604. # Modifies the given GCODE and injects the commands at the various targets
  605. def execute(self, data):
  606. # short cut the whole thing if we're not enabled
  607. if not self.enabled:
  608. return data
  609. # our layer cursor
  610. index = 0
  611. for active_layer in data:
  612. # will hold our updated gcode
  613. modified_gcode = ""
  614. # mark all the defaults for deletion
  615. active_layer = self.markChangesForDeletion(active_layer)
  616. # break apart the layer into commands
  617. lines = active_layer.split("\n")
  618. # evaluate each command individually
  619. for line in lines:
  620. # trim or command
  621. line = line.strip()
  622. # skip empty lines
  623. if len(line) == 0:
  624. continue
  625. # update our layer number if applicable
  626. self.processLayerNumber(line)
  627. # update our layer height if applicable
  628. self.processLayerHeight(line)
  629. # check if we're at the target layer or not
  630. self.processTargetLayer()
  631. # process any changes to the gcode
  632. modified_gcode += self.processLine(line)
  633. # remove any marked defaults
  634. modified_gcode = self.removeMarkedChanges(modified_gcode)
  635. # append our modified line
  636. data[index] = modified_gcode
  637. index += 1
  638. # return our modified gcode
  639. return data
  640. # Builds the restored layer settings based on the previous settings and returns the relevant GCODE lines
  641. def getChangedLastValues(self) -> Dict[str, any]:
  642. # capture the values that we've changed
  643. changed = {}
  644. # for each of our target values, get the value to restore
  645. # no point in restoring values we haven't changed
  646. for key in self.targetValues:
  647. # skip target values we can't restore
  648. if key not in self.lastValues:
  649. continue
  650. # save into our changed
  651. changed[key] = self.lastValues[key]
  652. # return our collection of changed values
  653. return changed
  654. # Builds the relevant display feedback for each of the values
  655. def getDisplayChangesFromValues(self, values: Dict[str, any]) -> str:
  656. # stop here if we're not outputting data
  657. if not self.displayChangesToLcd:
  658. return ""
  659. # will hold all the default settings for the target layer
  660. codes = []
  661. # looking for wait for bed temp
  662. if "bedTemp" in values:
  663. codes.append("BedTemp: " + str(round(values["bedTemp"])))
  664. # looking for wait for Build Volume Temperature
  665. if "buildVolumeTemperature" in values:
  666. codes.append("buildVolumeTemperature: " + str(round(values["buildVolumeTemperature"])))
  667. # set our extruder one temp (if specified)
  668. if "extruderOne" in values:
  669. codes.append("Extruder 1 Temp: " + str(round(values["extruderOne"])))
  670. # set our extruder two temp (if specified)
  671. if "extruderTwo" in values:
  672. codes.append("Extruder 2 Temp: " + str(round(values["extruderTwo"])))
  673. # set global flow rate
  674. if "flowrate" in values:
  675. codes.append("Extruder A Flow Rate: " + str(values["flowrate"]))
  676. # set extruder 0 flow rate
  677. if "flowrateOne" in values:
  678. codes.append("Extruder 1 Flow Rate: " + str(values["flowrateOne"]))
  679. # set second extruder flow rate
  680. if "flowrateTwo" in values:
  681. codes.append("Extruder 2 Flow Rate: " + str(values["flowrateTwo"]))
  682. # set our fan speed
  683. if "fanSpeed" in values:
  684. codes.append("Fan Speed: " + str(values["fanSpeed"]))
  685. # set feedrate percentage
  686. if "speed" in values:
  687. codes.append("Print Speed: " + str(values["speed"]))
  688. # set print rate percentage
  689. if "printspeed" in values:
  690. codes.append("Linear Print Speed: " + str(values["printspeed"]))
  691. # set retract rate
  692. if "retractfeedrate" in values:
  693. codes.append("Retract Feed Rate: " + str(values["retractfeedrate"]))
  694. # set retract length
  695. if "retractlength" in values:
  696. codes.append("Retract Length: " + str(values["retractlength"]))
  697. # stop here if there's nothing to output
  698. if len(codes) == 0:
  699. return ""
  700. # output our command to display the data
  701. return "M117 " + ", ".join(codes) + "\n"
  702. # Converts the last values to something that can be output on the LCD
  703. def getLastDisplayValues(self) -> str:
  704. # convert our last values to something we can output
  705. return self.getDisplayChangesFromValues(self.getChangedLastValues())
  706. # Converts the target values to something that can be output on the LCD
  707. def getTargetDisplayValues(self) -> str:
  708. # convert our target values to something we can output
  709. return self.getDisplayChangesFromValues(self.targetValues)
  710. # Builds the the relevant GCODE lines from the given collection of values
  711. def getCodeFromValues(self, values: Dict[str, any]) -> str:
  712. # will hold all the desired settings for the target layer
  713. codes = self.getCodeLinesFromValues(values)
  714. # stop here if there are no values that require changing
  715. if len(codes) == 0:
  716. return ""
  717. # return our default block for this layer
  718. return ";[CAZD:\n" + "\n".join(codes) + "\n;:CAZD]"
  719. # Builds the relevant GCODE lines from the given collection of values
  720. def getCodeLinesFromValues(self, values: Dict[str, any]) -> List[str]:
  721. # will hold all the default settings for the target layer
  722. codes = []
  723. # looking for wait for bed temp
  724. if "bedTemp" in values:
  725. codes.append("M140 S" + str(values["bedTemp"]))
  726. # looking for wait for Build Volume Temperature
  727. if "buildVolumeTemperature" in values:
  728. codes.append("M141 S" + str(values["buildVolumeTemperature"]))
  729. # set our extruder one temp (if specified)
  730. if "extruderOne" in values:
  731. codes.append("M104 S" + str(values["extruderOne"]) + " T0")
  732. # set our extruder two temp (if specified)
  733. if "extruderTwo" in values:
  734. codes.append("M104 S" + str(values["extruderTwo"]) + " T1")
  735. # set our fan speed
  736. if "fanSpeed" in values:
  737. # convert our fan speed percentage to PWM
  738. fan_speed = int((float(values["fanSpeed"]) / 100.0) * 255)
  739. # add our fan speed to the defaults
  740. codes.append("M106 S" + str(fan_speed))
  741. # set global flow rate
  742. if "flowrate" in values:
  743. codes.append("M221 S" + str(values["flowrate"]))
  744. # set extruder 0 flow rate
  745. if "flowrateOne" in values:
  746. codes.append("M221 S" + str(values["flowrateOne"]) + " T0")
  747. # set second extruder flow rate
  748. if "flowrateTwo" in values:
  749. codes.append("M221 S" + str(values["flowrateTwo"]) + " T1")
  750. # set feedrate percentage
  751. if "speed" in values:
  752. codes.append("M220 S" + str(values["speed"]) + "")
  753. # set print rate percentage
  754. if "printspeed" in values:
  755. codes.append(";PRINTSPEED " + str(values["printspeed"]) + "")
  756. # set retract rate
  757. if "retractfeedrate" in values:
  758. if self.linearRetraction:
  759. codes.append(";RETRACTFEEDRATE " + str(values["retractfeedrate"] * 60) + "")
  760. else:
  761. codes.append("M207 F" + str(values["retractfeedrate"] * 60) + "")
  762. # set retract length
  763. if "retractlength" in values:
  764. if self.linearRetraction:
  765. codes.append(";RETRACTLENGTH " + str(values["retractlength"]) + "")
  766. else:
  767. codes.append("M207 S" + str(values["retractlength"]) + "")
  768. return codes
  769. # Builds the restored layer settings based on the previous settings and returns the relevant GCODE lines
  770. def getLastValues(self) -> str:
  771. # build the gcode to restore our last values
  772. return self.getCodeFromValues(self.getChangedLastValues())
  773. # Builds the gcode to inject either the changed values we want or restore the previous values
  774. def getInjectCode(self) -> str:
  775. # if we're now outside of our target layer and haven't restored our last values, do so now
  776. if not self.insideTargetLayer and self.wasInsideTargetLayer and not self.lastValuesRestored:
  777. # mark that we've injected the last values
  778. self.lastValuesRestored = True
  779. # inject the defaults
  780. return self.getLastValues() + "\n" + self.getLastDisplayValues()
  781. # if we're inside our target layer but haven't added our values yet, do so now
  782. if self.insideTargetLayer and not self.targetValuesInjected:
  783. # mark that we've injected the target values
  784. self.targetValuesInjected = True
  785. # inject the defaults
  786. return self.getTargetValues() + "\n" + self.getTargetDisplayValues()
  787. # nothing to do
  788. return ""
  789. # Returns the unmodified GCODE line from previous ChangeAtZ edits
  790. @staticmethod
  791. def getOriginalLine(line: str) -> str:
  792. # get the change at z original (cazo) details
  793. original_line = re.search(r"\[CAZO:(.*?):CAZO\]", line)
  794. # if we didn't get a hit, this is the original line
  795. if original_line is None:
  796. return line
  797. return original_line.group(1)
  798. # Builds the target layer settings based on the specified values and returns the relevant GCODE lines
  799. def getTargetValues(self) -> str:
  800. # build the gcode to change our current values
  801. return self.getCodeFromValues(self.targetValues)
  802. # Determines if the current line is at or below the target required to start modifying
  803. def isTargetLayerOrHeight(self) -> bool:
  804. # target selected by layer no.
  805. if self.targetByLayer:
  806. # if we don't have a current layer, we're not there yet
  807. if self.currentLayer is None:
  808. return False
  809. # if we're applying to a single layer, stop if our layer is not identical
  810. if self.applyToSingleLayer:
  811. return self.currentLayer == self.targetLayer
  812. else:
  813. return self.currentLayer >= self.targetLayer
  814. else:
  815. # if we don't have a current Z, we're not there yet
  816. if self.currentZ is None:
  817. return False
  818. # if we're applying to a single layer, stop if our Z is not identical
  819. if self.applyToSingleLayer:
  820. return self.currentZ == self.targetZ
  821. else:
  822. return self.currentZ >= self.targetZ
  823. # Marks any current ChangeAtZ layer defaults in the layer for deletion
  824. @staticmethod
  825. def markChangesForDeletion(layer: str):
  826. return re.sub(r";\[CAZD:", ";[CAZD:DELETE:", layer)
  827. # Grabs the current height
  828. def processLayerHeight(self, line: str):
  829. # stop here if we haven't entered a layer yet
  830. if self.currentLayer is None:
  831. return
  832. # get our gcode command
  833. command = GCodeCommand.getFromLine(line)
  834. # skip if it's not a command we're interested in
  835. if command is None:
  836. return
  837. # stop here if this isn't a linear move command
  838. if command.command != "G0" and command.command != "G1":
  839. return
  840. # get our value from the command
  841. current_z = command.getArgumentAsFloat("Z", None)
  842. # stop here if we don't have a Z value defined, we can't get the height from this command
  843. if current_z is None:
  844. return
  845. # stop if there's no change
  846. if current_z == self.currentZ:
  847. return
  848. # set our current Z value
  849. self.currentZ = current_z
  850. # if we don't have a layer height yet, set it based on the current Z value
  851. if self.layerHeight is None:
  852. self.layerHeight = self.currentZ
  853. # Grabs the current layer number
  854. def processLayerNumber(self, line: str):
  855. # if this isn't a layer comment, stop here, nothing to update
  856. if ";LAYER:" not in line:
  857. return
  858. # get our current layer number
  859. current_layer = GCodeCommand.getDirectArgumentAsInt(line, ";LAYER:", None)
  860. # this should never happen, but if our layer number hasn't changed, stop here
  861. if current_layer == self.currentLayer:
  862. return
  863. # update our current layer
  864. self.currentLayer = current_layer
  865. # Makes any linear move changes and also injects either target or restored values depending on the plugin state
  866. def processLine(self, line: str) -> str:
  867. # used to change the given line of code
  868. modified_gcode = ""
  869. # track any values that we may be interested in
  870. self.trackChangeableValues(line)
  871. # if we're not inside the target layer, simply read the any
  872. # settings we can and revert any ChangeAtZ deletions
  873. if not self.insideTargetLayer:
  874. # read any settings if we haven't hit our target layer yet
  875. if not self.wasInsideTargetLayer:
  876. self.processSetting(line)
  877. # if we haven't hit our target yet, leave the defaults as is (unmark them for deletion)
  878. if "[CAZD:DELETE:" in line:
  879. line = line.replace("[CAZD:DELETE:", "[CAZD:")
  880. # if we're targeting by Z, we want to add our values before the first linear move
  881. if "G1 " in line or "G0 " in line:
  882. modified_gcode += self.getInjectCode()
  883. # modify our command if we're still inside our target layer, otherwise pass unmodified
  884. if self.insideTargetLayer:
  885. modified_gcode += self.processLinearMove(line) + "\n"
  886. else:
  887. modified_gcode += line + "\n"
  888. # if we're targeting by layer we want to add our values just after the layer label
  889. if ";LAYER:" in line:
  890. modified_gcode += self.getInjectCode()
  891. # return our changed code
  892. return modified_gcode
  893. # Handles any linear moves in the current line
  894. def processLinearMove(self, line: str) -> str:
  895. # if it's not a linear motion command we're not interested
  896. if not ("G1 " in line or "G0 " in line):
  897. return line
  898. # always get our original line, otherwise the effect will be cumulative
  899. line = self.getOriginalLine(line)
  900. # get our command from the line
  901. linear_command = GCodeCommand.getLinearMoveCommand(line)
  902. # if it's not a linear move, we don't care
  903. if linear_command is None:
  904. return line
  905. # get our linear move parameters
  906. feed_rate = linear_command.arguments["F"]
  907. x_coord = linear_command.arguments["X"]
  908. y_coord = linear_command.arguments["Y"]
  909. z_coord = linear_command.arguments["Z"]
  910. extrude_length = linear_command.arguments["E"]
  911. # set our new line to our old line
  912. new_line = line
  913. # handle retract length
  914. new_line = self.processRetractLength(extrude_length, feed_rate, new_line, x_coord, y_coord, z_coord)
  915. # handle retract feed rate
  916. new_line = self.processRetractFeedRate(extrude_length, feed_rate, new_line, x_coord, y_coord, z_coord)
  917. # handle print speed adjustments
  918. if extrude_length is not None: # Only for extrusion moves.
  919. new_line = self.processPrintSpeed(feed_rate, new_line)
  920. # set our current extrude position
  921. self.lastE = extrude_length if extrude_length is not None else self.lastE
  922. # if no changes have been made, stop here
  923. if new_line == line:
  924. return line
  925. # return our updated command
  926. return self.setOriginalLine(new_line, line)
  927. # Handles any changes to print speed for the given linear motion command
  928. def processPrintSpeed(self, feed_rate: float, new_line: str) -> str:
  929. # if we're not setting print speed or we don't have a feed rate, stop here
  930. if "printspeed" not in self.targetValues or feed_rate is None:
  931. return new_line
  932. # get our requested print speed
  933. print_speed = int(self.targetValues["printspeed"])
  934. # if they requested no change to print speed (ie: 100%), stop here
  935. if print_speed == 100:
  936. return new_line
  937. # get our feed rate from the command
  938. feed_rate = GCodeCommand.getDirectArgumentAsFloat(new_line, "F") * (float(print_speed) / 100.0)
  939. # change our feed rate
  940. return GCodeCommand.replaceDirectArgument(new_line, "F", feed_rate)
  941. # Handles any changes to retraction length for the given linear motion command
  942. def processRetractLength(self, extrude_length: float, feed_rate: float, new_line: str, x_coord: float, y_coord: float, z_coord: float) -> str:
  943. # if we don't have a retract length in the file we can't add one
  944. if "retractlength" not in self.lastValues or self.lastValues["retractlength"] == 0:
  945. return new_line
  946. # if we're not changing retraction length, stop here
  947. if "retractlength" not in self.targetValues:
  948. return new_line
  949. # retractions are only F (feed rate) and E (extrude), at least in cura
  950. if x_coord is not None or y_coord is not None or z_coord is not None:
  951. return new_line
  952. # since retractions require both F and E, and we don't have either, we can't process
  953. if feed_rate is None or extrude_length is None:
  954. return new_line
  955. # stop here if we don't know our last extrude value
  956. if self.lastE is None:
  957. return new_line
  958. # if there's no change in extrude we have nothing to change
  959. if self.lastE == extrude_length:
  960. return new_line
  961. # if our last extrude was lower than our current, we're restoring, so skip
  962. if self.lastE < extrude_length:
  963. return new_line
  964. # get our desired retract length
  965. retract_length = float(self.targetValues["retractlength"])
  966. # subtract the difference between the default and the desired
  967. extrude_length -= (retract_length - self.lastValues["retractlength"])
  968. # replace our extrude amount
  969. return GCodeCommand.replaceDirectArgument(new_line, "E", extrude_length)
  970. # Used for picking out the retract length set by Cura
  971. def processRetractLengthSetting(self, line: str):
  972. # skip if we're not doing linear retractions
  973. if not self.linearRetraction:
  974. return
  975. # get our command from the line
  976. linear_command = GCodeCommand.getLinearMoveCommand(line)
  977. # if it's not a linear move, we don't care
  978. if linear_command is None:
  979. return
  980. # get our linear move parameters
  981. feed_rate = linear_command.arguments["F"]
  982. x_coord = linear_command.arguments["X"]
  983. y_coord = linear_command.arguments["Y"]
  984. z_coord = linear_command.arguments["Z"]
  985. extrude_length = linear_command.arguments["E"]
  986. # the command we're looking for only has extrude and feed rate
  987. if x_coord is not None or y_coord is not None or z_coord is not None:
  988. return
  989. # if either extrude or feed is missing we're likely looking at the wrong command
  990. if extrude_length is None or feed_rate is None:
  991. return
  992. # cura stores the retract length as a negative E just before it starts printing
  993. extrude_length = extrude_length * -1
  994. # if it's a negative extrude after being inverted, it's not our retract length
  995. if extrude_length < 0:
  996. return
  997. # what ever the last negative retract length is it wins
  998. self.lastValues["retractlength"] = extrude_length
  999. # Handles any changes to retraction feed rate for the given linear motion command
  1000. def processRetractFeedRate(self, extrude_length: float, feed_rate: float, new_line: str, x_coord: float, y_coord: float, z_coord: float) -> str:
  1001. # skip if we're not doing linear retractions
  1002. if not self.linearRetraction:
  1003. return new_line
  1004. # if we're not changing retraction length, stop here
  1005. if "retractfeedrate" not in self.targetValues:
  1006. return new_line
  1007. # retractions are only F (feed rate) and E (extrude), at least in cura
  1008. if x_coord is not None or y_coord is not None or z_coord is not None:
  1009. return new_line
  1010. # since retractions require both F and E, and we don't have either, we can't process
  1011. if feed_rate is None or extrude_length is None:
  1012. return new_line
  1013. # get our desired retract feed rate
  1014. retract_feed_rate = float(self.targetValues["retractfeedrate"])
  1015. # convert to units/min
  1016. retract_feed_rate *= 60
  1017. # replace our feed rate
  1018. return GCodeCommand.replaceDirectArgument(new_line, "F", retract_feed_rate)
  1019. # Used for finding settings in the print file before we process anything else
  1020. def processSetting(self, line: str):
  1021. # if we're in layers already we're out of settings
  1022. if self.currentLayer is not None:
  1023. return
  1024. # check our retract length
  1025. self.processRetractLengthSetting(line)
  1026. # Sets the flags if we're at the target layer or not
  1027. def processTargetLayer(self):
  1028. # skip this line if we're not there yet
  1029. if not self.isTargetLayerOrHeight():
  1030. # flag that we're outside our target layer
  1031. self.insideTargetLayer = False
  1032. # skip to the next line
  1033. return
  1034. # flip if we hit our target layer
  1035. self.wasInsideTargetLayer = True
  1036. # flag that we're inside our target layer
  1037. self.insideTargetLayer = True
  1038. # Removes all the ChangeAtZ layer defaults from the given layer
  1039. @staticmethod
  1040. def removeMarkedChanges(layer: str) -> str:
  1041. return re.sub(r";\[CAZD:DELETE:[\s\S]+?:CAZD\](\n|$)", "", layer)
  1042. # Resets the class contents to defaults
  1043. def reset(self):
  1044. self.targetValues = {}
  1045. self.applyToSingleLayer = False
  1046. self.lastE = None
  1047. self.currentZ = None
  1048. self.currentLayer = None
  1049. self.targetByLayer = True
  1050. self.targetLayer = None
  1051. self.targetZ = None
  1052. self.layerHeight = None
  1053. self.lastValues = {"speed": 100}
  1054. self.linearRetraction = True
  1055. self.insideTargetLayer = False
  1056. self.targetValuesInjected = False
  1057. self.lastValuesRestored = False
  1058. self.wasInsideTargetLayer = False
  1059. self.enabled = True
  1060. # Sets the original GCODE line in a given GCODE command
  1061. @staticmethod
  1062. def setOriginalLine(line, original) -> str:
  1063. return line + ";[CAZO:" + original + ":CAZO]"
  1064. # Tracks the change in gcode values we're interested in
  1065. def trackChangeableValues(self, line: str):
  1066. # simulate a print speed command
  1067. if ";PRINTSPEED" in line:
  1068. line = line.replace(";PRINTSPEED ", "M220 S")
  1069. # simulate a retract feedrate command
  1070. if ";RETRACTFEEDRATE" in line:
  1071. line = line.replace(";RETRACTFEEDRATE ", "M207 F")
  1072. # simulate a retract length command
  1073. if ";RETRACTLENGTH" in line:
  1074. line = line.replace(";RETRACTLENGTH ", "M207 S")
  1075. # get our gcode command
  1076. command = GCodeCommand.getFromLine(line)
  1077. # stop here if it isn't a G or M command
  1078. if command is None:
  1079. return
  1080. # handle retract length changes
  1081. if command.command == "M207":
  1082. # get our retract length if provided
  1083. if "S" in command.arguments:
  1084. self.lastValues["retractlength"] = command.getArgumentAsFloat("S")
  1085. # get our retract feedrate if provided, convert from mm/m to mm/s
  1086. if "F" in command.arguments:
  1087. self.lastValues["retractfeedrate"] = command.getArgumentAsFloat("F") / 60.0
  1088. # move to the next command
  1089. return
  1090. # handle bed temp changes
  1091. if command.command == "M140" or command.command == "M190":
  1092. # get our bed temp if provided
  1093. if "S" in command.arguments:
  1094. self.lastValues["bedTemp"] = command.getArgumentAsFloat("S")
  1095. # move to the next command
  1096. return
  1097. # handle Build Volume Temperature changes, really shouldn't want to wait for enclousure temp mid print though.
  1098. if command.command == "M141" or command.command == "M191":
  1099. # get our bed temp if provided
  1100. if "S" in command.arguments:
  1101. self.lastValues["buildVolumeTemperature"] = command.getArgumentAsFloat("S")
  1102. # move to the next command
  1103. return
  1104. # handle extruder temp changes
  1105. if command.command == "M104" or command.command == "M109":
  1106. # get our temperature
  1107. temperature = command.getArgumentAsFloat("S")
  1108. # don't bother if we don't have a temperature
  1109. if temperature is None:
  1110. return
  1111. # get our extruder, default to extruder one
  1112. extruder = command.getArgumentAsInt("T", None)
  1113. # set our extruder temp based on the extruder
  1114. if extruder is None or extruder == 0:
  1115. self.lastValues["extruderOne"] = temperature
  1116. if extruder is None or extruder == 1:
  1117. self.lastValues["extruderTwo"] = temperature
  1118. # move to the next command
  1119. return
  1120. # handle fan speed changes
  1121. if command.command == "M106":
  1122. # get our bed temp if provided
  1123. if "S" in command.arguments:
  1124. self.lastValues["fanSpeed"] = (command.getArgumentAsInt("S") / 255.0) * 100
  1125. # move to the next command
  1126. return
  1127. # handle flow rate changes
  1128. if command.command == "M221":
  1129. # get our flow rate
  1130. temperature = command.getArgumentAsFloat("S")
  1131. # don't bother if we don't have a flow rate (for some reason)
  1132. if temperature is None:
  1133. return
  1134. # get our extruder, default to global
  1135. extruder = command.getArgumentAsInt("T", None)
  1136. # set our extruder temp based on the extruder
  1137. if extruder is None:
  1138. self.lastValues["flowrate"] = temperature
  1139. elif extruder == 1:
  1140. self.lastValues["flowrateOne"] = temperature
  1141. elif extruder == 1:
  1142. self.lastValues["flowrateTwo"] = temperature
  1143. # move to the next command
  1144. return
  1145. # handle print speed changes
  1146. if command.command == "M220":
  1147. # get our speed if provided
  1148. if "S" in command.arguments:
  1149. self.lastValues["speed"] = command.getArgumentAsInt("S")
  1150. # move to the next command
  1151. return