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 )
0 commit comments