ChangeAtZ.py 56 KB

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