check_gcode_buffer.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542
  1. #!/usr/bin/env python3
  2. # Copyright (c) 2020 Ultimaker B.V.
  3. # Cura is released under the terms of the LGPLv3 or higher.
  4. import copy
  5. import math
  6. import os
  7. import sys
  8. from typing import Dict, List, Optional, Tuple
  9. # ====================================
  10. # Constants and Default Values
  11. # ====================================
  12. DEFAULT_BUFFER_FILLING_RATE_IN_C_PER_S = 50.0 # The buffer filling rate in #commands/s
  13. DEFAULT_BUFFER_SIZE = 15 # The buffer size in #commands
  14. MINIMUM_PLANNER_SPEED = 0.05
  15. #Setting values for Ultimaker S5.
  16. MACHINE_MAX_FEEDRATE_X = 300
  17. MACHINE_MAX_FEEDRATE_Y = 300
  18. MACHINE_MAX_FEEDRATE_Z = 40
  19. MACHINE_MAX_FEEDRATE_E = 45
  20. MACHINE_MAX_ACCELERATION_X = 9000
  21. MACHINE_MAX_ACCELERATION_Y = 9000
  22. MACHINE_MAX_ACCELERATION_Z = 100
  23. MACHINE_MAX_ACCELERATION_E = 10000
  24. MACHINE_MAX_JERK_XY = 20
  25. MACHINE_MAX_JERK_Z = 0.4
  26. MACHINE_MAX_JERK_E = 5
  27. MACHINE_MINIMUM_FEEDRATE = 0.001
  28. MACHINE_ACCELERATION = 3000
  29. def get_code_and_num(gcode_line: str) -> Tuple[str, str]:
  30. """Gets the code and number from the given g-code line."""
  31. gcode_line = gcode_line.strip()
  32. cmd_code = gcode_line[0].upper()
  33. cmd_num = str(gcode_line[1:])
  34. return cmd_code, cmd_num
  35. def get_value_dict(parts: List[str]) -> Dict[str, str]:
  36. """Fetches arguments such as X1 Y2 Z3 from the given part list and returns a dict"""
  37. value_dict = {}
  38. for p in parts:
  39. p = p.strip()
  40. if not p:
  41. continue
  42. code, num = get_code_and_num(p)
  43. value_dict[code] = num
  44. return value_dict
  45. # ============================
  46. # Math Functions - Begin
  47. # ============================
  48. def calc_distance(pos1, pos2):
  49. delta = {k: pos1[k] - pos2[k] for k in pos1}
  50. distance = 0
  51. for value in delta.values():
  52. distance += value ** 2
  53. distance = math.sqrt(distance)
  54. return distance
  55. def calc_acceleration_distance(init_speed: float, target_speed: float, acceleration: float) -> float:
  56. """Given the initial speed, the target speed, and the acceleration
  57. calculate the distance that's neede for the acceleration to finish.
  58. """
  59. if acceleration == 0:
  60. return 0.0
  61. return (target_speed ** 2 - init_speed ** 2) / (2 * acceleration)
  62. def calc_acceleration_time_from_distance(initial_feedrate: float, distance: float, acceleration: float) -> float:
  63. """Gives the time it needs to accelerate from an initial speed to reach a final distance."""
  64. discriminant = initial_feedrate ** 2 - 2 * acceleration * -distance
  65. #If the discriminant is negative, we're moving in the wrong direction.
  66. #Making the discriminant 0 then gives the extremum of the parabola instead of the intersection.
  67. discriminant = max(0, discriminant)
  68. return (-initial_feedrate + math.sqrt(discriminant)) / acceleration
  69. def calc_intersection_distance(initial_feedrate: float, final_feedrate: float, acceleration: float, distance: float) -> float:
  70. """Calculates the point at which you must start braking.
  71. This gives the distance from the start of a line at which you must start
  72. decelerating (at a rate of `-acceleration`) if you started at speed
  73. `initial_feedrate` and accelerated until this point and want to end at the
  74. `final_feedrate` after a total travel of `distance`. This can be used to
  75. compute the intersection point between acceleration and deceleration in the
  76. cases where the trapezoid has no plateau (i.e. never reaches maximum speed).
  77. """
  78. if acceleration == 0:
  79. return 0
  80. return (2 * acceleration * distance - initial_feedrate * initial_feedrate + final_feedrate * final_feedrate) / (4 * acceleration)
  81. def calc_max_allowable_speed(acceleration: float, target_velocity: float, distance: float) -> float:
  82. """Calculates the maximum speed that is allowed at this point when you must be
  83. able to reach target_velocity using the acceleration within the allotted
  84. distance.
  85. """
  86. return math.sqrt(target_velocity * target_velocity - 2 * acceleration * distance)
  87. class Command:
  88. def __init__(self, cmd_str: str) -> None:
  89. self._cmd_str = cmd_str # type: str
  90. self.estimated_exec_time = 0.0 # type: float
  91. self._cmd_process_function_map = {
  92. "G": self._handle_g,
  93. "M": self._handle_m,
  94. "T": self._handle_t,
  95. }
  96. self._is_comment = False # type: bool
  97. self._is_empty = False # type: bool
  98. #Fields taken from CuraEngine's implementation.
  99. self._recalculate = False
  100. self._accelerate_until = 0
  101. self._decelerate_after = 0
  102. self._initial_feedrate = 0
  103. self._final_feedrate = 0
  104. self._entry_speed = 0
  105. self._max_entry_speed =0
  106. self._nominal_length = False
  107. self._nominal_feedrate = 0
  108. self._max_travel = 0
  109. self._distance = 0
  110. self._acceleration = 0
  111. self._delta = [0, 0, 0]
  112. self._abs_delta = [0, 0, 0]
  113. def calculate_trapezoid(self, entry_factor, exit_factor):
  114. """Calculate the velocity-time trapezoid function for this move.
  115. Each move has a three-part function mapping time to velocity.
  116. """
  117. initial_feedrate = self._nominal_feedrate * entry_factor
  118. final_feedrate = self._nominal_feedrate * exit_factor
  119. #How far are we accelerating and how far are we decelerating?
  120. accelerate_distance = calc_acceleration_distance(initial_feedrate, self._nominal_feedrate, self._acceleration)
  121. decelerate_distance = calc_acceleration_distance(self._nominal_feedrate, final_feedrate, -self._acceleration)
  122. plateau_distance = self._distance - accelerate_distance - decelerate_distance #And how far in between at max speed?
  123. #Is the plateau negative size? That means no cruising, and we'll have to
  124. #use intersection_distance to calculate when to abort acceleration and
  125. #start braking in order to reach the final_rate exactly at the end of
  126. #this command.
  127. if plateau_distance < 0:
  128. accelerate_distance = calc_intersection_distance(initial_feedrate, final_feedrate, self._acceleration, self._distance)
  129. accelerate_distance = max(accelerate_distance, 0) #Due to rounding errors.
  130. accelerate_distance = min(accelerate_distance, self._distance)
  131. plateau_distance = 0
  132. self._accelerate_until = accelerate_distance
  133. self._decelerate_after = accelerate_distance + plateau_distance
  134. self._initial_feedrate = initial_feedrate
  135. self._final_feedrate = final_feedrate
  136. @property
  137. def is_command(self) -> bool:
  138. return not self._is_comment and not self._is_empty
  139. def __str__(self) -> str:
  140. if self._is_comment or self._is_empty:
  141. return self._cmd_str
  142. info = "t=%s" % (self.estimated_exec_time)
  143. return self._cmd_str.strip() + " ; --- " + info + os.linesep
  144. def parse(self) -> None:
  145. """Estimates the execution time of this command and calculates the state after this command is executed."""
  146. line = self._cmd_str.strip()
  147. if not line:
  148. self._is_empty = True
  149. return
  150. if line.startswith(";"):
  151. self._is_comment = True
  152. return
  153. # Remove comment
  154. line = line.split(";", 1)[0].strip()
  155. parts = line.split(" ")
  156. cmd_code, cmd_num = get_code_and_num(parts[0])
  157. cmd_num = int(cmd_num)
  158. func = self._cmd_process_function_map.get(cmd_code)
  159. if func is None:
  160. print("!!! no handle function for command type [%s]" % cmd_code)
  161. return
  162. func(cmd_num, parts)
  163. def _handle_g(self, cmd_num: int, parts: List[str]) -> None:
  164. self.estimated_exec_time = 0.0
  165. # G10: Retract. Make this behave as if it's a retraction of 25mm.
  166. if cmd_num == 10:
  167. #TODO: If already retracted, this shouldn't add anything to the time.
  168. cmd_num = 1
  169. parts = ["G1", "E" + str(buf.current_position[3] - 25)]
  170. # G11: Unretract. Make this behave as if it's an unretraction of 25mm.
  171. elif cmd_num == 11:
  172. #TODO: If already unretracted, this shouldn't add anything to the time.
  173. cmd_num = 1
  174. parts = ["G1", "E" + str(buf.current_position[3] + 25)]
  175. # G0 and G1: Move
  176. if cmd_num in (0, 1):
  177. # Move
  178. if len(parts) > 0:
  179. value_dict = get_value_dict(parts[1:])
  180. new_position = copy.deepcopy(buf.current_position)
  181. new_position[0] = float(value_dict.get("X", new_position[0]))
  182. new_position[1] = float(value_dict.get("Y", new_position[1]))
  183. new_position[2] = float(value_dict.get("Z", new_position[2]))
  184. new_position[3] = float(value_dict.get("E", new_position[3]))
  185. buf.current_feedrate = float(value_dict.get("F", buf.current_feedrate * 60.0)) / 60.0
  186. if buf.current_feedrate < MACHINE_MINIMUM_FEEDRATE:
  187. buf.current_feedrate = MACHINE_MINIMUM_FEEDRATE
  188. self._delta = [
  189. new_position[0] - buf.current_position[0],
  190. new_position[1] - buf.current_position[1],
  191. new_position[2] - buf.current_position[2],
  192. new_position[3] - buf.current_position[3]
  193. ]
  194. self._abs_delta = [abs(x) for x in self._delta]
  195. self._max_travel = max(self._abs_delta)
  196. if self._max_travel > 0:
  197. self._nominal_feedrate = buf.current_feedrate
  198. self._distance = math.sqrt(self._abs_delta[0] ** 2 + self._abs_delta[1] ** 2 + self._abs_delta[2] ** 2)
  199. if self._distance == 0:
  200. self._distance = self._abs_delta[3]
  201. current_feedrate = [d * self._nominal_feedrate / self._distance for d in self._delta]
  202. current_abs_feedrate = [abs(f) for f in current_feedrate]
  203. feedrate_factor = min(1.0, MACHINE_MAX_FEEDRATE_X)
  204. feedrate_factor = min(feedrate_factor, MACHINE_MAX_FEEDRATE_Y)
  205. feedrate_factor = min(feedrate_factor, buf.max_z_feedrate)
  206. feedrate_factor = min(feedrate_factor, MACHINE_MAX_FEEDRATE_E)
  207. #TODO: XY_FREQUENCY_LIMIT
  208. current_feedrate = [f * feedrate_factor for f in current_feedrate]
  209. current_abs_feedrate = [f * feedrate_factor for f in current_abs_feedrate]
  210. self._nominal_feedrate *= feedrate_factor
  211. self._acceleration = MACHINE_ACCELERATION
  212. max_accelerations = [MACHINE_MAX_ACCELERATION_X, MACHINE_MAX_ACCELERATION_Y, MACHINE_MAX_ACCELERATION_Z, MACHINE_MAX_ACCELERATION_E]
  213. for n in range(len(max_accelerations)):
  214. if self._acceleration * self._abs_delta[n] / self._distance > max_accelerations[n]:
  215. self._acceleration = max_accelerations[n]
  216. vmax_junction = MACHINE_MAX_JERK_XY / 2
  217. vmax_junction_factor = 1.0
  218. if current_abs_feedrate[2] > buf.max_z_jerk / 2:
  219. vmax_junction = min(vmax_junction, buf.max_z_jerk)
  220. if current_abs_feedrate[3] > buf.max_e_jerk / 2:
  221. vmax_junction = min(vmax_junction, buf.max_e_jerk)
  222. vmax_junction = min(vmax_junction, self._nominal_feedrate)
  223. safe_speed = vmax_junction
  224. if buf.previous_nominal_feedrate > 0.0001:
  225. xy_jerk = math.sqrt((current_feedrate[0] - buf.previous_feedrate[0]) ** 2 + (current_feedrate[1] - buf.previous_feedrate[1]) ** 2)
  226. vmax_junction = self._nominal_feedrate
  227. if xy_jerk > MACHINE_MAX_JERK_XY:
  228. vmax_junction_factor = MACHINE_MAX_JERK_XY / xy_jerk
  229. if abs(current_feedrate[2] - buf.previous_feedrate[2]) > MACHINE_MAX_JERK_Z:
  230. vmax_junction_factor = min(vmax_junction_factor, (MACHINE_MAX_JERK_Z / abs(current_feedrate[2] - buf.previous_feedrate[2])))
  231. if abs(current_feedrate[3] - buf.previous_feedrate[3]) > MACHINE_MAX_JERK_E:
  232. vmax_junction_factor = min(vmax_junction_factor, (MACHINE_MAX_JERK_E / abs(current_feedrate[3] - buf.previous_feedrate[3])))
  233. vmax_junction = min(buf.previous_nominal_feedrate, vmax_junction * vmax_junction_factor) #Limit speed to max previous speed.
  234. self._max_entry_speed = vmax_junction
  235. v_allowable = calc_max_allowable_speed(-self._acceleration, MINIMUM_PLANNER_SPEED, self._distance)
  236. self._entry_speed = min(vmax_junction, v_allowable)
  237. self._nominal_length = self._nominal_feedrate <= v_allowable
  238. self._recalculate = True
  239. buf.previous_feedrate = current_feedrate
  240. buf.previous_nominal_feedrate = self._nominal_feedrate
  241. buf.current_position = new_position
  242. self.calculate_trapezoid(self._entry_speed / self._nominal_feedrate, safe_speed / self._nominal_feedrate)
  243. self.estimated_exec_time = -1 #Signal that we need to include this in our second pass.
  244. # G4: Dwell, pause the machine for a period of time.
  245. elif cmd_num == 4:
  246. # Pnnn is time to wait in milliseconds (P0 wait until all previous moves are finished)
  247. cmd, num = get_code_and_num(parts[1])
  248. num = float(num)
  249. if cmd == "P":
  250. if num > 0:
  251. self.estimated_exec_time = num
  252. def _handle_m(self, cmd_num: int, parts: List[str]) -> None:
  253. self.estimated_exec_time = 0.0
  254. # M203: Set maximum feedrate. Only Z is supported. Assume 0 execution time.
  255. if cmd_num == 203:
  256. value_dict = get_value_dict(parts[1:])
  257. buf.max_z_feedrate = value_dict.get("Z", buf.max_z_feedrate)
  258. # M204: Set default acceleration. Assume 0 execution time.
  259. if cmd_num == 204:
  260. value_dict = get_value_dict(parts[1:])
  261. buf.acceleration = value_dict.get("S", buf.acceleration)
  262. # M205: Advanced settings, we only set jerks for Griffin. Assume 0 execution time.
  263. if cmd_num == 205:
  264. value_dict = get_value_dict(parts[1:])
  265. buf.max_xy_jerk = value_dict.get("XY", buf.max_xy_jerk)
  266. buf.max_z_jerk = value_dict.get("Z", buf.max_z_jerk)
  267. buf.max_e_jerk = value_dict.get("E", buf.max_e_jerk)
  268. def _handle_t(self, cmd_num: int, parts: List[str]) -> None:
  269. # Tn: Switching extruder. Assume 0 seconds. Actually more like 2.
  270. self.estimated_exec_time = 0.0
  271. class CommandBuffer:
  272. def __init__(self, all_lines: List[str],
  273. buffer_filling_rate: float = DEFAULT_BUFFER_FILLING_RATE_IN_C_PER_S,
  274. buffer_size: int = DEFAULT_BUFFER_SIZE
  275. ) -> None:
  276. self._all_lines = all_lines
  277. self._all_commands = list()
  278. self._buffer_filling_rate = buffer_filling_rate # type: float
  279. self._buffer_size = buffer_size # type: int
  280. self.acceleration = 3000
  281. self.current_position = [0, 0, 0, 0]
  282. self.current_feedrate = 0
  283. self.max_xy_jerk = MACHINE_MAX_JERK_XY
  284. self.max_z_jerk = MACHINE_MAX_JERK_Z
  285. self.max_e_jerk = MACHINE_MAX_JERK_E
  286. self.max_z_feedrate = MACHINE_MAX_FEEDRATE_Z
  287. # If the buffer can depletes less than this amount time, it can be filled up in time.
  288. lower_bound_buffer_depletion_time = self._buffer_size / self._buffer_filling_rate # type: float
  289. self._detection_time_frame = lower_bound_buffer_depletion_time
  290. self._code_count_limit = self._buffer_size
  291. self.total_time = 0.0
  292. self.previous_feedrate = [0, 0, 0, 0]
  293. self.previous_nominal_feedrate = 0
  294. print("Command speed: %s" % buffer_filling_rate)
  295. print("Code Limit: %s" % self._code_count_limit)
  296. self._bad_frame_ranges = []
  297. def process(self) -> None:
  298. buf.total_time = 0.0
  299. cmd0_idx = 0
  300. total_frame_time = 0.0
  301. cmd_count = 0
  302. for idx, line in enumerate(self._all_lines):
  303. cmd = Command(line)
  304. cmd.parse()
  305. if not cmd.is_command:
  306. continue
  307. self._all_commands.append(cmd)
  308. #Second pass: Reverse kernel.
  309. kernel_commands = [None, None, None]
  310. for cmd in reversed(self._all_commands):
  311. if cmd.estimated_exec_time >= 0:
  312. continue #Not a movement command.
  313. kernel_commands[2] = kernel_commands[1]
  314. kernel_commands[1] = kernel_commands[0]
  315. kernel_commands[0] = cmd
  316. self.reverse_pass_kernel(kernel_commands[0], kernel_commands[1], kernel_commands[2])
  317. #Third pass: Forward kernel.
  318. kernel_commands = [None, None, None]
  319. for cmd in self._all_commands:
  320. if cmd.estimated_exec_time >= 0:
  321. continue #Not a movement command.
  322. kernel_commands[0] = kernel_commands[1]
  323. kernel_commands[1] = kernel_commands[2]
  324. kernel_commands[2] = cmd
  325. self.forward_pass_kernel(kernel_commands[0], kernel_commands[1], kernel_commands[2])
  326. self.forward_pass_kernel(kernel_commands[1], kernel_commands[2], None)
  327. #Fourth pass: Recalculate the commands that have _recalculate set.
  328. previous = None
  329. current = None
  330. for current in self._all_commands:
  331. if current.estimated_exec_time >= 0:
  332. current = None
  333. continue #Not a movement command.
  334. if previous:
  335. #Recalculate if current command entry or exit junction speed has changed.
  336. if previous._recalculate or current._recalculate:
  337. #Note: Entry and exit factors always >0 by all previous logic operators.
  338. previous.calculate_trapezoid(previous._entry_speed / previous._nominal_feedrate, current._entry_speed / previous._nominal_feedrate)
  339. previous._recalculate = False
  340. previous = current
  341. if current is not None and current.estimated_exec_time >= 0:
  342. current.calculate_trapezoid(current._entry_speed / current._nominal_feedrate, MINIMUM_PLANNER_SPEED / current._nominal_feedrate)
  343. current._recalculate = False
  344. #Fifth pass: Compute time for movement commands.
  345. for cmd in self._all_commands:
  346. if cmd.estimated_exec_time >= 0:
  347. continue #Not a movement command.
  348. plateau_distance = cmd._decelerate_after - cmd._accelerate_until
  349. cmd.estimated_exec_time = calc_acceleration_time_from_distance(cmd._initial_feedrate, cmd._accelerate_until, cmd._acceleration)
  350. cmd.estimated_exec_time += plateau_distance / cmd._nominal_feedrate
  351. cmd.estimated_exec_time += calc_acceleration_time_from_distance(cmd._final_feedrate, (cmd._distance - cmd._decelerate_after), cmd._acceleration)
  352. for idx, cmd in enumerate(self._all_commands):
  353. cmd_count += 1
  354. if idx > cmd0_idx or idx == 0:
  355. buf.total_time += cmd.estimated_exec_time
  356. total_frame_time += cmd.estimated_exec_time
  357. if total_frame_time > 1:
  358. # Find the next starting command which makes the total execution time of the frame to be less than
  359. # 1 second.
  360. cmd0_idx += 1
  361. total_frame_time -= self._all_commands[cmd0_idx].estimated_exec_time
  362. cmd_count -= 1
  363. while total_frame_time > 1:
  364. cmd0_idx += 1
  365. total_frame_time -= self._all_commands[cmd0_idx].estimated_exec_time
  366. cmd_count -= 1
  367. # If within the current time frame the code count exceeds the limit, record that.
  368. if total_frame_time <= self._detection_time_frame and cmd_count > self._code_count_limit:
  369. need_to_append = True
  370. if self._bad_frame_ranges:
  371. last_item = self._bad_frame_ranges[-1]
  372. if last_item["start_line"] == cmd0_idx:
  373. last_item["end_line"] = idx
  374. last_item["cmd_count"] = cmd_count
  375. last_item["time"] = total_frame_time
  376. need_to_append = False
  377. if need_to_append:
  378. self._bad_frame_ranges.append({"start_line": cmd0_idx,
  379. "end_line": idx,
  380. "cmd_count": cmd_count,
  381. "time": total_frame_time})
  382. def reverse_pass_kernel(self, previous: Optional[Command], current: Optional[Command], next: Optional[Command]) -> None:
  383. if not current or not next:
  384. return
  385. #If entry speed is already at the maximum entry speed, no need to
  386. #recheck. The command is cruising. If not, the command is in state of
  387. #acceleration or deceleration. Reset entry speed to maximum and check
  388. #for maximum allowable speed reductions to ensure maximum possible
  389. #planned speed.
  390. if current._entry_speed != current._max_entry_speed:
  391. #If nominal length is true, max junction speed is guaranteed to be
  392. #reached. Only compute for max allowable speed if block is
  393. #decelerating and nominal length is false.
  394. if not current._nominal_length and current._max_entry_speed > next._max_entry_speed:
  395. current._entry_speed = min(current._max_entry_speed, calc_max_allowable_speed(-current._acceleration, next._entry_speed, current._distance))
  396. else:
  397. current._entry_speed = current._max_entry_speed
  398. current._recalculate = True
  399. def forward_pass_kernel(self, previous: Optional[Command], current: Optional[Command], next: Optional[Command]) -> None:
  400. if not previous:
  401. return
  402. #If the previous command is an acceleration command, but it is not long
  403. #enough to complete the full speed change within the command, we need to
  404. #adjust the entry speed accordingly. Entry speeds have already been
  405. #reset, maximised and reverse planned by the reverse planner. If nominal
  406. #length is set, max junction speed is guaranteed to be reached. No need
  407. #to recheck.
  408. if not previous._nominal_length:
  409. if previous._entry_speed < current._entry_speed:
  410. entry_speed = min(current._entry_speed, calc_max_allowable_speed(-previous._acceleration, previous._entry_speed, previous._distance))
  411. if current._entry_speed != entry_speed:
  412. current._entry_speed = entry_speed
  413. current._recalculate = True
  414. def to_file(self, file_name: str) -> None:
  415. all_lines = [str(c) for c in self._all_commands]
  416. with open(file_name, "w", encoding = "utf-8") as f:
  417. f.writelines(all_lines)
  418. f.write(";---TOTAL ESTIMATED TIME:" + str(self.total_time))
  419. def report(self) -> None:
  420. for item in self._bad_frame_ranges:
  421. print("Potential buffer underrun from line {start_line} to {end_line}, code count = {code_count}, in {time}s ({speed} cmd/s)".format(
  422. start_line = item["start_line"],
  423. end_line = item["end_line"],
  424. code_count = item["cmd_count"],
  425. time = round(item["time"], 4),
  426. speed = round(item["cmd_count"] / item["time"], 2)))
  427. print("Total predicted number of buffer underruns:", len(self._bad_frame_ranges))
  428. if __name__ == "__main__":
  429. if len(sys.argv) < 2 or 3 < len(sys.argv):
  430. print("Usage: <input g-code> [output g-code]")
  431. sys.exit(1)
  432. in_filename = sys.argv[1]
  433. out_filename = None
  434. if len(sys.argv) == 3:
  435. out_filename = sys.argv[2]
  436. with open(in_filename, "r", encoding = "utf-8") as f:
  437. all_lines = f.readlines()
  438. buf = CommandBuffer(all_lines)
  439. buf.process()
  440. # Output annotated gcode is optional
  441. if out_filename is not None:
  442. buf.to_file(out_filename)
  443. buf.report()