├── .bumpversion.cfg ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── duetwebapi ├── __init__.py ├── api │ ├── __init__.py │ ├── base.py │ ├── dsf_api.py │ ├── dwc_api.py │ └── wrapper.py └── dwa_factory.py ├── examples.py ├── requirements.txt ├── requirements_dev.txt ├── setup.py └── test.gcode /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 1.3.0-b0 3 | commit = False 4 | tag = False 5 | allow_dirty = False 6 | parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\-(?P[a-z]+)(?P\d+))? 7 | serialize = 8 | {major}.{minor}.{patch}-{release}{build} 9 | {major}.{minor}.{patch} 10 | 11 | [bumpversion:part:release] 12 | optional_value = prod 13 | first_value = b 14 | values = 15 | b 16 | prod 17 | 18 | [bumpversion:part:build] 19 | 20 | [bumpversion:file:setup.py] 21 | 22 | [bumpversion:file:duetwebapi/__init__.py] 23 | replace = {new_version} 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # File created using '.gitignore Generator' for Visual Studio Code: https://bit.ly/vscode-gig 2 | 3 | # Created by https://www.toptal.com/developers/gitignore/api/windows,python 4 | # Edit at https://www.toptal.com/developers/gitignore?templates=windows,python 5 | 6 | ### Python ### 7 | # Byte-compiled / optimized / DLL files 8 | __pycache__/ 9 | *.py[cod] 10 | *$py.class 11 | 12 | # C extensions 13 | *.so 14 | 15 | # Distribution / packaging 16 | .Python 17 | build/ 18 | develop-eggs/ 19 | dist/ 20 | downloads/ 21 | eggs/ 22 | .eggs/ 23 | lib/ 24 | lib64/ 25 | parts/ 26 | sdist/ 27 | var/ 28 | wheels/ 29 | pip-wheel-metadata/ 30 | share/python-wheels/ 31 | *.egg-info/ 32 | .installed.cfg 33 | *.egg 34 | MANIFEST 35 | 36 | # PyInstaller 37 | # Usually these files are written by a python script from a template 38 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 39 | *.manifest 40 | *.spec 41 | 42 | # Installer logs 43 | pip-log.txt 44 | pip-delete-this-directory.txt 45 | 46 | # Unit test / coverage reports 47 | htmlcov/ 48 | .tox/ 49 | .nox/ 50 | .coverage 51 | .coverage.* 52 | .cache 53 | nosetests.xml 54 | coverage.xml 55 | *.cover 56 | *.py,cover 57 | .hypothesis/ 58 | .pytest_cache/ 59 | pytestdebug.log 60 | 61 | # Translations 62 | *.mo 63 | *.pot 64 | 65 | # Django stuff: 66 | *.log 67 | local_settings.py 68 | db.sqlite3 69 | db.sqlite3-journal 70 | 71 | # Flask stuff: 72 | instance/ 73 | .webassets-cache 74 | 75 | # Scrapy stuff: 76 | .scrapy 77 | 78 | # Sphinx documentation 79 | docs/_build/ 80 | doc/_build/ 81 | 82 | # PyBuilder 83 | target/ 84 | 85 | # Jupyter Notebook 86 | .ipynb_checkpoints 87 | 88 | # IPython 89 | profile_default/ 90 | ipython_config.py 91 | 92 | # pyenv 93 | .python-version 94 | 95 | # pipenv 96 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 97 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 98 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 99 | # install all needed dependencies. 100 | #Pipfile.lock 101 | 102 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 103 | __pypackages__/ 104 | 105 | # Celery stuff 106 | celerybeat-schedule 107 | celerybeat.pid 108 | 109 | # SageMath parsed files 110 | *.sage.py 111 | 112 | # Environments 113 | .env 114 | .venv 115 | env/ 116 | venv/ 117 | ENV/ 118 | env.bak/ 119 | venv.bak/ 120 | pythonenv* 121 | 122 | # Spyder project settings 123 | .spyderproject 124 | .spyproject 125 | 126 | # Rope project settings 127 | .ropeproject 128 | 129 | # mkdocs documentation 130 | /site 131 | 132 | # mypy 133 | .mypy_cache/ 134 | .dmypy.json 135 | dmypy.json 136 | 137 | # Pyre type checker 138 | .pyre/ 139 | 140 | # pytype static type analyzer 141 | .pytype/ 142 | 143 | # profiling data 144 | .prof 145 | 146 | ### Windows ### 147 | # Windows thumbnail cache files 148 | Thumbs.db 149 | Thumbs.db:encryptable 150 | ehthumbs.db 151 | ehthumbs_vista.db 152 | 153 | # Dump file 154 | *.stackdump 155 | 156 | # Folder config file 157 | [Dd]esktop.ini 158 | 159 | # Recycle Bin used on file shares 160 | $RECYCLE.BIN/ 161 | 162 | # Windows Installer files 163 | *.cab 164 | *.msi 165 | *.msix 166 | *.msm 167 | *.msp 168 | 169 | # Windows shortcuts 170 | *.lnk 171 | 172 | # End of https://www.toptal.com/developers/gitignore/api/windows,python 173 | 174 | # Custom rules (everything added below won't be overriden by 'Generate .gitignore File' if you use 'Update' option) 175 | 176 | .vscode/* -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Andy 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/bash 2 | verbosity=1 3 | 4 | ######################################### 5 | # bumpversion Usage 6 | ######################################### 7 | # `bumpversion [major|minor|patch|build]` 8 | # `bumpversion --tag release 9 | 10 | update_dist: 11 | rm dist/* -f 12 | python setup.py sdist bdist_wheel 13 | 14 | check_dist: 15 | twine check dist/* 16 | 17 | upload_test: check_dist 18 | twine upload --repository testpypi dist/* 19 | 20 | upload: check_dist 21 | twine upload dist/* -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DuetWebAPI 2 | Python interface to Duet RepRap V3 firmware via Http REST API. 3 | 4 | * Works over IP network. 5 | * Does not support passwords on the printer. 6 | * Supported boards: 7 | * Duet 3 + SBC 8 | * Duet 3 standalone 9 | * Duet 2 standalone 10 | 11 | # Install 12 | ``` 13 | pip install duetwebapi 14 | ``` 15 | 16 | Alternatively: 17 | ``` 18 | pip install -e "git+https://github.com/AndyEveritt/DuetWebAPI.git@master#egg=duetwebapi" 19 | ``` 20 | 21 | # Usage 22 | * See 'examples.py' for examples. 23 | 24 | ```python 25 | from duetwebapi import DuetWebAPI 26 | 27 | printer = DuetWebAPI(f'http://{printer_hostname}') 28 | ``` 29 | 30 | ## REST API 31 | The REST API allows for the following operations: 32 | 33 | Method | Description 34 | ------ | ----------- 35 | `connect(password: str = '') -> Dict` | Start a connection with Duet. DWC api only 36 | `disconnect() -> Dict` | End a connection with Duet. DWC api only 37 | `get_model(key: str = None, depth: int = 99, verbose: bool = True, null: bool = True, frequent: bool = False, obsolete: bool = False) -> Dict` | Get Duet object model. RRF3 only. Flags only supported in DWC api. 38 | `send_code(code: str) -> Dict` | Send G/M/T-code to Duet 39 | `get_file(filename: str, directory: str = 'gcodes', binary: bool = False) -> str` | Download file from Duet 40 | `upload_file(file: str \| bytes \| StringIO \| TextIOWrapper \| BytesIO, filename: str, directory: str = 'gcodes') -> Dict` | Upload file to Duet 41 | `get_fileinfo(filename: str = None, directory: str = 'gcodes') -> Dict` | Get file info 42 | `delete_file(filename: str, directory: str = 'gcodes') -> Dict` | Delete file on Duet 43 | `move_file(from_path: str, to_path: str, force: bool = False) -> Dict` | Move file on Duet, can be used to rename files 44 | `get_directory(directory: str) -> List[Dict]` | Get a list of all the files & directories in a directory 45 | `create_directory(directory: str) -> Dict` | Create a new directory 46 | 47 | 48 | ## Wrapper 49 | An additional wrapper is provided to make repetative tasks easier 50 | 51 | Method | Description 52 | ------ | ----------- 53 | `emergency_stop() -> None` | Send M112 > M999 54 | `start_print(filename: str) -> Dict` | start a print on duet 55 | `pause_print() -> Dict` | pause current print 56 | `stop_print(leave_heaters: bool) -> Dict` | stop current print, will pause first if not paused 57 | `get_coords() -> Dict` | return the current position of all the movement axes 58 | `get_layer() -> int` | return the current layer number of the print 59 | `get_num_extruders() -> int` | return the number of extruders currently configured 60 | `get_num_tools() -> int` | return the number of tools currently configured 61 | `get_status() -> str` | return the current Duet status 62 | `get_temperature() -> List[Dict]` | return a list of all the analog sensors and their value 63 | `get_current_tool() -> int` | return the current tool number 64 | `get_messagebox() -> Dict` | return the details of a message displayed via `M291` if one exists 65 | `acknowledge_message(response: int) -> Dict` | send an acknowledgement to a message if one exists. Response options are `0` (continue), and `1` (cancel) 66 | -------------------------------------------------------------------------------- /duetwebapi/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "1.3.0-b0" 2 | 3 | from .dwa_factory import DuetWebAPI 4 | -------------------------------------------------------------------------------- /duetwebapi/api/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import DuetAPI 2 | from .dsf_api import DSFAPI 3 | from .dwc_api import DWCAPI 4 | from .wrapper import DuetAPIWrapper 5 | -------------------------------------------------------------------------------- /duetwebapi/api/base.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, List, Union 2 | from io import StringIO, TextIOWrapper, BytesIO 3 | import requests 4 | import os 5 | import logging 6 | 7 | 8 | class DuetAPI: 9 | api_name = '' 10 | 11 | def __init__(self, base_url: str) -> None: 12 | self.base_url = base_url 13 | 14 | def __repr__(self): 15 | return f"DuetWebAPI('{self.base_url}') - {self.api_name}" 16 | 17 | @property 18 | def base_url(self): 19 | return self._base_url 20 | 21 | @base_url.setter 22 | def base_url(self, value: str): 23 | if not value.startswith('http://'): 24 | value = f'http://{value}' 25 | self._base_url = value 26 | 27 | def connect(self, **kwargs): 28 | """ Start connection to Duet """ 29 | raise NotImplementedError 30 | 31 | def disconnect(self): 32 | """ End connection to Duet """ 33 | raise NotImplementedError 34 | 35 | def get_model(self, key: str = None, **kwargs) -> Dict: 36 | """ Get Duet object model. RRF3 only """ 37 | raise NotImplementedError 38 | 39 | def send_code(self, code: str) -> Dict: 40 | """ Send G/M/T-code to Duet """ 41 | raise NotImplementedError 42 | 43 | def get_file(self, filename: str, directory: str = 'gcodes', binary: bool = False) -> str: 44 | """ Get file from Duet """ 45 | raise NotImplementedError 46 | 47 | def upload_file(self, file: Union[str, bytes, StringIO, TextIOWrapper, BytesIO], filename: str, directory: str = 'gcodes') -> Dict: 48 | """ Upload file to Duet """ 49 | raise NotImplementedError 50 | 51 | def get_fileinfo(self, filename: str = None, directory: str = 'gcodes') -> Dict: 52 | """ Get file info """ 53 | raise NotImplementedError 54 | 55 | def delete_file(self, filename: str, directory: str = 'gcodes') -> Dict: 56 | """ Delete file on Duet """ 57 | raise NotImplementedError 58 | 59 | def move_file(self, from_path: str, to_path: str, force: bool = False) -> Dict: 60 | """ Move file on Duet, can be used to rename files """ 61 | raise NotImplementedError 62 | 63 | def get_directory(self, directory: str) -> List[Dict]: 64 | """ Get a list of all the files & directories in a directory """ 65 | raise NotImplementedError 66 | 67 | def create_directory(self, directory: str) -> Dict: 68 | """ Create a new directory """ 69 | raise NotImplementedError 70 | -------------------------------------------------------------------------------- /duetwebapi/api/dsf_api.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | from typing import Dict, List, Union 4 | from io import StringIO, TextIOWrapper, BytesIO 5 | from functools import reduce 6 | 7 | import operator 8 | import requests 9 | 10 | from .base import DuetAPI 11 | 12 | 13 | class DSFAPI(DuetAPI): 14 | """ 15 | Duet Software Framework REST API Interface. 16 | 17 | Used with a Duet 3 + SBC. 18 | Must use RRF3. 19 | """ 20 | api_name = 'DSF_REST' 21 | 22 | def get_model(self, key: str = None, **kwargs) -> Dict: 23 | url = f'{self.base_url}/machine/status' 24 | r = requests.get(url) 25 | j = r.json() 26 | if key is not None: 27 | keys = key.split('.') 28 | return reduce(operator.getitem, keys, j) 29 | return j 30 | 31 | def send_code(self, code: str) -> Dict: 32 | url = f'{self.base_url}/machine/code' 33 | r = requests.post(url, data=code) 34 | return {'response': r.text} 35 | 36 | def get_file(self, filename: str, directory: str = 'gcodes', binary: bool = False) -> str: 37 | """ 38 | filename: name of the file you want to download including extension 39 | directory: the folder that the file is in, options are ['gcodes', 'macros', 'sys'] 40 | binary: return binary data instead of a string 41 | 42 | returns the file as a string or binary data 43 | """ 44 | url = f'{self.base_url}/machine/file/{directory}/{filename}' 45 | r = requests.get(url) 46 | if not r.ok: 47 | raise ValueError 48 | if binary: 49 | return r.content 50 | else: 51 | return r.text 52 | 53 | def upload_file(self, file: Union[str, bytes, StringIO, TextIOWrapper, BytesIO], filename: str, directory: str = 'gcodes') -> Dict: 54 | """ 55 | file: the path to the file you want to upload from your PC 56 | directory: the folder that the file is in, options are ['gcodes', 'macros', 'sys'] 57 | """ 58 | url = f'{self.base_url}/machine/file/{directory}/{filename}' 59 | r = requests.put(url, data=file, headers={'Content-Type': 'application/octet-stream'}) 60 | if not r.ok: 61 | raise ValueError 62 | return {'err': 0} 63 | 64 | def get_fileinfo(self, filename: str = None, directory: str = 'gcodes') -> Dict: 65 | url = f'{self.base_url}/machine/fileinfo/{directory}/{filename}' 66 | r = requests.get(url) 67 | if not r.ok: 68 | raise ValueError 69 | return r.json() 70 | 71 | def delete_file(self, filename: str, directory: str = 'gcodes') -> Dict: 72 | url = f'{self.base_url}/machine/file/{directory}/{filename}' 73 | r = requests.delete(url) 74 | if not r.ok: 75 | raise ValueError 76 | return {'err': 0} 77 | 78 | def move_file(self, from_path: str, to_path: str, force: bool = False) -> Dict: 79 | url = f'{self.base_url}/machine/file/move' 80 | r = requests.post(url, {'from': f'{from_path}', 'to': f'{to_path}', 'force': force}) 81 | if not r.ok: 82 | raise ValueError 83 | return {'err': 0} 84 | 85 | def get_directory(self, directory: str) -> List[Dict]: 86 | url = f'{self.base_url}/machine/directory/{directory}' 87 | r = requests.get(url) 88 | if not r.ok: 89 | raise ValueError 90 | return r.json() 91 | 92 | def create_directory(self, directory: str) -> Dict: 93 | url = f'{self.base_url}/machine/directory/{directory}' 94 | r = requests.put(url) 95 | if not r.ok: 96 | raise ValueError 97 | return {'err': 0} 98 | -------------------------------------------------------------------------------- /duetwebapi/api/dwc_api.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | from typing import Dict, List, Union 4 | from io import StringIO, TextIOWrapper, BytesIO 5 | 6 | import requests 7 | 8 | from .base import DuetAPI 9 | 10 | 11 | class DWCAPI(DuetAPI): 12 | """ 13 | Duet Web Control REST API Interface. 14 | 15 | Used with a Duet 2/3 in standalone mode. 16 | Must use RRF3. 17 | """ 18 | api_name = 'DWC_REST' 19 | 20 | def connect(self, password=''): 21 | """ Start connection to Duet """ 22 | url = f'{self.base_url}/rr_connect' 23 | r = requests.get(url, {'password': password}) 24 | if not r.ok: 25 | raise ValueError 26 | return r.json() 27 | 28 | def disconnect(self): 29 | """ End connection to Duet """ 30 | url = f'{self.base_url}/rr_disconnect' 31 | r = requests.get(url) 32 | if not r.ok: 33 | raise ValueError 34 | return r.json() 35 | 36 | def get_model(self, key: str = None, depth: int = 99, verbose: bool = True, null: bool = True, frequent: bool = False, obsolete: bool = False) -> Dict: 37 | url = f'{self.base_url}/rr_model' 38 | flags = f'd{depth}' 39 | flags += 'v' if verbose is True else '' 40 | flags += 'n' if null is True else '' 41 | flags += 'f' if frequent is True else '' 42 | flags += 'o' if obsolete is True else '' 43 | r = requests.get(url, {'flags': flags, 'key': key}) 44 | if not r.ok: 45 | raise ValueError 46 | j = r.json() 47 | return j['result'] 48 | 49 | def _get_reply(self) -> Dict: 50 | url = f'{self.base_url}/rr_reply' 51 | r = requests.get(url) 52 | if not r.ok: 53 | raise ValueError 54 | return r.text 55 | 56 | def send_code(self, code: str) -> Dict: 57 | url = f'{self.base_url}/rr_gcode' 58 | r = requests.get(url, {'gcode': code}) 59 | if not r.ok: 60 | raise ValueError 61 | reply = self._get_reply() 62 | return {'response': reply} 63 | 64 | def get_file(self, filename: str, directory: str = 'gcodes', binary: bool = False) -> str: 65 | """ 66 | filename: name of the file you want to download including extension 67 | directory: the folder that the file is in, options are ['gcodes', 'macros', 'sys'] 68 | binary: return binary data instead of a string 69 | 70 | returns the file as a string or binary data 71 | """ 72 | url = f'{self.base_url}/rr_download' 73 | r = requests.get(url, {'name': f'/{directory}/{filename}'}) 74 | if not r.ok: 75 | raise ValueError 76 | if binary: 77 | return r.content 78 | else: 79 | return r.text 80 | 81 | def upload_file(self, file: Union[str, bytes, StringIO, TextIOWrapper, BytesIO], filename: str, directory: str = 'gcodes') -> Dict: 82 | url = f'{self.base_url}/rr_upload?name=/{directory}/{filename}' 83 | r = requests.post(url, data=file) 84 | if not r.ok: 85 | raise ValueError 86 | return r.json() 87 | 88 | def get_fileinfo(self, filename: str = None, directory: str = 'gcodes') -> Dict: 89 | url = f'{self.base_url}/rr_fileinfo' 90 | if filename: 91 | r = requests.get(url, {'name': f'/{directory}/{filename}'}) 92 | else: 93 | r = requests.get(url) 94 | if not r.ok: 95 | raise ValueError 96 | return r.json() 97 | 98 | def delete_file(self, filename: str, directory: str = 'gcodes') -> Dict: 99 | url = f'{self.base_url}/rr_delete' 100 | r = requests.get(url, {'name': f'/{directory}/{filename}'}) 101 | if not r.ok: 102 | raise ValueError 103 | return r.json() 104 | 105 | def move_file(self, from_path, to_path, **_ignored): 106 | # BUG this doesn't work currently 107 | raise NotImplementedError 108 | url = f'{self.base_url}/rr_move' 109 | r = requests.get(url, {'old': f'{from_path}', 'new': f'{to_path}'}) 110 | if not r.ok: 111 | raise ValueError 112 | return r.json() 113 | 114 | def get_directory(self, directory: str) -> List[Dict]: 115 | url = f'{self.base_url}/rr_filelist' 116 | r = requests.get(url, {'dir': f'/{directory}'}) 117 | if not r.ok: 118 | raise ValueError 119 | return r.json()['files'] 120 | 121 | def create_directory(self, directory: str) -> Dict: 122 | url = f'{self.base_url}/rr_mkdir' 123 | r = requests.get(url, {'dir': f'/{directory}'}) 124 | if not r.ok: 125 | raise ValueError 126 | return r.json() 127 | -------------------------------------------------------------------------------- /duetwebapi/api/wrapper.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Dict, List 3 | 4 | from .base import DuetAPI 5 | 6 | 7 | class DuetAPIWrapper(DuetAPI): 8 | def emergency_stop(self) -> None: 9 | self.send_code('M112') 10 | self.send_code('M999') 11 | 12 | def start_print(self, filename: str) -> Dict: 13 | return self.send_code(f'M32 "{filename}"') 14 | 15 | def pause_print(self) -> Dict: 16 | return self.send_code(f'M25') 17 | 18 | def stop_print(self, leave_heaters: bool = True) -> Dict: 19 | status = self.get_status() 20 | if status == 'idle': 21 | return 22 | if not status == 'paused': 23 | self.pause_print() 24 | 25 | code = 'M0' 26 | if leave_heaters: 27 | code += ' H1' 28 | return self.send_code(code) 29 | 30 | def get_coords(self) -> Dict: 31 | axes = self.get_model(key='move.axes', verbose=False) 32 | ret = {} 33 | for i in range(0, len(axes)): 34 | ret[axes[i]['letter']] = axes[i]['userPosition'] 35 | return(ret) 36 | 37 | def get_layer(self) -> int: 38 | layer = self.get_model(key='job.layer', verbose=False) 39 | if not layer: 40 | layer = 0 41 | return(layer) 42 | 43 | def get_num_extruders(self) -> int: 44 | extruders = self.get_model(key='move.extruders', verbose=False, depth=2) 45 | return len(extruders) 46 | 47 | def get_num_tools(self) -> int: 48 | tools = self.get_model(key='tools', verbose=False, depth=1) 49 | return len(tools) 50 | 51 | def get_status(self) -> str: 52 | status = self.get_model(key='state.status') 53 | return status 54 | 55 | def get_temperatures(self) -> List[Dict]: 56 | sensors = self.get_model(key='sensors.analog') 57 | return sensors 58 | 59 | def get_current_tool(self) -> int: 60 | tool = self.get_model(key='state.currentTool', verbose=False) 61 | return tool 62 | 63 | def get_messagebox(self) -> Dict: 64 | messagebox = self.get_model(key='state.messageBox') 65 | return messagebox 66 | 67 | def acknowledge_message(self, response: int = 0) -> Dict: 68 | return self.send_code(f'M292 P{response}') 69 | -------------------------------------------------------------------------------- /duetwebapi/dwa_factory.py: -------------------------------------------------------------------------------- 1 | # Python Script containing a class to send commands to, and query specific information from, 2 | # Duet based printers running Duet RepRap V3 firmware. 3 | # 4 | # Does NOT hold open the connection. Use for low-volume requests. 5 | # Does NOT, at this time, support Duet passwords. 6 | # 7 | # Copyright (C) 2020 Andy Everitt all rights reserved. 8 | # Released under The MIT License. Full text available via https://opensource.org/licenses/MIT 9 | # 10 | # Requires Python3 11 | 12 | import logging 13 | from typing import Dict, List 14 | 15 | import requests 16 | 17 | from .api import DSFAPI, DWCAPI, DuetAPI, DuetAPIWrapper 18 | 19 | 20 | class DuetWebAPIFactory: 21 | def __init__(self) -> None: 22 | self._creators = {} 23 | 24 | def __call__(self, base_url: str): 25 | Api = self.get_api(base_url) 26 | Wrapper = self.create_wrapper(Api) 27 | return Wrapper(base_url) 28 | 29 | def register_api(self, name: str, creator: object, url_suffix: str) -> None: 30 | self._creators[name] = {'creator': creator, 'url_suffix': url_suffix} 31 | 32 | def get_api(self, base_url) -> DuetAPI: 33 | for api in self._creators.values(): 34 | if not base_url.startswith('http://'): 35 | base_url = f'http://{base_url}' 36 | url = base_url + api['url_suffix'] 37 | try: 38 | r = requests.get(url, timeout=(2, 60)) 39 | except requests.exceptions.ConnectionError: 40 | continue 41 | if r.ok: 42 | creator = api['creator'] 43 | return creator 44 | 45 | logging.error(f'Can not get API for {base_url}') 46 | raise ValueError 47 | 48 | def create_wrapper(self, creator: DuetAPI): 49 | Wrapper = type('DuetWebAPI', (DuetAPIWrapper, creator), {}) 50 | 51 | return Wrapper 52 | 53 | 54 | DuetWebAPI = DuetWebAPIFactory() 55 | DuetWebAPI.register_api(DWCAPI.api_name, DWCAPI, '/rr_model') 56 | DuetWebAPI.register_api(DSFAPI.api_name, DSFAPI, '/machine/status') 57 | -------------------------------------------------------------------------------- /examples.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from duetwebapi import DuetWebAPI as DWA 3 | import json 4 | 5 | ################################### 6 | # Create API link 7 | ################################### 8 | riley = DWA('http://riley') 9 | bruce = DWA('http://bruce') 10 | 11 | ################################### 12 | # REST API methods 13 | ################################### 14 | riley.get_model() 15 | bruce.get_model() 16 | 17 | riley.send_code('M115') 18 | bruce.send_code('M115') 19 | 20 | with open('test.gcode', 'r') as f: 21 | riley.upload_file(f, 'test.gcode') 22 | bruce.upload_file(f, 'test.gcode') 23 | 24 | riley.get_file('test.gcode') 25 | bruce.get_file('test.gcode') 26 | 27 | riley.move_file('gcodes/test.gcode', 'gcodes/test2.gcode') 28 | # bruce.move_file('gcodes/test.gcode', 'gcodes/test2.gcode') 29 | 30 | riley.delete_file('test2.gcode') 31 | bruce.delete_file('test2.gcode') 32 | 33 | riley.get_directory('gcodes') 34 | bruce.get_directory('gcodes') 35 | 36 | ################################### 37 | # Wrapper methods 38 | ################################### 39 | riley.get_coords() 40 | bruce.get_coords() 41 | 42 | riley.get_layer() 43 | bruce.get_layer() 44 | 45 | riley.get_num_extruders() 46 | bruce.get_num_extruders() 47 | 48 | riley.get_num_tools() 49 | bruce.get_num_tools() 50 | 51 | riley.get_status() 52 | bruce.get_status() 53 | 54 | riley.get_temperatures() 55 | bruce.get_temperatures() 56 | pass 57 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests==2.24.0 2 | -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | autopep8==1.5.4 2 | bleach==3.3.0 3 | bumpversion 4 | certifi==2020.6.20 5 | chardet==3.0.4 6 | colorama==0.4.4 7 | docutils==0.16 8 | idna==2.10 9 | keyring==21.4.0 10 | packaging==20.4 11 | pkginfo==1.6.1 12 | pycodestyle==2.6.0 13 | Pygments==2.7.2 14 | pyparsing==2.4.7 15 | pywin32-ctypes==0.2.0 16 | readme-renderer==28.0 17 | requests==2.24.0 18 | requests-toolbelt==0.9.1 19 | rfc3986==1.4.0 20 | six==1.15.0 21 | toml==0.10.1 22 | tqdm==4.51.0 23 | twine==3.2.0 24 | urllib3==1.25.11 25 | webencodings==0.5.1 26 | wheel 27 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages, setup 2 | import pathlib 3 | 4 | # The directory containing this file 5 | HERE = pathlib.Path(__file__).parent 6 | 7 | # The text of the README file 8 | README = (HERE / "README.md").read_text() 9 | 10 | setup( 11 | name="duetwebapi", 12 | version="1.3.0-b0", 13 | include_package_data=True, 14 | packages=find_packages(), 15 | 16 | install_requires=[ 17 | "requests >= 2.22.0", 18 | ], 19 | 20 | author="Andy Everitt", 21 | author_email="andreweveritt@e3d-online.com", 22 | description="Python interface to Duet REST API", 23 | long_description=README, 24 | long_description_content_type="text/markdown", 25 | url="https://github.com/AndyEveritt/DuetWebAPI", 26 | classifiers=[ 27 | "License :: OSI Approved :: MIT License", 28 | "Programming Language :: Python :: 3", 29 | "Operating System :: OS Independent", 30 | ], 31 | ) 32 | -------------------------------------------------------------------------------- /test.gcode: -------------------------------------------------------------------------------- 1 | G1 X10 2 | G1 Y10 --------------------------------------------------------------------------------