Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Exiles on 1.1 #160

Open
wants to merge 36 commits into
base: upcoming/v1.1
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
8f9a56d
Changing mqtt padding from zeros to bytes
mtoner23 Nov 28, 2023
b0f369d
remove .vscode from git ignore
mtoner23 Nov 29, 2023
6d3b9b5
1.0.1 - Fix mqtt bug release
billyjbryant Jan 15, 2024
12dfa94
Updating GHA to push docker images tagged 'edge/upcoming' from merge…
billyjbryant May 31, 2023
f157a57
Fix padding in mqttapi::make_mqtt_pkt
mtoner23 Dec 3, 2023
d6c727b
wss:// support
snoj Dec 13, 2023
a3287a8
Add initial support for the AnkerMake M5C
treitmayr Jan 21, 2024
6d16f3c
Split PPPP badge into proper PPPP and VIDEO badges
treitmayr Feb 10, 2024
8b6436a
Add support for fetching configuration via login
treitmayr Jan 29, 2024
f0d5843
Upon 'config login' try to rescue already configured printe IPs
treitmayr Feb 3, 2024
bd407f1
Add possiblity to log in via web interface
treitmayr Feb 4, 2024
509eb63
Provide more privacy by redacting the country code
treitmayr Feb 18, 2024
936f1d5
Support updating printer IP addresses from CLI
treitmayr Feb 4, 2024
7c40af7
Support updating printer IP addresses via web interface
treitmayr Feb 4, 2024
144a97b
Bind socket on broadcasts when running in Windows OS
treitmayr Feb 18, 2024
50bcc2c
Show proper error messages when printer IP address is not available
treitmayr Feb 18, 2024
601d1f0
Changed docker compose file to accept optional configuration paramete…
anselor Mar 13, 2024
f9dc158
Changed default yaml for exiles to pull pre-built exiles image
anselor Mar 16, 2024
4fe5704
Optimize chunk splitting in gcode file transfers
treitmayr Feb 10, 2024
6131f6b
Changed ankerctl to read from .env or .flaskenv on startup.
anselor Mar 17, 2024
0926fe0
Update ankersrv.js
sondregronas Jul 8, 2024
66120b0
Add some exceptions & shutdown on critical errors
sondregronas Jul 9, 2024
d761998
Forgot to import logging
sondregronas Jul 9, 2024
f41e6c6
Remove logging from exceptions
sondregronas Jul 9, 2024
eee2456
Rework auto-restart on pppp-disconnect
sondregronas Jul 9, 2024
157aa5d
Re-add deleted comments
sondregronas Jul 9, 2024
2741a0f
Add timestamp to log - everything seems to work :smile:
sondregronas Jul 9, 2024
9d82345
Add an /api/ankerctl/status endpoint
sondregronas Jul 9, 2024
b6cb72f
Don't restart an already starting service
sondregronas Jul 9, 2024
32f0550
More trial and error with pppp restarting
sondregronas Jul 9, 2024
a0a187c
Some more effort into restarting PPPP - turns out restart is unreliab…
sondregronas Jul 9, 2024
bb09336
Working code; with a caveat
sondregronas Jul 10, 2024
05f7070
Update API & update some dependencies
sondregronas Jul 10, 2024
d7314b7
Fix webserver access without config
sondregronas Jul 10, 2024
7fe40b5
Addressing some minor oversights + adding version to api status
sondregronas Jul 11, 2024
e430b9a
Reduce log level of pppp connection attempts
sondregronas Jul 11, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
FLASK_HOST=127.0.0.1
FLASK_PORT=4470
ANKERCTL_DATA=ankerctl_vol
12 changes: 9 additions & 3 deletions .github/workflows/build-and-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ on:
push:
tags:
- v**
branches:
- 'upcoming/*'
- 'master'
pull_request:
branches: [ master ]

