66from pathlib import Path
77import struct
88
9-
9+ IMG_PATH = Path ( "lib/pbio/test/animator/img" )
1010HOST = "127.0.0.1"
1111PORT = 5002
1212SCREEN_WIDTH = 1400
1313SCREEN_HEIGHT = 1000
1414FPS = 25
1515
16+ # Virtual display settings
17+ DISPLAY_WIDTH = 178
18+ DISPLAY_HEIGHT = 128
19+ DISPLAY_SCALE = 2 # Scale factor for display on screen
20+ DISPLAY_POS = (450 , 50 ) # Position on main screen
21+
1622PBIO_PYBRICKS_EVENT_STATUS_REPORT = 0
1723PBIO_PYBRICKS_EVENT_WRITE_STDOUT = 1
1824PBIO_PYBRICKS_EVENT_WRITE_APP_DATA = 2
19- PBIO_PYBRICKS_EVENT_WRITE_PORT_VIEW = 3
25+ PBIO_PYBRICKS_EVENT_WRITE_TELEMETRY = 3
2026
2127# Incoming events (stdout, status, app data, port view)
2228data_queue = queue .Queue ()
@@ -29,53 +35,63 @@ def socket_listener_thread():
2935 sock = socket .socket (socket .AF_INET , socket .SOCK_DGRAM )
3036 sock .bind ((HOST , PORT ))
3137 print (f"Listening on { HOST } :{ PORT } " )
32-
3338 while True :
34- data , _ = sock .recvfrom (1024 )
39+ data , _ = sock .recvfrom (DISPLAY_WIDTH * DISPLAY_HEIGHT )
3540 data_queue .put (data )
3641
3742
38- def blit_rotate_at_center (screen , image , position , angle ):
39- """Rotate image around a specific center point"""
40- rotated_image = pygame .transform .rotate (image , angle )
41- rotated_rect = rotated_image .get_rect (center = position )
42- screen .blit (rotated_image , rotated_rect )
43-
43+ # Hub state
44+ angles = [0 ] * 6
45+ virtual_display = pygame .Surface ((DISPLAY_WIDTH , DISPLAY_HEIGHT ))
46+ display_color = [0x90 , 0xC5 , 0xAD ]
47+ virtual_display .fill (display_color )
4448
45- # Define the base path for images
46- img_path = Path ("lib/pbio/test/animator/img" )
4749
50+ def update_display (data ):
51+ virtual_display .fill (display_color )
52+ for y in range (DISPLAY_HEIGHT ):
53+ for x in range (DISPLAY_WIDTH ):
54+ value = data [y * DISPLAY_WIDTH + x ]
55+ if value :
56+ brightness = (100 , 75 , 25 , 0 )[value ]
57+ color = [c * brightness // 100 for c in display_color ]
58+ virtual_display .set_at ((x , y ), color )
4859
49- def load_and_scale_image (filename , scale = 0.25 ):
50- """Load an image and scale it by the given scale factor."""
51- image = pygame .image .load (img_path / filename )
52- original_size = image .get_size ()
53- scaled_size = (int (original_size [0 ] * scale ), int (original_size [1 ] * scale ))
54- return pygame .transform .smoothscale (image , scaled_size )
5560
56-
57- # Hub state
58- angles = [0 ] * 6
61+ def process_telemetry (payload ):
62+ # Only supports motors for now.
63+ if len (payload ) == 6 :
64+ type_id , index , angle = struct .unpack ("<bbi" , payload [0 :6 ])
65+ if type_id == 96 :
66+ angles [index ] = angle
5967
6068
6169def update_state ():
6270 while not data_queue .empty ():
6371 data = data_queue .get_nowait ()
72+
73+ # Revisit: we should use the telemetry protocol for the display, but
74+ # this is not hooked up yet. For now, just assume that a huge payload
75+ # is a display buffer.
76+ if len (data ) > 6000 :
77+ update_display (data )
78+ continue
79+
80+ # Expecting a notification event code with payload
6481 if not isinstance (data , bytes ) or len (data ) < 2 :
6582 continue
6683 event = data [0 ]
6784 payload = data [1 :]
6885
86+ # Stdout goes to animation terminal.
6987 if event == PBIO_PYBRICKS_EVENT_WRITE_STDOUT :
7088 try :
7189 print (payload .decode (), end = "" )
7290 except UnicodeDecodeError :
7391 print (payload )
74- elif event == PBIO_PYBRICKS_EVENT_WRITE_PORT_VIEW :
75- if len (payload ) == 6 :
76- type_id , index , angle = struct .unpack ("<bbi" , payload [0 :6 ])
77- if type_id == 96 :
78- angles [index ] = angle
92+ # Telemetry is used to update the visual state.
93+ elif event == PBIO_PYBRICKS_EVENT_WRITE_TELEMETRY :
94+ process_telemetry (payload )
7995
8096
8197def main ():
@@ -85,6 +101,17 @@ def main():
85101 pygame .display .set_caption ("Virtual Hub Animator" )
86102 clock = pygame .time .Clock ()
87103
104+ def blit_rotate_at_center (image , position , angle ):
105+ rotated_image = pygame .transform .rotate (image , angle )
106+ rotated_rect = rotated_image .get_rect (center = position )
107+ screen .blit (rotated_image , rotated_rect )
108+
109+ def load_and_scale_image (filename , scale = 0.25 ):
110+ image = pygame .image .load (IMG_PATH / filename )
111+ original_size = image .get_size ()
112+ scaled_size = (int (original_size [0 ] * scale ), int (original_size [1 ] * scale ))
113+ return pygame .transform .smoothscale (image , scaled_size )
114+
88115 # Load images
89116 hub = load_and_scale_image ("main-model.png" )
90117 wheel_left = load_and_scale_image ("wheel-left.png" )
@@ -94,6 +121,13 @@ def main():
94121 gear_drive = load_and_scale_image ("gear-drive.png" )
95122 gear_follow = load_and_scale_image ("gear-follow.png" )
96123
124+ display_border_rect = pygame .Rect (
125+ DISPLAY_POS [0 ] - 2 ,
126+ DISPLAY_POS [1 ] - 2 ,
127+ DISPLAY_WIDTH * DISPLAY_SCALE + 4 ,
128+ DISPLAY_HEIGHT * DISPLAY_SCALE + 4 ,
129+ )
130+
97131 # Start the socket listener thread
98132 threading .Thread (target = socket_listener_thread , daemon = True ).start ()
99133
@@ -110,13 +144,21 @@ def main():
110144 screen .fill ((161 , 168 , 175 ))
111145
112146 # Draw and rotate components, with left wheel behind hub.
113- blit_rotate_at_center (screen , wheel_left , (1242 , 156 ), - angles [0 ])
147+ blit_rotate_at_center (wheel_left , (1242 , 156 ), - angles [0 ])
114148 screen .blit (hub , (0 , 0 ))
115- blit_rotate_at_center (screen , wheel_right , (1067 , 331 ), angles [1 ])
116- blit_rotate_at_center (screen , shaft , (247 , 712 ), angles [4 ])
117- blit_rotate_at_center (screen , stall , (245 , 189 ), angles [2 ])
118- blit_rotate_at_center (screen , gear_drive , (943 , 694 ), - angles [5 ])
119- blit_rotate_at_center (screen , gear_follow , (1037 , 694 ), angles [5 ] / 3 )
149+ blit_rotate_at_center (wheel_right , (1067 , 331 ), angles [1 ])
150+ blit_rotate_at_center (shaft , (247 , 712 ), angles [4 ])
151+ blit_rotate_at_center (stall , (245 , 189 ), angles [2 ])
152+ blit_rotate_at_center (gear_drive , (943 , 694 ), - angles [5 ])
153+ blit_rotate_at_center (gear_follow , (1037 , 694 ), angles [5 ] / 3 )
154+
155+ # Show virtual display.
156+ pygame .draw .rect (screen , (50 , 50 , 50 ), display_border_rect )
157+ scaled_surface = pygame .transform .scale (
158+ virtual_display ,
159+ (DISPLAY_WIDTH * DISPLAY_SCALE , DISPLAY_HEIGHT * DISPLAY_SCALE ),
160+ )
161+ screen .blit (scaled_surface , DISPLAY_POS )
120162
121163 # Update display
122164 pygame .display .flip ()
0 commit comments