Merge pull request #29 from FoamyGuy/xdisplay_mirror_example

add xdisplay_mirror
This commit is contained in:
foamyguy 2025-03-11 12:26:37 -05:00 committed by GitHub
commit 0e717476c4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 78 additions and 196 deletions

View file

@ -35,21 +35,6 @@ with open("/sys/class/graphics/fb0/stride") as f:
linux_framebuffer = np.memmap('/dev/fb0',mode='r', shape=(screeny, stride // bytes_per_pixel), dtype=dtype)
def make_pixelmap_multilane(width, height, n_addr_lines, n_lanes):
calc_height = n_lanes << n_addr_lines
if height != calc_height:
raise RuntimeError(f"Calculated height {calc_height} does not match requested height {height}")
n_addr = 1 << n_addr_lines
m = []
for addr in range(n_addr):
for x in range(width):
for lane in range(n_lanes):
y = addr + lane * n_addr
m.append(x + width * y)
print(m)
return m
@click.command
@click.option("--x-offset", "xoffset", type=int, help="The x offset of top left corner of the region to mirror", default=0)
@click.option("--y-offset", "yoffset", type=int, help="The y offset of top left corner of the region to mirror", default=0)

View file

@ -32,6 +32,7 @@ from pyvirtualdisplay.smartdisplay import SmartDisplay
import adafruit_blinka_raspberry_pi5_piomatter as piomatter
import adafruit_blinka_raspberry_pi5_piomatter.click as piomatter_click
from adafruit_blinka_raspberry_pi5_piomatter.pixelmappers import simple_multilane_mapper
@click.command
@ -42,14 +43,21 @@ import adafruit_blinka_raspberry_pi5_piomatter.click as piomatter_click
@click.option("--use-xauth/--no-use-xauth", help="If a Xauthority file should be created", default=False)
@piomatter_click.standard_options
@click.argument("command", nargs=-1)
def main(scale, backend, use_xauth, extra_args, rfbport, width, height, serpentine, rotation, pinout, n_planes, n_addr_lines, command):
def main(scale, backend, use_xauth, extra_args, rfbport, width, height, serpentine, rotation, pinout,
n_planes, n_temporal_planes, n_addr_lines, n_lanes, command):
kwargs = {}
if backend == "xvnc":
kwargs['rfbport'] = rfbport
if extra_args:
kwargs['extra_args'] = shlex.split(extra_args)
print("xauth", use_xauth)
geometry = piomatter.Geometry(width=width, height=height, n_planes=n_planes, n_addr_lines=n_addr_lines, rotation=rotation)
if n_lanes != 2:
pixelmap = simple_multilane_mapper(width, height, n_addr_lines, n_lanes)
geometry = piomatter.Geometry(width=width, height=height, n_planes=n_planes, n_addr_lines=n_addr_lines,
n_temporal_planes=n_temporal_planes, n_lanes=n_lanes, map=pixelmap)
else:
geometry = piomatter.Geometry(width=width, height=height, n_planes=n_planes, n_addr_lines=n_addr_lines,
n_temporal_planes=n_temporal_planes, rotation=rotation)
framebuffer = np.zeros(shape=(geometry.height, geometry.width, 3), dtype=np.uint8)
matrix = piomatter.PioMatter(colorspace=piomatter.Colorspace.RGB888Packed, pinout=pinout, framebuffer=framebuffer, geometry=geometry)

View file

@ -1,179 +0,0 @@
#!/usr/bin/python3
"""
Display a (possibly scaled) X session to a matrix
The display runs until the graphical program exits.
Raw keyboard inputs are read from stdin and then injected into the running programs session with xdotool.
For help with commandline arguments, run `python virtualdisplay.py --help`
This needs additional software to be installed (besides a graphical program to run). At a minimum you have to
install a virtual display server program (xvfb) and the pyvirtualdisplay importable Python module:
$ sudo apt install -y xvfb xdotool
$ pip install pyvirtualdisplay
Here's an example for running an emulator using a rom stored in "/tmp/snesrom.smc" on a virtual 128x128 panel made from 4 64x64 panels:
$ python virtualdisplay_keyboard.py --pinout AdafruitMatrixHatBGR --scale 2 --backend xvfb --width 128 --height 128 --serpentine --num-address-lines 5 --num-planes 4 -- mednafen -snes.xscalefs 1 -snes.yscalefs 1 -snes.xres 128 -video.fs 1 -video.driver softfb /tmp/snesrom.smc
"""
# To run a nice emulator:
import os
import selectors
import shlex
import string
import sys
import termios
import tty
from subprocess import Popen, run
import click
import numpy as np
from pyvirtualdisplay.smartdisplay import SmartDisplay
import adafruit_blinka_raspberry_pi5_piomatter as piomatter
import adafruit_blinka_raspberry_pi5_piomatter.click as piomatter_click
from adafruit_blinka_raspberry_pi5_piomatter.pixelmappers import simple_multilane_mapper
keyboard_debug = False
keys_down = set()
basic_characters = string.ascii_letters + string.digits
key_map = {
# https://gitlab.com/nokun/gestures/-/wikis/xdotool-list-of-key-codes
b' ': "space",
b'/': "slash",
b'\\': "backslash",
b"'": "apostrophe",
b'\x7f': "BackSpace",
b'.': "period",
b',': "comma",
b'\t': "Tab",
b'\r': "Return",
b'!': "exclam",
b'?': "question",
b'@': "at",
b'<': "less",
b'>': "greater",
b'=': "equal",
b';': "semicolon",
b':': "colon",
b'+': "plus",
b'-': "minus",
b'*': "asterisk",
b'(': "parenleft",
b')': "parenright",
b'&': "ampersand",
b'%': "percent",
b'$': "dollar",
b'#': "numbersign",
b'\x1b[A': "Up",
b'\x1b[B': "Down",
b'\x1b[C': "Right",
b'\x1b[D': "Left",
b'\x1b': "Escape",
b'^': "caret",
b'[': "bracketleft",
b']': "bracketright",
b'{': "braceleft",
b'}': "braceright",
b'_': "underscore",
#b'': "",
}
ctrl_modified_range = (1, 26)
@click.command
@click.option("--scale", type=float, help="The scale factor, larger numbers mean more virtual pixels", default=1)
@click.option("--backend", help="The pyvirtualdisplay backend to use", default="xvfb")
@click.option("--extra-args", help="Extra arguments to pass to the backend server", default="")
@click.option("--rfbport", help="The port number for the --backend xvnc", default=None, type=int)
@click.option("--use-xauth/--no-use-xauth", help="If a Xauthority file should be created", default=False)
@click.option("--ctrl-c-interrupt/--no-ctrl-c-interrupt", help="If Ctrl+C should be handled as an interrupt.", default=True)
@piomatter_click.standard_options
@click.argument("command", nargs=-1)
def main(scale, backend, use_xauth, extra_args, rfbport, width, height, serpentine, rotation, pinout, n_planes, n_temporal_planes,
n_addr_lines, n_lanes, ctrl_c_interrupt, command):
def handle_key_event(evt_data):
if evt_data in key_map.keys():
keys_down.add(key_map[evt_data])
run(["xdotool", "keydown", key_map[evt_data]], env=disp.env())
elif evt_data.decode() in basic_characters:
run(["xdotool", "keydown", f"{evt_data.decode()}"], env=disp.env())
keys_down.add(evt_data.decode())
elif ctrl_modified_range[0] <= int.from_bytes(evt_data) <= ctrl_modified_range[1]:
if evt_data == b'\x03' and ctrl_c_interrupt:
raise KeyboardInterrupt
keys_down.add("Control_L")
run(["xdotool", "keydown", "Control_L"], env=disp.env())
modified_key = chr(int.from_bytes(evt_data) + 96)
if keyboard_debug:
print(f"ctrl modified {modified_key}")
keys_down.add(modified_key)
run(["xdotool", "keydown", modified_key], env=disp.env())
elif len(evt_data) > 1:
if keyboard_debug:
print("recvd multiple")
for char_val in evt_data:
if keyboard_debug:
print(f"{char_val} {chr(char_val)}")
char_bytes = char_val.to_bytes(1)
handle_key_event(char_bytes)
else:
print(f"unknown input data: {evt_data}")
old_settings = termios.tcgetattr(sys.stdin)
selector = selectors.DefaultSelector()
selector.register(fileobj=sys.stdin, events=selectors.EVENT_READ)
tty.setraw(sys.stdin.fileno())
kwargs = {}
if backend == "xvnc":
kwargs['rfbport'] = rfbport
if extra_args:
kwargs['extra_args'] = shlex.split(extra_args)
print("xauth", use_xauth)
if n_lanes != 2:
pixelmap = simple_multilane_mapper(width, height, n_addr_lines, n_lanes)
geometry = piomatter.Geometry(width=width, height=height, n_planes=n_planes, n_addr_lines=n_addr_lines, n_temporal_planes=n_temporal_planes, n_lanes=n_lanes, map=pixelmap)
else:
geometry = piomatter.Geometry(width=width, height=height, n_planes=n_planes, n_temporal_planes=n_temporal_planes, n_addr_lines=n_addr_lines, rotation=rotation)
framebuffer = np.zeros(shape=(geometry.height, geometry.width, 3), dtype=np.uint8)
matrix = piomatter.PioMatter(colorspace=piomatter.Colorspace.RGB888Packed, pinout=pinout, framebuffer=framebuffer,
geometry=geometry)
try:
with SmartDisplay(backend=backend, use_xauth=use_xauth, size=(round(width * scale), round(height * scale)),
manage_global_env=False, **kwargs) as disp, Popen(command, env=disp.env()) as proc:
while proc.poll() is None:
img = disp.grab(autocrop=False)
# print(disp.env())
if img is None:
continue
img = img.resize((width, height))
framebuffer[:, :] = np.array(img)
matrix.show()
event_count = 0
for key, __ in selector.select(timeout=0):
event_count += 1
# read up 3 bytes, so we full data for arrow keys
kbd_data = os.read(key.fileobj.fileno(), 3)
if keyboard_debug:
print(kbd_data)
handle_key_event(kbd_data)
# no kbd events, so keyup all keys
if event_count == 0:
for key in keys_down:
run(["xdotool", "keyup", key], env=disp.env())
keys_down.clear()
finally:
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_settings)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,68 @@
#!/usr/bin/python3
"""
Display a (possibly scaled) X display to a matrix
The display runs until this script exits.
The display doesn't get a keyboard or mouse, so you have to use a program that
will get its input in some other way, such as from a gamepad.
For help with commandline arguments, run `python xdisplay_mirror.py --help`
This example command will mirror the entire display scaled onto a 2x2 grid of 64px panels, total matrix size 128x128.
$ python xdisplay_mirror.py --pinout AdafruitMatrixHatBGR --width 128 --height 128 --serpentine --num-address-lines 5 --num-planes 8
This example command will mirror a 128x128 pixel square from the top left of the display at real size on the same matrix panels
$ python xdisplay_mirror.py --pinout AdafruitMatrixHatBGR --width 128 --height 128 --serpentine --num-address-lines 5 --num-planes 8 --mirror-region 0,0,128,128
"""
import click
import numpy as np
from PIL import ImageGrab
import adafruit_blinka_raspberry_pi5_piomatter as piomatter
import adafruit_blinka_raspberry_pi5_piomatter.click as piomatter_click
from adafruit_blinka_raspberry_pi5_piomatter.pixelmappers import simple_multilane_mapper
@click.command
@click.option("--mirror-region", help="Region of X display to mirror. Comma seperated x,y,w,h. "
"Default will mirror entire display.", default="")
@click.option("--x-display", help="The X display to mirror. Default is :0", default=":0")
@piomatter_click.standard_options(n_lanes=2, n_temporal_planes=0)
def main(width, height, serpentine, rotation, pinout, n_planes,
n_temporal_planes, n_addr_lines, n_lanes, mirror_region, x_display):
if n_lanes != 2:
pixelmap = simple_multilane_mapper(width, height, n_addr_lines, n_lanes)
geometry = piomatter.Geometry(width=width, height=height, n_planes=n_planes, n_addr_lines=n_addr_lines,
n_temporal_planes=n_temporal_planes, n_lanes=n_lanes, map=pixelmap)
else:
geometry = piomatter.Geometry(width=width, height=height, n_planes=n_planes, n_addr_lines=n_addr_lines,
n_temporal_planes=n_temporal_planes, rotation=rotation)
framebuffer = np.zeros(shape=(geometry.height, geometry.width, 3), dtype=np.uint8)
matrix = piomatter.PioMatter(colorspace=piomatter.Colorspace.RGB888Packed, pinout=pinout, framebuffer=framebuffer,
geometry=geometry)
if mirror_region:
mirror_region = tuple(int(_) for _ in mirror_region.split(','))
else:
mirror_region = None
size_measure = ImageGrab.grab(xdisplay=":0")
print(f"Mirroring full display: {size_measure.width}, {size_measure.height}")
while True:
img = ImageGrab.grab(xdisplay=x_display)
if mirror_region is not None:
img = img.crop((mirror_region[0], mirror_region[1], # left,top
mirror_region[0] + mirror_region[2], # right
mirror_region[1] + mirror_region[3])) # bottom
img = img.resize((width, height))
framebuffer[:, :] = np.array(img)
matrix.show()
if __name__ == '__main__':
main()