Expand Down Expand Up @@ -48,8 +51,12 @@ jobs:
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
flavor: |
latest=${{ github.event_name == 'push' }}
tags: ${{ github.ref_name }}
latest=${{ github.ref == 'refs/heads/master' }}
tags: |
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/master' }}
type=edge,value=edge,enable=${{ startsWith(github.ref, 'refs/heads/upcoming/') }}
type=raw,value=upcoming,enable=${{ startsWith(github.ref, 'refs/heads/upcoming/') }}
type=ref,event=tag

- name: Build and push Docker images
uses: docker/[email protected]
Expand Down Expand Up @@ -87,4 +94,3 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
args: release/ankerctl*

1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
__pycache__
.DS_Store
/settings.json
.idea/
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [1.0.1] - 2024-01-15

- Fixes MQTT connection errors post AnkerMake Firmware Upgrades

## [1.0.0] - 2023-05-24

- Version 1.0.0!
Expand Down
34 changes: 34 additions & 0 deletions Dockerfile.runtime
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Second stage: runtime environment
FROM python:3.11-slim

# Copy the requirements file
COPY requirements.txt .

# Disable warning about running as "root"
ENV PIP_ROOT_USER_ACTION=ignore

# Disable caching - we just want the output
ENV PIP_NO_CACHE_DIR=1

# Install the dependencies
RUN pip install --upgrade pip && \
pip install -r requirements.txt


# Set the working directory to /app
WORKDIR /app

RUN mkdir -p /root/.config/

# Copy the script and libraries
COPY ankerctl.py /app/
COPY web /app/web/
COPY ssl /app/ssl/
COPY static /app/static/
COPY libflagship /app/libflagship/
COPY cli /app/cli/

STOPSIGNAL SIGINT

ENTRYPOINT ["/app/ankerctl.py"]
CMD ["webserver", "run"]
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
127 changes: 102 additions & 25 deletions ankerctl.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import json
import click
import platform
import getpass
import webbrowser
import logging as log
from os import path, environ
from rich import print # you need python3
Expand All @@ -15,17 +17,23 @@
import cli.util
import cli.pppp
import cli.checkver # check python version
import cli.countrycodes

import libflagship.httpapi
import libflagship.logincache
import libflagship.seccode

from libflagship.util import enhex
from libflagship.mqtt import MqttMsgType
from libflagship.pppp import PktLanSearch, P2PCmdType, P2PSubCmdType, FileTransfer
from libflagship.pppp import P2PCmdType, P2PSubCmdType, FileTransfer
from libflagship.ppppapi import FileUploadInfo, PPPPError


from dotenv import load_dotenv

load_dotenv() # take environment variables from .env.


class Environment:
def __init__(self):
pass
Expand Down Expand Up @@ -214,22 +222,26 @@ def pppp(): pass


@pppp.command("lan-search")
@click.option("--store", "-s", is_flag=True, help="Store found IP address(es) in configuration file")
@pass_env
def pppp_lan_search(env):
def pppp_lan_search(env, store):
"""
Attempt to find available printers on local LAN.

Works by broadcasting a LAN_SEARCH packet, and waiting for a reply.
"""
api = cli.pppp.pppp_open_broadcast(dumpfile=env.pppp_dump)
try:
api.send(PktLanSearch())
resp = api.recv(timeout=1.0)
except TimeoutError:
found_printers = dict()
for duid, ip in cli.pppp.pppp_find_printer_ip_addresses(dumpfile=env.pppp_dump):
log.info(f"Printer [{duid}] is online at {ip}")
found_printers[duid] = ip

if not found_printers:
log.error("No printers responded within timeout. Are you connected to the same network as the printer?")
else:
if isinstance(resp, libflagship.pppp.PktPunchPkt):
log.info(f"Printer [{str(resp.duid)}] is online at {str(api.addr[0])}")

# if requested, update stored printer IP addresses
if store and found_printers:
log.info(f"Checking configured printer IP addresses:")
cli.config.update_printer_ip_addresses(env.config, found_printers)


