├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .github └── workflows │ └── build-and-publish.yml ├── .gitignore ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── LICENSE ├── README.md ├── bin ├── python ├── python3 ├── python3.9 ├── wheel ├── wheel-3.9 ├── wheel3 └── wheel3.9 ├── img └── fakeprinter.png ├── pyproject.toml ├── pyvenv.cfg ├── requirements-test.txt ├── requirements.txt ├── setup.cfg ├── src └── uart_wifi │ ├── __init__.py │ ├── __main__.py │ ├── communication.py │ ├── errors.py │ ├── response.py │ ├── scripts │ ├── __init__.py │ ├── __pycache__ │ │ ├── __init__.cpython-39.pyc │ │ └── fake_printer.cpython-39.pyc │ ├── fake_printer │ ├── fake_printer.py │ ├── monox │ └── monox.py │ └── simulate_printer.py └── tests ├── __init__.py ├── test_communication.py └── test_errors.py /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.224.2/containers/python-3/.devcontainer/base.Dockerfile 2 | 3 | # [Choice] Python version (use -bullseye variants on local arm64/Apple Silicon): 3, 3.10, 3.9, 3.8, 3.7, 3.6, 3-bullseye, 3.10-bullseye, 3.9-bullseye, 3.8-bullseye, 3.7-bullseye, 3.6-bullseye, 3-buster, 3.10-buster, 3.9-buster, 3.8-buster, 3.7-buster, 3.6-buster 4 | ARG VARIANT="3.10-buster" 5 | FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT} 6 | # [Choice] Node.js version: none, lts/*, 16, 14, 12, 10 7 | ARG NODE_VERSION="none" 8 | RUN apt update && apt install git 9 | 10 | 11 | 12 | 13 | 14 | # [Optional] If your pip requirements rarely change, uncomment this section to add them to the image. 15 | # COPY requirements.txt /tmp/pip-tmp/ 16 | # RUN pip3 --disable-pip-version-check --no-cache-dir install -r /tmp/pip-tmp/requirements.txt \ 17 | # && rm -rf /tmp/pip-tmp 18 | 19 | # [Optional] Uncomment this section to install additional OS packages. 20 | # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 21 | # && apt-get -y install --no-install-recommends 22 | RUN sudo apt-get update && export DEBIAN_FRONTEND=noninteractive && sudo apt-get -y install --no-install-recommends telnet python3 python3-pip 23 | 24 | # [Optional] Uncomment this line to install global node packages. 25 | # RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g " 2>&1 -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.224.2/containers/python-3 3 | { 4 | "name": "Python 3", 5 | "build": { 6 | "dockerfile": "Dockerfile", 7 | "context": "..", 8 | "args": { 9 | // Update 'VARIANT' to pick a Python version: 3, 3.10, 3.9, 3.8, 3.7, 3.6 10 | // Append -bullseye or -buster to pin to an OS version. 11 | // Use -bullseye variants on local on arm64/Apple Silicon. 12 | "VARIANT": "3.10-buster", 13 | // Options 14 | "NODE_VERSION": "lts/*" 15 | } 16 | }, 17 | "postCreateCommand": "pip3 install -r requirements.txt; pip3 install -r requirements-test.txt;", 18 | // Set *default* container specific settings.json values on container create. 19 | "settings": { 20 | "files.eol": "\n", 21 | "editor.tabSize": 4, 22 | // "python.pythonPath": "/usr/bin/python3", 23 | "python.analysis.autoSearchPaths": false, 24 | "python.linting.pylintEnabled": true, 25 | "python.linting.enabled": true, 26 | "python.formatting.provider": "black", 27 | "editor.formatOnPaste": false, 28 | "editor.formatOnSave": true, 29 | "editor.formatOnType": true, 30 | "files.trimTrailingWhitespace": true, 31 | "python.defaultInterpreterPath": "/usr/local/bin/python", 32 | "python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8", 33 | "python.formatting.blackPath": "/usr/local/py-utils/bin/black", 34 | "python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf", 35 | "python.linting.banditPath": "/usr/local/py-utils/bin/bandit", 36 | "python.linting.flake8Path": "/usr/local/py-utils/bin/flake8", 37 | "python.linting.mypyPath": "/usr/local/py-utils/bin/mypy", 38 | "python.linting.pycodestylePath": "/usr/local/py-utils/bin/pycodestyle", 39 | "python.linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle", 40 | "python.linting.pylintPath": "/usr/local/py-utils/bin/pylint", 41 | "python.analysis.extraPaths": ["./src"], 42 | "terminal.integrated.profiles.linux": { 43 | "zsh": { 44 | "path": "/usr/bin/zsh" 45 | } 46 | }, 47 | "terminal.integrated.defaultProfile.linux": "zsh" 48 | }, 49 | // Add the IDs of extensions you want installed when the container is created. 50 | "extensions": [ 51 | "ms-python.vscode-pylance", 52 | "visualstudioexptteam.vscodeintellicode", 53 | "esbenp.prettier-vscode", 54 | "xirider.livecode", 55 | "the-compiler.python-tox", 56 | "ms-python.python", 57 | "GitHub.copilot", 58 | "github.vscode-pull-request-github", 59 | "ryanluker.vscode-coverage-gutters", 60 | "ms-python.vscode-pylance", 61 | "pamaron.pytest-runner", 62 | "GitHub.vscode-pull-request-github", 63 | "donjayamanne.python-extension-pack", 64 | "esbenp.prettier-vscode", 65 | "eamodio.gitlens", 66 | "GitHub.copilot" 67 | ], 68 | "containerEnv": { 69 | "PYTHONPATH ": "/workspaces/anycubic-python/src" 70 | }, 71 | "remoteEnv": { 72 | "PYTHONPATH": "${containerEnv:PYTHONPATH }:/workspaces/anycubic-python/src" 73 | }, 74 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 75 | "forwardPorts": [6000], 76 | // Use 'postCreateCommand' to run commands after the container is created. 77 | // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. 78 | "remoteUser": "vscode", 79 | "features": { 80 | "git": "latest" 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /.github/workflows/build-and-publish.yml: -------------------------------------------------------------------------------- 1 | name: Build anycubic-python 🐍 distributions 2 | on: push 3 | jobs: 4 | lint: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v2 8 | - name: Set up Python 3.8 9 | uses: actions/setup-python@v1 10 | with: 11 | python-version: 3.8 12 | - name: Install dependencies 13 | run: | 14 | python -m pip install --upgrade pip 15 | pip install pylint flake8 16 | - name: Analysing the code with pylint 17 | run: | 18 | find . -name '*.py' -exec pylint {} \; 19 | - name: Analyze trhe code with flake8 20 | run: | 21 | flake8 src/uart_wifi 22 | 23 | build: 24 | runs-on: ubuntu-18.04 25 | name: Build Python 🐍 ${{ matrix.python-version }} 26 | steps: 27 | - uses: actions/checkout@v2 28 | - uses: actions/setup-python@v2 29 | name: Set up Python 30 | with: 31 | python-version: "3.9" 32 | architecture: x64 33 | - run: >- 34 | python -m 35 | pip install 36 | build 37 | --user 38 | 39 | - name: Build a binary wheel and a source tarball 40 | run: >- 41 | python -m 42 | build 43 | --sdist 44 | --wheel 45 | --outdir dist/ 46 | Test: 47 | runs-on: ubuntu-18.04 48 | needs: 49 | - build 50 | name: Test 🐍 ${{ matrix.python-version }} 51 | steps: 52 | - uses: actions/checkout@v2 53 | - uses: actions/setup-python@v2 54 | name: Set up Python 55 | with: 56 | python-version: "3.9" 57 | architecture: x64 58 | - run: | 59 | python -m pip install --upgrade pip; 60 | pip install -r requirements.txt; 61 | pip install -r requirements-test.txt; 62 | - name: Test 63 | run: >- 64 | export PYTHONPATH=$(pwd)/src/; 65 | pytest 66 | publish: 67 | needs: 68 | - Test 69 | - lint 70 | runs-on: ubuntu-18.04 71 | name: Build 📦 and ship 🏗️ new tags to PyPI 72 | steps: 73 | - uses: actions/checkout@v2 74 | - uses: actions/setup-python@v2 75 | name: Set up Python 76 | with: 77 | python-version: pypy-3.9 78 | architecture: x64 79 | - run: >- 80 | python -m 81 | pip install 82 | build 83 | --user 84 | - name: Build a binary wheel and a source tarball 85 | run: >- 86 | python -m 87 | build 88 | --sdist 89 | --wheel 90 | --outdir dist/ 91 | - name: Publish 92 | if: startsWith(github.ref, 'refs/tags') 93 | uses: pypa/gh-action-pypi-publish@master 94 | with: 95 | password: ${{ secrets.PYPI_API_TOKEN }} 96 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/__pycache__/ 2 | src/uart_wifi.egg-info/ 3 | .pytest_cache 4 | dist/ 5 | bin/ 6 | bin/python* 7 | src/scripts/*.py 8 | build/ 9 | .history/ 10 | .tox 11 | lib/python* -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ms-python.python", 4 | "esbenp.prettier-vscode", 5 | "eamodio.gitlens" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.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 | "args": ["-i", "0.0.0.0", "-p6000"], 9 | "name": "Server", 10 | "type": "python", 11 | "request": "launch", 12 | "program": "src/uart_wifi/scripts/fake_printer.py", 13 | "console": "integratedTerminal" 14 | }, 15 | { 16 | "name": "Python: Debug Tests", 17 | "type": "python", 18 | "request": "launch", 19 | "program": "${file}", 20 | "purpose": ["debug-test"], 21 | "console": "integratedTerminal", 22 | "justMyCode": false 23 | }, 24 | { 25 | "name": "tox", 26 | "args": [], 27 | "type": "python", 28 | "request": "launch", 29 | "program": "tox", 30 | "console": "integratedTerminal" 31 | }, 32 | { 33 | "args": ["-i", "127.0.0.1", "-p", "6000", "--command=getstatus,", "-d"], 34 | "name": "status", 35 | "type": "python", 36 | "request": "launch", 37 | "program": "src/uart_wifi/scripts/monox.py", 38 | "console": "integratedTerminal" 39 | }, 40 | { 41 | "args": ["-i", "127.0.0.1", "-p", "6000", "--command=sysinfo,", "-d"], 42 | "name": "sysinfo", 43 | "type": "python", 44 | "request": "launch", 45 | "program": "src/uart_wifi/scripts/monox.py", 46 | "console": "integratedTerminal" 47 | }, 48 | { 49 | "args": ["-i", "127.0.0.1", "-p", "6000", "--command=timeout,", "-d"], 50 | "name": "timeout", 51 | "type": "python", 52 | "request": "launch", 53 | "program": "src/uart_wifi/scripts/monox.py", 54 | "console": "integratedTerminal" 55 | }, 56 | { 57 | "args": [ 58 | "-i", 59 | "127.0.0.1", 60 | "-p", 61 | "6000", 62 | "--raw", 63 | "--command=goprint,1.pwmb,end", 64 | "-d" 65 | ], 66 | "name": "print", 67 | "type": "python", 68 | "request": "launch", 69 | "program": "src/uart_wifi/scripts/monox.py", 70 | "console": "integratedTerminal" 71 | }, 72 | { 73 | "args": [ 74 | "-i", 75 | "192.168.1.254", 76 | "-p", 77 | "6000", 78 | "--command=getstatus", 79 | "-d" 80 | ], 81 | "name": "My Printer Status", 82 | "type": "python", 83 | "request": "launch", 84 | "program": "src/uart_wifi/scripts/monox.py", 85 | "console": "integratedTerminal" 86 | }, 87 | { 88 | "args": ["-i", "127.0.0.1", "-p", "6000", "--command=multi,", "-d", "-d"], 89 | "name": "multi", 90 | "type": "python", 91 | "request": "launch", 92 | "program": "src/uart_wifi/scripts/monox.py", 93 | "console": "integratedTerminal" 94 | }, 95 | { 96 | "args": [ 97 | "-i", 98 | "127.0.0.1", 99 | "-p", 100 | "6000", 101 | "--command=getstatus,", 102 | "-d", 103 | "-d", 104 | "-d" 105 | ], 106 | "name": "Client3", 107 | "type": "python", 108 | "request": "launch", 109 | "program": "src/uart_wifi/scripts/monox.py", 110 | "console": "integratedTerminal" 111 | }, 112 | { 113 | "args": [ 114 | "-i", 115 | "127.0.0.1", 116 | "-p", 117 | "6000", 118 | "--command=shutdown,", 119 | "-d", 120 | "-d", 121 | "-d", 122 | "-d", 123 | "-d", 124 | "-d" 125 | ], 126 | "name": "shutdown", 127 | "type": "python", 128 | "request": "launch", 129 | "program": "src/uart_wifi/scripts/monox.py", 130 | "console": "integratedTerminal" 131 | } 132 | ], 133 | "compounds": [ 134 | { 135 | "name": "Server/Client", 136 | "configurations": ["Server", "status", "multi", "shutdown"] 137 | } 138 | ] 139 | } 140 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "terminal.integrated.env.linux": { 3 | "PYTHONPATH": "$PYTHONPATH:/workspaces/anycubic-python/src:/workspaces/anycubic-python/src/uart_wifi"}, 4 | "python.analysis.logLevel": "Trace", 5 | "python.defaultInterpreterPath": "/usr/local/bin/python3", 6 | "python.autoComplete.extraPaths": ["/workspaces/anycubic-python/src"], 7 | "cSpell.words": [ 8 | "adamoutler", 9 | "gopause", 10 | "goprint", 11 | "goresume", 12 | "gostop", 13 | "INET", 14 | "monox", 15 | "Outler", 16 | "pwmb", 17 | "sysinfo", 18 | "uart", 19 | "Zhome", 20 | "Zmove" 21 | ], 22 | 23 | "python.linting.pylintEnabled": true, 24 | "python.linting.pycodestyleEnabled": true, 25 | "python.linting.flake8Enabled": true, 26 | "python.formatting.provider": "black", 27 | "python.formatting.blackArgs": [ 28 | "--line-length", 29 | "79" 30 | ], 31 | "python.envFile": "${workspaceFolder}/dev.env", 32 | "python.testing.pytestArgs": [], 33 | "python.testing.unittestEnabled": false, 34 | "python.testing.pytestEnabled": true, 35 | "python.analysis.extraPaths": [ 36 | "/workspaces/anycubic-python/src" 37 | ], 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Communicates with Anycubic uart-wifi protocol 2 | Copyright (C) 2022 Adam Outler 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # About 2 | This is a library to provide support for Mono X Printers. 3 | 4 | # uart-wifi 5 | the [uart-wifi](https://pypi.org/project/uart-wifi/) library can be downloaded from PyPI. It contains required python tools for communicating with MonoX Printers. To install, simply install Python, then type `pip install uart-wifi`. After which, you can create fake printers and communicate with them. 6 | ![](img/fakeprinter.png) 7 | 8 | ## monox.py 9 | A command line script to gather information from the Mono X printer. This is tested working on the Anycubic Mono X 6k and should work on any Mono X or Mono SE printer. 10 | 11 | Usage: monox.py -i -c 12 | args: 13 | -i [--ipaddress=] - The IP address which your Anycubic Mono X can be reached 14 | 15 | -c [--command=] - The command to send. 16 | 17 | Commands may be used one-at-a-time. Only one command may be sent and it is expected to be in the format below. 18 | 19 | Command: getstatus - Returns a list of printer statuses. 20 | 21 | Command: getfile - returns a list of files in format : . When referring to the file via command, use the . 22 | 23 | Command: sysinfo - returns Model, Firmware version, Serial Number, and wifi network. 24 | 25 | Command: getwifi - displays the current wifi network name. 26 | 27 | Command: gopause - pauses the current print. 28 | 29 | Command: goresume - ends the current print. 30 | 31 | Command: gostop,end - stops the current print. 32 | 33 | Command: delfile,,end - deletes a file. 34 | 35 | Command: gethistory,end - gets the history and print settings 36 | of previous prints. 37 | 38 | Command: delhistory,end - deletes printing history. 39 | 40 | Command: goprint,,end - Starts a print of the 41 | requested file 42 | 43 | Command: getPreview1,,end - returns a list of dimensions used for the print. 44 | 45 | ## fake_printer.py 46 | A command line script to simulate a MonoX 3D printer for testing purposes. You can simulate a fleet of Mono X 3D printers! 47 | 48 | Usage: fake_printer.py -i -c 49 | args: 50 | [-i, [--ipaddress=]] - The IP address which to acknowledge requests. This defaults to any or 0.0.0.0. 51 | 52 | [-p [--port=]] - The port to listen on. This defaults to 6000. 53 | -------------------------------------------------------------------------------- /bin/python: -------------------------------------------------------------------------------- 1 | /usr/local/bin/python3 -------------------------------------------------------------------------------- /bin/python3: -------------------------------------------------------------------------------- 1 | python -------------------------------------------------------------------------------- /bin/python3.9: -------------------------------------------------------------------------------- 1 | python -------------------------------------------------------------------------------- /bin/wheel: -------------------------------------------------------------------------------- 1 | #!/workspaces/anycubic-python/bin/python 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import sys 5 | from wheel.cli import main 6 | if __name__ == '__main__': 7 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 8 | sys.exit(main()) 9 | -------------------------------------------------------------------------------- /bin/wheel-3.9: -------------------------------------------------------------------------------- 1 | #!/workspaces/anycubic-python/bin/python 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import sys 5 | from wheel.cli import main 6 | if __name__ == '__main__': 7 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 8 | sys.exit(main()) 9 | -------------------------------------------------------------------------------- /bin/wheel3: -------------------------------------------------------------------------------- 1 | #!/workspaces/anycubic-python/bin/python 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import sys 5 | from wheel.cli import main 6 | if __name__ == '__main__': 7 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 8 | sys.exit(main()) 9 | -------------------------------------------------------------------------------- /bin/wheel3.9: -------------------------------------------------------------------------------- 1 | #!/workspaces/anycubic-python/bin/python 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import sys 5 | from wheel.cli import main 6 | if __name__ == '__main__': 7 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 8 | sys.exit(main()) 9 | -------------------------------------------------------------------------------- /img/fakeprinter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamoutler/anycubic-python/0c0363373c8d3689f982d6f12ededee93468c6ab/img/fakeprinter.png -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools>=42", 4 | "wheel" 5 | ] 6 | build-backend = "setuptools.build_meta" 7 | -------------------------------------------------------------------------------- /pyvenv.cfg: -------------------------------------------------------------------------------- 1 | home = /usr/local 2 | implementation = CPython 3 | version_info = 3.9.10.final.0 4 | virtualenv = 20.14.0 5 | include-system-site-packages = false 6 | base-prefix = /usr/local 7 | base-exec-prefix = /usr/local 8 | base-executable = /usr/local/bin/python3 9 | -------------------------------------------------------------------------------- /requirements-test.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | lint 3 | asyncio 4 | pytest-socket 5 | tomli 6 | pytest-asyncio 7 | prettier -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Pillow 2 | build 3 | typing 4 | asyncio 5 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = uart-wifi 3 | version = 0.2.1 4 | author = Adam Outler 5 | author_email = adamoutler@hackedyour.info 6 | description = Interface for Anycubic Mono X and similar printers. 7 | long_description = file: README.md 8 | long_description_content_type = text/markdown 9 | url = https://github.com/adamoutler/anycubic-python 10 | project_urls = 11 | Bug Tracker = https://github.com/adamoutler/anycubic-python/issues 12 | classifiers = 13 | Programming Language :: Python :: 3 14 | License :: OSI Approved :: GNU General Public License v3 (GPLv3) 15 | Operating System :: OS Independent 16 | Development Status :: 2 - Pre-Alpha 17 | Intended Audience :: Developers 18 | Natural Language :: English 19 | Topic :: Communications 20 | Topic :: Home Automation 21 | Topic :: Printing 22 | 23 | [options] 24 | package_dir = 25 | = src 26 | packages = find: 27 | python_requires = >=3.6 28 | scripts= 29 | ./src/uart_wifi/scripts/monox.py 30 | ./src/uart_wifi/scripts/fake_printer.py 31 | ./src/uart_wifi/scripts/monox 32 | ./src/uart_wifi/scripts/fake_printer 33 | 34 | 35 | 36 | [tool:pytest] 37 | testpaths = tests/ 38 | norecursedirs = 39 | .git 40 | .tox 41 | .env 42 | dist 43 | build 44 | migrations 45 | addopts = -ra -q --verbose 46 | python_classes = Test Describe 47 | python_files = test_*.py 48 | python_functions = test_ it_ they_ but_ and_it_ 49 | asyncio_mode=auto 50 | 51 | [options.packages.find] 52 | where =src 53 | 54 | [coverage:run] 55 | concurrency = multiprocessing 56 | -------------------------------------------------------------------------------- /src/uart_wifi/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamoutler/anycubic-python/0c0363373c8d3689f982d6f12ededee93468c6ab/src/uart_wifi/__init__.py -------------------------------------------------------------------------------- /src/uart_wifi/__main__.py: -------------------------------------------------------------------------------- 1 | """Main executable. Imports the monox script and causes execution.""" 2 | from uart_wifi.scripts import monox 3 | 4 | print(f'this should be executed from "{monox.__file__}".') 5 | -------------------------------------------------------------------------------- /src/uart_wifi/communication.py: -------------------------------------------------------------------------------- 1 | """Communications with MonoX devices. -Adam Outler - 2 | email: adamoutler(a)hackedyour.info 3 | The development environment is Visual Studio Code. 4 | See launch.json for auto-config. 5 | """ 6 | 7 | import logging 8 | import os 9 | import select 10 | import sys 11 | from queue import Empty 12 | 13 | from socket import AF_INET, SOCK_STREAM, socket 14 | import tempfile 15 | import time 16 | from typing import Iterable 17 | 18 | from uart_wifi.errors import ConnectionException 19 | 20 | from .response import ( 21 | FileList, 22 | InvalidResponse, 23 | MonoXPreviewImage, 24 | MonoXResponseType, 25 | MonoXStatus, 26 | MonoXSysInfo, 27 | ) 28 | 29 | # Port to listen on 30 | 31 | HOST = "192.168.1.254" 32 | COMMAND = "getstatus" 33 | endRequired = ["goprint", "gostop", "gopause", "delfile"] 34 | END = ",end" 35 | ENCODING = "gbk" 36 | _LOGGER = logging.getLogger(__name__) 37 | Any = object() 38 | MAX_REQUEST_TIME = 10 # seconds 39 | Response = Iterable[MonoXResponseType] 40 | 41 | 42 | class UartWifi: 43 | """Mono X Class""" 44 | 45 | max_request_time = MAX_REQUEST_TIME 46 | 47 | def __init__(self, ip_address: str, port: int) -> None: 48 | """Create a communications UartWifi class. 49 | :ip_address: The IP to initiate communications with. 50 | :port: The port to use. 51 | """ 52 | self.server_address = (ip_address, port) 53 | self.raw = False 54 | self.telnet_socket = socket(AF_INET, SOCK_STREAM) 55 | 56 | def set_maximum_request_time(self, max_request_time: int) -> None: 57 | """Set the maximum time to wait for a response. 58 | :max_request_time: The maximum time to wait for a response. 59 | """ 60 | self.max_request_time = max_request_time 61 | 62 | def set_raw(self, raw: bool = True) -> None: 63 | """Set raw mode. 64 | :raw: Set to true if we are outputting raw data instead of processed 65 | classes. 66 | """ 67 | self.raw = raw 68 | 69 | def send_request( 70 | self, message_to_be_sent: str 71 | ) -> Iterable[MonoXResponseType]: 72 | """sends the Mono X request. 73 | :message_to_be_sent: The properly-formatted uart-wifi message as it is 74 | to be sent. 75 | :returns: an object from Response class. 76 | """ 77 | 78 | return_value = self._send_request(message_to_be_sent) 79 | return return_value 80 | 81 | def _send_request(self, message: str) -> Iterable[MonoXResponseType]: 82 | """sends the Mono X request. 83 | :message_to_be_sent: The properly-formatted uart-wifi message as it is 84 | to be sent. 85 | :returns: an object from Response class. 86 | """ 87 | return_value = self._async_send_request(message) 88 | return return_value 89 | 90 | def _async_send_request( 91 | self, message_to_be_sent: str 92 | ) -> Iterable[MonoXResponseType]: 93 | """sends the Mono X request. 94 | :message_to_be_sent: The properly-formatted uart-wifi message as it is 95 | to be sent. 96 | :returns: an object from Response class. 97 | """ 98 | request = bytes(message_to_be_sent, "utf-8") 99 | received: str = _do_request( 100 | self.telnet_socket, 101 | self.server_address, 102 | request, 103 | self.max_request_time, 104 | ) 105 | if self.raw: 106 | return received 107 | processed = _do_handle(received) 108 | return processed 109 | 110 | 111 | def _do_request( 112 | sock: socket, 113 | socket_address: tuple, 114 | to_be_sent: bytes, 115 | max_request_time: int, 116 | ) -> str: 117 | """Perform the request 118 | 119 | :param sock: the socket to use for the request 120 | :param request: the request to send 121 | :return: a MonoX object""" 122 | text_received = "" 123 | try: 124 | sock = _setup_socket(socket_address) 125 | sock.sendall(to_be_sent) 126 | sent_string = to_be_sent.decode() 127 | if sent_string.endswith("shutdown"): 128 | return "shutdown,end" 129 | if sent_string.startswith("b'getPreview2"): 130 | text_received = bytearray() 131 | print(text_received) 132 | end_time = current_milli_time() + (max_request_time * 1000) 133 | while ( 134 | not str(text_received).endswith(END) 135 | and current_milli_time() < end_time 136 | ): 137 | text_received.extend(sock.recv(1)) 138 | else: 139 | 140 | read_list = [sock] 141 | port_read_delay = current_milli_time() 142 | end_time = current_milli_time() + (max_request_time * 1000) 143 | text_received = handle_request( 144 | sock, 145 | text_received, 146 | end_time, 147 | read_list, 148 | port_read_delay, 149 | max_request_time, 150 | ) 151 | 152 | except ( 153 | OSError, 154 | ConnectionRefusedError, 155 | ConnectionResetError, 156 | ) as exception: 157 | raise ConnectionException( 158 | "Could not connect to AnyCubic printer at " + socket_address[0] 159 | ) from exception 160 | finally: 161 | sock.close() 162 | return text_received 163 | 164 | 165 | def handle_request( 166 | sock, text_received, end_time, read_list, port_read_delay, max_request_time 167 | ) -> str: 168 | """performs the request handling""" 169 | while True: 170 | current_time = current_milli_time() 171 | if end_time > current_time or port_read_delay > current_time: 172 | readable, [], [] = select.select( 173 | read_list, [], [], max_request_time 174 | ) 175 | for read_port in readable: 176 | if read_port is sock: 177 | port_read_delay = current_milli_time() + 8000 178 | text_received += str(read_port.recv(1).decode()) 179 | if end_time < current_milli_time() or text_received.endswith(",end"): 180 | break 181 | return text_received 182 | 183 | 184 | def _setup_socket(socket_address): 185 | """Setup the socket for communication 186 | socket_address: the tupple consisting of (ip_address, port). 187 | """ 188 | _LOGGER.debug("connecting to %s", socket_address) 189 | sock = socket(AF_INET, SOCK_STREAM) 190 | sock.settimeout(2) 191 | sock.connect(socket_address) 192 | sock.settimeout(MAX_REQUEST_TIME) 193 | return sock 194 | 195 | 196 | def __do_preview2(received_message: bytearray()): 197 | """Handles preview by writing to file. 198 | :received_message: The message, as received from UART wifi 199 | protocol, to be converted to an image. 200 | """ 201 | 202 | filename = received_message.decode("utf_8").split(",", 3)[1] 203 | file = tempfile.gettempdir() + os.path.sep + filename + ".bmp" 204 | print(file) 205 | 206 | output_file = open(file=file, mode="rb") 207 | 208 | width = 240 209 | height = 168 210 | 211 | file_size = os.path.getsize(file) 212 | pos_in_image = 0 213 | max_line_length = width * 2 214 | slices = [] 215 | for row in range(0, height): 216 | current_slice = bytearray() 217 | for current_byte in range(0, max_line_length): 218 | current_byte = (row * max_line_length) + current_byte 219 | if file_size >= current_byte: 220 | current_slice.append(output_file.read(2)) 221 | current_byte += 2 222 | slices.append(current_slice) 223 | 224 | my_slice = [[]] 225 | for byte in slices: 226 | print(type(byte)) 227 | my_slice[pos_in_image] = byte[0] 228 | pos_in_image += 1 229 | if pos_in_image > (240 * 2): 230 | pos_in_image = 0 231 | current_slice = bytearray 232 | my_slice.append(current_slice) 233 | 234 | # image = Image.new("RGB", (width, height)) 235 | # file_format = "bmp" # The file extension of the sourced data 236 | print(len(my_slice)) 237 | 238 | print(my_slice) 239 | 240 | # bytes(byte_array) 241 | # image.write(output_file,file_format) 242 | 243 | # output_file.close() 244 | 245 | return MonoXPreviewImage("") 246 | 247 | 248 | def _do_handle(message: str) -> Iterable[MonoXResponseType]: 249 | """Perform handling of the message received by the request""" 250 | if message is None: 251 | return "no response" 252 | 253 | lines = message.split(",end") 254 | recognized_response: Iterable = list() 255 | for line in lines: 256 | fields: list(str) = line.split(",") 257 | message_type = fields[0] 258 | if len(fields) is Empty or len(fields) < 2: 259 | continue 260 | if message_type == "getstatus": 261 | recognized_response.append(__do_status(fields)) 262 | elif message_type == "getfile": 263 | recognized_response.append(__do_files(fields)) 264 | elif message_type == "sysinfo": 265 | recognized_response.append(__do_sys_info(fields)) 266 | elif message_type == "gethistory": 267 | recognized_response.append(__do_get_history(fields)) 268 | elif message_type == "doPreview2": 269 | recognized_response.append(__do_preview2(fields)) 270 | # goprint,49.pwmb,end 271 | elif message_type in [ 272 | "goprint", 273 | "gostop", 274 | "gopause", 275 | "getmode", 276 | "getwifi", 277 | ]: 278 | recognized_response.append(InvalidResponse(fields[1])) 279 | else: 280 | print("unrecognized command: " + message_type, file=sys.stderr) 281 | print(line, file=sys.stderr) 282 | if recognized_response is not None: 283 | recognized_response.append(InvalidResponse(fields[1])) 284 | 285 | return recognized_response 286 | 287 | 288 | def __do_get_history(fields: Iterable): 289 | """Handles history processing.""" 290 | items = [] 291 | for field in fields: 292 | if field in fields[0] or fields[-1]: 293 | continue 294 | items.append(field) 295 | return items 296 | 297 | 298 | def __do_sys_info(fields: Iterable): 299 | """Handles system info processing.""" 300 | sys_info = MonoXSysInfo() 301 | if len(fields) > 2: 302 | sys_info.model = fields[1] 303 | if len(fields) > 3: 304 | sys_info.firmware = fields[2] 305 | if len(fields) > 4: 306 | sys_info.serial = fields[3] 307 | if len(fields) > 5: 308 | sys_info.wifi = fields[4] 309 | return sys_info 310 | 311 | 312 | def __do_files(fields: Iterable): 313 | """Handles file processing.""" 314 | files = FileList(fields) 315 | return files 316 | 317 | 318 | def __do_status(fields: Iterable): 319 | """Handles status processing.""" 320 | status = MonoXStatus(fields) 321 | return status 322 | 323 | 324 | def current_milli_time(): 325 | return round(time.time() * 1000) 326 | -------------------------------------------------------------------------------- /src/uart_wifi/errors.py: -------------------------------------------------------------------------------- 1 | """Exceptions""" 2 | 3 | 4 | class AnycubicException(Exception): 5 | """Base class for Anycubic exceptions.""" 6 | 7 | 8 | class ConnectionException(AnycubicException): 9 | """Problem when connecting""" 10 | -------------------------------------------------------------------------------- /src/uart_wifi/response.py: -------------------------------------------------------------------------------- 1 | """Mono X Objects.""" 2 | 3 | 4 | # pylint: disable=too-few-public-methods 5 | class MonoXResponseType: 6 | """The baseline MonoX Response class. 7 | Use this to create other MonoX Responses.""" 8 | 9 | status: str = "error/offline" 10 | 11 | def print(self): 12 | """Print the MonoXResponse. Should be overridden 13 | by anything which implements this class.""" 14 | return "Status: " + self.status 15 | 16 | 17 | class MonoXFileEntry(MonoXResponseType): 18 | """A file entry consisting of an internal and external listing""" 19 | 20 | def __init__(self, internal_name: str, external_name: str) -> None: 21 | """Create a MonoXFileEntry 22 | :internal_name: the name the printer calls the file. eg "1.pwmb" 23 | :external_name: The name the user calls the file. 24 | eg "My (Super) Cool.pwmb" 25 | """ 26 | self.external = internal_name 27 | self.internal = external_name 28 | self.status = "file" 29 | 30 | def print(self): 31 | """Provide a human-readable response""" 32 | print(self.internal + ": " + self.external) 33 | 34 | 35 | class FileList(MonoXResponseType): 36 | """handles lists of files. 37 | eg. 38 | getfile, 39 | 2-phone-stands.pwmb/0.pwmb, 40 | SLA print puller supported.pwmb/1.pwmb, 41 | 2 phone stands on side.pwmb/2.pwmb, 42 | 5x USB_Cable_Holder_7w_Screws_hollow.pwmb/3.pwmb, 43 | end 44 | """ 45 | 46 | def __init__(self, data: MonoXFileEntry) -> None: 47 | """Create a FileList object. 48 | :data: a list of internal/external files. 49 | """ 50 | self.files = [] 51 | self.status = "getfile" 52 | 53 | for field in data: 54 | if field in data[0] or data[-1]: 55 | continue # not interested in packet open/close portion. 56 | split = field.split("/") 57 | self.files.append(MonoXFileEntry(split[0], split[1])) 58 | 59 | files = [MonoXFileEntry] 60 | 61 | def print(self): 62 | """Provide a human-readable response.""" 63 | for file in self.files: 64 | file.print() 65 | 66 | 67 | class InvalidResponse(MonoXResponseType): 68 | """Used when no response is provided.""" 69 | 70 | def __init__(self, message) -> None: 71 | """Construct the InvalidResponse type. 72 | :message: anything goes 73 | """ 74 | self.status = message 75 | 76 | def print(self): 77 | """Provide a human-readable response.""" 78 | print("Invalid Response: " + self.status) 79 | 80 | 81 | class SimpleResponse(MonoXResponseType): 82 | """Used when no response is provided.""" 83 | 84 | def __init__(self, message) -> None: 85 | """Construct a SimpleResponse. 86 | :message: anything goes.""" 87 | self.status = message 88 | 89 | def print(self): 90 | """Provide a human-readable response.""" 91 | print("Response: " + self.status) 92 | 93 | 94 | class MonoXSysInfo(MonoXResponseType): 95 | """The sysinfo handler. Handles sysinfo messages. 96 | eg message. 97 | sysinfo,Photon Mono X 6K,V0.2.2,0000170300020034,SkyNet,end 98 | """ 99 | 100 | def __init__(self, model="", firmware="", serial="", wifi="") -> None: 101 | """Construct the MonoXSysInfo response type""" 102 | self.model = model 103 | self.firmware = firmware 104 | self.serial = serial 105 | self.wifi = wifi 106 | self.status = "updated" 107 | 108 | def print(self): 109 | """Provide a human-readable response""" 110 | print("model: " + self.model) 111 | print("firmware: " + self.firmware) 112 | print("serial: " + self.serial) 113 | print("wifi: " + self.wifi) 114 | 115 | 116 | # pylint: disable=too-many-instance-attributes 117 | class MonoXStatus(MonoXResponseType): 118 | """Status object for MonoX. 119 | 120 | eg message. 121 | getstatus,print,Widget.pwmb/46.pwmb,2338,88,2062,51744,6844,~178mL,UV,39.38,0.05,0,end 122 | """ 123 | 124 | def __init__(self, message) -> None: 125 | """Construct the Status response. 126 | :message: a properly formated message of either length 3 or >12.""" 127 | 128 | self.status = message[1] 129 | if len(message) > 2: 130 | self.file = message[2] 131 | if len(message) > 3: 132 | self.total_layers = message[3] 133 | if len(message) > 4: 134 | self.percent_complete = message[4] 135 | if len(message) > 5: 136 | self.current_layer = message[5] 137 | if len(message) > 6: 138 | if str(message[6]).isnumeric(): 139 | self.seconds_elapse = int(message[6]) * 60 140 | else: 141 | self.seconds_elapse = message[6] 142 | if len(message) > 7: 143 | self.seconds_remaining = message[7] 144 | if len(message) > 8: 145 | self.total_volume = message[8] 146 | if len(message) > 9: 147 | self.mode = message[9] 148 | if len(message) > 10: 149 | self.unknown1 = message[10] 150 | if len(message) > 11: 151 | self.layer_height = message[11] 152 | if len(message) > 12: 153 | self.unknown2 = message[12] 154 | 155 | def print(self): 156 | """Provide a human-readable response.""" 157 | print("status: " + self.status) 158 | if hasattr(self, "file"): 159 | print("file: " + self.file) 160 | print("total_layers: " + str(self.total_layers)) 161 | print("percent_complete: " + str(self.percent_complete)) 162 | print("current_layer: " + str(self.current_layer)) 163 | print("seconds_remaining: " + str(self.seconds_remaining)) 164 | print("total_volume: " + str(self.total_volume)) 165 | print("mode: " + self.mode) 166 | print("unknown1: " + str(self.unknown1)) 167 | print("layer_height: " + str(self.layer_height)) 168 | print("unknown2: " + str(self.unknown2)) 169 | 170 | 171 | class MonoXPreviewImage(MonoXResponseType): 172 | """A file entry consisting of an internal and external listing.""" 173 | 174 | def __init__(self, file_path: str) -> None: 175 | """Construct the MonoXPreviewImage. 176 | :file_path: the path to the preview image. 177 | """ 178 | super().__init__() 179 | self.file_path = file_path 180 | self.status = "preview image" 181 | 182 | def print(self): 183 | """Provide a human-readable response.""" 184 | print(f"preview located at {self.file_path}") 185 | -------------------------------------------------------------------------------- /src/uart_wifi/scripts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamoutler/anycubic-python/0c0363373c8d3689f982d6f12ededee93468c6ab/src/uart_wifi/scripts/__init__.py -------------------------------------------------------------------------------- /src/uart_wifi/scripts/__pycache__/__init__.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamoutler/anycubic-python/0c0363373c8d3689f982d6f12ededee93468c6ab/src/uart_wifi/scripts/__pycache__/__init__.cpython-39.pyc -------------------------------------------------------------------------------- /src/uart_wifi/scripts/__pycache__/fake_printer.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamoutler/anycubic-python/0c0363373c8d3689f982d6f12ededee93468c6ab/src/uart_wifi/scripts/__pycache__/fake_printer.cpython-39.pyc -------------------------------------------------------------------------------- /src/uart_wifi/scripts/fake_printer: -------------------------------------------------------------------------------- 1 | #! python3 2 | """Fake Anycubic Printer for tests""" 3 | import getopt 4 | 5 | import sys 6 | from uart_wifi.simulate_printer import AnycubicSimulator 7 | 8 | 9 | def start_server(the_ip: str, port: int) -> None: 10 | """Starts the server 11 | :the_ip: The IP address to use internally for opening the port. 12 | eg. 127.0.0.1, or 0.0.0.0 13 | :the_port: The port to monitor for responses. 14 | """ 15 | AnycubicSimulator(the_ip, int(port)).start_server() 16 | 17 | 18 | opts, args = getopt.gnu_getopt(sys.argv, "i:p:", ["ipaddress=", "port="]) 19 | 20 | IP_ADDRESS = "0.0.0.0" 21 | PORT = 6000 22 | for opt, arg in opts: 23 | if opt in ("-i", "--ipaddress"): 24 | IP_ADDRESS = arg 25 | elif opt in ("-p", "--port"): 26 | PORT = arg 27 | print("Opening printer on port " + arg) 28 | 29 | 30 | start_server(IP_ADDRESS, PORT) 31 | -------------------------------------------------------------------------------- /src/uart_wifi/scripts/fake_printer.py: -------------------------------------------------------------------------------- 1 | fake_printer -------------------------------------------------------------------------------- /src/uart_wifi/scripts/monox: -------------------------------------------------------------------------------- 1 | #! python3 2 | """Uart wifi""" 3 | 4 | import getopt 5 | import sys 6 | import time 7 | from typing import Iterable 8 | 9 | from uart_wifi.communication import UartWifi 10 | from uart_wifi.errors import ConnectionException 11 | from uart_wifi.response import MonoXResponseType 12 | 13 | 14 | PORT = 6000 15 | HELP = ( 16 | __file__ 17 | + """ | Adam Outler (monox@hackedyour.info) | GPLv3 18 | 19 | Usage: monox.py -i -c 20 | args: 21 | -i [--ipaddress=] - The IP address which your Anycubic Mono X can be reached 22 | 23 | -c [--command=] - The command to send. 24 | Commands may be used one-at-a-time. Only one command may be sent and it is 25 | expected to be in the format below. 26 | Command: getstatus - Returns a list of printer statuses. 27 | Command: getfile - returns a list of files in format : 28 | . 29 | When referring to the file via command, use the . 30 | Command: sysinfo - returns Model, Firmware version, Serial Number, 31 | and wifi network. 32 | Command: getwifi - displays the current wifi network name. 33 | Command: gopause - pauses the current print. 34 | Command: goresume - ends the current print. 35 | Command: gostop,end - stops the current print. 36 | Command: delfile,,end - deletes a file. 37 | command: gethistory,end - gets the history and print settings of previous 38 | prints. 39 | Command: delhistory,end - deletes printing history. 40 | Command: goprint,,end - Starts a print of the requested file 41 | Command: getPreview1,,end - returns a list of dimensions 42 | used for the print. 43 | 44 | Not Supported Commands may return unusable results. 45 | Command (Not Supported): getPreview2,,end 46 | - returns a binary preview image of the print. 47 | 48 | Unknown Commands are at your own risk and experimentation. 49 | No attempt is made to process or stop execution of these commands. 50 | Command: detect 51 | Command: stopUV - unknown 52 | Command: getpara - unknown 53 | Command: getmode - unknown 54 | Command: setname - unknown 55 | Command: getname - unknown 56 | Command: setwifi - unknown 57 | Command: setZero - unknown 58 | Command: setZhome - unknown 59 | Command: setZmove - unknown 60 | Command: setZstop - unknown 61 | """ 62 | ) 63 | 64 | 65 | try: 66 | opts, args = getopt.gnu_getopt( 67 | sys.argv, "drhi:c:p:", ["raw", "ipaddress=", "command=", "port="] 68 | ) 69 | # pylint: disable=broad-except 70 | except Exception: 71 | print(HELP) 72 | sys.exit(0) 73 | 74 | USE_RAW = False 75 | for opt, arg in opts: 76 | if opt == "-h": 77 | print(HELP) 78 | sys.exit() 79 | elif opt in "-d": 80 | time.sleep(1) 81 | elif opt in ("-r", "--raw"): 82 | USE_RAW = True 83 | elif opt in ("-i", "--ipaddress"): 84 | ip_address = arg 85 | elif opt in ("-p", "--port"): 86 | PORT = int(arg) 87 | elif opt in ("-c", "--command"): 88 | command = arg 89 | print(arg) 90 | 91 | if "ip_address" not in locals(): 92 | print("You must specify the host ip address (-i xxx.xxx.xxx.xxx)") 93 | sys.exit(1) 94 | 95 | if ip_address == "127.0.0.1": 96 | time.sleep(1) 97 | responses = None # pylint: disable=invalid-name 98 | # Try 3 times to get the data. 99 | attempts: int = 0 100 | while attempts < 3: 101 | try: 102 | uart = UartWifi(ip_address, PORT) 103 | if USE_RAW: 104 | uart.raw = True 105 | responses: Iterable[MonoXResponseType] = uart.send_request(command) 106 | 107 | break 108 | except ConnectionException: 109 | attempts += 1 110 | 111 | 112 | if responses is not None and isinstance(responses, Iterable): 113 | for response in responses: 114 | if isinstance(response, MonoXResponseType): 115 | response.print() 116 | else: 117 | print(response) 118 | else: 119 | print(responses) 120 | -------------------------------------------------------------------------------- /src/uart_wifi/scripts/monox.py: -------------------------------------------------------------------------------- 1 | monox -------------------------------------------------------------------------------- /src/uart_wifi/simulate_printer.py: -------------------------------------------------------------------------------- 1 | """"Class to handle printer simulation""" 2 | import select 3 | import socket 4 | import threading 5 | import time 6 | 7 | 8 | class AnycubicSimulator: 9 | """ "Simulator for Anycubic Printer.""" 10 | 11 | port = "6000" 12 | printing = False 13 | serial = "0000170300020034" 14 | shutdown_signal = False 15 | 16 | def __init__(self, the_ip: str, the_port: int) -> None: 17 | """Construct the Anycubic Simulator 18 | :the_ip: The IP address to use internally for opening the port. 19 | eg. 127.0.0.1, or 0.0.0.0 20 | :the_port: The port to monitor for responses. 21 | """ 22 | self.host = the_ip 23 | self.port = the_port 24 | self.printing = False 25 | self.serial = "234234234" 26 | 27 | def sysinfo(self) -> str: 28 | """return sysinfo type""" 29 | return "sysinfo,Photon Mono X 6K,V0.2.2," + self.serial + ",SkyNet,end" 30 | 31 | def getfile(self) -> str: 32 | """return getfile type""" 33 | return "getfile,Widget.pwmb/0.pwmb,end" 34 | 35 | def getstatus(self) -> str: 36 | """return getstatus type""" 37 | if self.printing: 38 | return ( 39 | "getstatus,print,Widget.pwmb" 40 | "/46.pwmb,2338,88,2062,51744,6844,~178mL,UV,39.38,0.05,0,end" 41 | ) 42 | return "getstatus,stop\r\n,end" 43 | 44 | def goprint(self) -> str: 45 | """Do printing""" 46 | if self.printing: 47 | return "goprint,ERROR1,end" 48 | self.printing = True 49 | return "goprint,OK,end" 50 | 51 | def gostop(self) -> str: 52 | """Do Stop printing""" 53 | if not self.printing: 54 | return "gostop,ERROR1,end" 55 | self.printing = False 56 | return "gostop,OK,end" 57 | 58 | def start_server(self): 59 | """Start the uart_wifi simualtor server""" 60 | my_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 61 | my_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 62 | my_socket.bind((self.host, self.port)) 63 | self.port = my_socket.getsockname()[1] 64 | print(f"Starting printer on {self.host}:{self.port}") 65 | my_socket.listen(1) 66 | my_socket.setblocking(False) 67 | read_list = [my_socket] 68 | while not AnycubicSimulator.shutdown_signal: 69 | readable, [], [] = select.select(read_list, [], []) 70 | for the_socket in readable: 71 | if the_socket is my_socket: 72 | try: 73 | conn, addr = the_socket.accept() 74 | thread = threading.Thread( 75 | target=self.response_selector, 76 | args=(conn, addr), 77 | ) 78 | thread.daemon = True 79 | thread.start() 80 | except Exception: # pylint: disable=broad-except 81 | pass 82 | finally: 83 | time.sleep(1) 84 | 85 | def response_selector(self, conn: socket.socket, addr) -> None: 86 | """The connection handler 87 | :conn: The connection to use 88 | :addr: address tuple for ip and port 89 | """ 90 | print(f"Simulator: accepted connection to {addr}") 91 | decoded_data = "" 92 | with conn: 93 | while ( 94 | "," not in decoded_data 95 | and "\n" not in decoded_data 96 | and not AnycubicSimulator.shutdown_signal 97 | ): 98 | data = conn.recv(1) 99 | decoded_data += data.decode() 100 | if "111\n" in decoded_data: 101 | decoded_data = "" 102 | continue 103 | try: 104 | print("Hex:") 105 | print(" ".join(f"{hex:02x}" for hex in decoded_data.encode())) 106 | print("Data:") 107 | print(decoded_data) 108 | except UnicodeDecodeError: 109 | pass 110 | self.send_response(conn, decoded_data) 111 | decoded_data = "" 112 | 113 | def send_response(self, conn: socket.socket, decoded_data: str) -> None: 114 | """Send a response 115 | 116 | :conn: The connection to use 117 | :addr: address tuple for ip and port 118 | """ 119 | split_data = decoded_data.split(",") 120 | for split in split_data: 121 | if split == "": 122 | continue 123 | if "getstatus" in split: 124 | return_value = self.getstatus() 125 | conn.sendall(return_value.encode()) 126 | if "sysinfo" in split: 127 | conn.sendall(self.sysinfo().encode()) 128 | if "getfile" in split: 129 | conn.sendall(self.getfile().encode()) 130 | if "goprint" in split: 131 | conn.sendall(self.goprint().encode()) 132 | decoded_data = "" 133 | if "gostop" in split: 134 | value = self.gostop() 135 | print("sent:" + value) 136 | conn.sendall(value.encode()) 137 | if "getmode" in split: 138 | value = "getmode,0,end" 139 | print("sent:" + value) 140 | conn.sendall(value.encode()) 141 | decoded_data = "" 142 | if "incomplete" in split: 143 | value = "getmode,0," 144 | print("sent:" + value) 145 | conn.sendall(value.encode()) 146 | decoded_data = "" 147 | if "timeout" in split: 148 | time.sleep(99999) 149 | 150 | if "multi" in split: 151 | value = self.getstatus() + self.sysinfo() + "getmode,0,end" 152 | print("sent:" + value) 153 | conn.sendall(value.encode()) 154 | decoded_data = "" 155 | if decoded_data.endswith("shutdown,"): 156 | value = "shutdown,end" 157 | print("sent:" + value) 158 | conn.sendall(value.encode()) 159 | AnycubicSimulator.shutdown_signal = True 160 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamoutler/anycubic-python/0c0363373c8d3689f982d6f12ededee93468c6ab/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_communication.py: -------------------------------------------------------------------------------- 1 | """A file where we test things.""" 2 | import threading 3 | import time 4 | from typing import Iterable 5 | import unittest 6 | 7 | from pytest import fail 8 | 9 | from uart_wifi.communication import UartWifi 10 | from uart_wifi.errors import ConnectionException 11 | from uart_wifi.simulate_printer import AnycubicSimulator 12 | from uart_wifi.response import MonoXStatus, MonoXResponseType, MonoXSysInfo 13 | 14 | 15 | class TestComms(unittest.TestCase): 16 | """Tests""" 17 | 18 | @classmethod 19 | def setup_class(cls): 20 | """Called when setting up the class to start the fake printer""" 21 | fake_printer = AnycubicSimulator("127.0.0.1", 0) 22 | 23 | thread = threading.Thread(target=fake_printer.start_server) 24 | thread.daemon = False 25 | thread.start() 26 | while TestComms.port == 0: 27 | print("Sleeping while fake printer starts") 28 | time.sleep(0.2) 29 | time.sleep(1) # import time 30 | TestComms.port = fake_printer.port 31 | print("Fake printer assumed started") 32 | 33 | def test_connection(self): 34 | """test basic connection""" 35 | uart_wifi: UartWifi = get_api() 36 | response: Iterable[MonoXStatus] = uart_wifi.send_request("getstatus,") 37 | assert len(response) > 0, "No response from Fake Printer" 38 | assert ( 39 | response[0].status == "stop\r\n" 40 | ), "Invalid response from Fake Printer" 41 | 42 | def test_timeout(self): 43 | """test basic connection""" 44 | uart_wifi: UartWifi = get_api() 45 | response: Iterable[MonoXStatus] = uart_wifi.send_request("timeout,") 46 | assert len(response) == 0, ( 47 | "Expected no response from Fake Printer but got " + response[0] 48 | ) 49 | 50 | def test_print(self): 51 | """Test Print command""" 52 | uart_wifi: UartWifi = get_api() 53 | response: Iterable[MonoXStatus] = uart_wifi.send_request( 54 | "goprint,0.pwmb,end" 55 | ) 56 | assert len(response) > 0, "No response from Fake Printer" 57 | assert response[0].status == "OK", "Invalid response from Fake Printer" 58 | response_printing = uart_wifi.send_request("getstatus\n") 59 | assert response_printing[0].status == "print" 60 | response_stop = uart_wifi.send_request("gostop,end\n") 61 | assert response_stop[0].status == "OK" 62 | 63 | def test_gostop_error(self): 64 | """Test Print command""" 65 | uart_wifi: UartWifi = get_api() 66 | response: Iterable[MonoXStatus] = uart_wifi.send_request("gostop,end") 67 | assert len(response) > 0, "No response from Fake Printer" 68 | assert ( 69 | response[0].status == "ERROR1" 70 | ), "Invalid response from Fake Printer" 71 | 72 | def test_init_fail(self): 73 | """failure of init""" 74 | try: 75 | uart = UartWifi("foo", TestComms.port) 76 | response = uart.send_request("foo bar baz") 77 | print(response) 78 | fail("failure not seen for this request") 79 | except (ConnectionException, IndexError): 80 | pass 81 | 82 | def test_get_mode(self): 83 | """failure of init""" 84 | uart = get_api() 85 | response = uart.send_request("getmode\n") 86 | assert response[0].status == "0" 87 | 88 | def test_sysinfo(self): 89 | """failure of init""" 90 | uart = get_api() 91 | response: MonoXSysInfo = uart.send_request("sysinfo\n") 92 | assert response[0].status == "updated" 93 | assert response[0].firmware == "V0.2.2" 94 | assert response[0].model == "Photon Mono X 6K" 95 | assert response[0].serial == "234234234" 96 | assert response[0].wifi == "" 97 | 98 | @classmethod 99 | def teardown_class(cls): 100 | """Called when setting up the class to start the fake printer""" 101 | 102 | uart = UartWifi("127.0.0.1", TestComms.port) 103 | response: list[MonoXResponseType] = uart.send_request("shutdown,") 104 | try: 105 | print(response[0].print()) 106 | except IndexError: 107 | pass 108 | 109 | 110 | def get_api() -> UartWifi: 111 | """ "Get the UartWifi device to use for testing""" 112 | port = TestComms.port 113 | print(f"connecting to 127.0.0.1:{port}") 114 | uart_wifi: UartWifi = UartWifi("127.0.0.1", TestComms.port) 115 | return uart_wifi 116 | 117 | 118 | TestComms.port = 62134 119 | -------------------------------------------------------------------------------- /tests/test_errors.py: -------------------------------------------------------------------------------- 1 | """Tests for errors""" 2 | from uart_wifi.errors import AnycubicException, ConnectionException 3 | 4 | 5 | def test_commserror(): 6 | """test connection exception""" 7 | try: 8 | raise ConnectionException 9 | except ConnectionException: 10 | pass 11 | 12 | 13 | def test_anycubic_exception(): 14 | """Test anycubic exception""" 15 | try: 16 | raise AnycubicException 17 | except AnycubicException: 18 | pass 19 | --------------------------------------------------------------------------------