├── instax ├── tests │ ├── __init__.py │ ├── test_image.png │ ├── testEncodedImage.instax │ ├── test_socket.py │ ├── test_image_encoder.py │ ├── test_replay.py │ ├── test_sp2.py │ ├── test_sp3.py │ ├── replay.json │ ├── test_premade.py │ └── test_basic.py ├── __init__.py ├── exceptions.py ├── comms.py ├── instaxImage.py ├── print.py ├── sp2.py ├── sp3.py ├── debugServer.py └── packet.py ├── .flake8 ├── .travis.yml ├── pyproject.toml ├── setup.py ├── LICENSE ├── .pre-commit-config.yaml ├── .gitignore ├── development.md ├── .github └── workflows │ ├── python-publish.yml │ └── python-test.yml ├── README.md └── poetry.lock /instax/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /instax/__init__.py: -------------------------------------------------------------------------------- 1 | version = "0.8.0" 2 | -------------------------------------------------------------------------------- /instax/tests/test_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpwsutton/instax_api/HEAD/instax/tests/test_image.png -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 120 3 | 4 | select = C,E,F,W,B,B950 5 | extend-ignore = E501,E203,W503 6 | -------------------------------------------------------------------------------- /instax/tests/testEncodedImage.instax: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpwsutton/instax_api/HEAD/instax/tests/testEncodedImage.instax -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.7" 4 | # command to install dependencies 5 | install: "pip install -r requirements.txt" 6 | # command to run tests 7 | script: coverage run --source instax -m pytest instax/tests/*.py --doctest-modules 8 | after_success: coveralls 9 | -------------------------------------------------------------------------------- /instax/exceptions.py: -------------------------------------------------------------------------------- 1 | """Exceptions for use in client.""" 2 | 3 | 4 | class CommandTimedOutException(TimeoutError): 5 | """Raise this when a command has timed out.""" 6 | 7 | 8 | class ConnectError(Exception): 9 | """Raise this when a connect fails.""" 10 | 11 | 12 | class CommandError(Exception): 13 | """Raise this when a command fails.""" 14 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "instax-api" 3 | version = "0.8.0" 4 | description = "A Python module and app to print photos on the Fujifim Instax Printers" 5 | authors = ["James Sutton"] 6 | license = "MIT" 7 | readme = "README.md" 8 | packages = [{include = "instax"}] 9 | 10 | [tool.poetry.dependencies] 11 | python = "^3.10" 12 | Pillow = "^9.3.0" 13 | loguru = "^0.6.0" 14 | 15 | 16 | [tool.poetry.group.dev.dependencies] 17 | gitlint = "^0.17.0" 18 | pre-commit = "^2.20.0" 19 | pytest = "^7.2.0" 20 | pytest-cov = "^4.0.0" 21 | 22 | 23 | [build-system] 24 | requires = ["poetry-core"] 25 | build-backend = "poetry.core.masonry.api" 26 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from setuptools import setup 4 | 5 | this_directory = Path(__file__).parent 6 | long_description = (this_directory / "README.md").read_text() 7 | 8 | setup( 9 | name="instax_api", 10 | version="0.8.0", 11 | description="Fujifilm Instax SP2 & SP3 Library and CLI Utility", 12 | long_description=long_description, 13 | long_description_content_type="text/markdown", 14 | url="https://github.com/jpwsutton/instax_api", 15 | author="James Sutton", 16 | author_email="james@jsutton.co.uk", 17 | license="MIT", 18 | keywords="instax", 19 | packages=["instax"], 20 | install_requires=[ 21 | "Pillow", 22 | ], 23 | ) 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 James Sutton 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 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.3.0 4 | hooks: 5 | - id: end-of-file-fixer 6 | - id: check-added-large-files 7 | - id: requirements-txt-fixer 8 | - id: check-json 9 | - id: check-merge-conflict 10 | - repo: https://github.com/pycqa/flake8 11 | rev: 5.0.4 12 | hooks: 13 | - id: flake8 14 | - repo: https://github.com/pycqa/isort 15 | rev: 5.10.1 16 | hooks: 17 | - id: isort 18 | name: isort (python) 19 | args: ["--profile", "black"] 20 | - repo: https://github.com/asottile/pyupgrade 21 | rev: v3.2.0 22 | hooks: 23 | - id: pyupgrade 24 | args: ['--py3-plus', '--py36-plus'] 25 | - repo: https://github.com/psf/black 26 | rev: 22.10.0 27 | hooks: 28 | - id: black 29 | args: 30 | - --line-length=120 31 | - repo: https://github.com/jorisroovers/gitlint 32 | rev: v0.17.0 33 | hooks: 34 | - id: gitlint 35 | name: gitlint 36 | language: python 37 | entry: gitlint 38 | args: [--staged, --msg-filename] 39 | stages: [commit-msg] 40 | -------------------------------------------------------------------------------- /instax/tests/test_socket.py: -------------------------------------------------------------------------------- 1 | """ 2 | Instax SP* Socket Tests 3 | 4 | James Sutton 2020 5 | """ 6 | import threading 7 | import unittest 8 | 9 | import pytest 10 | 11 | from instax.debugServer import DebugServer 12 | from instax.sp2 import SP2 13 | 14 | 15 | class SocketTests(unittest.TestCase): 16 | """ 17 | Very basic first pass socket test to make sure a simple command works 18 | """ 19 | 20 | @pytest.fixture(autouse=True) 21 | def debug_server(self): 22 | server = DebugServer(host="0.0.0.0", port=0) 23 | self.server_port = server.getPort() 24 | print(f"Server running on port {self.server_port}") 25 | 26 | thread = threading.Thread(target=server.start) 27 | thread.daemon = True 28 | thread.start() 29 | yield server 30 | 31 | def test_send_recieve_command(self): 32 | sp2 = SP2(ip="0.0.0.0", port=self.server_port) 33 | sp2.connect() 34 | model_name = sp2.getPrinterModelName().payload["modelName"] 35 | print(f"Model name returned was: {model_name}") 36 | sp2.close() 37 | self.assertEqual("SP-2", model_name) 38 | 39 | 40 | if __name__ == "__main__": 41 | 42 | unittest.main() 43 | -------------------------------------------------------------------------------- /instax/tests/test_image_encoder.py: -------------------------------------------------------------------------------- 1 | """ 2 | Instax SP2 Test File. 3 | 4 | @jpwsutton 2016/17 5 | """ 6 | import unittest 7 | 8 | from instax.instaxImage import InstaxImage 9 | 10 | 11 | class ImageTests(unittest.TestCase): 12 | """Instax-SP2 Image Encoding / Decoding Test Class.""" 13 | 14 | def test_encode_and_decode_image(self): 15 | """Test Decoding and then Encoding a premade instax image.""" 16 | encodedImageFile = "instax/tests/testEncodedImage.instax" 17 | rawInstaxBytes = None 18 | with open(encodedImageFile, "rb") as infile: 19 | rawBytes = infile.read() 20 | rawInstaxBytes = bytearray(rawBytes) 21 | self.assertEqual(len(rawInstaxBytes), 1440000) 22 | 23 | # Initialize The Instax Image 24 | instaxImage = InstaxImage() 25 | 26 | # Decode the Image from the Instax Byte Array 27 | instaxImage.decodeImage(rawInstaxBytes) 28 | 29 | # Re-Encode the image 30 | 31 | encodedImage = instaxImage.encodeImage() 32 | self.assertEqual(len(encodedImage), 1440000) 33 | 34 | for x in range(1440000): 35 | if rawInstaxBytes[x] != encodedImage[x]: 36 | message = f"Mismatch: Index: {x}: {rawInstaxBytes[x]} != {encodedImage[x]}" 37 | self.fail(message, True) 38 | 39 | 40 | if __name__ == "__main__": 41 | 42 | unittest.main() 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | *.pyc 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | env/ 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 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 | # IPython Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # dotenv 80 | .env 81 | 82 | # virtualenv 83 | venv/ 84 | ENV/ 85 | 86 | # Spyder project settings 87 | .spyderproject 88 | 89 | # Rope project settings 90 | .ropeproject 91 | 92 | # Coverage 93 | .coverage 94 | -------------------------------------------------------------------------------- /instax/tests/test_replay.py: -------------------------------------------------------------------------------- 1 | """ 2 | Instax SP2 Test File. 3 | 4 | @jpwsutton 2016/17 5 | """ 6 | import json 7 | import unittest 8 | from pprint import pprint 9 | 10 | from instax.packet import Packet, PacketFactory 11 | 12 | 13 | class PacketTests(unittest.TestCase): 14 | """ 15 | Instax-SP2 Packet Test Class. 16 | 17 | A series of tests to verify that all commands and responses can be 18 | correctly encoded and decoded. 19 | """ 20 | 21 | def helper_verify_header( 22 | self, 23 | header, 24 | direction, 25 | type, 26 | length, 27 | time, 28 | pin=None, 29 | returnCode=None, 30 | unknown1=None, 31 | ejecting=None, 32 | battery=None, 33 | printCount=None, 34 | ): 35 | """Verify the Header of a packet.""" 36 | self.assertEqual(header["startByte"], direction) 37 | self.assertEqual(header["cmdByte"], type) 38 | self.assertEqual(header["packetLength"], length) 39 | self.assertEqual(header["sessionTime"], time) 40 | if direction == Packet.MESSAGE_MODE_COMMAND: 41 | self.assertEqual(header["password"], pin) 42 | if direction == Packet.MESSAGE_MODE_RESPONSE: 43 | self.assertEqual(header["returnCode"], returnCode) 44 | # self.assertEqual(header['unknown1'], unknown1) 45 | self.assertEqual(header["ejecting"], ejecting) 46 | self.assertEqual(header["battery"], battery) 47 | self.assertEqual(header["printCount"], printCount) 48 | 49 | def test_process_log(self): 50 | """Import a json log and replay the messages.""" 51 | filename = "instax/tests/replay.json" 52 | json_data = open(filename) 53 | data = json.load(json_data) 54 | json_data.close() 55 | decodedPacketList = [] 56 | for packet in data: 57 | readBytes = bytearray.fromhex(packet["bytes"]) 58 | packetFactory = PacketFactory() 59 | decodedPacket = packetFactory.decode(readBytes) 60 | # decodedPacket.printDebug() 61 | packetObj = decodedPacket.getPacketObject() 62 | decodedPacketList.append(packetObj) 63 | 64 | pprint(decodedPacketList) 65 | with open("log2.json", "w") as outfile: 66 | json.dump(decodedPacketList, outfile, indent=4) 67 | 68 | 69 | if __name__ == "__main__": 70 | 71 | unittest.main() 72 | -------------------------------------------------------------------------------- /instax/tests/test_sp2.py: -------------------------------------------------------------------------------- 1 | """ 2 | Instax SP* Socket Tests 3 | 4 | James Sutton 2020 5 | """ 6 | import threading 7 | import unittest 8 | 9 | import pytest 10 | 11 | from instax.debugServer import DebugServer 12 | from instax.instaxImage import InstaxImage 13 | from instax.sp2 import SP2 14 | 15 | test_image = "instax/tests/test_image.png" 16 | server_batt = 2 17 | server_remain = 10 18 | server_total = 20 19 | 20 | progress_log = [] 21 | 22 | 23 | def updateProgress(count, total, status=""): 24 | progress_log.append({"count": count, "total": total, "status": status}) 25 | 26 | 27 | class SP2Tests(unittest.TestCase): 28 | """ 29 | Tests on the SP2 class 30 | """ 31 | 32 | @pytest.fixture(autouse=True) 33 | def debug_server(self): 34 | server = DebugServer( 35 | host="0.0.0.0", port=0, version=2, battery=server_batt, remaining=server_remain, total=server_total 36 | ) 37 | self.server_port = server.getPort() 38 | print(f"SP-2 Server running on port {self.server_port}") 39 | 40 | thread = threading.Thread(target=server.start) 41 | thread.daemon = True 42 | thread.start() 43 | yield server 44 | 45 | def test_get_printer_info(self): 46 | # Getting Printer Information 47 | sp2 = SP2(ip="0.0.0.0", port=self.server_port) 48 | info = sp2.getPrinterInformation() 49 | # print(info) 50 | self.assertEqual(info["model"], "SP-2") 51 | self.assertEqual(info["battery"], 3) # Something odd here... 52 | self.assertEqual(info["printCount"], 4) # Something odd here... 53 | self.assertEqual(info["count"], server_total) 54 | 55 | def test_print_photo(self): 56 | sp2 = SP2(ip="0.0.0.0", port=self.server_port) 57 | 58 | instaxImage = InstaxImage(type=2) 59 | instaxImage.loadImage(test_image) 60 | instaxImage.convertImage() 61 | # Save a copy of the converted bitmap 62 | # instaxImage.saveImage("test.bmp") 63 | # Preview the image that is about to print 64 | # instaxImage.previewImage() 65 | encodedImage = instaxImage.encodeImage() 66 | sp2.printPhoto(encodedImage, updateProgress) 67 | # print(progress_log) 68 | self.assertEqual(progress_log[-1]["count"], 100) 69 | self.assertTrue("Print is complete!" in progress_log[-1]["status"]) 70 | 71 | 72 | if __name__ == "__main__": 73 | 74 | unittest.main() 75 | -------------------------------------------------------------------------------- /instax/tests/test_sp3.py: -------------------------------------------------------------------------------- 1 | """ 2 | Instax SP* Socket Tests 3 | 4 | James Sutton 2020 5 | """ 6 | import threading 7 | import unittest 8 | 9 | import pytest 10 | 11 | from instax.debugServer import DebugServer 12 | from instax.instaxImage import InstaxImage 13 | from instax.sp3 import SP3 14 | 15 | test_image = "instax/tests/test_image.png" 16 | server_batt = 2 17 | server_remain = 10 18 | server_total = 20 19 | 20 | progress_log = [] 21 | 22 | 23 | def updateProgress(count, total, status=""): 24 | progress_log.append({"count": count, "total": total, "status": status}) 25 | 26 | 27 | class SP3Tests(unittest.TestCase): 28 | """ 29 | Tests on the SP2 class 30 | """ 31 | 32 | @pytest.fixture(autouse=True) 33 | def debug_server(self): 34 | server = DebugServer( 35 | host="0.0.0.0", port=0, version=3, battery=server_batt, remaining=server_remain, total=server_total 36 | ) 37 | self.server_port = server.getPort() 38 | print(f"SP-2 Server running on port {self.server_port}") 39 | 40 | thread = threading.Thread(target=server.start) 41 | thread.daemon = True 42 | thread.start() 43 | yield server 44 | 45 | def test_get_printer_info(self): 46 | # Getting Printer Information 47 | sp2 = SP3(ip="0.0.0.0", port=self.server_port) 48 | info = sp2.getPrinterInformation() 49 | # print(info) 50 | self.assertEqual(info["model"], "SP-3") 51 | self.assertEqual(info["battery"], 3) # Something odd here... 52 | self.assertEqual(info["printCount"], 4) # Something odd here... 53 | self.assertEqual(info["count"], server_total) 54 | 55 | def test_print_photo(self): 56 | sp2 = SP3(ip="0.0.0.0", port=self.server_port) 57 | 58 | instaxImage = InstaxImage(type=3) 59 | instaxImage.loadImage(test_image) 60 | instaxImage.convertImage() 61 | # Save a copy of the converted bitmap 62 | # instaxImage.saveImage("test.bmp") 63 | # Preview the image that is about to print 64 | # instaxImage.previewImage() 65 | encodedImage = instaxImage.encodeImage() 66 | sp2.printPhoto(encodedImage, updateProgress) 67 | # print(progress_log) 68 | self.assertEqual(progress_log[-1]["count"], 100) 69 | self.assertTrue("Print is complete!" in progress_log[-1]["status"]) 70 | 71 | 72 | if __name__ == "__main__": 73 | 74 | unittest.main() 75 | -------------------------------------------------------------------------------- /development.md: -------------------------------------------------------------------------------- 1 | # Development Guide 2 | 3 | 4 | ## Developing using Poetry 5 | 6 | * Install poetry on your development machine: https://python-poetry.org/docs/ 7 | * Run `poetry shell` to launch the virtual environment. 8 | * Run `poetry install` to download all main and dev dependencies. 9 | * Run `pre-commit install` and then `pre-commit install --hook-type commit-msg` to set up the pre-commit checks. 10 | * When you are finished, just use `exit` to end your session. 11 | 12 | ## Running the Test Server 13 | 14 | The Test server was written to allow developers to simulate an Instax SP-2 printer in order to test the client. 15 | 16 | In order to connect the application, the device running the test server will need to be hosting a WiFi network with an SSID starting with `INSTAX-` followed by a number of hex characters (This would usually be the MAC address of the printer). In my case, I used a Raspberry Pi Model 3 to host the wireless network and run the Test Server on. 17 | Once you have set up the pi following these instructions: Tutorial link goes here..., you will need to usually run these three commands : 18 | 19 | ``` 20 | sudo service udhcpd start 21 | sudo hostapd /etc/hostapd/hostapd.conf (This will keep running so do it in another tab or bg) 22 | sudo ifconfig wlan0 192.168.0.251 23 | ``` 24 | 25 | Then run: `python3 -m instax.debugServer` 26 | 27 | ## Hidden options in instax-print CLI client 28 | In order to make debugging and using the test server easier, there are some hidden options in the instax-print application that you can use, these include: 29 | 30 | ``` 31 | -d, --debug Logs extra debug data to log. 32 | -l, --log Log information to log file ddmmyy-hhmmss.log 33 | -o HOST, --host HOST The Host IP to connect to the server on. 34 | -p PORT, --port PORT The port to connect to the server on. 35 | -t TIMEOUT, --timeout TIMEOUT 36 | The timeout to use when communicating. 37 | ``` 38 | 39 | For example, when using the test server, you can use the following command to print: `python3 -m instax.print myImage.jpg -o localhost -l` 40 | 41 | The `-d / --debug` option will print a lot more data to the log file, specifically detailed dumps of every command / response sent or received by the client. This is handy when trying to identify issues in the packet library. 42 | 43 | ## Running tests 44 | Simply run the command: `python3 -m pytest instax` 45 | 46 | 47 | ## Inspecting Packets in Wireshark 48 | * Useful filter: `tcp.port == 8080 && tcp.flags.push == 1` 49 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | name: Upload Instax Python Package 2 | 3 | 4 | on: 5 | release: 6 | types: [published] 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | deploy: 13 | 14 | runs-on: ubuntu-latest 15 | 16 | environment: publish 17 | 18 | steps: 19 | #---------------------------------------------- 20 | # check-out repo and set-up python 21 | #---------------------------------------------- 22 | - name: Check out repository 23 | uses: actions/checkout@v3 24 | 25 | - name: Set up Python 3.10 26 | uses: actions/setup-python@v4 27 | with: 28 | python-version: "3.10" 29 | 30 | #---------------------------------------------- 31 | # ----- install & configure poetry ----- 32 | #---------------------------------------------- 33 | - name: Cache poetry install 34 | uses: actions/cache@v3 35 | with: 36 | path: ~/.local # the path depends on OS 37 | key: poetry-0 # increment to reset cache 38 | - name: Install poetry 39 | if: steps.cached-poetry.outputs.cache-hit != 'true' 40 | uses: snok/install-poetry@v1 41 | with: 42 | virtualenvs-create: true 43 | virtualenvs-in-project: true 44 | installer-parallel: true 45 | 46 | #---------------------------------------------- 47 | # load cached venv if cache exists 48 | #---------------------------------------------- 49 | - name: Load Cached venv 50 | id: cached-poetry-dependencies 51 | uses: actions/cache@v3 52 | with: 53 | path: .venv 54 | key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version}}-${{ hashFiles('**/poetry.lock') }} 55 | 56 | #---------------------------------------------- 57 | # install dependencies if cache does not exist 58 | #---------------------------------------------- 59 | - name: Install dependencies 60 | if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' 61 | run: poetry install --no-interaction --no-root 62 | #---------------------------------------------- 63 | # Build Distributables 64 | #---------------------------------------------- 65 | - name: Install project 66 | run: poetry build 67 | 68 | #---------------------------------------------- 69 | # build and publish to pypi 70 | #---------------------------------------------- 71 | - name: Publish project 72 | env: 73 | POETRY_PYPI_TOKEN_PYPI: ${{ secrets.POETRY_PYPI_TOKEN_PYPI }} 74 | run: poetry publish 75 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # instax_api 2 | [![.github/workflows/python-test.yml](https://github.com/jpwsutton/instax_api/actions/workflows/python-test.yml/badge.svg)](https://github.com/jpwsutton/instax_api/actions/workflows/python-test.yml) 3 | [![Coverage Status](https://img.shields.io/coveralls/jpwsutton/instax_api/master.svg)](https://coveralls.io/github/jpwsutton/instax_api?branch=master) 4 | 5 | This is a Python Module to interact and print photos to the Fujifilm Instax SP-2 and SP-3 printers. 6 | 7 | 8 | ## Install this library 9 | 10 | In order to use this library, you will need to be using Python 3 11 | 12 | ``` 13 | pip3 install instax-api 14 | ``` 15 | 16 | 17 | ## Usage 18 | 19 | **note** - From version 0.7.0 to 0.8.0, I moved away from adding a script to just calling the module from pyton using the `-m` argument. 20 | 21 | ``` 22 | $ python3 -m instax.print --help 23 | usage: instax-print [-h] [-i PIN] [-v {1,2,3}] image 24 | 25 | positional arguments: 26 | image The location of the image to print. 27 | 28 | optional arguments: 29 | -h, --help show this help message and exit 30 | -i PIN, --pin PIN The pin code to use, default: 1111. 31 | -v {1,2,3}, --version {1,2,3} 32 | The version of Instax Printer to use (1, 2 or 3). 33 | Default is 2 (SP-2). 34 | ``` 35 | 36 | ### Examples: 37 | 38 | - Printing a Photo to an SP-2 printer: `python3 -m instax.print myPhoto.jpg` 39 | - Printing a Photo to an SP-3 printer: `python3 -m instax.print myPhoto.jpg -v 3` 40 | - Printing a Photo to a printer with a pin that is not the default (1111) `python3 -m instax.print myPhoto.jpg -i 1234` 41 | 42 | ### Hints and tips: 43 | - Make sure you are connected to the correct wifi network, once the printer is turned on, there will be an SSID / WiFi network available that starts with `INSTAX-` followed by 8 numbers. You'll need to connect to this. 44 | - If you have a static IP address set up on your computer, you'll need to turn on DHCP before attempting to print, the Instax printer will automatically assign you a new address once you connect. 45 | - Some Unix based operating systems may require you to use sudo in order to access the network. 46 | - The printer will automatically turn itself off after roughly 10 minutes of innactivity. 47 | - The instax.print utility will attempt to automatically rotate the image so that it either is correctly printed in portrait, or landscape with the thick bottom edge of the print on the left. If you wish to print your photos in a specific orientation that differs from this, then it's reccomended that you orient your photo in a tool like GIMP first, then strip out the rotation metadata. Once the rotation metadata has been stripped, the photo will need to be in a portrait orientation relative to the finished print (e.g. thick edge at the bottom). 48 | 49 | ## Install Manually 50 | 51 | ``` 52 | git clone https://github.com/jpwsutton/instax_api.git 53 | cd instax_api 54 | python3 setup.py install 55 | ``` 56 | -------------------------------------------------------------------------------- /.github/workflows/python-test.yml: -------------------------------------------------------------------------------- 1 | name: "Build, Lint and Test" 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | steps: 12 | #---------------------------------------------- 13 | # check-out repo and set-up python 14 | #---------------------------------------------- 15 | - name: Check out repository 16 | uses: actions/checkout@v3 17 | 18 | - name: Set up Python 3.10 19 | uses: actions/setup-python@v4 20 | with: 21 | python-version: "3.10" 22 | 23 | #---------------------------------------------- 24 | # ----- install & configure poetry ----- 25 | #---------------------------------------------- 26 | - name: Cache poetry install 27 | uses: actions/cache@v3 28 | with: 29 | path: ~/.local # the path depends on OS 30 | key: poetry-0 # increment to reset cache 31 | - name: Install poetry 32 | if: steps.cached-poetry.outputs.cache-hit != 'true' 33 | uses: snok/install-poetry@v1 34 | with: 35 | virtualenvs-create: true 36 | virtualenvs-in-project: true 37 | installer-parallel: true 38 | 39 | #---------------------------------------------- 40 | # load cached venv if cache exists 41 | #---------------------------------------------- 42 | - name: Load Cached venv 43 | id: cached-poetry-dependencies 44 | uses: actions/cache@v3 45 | with: 46 | path: .venv 47 | key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version}}-${{ hashFiles('**/poetry.lock') }} 48 | 49 | #---------------------------------------------- 50 | # install dependencies if cache does not exist 51 | #---------------------------------------------- 52 | - name: Install dependencies 53 | if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' 54 | run: poetry install --no-interaction --no-root 55 | #---------------------------------------------- 56 | # install your root project, if required 57 | #---------------------------------------------- 58 | - name: Install project 59 | run: poetry install --no-interaction 60 | 61 | #---------------------------------------------- 62 | # lint project 63 | #---------------------------------------------- 64 | - name: Lint Project 65 | uses: pre-commit/action@v3.0.0 66 | 67 | #---------------------------------------------- 68 | # run test suite 69 | #---------------------------------------------- 70 | - name: Run tests 71 | run: | 72 | source .venv/bin/activate 73 | pytest --cov-report=lcov:coverage/lcov.info --cov=instax 74 | coverage report 75 | 76 | - name: Coveralls 77 | uses: coverallsapp/github-action@master 78 | with: 79 | github-token: ${{ secrets.GITHUB_TOKEN }} 80 | -------------------------------------------------------------------------------- /instax/comms.py: -------------------------------------------------------------------------------- 1 | import queue 2 | import socket 3 | import threading 4 | 5 | 6 | class ClientCommand: 7 | """A command to the client thread. 8 | Each command type has it's associated data: 9 | 10 | CONNECT: (host, port) tuple 11 | SEND: Data byte array 12 | RECEIVE: None 13 | CLOSE: None 14 | """ 15 | 16 | CONNECT, SEND, RECEIVE, CLOSE = range(4) 17 | 18 | def __init__(self, type, data=None): 19 | self.type = type 20 | self.data = data 21 | 22 | 23 | class ClientReply: 24 | """A reply from the client thread. 25 | Each reply has it's associated data: 26 | 27 | ERROR: The error string. 28 | SUCCESS: Depends on the command, For RECEIVE it's the received 29 | data string, for others, None. 30 | """ 31 | 32 | ERROR, SUCCESS = range(2) 33 | 34 | def __init__(self, type, data=None): 35 | self.type = type 36 | self.data = data 37 | 38 | 39 | class SocketClientThread(threading.Thread): 40 | """Implements the threading.Thread interface (start, join, etc..) and 41 | can be controlled by the cmd_q Queue attribute. Replies are placed 42 | in the reply_q Queue attribute. 43 | """ 44 | 45 | def __init__(self, cmd_q=None, reply_q=None): 46 | super().__init__() 47 | self.cmd_q = cmd_q or queue.Queue() 48 | self.reply_q = reply_q or queue.Queue() 49 | self.alive = threading.Event() 50 | self.alive.set() 51 | self.socket = None 52 | 53 | self.handlers = { 54 | ClientCommand.CONNECT: self._handle_CONNECT, 55 | ClientCommand.CLOSE: self._handle_CLOSE, 56 | ClientCommand.SEND: self._handle_SEND, 57 | ClientCommand.RECEIVE: self._handle_RECEIVE, 58 | } 59 | 60 | def run(self): 61 | while self.alive.is_set(): 62 | try: 63 | # Queue.get with timeout to allow checking self.alive 64 | cmd = self.cmd_q.get(True, 0.1) 65 | self.handlers[cmd.type](cmd) 66 | except queue.Empty: 67 | continue 68 | 69 | def join(self, timeout=None): 70 | self.alive.clear() 71 | threading.Thread.join(self, timeout) 72 | 73 | def _handle_CONNECT(self, cmd): 74 | try: 75 | self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 76 | self.socket.settimeout(5) 77 | self.socket.connect((cmd.data[0], cmd.data[1])) 78 | self.reply_q.put(self._success_reply()) 79 | except OSError as e: 80 | self.reply_q.put(self._error_reply(str(e))) 81 | 82 | def _handle_CLOSE(self, cmd): 83 | self.socket.shutdown(socket.SHUT_RDWR) 84 | self.socket.close() 85 | reply = ClientReply(ClientReply.SUCCESS) 86 | self.reply_q.put(reply) 87 | 88 | def _handle_SEND(self, cmd): 89 | try: 90 | self.socket.sendall(cmd.data) 91 | self.reply_q.put(self._success_reply()) 92 | except OSError as e: 93 | self.reply_q.put(self._error_reply(str(e))) 94 | 95 | def _handle_RECEIVE(self, cmd): 96 | try: 97 | header_data = self._recv_n_bytes(4) 98 | if len(header_data) == 4: 99 | msg_len = (header_data[2] & 0xFF) << 8 | (header_data[3] & 0xFF) << 0 100 | data = self._recv_n_bytes(msg_len - 4) 101 | payload = header_data + data 102 | if len(payload) == msg_len: 103 | self.reply_q.put(self._success_reply(payload)) 104 | return 105 | self.reply_q.put(self._error_reply("Socket Closed Prematuerly")) 106 | except OSError as e: 107 | self.reply_q.put(self._error_reply(str(e))) 108 | 109 | def _recv_n_bytes(self, n): 110 | """Convenience method for receiving excactly n bytes from 111 | self.socket (assuming it's open and connected). 112 | """ 113 | data = bytearray() 114 | while len(data) < n: 115 | chunk = self.socket.recv(n - len(data)) 116 | if chunk == b"": 117 | break 118 | data += chunk 119 | return data 120 | 121 | def _error_reply(self, errstr): 122 | return ClientReply(ClientReply.ERROR, errstr) 123 | 124 | def _success_reply(self, data=None): 125 | return ClientReply(ClientReply.SUCCESS, data) 126 | -------------------------------------------------------------------------------- /instax/instaxImage.py: -------------------------------------------------------------------------------- 1 | """Image transformation utilities.""" 2 | from loguru import logger 3 | from PIL import Image, ImageOps 4 | 5 | 6 | class InstaxImage: 7 | """Image Utilities class.""" 8 | 9 | dimensions = {1: (600, 800), 2: (600, 800), 3: (800, 800)} 10 | 11 | def __init__(self, type=2): 12 | """Initialise the instax Image.""" 13 | self.type = type 14 | self.printHeight, self.printWidth = self.dimensions[self.type] 15 | 16 | def loadImage(self, imagePath): 17 | """Load an image from a path.""" 18 | self.sourceImage = Image.open(imagePath) 19 | 20 | def encodeImage(self): 21 | """Encode the loaded Image.""" 22 | imgWidth, imgHeight = self.myImage.size 23 | # Quick check that it's the right dimensions 24 | if imgWidth + imgHeight != (self.printHeight + self.printWidth): 25 | raise Exception("Image was not 800x600 or 600x800, it was : w:%d, h:%d" % (imgWidth, imgHeight)) 26 | if imgWidth != self.printWidth: 27 | # Rotate the image 28 | self.myImage = self.myImage.rotate(-90, expand=True) 29 | if self.printWidth == self.printHeight: 30 | # Square images are a bit tricky, we have to assume they are oriented correctly 31 | logger.info("Rotating Square Image") 32 | self.myImage = self.myImage.rotate(-90, expand=True) 33 | imagePixels = self.myImage.getdata() 34 | arrayLen = len(imagePixels) * 3 35 | encodedBytes = [None] * arrayLen 36 | for h in range(self.printHeight): 37 | for w in range(self.printWidth): 38 | r, g, b = imagePixels[(h * self.printWidth) + w] 39 | redTarget = (((w * self.printHeight) * 3) + (self.printHeight * 0)) + h 40 | greenTarget = (((w * self.printHeight) * 3) + (self.printHeight * 1)) + h 41 | blueTarget = (((w * self.printHeight) * 3) + (self.printHeight * 2)) + h 42 | encodedBytes[redTarget] = int(r) 43 | encodedBytes[greenTarget] = int(g) 44 | encodedBytes[blueTarget] = int(b) 45 | return encodedBytes 46 | 47 | def decodeImage(self, imageBytes): 48 | """Decode the byte array into an image.""" 49 | targetImg = [] 50 | # Packing the individual colours back together. 51 | for h in range(self.printHeight): 52 | for w in range(self.printWidth): 53 | redTarget = (((w * self.printHeight) * 3) + (self.printHeight * 0)) + h 54 | greenTarget = (((w * self.printHeight) * 3) + (self.printHeight * 1)) + h 55 | blueTarget = (((w * self.printHeight) * 3) + (self.printHeight * 2)) + h 56 | targetImg.append(imageBytes[redTarget]) 57 | targetImg.append(imageBytes[greenTarget]) 58 | targetImg.append(imageBytes[blueTarget]) 59 | preImage = Image.frombytes("RGB", (self.printWidth, self.printHeight), bytes(targetImg)) 60 | self.myImage = preImage.rotate(90, expand=True) 61 | 62 | def convertImage(self, crop_type="middle", backgroundColour=(255, 255, 255, 0)): 63 | """Rotate, Resize and Crop the image. 64 | 65 | Rotate, Resize and Crop the image, so that it is the correct 66 | dimensions for printing to the Instax SP-2. 67 | """ 68 | maxSize = self.printHeight, self.printWidth # The Max Image size 69 | 70 | # Strip Exif and rotate image correctly 71 | rotatedImage = ImageOps.exif_transpose(self.sourceImage) 72 | 73 | # Fit the image to the required ratio 74 | fittedImage = ImageOps.fit(rotatedImage, maxSize, bleed=0, centering=(0.5, 0.5)) 75 | 76 | self.myImage = pure_pil_alpha_to_color_v2(fittedImage, (255, 255, 255)) 77 | 78 | def previewImage(self): 79 | """Preview the image.""" 80 | self.myImage.show() 81 | 82 | def saveImage(self, filename): 83 | """Save the image to the specified path.""" 84 | logger.info(("Saving Image to: ", filename)) 85 | self.myImage.save(filename, "BMP", quality=100, optimise=True) 86 | 87 | def getBytes(self): 88 | """Get the Byte Array from the image.""" 89 | myBytes = self.myImage.tobytes() 90 | return myBytes 91 | 92 | 93 | def pure_pil_alpha_to_color_v2(image, color=(255, 255, 255)): 94 | """Alpha composite an RGBA Image with a specified color. 95 | 96 | Simpler, faster version than the solutions above. 97 | 98 | Source: 098 99 | 100 | Keyword Arguments: 101 | image -- PIL RGBA Image object 102 | color -- Tuple r, g, b (default 255, 255, 255) 103 | 104 | """ 105 | image.load() # needed for split() 106 | background = Image.new("RGB", image.size, color) 107 | background.paste(image) # 3 is the alpha channel 108 | return background 109 | -------------------------------------------------------------------------------- /instax/print.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Instax SP Print Script. 3 | 4 | Author: James Sutton 2017 - jsutton.co.uk 5 | 6 | This can be used to print an image to a Fujifilm Instax SP-2 printer. 7 | Parameters: 8 | - JSON Log File (Default ddmmyy-hhmmss.json) 9 | - Image to print 10 | - Port (Default 8080) 11 | - Host (Default 192.168.0.251) 12 | 13 | """ 14 | import argparse 15 | import datetime 16 | import sys 17 | 18 | from loguru import logger 19 | 20 | from instax.instaxImage import InstaxImage 21 | from instax.sp2 import SP2 22 | from instax.sp3 import SP3 23 | 24 | 25 | def printPrinterInfo(info): 26 | """Log Printer information""" 27 | logger.info("Model: %s" % info["model"]) 28 | logger.info("Firmware: %s" % info["version"]["firmware"]) 29 | logger.info("Battery State: %s" % info["battery"]) 30 | logger.info("Prints Remaining: %d" % info["printCount"]) 31 | logger.info("Total Lifetime Prints: %d" % info["count"]) 32 | logger.info("") 33 | 34 | 35 | # https://gist.github.com/vladignatyev/06860ec2040cb497f0f3 36 | def printProgress(count, total, status=""): 37 | # logger.info(status) 38 | bar_len = 60 39 | filled_len = int(round(bar_len * count / float(total))) 40 | percents = round(100.0 * count / float(total), 1) 41 | bar = "=" * filled_len + "-" * (bar_len - filled_len) 42 | sys.stdout.write("[{}] {}{} ...{}\r".format(bar, percents, "%", status)) 43 | sys.stdout.flush() # As suggested by Rom Ruben (see: http://stackoverflow.com/questions/3173320/text-progress-bar-in-the-console/27871113#comment50529068_27871113) 44 | 45 | 46 | if __name__ == "__main__": 47 | parser = argparse.ArgumentParser() 48 | parser.add_argument("-d", "--debug", action="store_true", default=False, help=argparse.SUPPRESS) 49 | parser.add_argument("-l", "--log", action="store_true", default=False, help=argparse.SUPPRESS) 50 | parser.add_argument("-o", "--host", default="192.168.0.251", help=argparse.SUPPRESS) 51 | parser.add_argument("-p", "--port", type=int, default=8080, help=argparse.SUPPRESS) 52 | parser.add_argument("-i", "--pin", type=int, default=1111, help="The pin code to use, default: 1111.") 53 | parser.add_argument( 54 | "-e", 55 | "--preview", 56 | action="store_true", 57 | default=False, 58 | help="Show a preview of the image before it is printed, then exit.", 59 | ) 60 | parser.add_argument("-t", "--timeout", type=int, default=10, help=argparse.SUPPRESS) 61 | parser.add_argument( 62 | "-v", 63 | "--version", 64 | type=int, 65 | default=2, 66 | choices=[1, 2, 3], 67 | help="The version of Instax Printer to use (1, 2 or 3). Default is 2 (SP-2).", 68 | ) 69 | parser.add_argument("image", help="The location of the image to print.") 70 | args = parser.parse_args() 71 | 72 | if not args.debug: 73 | logger.remove() 74 | logger.add(sys.stderr, level="INFO") 75 | 76 | # If Not specified, set the log file to a datestamp. 77 | if args.log: 78 | logFilename = f"{datetime.datetime.now():%Y-%m-%d.%H-%M-%S.log}" 79 | logger.add(logFilename) 80 | 81 | logger.info("--- Instax Printer Python Client ---") 82 | logger.info("") 83 | myInstax = None 84 | 85 | if args.version == 1: 86 | logger.info("Attempting to print to an Instax SP-1 printer.") 87 | # TODO - Need to find an SP-2 to test with. 88 | elif args.version == 2: 89 | logger.info("Attempting to print to an Instax SP-2 printer.") 90 | myInstax = SP2(ip=args.host, port=args.port, pinCode=args.pin, timeout=args.timeout) 91 | elif args.version == 3: 92 | logger.info("Attempting to print to an Instax SP-3 printer.") 93 | # Warning, this does not work in production yet. 94 | myInstax = SP3(ip=args.host, port=args.port, pinCode=args.pin, timeout=args.timeout) 95 | else: 96 | logger.error("Invalid Instax printer version given") 97 | exit(1) 98 | 99 | if args.preview is True: 100 | # Going to preview the image as it will be printed 101 | logger.info(f"Previewing Image: {args.image}") 102 | instaxImage = InstaxImage(type=args.version) 103 | instaxImage.loadImage(args.image) 104 | instaxImage.convertImage() 105 | instaxImage.previewImage() 106 | logger.info("Preview complete, exiting.") 107 | exit(0) 108 | else: 109 | # Attempt print 110 | logger.info("Connecting to Printer.") 111 | info = myInstax.getPrinterInformation() 112 | printPrinterInfo(info) 113 | 114 | logger.info("Printing Image: %s" % args.image) 115 | # Initialize The Instax Image 116 | instaxImage = InstaxImage(type=args.version) 117 | instaxImage.loadImage(args.image) 118 | instaxImage.convertImage() 119 | # Save a copy of the converted bitmap 120 | # instaxImage.saveImage("test.bmp") 121 | # Preview the image that is about to print 122 | # instaxImage.previewImage() 123 | encodedImage = instaxImage.encodeImage() 124 | myInstax.printPhoto(encodedImage, printProgress) 125 | logger.info("Thank you for using instax-print!") 126 | logger.info( 127 | r""" 128 | \ /\ 129 | ) ( ') 130 | ( / ) 131 | \(___)|""" 132 | ) 133 | -------------------------------------------------------------------------------- /instax/tests/replay.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "bytes": "24c4 0014 63fb 4a6b 0457 0000 0000 0001 fc94 0d0a", 4 | "header": { 5 | "startByte": 36, 6 | "cmdByte": 196, 7 | "packetLength": 20, 8 | "sessionTime": 1677412971, 9 | "password": 1111 10 | }, 11 | "payload": { 12 | "cmdNumber": 1 13 | } 14 | }, 15 | { 16 | "bytes": "2ac4 0018 63fb 4a6b 0000 0000 0000 0034 0001 0002 fcaf 0d0a", 17 | "header": { 18 | "startByte": 42, 19 | "cmdByte": 196, 20 | "packetLength": 24, 21 | "sessionTime": 1677412971, 22 | "returnCode": 0, 23 | "unknown1": 0, 24 | "ejecting": 0, 25 | "battery": 3, 26 | "printCount": 4 27 | }, 28 | "payload": { 29 | "cmdNumber": 1, 30 | "respNumber": 2 31 | } 32 | }, 33 | { 34 | "bytes": "24c4 0014 63fb 4a6b 0457 0000 0000 0002 fc93 0d0a", 35 | "header": { 36 | "startByte": 36, 37 | "cmdByte": 196, 38 | "packetLength": 20, 39 | "sessionTime": 1677412971, 40 | "password": 1111 41 | }, 42 | "payload": { 43 | "cmdNumber": 2 44 | } 45 | }, 46 | { 47 | "bytes": "2ac4 0018 63fb 4a6b 0000 0000 0000 0034 0002 0002 fcae 0d0a", 48 | "header": { 49 | "startByte": 42, 50 | "cmdByte": 196, 51 | "packetLength": 24, 52 | "sessionTime": 1677412971, 53 | "returnCode": 0, 54 | "unknown1": 0, 55 | "ejecting": 0, 56 | "battery": 3, 57 | "printCount": 4 58 | }, 59 | "payload": { 60 | "cmdNumber": 2, 61 | "respNumber": 2 62 | } 63 | }, 64 | { 65 | "bytes": "24c4 0014 63fb 4a6b 0457 0000 0000 0003 fc92 0d0a", 66 | "header": { 67 | "startByte": 36, 68 | "cmdByte": 196, 69 | "packetLength": 20, 70 | "sessionTime": 1677412971, 71 | "password": 1111 72 | }, 73 | "payload": { 74 | "cmdNumber": 3 75 | } 76 | }, 77 | { 78 | "bytes": "2ac4 0018 63fb 4a6b 0000 0000 0000 0034 0003 0002 fcad 0d0a", 79 | "header": { 80 | "startByte": 42, 81 | "cmdByte": 196, 82 | "packetLength": 24, 83 | "sessionTime": 1677412971, 84 | "returnCode": 0, 85 | "unknown1": 0, 86 | "ejecting": 0, 87 | "battery": 3, 88 | "printCount": 4 89 | }, 90 | "payload": { 91 | "cmdNumber": 3, 92 | "respNumber": 2 93 | } 94 | }, 95 | { 96 | "bytes": "24c4 0014 63fb 4a6b 0457 0000 0000 0004 fc91 0d0a", 97 | "header": { 98 | "startByte": 36, 99 | "cmdByte": 196, 100 | "packetLength": 20, 101 | "sessionTime": 1677412971, 102 | "password": 1111 103 | }, 104 | "payload": { 105 | "cmdNumber": 4 106 | } 107 | }, 108 | { 109 | "bytes": "2ac4 0018 63fb 4a6b 0000 0000 0000 0034 0004 0001 fcad 0d0a", 110 | "header": { 111 | "startByte": 42, 112 | "cmdByte": 196, 113 | "packetLength": 24, 114 | "sessionTime": 1677412971, 115 | "returnCode": 0, 116 | "unknown1": 0, 117 | "ejecting": 0, 118 | "battery": 3, 119 | "printCount": 4 120 | }, 121 | "payload": { 122 | "cmdNumber": 4, 123 | "respNumber": 1 124 | } 125 | }, 126 | { 127 | "bytes": "24c4 0014 63fb 4a6b 0457 0000 0000 0005 fc90 0d0a", 128 | "header": { 129 | "startByte": 36, 130 | "cmdByte": 196, 131 | "packetLength": 20, 132 | "sessionTime": 1677412971, 133 | "password": 1111 134 | }, 135 | "payload": { 136 | "cmdNumber": 5 137 | } 138 | }, 139 | { 140 | "bytes": "2ac4 0018 63fb 4a6b 0000 0000 0000 0034 0005 0001 fcac 0d0a", 141 | "header": { 142 | "startByte": 42, 143 | "cmdByte": 196, 144 | "packetLength": 24, 145 | "sessionTime": 1677412971, 146 | "returnCode": 0, 147 | "unknown1": 0, 148 | "ejecting": 0, 149 | "battery": 3, 150 | "printCount": 4 151 | }, 152 | "payload": { 153 | "cmdNumber": 5, 154 | "respNumber": 1 155 | } 156 | }, 157 | { 158 | "bytes": "24c4 0014 63fb 4a6b 0457 0000 0000 0006 fc8f 0d0a", 159 | "header": { 160 | "startByte": 36, 161 | "cmdByte": 196, 162 | "packetLength": 20, 163 | "sessionTime": 1677412971, 164 | "password": 1111 165 | }, 166 | "payload": { 167 | "cmdNumber": 6 168 | } 169 | }, 170 | { 171 | "bytes": "2ac4 0018 63fb 4a6b 0000 0000 0000 0034 0006 0000 fcac 0d0a", 172 | "header": { 173 | "startByte": 42, 174 | "cmdByte": 196, 175 | "packetLength": 24, 176 | "sessionTime": 1677412971, 177 | "returnCode": 0, 178 | "unknown1": 0, 179 | "ejecting": 0, 180 | "battery": 3, 181 | "printCount": 4 182 | }, 183 | "payload": { 184 | "cmdNumber": 6, 185 | "respNumber": 0 186 | } 187 | }, 188 | { 189 | "bytes": "24c4 0014 63fb 4a6b 0457 0000 0000 0007 fc8e 0d0a", 190 | "header": { 191 | "startByte": 36, 192 | "cmdByte": 196, 193 | "packetLength": 20, 194 | "sessionTime": 1677412971, 195 | "password": 1111 196 | }, 197 | "payload": { 198 | "cmdNumber": 7 199 | } 200 | }, 201 | { 202 | "bytes": "2ac4 0018 63fb 4a6b 0000 0000 0000 0034 0007 0000 fcab 0d0a", 203 | "header": { 204 | "startByte": 42, 205 | "cmdByte": 196, 206 | "packetLength": 24, 207 | "sessionTime": 1677412971, 208 | "returnCode": 0, 209 | "unknown1": 0, 210 | "ejecting": 0, 211 | "battery": 3, 212 | "printCount": 4 213 | }, 214 | "payload": { 215 | "cmdNumber": 7, 216 | "respNumber": 0 217 | } 218 | }, 219 | { 220 | "bytes": "24c4 0014 63fb 4a6b 0457 0000 0000 0008 fc8d 0d0a", 221 | "header": { 222 | "startByte": 36, 223 | "cmdByte": 196, 224 | "packetLength": 20, 225 | "sessionTime": 1677412971, 226 | "password": 1111 227 | }, 228 | "payload": { 229 | "cmdNumber": 8 230 | } 231 | }, 232 | { 233 | "bytes": "2ac4 0018 63fb 4a6b 0000 0000 0000 0034 0008 0000 fcaa 0d0a", 234 | "header": { 235 | "startByte": 42, 236 | "cmdByte": 196, 237 | "packetLength": 24, 238 | "sessionTime": 1677412971, 239 | "returnCode": 0, 240 | "unknown1": 0, 241 | "ejecting": 0, 242 | "battery": 3, 243 | "printCount": 4 244 | }, 245 | "payload": { 246 | "cmdNumber": 8, 247 | "respNumber": 0 248 | } 249 | } 250 | ] 251 | -------------------------------------------------------------------------------- /instax/tests/test_premade.py: -------------------------------------------------------------------------------- 1 | """ 2 | Instax SP2 Test File. 3 | 4 | @jpwsutton 2016/17 5 | """ 6 | import unittest 7 | 8 | from instax.packet import Packet, PacketFactory 9 | 10 | 11 | class PacketTests(unittest.TestCase): 12 | """ 13 | Instax-SP2 Premade Packet Test Class. 14 | 15 | A series of tests to verify that existing commands and responses can be 16 | correctly decoded. 17 | """ 18 | 19 | def helper_verify_header( 20 | self, 21 | header, 22 | direction, 23 | type, 24 | length, 25 | time, 26 | pin=None, 27 | returnCode=None, 28 | unknown1=None, 29 | ejecting=None, 30 | battery=None, 31 | printCount=None, 32 | ): 33 | """Verify the Header of a packet.""" 34 | self.assertEqual(header["startByte"], direction) 35 | self.assertEqual(header["cmdByte"], type) 36 | self.assertEqual(header["packetLength"], length) 37 | self.assertEqual(header["sessionTime"], time) 38 | if direction == Packet.MESSAGE_MODE_COMMAND: 39 | self.assertEqual(header["password"], pin) 40 | if direction == Packet.MESSAGE_MODE_RESPONSE: 41 | self.assertEqual(header["returnCode"], returnCode) 42 | # self.assertEqual(header['unknown1'], unknown1) 43 | self.assertEqual(header["ejecting"], ejecting) 44 | self.assertEqual(header["battery"], battery) 45 | self.assertEqual(header["printCount"], printCount) 46 | 47 | def test_premade_resp_specifications(self): 48 | """Test Decoding a Specifications Response with an existing payload.""" 49 | msg = bytearray.fromhex( 50 | "2a4f 0030 e759 eede 0000 0000 0000 0027 0258" 51 | " 0320 0100 000a 0000 0000 ea60 1000 0000 0000" 52 | " 0000 0000 0000 0000 fa41 0d0a" 53 | ) 54 | packetFactory = PacketFactory() 55 | decodedPacket = packetFactory.decode(msg) 56 | self.assertEqual(decodedPacket.payload["maxHeight"], 800) 57 | self.assertEqual(decodedPacket.payload["maxWidth"], 600) 58 | self.assertEqual(decodedPacket.payload["maxColours"], 256) 59 | self.assertEqual(decodedPacket.payload["unknown1"], 10) 60 | self.assertEqual(decodedPacket.payload["maxMsgSize"], 60000) 61 | self.assertEqual(decodedPacket.payload["unknown2"], 16) 62 | self.assertEqual(decodedPacket.payload["unknown3"], 0) 63 | 64 | def test_premade_resp_version(self): 65 | """Test Decoding a Version Response with an existing payload.""" 66 | msg = bytearray.fromhex("2ac0 001c e759 eede 0000" " 0000 0000 0027 0101 0113" " 0000 0000 fbb0 0d0a") 67 | packetFactory = PacketFactory() 68 | decodedPacket = packetFactory.decode(msg) 69 | self.assertEqual(decodedPacket.payload["unknown1"], 257) 70 | self.assertEqual(decodedPacket.payload["firmware"], "01.13") 71 | self.assertEqual(decodedPacket.payload["hardware"], "00.00") 72 | 73 | def test_premade_resp_printCount(self): 74 | """Test Decoding a Print Count Response with an existing payload.""" 75 | msg = bytearray.fromhex( 76 | "2ac1 0024 e759 eede 0000" " 0000 0000 0027 0000 0003" " 00f3 c048 0000 1645 001e" " 0000 f946 0d0a" 77 | ) 78 | packetFactory = PacketFactory() 79 | decodedPacket = packetFactory.decode(msg) 80 | self.assertEqual(decodedPacket.payload["printHistory"], 3) 81 | 82 | def test_premade_cmd_modelName(self): 83 | """Test Decoding a Model Name Command.""" 84 | msg = bytearray.fromhex("24c2 0010 0b8d c2b4 0457 0000 fca0 0d0a") 85 | packetFactory = PacketFactory() 86 | packetFactory.decode(msg) 87 | 88 | def test_premade_cmd_prePrint(self): 89 | """Test Decoding a Pre Print Command.""" 90 | msg = bytearray.fromhex("24c4 0014 4e40 684c 0457" " 0000 0000 0008 fd5e 0d0a") 91 | packetFactory = PacketFactory() 92 | packetFactory.decode(msg) 93 | 94 | def test_premade_cmd_lock(self): 95 | """Test Decoding a Lock Command.""" 96 | msg = bytearray.fromhex("24b3 0014 9619 02df 0457" " 0000 0100 0000 fd28 0d0a") 97 | packetFactory = PacketFactory() 98 | packetFactory.decode(msg) 99 | 100 | def test_premade_resp_lock(self): 101 | """Test Decoding a Lock Response.""" 102 | msg = bytearray.fromhex("2ab3 0014 75b8 bd8e 0000" " 0000 0000 003a fc5c 0d0a") 103 | packetFactory = PacketFactory() 104 | packetFactory.decode(msg) 105 | 106 | def test_premade_cmd_reset(self): 107 | """Test Decoding a Reset Command.""" 108 | msg = bytearray.fromhex("2450 0010 96c9 aada 0457" " 0000 fc3d 0d0a") 109 | packetFactory = PacketFactory() 110 | packetFactory.decode(msg) 111 | 112 | def test_premade_resp_reset(self): 113 | """Test Decoding a Reset Response.""" 114 | msg = bytearray.fromhex("2a50 0014 75b8 bd8e 0000" " 0000 0000 003a fcbf 0d0a") 115 | packetFactory = PacketFactory() 116 | packetFactory.decode(msg) 117 | 118 | def test_premade_resp_prep(self): 119 | """Test Decoding a Prep Response.""" 120 | msg = bytearray.fromhex("2451 001c 9b60 d511 0457" " 0000 1000 0015 f900 0000" " 0000 0000 fc14 0d0a") 121 | packetFactory = PacketFactory() 122 | packetFactory.decode(msg) 123 | 124 | def test_premade_resp_send(self): 125 | """Test decoding a send response.""" 126 | msg = bytearray.fromhex("2a52 0018 75b8 bd8e 0000" " 0000 0000 003a 0000 0014 fca5 0d0a") 127 | packetFactory = PacketFactory() 128 | packetFactory.decode(msg) 129 | 130 | def test_premade_cmd_83(self): 131 | """Test decoding a type 83 command.""" 132 | msg = bytearray.fromhex("2453 0010 c9a9 b71e 0457 0000 fcd6 0d0a") 133 | packetFactory = PacketFactory() 134 | packetFactory.decode(msg) 135 | 136 | def test_premade_resp_83(self): 137 | """Test decoding a type 83 response.""" 138 | msg = bytearray.fromhex("2a53 0014 75b8 bd8e 0000" " 0000 0000 003a fcbc 0d0a") 139 | packetFactory = PacketFactory() 140 | packetFactory.decode(msg) 141 | 142 | def test_premade_cmd_lock_state(self): 143 | """Test decoding a Lock state command.""" 144 | msg = bytearray.fromhex("24b0 0010 f776 ecbe 0457 0000 fba9 0d0a") 145 | packetFactory = PacketFactory() 146 | packetFactory.decode(msg) 147 | 148 | def test_premade_resp_lock_state(self): 149 | """Test decoding a Lock state response.""" 150 | msg = bytearray.fromhex("2ab0 0018 f130 d7cc 0000 0000" " 0000 0036 0000 0064 fbaf 0d0a") 151 | packetFactory = PacketFactory() 152 | packetFactory.decode(msg) 153 | 154 | def test_premade_cmd_195(self): 155 | """Test decoding a 195 command.""" 156 | msg = bytearray.fromhex("24c3 0010 f130 d7cc 0457 0000 fbe9 0d0a") 157 | packetFactory = PacketFactory() 158 | packetFactory.decode(msg) 159 | 160 | def test_premade_resp_195_first(self): 161 | """Test decoding a 195 first response.""" 162 | msg = bytearray.fromhex("2ac3 0014 f130 d7cc 0000 0000" " 7f00 0436 fb81 0d0a") 163 | packetFactory = PacketFactory() 164 | packetFactory.decode(msg) 165 | 166 | def test_premade_resp_195_last(self): 167 | """Test decoding a 195 last response.""" 168 | msg = bytearray.fromhex("2ac3 0014 f130 d7cc 0000 0000" " 0000 0035 fc05 0d0a") 169 | packetFactory = PacketFactory() 170 | packetFactory.decode(msg) 171 | 172 | 173 | if __name__ == "__main__": 174 | 175 | unittest.main() 176 | -------------------------------------------------------------------------------- /instax/sp2.py: -------------------------------------------------------------------------------- 1 | """Main SP2 Interface Class.""" 2 | 3 | import logging 4 | import queue 5 | import time 6 | 7 | from instax.comms import ClientCommand, ClientReply, SocketClientThread 8 | from instax.exceptions import CommandTimedOutException, ConnectError 9 | from instax.packet import ( 10 | LockStateCommand, 11 | ModelNameCommand, 12 | Packet, 13 | PacketFactory, 14 | PrepImageCommand, 15 | PrePrintCommand, 16 | PrintCountCommand, 17 | PrinterLockCommand, 18 | ResetCommand, 19 | SendImageCommand, 20 | SpecificationsCommand, 21 | Type83Command, 22 | Type195Command, 23 | VersionCommand, 24 | ) 25 | 26 | 27 | class SP2: 28 | """SP2 Client interface.""" 29 | 30 | def __init__(self, ip="192.168.0.251", port=8080, timeout=10, pinCode=1111): 31 | """Initialise the client.""" 32 | logging.debug("Initialising Instax SP-2 Class") 33 | self.currentTimeMillis = int(round(time.time() * 1000)) 34 | self.ip = ip 35 | self.port = port 36 | self.timeout = 10 37 | self.pinCode = pinCode 38 | self.packetFactory = PacketFactory() 39 | 40 | def connect(self): 41 | """Connect to a printer.""" 42 | logging.debug("Connecting to Instax SP-2 with timeout of: %s" % self.timeout) 43 | self.comms = SocketClientThread() 44 | self.comms.start() 45 | self.comms.cmd_q.put(ClientCommand(ClientCommand.CONNECT, [self.ip, self.port])) 46 | # Get current time 47 | start = int(time.time()) 48 | while int(time.time()) < (start + self.timeout): 49 | try: 50 | reply = self.comms.reply_q.get(False) 51 | if reply.type == ClientReply.SUCCESS: 52 | return 53 | else: 54 | raise (ConnectError(reply.data)) 55 | except queue.Empty: 56 | time.sleep(0.1) 57 | pass 58 | raise (CommandTimedOutException()) 59 | 60 | def send_and_recieve(self, cmdBytes, timeout): 61 | """Send a command and waits for a response. 62 | 63 | This is a blocking call and will not check that the response is 64 | the correct command type for the command. 65 | """ 66 | self.comms.cmd_q.put(ClientCommand(ClientCommand.SEND, cmdBytes)) 67 | self.comms.cmd_q.put(ClientCommand(ClientCommand.RECEIVE)) 68 | 69 | # Get current time 70 | start = int(time.time()) 71 | while int(time.time()) < (start + timeout): 72 | try: 73 | reply = self.comms.reply_q.get(False) 74 | if reply.data is not None: 75 | if reply.type == ClientReply.SUCCESS: 76 | return reply 77 | else: 78 | raise (ConnectError(reply.data)) 79 | except queue.Empty: 80 | time.sleep(0.1) 81 | pass 82 | raise (CommandTimedOutException()) 83 | 84 | def sendCommand(self, commandPacket): 85 | """Send a command packet and returns the response.""" 86 | encodedPacket = commandPacket.encodeCommand(self.currentTimeMillis, self.pinCode) 87 | decodedCommand = self.packetFactory.decode(encodedPacket) 88 | decodedCommand.printDebug() 89 | reply = self.send_and_recieve(encodedPacket, 5) 90 | decodedResponse = self.packetFactory.decode(reply.data) 91 | decodedResponse.printDebug() 92 | return decodedResponse 93 | 94 | def getPrinterVersion(self): 95 | """Get the version of the Printer hardware.""" 96 | cmdPacket = VersionCommand(Packet.MESSAGE_MODE_COMMAND) 97 | response = self.sendCommand(cmdPacket) 98 | return response 99 | 100 | def getPrinterModelName(self): 101 | """Get the Model Name of the Printer.""" 102 | cmdPacket = ModelNameCommand(Packet.MESSAGE_MODE_COMMAND) 103 | response = self.sendCommand(cmdPacket) 104 | return response 105 | 106 | def getPrintCount(self): 107 | """Get the historical number of prints.""" 108 | cmdPacket = PrintCountCommand(Packet.MESSAGE_MODE_COMMAND) 109 | response = self.sendCommand(cmdPacket) 110 | return response 111 | 112 | def getPrinterSpecifications(self): 113 | """Get the printer specifications.""" 114 | cmdPacket = SpecificationsCommand(Packet.MESSAGE_MODE_COMMAND) 115 | response = self.sendCommand(cmdPacket) 116 | return response 117 | 118 | def sendPrePrintCommand(self, cmdNumber): 119 | """Send a PrePrint Command.""" 120 | cmdPacket = PrePrintCommand(Packet.MESSAGE_MODE_COMMAND, cmdNumber=cmdNumber) 121 | response = self.sendCommand(cmdPacket) 122 | return response 123 | 124 | def sendLockCommand(self, lockState): 125 | """Send a Lock State Commmand.""" 126 | cmdPacket = PrinterLockCommand(Packet.MESSAGE_MODE_COMMAND, lockState=lockState) 127 | response = self.sendCommand(cmdPacket) 128 | return response 129 | 130 | def sendResetCommand(self): 131 | """Send a Reset Command.""" 132 | cmdPacket = ResetCommand(Packet.MESSAGE_MODE_COMMAND) 133 | response = self.sendCommand(cmdPacket) 134 | return response 135 | 136 | def sendPrepImageCommand(self, format, options, imgLength): 137 | """Send a Prep for Image Command.""" 138 | cmdPacket = PrepImageCommand(Packet.MESSAGE_MODE_COMMAND, format=format, options=options, imgLength=imgLength) 139 | response = self.sendCommand(cmdPacket) 140 | return response 141 | 142 | def sendSendImageCommand(self, sequenceNumber, payloadBytes): 143 | """Send an Image Segment Command.""" 144 | cmdPacket = SendImageCommand( 145 | Packet.MESSAGE_MODE_COMMAND, sequenceNumber=sequenceNumber, payloadBytes=payloadBytes 146 | ) 147 | response = self.sendCommand(cmdPacket) 148 | return response 149 | 150 | def sendT83Command(self): 151 | """Send a Type 83 Command.""" 152 | cmdPacket = Type83Command(Packet.MESSAGE_MODE_COMMAND) 153 | response = self.sendCommand(cmdPacket) 154 | return response 155 | 156 | def sendT195Command(self): 157 | """Send a Type 195 Command.""" 158 | cmdPacket = Type195Command(Packet.MESSAGE_MODE_COMMAND) 159 | response = self.sendCommand(cmdPacket) 160 | return response 161 | 162 | def sendLockStateCommand(self): 163 | """Send a LockState Command.""" 164 | cmdPacket = LockStateCommand(Packet.MESSAGE_MODE_COMMAND) 165 | response = self.sendCommand(cmdPacket) 166 | return response 167 | 168 | def close(self, timeout=10): 169 | """Close the connection to the Printer.""" 170 | logging.debug("Closing connection to Instax SP2") 171 | self.comms.cmd_q.put(ClientCommand(ClientCommand.CLOSE)) 172 | # Get current time 173 | start = int(time.time()) 174 | while int(time.time()) < (start + timeout): 175 | try: 176 | reply = self.comms.reply_q.get(False) 177 | if reply.type == ClientReply.SUCCESS: 178 | self.comms.join() 179 | self.comms = None 180 | return 181 | else: 182 | raise (ConnectError(reply.data)) 183 | except queue.Empty: 184 | time.sleep(0.1) 185 | pass 186 | self.comms.join() 187 | self.comms = None 188 | raise (CommandTimedOutException()) 189 | 190 | def getPrinterInformation(self): 191 | """Primary function to get SP-2 information.""" 192 | self.connect() 193 | printerVersion = self.getPrinterVersion() 194 | printerModel = self.getPrinterModelName() 195 | printerSpecifications = self.getPrinterSpecifications() 196 | printCount = self.getPrintCount() 197 | printerInformation = { 198 | "version": printerVersion.payload, 199 | "model": printerModel.payload["modelName"], 200 | "battery": printerVersion.header["battery"], 201 | "printCount": printerVersion.header["printCount"], 202 | "specs": printerSpecifications.payload, 203 | "count": printCount.payload["printHistory"], 204 | } 205 | self.close() 206 | return printerInformation 207 | 208 | def printPhoto(self, imageBytes, progress): 209 | """Print a Photo to the Printer.""" 210 | progressTotal = 100 211 | progress(0, progressTotal, status="Connecting to instax Printer. ") 212 | # Send Pre Print Commands 213 | self.connect() 214 | progress(10, progressTotal, status="Connected! - Sending Pre Print Commands.") 215 | for x in range(1, 9): 216 | self.sendPrePrintCommand(x) 217 | self.close() 218 | 219 | # Lock The Printer 220 | time.sleep(1) 221 | self.connect() 222 | progress(20, progressTotal, status="Locking Printer for Print. ") 223 | self.sendLockCommand(1) 224 | self.close() 225 | 226 | # Reset the Printer 227 | time.sleep(1) 228 | self.connect() 229 | progress(30, progressTotal, status="Resetting Printer. ") 230 | self.sendResetCommand() 231 | self.close() 232 | 233 | # Send the Image 234 | time.sleep(1) 235 | self.connect() 236 | progress(40, progressTotal, status="About to send Image. ") 237 | self.sendPrepImageCommand(16, 0, 1440000) 238 | for segment in range(24): 239 | start = segment * 60000 240 | end = start + 60000 241 | segmentBytes = imageBytes[start:end] 242 | self.sendSendImageCommand(segment, bytes(segmentBytes)) 243 | progress(40 + segment, progressTotal, status=("Sent image segment %s. " % segment)) 244 | self.sendT83Command() 245 | self.close() 246 | progress(70, progressTotal, status="Image Print Started. ") 247 | # Send Print State Req 248 | time.sleep(1) 249 | self.connect() 250 | self.sendLockStateCommand() 251 | self.getPrinterVersion() 252 | self.getPrinterModelName() 253 | progress(90, progressTotal, status="Checking status of print. ") 254 | printStatus = self.checkPrintStatus(30) 255 | if printStatus is True: 256 | progress(100, progressTotal, status="Print is complete! \n") 257 | else: 258 | progress(100, progressTotal, status="Timed out waiting for print.. \n") 259 | self.close() 260 | 261 | def checkPrintStatus(self, timeout=30): 262 | """Check the status of a print.""" 263 | for _ in range(timeout): 264 | printStateCmd = self.sendT195Command() 265 | if printStateCmd.header["returnCode"] is Packet.RTN_E_RCV_FRAME: 266 | return True 267 | else: 268 | time.sleep(1) 269 | return False 270 | -------------------------------------------------------------------------------- /instax/sp3.py: -------------------------------------------------------------------------------- 1 | """Main SP3 Interface Class.""" 2 | 3 | import logging 4 | import queue 5 | import time 6 | 7 | from instax.comms import ClientCommand, ClientReply, SocketClientThread 8 | from instax.exceptions import CommandTimedOutException, ConnectError 9 | from instax.packet import ( 10 | LockStateCommand, 11 | ModelNameCommand, 12 | Packet, 13 | PacketFactory, 14 | PrepImageCommand, 15 | PrePrintCommand, 16 | PrintCountCommand, 17 | PrinterLockCommand, 18 | ResetCommand, 19 | SendImageCommand, 20 | SpecificationsCommand, 21 | Type83Command, 22 | Type195Command, 23 | VersionCommand, 24 | ) 25 | 26 | 27 | class SP3: 28 | """SP3 Client interface.""" 29 | 30 | def __init__(self, ip="192.168.0.251", port=8080, timeout=10, pinCode=1111): 31 | """Initialise the client.""" 32 | self.currentTimeMillis = int(round(time.time() * 1000)) 33 | self.ip = ip 34 | self.port = port 35 | self.timeout = 10 36 | self.pinCode = pinCode 37 | self.packetFactory = PacketFactory() 38 | 39 | def connect(self): 40 | """Connect to a printer.""" 41 | logging.info( 42 | "Connecting to Instax SP-3 with timeout of: %s on: tcp://%s:%d" % (self.timeout, self.ip, self.port) 43 | ) 44 | self.comms = SocketClientThread() 45 | self.comms.start() 46 | self.comms.cmd_q.put(ClientCommand(ClientCommand.CONNECT, [self.ip, self.port])) 47 | # Get current time 48 | start = int(time.time()) 49 | while int(time.time()) < (start + self.timeout): 50 | try: 51 | reply = self.comms.reply_q.get(False) 52 | if reply.type == ClientReply.SUCCESS: 53 | return 54 | else: 55 | raise (ConnectError(reply.data)) 56 | except queue.Empty: 57 | time.sleep(0.1) 58 | pass 59 | raise (CommandTimedOutException()) 60 | 61 | def send_and_recieve(self, cmdBytes, timeout): 62 | """Send a command and waits for a response. 63 | 64 | This is a blocking call and will not check that the response is 65 | the correct command type for the command. 66 | """ 67 | self.comms.cmd_q.put(ClientCommand(ClientCommand.SEND, cmdBytes)) 68 | self.comms.cmd_q.put(ClientCommand(ClientCommand.RECEIVE)) 69 | 70 | # Get current time 71 | start = int(time.time()) 72 | while int(time.time()) < (start + timeout): 73 | try: 74 | reply = self.comms.reply_q.get(False) 75 | if reply.data is not None: 76 | if reply.type == ClientReply.SUCCESS: 77 | return reply 78 | else: 79 | raise (ConnectError(reply.data)) 80 | except queue.Empty: 81 | time.sleep(0.1) 82 | pass 83 | raise (CommandTimedOutException()) 84 | 85 | def sendCommand(self, commandPacket): 86 | """Send a command packet and returns the response.""" 87 | encodedPacket = commandPacket.encodeCommand(self.currentTimeMillis, self.pinCode) 88 | decodedCommand = self.packetFactory.decode(encodedPacket) 89 | decodedCommand.printDebug() 90 | reply = self.send_and_recieve(encodedPacket, 5) 91 | decodedResponse = self.packetFactory.decode(reply.data) 92 | decodedResponse.printDebug() 93 | return decodedResponse 94 | 95 | def getPrinterVersion(self): 96 | """Get the version of the Printer hardware.""" 97 | cmdPacket = VersionCommand(Packet.MESSAGE_MODE_COMMAND) 98 | response = self.sendCommand(cmdPacket) 99 | return response 100 | 101 | def getPrinterModelName(self): 102 | """Get the Model Name of the Printer.""" 103 | cmdPacket = ModelNameCommand(Packet.MESSAGE_MODE_COMMAND) 104 | response = self.sendCommand(cmdPacket) 105 | return response 106 | 107 | def getPrintCount(self): 108 | """Get the historical number of prints.""" 109 | cmdPacket = PrintCountCommand(Packet.MESSAGE_MODE_COMMAND) 110 | response = self.sendCommand(cmdPacket) 111 | return response 112 | 113 | def getPrinterSpecifications(self): 114 | """Get the printer specifications.""" 115 | cmdPacket = SpecificationsCommand(Packet.MESSAGE_MODE_COMMAND) 116 | response = self.sendCommand(cmdPacket) 117 | return response 118 | 119 | def sendPrePrintCommand(self, cmdNumber): 120 | """Send a PrePrint Command.""" 121 | cmdPacket = PrePrintCommand(Packet.MESSAGE_MODE_COMMAND, cmdNumber=cmdNumber) 122 | response = self.sendCommand(cmdPacket) 123 | return response 124 | 125 | def sendLockCommand(self, lockState): 126 | """Send a Lock State Commmand.""" 127 | cmdPacket = PrinterLockCommand(Packet.MESSAGE_MODE_COMMAND, lockState=lockState) 128 | response = self.sendCommand(cmdPacket) 129 | return response 130 | 131 | def sendResetCommand(self): 132 | """Send a Reset Command.""" 133 | cmdPacket = ResetCommand(Packet.MESSAGE_MODE_COMMAND) 134 | response = self.sendCommand(cmdPacket) 135 | return response 136 | 137 | def sendPrepImageCommand(self, format, options, imgLength): 138 | """Send a Prep for Image Command.""" 139 | cmdPacket = PrepImageCommand(Packet.MESSAGE_MODE_COMMAND, format=format, options=options, imgLength=imgLength) 140 | response = self.sendCommand(cmdPacket) 141 | return response 142 | 143 | def sendSendImageCommand(self, sequenceNumber, payloadBytes): 144 | """Send an Image Segment Command.""" 145 | cmdPacket = SendImageCommand( 146 | Packet.MESSAGE_MODE_COMMAND, sequenceNumber=sequenceNumber, payloadBytes=payloadBytes 147 | ) 148 | response = self.sendCommand(cmdPacket) 149 | return response 150 | 151 | def sendT83Command(self): 152 | """Send a Type 83 Command.""" 153 | cmdPacket = Type83Command(Packet.MESSAGE_MODE_COMMAND) 154 | response = self.sendCommand(cmdPacket) 155 | return response 156 | 157 | def sendT195Command(self): 158 | """Send a Type 195 Command.""" 159 | cmdPacket = Type195Command(Packet.MESSAGE_MODE_COMMAND) 160 | response = self.sendCommand(cmdPacket) 161 | return response 162 | 163 | def sendLockStateCommand(self): 164 | """Send a LockState Command.""" 165 | cmdPacket = LockStateCommand(Packet.MESSAGE_MODE_COMMAND) 166 | response = self.sendCommand(cmdPacket) 167 | return response 168 | 169 | def close(self, timeout=10): 170 | """Close the connection to the Printer.""" 171 | logging.info("Closing connection to Instax SP3") 172 | self.comms.cmd_q.put(ClientCommand(ClientCommand.CLOSE)) 173 | # Get current time 174 | start = int(time.time()) 175 | while int(time.time()) < (start + timeout): 176 | try: 177 | reply = self.comms.reply_q.get(False) 178 | if reply.type == ClientReply.SUCCESS: 179 | self.comms.join() 180 | self.comms = None 181 | return 182 | else: 183 | raise (ConnectError(reply.data)) 184 | except queue.Empty: 185 | time.sleep(0.1) 186 | pass 187 | self.comms.join() 188 | self.comms = None 189 | raise (CommandTimedOutException()) 190 | 191 | def getPrinterInformation(self): 192 | """Primary function to get SP-2 information.""" 193 | self.connect() 194 | printerVersion = self.getPrinterVersion() 195 | printerModel = self.getPrinterModelName() 196 | printerSpecifications = self.getPrinterSpecifications() 197 | printCount = self.getPrintCount() 198 | printerInformation = { 199 | "version": printerVersion.payload, 200 | "model": printerModel.payload["modelName"], 201 | "battery": printerVersion.header["battery"], 202 | "printCount": printerVersion.header["printCount"], 203 | "specs": printerSpecifications.payload, 204 | "count": printCount.payload["printHistory"], 205 | } 206 | self.close() 207 | return printerInformation 208 | 209 | def printPhoto(self, imageBytes, progress): 210 | """Print a Photo to the Printer.""" 211 | progressTotal = 100 212 | progress(0, progressTotal, status="Connecting to instax Printer. ") 213 | # Send Pre Print Commands 214 | self.connect() 215 | progress(10, progressTotal, status="Connected! - Sending Pre Print Commands.") 216 | for x in range(1, 9): 217 | resp = self.sendPrePrintCommand(x) 218 | self.close() 219 | # Lock The Printer 220 | time.sleep(1) 221 | self.connect() 222 | progress(20, progressTotal, status="Locking Printer for Print. ") 223 | resp = self.sendLockCommand(1) 224 | self.close() 225 | 226 | # Reset the Printer 227 | time.sleep(1) 228 | self.connect() 229 | progress(30, progressTotal, status="Resetting Printer. ") 230 | resp = self.sendResetCommand() 231 | self.close() 232 | 233 | # Send the Image 234 | time.sleep(1) 235 | self.connect() 236 | progress(40, progressTotal, status="About to send Image. ") 237 | resp = self.sendPrepImageCommand(16, 0, 1920000) 238 | for segment in range(32): 239 | start = segment * 60000 240 | end = start + 60000 241 | segmentBytes = imageBytes[start:end] 242 | resp = self.sendSendImageCommand(segment, bytes(segmentBytes)) 243 | progress(40 + segment, progressTotal, status=("Sent image segment %s. " % segment)) 244 | resp = self.sendT83Command() 245 | resp.printDebug() 246 | self.close() 247 | progress(70, progressTotal, status="Image Print Started. ") 248 | # Send Print State Req 249 | time.sleep(1) 250 | self.connect() 251 | self.sendLockStateCommand() 252 | self.getPrinterVersion() 253 | self.getPrinterModelName() 254 | progress(90, progressTotal, status="Checking status of print. ") 255 | printStatus = self.checkPrintStatus(30) 256 | if printStatus is True: 257 | progress(100, progressTotal, status="Print is complete! \n") 258 | else: 259 | progress(100, progressTotal, status="Timed out waiting for print.. \n") 260 | self.close() 261 | 262 | def checkPrintStatus(self, timeout=30): 263 | """Check the status of a print.""" 264 | for _ in range(timeout): 265 | printStateCmd = self.sendT195Command() 266 | if printStateCmd.header["returnCode"] is Packet.RTN_E_RCV_FRAME: 267 | return True 268 | else: 269 | time.sleep(1) 270 | return False 271 | -------------------------------------------------------------------------------- /instax/debugServer.py: -------------------------------------------------------------------------------- 1 | """ 2 | Fujifilm Instax-SP2 Server for testing. 3 | 4 | Author: James Sutton 2017 - jsutton.co.uk 5 | 6 | This wrapper can be used to start a test server implementation. 7 | You can configure a number of useful parameters to use whist the server is 8 | running. 9 | Parameters: 10 | - Verbose (Default False) 11 | - JSON Log File (Default ddmmyy-hhmmss.json) 12 | - Port (Default 8080) 13 | - Photo Destination Directory: (Default: images) 14 | - Battery Level: (Default 100%) 15 | - Prints Remaining: (Default 10) 16 | - Total Prints in History: (Default 20) 17 | 18 | """ 19 | import argparse 20 | import datetime 21 | import json 22 | import logging 23 | import signal 24 | import socket 25 | import sys 26 | import threading 27 | import time 28 | 29 | from loguru import logger 30 | 31 | from instax.instaxImage import InstaxImage 32 | from instax.packet import ( 33 | LockStateCommand, 34 | ModelNameCommand, 35 | Packet, 36 | PacketFactory, 37 | PrepImageCommand, 38 | PrePrintCommand, 39 | PrintCountCommand, 40 | PrinterLockCommand, 41 | ResetCommand, 42 | SendImageCommand, 43 | SpecificationsCommand, 44 | Type83Command, 45 | Type195Command, 46 | VersionCommand, 47 | ) 48 | 49 | 50 | class DebugServer: 51 | """A Test Server for the Instax Library.""" 52 | 53 | def __init__(self, host="0.0.0.0", port=8080, dest="images", battery=2, remaining=10, total=20, version=2): 54 | """Initialise Server.""" 55 | self.logger = logging.getLogger("instax_server") 56 | self.packetFactory = PacketFactory() 57 | self.host = host 58 | self.dest = dest 59 | self.port = port 60 | self.backlog = 5 61 | self.returnCode = Packet.RTN_E_RCV_FRAME 62 | self.ejecting = 0 63 | if version in [2, 3]: 64 | self.version = version 65 | else: 66 | self.logger.warning("Invalid Instax SP version, defaulting to SP-2") 67 | self.version = 2 68 | self.printingState = 0 69 | self.battery = battery 70 | self.printCount = total 71 | self.remaining = remaining 72 | self.running = True 73 | self.finished = False 74 | self.messageLog = [] 75 | self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 76 | self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 77 | self.socket.bind((self.host, self.port)) 78 | signal.signal(signal.SIGINT, self.signal_handler) 79 | self.imageMap = {} 80 | 81 | def start(self): 82 | """Start the Server.""" 83 | self.socket.listen(self.backlog) 84 | self.logger.info("Instax SP-%d Server Listening on %s port %s" % (self.version, self.host, self.port)) 85 | while True: 86 | client, address = self.socket.accept() 87 | client.settimeout(60) 88 | threading.Thread(target=self.listenToClient, args=(client, address)).start() 89 | 90 | def getPort(self): 91 | self.logger.info(self.socket.getsockname()) 92 | return self.socket.getsockname()[1] 93 | 94 | def listenToClient(self, client, address): 95 | """Interact with client.""" 96 | self.logger.info("New Client Connected") 97 | length = None 98 | buffer = bytearray() 99 | while True: 100 | data = client.recv(70000) 101 | if not data: 102 | break 103 | buffer += data 104 | while True: 105 | if length is None: 106 | length = (buffer[2] & 0xFF) << 8 | (buffer[3] & 0xFF) << 0 107 | if len(buffer) < length: 108 | break 109 | 110 | response = self.processIncomingMessage(buffer) 111 | client.send(response) 112 | buffer = bytearray() 113 | length = None 114 | break 115 | self.logger.info("Client Disconnected") 116 | 117 | def signal_handler(self, signal, frame): 118 | """Handle Ctrl+C events.""" 119 | print("You pressed Ctrl+C! Saving Log and shutting down.") 120 | self.logger.info("Shutting down Server") 121 | timestr = time.strftime("%Y%m%d-%H%M%S") 122 | filename = "instaxServer-" + timestr + ".json" 123 | self.logger.info("Saving Log to: %s" % filename) 124 | with open(filename, "w") as outfile: 125 | json.dump(self.messageLog, outfile, indent=4) 126 | self.logger.info("Log file written, have a nice day!") 127 | sys.exit(0) 128 | 129 | def decodeImage(self, segments): 130 | """Decode an encoded image.""" 131 | self.logger.info("Decoding Image of %s segments." % len(segments)) 132 | combined = bytearray() 133 | for seg_key in range(len(segments)): 134 | combined += segments[seg_key] 135 | self.logger.info("Combined image is %s bytes long" % len(combined)) 136 | instaxImage = InstaxImage(type=self.version) 137 | instaxImage.decodeImage(combined) 138 | timestr = time.strftime("%Y%m%d-%H%M%S") 139 | filename = timestr + ".bmp" 140 | instaxImage.saveImage(filename) 141 | self.logger.info("Saved image to: %s" % filename) 142 | 143 | def printByteArray(self, byteArray): 144 | """Print a Byte Array. 145 | 146 | Prints a Byte array in the following format: b1b2 b3b4... 147 | """ 148 | hexString = "".join("%02x" % i for i in byteArray) 149 | data = " ".join(hexString[i : i + 4] for i in range(0, len(hexString), 4)) 150 | info = (data[:80] + "..") if len(data) > 80 else data 151 | return info 152 | 153 | def processIncomingMessage(self, payload): 154 | """Take an incoming message and return the response.""" 155 | packetFactory = PacketFactory() 156 | decodedPacket = packetFactory.decode(payload) 157 | decodedPacketObj = decodedPacket.getPacketObject() 158 | self.messageLog.append(decodedPacketObj) 159 | self.logger.info("Processing message type: %s" % decodedPacket.NAME) 160 | response = None 161 | 162 | if decodedPacket.TYPE == Packet.MESSAGE_TYPE_PRINTER_VERSION: 163 | response = self.processVersionCommand(decodedPacket) 164 | elif decodedPacket.TYPE == Packet.MESSAGE_TYPE_SPECIFICATIONS: 165 | response = self.processSpecificationsCommand(decodedPacket) 166 | elif decodedPacket.TYPE == Packet.MESSAGE_TYPE_MODEL_NAME: 167 | response = self.processModelNameCommand(decodedPacket) 168 | elif decodedPacket.TYPE == Packet.MESSAGE_TYPE_PRINT_COUNT: 169 | response = self.processPrintCountCommand(decodedPacket) 170 | elif decodedPacket.TYPE == Packet.MESSAGE_TYPE_PRE_PRINT: 171 | response = self.processPrePrintCommand(decodedPacket) 172 | elif decodedPacket.TYPE == Packet.MESSAGE_TYPE_LOCK_DEVICE: 173 | response = self.processLockPrinterCommand(decodedPacket) 174 | elif decodedPacket.TYPE == Packet.MESSAGE_TYPE_RESET: 175 | response = self.processResetCommand(decodedPacket) 176 | elif decodedPacket.TYPE == Packet.MESSAGE_TYPE_PREP_IMAGE: 177 | response = self.processPrepImageCommand(decodedPacket) 178 | elif decodedPacket.TYPE == Packet.MESSAGE_TYPE_SEND_IMAGE: 179 | response = self.processSendImageCommand(decodedPacket) 180 | elif decodedPacket.TYPE == Packet.MESSAGE_TYPE_83: 181 | response = self.processType83Command(decodedPacket) 182 | elif decodedPacket.TYPE == Packet.MESSAGE_TYPE_195: 183 | response = self.processType195Command(decodedPacket) 184 | elif decodedPacket.TYPE == Packet.MESSAGE_TYPE_SET_LOCK_STATE: 185 | response = self.processSetLockStateCommand(decodedPacket) 186 | else: 187 | self.logger.info("Unknown Command. Failing!: " + str(decodedPacket.TYPE)) 188 | 189 | decodedResponsePacket = packetFactory.decode(response) 190 | self.messageLog.append(decodedResponsePacket.getPacketObject()) 191 | return response 192 | 193 | def processVersionCommand(self, decodedPacket): 194 | """Process a version command.""" 195 | sessionTime = decodedPacket.header["sessionTime"] 196 | resPacket = VersionCommand(Packet.MESSAGE_MODE_RESPONSE, unknown1=254, firmware=275, hardware=0) 197 | encodedResponse = resPacket.encodeResponse( 198 | sessionTime, self.returnCode, self.ejecting, self.battery, self.printCount 199 | ) 200 | return encodedResponse 201 | 202 | def processSpecificationsCommand(self, decodedPacket): 203 | """Process a specifications command.""" 204 | sessionTime = decodedPacket.header["sessionTime"] 205 | resPacket = SpecificationsCommand( 206 | Packet.MESSAGE_MODE_RESPONSE, 207 | maxHeight=800, 208 | maxWidth=600, 209 | maxColours=256, 210 | unknown1=10, 211 | maxMsgSize=60000, 212 | unknown2=16, 213 | unknown3=0, 214 | ) 215 | encodedResponse = resPacket.encodeResponse( 216 | sessionTime, self.returnCode, self.ejecting, self.battery, self.printCount 217 | ) 218 | return encodedResponse 219 | 220 | def processModelNameCommand(self, decodedPacket): 221 | """Process a model name command.""" 222 | sessionTime = decodedPacket.header["sessionTime"] 223 | resPacket = ModelNameCommand(Packet.MESSAGE_MODE_RESPONSE, modelName=("SP-%d" % self.version)) 224 | encodedResponse = resPacket.encodeResponse( 225 | sessionTime, self.returnCode, self.ejecting, self.battery, self.printCount 226 | ) 227 | return encodedResponse 228 | 229 | def processPrintCountCommand(self, decodedPacket): 230 | """Process a Print Count command.""" 231 | sessionTime = decodedPacket.header["sessionTime"] 232 | resPacket = PrintCountCommand(Packet.MESSAGE_MODE_RESPONSE, printHistory=20) 233 | encodedResponse = resPacket.encodeResponse( 234 | sessionTime, self.returnCode, self.ejecting, self.battery, self.printCount 235 | ) 236 | return encodedResponse 237 | 238 | def processPrePrintCommand(self, decodedPacket): 239 | """Process a Pre Print command.""" 240 | cmdNumber = decodedPacket.payload["cmdNumber"] 241 | if cmdNumber in [6, 7, 8]: 242 | respNumber = 0 243 | elif cmdNumber in [4, 5]: 244 | respNumber = 1 245 | elif cmdNumber in [1, 2, 3]: 246 | respNumber = 2 247 | else: 248 | self.logger.warning("Unknown cmdNumber") 249 | respNumber = 0 250 | sessionTime = decodedPacket.header["sessionTime"] 251 | resPacket = PrePrintCommand(Packet.MESSAGE_MODE_RESPONSE, cmdNumber=cmdNumber, respNumber=respNumber) 252 | encodedResponse = resPacket.encodeResponse( 253 | sessionTime, self.returnCode, self.ejecting, self.battery, self.printCount 254 | ) 255 | return encodedResponse 256 | 257 | def processLockPrinterCommand(self, decodedPacket): 258 | """Process a Lock Printer Command.""" 259 | sessionTime = decodedPacket.header["sessionTime"] 260 | resPacket = PrinterLockCommand(Packet.MESSAGE_MODE_RESPONSE) 261 | encodedResponse = resPacket.encodeResponse( 262 | sessionTime, self.returnCode, self.ejecting, self.battery, self.printCount 263 | ) 264 | return encodedResponse 265 | 266 | def processResetCommand(self, decodedPacket): 267 | """Process a Rest command.""" 268 | sessionTime = decodedPacket.header["sessionTime"] 269 | resPacket = ResetCommand(Packet.MESSAGE_MODE_RESPONSE) 270 | encodedResponse = resPacket.encodeResponse( 271 | sessionTime, self.returnCode, self.ejecting, self.battery, self.printCount 272 | ) 273 | return encodedResponse 274 | 275 | def processPrepImageCommand(self, decodedPacket): 276 | """Process a Prep Image Commnand.""" 277 | sessionTime = decodedPacket.header["sessionTime"] 278 | resPacket = PrepImageCommand(Packet.MESSAGE_MODE_RESPONSE, maxLen=60000) 279 | encodedResponse = resPacket.encodeResponse( 280 | sessionTime, self.returnCode, self.ejecting, self.battery, self.printCount 281 | ) 282 | return encodedResponse 283 | 284 | def processSendImageCommand(self, decodedPacket): 285 | """Process a Send Image Command.""" 286 | sessionTime = decodedPacket.header["sessionTime"] 287 | sequenceNumber = decodedPacket.payload["sequenceNumber"] 288 | payloadBytes = decodedPacket.payload["payloadBytes"] 289 | resPacket = SendImageCommand(Packet.MESSAGE_MODE_RESPONSE, sequenceNumber=sequenceNumber) 290 | if sessionTime not in self.imageMap: 291 | self.imageMap[sessionTime] = {} 292 | self.imageMap[sessionTime][sequenceNumber] = payloadBytes 293 | encodedResponse = resPacket.encodeResponse( 294 | sessionTime, self.returnCode, self.ejecting, self.battery, self.printCount 295 | ) 296 | return encodedResponse 297 | 298 | def processType83Command(self, decodedPacket): 299 | """Process a Type 83 command.""" 300 | sessionTime = decodedPacket.header["sessionTime"] 301 | resPacket = Type83Command(Packet.MESSAGE_MODE_RESPONSE) 302 | encodedResponse = resPacket.encodeResponse( 303 | sessionTime, self.returnCode, self.ejecting, self.battery, self.printCount 304 | ) 305 | # Start a thread to decode the image 306 | imageSegments = self.imageMap[sessionTime] 307 | threading.Thread(target=self.decodeImage, args=(imageSegments,)).start() 308 | return encodedResponse 309 | 310 | def processType195Command(self, decodedPacket): 311 | sessionTime = decodedPacket.header["sessionTime"] 312 | returnCode = Packet.RTN_E_PRINTING 313 | if self.printingState == 100: 314 | returnCode = Packet.RTN_E_RCV_FRAME 315 | self.printingState = 0 316 | else: 317 | self.printingState += 25 318 | resPacket = Type195Command(Packet.MESSAGE_MODE_RESPONSE) 319 | encodedResponse = resPacket.encodeResponse( 320 | sessionTime, returnCode, self.ejecting, self.battery, self.printCount 321 | ) 322 | return encodedResponse 323 | 324 | def processSetLockStateCommand(self, decodedPacket): 325 | """Process a Lock State Command.""" 326 | unknownFourByteInt = 100 327 | sessionTime = decodedPacket.header["sessionTime"] 328 | resPacket = LockStateCommand(Packet.MESSAGE_MODE_RESPONSE, unknownFourByteInt=unknownFourByteInt) 329 | encodedResponse = resPacket.encodeResponse( 330 | sessionTime, self.returnCode, self.ejecting, self.battery, self.printCount 331 | ) 332 | return encodedResponse 333 | 334 | 335 | if __name__ == "__main__": 336 | logger.info("---------- Instax SP-2 Test Server ---------- ") 337 | 338 | def remaining_type(x): 339 | """Validate Remaining count is between 0 and 10.""" 340 | x = int(x) 341 | if x < 10 and x >= 0: 342 | raise argparse.ArgumentTypeError("Remaining must be between 0 and 10.") 343 | return x 344 | 345 | parser = argparse.ArgumentParser() 346 | parser.add_argument( 347 | "-v", "--verbose", action="store_true", default=False, help="Print Verbose log messages to console." 348 | ) 349 | parser.add_argument("-D", "--debug", action="store_true", default=False, help="Logs extra debug data to log.") 350 | parser.add_argument( 351 | "-l", "--log", action="store_true", default=False, help="Log information to log file ddmmyy-hhmmss-server.log" 352 | ) 353 | parser.add_argument("-o", "--host", default="0.0.0.0", help="The Host IP to expose the server on.") 354 | parser.add_argument("-p", "--port", type=int, default=8080, help="The port to expose the server on.") 355 | parser.add_argument( 356 | "-d", "--dest", default="images", help="The Directory to save incoming photos," "default: 'images'" 357 | ) 358 | parser.add_argument( 359 | "-b", 360 | "--battery", 361 | type=int, 362 | choices=range(0, 4), 363 | default=2, 364 | help="The Battery level of the printer" " 0-4, default: 2", 365 | ) 366 | parser.add_argument( 367 | "-r", "--remaining", type=remaining_type, default=10, help="The number of remaining prints 0-10, default: 10" 368 | ) 369 | parser.add_argument( 370 | "-t", "--total", type=int, default=20, help="The total number of prints in the printers lifetime" ", default 20" 371 | ) 372 | parser.add_argument("-V", "--version", type=int, default=2, help="The Instax SP-* version, 2 or 3, default is 2") 373 | args = parser.parse_args() 374 | 375 | # Create Log Formatter 376 | formatter = logging.Formatter("%(asctime)s:%(name)s:%(levelname)s:%(message)s") 377 | 378 | # If Not specified, set the log file to a datestamp. 379 | if args.log: 380 | logFilename = f"{datetime.datetime.now():%Y-%m-%d.%H:%M:%S-server.log}" 381 | logger.add(logFilename) 382 | 383 | testServer = DebugServer( 384 | host=args.host, 385 | port=args.port, 386 | dest=args.dest, 387 | battery=args.battery, 388 | remaining=args.remaining, 389 | total=args.total, 390 | version=args.version, 391 | ) 392 | testServer.start() 393 | -------------------------------------------------------------------------------- /instax/tests/test_basic.py: -------------------------------------------------------------------------------- 1 | """ 2 | Instax SP2 Test File. 3 | 4 | @jpwsutton 2016/17 5 | """ 6 | import time 7 | import unittest 8 | 9 | from instax.packet import ( 10 | LockStateCommand, 11 | ModelNameCommand, 12 | Packet, 13 | PacketFactory, 14 | PrepImageCommand, 15 | PrePrintCommand, 16 | PrintCountCommand, 17 | PrinterLockCommand, 18 | ResetCommand, 19 | SendImageCommand, 20 | SpecificationsCommand, 21 | Type83Command, 22 | Type195Command, 23 | VersionCommand, 24 | ) 25 | 26 | 27 | class PacketTests(unittest.TestCase): 28 | """ 29 | Instax-SP2 Packet Test Class. 30 | 31 | A series of tests to verify that all commands and responses can be 32 | correctly encoded and decoded. 33 | """ 34 | 35 | def helper_verify_header( 36 | self, 37 | header, 38 | direction, 39 | type, 40 | length, 41 | time, 42 | pin=None, 43 | returnCode=None, 44 | unknown1=None, 45 | ejecting=None, 46 | battery=None, 47 | printCount=None, 48 | ): 49 | """Verify the Header of a packet.""" 50 | self.assertEqual(header["startByte"], direction) 51 | self.assertEqual(header["cmdByte"], type) 52 | self.assertEqual(header["packetLength"], length) 53 | self.assertEqual(header["sessionTime"], time) 54 | if direction == Packet.MESSAGE_MODE_COMMAND: 55 | self.assertEqual(header["password"], pin) 56 | if direction == Packet.MESSAGE_MODE_RESPONSE: 57 | self.assertEqual(header["returnCode"], returnCode) 58 | # self.assertEqual(header['unknown1'], unknown1) 59 | self.assertEqual(header["ejecting"], ejecting) 60 | self.assertEqual(header["battery"], battery) 61 | self.assertEqual(header["printCount"], printCount) 62 | 63 | def test_encode_cmd_specifications(self): 64 | """Test the process of encoding a spcecifications command.""" 65 | # Create Specifications Command Packet 66 | sessionTime = int(round(time.time() * 1000)) 67 | pinCode = 1111 68 | cmdPacket = SpecificationsCommand(Packet.MESSAGE_MODE_COMMAND) 69 | # Encode the command to raw byte array 70 | encodedCommand = cmdPacket.encodeCommand(sessionTime, pinCode) 71 | # Decode the command back into a packet object 72 | packetFactory = PacketFactory() 73 | decodedPacket = packetFactory.decode(encodedCommand) 74 | # decodedPacket.printDebug() 75 | postHeader = decodedPacket.header 76 | self.helper_verify_header( 77 | postHeader, 78 | Packet.MESSAGE_MODE_COMMAND, 79 | Packet.MESSAGE_TYPE_SPECIFICATIONS, 80 | len(encodedCommand), 81 | cmdPacket.encodedSessionTime, 82 | pinCode, 83 | ) 84 | 85 | def test_encode_resp_specifications(self): 86 | """Test the process of encoding a specifications response.""" 87 | sessionTime = int(round(time.time() * 1000)) 88 | returnCode = Packet.RTN_E_RCV_FRAME 89 | ejecting = 0 90 | battery = 2 91 | printCount = 7 92 | resPacket = SpecificationsCommand( 93 | Packet.MESSAGE_MODE_RESPONSE, 94 | maxHeight=800, 95 | maxWidth=600, 96 | maxColours=256, 97 | unknown1=10, 98 | maxMsgSize=60000, 99 | unknown2=16, 100 | unknown3=0, 101 | ) 102 | encodedResponse = resPacket.encodeResponse(sessionTime, returnCode, ejecting, battery, printCount) 103 | packetFactory = PacketFactory() 104 | decodedPacket = packetFactory.decode(encodedResponse) 105 | # decodedPacket.printDebug() 106 | postHeader = decodedPacket.header 107 | self.helper_verify_header( 108 | postHeader, 109 | Packet.MESSAGE_MODE_RESPONSE, 110 | Packet.MESSAGE_TYPE_SPECIFICATIONS, 111 | len(encodedResponse), 112 | resPacket.encodedSessionTime, 113 | returnCode=returnCode, 114 | ejecting=ejecting, 115 | battery=battery, 116 | printCount=printCount, 117 | ) 118 | 119 | # Verify Payload 120 | # print(decodedPacket.payload) 121 | self.assertEqual(decodedPacket.payload["maxHeight"], 800) 122 | self.assertEqual(decodedPacket.payload["maxWidth"], 600) 123 | self.assertEqual(decodedPacket.payload["maxColours"], 256) 124 | self.assertEqual(decodedPacket.payload["unknown1"], 10) 125 | self.assertEqual(decodedPacket.payload["maxMsgSize"], 60000) 126 | self.assertEqual(decodedPacket.payload["unknown2"], 16) 127 | self.assertEqual(decodedPacket.payload["unknown3"], 0) 128 | 129 | def test_encode_cmd_version(self): 130 | """Test the process of encoding a version command.""" 131 | # Create Specifications Command Packet 132 | sessionTime = int(round(time.time() * 1000)) 133 | pinCode = 1111 134 | cmdPacket = VersionCommand(Packet.MESSAGE_MODE_COMMAND) 135 | # Encode the command to raw byte array 136 | encodedCommand = cmdPacket.encodeCommand(sessionTime, pinCode) 137 | # Decode the command back into a packet object 138 | packetFactory = PacketFactory() 139 | decodedPacket = packetFactory.decode(encodedCommand) 140 | # decodedPacket.printDebug() 141 | postHeader = decodedPacket.header 142 | self.helper_verify_header( 143 | postHeader, 144 | Packet.MESSAGE_MODE_COMMAND, 145 | Packet.MESSAGE_TYPE_PRINTER_VERSION, 146 | len(encodedCommand), 147 | cmdPacket.encodedSessionTime, 148 | pinCode, 149 | ) 150 | 151 | def test_encode_resp_version(self): 152 | """Test the process of encoding a version response.""" 153 | sessionTime = int(round(time.time() * 1000)) 154 | returnCode = Packet.RTN_E_RCV_FRAME 155 | ejecting = 0 156 | battery = 2 157 | printCount = 7 158 | resPacket = VersionCommand(Packet.MESSAGE_MODE_RESPONSE, unknown1=254, firmware=275, hardware=0) 159 | encodedResponse = resPacket.encodeResponse(sessionTime, returnCode, ejecting, battery, printCount) 160 | packetFactory = PacketFactory() 161 | decodedPacket = packetFactory.decode(encodedResponse) 162 | # decodedPacket.printDebug() 163 | postHeader = decodedPacket.header 164 | self.helper_verify_header( 165 | postHeader, 166 | Packet.MESSAGE_MODE_RESPONSE, 167 | Packet.MESSAGE_TYPE_PRINTER_VERSION, 168 | len(encodedResponse), 169 | resPacket.encodedSessionTime, 170 | returnCode=returnCode, 171 | ejecting=ejecting, 172 | battery=battery, 173 | printCount=printCount, 174 | ) 175 | 176 | # Verify Payload 177 | self.assertEqual(decodedPacket.payload["unknown1"], 254) 178 | self.assertEqual(decodedPacket.payload["firmware"], "01.13") 179 | self.assertEqual(decodedPacket.payload["hardware"], "00.00") 180 | 181 | def test_encode_cmd_printCount(self): 182 | """Test the process of encoding a print count command.""" 183 | # Create Print Count Command Packet 184 | sessionTime = int(round(time.time() * 1000)) 185 | pinCode = 1111 186 | cmdPacket = PrintCountCommand(Packet.MESSAGE_MODE_COMMAND) 187 | # Encode the command to raw byte array 188 | encodedCommand = cmdPacket.encodeCommand(sessionTime, pinCode) 189 | # Decode the command back into a packet object 190 | packetFactory = PacketFactory() 191 | decodedPacket = packetFactory.decode(encodedCommand) 192 | # decodedPacket.printDebug() 193 | postHeader = decodedPacket.header 194 | self.helper_verify_header( 195 | postHeader, 196 | Packet.MESSAGE_MODE_COMMAND, 197 | Packet.MESSAGE_TYPE_PRINT_COUNT, 198 | len(encodedCommand), 199 | cmdPacket.encodedSessionTime, 200 | pinCode, 201 | ) 202 | 203 | def test_encode_resp_printCount(self): 204 | """Test the process of encoding a print count response.""" 205 | sessionTime = int(round(time.time() * 1000)) 206 | returnCode = Packet.RTN_E_RCV_FRAME 207 | ejecting = 0 208 | battery = 2 209 | printCount = 7 210 | printHistory = 42 211 | resPacket = PrintCountCommand(Packet.MESSAGE_MODE_RESPONSE, printHistory=printHistory) 212 | encodedResponse = resPacket.encodeResponse(sessionTime, returnCode, ejecting, battery, printCount) 213 | packetFactory = PacketFactory() 214 | decodedPacket = packetFactory.decode(encodedResponse) 215 | # decodedPacket.printDebug() 216 | postHeader = decodedPacket.header 217 | self.helper_verify_header( 218 | postHeader, 219 | Packet.MESSAGE_MODE_RESPONSE, 220 | Packet.MESSAGE_TYPE_PRINT_COUNT, 221 | len(encodedResponse), 222 | resPacket.encodedSessionTime, 223 | returnCode=returnCode, 224 | ejecting=ejecting, 225 | battery=battery, 226 | printCount=printCount, 227 | ) 228 | 229 | # Verify Payload 230 | self.assertEqual(decodedPacket.payload["printHistory"], printHistory) 231 | 232 | def test_encode_cmd_modelName(self): 233 | """Test the process of encoding a model name command.""" 234 | # Create Model Name Command Packet 235 | sessionTime = int(round(time.time() * 1000)) 236 | pinCode = 1111 237 | cmdPacket = ModelNameCommand(Packet.MESSAGE_MODE_COMMAND) 238 | # Encodde the command to raw byte array 239 | encodedCommand = cmdPacket.encodeCommand(sessionTime, pinCode) 240 | # Decode the command back into a packet object 241 | packetFactory = PacketFactory() 242 | decodedPacket = packetFactory.decode(encodedCommand) 243 | postHeader = decodedPacket.header 244 | self.helper_verify_header( 245 | postHeader, 246 | Packet.MESSAGE_MODE_COMMAND, 247 | Packet.MESSAGE_TYPE_MODEL_NAME, 248 | len(encodedCommand), 249 | cmdPacket.encodedSessionTime, 250 | pinCode, 251 | ) 252 | 253 | def test_encode_resp_modelName(self): 254 | """Test the process of encoding a model name response.""" 255 | sessionTime = int(round(time.time() * 1000)) 256 | returnCode = Packet.RTN_E_RCV_FRAME 257 | ejecting = 0 258 | battery = 2 259 | printCount = 7 260 | modelName = "SP-2" 261 | resPacket = ModelNameCommand(Packet.MESSAGE_MODE_RESPONSE, modelName=modelName) 262 | encodedResponse = resPacket.encodeResponse(sessionTime, returnCode, ejecting, battery, printCount) 263 | packetFactory = PacketFactory() 264 | decodedPacket = packetFactory.decode(encodedResponse) 265 | # decodedPacket.printDebug() 266 | postHeader = decodedPacket.header 267 | self.helper_verify_header( 268 | postHeader, 269 | Packet.MESSAGE_MODE_RESPONSE, 270 | Packet.MESSAGE_TYPE_MODEL_NAME, 271 | len(encodedResponse), 272 | resPacket.encodedSessionTime, 273 | returnCode=returnCode, 274 | ejecting=ejecting, 275 | battery=battery, 276 | printCount=printCount, 277 | ) 278 | 279 | # Verify Payload 280 | self.assertEqual(decodedPacket.payload["modelName"], modelName) 281 | 282 | def test_encode_cmd_prePrint(self): 283 | """Test the process of encoding a prePrint command.""" 284 | # Create Model Name Command Packet 285 | sessionTime = int(round(time.time() * 1000)) 286 | pinCode = 1111 287 | cmdNumber = 8 288 | cmdPacket = PrePrintCommand(Packet.MESSAGE_MODE_COMMAND, cmdNumber=cmdNumber) 289 | # Encodde the command to raw byte array 290 | encodedCommand = cmdPacket.encodeCommand(sessionTime, pinCode) 291 | # Decodee the command back into a packet object 292 | packetFactory = PacketFactory() 293 | decodedPacket = packetFactory.decode(encodedCommand) 294 | postHeader = decodedPacket.header 295 | self.helper_verify_header( 296 | postHeader, 297 | Packet.MESSAGE_MODE_COMMAND, 298 | Packet.MESSAGE_TYPE_PRE_PRINT, 299 | len(encodedCommand), 300 | cmdPacket.encodedSessionTime, 301 | pinCode, 302 | ) 303 | # Verify Payload 304 | self.assertEqual(decodedPacket.payload["cmdNumber"], cmdNumber) 305 | 306 | def test_encode_resp_prePrint(self): 307 | """Test the process of encoding a pre print response.""" 308 | sessionTime = int(round(time.time() * 1000)) 309 | returnCode = Packet.RTN_E_RCV_FRAME 310 | ejecting = 0 311 | battery = 2 312 | printCount = 7 313 | cmdNumber = 8 314 | respNumber = 1 315 | resPacket = PrePrintCommand(Packet.MESSAGE_MODE_RESPONSE, cmdNumber=cmdNumber, respNumber=respNumber) 316 | encodedResponse = resPacket.encodeResponse(sessionTime, returnCode, ejecting, battery, printCount) 317 | packetFactory = PacketFactory() 318 | decodedPacket = packetFactory.decode(encodedResponse) 319 | # decodedPacket.printDebug() 320 | postHeader = decodedPacket.header 321 | self.helper_verify_header( 322 | postHeader, 323 | Packet.MESSAGE_MODE_RESPONSE, 324 | Packet.MESSAGE_TYPE_PRE_PRINT, 325 | len(encodedResponse), 326 | resPacket.encodedSessionTime, 327 | returnCode=returnCode, 328 | ejecting=ejecting, 329 | battery=battery, 330 | printCount=printCount, 331 | ) 332 | 333 | # Verify Payload 334 | self.assertEqual(decodedPacket.payload["cmdNumber"], cmdNumber) 335 | self.assertEqual(decodedPacket.payload["respNumber"], respNumber) 336 | 337 | def test_encode_cmd_lock(self): 338 | """Test encoding a Lock Printer Command.""" 339 | sessionTime = int(round(time.time() * 1000)) 340 | pinCode = 1111 341 | lockState = 1 342 | cmdPacket = PrinterLockCommand(Packet.MESSAGE_MODE_COMMAND, lockState=lockState) 343 | encodedCommand = cmdPacket.encodeCommand(sessionTime, pinCode) 344 | packetFactory = PacketFactory() 345 | decodedPacket = packetFactory.decode(encodedCommand) 346 | postHeader = decodedPacket.header 347 | self.helper_verify_header( 348 | postHeader, 349 | Packet.MESSAGE_MODE_COMMAND, 350 | Packet.MESSAGE_TYPE_LOCK_DEVICE, 351 | len(encodedCommand), 352 | cmdPacket.encodedSessionTime, 353 | pinCode, 354 | ) 355 | # Verify Payload 356 | self.assertEqual(decodedPacket.payload["lockState"], lockState) 357 | 358 | def test_encode_resp_lock(self): 359 | """Test encoding a Lock Printer Response.""" 360 | sessionTime = int(round(time.time() * 1000)) 361 | returnCode = Packet.RTN_E_RCV_FRAME 362 | ejecting = 0 363 | battery = 2 364 | printCount = 7 365 | resPacket = PrinterLockCommand(Packet.MESSAGE_MODE_RESPONSE) 366 | encodedResponse = resPacket.encodeResponse(sessionTime, returnCode, ejecting, battery, printCount) 367 | packetFactory = PacketFactory() 368 | decodedPacket = packetFactory.decode(encodedResponse) 369 | # decodedPacket.printDebug() 370 | postHeader = decodedPacket.header 371 | self.helper_verify_header( 372 | postHeader, 373 | Packet.MESSAGE_MODE_RESPONSE, 374 | Packet.MESSAGE_TYPE_LOCK_DEVICE, 375 | len(encodedResponse), 376 | resPacket.encodedSessionTime, 377 | returnCode=returnCode, 378 | ejecting=ejecting, 379 | battery=battery, 380 | printCount=printCount, 381 | ) 382 | 383 | def test_encode_cmd_reset(self): 384 | """Test encoding a Reset Command.""" 385 | sessionTime = int(round(time.time() * 1000)) 386 | pinCode = 1111 387 | cmdPacket = ResetCommand(Packet.MESSAGE_MODE_COMMAND) 388 | encodedCommand = cmdPacket.encodeCommand(sessionTime, pinCode) 389 | packetFactory = PacketFactory() 390 | decodedPacket = packetFactory.decode(encodedCommand) 391 | postHeader = decodedPacket.header 392 | self.helper_verify_header( 393 | postHeader, 394 | Packet.MESSAGE_MODE_COMMAND, 395 | Packet.MESSAGE_TYPE_RESET, 396 | len(encodedCommand), 397 | cmdPacket.encodedSessionTime, 398 | pinCode, 399 | ) 400 | 401 | def test_encode_resp_reset(self): 402 | """Test encoding a Reset Response.""" 403 | sessionTime = int(round(time.time() * 1000)) 404 | returnCode = Packet.RTN_E_RCV_FRAME 405 | ejecting = 0 406 | battery = 2 407 | printCount = 7 408 | resPacket = ResetCommand(Packet.MESSAGE_MODE_RESPONSE) 409 | encodedResponse = resPacket.encodeResponse(sessionTime, returnCode, ejecting, battery, printCount) 410 | packetFactory = PacketFactory() 411 | decodedPacket = packetFactory.decode(encodedResponse) 412 | # decodedPacket.printDebug() 413 | postHeader = decodedPacket.header 414 | self.helper_verify_header( 415 | postHeader, 416 | Packet.MESSAGE_MODE_RESPONSE, 417 | Packet.MESSAGE_TYPE_RESET, 418 | len(encodedResponse), 419 | resPacket.encodedSessionTime, 420 | returnCode=returnCode, 421 | ejecting=ejecting, 422 | battery=battery, 423 | printCount=printCount, 424 | ) 425 | 426 | def test_encode_cmd_prep(self): 427 | """Test encoding a Prep Image Command.""" 428 | sessionTime = int(round(time.time() * 1000)) 429 | pinCode = 1111 430 | format = 16 431 | options = 128 432 | imgLength = 1440000 433 | cmdPacket = PrepImageCommand(Packet.MESSAGE_MODE_COMMAND, format=format, options=options, imgLength=imgLength) 434 | encodedCommand = cmdPacket.encodeCommand(sessionTime, pinCode) 435 | packetFactory = PacketFactory() 436 | decodedPacket = packetFactory.decode(encodedCommand) 437 | postHeader = decodedPacket.header 438 | self.helper_verify_header( 439 | postHeader, 440 | Packet.MESSAGE_MODE_COMMAND, 441 | Packet.MESSAGE_TYPE_PREP_IMAGE, 442 | len(encodedCommand), 443 | cmdPacket.encodedSessionTime, 444 | pinCode, 445 | ) 446 | # Verify Payload 447 | self.assertEqual(decodedPacket.payload["format"], format) 448 | self.assertEqual(decodedPacket.payload["options"], options) 449 | self.assertEqual(decodedPacket.payload["imgLength"], imgLength) 450 | 451 | def test_encode_resp_prep(self): 452 | """Test encoding a Prep Image Response.""" 453 | sessionTime = int(round(time.time() * 1000)) 454 | returnCode = Packet.RTN_E_RCV_FRAME 455 | ejecting = 0 456 | battery = 2 457 | printCount = 7 458 | maxLen = 60000 459 | resPacket = PrepImageCommand(Packet.MESSAGE_MODE_RESPONSE, maxLen=maxLen) 460 | encodedResponse = resPacket.encodeResponse(sessionTime, returnCode, ejecting, battery, printCount) 461 | packetFactory = PacketFactory() 462 | decodedPacket = packetFactory.decode(encodedResponse) 463 | # decodedPacket.printDebug() 464 | postHeader = decodedPacket.header 465 | self.helper_verify_header( 466 | postHeader, 467 | Packet.MESSAGE_MODE_RESPONSE, 468 | Packet.MESSAGE_TYPE_PREP_IMAGE, 469 | len(encodedResponse), 470 | resPacket.encodedSessionTime, 471 | returnCode=returnCode, 472 | ejecting=ejecting, 473 | battery=battery, 474 | printCount=printCount, 475 | ) 476 | 477 | # Verify Payload 478 | self.assertEqual(decodedPacket.payload["maxLen"], maxLen) 479 | 480 | def test_encode_cmd_send(self): 481 | """Test encoding a Send Image Command.""" 482 | sessionTime = int(round(time.time() * 1000)) 483 | pinCode = 1111 484 | sequenceNumber = 5 485 | payloadBytes = bytearray(10) 486 | cmdPacket = SendImageCommand( 487 | Packet.MESSAGE_MODE_COMMAND, sequenceNumber=sequenceNumber, payloadBytes=payloadBytes 488 | ) 489 | encodedCommand = cmdPacket.encodeCommand(sessionTime, pinCode) 490 | packetFactory = PacketFactory() 491 | decodedPacket = packetFactory.decode(encodedCommand) 492 | postHeader = decodedPacket.header 493 | self.helper_verify_header( 494 | postHeader, 495 | Packet.MESSAGE_MODE_COMMAND, 496 | Packet.MESSAGE_TYPE_SEND_IMAGE, 497 | len(encodedCommand), 498 | cmdPacket.encodedSessionTime, 499 | pinCode, 500 | ) 501 | # Verify Payload 502 | self.assertEqual(decodedPacket.payload["sequenceNumber"], sequenceNumber) 503 | self.assertEqual(decodedPacket.payload["payloadBytes"], payloadBytes) 504 | 505 | def test_encode_resp_send(self): 506 | """Test encoding a Send Image Response.""" 507 | sessionTime = int(round(time.time() * 1000)) 508 | returnCode = Packet.RTN_E_RCV_FRAME 509 | ejecting = 0 510 | battery = 2 511 | printCount = 7 512 | sequenceNumber = 5 513 | resPacket = SendImageCommand(Packet.MESSAGE_MODE_RESPONSE, sequenceNumber=sequenceNumber) 514 | encodedResponse = resPacket.encodeResponse(sessionTime, returnCode, ejecting, battery, printCount) 515 | packetFactory = PacketFactory() 516 | decodedPacket = packetFactory.decode(encodedResponse) 517 | # decodedPacket.printDebug() 518 | postHeader = decodedPacket.header 519 | self.helper_verify_header( 520 | postHeader, 521 | Packet.MESSAGE_MODE_RESPONSE, 522 | Packet.MESSAGE_TYPE_SEND_IMAGE, 523 | len(encodedResponse), 524 | resPacket.encodedSessionTime, 525 | returnCode=returnCode, 526 | ejecting=ejecting, 527 | battery=battery, 528 | printCount=printCount, 529 | ) 530 | 531 | # Verify Payload 532 | self.assertEqual(decodedPacket.payload["sequenceNumber"], sequenceNumber) 533 | 534 | def test_encode_cmd_83(self): 535 | """Test encoding a Type 83 Command.""" 536 | sessionTime = int(round(time.time() * 1000)) 537 | pinCode = 1111 538 | cmdPacket = Type83Command(Packet.MESSAGE_MODE_COMMAND) 539 | encodedCommand = cmdPacket.encodeCommand(sessionTime, pinCode) 540 | packetFactory = PacketFactory() 541 | decodedPacket = packetFactory.decode(encodedCommand) 542 | postHeader = decodedPacket.header 543 | self.helper_verify_header( 544 | postHeader, 545 | Packet.MESSAGE_MODE_COMMAND, 546 | Packet.MESSAGE_TYPE_83, 547 | len(encodedCommand), 548 | cmdPacket.encodedSessionTime, 549 | pinCode, 550 | ) 551 | 552 | def test_encode_resp_83(self): 553 | """Test encoding a Type 83 Response.""" 554 | sessionTime = int(round(time.time() * 1000)) 555 | returnCode = Packet.RTN_E_RCV_FRAME 556 | ejecting = 0 557 | battery = 2 558 | printCount = 7 559 | resPacket = Type83Command(Packet.MESSAGE_MODE_RESPONSE) 560 | encodedResponse = resPacket.encodeResponse(sessionTime, returnCode, ejecting, battery, printCount) 561 | packetFactory = PacketFactory() 562 | decodedPacket = packetFactory.decode(encodedResponse) 563 | # decodedPacket.printDebug() 564 | postHeader = decodedPacket.header 565 | self.helper_verify_header( 566 | postHeader, 567 | Packet.MESSAGE_MODE_RESPONSE, 568 | Packet.MESSAGE_TYPE_83, 569 | len(encodedResponse), 570 | resPacket.encodedSessionTime, 571 | returnCode=returnCode, 572 | ejecting=ejecting, 573 | battery=battery, 574 | printCount=printCount, 575 | ) 576 | 577 | def test_encode_cmd_195(self): 578 | """Test encoding a Type 195 Command.""" 579 | sessionTime = int(round(time.time() * 1000)) 580 | pinCode = 1111 581 | cmdPacket = Type195Command(Packet.MESSAGE_MODE_COMMAND) 582 | encodedCommand = cmdPacket.encodeCommand(sessionTime, pinCode) 583 | packetFactory = PacketFactory() 584 | decodedPacket = packetFactory.decode(encodedCommand) 585 | postHeader = decodedPacket.header 586 | self.helper_verify_header( 587 | postHeader, 588 | Packet.MESSAGE_MODE_COMMAND, 589 | Packet.MESSAGE_TYPE_195, 590 | len(encodedCommand), 591 | cmdPacket.encodedSessionTime, 592 | pinCode, 593 | ) 594 | 595 | def test_encode_resp_195(self): 596 | """Test encoding a Type 195 Response.""" 597 | sessionTime = int(round(time.time() * 1000)) 598 | returnCode = Packet.RTN_E_RCV_FRAME 599 | ejecting = 0 600 | battery = 2 601 | printCount = 7 602 | resPacket = Type195Command(Packet.MESSAGE_MODE_RESPONSE) 603 | encodedResponse = resPacket.encodeResponse(sessionTime, returnCode, ejecting, battery, printCount) 604 | packetFactory = PacketFactory() 605 | decodedPacket = packetFactory.decode(encodedResponse) 606 | # decodedPacket.printDebug() 607 | postHeader = decodedPacket.header 608 | self.helper_verify_header( 609 | postHeader, 610 | Packet.MESSAGE_MODE_RESPONSE, 611 | Packet.MESSAGE_TYPE_195, 612 | len(encodedResponse), 613 | resPacket.encodedSessionTime, 614 | returnCode=returnCode, 615 | ejecting=ejecting, 616 | battery=battery, 617 | printCount=printCount, 618 | ) 619 | 620 | def test_encode_cmd_lock_state(self): 621 | """Test encoding a lock state Command.""" 622 | sessionTime = int(round(time.time() * 1000)) 623 | pinCode = 1111 624 | cmdPacket = LockStateCommand(Packet.MESSAGE_MODE_COMMAND) 625 | encodedCommand = cmdPacket.encodeCommand(sessionTime, pinCode) 626 | packetFactory = PacketFactory() 627 | decodedPacket = packetFactory.decode(encodedCommand) 628 | postHeader = decodedPacket.header 629 | self.helper_verify_header( 630 | postHeader, 631 | Packet.MESSAGE_MODE_COMMAND, 632 | Packet.MESSAGE_TYPE_SET_LOCK_STATE, 633 | len(encodedCommand), 634 | cmdPacket.encodedSessionTime, 635 | pinCode, 636 | ) 637 | 638 | def test_encode_resp_lock_state(self): 639 | """Test encoding a lock state Response.""" 640 | sessionTime = int(round(time.time() * 1000)) 641 | returnCode = Packet.RTN_E_RCV_FRAME 642 | ejecting = 0 643 | battery = 2 644 | printCount = 7 645 | unknownFourByteInt = 100 646 | resPacket = LockStateCommand(Packet.MESSAGE_MODE_RESPONSE, unknownFourByteInt=unknownFourByteInt) 647 | encodedResponse = resPacket.encodeResponse(sessionTime, returnCode, ejecting, battery, printCount) 648 | packetFactory = PacketFactory() 649 | decodedPacket = packetFactory.decode(encodedResponse) 650 | # decodedPacket.printDebug() 651 | postHeader = decodedPacket.header 652 | self.helper_verify_header( 653 | postHeader, 654 | Packet.MESSAGE_MODE_RESPONSE, 655 | Packet.MESSAGE_TYPE_SET_LOCK_STATE, 656 | len(encodedResponse), 657 | resPacket.encodedSessionTime, 658 | returnCode=returnCode, 659 | ejecting=ejecting, 660 | battery=battery, 661 | printCount=printCount, 662 | ) 663 | # Verify Payload 664 | self.assertEqual(decodedPacket.payload["unknownFourByteInt"], unknownFourByteInt) 665 | 666 | 667 | if __name__ == "__main__": 668 | 669 | unittest.main() 670 | -------------------------------------------------------------------------------- /instax/packet.py: -------------------------------------------------------------------------------- 1 | """Fujifilm Instax SP-2 Packet Library. 2 | 3 | This packet library can be used to encode and decode packets to be sent to 4 | or recieved from a Fujifilm Instax SP-2. It is designed to be used with the 5 | instax_api Python Library. 6 | """ 7 | from loguru import logger 8 | 9 | 10 | class PacketFactory: 11 | """Packet Factory. 12 | 13 | Used to generate new pakcets and to decode existing packets. 14 | """ 15 | 16 | MESSAGE_TYPE_SPECIFICATIONS = 79 17 | MESSAGE_TYPE_RESET = 80 18 | MESSAGE_TYPE_PREP_IMAGE = 81 19 | MESSAGE_TYPE_SEND_IMAGE = 82 20 | MESSAGE_TYPE_83 = 83 21 | MESSAGE_TYPE_SET_LOCK_STATE = 176 22 | MESSAGE_TYPE_LOCK_DEVICE = 179 23 | MESSAGE_TYPE_CHANGE_PASSWORD = 182 24 | MESSAGE_TYPE_PRINTER_VERSION = 192 25 | MESSAGE_TYPE_PRINT_COUNT = 193 26 | MESSAGE_TYPE_MODEL_NAME = 194 27 | MESSAGE_TYPE_195 = 195 28 | MESSAGE_TYPE_PRE_PRINT = 196 29 | 30 | MESSAGE_MODE_COMMAND = 36 # Command from Client 31 | MESSAGE_MODE_RESPONSE = 42 # Response from Server 32 | 33 | def __init__(self): 34 | """Init for Packet Factory.""" 35 | pass 36 | 37 | def printRawByteArray(self, byteArray): 38 | """Print a byte array fully.""" 39 | hexString = "".join("%02x" % i for i in byteArray) 40 | return " ".join(hexString[i : i + 4] for i in range(0, len(hexString), 4)) 41 | 42 | def decode(self, byteArray): 43 | """Decode a byte array into an instax Packet.""" 44 | self.byteArray = byteArray 45 | # Get first two bytes as they will help identify the type of packet 46 | self.mode = byteArray[0] 47 | pType = byteArray[1] 48 | 49 | # Identify the type of packet and hand over to that packets class 50 | if pType == self.MESSAGE_TYPE_SPECIFICATIONS: 51 | return SpecificationsCommand(mode=self.mode, byteArray=byteArray) 52 | elif pType == self.MESSAGE_TYPE_PRINTER_VERSION: 53 | return VersionCommand(mode=self.mode, byteArray=byteArray) 54 | elif pType == self.MESSAGE_TYPE_PRINT_COUNT: 55 | return PrintCountCommand(mode=self.mode, byteArray=byteArray) 56 | elif pType == self.MESSAGE_TYPE_RESET: 57 | return ResetCommand(mode=self.mode, byteArray=byteArray) 58 | elif pType == self.MESSAGE_TYPE_PREP_IMAGE: 59 | return PrepImageCommand(mode=self.mode, byteArray=byteArray) 60 | elif pType == self.MESSAGE_TYPE_SEND_IMAGE: 61 | return SendImageCommand(mode=self.mode, byteArray=byteArray) 62 | elif pType == self.MESSAGE_TYPE_MODEL_NAME: 63 | return ModelNameCommand(mode=self.mode, byteArray=byteArray) 64 | elif pType == self.MESSAGE_TYPE_PRE_PRINT: 65 | return PrePrintCommand(mode=self.mode, byteArray=byteArray) 66 | elif pType == self.MESSAGE_TYPE_LOCK_DEVICE: 67 | return PrinterLockCommand(mode=self.mode, byteArray=byteArray) 68 | elif pType == self.MESSAGE_TYPE_83: 69 | return Type83Command(mode=self.mode, byteArray=byteArray) 70 | elif pType == self.MESSAGE_TYPE_195: 71 | return Type195Command(mode=self.mode, byteArray=byteArray) 72 | elif pType == self.MESSAGE_TYPE_SET_LOCK_STATE: 73 | return LockStateCommand(mode=self.mode, byteArray=byteArray) 74 | else: 75 | logger.debug("Unknown Packet Type: " + str(pType)) 76 | logger.debug("Packet Bytes: [" + self.printRawByteArray(byteArray) + "]") 77 | 78 | 79 | class Packet: 80 | """Base Packet Class.""" 81 | 82 | MESSAGE_TYPE_SPECIFICATIONS = 79 83 | MESSAGE_TYPE_RESET = 80 84 | MESSAGE_TYPE_PREP_IMAGE = 81 85 | MESSAGE_TYPE_SEND_IMAGE = 82 86 | MESSAGE_TYPE_83 = 83 87 | MESSAGE_TYPE_SET_LOCK_STATE = 176 88 | MESSAGE_TYPE_LOCK_DEVICE = 179 89 | MESSAGE_TYPE_CHANGE_PASSWORD = 182 90 | MESSAGE_TYPE_PRINTER_VERSION = 192 91 | MESSAGE_TYPE_PRINT_COUNT = 193 92 | MESSAGE_TYPE_MODEL_NAME = 194 93 | MESSAGE_TYPE_195 = 195 94 | MESSAGE_TYPE_PRE_PRINT = 196 95 | 96 | MESSAGE_MODE_COMMAND = 36 # Command from Client 97 | MESSAGE_MODE_RESPONSE = 42 # Response from Server 98 | 99 | RTN_E_RCV_FRAME = 0 100 | RTN_E_PI_SENSOR = 248 101 | RTN_E_UNMATCH_PASS = 247 102 | RTN_E_MOTOR = 246 103 | RTN_E_CAM_POINT = 245 104 | RTN_E_FILM_EMPTY = 244 105 | RTN_E_RCV_FRAME_1 = 243 106 | RTN_E_RCV_FRAME_2 = 242 107 | RTN_E_RCV_FRAME_3 = 241 108 | RTN_E_RCV_FRAME_4 = 240 109 | RTN_E_CONNECT = 224 110 | RTN_E_CHARGE = 180 111 | RTN_E_TESTING = 165 112 | RTN_E_EJECTING = 164 113 | RTN_E_PRINTING = 163 # ST_PRINT 114 | RTN_E_BATTERY_EMPTY = 162 115 | RTN_E_NOT_IMAGE_DATA = 161 116 | RTN_E_OTHER_USED = 160 117 | RTN_ST_UPDATE = 127 # RET_HOLD 118 | 119 | strings = {MESSAGE_MODE_COMMAND: "Command", MESSAGE_MODE_RESPONSE: "Response"} 120 | 121 | def __init__(self, mode=None): 122 | """Init for Packet.""" 123 | pass 124 | 125 | def printByteArray(self, byteArray): 126 | """Print a Byte Array. 127 | 128 | Prints a Byte array in the following format: b1b2 b3b4... 129 | """ 130 | hexString = "".join("%02x" % i for i in byteArray) 131 | data = " ".join(hexString[i : i + 4] for i in range(0, len(hexString), 4)) 132 | info = (data[:80] + "..") if len(data) > 80 else data 133 | return info 134 | 135 | def printRawByteArray(self, byteArray): 136 | """Print a byte array fully.""" 137 | hexString = "".join("%02x" % i for i in byteArray) 138 | return " ".join(hexString[i : i + 4] for i in range(0, len(hexString), 4)) 139 | 140 | def printDebug(self): 141 | """Print Debug information about packet.""" 142 | logger.debug("--------------------- Packet Debug Data --------------------") 143 | logger.debug("Bytes: %s" % (self.printByteArray(self.byteArray))) 144 | logger.debug("Mode: %s" % (self.strings[self.mode])) 145 | logger.debug("Type: %s" % (self.NAME)) 146 | logger.debug("Valid: %s" % (self.valid)) 147 | logger.debug("Header:") 148 | logger.debug(" Start Byte: %s" % (self.header["startByte"])) 149 | logger.debug(" Command: %s" % (self.header["cmdByte"])) 150 | logger.debug(" Packet Length: %s" % (self.header["packetLength"])) 151 | logger.debug(" Session Time: %s" % (self.header["sessionTime"])) 152 | if self.mode == self.MESSAGE_MODE_COMMAND: 153 | logger.debug(" Password: %s" % (self.header["password"])) 154 | elif self.mode == self.MESSAGE_MODE_RESPONSE: 155 | logger.debug(" Return Code: %s" % (self.header["returnCode"])) 156 | logger.debug(" Unknown 1: %s" % (self.header["unknown1"])) 157 | logger.debug(" Ejecting: %s" % (self.header["ejecting"])) 158 | logger.debug(" Battery: %s" % (self.header["battery"])) 159 | logger.debug(" Prints Left: %s" % (self.header["printCount"])) 160 | 161 | if len(self.payload) == 0: 162 | logger.debug("Payload: None") 163 | else: 164 | logger.debug("Payload:") 165 | for key in self.payload: 166 | if key == "payloadBytes": 167 | logger.debug( 168 | " payloadBytes: (length: %s) : [%s]" 169 | % (str(len(self.payload[key])), self.printByteArray(self.payload[key])) 170 | ) 171 | else: 172 | logger.debug(f" {key} : {self.payload[key]}") 173 | logger.debug("------------------------------------------------------------") 174 | 175 | def getPacketObject(self): 176 | """Return a simple object containing all packet details.""" 177 | packetObj = {} 178 | packetObj["bytes"] = self.printByteArray(self.byteArray) 179 | packetObj["header"] = self.header 180 | packetPayload = {} 181 | for key in self.payload: 182 | if key == "payloadBytes": 183 | packetPayload["payloadBytes"] = self.printByteArray(self.payload[key]) 184 | else: 185 | packetPayload[key] = self.payload[key] 186 | packetObj["payload"] = packetPayload 187 | return packetObj 188 | 189 | def decodeHeader(self, mode, byteArray): 190 | """Decode packet header.""" 191 | startByte = self.getOneByteInt(0, byteArray) 192 | cmdByte = self.getOneByteInt(1, byteArray) 193 | packetLength = self.getTwoByteInt(2, byteArray) 194 | responseTime = self.getFourByteInt(4, byteArray) 195 | header = {"startByte": startByte, "cmdByte": cmdByte, "packetLength": packetLength, "sessionTime": responseTime} 196 | 197 | if mode == self.MESSAGE_MODE_COMMAND: 198 | # Command Specific Header Fields 199 | header["password"] = self.getTwoByteInt(8, byteArray) 200 | elif mode == self.MESSAGE_MODE_RESPONSE: 201 | # Payload Specific Header Fields 202 | header["returnCode"] = self.getOneByteInt(12, byteArray) 203 | header["unknown1"] = self.getOneByteInt(13, byteArray) 204 | header["ejecting"] = self.getEjecting(14, byteArray) 205 | header["battery"] = self.getBatteryLevel(byteArray) 206 | header["printCount"] = self.getPrintCount(byteArray) 207 | 208 | self.header = header 209 | 210 | return header 211 | 212 | def validatePacket(self, byteArray, packetLength): 213 | """ 214 | Validate that a payload ends correctly. 215 | 216 | This is done by checking the end bytes and the checksum. 217 | """ 218 | try: 219 | checkSumIndex = 0 220 | checkSum = 0 221 | while checkSumIndex < (packetLength - 4): 222 | checkSum += byteArray[checkSumIndex] & 0xFF 223 | checkSumIndex += 1 224 | if (byteArray[checkSumIndex + 2] == 13) and (byteArray[checkSumIndex + 3] == 10): 225 | expectedCB = checkSum + ( 226 | ((byteArray[checkSumIndex] & 0xFF) << 8) | ((byteArray[checkSumIndex + 1] & 0xFF) << 0) 227 | ) 228 | if (expectedCB & 65535) == 65535: 229 | return True 230 | else: 231 | return False 232 | else: 233 | return False 234 | except Exception as ex: 235 | logger.debug("Unexpected Error validating packet: " + str(type(ex))) 236 | logger.debug(ex.args) 237 | logger.debug(ex) 238 | logger.debug("Expected: %s" % (packetLength)) 239 | logger.debug("Actual: %s" % (str(len(byteArray)))) 240 | logger.debug("Final 4 bytes: %s" % (self.printByteArray(byteArray[-4:]))) 241 | 242 | def generateCommand(self, mode, cmdType, sessionTime, payload, pinCode): 243 | """Generate a command. 244 | 245 | Takes Command arguments and packs them into a byteArray to be 246 | sent to the Instax SP-2. 247 | """ 248 | self.encodedSessionTime = self.getFourByteInt(0, self.encodeFourByteInt(sessionTime)) 249 | commandPayloadLength = 16 + len(payload) 250 | commandPayload = bytearray() 251 | commandPayload.append(mode & 0xFF) # Start of payload is 36 252 | commandPayload.append(cmdType & 0xFF) # The Command bytes 253 | commandPayload = commandPayload + self.encodeTwoByteInt(commandPayloadLength) 254 | commandPayload = commandPayload + self.encodeFourByteInt(sessionTime) 255 | commandPayload = commandPayload + self.encodeTwoByteInt(pinCode) 256 | commandPayload.append(0) # Nothing 257 | commandPayload.append(0) # Nothing 258 | if len(payload) > 0: 259 | commandPayload = commandPayload + payload 260 | # Generating the Checksum & End of payload 261 | checkSumIndex = 0 262 | checkSum = 0 263 | while checkSumIndex < (commandPayloadLength - 4): 264 | checkSum += commandPayload[checkSumIndex] & 0xFF 265 | checkSumIndex += 1 266 | commandPayload.append(((checkSum ^ -1) >> 8) & 0xFF) 267 | commandPayload.append(((checkSum ^ -1) >> 0) & 0xFF) 268 | commandPayload.append(13) 269 | commandPayload.append(10) 270 | return commandPayload 271 | 272 | def generateResponse(self, mode, cmdType, sessionTime, payload, returnCode, ejectState, battery, printCount): 273 | """Generate a response Byte Array. 274 | 275 | Takes Response arguments and packs them into a byteArray to be 276 | sent to the Instax-SP2. 277 | """ 278 | self.encodedSessionTime = self.getFourByteInt(0, self.encodeFourByteInt(sessionTime)) 279 | responsePayloadLength = 20 + len(payload) 280 | responsePayload = bytearray() 281 | responsePayload.append(mode & 0xFF) # Start of payload is 42 282 | responsePayload.append(cmdType & 0xFF) # The Response type bytes 283 | responsePayload = responsePayload + self.encodeTwoByteInt(responsePayloadLength) 284 | responsePayload = responsePayload + self.encodeFourByteInt(sessionTime) 285 | responsePayload = responsePayload + bytearray(4) 286 | responsePayload = responsePayload + self.encodeOneByteInt(returnCode) 287 | responsePayload.append(0) # Nothing 288 | responsePayload = responsePayload + self.encodeEjecting(0) 289 | responsePayload = responsePayload + self.encodeBatteryAndPrintCount(battery, printCount) 290 | 291 | if len(payload) > 0: 292 | responsePayload = responsePayload + payload 293 | # Generating the Checksum & End of payload 294 | checkSumIndex = 0 295 | checkSum = 0 296 | while checkSumIndex < (responsePayloadLength - 4): 297 | checkSum += responsePayload[checkSumIndex] & 0xFF 298 | checkSumIndex += 1 299 | responsePayload.append(((checkSum ^ -1) >> 8) & 0xFF) 300 | responsePayload.append(((checkSum ^ -1) >> 0) & 0xFF) 301 | responsePayload.append(13) 302 | responsePayload.append(10) 303 | return responsePayload 304 | 305 | def encodeCommand(self, sessionTime, pinCode): 306 | """Encode a command packet into a byteArray.""" 307 | payload = self.encodeComPayload() 308 | encodedPacket = self.generateCommand(self.mode, self.TYPE, sessionTime, payload, pinCode) 309 | return encodedPacket 310 | 311 | def encodeResponse(self, sessionTime, returnCode, ejectState, battery, printCount): 312 | """Encode a response packet into a byteArray.""" 313 | payload = self.encodeRespPayload() 314 | encodedPacket = self.generateResponse( 315 | self.mode, self.TYPE, sessionTime, payload, returnCode, ejectState, battery, printCount 316 | ) 317 | return encodedPacket 318 | 319 | def getFourByteInt(self, offset, byteArray): 320 | """Decode a Four Byte Integer.""" 321 | if len(byteArray) < (offset + 4): 322 | return 0 323 | else: 324 | return ( 325 | ((byteArray[offset] & 0xFF) << 24) 326 | | ((byteArray[(offset) + 1] & 0xFF) << 16) 327 | | ((byteArray[(offset) + 2] & 0xFF) << 8) 328 | | ((byteArray[(offset) + 3] & 0xFF) << 0) 329 | ) 330 | 331 | def encodeFourByteInt(self, numberToEncode): 332 | """Encode a Four Byte Integer.""" 333 | fourByteInt = bytearray() 334 | fourByteInt.append((numberToEncode >> 24) & 0xFF) # B1 335 | fourByteInt.append((numberToEncode >> 16) & 0xFF) # B2 336 | fourByteInt.append((numberToEncode >> 8) & 0xFF) # B3 337 | fourByteInt.append((numberToEncode >> 0) & 0xFF) # B4 338 | return fourByteInt 339 | 340 | def getTwoByteInt(self, offset, byteArray): 341 | """Decode a Two Byte Integer.""" 342 | if len(byteArray) < (offset + 2): 343 | return 0 344 | else: 345 | return ((byteArray[offset] & 0xFF) << 8) | ((byteArray[(offset) + 1] & 0xFF) << 0) 346 | 347 | def encodeTwoByteInt(self, numberToEncode): 348 | """Encode a Two Byte Integer.""" 349 | twoByteInt = bytearray() 350 | twoByteInt.append((numberToEncode >> 8) & 0xFF) # B1 351 | twoByteInt.append((numberToEncode >> 0) & 0xFF) # B2 352 | return twoByteInt 353 | 354 | def getOneByteInt(self, offset, byteArray): 355 | """Decode a One Byte Integer.""" 356 | if len(byteArray) < (offset + 1): 357 | return 0 358 | else: 359 | return byteArray[offset] & 0xFF 360 | 361 | def encodeOneByteInt(self, numberToEncode): 362 | """Encode a One Byte Integer.""" 363 | oneByteInt = bytearray() 364 | oneByteInt.append((numberToEncode >> 0) & 0xFF) 365 | return oneByteInt 366 | 367 | def getEjecting(self, offset, byteArray): 368 | """Decode the Ejecting State.""" 369 | if len(byteArray) < (offset + 1): 370 | return 0 371 | else: 372 | return (byteArray[offset] >> 2) & 0xFF 373 | 374 | def encodeEjecting(self, eject): 375 | """Encode the Ejecting State.""" 376 | ejectState = bytearray() 377 | ejectState.append((eject >> 2) & 0xFF) 378 | return ejectState 379 | 380 | def getBatteryLevel(self, byteArray): 381 | """Decode the Battery Level.""" 382 | if len(byteArray) < 16: 383 | return -1 384 | else: 385 | return (byteArray[15] >> 4) & 7 386 | 387 | def getPrintCount(self, byteArray): 388 | """Decode the Print Count.""" 389 | if len(byteArray) < 16: 390 | return -1 391 | else: 392 | return (byteArray[15] >> 0) & 15 393 | 394 | def encodeBatteryAndPrintCount(self, battery, printCount): 395 | """Encode Battery Level and Print Count.""" 396 | oneByteInt = bytearray() 397 | oneByteInt.append((battery << 4) | printCount << 0) 398 | return oneByteInt 399 | 400 | def formatVersionNumber(self, version): 401 | """Encode a Version Number.""" 402 | part2 = version & 0xFF 403 | part1 = (65280 & version) >> 8 404 | return "{}.{}".format("%0.2X" % part1, "%0.2X" % part2) 405 | 406 | def encodeModelString(self, model): 407 | """Encode a Model String.""" 408 | return bytes(model, encoding="UTF-8") 409 | 410 | def getPrinterModelString(self, offset, byteArray): 411 | """Decode a Model String.""" 412 | if len(byteArray) < (offset + 4): 413 | return "" 414 | else: 415 | return str(byteArray[offset : offset + 4], "ascii") 416 | 417 | def getPayloadBytes(self, offset, length, byteArray): 418 | """Return Payload Bytes.""" 419 | return byteArray[offset : offset + length] 420 | 421 | 422 | class SpecificationsCommand(Packet): 423 | """Specifications Command and Response.""" 424 | 425 | NAME = "Specifications" 426 | TYPE = Packet.MESSAGE_TYPE_SPECIFICATIONS 427 | 428 | def __init__( 429 | self, 430 | mode, 431 | byteArray=None, 432 | maxHeight=800, 433 | maxWidth=600, 434 | maxColours=256, 435 | unknown1=None, 436 | maxMsgSize=None, 437 | unknown2=None, 438 | unknown3=None, 439 | ): 440 | """Initialise the Packet.""" 441 | super().__init__(mode) 442 | self.payload = {} 443 | self.mode = mode 444 | 445 | if byteArray is not None: 446 | self.byteArray = byteArray 447 | self.header = super().decodeHeader(mode, byteArray) 448 | self.valid = self.validatePacket(byteArray, self.header["packetLength"]) 449 | if mode == self.MESSAGE_MODE_COMMAND: 450 | self.decodedCommandPayload = self.decodeComPayload(byteArray) 451 | elif mode == self.MESSAGE_MODE_RESPONSE: 452 | self.payload = self.decodeRespPayload(byteArray) 453 | else: 454 | self.mode = mode 455 | self.maxHeight = maxHeight 456 | self.maxWidth = maxWidth 457 | self.maxColours = maxColours 458 | self.unknown1 = unknown1 459 | self.unknown2 = unknown2 460 | self.unknown3 = unknown3 461 | self.maxMsgSize = maxMsgSize 462 | 463 | def encodeComPayload(self): 464 | """Encode Command payload. 465 | 466 | This command does not have a payload, pass. 467 | """ 468 | return {} 469 | 470 | def decodeComPayload(self, byteArray): 471 | """Decode Command payload. 472 | 473 | This command does not have a payload, pass. 474 | """ 475 | return {} 476 | 477 | def encodeRespPayload(self): 478 | """Encode Response payload.""" 479 | payload = bytearray() 480 | payload = payload + self.encodeTwoByteInt(self.maxWidth) 481 | payload = payload + self.encodeTwoByteInt(self.maxHeight) 482 | payload = payload + self.encodeTwoByteInt(self.maxColours) 483 | payload = payload + self.encodeTwoByteInt(self.unknown1) 484 | payload = payload + bytearray(4) # Nothing 485 | payload = payload + self.encodeTwoByteInt(self.maxMsgSize) 486 | payload = payload + self.encodeOneByteInt(self.unknown2) 487 | payload.append(0) # Nothing 488 | payload = payload + self.encodeFourByteInt(self.unknown3) 489 | payload = payload + bytearray(8) # Nothing 490 | return payload 491 | 492 | def decodeRespPayload(self, byteArray): 493 | """Decode Response payload.""" 494 | self.maxWidth = self.getTwoByteInt(16, byteArray) 495 | self.maxHeight = self.getTwoByteInt(18, byteArray) 496 | self.maxColours = self.getTwoByteInt(20, byteArray) 497 | self.unknown1 = self.getTwoByteInt(22, byteArray) 498 | self.maxMsgSize = self.getTwoByteInt(28, byteArray) 499 | self.unknown2 = self.getOneByteInt(30, byteArray) 500 | self.unknown3 = self.getFourByteInt(32, byteArray) 501 | self.payload = { 502 | "maxHeight": self.maxHeight, 503 | "maxWidth": self.maxWidth, 504 | "maxColours": self.maxColours, 505 | "unknown1": self.unknown1, 506 | "maxMsgSize": self.maxMsgSize, 507 | "unknown2": self.unknown2, 508 | "unknown3": self.unknown3, 509 | } 510 | return self.payload 511 | 512 | 513 | class VersionCommand(Packet): 514 | """Version Command.""" 515 | 516 | NAME = "Version" 517 | TYPE = Packet.MESSAGE_TYPE_PRINTER_VERSION 518 | 519 | def __init__(self, mode, byteArray=None, unknown1=None, firmware=None, hardware=None): 520 | """Initialise the packet.""" 521 | super().__init__(mode) 522 | self.payload = {} 523 | self.mode = mode 524 | 525 | if byteArray is not None: 526 | self.byteArray = byteArray 527 | self.header = super().decodeHeader(mode, byteArray) 528 | self.valid = self.validatePacket(byteArray, self.header["packetLength"]) 529 | if mode == self.MESSAGE_MODE_COMMAND: 530 | self.decodedCommandPayload = self.decodeComPayload(byteArray) 531 | elif mode == self.MESSAGE_MODE_RESPONSE: 532 | self.payload = self.decodeRespPayload(byteArray) 533 | else: 534 | self.mode = mode 535 | self.unknown1 = unknown1 536 | self.firmware = firmware 537 | self.hardware = hardware 538 | 539 | def encodeComPayload(self): 540 | """Encode Command payload. 541 | 542 | This command does not have a payload, pass. 543 | """ 544 | return {} 545 | 546 | def decodeComPayload(self, byteArray): 547 | """Decode Command payload. 548 | 549 | This command does not have a payload, pass. 550 | """ 551 | return {} 552 | 553 | def encodeRespPayload(self): 554 | """Encode Response payload.""" 555 | payload = bytearray() 556 | payload = payload + self.encodeTwoByteInt(self.unknown1) 557 | payload = payload + self.encodeTwoByteInt(self.firmware) 558 | payload = payload + self.encodeTwoByteInt(self.hardware) 559 | payload = payload + bytearray(2) # Nothing 560 | return payload 561 | 562 | def decodeRespPayload(self, byteArray): 563 | """Decode Response payload.""" 564 | self.unknown1 = self.getTwoByteInt(16, byteArray) 565 | self.firmware = self.formatVersionNumber(self.getTwoByteInt(18, byteArray)) 566 | self.hardware = self.formatVersionNumber(self.getTwoByteInt(20, byteArray)) 567 | self.payload = {"unknown1": self.unknown1, "firmware": self.firmware, "hardware": self.hardware} 568 | return self.payload 569 | 570 | 571 | class PrintCountCommand(Packet): 572 | """Print Count Command.""" 573 | 574 | NAME = "Print Count" 575 | TYPE = Packet.MESSAGE_TYPE_PRINT_COUNT 576 | 577 | def __init__(self, mode, byteArray=None, printHistory=None): 578 | """Initialise the packet.""" 579 | super().__init__(mode) 580 | self.payload = {} 581 | self.mode = mode 582 | 583 | if byteArray is not None: 584 | self.byteArray = byteArray 585 | self.header = super().decodeHeader(mode, byteArray) 586 | self.valid = self.validatePacket(byteArray, self.header["packetLength"]) 587 | if mode == self.MESSAGE_MODE_COMMAND: 588 | self.decodedCommandPayload = self.decodeComPayload(byteArray) 589 | elif mode == self.MESSAGE_MODE_RESPONSE: 590 | self.payload = self.decodeRespPayload(byteArray) 591 | else: 592 | self.mode = mode 593 | self.printHistory = printHistory 594 | 595 | def encodeComPayload(self): 596 | """Encode Command payload. 597 | 598 | This command does not have a payload, pass. 599 | """ 600 | return {} 601 | 602 | def decodeComPayload(self, byteArray): 603 | """Decode Command payload. 604 | 605 | This command does not have a payload, pass. 606 | """ 607 | return {} 608 | 609 | def encodeRespPayload(self): 610 | """Encode Response payload.""" 611 | payload = bytearray() 612 | payload = payload + self.encodeFourByteInt(self.printHistory) 613 | payload = payload + bytearray(12) 614 | return payload 615 | 616 | def decodeRespPayload(self, byteArray): 617 | """Decode Response payload.""" 618 | self.printHistory = self.getFourByteInt(16, byteArray) 619 | self.payload = {"printHistory": self.printHistory} 620 | return self.payload 621 | 622 | 623 | class ModelNameCommand(Packet): 624 | """Model Name Command.""" 625 | 626 | NAME = "Model Name" 627 | TYPE = Packet.MESSAGE_TYPE_MODEL_NAME 628 | 629 | def __init__(self, mode, byteArray=None, modelName=None): 630 | """Initialise the packet.""" 631 | super().__init__(mode) 632 | self.payload = {} 633 | self.mode = mode 634 | 635 | if byteArray is not None: 636 | self.byteArray = byteArray 637 | self.header = super().decodeHeader(mode, byteArray) 638 | self.valid = self.validatePacket(byteArray, self.header["packetLength"]) 639 | if mode == self.MESSAGE_MODE_COMMAND: 640 | self.decodedCommandPayload = self.decodeComPayload(byteArray) 641 | elif mode == self.MESSAGE_MODE_RESPONSE: 642 | self.payload = self.decodeRespPayload(byteArray) 643 | else: 644 | self.mode = mode 645 | self.modelName = modelName 646 | 647 | def encodeComPayload(self): 648 | """Encode Command payload. 649 | 650 | This command does not have a payload, pass. 651 | """ 652 | return {} 653 | 654 | def decodeComPayload(self, byteArray): 655 | """Decode Command payload. 656 | 657 | This command does not have a payload, pass. 658 | """ 659 | return {} 660 | 661 | def encodeRespPayload(self): 662 | """Encode Response payload.""" 663 | payload = bytearray() 664 | payload = payload + self.encodeModelString(self.modelName) 665 | return payload 666 | 667 | def decodeRespPayload(self, byteArray): 668 | """Decode Response payload.""" 669 | self.modelName = self.getPrinterModelString(16, byteArray) 670 | self.payload = {"modelName": self.modelName} 671 | return self.payload 672 | 673 | 674 | class PrePrintCommand(Packet): 675 | """Pre Print Command.""" 676 | 677 | NAME = "Pre Print" 678 | TYPE = Packet.MESSAGE_TYPE_PRE_PRINT 679 | 680 | def __init__(self, mode, byteArray=None, cmdNumber=None, respNumber=None): 681 | """Initialise the packet.""" 682 | super().__init__(mode) 683 | self.payload = {} 684 | self.mode = mode 685 | 686 | if byteArray is not None: 687 | self.byteArray = byteArray 688 | self.header = super().decodeHeader(mode, byteArray) 689 | self.valid = self.validatePacket(byteArray, self.header["packetLength"]) 690 | if mode == self.MESSAGE_MODE_COMMAND: 691 | self.decodedCommandPayload = self.decodeComPayload(byteArray) 692 | elif mode == self.MESSAGE_MODE_RESPONSE: 693 | self.payload = self.decodeRespPayload(byteArray) 694 | else: 695 | self.mode = mode 696 | self.cmdNumber = cmdNumber 697 | self.respNumber = respNumber 698 | 699 | def encodeComPayload(self): 700 | """Encode Command Payload.""" 701 | payload = bytearray() 702 | payload = payload + bytearray(2) 703 | payload = payload + self.encodeTwoByteInt(self.cmdNumber) 704 | return payload 705 | 706 | def decodeComPayload(self, byteArray): 707 | """Decode the Command Payload.""" 708 | self.cmdNumber = self.getTwoByteInt(14, byteArray) 709 | self.payload = {"cmdNumber": self.cmdNumber} 710 | return self.payload 711 | 712 | def encodeRespPayload(self): 713 | """Encode Response Payload.""" 714 | payload = bytearray() 715 | payload = payload + self.encodeTwoByteInt(self.cmdNumber) 716 | payload = payload + self.encodeTwoByteInt(self.respNumber) 717 | return payload 718 | 719 | def decodeRespPayload(self, byteArray): 720 | """Decode Response Payload.""" 721 | self.cmdNumber = self.getTwoByteInt(16, byteArray) 722 | self.respNumber = self.getTwoByteInt(18, byteArray) 723 | self.payload = {"cmdNumber": self.cmdNumber, "respNumber": self.respNumber} 724 | return self.payload 725 | 726 | 727 | class PrinterLockCommand(Packet): 728 | """Printer Lock Command.""" 729 | 730 | NAME = "LockPrinter" 731 | TYPE = Packet.MESSAGE_TYPE_LOCK_DEVICE 732 | 733 | def __init__(self, mode, lockState=None, byteArray=None): 734 | """Initialise Lock Printer Packet.""" 735 | super().__init__(mode) 736 | self.payload = {} 737 | self.mode = mode 738 | if byteArray is not None: 739 | self.byteArray = byteArray 740 | self.header = super().decodeHeader(mode, byteArray) 741 | self.valid = self.validatePacket(byteArray, self.header["packetLength"]) 742 | if mode == self.MESSAGE_MODE_COMMAND: 743 | self.decodedCommandPayload = self.decodeComPayload(byteArray) 744 | elif mode == self.MESSAGE_MODE_RESPONSE: 745 | self.decodedCommandPayload = self.decodeRespPayload(byteArray) 746 | else: 747 | self.mode = mode 748 | self.lockState = lockState 749 | 750 | def encodeComPayload(self): 751 | """Encode Command Payload.""" 752 | payload = bytearray() 753 | payload = payload + self.encodeOneByteInt(self.lockState) 754 | payload = payload + self.encodeOneByteInt(0) 755 | payload = payload + self.encodeTwoByteInt(0) 756 | return payload 757 | 758 | def decodeComPayload(self, byteArray): 759 | """Decode the Command Payload.""" 760 | self.lockState = self.getOneByteInt(12, byteArray) 761 | self.payload = {"lockState": self.lockState} 762 | return self.payload 763 | 764 | def encodeRespPayload(self): 765 | """Encode Response Payload.""" 766 | return bytearray() 767 | 768 | def decodeRespPayload(self, byteArray): 769 | """Decode Response Payload.""" 770 | return {} 771 | 772 | 773 | class ResetCommand(Packet): 774 | """Reset Command.""" 775 | 776 | NAME = "Reset" 777 | TYPE = Packet.MESSAGE_TYPE_RESET 778 | 779 | def __init__(self, mode, byteArray=None): 780 | """Initialise Reset Command Packet.""" 781 | super().__init__(mode) 782 | self.payload = {} 783 | self.mode = mode 784 | if byteArray is not None: 785 | self.byteArray = byteArray 786 | self.header = super().decodeHeader(mode, byteArray) 787 | self.valid = self.validatePacket(byteArray, self.header["packetLength"]) 788 | if mode == self.MESSAGE_MODE_COMMAND: 789 | self.decodedCommandPayload = self.decodeComPayload(byteArray) 790 | elif mode == self.MESSAGE_MODE_RESPONSE: 791 | self.decodedCommandPayload = self.decodeRespPayload(byteArray) 792 | else: 793 | self.mode = mode 794 | 795 | def encodeComPayload(self): 796 | """Encode Command Payload.""" 797 | return bytearray() 798 | 799 | def decodeComPayload(self, byteArray): 800 | """Decode the Command Payload.""" 801 | return {} 802 | 803 | def encodeRespPayload(self): 804 | """Encode Response Payload.""" 805 | return bytearray() 806 | 807 | def decodeRespPayload(self, byteArray): 808 | """Decode Response Payload.""" 809 | return {} 810 | 811 | 812 | class PrepImageCommand(Packet): 813 | """Prep Image Command.""" 814 | 815 | NAME = "PrepImage" 816 | TYPE = Packet.MESSAGE_TYPE_PREP_IMAGE 817 | 818 | def __init__(self, mode, byteArray=None, format=None, options=None, imgLength=None, maxLen=None): 819 | """Initialise Prep Image Command Packet.""" 820 | super().__init__(mode) 821 | self.payload = {} 822 | self.mode = mode 823 | if byteArray is not None: 824 | self.byteArray = byteArray 825 | self.header = super().decodeHeader(mode, byteArray) 826 | self.valid = self.validatePacket(byteArray, self.header["packetLength"]) 827 | if mode == self.MESSAGE_MODE_COMMAND: 828 | self.decodedCommandPayload = self.decodeComPayload(byteArray) 829 | elif mode == self.MESSAGE_MODE_RESPONSE: 830 | self.decodedCommandPayload = self.decodeRespPayload(byteArray) 831 | else: 832 | self.mode = mode 833 | self.format = format 834 | self.options = options 835 | self.imgLength = imgLength 836 | self.maxLen = maxLen 837 | 838 | def encodeComPayload(self): 839 | """Encode Command Payload.""" 840 | payload = bytearray() 841 | payload = payload + self.encodeOneByteInt(self.format) 842 | payload = payload + self.encodeOneByteInt(self.options) 843 | payload = payload + self.encodeFourByteInt(self.imgLength) 844 | payload = payload + self.encodeTwoByteInt(0) 845 | payload = payload + self.encodeTwoByteInt(0) 846 | payload = payload + self.encodeTwoByteInt(0) 847 | return payload 848 | 849 | def decodeComPayload(self, byteArray): 850 | """Decode the Command Payload.""" 851 | self.format = self.getOneByteInt(12, byteArray) 852 | self.options = self.getOneByteInt(13, byteArray) 853 | self.imgLength = self.getFourByteInt(14, byteArray) 854 | 855 | self.payload = {"format": self.format, "options": self.options, "imgLength": self.imgLength} 856 | return self.payload 857 | 858 | def encodeRespPayload(self): 859 | """Encode Response Payload.""" 860 | payload = bytearray(2) 861 | payload = payload + self.encodeTwoByteInt(self.maxLen) 862 | return payload 863 | 864 | def decodeRespPayload(self, byteArray): 865 | """Decode Response Payload.""" 866 | self.maxLen = self.getTwoByteInt(18, byteArray) 867 | self.payload = {"maxLen": self.maxLen} 868 | return self.payload 869 | 870 | 871 | class SendImageCommand(Packet): 872 | """Send Image Command.""" 873 | 874 | NAME = "Send Image" 875 | TYPE = Packet.MESSAGE_TYPE_SEND_IMAGE 876 | 877 | def __init__(self, mode, byteArray=None, sequenceNumber=None, payloadBytes=None): 878 | """Initialise Send Image Command Packet.""" 879 | super().__init__(mode) 880 | self.payload = {} 881 | self.mode = mode 882 | if byteArray is not None: 883 | self.byteArray = byteArray 884 | self.header = super().decodeHeader(mode, byteArray) 885 | self.valid = self.validatePacket(byteArray, self.header["packetLength"]) 886 | if mode == self.MESSAGE_MODE_COMMAND: 887 | self.decodedCommandPayload = self.decodeComPayload(byteArray) 888 | elif mode == self.MESSAGE_MODE_RESPONSE: 889 | self.decodedCommandPayload = self.decodeRespPayload(byteArray) 890 | else: 891 | self.mode = mode 892 | self.sequenceNumber = sequenceNumber 893 | self.payloadBytes = payloadBytes 894 | 895 | def encodeComPayload(self): 896 | """Encode Command Payload.""" 897 | payload = bytearray(0) 898 | payload = payload + self.encodeFourByteInt(self.sequenceNumber) 899 | payload = payload + self.payloadBytes 900 | return payload 901 | 902 | def decodeComPayload(self, byteArray): 903 | """Decode the Command Payload.""" 904 | self.sequenceNumber = self.getFourByteInt(12, byteArray) 905 | payloadBytesLength = self.header["packetLength"] - 20 906 | self.payloadBytes = self.getPayloadBytes(16, payloadBytesLength, byteArray) 907 | self.payload = {"sequenceNumber": self.sequenceNumber, "payloadBytes": self.payloadBytes} 908 | return self.payload 909 | 910 | def encodeRespPayload(self): 911 | """Encode Response Payload.""" 912 | payload = bytearray(3) 913 | payload = payload + self.encodeOneByteInt(self.sequenceNumber) 914 | return payload 915 | 916 | def decodeRespPayload(self, byteArray): 917 | """Decode Response Payload.""" 918 | self.sequenceNumber = self.getOneByteInt(19, byteArray) 919 | self.payload = {"sequenceNumber": self.sequenceNumber} 920 | return self.payload 921 | 922 | 923 | class Type83Command(Packet): 924 | """Type 83 Command.""" 925 | 926 | NAME = "Type 83" 927 | TYPE = Packet.MESSAGE_TYPE_83 928 | 929 | def __init__(self, mode, byteArray=None): 930 | """Initialise Type 83 Command Packet.""" 931 | super().__init__(mode) 932 | self.payload = {} 933 | self.mode = mode 934 | if byteArray is not None: 935 | self.byteArray = byteArray 936 | self.header = super().decodeHeader(mode, byteArray) 937 | self.valid = self.validatePacket(byteArray, self.header["packetLength"]) 938 | if mode == self.MESSAGE_MODE_COMMAND: 939 | self.decodedCommandPayload = self.decodeComPayload(byteArray) 940 | elif mode == self.MESSAGE_MODE_RESPONSE: 941 | self.decodedCommandPayload = self.decodeRespPayload(byteArray) 942 | else: 943 | self.mode = mode 944 | 945 | def encodeComPayload(self): 946 | """Encode Command Payload.""" 947 | return bytearray() 948 | 949 | def decodeComPayload(self, byteArray): 950 | """Decode the Command Payload.""" 951 | return {} 952 | 953 | def encodeRespPayload(self): 954 | """Encode Response Payload.""" 955 | return bytearray() 956 | 957 | def decodeRespPayload(self, byteArray): 958 | """Decode Response Payload.""" 959 | return {} 960 | 961 | 962 | class Type195Command(Packet): 963 | """Type 195 Command.""" 964 | 965 | NAME = "Type 195" 966 | TYPE = Packet.MESSAGE_TYPE_195 967 | 968 | def __init__(self, mode, byteArray=None): 969 | """Initialise Type 195 Command Packet.""" 970 | super().__init__(mode) 971 | self.payload = {} 972 | self.mode = mode 973 | if byteArray is not None: 974 | self.byteArray = byteArray 975 | self.header = super().decodeHeader(mode, byteArray) 976 | self.valid = self.validatePacket(byteArray, self.header["packetLength"]) 977 | if mode == self.MESSAGE_MODE_COMMAND: 978 | self.decodedCommandPayload = self.decodeComPayload(byteArray) 979 | elif mode == self.MESSAGE_MODE_RESPONSE: 980 | self.decodedCommandPayload = self.decodeRespPayload(byteArray) 981 | else: 982 | self.mode = mode 983 | 984 | def encodeComPayload(self): 985 | """Encode Command Payload.""" 986 | return bytearray() 987 | 988 | def decodeComPayload(self, byteArray): 989 | """Decode the Command Payload.""" 990 | return {} 991 | 992 | def encodeRespPayload(self): 993 | """Encode Response Payload.""" 994 | return bytearray() 995 | 996 | def decodeRespPayload(self, byteArray): 997 | """Decode Response Payload.""" 998 | return {} 999 | 1000 | 1001 | class LockStateCommand(Packet): 1002 | """LockState Command.""" 1003 | 1004 | NAME = "Lock State" 1005 | TYPE = Packet.MESSAGE_TYPE_SET_LOCK_STATE 1006 | 1007 | def __init__(self, mode, byteArray=None, unknownFourByteInt=None): 1008 | """Initialise Lock State Command Packet.""" 1009 | super().__init__(mode) 1010 | self.payload = {} 1011 | self.mode = mode 1012 | if byteArray is not None: 1013 | self.byteArray = byteArray 1014 | self.header = super().decodeHeader(mode, byteArray) 1015 | self.valid = self.validatePacket(byteArray, self.header["packetLength"]) 1016 | if mode == self.MESSAGE_MODE_COMMAND: 1017 | self.decodedCommandPayload = self.decodeComPayload(byteArray) 1018 | elif mode == self.MESSAGE_MODE_RESPONSE: 1019 | self.decodedCommandPayload = self.decodeRespPayload(byteArray) 1020 | else: 1021 | self.mode = mode 1022 | self.unknownFourByteInt = unknownFourByteInt 1023 | 1024 | def encodeComPayload(self): 1025 | """Encode Command Payload.""" 1026 | return bytearray() 1027 | 1028 | def decodeComPayload(self, byteArray): 1029 | """Decode the Command Payload.""" 1030 | return {} 1031 | 1032 | def encodeRespPayload(self): 1033 | """Encode Response Payload.""" 1034 | payload = bytearray(0) 1035 | payload = payload + self.encodeFourByteInt(self.unknownFourByteInt) 1036 | return payload 1037 | 1038 | def decodeRespPayload(self, byteArray): 1039 | """Decode Response Payload.""" 1040 | self.unknownFourByteInt = self.getFourByteInt(16, byteArray) 1041 | self.payload = {"unknownFourByteInt": self.unknownFourByteInt} 1042 | return self.payload 1043 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "arrow" 5 | version = "1.2.1" 6 | description = "Better dates & times for Python" 7 | category = "dev" 8 | optional = false 9 | python-versions = ">=3.6" 10 | files = [ 11 | {file = "arrow-1.2.1-py3-none-any.whl", hash = "sha256:6b2914ef3997d1fd7b37a71ce9dd61a6e329d09e1c7b44f4d3099ca4a5c0933e"}, 12 | {file = "arrow-1.2.1.tar.gz", hash = "sha256:c2dde3c382d9f7e6922ce636bf0b318a7a853df40ecb383b29192e6c5cc82840"}, 13 | ] 14 | 15 | [package.dependencies] 16 | python-dateutil = ">=2.7.0" 17 | 18 | [[package]] 19 | name = "attrs" 20 | version = "22.2.0" 21 | description = "Classes Without Boilerplate" 22 | category = "main" 23 | optional = false 24 | python-versions = ">=3.6" 25 | files = [ 26 | {file = "attrs-22.2.0-py3-none-any.whl", hash = "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836"}, 27 | {file = "attrs-22.2.0.tar.gz", hash = "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"}, 28 | ] 29 | 30 | [package.extras] 31 | cov = ["attrs[tests]", "coverage-enable-subprocess", "coverage[toml] (>=5.3)"] 32 | dev = ["attrs[docs,tests]"] 33 | docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope.interface"] 34 | tests = ["attrs[tests-no-zope]", "zope.interface"] 35 | tests-no-zope = ["cloudpickle", "cloudpickle", "hypothesis", "hypothesis", "mypy (>=0.971,<0.990)", "mypy (>=0.971,<0.990)", "pympler", "pympler", "pytest (>=4.3.0)", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-mypy-plugins", "pytest-xdist[psutil]", "pytest-xdist[psutil]"] 36 | 37 | [[package]] 38 | name = "certifi" 39 | version = "2022.12.7" 40 | description = "Python package for providing Mozilla's CA Bundle." 41 | category = "dev" 42 | optional = false 43 | python-versions = ">=3.6" 44 | files = [ 45 | {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, 46 | {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, 47 | ] 48 | 49 | [[package]] 50 | name = "cfgv" 51 | version = "3.3.1" 52 | description = "Validate configuration and produce human readable error messages." 53 | category = "dev" 54 | optional = false 55 | python-versions = ">=3.6.1" 56 | files = [ 57 | {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, 58 | {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, 59 | ] 60 | 61 | [[package]] 62 | name = "charset-normalizer" 63 | version = "2.1.1" 64 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 65 | category = "dev" 66 | optional = false 67 | python-versions = ">=3.6.0" 68 | files = [ 69 | {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"}, 70 | {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"}, 71 | ] 72 | 73 | [package.extras] 74 | unicode-backport = ["unicodedata2"] 75 | 76 | [[package]] 77 | name = "click" 78 | version = "8.0.3" 79 | description = "Composable command line interface toolkit" 80 | category = "dev" 81 | optional = false 82 | python-versions = ">=3.6" 83 | files = [ 84 | {file = "click-8.0.3-py3-none-any.whl", hash = "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3"}, 85 | {file = "click-8.0.3.tar.gz", hash = "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b"}, 86 | ] 87 | 88 | [package.dependencies] 89 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 90 | 91 | [[package]] 92 | name = "colorama" 93 | version = "0.4.6" 94 | description = "Cross-platform colored terminal text." 95 | category = "main" 96 | optional = false 97 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 98 | files = [ 99 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 100 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 101 | ] 102 | 103 | [[package]] 104 | name = "coverage" 105 | version = "6.5.0" 106 | description = "Code coverage measurement for Python" 107 | category = "main" 108 | optional = false 109 | python-versions = ">=3.7" 110 | files = [ 111 | {file = "coverage-6.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef8674b0ee8cc11e2d574e3e2998aea5df5ab242e012286824ea3c6970580e53"}, 112 | {file = "coverage-6.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:784f53ebc9f3fd0e2a3f6a78b2be1bd1f5575d7863e10c6e12504f240fd06660"}, 113 | {file = "coverage-6.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4a5be1748d538a710f87542f22c2cad22f80545a847ad91ce45e77417293eb4"}, 114 | {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83516205e254a0cb77d2d7bb3632ee019d93d9f4005de31dca0a8c3667d5bc04"}, 115 | {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af4fffaffc4067232253715065e30c5a7ec6faac36f8fc8d6f64263b15f74db0"}, 116 | {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:97117225cdd992a9c2a5515db1f66b59db634f59d0679ca1fa3fe8da32749cae"}, 117 | {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a1170fa54185845505fbfa672f1c1ab175446c887cce8212c44149581cf2d466"}, 118 | {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:11b990d520ea75e7ee8dcab5bc908072aaada194a794db9f6d7d5cfd19661e5a"}, 119 | {file = "coverage-6.5.0-cp310-cp310-win32.whl", hash = "sha256:5dbec3b9095749390c09ab7c89d314727f18800060d8d24e87f01fb9cfb40b32"}, 120 | {file = "coverage-6.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:59f53f1dc5b656cafb1badd0feb428c1e7bc19b867479ff72f7a9dd9b479f10e"}, 121 | {file = "coverage-6.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4a5375e28c5191ac38cca59b38edd33ef4cc914732c916f2929029b4bfb50795"}, 122 | {file = "coverage-6.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4ed2820d919351f4167e52425e096af41bfabacb1857186c1ea32ff9983ed75"}, 123 | {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33a7da4376d5977fbf0a8ed91c4dffaaa8dbf0ddbf4c8eea500a2486d8bc4d7b"}, 124 | {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8fb6cf131ac4070c9c5a3e21de0f7dc5a0fbe8bc77c9456ced896c12fcdad91"}, 125 | {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a6b7d95969b8845250586f269e81e5dfdd8ff828ddeb8567a4a2eaa7313460c4"}, 126 | {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1ef221513e6f68b69ee9e159506d583d31aa3567e0ae84eaad9d6ec1107dddaa"}, 127 | {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cca4435eebea7962a52bdb216dec27215d0df64cf27fc1dd538415f5d2b9da6b"}, 128 | {file = "coverage-6.5.0-cp311-cp311-win32.whl", hash = "sha256:98e8a10b7a314f454d9eff4216a9a94d143a7ee65018dd12442e898ee2310578"}, 129 | {file = "coverage-6.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:bc8ef5e043a2af066fa8cbfc6e708d58017024dc4345a1f9757b329a249f041b"}, 130 | {file = "coverage-6.5.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4433b90fae13f86fafff0b326453dd42fc9a639a0d9e4eec4d366436d1a41b6d"}, 131 | {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4f05d88d9a80ad3cac6244d36dd89a3c00abc16371769f1340101d3cb899fc3"}, 132 | {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94e2565443291bd778421856bc975d351738963071e9b8839ca1fc08b42d4bef"}, 133 | {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:027018943386e7b942fa832372ebc120155fd970837489896099f5cfa2890f79"}, 134 | {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:255758a1e3b61db372ec2736c8e2a1fdfaf563977eedbdf131de003ca5779b7d"}, 135 | {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:851cf4ff24062c6aec510a454b2584f6e998cada52d4cb58c5e233d07172e50c"}, 136 | {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:12adf310e4aafddc58afdb04d686795f33f4d7a6fa67a7a9d4ce7d6ae24d949f"}, 137 | {file = "coverage-6.5.0-cp37-cp37m-win32.whl", hash = "sha256:b5604380f3415ba69de87a289a2b56687faa4fe04dbee0754bfcae433489316b"}, 138 | {file = "coverage-6.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4a8dbc1f0fbb2ae3de73eb0bdbb914180c7abfbf258e90b311dcd4f585d44bd2"}, 139 | {file = "coverage-6.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d900bb429fdfd7f511f868cedd03a6bbb142f3f9118c09b99ef8dc9bf9643c3c"}, 140 | {file = "coverage-6.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2198ea6fc548de52adc826f62cb18554caedfb1d26548c1b7c88d8f7faa8f6ba"}, 141 | {file = "coverage-6.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c4459b3de97b75e3bd6b7d4b7f0db13f17f504f3d13e2a7c623786289dd670e"}, 142 | {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:20c8ac5386253717e5ccc827caad43ed66fea0efe255727b1053a8154d952398"}, 143 | {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b07130585d54fe8dff3d97b93b0e20290de974dc8177c320aeaf23459219c0b"}, 144 | {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:dbdb91cd8c048c2b09eb17713b0c12a54fbd587d79adcebad543bc0cd9a3410b"}, 145 | {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:de3001a203182842a4630e7b8d1a2c7c07ec1b45d3084a83d5d227a3806f530f"}, 146 | {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e07f4a4a9b41583d6eabec04f8b68076ab3cd44c20bd29332c6572dda36f372e"}, 147 | {file = "coverage-6.5.0-cp38-cp38-win32.whl", hash = "sha256:6d4817234349a80dbf03640cec6109cd90cba068330703fa65ddf56b60223a6d"}, 148 | {file = "coverage-6.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:7ccf362abd726b0410bf8911c31fbf97f09f8f1061f8c1cf03dfc4b6372848f6"}, 149 | {file = "coverage-6.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:633713d70ad6bfc49b34ead4060531658dc6dfc9b3eb7d8a716d5873377ab745"}, 150 | {file = "coverage-6.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:95203854f974e07af96358c0b261f1048d8e1083f2de9b1c565e1be4a3a48cfc"}, 151 | {file = "coverage-6.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9023e237f4c02ff739581ef35969c3739445fb059b060ca51771e69101efffe"}, 152 | {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:265de0fa6778d07de30bcf4d9dc471c3dc4314a23a3c6603d356a3c9abc2dfcf"}, 153 | {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f830ed581b45b82451a40faabb89c84e1a998124ee4212d440e9c6cf70083e5"}, 154 | {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7b6be138d61e458e18d8e6ddcddd36dd96215edfe5f1168de0b1b32635839b62"}, 155 | {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:42eafe6778551cf006a7c43153af1211c3aaab658d4d66fa5fcc021613d02518"}, 156 | {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:723e8130d4ecc8f56e9a611e73b31219595baa3bb252d539206f7bbbab6ffc1f"}, 157 | {file = "coverage-6.5.0-cp39-cp39-win32.whl", hash = "sha256:d9ecf0829c6a62b9b573c7bb6d4dcd6ba8b6f80be9ba4fc7ed50bf4ac9aecd72"}, 158 | {file = "coverage-6.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:fc2af30ed0d5ae0b1abdb4ebdce598eafd5b35397d4d75deb341a614d333d987"}, 159 | {file = "coverage-6.5.0-pp36.pp37.pp38-none-any.whl", hash = "sha256:1431986dac3923c5945271f169f59c45b8802a114c8f548d611f2015133df77a"}, 160 | {file = "coverage-6.5.0.tar.gz", hash = "sha256:f642e90754ee3e06b0e7e51bce3379590e76b7f76b708e1a71ff043f87025c84"}, 161 | ] 162 | 163 | [package.dependencies] 164 | tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} 165 | 166 | [package.extras] 167 | toml = ["tomli"] 168 | 169 | [[package]] 170 | name = "coveralls" 171 | version = "3.3.1" 172 | description = "Show coverage stats online via coveralls.io" 173 | category = "dev" 174 | optional = false 175 | python-versions = ">= 3.5" 176 | files = [ 177 | {file = "coveralls-3.3.1-py2.py3-none-any.whl", hash = "sha256:f42015f31d386b351d4226389b387ae173207058832fbf5c8ec4b40e27b16026"}, 178 | {file = "coveralls-3.3.1.tar.gz", hash = "sha256:b32a8bb5d2df585207c119d6c01567b81fba690c9c10a753bfe27a335bfc43ea"}, 179 | ] 180 | 181 | [package.dependencies] 182 | coverage = ">=4.1,<6.0.0 || >6.1,<6.1.1 || >6.1.1,<7.0" 183 | docopt = ">=0.6.1" 184 | requests = ">=1.0.0" 185 | 186 | [package.extras] 187 | yaml = ["PyYAML (>=3.10)"] 188 | 189 | [[package]] 190 | name = "distlib" 191 | version = "0.3.6" 192 | description = "Distribution utilities" 193 | category = "dev" 194 | optional = false 195 | python-versions = "*" 196 | files = [ 197 | {file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"}, 198 | {file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"}, 199 | ] 200 | 201 | [[package]] 202 | name = "docopt" 203 | version = "0.6.2" 204 | description = "Pythonic argument parser, that will make you smile" 205 | category = "dev" 206 | optional = false 207 | python-versions = "*" 208 | files = [ 209 | {file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"}, 210 | ] 211 | 212 | [[package]] 213 | name = "exceptiongroup" 214 | version = "1.1.0" 215 | description = "Backport of PEP 654 (exception groups)" 216 | category = "main" 217 | optional = false 218 | python-versions = ">=3.7" 219 | files = [ 220 | {file = "exceptiongroup-1.1.0-py3-none-any.whl", hash = "sha256:327cbda3da756e2de031a3107b81ab7b3770a602c4d16ca618298c526f4bec1e"}, 221 | {file = "exceptiongroup-1.1.0.tar.gz", hash = "sha256:bcb67d800a4497e1b404c2dd44fca47d3b7a5e5433dbab67f96c1a685cdfdf23"}, 222 | ] 223 | 224 | [package.extras] 225 | test = ["pytest (>=6)"] 226 | 227 | [[package]] 228 | name = "filelock" 229 | version = "3.9.0" 230 | description = "A platform independent file lock." 231 | category = "dev" 232 | optional = false 233 | python-versions = ">=3.7" 234 | files = [ 235 | {file = "filelock-3.9.0-py3-none-any.whl", hash = "sha256:f58d535af89bb9ad5cd4df046f741f8553a418c01a7856bf0d173bbc9f6bd16d"}, 236 | {file = "filelock-3.9.0.tar.gz", hash = "sha256:7b319f24340b51f55a2bf7a12ac0755a9b03e718311dac567a0f4f7fabd2f5de"}, 237 | ] 238 | 239 | [package.extras] 240 | docs = ["furo (>=2022.12.7)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"] 241 | testing = ["covdefaults (>=2.2.2)", "coverage (>=7.0.1)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-timeout (>=2.1)"] 242 | 243 | [[package]] 244 | name = "gitlint" 245 | version = "0.17.0" 246 | description = "Git commit message linter written in python, checks your commit messages for style." 247 | category = "dev" 248 | optional = false 249 | python-versions = ">=3.6" 250 | files = [ 251 | {file = "gitlint-0.17.0-py2.py3-none-any.whl", hash = "sha256:46469d5db3f3bca72fa946c159d0733dc8c75211309477676295cf2d80d177b4"}, 252 | {file = "gitlint-0.17.0.tar.gz", hash = "sha256:8c10c6b404d255b43ddc4a2f5f13bcb10284bc162adfb2c03b10708309009189"}, 253 | ] 254 | 255 | [package.dependencies] 256 | gitlint-core = {version = "0.17.0", extras = ["trusted-deps"]} 257 | 258 | [[package]] 259 | name = "gitlint-core" 260 | version = "0.17.0" 261 | description = "Git commit message linter written in python, checks your commit messages for style." 262 | category = "dev" 263 | optional = false 264 | python-versions = ">=3.6" 265 | files = [ 266 | {file = "gitlint-core-0.17.0.tar.gz", hash = "sha256:772dfd33effaa8515ca73e901466aa938c19ced894bec6783d19691f57429691"}, 267 | {file = "gitlint_core-0.17.0-py2.py3-none-any.whl", hash = "sha256:cb99ccd736a698b910385211203bda94bf4ce29086d0c08f8f58a18c40a98377"}, 268 | ] 269 | 270 | [package.dependencies] 271 | arrow = [ 272 | {version = ">=1"}, 273 | {version = "1.2.1", optional = true, markers = "extra == \"trusted-deps\""}, 274 | ] 275 | Click = [ 276 | {version = ">=8"}, 277 | {version = "8.0.3", optional = true, markers = "extra == \"trusted-deps\""}, 278 | ] 279 | sh = [ 280 | {version = ">=1.13.0", markers = "sys_platform != \"win32\""}, 281 | {version = "1.14.2", optional = true, markers = "sys_platform != \"win32\""}, 282 | ] 283 | 284 | [package.extras] 285 | trusted-deps = ["Click (==8.0.3)", "arrow (==1.2.1)", "sh (==1.14.2)"] 286 | 287 | [[package]] 288 | name = "identify" 289 | version = "2.5.11" 290 | description = "File identification library for Python" 291 | category = "dev" 292 | optional = false 293 | python-versions = ">=3.7" 294 | files = [ 295 | {file = "identify-2.5.11-py2.py3-none-any.whl", hash = "sha256:e7db36b772b188099616aaf2accbee122949d1c6a1bac4f38196720d6f9f06db"}, 296 | {file = "identify-2.5.11.tar.gz", hash = "sha256:14b7076b29c99b1b0b8b08e96d448c7b877a9b07683cd8cfda2ea06af85ffa1c"}, 297 | ] 298 | 299 | [package.extras] 300 | license = ["ukkonen"] 301 | 302 | [[package]] 303 | name = "idna" 304 | version = "3.4" 305 | description = "Internationalized Domain Names in Applications (IDNA)" 306 | category = "dev" 307 | optional = false 308 | python-versions = ">=3.5" 309 | files = [ 310 | {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, 311 | {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, 312 | ] 313 | 314 | [[package]] 315 | name = "iniconfig" 316 | version = "1.1.1" 317 | description = "iniconfig: brain-dead simple config-ini parsing" 318 | category = "main" 319 | optional = false 320 | python-versions = "*" 321 | files = [ 322 | {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, 323 | {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, 324 | ] 325 | 326 | [[package]] 327 | name = "loguru" 328 | version = "0.6.0" 329 | description = "Python logging made (stupidly) simple" 330 | category = "main" 331 | optional = false 332 | python-versions = ">=3.5" 333 | files = [ 334 | {file = "loguru-0.6.0-py3-none-any.whl", hash = "sha256:4e2414d534a2ab57573365b3e6d0234dfb1d84b68b7f3b948e6fb743860a77c3"}, 335 | {file = "loguru-0.6.0.tar.gz", hash = "sha256:066bd06758d0a513e9836fd9c6b5a75bfb3fd36841f4b996bc60b547a309d41c"}, 336 | ] 337 | 338 | [package.dependencies] 339 | colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""} 340 | win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""} 341 | 342 | [package.extras] 343 | dev = ["Sphinx (>=4.1.1)", "black (>=19.10b0)", "colorama (>=0.3.4)", "docutils (==0.16)", "flake8 (>=3.7.7)", "isort (>=5.1.1)", "pytest (>=4.6.2)", "pytest-cov (>=2.7.1)", "sphinx-autobuild (>=0.7.1)", "sphinx-rtd-theme (>=0.4.3)", "tox (>=3.9.0)"] 344 | 345 | [[package]] 346 | name = "nodeenv" 347 | version = "1.7.0" 348 | description = "Node.js virtual environment builder" 349 | category = "dev" 350 | optional = false 351 | python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" 352 | files = [ 353 | {file = "nodeenv-1.7.0-py2.py3-none-any.whl", hash = "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e"}, 354 | {file = "nodeenv-1.7.0.tar.gz", hash = "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b"}, 355 | ] 356 | 357 | [package.dependencies] 358 | setuptools = "*" 359 | 360 | [[package]] 361 | name = "packaging" 362 | version = "22.0" 363 | description = "Core utilities for Python packages" 364 | category = "main" 365 | optional = false 366 | python-versions = ">=3.7" 367 | files = [ 368 | {file = "packaging-22.0-py3-none-any.whl", hash = "sha256:957e2148ba0e1a3b282772e791ef1d8083648bc131c8ab0c1feba110ce1146c3"}, 369 | {file = "packaging-22.0.tar.gz", hash = "sha256:2198ec20bd4c017b8f9717e00f0c8714076fc2fd93816750ab48e2c41de2cfd3"}, 370 | ] 371 | 372 | [[package]] 373 | name = "pillow" 374 | version = "9.3.0" 375 | description = "Python Imaging Library (Fork)" 376 | category = "main" 377 | optional = false 378 | python-versions = ">=3.7" 379 | files = [ 380 | {file = "Pillow-9.3.0-1-cp37-cp37m-win32.whl", hash = "sha256:e6ea6b856a74d560d9326c0f5895ef8050126acfdc7ca08ad703eb0081e82b74"}, 381 | {file = "Pillow-9.3.0-1-cp37-cp37m-win_amd64.whl", hash = "sha256:32a44128c4bdca7f31de5be641187367fe2a450ad83b833ef78910397db491aa"}, 382 | {file = "Pillow-9.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:0b7257127d646ff8676ec8a15520013a698d1fdc48bc2a79ba4e53df792526f2"}, 383 | {file = "Pillow-9.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b90f7616ea170e92820775ed47e136208e04c967271c9ef615b6fbd08d9af0e3"}, 384 | {file = "Pillow-9.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68943d632f1f9e3dce98908e873b3a090f6cba1cbb1b892a9e8d97c938871fbe"}, 385 | {file = "Pillow-9.3.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:be55f8457cd1eac957af0c3f5ece7bc3f033f89b114ef30f710882717670b2a8"}, 386 | {file = "Pillow-9.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d77adcd56a42d00cc1be30843d3426aa4e660cab4a61021dc84467123f7a00c"}, 387 | {file = "Pillow-9.3.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:829f97c8e258593b9daa80638aee3789b7df9da5cf1336035016d76f03b8860c"}, 388 | {file = "Pillow-9.3.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:801ec82e4188e935c7f5e22e006d01611d6b41661bba9fe45b60e7ac1a8f84de"}, 389 | {file = "Pillow-9.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:871b72c3643e516db4ecf20efe735deb27fe30ca17800e661d769faab45a18d7"}, 390 | {file = "Pillow-9.3.0-cp310-cp310-win32.whl", hash = "sha256:655a83b0058ba47c7c52e4e2df5ecf484c1b0b0349805896dd350cbc416bdd91"}, 391 | {file = "Pillow-9.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:9f47eabcd2ded7698106b05c2c338672d16a6f2a485e74481f524e2a23c2794b"}, 392 | {file = "Pillow-9.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:57751894f6618fd4308ed8e0c36c333e2f5469744c34729a27532b3db106ee20"}, 393 | {file = "Pillow-9.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7db8b751ad307d7cf238f02101e8e36a128a6cb199326e867d1398067381bff4"}, 394 | {file = "Pillow-9.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3033fbe1feb1b59394615a1cafaee85e49d01b51d54de0cbf6aa8e64182518a1"}, 395 | {file = "Pillow-9.3.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22b012ea2d065fd163ca096f4e37e47cd8b59cf4b0fd47bfca6abb93df70b34c"}, 396 | {file = "Pillow-9.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9a65733d103311331875c1dca05cb4606997fd33d6acfed695b1232ba1df193"}, 397 | {file = "Pillow-9.3.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:502526a2cbfa431d9fc2a079bdd9061a2397b842bb6bc4239bb176da00993812"}, 398 | {file = "Pillow-9.3.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:90fb88843d3902fe7c9586d439d1e8c05258f41da473952aa8b328d8b907498c"}, 399 | {file = "Pillow-9.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:89dca0ce00a2b49024df6325925555d406b14aa3efc2f752dbb5940c52c56b11"}, 400 | {file = "Pillow-9.3.0-cp311-cp311-win32.whl", hash = "sha256:3168434d303babf495d4ba58fc22d6604f6e2afb97adc6a423e917dab828939c"}, 401 | {file = "Pillow-9.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:18498994b29e1cf86d505edcb7edbe814d133d2232d256db8c7a8ceb34d18cef"}, 402 | {file = "Pillow-9.3.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:772a91fc0e03eaf922c63badeca75e91baa80fe2f5f87bdaed4280662aad25c9"}, 403 | {file = "Pillow-9.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa4107d1b306cdf8953edde0534562607fe8811b6c4d9a486298ad31de733b2"}, 404 | {file = "Pillow-9.3.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b4012d06c846dc2b80651b120e2cdd787b013deb39c09f407727ba90015c684f"}, 405 | {file = "Pillow-9.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77ec3e7be99629898c9a6d24a09de089fa5356ee408cdffffe62d67bb75fdd72"}, 406 | {file = "Pillow-9.3.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:6c738585d7a9961d8c2821a1eb3dcb978d14e238be3d70f0a706f7fa9316946b"}, 407 | {file = "Pillow-9.3.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:828989c45c245518065a110434246c44a56a8b2b2f6347d1409c787e6e4651ee"}, 408 | {file = "Pillow-9.3.0-cp37-cp37m-win32.whl", hash = "sha256:82409ffe29d70fd733ff3c1025a602abb3e67405d41b9403b00b01debc4c9a29"}, 409 | {file = "Pillow-9.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:41e0051336807468be450d52b8edd12ac60bebaa97fe10c8b660f116e50b30e4"}, 410 | {file = "Pillow-9.3.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:b03ae6f1a1878233ac620c98f3459f79fd77c7e3c2b20d460284e1fb370557d4"}, 411 | {file = "Pillow-9.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4390e9ce199fc1951fcfa65795f239a8a4944117b5935a9317fb320e7767b40f"}, 412 | {file = "Pillow-9.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40e1ce476a7804b0fb74bcfa80b0a2206ea6a882938eaba917f7a0f004b42502"}, 413 | {file = "Pillow-9.3.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a0a06a052c5f37b4ed81c613a455a81f9a3a69429b4fd7bb913c3fa98abefc20"}, 414 | {file = "Pillow-9.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03150abd92771742d4a8cd6f2fa6246d847dcd2e332a18d0c15cc75bf6703040"}, 415 | {file = "Pillow-9.3.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:15c42fb9dea42465dfd902fb0ecf584b8848ceb28b41ee2b58f866411be33f07"}, 416 | {file = "Pillow-9.3.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:51e0e543a33ed92db9f5ef69a0356e0b1a7a6b6a71b80df99f1d181ae5875636"}, 417 | {file = "Pillow-9.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:3dd6caf940756101205dffc5367babf288a30043d35f80936f9bfb37f8355b32"}, 418 | {file = "Pillow-9.3.0-cp38-cp38-win32.whl", hash = "sha256:f1ff2ee69f10f13a9596480335f406dd1f70c3650349e2be67ca3139280cade0"}, 419 | {file = "Pillow-9.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:276a5ca930c913f714e372b2591a22c4bd3b81a418c0f6635ba832daec1cbcfc"}, 420 | {file = "Pillow-9.3.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:73bd195e43f3fadecfc50c682f5055ec32ee2c933243cafbfdec69ab1aa87cad"}, 421 | {file = "Pillow-9.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1c7c8ae3864846fc95f4611c78129301e203aaa2af813b703c55d10cc1628535"}, 422 | {file = "Pillow-9.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e0918e03aa0c72ea56edbb00d4d664294815aa11291a11504a377ea018330d3"}, 423 | {file = "Pillow-9.3.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0915e734b33a474d76c28e07292f196cdf2a590a0d25bcc06e64e545f2d146c"}, 424 | {file = "Pillow-9.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af0372acb5d3598f36ec0914deed2a63f6bcdb7b606da04dc19a88d31bf0c05b"}, 425 | {file = "Pillow-9.3.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:ad58d27a5b0262c0c19b47d54c5802db9b34d38bbf886665b626aff83c74bacd"}, 426 | {file = "Pillow-9.3.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:97aabc5c50312afa5e0a2b07c17d4ac5e865b250986f8afe2b02d772567a380c"}, 427 | {file = "Pillow-9.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9aaa107275d8527e9d6e7670b64aabaaa36e5b6bd71a1015ddd21da0d4e06448"}, 428 | {file = "Pillow-9.3.0-cp39-cp39-win32.whl", hash = "sha256:bac18ab8d2d1e6b4ce25e3424f709aceef668347db8637c2296bcf41acb7cf48"}, 429 | {file = "Pillow-9.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:b472b5ea442148d1c3e2209f20f1e0bb0eb556538690fa70b5e1f79fa0ba8dc2"}, 430 | {file = "Pillow-9.3.0-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:ab388aaa3f6ce52ac1cb8e122c4bd46657c15905904b3120a6248b5b8b0bc228"}, 431 | {file = "Pillow-9.3.0-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbb8e7f2abee51cef77673be97760abff1674ed32847ce04b4af90f610144c7b"}, 432 | {file = "Pillow-9.3.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bca31dd6014cb8b0b2db1e46081b0ca7d936f856da3b39744aef499db5d84d02"}, 433 | {file = "Pillow-9.3.0-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c7025dce65566eb6e89f56c9509d4f628fddcedb131d9465cacd3d8bac337e7e"}, 434 | {file = "Pillow-9.3.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:ebf2029c1f464c59b8bdbe5143c79fa2045a581ac53679733d3a91d400ff9efb"}, 435 | {file = "Pillow-9.3.0-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b59430236b8e58840a0dfb4099a0e8717ffb779c952426a69ae435ca1f57210c"}, 436 | {file = "Pillow-9.3.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:12ce4932caf2ddf3e41d17fc9c02d67126935a44b86df6a206cf0d7161548627"}, 437 | {file = "Pillow-9.3.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae5331c23ce118c53b172fa64a4c037eb83c9165aba3a7ba9ddd3ec9fa64a699"}, 438 | {file = "Pillow-9.3.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:0b07fffc13f474264c336298d1b4ce01d9c5a011415b79d4ee5527bb69ae6f65"}, 439 | {file = "Pillow-9.3.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:073adb2ae23431d3b9bcbcff3fe698b62ed47211d0716b067385538a1b0f28b8"}, 440 | {file = "Pillow-9.3.0.tar.gz", hash = "sha256:c935a22a557a560108d780f9a0fc426dd7459940dc54faa49d83249c8d3e760f"}, 441 | ] 442 | 443 | [package.extras] 444 | docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-issues (>=3.0.1)", "sphinx-removed-in", "sphinxext-opengraph"] 445 | tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] 446 | 447 | [[package]] 448 | name = "platformdirs" 449 | version = "2.6.2" 450 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 451 | category = "dev" 452 | optional = false 453 | python-versions = ">=3.7" 454 | files = [ 455 | {file = "platformdirs-2.6.2-py3-none-any.whl", hash = "sha256:83c8f6d04389165de7c9b6f0c682439697887bca0aa2f1c87ef1826be3584490"}, 456 | {file = "platformdirs-2.6.2.tar.gz", hash = "sha256:e1fea1fe471b9ff8332e229df3cb7de4f53eeea4998d3b6bfff542115e998bd2"}, 457 | ] 458 | 459 | [package.extras] 460 | docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"] 461 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] 462 | 463 | [[package]] 464 | name = "pluggy" 465 | version = "1.0.0" 466 | description = "plugin and hook calling mechanisms for python" 467 | category = "main" 468 | optional = false 469 | python-versions = ">=3.6" 470 | files = [ 471 | {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, 472 | {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, 473 | ] 474 | 475 | [package.extras] 476 | dev = ["pre-commit", "tox"] 477 | testing = ["pytest", "pytest-benchmark"] 478 | 479 | [[package]] 480 | name = "pre-commit" 481 | version = "2.21.0" 482 | description = "A framework for managing and maintaining multi-language pre-commit hooks." 483 | category = "dev" 484 | optional = false 485 | python-versions = ">=3.7" 486 | files = [ 487 | {file = "pre_commit-2.21.0-py2.py3-none-any.whl", hash = "sha256:e2f91727039fc39a92f58a588a25b87f936de6567eed4f0e673e0507edc75bad"}, 488 | {file = "pre_commit-2.21.0.tar.gz", hash = "sha256:31ef31af7e474a8d8995027fefdfcf509b5c913ff31f2015b4ec4beb26a6f658"}, 489 | ] 490 | 491 | [package.dependencies] 492 | cfgv = ">=2.0.0" 493 | identify = ">=1.0.0" 494 | nodeenv = ">=0.11.1" 495 | pyyaml = ">=5.1" 496 | virtualenv = ">=20.10.0" 497 | 498 | [[package]] 499 | name = "pytest" 500 | version = "7.2.0" 501 | description = "pytest: simple powerful testing with Python" 502 | category = "main" 503 | optional = false 504 | python-versions = ">=3.7" 505 | files = [ 506 | {file = "pytest-7.2.0-py3-none-any.whl", hash = "sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71"}, 507 | {file = "pytest-7.2.0.tar.gz", hash = "sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59"}, 508 | ] 509 | 510 | [package.dependencies] 511 | attrs = ">=19.2.0" 512 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 513 | exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} 514 | iniconfig = "*" 515 | packaging = "*" 516 | pluggy = ">=0.12,<2.0" 517 | tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} 518 | 519 | [package.extras] 520 | testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] 521 | 522 | [[package]] 523 | name = "pytest-cov" 524 | version = "4.0.0" 525 | description = "Pytest plugin for measuring coverage." 526 | category = "main" 527 | optional = false 528 | python-versions = ">=3.6" 529 | files = [ 530 | {file = "pytest-cov-4.0.0.tar.gz", hash = "sha256:996b79efde6433cdbd0088872dbc5fb3ed7fe1578b68cdbba634f14bb8dd0470"}, 531 | {file = "pytest_cov-4.0.0-py3-none-any.whl", hash = "sha256:2feb1b751d66a8bd934e5edfa2e961d11309dc37b73b0eabe73b5945fee20f6b"}, 532 | ] 533 | 534 | [package.dependencies] 535 | coverage = {version = ">=5.2.1", extras = ["toml"]} 536 | pytest = ">=4.6" 537 | 538 | [package.extras] 539 | testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] 540 | 541 | [[package]] 542 | name = "python-dateutil" 543 | version = "2.8.2" 544 | description = "Extensions to the standard Python datetime module" 545 | category = "dev" 546 | optional = false 547 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" 548 | files = [ 549 | {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, 550 | {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, 551 | ] 552 | 553 | [package.dependencies] 554 | six = ">=1.5" 555 | 556 | [[package]] 557 | name = "pyyaml" 558 | version = "6.0" 559 | description = "YAML parser and emitter for Python" 560 | category = "dev" 561 | optional = false 562 | python-versions = ">=3.6" 563 | files = [ 564 | {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, 565 | {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, 566 | {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, 567 | {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, 568 | {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, 569 | {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, 570 | {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, 571 | {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"}, 572 | {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"}, 573 | {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"}, 574 | {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"}, 575 | {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"}, 576 | {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"}, 577 | {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"}, 578 | {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, 579 | {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, 580 | {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, 581 | {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, 582 | {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, 583 | {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, 584 | {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, 585 | {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, 586 | {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, 587 | {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, 588 | {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, 589 | {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, 590 | {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, 591 | {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, 592 | {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, 593 | {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, 594 | {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, 595 | {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, 596 | {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, 597 | {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, 598 | {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, 599 | {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, 600 | {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, 601 | {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, 602 | {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, 603 | {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, 604 | ] 605 | 606 | [[package]] 607 | name = "requests" 608 | version = "2.28.1" 609 | description = "Python HTTP for Humans." 610 | category = "dev" 611 | optional = false 612 | python-versions = ">=3.7, <4" 613 | files = [ 614 | {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"}, 615 | {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"}, 616 | ] 617 | 618 | [package.dependencies] 619 | certifi = ">=2017.4.17" 620 | charset-normalizer = ">=2,<3" 621 | idna = ">=2.5,<4" 622 | urllib3 = ">=1.21.1,<1.27" 623 | 624 | [package.extras] 625 | socks = ["PySocks (>=1.5.6,!=1.5.7)"] 626 | use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] 627 | 628 | [[package]] 629 | name = "setuptools" 630 | version = "65.6.3" 631 | description = "Easily download, build, install, upgrade, and uninstall Python packages" 632 | category = "dev" 633 | optional = false 634 | python-versions = ">=3.7" 635 | files = [ 636 | {file = "setuptools-65.6.3-py3-none-any.whl", hash = "sha256:57f6f22bde4e042978bcd50176fdb381d7c21a9efa4041202288d3737a0c6a54"}, 637 | {file = "setuptools-65.6.3.tar.gz", hash = "sha256:a7620757bf984b58deaf32fc8a4577a9bbc0850cf92c20e1ce41c38c19e5fb75"}, 638 | ] 639 | 640 | [package.extras] 641 | docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] 642 | testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] 643 | testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] 644 | 645 | [[package]] 646 | name = "sh" 647 | version = "1.14.2" 648 | description = "Python subprocess replacement" 649 | category = "dev" 650 | optional = false 651 | python-versions = "*" 652 | files = [ 653 | {file = "sh-1.14.2-py2.py3-none-any.whl", hash = "sha256:4921ac9c1a77ec8084bdfaf152fe14138e2b3557cc740002c1a97076321fce8a"}, 654 | {file = "sh-1.14.2.tar.gz", hash = "sha256:9d7bd0334d494b2a4609fe521b2107438cdb21c0e469ffeeb191489883d6fe0d"}, 655 | ] 656 | 657 | [[package]] 658 | name = "six" 659 | version = "1.16.0" 660 | description = "Python 2 and 3 compatibility utilities" 661 | category = "dev" 662 | optional = false 663 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 664 | files = [ 665 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 666 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 667 | ] 668 | 669 | [[package]] 670 | name = "tomli" 671 | version = "2.0.1" 672 | description = "A lil' TOML parser" 673 | category = "main" 674 | optional = false 675 | python-versions = ">=3.7" 676 | files = [ 677 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 678 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 679 | ] 680 | 681 | [[package]] 682 | name = "urllib3" 683 | version = "1.26.13" 684 | description = "HTTP library with thread-safe connection pooling, file post, and more." 685 | category = "dev" 686 | optional = false 687 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" 688 | files = [ 689 | {file = "urllib3-1.26.13-py2.py3-none-any.whl", hash = "sha256:47cc05d99aaa09c9e72ed5809b60e7ba354e64b59c9c173ac3018642d8bb41fc"}, 690 | {file = "urllib3-1.26.13.tar.gz", hash = "sha256:c083dd0dce68dbfbe1129d5271cb90f9447dea7d52097c6e0126120c521ddea8"}, 691 | ] 692 | 693 | [package.extras] 694 | brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] 695 | secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] 696 | socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] 697 | 698 | [[package]] 699 | name = "virtualenv" 700 | version = "20.17.1" 701 | description = "Virtual Python Environment builder" 702 | category = "dev" 703 | optional = false 704 | python-versions = ">=3.6" 705 | files = [ 706 | {file = "virtualenv-20.17.1-py3-none-any.whl", hash = "sha256:ce3b1684d6e1a20a3e5ed36795a97dfc6af29bc3970ca8dab93e11ac6094b3c4"}, 707 | {file = "virtualenv-20.17.1.tar.gz", hash = "sha256:f8b927684efc6f1cc206c9db297a570ab9ad0e51c16fa9e45487d36d1905c058"}, 708 | ] 709 | 710 | [package.dependencies] 711 | distlib = ">=0.3.6,<1" 712 | filelock = ">=3.4.1,<4" 713 | platformdirs = ">=2.4,<3" 714 | 715 | [package.extras] 716 | docs = ["proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-argparse (>=0.3.2)", "sphinx-rtd-theme (>=1)", "towncrier (>=22.8)"] 717 | testing = ["coverage (>=6.2)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=21.3)", "pytest (>=7.0.1)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.6.1)", "pytest-randomly (>=3.10.3)", "pytest-timeout (>=2.1)"] 718 | 719 | [[package]] 720 | name = "win32-setctime" 721 | version = "1.1.0" 722 | description = "A small Python utility to set file creation time on Windows" 723 | category = "main" 724 | optional = false 725 | python-versions = ">=3.5" 726 | files = [ 727 | {file = "win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad"}, 728 | {file = "win32_setctime-1.1.0.tar.gz", hash = "sha256:15cf5750465118d6929ae4de4eb46e8edae9a5634350c01ba582df868e932cb2"}, 729 | ] 730 | 731 | [package.extras] 732 | dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"] 733 | 734 | [metadata] 735 | lock-version = "2.0" 736 | python-versions = "^3.10" 737 | content-hash = "d4252e54a7c0bb3c43cf86d713633e55b77cc579737645f7eb11f785c070e560" 738 | --------------------------------------------------------------------------------