@pppp.command("print-file")
Expand Down Expand Up @@ -382,6 +394,82 @@ def config_decode(env, fd):
print(json.dumps(cache, indent=4))


@config.command("login")
@click.argument("country", required=False, metavar="[COUNTRY (2 letter code)]")
@click.argument("email", required=False)
@click.argument("password", required=False)
@pass_env
def config_login(env, country, email, password):
"""
Fetch configuration by logging in with provided credentials.
"""

try:
with env.config.open() as cfg:
if cfg.account:
if country is None and cfg.account.country:
country = cfg.account.country
log.info(f"Country: {country.upper()}")
if email is None:
email = cfg.account.email
log.info(f"Email address: {email}")
except KeyError:
pass

# interactive user input (only if arguments not given on command line)
if email is None:
email = input("Please enter your email address: ").strip()

if password is None:
password = getpass.getpass("Please enter your password: ")

if country:
country = country.upper()
while not cli.countrycodes.code_to_country(country):
country = input("Please enter your country (2 digit code): ").strip().upper()

region = libflagship.logincache.guess_region(country)
login = None
tries = 3
captcha = {"id": None, "answer": None}
# retry until login was successful
while not login and tries > 0:
tries -= 1
try:
login = cli.config.fetch_config_by_login(email, password, region, env.insecure,
captcha_id=captcha["id"], captcha_answer=captcha["answer"])
break
except libflagship.httpapi.APIError as E:
# check if the error is actually a request to solve a captcha
if E.json and "data" in E.json:
data = E.json["data"]
if "captcha_id" in data:
captcha = {
"id": data["captcha_id"],
"img": data["item"]
}

# ask the user to resolve the captcha
if captcha["id"]:
log.warning(f"Login requires solving a captcha")
if webbrowser.open(captcha["img"], new=2):
captcha["answer"] = input("Please enter the captcha answer: ").strip()
else:
log.critical(f"Cannot open webbrowser for displaying captcha, aborting.")
tries = 0
else:
log.critical(f"Unknown login error: {E}")
tries = 0

if login:
log.info(f"Login successful, importing configuration from server..")

# load remaining configuration items from the server
cli.config.import_config_from_server(env.config, login, env.insecure)

log.info("Finished import")


@config.command("import")
@click.argument("fd", required=False, type=click.File("r"), metavar="path/to/login.json")
@pass_env
Expand Down Expand Up @@ -415,23 +503,11 @@ def config_import(env, fd):

log.info("Loading cache..")

# extract auth token
# load the login configuration from the provided file
cache = libflagship.logincache.load(fd.read())["data"]
auth_token = cache["auth_token"]

# extract account region
region = libflagship.logincache.guess_region(cache["ab_code"])

try:
config = cli.config.load_config_from_api(auth_token, region, env.insecure)
except libflagship.httpapi.APIError as E:
log.critical(f"Config import failed: {E} "
"(auth token might be expired: make sure Ankermake Slicer can connect, then try again)")
except Exception as E:
log.critical(f"Config import failed: {E}")

# save config to json file named `ankerctl/default.json`
env.config.save("default", config)
# import the remaining configuration from the server
cli.config.import_config_from_server(env.config, cache, env.insecure)

log.info("Finished import")

Expand All @@ -455,6 +531,7 @@ def config_show(env):
print(f" auth_token: {cfg.account.auth_token[:10]}...<REDACTED>")
print(f" email: {cfg.account.email}")
print(f" region: {cfg.account.region.upper()}")
print(f" country: {'<REDACTED>' if cfg.account.country else ''}")
print()

log.info("Printers:")
Expand Down
1 change: 1 addition & 0 deletions build-exe.bat
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pyinstaller -F --add-data templates:templates --add-data web:web --add-data static:static --add-data ssl:ssl --add-data cli:cli --add-data libflagship:libflagship --add-data specification:specification --icon .\packaging\logo.ico .\ankerctl.py
Loading