From 083b61fbb04f8fc178fb4b3edb59d982e44dada1 Mon Sep 17 00:00:00 2001 From: rorosentrater Date: Sat, 9 Dec 2023 18:24:53 -0500 Subject: [PATCH 1/5] Made some little animations to go along with the status changes --- ascii_art.py | 267 ++++++++++++++++++++++++++++++++++++++++++++++ cassini.py | 51 ++++++++- saturn_printer.py | 3 + 3 files changed, 320 insertions(+), 1 deletion(-) create mode 100644 ascii_art.py diff --git a/ascii_art.py b/ascii_art.py new file mode 100644 index 0000000..a51d563 --- /dev/null +++ b/ascii_art.py @@ -0,0 +1,267 @@ + +class PrinterArt: + unkown_ascii_art = [ + """ +===================================== +|| || +|| PRINTER || +|| unknown status || +===================================== +|| || +|| || +|| ??????????????? || +|| ??????????????? || +|| ??????????????? || +|| ??????????????? || +|| ??????????????? || +|| ??????????????? || +|| ??????????????? || +|| ??????????????? || +|| ??????????????? || +|| || +|| || +------------------------------------- +|| || +|| || +|| || +===================================== + """ + ] + idle_ascii_art = [ + """ +===================================== +|| || +|| PRINTER || +|| (IDLE) || +===================================== +|| || +|| || +|| || +|| || +|| || +|| || +|| || +|| || +|| || +|| || +|| || +|| || +|| || +------------------------------------- +|| || +|| || +|| || +===================================== + """ + ] + complete_ascii_art = [ + """ +===================================== +|| || +|| PRINTER || +|| COMPLETE! || +===================================== +|| | || +|| | || +|| | || +|| --------------------------- || +|| | | || +|| --------------------------- || +|| | | | | / | | || +|| +-+ | |/ V || +|| +----+ || +||~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~|| +|| || +|| || +|| || +------------------------------------- +|| || +|| || +|| || +===================================== + """ + ] + # For lowering, exposure and retracting animation frames. + # Play in different order to simulate direction. + # Limit range in case printer remains in phase + print_cycle_ascii_art = [ + # ⌄ Start Lowering... End Retract + """ +===================================== +|| || +|| PRINTER || +|| || +===================================== +|| | || +|| | || +|| --------------------------- || +|| | | || +|| --------------------------- || +|| || +|| || +|| || +|| || +||~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~|| +|| || +|| || +|| || +------------------------------------- +|| || +|| || +|| || +===================================== + """, + """ +===================================== +|| || +|| PRINTER || +|| || +===================================== +|| | || +|| | || +|| | || +|| | || +|| --------------------------- || +|| | | || +|| --------------------------- || +|| || +|| || +||~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~|| +|| || +|| || +|| || +------------------------------------- +|| || +|| || +|| || +===================================== + """, + """ +===================================== +|| || +|| PRINTER || +|| || +===================================== +|| | || +|| | || +|| | || +|| | || +|| | || +|| | || +|| --------------------------- || +|| | | || +|| --------------------------- || +||~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~|| +|| || +|| || +|| || +------------------------------------- +|| || +|| || +|| || +===================================== + """, + """ +===================================== +|| || +|| PRINTER || +|| || +===================================== +|| | || +|| | || +|| | || +|| | || +|| | || +|| | || +|| | || +|| | || +|| --------------------------- || +||~~~| |~~~|| +|| --------------------------- || +|| || +|| || +------------------------------------- +|| || +|| || +|| || +===================================== + """, + """ +===================================== +|| || +|| PRINTER || +|| || +===================================== +|| | || +|| | || +|| | || +|| | || +|| | || +|| | || +|| | || +|| | || +|| | || +||~~~---------------------------~~~|| +|| | | || +|| --------------------------- || +|| || +------------------------------------- +|| || +|| || +|| || +===================================== + """, + # End Lowering...Begin Retract ^ + # Start Exposure ⌄ + """ +===================================== +|| || +|| PRINTER || +|| || +===================================== +|| | || +|| | || +|| | || +|| | || +|| | || +|| | || +|| | || +|| | || +|| | || +||~~~---------------------------~~~|| +|| | | || +|| --------------------------- || +|| \ | / || +------------------------------------- +|| \ | / || +|| \ | / || +|| \|/ || +==================0================== + """, + """ +===================================== +|| || +|| PRINTER || +|| || +===================================== +|| | || +|| | || +|| | || +|| | || +|| | || +|| | || +|| | || +|| | || +|| | || +||~~~---------------------------~~~|| +|| | | || +|| --------------------------- || +|| \ | / || +--------------\---|---/-------------- +|| \ | / || +|| \ | / || +|| \|/ || +==================0================== + """ + # End Exposure + ] \ No newline at end of file diff --git a/cassini.py b/cassini.py index 5794e1a..f8dfd3e 100755 --- a/cassini.py +++ b/cassini.py @@ -15,6 +15,7 @@ from simple_mqtt_server import SimpleMQTTServer from simple_http_server import SimpleHTTPServer from saturn_printer import SaturnPrinter, PrintInfoStatus, CurrentStatus, FileStatus +from ascii_art import PrinterArt logging.basicConfig( level=logging.INFO, @@ -66,11 +67,59 @@ def do_status(printers): def do_watch(printer, interval=5, broadcast=None): status = printer.status() with alive_bar(total=status['totalLayers'], manual=True, elapsed=False, title=status['filename']) as bar: + ascii_art_category = PrinterArt.unkown_ascii_art + ascii_frame = 0 + frame_increments = True + frame_range = None while True: printers = SaturnPrinter.find_printers(broadcast=broadcast) if len(printers) > 0: status = printers[0].status() - pct = status['currentLayer'] / status['totalLayers'] + os.system('cls' if os.name == 'nt' else 'clear') # Clear terminal to print next ascii art frame + progress bar + + match PrintInfoStatus(status['printStatus']): + case PrintInfoStatus.LOWERING: + ascii_art_category = PrinterArt.print_cycle_ascii_art + frame_increments = True + frame_range = [0, 4] + case PrintInfoStatus.RETRACTING: + ascii_art_category = PrinterArt.print_cycle_ascii_art + frame_increments = False # Retracting plays in reverse order + frame_range = [0, 4] + case PrintInfoStatus.EXPOSURE: + ascii_art_category = PrinterArt.print_cycle_ascii_art + frame_increments = True + frame_range = [5, 6] + case PrintInfoStatus.COMPLETE: + ascii_art_category = PrinterArt.complete_ascii_art + frame_range = None # Only has 1 frame + case PrintInfoStatus.IDLE: + ascii_art_category = PrinterArt.idle_ascii_art + frame_range = None # Only has 1 frame + case _: + ascii_art_category = PrinterArt.unkown_ascii_art + frame_range = None # Only has 1 frame + + if frame_range: + if frame_increments and frame_range[1] > ascii_frame: + ascii_frame += 1 + elif frame_increments == False and frame_range[0] < ascii_frame: + ascii_frame -= 1 + if not (frame_range[0] <= ascii_frame <= frame_range[1]): + if frame_increments: + ascii_frame = frame_range[0] + else: + ascii_frame = frame_range[1] + else: + ascii_frame = 0 + + print(ascii_art_category[ascii_frame]) + print(f"Print Status: {PrintInfoStatus(status['printStatus']).name}") + + if status['totalLayers'] == 0: # Usually encountered when printer is IDLE. No file + break + else: + pct = status['currentLayer'] / status['totalLayers'] bar(pct) if pct >= 1.0: break diff --git a/saturn_printer.py b/saturn_printer.py index ac1bb49..c736616 100644 --- a/saturn_printer.py +++ b/saturn_printer.py @@ -26,6 +26,8 @@ class CurrentStatus(Enum): # Status field inside PrintInfo class PrintInfoStatus(Enum): # TODO: double check these + IDLE = 0 + # UNKNOWN = 1 # I suspect there is a 1. Haven't seen it yet. Not sure what it represents. EXPOSURE = 2 RETRACTING = 3 LOWERING = 4 @@ -298,6 +300,7 @@ def status(self): printinfo = self.desc['Data']['Status']['PrintInfo'] return { 'status': self.desc['Data']['Status']['CurrentStatus'], + 'printStatus': printinfo['Status'], 'filename': printinfo['Filename'], 'currentLayer': printinfo['CurrentLayer'], 'totalLayers': printinfo['TotalLayer'] From 7ef0afed4580243ff1e3055015f4211555b15d17 Mon Sep 17 00:00:00 2001 From: rorosentrater Date: Sun, 10 Dec 2023 21:06:26 -0500 Subject: [PATCH 2/5] With the switch to enums we need to get the actual int value before dumping json payload --- saturn_printer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/saturn_printer.py b/saturn_printer.py index ac1bb49..242af28 100644 --- a/saturn_printer.py +++ b/saturn_printer.py @@ -309,7 +309,7 @@ def send_command(self, cmdid, data=None): timestamp = int(time.time() * 1000) cmd_data = { "Data": { - "Cmd": cmdid, + "Cmd": cmdid.value, "Data": data, "From": 0, "MainboardID": self.id, From 34e56cd79611971f6725d048642fad5f4e6f612d Mon Sep 17 00:00:00 2001 From: rorosentrater Date: Mon, 11 Dec 2023 23:04:18 -0500 Subject: [PATCH 3/5] changed watch command to only break when COMPLETE status is reported. The new status RETURN_HOME also occurs on the last layer before COMPLETE so checking layer # might not indicate the machine has finished printing yet. Print status 1 was observed and appears to occur just before the first layer begins printing. Unsure what it represents yet. Best guess is INITIAL_DESCENT. --- cassini.py | 9 +++++---- saturn_printer.py | 3 ++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/cassini.py b/cassini.py index f8dfd3e..f5c8672 100755 --- a/cassini.py +++ b/cassini.py @@ -66,6 +66,7 @@ def do_status(printers): def do_watch(printer, interval=5, broadcast=None): status = printer.status() + print_info_status = PrintInfoStatus(status['printStatus']) with alive_bar(total=status['totalLayers'], manual=True, elapsed=False, title=status['filename']) as bar: ascii_art_category = PrinterArt.unkown_ascii_art ascii_frame = 0 @@ -77,7 +78,7 @@ def do_watch(printer, interval=5, broadcast=None): status = printers[0].status() os.system('cls' if os.name == 'nt' else 'clear') # Clear terminal to print next ascii art frame + progress bar - match PrintInfoStatus(status['printStatus']): + match print_info_status: case PrintInfoStatus.LOWERING: ascii_art_category = PrinterArt.print_cycle_ascii_art frame_increments = True @@ -116,13 +117,13 @@ def do_watch(printer, interval=5, broadcast=None): print(ascii_art_category[ascii_frame]) print(f"Print Status: {PrintInfoStatus(status['printStatus']).name}") - if status['totalLayers'] == 0: # Usually encountered when printer is IDLE. No file + if status['totalLayers'] == 0: # Usually encountered when printer is IDLE. No file. No layers. break else: pct = status['currentLayer'] / status['totalLayers'] bar(pct) - if pct >= 1.0: - break + if print_info_status == PrintInfoStatus.COMPLETE: + break time.sleep(interval) async def create_servers(): diff --git a/saturn_printer.py b/saturn_printer.py index 428503a..30c82d1 100644 --- a/saturn_printer.py +++ b/saturn_printer.py @@ -27,10 +27,11 @@ class CurrentStatus(Enum): class PrintInfoStatus(Enum): # TODO: double check these IDLE = 0 - # UNKNOWN = 1 # I suspect there is a 1. Haven't seen it yet. Not sure what it represents. + INITIAL_DESCENT = 1 # Unsure what exactly this status # represents. Name may be misleading. EXPOSURE = 2 RETRACTING = 3 LOWERING = 4 + RETURN_HOME = 12 # Unsure what exactly this status # represents. Name may be misleading. COMPLETE = 16 # pretty sure this is correct # Status field inside FileTransferInfo From 538a96d5d174535839c6db7bdc287b93297ad60e Mon Sep 17 00:00:00 2001 From: rorosentrater Date: Mon, 11 Dec 2023 23:53:56 -0500 Subject: [PATCH 4/5] made ASCII animations optional with new --no-animation arg --- cassini.py | 84 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 43 insertions(+), 41 deletions(-) diff --git a/cassini.py b/cassini.py index f5c8672..4144ae0 100755 --- a/cassini.py +++ b/cassini.py @@ -64,7 +64,7 @@ def do_status(printers): print(f" File: {print_info['Filename']}") print(f" File Transfer Status: {FileStatus(file_info['Status']).name}") -def do_watch(printer, interval=5, broadcast=None): +def do_watch(printer, interval=5, broadcast=None, no_animation=False): status = printer.status() print_info_status = PrintInfoStatus(status['printStatus']) with alive_bar(total=status['totalLayers'], manual=True, elapsed=False, title=status['filename']) as bar: @@ -76,45 +76,46 @@ def do_watch(printer, interval=5, broadcast=None): printers = SaturnPrinter.find_printers(broadcast=broadcast) if len(printers) > 0: status = printers[0].status() - os.system('cls' if os.name == 'nt' else 'clear') # Clear terminal to print next ascii art frame + progress bar - - match print_info_status: - case PrintInfoStatus.LOWERING: - ascii_art_category = PrinterArt.print_cycle_ascii_art - frame_increments = True - frame_range = [0, 4] - case PrintInfoStatus.RETRACTING: - ascii_art_category = PrinterArt.print_cycle_ascii_art - frame_increments = False # Retracting plays in reverse order - frame_range = [0, 4] - case PrintInfoStatus.EXPOSURE: - ascii_art_category = PrinterArt.print_cycle_ascii_art - frame_increments = True - frame_range = [5, 6] - case PrintInfoStatus.COMPLETE: - ascii_art_category = PrinterArt.complete_ascii_art - frame_range = None # Only has 1 frame - case PrintInfoStatus.IDLE: - ascii_art_category = PrinterArt.idle_ascii_art - frame_range = None # Only has 1 frame - case _: - ascii_art_category = PrinterArt.unkown_ascii_art - frame_range = None # Only has 1 frame - - if frame_range: - if frame_increments and frame_range[1] > ascii_frame: - ascii_frame += 1 - elif frame_increments == False and frame_range[0] < ascii_frame: - ascii_frame -= 1 - if not (frame_range[0] <= ascii_frame <= frame_range[1]): - if frame_increments: - ascii_frame = frame_range[0] - else: - ascii_frame = frame_range[1] - else: - ascii_frame = 0 - - print(ascii_art_category[ascii_frame]) + if not no_animation: + os.system('cls' if os.name == 'nt' else 'clear') # Clear terminal to print next ascii art frame + progress bar + + match print_info_status: + case PrintInfoStatus.LOWERING: + ascii_art_category = PrinterArt.print_cycle_ascii_art + frame_increments = True + frame_range = [0, 4] + case PrintInfoStatus.RETRACTING: + ascii_art_category = PrinterArt.print_cycle_ascii_art + frame_increments = False # Retracting plays in reverse order + frame_range = [0, 4] + case PrintInfoStatus.EXPOSURE: + ascii_art_category = PrinterArt.print_cycle_ascii_art + frame_increments = True + frame_range = [5, 6] + case PrintInfoStatus.COMPLETE: + ascii_art_category = PrinterArt.complete_ascii_art + frame_range = None # Only has 1 frame + case PrintInfoStatus.IDLE: + ascii_art_category = PrinterArt.idle_ascii_art + frame_range = None # Only has 1 frame + case _: + ascii_art_category = PrinterArt.unkown_ascii_art + frame_range = None # Only has 1 frame + + if frame_range: + if frame_increments and frame_range[1] > ascii_frame: + ascii_frame += 1 + elif frame_increments == False and frame_range[0] < ascii_frame: + ascii_frame -= 1 + if not (frame_range[0] <= ascii_frame <= frame_range[1]): + if frame_increments: + ascii_frame = frame_range[0] + else: + ascii_frame = frame_range[1] + else: + ascii_frame = 0 + + print(ascii_art_category[ascii_frame]) print(f"Print Status: {PrintInfoStatus(status['printStatus']).name}") if status['totalLayers'] == 0: # Usually encountered when printer is IDLE. No file. No layers. @@ -188,6 +189,7 @@ def main(): parser_watch = subparsers.add_parser('watch', help='Continuously update the status of the selected printer') parser_watch.add_argument('--interval', type=int, help='Status update interval (seconds)', default=5) + parser_watch.add_argument('--no-animation', help='Disables ASCII art animations that are shown based on reported status', action='store_true') parser_upload = subparsers.add_parser('upload', help='Upload a file to the printer') parser_upload.add_argument('--start-printing', help='Start printing after upload is complete', action='store_true') @@ -222,7 +224,7 @@ def main(): sys.exit(0) if args.command == "watch": - do_watch(printer, interval=args.interval, broadcast=broadcast) + do_watch(printer, interval=args.interval, broadcast=broadcast, no_animation=args.no_animation) sys.exit(0) logging.info(f'Printer: {printer.describe()} ({printer.addr[0]})') From 16fcf9330736f99fcbdcaa0cb9e70f695fa1e3f1 Mon Sep 17 00:00:00 2001 From: rorosentrater Date: Tue, 12 Dec 2023 00:23:03 -0500 Subject: [PATCH 5/5] updated command examples in README. Also formatted some of the example payloads --- README.md | 96 +++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 86 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 3c9e193..fefd94c 100644 --- a/README.md +++ b/README.md @@ -23,15 +23,19 @@ install the `alive-progress` package for nicer progress bars (`pip3 install aliv ``` $ ./cassini.py status -192.168.7.128: Saturn3Ultra (ELEGOO Saturn 3 Ultra) Status: 1 - Print Status: 2 Layers: 19/130 - File Transfer Status: 0 +192.168.1.15: + Saturn3Ultra (ELEGOO Saturn 3 Ultra) + Machine Status: READY + Print Status: COMPLETE + Layers: 500/500 + File: _Magnet_holder.ctb + File Transfer Status: NONE ``` ### Watch live print progress ``` -$ ./cassini.py watch [interval] +$ ./cassini.py watch [interval] --no-animation _STL_B_Warriors_1_Sword_Combined_Supported.goo |███████████████████████████████████▉ ︎ | 90% ``` @@ -126,11 +130,83 @@ The id is the `MainboardID` from the original discovery. Each payload The printer publishes messages to three topics: -`/sdcp/status/ABCD1234ABCD1234`: Status messages, same format as the `"Status"` content of the broadcast message: `{"Id":"f25273b12b094c5a8b9513a30ca60049","Data":{"Status":{"CurrentStatus":0,"PreviousStatus":0,"PrintInfo":{"Status":0,"CurrentLayer":0,"TotalLayer":0,"CurrentTicks":0,"TotalTicks":0,"ErrorNumber":0,"Filename":""},"FileTransferInfo":{"Status":0,"DownloadOffset":0,"CheckOffset":0,"FileTotalSize":0,"Filename":""}},"MainboardID":"ABCD1234ABCD1234","TimeStamp":8629636}}` +`/sdcp/status/ABCD1234ABCD1234`: Status messages, same format as the `"Status"` content of the broadcast message: +``` +{ + "Id": "f25273b12b094c5a8b9513a30ca60049", + "Data": { + "Status": { + "CurrentStatus": 0, + "PreviousStatus": 0, + "PrintInfo": { + "Status": 0, + "CurrentLayer": 0, + "TotalLayer": 0, + "CurrentTicks": 0, + "TotalTicks": 0, + "ErrorNumber": 0, + "Filename": "" + }, + "FileTransferInfo": { + "Status": 0, + "DownloadOffset": 0, + "CheckOffset": 0, + "FileTotalSize": 0, + "Filename": "" + } + }, + "MainboardID": "ABCD1234ABCD1234", + "TimeStamp": 8629636 + } +} +``` -`/sdcp/attributes/ABCD1234ABCD1234`: Unclear what this is used for, as it seems to repeat the Status information: `{"Id":"f25273b12b094c5a8b9513a30ca60049","Data":{"Attributes":{"CurrentStatus":0,"PreviousStatus":0,"PrintInfo":{"Status":0,"CurrentLayer":0,"TotalLayer":0,"CurrentTicks":0,"TotalTicks":0,"ErrorNumber":0,"Filename":""},"FileTransferInfo":{"Status":0,"DownloadOffset":0,"CheckOffset":0,"FileTotalSize":0,"Filename":""}},"MainboardID":"ABCD1234ABCD1234","TimeStamp":8629737}}` +`/sdcp/attributes/ABCD1234ABCD1234`: Unclear what this is used for, as it seems to repeat the Status information: +``` +{ + "Id": "f25273b12b094c5a8b9513a30ca60049", + "Data": { + "Attributes": { + "CurrentStatus": 0, + "PreviousStatus": 0, + "PrintInfo": { + "Status": 0, + "CurrentLayer": 0, + "TotalLayer": 0, + "CurrentTicks": 0, + "TotalTicks": 0, + "ErrorNumber": 0, + "Filename": "" + }, + "FileTransferInfo": { + "Status": 0, + "DownloadOffset": 0, + "CheckOffset": 0, + "FileTotalSize": 0, + "Filename": "" + } + }, + "MainboardID": "ABCD1234ABCD1234", + "TimeStamp": 8629737 + } +} +``` -`/sdcp/response/ABCD1234ABCD1234`: Responses to `/sdcp/request/XXX` messages. The `Cmd` inside Data (if present) and `RequestID` values will match what was sent in the request. Example: `{"Id":"f25273b12b094c5a8b9513a30ca60049","Data":{"Cmd":1,"Data":{"Ack":0},"RequestID":"130fdded918e4276a47e504f554bed54","MainboardID":"ABCD1234ABCD1234","TimeStamp":8567213}}` +`/sdcp/response/ABCD1234ABCD1234`: Responses to `/sdcp/request/XXX` messages. The `Cmd` inside Data (if present) and `RequestID` values will match what was sent in the request. Example: +``` +{ + "Id": "f25273b12b094c5a8b9513a30ca60049", + "Data": { + "Cmd": 1, + "Data": { + "Ack": 0 + }, + "RequestID": "130fdded918e4276a47e504f554bed54", + "MainboardID": "ABCD1234ABCD1234", + "TimeStamp": 8567213 + } +} +``` The printer subscribes to a request topic specific to its mainboard ID `/sdcp/request/ABCD1234ABCD1234`, with payloads looking like: @@ -202,8 +278,8 @@ is connected to. The file needs to be accessible at the specified URL. "TimeStamp": 1693671336846 }, "Id": "0a69ee780fbd40d7bfb95b312250bf46" -}``` +} +``` The printer will set CurrentStatus = 2 (Busy), and FileTransferInfo.Status = 0 while the transfer is in progress, -and will give Status updates. When finished, CurrentStatus will return to 0, and FileTransferInfo will be either 2 (success) -or 3 (failure). +and will give Status updates. When finished, CurrentStatus will return to 0, and FileTransferInfo will be either 2 (success) or 3 (failure).