From c5b463dead43777b8f6818c97ec2441119d06f6d Mon Sep 17 00:00:00 2001 From: Laurens Valk Date: Tue, 14 Oct 2025 12:36:08 +0200 Subject: [PATCH 01/26] bricks: Add simhub for testing embedded ports on CI. The existing VirtualHub works well enough for MicroPython coverage testing, but it is too far removed from the other Pybricks ports to be used for everyday debugging. This introduces a new port based on the minimal port for running on Unix systems. Currently supports the REPL and works well with the motor simulation and animation. It uses a simulated Bluetooth driver to provide stdio, closely mimicing the behavior or of the real hubs. Can also be used to debug importing, running mpy files, and so on. --- .vscode/c_cpp_properties.json | 25 +++- .vscode/launch.json | 32 +++++ .vscode/tasks.json | 8 ++ Makefile | 12 +- bricks/_common/arm_none_eabi.mk | 48 +++++++- bricks/simhub/Makefile | 10 ++ bricks/simhub/manifest.py | 1 + bricks/simhub/mpconfigport.h | 56 +++++++++ bricks/simhub/pbio_os_hook.c | 36 ++++++ bricks/simhub/qstrdefsport.h | 2 + lib/pbio/platform/sim_hub/contiki-conf.h | 18 +++ lib/pbio/platform/sim_hub/pbdrvconfig.h | 39 ++++++ lib/pbio/platform/sim_hub/pbio_os_config.h | 9 ++ lib/pbio/platform/sim_hub/pbioconfig.h | 29 +++++ lib/pbio/platform/sim_hub/pbsysconfig.h | 29 +++++ lib/pbio/platform/sim_hub/platform.c | 132 +++++++++++++++++++++ lib/pbio/platform/sim_hub/startup.s | 1 + 17 files changed, 479 insertions(+), 8 deletions(-) create mode 100644 bricks/simhub/Makefile create mode 100644 bricks/simhub/manifest.py create mode 100644 bricks/simhub/mpconfigport.h create mode 100644 bricks/simhub/pbio_os_hook.c create mode 100644 bricks/simhub/qstrdefsport.h create mode 100644 lib/pbio/platform/sim_hub/contiki-conf.h create mode 100644 lib/pbio/platform/sim_hub/pbdrvconfig.h create mode 100644 lib/pbio/platform/sim_hub/pbio_os_config.h create mode 100644 lib/pbio/platform/sim_hub/pbioconfig.h create mode 100644 lib/pbio/platform/sim_hub/pbsysconfig.h create mode 100644 lib/pbio/platform/sim_hub/platform.c create mode 100644 lib/pbio/platform/sim_hub/startup.s diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json index 2ff599d39..8ee514718 100644 --- a/.vscode/c_cpp_properties.json +++ b/.vscode/c_cpp_properties.json @@ -386,7 +386,30 @@ "UNIX" ], "cStandard": "c11" - } + }, + { + "name": "simhub", + "includePath": [ + "${workspaceFolder}/bricks/simhub", + "${workspaceFolder}/bricks/simhub/build", + "${workspaceFolder}/lib/contiki-core", + "${workspaceFolder}/lib/lego", + "${workspaceFolder}/lib/lwrb/src/include", + "${workspaceFolder}/lib/pbio", + "${workspaceFolder}/lib/pbio/include", + "${workspaceFolder}/lib/pbio/platform/sim_hub", + "${workspaceFolder}/micropython", + "${workspaceFolder}", + "/usr/include/python3.10" + ], + "defines": [ + "MICROPY_MODULE_FROZEN_MPY", + "MICROPY_USE_READLINE=1" + ], + "compilerPath": "/usr/bin/gcc", + "cStandard": "c11", + "intelliSenseMode": "gcc-x64" + }, ], "version": 4 } diff --git a/.vscode/launch.json b/.vscode/launch.json index 9612447dc..66b29c283 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -92,6 +92,38 @@ "program":"${workspaceFolder}/lib/pbio/platform/virtual_hub/animation.py", "console":"integratedTerminal" }, + { + "name": "simhub", + "type": "cppdbg", + "request": "launch", + "program": "${workspaceFolder}/bricks/simhub/build/firmware.elf", + "args": [ + "${workspaceFolder}/tests/virtualhub/motor/car.py" + ], + "stopAtEntry": false, + "cwd": "${workspaceFolder}", + "environment": [ + { + "name": "PBIO_TEST_CONNECT_SOCKET", + "value": "true" + }, + ], + "externalConsole": false, + "MIMode": "gdb", + "setupCommands": [ + { + "description": "Enable pretty-printing for gdb", + "text": "-enable-pretty-printing", + "ignoreFailures": true + }, + { + "description": "Ignore timer signal", + "text": "handle SIG34 noprint pass", + "ignoreFailures": false + } + ], + "preLaunchTask": "build simhub" + }, { "name": "test-pbio", "type": "cppdbg", diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 8f1f37938..1431ad03d 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -41,6 +41,14 @@ "type": "shell", "command": "poetry run make -C bricks/virtualhub DEBUG=1 COPT=-O0 CROSS_COMPILE= -j" }, + { + "options": { + "cwd": "${workspaceFolder}/bricks/simhub" + }, + "label": "build simhub", + "type": "shell", + "command": "poetry run make DEBUG=1 COPT=-O0 -j", + }, { "label": "build test-pbio", "type": "shell", diff --git a/Makefile b/Makefile index 002bbcc47..d096ce9c8 100644 --- a/Makefile +++ b/Makefile @@ -28,9 +28,9 @@ doc: clean-doc: @$(MAKE) -C lib/pbio/doc clean -all: movehub cityhub technichub primehub essentialhub virtualhub nxt ev3 doc +all: movehub cityhub technichub primehub essentialhub virtualhub simhub nxt ev3 doc -clean-all: clean-movehub clean-cityhub clean-technichub clean-primehub clean-essentialhub clean-virtualhub clean-nxt clean-ev3 clean-doc +clean-all: clean-movehub clean-cityhub clean-technichub clean-primehub clean-essentialhub clean-virtualhub clean-simhub clean-nxt clean-ev3 clean-doc ev3: mpy-cross @$(MAKE) -C bricks/ev3 @@ -81,6 +81,14 @@ clean-virtualhub: clean-mpy-cross @$(MAKE) -C bricks/virtualhub clean CROSS_COMPILE= @$(MAKE) -C bricks/virtualhub clean DEBUG=1 +simhub: mpy-cross + @$(MAKE) -C bricks/simhub CROSS_COMPILE= + +clean-simhub: clean-mpy-cross + @$(MAKE) -C bricks/simhub clean CROSS_COMPILE= + @$(MAKE) -C bricks/simhub clean DEBUG=1 + + mpy-cross: @$(MAKE) -C micropython/mpy-cross CROSS_COMPILE=$(HOST_CROSS_COMPILE) diff --git a/bricks/_common/arm_none_eabi.mk b/bricks/_common/arm_none_eabi.mk index 764e55fff..9406e2e60 100644 --- a/bricks/_common/arm_none_eabi.mk +++ b/bricks/_common/arm_none_eabi.mk @@ -101,8 +101,6 @@ MICROPY_ROM_TEXT_COMPRESSION ?= 1 include $(TOP)/py/py.mk include $(TOP)/extmod/extmod.mk -CROSS_COMPILE ?= arm-none-eabi- - INC += -I. INC += -I$(TOP) ifeq ($(PB_MCU_FAMILY),STM32) @@ -155,6 +153,18 @@ OPENOCD ?= openocd OPENOCD_CONFIG ?= openocd_stm32$(PB_MCU_SERIES_LCASE).cfg TEXT0_ADDR ?= 0x08000000 +ifeq ($(PB_MCU_FAMILY),desktop) +UNAME_S := $(shell uname -s) +LD = $(CC) +CFLAGS += $(INC) -Wall -Werror -Wdouble-promotion -Wfloat-conversion -std=gnu99 $(COPT) -D_GNU_SOURCE +ifeq ($(UNAME_S),Linux) +LDFLAGS += -Wl,-Map=$@.map,--cref -Wl,--gc-sections +else ifeq ($(UNAME_S),Darwin) +LDFLAGS += -Wl,-map,$@.map -Wl,-dead_strip +endif +LIBS = +else # end desktop, begin embedded +CROSS_COMPILE ?= arm-none-eabi- ifeq ($(PB_MCU_FAMILY),STM32) CFLAGS_MCU_F0 = -mthumb -mtune=cortex-m0 -mcpu=cortex-m0 -msoft-float CFLAGS_MCU_F4 = -mthumb -mtune=cortex-m4 -mcpu=cortex-m4 -mfpu=fpv4-sp-d16 -mfloat-abi=hard @@ -200,6 +210,8 @@ endif # avoid doubles CFLAGS += -fsingle-precision-constant -Wdouble-promotion +endif # end embedded, begin common + # Tune for Debugging or Optimization ifeq ($(DEBUG), 1) CFLAGS += -Og -ggdb @@ -233,13 +245,25 @@ include $(PBTOP)/bricks/_common/sources.mk # between the top level directory and the micropython/ subdirectory. PY_EXTRA_SRC_C = $(addprefix shared/,\ - libc/string0.c \ - runtime/gchelper_native.c \ runtime/interrupt_char.c \ runtime/pyexec.c \ runtime/stdout_helpers.c \ + ) + +ifeq ($(PB_MCU_FAMILY),desktop) +PY_EXTRA_SRC_C += $(addprefix shared/,\ + runtime/gchelper_generic.c \ + ) +PY_EXTRA_SRC_C += $(addprefix bricks/simhub/,\ + pbio_os_hook.c \ + ) +else +PY_EXTRA_SRC_C += $(addprefix shared/,\ + libc/string0.c \ + runtime/gchelper_native.c \ runtime/sys_stdio_mphal.c \ ) +endif ifneq ($(PBIO_PLATFORM),move_hub) # to avoid adding unused root pointers @@ -262,7 +286,9 @@ PY_EXTRA_SRC_C += $(addprefix bricks/_common/,\ endif # Not all MCUs support thumb2 instructions. -ifeq ($(PB_MCU_SERIES),$(filter $(PB_MCU_SERIES),AT91SAM7 F0 TIAM1808)) +ifeq ($(PB_MCU_FAMILY),desktop) +SRC_S += +else ifeq ($(PB_MCU_SERIES),$(filter $(PB_MCU_SERIES),AT91SAM7 F0 TIAM1808)) SRC_S += shared/runtime/gchelper_thumb1.s else SRC_S += shared/runtime/gchelper_thumb2.s @@ -561,8 +587,12 @@ CFLAGS += -DMICROPY_MODULE_FROZEN_MPY MPY_TOOL_FLAGS += -mlongint-impl none endif +ifneq ($(PB_MCU_FAMILY),desktop) # Main firmware build targets TARGETS := $(BUILD)/firmware.zip +else +TARGETS := $(BUILD)/firmware.elf +endif all: $(TARGETS) @@ -673,4 +703,12 @@ deploy-openocd: $(BUILD)/firmware-base.bin $(ECHO) "Writing $< to the board via ST-LINK using OpenOCD" $(Q)$(OPENOCD) -f $(OPENOCD_CONFIG) -c "stm_flash $< $(TEXT0_ADDR)" +# Run emulation build on a POSIX system using normal stdio +run: $(BUILD)/firmware.elf + @$(BUILD)/firmware.elf + @echo "Exit status: $$?" + +test: $(BUILD)/firmware.elf + $(Q)/usr/bin/printf "print('hello world!', list(x+1 for x in range(10)), end='eol\\\\n')\\r\\n\\004" | $(BUILD)/firmware.elf | tail -n2 | grep "^hello world! \\[1, 2, 3, 4, 5, 6, 7, 8, 9, 10\\]eol" + include $(TOP)/py/mkrules.mk diff --git a/bricks/simhub/Makefile b/bricks/simhub/Makefile new file mode 100644 index 000000000..2ba8850a6 --- /dev/null +++ b/bricks/simhub/Makefile @@ -0,0 +1,10 @@ +# SPDX-License-Identifier: MIT +# Copyright (c) 2025 The Pybricks Authors + +PBIO_PLATFORM = sim_hub +PB_MCU_FAMILY = desktop +PB_FROZEN_MODULES = 1 +MICROPY_ROM_TEXT_COMPRESSION = 1 + + +include ../_common/arm_none_eabi.mk diff --git a/bricks/simhub/manifest.py b/bricks/simhub/manifest.py new file mode 100644 index 000000000..d901ff875 --- /dev/null +++ b/bricks/simhub/manifest.py @@ -0,0 +1 @@ +include("../_common/manifest.py") diff --git a/bricks/simhub/mpconfigport.h b/bricks/simhub/mpconfigport.h new file mode 100644 index 000000000..605eeeb53 --- /dev/null +++ b/bricks/simhub/mpconfigport.h @@ -0,0 +1,56 @@ +#include + +#define MICROPY_HW_BOARD_NAME "Desktop" +#define MICROPY_HW_MCU_NAME "Desktop" + +#define PYBRICKS_HUB_CLASS_NAME (MP_QSTR_VirtualHub) + +#define PYBRICKS_HUB_NAME "virtualhub" +#define PYBRICKS_HUB_VIRTUALHUB (1) + +// Pybricks modules +#define PYBRICKS_PY_COMMON (1) +#define PYBRICKS_PY_COMMON_BLE (0) +#define PYBRICKS_PY_COMMON_CHARGER (1) +#define PYBRICKS_PY_COMMON_COLOR_LIGHT (1) +#define PYBRICKS_PY_COMMON_CONTROL (1) +#define PYBRICKS_PY_COMMON_IMU (0) +#define PYBRICKS_PY_COMMON_KEYPAD (1) +#define PYBRICKS_PY_COMMON_KEYPAD_HUB_BUTTONS (1) +#define PYBRICKS_PY_COMMON_LIGHT_ARRAY (1) +#define PYBRICKS_PY_COMMON_LIGHT_MATRIX (0) +#define PYBRICKS_PY_COMMON_LOGGER (1) +#define PYBRICKS_PY_COMMON_LOGGER_REAL_FILE (1) +#define PYBRICKS_PY_COMMON_MOTORS (1) +#define PYBRICKS_PY_COMMON_SPEAKER (0) +#define PYBRICKS_PY_COMMON_SYSTEM (1) +#define PYBRICKS_PY_EV3DEVICES (0) +#define PYBRICKS_PY_EXPERIMENTAL (1) +#define PYBRICKS_PY_HUBS (1) +#define PYBRICKS_PY_IODEVICES (0) +#define PYBRICKS_PY_NXTDEVICES (0) +#define PYBRICKS_PY_PARAMETERS (1) +#define PYBRICKS_PY_PARAMETERS_BUTTON (1) +#define PYBRICKS_PY_PARAMETERS_ICON (0) +#define PYBRICKS_PY_PARAMETERS_IMAGE (0) +#define PYBRICKS_PY_PUPDEVICES (1) +#define PYBRICKS_PY_PUPDEVICES_REMOTE (0) +#define PYBRICKS_PY_DEVICES (1) +#define PYBRICKS_PY_ROBOTICS (1) +#define PYBRICKS_PY_ROBOTICS_DRIVEBASE_SPIKE (0) +#define PYBRICKS_PY_TOOLS (1) +#define PYBRICKS_PY_TOOLS_HUB_MENU (0) +#define PYBRICKS_PY_TOOLS_APP_DATA (1) + +// Pybricks options +#define PYBRICKS_OPT_COMPILER (1) +#define PYBRICKS_OPT_USE_STACK_END_AS_TOP (1) +#define PYBRICKS_OPT_RAW_REPL (0) +#define PYBRICKS_OPT_FLOAT (0) +#define PYBRICKS_OPT_TERSE_ERR (0) +#define PYBRICKS_OPT_EXTRA_LEVEL1 (0) +#define PYBRICKS_OPT_EXTRA_LEVEL2 (0) +#define PYBRICKS_OPT_CUSTOM_IMPORT (0) +#define PYBRICKS_OPT_NATIVE_MOD (0) + +#include "../_common/mpconfigport.h" diff --git a/bricks/simhub/pbio_os_hook.c b/bricks/simhub/pbio_os_hook.c new file mode 100644 index 000000000..a33e34fa0 --- /dev/null +++ b/bricks/simhub/pbio_os_hook.c @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2025 The Pybricks Authors + +#include +#include + +#include + +#include "py/runtime.h" +#include "py/mphal.h" + +pbio_os_irq_flags_t pbio_os_hook_disable_irq(void) { + sigset_t sigmask; + sigfillset(&sigmask); + + sigset_t origmask; + pthread_sigmask(SIG_SETMASK, &sigmask, &origmask); + return origmask; +} + +void pbio_os_hook_enable_irq(pbio_os_irq_flags_t flags) { + sigset_t origmask = (sigset_t)flags; + pthread_sigmask(SIG_SETMASK, &origmask, NULL); +} + +void pbio_os_hook_wait_for_interrupt(pbio_os_irq_flags_t flags) { + struct timespec timeout = { + .tv_sec = 0, + .tv_nsec = 100000, + }; + // "sleep" with "interrupts" enabled + sigset_t origmask = flags; + MP_THREAD_GIL_EXIT(); + pselect(0, NULL, NULL, NULL, &timeout, &origmask); + MP_THREAD_GIL_ENTER(); +} diff --git a/bricks/simhub/qstrdefsport.h b/bricks/simhub/qstrdefsport.h new file mode 100644 index 000000000..00d3e2ae3 --- /dev/null +++ b/bricks/simhub/qstrdefsport.h @@ -0,0 +1,2 @@ +// qstrs specific to this port +// *FORMAT-OFF* diff --git a/lib/pbio/platform/sim_hub/contiki-conf.h b/lib/pbio/platform/sim_hub/contiki-conf.h new file mode 100644 index 000000000..c3c18d5f3 --- /dev/null +++ b/lib/pbio/platform/sim_hub/contiki-conf.h @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2018-2022 The Pybricks Authors + +#ifndef _PBIO_CONF_H_ +#define _PBIO_CONF_H_ + +#include + +#define CCIF +#define CLIF + +typedef uint32_t clock_time_t; +#define CLOCK_CONF_SECOND 1000 + +#define clock_time pbdrv_clock_get_ms +#define clock_usecs pbdrv_clock_get_us + +#endif /* _PBIO_CONF_H_ */ diff --git a/lib/pbio/platform/sim_hub/pbdrvconfig.h b/lib/pbio/platform/sim_hub/pbdrvconfig.h new file mode 100644 index 000000000..84f873486 --- /dev/null +++ b/lib/pbio/platform/sim_hub/pbdrvconfig.h @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2022 The Pybricks Authors + +#define PBDRV_CONFIG_BATTERY (1) +#define PBDRV_CONFIG_BATTERY_TEST (1) + +#define PBDRV_CONFIG_BLOCK_DEVICE (1) +#define PBDRV_CONFIG_BLOCK_DEVICE_RAM_SIZE (10 * 1024) +#define PBDRV_CONFIG_BLOCK_DEVICE_TEST (1) + +#define PBDRV_CONFIG_BLUETOOTH (1) +#define PBDRV_CONFIG_BLUETOOTH_SIMULATION (1) + +#define PBDRV_CONFIG_BUTTON (1) +#define PBDRV_CONFIG_BUTTON_TEST (1) + +#define PBDRV_CONFIG_CLOCK (1) +#define PBDRV_CONFIG_CLOCK_LINUX (1) +#define PBDRV_CONFIG_CLOCK_LINUX_SIGNAL (1) + +#define PBDRV_CONFIG_COUNTER (1) + +#define PBDRV_CONFIG_GPIO (1) +#define PBDRV_CONFIG_GPIO_VIRTUAL (1) + +#define PBDRV_CONFIG_IOPORT (1) +#define PBDRV_CONFIG_IOPORT_NUM_DEV (6) + +#define PBDRV_CONFIG_MOTOR_DRIVER (1) +#define PBDRV_CONFIG_MOTOR_DRIVER_NUM_DEV (6) +#define PBDRV_CONFIG_MOTOR_DRIVER_VIRTUAL_SIMULATION (1) + +#define PBDRV_CONFIG_HAS_PORT_A (1) +#define PBDRV_CONFIG_HAS_PORT_B (1) +#define PBDRV_CONFIG_HAS_PORT_C (1) +#define PBDRV_CONFIG_HAS_PORT_D (1) +#define PBDRV_CONFIG_HAS_PORT_E (1) +#define PBDRV_CONFIG_HAS_PORT_F (1) +#define PBDRV_CONFIG_HAS_PORT_VCC_CONTROL (1) diff --git a/lib/pbio/platform/sim_hub/pbio_os_config.h b/lib/pbio/platform/sim_hub/pbio_os_config.h new file mode 100644 index 000000000..584366100 --- /dev/null +++ b/lib/pbio/platform/sim_hub/pbio_os_config.h @@ -0,0 +1,9 @@ +#include + +typedef sigset_t pbio_os_irq_flags_t; + +pbio_os_irq_flags_t pbio_os_hook_disable_irq(void); + +void pbio_os_hook_enable_irq(pbio_os_irq_flags_t flags); + +void pbio_os_hook_wait_for_interrupt(pbio_os_irq_flags_t flags); diff --git a/lib/pbio/platform/sim_hub/pbioconfig.h b/lib/pbio/platform/sim_hub/pbioconfig.h new file mode 100644 index 000000000..7e9960b17 --- /dev/null +++ b/lib/pbio/platform/sim_hub/pbioconfig.h @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2022 The Pybricks Authors + +#define PBIO_CONFIG_BATTERY (1) +#define PBIO_CONFIG_DCMOTOR (6) +#define PBIO_CONFIG_DCMOTOR_NUM_DEV (6) +#define PBIO_CONFIG_DRIVEBASE_SPIKE (1) +#define PBIO_CONFIG_LIGHT (0) +#define PBIO_CONFIG_LOGGER (1) +#define PBIO_CONFIG_LIGHT_MATRIX (0) +#define PBIO_CONFIG_MOTOR_PROCESS (1) +#define PBIO_CONFIG_IMU (0) +#define PBIO_CONFIG_PORT (1) +#define PBIO_CONFIG_PORT_NUM_DEV (6) +#define PBIO_CONFIG_PORT_DCM (0) +#define PBIO_CONFIG_PORT_DCM_PUP (0) +#define PBIO_CONFIG_PORT_DCM_EV3 (0) +#define PBIO_CONFIG_PORT_DCM_NUM_DEV (0) +#define PBIO_CONFIG_PORT_LUMP (0) +#define PBIO_CONFIG_PORT_LUMP_MODE_INFO (0) +#define PBIO_CONFIG_PORT_LUMP_NUM_DEV (0) +#define PBIO_CONFIG_SERVO (1) +#define PBIO_CONFIG_SERVO_NUM_DEV (6) +#define PBIO_CONFIG_SERVO_EV3_NXT (1) +#define PBIO_CONFIG_SERVO_PUP (1) +#define PBIO_CONFIG_SERVO_PUP_MOVE_HUB (1) +#define PBIO_CONFIG_TACHO (1) + +#define PBIO_CONFIG_ENABLE_SYS (1) diff --git a/lib/pbio/platform/sim_hub/pbsysconfig.h b/lib/pbio/platform/sim_hub/pbsysconfig.h new file mode 100644 index 000000000..df1d1d666 --- /dev/null +++ b/lib/pbio/platform/sim_hub/pbsysconfig.h @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2022-2023 The Pybricks Authors + +#define PBSYS_CONFIG_FEATURE_BUILTIN_USER_PROGRAM_REPL (1) +#define PBSYS_CONFIG_FEATURE_BUILTIN_USER_PROGRAM_PORT_VIEW (1) +#define PBSYS_CONFIG_FEATURE_BUILTIN_USER_PROGRAM_IMU_CALIBRATION (0) +#define PBSYS_CONFIG_FEATURE_PROGRAM_FORMAT_MULTI_MPY_V6 (1) +#define PBSYS_CONFIG_FEATURE_PROGRAM_FORMAT_MULTI_MPY_V6_3_NATIVE (0) + +#define PBSYS_CONFIG_BATTERY_CHARGER (0) +#define PBSYS_CONFIG_BLUETOOTH (1) +#define PBSYS_CONFIG_HOST (1) +#define PBSYS_CONFIG_HOST_STDIN_BUF_SIZE (21) +#define PBSYS_CONFIG_HMI (1) +#define PBSYS_CONFIG_HMI_STOP_BUTTON (1 << 5) // center +#define PBSYS_CONFIG_HMI_NONE (1) +#define PBSYS_CONFIG_HMI_NUM_SLOTS (0) +#define PBSYS_CONFIG_HUB_LIGHT_MATRIX (0) +#define PBSYS_CONFIG_MAIN (1) +#define PBSYS_CONFIG_STORAGE (1) +#define PBSYS_CONFIG_STORAGE_NUM_SLOTS (1) +#define PBSYS_CONFIG_STORAGE_USER_DATA_SIZE (512) +#define PBSYS_CONFIG_STATUS_LIGHT (0) +#define PBSYS_CONFIG_STATUS_LIGHT_BATTERY (0) +#define PBSYS_CONFIG_STATUS_LIGHT_BLUETOOTH (0) +#define PBSYS_CONFIG_STATUS_LIGHT_STATE_ANIMATIONS (1) +#define PBSYS_CONFIG_STATUS_LIGHT_STATE_ANIMATIONS_HUE (240) +#define PBSYS_CONFIG_USER_PROGRAM (0) +#define PBSYS_CONFIG_PROGRAM_STOP (0) diff --git a/lib/pbio/platform/sim_hub/platform.c b/lib/pbio/platform/sim_hub/platform.c new file mode 100644 index 000000000..c6722bfa9 --- /dev/null +++ b/lib/pbio/platform/sim_hub/platform.c @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2023 The Pybricks Authors + +#include "../../drv/motor_driver/motor_driver_virtual_simulation.h" + +#include "pbio_os_config.h" + +#include +#include +#include + +const pbdrv_gpio_t pbdrv_ioport_platform_data_vcc_pin = { + .bank = NULL, + .pin = 0, +}; + +const pbdrv_ioport_platform_data_t pbdrv_ioport_platform_data[PBDRV_CONFIG_IOPORT_NUM_DEV] = { + { + .port_id = PBIO_PORT_ID_A, + .motor_driver_index = 0, + .counter_driver_index = 0, + .external_port_index = 0, + .i2c_driver_index = PBDRV_IOPORT_INDEX_NOT_AVAILABLE, + .uart_driver_index = PBDRV_IOPORT_INDEX_NOT_AVAILABLE, + .pins = NULL, + .supported_modes = PBIO_PORT_MODE_QUADRATURE, + }, + { + .port_id = PBIO_PORT_ID_B, + .motor_driver_index = 1, + .counter_driver_index = 1, + .external_port_index = 1, + .i2c_driver_index = PBDRV_IOPORT_INDEX_NOT_AVAILABLE, + .uart_driver_index = PBDRV_IOPORT_INDEX_NOT_AVAILABLE, + .pins = NULL, + .supported_modes = PBIO_PORT_MODE_QUADRATURE, + }, + { + .port_id = PBIO_PORT_ID_C, + .motor_driver_index = 2, + .external_port_index = 2, + .counter_driver_index = 2, + .i2c_driver_index = PBDRV_IOPORT_INDEX_NOT_AVAILABLE, + .uart_driver_index = PBDRV_IOPORT_INDEX_NOT_AVAILABLE, + .pins = NULL, + .supported_modes = PBIO_PORT_MODE_QUADRATURE, + }, + { + .port_id = PBIO_PORT_ID_D, + .motor_driver_index = 3, + .external_port_index = 3, + .counter_driver_index = 3, + .i2c_driver_index = PBDRV_IOPORT_INDEX_NOT_AVAILABLE, + .uart_driver_index = PBDRV_IOPORT_INDEX_NOT_AVAILABLE, + .pins = NULL, + .supported_modes = PBIO_PORT_MODE_QUADRATURE, + }, + { + .port_id = PBIO_PORT_ID_E, + .motor_driver_index = 4, + .external_port_index = 4, + .counter_driver_index = 4, + .i2c_driver_index = PBDRV_IOPORT_INDEX_NOT_AVAILABLE, + .uart_driver_index = PBDRV_IOPORT_INDEX_NOT_AVAILABLE, + .pins = NULL, + .supported_modes = PBIO_PORT_MODE_QUADRATURE, + }, + { + .port_id = PBIO_PORT_ID_F, + .motor_driver_index = 5, + .external_port_index = 5, + .counter_driver_index = 5, + .i2c_driver_index = PBDRV_IOPORT_INDEX_NOT_AVAILABLE, + .uart_driver_index = PBDRV_IOPORT_INDEX_NOT_AVAILABLE, + .pins = NULL, + .supported_modes = PBIO_PORT_MODE_QUADRATURE, + }, +}; + +#define INFINITY (1e100) + +const pbdrv_motor_driver_virtual_simulation_platform_data_t + pbdrv_motor_driver_virtual_simulation_platform_data[PBDRV_CONFIG_MOTOR_DRIVER_NUM_DEV] = { + { + .port_id = PBIO_PORT_ID_A, + .type_id = LEGO_DEVICE_TYPE_ID_SPIKE_M_MOTOR, + .initial_angle = 123456, + .initial_speed = 0, + .endstop_angle_negative = -INFINITY, + .endstop_angle_positive = INFINITY, + }, + { + .port_id = PBIO_PORT_ID_B, + .type_id = LEGO_DEVICE_TYPE_ID_SPIKE_M_MOTOR, + .initial_angle = 0, + .initial_speed = 0, + .endstop_angle_negative = -INFINITY, + .endstop_angle_positive = INFINITY, + }, + { + .port_id = PBIO_PORT_ID_C, + .type_id = LEGO_DEVICE_TYPE_ID_SPIKE_L_MOTOR, + .initial_angle = 0, + .initial_speed = 0, + .endstop_angle_negative = -142000, + .endstop_angle_positive = 142000, + }, + { + .port_id = PBIO_PORT_ID_D, + .type_id = LEGO_DEVICE_TYPE_ID_NONE, + .initial_angle = 0, + .initial_speed = 0, + .endstop_angle_negative = -INFINITY, + .endstop_angle_positive = INFINITY, + }, + { + .port_id = PBIO_PORT_ID_E, + .type_id = LEGO_DEVICE_TYPE_ID_SPIKE_S_MOTOR, + .initial_angle = 0, + .initial_speed = 0, + .endstop_angle_negative = -INFINITY, + .endstop_angle_positive = INFINITY, + }, + { + .port_id = PBIO_PORT_ID_F, + .type_id = LEGO_DEVICE_TYPE_ID_SPIKE_L_MOTOR, + .initial_angle = 45000, + .initial_speed = 0, + .endstop_angle_negative = -INFINITY, + .endstop_angle_positive = INFINITY, + }, +}; diff --git a/lib/pbio/platform/sim_hub/startup.s b/lib/pbio/platform/sim_hub/startup.s new file mode 100644 index 000000000..d799fb2d4 --- /dev/null +++ b/lib/pbio/platform/sim_hub/startup.s @@ -0,0 +1 @@ +// not used From a66cbf70483b3e60f3a4402d36d63c95f9e1508b Mon Sep 17 00:00:00 2001 From: Laurens Valk Date: Tue, 14 Oct 2025 13:07:41 +0200 Subject: [PATCH 02/26] bricks/_common: Rename common makefile. This is not just for arm_none_eabi anymore. --- bricks/_common/{arm_none_eabi.mk => common.mk} | 2 +- bricks/cityhub/Makefile | 2 +- bricks/essentialhub/Makefile | 2 +- bricks/ev3/Makefile | 2 +- bricks/movehub/Makefile | 2 +- bricks/nxt/Makefile | 2 +- bricks/primehub/Makefile | 2 +- bricks/simhub/Makefile | 2 +- bricks/technichub/Makefile | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) rename bricks/_common/{arm_none_eabi.mk => common.mk} (99%) diff --git a/bricks/_common/arm_none_eabi.mk b/bricks/_common/common.mk similarity index 99% rename from bricks/_common/arm_none_eabi.mk rename to bricks/_common/common.mk index 9406e2e60..1a73fd373 100644 --- a/bricks/_common/arm_none_eabi.mk +++ b/bricks/_common/common.mk @@ -5,7 +5,7 @@ # This file is shared by all bare-metal Arm Pybricks ports. THIS_MAKEFILE := $(lastword $(MAKEFILE_LIST)) -PBTOP := ../$(patsubst %/_common/arm_none_eabi.mk,%,$(THIS_MAKEFILE)) +PBTOP := ../$(patsubst %/_common/common.mk,%,$(THIS_MAKEFILE)) # Bricks must specify the following variables in their Makefile diff --git a/bricks/cityhub/Makefile b/bricks/cityhub/Makefile index d854757b8..feb1254cd 100644 --- a/bricks/cityhub/Makefile +++ b/bricks/cityhub/Makefile @@ -10,4 +10,4 @@ PB_LIB_STM32_HAL = 1 PB_LIB_BLE5STACK = 1 PB_FROZEN_MODULES = 1 -include ../_common/arm_none_eabi.mk +include ../_common/common.mk diff --git a/bricks/essentialhub/Makefile b/bricks/essentialhub/Makefile index 1d76c8370..ceb1cef8a 100644 --- a/bricks/essentialhub/Makefile +++ b/bricks/essentialhub/Makefile @@ -16,4 +16,4 @@ DFU_VID = 0x0694 DFU_PID = 0x000C PB_FROZEN_MODULES = 1 -include ../_common/arm_none_eabi.mk +include ../_common/common.mk diff --git a/bricks/ev3/Makefile b/bricks/ev3/Makefile index f1ceafa43..9534c729c 100644 --- a/bricks/ev3/Makefile +++ b/bricks/ev3/Makefile @@ -3,4 +3,4 @@ PB_MCU_FAMILY = TIAM1808 PB_LIB_UMM_MALLOC = 1 -include ../_common/arm_none_eabi.mk +include ../_common/common.mk diff --git a/bricks/movehub/Makefile b/bricks/movehub/Makefile index b5a15c9f4..2974f30c3 100644 --- a/bricks/movehub/Makefile +++ b/bricks/movehub/Makefile @@ -11,4 +11,4 @@ PB_LIB_BLUENRG = 1 PB_FROZEN_MODULES = 0 MICROPY_ROM_TEXT_COMPRESSION = 0 # Needed for PYBRICKS_OPT_TERSE_ERR -include ../_common/arm_none_eabi.mk +include ../_common/common.mk diff --git a/bricks/nxt/Makefile b/bricks/nxt/Makefile index 2717bf18b..d5113b754 100644 --- a/bricks/nxt/Makefile +++ b/bricks/nxt/Makefile @@ -4,4 +4,4 @@ PBIO_PLATFORM = nxt PB_MCU_FAMILY = AT91SAM7 -include ../_common/arm_none_eabi.mk +include ../_common/common.mk diff --git a/bricks/primehub/Makefile b/bricks/primehub/Makefile index 48aaab636..33e867158 100644 --- a/bricks/primehub/Makefile +++ b/bricks/primehub/Makefile @@ -16,4 +16,4 @@ DFU_VID = 0x0694 DFU_PID = 0x0008 PB_FROZEN_MODULES = 1 -include ../_common/arm_none_eabi.mk +include ../_common/common.mk diff --git a/bricks/simhub/Makefile b/bricks/simhub/Makefile index 2ba8850a6..a3c334115 100644 --- a/bricks/simhub/Makefile +++ b/bricks/simhub/Makefile @@ -7,4 +7,4 @@ PB_FROZEN_MODULES = 1 MICROPY_ROM_TEXT_COMPRESSION = 1 -include ../_common/arm_none_eabi.mk +include ../_common/common.mk diff --git a/bricks/technichub/Makefile b/bricks/technichub/Makefile index 7e2152458..2df580be4 100644 --- a/bricks/technichub/Makefile +++ b/bricks/technichub/Makefile @@ -11,4 +11,4 @@ PB_LIB_BLE5STACK = 1 PB_LIB_LSM6DS3TR_C = 1 PB_FROZEN_MODULES = 1 -include ../_common/arm_none_eabi.mk +include ../_common/common.mk From f35cf4af5d5f251ca6d7eb9a28912a4158cebff5 Mon Sep 17 00:00:00 2001 From: Laurens Valk Date: Tue, 14 Oct 2025 13:43:49 +0200 Subject: [PATCH 03/26] bricks/simhub: Compile option for CI clock. For CI tests, we don't want to wait on the wall clock. --- bricks/simhub/pbio_os_hook.c | 22 +++++++++++++++++++++- lib/pbio/platform/sim_hub/pbdrvconfig.h | 5 ++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/bricks/simhub/pbio_os_hook.c b/bricks/simhub/pbio_os_hook.c index a33e34fa0..62a1b5a07 100644 --- a/bricks/simhub/pbio_os_hook.c +++ b/bricks/simhub/pbio_os_hook.c @@ -9,21 +9,41 @@ #include "py/runtime.h" #include "py/mphal.h" +#if PBDRV_CONFIG_CLOCK_TEST +#include +#endif + pbio_os_irq_flags_t pbio_os_hook_disable_irq(void) { sigset_t sigmask; - sigfillset(&sigmask); + #if PBDRV_CONFIG_CLOCK_TEST + sigemptyset(&sigmask); + return sigmask; + #endif + + sigfillset(&sigmask); sigset_t origmask; pthread_sigmask(SIG_SETMASK, &sigmask, &origmask); return origmask; } void pbio_os_hook_enable_irq(pbio_os_irq_flags_t flags) { + #if PBDRV_CONFIG_CLOCK_TEST + return; + #endif + sigset_t origmask = (sigset_t)flags; pthread_sigmask(SIG_SETMASK, &origmask, NULL); } void pbio_os_hook_wait_for_interrupt(pbio_os_irq_flags_t flags) { + #if PBDRV_CONFIG_CLOCK_TEST + // All events have been handled at this time. Advance the clock + // and continue immediately. + pbio_test_clock_tick(1); + return; + #endif + struct timespec timeout = { .tv_sec = 0, .tv_nsec = 100000, diff --git a/lib/pbio/platform/sim_hub/pbdrvconfig.h b/lib/pbio/platform/sim_hub/pbdrvconfig.h index 84f873486..ddbd38556 100644 --- a/lib/pbio/platform/sim_hub/pbdrvconfig.h +++ b/lib/pbio/platform/sim_hub/pbdrvconfig.h @@ -15,8 +15,11 @@ #define PBDRV_CONFIG_BUTTON_TEST (1) #define PBDRV_CONFIG_CLOCK (1) -#define PBDRV_CONFIG_CLOCK_LINUX (1) +#ifdef PBDRV_CONFIG_CLOCK_LINUX #define PBDRV_CONFIG_CLOCK_LINUX_SIGNAL (1) +#else +#define PBDRV_CONFIG_CLOCK_TEST (1) +#endif #define PBDRV_CONFIG_COUNTER (1) From 7361ef729af6f5db3349d47197712d846141ee59 Mon Sep 17 00:00:00 2001 From: Laurens Valk Date: Tue, 14 Oct 2025 13:52:37 +0200 Subject: [PATCH 04/26] bricks/simhub: Debug locally with wall clock. Use CI for fast tests. Locally, viewing the animation only makes sense at normal speed. --- .vscode/launch.json | 4 ++-- .vscode/tasks.json | 4 ++-- Makefile | 5 +++-- bricks/simhub/.gitignore | 1 + 4 files changed, 8 insertions(+), 6 deletions(-) create mode 100644 bricks/simhub/.gitignore diff --git a/.vscode/launch.json b/.vscode/launch.json index 66b29c283..7f88b6e94 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -96,7 +96,7 @@ "name": "simhub", "type": "cppdbg", "request": "launch", - "program": "${workspaceFolder}/bricks/simhub/build/firmware.elf", + "program": "${workspaceFolder}/bricks/simhub/debug/firmware.elf", "args": [ "${workspaceFolder}/tests/virtualhub/motor/car.py" ], @@ -122,7 +122,7 @@ "ignoreFailures": false } ], - "preLaunchTask": "build simhub" + "preLaunchTask": "build simhub (debug)" }, { "name": "test-pbio", diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 1431ad03d..b5f7e9da3 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -45,9 +45,9 @@ "options": { "cwd": "${workspaceFolder}/bricks/simhub" }, - "label": "build simhub", + "label": "build simhub (debug)", "type": "shell", - "command": "poetry run make DEBUG=1 COPT=-O0 -j", + "command": "poetry run make DEBUG=1 BUILD=debug COPT=-DPBDRV_CONFIG_CLOCK_LINUX -j", }, { "label": "build test-pbio", diff --git a/Makefile b/Makefile index d096ce9c8..0a9336ce7 100644 --- a/Makefile +++ b/Makefile @@ -85,8 +85,9 @@ simhub: mpy-cross @$(MAKE) -C bricks/simhub CROSS_COMPILE= clean-simhub: clean-mpy-cross - @$(MAKE) -C bricks/simhub clean CROSS_COMPILE= - @$(MAKE) -C bricks/simhub clean DEBUG=1 + @$(MAKE) -C bricks/simhub clean + @$(MAKE) -C bricks/simhub clean + @$(MAKE) -C bricks/simhub clean BUILD=debug mpy-cross: diff --git a/bricks/simhub/.gitignore b/bricks/simhub/.gitignore new file mode 100644 index 000000000..b7443460f --- /dev/null +++ b/bricks/simhub/.gitignore @@ -0,0 +1 @@ +debug/ From ea884c0df60dd773d3bd75c1c7ae8c6d64abab66 Mon Sep 17 00:00:00 2001 From: Laurens Valk Date: Tue, 14 Oct 2025 16:01:05 +0200 Subject: [PATCH 05/26] pbio/platform/sim_hub: Enable program stop. Enables shutdown from a user program, including REPL. --- lib/pbio/platform/sim_hub/pbsysconfig.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pbio/platform/sim_hub/pbsysconfig.h b/lib/pbio/platform/sim_hub/pbsysconfig.h index df1d1d666..c842ca58d 100644 --- a/lib/pbio/platform/sim_hub/pbsysconfig.h +++ b/lib/pbio/platform/sim_hub/pbsysconfig.h @@ -26,4 +26,4 @@ #define PBSYS_CONFIG_STATUS_LIGHT_STATE_ANIMATIONS (1) #define PBSYS_CONFIG_STATUS_LIGHT_STATE_ANIMATIONS_HUE (240) #define PBSYS_CONFIG_USER_PROGRAM (0) -#define PBSYS_CONFIG_PROGRAM_STOP (0) +#define PBSYS_CONFIG_PROGRAM_STOP (1) From 28f454991809292fdda1849a00ae195acd573c3c Mon Sep 17 00:00:00 2001 From: Laurens Valk Date: Tue, 14 Oct 2025 16:46:28 +0200 Subject: [PATCH 06/26] pbio/drv/bluetooth_simulation: Clean up terminal on exit. --- lib/pbio/drv/bluetooth/bluetooth_simulation.c | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/lib/pbio/drv/bluetooth/bluetooth_simulation.c b/lib/pbio/drv/bluetooth/bluetooth_simulation.c index bfa2a4ffb..e8cbc8a5c 100644 --- a/lib/pbio/drv/bluetooth/bluetooth_simulation.c +++ b/lib/pbio/drv/bluetooth/bluetooth_simulation.c @@ -6,11 +6,12 @@ #if PBDRV_CONFIG_BLUETOOTH_SIMULATION #include -#include #include -#include +#include #include #include +#include +#include #include "bluetooth.h" #include @@ -169,14 +170,32 @@ static pbio_error_t pbdrv_bluetooth_simulation_process_thread(pbio_os_state_t *s return bluetooth_thread_err; } +static struct termios oldt; + +static void restore_terminal_settings(void) { + tcsetattr(STDIN_FILENO, TCSANOW, &oldt); +} + +static void handle_signal(int sig) { + restore_terminal_settings(); + signal(sig, SIG_DFL); + raise(sig); +} + void pbdrv_bluetooth_init_hci(void) { - struct termios oldt, newt; + struct termios newt; + // Save the original terminal settings if (tcgetattr(STDIN_FILENO, &oldt) != 0) { printf("DEBUG: Failed to get terminal attributes\n"); return; } + // Register the cleanup function to restore terminal settings on exit + atexit(restore_terminal_settings); + signal(SIGINT, handle_signal); + signal(SIGTERM, handle_signal); + newt = oldt; // Get one char at a time instead of newline and disable CTRL+C for exit. From 05b6765d4a5a334ce495d2eccbfdd5a56cb4549b Mon Sep 17 00:00:00 2001 From: Laurens Valk Date: Wed, 15 Oct 2025 12:23:36 +0200 Subject: [PATCH 07/26] pbio/sys/hmi_mpy_env: Compile script with Pybricksdev. Drop-in for HMI module that simulates sending a multi-mpy file to the hub and then starts it. Used in the simulated hub to test different programs. The script to start is set with an environment variable and compiled with Pybricksdev. --- .vscode/launch.json | 7 ++- bricks/_common/common.mk | 4 -- bricks/_common/sources.mk | 1 + bricks/simhub/make_mpy.py | 15 +++++ lib/pbio/platform/sim_hub/pbdrvconfig.h | 2 +- lib/pbio/platform/sim_hub/pbsysconfig.h | 2 +- lib/pbio/sys/hmi_env_mpy.c | 79 +++++++++++++++++++++++++ tests/virtualhub/basics/hello.py | 5 ++ tests/virtualhub/basics/hello.py.exp | 4 ++ 9 files changed, 110 insertions(+), 9 deletions(-) create mode 100644 bricks/simhub/make_mpy.py create mode 100644 lib/pbio/sys/hmi_env_mpy.c create mode 100644 tests/virtualhub/basics/hello.py create mode 100644 tests/virtualhub/basics/hello.py.exp diff --git a/.vscode/launch.json b/.vscode/launch.json index 7f88b6e94..e7d61b473 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -97,9 +97,6 @@ "type": "cppdbg", "request": "launch", "program": "${workspaceFolder}/bricks/simhub/debug/firmware.elf", - "args": [ - "${workspaceFolder}/tests/virtualhub/motor/car.py" - ], "stopAtEntry": false, "cwd": "${workspaceFolder}", "environment": [ @@ -107,6 +104,10 @@ "name": "PBIO_TEST_CONNECT_SOCKET", "value": "true" }, + { + "name": "TEST_SCRIPT", + "value": "${workspaceFolder}/tests/virtualhub/basics/hello.py" + }, ], "externalConsole": false, "MIMode": "gdb", diff --git a/bricks/_common/common.mk b/bricks/_common/common.mk index 1a73fd373..deb758387 100644 --- a/bricks/_common/common.mk +++ b/bricks/_common/common.mk @@ -706,9 +706,5 @@ deploy-openocd: $(BUILD)/firmware-base.bin # Run emulation build on a POSIX system using normal stdio run: $(BUILD)/firmware.elf @$(BUILD)/firmware.elf - @echo "Exit status: $$?" - -test: $(BUILD)/firmware.elf - $(Q)/usr/bin/printf "print('hello world!', list(x+1 for x in range(10)), end='eol\\\\n')\\r\\n\\004" | $(BUILD)/firmware.elf | tail -n2 | grep "^hello world! \\[1, 2, 3, 4, 5, 6, 7, 8, 9, 10\\]eol" include $(TOP)/py/mkrules.mk diff --git a/bricks/_common/sources.mk b/bricks/_common/sources.mk index 503380606..02acfb96c 100644 --- a/bricks/_common/sources.mk +++ b/bricks/_common/sources.mk @@ -237,6 +237,7 @@ PBIO_SRC_C = $(addprefix lib/pbio/,\ sys/battery.c \ sys/command.c \ sys/core.c \ + sys/hmi_env_mpy.c \ sys/hmi_lcd.c \ sys/hmi_pup.c \ sys/hmi_none.c \ diff --git a/bricks/simhub/make_mpy.py b/bricks/simhub/make_mpy.py new file mode 100644 index 000000000..817d76d19 --- /dev/null +++ b/bricks/simhub/make_mpy.py @@ -0,0 +1,15 @@ +from pybricksdev.compile import compile_multi_file +import asyncio +import argparse +import sys + +# Set up argument parser +parser = argparse.ArgumentParser(description="Compile to multi-mpy using pybricksdev.") +parser.add_argument("file", help="The path to the Python file to compile.") +args = parser.parse_args() + +# Compile the file +result = asyncio.run(compile_multi_file(args.file, (6, 1))) + +# Write the result to stdout +sys.stdout.buffer.write(result) diff --git a/lib/pbio/platform/sim_hub/pbdrvconfig.h b/lib/pbio/platform/sim_hub/pbdrvconfig.h index ddbd38556..e65ca061f 100644 --- a/lib/pbio/platform/sim_hub/pbdrvconfig.h +++ b/lib/pbio/platform/sim_hub/pbdrvconfig.h @@ -5,7 +5,7 @@ #define PBDRV_CONFIG_BATTERY_TEST (1) #define PBDRV_CONFIG_BLOCK_DEVICE (1) -#define PBDRV_CONFIG_BLOCK_DEVICE_RAM_SIZE (10 * 1024) +#define PBDRV_CONFIG_BLOCK_DEVICE_RAM_SIZE (50 * 1024) #define PBDRV_CONFIG_BLOCK_DEVICE_TEST (1) #define PBDRV_CONFIG_BLUETOOTH (1) diff --git a/lib/pbio/platform/sim_hub/pbsysconfig.h b/lib/pbio/platform/sim_hub/pbsysconfig.h index c842ca58d..a07ba1d34 100644 --- a/lib/pbio/platform/sim_hub/pbsysconfig.h +++ b/lib/pbio/platform/sim_hub/pbsysconfig.h @@ -13,7 +13,7 @@ #define PBSYS_CONFIG_HOST_STDIN_BUF_SIZE (21) #define PBSYS_CONFIG_HMI (1) #define PBSYS_CONFIG_HMI_STOP_BUTTON (1 << 5) // center -#define PBSYS_CONFIG_HMI_NONE (1) +#define PBSYS_CONFIG_HMI_ENV_MPY (1) #define PBSYS_CONFIG_HMI_NUM_SLOTS (0) #define PBSYS_CONFIG_HUB_LIGHT_MATRIX (0) #define PBSYS_CONFIG_MAIN (1) diff --git a/lib/pbio/sys/hmi_env_mpy.c b/lib/pbio/sys/hmi_env_mpy.c new file mode 100644 index 000000000..ccf6f75c3 --- /dev/null +++ b/lib/pbio/sys/hmi_env_mpy.c @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2018-2025 The Pybricks Authors + +// Drop-in for HMI module that simulates sending a multi-mpy file to the hub +// and then starts it. Used in the simulated hub to test different programs. +// The script to start is set with an environment variable. + +#include + +#if PBSYS_CONFIG_HMI_ENV_MPY + +#include +#include +#include +#include +#include +#include "storage.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +void pbsys_hmi_init(void) { +} + +void pbsys_hmi_deinit(void) { +} + +pbio_error_t pbsys_hmi_await_program_selection(void) { + + pbio_os_run_processes_and_wait_for_event(); + + // With this HMI, we run a script once and then exit. + static bool ran_once = false; + if (ran_once) { + return PBIO_ERROR_CANCELED; + } + ran_once = true; + + // Test if script is provided via environment. + const char *script_path = getenv("TEST_SCRIPT"); + if (!script_path) { + // No script given, just start REPL + return pbsys_main_program_request_start(PBIO_PYBRICKS_USER_PROGRAM_ID_REPL, PBSYS_MAIN_PROGRAM_START_REQUEST_TYPE_BOOT); + } + + // Pybricksdev helper script, pipes multi-mpy to us. + char command[512]; + snprintf(command, sizeof(command), "python ./bricks/simhub/make_mpy.py %s", script_path); + FILE *pipe = popen(command, "r"); + if (!pipe) { + perror("Failed to compile program with Pybricksdev\n"); + return PBIO_ERROR_CANCELED; + } + + // Read the multi-mpy file from pipe. + uint8_t program_buf[PBDRV_CONFIG_BLOCK_DEVICE_RAM_SIZE]; + size_t read_size = fread(program_buf, 1, sizeof(program_buf), pipe); + pclose(pipe); + + if (read_size == 0) { + perror("Error reading from pipe"); + return PBIO_ERROR_CANCELED; + } + + // Load the program in storage, as if receiving it. + pbsys_storage_set_program_size(0); + pbsys_storage_set_program_data(0, program_buf, read_size); + pbsys_storage_set_program_size(read_size); + + return pbsys_main_program_request_start(PBIO_PYBRICKS_USER_PROGRAM_ID_FIRST_SLOT, PBSYS_MAIN_PROGRAM_START_REQUEST_TYPE_BOOT); +} + +#endif // PBSYS_CONFIG_HMI_ENV_MPY diff --git a/tests/virtualhub/basics/hello.py b/tests/virtualhub/basics/hello.py new file mode 100644 index 000000000..f503dd125 --- /dev/null +++ b/tests/virtualhub/basics/hello.py @@ -0,0 +1,5 @@ +from pybricks.tools import wait + +print("\nHello") +wait(1000) +print("World\n") diff --git a/tests/virtualhub/basics/hello.py.exp b/tests/virtualhub/basics/hello.py.exp new file mode 100644 index 000000000..cd143591f --- /dev/null +++ b/tests/virtualhub/basics/hello.py.exp @@ -0,0 +1,4 @@ + +Hello +World + From 41965e7d40b306d228c3078dc69cb5d5cf849593 Mon Sep 17 00:00:00 2001 From: Laurens Valk Date: Wed, 15 Oct 2025 12:33:24 +0200 Subject: [PATCH 08/26] bricks/simhub: Fix debug build paths and mpy-cross. Then we don't end up with unexpected build files in the MicroPython submodule when debugging. --- .vscode/launch.json | 2 +- .vscode/tasks.json | 2 +- Makefile | 4 +--- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index e7d61b473..c84d53295 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -96,7 +96,7 @@ "name": "simhub", "type": "cppdbg", "request": "launch", - "program": "${workspaceFolder}/bricks/simhub/debug/firmware.elf", + "program": "${workspaceFolder}/bricks/simhub/build-debug/firmware.elf", "stopAtEntry": false, "cwd": "${workspaceFolder}", "environment": [ diff --git a/.vscode/tasks.json b/.vscode/tasks.json index b5f7e9da3..c4f898d63 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -47,7 +47,7 @@ }, "label": "build simhub (debug)", "type": "shell", - "command": "poetry run make DEBUG=1 BUILD=debug COPT=-DPBDRV_CONFIG_CLOCK_LINUX -j", + "command": "make -C ../../micropython/mpy-cross -j && poetry run make DEBUG=1 BUILD=build-debug COPT=-DPBDRV_CONFIG_CLOCK_LINUX -j", }, { "label": "build test-pbio", diff --git a/Makefile b/Makefile index 0a9336ce7..69900f705 100644 --- a/Makefile +++ b/Makefile @@ -86,9 +86,7 @@ simhub: mpy-cross clean-simhub: clean-mpy-cross @$(MAKE) -C bricks/simhub clean - @$(MAKE) -C bricks/simhub clean - @$(MAKE) -C bricks/simhub clean BUILD=debug - + @$(MAKE) -C bricks/simhub clean BUILD=build-debug mpy-cross: @$(MAKE) -C micropython/mpy-cross CROSS_COMPILE=$(HOST_CROSS_COMPILE) From 129fbf617fdc488c875905141d0deaf0bcaa86f8 Mon Sep 17 00:00:00 2001 From: Laurens Valk Date: Mon, 27 Oct 2025 13:54:34 +0100 Subject: [PATCH 09/26] pbio/platform/sim_hub: Don't use Linux signal for clock tick. This interrupts syscalls like reading data from a pipe, especially when debugging. We can instead just request polling when MicroPython drives the poll hook and the wall clock time has changed. For the CI clock variant, this was not implented at all yet. While we are at it, ensure it increments the clock every N instructions, so that even tight loops without any waits will advance. For example, this would be needed to run asynchronous scripts that don't _WFI anywhere. --- .vscode/tasks.json | 2 +- bricks/_common/mpconfigport.h | 2 ++ bricks/simhub/mpconfigport.h | 29 +++++++++++++++++++++++++ bricks/simhub/pbio_os_hook.c | 3 +++ lib/pbio/platform/sim_hub/pbdrvconfig.h | 4 ++-- 5 files changed, 37 insertions(+), 3 deletions(-) diff --git a/.vscode/tasks.json b/.vscode/tasks.json index c4f898d63..ebf34b50c 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -47,7 +47,7 @@ }, "label": "build simhub (debug)", "type": "shell", - "command": "make -C ../../micropython/mpy-cross -j && poetry run make DEBUG=1 BUILD=build-debug COPT=-DPBDRV_CONFIG_CLOCK_LINUX -j", + "command": "make -C ../../micropython/mpy-cross -j && poetry run make DEBUG=1 BUILD=build-debug COPT=-DPBDRV_CONFIG_CLOCK_LOCAL -j", }, { "label": "build test-pbio", diff --git a/bricks/_common/mpconfigport.h b/bricks/_common/mpconfigport.h index 7cbbf6988..f9dba93b0 100644 --- a/bricks/_common/mpconfigport.h +++ b/bricks/_common/mpconfigport.h @@ -149,11 +149,13 @@ typedef long mp_off_t; #define MICROPY_END_ATOMIC_SECTION(state) (void)(state) #endif +#ifndef MICROPY_VM_HOOK_LOOP #define MICROPY_VM_HOOK_LOOP \ do { \ extern bool pbio_os_run_processes_once(void); \ pbio_os_run_processes_once(); \ } while (0); +#endif #define MICROPY_GC_HOOK_LOOP(i) do { \ if (((i) & 0xf) == 0) { \ diff --git a/bricks/simhub/mpconfigport.h b/bricks/simhub/mpconfigport.h index 605eeeb53..c4164c76c 100644 --- a/bricks/simhub/mpconfigport.h +++ b/bricks/simhub/mpconfigport.h @@ -1,4 +1,5 @@ #include +#include #define MICROPY_HW_BOARD_NAME "Desktop" #define MICROPY_HW_MCU_NAME "Desktop" @@ -53,4 +54,32 @@ #define PYBRICKS_OPT_CUSTOM_IMPORT (0) #define PYBRICKS_OPT_NATIVE_MOD (0) +#if PBDRV_CONFIG_CLOCK_TEST +#define MICROPY_VM_HOOK_LOOP \ + do { \ + static uint32_t count; \ + if ((count % 16) == 0) { \ + extern void pbio_test_clock_tick(uint32_t ticks); \ + pbio_test_clock_tick(1); \ + } \ + extern bool pbio_os_run_processes_once(void); \ + pbio_os_run_processes_once(); \ + } while (0); +#else +#define MICROPY_VM_HOOK_LOOP \ + do { \ + static uint32_t clock_last; \ + extern uint32_t pbdrv_clock_get_ms(void); \ + uint32_t clock_now = pbdrv_clock_get_ms(); \ + if (clock_last != clock_now) { \ + extern void pbio_os_request_poll(void); \ + pbio_os_request_poll(); \ + clock_last = clock_now; \ + } \ + extern bool pbio_os_run_processes_once(void); \ + pbio_os_run_processes_once(); \ + } while (0); +#endif + + #include "../_common/mpconfigport.h" diff --git a/bricks/simhub/pbio_os_hook.c b/bricks/simhub/pbio_os_hook.c index 62a1b5a07..a46d1d4a1 100644 --- a/bricks/simhub/pbio_os_hook.c +++ b/bricks/simhub/pbio_os_hook.c @@ -53,4 +53,7 @@ void pbio_os_hook_wait_for_interrupt(pbio_os_irq_flags_t flags) { MP_THREAD_GIL_EXIT(); pselect(0, NULL, NULL, NULL, &timeout, &origmask); MP_THREAD_GIL_ENTER(); + + // There is a new timer event to handle. + pbio_os_request_poll(); } diff --git a/lib/pbio/platform/sim_hub/pbdrvconfig.h b/lib/pbio/platform/sim_hub/pbdrvconfig.h index e65ca061f..097fd853e 100644 --- a/lib/pbio/platform/sim_hub/pbdrvconfig.h +++ b/lib/pbio/platform/sim_hub/pbdrvconfig.h @@ -15,8 +15,8 @@ #define PBDRV_CONFIG_BUTTON_TEST (1) #define PBDRV_CONFIG_CLOCK (1) -#ifdef PBDRV_CONFIG_CLOCK_LINUX -#define PBDRV_CONFIG_CLOCK_LINUX_SIGNAL (1) +#ifdef PBDRV_CONFIG_CLOCK_LOCAL +#define PBDRV_CONFIG_CLOCK_LINUX (1) #else #define PBDRV_CONFIG_CLOCK_TEST (1) #endif From 63f16a4ef259b048e6a9fcae0482264f143340ce Mon Sep 17 00:00:00 2001 From: Laurens Valk Date: Mon, 27 Oct 2025 19:23:12 +0100 Subject: [PATCH 10/26] bricks/simhub: Clean up VM_HOOK. Also rename desktop target to native for clarity. --- bricks/_common/common.mk | 10 +++++----- bricks/_common/mpconfigport.h | 8 ++++++-- bricks/simhub/Makefile | 2 +- bricks/simhub/mpconfigport.h | 21 +++++++++++++++------ bricks/simhub/pbio_os_hook.c | 30 ++++++++++++++++-------------- 5 files changed, 43 insertions(+), 28 deletions(-) diff --git a/bricks/_common/common.mk b/bricks/_common/common.mk index deb758387..5eed0b0fe 100644 --- a/bricks/_common/common.mk +++ b/bricks/_common/common.mk @@ -153,7 +153,7 @@ OPENOCD ?= openocd OPENOCD_CONFIG ?= openocd_stm32$(PB_MCU_SERIES_LCASE).cfg TEXT0_ADDR ?= 0x08000000 -ifeq ($(PB_MCU_FAMILY),desktop) +ifeq ($(PB_MCU_FAMILY),native) UNAME_S := $(shell uname -s) LD = $(CC) CFLAGS += $(INC) -Wall -Werror -Wdouble-promotion -Wfloat-conversion -std=gnu99 $(COPT) -D_GNU_SOURCE @@ -163,7 +163,7 @@ else ifeq ($(UNAME_S),Darwin) LDFLAGS += -Wl,-map,$@.map -Wl,-dead_strip endif LIBS = -else # end desktop, begin embedded +else # end native, begin embedded CROSS_COMPILE ?= arm-none-eabi- ifeq ($(PB_MCU_FAMILY),STM32) CFLAGS_MCU_F0 = -mthumb -mtune=cortex-m0 -mcpu=cortex-m0 -msoft-float @@ -250,7 +250,7 @@ PY_EXTRA_SRC_C = $(addprefix shared/,\ runtime/stdout_helpers.c \ ) -ifeq ($(PB_MCU_FAMILY),desktop) +ifeq ($(PB_MCU_FAMILY),native) PY_EXTRA_SRC_C += $(addprefix shared/,\ runtime/gchelper_generic.c \ ) @@ -286,7 +286,7 @@ PY_EXTRA_SRC_C += $(addprefix bricks/_common/,\ endif # Not all MCUs support thumb2 instructions. -ifeq ($(PB_MCU_FAMILY),desktop) +ifeq ($(PB_MCU_FAMILY),native) SRC_S += else ifeq ($(PB_MCU_SERIES),$(filter $(PB_MCU_SERIES),AT91SAM7 F0 TIAM1808)) SRC_S += shared/runtime/gchelper_thumb1.s @@ -587,7 +587,7 @@ CFLAGS += -DMICROPY_MODULE_FROZEN_MPY MPY_TOOL_FLAGS += -mlongint-impl none endif -ifneq ($(PB_MCU_FAMILY),desktop) +ifneq ($(PB_MCU_FAMILY),native) # Main firmware build targets TARGETS := $(BUILD)/firmware.zip else diff --git a/bricks/_common/mpconfigport.h b/bricks/_common/mpconfigport.h index f9dba93b0..32eb3819d 100644 --- a/bricks/_common/mpconfigport.h +++ b/bricks/_common/mpconfigport.h @@ -149,13 +149,17 @@ typedef long mp_off_t; #define MICROPY_END_ATOMIC_SECTION(state) (void)(state) #endif -#ifndef MICROPY_VM_HOOK_LOOP +// Optional extra code to run before MicroPython drives the event loop. +#ifndef PYBRICKS_VM_HOOK_LOOP_EXTRA +#define PYBRICKS_VM_HOOK_LOOP_EXTRA +#endif + #define MICROPY_VM_HOOK_LOOP \ do { \ + PYBRICKS_VM_HOOK_LOOP_EXTRA \ extern bool pbio_os_run_processes_once(void); \ pbio_os_run_processes_once(); \ } while (0); -#endif #define MICROPY_GC_HOOK_LOOP(i) do { \ if (((i) & 0xf) == 0) { \ diff --git a/bricks/simhub/Makefile b/bricks/simhub/Makefile index a3c334115..56fe05ee3 100644 --- a/bricks/simhub/Makefile +++ b/bricks/simhub/Makefile @@ -2,7 +2,7 @@ # Copyright (c) 2025 The Pybricks Authors PBIO_PLATFORM = sim_hub -PB_MCU_FAMILY = desktop +PB_MCU_FAMILY = native PB_FROZEN_MODULES = 1 MICROPY_ROM_TEXT_COMPRESSION = 1 diff --git a/bricks/simhub/mpconfigport.h b/bricks/simhub/mpconfigport.h index c4164c76c..846a87ec6 100644 --- a/bricks/simhub/mpconfigport.h +++ b/bricks/simhub/mpconfigport.h @@ -54,19 +54,30 @@ #define PYBRICKS_OPT_CUSTOM_IMPORT (0) #define PYBRICKS_OPT_NATIVE_MOD (0) +// The Virtual Hub has no hardware interrupt that requests polling every 1ms. +// We solve this by polling manually as appropriate for the simulation, as +// indicated below. #if PBDRV_CONFIG_CLOCK_TEST -#define MICROPY_VM_HOOK_LOOP \ +// In the CI variant ("counting clock"), the clock is advanced on every +// pbio_os_hook_wait_for_interrupt, which is called from mp_hal_delay_ms. But +// the user could be running a tight loop without any waits. We still want to +// advance the clock in those cases, which we mimic here by advancing the clock +// every couple of MicroPython byte codes. This also polls to the event loop. +#define PYBRICKS_VM_HOOK_LOOP_EXTRA \ do { \ static uint32_t count; \ if ((count % 16) == 0) { \ extern void pbio_test_clock_tick(uint32_t ticks); \ pbio_test_clock_tick(1); \ } \ - extern bool pbio_os_run_processes_once(void); \ - pbio_os_run_processes_once(); \ } while (0); #else -#define MICROPY_VM_HOOK_LOOP \ +// When using the wall clock, time advances automatically but we still need to +// request polling. This is done at the end of pbio_os_hook_wait_for_interrupt. +// As above, we also need something to move it along with blocking user loops. +// Instead of guessing with a number of instructions, here we can just poll +// whenever the wall clock changes. +#define PYBRICKS_VM_HOOK_LOOP_EXTRA \ do { \ static uint32_t clock_last; \ extern uint32_t pbdrv_clock_get_ms(void); \ @@ -76,8 +87,6 @@ pbio_os_request_poll(); \ clock_last = clock_now; \ } \ - extern bool pbio_os_run_processes_once(void); \ - pbio_os_run_processes_once(); \ } while (0); #endif diff --git a/bricks/simhub/pbio_os_hook.c b/bricks/simhub/pbio_os_hook.c index a46d1d4a1..ea185d19d 100644 --- a/bricks/simhub/pbio_os_hook.c +++ b/bricks/simhub/pbio_os_hook.c @@ -11,16 +11,26 @@ #if PBDRV_CONFIG_CLOCK_TEST #include -#endif pbio_os_irq_flags_t pbio_os_hook_disable_irq(void) { sigset_t sigmask; - - #if PBDRV_CONFIG_CLOCK_TEST sigemptyset(&sigmask); return sigmask; - #endif +} + +void pbio_os_hook_enable_irq(pbio_os_irq_flags_t flags) { +} +void pbio_os_hook_wait_for_interrupt(pbio_os_irq_flags_t flags) { + // All events have been handled at this time. Advance the clock + // and continue immediately. + pbio_test_clock_tick(1); +} + +#else + +pbio_os_irq_flags_t pbio_os_hook_disable_irq(void) { + sigset_t sigmask; sigfillset(&sigmask); sigset_t origmask; pthread_sigmask(SIG_SETMASK, &sigmask, &origmask); @@ -28,21 +38,11 @@ pbio_os_irq_flags_t pbio_os_hook_disable_irq(void) { } void pbio_os_hook_enable_irq(pbio_os_irq_flags_t flags) { - #if PBDRV_CONFIG_CLOCK_TEST - return; - #endif - sigset_t origmask = (sigset_t)flags; pthread_sigmask(SIG_SETMASK, &origmask, NULL); } void pbio_os_hook_wait_for_interrupt(pbio_os_irq_flags_t flags) { - #if PBDRV_CONFIG_CLOCK_TEST - // All events have been handled at this time. Advance the clock - // and continue immediately. - pbio_test_clock_tick(1); - return; - #endif struct timespec timeout = { .tv_sec = 0, @@ -57,3 +57,5 @@ void pbio_os_hook_wait_for_interrupt(pbio_os_irq_flags_t flags) { // There is a new timer event to handle. pbio_os_request_poll(); } + +#endif From 7314162c03c09c6a7855c5e27c6e2c811f79f208 Mon Sep 17 00:00:00 2001 From: Laurens Valk Date: Tue, 28 Oct 2025 13:36:50 +0100 Subject: [PATCH 11/26] pbio/platform: Rename main to _main on embedded. This allows the virtual hub to have a proper main(int argc, char **argv), so we can parse the input argument (a user script). --- lib/pbio/platform/city_hub/startup.s | 4 ++-- lib/pbio/platform/essential_hub/startup.s | 6 +++--- lib/pbio/platform/ev3/start.S | 6 +++--- lib/pbio/platform/move_hub/startup.s | 4 ++-- lib/pbio/platform/nxt/startup.S | 4 ++-- lib/pbio/platform/prime_hub/startup.s | 6 +++--- lib/pbio/platform/sim_hub/platform.c | 5 +++++ lib/pbio/platform/technic_hub/startup.s | 6 +++--- lib/pbio/sys/main.c | 4 +--- 9 files changed, 24 insertions(+), 21 deletions(-) diff --git a/lib/pbio/platform/city_hub/startup.s b/lib/pbio/platform/city_hub/startup.s index 707a6a4f4..3388cd3bc 100644 --- a/lib/pbio/platform/city_hub/startup.s +++ b/lib/pbio/platform/city_hub/startup.s @@ -8,7 +8,7 @@ * - Set the initial PC == Reset_Handler, * - Set the vector table entries with the exceptions ISR address * - Branches to main in the C library (which eventually - * calls main()). + * calls _main()). * After Reset the Cortex-M0 processor is in Thread mode, * priority is Privileged, and the Stack is set to Main. ****************************************************************************** @@ -102,7 +102,7 @@ LoopFillZerobss: /* Call static constructors */ /* bl __libc_init_array */ /* Call the application's entry point.*/ - bl main + bl _main LoopForever: b LoopForever diff --git a/lib/pbio/platform/essential_hub/startup.s b/lib/pbio/platform/essential_hub/startup.s index ee3dde9ca..f188ad666 100644 --- a/lib/pbio/platform/essential_hub/startup.s +++ b/lib/pbio/platform/essential_hub/startup.s @@ -10,7 +10,7 @@ * - Set the initial PC == Reset_Handler, * - Set the vector table entries with the exceptions ISR address * - Branches to main in the C library (which eventually - * calls main()). + * calls _main()). * After Reset the Cortex-M4 processor is in Thread mode, * priority is Privileged, and the Stack is set to Main. ****************************************************************************** @@ -68,7 +68,7 @@ defined in linker script */ * @brief This is the code that gets called when the processor first * starts execution following a reset event. Only the absolutely * necessary set is performed, after which the application - * supplied main() routine is called. + * supplied _main() routine is called. * @param None * @retval : None */ @@ -112,7 +112,7 @@ LoopFillZerobss: /* Call static constructors */ /* bl __libc_init_array */ /* Call the application's entry point.*/ - bl main + bl _main bx lr .size Reset_Handler, .-Reset_Handler diff --git a/lib/pbio/platform/ev3/start.S b/lib/pbio/platform/ev3/start.S index 4a7306c1c..911e19d60 100755 --- a/lib/pbio/platform/ev3/start.S +++ b/lib/pbio/platform/ev3/start.S @@ -48,7 +48,7 @@ @ @ The reset handler sets up the stack pointers for all the modes. The FIQ and @ IRQ shall be disabled during this. Then, clearthe BSS sections, switch to the -@ main() function. +@ _main() function. @ Entry: @ U-Boot invokes us with caches and MMU enabled (since we use the @@ -126,11 +126,11 @@ Enter_main: MOV lr, pc @ Dummy return BLX r10 @ Branch to SystemInit - LDR r10, =main @ Get the address of main + LDR r10, =_main @ Get the address of _main MOV r0, #0 @ Set the first argument to 0 MOV r1, #0 @ Set the second argument to NULL (0) MOV lr, pc @ Dummy return - BLX r10 @ Branch to main + BLX r10 @ Branch to _main SUB pc, pc, #0x08 @ looping diff --git a/lib/pbio/platform/move_hub/startup.s b/lib/pbio/platform/move_hub/startup.s index bf5835732..1c015f910 100644 --- a/lib/pbio/platform/move_hub/startup.s +++ b/lib/pbio/platform/move_hub/startup.s @@ -9,7 +9,7 @@ * - Set the initial PC == Reset_Handler, * - Set the vector table entries with the exceptions ISR address * - Branches to main in the C library (which eventually - * calls main()). + * calls _main()). * After Reset the Cortex-M0 processor is in Thread mode, * priority is Privileged, and the Stack is set to Main. ****************************************************************************** @@ -103,7 +103,7 @@ LoopFillZerobss: /* Call static constructors */ /* bl __libc_init_array */ /* Call the application's entry point.*/ - bl main + bl _main LoopForever: b LoopForever diff --git a/lib/pbio/platform/nxt/startup.S b/lib/pbio/platform/nxt/startup.S index 056f0f32c..d827bbcd0 100644 --- a/lib/pbio/platform/nxt/startup.S +++ b/lib/pbio/platform/nxt/startup.S @@ -42,7 +42,7 @@ .global nx_start .extern SystemInit - .extern main + .extern _main /********************************************************** @@ -229,7 +229,7 @@ wait_mclk_3: bx r5 /* Start the main kernel code using an interworking branch. */ - ldr r5, =main + ldr r5, =_main mov lr, pc bx r5 diff --git a/lib/pbio/platform/prime_hub/startup.s b/lib/pbio/platform/prime_hub/startup.s index ee3dde9ca..f188ad666 100644 --- a/lib/pbio/platform/prime_hub/startup.s +++ b/lib/pbio/platform/prime_hub/startup.s @@ -10,7 +10,7 @@ * - Set the initial PC == Reset_Handler, * - Set the vector table entries with the exceptions ISR address * - Branches to main in the C library (which eventually - * calls main()). + * calls _main()). * After Reset the Cortex-M4 processor is in Thread mode, * priority is Privileged, and the Stack is set to Main. ****************************************************************************** @@ -68,7 +68,7 @@ defined in linker script */ * @brief This is the code that gets called when the processor first * starts execution following a reset event. Only the absolutely * necessary set is performed, after which the application - * supplied main() routine is called. + * supplied _main() routine is called. * @param None * @retval : None */ @@ -112,7 +112,7 @@ LoopFillZerobss: /* Call static constructors */ /* bl __libc_init_array */ /* Call the application's entry point.*/ - bl main + bl _main bx lr .size Reset_Handler, .-Reset_Handler diff --git a/lib/pbio/platform/sim_hub/platform.c b/lib/pbio/platform/sim_hub/platform.c index c6722bfa9..e013f06dd 100644 --- a/lib/pbio/platform/sim_hub/platform.c +++ b/lib/pbio/platform/sim_hub/platform.c @@ -130,3 +130,8 @@ const pbdrv_motor_driver_virtual_simulation_platform_data_t .endstop_angle_positive = INFINITY, }, }; + +int main(int argc, char **argv) { + extern void _main(void); + _main(); +} diff --git a/lib/pbio/platform/technic_hub/startup.s b/lib/pbio/platform/technic_hub/startup.s index 40dd01880..f9d7fd8df 100644 --- a/lib/pbio/platform/technic_hub/startup.s +++ b/lib/pbio/platform/technic_hub/startup.s @@ -9,7 +9,7 @@ * - Set the vector table entries with the exceptions ISR address, * - Configure the clock system * - Branches to main in the C library (which eventually - * calls main()). + * calls _main()). * After Reset the Cortex-M4 processor is in Thread mode, * priority is Privileged, and the Stack is set to Main. ****************************************************************************** @@ -67,7 +67,7 @@ defined in linker script */ * @brief This is the code that gets called when the processor first * starts execution following a reset event. Only the absolutely * necessary set is performed, after which the application - * supplied main() routine is called. + * supplied _main() routine is called. * @param None * @retval : None */ @@ -112,7 +112,7 @@ LoopFillZerobss: /* Call static constructors */ @ bl __libc_init_array /* Call the application's entry point.*/ - bl main + bl _main LoopForever: b LoopForever diff --git a/lib/pbio/sys/main.c b/lib/pbio/sys/main.c index 1b6f11ee7..6ac398307 100644 --- a/lib/pbio/sys/main.c +++ b/lib/pbio/sys/main.c @@ -75,10 +75,8 @@ pbio_error_t pbsys_main_program_request_start(pbio_pybricks_user_program_id_t id /** * Initializes the PBIO library, runs custom main program, and handles shutdown. - * - * @param [in] main The main program. */ -int main(int argc, char **argv) { +void _main(void) { pbdrv_init(); pbio_init(true); From aa5a57dda26fd1e67de90d39c1549ba6d0854873 Mon Sep 17 00:00:00 2001 From: Laurens Valk Date: Tue, 28 Oct 2025 19:48:52 +0100 Subject: [PATCH 12/26] bricks/simhub: Poll Contiki. There are still some processes that need it, so we have to include this for now. This can be removed once all processes have been migrated to pbio/os. --- bricks/simhub/mpconfigport.h | 2 ++ bricks/simhub/pbio_os_hook.c | 2 ++ 2 files changed, 4 insertions(+) diff --git a/bricks/simhub/mpconfigport.h b/bricks/simhub/mpconfigport.h index 846a87ec6..a7d39d9b4 100644 --- a/bricks/simhub/mpconfigport.h +++ b/bricks/simhub/mpconfigport.h @@ -85,6 +85,8 @@ if (clock_last != clock_now) { \ extern void pbio_os_request_poll(void); \ pbio_os_request_poll(); \ + extern void etimer_request_poll(); \ + etimer_request_poll(); \ clock_last = clock_now; \ } \ } while (0); diff --git a/bricks/simhub/pbio_os_hook.c b/bricks/simhub/pbio_os_hook.c index ea185d19d..2a9db10c9 100644 --- a/bricks/simhub/pbio_os_hook.c +++ b/bricks/simhub/pbio_os_hook.c @@ -56,6 +56,8 @@ void pbio_os_hook_wait_for_interrupt(pbio_os_irq_flags_t flags) { // There is a new timer event to handle. pbio_os_request_poll(); + extern void etimer_request_poll(); + etimer_request_poll(); } #endif From 0e2ebde0eb12597f24b3ea0ab8c1c54a682eb38d Mon Sep 17 00:00:00 2001 From: Laurens Valk Date: Wed, 29 Oct 2025 10:51:53 +0100 Subject: [PATCH 13/26] pbio/platform/virtual_hub: Use UDP to send data. Instead of sending a stream of angle strings, we can just send packets separately. Then we can just use the Pybricks protocol, and drive the animation using data that will eventually become Port View for all hubs. --- .../motor_driver_virtual_simulation.c | 29 ++------ lib/pbio/platform/virtual_hub/animation.py | 69 +++++++++++-------- 2 files changed, 48 insertions(+), 50 deletions(-) diff --git a/lib/pbio/drv/motor_driver/motor_driver_virtual_simulation.c b/lib/pbio/drv/motor_driver/motor_driver_virtual_simulation.c index bb16f0eec..edf3a6925 100644 --- a/lib/pbio/drv/motor_driver/motor_driver_virtual_simulation.c +++ b/lib/pbio/drv/motor_driver/motor_driver_virtual_simulation.c @@ -149,11 +149,11 @@ pbio_error_t pbdrv_motor_driver_set_duty_cycle(pbdrv_motor_driver_dev_t *driver, } static int data_socket = -1; +static struct sockaddr_in serv_addr; static void animation_socket_connect(void) { - struct sockaddr_in serv_addr; - data_socket = socket(AF_INET, SOCK_STREAM, 0); + data_socket = socket(AF_INET, SOCK_DGRAM, 0); if (data_socket < 0) { perror("socket() failed"); return; @@ -167,15 +167,6 @@ static void animation_socket_connect(void) { data_socket = -1; return; } - - if (connect(data_socket, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) { - perror("connect() failed. Animation not running?"); - close(data_socket); - data_socket = -1; - return; - } - - printf("Connected to animation socket.\n"); } PROCESS(pbdrv_motor_driver_virtual_simulation_process, "pbdrv_motor_driver_virtual_simulation"); @@ -198,24 +189,16 @@ PROCESS_THREAD(pbdrv_motor_driver_virtual_simulation_process, ev, data) { // If data parser pipe is connected, output the motor angles. if (data_socket >= 0 && timer_expired(&frame_timer)) { timer_reset(&frame_timer); - char buf[PBDRV_CONFIG_MOTOR_DRIVER_NUM_DEV * 20]; - size_t len = 0; + uint8_t buf[sizeof(uint32_t) * PBDRV_CONFIG_MOTOR_DRIVER_NUM_DEV + 1] = { 3 }; // Output motor angles on one line. for (dev_index = 0; dev_index < PBDRV_CONFIG_MOTOR_DRIVER_NUM_DEV; dev_index++) { driver = &motor_driver_devs[dev_index]; - - // Append each motor angle to the buffer - len += snprintf(buf + len, sizeof(buf) - len, "%d ", (int)(driver->angle / 1000)); + pbio_set_uint32_le(&buf[dev_index * sizeof(uint32_t) + 1], (uint32_t)(driver->angle / 1000)); } - // Replace last space with newline - buf[len++] = '\r'; - buf[len++] = '\n'; - buf[len] = '\0'; - // Send the constructed message - ssize_t sent = send(data_socket, buf, len, MSG_NOSIGNAL); - if (sent == -1) { + ssize_t sent = sendto(data_socket, buf, sizeof(buf), 0, (struct sockaddr *)&serv_addr, sizeof(serv_addr)); + if (sent < 0) { perror("send() failed"); close(data_socket); data_socket = -1; diff --git a/lib/pbio/platform/virtual_hub/animation.py b/lib/pbio/platform/virtual_hub/animation.py index 9d541eb69..d3fe647dd 100755 --- a/lib/pbio/platform/virtual_hub/animation.py +++ b/lib/pbio/platform/virtual_hub/animation.py @@ -2,7 +2,9 @@ import pygame import socket import threading +import queue from pathlib import Path +import struct HOST = "127.0.0.1" @@ -11,37 +13,26 @@ SCREEN_HEIGHT = 1000 FPS = 25 -# Initialize shared state -angles = [0, 0, 0, 0, 0, 0] +PBIO_PYBRICKS_EVENT_STATUS_REPORT = 0 +PBIO_PYBRICKS_EVENT_WRITE_STDOUT = 1 +PBIO_PYBRICKS_EVENT_WRITE_APP_DATA = 2 +PBIO_PYBRICKS_EVENT_WRITE_PORT_VIEW = 3 + +# Incoming events (stdout, status, app data, port view) +data_queue = queue.Queue() # Handles socket communication. Accepts new connection when client disconnects. # This means it can stay open at all times even when restarting pybricks-micropython # for fast debugging. def socket_listener_thread(): - global angles - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: - s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - s.bind((HOST, PORT)) - s.listen(1) - print(f"Listening on {HOST}:{PORT}") - while True: - conn, addr = s.accept() - print(f"Client connected: {addr}") - with conn: - buffer = b"" - while True: - data = conn.recv(1024) - if not data: - print("Client disconnected.") - break - buffer += data - while b"\n" in buffer: - line, buffer = buffer.split(b"\n", 1) - try: - angles = list(map(int, line.decode().strip().split())) - except ValueError: - print("Invalid data received:", line.decode().strip()) + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.bind((HOST, PORT)) + print(f"Listening on {HOST}:{PORT}") + + while True: + data, _ = sock.recvfrom(1024) + data_queue.put(data) def blit_rotate_at_center(screen, image, position, angle): @@ -63,10 +54,31 @@ def load_and_scale_image(filename, scale=0.25): return pygame.transform.smoothscale(image, scaled_size) -# Main Pygame function -def main(): +# Hub state +angles = [0] * 6 + + +def update_state(): global angles + while not data_queue.empty(): + data = data_queue.get_nowait() + print(data) + if not isinstance(data, bytes) or len(data) < 2: + continue + event = data[0] + payload = data[1:] + + if event == PBIO_PYBRICKS_EVENT_WRITE_STDOUT: + try: + print(payload.decode(), end="") + except UnicodeDecodeError: + print(payload) + elif event == PBIO_PYBRICKS_EVENT_WRITE_PORT_VIEW: + angles = struct.unpack(" Date: Fri, 31 Oct 2025 16:07:04 +0100 Subject: [PATCH 14/26] pbio/platform/sim_hub: Use CLI args for file instead of ENV. We can do this since we renamed main in sys/main recently. This is more practical for running tests. --- .vscode/launch.json | 7 +++--- bricks/simhub/mpconfigport.h | 4 ++++ lib/pbio/platform/sim_hub/platform.c | 29 ++++++++++++++++++++++++- lib/pbio/sys/hmi_env_mpy.c | 32 ++++++---------------------- 4 files changed, 42 insertions(+), 30 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index c84d53295..37a9928ad 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -97,6 +97,9 @@ "type": "cppdbg", "request": "launch", "program": "${workspaceFolder}/bricks/simhub/build-debug/firmware.elf", + "args": [ + "${workspaceFolder}/tests/virtualhub/basics/hello.py", + ], "stopAtEntry": false, "cwd": "${workspaceFolder}", "environment": [ @@ -104,10 +107,6 @@ "name": "PBIO_TEST_CONNECT_SOCKET", "value": "true" }, - { - "name": "TEST_SCRIPT", - "value": "${workspaceFolder}/tests/virtualhub/basics/hello.py" - }, ], "externalConsole": false, "MIMode": "gdb", diff --git a/bricks/simhub/mpconfigport.h b/bricks/simhub/mpconfigport.h index a7d39d9b4..8bdc53449 100644 --- a/bricks/simhub/mpconfigport.h +++ b/bricks/simhub/mpconfigport.h @@ -92,5 +92,9 @@ } while (0); #endif +// Allow printf for conventional purposes on native host, such as printing +// the help info for the executable. This will not go through the simulated +// i/o drivers, but just to stdout. +#define MICROPY_USE_INTERNAL_PRINTF (0) #include "../_common/mpconfigport.h" diff --git a/lib/pbio/platform/sim_hub/platform.c b/lib/pbio/platform/sim_hub/platform.c index e013f06dd..380ca228b 100644 --- a/lib/pbio/platform/sim_hub/platform.c +++ b/lib/pbio/platform/sim_hub/platform.c @@ -1,5 +1,7 @@ // SPDX-License-Identifier: MIT -// Copyright (c) 2023 The Pybricks Authors +// Copyright (c) 2025 The Pybricks Authors + +#include #include "../../drv/motor_driver/motor_driver_virtual_simulation.h" @@ -131,7 +133,32 @@ const pbdrv_motor_driver_virtual_simulation_platform_data_t }, }; +extern uint8_t pbsys_hmi_native_program_buf[PBDRV_CONFIG_BLOCK_DEVICE_RAM_SIZE]; +extern uint32_t pbsys_hmi_native_program_size; + int main(int argc, char **argv) { + + if (argc > 1) { + + // Pybricksdev helper script, pipes multi-mpy to us. + char command[512]; + snprintf(command, sizeof(command), "python ./bricks/simhub/make_mpy.py %s", argv[1]); + FILE *pipe = popen(command, "r"); + if (!pipe) { + printf("Failed to compile program with Pybricksdev\n"); + return 0; + } + + // Read the multi-mpy file from pipe. + pbsys_hmi_native_program_size = fread(pbsys_hmi_native_program_buf, 1, sizeof(pbsys_hmi_native_program_buf), pipe); + pclose(pipe); + + if (pbsys_hmi_native_program_size == 0) { + printf("Error reading from pipe\n"); + return 0; + } + } + extern void _main(void); _main(); } diff --git a/lib/pbio/sys/hmi_env_mpy.c b/lib/pbio/sys/hmi_env_mpy.c index ccf6f75c3..142b4ce26 100644 --- a/lib/pbio/sys/hmi_env_mpy.c +++ b/lib/pbio/sys/hmi_env_mpy.c @@ -31,6 +31,9 @@ void pbsys_hmi_init(void) { void pbsys_hmi_deinit(void) { } +uint8_t pbsys_hmi_native_program_buf[PBDRV_CONFIG_BLOCK_DEVICE_RAM_SIZE]; +uint32_t pbsys_hmi_native_program_size; + pbio_error_t pbsys_hmi_await_program_selection(void) { pbio_os_run_processes_and_wait_for_event(); @@ -42,36 +45,15 @@ pbio_error_t pbsys_hmi_await_program_selection(void) { } ran_once = true; - // Test if script is provided via environment. - const char *script_path = getenv("TEST_SCRIPT"); - if (!script_path) { - // No script given, just start REPL + // Start REPL if no program given. + if (pbsys_hmi_native_program_size == 0) { return pbsys_main_program_request_start(PBIO_PYBRICKS_USER_PROGRAM_ID_REPL, PBSYS_MAIN_PROGRAM_START_REQUEST_TYPE_BOOT); } - // Pybricksdev helper script, pipes multi-mpy to us. - char command[512]; - snprintf(command, sizeof(command), "python ./bricks/simhub/make_mpy.py %s", script_path); - FILE *pipe = popen(command, "r"); - if (!pipe) { - perror("Failed to compile program with Pybricksdev\n"); - return PBIO_ERROR_CANCELED; - } - - // Read the multi-mpy file from pipe. - uint8_t program_buf[PBDRV_CONFIG_BLOCK_DEVICE_RAM_SIZE]; - size_t read_size = fread(program_buf, 1, sizeof(program_buf), pipe); - pclose(pipe); - - if (read_size == 0) { - perror("Error reading from pipe"); - return PBIO_ERROR_CANCELED; - } - // Load the program in storage, as if receiving it. pbsys_storage_set_program_size(0); - pbsys_storage_set_program_data(0, program_buf, read_size); - pbsys_storage_set_program_size(read_size); + pbsys_storage_set_program_data(0, pbsys_hmi_native_program_buf, pbsys_hmi_native_program_size); + pbsys_storage_set_program_size(pbsys_hmi_native_program_size); return pbsys_main_program_request_start(PBIO_PYBRICKS_USER_PROGRAM_ID_FIRST_SLOT, PBSYS_MAIN_PROGRAM_START_REQUEST_TYPE_BOOT); } From ed963db3d7b7c4838303fdc897cafa9367eb380c Mon Sep 17 00:00:00 2001 From: Laurens Valk Date: Fri, 31 Oct 2025 17:00:01 +0100 Subject: [PATCH 15/26] bricks/simhub: Enable sys module. This is needed to run the MicroPython test suite. --- bricks/_common/common.mk | 1 + bricks/simhub/mpconfigport.h | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/bricks/_common/common.mk b/bricks/_common/common.mk index 5eed0b0fe..1af4580ca 100644 --- a/bricks/_common/common.mk +++ b/bricks/_common/common.mk @@ -253,6 +253,7 @@ PY_EXTRA_SRC_C = $(addprefix shared/,\ ifeq ($(PB_MCU_FAMILY),native) PY_EXTRA_SRC_C += $(addprefix shared/,\ runtime/gchelper_generic.c \ + runtime/sys_stdio_mphal.c \ ) PY_EXTRA_SRC_C += $(addprefix bricks/simhub/,\ pbio_os_hook.c \ diff --git a/bricks/simhub/mpconfigport.h b/bricks/simhub/mpconfigport.h index 8bdc53449..f56846adb 100644 --- a/bricks/simhub/mpconfigport.h +++ b/bricks/simhub/mpconfigport.h @@ -49,8 +49,8 @@ #define PYBRICKS_OPT_RAW_REPL (0) #define PYBRICKS_OPT_FLOAT (0) #define PYBRICKS_OPT_TERSE_ERR (0) -#define PYBRICKS_OPT_EXTRA_LEVEL1 (0) -#define PYBRICKS_OPT_EXTRA_LEVEL2 (0) +#define PYBRICKS_OPT_EXTRA_LEVEL1 (1) +#define PYBRICKS_OPT_EXTRA_LEVEL2 (1) #define PYBRICKS_OPT_CUSTOM_IMPORT (0) #define PYBRICKS_OPT_NATIVE_MOD (0) From f002dcca27b267ef6b48df9e4b470e802af494e3 Mon Sep 17 00:00:00 2001 From: Laurens Valk Date: Fri, 31 Oct 2025 19:29:10 +0100 Subject: [PATCH 16/26] bricks/simhub: Enable floating point. Needed for several tests, and this is the closest match to most of our embedded targets. --- bricks/_common/common.mk | 4 ++-- bricks/simhub/mpconfigport.h | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bricks/_common/common.mk b/bricks/_common/common.mk index 1af4580ca..a8f4c0cfa 100644 --- a/bricks/_common/common.mk +++ b/bricks/_common/common.mk @@ -162,7 +162,7 @@ LDFLAGS += -Wl,-Map=$@.map,--cref -Wl,--gc-sections else ifeq ($(UNAME_S),Darwin) LDFLAGS += -Wl,-map,$@.map -Wl,-dead_strip endif -LIBS = +LIBS = -lm else # end native, begin embedded CROSS_COMPILE ?= arm-none-eabi- ifeq ($(PB_MCU_FAMILY),STM32) @@ -230,7 +230,7 @@ CFLAGS += -DSTM32_H='' CFLAGS += -DSTM32_HAL_H='' endif -LIBS = "$(shell $(CC) $(CFLAGS) -print-libgcc-file-name)" +LIBS += "$(shell $(CC) $(CFLAGS) -print-libgcc-file-name)" # Sources and libraries common to all pybricks bricks diff --git a/bricks/simhub/mpconfigport.h b/bricks/simhub/mpconfigport.h index f56846adb..660ab6bb9 100644 --- a/bricks/simhub/mpconfigport.h +++ b/bricks/simhub/mpconfigport.h @@ -47,7 +47,7 @@ #define PYBRICKS_OPT_COMPILER (1) #define PYBRICKS_OPT_USE_STACK_END_AS_TOP (1) #define PYBRICKS_OPT_RAW_REPL (0) -#define PYBRICKS_OPT_FLOAT (0) +#define PYBRICKS_OPT_FLOAT (1) #define PYBRICKS_OPT_TERSE_ERR (0) #define PYBRICKS_OPT_EXTRA_LEVEL1 (1) #define PYBRICKS_OPT_EXTRA_LEVEL2 (1) From faafa415dcee4e865b17eb7f2832052190e3026f Mon Sep 17 00:00:00 2001 From: Laurens Valk Date: Fri, 31 Oct 2025 20:02:23 +0100 Subject: [PATCH 17/26] pbio/platform/sim_hub: Use wall clock by default. --- .vscode/tasks.json | 2 +- lib/pbio/platform/sim_hub/pbdrvconfig.h | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.vscode/tasks.json b/.vscode/tasks.json index ebf34b50c..db6606257 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -47,7 +47,7 @@ }, "label": "build simhub (debug)", "type": "shell", - "command": "make -C ../../micropython/mpy-cross -j && poetry run make DEBUG=1 BUILD=build-debug COPT=-DPBDRV_CONFIG_CLOCK_LOCAL -j", + "command": "make -C ../../micropython/mpy-cross -j && poetry run make DEBUG=1 BUILD=build-debug -j", }, { "label": "build test-pbio", diff --git a/lib/pbio/platform/sim_hub/pbdrvconfig.h b/lib/pbio/platform/sim_hub/pbdrvconfig.h index 097fd853e..c8701a2bc 100644 --- a/lib/pbio/platform/sim_hub/pbdrvconfig.h +++ b/lib/pbio/platform/sim_hub/pbdrvconfig.h @@ -15,10 +15,10 @@ #define PBDRV_CONFIG_BUTTON_TEST (1) #define PBDRV_CONFIG_CLOCK (1) -#ifdef PBDRV_CONFIG_CLOCK_LOCAL -#define PBDRV_CONFIG_CLOCK_LINUX (1) -#else +#ifdef PBDRV_CONFIG_CLOCK_CI #define PBDRV_CONFIG_CLOCK_TEST (1) +#else +#define PBDRV_CONFIG_CLOCK_LINUX (1) #endif #define PBDRV_CONFIG_COUNTER (1) From 8e9848f4f1cc91475debdee6ad60ad9c0d7613b3 Mon Sep 17 00:00:00 2001 From: Laurens Valk Date: Fri, 31 Oct 2025 20:45:18 +0100 Subject: [PATCH 18/26] bricks/simhub: Replace mpy wrapper script with Pybricksdev call. This has been added to Pybricksdev. We want to avoid using a local script since it makes relative paths complicated when running this from the MicroPython test scripts. Also explicitly pass the last argument, in order to ignore the test suite passing flags such as -X. --- bricks/simhub/make_mpy.py | 15 --------------- lib/pbio/platform/sim_hub/platform.c | 2 +- 2 files changed, 1 insertion(+), 16 deletions(-) delete mode 100644 bricks/simhub/make_mpy.py diff --git a/bricks/simhub/make_mpy.py b/bricks/simhub/make_mpy.py deleted file mode 100644 index 817d76d19..000000000 --- a/bricks/simhub/make_mpy.py +++ /dev/null @@ -1,15 +0,0 @@ -from pybricksdev.compile import compile_multi_file -import asyncio -import argparse -import sys - -# Set up argument parser -parser = argparse.ArgumentParser(description="Compile to multi-mpy using pybricksdev.") -parser.add_argument("file", help="The path to the Python file to compile.") -args = parser.parse_args() - -# Compile the file -result = asyncio.run(compile_multi_file(args.file, (6, 1))) - -# Write the result to stdout -sys.stdout.buffer.write(result) diff --git a/lib/pbio/platform/sim_hub/platform.c b/lib/pbio/platform/sim_hub/platform.c index 380ca228b..2f26a4c25 100644 --- a/lib/pbio/platform/sim_hub/platform.c +++ b/lib/pbio/platform/sim_hub/platform.c @@ -142,7 +142,7 @@ int main(int argc, char **argv) { // Pybricksdev helper script, pipes multi-mpy to us. char command[512]; - snprintf(command, sizeof(command), "python ./bricks/simhub/make_mpy.py %s", argv[1]); + snprintf(command, sizeof(command), "pybricksdev compile --bin %s", argv[argc - 1]); FILE *pipe = popen(command, "r"); if (!pipe) { printf("Failed to compile program with Pybricksdev\n"); From fd165dd36cab5e8efd13bf8ea1456a6210be3c11 Mon Sep 17 00:00:00 2001 From: Laurens Valk Date: Fri, 31 Oct 2025 20:50:47 +0100 Subject: [PATCH 19/26] tests: Use embedded virtual hub. This is closer to the real deal. --- test-virtualhub.sh | 8 +++---- tests/virtualhub/motor/test_1.py | 34 ---------------------------- tests/virtualhub/motor/test_1.py.exp | 1 - 3 files changed, 3 insertions(+), 40 deletions(-) delete mode 100644 tests/virtualhub/motor/test_1.py delete mode 100644 tests/virtualhub/motor/test_1.py.exp diff --git a/test-virtualhub.sh b/test-virtualhub.sh index 7ed952f74..492637e43 100755 --- a/test-virtualhub.sh +++ b/test-virtualhub.sh @@ -1,6 +1,6 @@ #!/bin/bash # -# Runs tests on virtualhub. +# Runs tests on simhub. # # Use `--list-test` to list tests or `--include ` to run single tests. # @@ -12,7 +12,7 @@ if [[ $CI != "true" ]]; then fi SCRIPT_DIR=$(readlink -f "$(dirname "$0")") -BRICK_DIR="$SCRIPT_DIR/bricks/virtualhub" +BRICK_DIR="$SCRIPT_DIR/bricks/simhub" MP_TEST_DIR="$SCRIPT_DIR/micropython/tests" PB_TEST_DIR=$"$SCRIPT_DIR/tests" BUILD_DIR="$BRICK_DIR/build${COVERAGE:+-coverage}" @@ -20,9 +20,7 @@ PBIO_DIR="$SCRIPT_DIR/lib/pbio" make -s -j $(nproc --all) -C "$BRICK_DIR" -export MICROPY_MICROPYTHON="$BUILD_DIR/virtualhub-micropython" -export PYTHONPATH="$PBIO_DIR/cpython" -export PBIO_VIRTUAL_PLATFORM_MODULE=pbio_virtual.platform.robot +export MICROPY_MICROPYTHON="$BUILD_DIR/firmware.elf" cd "$MP_TEST_DIR" ./run-tests.py --test-dirs $(find "$PB_TEST_DIR/virtualhub" -type d -and ! -wholename "*/build/*" -and ! -wholename "*/run_test.py") "$@" || \ diff --git a/tests/virtualhub/motor/test_1.py b/tests/virtualhub/motor/test_1.py deleted file mode 100644 index 7fd433c88..000000000 --- a/tests/virtualhub/motor/test_1.py +++ /dev/null @@ -1,34 +0,0 @@ -from pybricks.pupdevices import Motor -from pybricks.parameters import Port -from pybricks.tools import wait, StopWatch - -from uerrno import ETIMEDOUT -from uos import getenv - -# environment variables allow running one-off tests -TARGET_ANGLE = int(getenv("TARGET_ANGLE", 1000)) -SPEED = int(getenv("SPEED", 500)) -TIMEOUT = int(getenv("TIMEOUT", 10000)) -TOLERANCE = int(getenv("TOLERANCE", 5)) - -motor = Motor(Port.A) -try: - motor.control.limits(speed=2001) -except ValueError: - print("ValueError") - motor.control.limits(speed=2000) -watch = StopWatch() - -# In the future, replace with async, but this will do fine today -motor.run_target(speed=SPEED, target_angle=TARGET_ANGLE, wait=False) - -while not motor.done(): - if watch.time() > TIMEOUT: - raise OSError(ETIMEDOUT) - - wait(10) - -# Various checks -assert TARGET_ANGLE - TOLERANCE <= motor.angle() <= TARGET_ANGLE + TOLERANCE - -# No exceptions or output means success diff --git a/tests/virtualhub/motor/test_1.py.exp b/tests/virtualhub/motor/test_1.py.exp deleted file mode 100644 index 94274de1b..000000000 --- a/tests/virtualhub/motor/test_1.py.exp +++ /dev/null @@ -1 +0,0 @@ -ValueError From 8a2ac9941eb931200cecc767d67a186e946522b3 Mon Sep 17 00:00:00 2001 From: Laurens Valk Date: Sat, 1 Nov 2025 18:56:06 +0100 Subject: [PATCH 20/26] tests/virtualhub/motor: Make drivebase tests actually run. These used to be artificially shortened on the virtual hub, so never ran in full. Split it in two parts to get under the test timeout instead. --- tests/virtualhub/motor/drivebase_turns.py | 104 +----------------- tests/virtualhub/motor/drivebase_turns2.py | 76 +++++++++++++ .../virtualhub/motor/drivebase_turns2.py.exp | 0 3 files changed, 79 insertions(+), 101 deletions(-) create mode 100644 tests/virtualhub/motor/drivebase_turns2.py create mode 100644 tests/virtualhub/motor/drivebase_turns2.py.exp diff --git a/tests/virtualhub/motor/drivebase_turns.py b/tests/virtualhub/motor/drivebase_turns.py index 7ece9d939..f4502d32a 100644 --- a/tests/virtualhub/motor/drivebase_turns.py +++ b/tests/virtualhub/motor/drivebase_turns.py @@ -8,28 +8,8 @@ motorR = Motor(Port.B) drive_base = DriveBase(motorL, motorR, wheel_diameter=56, axle_track=80) -try: - import ffi - - virtual = True -except ImportError: - virtual = False - - -# Wait on a real robot but not in the simulator. -def delay(ms): - if virtual: - return - wait(ms) - def wait_until_done(): - # Stop immediately if virtual. - if virtual: - drive_base.straight(0) - wait(30) - return - # Wait until the drive base command completes normally. while not drive_base.done(): wait(10) @@ -77,25 +57,12 @@ def wait_until_done(): ) wait_until_done() -drive_base.turn(1080, wait=False) +drive_base.turn(540, wait=False) wait(STARTUP_DELAY) -assert motorL.speed() > 0, "For CW in-place turn 1080 degrees, left speed should be +ve" -assert motorR.speed() < 0, ( - "For CW in-place turn 1080 degrees, right speed should be -ve" -) +assert motorL.speed() > 0, "For CW in-place turn 540 degrees, left speed should be +ve" +assert motorR.speed() < 0, "For CW in-place turn 540 degrees, right speed should be -ve" wait_until_done() -drive_base.turn(-1440, wait=False) -wait(STARTUP_DELAY) -assert motorL.speed() < 0, ( - "For CCW in-place turn 1440 degrees, left speed should be -ve" -) -assert motorR.speed() > 0, ( - "For CCW in-place turn 1440 degrees, right speed should be +ve" -) -wait_until_done() - - # Demo 3: Using curve() drives the 'center point' between the wheels a full # circle (360 degrees or part thereof) around a circle of 12cm radius. drive_base.curve(120, 360, wait=False) # Drives forward along circle to robot's right @@ -135,68 +102,3 @@ def wait_until_done(): "When driving Rev in CW Curve, motorL should be greater than motorR" ) wait_until_done() - -# Demo 4: Using drive() to control the robot in various ways. -drive_base.drive(200, 0) # Drive straight Fwd -wait(STARTUP_DELAY_LONG) -assert motorL.speed() > 0, "When driving Fwd in straight line, left speed should be +ve" -assert motorR.speed() > 0, ( - "When driving Fwd in straight line, right speed should be +ve" -) -delay(500) -drive_base.stop() -delay(250) - -drive_base.drive(-200, 0) # Drive straight Rev -wait(STARTUP_DELAY_LONG) -assert motorL.speed() < 0, "When driving Rev in straight line, left speed should be -ve" -assert motorR.speed() < 0, ( - "When driving Rev in straight line, right speed should be -ve" -) -delay(500) -drive_base.stop() -delay(250) - -drive_base.drive(200, 90) # Drives CW Fwd -wait(STARTUP_DELAY_LONG) -assert motorL.speed() > 0, "When driving CW Curve Fwd, left speed should be +ve" -assert motorR.speed() > 0, "When driving CW Curve Fwd, right speed should be +ve" -assert motorL.speed() > motorR.speed(), ( - "When driving Fwd in CW Curve, motorL should be greater than motorR" -) -delay(3000) # 4 seconds x 90 deg/s approx full circle -drive_base.stop() -delay(250) - -drive_base.drive(-200, 90) # Drives CW Rev -wait(STARTUP_DELAY_LONG) -assert motorL.speed() < 0, "When driving CCW Curve Rev, left speed should be -ve" -assert motorR.speed() < 0, "When driving CCW Curve Rev, right speed should be -ve" -assert motorR.speed() < motorL.speed(), ( - "When driving Rev in CCW Curve, motorR should be less than motorL" -) -delay(3000) # 4 seconds x 90 deg/s = full circle -drive_base.stop() -delay(250) - -drive_base.drive(200, -90) # Drives CCW Fwd -wait(STARTUP_DELAY_LONG) -assert motorL.speed() > 0, "When driving CCW Curve Fwd, left speed should be +ve" -assert motorR.speed() > 0, "When driving CCW Curve Fwd, right speed should be +ve" -assert motorL.speed() < motorR.speed(), ( - "When driving Fwd in CCW Curve, motorL should be less than motorR" -) -delay(3000) # 4 seconds x 90 deg/s = full circle -drive_base.stop() -delay(250) - -drive_base.drive(-200, -90) # In corrected version it should drive CW in Rev -wait(STARTUP_DELAY_LONG) -assert motorL.speed() < 0, "When driving CW Curve Rev, left speed should be +ve" -assert motorR.speed() < 0, "When driving CW Curve Rev, right speed should be +ve" -assert motorL.speed() < motorR.speed(), ( - "When driving Rev in CW Curve, motorL should be less than motorR" -) -delay(3000) # 4 seconds x 90 deg/s = full circle -drive_base.stop() -delay(250) diff --git a/tests/virtualhub/motor/drivebase_turns2.py b/tests/virtualhub/motor/drivebase_turns2.py new file mode 100644 index 000000000..056394192 --- /dev/null +++ b/tests/virtualhub/motor/drivebase_turns2.py @@ -0,0 +1,76 @@ +from pybricks.pupdevices import Motor +from pybricks.parameters import Direction, Port +from pybricks.robotics import DriveBase +from pybricks.tools import wait + +# Initialize the drive base. +motorL = Motor(Port.A, Direction.COUNTERCLOCKWISE) +motorR = Motor(Port.B) +drive_base = DriveBase(motorL, motorR, wheel_diameter=56, axle_track=80) + +STARTUP_DELAY = 500 + +# Demo 4: Using drive() to control the robot in various ways. +drive_base.drive(200, 0) # Drive straight Fwd +wait(STARTUP_DELAY) +assert motorL.speed() > 0, "When driving Fwd in straight line, left speed should be +ve" +assert motorR.speed() > 0, ( + "When driving Fwd in straight line, right speed should be +ve" +) +wait(500) +drive_base.stop() +wait(250) + +drive_base.drive(-200, 0) # Drive straight Rev +wait(STARTUP_DELAY) +assert motorL.speed() < 0, "When driving Rev in straight line, left speed should be -ve" +assert motorR.speed() < 0, ( + "When driving Rev in straight line, right speed should be -ve" +) +wait(500) +drive_base.stop() +wait(250) + +drive_base.drive(200, 90) # Drives CW Fwd +wait(STARTUP_DELAY) +assert motorL.speed() > 0, "When driving CW Curve Fwd, left speed should be +ve" +assert motorR.speed() > 0, "When driving CW Curve Fwd, right speed should be +ve" +assert motorL.speed() > motorR.speed(), ( + "When driving Fwd in CW Curve, motorL should be greater than motorR" +) +wait(3000) # 4 seconds x 90 deg/s approx full circle +drive_base.stop() +wait(250) + +drive_base.drive(-200, 90) # Drives CW Rev +wait(STARTUP_DELAY) +assert motorL.speed() < 0, "When driving CCW Curve Rev, left speed should be -ve" +assert motorR.speed() < 0, "When driving CCW Curve Rev, right speed should be -ve" +assert motorR.speed() < motorL.speed(), ( + "When driving Rev in CCW Curve, motorR should be less than motorL" +) +wait(3000) # 4 seconds x 90 deg/s = full circle +drive_base.stop() +wait(250) + +drive_base.drive(200, -90) # Drives CCW Fwd +wait(STARTUP_DELAY) +assert motorL.speed() > 0, "When driving CCW Curve Fwd, left speed should be +ve" +assert motorR.speed() > 0, "When driving CCW Curve Fwd, right speed should be +ve" +assert motorL.speed() < motorR.speed(), ( + "When driving Fwd in CCW Curve, motorL should be less than motorR" +) +wait(3000) # 4 seconds x 90 deg/s = full circle +drive_base.stop() +wait(250) + +drive_base.drive(-200, -90) # In corrected version it should drive CW in Rev +wait(STARTUP_DELAY) +assert motorL.speed() < 0, "When driving CW Curve Rev, left speed should be +ve" +assert motorR.speed() < 0, "When driving CW Curve Rev, right speed should be +ve" +assert motorL.speed() < motorR.speed(), ( + "When driving Rev in CW Curve, motorL should be less than motorR" +) +wait(3000) # 4 seconds x 90 deg/s = full circle +drive_base.stop() +wait(250) diff --git a/tests/virtualhub/motor/drivebase_turns2.py.exp b/tests/virtualhub/motor/drivebase_turns2.py.exp new file mode 100644 index 000000000..e69de29bb From 7360359c9af4a9b13ffab9f28efe57f7c2a514ad Mon Sep 17 00:00:00 2001 From: Laurens Valk Date: Sun, 2 Nov 2025 11:37:20 +0100 Subject: [PATCH 21/26] pbio/drv/clock_test: Fix counting clock. It did not advance, and even if it did it would use its own counter in different places. --- bricks/simhub/mpconfigport.h | 7 ++----- lib/pbio/drv/clock/clock_test.c | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/bricks/simhub/mpconfigport.h b/bricks/simhub/mpconfigport.h index 660ab6bb9..dd624357e 100644 --- a/bricks/simhub/mpconfigport.h +++ b/bricks/simhub/mpconfigport.h @@ -65,11 +65,8 @@ // every couple of MicroPython byte codes. This also polls to the event loop. #define PYBRICKS_VM_HOOK_LOOP_EXTRA \ do { \ - static uint32_t count; \ - if ((count % 16) == 0) { \ - extern void pbio_test_clock_tick(uint32_t ticks); \ - pbio_test_clock_tick(1); \ - } \ + extern void pbio_clock_test_advance_eventually(void); \ + pbio_clock_test_advance_eventually(); \ } while (0); #else // When using the wall clock, time advances automatically but we still need to diff --git a/lib/pbio/drv/clock/clock_test.c b/lib/pbio/drv/clock/clock_test.c index 670064028..39b90f623 100644 --- a/lib/pbio/drv/clock/clock_test.c +++ b/lib/pbio/drv/clock/clock_test.c @@ -26,6 +26,23 @@ void pbio_test_clock_tick(uint32_t ticks) { pbio_os_request_poll(); } +/** + * Simulates incrementing the clock after a certain number of CPU cycles. + * + * This should be called from loops that take a very small but nonzero amount + * of time. + */ +void pbio_clock_test_advance_eventually(void) { + + static uint32_t count = 0; + + if (++count % 16) { + return; + } + + pbio_test_clock_tick(1); +} + void pbdrv_clock_init(void) { } From 92a6bb03c5aaa85fe9558085f5a15db2b8345a04 Mon Sep 17 00:00:00 2001 From: Laurens Valk Date: Sun, 2 Nov 2025 14:26:51 +0100 Subject: [PATCH 22/26] pbio/platform/sim_hub: Set and reset terminal settings from main. This makes more sense as the entry and exit point. --- lib/pbio/drv/bluetooth/bluetooth_simulation.c | 56 +----------------- lib/pbio/platform/sim_hub/platform.c | 58 ++++++++++++++++++- 2 files changed, 57 insertions(+), 57 deletions(-) diff --git a/lib/pbio/drv/bluetooth/bluetooth_simulation.c b/lib/pbio/drv/bluetooth/bluetooth_simulation.c index e8cbc8a5c..619aaa391 100644 --- a/lib/pbio/drv/bluetooth/bluetooth_simulation.c +++ b/lib/pbio/drv/bluetooth/bluetooth_simulation.c @@ -6,11 +6,9 @@ #if PBDRV_CONFIG_BLUETOOTH_SIMULATION #include -#include #include #include #include -#include #include #include "bluetooth.h" @@ -132,6 +130,8 @@ pbio_error_t pbdrv_bluetooth_controller_initialize(pbio_os_state_t *state, pbio_ static void pbdrv_bluetooth_simulation_tick_handler() { uint8_t buf[256 + STDIN_HEADER_SIZE]; + + // This has been made non-blocking in platform.c. ssize_t r = read(STDIN_FILENO, buf + STDIN_HEADER_SIZE, sizeof(buf) - STDIN_HEADER_SIZE); if (r > 0) { @@ -170,59 +170,7 @@ static pbio_error_t pbdrv_bluetooth_simulation_process_thread(pbio_os_state_t *s return bluetooth_thread_err; } -static struct termios oldt; - -static void restore_terminal_settings(void) { - tcsetattr(STDIN_FILENO, TCSANOW, &oldt); -} - -static void handle_signal(int sig) { - restore_terminal_settings(); - signal(sig, SIG_DFL); - raise(sig); -} - void pbdrv_bluetooth_init_hci(void) { - struct termios newt; - - // Save the original terminal settings - if (tcgetattr(STDIN_FILENO, &oldt) != 0) { - printf("DEBUG: Failed to get terminal attributes\n"); - return; - } - - // Register the cleanup function to restore terminal settings on exit - atexit(restore_terminal_settings); - signal(SIGINT, handle_signal); - signal(SIGTERM, handle_signal); - - newt = oldt; - - // Get one char at a time instead of newline and disable CTRL+C for exit. - newt.c_lflag &= ~(ICANON | ECHO | ISIG); - - // MicroPython REPL expects \r for newline. - newt.c_iflag |= INLCR; - newt.c_iflag &= ~ICRNL; - - if (tcsetattr(STDIN_FILENO, TCSANOW, &newt) != 0) { - printf("Failed to set terminal attributes\n"); - return; - } - - // Set stdin non-blocking so we can service it in the runloop like on - // embedded hubs. - int flags = fcntl(STDIN_FILENO, F_GETFL, 0); - if (flags == -1) { - printf("Failed to get fcntl flags\n"); - return; - } - - if (fcntl(STDIN_FILENO, F_SETFL, flags | O_NONBLOCK) == -1) { - printf("Failed to set non-blocking\n"); - return; - } - bluetooth_thread_err = PBIO_ERROR_AGAIN; bluetooth_thread_state = 0; pbio_os_process_start(&pbdrv_bluetooth_simulation_process, pbdrv_bluetooth_simulation_process_thread, NULL); diff --git a/lib/pbio/platform/sim_hub/platform.c b/lib/pbio/platform/sim_hub/platform.c index 2f26a4c25..c310cd35a 100644 --- a/lib/pbio/platform/sim_hub/platform.c +++ b/lib/pbio/platform/sim_hub/platform.c @@ -1,7 +1,13 @@ // SPDX-License-Identifier: MIT // Copyright (c) 2025 The Pybricks Authors +#include +#include +#include #include +#include +#include +#include #include "../../drv/motor_driver/motor_driver_virtual_simulation.h" @@ -133,11 +139,9 @@ const pbdrv_motor_driver_virtual_simulation_platform_data_t }, }; -extern uint8_t pbsys_hmi_native_program_buf[PBDRV_CONFIG_BLOCK_DEVICE_RAM_SIZE]; -extern uint32_t pbsys_hmi_native_program_size; - int main(int argc, char **argv) { + // Parse given program, else otherwise default to REPL. if (argc > 1) { // Pybricksdev helper script, pipes multi-mpy to us. @@ -150,6 +154,8 @@ int main(int argc, char **argv) { } // Read the multi-mpy file from pipe. + extern uint8_t pbsys_hmi_native_program_buf[PBDRV_CONFIG_BLOCK_DEVICE_RAM_SIZE]; + extern uint32_t pbsys_hmi_native_program_size; pbsys_hmi_native_program_size = fread(pbsys_hmi_native_program_buf, 1, sizeof(pbsys_hmi_native_program_buf), pipe); pclose(pipe); @@ -159,6 +165,52 @@ int main(int argc, char **argv) { } } + // Save the original terminal settings + struct termios term_old, term_new; + if (tcgetattr(STDIN_FILENO, &term_old) != 0) { + printf("DEBUG: Failed to get terminal attributes\n"); + return 0; + } + term_new = term_old; + + // Get one char at a time instead of newline and disable CTRL+C for exit. + term_new.c_lflag &= ~(ICANON | ECHO | ISIG); + + // MicroPython REPL expects \r for newline. + term_new.c_iflag |= INLCR; + term_new.c_iflag &= ~ICRNL; + + if (tcsetattr(STDIN_FILENO, TCSANOW, &term_new) != 0) { + printf("Failed to set terminal attributes\n"); + return 0; + } + + // Set stdin non-blocking so we can service it in the runloop like on + // embedded hubs. + int stdin_flags = fcntl(STDIN_FILENO, F_GETFL, 0); + if (stdin_flags == -1) { + printf("Failed to get fcntl flags\n"); + return 0; + } + if (fcntl(STDIN_FILENO, F_SETFL, stdin_flags | O_NONBLOCK) == -1) { + printf("Failed to set non-blocking\n"); + return 0; + } + + // Runs the 'embedded' main. extern void _main(void); _main(); + + // Restore stdin flags. + if (fcntl(STDIN_FILENO, F_SETFL, stdin_flags) == -1) { + printf("Failed to restore stdin flags\n"); + } + + // Restore terminal settings. + if (tcsetattr(STDIN_FILENO, TCSANOW, &term_old) != 0) { + printf("Failed to restore terminal attributes\n"); + return 0; + } + + return 0; } From 44eea1097b7fe15fe6bca83e07d2986bf77fe775 Mon Sep 17 00:00:00 2001 From: Laurens Valk Date: Sun, 2 Nov 2025 15:04:23 +0100 Subject: [PATCH 23/26] pbio/platform/sim_hub: Don't modify stdin on CI. This caused a lot of headaches when the MicroPython test suite runs many instances at the same time. --- lib/pbio/drv/bluetooth/bluetooth_simulation.c | 6 ++++++ lib/pbio/platform/sim_hub/platform.c | 13 +++++++++++-- test-virtualhub.sh | 2 +- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/lib/pbio/drv/bluetooth/bluetooth_simulation.c b/lib/pbio/drv/bluetooth/bluetooth_simulation.c index 619aaa391..cb239f22f 100644 --- a/lib/pbio/drv/bluetooth/bluetooth_simulation.c +++ b/lib/pbio/drv/bluetooth/bluetooth_simulation.c @@ -129,6 +129,12 @@ pbio_error_t pbdrv_bluetooth_controller_initialize(pbio_os_state_t *state, pbio_ #define STDIN_HEADER_SIZE (1) static void pbdrv_bluetooth_simulation_tick_handler() { + #ifdef PBDRV_CONFIG_RUN_ON_CI + // CI and MicroPython test suite have lots of problems with stdin. It is + // only needed for the REPL and interactive input, so don't bother on CI. + return; + #endif + uint8_t buf[256 + STDIN_HEADER_SIZE]; // This has been made non-blocking in platform.c. diff --git a/lib/pbio/platform/sim_hub/platform.c b/lib/pbio/platform/sim_hub/platform.c index c310cd35a..7e5234d8b 100644 --- a/lib/pbio/platform/sim_hub/platform.c +++ b/lib/pbio/platform/sim_hub/platform.c @@ -139,6 +139,9 @@ const pbdrv_motor_driver_virtual_simulation_platform_data_t }, }; +// The 'embedded' main. +extern void _main(void); + int main(int argc, char **argv) { // Parse given program, else otherwise default to REPL. @@ -165,6 +168,13 @@ int main(int argc, char **argv) { } } + #ifdef PBDRV_CONFIG_RUN_ON_CI + // On the CI modifying settings for stdin causes problems. The REPL isn't + // used on CI anyway. + _main(); + return 0; + #endif + // Save the original terminal settings struct termios term_old, term_new; if (tcgetattr(STDIN_FILENO, &term_old) != 0) { @@ -197,8 +207,7 @@ int main(int argc, char **argv) { return 0; } - // Runs the 'embedded' main. - extern void _main(void); + // Simulate running embedded main. _main(); // Restore stdin flags. diff --git a/test-virtualhub.sh b/test-virtualhub.sh index 492637e43..04414e4d3 100755 --- a/test-virtualhub.sh +++ b/test-virtualhub.sh @@ -18,7 +18,7 @@ PB_TEST_DIR=$"$SCRIPT_DIR/tests" BUILD_DIR="$BRICK_DIR/build${COVERAGE:+-coverage}" PBIO_DIR="$SCRIPT_DIR/lib/pbio" -make -s -j $(nproc --all) -C "$BRICK_DIR" +make -s -j $(nproc --all) -C "$BRICK_DIR" COPT=-DPBDRV_CONFIG_RUN_ON_CI export MICROPY_MICROPYTHON="$BUILD_DIR/firmware.elf" From 1800141cae0336dc1a387d555d1e9138b01b5821 Mon Sep 17 00:00:00 2001 From: Laurens Valk Date: Sun, 2 Nov 2025 15:12:32 +0100 Subject: [PATCH 24/26] bricks/simhub: Run tests with coverage. --- bricks/_common/common.mk | 7 +++++-- lib/pbio/platform/sim_hub/pbdrvconfig.h | 2 +- test-virtualhub.sh | 6 ++++-- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/bricks/_common/common.mk b/bricks/_common/common.mk index a8f4c0cfa..16f67d84f 100644 --- a/bricks/_common/common.mk +++ b/bricks/_common/common.mk @@ -213,8 +213,11 @@ CFLAGS += -fsingle-precision-constant -Wdouble-promotion endif # end embedded, begin common # Tune for Debugging or Optimization -ifeq ($(DEBUG), 1) -CFLAGS += -Og -ggdb +ifeq ($(COVERAGE), 1) +CFLAGS += --coverage -fprofile-arcs -ftest-coverage +LDFLAGS += --coverage +else ifeq ($(DEBUG), 1) +CFLAGS += -O0 -ggdb else ifeq ($(DEBUG), 2) CFLAGS += -Os -DNDEBUG -flto=auto else diff --git a/lib/pbio/platform/sim_hub/pbdrvconfig.h b/lib/pbio/platform/sim_hub/pbdrvconfig.h index c8701a2bc..5282d601d 100644 --- a/lib/pbio/platform/sim_hub/pbdrvconfig.h +++ b/lib/pbio/platform/sim_hub/pbdrvconfig.h @@ -15,7 +15,7 @@ #define PBDRV_CONFIG_BUTTON_TEST (1) #define PBDRV_CONFIG_CLOCK (1) -#ifdef PBDRV_CONFIG_CLOCK_CI +#ifdef PBDRV_CONFIG_RUN_ON_CI #define PBDRV_CONFIG_CLOCK_TEST (1) #else #define PBDRV_CONFIG_CLOCK_LINUX (1) diff --git a/test-virtualhub.sh b/test-virtualhub.sh index 04414e4d3..7ebd5908b 100755 --- a/test-virtualhub.sh +++ b/test-virtualhub.sh @@ -11,14 +11,16 @@ if [[ $CI != "true" ]]; then NOT_CI="true" fi +: ${COVERAGE:=1} SCRIPT_DIR=$(readlink -f "$(dirname "$0")") BRICK_DIR="$SCRIPT_DIR/bricks/simhub" MP_TEST_DIR="$SCRIPT_DIR/micropython/tests" PB_TEST_DIR=$"$SCRIPT_DIR/tests" -BUILD_DIR="$BRICK_DIR/build${COVERAGE:+-coverage}" +BUILD_DIR_NAME="build${COVERAGE:+-coverage}" +BUILD_DIR="$BRICK_DIR/$BUILD_DIR_NAME" PBIO_DIR="$SCRIPT_DIR/lib/pbio" -make -s -j $(nproc --all) -C "$BRICK_DIR" COPT=-DPBDRV_CONFIG_RUN_ON_CI +make -s -j $(nproc --all) -C "$BRICK_DIR" BUILD="$BUILD_DIR_NAME" COPT=-DPBDRV_CONFIG_RUN_ON_CI export MICROPY_MICROPYTHON="$BUILD_DIR/firmware.elf" From 489769a7b07779f7fb5448eed0a88a25017072fb Mon Sep 17 00:00:00 2001 From: Laurens Valk Date: Sun, 2 Nov 2025 15:50:09 +0100 Subject: [PATCH 25/26] .github: Run new simulated hub on CI. --- .github/workflows/build.yml | 10 ++++++---- test-virtualhub.sh | 4 +++- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cf089e3c9..1dca90169 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -145,8 +145,10 @@ jobs: needs: [mpy_cross] runs-on: ubuntu-24.04 steps: - - name: Install depedencies - run: sudo apt-get update && sudo apt-get install lcov python3-numpy --yes + - name: Install dependencies + run: sudo apt-get update && sudo apt-get install lcov pipx + - name: Install Pybricksdev + run: pipx install pybricksdev - name: Checkout repo uses: actions/checkout@v4 with: @@ -166,12 +168,12 @@ jobs: - name: Fix file permission run: chmod +x micropython/mpy-cross/build/mpy-cross - name: Build and test - run: COVERAGE=1 ./test-virtualhub.sh + run: ./test-virtualhub.sh - name: Coveralls uses: coverallsapp/github-action@v2 with: github-token: ${{ secrets.GITHUB_TOKEN }} - file: bricks/virtualhub/build-coverage/lcov.info + file: bricks/simhub/build-coverage/lcov.info flag-name: virtualhub parallel: true diff --git a/test-virtualhub.sh b/test-virtualhub.sh index 7ebd5908b..671841ca9 100755 --- a/test-virtualhub.sh +++ b/test-virtualhub.sh @@ -11,7 +11,9 @@ if [[ $CI != "true" ]]; then NOT_CI="true" fi -: ${COVERAGE:=1} +COVERAGE=1 +export COVERAGE + SCRIPT_DIR=$(readlink -f "$(dirname "$0")") BRICK_DIR="$SCRIPT_DIR/bricks/simhub" MP_TEST_DIR="$SCRIPT_DIR/micropython/tests" From 3f9b0a3c0f849589d03261e1fcc95a24869ac69e Mon Sep 17 00:00:00 2001 From: Laurens Valk Date: Sun, 2 Nov 2025 16:00:55 +0100 Subject: [PATCH 26/26] bricks/virtualhub: Replace with embedded simulation. Instead of using the newly introduced simhub alongside the virtualhub, we'll just replace the old one entirely now that it has reached feature parity. We can keep calling it the virtualhub. --- .github/workflows/build.yml | 2 +- .vscode/c_cpp_properties.json | 33 +-- .vscode/launch.json | 40 +--- .vscode/tasks.json | 9 +- Makefile | 15 +- bricks/_common/common.mk | 2 +- bricks/simhub/Makefile | 10 - bricks/simhub/qstrdefsport.h | 2 - bricks/{simhub => virtualhub}/.gitignore | 0 bricks/virtualhub/Makefile | 51 +--- bricks/virtualhub/README.md | 60 ----- bricks/{simhub => virtualhub}/manifest.py | 0 bricks/virtualhub/mp_port.c | 157 ------------ bricks/{simhub => virtualhub}/mpconfigport.h | 0 bricks/virtualhub/mpconfigvariant.h | 114 --------- bricks/virtualhub/mpconfigvariant.mk | 73 ------ bricks/virtualhub/mpvarianthal.h | 109 --------- bricks/{simhub => virtualhub}/pbio_os_hook.c | 0 bricks/virtualhub/qstrdefsport.h | 3 +- lib/pbio/platform/sim_hub/contiki-conf.h | 18 -- lib/pbio/platform/sim_hub/pbdrvconfig.h | 42 ---- lib/pbio/platform/sim_hub/pbio_os_config.h | 9 - lib/pbio/platform/sim_hub/pbioconfig.h | 29 --- lib/pbio/platform/sim_hub/pbsysconfig.h | 29 --- lib/pbio/platform/sim_hub/platform.c | 225 ------------------ lib/pbio/platform/virtual_hub/pbdrvconfig.h | 12 +- lib/pbio/platform/virtual_hub/pbsysconfig.h | 21 +- lib/pbio/platform/virtual_hub/platform.c | 95 +++++++- .../{sim_hub => virtual_hub}/startup.s | 0 test-virtualhub.sh | 4 +- tests/README.md | 8 +- 31 files changed, 148 insertions(+), 1024 deletions(-) delete mode 100644 bricks/simhub/Makefile delete mode 100644 bricks/simhub/qstrdefsport.h rename bricks/{simhub => virtualhub}/.gitignore (100%) delete mode 100644 bricks/virtualhub/README.md rename bricks/{simhub => virtualhub}/manifest.py (100%) delete mode 100644 bricks/virtualhub/mp_port.c rename bricks/{simhub => virtualhub}/mpconfigport.h (100%) delete mode 100644 bricks/virtualhub/mpconfigvariant.h delete mode 100644 bricks/virtualhub/mpconfigvariant.mk delete mode 100644 bricks/virtualhub/mpvarianthal.h rename bricks/{simhub => virtualhub}/pbio_os_hook.c (100%) delete mode 100644 lib/pbio/platform/sim_hub/contiki-conf.h delete mode 100644 lib/pbio/platform/sim_hub/pbdrvconfig.h delete mode 100644 lib/pbio/platform/sim_hub/pbio_os_config.h delete mode 100644 lib/pbio/platform/sim_hub/pbioconfig.h delete mode 100644 lib/pbio/platform/sim_hub/pbsysconfig.h delete mode 100644 lib/pbio/platform/sim_hub/platform.c rename lib/pbio/platform/{sim_hub => virtual_hub}/startup.s (100%) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1dca90169..819ed5097 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -173,7 +173,7 @@ jobs: uses: coverallsapp/github-action@v2 with: github-token: ${{ secrets.GITHUB_TOKEN }} - file: bricks/simhub/build-coverage/lcov.info + file: bricks/virtualhub/build-coverage/lcov.info flag-name: virtualhub parallel: true diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json index 8ee514718..08115eb46 100644 --- a/.vscode/c_cpp_properties.json +++ b/.vscode/c_cpp_properties.json @@ -341,31 +341,6 @@ "cStandard": "c11", "intelliSenseMode": "gcc-arm" }, - { - "name": "virtualhub", - "includePath": [ - "${workspaceFolder}/bricks/virtualhub", - "${workspaceFolder}/bricks/virtualhub/build", - "${workspaceFolder}/lib/contiki-core", - "${workspaceFolder}/lib/lego", - "${workspaceFolder}/lib/lwrb/src/include", - "${workspaceFolder}/lib/pbio", - "${workspaceFolder}/lib/pbio/include", - "${workspaceFolder}/lib/pbio/platform/virtual_hub", - "${workspaceFolder}/micropython/ports/unix", - "${workspaceFolder}/micropython", - "${workspaceFolder}", - "/usr/include/python3.10" - ], - "defines": [ - "UNIX", - "MICROPY_MODULE_FROZEN_MPY", - "MICROPY_USE_READLINE=1" - ], - "compilerPath": "/usr/bin/gcc", - "cStandard": "c11", - "intelliSenseMode": "gcc-x64" - }, { "name": "pbio/test", "includePath": [ @@ -388,16 +363,16 @@ "cStandard": "c11" }, { - "name": "simhub", + "name": "virtualhub", "includePath": [ - "${workspaceFolder}/bricks/simhub", - "${workspaceFolder}/bricks/simhub/build", + "${workspaceFolder}/bricks/virtualhub", + "${workspaceFolder}/bricks/virtualhub/build", "${workspaceFolder}/lib/contiki-core", "${workspaceFolder}/lib/lego", "${workspaceFolder}/lib/lwrb/src/include", "${workspaceFolder}/lib/pbio", "${workspaceFolder}/lib/pbio/include", - "${workspaceFolder}/lib/pbio/platform/sim_hub", + "${workspaceFolder}/lib/pbio/platform/virtual_hub", "${workspaceFolder}/micropython", "${workspaceFolder}", "/usr/include/python3.10" diff --git a/.vscode/launch.json b/.vscode/launch.json index 37a9928ad..32835093b 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -53,38 +53,6 @@ "args": [], "console": "integratedTerminal" }, - { - "name": "virtualhub", - "type": "cppdbg", - "request": "launch", - "program": "${workspaceFolder}/bricks/virtualhub/build-debug/virtualhub-micropython", - "args": [ - "${workspaceFolder}/tests/virtualhub/motor/car.py" - ], - "stopAtEntry": false, - "cwd": "${workspaceFolder}", - "environment": [ - { - "name": "PBIO_TEST_CONNECT_SOCKET", - "value": "true" - }, - ], - "externalConsole": false, - "MIMode": "gdb", - "setupCommands": [ - { - "description": "Enable pretty-printing for gdb", - "text": "-enable-pretty-printing", - "ignoreFailures": true - }, - { - "description": "Ignore timer signal", - "text": "handle SIG34 noprint pass", - "ignoreFailures": false - } - ], - "preLaunchTask": "build virtualhub" - }, { "name":"Virtual Hub Animation", "type":"debugpy", @@ -93,12 +61,12 @@ "console":"integratedTerminal" }, { - "name": "simhub", + "name": "virtualhub", "type": "cppdbg", "request": "launch", - "program": "${workspaceFolder}/bricks/simhub/build-debug/firmware.elf", + "program": "${workspaceFolder}/bricks/virtualhub/build-debug/firmware.elf", "args": [ - "${workspaceFolder}/tests/virtualhub/basics/hello.py", + "${workspaceFolder}/tests/virtualhub/motor/car.py", ], "stopAtEntry": false, "cwd": "${workspaceFolder}", @@ -122,7 +90,7 @@ "ignoreFailures": false } ], - "preLaunchTask": "build simhub (debug)" + "preLaunchTask": "build virtualhub (debug)" }, { "name": "test-pbio", diff --git a/.vscode/tasks.json b/.vscode/tasks.json index db6606257..8c541f50c 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -36,16 +36,11 @@ ], "detail": "Build the PrimeHub project" }, - { - "label": "build virtualhub", - "type": "shell", - "command": "poetry run make -C bricks/virtualhub DEBUG=1 COPT=-O0 CROSS_COMPILE= -j" - }, { "options": { - "cwd": "${workspaceFolder}/bricks/simhub" + "cwd": "${workspaceFolder}/bricks/virtualhub" }, - "label": "build simhub (debug)", + "label": "build virtualhub (debug)", "type": "shell", "command": "make -C ../../micropython/mpy-cross -j && poetry run make DEBUG=1 BUILD=build-debug -j", }, diff --git a/Makefile b/Makefile index 69900f705..545e7be3d 100644 --- a/Makefile +++ b/Makefile @@ -28,9 +28,9 @@ doc: clean-doc: @$(MAKE) -C lib/pbio/doc clean -all: movehub cityhub technichub primehub essentialhub virtualhub simhub nxt ev3 doc +all: movehub cityhub technichub primehub essentialhub virtualhub nxt ev3 doc -clean-all: clean-movehub clean-cityhub clean-technichub clean-primehub clean-essentialhub clean-virtualhub clean-simhub clean-nxt clean-ev3 clean-doc +clean-all: clean-movehub clean-cityhub clean-technichub clean-primehub clean-essentialhub clean-virtualhub clean-nxt clean-ev3 clean-doc ev3: mpy-cross @$(MAKE) -C bricks/ev3 @@ -78,15 +78,8 @@ virtualhub: mpy-cross @$(MAKE) -C bricks/virtualhub CROSS_COMPILE= clean-virtualhub: clean-mpy-cross - @$(MAKE) -C bricks/virtualhub clean CROSS_COMPILE= - @$(MAKE) -C bricks/virtualhub clean DEBUG=1 - -simhub: mpy-cross - @$(MAKE) -C bricks/simhub CROSS_COMPILE= - -clean-simhub: clean-mpy-cross - @$(MAKE) -C bricks/simhub clean - @$(MAKE) -C bricks/simhub clean BUILD=build-debug + @$(MAKE) -C bricks/virtualhub clean + @$(MAKE) -C bricks/virtualhub clean BUILD=build-debug mpy-cross: @$(MAKE) -C micropython/mpy-cross CROSS_COMPILE=$(HOST_CROSS_COMPILE) diff --git a/bricks/_common/common.mk b/bricks/_common/common.mk index 16f67d84f..d282feb58 100644 --- a/bricks/_common/common.mk +++ b/bricks/_common/common.mk @@ -258,7 +258,7 @@ PY_EXTRA_SRC_C += $(addprefix shared/,\ runtime/gchelper_generic.c \ runtime/sys_stdio_mphal.c \ ) -PY_EXTRA_SRC_C += $(addprefix bricks/simhub/,\ +PY_EXTRA_SRC_C += $(addprefix bricks/virtualhub/,\ pbio_os_hook.c \ ) else diff --git a/bricks/simhub/Makefile b/bricks/simhub/Makefile deleted file mode 100644 index 56fe05ee3..000000000 --- a/bricks/simhub/Makefile +++ /dev/null @@ -1,10 +0,0 @@ -# SPDX-License-Identifier: MIT -# Copyright (c) 2025 The Pybricks Authors - -PBIO_PLATFORM = sim_hub -PB_MCU_FAMILY = native -PB_FROZEN_MODULES = 1 -MICROPY_ROM_TEXT_COMPRESSION = 1 - - -include ../_common/common.mk diff --git a/bricks/simhub/qstrdefsport.h b/bricks/simhub/qstrdefsport.h deleted file mode 100644 index 00d3e2ae3..000000000 --- a/bricks/simhub/qstrdefsport.h +++ /dev/null @@ -1,2 +0,0 @@ -// qstrs specific to this port -// *FORMAT-OFF* diff --git a/bricks/simhub/.gitignore b/bricks/virtualhub/.gitignore similarity index 100% rename from bricks/simhub/.gitignore rename to bricks/virtualhub/.gitignore diff --git a/bricks/virtualhub/Makefile b/bricks/virtualhub/Makefile index 5b0c2dfed..f42e0a058 100644 --- a/bricks/virtualhub/Makefile +++ b/bricks/virtualhub/Makefile @@ -1,49 +1,10 @@ # SPDX-License-Identifier: MIT -# Copyright (c) 2022 The Pybricks Authors +# Copyright (c) 2025 The Pybricks Authors -# Need a default target so that make can be called without specifying one. -all: +PBIO_PLATFORM = virtual_hub +PB_MCU_FAMILY = native +PB_FROZEN_MODULES = 1 +MICROPY_ROM_TEXT_COMPRESSION = 1 -# place coverage/debug build in separate folder so we don't have to remember to clean and rebuild -ifeq ($(COVERAGE),1) -BUILD_DIR = build-coverage -export CFLAGS = --coverage -export LDFLAGS = --coverage -else -ifeq ($(DEBUG),1) -BUILD_DIR = build-debug -export COPT = -O0 -else -BUILD_DIR = build -endif -endif -mkfile_path := $(abspath $(lastword $(MAKEFILE_LIST))) -PBTOP := $(patsubst %/bricks/virtualhub/Makefile,%,$(mkfile_path)) -ifeq ("$(wildcard $(PBTOP)/micropython/lib/micropython-lib/README.md)","") -$(info GIT cloning micropython-lib submodule) -$(info $(shell cd $(PBTOP)/micropython && git submodule update --init lib/micropython-lib)) -ifeq ("$(wildcard $(PBTOP)/micropython/lib/micropython-lib/README.md)","") -$(error failed) -endif -endif - -# Include frozen manifest only if there is anything to freeze. -ifneq ("$(wildcard ../../bricks/_common/modules/*.py)","") -FROZEN_MANIFEST ?= ../../../bricks/_common/manifest.py -else -FROZEN_MANIFEST = -endif - -# The virtual hub is a unix port variant, so pass everything to the upstream -# MicroPython makefile. -%: - $(MAKE) -C ../../micropython/ports/unix \ - VARIANT=virtualhub \ - VARIANT_DIR=../../../bricks/virtualhub \ - BUILD=../../../bricks/virtualhub/$(BUILD_DIR) \ - PROG=../../../bricks/virtualhub/$(BUILD_DIR)/virtualhub-micropython \ - FROZEN_MANIFEST=$(FROZEN_MANIFEST) \ - QSTR_DEFS=../../../bricks/_common/qstrdefs.h \ - QSTR_GLOBAL_DEPENDENCIES=../../../bricks/virtualhub/mpconfigvariant.h \ - $@ +include ../_common/common.mk diff --git a/bricks/virtualhub/README.md b/bricks/virtualhub/README.md deleted file mode 100644 index 8b0f4a426..000000000 --- a/bricks/virtualhub/README.md +++ /dev/null @@ -1,60 +0,0 @@ - -# Virtual hub - -Pybricks has a `virtualhub` implementation that allows implementing virtual -hardware drivers in Python. This could be used for things like automated testing -and creating virtual robots in a 3-D environment. - -## Building - -Prerequisites: - - make -C micropython/ports/unix submodules - -Run: - - make virtualhub - -Or if you want to pass additional options: - - make -C bricks/virtualhub ... - -## Running the virtual hub - -Building with the commands above will result in a `virtualhub-micropython` -executable in the `bricks/virtualhub/build/` directory. Running this executable -by itself will likely result in the error: - - ModuleNotFoundError: No module named 'pbio_virtual' - -This is because it requires a module named `pbio_virtual` to be in the Python -path. Technically, it is trying to import `pbio_virtual.platform.default` but -the error only shows the first module in the chain that fails. To solve this, -set the `PYTHONPATH` environment variable to a directory containing the -`pbio_virtual` module. You can also use a different platform module by setting -the `PBIO_VIRTUAL_PLATFORM_MODULE` environment variable. - -For example, this will run a virtual hub implemented using Python turtle graphics: - - PYTHONPATH=lib/pbio/cpython PBIO_VIRTUAL_PLATFORM_MODULE=pbio_virtual.platform.turtle ./bricks/virtualhub/build/virtualhub-micropython - - -## Internals - -The `virtualhub-micropython` executable is a MicroPython runtime (based on UNIX -port). However, it also hosts a CPython runtime in the same process. The CPython -runtime only runs during callback from the MicroPython runtime, so the main -thread does not run continuously. - -There are two basic kinds of hooks for passing information between the Pybricks -MicroPython runtime and the virtual driver Python runtime. For events, there are -`on_()` methods in Python that will be called whenever the MicroPython -runtime emits the event. For polled values, there are properties/attributes -that are read by the MicroPython runtime whenever it needs to use the value. - -## Implementing virtual hub drivers in Python - -To start a new virtual hub implementation, create a Python module with a class -named `Platform`. The class needs to contain all of the required methods and -properties used by the virtual drivers enabled in the MicroPython build. See -`lib/pbio/cpython/pbio_virtual/platform/` for example implementations. diff --git a/bricks/simhub/manifest.py b/bricks/virtualhub/manifest.py similarity index 100% rename from bricks/simhub/manifest.py rename to bricks/virtualhub/manifest.py diff --git a/bricks/virtualhub/mp_port.c b/bricks/virtualhub/mp_port.c deleted file mode 100644 index 3360ed0d4..000000000 --- a/bricks/virtualhub/mp_port.c +++ /dev/null @@ -1,157 +0,0 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2022-2023 The Pybricks Authors - -// MicroPython port-specific implementation hooks - -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#include "pbio_os_config.h" - -#include - -#include -#include -#include -#include -#include - -#include "py/mphal.h" -#include "py/mpconfig.h" -#include "py/obj.h" -#include "py/objexcept.h" -#include "py/objstr.h" -#include "py/objtuple.h" -#include "py/runtime.h" -#include "py/stream.h" - -#include "pybricks/util_pb/pb_error.h" -#include - -// from micropython/ports/unix/main.c -#define FORCED_EXIT (0x100) - -// callback for when stop button is pressed in IDE or on hub -void pbsys_main_stop_program(bool force_stop) { - static const mp_rom_obj_tuple_t args = { - .base = { .type = &mp_type_tuple }, - .len = 2, - .items = { - // NB: currently, first arg has to be FORCED_EXIT for default - // unix unhandled exception handler. - // https://github.com/micropython/micropython/pull/8151 - MP_ROM_INT(FORCED_EXIT), - MP_ROM_QSTR(MP_QSTR_stop_space_button_space_pressed), - }, - }; - - static mp_obj_exception_t system_exit; - - // Schedule SystemExit exception. - system_exit.base.type = &mp_type_SystemExit; - system_exit.traceback_alloc = 0; - system_exit.traceback_len = 0; - system_exit.traceback_data = NULL; - system_exit.args = (mp_obj_tuple_t *)&args; - - mp_sched_exception(MP_OBJ_FROM_PTR(&system_exit)); -} - -bool pbsys_main_stdin_event(uint8_t c) { - return false; -} - -// MICROPY_PORT_INIT_FUNC -void pb_virtualhub_port_init(void) { - - pbdrv_init(); - pbio_init(true); - pbsys_init(); - - pbsys_status_set(PBIO_PYBRICKS_STATUS_USER_PROGRAM_RUNNING); - - while (pbio_os_run_processes_once()) { - } - - pb_package_pybricks_init(true); -} - -// MICROPY_PORT_DEINIT_FUNC -void pb_virtualhub_port_deinit(void) { -} - -// Implementation for MICROPY_EVENT_POLL_HOOK -void pb_event_poll_hook(void) { - - while (pbio_os_run_processes_once()) { - } - - mp_handle_pending(true); - - pbio_os_run_processes_and_wait_for_event(); -} - -pbio_os_irq_flags_t pbio_os_hook_disable_irq(void) { - sigset_t sigmask; - sigfillset(&sigmask); - - sigset_t origmask; - pthread_sigmask(SIG_SETMASK, &sigmask, &origmask); - return origmask; -} - -void pbio_os_hook_enable_irq(pbio_os_irq_flags_t flags) { - sigset_t origmask = (sigset_t)flags; - pthread_sigmask(SIG_SETMASK, &origmask, NULL); -} - -void pbio_os_hook_wait_for_interrupt(pbio_os_irq_flags_t flags) { - struct timespec timeout = { - .tv_sec = 0, - .tv_nsec = 100000, - }; - // "sleep" with "interrupts" enabled - sigset_t origmask = flags; - MP_THREAD_GIL_EXIT(); - pselect(0, NULL, NULL, NULL, &timeout, &origmask); - MP_THREAD_GIL_ENTER(); -} - -void pb_virtualhub_delay_us(mp_uint_t us) { - mp_uint_t start = mp_hal_ticks_us(); - - while (mp_hal_ticks_us() - start < us) { - MICROPY_VM_HOOK_LOOP; - } -} - -uintptr_t mp_hal_stdio_poll(uintptr_t flags) { - struct pollfd fds[] = { - { .fd = STDIN_FILENO, .events = flags & MP_STREAM_POLL_RD ? POLLIN : 0, }, - { .fd = STDOUT_FILENO, .events = flags & MP_STREAM_POLL_WR ? POLLOUT : 0, }, - }; - int ret; - - MP_HAL_RETRY_SYSCALL(ret, poll(fds, MP_ARRAY_SIZE(fds), 0), mp_raise_OSError(err)); - - uintptr_t rflags = 0; - - if (ret > 0) { - if (fds[0].revents & POLLIN) { - rflags |= MP_STREAM_POLL_RD; - } - if (fds[1].revents & POLLOUT) { - rflags |= MP_STREAM_POLL_WR; - } - } - - return rflags; -} diff --git a/bricks/simhub/mpconfigport.h b/bricks/virtualhub/mpconfigport.h similarity index 100% rename from bricks/simhub/mpconfigport.h rename to bricks/virtualhub/mpconfigport.h diff --git a/bricks/virtualhub/mpconfigvariant.h b/bricks/virtualhub/mpconfigvariant.h deleted file mode 100644 index dc4ac4077..000000000 --- a/bricks/virtualhub/mpconfigvariant.h +++ /dev/null @@ -1,114 +0,0 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2022-2023 The Pybricks Authors - -#define PYBRICKS_HUB_CLASS_NAME (MP_QSTR_VirtualHub) - -#define PYBRICKS_HUB_NAME "virtualhub" -#define PYBRICKS_HUB_VIRTUALHUB (1) - -// Pybricks modules -#define PYBRICKS_PY_COMMON (1) -#define PYBRICKS_PY_COMMON_BLE (0) -#define PYBRICKS_PY_COMMON_CHARGER (1) -#define PYBRICKS_PY_COMMON_COLOR_LIGHT (1) -#define PYBRICKS_PY_COMMON_CONTROL (1) -#define PYBRICKS_PY_COMMON_IMU (0) -#define PYBRICKS_PY_COMMON_KEYPAD (1) -#define PYBRICKS_PY_COMMON_KEYPAD_HUB_BUTTONS (1) -#define PYBRICKS_PY_COMMON_LIGHT_ARRAY (1) -#define PYBRICKS_PY_COMMON_LIGHT_MATRIX (0) -#define PYBRICKS_PY_COMMON_LOGGER (1) -#define PYBRICKS_PY_COMMON_LOGGER_REAL_FILE (1) -#define PYBRICKS_PY_COMMON_MOTORS (1) -#define PYBRICKS_PY_COMMON_SPEAKER (0) -#define PYBRICKS_PY_COMMON_SYSTEM (1) -#define PYBRICKS_PY_EV3DEVICES (0) -#define PYBRICKS_PY_EXPERIMENTAL (1) -#define PYBRICKS_PY_HUBS (1) -#define PYBRICKS_PY_IODEVICES (0) -#define PYBRICKS_PY_NXTDEVICES (0) -#define PYBRICKS_PY_PARAMETERS (1) -#define PYBRICKS_PY_PARAMETERS_BUTTON (1) -#define PYBRICKS_PY_PARAMETERS_ICON (1) -#define PYBRICKS_PY_PARAMETERS_IMAGE (0) -#define PYBRICKS_PY_PUPDEVICES (1) -#define PYBRICKS_PY_PUPDEVICES_REMOTE (0) -#define PYBRICKS_PY_DEVICES (1) -#define PYBRICKS_PY_ROBOTICS (1) -#define PYBRICKS_PY_ROBOTICS_DRIVEBASE_SPIKE (0) -#define PYBRICKS_PY_TOOLS (1) -#define PYBRICKS_PY_TOOLS_HUB_MENU (0) -#define PYBRICKS_PY_TOOLS_APP_DATA (1) - -// Pybricks options -#define PYBRICKS_OPT_COMPILER (1) -#define PYBRICKS_OPT_USE_STACK_END_AS_TOP (1) -#define PYBRICKS_OPT_RAW_REPL (0) -#define PYBRICKS_OPT_FLOAT (1) -#define PYBRICKS_OPT_TERSE_ERR (0) -#define PYBRICKS_OPT_EXTRA_LEVEL1 (1) -#define PYBRICKS_OPT_EXTRA_LEVEL2 (0) -#define PYBRICKS_OPT_CUSTOM_IMPORT (1) - -// Upstream MicroPython options -#define MICROPY_FLOAT_IMPL (MICROPY_FLOAT_IMPL_FLOAT) -#define MICROPY_LONGINT_IMPL (MICROPY_LONGINT_IMPL_MPZ) -#define MICROPY_DEBUG_PRINTERS (1) -#define MICROPY_MODULE_ATTR_DELEGATION (1) -#define MICROPY_MODULE_BUILTIN_INIT (1) -#define MICROPY_MODULE_BUILTIN_SUBPACKAGES (1) -#define MICROPY_PY_MICROPYTHON_MEM_INFO (1) -#define MICROPY_PY_BUILTINS_HELP (1) -#define MICROPY_PY_BUILTINS_HELP_MODULES (1) -#define MICROPY_PY_SYS_SETTRACE (1) -#define MICROPY_PY_ERRNO (1) -#define MICROPY_PY_OS (1) -#define MICROPY_PY_OS_GETENV_PUTENV_UNSETENV (1) -#define MICROPY_PY_OS_INCLUDEFILE "ports/unix/modos.c" -#define MICROPY_PY_RANDOM_EXTRA_FUNCS (1) -#define MICROPY_PY_BUILTINS_SLICE_INDICES (1) -#define MICROPY_PERSISTENT_CODE_SAVE (1) -#define MICROPY_STREAMS_POSIX_API (1) -#define MICROPY_HELPER_REPL (1) -#define MICROPY_KBD_EXCEPTION (1) - -// REVISIT: This list currently matches the stm32 builds. -#define MICROPY_PY_ERRNO_LIST \ - X(EPERM) \ - X(EIO) \ - X(EBUSY) \ - X(ENODEV) \ - X(EINVAL) \ - X(EOPNOTSUPP) \ - X(EAGAIN) \ - X(ETIMEDOUT) \ - X(ECANCELED) \ - -#define MICROPY_MPHALPORT_H "mpvarianthal.h" -#define MICROPY_VARIANT_QSTR_DEFS_H "../_common/qstrdefs.h" - -#define MICROPY_PORT_INIT_FUNC do { \ - extern void pb_virtualhub_port_init(void); \ - pb_virtualhub_port_init(); \ -} while (0) - -#define MICROPY_PORT_DEINIT_FUNC do { \ - extern void pb_virtualhub_port_deinit(void); \ - pb_virtualhub_port_deinit(); \ -} while (0) - -#define MICROPY_VM_HOOK_LOOP do { \ - extern bool pbio_os_run_processes_once(void); \ - pbio_os_run_processes_once(); \ -} while (0); - -#define MICROPY_GC_HOOK_LOOP(i) do { \ - if (((i) & 0xf) == 0) { \ - MICROPY_VM_HOOK_LOOP \ - } \ -} while (0) - -#define MICROPY_EVENT_POLL_HOOK do { \ - extern void pb_event_poll_hook(void); \ - pb_event_poll_hook(); \ -} while (0); diff --git a/bricks/virtualhub/mpconfigvariant.mk b/bricks/virtualhub/mpconfigvariant.mk deleted file mode 100644 index 344bc1e5e..000000000 --- a/bricks/virtualhub/mpconfigvariant.mk +++ /dev/null @@ -1,73 +0,0 @@ -# SPDX-License-Identifier: MIT -# Copyright (c) 2022-2023 The Pybricks Authors - -############################### -# Standard MicroPython config # -############################### - -# Enable/disable modules and 3rd-party libs to be included in interpreter - -# Build 32-bit binaries on a 64-bit host -MICROPY_FORCE_32BIT = 0 - -# This variable can take the following values: -# 0 - no readline, just simple stdin input -# 1 - use MicroPython version of readline -MICROPY_USE_READLINE = 1 - -# btree module using Berkeley DB 1.xx -MICROPY_PY_BTREE = 1 - -# _thread module using pthreads -MICROPY_PY_THREAD = 1 - -# Subset of CPython termios module -MICROPY_PY_TERMIOS = 1 - -# Subset of CPython socket module -MICROPY_PY_SOCKET = 1 - -# ffi module requires libffi (libffi-dev Debian package) -MICROPY_PY_FFI = 1 - -# ssl module requires one of the TLS libraries below -MICROPY_PY_SSL = 0 -# axTLS has minimal size but implements only a subset of modern TLS -# functionality, so may have problems with some servers. -MICROPY_SSL_AXTLS = 0 -# mbedTLS is more up to date and complete implementation, but also -# more bloated. -MICROPY_SSL_MBEDTLS = 0 - -# jni module requires JVM/JNI -MICROPY_PY_JNI = 0 - -# Avoid using system libraries, use copies bundled with MicroPython -# as submodules (currently affects only libffi). -MICROPY_STANDALONE = 0 - -MICROPY_VFS_FAT = 0 -MICROPY_VFS_LFS1 = 0 -MICROPY_VFS_LFS2 = 0 - -###################### -# Pybricks additions # -###################### - -USER_C_MODULES = ../../.. - -INC += -I../../.. -INC += -I../../../lib/contiki-core -INC += -I../../../lib/lego -INC += -I../../../lib/pbio/include -INC += -I../../../lib/pbio/platform/virtual_hub - -# Sources and libraries common to all pybricks bricks -PBIO_PLATFORM = virtual_hub -include ../../../bricks/_common/sources.mk - -SRC_EXTMOD_C += $(PYBRICKS_PYBRICKS_SRC_C) -SRC_THIRDPARTY_C += $(CONTIKI_SRC_C) $(PBIO_SRC_C) - -# realtime library for timer signals -LIB += -lrt diff --git a/bricks/virtualhub/mpvarianthal.h b/bricks/virtualhub/mpvarianthal.h deleted file mode 100644 index eac620cc7..000000000 --- a/bricks/virtualhub/mpvarianthal.h +++ /dev/null @@ -1,109 +0,0 @@ -/* - * This file is part of the MicroPython project, http://micropython.org/ - * - * The MIT License (MIT) - * - * Copyright (c) 2015 Damien P. George - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -#include -#include - -#ifndef CHAR_CTRL_C -#define CHAR_CTRL_C (3) -#endif - -void mp_hal_set_interrupt_char(char c); - -uintptr_t mp_hal_stdio_poll(uintptr_t); -void mp_hal_stdio_mode_raw(void); -void mp_hal_stdio_mode_orig(void); - -#if MICROPY_PY_BUILTINS_INPUT && MICROPY_USE_READLINE == 0 - -#include -#include "py/misc.h" -#include "input.h" -#define mp_hal_readline mp_hal_readline -static inline int mp_hal_readline(vstr_t *vstr, const char *p) { - char *line = prompt((char *)p); - vstr_add_str(vstr, line); - free(line); - return 0; -} - -#elif MICROPY_PY_BUILTINS_INPUT && MICROPY_USE_READLINE == 1 - -#include "py/misc.h" -#include "shared/readline/readline.h" -// For built-in input() we need to wrap the standard readline() to enable raw mode -#define mp_hal_readline mp_hal_readline -static inline int mp_hal_readline(vstr_t *vstr, const char *p) { - mp_hal_stdio_mode_raw(); - int ret = readline(vstr, p); - mp_hal_stdio_mode_orig(); - return ret; -} - -#endif - -// MicroPython time needs to be driven by the virtual clock driver. -#include -void pb_virtualhub_delay_us(mp_uint_t us); - -#define mp_hal_delay_us pb_virtualhub_delay_us -#define mp_hal_ticks_ms pbdrv_clock_get_ms -#define mp_hal_ticks_us pbdrv_clock_get_us -#define mp_hal_ticks_cpu() 0 - -void mp_hal_get_random(size_t n, void *buf); - -// This macro is used to implement PEP 475 to retry specified syscalls on EINTR -#define MP_HAL_RETRY_SYSCALL(ret, syscall, raise) { \ - for (;;) { \ - MP_THREAD_GIL_EXIT(); \ - ret = syscall; \ - MP_THREAD_GIL_ENTER(); \ - if (ret == -1) { \ - int err = errno; \ - if (err == EINTR) { \ - MICROPY_EVENT_POLL_HOOK \ - continue; \ - } \ - raise; \ - } \ - break; \ - } \ -} - -#define RAISE_ERRNO(err_flag, error_val) \ - { if (err_flag == -1) \ - { mp_raise_OSError(error_val); } } - -#if MICROPY_PY_BLUETOOTH -enum { - MP_HAL_MAC_BDADDR, -}; - -void mp_hal_get_mac(int idx, uint8_t buf[6]); -#endif - -// TODO -#define mp_hal_stdout_tx_flush() diff --git a/bricks/simhub/pbio_os_hook.c b/bricks/virtualhub/pbio_os_hook.c similarity index 100% rename from bricks/simhub/pbio_os_hook.c rename to bricks/virtualhub/pbio_os_hook.c diff --git a/bricks/virtualhub/qstrdefsport.h b/bricks/virtualhub/qstrdefsport.h index da429bc07..00d3e2ae3 100644 --- a/bricks/virtualhub/qstrdefsport.h +++ b/bricks/virtualhub/qstrdefsport.h @@ -1,3 +1,2 @@ +// qstrs specific to this port // *FORMAT-OFF* - -Q(stop button pressed) diff --git a/lib/pbio/platform/sim_hub/contiki-conf.h b/lib/pbio/platform/sim_hub/contiki-conf.h deleted file mode 100644 index c3c18d5f3..000000000 --- a/lib/pbio/platform/sim_hub/contiki-conf.h +++ /dev/null @@ -1,18 +0,0 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2018-2022 The Pybricks Authors - -#ifndef _PBIO_CONF_H_ -#define _PBIO_CONF_H_ - -#include - -#define CCIF -#define CLIF - -typedef uint32_t clock_time_t; -#define CLOCK_CONF_SECOND 1000 - -#define clock_time pbdrv_clock_get_ms -#define clock_usecs pbdrv_clock_get_us - -#endif /* _PBIO_CONF_H_ */ diff --git a/lib/pbio/platform/sim_hub/pbdrvconfig.h b/lib/pbio/platform/sim_hub/pbdrvconfig.h deleted file mode 100644 index 5282d601d..000000000 --- a/lib/pbio/platform/sim_hub/pbdrvconfig.h +++ /dev/null @@ -1,42 +0,0 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2022 The Pybricks Authors - -#define PBDRV_CONFIG_BATTERY (1) -#define PBDRV_CONFIG_BATTERY_TEST (1) - -#define PBDRV_CONFIG_BLOCK_DEVICE (1) -#define PBDRV_CONFIG_BLOCK_DEVICE_RAM_SIZE (50 * 1024) -#define PBDRV_CONFIG_BLOCK_DEVICE_TEST (1) - -#define PBDRV_CONFIG_BLUETOOTH (1) -#define PBDRV_CONFIG_BLUETOOTH_SIMULATION (1) - -#define PBDRV_CONFIG_BUTTON (1) -#define PBDRV_CONFIG_BUTTON_TEST (1) - -#define PBDRV_CONFIG_CLOCK (1) -#ifdef PBDRV_CONFIG_RUN_ON_CI -#define PBDRV_CONFIG_CLOCK_TEST (1) -#else -#define PBDRV_CONFIG_CLOCK_LINUX (1) -#endif - -#define PBDRV_CONFIG_COUNTER (1) - -#define PBDRV_CONFIG_GPIO (1) -#define PBDRV_CONFIG_GPIO_VIRTUAL (1) - -#define PBDRV_CONFIG_IOPORT (1) -#define PBDRV_CONFIG_IOPORT_NUM_DEV (6) - -#define PBDRV_CONFIG_MOTOR_DRIVER (1) -#define PBDRV_CONFIG_MOTOR_DRIVER_NUM_DEV (6) -#define PBDRV_CONFIG_MOTOR_DRIVER_VIRTUAL_SIMULATION (1) - -#define PBDRV_CONFIG_HAS_PORT_A (1) -#define PBDRV_CONFIG_HAS_PORT_B (1) -#define PBDRV_CONFIG_HAS_PORT_C (1) -#define PBDRV_CONFIG_HAS_PORT_D (1) -#define PBDRV_CONFIG_HAS_PORT_E (1) -#define PBDRV_CONFIG_HAS_PORT_F (1) -#define PBDRV_CONFIG_HAS_PORT_VCC_CONTROL (1) diff --git a/lib/pbio/platform/sim_hub/pbio_os_config.h b/lib/pbio/platform/sim_hub/pbio_os_config.h deleted file mode 100644 index 584366100..000000000 --- a/lib/pbio/platform/sim_hub/pbio_os_config.h +++ /dev/null @@ -1,9 +0,0 @@ -#include - -typedef sigset_t pbio_os_irq_flags_t; - -pbio_os_irq_flags_t pbio_os_hook_disable_irq(void); - -void pbio_os_hook_enable_irq(pbio_os_irq_flags_t flags); - -void pbio_os_hook_wait_for_interrupt(pbio_os_irq_flags_t flags); diff --git a/lib/pbio/platform/sim_hub/pbioconfig.h b/lib/pbio/platform/sim_hub/pbioconfig.h deleted file mode 100644 index 7e9960b17..000000000 --- a/lib/pbio/platform/sim_hub/pbioconfig.h +++ /dev/null @@ -1,29 +0,0 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2022 The Pybricks Authors - -#define PBIO_CONFIG_BATTERY (1) -#define PBIO_CONFIG_DCMOTOR (6) -#define PBIO_CONFIG_DCMOTOR_NUM_DEV (6) -#define PBIO_CONFIG_DRIVEBASE_SPIKE (1) -#define PBIO_CONFIG_LIGHT (0) -#define PBIO_CONFIG_LOGGER (1) -#define PBIO_CONFIG_LIGHT_MATRIX (0) -#define PBIO_CONFIG_MOTOR_PROCESS (1) -#define PBIO_CONFIG_IMU (0) -#define PBIO_CONFIG_PORT (1) -#define PBIO_CONFIG_PORT_NUM_DEV (6) -#define PBIO_CONFIG_PORT_DCM (0) -#define PBIO_CONFIG_PORT_DCM_PUP (0) -#define PBIO_CONFIG_PORT_DCM_EV3 (0) -#define PBIO_CONFIG_PORT_DCM_NUM_DEV (0) -#define PBIO_CONFIG_PORT_LUMP (0) -#define PBIO_CONFIG_PORT_LUMP_MODE_INFO (0) -#define PBIO_CONFIG_PORT_LUMP_NUM_DEV (0) -#define PBIO_CONFIG_SERVO (1) -#define PBIO_CONFIG_SERVO_NUM_DEV (6) -#define PBIO_CONFIG_SERVO_EV3_NXT (1) -#define PBIO_CONFIG_SERVO_PUP (1) -#define PBIO_CONFIG_SERVO_PUP_MOVE_HUB (1) -#define PBIO_CONFIG_TACHO (1) - -#define PBIO_CONFIG_ENABLE_SYS (1) diff --git a/lib/pbio/platform/sim_hub/pbsysconfig.h b/lib/pbio/platform/sim_hub/pbsysconfig.h deleted file mode 100644 index a07ba1d34..000000000 --- a/lib/pbio/platform/sim_hub/pbsysconfig.h +++ /dev/null @@ -1,29 +0,0 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2022-2023 The Pybricks Authors - -#define PBSYS_CONFIG_FEATURE_BUILTIN_USER_PROGRAM_REPL (1) -#define PBSYS_CONFIG_FEATURE_BUILTIN_USER_PROGRAM_PORT_VIEW (1) -#define PBSYS_CONFIG_FEATURE_BUILTIN_USER_PROGRAM_IMU_CALIBRATION (0) -#define PBSYS_CONFIG_FEATURE_PROGRAM_FORMAT_MULTI_MPY_V6 (1) -#define PBSYS_CONFIG_FEATURE_PROGRAM_FORMAT_MULTI_MPY_V6_3_NATIVE (0) - -#define PBSYS_CONFIG_BATTERY_CHARGER (0) -#define PBSYS_CONFIG_BLUETOOTH (1) -#define PBSYS_CONFIG_HOST (1) -#define PBSYS_CONFIG_HOST_STDIN_BUF_SIZE (21) -#define PBSYS_CONFIG_HMI (1) -#define PBSYS_CONFIG_HMI_STOP_BUTTON (1 << 5) // center -#define PBSYS_CONFIG_HMI_ENV_MPY (1) -#define PBSYS_CONFIG_HMI_NUM_SLOTS (0) -#define PBSYS_CONFIG_HUB_LIGHT_MATRIX (0) -#define PBSYS_CONFIG_MAIN (1) -#define PBSYS_CONFIG_STORAGE (1) -#define PBSYS_CONFIG_STORAGE_NUM_SLOTS (1) -#define PBSYS_CONFIG_STORAGE_USER_DATA_SIZE (512) -#define PBSYS_CONFIG_STATUS_LIGHT (0) -#define PBSYS_CONFIG_STATUS_LIGHT_BATTERY (0) -#define PBSYS_CONFIG_STATUS_LIGHT_BLUETOOTH (0) -#define PBSYS_CONFIG_STATUS_LIGHT_STATE_ANIMATIONS (1) -#define PBSYS_CONFIG_STATUS_LIGHT_STATE_ANIMATIONS_HUE (240) -#define PBSYS_CONFIG_USER_PROGRAM (0) -#define PBSYS_CONFIG_PROGRAM_STOP (1) diff --git a/lib/pbio/platform/sim_hub/platform.c b/lib/pbio/platform/sim_hub/platform.c deleted file mode 100644 index 7e5234d8b..000000000 --- a/lib/pbio/platform/sim_hub/platform.c +++ /dev/null @@ -1,225 +0,0 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2025 The Pybricks Authors - -#include -#include -#include -#include -#include -#include -#include - -#include "../../drv/motor_driver/motor_driver_virtual_simulation.h" - -#include "pbio_os_config.h" - -#include -#include -#include - -const pbdrv_gpio_t pbdrv_ioport_platform_data_vcc_pin = { - .bank = NULL, - .pin = 0, -}; - -const pbdrv_ioport_platform_data_t pbdrv_ioport_platform_data[PBDRV_CONFIG_IOPORT_NUM_DEV] = { - { - .port_id = PBIO_PORT_ID_A, - .motor_driver_index = 0, - .counter_driver_index = 0, - .external_port_index = 0, - .i2c_driver_index = PBDRV_IOPORT_INDEX_NOT_AVAILABLE, - .uart_driver_index = PBDRV_IOPORT_INDEX_NOT_AVAILABLE, - .pins = NULL, - .supported_modes = PBIO_PORT_MODE_QUADRATURE, - }, - { - .port_id = PBIO_PORT_ID_B, - .motor_driver_index = 1, - .counter_driver_index = 1, - .external_port_index = 1, - .i2c_driver_index = PBDRV_IOPORT_INDEX_NOT_AVAILABLE, - .uart_driver_index = PBDRV_IOPORT_INDEX_NOT_AVAILABLE, - .pins = NULL, - .supported_modes = PBIO_PORT_MODE_QUADRATURE, - }, - { - .port_id = PBIO_PORT_ID_C, - .motor_driver_index = 2, - .external_port_index = 2, - .counter_driver_index = 2, - .i2c_driver_index = PBDRV_IOPORT_INDEX_NOT_AVAILABLE, - .uart_driver_index = PBDRV_IOPORT_INDEX_NOT_AVAILABLE, - .pins = NULL, - .supported_modes = PBIO_PORT_MODE_QUADRATURE, - }, - { - .port_id = PBIO_PORT_ID_D, - .motor_driver_index = 3, - .external_port_index = 3, - .counter_driver_index = 3, - .i2c_driver_index = PBDRV_IOPORT_INDEX_NOT_AVAILABLE, - .uart_driver_index = PBDRV_IOPORT_INDEX_NOT_AVAILABLE, - .pins = NULL, - .supported_modes = PBIO_PORT_MODE_QUADRATURE, - }, - { - .port_id = PBIO_PORT_ID_E, - .motor_driver_index = 4, - .external_port_index = 4, - .counter_driver_index = 4, - .i2c_driver_index = PBDRV_IOPORT_INDEX_NOT_AVAILABLE, - .uart_driver_index = PBDRV_IOPORT_INDEX_NOT_AVAILABLE, - .pins = NULL, - .supported_modes = PBIO_PORT_MODE_QUADRATURE, - }, - { - .port_id = PBIO_PORT_ID_F, - .motor_driver_index = 5, - .external_port_index = 5, - .counter_driver_index = 5, - .i2c_driver_index = PBDRV_IOPORT_INDEX_NOT_AVAILABLE, - .uart_driver_index = PBDRV_IOPORT_INDEX_NOT_AVAILABLE, - .pins = NULL, - .supported_modes = PBIO_PORT_MODE_QUADRATURE, - }, -}; - -#define INFINITY (1e100) - -const pbdrv_motor_driver_virtual_simulation_platform_data_t - pbdrv_motor_driver_virtual_simulation_platform_data[PBDRV_CONFIG_MOTOR_DRIVER_NUM_DEV] = { - { - .port_id = PBIO_PORT_ID_A, - .type_id = LEGO_DEVICE_TYPE_ID_SPIKE_M_MOTOR, - .initial_angle = 123456, - .initial_speed = 0, - .endstop_angle_negative = -INFINITY, - .endstop_angle_positive = INFINITY, - }, - { - .port_id = PBIO_PORT_ID_B, - .type_id = LEGO_DEVICE_TYPE_ID_SPIKE_M_MOTOR, - .initial_angle = 0, - .initial_speed = 0, - .endstop_angle_negative = -INFINITY, - .endstop_angle_positive = INFINITY, - }, - { - .port_id = PBIO_PORT_ID_C, - .type_id = LEGO_DEVICE_TYPE_ID_SPIKE_L_MOTOR, - .initial_angle = 0, - .initial_speed = 0, - .endstop_angle_negative = -142000, - .endstop_angle_positive = 142000, - }, - { - .port_id = PBIO_PORT_ID_D, - .type_id = LEGO_DEVICE_TYPE_ID_NONE, - .initial_angle = 0, - .initial_speed = 0, - .endstop_angle_negative = -INFINITY, - .endstop_angle_positive = INFINITY, - }, - { - .port_id = PBIO_PORT_ID_E, - .type_id = LEGO_DEVICE_TYPE_ID_SPIKE_S_MOTOR, - .initial_angle = 0, - .initial_speed = 0, - .endstop_angle_negative = -INFINITY, - .endstop_angle_positive = INFINITY, - }, - { - .port_id = PBIO_PORT_ID_F, - .type_id = LEGO_DEVICE_TYPE_ID_SPIKE_L_MOTOR, - .initial_angle = 45000, - .initial_speed = 0, - .endstop_angle_negative = -INFINITY, - .endstop_angle_positive = INFINITY, - }, -}; - -// The 'embedded' main. -extern void _main(void); - -int main(int argc, char **argv) { - - // Parse given program, else otherwise default to REPL. - if (argc > 1) { - - // Pybricksdev helper script, pipes multi-mpy to us. - char command[512]; - snprintf(command, sizeof(command), "pybricksdev compile --bin %s", argv[argc - 1]); - FILE *pipe = popen(command, "r"); - if (!pipe) { - printf("Failed to compile program with Pybricksdev\n"); - return 0; - } - - // Read the multi-mpy file from pipe. - extern uint8_t pbsys_hmi_native_program_buf[PBDRV_CONFIG_BLOCK_DEVICE_RAM_SIZE]; - extern uint32_t pbsys_hmi_native_program_size; - pbsys_hmi_native_program_size = fread(pbsys_hmi_native_program_buf, 1, sizeof(pbsys_hmi_native_program_buf), pipe); - pclose(pipe); - - if (pbsys_hmi_native_program_size == 0) { - printf("Error reading from pipe\n"); - return 0; - } - } - - #ifdef PBDRV_CONFIG_RUN_ON_CI - // On the CI modifying settings for stdin causes problems. The REPL isn't - // used on CI anyway. - _main(); - return 0; - #endif - - // Save the original terminal settings - struct termios term_old, term_new; - if (tcgetattr(STDIN_FILENO, &term_old) != 0) { - printf("DEBUG: Failed to get terminal attributes\n"); - return 0; - } - term_new = term_old; - - // Get one char at a time instead of newline and disable CTRL+C for exit. - term_new.c_lflag &= ~(ICANON | ECHO | ISIG); - - // MicroPython REPL expects \r for newline. - term_new.c_iflag |= INLCR; - term_new.c_iflag &= ~ICRNL; - - if (tcsetattr(STDIN_FILENO, TCSANOW, &term_new) != 0) { - printf("Failed to set terminal attributes\n"); - return 0; - } - - // Set stdin non-blocking so we can service it in the runloop like on - // embedded hubs. - int stdin_flags = fcntl(STDIN_FILENO, F_GETFL, 0); - if (stdin_flags == -1) { - printf("Failed to get fcntl flags\n"); - return 0; - } - if (fcntl(STDIN_FILENO, F_SETFL, stdin_flags | O_NONBLOCK) == -1) { - printf("Failed to set non-blocking\n"); - return 0; - } - - // Simulate running embedded main. - _main(); - - // Restore stdin flags. - if (fcntl(STDIN_FILENO, F_SETFL, stdin_flags) == -1) { - printf("Failed to restore stdin flags\n"); - } - - // Restore terminal settings. - if (tcsetattr(STDIN_FILENO, TCSANOW, &term_old) != 0) { - printf("Failed to restore terminal attributes\n"); - return 0; - } - - return 0; -} diff --git a/lib/pbio/platform/virtual_hub/pbdrvconfig.h b/lib/pbio/platform/virtual_hub/pbdrvconfig.h index 6cbc45b38..5282d601d 100644 --- a/lib/pbio/platform/virtual_hub/pbdrvconfig.h +++ b/lib/pbio/platform/virtual_hub/pbdrvconfig.h @@ -4,12 +4,22 @@ #define PBDRV_CONFIG_BATTERY (1) #define PBDRV_CONFIG_BATTERY_TEST (1) +#define PBDRV_CONFIG_BLOCK_DEVICE (1) +#define PBDRV_CONFIG_BLOCK_DEVICE_RAM_SIZE (50 * 1024) +#define PBDRV_CONFIG_BLOCK_DEVICE_TEST (1) + +#define PBDRV_CONFIG_BLUETOOTH (1) +#define PBDRV_CONFIG_BLUETOOTH_SIMULATION (1) + #define PBDRV_CONFIG_BUTTON (1) #define PBDRV_CONFIG_BUTTON_TEST (1) #define PBDRV_CONFIG_CLOCK (1) +#ifdef PBDRV_CONFIG_RUN_ON_CI +#define PBDRV_CONFIG_CLOCK_TEST (1) +#else #define PBDRV_CONFIG_CLOCK_LINUX (1) -#define PBDRV_CONFIG_CLOCK_LINUX_SIGNAL (1) +#endif #define PBDRV_CONFIG_COUNTER (1) diff --git a/lib/pbio/platform/virtual_hub/pbsysconfig.h b/lib/pbio/platform/virtual_hub/pbsysconfig.h index 93c2f432f..a07ba1d34 100644 --- a/lib/pbio/platform/virtual_hub/pbsysconfig.h +++ b/lib/pbio/platform/virtual_hub/pbsysconfig.h @@ -1,16 +1,29 @@ // SPDX-License-Identifier: MIT // Copyright (c) 2022-2023 The Pybricks Authors +#define PBSYS_CONFIG_FEATURE_BUILTIN_USER_PROGRAM_REPL (1) +#define PBSYS_CONFIG_FEATURE_BUILTIN_USER_PROGRAM_PORT_VIEW (1) +#define PBSYS_CONFIG_FEATURE_BUILTIN_USER_PROGRAM_IMU_CALIBRATION (0) +#define PBSYS_CONFIG_FEATURE_PROGRAM_FORMAT_MULTI_MPY_V6 (1) +#define PBSYS_CONFIG_FEATURE_PROGRAM_FORMAT_MULTI_MPY_V6_3_NATIVE (0) + #define PBSYS_CONFIG_BATTERY_CHARGER (0) -#define PBSYS_CONFIG_BLUETOOTH (0) +#define PBSYS_CONFIG_BLUETOOTH (1) +#define PBSYS_CONFIG_HOST (1) +#define PBSYS_CONFIG_HOST_STDIN_BUF_SIZE (21) +#define PBSYS_CONFIG_HMI (1) +#define PBSYS_CONFIG_HMI_STOP_BUTTON (1 << 5) // center +#define PBSYS_CONFIG_HMI_ENV_MPY (1) #define PBSYS_CONFIG_HMI_NUM_SLOTS (0) #define PBSYS_CONFIG_HUB_LIGHT_MATRIX (0) -#define PBSYS_CONFIG_MAIN (0) -#define PBSYS_CONFIG_STORAGE (0) +#define PBSYS_CONFIG_MAIN (1) +#define PBSYS_CONFIG_STORAGE (1) +#define PBSYS_CONFIG_STORAGE_NUM_SLOTS (1) +#define PBSYS_CONFIG_STORAGE_USER_DATA_SIZE (512) #define PBSYS_CONFIG_STATUS_LIGHT (0) #define PBSYS_CONFIG_STATUS_LIGHT_BATTERY (0) #define PBSYS_CONFIG_STATUS_LIGHT_BLUETOOTH (0) #define PBSYS_CONFIG_STATUS_LIGHT_STATE_ANIMATIONS (1) #define PBSYS_CONFIG_STATUS_LIGHT_STATE_ANIMATIONS_HUE (240) #define PBSYS_CONFIG_USER_PROGRAM (0) -#define PBSYS_CONFIG_PROGRAM_STOP (0) +#define PBSYS_CONFIG_PROGRAM_STOP (1) diff --git a/lib/pbio/platform/virtual_hub/platform.c b/lib/pbio/platform/virtual_hub/platform.c index c6722bfa9..7e5234d8b 100644 --- a/lib/pbio/platform/virtual_hub/platform.c +++ b/lib/pbio/platform/virtual_hub/platform.c @@ -1,5 +1,13 @@ // SPDX-License-Identifier: MIT -// Copyright (c) 2023 The Pybricks Authors +// Copyright (c) 2025 The Pybricks Authors + +#include +#include +#include +#include +#include +#include +#include #include "../../drv/motor_driver/motor_driver_virtual_simulation.h" @@ -130,3 +138,88 @@ const pbdrv_motor_driver_virtual_simulation_platform_data_t .endstop_angle_positive = INFINITY, }, }; + +// The 'embedded' main. +extern void _main(void); + +int main(int argc, char **argv) { + + // Parse given program, else otherwise default to REPL. + if (argc > 1) { + + // Pybricksdev helper script, pipes multi-mpy to us. + char command[512]; + snprintf(command, sizeof(command), "pybricksdev compile --bin %s", argv[argc - 1]); + FILE *pipe = popen(command, "r"); + if (!pipe) { + printf("Failed to compile program with Pybricksdev\n"); + return 0; + } + + // Read the multi-mpy file from pipe. + extern uint8_t pbsys_hmi_native_program_buf[PBDRV_CONFIG_BLOCK_DEVICE_RAM_SIZE]; + extern uint32_t pbsys_hmi_native_program_size; + pbsys_hmi_native_program_size = fread(pbsys_hmi_native_program_buf, 1, sizeof(pbsys_hmi_native_program_buf), pipe); + pclose(pipe); + + if (pbsys_hmi_native_program_size == 0) { + printf("Error reading from pipe\n"); + return 0; + } + } + + #ifdef PBDRV_CONFIG_RUN_ON_CI + // On the CI modifying settings for stdin causes problems. The REPL isn't + // used on CI anyway. + _main(); + return 0; + #endif + + // Save the original terminal settings + struct termios term_old, term_new; + if (tcgetattr(STDIN_FILENO, &term_old) != 0) { + printf("DEBUG: Failed to get terminal attributes\n"); + return 0; + } + term_new = term_old; + + // Get one char at a time instead of newline and disable CTRL+C for exit. + term_new.c_lflag &= ~(ICANON | ECHO | ISIG); + + // MicroPython REPL expects \r for newline. + term_new.c_iflag |= INLCR; + term_new.c_iflag &= ~ICRNL; + + if (tcsetattr(STDIN_FILENO, TCSANOW, &term_new) != 0) { + printf("Failed to set terminal attributes\n"); + return 0; + } + + // Set stdin non-blocking so we can service it in the runloop like on + // embedded hubs. + int stdin_flags = fcntl(STDIN_FILENO, F_GETFL, 0); + if (stdin_flags == -1) { + printf("Failed to get fcntl flags\n"); + return 0; + } + if (fcntl(STDIN_FILENO, F_SETFL, stdin_flags | O_NONBLOCK) == -1) { + printf("Failed to set non-blocking\n"); + return 0; + } + + // Simulate running embedded main. + _main(); + + // Restore stdin flags. + if (fcntl(STDIN_FILENO, F_SETFL, stdin_flags) == -1) { + printf("Failed to restore stdin flags\n"); + } + + // Restore terminal settings. + if (tcsetattr(STDIN_FILENO, TCSANOW, &term_old) != 0) { + printf("Failed to restore terminal attributes\n"); + return 0; + } + + return 0; +} diff --git a/lib/pbio/platform/sim_hub/startup.s b/lib/pbio/platform/virtual_hub/startup.s similarity index 100% rename from lib/pbio/platform/sim_hub/startup.s rename to lib/pbio/platform/virtual_hub/startup.s diff --git a/test-virtualhub.sh b/test-virtualhub.sh index 671841ca9..c65a204d5 100755 --- a/test-virtualhub.sh +++ b/test-virtualhub.sh @@ -1,6 +1,6 @@ #!/bin/bash # -# Runs tests on simhub. +# Runs tests on virtualhub. # # Use `--list-test` to list tests or `--include ` to run single tests. # @@ -15,7 +15,7 @@ COVERAGE=1 export COVERAGE SCRIPT_DIR=$(readlink -f "$(dirname "$0")") -BRICK_DIR="$SCRIPT_DIR/bricks/simhub" +BRICK_DIR="$SCRIPT_DIR/bricks/virtualhub" MP_TEST_DIR="$SCRIPT_DIR/micropython/tests" PB_TEST_DIR=$"$SCRIPT_DIR/tests" BUILD_DIR_NAME="build${COVERAGE:+-coverage}" diff --git a/tests/README.md b/tests/README.md index 9cbece1a2..0c26565ff 100644 --- a/tests/README.md +++ b/tests/README.md @@ -1,17 +1,11 @@ # Pybricks MicroPython tests -Currently, the tests in the `ev3dev` and `virtualhub` folders are automated +Currently, the tests in the `virtualhub` folder are automated while the remaining folders contain tests that must be run manually. -Use `./test-ev3dev.sh` in the top-level directory to run the automated tests -for ev3dev (requires Linux). - Use `./test-virtualhub.sh` in the top-level directory to run the automated tests for virtualhub. Use `--list-test` to list tests or `--include ` to run single tests. Use `--clean-failures` to remove previous failure logs. - -Set environment variable `COVERAGE=1` to run code coverage (virtualhub only). -Report can be viewed at `bricks/virtualhub/build-coverage/html/index.html`.