ChangeAtZ.py 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957
  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. # Uses -
  38. # M220 S<factor in percent> - set speed factor override percentage
  39. # M221 S<factor in percent> - set flow factor override percentage
  40. # M221 S<factor in percent> T<0-#toolheads> - set flow factor override percentage for single extruder
  41. # M104 S<temp> T<0-#toolheads> - set extruder <T> to target temperature <S>
  42. # M140 S<temp> - set bed target temperature
  43. # M106 S<PWM> - set fan speed to target speed <S>
  44. # M605/606 to save and recall material settings on the UM2
  45. from ..Script import Script
  46. import re
  47. # this was broken up into a separate class so the main ChangeZ script could be debugged outside of Cura
  48. class ChangeAtZ(Script):
  49. version = "5.2.0"
  50. def getSettingDataString(self):
  51. return """{
  52. "name": "ChangeAtZ """ + self.version + """(Experimental)",
  53. "key": "ChangeAtZ",
  54. "metadata": {},
  55. "version": 2,
  56. "settings": {
  57. "a_trigger": {
  58. "label": "Trigger",
  59. "description": "Trigger at height or at layer no.",
  60. "type": "enum",
  61. "options": {
  62. "height": "Height",
  63. "layer_no": "Layer No."
  64. },
  65. "default_value": "height"
  66. },
  67. "b_targetZ": {
  68. "label": "Change Height",
  69. "description": "Z height to change at",
  70. "unit": "mm",
  71. "type": "float",
  72. "default_value": 5.0,
  73. "minimum_value": "0",
  74. "minimum_value_warning": "0.1",
  75. "maximum_value_warning": "230",
  76. "enabled": "a_trigger == 'height'"
  77. },
  78. "b_targetL": {
  79. "label": "Change Layer",
  80. "description": "Layer no. to change at",
  81. "unit": "",
  82. "type": "int",
  83. "default_value": 1,
  84. "minimum_value": "-100",
  85. "minimum_value_warning": "-1",
  86. "enabled": "a_trigger == 'layer_no'"
  87. },
  88. "c_behavior": {
  89. "label": "Behavior",
  90. "description": "Select behavior: Change value and keep it for the rest, Change value for single layer only",
  91. "type": "enum",
  92. "options": {
  93. "keep_value": "Keep value",
  94. "single_layer": "Single Layer"
  95. },
  96. "default_value": "keep_value"
  97. },
  98. "e1_Change_speed": {
  99. "label": "Change Speed",
  100. "description": "Select if total speed (print and travel) has to be changed",
  101. "type": "bool",
  102. "default_value": false
  103. },
  104. "e2_speed": {
  105. "label": "Speed",
  106. "description": "New total speed (print and travel)",
  107. "unit": "%",
  108. "type": "int",
  109. "default_value": 100,
  110. "minimum_value": "1",
  111. "minimum_value_warning": "10",
  112. "maximum_value_warning": "200",
  113. "enabled": "e1_Change_speed"
  114. },
  115. "f1_Change_printspeed": {
  116. "label": "Change Print Speed",
  117. "description": "Select if print speed has to be changed",
  118. "type": "bool",
  119. "default_value": false
  120. },
  121. "f2_printspeed": {
  122. "label": "Print Speed",
  123. "description": "New print speed",
  124. "unit": "%",
  125. "type": "int",
  126. "default_value": 100,
  127. "minimum_value": "1",
  128. "minimum_value_warning": "10",
  129. "maximum_value_warning": "200",
  130. "enabled": "f1_Change_printspeed"
  131. },
  132. "g1_Change_flowrate": {
  133. "label": "Change Flow Rate",
  134. "description": "Select if flow rate has to be changed",
  135. "type": "bool",
  136. "default_value": false
  137. },
  138. "g2_flowrate": {
  139. "label": "Flow Rate",
  140. "description": "New Flow rate",
  141. "unit": "%",
  142. "type": "int",
  143. "default_value": 100,
  144. "minimum_value": "1",
  145. "minimum_value_warning": "10",
  146. "maximum_value_warning": "200",
  147. "enabled": "g1_Change_flowrate"
  148. },
  149. "g3_Change_flowrateOne": {
  150. "label": "Change Flow Rate 1",
  151. "description": "Select if first extruder flow rate has to be changed",
  152. "type": "bool",
  153. "default_value": false
  154. },
  155. "g4_flowrateOne": {
  156. "label": "Flow Rate One",
  157. "description": "New Flow rate Extruder 1",
  158. "unit": "%",
  159. "type": "int",
  160. "default_value": 100,
  161. "minimum_value": "1",
  162. "minimum_value_warning": "10",
  163. "maximum_value_warning": "200",
  164. "enabled": "g3_Change_flowrateOne"
  165. },
  166. "g5_Change_flowrateTwo": {
  167. "label": "Change Flow Rate 2",
  168. "description": "Select if second extruder flow rate has to be changed",
  169. "type": "bool",
  170. "default_value": false
  171. },
  172. "g6_flowrateTwo": {
  173. "label": "Flow Rate two",
  174. "description": "New Flow rate Extruder 2",
  175. "unit": "%",
  176. "type": "int",
  177. "default_value": 100,
  178. "minimum_value": "1",
  179. "minimum_value_warning": "10",
  180. "maximum_value_warning": "200",
  181. "enabled": "g5_Change_flowrateTwo"
  182. },
  183. "h1_Change_bedTemp": {
  184. "label": "Change Bed Temp",
  185. "description": "Select if Bed Temperature has to be changed",
  186. "type": "bool",
  187. "default_value": false
  188. },
  189. "h2_bedTemp": {
  190. "label": "Bed Temp",
  191. "description": "New Bed Temperature",
  192. "unit": "C",
  193. "type": "float",
  194. "default_value": 60,
  195. "minimum_value": "0",
  196. "minimum_value_warning": "30",
  197. "maximum_value_warning": "120",
  198. "enabled": "h1_Change_bedTemp"
  199. },
  200. "i1_Change_extruderOne": {
  201. "label": "Change Extruder 1 Temp",
  202. "description": "Select if First Extruder Temperature has to be changed",
  203. "type": "bool",
  204. "default_value": false
  205. },
  206. "i2_extruderOne": {
  207. "label": "Extruder 1 Temp",
  208. "description": "New First Extruder Temperature",
  209. "unit": "C",
  210. "type": "float",
  211. "default_value": 190,
  212. "minimum_value": "0",
  213. "minimum_value_warning": "160",
  214. "maximum_value_warning": "250",
  215. "enabled": "i1_Change_extruderOne"
  216. },
  217. "i3_Change_extruderTwo": {
  218. "label": "Change Extruder 2 Temp",
  219. "description": "Select if Second Extruder Temperature has to be changed",
  220. "type": "bool",
  221. "default_value": false
  222. },
  223. "i4_extruderTwo": {
  224. "label": "Extruder 2 Temp",
  225. "description": "New Second Extruder Temperature",
  226. "unit": "C",
  227. "type": "float",
  228. "default_value": 190,
  229. "minimum_value": "0",
  230. "minimum_value_warning": "160",
  231. "maximum_value_warning": "250",
  232. "enabled": "i3_Change_extruderTwo"
  233. },
  234. "j1_Change_fanSpeed": {
  235. "label": "Change Fan Speed",
  236. "description": "Select if Fan Speed has to be changed",
  237. "type": "bool",
  238. "default_value": false
  239. },
  240. "j2_fanSpeed": {
  241. "label": "Fan Speed",
  242. "description": "New Fan Speed (0-100)",
  243. "unit": "%",
  244. "type": "int",
  245. "default_value": 100,
  246. "minimum_value": "0",
  247. "minimum_value_warning": "0",
  248. "maximum_value_warning": "100",
  249. "enabled": "j1_Change_fanSpeed"
  250. },
  251. "caz_change_retractfeedrate": {
  252. "label": "Change Retract Feed Rate",
  253. "description": "Changes the retraction feed rate during print (M207)",
  254. "type": "bool",
  255. "default_value": false
  256. },
  257. "caz_retractfeedrate": {
  258. "label": "Retract Feed Rate",
  259. "description": "New Retract Feed Rate (units/s)",
  260. "unit": "units/s",
  261. "type": "int",
  262. "default_value": 40,
  263. "minimum_value": "0",
  264. "minimum_value_warning": "0",
  265. "maximum_value_warning": "100",
  266. "enabled": "caz_change_retractfeedrate"
  267. },
  268. "caz_change_retractlength": {
  269. "label": "Change Retract Length",
  270. "description": "Changes the retraction length during print (M207)",
  271. "type": "bool",
  272. "default_value": false
  273. },
  274. "caz_retractlength": {
  275. "label": "Retract Length",
  276. "description": "New Retract Length (units)",
  277. "unit": "units",
  278. "type": "int",
  279. "default_value": 6,
  280. "minimum_value": "0",
  281. "minimum_value_warning": "0",
  282. "maximum_value_warning": "20",
  283. "enabled": "caz_change_retractlength"
  284. }
  285. }
  286. }"""
  287. def __init__(self):
  288. super().__init__()
  289. def execute(self, data):
  290. caz_instance = ChangeAtZProcessor()
  291. caz_instance.TargetValues = {}
  292. # copy over our settings to our change z class
  293. self.setIntSettingIfEnabled(caz_instance, "e1_Change_speed", "speed", "e2_speed")
  294. self.setIntSettingIfEnabled(caz_instance, "f1_Change_printspeed", "printspeed", "f2_printspeed")
  295. self.setIntSettingIfEnabled(caz_instance, "g1_Change_flowrate", "flowrate", "g2_flowrate")
  296. self.setIntSettingIfEnabled(caz_instance, "g3_Change_flowrateOne", "flowrateOne", "g4_flowrateOne")
  297. self.setIntSettingIfEnabled(caz_instance, "g5_Change_flowrateTwo", "flowrateTwo", "g6_flowrateTwo")
  298. self.setIntSettingIfEnabled(caz_instance, "h1_Change_bedTemp", "bedTemp", "h2_bedTemp")
  299. self.setIntSettingIfEnabled(caz_instance, "i1_Change_extruderOne", "extruderOne", "i2_extruderOne")
  300. self.setIntSettingIfEnabled(caz_instance, "i3_Change_extruderTwo", "extruderTwo", "i4_extruderTwo")
  301. self.setIntSettingIfEnabled(caz_instance, "j1_Change_fanSpeed", "fanSpeed", "j2_fanSpeed")
  302. self.setIntSettingIfEnabled(caz_instance, "caz_change_retractfeedrate", "retractfeedrate", "caz_retractfeedrate")
  303. self.setIntSettingIfEnabled(caz_instance, "caz_change_retractlength", "retractlength", "caz_retractlength")
  304. # see if we're applying to a single layer or to all layers hence forth
  305. caz_instance.IsApplyToSingleLayer = self.getSettingValueByKey("c_behavior") == "single_layer"
  306. # used for easy reference of layer or height targeting
  307. caz_instance.IsTargetByLayer = self.getSettingValueByKey("a_trigger") == "layer_no"
  308. # change our target based on what we're targeting
  309. caz_instance.TargetLayer = self.getIntSettingByKey("b_targetL", None)
  310. caz_instance.TargetZ = self.getIntSettingByKey("b_targetZ", None)
  311. # run our script
  312. return caz_instance.execute(data)
  313. # Sets the given TargetValue in the ChangeAtZ instance if the trigger is specified
  314. def setIntSettingIfEnabled(self, caz_instance, trigger, target, setting):
  315. # stop here if our trigger isn't enabled
  316. if not self.getSettingValueByKey(trigger):
  317. return
  318. # get our value from the settings
  319. value = self.getIntSettingByKey(setting, None)
  320. # skip if there's no value or we can't interpret it
  321. if value is None:
  322. return
  323. # set our value in the target settings
  324. caz_instance.TargetValues[target] = value
  325. # Returns the given settings value as an integer or the default if it cannot parse it
  326. def getIntSettingByKey(self, key, default):
  327. # change our target based on what we're targeting
  328. try:
  329. return int(self.getSettingValueByKey(key))
  330. except:
  331. return default
  332. # The primary ChangeAtZ class that does all the gcode editing. This was broken out into an
  333. # independent class so it could be debugged using a standard IDE
  334. class ChangeAtZProcessor:
  335. TargetValues = {}
  336. IsApplyToSingleLayer = False
  337. LastE = None
  338. CurrentZ = None
  339. CurrentLayer = None
  340. IsTargetByLayer = True
  341. TargetLayer = None
  342. TargetZ = None
  343. LayerHeight = None
  344. RetractLength = 0
  345. # boots up the class with defaults
  346. def __init__(self):
  347. self.reset()
  348. # Modifies the given GCODE and injects the commands at the various targets
  349. def execute(self, data):
  350. # indicates if we should inject our defaults or not
  351. inject_defaults = True
  352. # our layer cursor
  353. index = 0
  354. for active_layer in data:
  355. # will hold our updated gcode
  356. modified_gcode = ""
  357. # mark all the defaults for deletion
  358. active_layer = self.markDefaultsForDeletion(active_layer)
  359. # break apart the layer into commands
  360. lines = active_layer.split("\n")
  361. # evaluate each command individually
  362. for line in lines:
  363. # skip empty lines
  364. if line.strip() == "":
  365. continue
  366. # update our layer number if applicable
  367. self.processLayerNumber(line)
  368. # update our layer height if applicable
  369. self.processLayerHeight(line)
  370. # skip this line if we're not there yet
  371. if not self.isTargetLayerOrHeight(line):
  372. # read any settings we might need
  373. self.processSetting(line)
  374. # if we haven't hit our target yet, leave the defaults as is (unmark them for deletion)
  375. if "[CAZD:DELETE:" in line:
  376. line = line.replace("[CAZD:DELETE:", "[CAZD:")
  377. # set our line
  378. modified_gcode += line + "\n"
  379. # skip to the next line
  380. continue
  381. # inject our defaults before linear motion commands
  382. if inject_defaults and ("G1" in line or "G0" in line):
  383. # inject the defaults
  384. modified_gcode += self.getTargetDefaults() + "\n"
  385. # mark that we've injected the defaults
  386. inject_defaults = False
  387. # append to our modified layer
  388. modified_gcode += self.processLinearMove(line) + "\n"
  389. # inject our defaults after the layer indicator
  390. if inject_defaults and ";LAYER:" in line:
  391. # inject the defaults
  392. modified_gcode += self.getTargetDefaults() + "\n"
  393. # mark that we've injected the defaults
  394. inject_defaults = False
  395. # remove any marked defaults
  396. modified_gcode = self.removeMarkedTargetDefaults(modified_gcode)
  397. # append our modified line
  398. data[index] = modified_gcode
  399. index += 1
  400. return data
  401. # Converts the command parameter to a float or returns the default
  402. @staticmethod
  403. def getFloatValue(line, key, default=None):
  404. # get the value from the command
  405. value = ChangeAtZProcessor.getValue(line, key, default)
  406. # stop here if it's the default
  407. if value == default:
  408. return value
  409. try:
  410. return float(value)
  411. except:
  412. return default
  413. # Converts the command parameter to a int or returns the default
  414. @staticmethod
  415. def getIntValue(line, key, default=None):
  416. # get the value from the command
  417. value = ChangeAtZProcessor.getValue(line, key, default)
  418. # stop here if it's the default
  419. if value == default:
  420. return value
  421. try:
  422. return int(value)
  423. except:
  424. return default
  425. # Handy function for reading a linear move command
  426. def getLinearMoveParams(self, line):
  427. # get our motion parameters
  428. feed_rate = self.getFloatValue(line, "F", None)
  429. x_coord = self.getFloatValue(line, "X", None)
  430. y_coord = self.getFloatValue(line, "Y", None)
  431. z_coord = self.getFloatValue(line, "Z", None)
  432. extrude_length = self.getFloatValue(line, "E", None)
  433. return extrude_length, feed_rate, x_coord, y_coord, z_coord
  434. # Returns the unmodified GCODE line from previous ChangeZ edits
  435. @staticmethod
  436. def getOriginalLine(line):
  437. # get the change at z original (cazo) details
  438. original_line = re.search(r"\[CAZO:(.*?):CAZO\]", line)
  439. # if we didn't get a hit, this is the original line
  440. if original_line is None:
  441. return line
  442. return original_line.group(1)
  443. # Builds the layer defaults based on the settings and returns the relevant GCODE lines
  444. def getTargetDefaults(self):
  445. # will hold all the default settings for the target layer
  446. defaults = []
  447. # used to trim other defaults
  448. defaults.append(";[CAZD:")
  449. # looking for wait for bed temp
  450. if "bedTemp" in self.TargetValues:
  451. defaults.append("M190 S" + str(self.TargetValues["bedTemp"]))
  452. # set our extruder temps
  453. if "extruderOne" in self.TargetValues:
  454. defaults.append("M109 S" + str(self.TargetValues["extruderOne"]))
  455. elif "extruderTwo" in self.TargetValues:
  456. defaults.append("M109 S" + str(self.TargetValues["extruderTwo"]))
  457. # set our fan speed
  458. if "fanSpeed" in self.TargetValues:
  459. # convert our fan speed percentage to PWM
  460. fan_speed = int((float(self.TargetValues["fanSpeed"]) / 100.0) * 255)
  461. # add our fan speed to the defaults
  462. defaults.append("M106 S" + str(fan_speed))
  463. # set global flow rate
  464. if "flowrate" in self.TargetValues:
  465. defaults.append("M221 S" + str(self.TargetValues["flowrate"]))
  466. # set extruder 0 flow rate
  467. if "flowrateOne" in self.TargetValues:
  468. defaults.append("M221 S" + str(self.TargetValues["flowrateOne"]) + " T0")
  469. # set second extruder flow rate
  470. if "flowrateTwo" in self.TargetValues:
  471. defaults.append("M221 S" + str(self.TargetValues["flowrateTwo"]) + " T1")
  472. # set feedrate percentage
  473. if "speed" in self.TargetValues:
  474. defaults.append("M220 S" + str(self.TargetValues["speed"]) + " T1")
  475. # set print rate percentage
  476. if "printspeed" in self.TargetValues:
  477. defaults.append(";PRINTSPEED " + str(self.TargetValues["printspeed"]) + "")
  478. # set retract rate
  479. if "retractfeedrate" in self.TargetValues:
  480. defaults.append(";RETRACTFEEDRATE " + str(self.TargetValues["retractfeedrate"]) + "")
  481. # set retract length
  482. if "retractlength" in self.TargetValues:
  483. defaults.append(";RETRACTLENGTH " + str(self.TargetValues["retractlength"]) + "")
  484. # used to trim other defaults
  485. defaults.append(";:CAZD]")
  486. # if there are no defaults, stop here
  487. if len(defaults) == 2:
  488. return ""
  489. # return our default block for this layer
  490. return "\n".join(defaults)
  491. # Allows retrieving values from the given GCODE line
  492. @staticmethod
  493. def getValue(line, key, default=None):
  494. if not key in line or (";" in line and line.find(key) > line.find(";") and not ";ChangeAtZ" in key and not ";LAYER:" in key):
  495. return default
  496. sub_part = line[line.find(key) + len(key):] # allows for string lengths larger than 1
  497. if ";ChangeAtZ" in key:
  498. m = re.search("^[0-4]", sub_part)
  499. elif ";LAYER:" in key:
  500. m = re.search("^[+-]?[0-9]*", sub_part)
  501. else:
  502. # the minus at the beginning allows for negative values, e.g. for delta printers
  503. m = re.search(r"^[-]?[0-9]*\.?[0-9]*", sub_part)
  504. if m is None:
  505. return default
  506. try:
  507. return float(m.group(0))
  508. except:
  509. return default
  510. # Determines if the current line is at or below the target required to start modifying
  511. def isTargetLayerOrHeight(self, line):
  512. # target selected by layer no.
  513. if self.IsTargetByLayer:
  514. # if we don't have a current layer, we're not there yet
  515. if self.CurrentLayer is None:
  516. return False
  517. # if we're applying to a single layer, stop if our layer is not identical
  518. if self.IsApplyToSingleLayer:
  519. return self.CurrentLayer == self.TargetLayer
  520. else:
  521. return self.CurrentLayer >= self.TargetLayer
  522. else:
  523. # if we don't have a current Z, we're not there yet
  524. if self.CurrentZ is None:
  525. return False
  526. # if we're applying to a single layer, stop if our Z is not identical
  527. if self.IsApplyToSingleLayer:
  528. return self.CurrentZ == self.TargetZ
  529. else:
  530. return self.CurrentZ >= self.TargetZ
  531. # Marks any current ChangeZ layer defaults in the layer for deletion
  532. @staticmethod
  533. def markDefaultsForDeletion(layer):
  534. return re.sub(r";\[CAZD:", ";[CAZD:DELETE:", layer)
  535. # Grabs the current height
  536. def processLayerHeight(self, line):
  537. # stop here if we haven't entered a layer yet
  538. if self.CurrentLayer is None:
  539. return
  540. # expose the main command
  541. line_no_comments = self.stripComments(line)
  542. # stop here if this isn't a linear move command
  543. if not ("G1" in line_no_comments or "G0" in line_no_comments):
  544. return
  545. # stop here if we don't have a Z value defined, we can't get the height from this command
  546. if "Z" not in line_no_comments:
  547. return
  548. # get our value from the command
  549. current_z = self.getIntValue(line_no_comments, "Z", None)
  550. # stop if there's no change
  551. if current_z == self.CurrentZ:
  552. return
  553. # set our current Z value
  554. self.CurrentZ = current_z
  555. # if we don't have a layer height yet, set it based on the current Z value
  556. if self.LayerHeight is None:
  557. self.LayerHeight = self.CurrentZ
  558. # Grabs the current layer number
  559. def processLayerNumber(self, line):
  560. # if this isn't a layer comment, stop here, nothing to update
  561. if ";LAYER:" not in line:
  562. return
  563. # get our current layer number
  564. current_layer = self.getIntValue(line, ";LAYER:", None)
  565. # this should never happen, but if our layer number hasn't changed, stop here
  566. if current_layer == self.CurrentLayer:
  567. return
  568. # update our current layer
  569. self.CurrentLayer = current_layer
  570. # Handles any linear moves in the current line
  571. def processLinearMove(self, line):
  572. # if it's not a linear motion command we're not interested
  573. if not ("G1" in line or "G0" in line):
  574. return line
  575. # always get our original line, otherwise the effect will be cumulative
  576. line = self.getOriginalLine(line)
  577. # get the details from our linear move command
  578. extrude_length, feed_rate, x_coord, y_coord, z_coord = self.getLinearMoveParams(line)
  579. # set our new line to our old line
  580. new_line = line
  581. # handle retract length
  582. new_line = self.processRetractLength(extrude_length, feed_rate, new_line, x_coord, y_coord, z_coord)
  583. # handle retract feed rate
  584. new_line = self.processRetractFeedRate(extrude_length, feed_rate, new_line, x_coord, y_coord, z_coord)
  585. # handle print speed adjustments
  586. new_line = self.processPrintSpeed(feed_rate, new_line)
  587. # set our current extrude position
  588. self.LastE = extrude_length if extrude_length is not None else self.LastE
  589. # if no changes have been made, stop here
  590. if new_line == line:
  591. return line
  592. # return our updated command
  593. return self.setOriginalLine(new_line, line)
  594. # Handles any changes to print speed for the given linear motion command
  595. def processPrintSpeed(self, feed_rate, new_line):
  596. # if we're not setting print speed or we don't have a feed rate, stop here
  597. if "printspeed" not in self.TargetValues or feed_rate is None:
  598. return new_line
  599. # get our requested print speed
  600. print_speed = int(self.TargetValues["printspeed"])
  601. # if they requested no change to print speed (ie: 100%), stop here
  602. if print_speed == 100:
  603. return new_line
  604. # get our feed rate from the command
  605. feed_rate = float(self.getValue(new_line, "F")) * (float(print_speed) / 100.0)
  606. # change our feed rate
  607. return self.replaceParameter(new_line, "F", feed_rate)
  608. # Handles any changes to retraction length for the given linear motion command
  609. def processRetractLength(self, extrude_length, feed_rate, new_line, x_coord, y_coord, z_coord):
  610. # if we don't have a retract length in the file we can't add one
  611. if self.RetractLength == 0:
  612. return new_line
  613. # if we're not changing retraction length, stop here
  614. if "retractlength" not in self.TargetValues:
  615. return new_line
  616. # retractions are only F (feed rate) and E (extrude), at least in cura
  617. if x_coord is not None or y_coord is not None or z_coord is not None:
  618. return new_line
  619. # since retractions require both F and E, and we don't have either, we can't process
  620. if feed_rate is None or extrude_length is None:
  621. return new_line
  622. # stop here if we don't know our last extrude value
  623. if self.LastE is None:
  624. return new_line
  625. # if there's no change in extrude we have nothing to change
  626. if self.LastE == extrude_length:
  627. return new_line
  628. # if our last extrude was lower than our current, we're restoring, so skip
  629. if self.LastE < extrude_length:
  630. return new_line
  631. # get our desired retract length
  632. retract_length = int(self.TargetValues["retractlength"])
  633. # subtract the difference between the default and the desired
  634. extrude_length -= (retract_length - self.RetractLength)
  635. # replace our extrude amount
  636. return self.replaceParameter(new_line, "E", extrude_length)
  637. # Used for picking out the retract length set by Cura
  638. def processRetractLengthSetting(self, line):
  639. # if it's not a linear move, we don't care
  640. if "G0" not in line and "G1" not in line:
  641. return
  642. # get the details from our linear move command
  643. extrude_length, feed_rate, x_coord, y_coord, z_coord = self.getLinearMoveParams(line)
  644. # the command we're looking for only has extrude and feed rate
  645. if x_coord is not None or y_coord is not None or z_coord is not None:
  646. return
  647. # if either extrude or feed is missing we're likely looking at the wrong command
  648. if extrude_length is None or feed_rate is None:
  649. return
  650. # cura stores the retract length as a negative E just before it starts printing
  651. extrude_length = extrude_length * -1
  652. # if it's a negative extrude after being inverted, it's not our retract length
  653. if extrude_length < 0:
  654. return
  655. # what ever the last negative retract length is it wins
  656. self.RetractLength = extrude_length
  657. # Handles any changes to retraction feed rate for the given linear motion command
  658. def processRetractFeedRate(self, extrude_length, feed_rate, new_line, x_coord, y_coord, z_coord):
  659. # if we're not changing retraction length, stop here
  660. if "retractfeedrate" not in self.TargetValues:
  661. return new_line
  662. # retractions are only F (feed rate) and E (extrude), at least in cura
  663. if x_coord is not None or y_coord is not None or z_coord is not None:
  664. return new_line
  665. # since retractions require both F and E, and we don't have either, we can't process
  666. if feed_rate is None or extrude_length is None:
  667. return new_line
  668. # get our desired retract feed rate
  669. retract_feed_rate = int(self.TargetValues["retractfeedrate"])
  670. # convert to units/min
  671. retract_feed_rate *= 60
  672. # replace our feed rate
  673. return self.replaceParameter(new_line, "F", retract_feed_rate)
  674. # Used for finding settings in the print file before we process anything else
  675. def processSetting(self, line):
  676. # if we're in layers already we're out of settings
  677. if self.CurrentLayer is not None:
  678. return
  679. # check our retract length
  680. self.processRetractLengthSetting(line)
  681. # Removes all the ChangeZ layer defaults from the given layer
  682. @staticmethod
  683. def removeMarkedTargetDefaults(layer):
  684. return re.sub(r";\[CAZD:DELETE:[\s\S]+?:CAZD\](\n|$)", "", layer)
  685. # Easy function for replacing any GCODE parameter variable in a given GCODE command
  686. @staticmethod
  687. def replaceParameter(line, key, value):
  688. return re.sub(r"(^|\s)" + key + r"[\d\.]+(\s|$)", r"\1" + key + str(value) + r"\2", line)
  689. # Resets the class contents to defaults
  690. def reset(self):
  691. self.TargetValues = {}
  692. self.IsApplyToSingleLayer = False
  693. self.LastE = None
  694. self.CurrentZ = None
  695. self.CurrentLayer = None
  696. self.IsTargetByLayer = True
  697. self.TargetLayer = None
  698. self.TargetZ = None
  699. self.LayerHeight = None
  700. self.RetractLength = 0
  701. # Sets the original GCODE line in a given GCODE command
  702. @staticmethod
  703. def setOriginalLine(line, original):
  704. return line + ";[CAZO:" + original + ":CAZO]"
  705. # Removes the gcode comments from a given gcode command
  706. @staticmethod
  707. def stripComments(line):
  708. return re.sub(r";.*?$", "", line).strip()
  709. def debug():
  710. # get our input file
  711. file = r"PATH_TO_SOME_GCODE.gcode"
  712. # read the whole thing
  713. f = open(file, "r")
  714. gcode = f.read()
  715. f.close()
  716. # boot up change
  717. caz_instance = ChangeAtZProcessor()
  718. caz_instance.IsTargetByLayer = False
  719. caz_instance.TargetZ = 5
  720. caz_instance.TargetValues["printspeed"] = 100
  721. caz_instance.TargetValues["retractfeedrate"] = 60
  722. # process gcode
  723. gcode = debug_iteration(gcode, caz_instance)
  724. # write our file
  725. debug_write(gcode, file + ".1.modified")
  726. caz_instance.reset()
  727. caz_instance.IsTargetByLayer = False
  728. caz_instance.TargetZ = 10
  729. caz_instance.TargetValues["bedTemp"] = 75
  730. caz_instance.TargetValues["printspeed"] = 150
  731. caz_instance.TargetValues["retractfeedrate"] = 40
  732. caz_instance.TargetValues["retractlength"] = 10
  733. # and again
  734. gcode = debug_iteration(gcode, caz_instance)
  735. # write our file
  736. debug_write(gcode, file + ".2.modified")
  737. caz_instance.reset()
  738. caz_instance.IsTargetByLayer = False
  739. caz_instance.TargetZ = 15
  740. caz_instance.TargetValues["bedTemp"] = 80
  741. caz_instance.TargetValues["printspeed"] = 100
  742. caz_instance.TargetValues["retractfeedrate"] = 10
  743. caz_instance.TargetValues["retractlength"] = 0
  744. # and again
  745. gcode = debug_iteration(gcode, caz_instance)
  746. # write our file
  747. debug_write(gcode, file + ".3.modified")
  748. def debug_write(gcode, file):
  749. # write our file
  750. f = open(file, "w")
  751. f.write(gcode)
  752. f.close()
  753. def debug_iteration(gcode, caz_instance):
  754. index = 0
  755. # break apart the GCODE like cura
  756. layers = re.split(r"^;LAYER:\d+\n", gcode)
  757. # add the layer numbers back
  758. for layer in layers:
  759. # if this is the first layer, skip it, basically
  760. if index == 0:
  761. # leave our first layer as is
  762. layers[index] = layer
  763. # move the cursor
  764. index += 1
  765. # skip to the next layer
  766. continue
  767. layers[index] = ";LAYER:" + str(index - 1) + ";\n" + layer
  768. return "".join(caz_instance.execute(layers))
  769. # debug()