Skip to content

Commit 019f427

Browse files
committed
pbio/sys/hmi: Split Prime HMI from Powered Up.
It gets a bit tedious to keep them in sync, and the interface for the basic hubs is expected to stay mostly unchanged.
1 parent 5df8453 commit 019f427

File tree

5 files changed

+311
-136
lines changed

5 files changed

+311
-136
lines changed

bricks/_common/sources.mk

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,7 @@ PBIO_SRC_C = $(addprefix lib/pbio/,\
245245
sys/core.c \
246246
sys/hmi_env_mpy.c \
247247
sys/hmi_lcd.c \
248+
sys/hmi_prime.c \
248249
sys/hmi_pup.c \
249250
sys/hmi_none.c \
250251
sys/host.c \

lib/pbio/platform/prime_hub/pbsysconfig.h

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,8 @@
1111
#define PBSYS_CONFIG_HMI (1)
1212
#define PBSYS_CONFIG_HMI_STOP_BUTTON (1 << 5) // center
1313
#define PBSYS_CONFIG_HMI_NUM_SLOTS (5)
14-
#define PBSYS_CONFIG_HMI_PUP (1)
15-
#define PBSYS_CONFIG_HMI_PUP_LIGHT_MATRIX_INDEX (0)
16-
#define PBSYS_CONFIG_HMI_PUP_BLUETOOTH_BUTTON (1 << 9) // right up
17-
#define PBSYS_CONFIG_HMI_PUP_LEFT_RIGHT_ENABLE (1)
14+
#define PBSYS_CONFIG_HMI_PRIME (1)
15+
#define PBSYS_CONFIG_HMI_BLUETOOTH_BUTTON (1 << 9) // right up
1816
#define PBSYS_CONFIG_HUB_LIGHT_MATRIX (1)
1917
#define PBSYS_CONFIG_HUB_LIGHT_MATRIX_LED_ARRAY (1)
2018
#define PBSYS_CONFIG_HOST (1)

lib/pbio/sys/hmi_prime.c

Lines changed: 301 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,301 @@
1+
// SPDX-License-Identifier: MIT
2+
// Copyright (c) 2018-2025 The Pybricks Authors
3+
4+
// Provides Human Machine Interface (HMI) between hub and user for Spike Prime
5+
// and Mindstorms Robot Inventor.
6+
7+
#include <pbsys/config.h>
8+
9+
#if PBSYS_CONFIG_HMI_PRIME
10+
11+
#include <stdbool.h>
12+
#include <stddef.h>
13+
#include <stdint.h>
14+
15+
#include <pbdrv/bluetooth.h>
16+
#include <pbdrv/led.h>
17+
#include <pbdrv/usb.h>
18+
19+
#include <pbio/button.h>
20+
#include <pbio/busy_count.h>
21+
#include <pbio/light_matrix.h>
22+
#include <pbio/os.h>
23+
#include <pbsys/host.h>
24+
#include <pbsys/light.h>
25+
#include <pbsys/main.h>
26+
#include <pbsys/status.h>
27+
#include <pbsys/storage_settings.h>
28+
29+
#include "hmi.h"
30+
31+
#define DEBUG 0
32+
33+
#if DEBUG
34+
#include <pbio/debug.h>
35+
#define DEBUG_PRINT pbio_debug
36+
#else
37+
#define DEBUG_PRINT(...)
38+
#endif
39+
40+
pbio_light_matrix_t *pbsys_hub_light_matrix;
41+
42+
/**
43+
* Displays the idle UI. Has a square stop sign and selected slot on bottom row.
44+
*
45+
* @param brightness Brightness (0--100%).
46+
*/
47+
static void light_matrix_show_idle_ui(uint8_t brightness) {
48+
for (uint8_t r = 0; r < PBSYS_CONFIG_HMI_NUM_SLOTS; r++) {
49+
for (uint8_t c = 0; c < PBSYS_CONFIG_HMI_NUM_SLOTS; c++) {
50+
bool is_on = r < 3 && c > 0 && c < 4;
51+
is_on |= (r == 4 && c == pbsys_status_get_selected_slot());
52+
pbio_light_matrix_set_pixel(pbsys_hub_light_matrix, r, c, is_on ? brightness : 0, true);
53+
}
54+
}
55+
}
56+
57+
/**
58+
* Bootup and shutdown animation. This is a process rather than an animation
59+
* process so we can await completion, and later add sound.
60+
*/
61+
static pbio_error_t boot_animation_process_boot_thread(pbio_os_state_t *state, void *context) {
62+
63+
static pbio_os_timer_t timer;
64+
static uint8_t step;
65+
66+
const int num_steps = 10;
67+
68+
// Makes the brightness increment or decrement.
69+
bool booting = context;
70+
71+
PBIO_OS_ASYNC_BEGIN(state);
72+
73+
for (step = 1; step <= num_steps; step++) {
74+
uint8_t brightness = booting ? step * (100 / num_steps) : 100 - (100 / num_steps) * step;
75+
light_matrix_show_idle_ui(brightness);
76+
PBIO_OS_AWAIT_MS(state, &timer, 200 / num_steps);
77+
}
78+
pbio_busy_count_down();
79+
80+
PBIO_OS_ASYNC_END(PBIO_SUCCESS);
81+
}
82+
83+
// Boot and shutdown animation are the same.
84+
static pbio_os_process_func_t boot_animation_process_shutdown_thread = boot_animation_process_boot_thread;
85+
86+
/**
87+
* Animation frame for program running animation.
88+
*/
89+
static uint32_t pbio_light_matrix_5x5_spinner_animation_next(pbio_light_animation_t *animation) {
90+
91+
// The indexes of pixels to light up
92+
static const uint8_t indexes[] = { 1, 2, 3, 8, 13, 12, 11, 6 };
93+
94+
// Each pixel has a repeating brightness pattern of the form /\_ through
95+
// which we can cycle in 256 steps.
96+
static uint8_t cycle = 0;
97+
98+
for (size_t i = 0; i < PBIO_ARRAY_SIZE(indexes); i++) {
99+
// The pixels are spread equally across the pattern.
100+
uint8_t offset = cycle + i * (UINT8_MAX / PBIO_ARRAY_SIZE(indexes));
101+
uint8_t brightness = offset > 200 ? 0 : (offset < 100 ? offset : 200 - offset);
102+
103+
// Set the brightness for this pixel
104+
pbio_light_matrix_set_pixel(pbsys_hub_light_matrix, indexes[i] / 5, indexes[i] % 5, brightness, false);
105+
}
106+
// This increment controls the speed of the pattern
107+
cycle += 9;
108+
109+
return 40;
110+
}
111+
112+
static void light_matrix_start_run_animation(void) {
113+
// Central pixel in spinner is off and will not be updated.
114+
pbio_light_matrix_set_pixel(pbsys_hub_light_matrix, 1, 2, 0, true);
115+
pbio_light_animation_init(&pbsys_hub_light_matrix->animation, pbio_light_matrix_5x5_spinner_animation_next);
116+
pbio_light_animation_start(&pbsys_hub_light_matrix->animation);
117+
}
118+
119+
static pbio_os_process_t boot_animation_process;
120+
121+
void pbsys_hmi_init(void) {
122+
pbio_busy_count_up();
123+
pbio_error_t err = pbio_light_matrix_get_dev(0, 5, &pbsys_hub_light_matrix);
124+
if (err != PBIO_SUCCESS) {
125+
// Effectively stopping boot if we can't get hardware.
126+
return;
127+
}
128+
pbio_os_process_start(&boot_animation_process, boot_animation_process_boot_thread, (void *)true);
129+
}
130+
131+
void pbsys_hmi_deinit(void) {
132+
pbsys_status_clear(PBIO_PYBRICKS_STATUS_BLE_ADVERTISING);
133+
pbsys_status_clear(PBIO_PYBRICKS_STATUS_BLE_HOST_CONNECTED);
134+
pbsys_status_clear(PBIO_PYBRICKS_STATUS_USB_HOST_CONNECTED);
135+
136+
pbio_busy_count_up();
137+
pbio_os_process_start(&boot_animation_process, boot_animation_process_shutdown_thread, (void *)false);
138+
}
139+
140+
/**
141+
* The HMI is a loop running the following steps:
142+
*
143+
* - Wait for any buttons to be released in case they were pressed
144+
* - Wait for a button press, external program start, while monitoring idle
145+
* timeout and BLE advertising.
146+
* - If valid program requested, break out of loop to start program.
147+
* Otherwise, update state based on what happened and start over.
148+
* - After leaving the loop, wait for all buttons to be released.
149+
*
150+
* The three waiting operations are cancelled if poweroff is requested.
151+
*/
152+
static pbio_error_t run_ui(pbio_os_state_t *state) {
153+
154+
static pbio_os_state_t sub;
155+
156+
static pbio_os_timer_t idle_timer;
157+
static pbio_os_timer_t input_timer;
158+
159+
PBIO_OS_ASYNC_BEGIN(state);
160+
161+
pbio_os_timer_set(&idle_timer, PBSYS_CONFIG_HMI_IDLE_TIMEOUT_MS);
162+
163+
for (;;) {
164+
165+
DEBUG_PRINT("Start HMI loop\n");
166+
167+
// Visually indicate current state on supported hubs.
168+
light_matrix_show_idle_ui(100);
169+
170+
// Buttons could be pressed at the end of the user program, so wait for
171+
// a release and then a new press, or until we have to exit early.
172+
DEBUG_PRINT("Waiting for initial button release.\n");
173+
PBIO_OS_AWAIT_WHILE(state, ({
174+
if (pbsys_status_test(PBIO_PYBRICKS_STATUS_SHUTDOWN_REQUEST)) {
175+
return PBIO_ERROR_CANCELED;
176+
}
177+
pbdrv_button_get_pressed();
178+
}));
179+
180+
DEBUG_PRINT("Start waiting for input.\n");
181+
182+
// Wait on a button, external program start, or connection change. Stop
183+
// waiting on timeout or shutdown.
184+
while (!pbdrv_button_get_pressed() && !pbsys_main_program_start_is_requested()) {
185+
186+
// Shutdown may be requested by a background process such as critical
187+
// battery or holding the power button.
188+
if (pbsys_status_test(PBIO_PYBRICKS_STATUS_SHUTDOWN_REQUEST)) {
189+
return PBIO_ERROR_CANCELED;
190+
}
191+
192+
// Exit on timeout except while connected to host.
193+
if (pbsys_host_is_connected()) {
194+
pbio_os_timer_reset(&idle_timer);
195+
} else if (pbio_os_timer_is_expired(&idle_timer)) {
196+
return PBIO_ERROR_TIMEDOUT;
197+
}
198+
199+
// Advertise if BLE enabled and there is no host connection.
200+
bool advertise = pbsys_storage_settings_bluetooth_enabled_get() && !pbsys_host_is_connected();
201+
if (advertise) {
202+
pbsys_status_set(PBIO_PYBRICKS_STATUS_BLE_ADVERTISING);
203+
} else {
204+
pbsys_status_clear(PBIO_PYBRICKS_STATUS_BLE_ADVERTISING);
205+
}
206+
pbdrv_bluetooth_start_advertising(advertise);
207+
PBIO_OS_AWAIT(state, &sub, pbdrv_bluetooth_await_advertise_or_scan_command(&sub, NULL));
208+
209+
// Don't block the loop.
210+
PBIO_OS_AWAIT_MS(state, &input_timer, 10);
211+
}
212+
213+
// External program request takes precedence over buttons.
214+
if (pbsys_main_program_start_is_requested()) {
215+
DEBUG_PRINT("Start program from Pybricks Code.\n");
216+
break;
217+
}
218+
219+
// Toggle Bluetooth enable setting if Bluetooth button pressed. Only when disconnected.
220+
if ((pbdrv_button_get_pressed() & PBSYS_CONFIG_HMI_BLUETOOTH_BUTTON) && !pbsys_host_is_connected()) {
221+
pbsys_storage_settings_bluetooth_enabled_set(!pbsys_storage_settings_bluetooth_enabled_get());
222+
DEBUG_PRINT("Toggling BLE advertising to: %s. \n", pbsys_storage_settings_bluetooth_enabled_get() ? "on" : "off");
223+
continue;
224+
}
225+
226+
// On right, increment slot when possible, then start waiting on new inputs.
227+
if (pbdrv_button_get_pressed() & PBIO_BUTTON_RIGHT) {
228+
pbsys_status_increment_selected_slot(true);
229+
continue;
230+
}
231+
// On left, decrement slot when possible, then start waiting on new inputs.
232+
if (pbdrv_button_get_pressed() & PBIO_BUTTON_LEFT) {
233+
pbsys_status_increment_selected_slot(false);
234+
continue;
235+
}
236+
237+
// On center, attempt to start program.
238+
if (pbdrv_button_get_pressed() & PBIO_BUTTON_CENTER) {
239+
pbio_error_t err = pbsys_main_program_request_start(pbsys_status_get_selected_slot(), PBSYS_MAIN_PROGRAM_START_REQUEST_TYPE_HUB_UI);
240+
if (err == PBIO_SUCCESS) {
241+
DEBUG_PRINT("Start program with button\n");
242+
break;
243+
} else {
244+
DEBUG_PRINT("Requested program not available.\n");
245+
// We can run an animation here to indicate that the program is not available.
246+
}
247+
}
248+
249+
DEBUG_PRINT("No valid action selected, start over.\n");
250+
}
251+
252+
// Stop advertising if we are still doing so.
253+
DEBUG_PRINT("Stop advertising on HMI exit.\n");
254+
pbdrv_bluetooth_start_advertising(false);
255+
PBIO_OS_AWAIT(state, &sub, pbdrv_bluetooth_await_advertise_or_scan_command(&sub, NULL));
256+
257+
// Wait for all buttons to be released so the user doesn't accidentally
258+
// push their robot off course.
259+
DEBUG_PRINT("Waiting for final button release.\n");
260+
PBIO_OS_AWAIT_WHILE(state, ({
261+
if (pbsys_status_test(PBIO_PYBRICKS_STATUS_SHUTDOWN_REQUEST)) {
262+
return PBIO_ERROR_CANCELED;
263+
}
264+
pbdrv_button_get_pressed();
265+
}));
266+
267+
// We have already stopped advertising above, but it is nicer to keep the
268+
// visual active until you release the button. Otherwise, it appears as if
269+
// the hub is already off when you are shutting down.
270+
pbsys_status_clear(PBIO_PYBRICKS_STATUS_BLE_ADVERTISING);
271+
272+
// Start run animations
273+
light_matrix_start_run_animation();
274+
pbio_color_light_off(pbsys_status_light_main);
275+
276+
PBIO_OS_ASYNC_END(PBIO_SUCCESS);
277+
}
278+
279+
/**
280+
* Drives all processes while we wait for user input. This completes when a
281+
* user program request is made using the buttons or by a connected host.
282+
*
283+
* @return Error code.
284+
* ::PBIO_SUCCESS when a program is selected.
285+
* ::PBIO_ERROR_CANCELED when selection was cancelled by shutdown request.
286+
* ::PBIO_ERROR_TIMEDOUT when there was no user interaction for a long time.
287+
*/
288+
pbio_error_t pbsys_hmi_await_program_selection(void) {
289+
290+
pbio_os_state_t state = 0;
291+
292+
pbio_error_t err;
293+
while ((err = run_ui(&state)) == PBIO_ERROR_AGAIN) {
294+
// run all processes and wait for next event.
295+
pbio_os_run_processes_and_wait_for_event();
296+
}
297+
DEBUG_PRINT("Finished program selection with status: %d\n", err);
298+
return err;
299+
}
300+
301+
#endif // PBSYS_CONFIG_HMI_PRIME

0 commit comments

Comments
 (0)