From d9af57732b163ed81115dab0dea6386c3e564530 Mon Sep 17 00:00:00 2001 From: Thomas Reitmayr Date: Sun, 21 Jan 2024 21:20:46 +0100 Subject: [PATCH] Add initial support for the AnkerMake M5C This change allows to monitor and send MQTT messages from/to the AnkerMake M5C on the command line as well as observe the printer status via the Web GUI. It is also possible to send print jobs from your slicer to the printer without using AnkerMake Slicer or Studio (see README.md). Due to the M5's additional hardware (camera, light) still being expected to exist, the Web GUI will continuously wait for a camera connection. This should be fixed in the future. --- README.md | 10 ++++----- libflagship/mqtt.py | 47 +++++++++++++++++++++++++++++++----------- libflagship/mqttapi.py | 5 +++-- 3 files changed, 43 insertions(+), 19 deletions(-) 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, )