Skip to content

Commit 2237dc5

Browse files
committed
tools: Replace animator with Python.
We used to have: pybricks-micropython --> [pipe] --> python --> [webserver] --> react app This simplifies it to pybricks-micropython --> [socket] --> python The animator can stay open at all times for easy pybricks-micropython debugging.
1 parent edbce2f commit 2237dc5

File tree

20 files changed

+209
-10452
lines changed

20 files changed

+209
-10452
lines changed

.vscode/launch.json

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,8 @@
6565
"cwd": "${workspaceFolder}",
6666
"environment": [
6767
{
68-
"name": "PBIO_TEST_DATA_PARSER",
69-
"value": "./tools/virtual-hub-animator/data_server.py"
68+
"name": "PBIO_TEST_CONNECT_SOCKET",
69+
"value": "true"
7070
},
7171
],
7272
"externalConsole": false,
@@ -85,6 +85,13 @@
8585
],
8686
"preLaunchTask": "build virtualhub"
8787
},
88+
{
89+
"name":"Virtual Hub Animation",
90+
"type":"debugpy",
91+
"request":"launch",
92+
"program":"${workspaceFolder}/lib/pbio/platform/virtual_hub/animation.py",
93+
"console":"integratedTerminal"
94+
},
8895
{
8996
"name": "test-pbio",
9097
"type": "cppdbg",

lib/pbio/drv/motor_driver/motor_driver_virtual_simulation.c

Lines changed: 55 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,14 @@
55

66
#if PBDRV_CONFIG_MOTOR_DRIVER_VIRTUAL_SIMULATION
77

8+
#include <arpa/inet.h>
89
#include <stdint.h>
9-
#include <stdlib.h>
1010
#include <stdio.h>
11+
#include <stdlib.h>
12+
#include <string.h>
1113
#include <sys/wait.h>
1214
#include <unistd.h>
15+
#include <unistd.h>
1316

1417
#include <contiki.h>
1518

@@ -145,8 +148,35 @@ pbio_error_t pbdrv_motor_driver_set_duty_cycle(pbdrv_motor_driver_dev_t *driver,
145148
return PBIO_SUCCESS;
146149
}
147150

148-
static pid_t data_parser_pid;
149-
static FILE *data_parser_in;
151+
static int data_socket = -1;
152+
153+
static void animation_socket_connect(void) {
154+
155+
struct sockaddr_in serv_addr;
156+
data_socket = socket(AF_INET, SOCK_STREAM, 0);
157+
if (data_socket < 0) {
158+
perror("socket() failed");
159+
return;
160+
}
161+
162+
serv_addr.sin_family = AF_INET;
163+
serv_addr.sin_port = htons(5002);
164+
if (inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr) <= 0) {
165+
perror("inet_pton() failed");
166+
close(data_socket);
167+
data_socket = -1;
168+
return;
169+
}
170+
171+
if (connect(data_socket, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
172+
perror("connect() failed. Animation not running?");
173+
close(data_socket);
174+
data_socket = -1;
175+
return;
176+
}
177+
178+
printf("Connected to animation socket.\n");
179+
}
150180

151181
PROCESS(pbdrv_motor_driver_virtual_simulation_process, "pbdrv_motor_driver_virtual_simulation");
152182

@@ -166,27 +196,29 @@ PROCESS_THREAD(pbdrv_motor_driver_virtual_simulation_process, ev, data) {
166196
PROCESS_WAIT_EVENT_UNTIL(ev == PROCESS_EVENT_TIMER && etimer_expired(&tick_timer));
167197

168198
// If data parser pipe is connected, output the motor angles.
169-
if (data_parser_in && timer_expired(&frame_timer)) {
199+
if (data_socket >= 0 && timer_expired(&frame_timer)) {
170200
timer_reset(&frame_timer);
171-
201+
char buf[PBDRV_CONFIG_MOTOR_DRIVER_NUM_DEV * 20];
202+
size_t len = 0;
172203
// Output motor angles on one line.
173204
for (dev_index = 0; dev_index < PBDRV_CONFIG_MOTOR_DRIVER_NUM_DEV; dev_index++) {
174205
driver = &motor_driver_devs[dev_index];
175-
fprintf(data_parser_in, "%d ", ((int32_t)(driver->angle / 1000)));
176-
}
177-
fprintf(data_parser_in, "\r\n");
178-
179-
// Check that process is still running.
180-
pid_t p = waitpid(data_parser_pid, NULL, WNOHANG);
181-
if (p == -1 || p == data_parser_pid) {
182-
fclose(data_parser_in);
183-
printf("Process failed or ended.");
184-
exit(1);
206+
207+
// Append each motor angle to the buffer
208+
len += snprintf(buf + len, sizeof(buf) - len, "%d ", (int)(driver->angle / 1000));
185209
}
186-
if (fflush(data_parser_in) == -1) {
187-
printf("Flush failed.");
188-
fclose(data_parser_in);
189-
exit(1);
210+
211+
// Replace last space with newline
212+
buf[len++] = '\r';
213+
buf[len++] = '\n';
214+
buf[len] = '\0';
215+
216+
// Send the constructed message
217+
ssize_t sent = send(data_socket, buf, len, MSG_NOSIGNAL);
218+
if (sent == -1) {
219+
perror("send() failed");
220+
close(data_socket);
221+
data_socket = -1;
190222
}
191223
}
192224

@@ -252,51 +284,6 @@ PROCESS_THREAD(pbdrv_motor_driver_virtual_simulation_process, ev, data) {
252284
PROCESS_END();
253285
}
254286

255-
// Optionally starts script that receives motor angles through a pipe
256-
// for visualization and debugging purposes.
257-
static void pbdrv_motor_driver_virtual_simulation_prepare_parser(void) {
258-
259-
const char *data_parser_cmd = getenv("PBIO_TEST_DATA_PARSER");
260-
261-
// Skip if no data parser is given.
262-
if (!data_parser_cmd) {
263-
return;
264-
}
265-
266-
// Create the pipe to parser script.
267-
int data_parser_stdin[2];
268-
if (pipe(data_parser_stdin) == -1) {
269-
printf("pipe(data_parser_in) failed\n");
270-
return;
271-
}
272-
273-
// For the process to run the parser in parallel.
274-
data_parser_pid = fork();
275-
if (data_parser_pid == -1) {
276-
printf("fork() failed");
277-
return;
278-
}
279-
280-
// The child process executes the data parser.
281-
if (data_parser_pid == 0) {
282-
dup2(data_parser_stdin[0], STDIN_FILENO);
283-
close(data_parser_stdin[1]);
284-
char *args[] = {NULL, NULL};
285-
if (execvp(data_parser_cmd, args) == -1) {
286-
printf("Failed to start data parser.");
287-
}
288-
exit(EXIT_FAILURE);
289-
}
290-
291-
// Get file streams for pipes to data parser.
292-
close(data_parser_stdin[0]);
293-
data_parser_in = fdopen(data_parser_stdin[1], "w");
294-
if (data_parser_in == NULL) {
295-
printf("fdopen(data_parser_stdin[1], \"w\") failed");
296-
exit(1);
297-
}
298-
}
299-
300287
static bool simulation_enabled = true;
301288

302289
void pbdrv_motor_driver_disable_process(void) {
@@ -334,8 +321,11 @@ void pbdrv_motor_driver_init(void) {
334321
}
335322
}
336323

324+
// Skip if no data parser is given.
325+
if (getenv("PBIO_TEST_CONNECT_SOCKET")) {
326+
animation_socket_connect();
327+
}
337328

338-
pbdrv_motor_driver_virtual_simulation_prepare_parser();
339329
if (simulation_enabled) {
340330
process_start(&pbdrv_motor_driver_virtual_simulation_process);
341331
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
#!/usr/bin/env python3
2+
import pygame
3+
import socket
4+
import threading
5+
from pathlib import Path
6+
7+
8+
HOST = "127.0.0.1"
9+
PORT = 5002
10+
SCREEN_WIDTH = 1400
11+
SCREEN_HEIGHT = 1000
12+
FPS = 25
13+
14+
# Initialize shared state
15+
angles = [0, 0, 0, 0, 0, 0]
16+
17+
18+
# Handles socket communication. Accepts new connection when client disconnects.
19+
# This means it can stay open at all times even when restarting pybricks-micropython
20+
# for fast debugging.
21+
def socket_listener_thread():
22+
global angles
23+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
24+
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
25+
s.bind((HOST, PORT))
26+
s.listen(1)
27+
print(f"Listening on {HOST}:{PORT}")
28+
while True:
29+
conn, addr = s.accept()
30+
print(f"Client connected: {addr}")
31+
with conn:
32+
buffer = b""
33+
while True:
34+
data = conn.recv(1024)
35+
if not data:
36+
print("Client disconnected.")
37+
break
38+
buffer += data
39+
while b"\n" in buffer:
40+
line, buffer = buffer.split(b"\n", 1)
41+
try:
42+
angles = list(map(int, line.decode().strip().split()))
43+
except ValueError:
44+
print("Invalid data received:", line.decode().strip())
45+
46+
47+
def blit_rotate_at_center(screen, image, position, angle):
48+
"""Rotate image around a specific center point"""
49+
rotated_image = pygame.transform.rotate(image, angle)
50+
rotated_rect = rotated_image.get_rect(center=position)
51+
screen.blit(rotated_image, rotated_rect)
52+
53+
54+
# Define the base path for images
55+
img_path = Path("lib/pbio/test/animator/img")
56+
57+
58+
def load_and_scale_image(filename, scale=0.25):
59+
"""Load an image and scale it by the given scale factor."""
60+
image = pygame.image.load(img_path / filename)
61+
original_size = image.get_size()
62+
scaled_size = (int(original_size[0] * scale), int(original_size[1] * scale))
63+
return pygame.transform.smoothscale(image, scaled_size)
64+
65+
66+
# Main Pygame function
67+
def main():
68+
global angles
69+
70+
# Initialize Pygame
71+
pygame.init()
72+
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
73+
pygame.display.set_caption("Virtual Hub Animator")
74+
clock = pygame.time.Clock()
75+
76+
# Load images
77+
hub = load_and_scale_image("main-model.png")
78+
wheel_left = load_and_scale_image("wheel-left.png")
79+
wheel_right = load_and_scale_image("wheel-right.png")
80+
shaft = load_and_scale_image("shaft.png")
81+
stall = load_and_scale_image("stall.png")
82+
gear_drive = load_and_scale_image("gear-drive.png")
83+
gear_follow = load_and_scale_image("gear-follow.png")
84+
85+
# Start the socket listener thread
86+
threading.Thread(target=socket_listener_thread, daemon=True).start()
87+
88+
running = True
89+
while running:
90+
for event in pygame.event.get():
91+
if event.type == pygame.QUIT:
92+
running = False
93+
94+
# Clear screen.
95+
screen.fill((161, 168, 175))
96+
97+
# Draw and rotate components, with left wheel behind hub.
98+
blit_rotate_at_center(screen, wheel_left, (1242, 156), -angles[0])
99+
screen.blit(hub, (0, 0))
100+
blit_rotate_at_center(screen, wheel_right, (1067, 331), angles[1])
101+
blit_rotate_at_center(screen, shaft, (247, 712), angles[4])
102+
blit_rotate_at_center(screen, stall, (245, 189), angles[2])
103+
blit_rotate_at_center(screen, gear_drive, (943, 694), -angles[5])
104+
blit_rotate_at_center(screen, gear_follow, (1037, 694), angles[5] / 3)
105+
106+
# Update display
107+
pygame.display.flip()
108+
clock.tick(FPS)
109+
110+
pygame.quit()
111+
112+
113+
if __name__ == "__main__":
114+
main()

lib/pbio/test/animator/data_parser.py

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,38 @@
44
# driver outputs it at intervals of 40 ms (25 fps).
55

66
from collections import namedtuple
7+
import socket
78

8-
# Get live output from process.
9-
with open("output.txt", "w") as f:
10-
while True:
11-
try:
12-
data = input()
13-
except EOFError:
14-
break
15-
f.write(data)
9+
HOST = "127.0.0.1"
10+
PORT = 5002
11+
12+
angles = []
13+
14+
print(f"Listening on {HOST}:{PORT}")
15+
16+
17+
# Wait for a connection once and writes results when simulator disconnects.
18+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
19+
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
20+
s.bind((HOST, PORT))
21+
s.listen(1)
22+
conn, addr = s.accept()
23+
print(f"Client connected: {addr}")
24+
with conn:
25+
# receive until client disconnects
26+
buffer = b""
27+
while True:
28+
data = conn.recv(1024)
29+
if not data:
30+
print("Client disconnected.")
31+
break
32+
buffer += data
33+
while b"\n" in buffer:
34+
line, buffer = buffer.split(b"\n", 1)
35+
values = list(map(int, line.decode().strip().split()))
36+
print("Angles:", values)
37+
angles.append(values)
1638

17-
# Load angle data from file
18-
with open("output.txt", "r") as f:
19-
angles = [line.split(" ") for line in f]
2039

2140
if len(angles) < 1:
2241
exit()
@@ -36,7 +55,7 @@
3655
)
3756

3857
# Write the CSS component with frames.
39-
with open("../results/frames.css", "w") as frame_file:
58+
with open("lib/pbio/test/results/frames.css", "w") as frame_file:
4059
for info in FRAME_INFO:
4160
# CSS rows for each frame.
4261
frames = "".join(

pyproject.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ package-mode = false
1010
python = ">=3.10,<4.0"
1111
freetype-py = "^2.5.1"
1212

13-
1413
[tool.poetry.group.dev.dependencies]
1514
eventlet = "^0.40.3"
1615
flake8 = "^3.8.3"

0 commit comments

Comments
 (0)