Skip to content

Commit e0779d7

Browse files
committed
Initial Dummy Monitor
1 parent c31f212 commit e0779d7

File tree

6 files changed

+270
-2
lines changed

6 files changed

+270
-2
lines changed

.vscode/launch.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"configurations": [
3+
{
4+
"name": "pyevdi: Dummy Monitor",
5+
"type": "python",
6+
"request": "launch",
7+
"program": "${workspaceFolder}/pyevdi/examples/dummy_monitor/dummy_monitor.py",
8+
"cwd": "${workspaceFolder}/pyevdi/examples/dummy_monitor",
9+
"args": [
10+
"--edid-file",
11+
"../../sample_edid/1920x1080_benq.edid"
12+
],
13+
"console": "integratedTerminal",
14+
"justMyCode": true,
15+
"sudo": true,
16+
"python": "${workspaceFolder}/pyevdi/evdienv/bin/python"
17+
}
18+
]
19+
}

pyevdi/Card.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,9 @@ class Card {
3333
public:
3434
/// used py::function to allow lambdas to work
3535
/// void(struct evdi_mode)
36-
py::function mode_handler;
36+
py::function mode_handler_cb;
3737
/// void(std::shared_ptr<Buffer> buffer)
38-
py::function acquire_framebuffer_handler;
38+
py::function acquire_framebuffer_cb;
3939

4040
explicit Card(int device);
4141
~Card();
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
import signal
2+
import time
3+
import PyEvdi
4+
import argparse
5+
import os
6+
import sys
7+
from PySide6.QtCore import Qt, QSize, QTimer, QByteArray
8+
from PySide6.QtGui import QImage, QPainter, QColor
9+
from PySide6.QtWidgets import QApplication, QMainWindow, QWidget, QVBoxLayout, QLabel
10+
import numpy as np
11+
12+
from moving_average import MovingAverage
13+
14+
def is_not_running_as_root():
15+
return os.geteuid() != 0
16+
17+
def get_available_evdi_card():
18+
for i in range(20):
19+
if PyEvdi.check_device(i) == PyEvdi.AVAILABLE:
20+
return i
21+
PyEvdi.add_device()
22+
for i in range(20):
23+
if PyEvdi.check_device(i) == PyEvdi.AVAILABLE:
24+
return i
25+
return -1
26+
27+
def load_edid_file(file):
28+
if os.path.exists(file):
29+
with open(file, mode='rb') as f:
30+
ed = f.read()
31+
return ed
32+
elif os.path.exists(file + '.edid'):
33+
with open(file + '.edid', mode='rb') as f:
34+
ed = f.read()
35+
return ed
36+
else:
37+
return None
38+
39+
class Options:
40+
headless: bool = False
41+
resolution: tuple[int, int] = (1920, 1080)
42+
refresh_rate: int = 60
43+
edid_file: str = None
44+
fps_limit: int = 60
45+
46+
class ImageBufferWidget(QWidget):
47+
def __init__(self, width, height, options: Options):
48+
super().__init__()
49+
self.setMinimumSize(width, height)
50+
self.options = options
51+
self.image = QImage(self.options.resolution[0], self.options.resolution[1], QImage.Format_RGB888)
52+
self.image.fill(QColor(255, 255, 0))
53+
54+
def paintEvent(self, event):
55+
print("paintEvent")
56+
painter = QPainter(self)
57+
painter.drawImage(self.rect(), self.image)
58+
59+
def update_image(self, buffer):
60+
now = time.time()
61+
print("update_image: buffer id:", buffer.id)
62+
63+
x_size, y_size = buffer.width, buffer.height
64+
#for y in range(y_size):
65+
# for x in range(x_size):
66+
# color = buffer_get_color(buffer, x, y)
67+
# rgb: int = buffer.bytes[y, x]
68+
# bytes = [rgb >> 24, (rgb >> 16) & 0xFF, (rgb >> 8) & 0xFF, rgb & 0xFF]
69+
# color = QColor(bytes[1], bytes[2], bytes[3])
70+
# self.image.setPixelColor(x, y, color)
71+
72+
# np_array = np.array(buffer, copy = False) # This is possible thanks to buffer protocol
73+
self.image = QImage(buffer.bytes, buffer.height, buffer.width, QImage.Format_RGB32)
74+
75+
took = time.time() - now
76+
print("update_image: took", took, "seconds")
77+
self.repaint()
78+
79+
class MainWindow(QMainWindow):
80+
def __init__(self, options: Options):
81+
super().__init__()
82+
self.setWindowTitle("EVDI virtual monitor")
83+
self.image_buffer_widget = ImageBufferWidget(400, 300, options)
84+
self.setCentralWidget(self.image_buffer_widget)
85+
86+
def resizeEvent(self, event):
87+
self.image_buffer_widget.resize(event.size())
88+
89+
last_frame_time = 0
90+
fps_move_average = MovingAverage(10)
91+
92+
def format_buffer(buffer):
93+
result = []
94+
result.append(f"received buffer id: {buffer.id}")
95+
result.append(f"rect_count: {buffer.rect_count}")
96+
result.append(f"width: {buffer.width}")
97+
result.append(f"height: {buffer.height}")
98+
result.append(f"stride: {buffer.stride}")
99+
result.append("rects:")
100+
for rect in buffer.rects:
101+
result.append(f"{rect.x1}, {rect.y1}, {rect.x2}, {rect.y2}")
102+
return "\n".join(result)
103+
104+
def framebuffer_handler(buffer, app):
105+
global last_frame_time
106+
107+
print(format_buffer(buffer))
108+
109+
now = time.time()
110+
time_since_last_frame = now - last_frame_time
111+
112+
fps = 1000 / time_since_last_frame
113+
fps_move_average.push(fps)
114+
115+
116+
117+
if app is not None:
118+
app.image_buffer_widget.update_image(buffer)
119+
del buffer
120+
last_frame_time = time.time()
121+
122+
def mode_changed_handler(mode, app) -> None:
123+
print(format_mode(mode))
124+
125+
def format_mode(mode) -> None:
126+
return 'Mode: ' + str(mode.width) + 'x' + str(mode.height) + '@' + str(mode.refresh_rate) + ' ' + str(mode.bits_per_pixel) + 'bpp ' + str(mode.pixel_format)
127+
128+
def main(options: Options) -> None:
129+
card = PyEvdi.Card(get_available_evdi_card())
130+
area = options.resolution[0] * options.resolution[1]
131+
connect_ret = None
132+
if options.edid_file:
133+
edid = load_edid_file(options.edid_file)
134+
connect_ret = card.connect(edid, len(edid), area, area * options.refresh_rate)
135+
else:
136+
connect_ret = card.connect(None, 0, area, area * options.refresh_rate)
137+
138+
139+
my_app = None
140+
def my_acquire_framebuffer_handler(buffer):
141+
framebuffer_handler(buffer, my_app)
142+
def my_mode_changed_handler(mode):
143+
mode_changed_handler(mode, my_app)
144+
card.acquire_framebuffer_handler = my_acquire_framebuffer_handler
145+
card.mode_changed_handler = my_mode_changed_handler
146+
mode = card.getMode()
147+
148+
if not options.headless:
149+
print("RET:", connect_ret)
150+
app = QApplication([])
151+
my_app = MainWindow(options)
152+
my_app.show()
153+
# set window size to 480x270
154+
my_app.resize(480, 270)
155+
156+
card_timer = QTimer()
157+
card_timer.timeout.connect(lambda: card.handle_events(0))
158+
card_timer.setInterval(20)
159+
card_timer.start()
160+
161+
signal.signal(signal.SIGINT, lambda *args: app.quit())
162+
163+
def on_app_quit():
164+
now = time.time()
165+
print("Quitting at", now)
166+
card.disconnect()
167+
card.close()
168+
took = time.time() - now
169+
print("Took", took, "seconds")
170+
app.aboutToQuit.connect(on_app_quit)
171+
172+
print("Starting event loop")
173+
174+
sys.exit(app.exec())
175+
else:
176+
177+
print("Running headless")
178+
while(True):
179+
now = time.time()
180+
#print("Handling events at", now)
181+
card.handle_events(100)
182+
took = time.time() - now
183+
#print("Took", took, "seconds")
184+
185+
card.disconnect()
186+
card.close()
187+
188+
if __name__ == '__main__':
189+
# read arguments into options
190+
options = Options()
191+
192+
# parse arguments
193+
parser = argparse.ArgumentParser()
194+
parser.add_argument('--headless', action='store_true')
195+
parser.add_argument('--resolution', nargs=2, type=int)
196+
parser.add_argument('--refresh-rate', type=int)
197+
parser.add_argument('--edid-file', type=str)
198+
parser.add_argument('--fps-limit', type=int)
199+
args = parser.parse_args()
200+
201+
# set options
202+
if args.headless:
203+
options.headless = args.headless
204+
if args.edid_file:
205+
options.edid_file = args.edid_file
206+
if args.resolution:
207+
options.resolution = args.resolution
208+
if args.refresh_rate:
209+
options.refresh_rate = args.refresh_rate
210+
if args.fps_limit:
211+
options.fps_limit = args.fps_limit
212+
213+
main(options)
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import numpy as np
2+
3+
class MovingAverage:
4+
def __init__(self, depth: int):
5+
self.depth = depth
6+
self.values = np.zeros(depth)
7+
self.pointer = 0
8+
self.size = 0
9+
self.current_avg = 0.0
10+
11+
def push(self, value) -> None:
12+
"""Updates the moving average with the new value"""
13+
# check if the input is a numpy array
14+
if isinstance(value, np.ndarray):
15+
for val in value:
16+
self._push_single(val)
17+
else:
18+
self._push_single(value)
19+
20+
def _push_single(self, value: float) -> None:
21+
if self.size < self.depth:
22+
# We are still filling our initial array
23+
old = 0
24+
self.size += 1
25+
else:
26+
old = self.values[self.pointer]
27+
28+
self.values[self.pointer] = value
29+
self.pointer = (self.pointer + 1) % self.depth
30+
self.current_avg += (value - old) / self.size
31+
32+
def average(self) -> float:
33+
"""Returns the current moving average"""
34+
return self.current_avg
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
pyside6
2+
numpy
256 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)