1#!/usr/local/bin/python3.8 2''' 3This script reads/writes egv format 4 5Copyright (C) 2017-2020 Scorch www.scorchworks.com 6 7This program is free software; you can redistribute it and/or modify 8it under the terms of the GNU General Public License as published by 9the Free Software Foundation; either version 2 of the License, or 10(at your option) any later version. 11 12This program is distributed in the hope that it will be useful, 13but WITHOUT ANY WARRANTY; without even the implied warranty of 14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15GNU General Public License for more details. 16 17You should have received a copy of the GNU General Public License 18along with this program; if not, write to the Free Software 19Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 20''' 21 22import sys 23import struct 24import os 25from shutil import copyfile 26from math import * 27from interpolate import interpolate 28from time import time 29from LaserSpeed import LaserSpeed 30 31############################################################################## 32class egv: 33 def __init__(self, target=lambda s: sys.stdout.write(s)): 34 self.write = target 35 self.Modal_dir = 0 36 self.Modal_dist = 0 37 self.Modal_on = False 38 self.Modal_AX = 0 39 self.Modal_AY = 0 40 41 self.RIGHT = 66 #ord("B")=66 42 self.LEFT = 84 #ord("T")=84 43 self.UP = 76 #ord("L")=76 44 self.DOWN = 82 #ord("R")=82 45 self.ANGLE = 77 #ord("M")=77 46 self.ON = 68 #ord("D")=68 47 self.OFF = 85 #ord("U")=85 48 49 # % Yxtart % Xstart % Yend % Xend % I % C VXXXXXXX CUT_TYPE 50 # 51 # %Ystart_pos %Xstart_pos %Yend_pos %Xend_pos (start pos is the location of the head before the code is run) 52 # I is always I ? 53 # C is C for cutting or Marking otherwise it is omitted 54 # V is the start of 7 digits indicating the feed rate 255 255 1 55 # CUT_TYPE cutting/marking, Engraving=G followed by the raster step in thousandths of an inch 56 57 def move(self,direction,distance,laser_on=False,angle_dirs=None): 58 59 if angle_dirs==None: 60 angle_dirs = [self.Modal_AX,self.Modal_AY] 61 62 if direction == self.Modal_dir \ 63 and laser_on == self.Modal_on \ 64 and angle_dirs[0] == self.Modal_AX \ 65 and angle_dirs[1] == self.Modal_AY: 66 self.Modal_dist = self.Modal_dist + distance 67 68 else: 69 self.flush() 70 if laser_on != self.Modal_on: 71 if laser_on: 72 self.write(self.ON) 73 else: 74 self.write(self.OFF) 75 self.Modal_on = laser_on 76 77 if direction == self.ANGLE: 78 if angle_dirs[0]!=self.Modal_AX: 79 self.write(angle_dirs[0]) 80 self.Modal_AX = angle_dirs[0] 81 if angle_dirs[1]!=self.Modal_AY: 82 self.write(angle_dirs[1]) 83 self.Modal_AY = angle_dirs[1] 84 85 self.Modal_dir = direction 86 self.Modal_dist = distance 87 88 if direction == self.RIGHT or direction == self.LEFT: 89 self.Modal_AX = direction 90 if direction == self.UP or direction == self.DOWN: 91 self.Modal_AY = direction 92 93 94 def flush(self,laser_on=None): 95 if self.Modal_dist > 0: 96 self.write(self.Modal_dir) 97 for code in self.make_distance(self.Modal_dist): 98 self.write(code) 99 if (laser_on!=None) and (laser_on!=self.Modal_on): 100 if laser_on: 101 self.write(self.ON) 102 else: 103 self.write(self.OFF) 104 self.Modal_on = laser_on 105 self.Modal_dist = 0 106 107 108 # The one wire CRC algorithm is derived from the OneWire.cpp Library 109 # The library location: http://www.pjrc.com/teensy/td_libs_OneWire.html 110 def OneWireCRC(self,line): 111 crc=0 112 for i in range(len(line)): 113 inbyte=line[i] 114 for j in range(8): 115 mix = (crc ^ inbyte) & 0x01 116 crc >>= 1 117 if (mix): 118 crc ^= 0x8C 119 inbyte >>= 1 120 return crcS 121 122 123 def make_distance(self,dist_mils): 124 dist_mils=float(dist_mils) 125 if abs(dist_mils-round(dist_mils,0)) > 0.000001: 126 raise Exception('Distance values should be integer value (inches*1000)') 127 DIST=0.0 128 code = [] 129 v122 = 255 130 dist_milsA = int(dist_mils) 131 132 for i in range(0,int(floor(dist_mils/v122))): 133 code.append(122) 134 dist_milsA = dist_milsA-v122 135 DIST = DIST+v122 136 if dist_milsA==0: 137 pass 138 elif dist_milsA < 26: # codes "a" through "y" 139 code.append(96+dist_milsA) 140 elif dist_milsA < 52: # codes "|a" through "|z" 141 code.append(124) 142 code.append(96+dist_milsA-25) 143 elif dist_milsA < 255: 144 num_str = "%03d" %(int(round(dist_milsA))) 145 code.append(ord(num_str[0])) 146 code.append(ord(num_str[1])) 147 code.append(ord(num_str[2])) 148 else: 149 raise Exception("Error in EGV make_distance_in(): dist_milsA=",dist_milsA) 150 return code 151 152 def make_dir_dist(self,dxmils,dymils,laser_on=False): 153 adx = abs(dxmils) 154 ady = abs(dymils) 155 if adx > 0 or ady > 0: 156 if ady > 0: 157 if dymils > 0: 158 self.move(self.UP ,ady,laser_on) 159 else: 160 self.move(self.DOWN,ady,laser_on) 161 if adx > 0: 162 if dxmils > 0: 163 self.move(self.RIGHT,adx,laser_on) 164 else: 165 self.move(self.LEFT ,adx,laser_on) 166 167 def make_cut_line(self,dxmils,dymils,Spindle): 168 XCODE = self.RIGHT 169 if dxmils < 0.0: 170 XCODE = self.LEFT 171 YCODE = self.UP 172 if dymils < 0.0: 173 YCODE = self.DOWN 174 175 if abs(dxmils-round(dxmils,0)) > 0.0 or abs(dymils-round(dymils,0)) > 0.0: 176 raise Exception('Distance values should be integer value (inches*1000)') 177 178 adx = abs(dxmils/1000.0) 179 ady = abs(dymils/1000.0) 180 181 if dxmils == 0: 182 self.move(YCODE,abs(dymils),laser_on=Spindle) 183 elif dymils == 0: 184 self.move(XCODE,abs(dxmils),laser_on=Spindle) 185 elif dxmils==dymils: 186 self.move(self.ANGLE,abs(dxmils),laser_on=Spindle,angle_dirs=[XCODE,YCODE]) 187 else: 188 h=[] 189 if adx > ady: 190 slope = ady/adx 191 n = int(abs(dxmils)) 192 CODE = XCODE 193 CODE1 = YCODE 194 else: 195 slope = adx/ady 196 n = int(abs(dymils)) 197 CODE = YCODE 198 CODE1 = XCODE 199 200 for i in range(1,n+1): 201 h.append(round(i*slope,0)) 202 203 Lh=0.0 204 d1=0.0 205 d2=0.0 206 d1cnt=0.0 207 d2cnt=0.0 208 for i in range(len(h)): 209 if h[i]==Lh: 210 d1=d1+1 211 if d2>0.0: 212 self.move(self.ANGLE,d2,laser_on=Spindle,angle_dirs=[XCODE,YCODE]) 213 d2cnt=d2cnt+d2 214 d2=0.0 215 else: 216 d2=d2+1 217 if d1>0.0: 218 self.move(CODE,d1,laser_on=Spindle) 219 d1cnt=d1cnt+d1 220 d1=0.0 221 Lh=h[i] 222 223 if d1>0.0: 224 self.move(CODE,d1,laser_on=Spindle) 225 d1cnt=d1cnt+d1 226 d1=0.0 227 if d2>0.0: 228 self.move(self.ANGLE,d2,laser_on=Spindle,angle_dirs=[XCODE,YCODE]) 229 d2cnt=d2cnt+d2 230 d2=0.0 231 232 233 DX = d2cnt 234 DY = (d1cnt+d2cnt) 235 if adx < ady: 236 error = max(DX-abs(dxmils),DY-abs(dymils)) 237 else: 238 error = max(DY-abs(dxmils),DX-abs(dymils)) 239 if error > 0: 240 raise Exception("egv.py: Error delta =%f" %(error)) 241 242 243 def make_speed(self,Feed=None,board_name="LASER-M2",Raster_step=0): 244 board_code = board_name.split('-')[1] 245 speed_text = LaserSpeed.get_code_from_speed(Feed, abs(Raster_step), board=board_code) 246 247 speed=[] 248 for c in speed_text: 249 speed.append(ord(c)) 250 return speed 251 252 253 def make_move_data(self,dxmils,dymils): 254 if (abs(dxmils)+abs(dymils)) > 0: 255 self.write(73) # I 256 self.make_dir_dist(dxmils,dymils) 257 self.flush() 258 self.write(83) #S 259 self.write(49) #1 (one) 260 self.write(80) #P 261 262 ####################################################################### 263 def none_function(self,dummy=None): 264 #Don't delete this function (used in make_egv_data) 265 pass 266 267 def ecoord_adj(self,ecoords_adj_in,scale,FlipXoffset): 268 if FlipXoffset > 0: 269 e0 = int(round((FlipXoffset-ecoords_adj_in[0])*scale,0)) 270 else: 271 e0 = int(round(ecoords_adj_in[0]*scale,0)) 272 e1 = int(round(ecoords_adj_in[1]*scale,0)) 273 e2 = ecoords_adj_in[2] 274 return e0,e1,e2 275 276 277 def make_egv_data(self, ecoords_in, 278 startX=0, 279 startY=0, 280 units = 'in', 281 Feed = None, 282 board_name="LASER-M2", 283 Raster_step=0, 284 update_gui=None, 285 stop_calc=None, 286 FlipXoffset=0, 287 Rapid_Feed_Rate=0, 288 use_laser=True): 289 290 #print("make_egv_data",Rapid_Feed_Rate,len(ecoords_in)) 291 #print("Rapid_Feed_Rate=",Rapid_Feed_Rate) 292 ######################################################## 293 if stop_calc == None: 294 stop_calc=[] 295 stop_calc.append(0) 296 if update_gui == None: 297 update_gui = self.none_function 298 ######################################################## 299 if units == 'in': 300 scale = 1000.0 301 if units == 'mm': 302 scale = 1000.0/25.4; 303 304 startX = int(round(startX*scale,0)) 305 startY = int(round(startY*scale,0)) 306 307 ######################################################## 308 variable_feed_scale=None 309 Spindle = True and use_laser 310 if Feed==None: 311 variable_feed_scale = 25.4/60.0 312 Feed = round(ecoords_in[0][3]*variable_feed_scale,2) 313 Spindle = False 314 315 speed = self.make_speed(Feed,board_name=board_name,Raster_step=Raster_step) 316 317 ##self.write(ord("I")) 318 #for code in speed: 319 # self.write(code) 320 321 if Raster_step==0: 322 #self.write(ord("I")) 323 for code in speed: 324 self.write(code) 325 326 lastx,lasty,last_loop = self.ecoord_adj(ecoords_in[0],scale,FlipXoffset) 327 if not Rapid_Feed_Rate: 328 self.make_dir_dist(lastx-startX,lasty-startY) 329 330 self.flush(laser_on=False) 331 self.write(ord("N")) 332 if lasty-startY <= 0: 333 self.write(self.DOWN) 334 else: 335 self.write(self.UP) 336 337 if lastx-startX >= 0: 338 self.write(self.RIGHT) 339 else: 340 self.write(self.LEFT) 341 342 # Insert "S1E" 343 self.write(ord("S")) 344 self.write(ord("1")) 345 self.write(ord("E")) 346 ########################################################### 347 laser = False 348 349 if Rapid_Feed_Rate: 350 self.rapid_move_slow(lastx-startX,lasty-startY,Rapid_Feed_Rate,Feed,board_name) 351 timestamp=0 352 for i in range(1,len(ecoords_in)): 353 e0,e1,e2 = self.ecoord_adj(ecoords_in[i] ,scale,FlipXoffset) 354 stamp=int(3*time()) #update every 1/3 of a second 355 if (stamp != timestamp): 356 timestamp=stamp #interlock 357 update_gui("Generating EGV Data: %.1f%%" %(100.0*float(i)/float(len(ecoords_in)))) 358 if stop_calc[0]==True: 359 raise Exception("Action Stopped by User.") 360 361 if ( e2 == last_loop) and (not laser): 362 laser = True 363 elif ( e2 != last_loop) and (laser): 364 laser = False 365 dx = e0 - lastx 366 dy = e1 - lasty 367 368 min_rapid = 5 369 if (abs(dx)+abs(dy))>0: 370 if laser: 371 if variable_feed_scale!=None: 372 Feed_current = round(ecoords_in[i][3]*variable_feed_scale,2) 373 Spindle = ecoords_in[i][4] > 0 and use_laser 374 if Feed != Feed_current: 375 Feed = Feed_current 376 self.flush() 377 self.change_speed(Feed,board_name,laser_on=Spindle) 378 self.make_cut_line(dx,dy,Spindle) 379 else: 380 if ((abs(dx) < min_rapid) and (abs(dy) < min_rapid)) or Rapid_Feed_Rate: 381 self.rapid_move_slow(dx,dy,Rapid_Feed_Rate,Feed,board_name) 382 else: 383 self.rapid_move_fast(dx,dy) 384 385 lastx = e0 386 lasty = e1 387 last_loop = e2 388 389 if laser: 390 laser = False 391 392 dx = startX-lastx 393 dy = startY-lasty 394 if ((abs(dx) < min_rapid) and (abs(dy) < min_rapid)) or Rapid_Feed_Rate: 395 self.rapid_move_slow(dx,dy,Rapid_Feed_Rate,Feed,board_name) 396 else: 397 self.rapid_move_fast(dx,dy) 398 399 ########################################################### 400 else: # Raster 401 ########################################################### 402 Rapid_flag=True 403 ################################################### 404 scanline = [] 405 scanline_y = None 406 if Raster_step < 0.0: 407 irange = range(len(ecoords_in)) 408 else: 409 irange = range(len(ecoords_in)-1,-1,-1) 410 timestamp=0 411 for i in irange: 412 #if i%1000 == 0: 413 stamp=int(3*time()) #update every 1/3 of a second 414 if (stamp != timestamp): 415 timestamp=stamp #interlock 416 update_gui("Preprocessing Raster Data: %.1f%%" %(100.0*float(i)/float(len(ecoords_in)))) 417 y = ecoords_in[i][1] 418 if y != scanline_y: 419 scanline.append([ecoords_in[i]]) 420 scanline_y = y 421 else: 422 if bool(FlipXoffset) ^ bool(Raster_step > 0.0): # ^ is bitwise XOR 423 scanline[-1].insert(0,ecoords_in[i]) 424 else: 425 scanline[-1].append(ecoords_in[i]) 426 update_gui("Raster Data Ready") 427 ################################################### 428 lastx,lasty,last_loop = self.ecoord_adj(scanline[0][0],scale,FlipXoffset) 429 430 DXstart = lastx-startX 431 DYstart = lasty-startY 432 433 if Rapid_Feed_Rate: 434 self.make_egv_rapid(DXstart,DYstart,Rapid_Feed_Rate,board_name,finish=False) 435 436 ##self.write(ord("I")) 437 for code in speed: 438 self.write(code) 439 440 if not Rapid_Feed_Rate: 441 self.make_dir_dist(DXstart,DYstart) 442 443 #insert "NRB" 444 self.flush(laser_on=False) 445 self.write(ord("N")) 446 if (Raster_step < 0.0): 447 self.write(ord("R")) 448 else: 449 self.write(ord("L")) 450 self.write(ord("B")) 451 # Insert "S1E" 452 self.write(ord("S")) 453 self.write(ord("1")) 454 self.write(ord("E")) 455 dx_last = 0 456 457 sign = -1 458 cnt = 1 459 timestamp=0 460 for scan_raw in scanline: 461 scan = [] 462 for point in scan_raw: 463 e0,e1,e2 = self.ecoord_adj(point,scale,FlipXoffset) 464 scan.append([e0,e1,e2]) 465 stamp=int(3*time()) #update every 1/3 of a second 466 if (stamp != timestamp): 467 timestamp=stamp #interlock 468 update_gui("Generating EGV Data: %.1f%%" %(100.0*float(cnt)/float(len(scanline)))) 469 if stop_calc[0]==True: 470 raise Exception("Action Stopped by User.") 471 cnt = cnt+1 472 ###################################### 473 ## Flip direction and reset loop ## 474 ###################################### 475 sign = -sign 476 last_loop = None 477 y = scan[0][1] 478 dy = y-lasty 479 if sign == 1: 480 xr = scan[0][0] 481 else: 482 xr = scan[-1][0] 483 dxr = xr - lastx 484 ###################################### 485 ## Make Rapid move if needed ## 486 ###################################### 487 if abs(dy-Raster_step) != 0 and not Rapid_flag: 488 489 if dxr*sign < 0: 490 yoffset = -Raster_step*3 491 else: 492 yoffset = -Raster_step 493 494 if (dy+yoffset)*(abs(yoffset)/yoffset) < 0: 495 self.flush(laser_on=False) 496 497 if not Rapid_Feed_Rate: 498 self.write(ord("N")) 499 self.make_dir_dist(0,dy+yoffset) 500 self.flush(laser_on=False) 501 self.write(ord("S")) 502 self.write(ord("E")) 503 else: 504 DX=0 505 DY=dy+yoffset 506 self.raster_rapid_move_slow(DX,DY,Raster_step,Rapid_Feed_Rate,Feed,board_name) 507 508 Rapid_flag=True 509 else: 510 adj_steps = int(dy/Raster_step) 511 for stp in range(1,adj_steps): 512 513 adj_dist=5 514 self.make_dir_dist(sign*adj_dist,0) 515 lastx = lastx + sign*adj_dist 516 517 sign = -sign 518 if sign == 1: 519 xr = scan[0][0] 520 else: 521 xr = scan[-1][0] 522 dxr = xr - lastx 523 lasty = y 524 525 526 ###################################### 527 if sign == 1: 528 rng = range(0,len(scan),1) 529 else: 530 rng = range(len(scan)-1,-1,-1) 531 ###################################### 532 ## Pad row end if needed ## 533 ########################### 534 pad = 2 535 if (dxr*sign <= 0.0): 536 if (Rapid_flag == False): 537 self.make_dir_dist(-sign*pad,0) 538 self.make_dir_dist( dxr,0) 539 self.make_dir_dist( sign*pad,0) 540 else: 541 self.make_dir_dist( dxr,0) 542 lastx = lastx+dxr 543 544 Rapid_flag=False 545 ###################################### 546 for j in rng: 547 x = scan[j][0] 548 dx = x - lastx 549 ################################## 550 loop = scan[j][2] 551 if loop==last_loop: 552 self.make_cut_line(dx,0,True) 553 else: 554 if dx*sign > 0.0: 555 self.make_dir_dist(dx,0) 556 lastx = x 557 last_loop = loop 558 lasty = y 559 560 # Make final move to ensure last move is to the right 561 self.make_dir_dist(pad,0) 562 lastx = lastx + pad 563 # If sign is negative the final move will have incremented the 564 # "y" position so adjust the lasty to acoomodate 565 if sign < 0: 566 lasty = lasty + Raster_step 567 568 self.flush(laser_on=False) 569 570 571 dx_final = (startX - lastx) 572 if Raster_step < 0: 573 dy_final = (startY - lasty) + Raster_step 574 else: 575 dy_final = (startY - lasty) - Raster_step 576 577 ############################################################## 578 max_return_feed = 50.0 579 final_feed = 0 580 if Rapid_Feed_Rate: 581 final_feed = Rapid_Feed_Rate 582 elif Feed > max_return_feed: 583 final_feed = max_return_feed 584 585 if final_feed: 586 self.change_speed(final_feed,board_name,laser_on=False,pad=False) 587 dy_final = dy_final + abs(Raster_step) 588 self.make_dir_dist(dx_final,dy_final) 589 else: 590 self.write(ord("N")) 591 self.make_dir_dist(dx_final,dy_final) 592 self.flush(laser_on=False) 593 self.write(ord("S")) 594 self.write(ord("E")) 595 ############################################################## 596 597 598 # Append Footer 599 self.flush(laser_on=False) 600 self.write(ord("F")) 601 self.write(ord("N")) 602 self.write(ord("S")) 603 self.write(ord("E")) 604 update_gui("EGV Data Complete") 605 return 606 607 def make_egv_rapid(self, DX,DY,Feed = None,board_name="LASER-M2",finish=True): 608 speed = self.make_speed(Feed,board_name=board_name,Raster_step=0) 609 if finish: 610 self.write(ord("I")) 611 for code in speed: 612 self.write(code) 613 self.flush(laser_on=False) 614 self.write(ord("N")) 615 self.write(ord("R")) 616 self.write(ord("B")) 617 # Insert "S1E" 618 self.write(ord("S")) 619 self.write(ord("1")) 620 self.write(ord("E")) 621 ########################################################### 622 # Move Distance 623 self.make_cut_line(DX,DY,Spindle=0) 624 ########################################################### 625 # Append Footer 626 self.flush(laser_on=False) 627 if finish: 628 self.write(ord("F")) 629 else: 630 self.write(ord("@")) 631 self.write(ord("N")) 632 self.write(ord("S")) 633 self.write(ord("E")) 634 return 635 636 def rapid_move_slow(self,dx,dy,Rapid_Feed_Rate,Feed,board_name): 637 if Rapid_Feed_Rate: 638 self.change_speed(Rapid_Feed_Rate,board_name,laser_on=False) 639 self.make_dir_dist(dx,dy) 640 self.change_speed(Feed,board_name,laser_on=False) 641 else: 642 self.make_dir_dist(dx,dy) 643 644 def raster_rapid_move_slow(self,DX,DY,Raster_step,Rapid_Feed_Rate,Feed,board_name): 645 tiny_step = Raster_step/abs(Raster_step) 646 self.change_speed(Rapid_Feed_Rate,board_name,laser_on=False,pad=False) 647 self.make_dir_dist(DX,DY-tiny_step) 648 self.flush(laser_on=False) 649 self.change_speed(Feed,board_name,laser_on=False,Raster_step=Raster_step,pad=False) 650 #Tiny Rapid 651 self.write(ord("N")) 652 self.make_dir_dist(0,tiny_step) 653 self.flush(laser_on=False) 654 self.write(ord("S")) 655 self.write(ord("E")) 656 657 658 def rapid_move_fast(self,dx,dy): 659 pad = 3 660 if pad == -dx: 661 pad = pad+3 662 self.make_dir_dist(-pad, 0 ) #add "T" move 663 self.make_dir_dist( 0, pad) #add "L" move 664 self.flush(laser_on=False) 665 666 if dx+pad < 0.0: 667 self.write(ord("B")) 668 else: 669 self.write(ord("T")) 670 self.write(ord("N")) 671 self.make_dir_dist(dx+pad,dy-pad) 672 self.flush(laser_on=False) 673 self.write(ord("S")) 674 self.write(ord("E")) 675 676 677 def change_speed(self,Feed,board_name,laser_on=False,Raster_step=0,pad=True): 678 cspad = 5 679 if laser_on: 680 self.write(self.OFF) 681 682 if pad: 683 self.make_dir_dist(-cspad,-cspad) 684 self.flush(laser_on=False) 685 686 self.write(ord("@")) 687 self.write(ord("N")) 688 self.write(ord("S")) 689 self.write(ord("E")) 690 speed = self.make_speed(Feed,board_name,Raster_step=Raster_step) 691 #print Feed,speed 692 for code in speed: 693 self.write(code) 694 self.write(ord("N")) 695 self.write(ord("R")) 696 self.write(ord("B")) 697 ## Insert "SIE" 698 self.write(ord("S")) 699 self.write(ord("1")) 700 self.write(ord("E")) 701 702 if pad: 703 self.make_dir_dist(cspad,cspad) 704 self.flush(laser_on=False) 705 706 if laser_on: 707 self.write(self.ON) 708 709 710if __name__ == "__main__": 711 EGV=egv() 712 bname = "LASER-M2" 713 values = [.1,.2,.3,.4,.5,.6,.7,.8,.9,1,2,3,4,5,6,7,8,9,10,20,30,40,50,70,90,100] 714 step=0 715 for value_in in values: 716 #print ("% 8.2f" %(value_in),": ",end='') 717 val=EGV.make_speed(value_in,board_name=bname,Raster_step=step) 718 txt="" 719 for c in val: 720 txt=txt+chr(c) 721 print(txt) 722 print("DONE") 723 724 725 726 727 728 729