diff --git a/.gitignore b/.gitignore index 495abed..9751c1c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,219 @@ -__pycache__/ -*.pyc -*.swp +### Generated by gibo (https://github.com/simonwhitaker/gibo) +### https://raw.github.com/github/gitignore/e5323759e387ba347a9d50f8b0ddd16502eb71d4/Python.gitignore + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + + +### https://raw.github.com/github/gitignore/e5323759e387ba347a9d50f8b0ddd16502eb71d4/Global/macOS.gitignore + +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + + +### https://raw.github.com/github/gitignore/e5323759e387ba347a9d50f8b0ddd16502eb71d4/Global/Vim.gitignore + +# Swap +[._]*.s[a-v][a-z] +!*.svg # comment out if you don't need vector files +[._]*.sw[a-p] +[._]s[a-rt-v][a-z] +[._]ss[a-gi-z] +[._]sw[a-p] + +# Session +Session.vim +Sessionx.vim + +# Temporary +.netrwhist +*~ +# Auto-generated tag files +tags +# Persistent undo +[._]*.un~ + diff --git a/__main__.py b/__main__.py index 6c65e1b..efb4182 100755 --- a/__main__.py +++ b/__main__.py @@ -1,90 +1,11 @@ #! /usr/bin/env python3 -from serial import Serial from argparse import ArgumentParser -from time import sleep - - -# w == escape -# | == carriage return -POWER_SAVE_ON = "w1PSAV|" -POWER_SAVE_OFF = "w0PSAV|" -POWER_SAVE = "wpsav|" -INPUT = lambda x: f"{x}!" - -EDIDS = { - "automatic": 0, - "1280x800": 14, - "720p60": 34, - "1080p60": 45, -} - -ERRORS = { - "E01": "Invalid input number", - "E06": "Invalid switch attempt in this mode", - "E10": "Invalid command", - "E11": "Invalid preset number", - "E12": "Invalid port number", - "E13": "Invalid parameter", - "E14": "Not valid for this configuration", - "E17": "Invalid command for signal type", - "E22": "Busy", -} +from projector import ProjectorSerial +from extron import ExtronSerial verbose = False -def send_command(command: str) -> str: - with Serial("/dev/serial/by-id/usb-Extron_Product-if00", 9600, timeout=1) as ser: - if verbose: - print("Send:", command) - - ser.write(command.encode()) - # response = ser.readline().decode().strip() - response = None - - while not (response := ser.readline()): - pass - response = response.decode().strip() - - if verbose: - print("Resp:", response) - if not response: - print("no reponse") - exit(1) - if response[0] == "E": - print(response, ERRORS.get(response, "Unknown")) - exit(1) - - return response - - -def send_command_projector(command: str) -> str: - with Serial("/dev/serial0", 9600, timeout=1) as ser: - ser.write(command.encode()) - response = ser.readline() - - if response: - print(response.decode()) - - -def check_power_save(): - resp = send_command(POWER_SAVE) - - return int(resp) - - -def sleep_extron(): - send_command(POWER_SAVE_ON) - - -def wake(): - send_command(POWER_SAVE_OFF) - - -def change_input(input: int): - send_command(INPUT(input)) - - if __name__ == "__main__": parser = ArgumentParser() @@ -105,16 +26,14 @@ if __name__ == "__main__": verbose = args.verbose - if args.input: - send_command_projector("\x02ADZZ;PON\x03") - wake() - change_input(args.input) + proj = ProjectorSerial() + extr = ExtronSerial() - if verbose: - print("Waking projector") - #send_command_projector("\x02ADZZ;PON\x03") + if args.input: + proj.power_on() + extr.wake() + extr.change_input(args.input) elif args.sleep: - send_command_projector("\x02ADZZ;POF\x03") - - sleep_extron() + proj.power_off() + extr.sleep() diff --git a/extron.py b/extron.py new file mode 100644 index 0000000..2466a3b --- /dev/null +++ b/extron.py @@ -0,0 +1,88 @@ +# w == escape +# | == carriage return +from types import LambdaType +from typing import Callable +from serialdevice import SerialDevice + +# \x1b == ESC +C: Callable[[str], str] = lambda command: f"\x1b{command}\r" + + +class ExtronSerial(SerialDevice): + ERRORS = { + "E01": "Invalid input number", + "E06": "Invalid switch attempt in this mode", + "E10": "Invalid command", + "E11": "Invalid preset number", + "E12": "Invalid port number", + "E13": "Invalid parameter", + "E14": "Not valid for this configuration", + "E17": "Invalid command for signal type", + "E22": "Busy", + } + + EDIDS = { + "automatic": 0, + "1280x800": 14, + "720p60": 34, + "1080p60": 45, + } + + # w == escape + # | == carriage return + + def __init__(self) -> None: + serial_port = "/dev/serial/by-id/usb-Extron_Product-if00" + baudrate = 9600 + super().__init__(serial_port, baudrate) + + def send_command(self, command: str, verbose: bool = False) -> str: + response = super().send_command(command, verbose) + + if response[0] == "E": + print(response, self.ERRORS.get(response, "Unknown")) + + return response + + def sleep(self) -> None: + self.send_command(C("1PSAV")) + + def wake(self) -> None: + self.send_command(C("0PSAV")) + + def change_input(self, input: int) -> None: + self.send_command(f"{input}!") + + def is_sleeping(self) -> bool: + response = self.send_command(C("PSAV")) + + return bool(int(response)) + + def volume_up(self) -> int: + volume = self.send_command("+V") + return int(volume[3:]) + + def volume_down(self) -> int: + volume = self.send_command("-V") + return int(volume[3:]) + + def current_volume(self) -> int: + return int(self.send_command("V")) + + def menu_toggle(self) -> None: + self.send_command(C("MMENU")) + + def menu_enter(self) -> None: + self.send_command(C("EMENU")) + + def menu_up(self) -> None: + self.send_command(C("UMENU")) + + def menu_down(self) -> None: + self.send_command(C("DMENU")) + + def menu_left(self) -> None: + self.send_command(C("LMENU")) + + def menu_right(self) -> None: + self.send_command(C("RMENU")) diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..b893c84 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,21 @@ +[[package]] +name = "pyserial" +version = "3.5" +description = "Python Serial Port Extension" +category = "main" +optional = false +python-versions = "*" + +[package.extras] +cp2110 = ["hidapi"] + +[metadata] +lock-version = "1.1" +python-versions = "^3.9" +content-hash = "8b78a1177d4e0e35d0b7629d05be94849e8563c39f4bb985609872502bc45f32" + +[metadata.files] +pyserial = [ + {file = "pyserial-3.5-py2.py3-none-any.whl", hash = "sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0"}, + {file = "pyserial-3.5.tar.gz", hash = "sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb"}, +] diff --git a/projector.py b/projector.py index 372e0ae..09f17cf 100644 --- a/projector.py +++ b/projector.py @@ -1,12 +1,25 @@ -from serial import Serial +from serialdevice import SerialDevice STX = chr(0x02) ETX = chr(0x03) -if __name__ == "__main__": - with Serial("/dev/ttyAMA0", 9600, timeout=1) as ser: - ser.write((STX + "ADZZ" + ";" + "PON" + ETX).encode()) - while not (response := ser.readline()): - pass - print(response) - + +class ProjectorSerial(SerialDevice): + def __init__(self) -> None: + serial_port = "/dev/serial0" + baudrate = 9600 + + super().__init__(serial_port, baudrate) + + def send_command(self, command: str, verbose: bool = False, device_id: str = "ZZ"): + assert device_id in ["01", "02", "03", "04", "05", "06", "ZZ"] + + full_command = f"\x02AD{device_id};{command}\x03" + + return super().send_command(full_command, verbose) + + def power_on(self): + self.send_command("PON") + + def power_off(self): + self.send_command("POF") diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..ac860b9 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,15 @@ +[tool.poetry] +name = "ProjectorPi" +version = "0.1.0" +description = "" +authors = ["Marijn Doeve "] + +[tool.poetry.dependencies] +python = "^3.9" +pyserial = "^3.5" + +[tool.poetry.dev-dependencies] + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/serialdevice.py b/serialdevice.py new file mode 100644 index 0000000..d568046 --- /dev/null +++ b/serialdevice.py @@ -0,0 +1,33 @@ +from doctest import REPORT_CDIFF +from serial import Serial + + +class SerialDevice: + def __init__(self, serial_port: str, baudrate: int = 9600) -> None: + self.serial_port: str = serial_port + self.baudrate = baudrate + + def send_command(self, command: str, verbose: bool = False): + with Serial(self.serial_port, self.baudrate, timeout=1) as s: + if verbose: + print("Send:", command) + + s.write(command.encode()) + + response_raw = None + + count = 0 + while count < 10 and not (response_raw := s.readline()): + pass + + if response_raw: + response = response_raw.decode().strip() + else: + response = "" + + if verbose: + print("Resp:", response) + if not response: + print("No response") + + return response