Skip to content

Instantly share code, notes, and snippets.

@jpramosi
Created April 11, 2022 06:08
Show Gist options
  • Save jpramosi/dbee7e8ef0c8baa95005fdc928831c1c to your computer and use it in GitHub Desktop.
Save jpramosi/dbee7e8ef0c8baa95005fdc928831c1c to your computer and use it in GitHub Desktop.
Waveshare brightness control via hidraw. Other models with hidraw support may also work.
#!/usr/bin/python3
"""
usage:
python waveshare-7HP-CAPQLED.py --brightness 1
python waveshare-7HP-CAPQLED.py --hidraw 0 --brightness 9
"""
import os
import argparse
import array
import ctypes
import dataclasses
import fcntl
import os
import time
from inspect import cleandoc
from typing import BinaryIO, List, Optional, Union
# THIS IS A MODIFIED VERSION OF:
# https://github.com/openinput-fw/openinput/blob/80885317275bc69399e4fdca22a9347e70398f84/tools/hidraw.py
@dataclasses.dataclass
class DeviceInfo(object):
bus: int = 0x03
vid: Optional[int] = None
pid: Optional[int] = None
def __repr__(self) -> str:
return str(self)
def __str__(self) -> str:
if self.vid is not None and self.pid is not None:
return f"{self.__class__.__name__}(bustype:{hex(self.bus)}, vendor:{hex(self.vid)}, product:{hex(self.pid)})"
return f"{self.__class__.__name__}()"
class IOCTL(object):
"""
Constructs and performs ioctl(s)
See include/asm-generic/ioctl.h
"""
NRBITS: int = 8
TYPEBITS: int = 8
SIZEBITS: int = 14
DIRBITS: int = 2
NRMASK: int = (1 << NRBITS) - 1
TYPEMASK: int = (1 << TYPEBITS) - 1
SIZEMASK: int = (1 << SIZEBITS) - 1
DIRMASK: int = (1 << DIRBITS) - 1
NRSHIFT: int = 0
TYPESHIFT: int = NRSHIFT + NRBITS
SIZESHIFT: int = TYPESHIFT + TYPEBITS
DIRSHIFT: int = SIZESHIFT + SIZEBITS
class Direction:
NONE = 0
WRITE = 1
READ = 2
def __init__(self, dir: int, ty: str, nr: int, size: int = 0, bad: bool = False) -> None:
assert self.Direction.NONE <= dir <= self.Direction.READ + self.Direction.WRITE
if dir == self.Direction.NONE:
size = 0
elif not bad:
assert size, size <= self.SIZEMASK
self.op = (dir << self.DIRSHIFT) | \
(ord(ty) << self.TYPESHIFT) | \
(nr << self.NRSHIFT) | \
(size << self.SIZESHIFT)
def perform(self, fd: BinaryIO, buf: Optional[Union[str, bytes, "array.array[int]"]] = None) -> bytearray:
"""
Performs the ioctl
"""
size = self.unpack_size(self.op)
if buf is None:
buf = (size * "\x00").encode()
return bytearray(fcntl.ioctl(fd, self.op, buf)) # type: ignore
@classmethod
def unpack_dir(cls, nr: int) -> int:
return (nr >> cls.DIRSHIFT) & cls.DIRMASK
@classmethod
def unpack_type(cls, nr: int) -> int:
return (nr >> cls.TYPESHIFT) & cls.TYPEMASK
@classmethod
def unpack_nr(cls, nr: int) -> int:
return (nr >> cls.NRSHIFT) & cls.NRMASK
@classmethod
def unpack_size(cls, nr: int) -> int:
return (nr >> cls.SIZESHIFT) & cls.SIZEMASK
@classmethod
def IO(cls, ty: str, nr: int) -> "IOCTL":
"""
Default constructor for no direction
"""
return cls(cls.Direction.NONE, ty, nr)
@classmethod
def IOR(cls, ty: str, nr: int, size: int) -> "IOCTL":
"""
Default constructor for read
"""
return cls(cls.Direction.READ, ty, nr, size)
@classmethod
def IOW(cls, ty: str, nr: int, size: int) -> "IOCTL":
"""
Default constructor for write
"""
return cls(cls.Direction.WRITE, ty, nr, size)
@classmethod
def IORW(cls, ty: str, nr: int, size: int) -> "IOCTL":
"""
Default constructor for read & write
"""
return cls(cls.Direction.READ | cls.Direction.WRITE, ty, nr, size)
class Hidraw(object):
"""
Represents a hidraw node
See linux/hidraw.h
"""
HIDIOCGRDESCSIZE = 0x01
HIDIOCGRDESC = 0x02
HIDIOCGRAWINFO = 0x03
HIDIOCGRAWNAME = 0x04
HIDIOCGRAWPHYS = 0x05
HIDIOCSFEATURE = 0x06
HIDIOCGFEATURE = 0x07
HID_NAME_SIZE = 1024
class hidraw_report_descriptor(ctypes.Structure):
HID_MAX_DESCRIPTOR_SIZE = 4096
_fields_ = [
("size", ctypes.c_uint),
("value", ctypes.c_ubyte * HID_MAX_DESCRIPTOR_SIZE),
]
def __repr__(self) -> str:
return str(self)
def __str__(self) -> str:
return f"{self.__class__.__name__}(size:{self.size}, value:{self.value})"
class hidraw_devinfo(ctypes.Structure):
_fields_ = [
("bustype", ctypes.c_uint),
("vendor", ctypes.c_ushort),
("product", ctypes.c_ushort),
]
def __repr__(self) -> str:
return str(self)
def __str__(self) -> str:
return f"{self.__class__.__name__}(bustype:{self.bustype}, vendor:{self.vendor}, product:{self.product})"
def __init__(self, path: str) -> None:
self._path = path
self._fd = open(path, "rb+")
fcntl.fcntl(self._fd, fcntl.F_SETFL, os.O_NONBLOCK)
def __str__(self) -> str:
return f"Hidraw({self.path})"
@property
def path(self) -> str:
return self._path
@property
def report_descriptor_size(self) -> int:
"""
Size of the report descriptor of the hidraw node
"""
return ctypes.c_uint.from_buffer(
IOCTL.IOR("H", self.HIDIOCGRDESCSIZE, ctypes.sizeof(ctypes.c_uint)).perform(self._fd)).value
@property
def report_descriptor(self) -> List[int]:
"""
Report descriptor of the hidraw node
"""
# fcntl.ioctl does not support such big buffer sizes when using the default buffer so we need to provide our own buffer
buf = array.array("B", self.report_descriptor_size.to_bytes(4, "little") +
self.hidraw_report_descriptor.HID_MAX_DESCRIPTOR_SIZE * b"\x00")
IOCTL.IOR("H", self.HIDIOCGRDESC, ctypes.sizeof(
self.hidraw_report_descriptor)).perform(self._fd, buf=buf)
ret = self.hidraw_report_descriptor.from_buffer(buf)
return list(ret.value)[:ret.size]
@property
def info(self) -> DeviceInfo:
"""
Device info of the hidraw node
"""
dev_info = self.hidraw_devinfo.from_buffer(
IOCTL.IOR("H", self.HIDIOCGRAWINFO, ctypes.sizeof(self.hidraw_devinfo)).perform(self._fd))
return DeviceInfo(dev_info.bustype, dev_info.vendor, dev_info.product)
@property
def name(self) -> str:
"""
HID name of the hidraw node
"""
return IOCTL.IOR("H", self.HIDIOCGRAWNAME, self.HID_NAME_SIZE).perform(self._fd).decode("utf-8")
@property
def has_vendor_page(self) -> bool:
"""
Whether or not the report descriptor of a hidraw node contains a vendor page
Really basic HID report descriptor parser. You can find the documentation
in items 5 (Operational Mode) and 6 (Descriptors) of the Device Class
Definition for HID
"""
class Type(object):
MAIN = 0
GLOBAL = 1
LOCAL = 2
RESERVED = 3
class TagGlobal(object):
USAGE_PAGE = 0b0000
LOGICAL_MINIMUM = 0b0001
LOGICAL_MAXIMUM = 0b0010
PHYSICAL_MINIMUM = 0b0011
PHYSICAL_MAXIMUM = 0b0100
UNIT_EXPONENT = 0b0101
UNIT = 0b0110
REPORT_SIZE = 0b0111
REPORT_ID = 0b1000
REPORT_COUNT = 0b1001
PUSH = 0b1010
POP = 0b1011
rdesc = self.report_descriptor
i = 0
while i < len(rdesc):
prefix = rdesc[i]
tag = (prefix & 0b11110000) >> 4
typ = (prefix & 0b00001100) >> 2
size = prefix & 0b00000011
if size == 3: # 6.2.2.2
size = 4
# vendor page
if typ == Type.GLOBAL and tag == TagGlobal.USAGE_PAGE and rdesc[i+2] == 0xff:
return True
i += size + 1
return False
def read_raw(self) -> List[int]:
"""
Simple read action
"""
return self._fd.read()
def read(self, timeout: Union[int, float] = 1) -> List[int]:
"""
Reads data from the hidraw node
"""
max_time = time.time() + timeout
buf: Optional[bytes] = None
while not buf and time.time() < max_time:
try:
buf = self._fd.read()
except BrokenPipeError:
pass
time.sleep(0.001)
return list(buf or [])
def write(self, buf: List[int]) -> None:
"""
Writes data to the hidraw node
"""
print("".join(f"{byte:02x}" for byte in buf))
self._fd.write(bytes(buf))
def write_raw(self, buf) -> None:
"""
Writes raw data to the hidraw node
"""
self._fd.write(buf)
def close(self):
"""
Closes the Hidraw device.
"""
self._fd.close()
def command(self, buf: List[int], timeout: int = 1) -> List[int]:
"""
Writes data to the device node and reads the reply
"""
self.write(buf)
return self.read(timeout)
def command_raw(self, buf: List[int], delay: int = 0.02) -> List[int]:
"""
Writes data to the device node and reads the reply (non-blocking)
"""
self.write(buf)
time.sleep(delay)
return self.read_raw()
def main():
vendor = "waveshare"
buffer_size = 38
buffer = [0x00]*buffer_size
device = None
parser = argparse.ArgumentParser(description=cleandoc("""
Brightness control for Waveshare 7HP-CAPQLED touchscreen.
Other models may also work as long it supports the hidraw interface standard."""
))
parser.add_argument("-b", "--brightness", type=int, default=1,
help="The brightness level. Must be between 1 and 10")
parser.add_argument("-i", "--hidraw", type=int, default=None,
help="The hidraw device to use.")
args, _ = parser.parse_known_args()
if (args.brightness > 10 or args.brightness < 1):
raise ValueError("Argument '--brightness' must be between 1 and 10")
# actually limited to 4096, but scanning till 64 is enough
for i in range(0, 64):
if (args.hidraw is not None):
i = int(args.hidraw)
hidraw = None
try:
hidraw = Hidraw(f"/dev/hidraw{i}")
except:
continue
name = hidraw.name.lower()
if (vendor in name):
device = hidraw
break
if (device is None):
raise RuntimeError("Waveshare display could not be found")
try:
buffer[0] = 0x4
buffer[1] = 0xAA
buffer[2] = 0x1
buffer[6] = args.brightness*0xA
device.write_raw(bytes(buffer))
except Exception as ex:
print(ex)
exit(1)
device.close()
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment