├── .gitignore ├── .vscode └── launch.json ├── Dockerfile ├── LICENSE ├── README.md ├── api ├── app.py ├── packets.py ├── protocol.py ├── regex_patterns.py └── socket_handler.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Python: Flask", 9 | "type": "python", 10 | "request": "launch", 11 | "module": "flask", 12 | "env": { 13 | "FLASK_APP": "./api/app.py", 14 | "FLASK_DEBUG": "1" 15 | }, 16 | "args": [ 17 | "run", 18 | "--no-debugger", 19 | "--no-reload", 20 | "--host=0.0.0.0", 21 | "--without-threads" 22 | ], 23 | "jinja": true, 24 | "justMyCode": true 25 | } 26 | ] 27 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8-slim-buster 2 | WORKDIR /app 3 | COPY requirements.txt requirements.txt 4 | RUN pip3 install -r requirements.txt 5 | COPY ./api . 6 | EXPOSE 5000 7 | CMD [ "python3", "-m" , "flask", "run", "--host=0.0.0.0", "--port=5000"] 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 01F0 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FlashForge Finder API 2 | 3 | This is an unofficial interpretation of the FlashForge API. 4 | It is served with Flask to make it as easy as possible to create your own UI. 5 | 6 | Confirmed models (so far!): 7 | * FlashForge Finder 1 8 | * FlashForge Adventurer 3 (Thanks @jptrsn) 9 | * FlashForge Adventurer 4 (Thanks @vazman13) 10 | * FlashForge Adventurer 5M Pro (Thanks @JMcrafter26) 11 | 12 | # Warning 13 | Use at your own risk. It only does reading operations but it is unofficial and may of course have bugs etc. 14 | This API is done solely by reverse engineering. 15 | 16 | # How does it work? 17 | You start the Flask app which serves a lightweight HTTP server that exposes the API functions. 18 | 19 | Example output: 20 | 21 | `http://localhost:5000/YOUR_PRINTER_IP_ADDRESS/info`: 22 | ``` 23 | { 24 | "Firmware": " V1.5 20170419", 25 | "Name": "A Finder", 26 | "SN": " 52985A58", 27 | "Tool Count": " 1", 28 | "Type": " Flashforge Finder", 29 | "X": " 140 Y: 140 Z: 140" 30 | } 31 | ``` 32 | If your printer runs on a different port (default `8899`), you can change the setting in `api/app.py`. 33 | # Run it as a Docker container 34 | Run this: 35 | 36 | `docker build --tag flashforge-api . && docker run --publish 5000:5000 flashforge-api` 37 | 38 | # Start it on your machine 39 | 1. Make sure you have Flask installed: 40 | 41 | `pip3 install -r requirements.txt` 42 | 43 | 2. Add this environment variable: 44 | 45 | **CMD**: `set FLASK_APP=app.py` 46 | 47 | **Unix Bash**: `export FLASKAPP=app.py` 48 | 49 | **PowerShell**: `$env:FLASK_APP=app.py` 50 | 51 | 3. Run it: 52 | 53 | `flask run --host=0.0.0.0 --port=5000 --without-threads` 54 | 55 | 4. By default, you should now have access to the API at `localhost:5000`. 56 | 57 | Try `http://localhost:5000/YOUR_PRINTER_IP_ADDRESS/info` to see if you get any info from the printer. 58 | 59 | # Run it in VS Code 60 | 1. Make sure you have Flask installed: 61 | 62 | `pip3 install -r requirements.txt` 63 | 64 | 2. Run `Run: Start Debugging` in VS Code, it will launch the app through `/.vscode/launch.json` 65 | # What information does the API give me? 66 | 67 | It supports: 68 | 69 | `/info`: General printer info: 70 | ``` 71 | { 72 | "Firmware": "V1.5 20170419", 73 | "Name": "My Finder", 74 | "SN": "6A8D887A", 75 | "Tool Count": "1", 76 | "Type": "Flashforge Finder", 77 | "X": "140 Y: 140 Z: 140" 78 | } 79 | ``` 80 | 81 | 82 | `/head-location`: Printer head location (as X, Y Z): 83 | ``` 84 | { 85 | "X": "86.9984", 86 | "Y": "70.5016", 87 | "Z": "140" 88 | } 89 | ``` 90 | 91 | 92 | `/temp`: Current/target temperature 93 | ``` 94 | { 95 | "TargetTemperature": "35", 96 | "Temperature": "31" 97 | } 98 | ``` 99 | 100 | `/progress`: Print progress 101 | ``` 102 | { 103 | "BytesPrinted": 4276, 104 | "BytesTotal": 4275, 105 | "PercentageCompleted": 100 106 | } 107 | ``` 108 | 109 | `/status`: Status (i.e. if it's printing or not) 110 | ``` 111 | { 112 | "Endstop": "X-max: 1 Y-max: 0 Z-max: 1", 113 | "MachineStatus": "READY", 114 | "MoveMode": "READY", 115 | "Status": "S:0 L:0 J:0 F:1" 116 | } 117 | ``` 118 | # Contributing 119 | * Suggestions and ideas are welcomed! 120 | * Does the API work and your printer model isn't listed? Let me know! 121 | * Want to add a new feature? Run `tcpdump tcp src port 8899 -A` to find commands from your printer, then share them or implement them and create a PR. 122 | -------------------------------------------------------------------------------- /api/app.py: -------------------------------------------------------------------------------- 1 | from protocol import get_info 2 | from protocol import get_head_position 3 | from protocol import get_temp 4 | from protocol import get_progress 5 | from protocol import get_status 6 | 7 | from flask import Flask 8 | from flask import jsonify 9 | 10 | from flask_cors import CORS 11 | app = Flask(__name__) 12 | CORS(app) 13 | 14 | PORT = 8899 # default port 15 | 16 | 17 | @app.route("/") 18 | def index(): 19 | return '' 20 | 21 | 22 | @app.route("//info") 23 | def info(ip_address): 24 | printer_info = get_info({'ip': ip_address, 'port': PORT}) 25 | return jsonify(printer_info) 26 | 27 | 28 | @app.route("//head-location") 29 | def head_location(ip_address): 30 | printer_info = get_head_position({'ip': ip_address, 'port': PORT}) 31 | return jsonify(printer_info) 32 | 33 | 34 | @app.route("//temp") 35 | def temp(ip_address): 36 | printer_info = get_temp({'ip': ip_address, 'port': PORT}) 37 | return jsonify(printer_info) 38 | 39 | 40 | @app.route("//progress") 41 | def progress(ip_address): 42 | printer_info = get_progress({'ip': ip_address, 'port': PORT}) 43 | return jsonify(printer_info) 44 | 45 | 46 | @app.route("//status") 47 | def status(ip_address): 48 | printer_info = get_status({'ip': ip_address, 'port': PORT}) 49 | return jsonify(printer_info) 50 | -------------------------------------------------------------------------------- /api/packets.py: -------------------------------------------------------------------------------- 1 | request_control_message = '~M601 S1\r\n' 2 | request_info_message = '~M115\r\n' 3 | request_head_position = '~M114\r\n' 4 | request_temp = '~M105\r\n' 5 | request_progress = '~M27\r\n' 6 | request_status = '~M119\r\n' 7 | -------------------------------------------------------------------------------- /api/protocol.py: -------------------------------------------------------------------------------- 1 | from packets import request_control_message 2 | from packets import request_info_message 3 | from packets import request_head_position 4 | from packets import request_temp 5 | from packets import request_progress 6 | from packets import request_status 7 | 8 | from regex_patterns import regex_for_field 9 | from regex_patterns import regex_for_coordinates 10 | from regex_patterns import regex_for_current_temperature 11 | from regex_patterns import regex_for_target_temperature 12 | from regex_patterns import regex_for_progress 13 | from socket_handler import send_and_receive 14 | 15 | 16 | import re 17 | 18 | 19 | def get_info(printer_address): 20 | """ Returns an object with basic printer information such as name etc.""" 21 | 22 | send_and_receive(printer_address, request_control_message) 23 | info_result = send_and_receive(printer_address, request_info_message) 24 | 25 | printer_info = {} 26 | info_fields = ['Type', 'Name', 'Firmware', 'SN', 'X', 'Tool Count'] 27 | for field in info_fields: 28 | regex_string = regex_for_field(field) 29 | printer_info[field] = re.search(regex_string, info_result).groups()[0] 30 | 31 | return printer_info 32 | 33 | 34 | def get_head_position(printer_address): 35 | """ Returns the current x/y/z coordinates of the printer head. """ 36 | 37 | send_and_receive(printer_address, request_control_message) 38 | info_result = send_and_receive(printer_address, request_head_position) 39 | 40 | printer_info = {} 41 | printer_info_fields = ['X', 'Y', 'Z'] 42 | for field in printer_info_fields: 43 | regex_string = regex_for_coordinates(field) 44 | printer_info[field] = re.search(regex_string, info_result).groups()[0] 45 | 46 | return printer_info 47 | 48 | 49 | def get_temp(printer_address): 50 | """ Returns printer temp. Both targeted and current. """ 51 | 52 | send_and_receive(printer_address, request_control_message) 53 | info_result = send_and_receive(printer_address, request_temp) 54 | 55 | regex_temp = regex_for_current_temperature() 56 | regex_target_temp = regex_for_target_temperature() 57 | temp = re.search(regex_temp, info_result).groups()[0] 58 | target_temp = re.search(regex_target_temp, info_result).groups()[0] 59 | 60 | return {'Temperature': temp, 'TargetTemperature': target_temp} 61 | 62 | 63 | def get_progress(printer_address): 64 | send_and_receive(printer_address, request_control_message) 65 | info_result = send_and_receive(printer_address, request_progress) 66 | 67 | regex_groups = re.search(regex_for_progress(), info_result).groups() 68 | printed = int(regex_groups[0]) 69 | total = int(regex_groups[1]) 70 | 71 | if total == 0: 72 | percentage = 0 73 | else: 74 | percentage = int(float(printed) / total * 100) 75 | 76 | return {'BytesPrinted': printed, 77 | 'BytesTotal': total, 78 | 'PercentageCompleted': percentage} 79 | 80 | 81 | def get_status(printer_address): 82 | """ Returns the current printer status. """ 83 | 84 | send_and_receive(printer_address, request_control_message) 85 | info_result = send_and_receive(printer_address, request_status) 86 | 87 | printer_info = {} 88 | printer_info_fields = ['Status', 'MachineStatus', 'MoveMode', 'Endstop'] 89 | for field in printer_info_fields: 90 | regex_string = regex_for_field(field) 91 | printer_info[field] = re.search(regex_string, info_result).groups()[0] 92 | 93 | return printer_info 94 | -------------------------------------------------------------------------------- /api/regex_patterns.py: -------------------------------------------------------------------------------- 1 | def regex_for_field(field_name): 2 | """Machine Type: Flashforge Finder""" 3 | 4 | return field_name + ': ?(.+?)\\r\\n' 5 | 6 | 7 | def regex_for_coordinates(field_name): 8 | """ X:-19.19 Y:6 Z:7.3 A:846.11 B:0 """ 9 | return field_name + ':(.+?) ' 10 | 11 | 12 | def regex_for_current_temperature(): 13 | """T0:210 /210 B:0 /0""" 14 | 15 | return 'T0:(-?[0-9].*?) ' 16 | 17 | 18 | def regex_for_target_temperature(): 19 | """ T0:210 /210 B:0 /0 """ 20 | 21 | return '\/(-?[0-9].*?) ' 22 | 23 | 24 | def regex_for_progress(): 25 | """ T0:210 /210 B:0 /0 """ 26 | 27 | return '([0-9].*)\/([0-9].*?)\\r' 28 | -------------------------------------------------------------------------------- /api/socket_handler.py: -------------------------------------------------------------------------------- 1 | import socket 2 | 3 | BUFFER_SIZE = 1024 4 | TIMEOUT_SECONDS = 5 5 | 6 | 7 | def send_and_receive(printer_adress, message_data): 8 | """Sends and receives data""" 9 | 10 | printer_socket = socket.socket() 11 | printer_socket.settimeout(TIMEOUT_SECONDS) 12 | printer_socket.connect((printer_adress['ip'], printer_adress['port'])) 13 | printer_socket.send(message_data.encode()) 14 | data = printer_socket.recv(BUFFER_SIZE) 15 | printer_socket.close() 16 | 17 | return data.decode() 18 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | flask 2 | flask-cors 3 | --------------------------------------------------------------------------------