Skip to content

Commit b596073

Browse files
committed
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.
1 parent cff1933 commit b596073

File tree

17 files changed

+479
-8
lines changed

17 files changed

+479
-8
lines changed

.vscode/c_cpp_properties.json

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -386,7 +386,30 @@
386386
"UNIX"
387387
],
388388
"cStandard": "c11"
389-
}
389+
},
390+
{
391+
"name": "simhub",
392+
"includePath": [
393+
"${workspaceFolder}/bricks/simhub",
394+
"${workspaceFolder}/bricks/simhub/build",
395+
"${workspaceFolder}/lib/contiki-core",
396+
"${workspaceFolder}/lib/lego",
397+
"${workspaceFolder}/lib/lwrb/src/include",
398+
"${workspaceFolder}/lib/pbio",
399+
"${workspaceFolder}/lib/pbio/include",
400+
"${workspaceFolder}/lib/pbio/platform/sim_hub",
401+
"${workspaceFolder}/micropython",
402+
"${workspaceFolder}",
403+
"/usr/include/python3.10"
404+
],
405+
"defines": [
406+
"MICROPY_MODULE_FROZEN_MPY",
407+
"MICROPY_USE_READLINE=1"
408+
],
409+
"compilerPath": "/usr/bin/gcc",
410+
"cStandard": "c11",
411+
"intelliSenseMode": "gcc-x64"
412+
},
390413
],
391414
"version": 4
392415
}

