If MBOOT_BOARD_ENTRY_INIT is defined by a board then that function must now
make sure system clocks are configured, eg by calling mboot_entry_init().
Signed-off-by: Damien George <damien@micropython.org>
If a board wants to customise the clocks it can define the following:
MBOOT_CLK_PLLM
MBOOT_CLK_PLLN
MBOOT_CLK_PLLP
MBOOT_CLK_PLLQ
MBOOT_CLK_PLLR (only needed on STM32H7)
MBOOT_FLASH_LATENCY
MBOOT_CLK_AHB_DIV
MBOOT_CLK_APB1_DIV
MBOOT_CLK_APB2_DIV
MBOOT_CLK_APB3_DIV (only needed on STM32H7)
MBOOT_CLK_APB4_DIV (only needed on STM32H7)
Signed-off-by: Damien George <damien@micropython.org>
The default stm32lib remains lib/stm32lib, but it can now be easily
overriden at build time by specifying STM32LIB_DIR, or STM32LIB_CMSIS_DIR
and STM32LIB_HAL_DIR.
Signed-off-by: Damien George <damien@micropython.org>
This also fixes a possible race condition when exiting initialisation mode:
reading then writing to ISR (via ISR &= ~RTC_ISR_INIT) will clear any flags
that were set by the hardware between the read and the write. The correct
way to clear just the INIT bit is to just do a single write via ISR =
~RTC_ISR_INIT, which will not clear any other flags (they must be written
to 0 to clear), and that is exactly what LL_RTC_DisableInitMode does.
Signed-off-by: Damien George <damien@micropython.org>
And don't assert on the sector number in sector_erase, so it can support
erasing arbitrary sectors.
Signed-off-by: Damien George <damien@micropython.org>
The inclusion of `umachine` in the list of built-in modules is now done
centrally in py/objmodule.c. Enabling MICROPY_PY_MACHINE will include this
module.
As part of this, all ports now have `umachine` as the core module name
(previously some had only `machine` as the name).
Signed-off-by: Damien George <damien@micropython.org>
This commit adds support for the STM32G4 series of MCUs, and a board
definition for NUCLEO_G474RE. This board has the REPL on LPUART1 which is
connected to the on-board ST-link USB-UART.
It's needed at least on F4 because this file overrides the weak function
HAL_RCC_DeInit() from hal_rcc.c.
Signed-off-by: Damien George <damien@micropython.org>
PLL3-Q is more reliable than PLL1-Q for the USB clock source when entering
mboot from various reset states (eg power on vs MCU reset). (It was found
that if the main application used PLL3-Q then sometimes the USB clock
source would stay stuck on PLL3-Q and not switch to PLL1-Q after a reset.)
Other related changes:
- SystemCoreClockUpdate() should be called on H7 because the calculation
can be involved in some cases.
- __set_PRIMASK(0) should be called because on H7 the built-in ST DFU
bootloader exits with IRQs disabled.
Signed-off-by: Damien George <damien@micropython.org>
H7 MCUs have ECC and writes do not go through to SRAM until 64-bits have
been written (on another location is written). So use 64-bit writes for
the bootloader-state variable so it is committed before the system reset.
As part of this change, the lower byte of the bootloader address in
BL_STATE must now be the magic number 0x5a5 for the state to be valid
(previously this was 0x000 which is not as robust).
Signed-off-by: Damien George <damien@micropython.org>
The original code used a independent state with regards to the interrupt.
During heavy bus error conditions the internal state could become
out-of-sync with the interrupts.
Further explanation: during the development of an application using CAN
communication, a interrupt-run-away was found in some situations. It was
found that the error interrupt triggered (Warning, Passive or Bus-Off, all
triggered it) the run-away. The only recovery was a reset.
Two problems were found:
- the error interrupt is enabled but not cleared in the interrupt routine;
- an internal variable 'State' that was used to track the message received
state (empty, new, full, overflow) that was not directly related to
interrupt that indicated the state.
In this commit these issues are fixed by adding more values for the
interrupt reason (warning, passive, bus off) and clearing the error
interrupts, and making the internal state directly dependent on the
interrupt state for received messages.
Furthermore, introducing the FIFO1 in the CAN receive stage, another issue
existed. Even if the messages are received into the FIFO1 (by selecting
message filtering for FIFO0 and FIFO1), the interrupt firing was indicating
FIFO0 Rx. The configuration of the interrupts for this is now also fixed.
The CAN peripheral has 2 interrupt lines going into the NVIC controller.
The assignment of the interrupt reasons to these 2 interrupt lines was
missing. Now the reception of FIFO1 messages triggers the second interrupt
line. Other interrupts (Rx FIFO0 and bus error) are assigned to the first
interrupt line.
Tested on a Nucleo-G474, and also checked the HAL function to work with the
H7 family.
The keyword "af" has been deprecated for some time and "alt" should be used
instead (but "af" still works).
Signed-off-by: Damien George <damien@micropython.org>
So that a board can access other HAL_RCC functions if it needs them (this
was not possible previously by just adding hal_rcc.c to the src list for a
board because it would clash with the custom HAL_RCC_GetHCLKFreq function).
Signed-off-by: Damien George <damien@micropython.org>
This adds MBEDTLS_MD_SHA1 to the list of default hashes for TLS 1.2
handshake signatures. Although SHA-1 is weak, this option is turned on in
the default mbedtls configuration file, and allows better compatibility
with older servers. In particular it allows an stm32-mbedtls-based client
to connect to an axtls-based client (eg default unix port and esp8266).
Signed-off-by: Damien George <damien@micropython.org>
This is needed because these ports allocate mbedtls data on the MicroPython
heap, and SSL socket objects must be fully cleaned up when they are garbage
collected, to free this memory allocated by mbedtls. As part of this,
gc_sweep_all() will now ensure that the MP_STATE_PORT(mbedtls_memory)
linked-list is fully deallocated on soft reset.
Signed-off-by: Damien George <damien@micropython.org>
If MICROPY_PY_SYS_PATH_ARGV_DEFAULTS is enabled (which it is by default)
then sys.path and sys.argv will be initialised and populated with default
values. This keeps all bare-metal ports aligned.
Signed-off-by: Damien George <damien@micropython.org>
Frozen modules will be searched preferentially, but gives the user the
ability to override this behavior.
This matches the previous behavior where "" was implicitly the frozen
search path, but the frozen list was checked before the filesystem.
Signed-off-by: Jim Mussared <jim.mussared@gmail.com>
The current ST HAL does not support reading the extended CSD so cannot
correctly detect the capacity of high-capacity cards. As a workaround, the
capacity can be forced via the MICROPY_HW_MMCARD_LOG_BLOCK_NBR config
option.
Signed-off-by: Damien George <damien@micropython.org>
These were commented correctly by their colour, but in the wrong order with
respect to the PCB silkscreen.
Fixes issue #8054.
Signed-off-by: Jim Mussared <jim.mussared@gmail.com>
Instead of board pins, so that pins which have only the CPU specified in
pins.csv can still be used with mp_hal_pin_config_alt_static().
Signed-off-by: Damien George <damien@micropython.org>
A board can now define the following linker symbols to configure its flash
storage layout:
_micropy_hw_internal_flash_storage_start
_micropy_hw_internal_flash_storage_end
_micropy_hw_internal_flash_storage_ram_cache_start
_micropy_hw_internal_flash_storage_ram_cache_end
And optionally have a second flash segment by configuring
MICROPY_HW_ENABLE_INTERNAL_FLASH_STORAGE_SEGMENT2 to 1 and defining:
_micropy_hw_internal_flash_storage2_start
_micropy_hw_internal_flash_storage2_end
Signed-off-by: Damien George <damien@micropython.org>
This reduces code size and code duplication, and fixes `pyb.usb_mode()` so
that it now returns the correct string when in multi-VCP mode (before, it
would return None when in one of these modes).
Signed-off-by: Damien George <damien@micropython.org>
This is to make the builds for all nucleo/discovery boards uniform, so they
can be treated the same by the auto build scripts.
The CI script is updated to explicitly enable mboot and packing, to test
these features.
Signed-off-by: Damien George <damien@micropython.org>
This prevents SPI4/5 from being used if SDIO and CYW43 are enabled, because
the DMA for the SDIO is used on an IRQ and must be exclusivly available for
use by the SDIO peripheral.
Signed-off-by: Damien George <damien@micropython.org>
Because DMA2 may be in use by other peripherals, eg SPI1.
On PYBD-SF6 it's possible to trigger a bug in the existing code by turning
on WLAN and connecting to an AP, pinging the IP address from a PC and
running the following code on the PYBD:
def spi_test(s):
while 1:
s.write('test')
s.read(4)
spi_test(machine.SPI(1,100000000))
This will eventually fail with `OSError: [Errno 110] ETIMEDOUT` because
DMA2 was turned off by the CYW43 driver during the SPI1 transfer.
This commit fixes the bug by removing the code that explicitly disables
DMA2. Instead DMA2 will be automatically disabled after an inactivity
timeout, see commit a96afae90f
Signed-off-by: Damien George <damien@micropython.org>
Quail (https://www.mikroe.com/quail, PID: MIKROE-1793) is based on an
STM32F427VI CPU, featuring 2048 kB of Flash memory and 192 kB of RAM. An
on-board Cypress S25FL164K adds 8 MB of SPI Flash.
Quail has 4 mikroBUS(TM) sockets for Mikroe click(TM) board connectivity,
along with 24 screw terminals for connecting additional electronics and two
USB ports (one for programming, the other for external mass storage).
4 UARTs, 2 SPIs and 1 I2C bus are available for communication.
Signed-off-by: Lorenzo Cappelletti <lorenzo.cappelletti@gmail.com>
Don't force the 'HAL' string to be part of the platform string because
it doesn't have a sensible meaning for all possible platforms, and
swap it with the PLATFORM_ARCH string so the strings which most platforms
have come first.
Although the pyboard has only 4 LEDs, there are some boards that (may) have
more. This commit adds 2 more LEDs to the led.c file that if defined in
the board-specific config file will be compiled in.
Eliminate noise data from being sent to the I2S peripheral when the
transmitted sample stream is stopped.
Signed-off-by: Mike Teachman <mike.teachman@gmail.com>
This is an stm32-specific feature that's accessed via the pyb module, so
not something that will be widely enabled.
Signed-off-by: Damien George <damien@micropython.org>
This commit is a no-op change. Future improvements can come from making
individual boards use CORE or BASIC.
Signed-off-by: Jim Mussared <jim.mussared@gmail.com>
Prior to this commit IRQs on STM32F4 could be lost because SR is cleared by
reading SR then reading DR. For example, if both RXNE and IDLE IRQs were
active upon entry to the IRQ handler, then IDLE is lost because the code
that handles RXNE comes first and accidentally clears SR (by reading SR
then DR to get the incoming character).
This commit fixes this problem by making the IRQ handler more atomic in the
following operations:
- get current IRQ status flags
- deal with RX character
- clear remaining status flags
- call user handler
On the STM32F4 it's very hard to get this right because the only way to
clear IRQ status flags is to read SR then DR, but the read of DR may read
some data which should remain in the register until the user wants to read
it. And it won't work to cache the read because RTS/CTS flow control will
then not work. So instead the new code disables interrupts if the DR is
full and waits for the user to read it before reenabling the interrupts.
Fixes issue mentioned in #4599 and #6082.
Signed-off-by: Damien George <damien@micropython.org>
This will be used by https://micropython.org/download/ to generate the
full listing of boards and firmware files.
Optionally supports a board.md for additional customisation of the
download page, as well as deploy.md for flashing instructions.
Signed-off-by: Jim Mussared <jim.mussared@gmail.com>
Ensures consistent behaviour and resolves the D-Cache bug (the "exhaustive"
argument being lost due to cache being turned off) when O0 is used.
The changes in this commit are:
- Change -O0 to -Os because "gcc is considered broken at -O0" according to
https://github.com/ARM-software/CMSIS_5/issues/620#issuecomment-550235656
- Use volatile for mem_base so the compiler doesn't optimise away reads or
writes to the SDRAM, which is being tested.
- Use DSB to prevent any other compiler optimisations that would change the
testing logic.
- Use alternating pattern/antipattern in exhaustive test to catch more
hardware/configuration errors.
Implementation adapted by @andrewleech, taken directly from investigation
by @iabdalkader and @dpgeorge.
See #7841 and #7869 for further discussion.
The H743 has equal sized pages of 128k, which means the filesystem doesn't
need to be near the beginning. This commit moves the filesystem to the
very end of flash, and extends it to 512k (4 pages).
Signed-off-by: Damien George <damien@micropython.org>
This change adds the OLIMEX H407 support to the STM32 port. The H407
(https://www.olimex.com/Products/ARM/ST/STM32-H407/) is simliar to the
already existing E407
(https://www.olimex.com/Products/ARM/ST/STM32-E407) but does not support
Ethernet and has a full-size USB-A port instead of a Mini-USB socket.
Both boards use the STM32F407ZGT6 CPU.
This port is basically a copy of the E407 but with changed pinmux:
* Removed Ethernet pin definition
* Removed UART1 (pins are used for other functions)
* Removed UART3 flow control pins (pins are used for other functions)
* Removed SD-Card detect pin (since it is not connected on the H407)
A REPL on UART3 is connected to the U3BOOT-header, a 3-pin header with RX,
TX and GND that is intended for the serial terminal.
Tested:
* Micro-SD Card is detected when inserted on RESET
* REPL on UART3 works
* Serial port on the mini USB socket
Signed-off-by: Chris Fiege <cfi@pengutronix.de>
This commit removes all parts of code associated with the existing
MICROPY_OPT_CACHE_MAP_LOOKUP_IN_BYTECODE optimisation option, including the
-mcache-lookup-bc option to mpy-cross.
This feature originally provided a significant performance boost for Unix,
but wasn't able to be enabled for MCU targets (due to frozen bytecode), and
added significant extra complexity to generating and distributing .mpy
files.
The equivalent performance gain is now provided by the combination of
MICROPY_OPT_LOAD_ATTR_FAST_PATH and MICROPY_OPT_MAP_LOOKUP_CACHE (which has
been enabled on the unix port in the previous commit).
It's hard to provide precise performance numbers, but tests have been run
on a wide variety of architectures (x86-64, ARM Cortex, Aarch64, RISC-V,
xtensa) and they all generally agree on the qualitative improvements seen
by the combination of MICROPY_OPT_LOAD_ATTR_FAST_PATH and
MICROPY_OPT_MAP_LOOKUP_CACHE.
For example, on a "quiet" Linux x64 environment (i3-5010U @ 2.10GHz) the
change from CACHE_MAP_LOOKUP_IN_BYTECODE, to LOAD_ATTR_FAST_PATH combined
with MAP_LOOKUP_CACHE is:
diff of scores (higher is better)
N=2000 M=2000 bccache -> attrmapcache diff diff% (error%)
bm_chaos.py 13742.56 -> 13905.67 : +163.11 = +1.187% (+/-3.75%)
bm_fannkuch.py 60.13 -> 61.34 : +1.21 = +2.012% (+/-2.11%)
bm_fft.py 113083.20 -> 114793.68 : +1710.48 = +1.513% (+/-1.57%)
bm_float.py 256552.80 -> 243908.29 : -12644.51 = -4.929% (+/-1.90%)
bm_hexiom.py 521.93 -> 625.41 : +103.48 = +19.826% (+/-0.40%)
bm_nqueens.py 197544.25 -> 217713.12 : +20168.87 = +10.210% (+/-3.01%)
bm_pidigits.py 8072.98 -> 8198.75 : +125.77 = +1.558% (+/-3.22%)
misc_aes.py 17283.45 -> 16480.52 : -802.93 = -4.646% (+/-0.82%)
misc_mandel.py 99083.99 -> 128939.84 : +29855.85 = +30.132% (+/-5.88%)
misc_pystone.py 83860.10 -> 82592.56 : -1267.54 = -1.511% (+/-2.27%)
misc_raytrace.py 21490.40 -> 22227.23 : +736.83 = +3.429% (+/-1.88%)
This shows that the new optimisations are at least as good as the existing
inline-bytecode-caching, and are sometimes much better (because the new
ones apply caching to a wider variety of map lookups).
The new optimisations can also benefit code generated by the native
emitter, because they apply to the runtime rather than the generated code.
The improvement for the native emitter when LOAD_ATTR_FAST_PATH and
MAP_LOOKUP_CACHE are enabled is (same Linux environment as above):
diff of scores (higher is better)
N=2000 M=2000 native -> nat-attrmapcache diff diff% (error%)
bm_chaos.py 14130.62 -> 15464.68 : +1334.06 = +9.441% (+/-7.11%)
bm_fannkuch.py 74.96 -> 76.16 : +1.20 = +1.601% (+/-1.80%)
bm_fft.py 166682.99 -> 168221.86 : +1538.87 = +0.923% (+/-4.20%)
bm_float.py 233415.23 -> 265524.90 : +32109.67 = +13.756% (+/-2.57%)
bm_hexiom.py 628.59 -> 734.17 : +105.58 = +16.796% (+/-1.39%)
bm_nqueens.py 225418.44 -> 232926.45 : +7508.01 = +3.331% (+/-3.10%)
bm_pidigits.py 6322.00 -> 6379.52 : +57.52 = +0.910% (+/-5.62%)
misc_aes.py 20670.10 -> 27223.18 : +6553.08 = +31.703% (+/-1.56%)
misc_mandel.py 138221.11 -> 152014.01 : +13792.90 = +9.979% (+/-2.46%)
misc_pystone.py 85032.14 -> 105681.44 : +20649.30 = +24.284% (+/-2.25%)
misc_raytrace.py 19800.01 -> 23350.73 : +3550.72 = +17.933% (+/-2.79%)
In summary, compared to MICROPY_OPT_CACHE_MAP_LOOKUP_IN_BYTECODE, the new
MICROPY_OPT_LOAD_ATTR_FAST_PATH and MICROPY_OPT_MAP_LOOKUP_CACHE options:
- are simpler;
- take less code size;
- are faster (generally);
- work with code generated by the native emitter;
- can be used on embedded targets with a small and constant RAM overhead;
- allow the same .mpy bytecode to run on all targets.
See #7680 for further discussion. And see also #7653 for a discussion
about simplifying mpy-cross options.
Signed-off-by: Jim Mussared <jim.mussared@gmail.com>
Any external user of DMA (eg a board with a custom DMA driver) must call
dma_external_acquire() for their DMA controller/stream to ensure that the
DMA clock is not automatically turned off while it's still being used
externally.
Signed-off-by: Damien George <damien@micropython.org>
This change allows a CPU pin to be hidden from the user by prefixing it
with a "-" in the pins.csv file for a board. It will still be available in
C code, just not exposed to Python.
Signed-off-by: Damien George <damien@micropython.org>
Don't want users to accidentally use boot.py (because recovering requires
knowing how to activate safe mode).
Signed-off-by: Jim Mussared <jim.mussared@gmail.com>
This commit is based upon prior work of @dpgeorge and @koendv.
MCU support for the STM32H7A3 and B3 families MCUs:
- STM32H7A3xx
- STM32H7A3xxQ (SMPS)
- STM32H7B3xx
- STM32H7B3xxQ (SMPS)
Support has been added for the STM32H7B3I_DK board.
Signed-off-by: Jan Staal <info@janstaal.com>
To simplify the socket state.
The CC3K driver (see drivers/cc3000/inc/socket.h and src/socket.c) has
socket() returning an INT16 so there is now enough room to store it
directly in the fileno member.
The zephyr port doesn't support SoftI2C so it's not enabled, and the legacy
I2C constructor check can be removed.
Signed-off-by: Damien George <damien@micropython.org>
To keep things neat and tidy, we ensure that each file has 1 and only 1
newline at the end of each file.
Signed-off-by: David Lechner <david@pybricks.com>
A board can now define the following to fully customise the extended block
device interface provided by the storage sub-system:
- MICROPY_HW_BDEV_BLOCKSIZE_EXT
- MICROPY_HW_BDEV_READBLOCKS_EXT
- MICROPY_HW_BDEV_WRITEBLOCKS_EXT
- MICROPY_HW_BDEV_ERASEBLOCKS_EXT
Signed-off-by: Damien George <damien@micropython.org>
This commit creates a new stm32 board for the NUCLEO_H743ZI2, which is the
current version of this from ST. This is a modified copy of the
NUCLEO_H743ZI board, and the ZI2 board differs in a few minor ways:
- LED2 has moved from PB7 to PE1 and is now yellow rather than blue
- the USB power enable has moved from PG6 to PG10
- the USER button is now pulled down
This allows changing the frequency to: 100kHz, 200kHz, 400kHz, 800kHz,
1MHz, 2MHz, 4MHz, 8MHz, 16MHz, 32MHz, 64MHz. For frequencies 2MHz and
below, low power run (LPR) mode is enabled automatically.
Signed-off-by: Damien George <damien@micropython.org>
For consistency with other board-level config macros that begin with
MICROPY_HW_USB.
Also allow boards in the mimxrt, nrf and samd ports to configure these
values.
Signed-off-by: Damien George <damien@micropython.org>
Add basic support for LEGO HUB NO.6 (e.g. LEGO SPIKE Prime, LEGO MINDSTORMS
Robot Inventor).
See README.md for details.
Thanks to @dpgeorge for helping put this together.
Signed-off-by: Tobias Thyrrestrup <tt@LEGO.com>
It needs to use a different function because the formula to compute the
baudrate on LPUART1 is different to a normal UART.
Fixes issue #7466.
Signed-off-by: Damien George <damien@micropython.org>
* Make SDRAM test cache-aware for newer MCUs.
* Use the defined data bus width (instead of the fixed 8-bits).
* Allow optional failure on error with verbose error messages.
* Test speed is now inverted (test accepts exhaustive instead fast).
Call MICROPY_EVENT_POLL_HOOK even on very short delays so that busy loops
that call sleep_ms still yield to events and other threads.
See related issue #5344.
Replace "master" with "controller" and "slave" with "peripheral" in
comments, errors, and debug messages.
Add CONTROLLER and PERIPHERAL constants to pyb.SPI and pyb.I2C classes;
retain MASTER and SLAVE constants for backward compatiblity.
This commit adds I2S protocol support for the esp32 and stm32 ports, via
a new machine.I2S class. It builds on the stm32 work of blmorris, #1361.
Features include:
- a consistent I2S API across the esp32 and stm32 ports
- I2S configurations supported:
- master transmit and master receive
- 16-bit and 32-bit sample sizes
- mono and stereo formats
- sampling frequency
- 3 modes of operation:
- blocking
- non-blocking with callback
- uasyncio
- internal ring buffer size can be tuned
- documentation for Pyboards and esp32-based boards
- tested on the following development boards:
- Pyboard D SF2W
- Pyboard V1.1
- ESP32 with SPIRAM
- ESP32
Signed-off-by: Mike Teachman <mike.teachman@gmail.com>
The ADC_FIRST_GPIO_CHANNEL and ADC_LAST_GPIO_CHANNEL macros are no longer
needed. Instead the pin_adcX table (X = 1, 2, 3) is now generated to be
the exact size needed for a given MCU, and MP_ARRAY_SIZE(pin_adcX) is used
to determine the upper bound.
This commit also allows CPU pins to be excluded from ADC configuration if
they are hidden by prefixing their name with a "-".
Signed-off-by: Damien George <damien@micropython.org>
* Modify common functions in adc.c to accept ADC handle.
* Most external channels are connected to ADC12 which is used by default.
* For ADCAll (internal channels) ADC3 is used instead.
* Issue #4435 is possibly related (at least partially fixed).
This adds a call to mp_deinit() in the main function of the STM32 port.
This enables the use of MICROPY_PORT_DEINIT_FUNC on that port, as well as
cleaning up the GIL if threading is enabled.
Previous behaviour was: if boot.py had an exception then main.py would
still run, which is arguably unexpected behaviour.
This commit changes the behaviour so main.py is not run if boot.py has an
error.
Signed-off-by: Damien George <damien@micropython.org>
It reschedules the BT HCI poll soft timer so that it is called exactly when
the next timer expires.
Signed-off-by: Damien George <damien@micropython.org>
Instead of using systick the BT subsystem is now scheduled using a soft
timer. This means it is scheduled only when it is enabled.
Signed-off-by: Damien George <damien@micropython.org>
And call mp_pairheap_init_node() in soft_timer_static_init() so that
reinsert can be called after static_init.
Signed-off-by: Damien George <damien@micropython.org>
This introduces a new macro to get the main thread and uses it to ensure
that asynchronous exceptions such as KeyboardInterrupt (CTRL+C) are only
scheduled on the main thread. This is more deterministic than being
scheduled on a random thread and is more in line with CPython that only
allow signal handlers to run on the main thread.
Fixes issue #7026.
Signed-off-by: David Lechner <david@pybricks.com>
This moves mp_pending_exception from mp_state_vm_t to mp_state_thread_t.
This allows exceptions to be scheduled on a specific thread.
Signed-off-by: David Lechner <david@pybricks.com>