vvuk / cassini

ELEGOO 3D printer network protocol client
MIT License
28 stars 14 forks source link
elegoo resin-3d-printers saturn

Cassini

ELEGOO has a number of printers, such as the Saturn 3 Ultra that support file transfer and printing over WiFi. Chitubox, Voxeldance Tango, and Lychee support this feature, but UVTools does not offer a way to print using it.

Cassini is a command-line tool to allow getting the status of your printer(s), transferring files to them, and starting a print. It has only been tested with a Saturn 3 Ultra, but may work with other ChiTu-mainboard printers with WiFi.

Note: This is still a work in progress.

License: MIT Copyright (C) 2023 Vladimir Vukicevic

Usage

Python 3 is required. Use pip install -r requirements.txt to install dependencies.

Printer status

$ ./cassini.py status
192.168.7.128: Saturn3Ultra (ELEGOO Saturn 3 Ultra)  Status: 1
          Print Status: 2 Layers: 19/130
  File Transfer Status: 0

Printer(s) full status

$ ./cassini.py status-full

Will print out the full json status of all printers found.

Watch live print progress

$ ./cassini.py watch [interval]
_STL_B_Warriors_1_Sword_Combined_Supported.goo |███████████████████████████████████▉ ︎   | 90% 

File transfer

$ ./cassini.py [--printer printer_ip] upload MyFile.goo
15:39:15,190 INFO: Using printer Saturn3Ultra (ELEGOO Saturn 3 Ultra)
MyFile.goo |████████████████████████████████████████| 100% [5750174/5750174] (3291238.22/s)

Start a print (of an existing file)

$ ./cassini.py [--printer printer_ip] print Myfile.goo

Connect printer(s) to particular MQTT server

$ [sudo] python cassini.py [--printer printer_ip] connect-mqtt mqtt.local:1883

Probably you need to use sudo (at least on MacOS, or you'll get Permission denied: could not open /dev/bpf0. Make sure to be running Scapy as root).

If --printer is not specified, all printers found will be connected to the same MQTT server.

Protocol Description

The protocol is pretty simple. There is no encryption or any obfuscation that I could find.

There is a UDP discovery, status, and MQTT connection protocol. When the UDP command to connect to a MQTT server is given, the printer connects to a MQTT server, and can be controlled via MQTT messages. A HTTP server is also needed for file downloads.

Note: this seems nicely set up to be able to control multiple printers from one central system. Cassini doesn't implement this, though it does allow you to choose which printer to send commands to.

(SDCP might be a standard protocol on top of MQTT, but I didn't investigate too deeply.)

UDP Status and Discovery

A UDP broadcast to port 3000 with M99999 will cause all printers to send back a JSON status blob via UDP to the sending port. This response looks like this. LASTHOST below refers to the last MQTT/SDCP server this printer was connected to.

{
  "Id": "0a69ee780fbd40d7bfb95b312250bf46",
  "Data": {
    "Attributes": {
      "Name": "Saturn3Ultra",
      "MachineName": "ELEGOO Saturn 3 Ultra",
      "ProtocolVersion": "V1.0.0",
      "FirmwareVersion": "V1.4.2",
      "Resolution": "11520x5120",
      "MainboardIP": "192.168.7.128",
      "MainboardID": "ABCD1234ABCD1234",
      "SDCPStatus": 0,
      "LocalSDCPAddress": "tcp://LASTHOST:33288",
      "SDCPAddress": "",
      "Capabilities": [
        "FILE_TRANSFER",
        "PRINT_CONTROL"
      ]
    },
    "Status": {
      "CurrentStatus": 0,
      "PreviousStatus": 1,
      "PrintInfo": {
        "Status": 16,
        "CurrentLayer": 310,
        "TotalLayer": 310,
        "CurrentTicks": 3222039,
        "TotalTicks": 3218949,
        "ErrorNumber": 0,
        "Filename": "ResinXP2-ValidationMatrix.goo"
      },
      "FileTransferInfo": {
        "Status": 0,
        "DownloadOffset": 0,
        "CheckOffset": 0,
        "FileTotalSize": 0,
        "Filename": ""
      }
    }
  }
}

Sending a UDP message to port 3000 of a specific printer with the content M66666 12345 causes the printer to make a MQTT client connection to the server that sent the message on port 12345.

The Id from this message is repeated in every MQTT message below, published by either the client or the server.

MQTT/SDCP Control

When connected via MQTT, the printer subscribes to a request topic for the printer: /sdcp/request/ABCD1234ABCD1234. 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/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}}

The printer subscribes to a request topic specific to its mainboard ID /sdcp/request/ABCD1234ABCD1234, with payloads looking like:

{
    "Data": {
        "Cmd": 1,
        "Data": null,
        "From": 0,
        "MainboardID": "ABCD1234ABCD1234",
        "RequestID": "3676747651dd44b0bdbd630f38b61754",
        "TimeStamp": 1693671336726
    },
    "Id": "0a69ee780fbd40d7bfb95b312250bf46"
}

Commands discovered:

ID Description Data
0 Unknown. Sent by CHITUBOX first. None
1 Unknown. Sent by CHITUBOX after 0. None
64 Maybe a disconnect? None
128 Start printing. See below.
256 Upload file. See below.
512 Set some kind of time period. { "TimePeriod": 5000 }

128: Start Printing

{
    "Data": {
        "Cmd": 128,
        "Data": {
            "Filename": "_ResinXP2-ValidationMatrix_v2.goo",
            "StartLayer": 0
        },
        "From": 0,
        "MainboardID": "ABCD1234ABCD1234",
        "RequestID": "b353f511680d40278c48602821a9e6ec",
        "TimeStamp": 1693671341990
    },
    "Id": "0a69ee780fbd40d7bfb95b312250bf46"
}

256: Upload file

Note: in the URL, ${ipaddr} seems to be replaced by the IP address that the printer is connected to. The file needs to be accessible at the specified URL.


{
    "Data": {
        "Cmd": 256,
        "Data": {
            "Check": 0,
            "CleanCache": 1,
            "Compress": 0,
            "FileSize": 3541068,
            "Filename": "_ResinXP2-ValidationMatrix_v2.goo",
            "MD5": "205abc8fab0762ad2b0ee1f6b63b1750",
            "URL": "http://${ipaddr}:58883/f60c0718c8144b0db48b7149d4d85390.goo"
        },
        "From": 0,
        "MainboardID": "ABCD1234ABCD1234",
        "RequestID": "5b72361a76774a96b73f091bf5f79590",
        "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).