.vscode/launch.json

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,38 @@
9292
"program":"${workspaceFolder}/lib/pbio/platform/virtual_hub/animation.py",
9393
"console":"integratedTerminal"
9494
},
95+
{
96+
"name": "simhub",
97+
"type": "cppdbg",
98+
"request": "launch",
99+
"program": "${workspaceFolder}/bricks/simhub/build/firmware.elf",
100+
"args": [
101+
"${workspaceFolder}/tests/virtualhub/motor/car.py"
102+
],
103+
"stopAtEntry": false,
104+
"cwd": "${workspaceFolder}",
105+
"environment": [
106+
{
107+
"name": "PBIO_TEST_CONNECT_SOCKET",
108+
"value": "true"
109+
},
110+
],
111+
"externalConsole": false,
112+
"MIMode": "gdb",
113+
"setupCommands": [
114+
{
115+
"description": "Enable pretty-printing for gdb",
116+
"text": "-enable-pretty-printing",
117+
"ignoreFailures": true
118+
},
119+
{
120+
"description": "Ignore timer signal",
121+
"text": "handle SIG34 noprint pass",
122+
"ignoreFailures": false
123+
}
124+
],
125+
"preLaunchTask": "build simhub"
126+
},
95127
{
96128
"name": "test-pbio",
97129
"type": "cppdbg",

.vscode/tasks.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,14 @@
4141
"type": "shell",
4242
"command": "poetry run make -C bricks/virtualhub DEBUG=1 COPT=-O0 CROSS_COMPILE= -j"
4343
},
44+
{
45+
"options": {
46+
"cwd": "${workspaceFolder}/bricks/simhub"
47+
},
48+
"label": "build simhub",
49+
"type": "shell",
50+
"command": "poetry run make DEBUG=1 COPT=-O0 -j",
51+
},
4452
{
4553
"label": "build test-pbio",
4654
"type": "shell",

Makefile

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@ doc:
2828
clean-doc:
2929
@$(MAKE) -C lib/pbio/doc clean
3030

31-
all: movehub cityhub technichub primehub essentialhub virtualhub nxt ev3 doc
31+
all: movehub cityhub technichub primehub essentialhub virtualhub simhub nxt ev3 doc
3232

33-
clean-all: clean-movehub clean-cityhub clean-technichub clean-primehub clean-essentialhub clean-virtualhub clean-nxt clean-ev3 clean-doc
33+
clean-all: clean-movehub clean-cityhub clean-technichub clean-primehub clean-essentialhub clean-virtualhub clean-simhub clean-nxt clean-ev3 clean-doc
3434

3535
ev3: mpy-cross
3636
@$(MAKE) -C bricks/ev3
@@ -81,6 +81,14 @@ clean-virtualhub: clean-mpy-cross
8181
@$(MAKE) -C bricks/virtualhub clean CROSS_COMPILE=
8282
@$(MAKE) -C bricks/virtualhub clean DEBUG=1
8383

84+
simhub: mpy-cross
85+
@$(MAKE) -C bricks/simhub CROSS_COMPILE=
86+
87+
clean-simhub: clean-mpy-cross
88+
@$(MAKE) -C bricks/simhub clean CROSS_COMPILE=
89+
@$(MAKE) -C bricks/simhub clean DEBUG=1
90+
91+
8492
mpy-cross:
8593
@$(MAKE) -C micropython/mpy-cross CROSS_COMPILE=$(HOST_CROSS_COMPILE)
8694

bricks/_common/arm_none_eabi.mk

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,6 @@ MICROPY_ROM_TEXT_COMPRESSION ?= 1
101101
include $(TOP)/py/py.mk
102102
include $(TOP)/extmod/extmod.mk
103103

104-
CROSS_COMPILE ?= arm-none-eabi-
105-
106104
INC += -I.
107105
INC += -I$(TOP)
108106
ifeq ($(PB_MCU_FAMILY),STM32)
@@ -155,6 +153,18 @@ OPENOCD ?= openocd
155153
OPENOCD_CONFIG ?= openocd_stm32$(PB_MCU_SERIES_LCASE).cfg
156154
TEXT0_ADDR ?= 0x08000000
157155

156+
ifeq ($(PB_MCU_FAMILY),desktop)
157+
UNAME_S := $(shell uname -s)
158+
LD = $(CC)
159+
CFLAGS += $(INC) -Wall -Werror -Wdouble-promotion -Wfloat-conversion -std=gnu99 $(COPT) -D_GNU_SOURCE
160+
ifeq ($(UNAME_S),Linux)
161+
LDFLAGS += -Wl,-Map=$@.map,--cref -Wl,--gc-sections
162+
else ifeq ($(UNAME_S),Darwin)
163+
LDFLAGS += -Wl,-map,$@.map -Wl,-dead_strip
164+
endif
165+
LIBS =
166+
else # end desktop, begin embedded
167+
CROSS_COMPILE ?= arm-none-eabi-
158168
ifeq ($(PB_MCU_FAMILY),STM32)
159169
CFLAGS_MCU_F0 = -mthumb -mtune=cortex-m0 -mcpu=cortex-m0 -msoft-float
160170
CFLAGS_MCU_F4 = -mthumb -mtune=cortex-m4 -mcpu=cortex-m4 -mfpu=fpv4-sp-d16 -mfloat-abi=hard
@@ -200,6 +210,8 @@ endif
200210
# avoid doubles
201211
CFLAGS += -fsingle-precision-constant -Wdouble-promotion
202212

213+
endif # end embedded, begin common
214+
203215
# Tune for Debugging or Optimization
204216
ifeq ($(DEBUG), 1)
205217
CFLAGS += -Og -ggdb
@@ -233,13 +245,25 @@ include $(PBTOP)/bricks/_common/sources.mk
233245
# between the top level directory and the micropython/ subdirectory.
234246

235247
PY_EXTRA_SRC_C = $(addprefix shared/,\
236-
libc/string0.c \
237-
runtime/gchelper_native.c \
238248
runtime/interrupt_char.c \
239249
runtime/pyexec.c \
240250
runtime/stdout_helpers.c \
251+
)
252+
253+
ifeq ($(PB_MCU_FAMILY),desktop)
254+
PY_EXTRA_SRC_C += $(addprefix shared/,\
255+
runtime/gchelper_generic.c \
256+
)
257+
PY_EXTRA_SRC_C += $(addprefix bricks/simhub/,\
258+
pbio_os_hook.c \
259+
)
260+
else
261+
PY_EXTRA_SRC_C += $(addprefix shared/,\
262+
libc/string0.c \
263+
runtime/gchelper_native.c \
241264
runtime/sys_stdio_mphal.c \
242265
)
266+
endif
243267

244268
ifneq ($(PBIO_PLATFORM),move_hub)
245269
# to avoid adding unused root pointers
@@ -262,7 +286,9 @@ PY_EXTRA_SRC_C += $(addprefix bricks/_common/,\
262286
endif
263287

264288
# Not all MCUs support thumb2 instructions.
265-
ifeq ($(PB_MCU_SERIES),$(filter $(PB_MCU_SERIES),AT91SAM7 F0 TIAM1808))
289+
ifeq ($(PB_MCU_FAMILY),desktop)
290+
SRC_S +=
291+
else ifeq ($(PB_MCU_SERIES),$(filter $(PB_MCU_SERIES),AT91SAM7 F0 TIAM1808))
266292
SRC_S += shared/runtime/gchelper_thumb1.s
267293
else
268294
SRC_S += shared/runtime/gchelper_thumb2.s
@@ -561,8 +587,12 @@ CFLAGS += -DMICROPY_MODULE_FROZEN_MPY
561587
MPY_TOOL_FLAGS += -mlongint-impl none
562588
endif
563589

590+
ifneq ($(PB_MCU_FAMILY),desktop)
564591
# Main firmware build targets
565592
TARGETS := $(BUILD)/firmware.zip
593+
else
594+
TARGETS := $(BUILD)/firmware.elf
595+
endif
566596

567597
all: $(TARGETS)
568598

@@ -673,4 +703,12 @@ deploy-openocd: $(BUILD)/firmware-base.bin
673703
$(ECHO) "Writing $< to the board via ST-LINK using OpenOCD"
674704
$(Q)$(OPENOCD) -f $(OPENOCD_CONFIG) -c "stm_flash $< $(TEXT0_ADDR)"
675705

706+
# Run emulation build on a POSIX system using normal stdio
707+
run: $(BUILD)/firmware.elf
708+
@$(BUILD)/firmware.elf
709+
@echo "Exit status: $$?"
710+
711+
test: $(BUILD)/firmware.elf
712+
$(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"
713+
676714
include $(TOP)/py/mkrules.mk

bricks/simhub/Makefile

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# SPDX-License-Identifier: MIT
2+
# Copyright (c) 2025 The Pybricks Authors
3+
4+
PBIO_PLATFORM = sim_hub
5+
PB_MCU_FAMILY = desktop
6+
PB_FROZEN_MODULES = 1
7+
MICROPY_ROM_TEXT_COMPRESSION = 1
8+
9+
10+
include ../_common/arm_none_eabi.mk

bricks/simhub/manifest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
include("../_common/manifest.py")

bricks/simhub/mpconfigport.h

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
#include <stdint.h>
2+
3+
#define MICROPY_HW_BOARD_NAME "Desktop"
4+
#define MICROPY_HW_MCU_NAME "Desktop"
5+
6+
#define PYBRICKS_HUB_CLASS_NAME (MP_QSTR_VirtualHub)
7+
8+
#define PYBRICKS_HUB_NAME "virtualhub"
9+
#define PYBRICKS_HUB_VIRTUALHUB (1)
10+
11+
// Pybricks modules
12+
#define PYBRICKS_PY_COMMON (1)
13+
#define PYBRICKS_PY_COMMON_BLE (0)
14+
#define PYBRICKS_PY_COMMON_CHARGER (1)
15+
#define PYBRICKS_PY_COMMON_COLOR_LIGHT (1)
16+
#define PYBRICKS_PY_COMMON_CONTROL (1)
17+
#define PYBRICKS_PY_COMMON_IMU (0)
18+
#define PYBRICKS_PY_COMMON_KEYPAD (1)
19+
#define PYBRICKS_PY_COMMON_KEYPAD_HUB_BUTTONS (1)
20+
#define PYBRICKS_PY_COMMON_LIGHT_ARRAY (1)
21+
#define PYBRICKS_PY_COMMON_LIGHT_MATRIX (0)
22+
#define PYBRICKS_PY_COMMON_LOGGER (1)
23+
#define PYBRICKS_PY_COMMON_LOGGER_REAL_FILE (1)
24+
#define PYBRICKS_PY_COMMON_MOTORS (1)
25+
#define PYBRICKS_PY_COMMON_SPEAKER (0)
26+
#define PYBRICKS_PY_COMMON_SYSTEM (1)
27+
#define PYBRICKS_PY_EV3DEVICES (0)
28+
#define PYBRICKS_PY_EXPERIMENTAL (1)
29+
#define PYBRICKS_PY_HUBS (1)
30+
#define PYBRICKS_PY_IODEVICES (0)
31+
#define PYBRICKS_PY_NXTDEVICES (0)
32+
#define PYBRICKS_PY_PARAMETERS (1)
33+
#define PYBRICKS_PY_PARAMETERS_BUTTON (1)
34+
#define PYBRICKS_PY_PARAMETERS_ICON (0)
35+
#define PYBRICKS_PY_PARAMETERS_IMAGE (0)
36+
#define PYBRICKS_PY_PUPDEVICES (1)
37+
#define PYBRICKS_PY_PUPDEVICES_REMOTE (0)
38+
#define PYBRICKS_PY_DEVICES (1)
39+
#define PYBRICKS_PY_ROBOTICS (1)
40+
#define PYBRICKS_PY_ROBOTICS_DRIVEBASE_SPIKE (0)
41+
#define PYBRICKS_PY_TOOLS (1)
42+
#define PYBRICKS_PY_TOOLS_HUB_MENU (0)
43+
#define PYBRICKS_PY_TOOLS_APP_DATA (1)
44+
45+
// Pybricks options
46+
#define PYBRICKS_OPT_COMPILER (1)
47+
#define PYBRICKS_OPT_USE_STACK_END_AS_TOP (1)
48+
#define PYBRICKS_OPT_RAW_REPL (0)
49+
#define PYBRICKS_OPT_FLOAT (0)
50+
#define PYBRICKS_OPT_TERSE_ERR (0)
51+
#define PYBRICKS_OPT_EXTRA_LEVEL1 (0)
52+
#define PYBRICKS_OPT_EXTRA_LEVEL2 (0)
53+
#define PYBRICKS_OPT_CUSTOM_IMPORT (0)
54+
#define PYBRICKS_OPT_NATIVE_MOD (0)
55+
56+
#include "../_common/mpconfigport.h"

bricks/simhub/pbio_os_hook.c

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// SPDX-License-Identifier: MIT
2+
// Copyright (c) 2025 The Pybricks Authors
3+
4+
#include <signal.h>
5+
#include <sys/select.h>
6+
7+
#include <pbio/os.h>
8+
9+
#include "py/runtime.h"
10+
#include "py/mphal.h"
11+
12+
pbio_os_irq_flags_t pbio_os_hook_disable_irq(void) {
13+
sigset_t sigmask;
14+
sigfillset(&sigmask);
15+
16+
sigset_t origmask;
17+
pthread_sigmask(SIG_SETMASK, &sigmask, &origmask);
18+
return origmask;
19+
}
20+
21+
void pbio_os_hook_enable_irq(pbio_os_irq_flags_t flags) {
22+
sigset_t origmask = (sigset_t)flags;
23+
pthread_sigmask(SIG_SETMASK, &origmask, NULL);
24+
}
25+
26+
void pbio_os_hook_wait_for_interrupt(pbio_os_irq_flags_t flags) {
27+
struct timespec timeout = {
28+
.tv_sec = 0,
29+
.tv_nsec = 100000,
30+
};
31+
// "sleep" with "interrupts" enabled
32+
sigset_t origmask = flags;
33+
MP_THREAD_GIL_EXIT();
34+
pselect(0, NULL, NULL, NULL, &timeout, &origmask);
35+
MP_THREAD_GIL_ENTER();
36+
}

bricks/simhub/qstrdefsport.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// qstrs specific to this port
2+
// *FORMAT-OFF*

0 commit comments

Comments
 (0)