Merge pull request #29 from FoamyGuy/xdisplay_mirror_example
add xdisplay_mirror
This commit is contained in:
commit
0e717476c4
4 changed files with 78 additions and 196 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
68
examples/xdisplay_mirror.py
Normal file
68
examples/xdisplay_mirror.py
Normal 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()
|
||||
Loading…
Reference in a new issue