diff --git a/README.md b/README.md index a580d7b9..3d71f59d 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # AnkerMake M5 Protocol -Welcome! This repository contains `ankerctl`, a command-line interface and web UI for monitoring, controlling and interfacing with AnkerMake M5 3D printers. +Welcome! This repository contains `ankerctl`, a command-line interface and web UI for monitoring, controlling and interfacing with AnkerMake M5 and M5C 3D printers. **NOTE:** This is our first major release and while we have tested thoroughly there may be bugs. If you encounter one please open a [Github Issue](https://github.com/Ankermgmt/ankermake-m5-protocol/issues/new/choose) -The `ankerctl` program uses [`libflagship`](documentation/developer-docs/libflagship.md), a library for communicating with the numerous different protocols required for connecting to an AnkerMake M5 printer. The `libflagship` library is also maintained in this repo, under [`libflagship/`](libflagship/). +The `ankerctl` program uses [`libflagship`](documentation/developer-docs/libflagship.md), a library for communicating with the numerous different protocols required for connecting to an AnkerMake M5 or M5C printer. The `libflagship` library is also maintained in this repo, under [`libflagship/`](libflagship/). ![Screenshot of ankerctl](/documentation/web-interface.png "Screenshot of ankerctl web interface") @@ -14,7 +14,7 @@ The `ankerctl` program uses [`libflagship`](documentation/developer-docs/libflag - Print directly from PrusaSlicer and its derivatives (SuperSlicer, Bamboo Studio, OrcaSlicer, etc.) - - Connect to AnkerMake M5 and AnkerMake APIs without using closed-source Anker software. + - Connect to AnkerMake M5/M5C and AnkerMake APIs without using closed-source Anker software. - Send raw gcode commands to the printer (and see the response). @@ -22,7 +22,7 @@ The `ankerctl` program uses [`libflagship`](documentation/developer-docs/libflag - Send print jobs (gcode files) to the printer. - - Stream camera image/video to your computer. + - Stream camera image/video to your computer (AnkerMake M5 only). - Easily monitor print status. @@ -177,7 +177,7 @@ Some examples: This project is **NOT** endorsed, affiliated with, or supported by AnkerMake. All information found herein is gathered entirely from reverse engineering using publicly available knowledge and resources. -The goal of this project is to make the AnkerMake M5 usable and accessible using only Free and Open Source Software (FOSS). +The goal of this project is to make the AnkerMake M5 and M5C usable and accessible using only Free and Open Source Software (FOSS). This project is [licensed under the GNU GPLv3](LICENSE), and copyright © 2023 Christian Iversen. diff --git a/libflagship/mqtt.py b/libflagship/mqtt.py index 18b80022..72712060 100644 --- a/libflagship/mqtt.py +++ b/libflagship/mqtt.py @@ -82,7 +82,7 @@ class _MqttMsg: size : u16le # length of packet, including header and checksum (minimum 65). m3 : u8 # Magic constant: 5 m4 : u8 # Magic constant: 1 - m5 : u8 # Magic constant: 2 + m5 : u8 # Magic constant: 2 (M5) or 1 (M5C) m6 : u8 # Magic constant: 5 m7 : u8 # Magic constant: 'F' packet_type: MqttPktType # Packet type @@ -103,9 +103,19 @@ def parse(cls, p): m7, p = u8.parse(p) packet_type, p = MqttPktType.parse(p) packet_num, p = u16le.parse(p) - time, p = u32le.parse(p) - device_guid, p = String.parse(p, 37) - padding, p = Bytes.parse(p, 11) + if m5 == 2: + # AnkerMake M5 + time, p = u32le.parse(p) + device_guid, p = String.parse(p, 37) + padding, p = Bytes.parse(p, 11) + elif m5 == 1: + # AnkerMake M5C + time = 0 # does not seem to be sent for M5C + device_guid = "none" # still present for M5C??? + padding, p = Bytes.parse(p, 12) # first 6 bytes seem to change with each packet, rest is all zeros + else: + raise ValueError(f"Unsupported mqtt message format (expected 1 or 2, but found {m5})") + data, p = Tail.parse(p) return cls(signature=signature, size=size, m3=m3, m4=m4, m5=m5, m6=m6, m7=m7, packet_type=packet_type, packet_num=packet_num, time=time, device_guid=device_guid, padding=padding, data=data), p @@ -119,9 +129,14 @@ def pack(self): p += u8.pack(self.m7) p += MqttPktType.pack(self.packet_type) p += u16le.pack(self.packet_num) - p += u32le.pack(self.time) - p += String.pack(self.device_guid, 37) - p += Bytes.pack(self.padding, 11) + if self.m5 == 2: + p += u32le.pack(self.time) + p += String.pack(self.device_guid, 37) + padding_len = 11 + elif self.m5 == 1: + padding_len = 12 + padding_missing = padding_len - len(self.padding) + p += Bytes.pack(self.padding + b"\x00" * padding_missing, padding_len) p += Tail.pack(self.data) return p @@ -131,17 +146,25 @@ class MqttMsg(_MqttMsg): @classmethod def parse(cls, p, key): p = mqtt_checksum_remove(p) - if p[6] != 2: - raise ValueError(f"Unsupported mqtt message format (expected 2, but found {p[6]})") - body, data = p[:64], mqtt_aes_decrypt(p[64:], key) + try: + body_len = {1:24, 2:64}[p[6]] + except KeyError: + raise ValueError("Unsupported mqtt message format " + + f"(expected 1 or 2, but found {p[6]})") + body, data = p[:body_len], mqtt_aes_decrypt(p[body_len:], key) res = super().parse(body + data) assert res[0].size == (len(p) + 1) return res def pack(self, key): data = mqtt_aes_encrypt(self.data, key) - self.size = 64 + len(data) + 1 - body = super().pack()[:64] + try: + body_len = {1:24, 2:64}[self.m5] + except KeyError: + raise ValueError("Unsupported mqtt message format " + + f"(expected 1 or 2, but found {self.m5})") + self.size = body_len + len(data) + 1 + body = super().pack()[:body_len] final = mqtt_checksum_add(body + data) return final diff --git a/libflagship/mqttapi.py b/libflagship/mqttapi.py index 6d45ee4d..fcd94451 100644 --- a/libflagship/mqttapi.py +++ b/libflagship/mqttapi.py @@ -51,7 +51,7 @@ def _on_message(self, client, userdata, msg): try: pkt, tail = MqttMsg.parse(msg.payload, key=self._key) except Exception as E: - hexStr =' '.join([f'0x{byte:02x}' for byte in msg.payload]) + hexStr = ' '.join([f'0x{byte:02x}' for byte in msg.payload]) log.error(f"Failed to decode mqtt message\n Exception: {E}\n Message : {hexStr}") return @@ -111,10 +111,11 @@ def make_mqtt_pkt(guid, data, packet_type=MqttPktType.Single, packet_num=0): m5=2, m6=5, m7=ord('F'), - packet_type=MqttPktType.Single, + packet_type=packet_type, packet_num=0, time=0, device_guid=guid, + padding=b'', # fixed by .pack() data=data, )