Skip to content

Commit

Permalink
Add initial support for the AnkerMake M5C
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
treitmayr committed Jan 22, 2024
1 parent bcf6855 commit d9af577
Show file tree
Hide file tree
Showing 3 changed files with 43 additions and 19 deletions.
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -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")

Expand All @@ -14,15 +14,15 @@ 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).

- Low-level access to MQTT, PPPP and HTTPS APIs.

- 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.

Expand Down Expand Up @@ -177,7 +177,7 @@ Some examples:

This project is **<u>NOT</u>** 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.

Expand Down
47 changes: 35 additions & 12 deletions libflagship/mqtt.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand All @@ -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

Expand All @@ -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

Expand Down
5 changes: 3 additions & 2 deletions libflagship/mqttapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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,
)

Expand Down

0 comments on commit d9af577

Please sign in to comment.