diff --git a/bricks/_common/sources.mk b/bricks/_common/sources.mk index 8ae728a3d..81a19f53c 100644 --- a/bricks/_common/sources.mk +++ b/bricks/_common/sources.mk @@ -63,6 +63,7 @@ PYBRICKS_PYBRICKS_SRC_C = $(addprefix pybricks/,\ iodevices/pb_type_uart_device.c \ iodevices/pb_type_iodevices_xbox_controller.c \ media/pb_module_media.c \ + media/pb_type_image.c \ nxtdevices/pb_module_nxtdevices.c \ nxtdevices/pb_type_nxtdevices_colorsensor.c \ nxtdevices/pb_type_nxtdevices_energymeter.c \ @@ -198,6 +199,7 @@ PBIO_SRC_C = $(addprefix lib/pbio/,\ src/drivebase.c \ src/error.c \ src/geometry.c \ + src/image/image.c \ src/imu.c \ src/int_math.c \ src/integrator.c \ diff --git a/bricks/cityhub/mpconfigport.h b/bricks/cityhub/mpconfigport.h index 99f69af52..4076ac78b 100644 --- a/bricks/cityhub/mpconfigport.h +++ b/bricks/cityhub/mpconfigport.h @@ -32,6 +32,7 @@ #define PYBRICKS_PY_IODEVICES (1) #define PYBRICKS_PY_IODEVICES_XBOX_CONTROLLER (0) #define PYBRICKS_PY_MEDIA (0) +#define PYBRICKS_PY_MEDIA_IMAGE (0) #define PYBRICKS_PY_NXTDEVICES (0) #define PYBRICKS_PY_PARAMETERS (1) #define PYBRICKS_PY_PARAMETERS_BUTTON (1) diff --git a/bricks/essentialhub/mpconfigport.h b/bricks/essentialhub/mpconfigport.h index 7463b95db..8f60041d3 100644 --- a/bricks/essentialhub/mpconfigport.h +++ b/bricks/essentialhub/mpconfigport.h @@ -33,6 +33,7 @@ #define PYBRICKS_PY_IODEVICES (1) #define PYBRICKS_PY_IODEVICES_XBOX_CONTROLLER (1) #define PYBRICKS_PY_MEDIA (0) +#define PYBRICKS_PY_MEDIA_IMAGE (0) #define PYBRICKS_PY_NXTDEVICES (0) #define PYBRICKS_PY_PARAMETERS (1) #define PYBRICKS_PY_PARAMETERS_BUTTON (1) diff --git a/bricks/ev3/mpconfigport.h b/bricks/ev3/mpconfigport.h index 5767d7857..c3b454e93 100644 --- a/bricks/ev3/mpconfigport.h +++ b/bricks/ev3/mpconfigport.h @@ -30,7 +30,8 @@ #define PYBRICKS_PY_HUBS (1) #define PYBRICKS_PY_IODEVICES (1) #define PYBRICKS_PY_IODEVICES_XBOX_CONTROLLER (0) -#define PYBRICKS_PY_MEDIA (0) +#define PYBRICKS_PY_MEDIA (1) +#define PYBRICKS_PY_MEDIA_IMAGE (1) #define PYBRICKS_PY_NXTDEVICES (1) #define PYBRICKS_PY_PARAMETERS (1) #define PYBRICKS_PY_PARAMETERS_BUTTON (1) diff --git a/bricks/movehub/mpconfigport.h b/bricks/movehub/mpconfigport.h index 580e79d19..b713a106c 100644 --- a/bricks/movehub/mpconfigport.h +++ b/bricks/movehub/mpconfigport.h @@ -29,6 +29,7 @@ #define PYBRICKS_PY_HUBS (1) #define PYBRICKS_PY_IODEVICES (0) #define PYBRICKS_PY_MEDIA (0) +#define PYBRICKS_PY_MEDIA_IMAGE (0) #define PYBRICKS_PY_PARAMETERS (1) #define PYBRICKS_PY_PARAMETERS_BUTTON (1) #define PYBRICKS_PY_PARAMETERS_BUTTON_REMOTE_ONLY (1) diff --git a/bricks/nxt/mpconfigport.h b/bricks/nxt/mpconfigport.h index 6837749bb..54bb77380 100644 --- a/bricks/nxt/mpconfigport.h +++ b/bricks/nxt/mpconfigport.h @@ -33,6 +33,7 @@ #define PYBRICKS_PY_HUBS (1) #define PYBRICKS_PY_IODEVICES (0) #define PYBRICKS_PY_MEDIA (0) +#define PYBRICKS_PY_MEDIA_IMAGE (0) #define PYBRICKS_PY_NXTDEVICES (0) #define PYBRICKS_PY_PARAMETERS (1) #define PYBRICKS_PY_PARAMETERS_BUTTON (1) diff --git a/bricks/primehub/mpconfigport.h b/bricks/primehub/mpconfigport.h index 42e930da6..d88884174 100644 --- a/bricks/primehub/mpconfigport.h +++ b/bricks/primehub/mpconfigport.h @@ -34,6 +34,7 @@ #define PYBRICKS_PY_IODEVICES (1) #define PYBRICKS_PY_IODEVICES_XBOX_CONTROLLER (1) #define PYBRICKS_PY_MEDIA (1) +#define PYBRICKS_PY_MEDIA_IMAGE (0) #define PYBRICKS_PY_NXTDEVICES (0) #define PYBRICKS_PY_PARAMETERS (1) #define PYBRICKS_PY_PARAMETERS_BUTTON (1) diff --git a/bricks/technichub/mpconfigport.h b/bricks/technichub/mpconfigport.h index c802e7add..db1a5fc9f 100644 --- a/bricks/technichub/mpconfigport.h +++ b/bricks/technichub/mpconfigport.h @@ -32,6 +32,7 @@ #define PYBRICKS_PY_IODEVICES (1) #define PYBRICKS_PY_IODEVICES_XBOX_CONTROLLER (1) #define PYBRICKS_PY_MEDIA (0) +#define PYBRICKS_PY_MEDIA_IMAGE (0) #define PYBRICKS_PY_NXTDEVICES (0) #define PYBRICKS_PY_PARAMETERS (1) #define PYBRICKS_PY_PARAMETERS_BUTTON (1) diff --git a/bricks/virtualhub/mpconfigvariant.h b/bricks/virtualhub/mpconfigvariant.h index ab4e2cd1a..2e8867808 100644 --- a/bricks/virtualhub/mpconfigvariant.h +++ b/bricks/virtualhub/mpconfigvariant.h @@ -27,6 +27,7 @@ #define PYBRICKS_PY_HUBS (1) #define PYBRICKS_PY_IODEVICES (0) #define PYBRICKS_PY_MEDIA (1) +#define PYBRICKS_PY_MEDIA_IMAGE (0) #define PYBRICKS_PY_NXTDEVICES (0) #define PYBRICKS_PY_PARAMETERS (1) #define PYBRICKS_PY_PARAMETERS_BUTTON (1) diff --git a/lib/pbio/doc/doxygen.conf b/lib/pbio/doc/doxygen.conf index a1fef2486..e8675371e 100644 --- a/lib/pbio/doc/doxygen.conf +++ b/lib/pbio/doc/doxygen.conf @@ -2128,6 +2128,7 @@ PREDEFINED = \ PBIO_CONFIG_DCMOTOR=1 \ PBIO_CONFIG_DCMOTOR_NUM_DEV=4 \ PBIO_CONFIG_DRIVEBASE_SPIKE=1 \ + PBIO_CONFIG_IMAGE=1 \ PBIO_CONFIG_LIGHT_MATRIX=1 \ PBIO_CONFIG_LIGHT=1 \ PBIO_CONFIG_MOTOR_PROCESS=1 \ diff --git a/lib/pbio/drv/display/display_ev3.c b/lib/pbio/drv/display/display_ev3.c index 32debe23a..e2a8a446b 100644 --- a/lib/pbio/drv/display/display_ev3.c +++ b/lib/pbio/drv/display/display_ev3.c @@ -13,12 +13,12 @@ #include #include -#include - #include "../core.h" +#include #include #include +#include #include #include @@ -111,13 +111,11 @@ static const pbdrv_gpio_t pin_lcd_reset = PBDRV_GPIO_EV3_PIN(12, 31, 28, 5, 0); static volatile spi_status_t spi_status = SPI_STATUS_ERROR; -PROCESS(pbdrv_display_ev3_init_process, "st7586s"); - /** * Number of column triplets. Each triplet is 3 columns of pixels, as detailed * below in the description of the display buffer. */ -#define ST7586S_NUM_COL_TRIPLETS (60) +#define ST7586S_NUM_COL_TRIPLETS ((PBDRV_CONFIG_DISPLAY_NUM_COLS + 2) / 3) /** * Number of rows. This is the same as the number of display rows. @@ -134,7 +132,7 @@ PROCESS(pbdrv_display_ev3_init_process, "st7586s"); * * Non-atomic updated by the application are allowed. */ -static uint8_t pbdrv_display_user_frame[PBDRV_CONFIG_DISPLAY_NUM_ROWS][PBDRV_CONFIG_DISPLAY_NUM_COLS] __attribute__((section(".noinit"), used)); +static uint8_t pbdrv_display_user_frame[PBDRV_CONFIG_DISPLAY_NUM_ROWS][ST7586S_NUM_COL_TRIPLETS * 3] __attribute__((section(".noinit"), used)); /** * Flag to indicate that the user frame has been updated and needs to be @@ -227,7 +225,7 @@ static const uint16_t pbdrv_display_pybricks_logo[] = { 15542, 15608, 15648, 15680, 15720, 15786, 15826, 15858, 15898, 15964, 16004, 16036, 16075, 16143, 16182, 16215, 16253, 16321, 16359, 16393, 16431, 16499, 16537, 16572, 16608, 16678, 16714, 16751, 16785, 16857, 16891, 16930, 16962, 17036, 17068, 17109, 17139, 17215, 17245, - 17289, 17314, 17396, 17421 + 17289, 17314, 17396, 17421, 22784 }; /** @@ -246,6 +244,10 @@ static void pbdrv_display_load_indexed_bitmap(const uint16_t *indexed_bitmap) { } pbdrv_display_user_frame[r][c] = set ? 3 : 0; } + // Fill unused columns out of screen. + for (size_t c = PBDRV_CONFIG_DISPLAY_NUM_COLS; c < ST7586S_NUM_COL_TRIPLETS * 3; c++) { + pbdrv_display_user_frame[r][c] = 0; + } } } @@ -258,9 +260,8 @@ void pbdrv_display_st7586s_encode_user_frame(void) { // Iterating ST7586S column-triplets, which are 3 columns each. for (size_t triplet = 0; triplet < ST7586S_NUM_COL_TRIPLETS; triplet++) { uint8_t p0 = pbdrv_display_user_frame[row][triplet * 3]; - // The last triplet has no second and third pixel. - uint8_t p1 = triplet == ST7586S_NUM_COL_TRIPLETS - 1 ? 0 : pbdrv_display_user_frame[row][triplet * 3 + 1]; - uint8_t p2 = triplet == ST7586S_NUM_COL_TRIPLETS - 1 ? 0 : pbdrv_display_user_frame[row][triplet * 3 + 2]; + uint8_t p1 = pbdrv_display_user_frame[row][triplet * 3 + 1]; + uint8_t p2 = pbdrv_display_user_frame[row][triplet * 3 + 2]; st7586s_send_buf[row * ST7586S_NUM_COL_TRIPLETS + triplet] = encode_triplet(p0, p1, p2); } } @@ -349,7 +350,7 @@ static const pbdrv_display_st7586s_action_t init_script[] = { void pbdrv_display_ev3_spi1_tx_complete(uint32_t status) { SPIIntDisable(SOC_SPI_1_REGS, SPI_DMA_REQUEST_ENA_INT); spi_status = SPI_STATUS_COMPLETE; - process_poll(&pbdrv_display_ev3_init_process); + pbio_os_request_poll(); } /** @@ -411,7 +412,7 @@ void pbdrv_display_st7586s_write_data_begin(uint8_t *data, uint32_t size) { * * Pinmux and common EDMA handlers are already set up in platform.c. */ -void pbdrv_display_init(void) { +static void pbdrv_display_ev3_spi_init(void) { // GPIO Mux. CS is in GPIO mode (manual control). pbdrv_gpio_alt(&pin_spi1_mosi, SYSCFG_PINMUX5_PINMUX5_23_20_SPI1_SIMO0); @@ -444,32 +445,27 @@ void pbdrv_display_init(void) { // Enable the SPI controller. SPIEnable(SOC_SPI_1_REGS); - - // Start SPI process and ask pbdrv to wait until it is initialized. - pbdrv_init_busy_up(); - process_start(&pbdrv_display_ev3_init_process); } +static pbio_os_process_t pbdrv_display_ev3_process; + /** * Display driver process. Initializes the display and updates the display - * with the user frame buffer at a regular interval if the user data was - * updated. + * with the user frame buffer if the user data was updated. */ -PROCESS_THREAD(pbdrv_display_ev3_init_process, ev, data) { +static pbio_error_t pbdrv_display_ev3_process_thread(pbio_os_state_t *state, void *context) { - static struct etimer etimer; + static pbio_os_timer_t timer; static uint32_t script_index; static uint8_t payload; - PROCESS_BEGIN(); + PBIO_OS_ASYNC_BEGIN(state); #if ST7586S_DO_RESET_AND_INIT pbdrv_gpio_out_low(&pin_lcd_reset); - etimer_set(&etimer, 10); - PROCESS_WAIT_EVENT_UNTIL(ev == PROCESS_EVENT_TIMER && etimer_expired(&etimer)); + PBIO_OS_AWAIT_MS(state, &timer, 10); pbdrv_gpio_out_high(&pin_lcd_reset); - etimer_set(&etimer, 120); - PROCESS_WAIT_EVENT_UNTIL(ev == PROCESS_EVENT_TIMER && etimer_expired(&etimer)); + PBIO_OS_AWAIT_MS(state, &timer, 120); #endif // ST7586S_DO_RESET_AND_INIT // For every action in the init script, either send a command or data, or @@ -479,8 +475,7 @@ PROCESS_THREAD(pbdrv_display_ev3_init_process, ev, data) { if (action->type == ST7586S_ACTION_DELAY) { // Simple delay. - etimer_set(&etimer, action->payload); - PROCESS_WAIT_EVENT_UNTIL(ev == PROCESS_EVENT_TIMER && etimer_expired(&etimer)); + PBIO_OS_AWAIT_MS(state, &timer, action->payload); } else { // Send command or data. payload = action->payload; @@ -490,7 +485,7 @@ PROCESS_THREAD(pbdrv_display_ev3_init_process, ev, data) { pbdrv_gpio_out_low(&pin_lcd_a0); } pbdrv_display_st7586s_write_data_begin(&payload, sizeof(payload)); - PROCESS_WAIT_EVENT_UNTIL(ev == PROCESS_EVENT_POLL && spi_status == SPI_STATUS_COMPLETE); + PBIO_OS_AWAIT_UNTIL(state, spi_status == SPI_STATUS_COMPLETE); pbdrv_gpio_out_high(&pin_lcd_cs); } } @@ -502,27 +497,58 @@ PROCESS_THREAD(pbdrv_display_ev3_init_process, ev, data) { pbdrv_display_load_indexed_bitmap(pbdrv_display_pybricks_logo); pbdrv_display_st7586s_encode_user_frame(); pbdrv_display_st7586s_write_data_begin(st7586s_send_buf, sizeof(st7586s_send_buf)); - PROCESS_WAIT_EVENT_UNTIL(ev == PROCESS_EVENT_POLL && spi_status == SPI_STATUS_COMPLETE); + PBIO_OS_AWAIT_UNTIL(state, spi_status == SPI_STATUS_COMPLETE); pbdrv_gpio_out_high(&pin_lcd_cs); // Done initializing. pbdrv_init_busy_down(); - // Regularly update the display with the user frame buffer, if changed. - etimer_set(&etimer, 40); + // Update the display with the user frame buffer, if changed. for (;;) { - PROCESS_WAIT_EVENT_UNTIL(ev == PROCESS_EVENT_TIMER && etimer_expired(&etimer)); - if (pbdrv_display_user_frame_update_requested) { - pbdrv_display_user_frame_update_requested = false; - pbdrv_display_st7586s_encode_user_frame(); - pbdrv_display_st7586s_write_data_begin(st7586s_send_buf, sizeof(st7586s_send_buf)); - PROCESS_WAIT_EVENT_UNTIL(ev == PROCESS_EVENT_POLL && spi_status == SPI_STATUS_COMPLETE); - pbdrv_gpio_out_high(&pin_lcd_cs); - } - etimer_reset(&etimer); + PBIO_OS_AWAIT_UNTIL(state, pbdrv_display_user_frame_update_requested); + pbdrv_display_user_frame_update_requested = false; + pbdrv_display_st7586s_encode_user_frame(); + pbdrv_display_st7586s_write_data_begin(st7586s_send_buf, sizeof(st7586s_send_buf)); + PBIO_OS_AWAIT_UNTIL(state, spi_status == SPI_STATUS_COMPLETE); + pbdrv_gpio_out_high(&pin_lcd_cs); } - PROCESS_END(); + PBIO_OS_ASYNC_END(PBIO_SUCCESS); +} + +/** + * Image corresponding to the display. + */ +static pbio_image_t display_image; + +/** + * Initialize the display driver. + */ +void pbdrv_display_init(void) { + // Initialize SPI. + pbdrv_display_ev3_spi_init(); + + // Initialize image. + pbio_image_init(&display_image, (uint8_t *)pbdrv_display_user_frame, + PBDRV_CONFIG_DISPLAY_NUM_COLS, PBDRV_CONFIG_DISPLAY_NUM_ROWS, + ST7586S_NUM_COL_TRIPLETS * 3); + + // Start display process and ask pbdrv to wait until it is initialized. + pbdrv_init_busy_up(); + pbio_os_process_start(&pbdrv_display_ev3_process, pbdrv_display_ev3_process_thread, NULL); +} + +pbio_image_t *pbdrv_display_get_image(void) { + return &display_image; +} + +uint8_t pbdrv_display_get_max_value(void) { + return 3; +} + +void pbdrv_display_update(void) { + pbdrv_display_user_frame_update_requested = true; + pbio_os_request_poll(); } #endif // PBDRV_CONFIG_DISPLAY_EV3 diff --git a/lib/pbio/include/pbdrv/display.h b/lib/pbio/include/pbdrv/display.h new file mode 100644 index 000000000..237ca4a25 --- /dev/null +++ b/lib/pbio/include/pbdrv/display.h @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2025 The Pybricks Authors + +/** + * @addtogroup DisplayDriver Driver: Display + * @{ + */ + +#ifndef _PBDRV_DISPLAY_H_ +#define _PBDRV_DISPLAY_H_ + +#include +#include + +#if PBDRV_CONFIG_DISPLAY + +/** + * Get an image container representing the display. + * @return Image container, or NULL if no display. + */ +pbio_image_t *pbdrv_display_get_image(void); + +/** + * Get the maximum value of a pixel. + * @return Maximum value, corresponding to black on a LCD screen. + */ +uint8_t pbdrv_display_get_max_value(void); + +/** + * Update the display to show current content of image container. + */ +void pbdrv_display_update(void); + +#else // PBDRV_CONFIG_DISPLAY + +static inline pbio_image_t *pbdrv_display_get_image(void) { + return NULL; +} + +static inline uint8_t pbdrv_display_get_max_value(void) { + return 0; +} + +static inline void pbdrv_display_update(void) { +} + +#endif // PBDRV_CONFIG_DISPLAY + +#endif // _PBDRV_DISPLAY_H_ + +/** @} */ diff --git a/lib/pbio/include/pbio/image.h b/lib/pbio/include/pbio/image.h new file mode 100644 index 000000000..6b9a7a649 --- /dev/null +++ b/lib/pbio/include/pbio/image.h @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2025 Nicolas Schodet + +/** + * @addtogroup Image Grayscale image manipulation and drawing. + * @{ + */ + +#ifndef _PBIO_IMAGE_H_ +#define _PBIO_IMAGE_H_ + +#include + +#if PBIO_CONFIG_IMAGE + +#include + +/** + * Image container. + * + * Images are oriented with the 0,0 coordinate corresponding to the top left + * corner. But this is only an arbitrary convention to write comments. + */ +typedef struct _pbio_image_t { + /** + * Start of pixel buffer storing the pixels values. + * + * Each pixel is stored using one byte and is expected to be a value + * between 0 and some number fitting in one byte. This code has no opinion + * on the maximum value as long as it fits. + * + * Rows are continuous in memory. + * + * This is not an owning type, buffer lifetime must be handled externally. + * It can actually point to memory shared with another image. + */ + uint8_t *pixels; + /** + * Width of the image, or number of columns. + */ + int width; + /** + * Height of the image, or number of rows. + */ + int height; + /** + * Distance in bytes inside the pixel buffer to go from one row to the + * next one. + * + * This is signed on purpose so that an image can be mirrored cheaply + * using negative value. + */ + int stride; +} pbio_image_t; + +void pbio_image_init(pbio_image_t *image, uint8_t *pixels, int width, + int height, int stride); + +void pbio_image_init_sub(pbio_image_t *image, const pbio_image_t *source, + int x, int y, int width, int height); + +void pbio_image_fill(pbio_image_t *image, uint8_t value); + +void pbio_image_draw_image(pbio_image_t *image, const pbio_image_t *source, + int x, int y); + +void pbio_image_draw_image_transparent(pbio_image_t *image, + const pbio_image_t *source, int x, int y, uint8_t value); + +void pbio_image_draw_pixel(pbio_image_t *image, int x, int y, uint8_t value); + +void pbio_image_draw_hline(pbio_image_t *image, int x, int y, int l, + uint8_t value); + +void pbio_image_draw_vline(pbio_image_t *image, int x, int y, int l, + uint8_t value); + +void pbio_image_draw_line(pbio_image_t *image, int x1, int y1, int x2, int y2, + uint8_t value); + +void pbio_image_draw_thick_line(pbio_image_t *image, int x1, int y1, int x2, + int y2, int thickness, uint8_t value); + +void pbio_image_draw_rect(pbio_image_t *image, int x, int y, int width, + int height, uint8_t value); + +void pbio_image_fill_rect(pbio_image_t *image, int x, int y, int width, + int height, uint8_t value); + +void pbio_image_draw_rounded_rect(pbio_image_t *image, int x, int y, + int width, int height, int r, uint8_t value); + +void pbio_image_fill_rounded_rect(pbio_image_t *image, int x, int y, + int width, int height, int r, uint8_t value); + +void pbio_image_draw_circle(pbio_image_t *image, int x, int y, int r, + uint8_t value); + +void pbio_image_fill_circle(pbio_image_t *image, int x, int y, int r, + uint8_t value); + +#endif // PBIO_CONFIG_IMAGE + +#endif // _PBIO_IMAGE_H_ + +/** @} */ diff --git a/lib/pbio/platform/ev3/pbioconfig.h b/lib/pbio/platform/ev3/pbioconfig.h index 360420119..3b9ae25d7 100644 --- a/lib/pbio/platform/ev3/pbioconfig.h +++ b/lib/pbio/platform/ev3/pbioconfig.h @@ -5,6 +5,7 @@ #define PBIO_CONFIG_DCMOTOR (1) #define PBIO_CONFIG_DCMOTOR_NUM_DEV (4) #define PBIO_CONFIG_DRIVEBASE_SPIKE (0) +#define PBIO_CONFIG_IMAGE (1) #define PBIO_CONFIG_IMU (0) #define PBIO_CONFIG_LIGHT (1) #define PBIO_CONFIG_LOGGER (1) diff --git a/lib/pbio/src/image/image.c b/lib/pbio/src/image/image.c new file mode 100644 index 000000000..d2d50a493 --- /dev/null +++ b/lib/pbio/src/image/image.c @@ -0,0 +1,554 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2025 Nicolas Schodet + +#include + +#if PBIO_CONFIG_IMAGE + +#include + +#include + +/** + * Clip range between 0 and given length, return if out. + * @param [in,out] c1 First coordinate, included. + * @param [in,out] c2 Second coordinate, excluded. + * @param [in] l Length. + * + * First coordinate must not be greater than the second one. + */ +#define clip_or_return(c1, c2, l) \ + do { \ + if ((c1) >= (l) || (c2) <= 0) { \ + return; \ + } \ + if ((c1) < 0) { \ + (c1) = 0; \ + } \ + if ((c2) > (l)) { \ + (c2) = (l); \ + } \ + } while (0) + +/** + * Initialize an image, using external storage. + * @param [out] image Uninitialized image to initialize. + * @param [in] pixels Buffer storing the pixels values, not changed. + * @param [in] width Number of columns. + * @param [in] height Number of rows. + * @param [in] stride Distance in bytes inside the pixel buffer to go from + * one row to the next one. + * + * No memory allocation is done. This function takes an uninitialized image + * structure. The pixel buffer is left untouched, pbio_image_fill() can be + * used to clear it. + */ +void pbio_image_init(pbio_image_t *image, uint8_t *pixels, int width, + int height, int stride) { + image->pixels = pixels; + image->width = width; + image->height = height; + image->stride = stride; +} + +/** + * Initialize an image as a viewport into another bigger image. + * @param [out] image Uninitialized image to initialize. + * @param [in] source Source image. + * @param [in] x X coordinate of the top-left point of the viewport in + * source. + * @param [in] y Y coordinate of the top-left point of the viewport in + * source. + * @param [in] width Width of the viewport. + * @param [in] height Height of the viewport. + * + * After this call, the two images shares the same buffer, modifying a pixel + * inside the viewport in one image will change the same pixel in the other + * image. + * + * Clipping: viewport is clipped to source image dimensions. If completely out + * of the source image, the destination image may be empty. + */ +void pbio_image_init_sub(pbio_image_t *image, const pbio_image_t *source, + int x, int y, int width, int height) { + // Start with an empty image in case of early return. + pbio_image_init(image, source->pixels, 0, 0, 0); + + // Eliminate weird cases. + if (width <= 0 || height <= 0) { + return; + } + + // Clipping, may result in an empty image. + int x2 = x + width; + int y2 = y + height; + clip_or_return(x, x2, source->width); + clip_or_return(y, y2, source->height); + + // Select the right part of source image. + pbio_image_init(image, source->pixels + y * source->stride + x, x2 - x, + y2 - y, source->stride); +} + +/** + * Fill an image with a value. + * @param [in,out] image Image to fill. + * @param [in] value Pixel value. + */ +void pbio_image_fill(pbio_image_t *image, uint8_t value) { + uint8_t *p = image->pixels; + for (int h = image->height; h; h--) { + memset(p, value, image->width); + p += image->stride; + } +} + +/** + * Draw an image inside another image. + * @param [in,out] image Destination image to draw into. + * @param [in] source Source image. + * @param [in] x X coordinate of the top-left point in destination + * image. + * @param [in] y Y coordinate of the top-left point in destination + * image. + * + * Source image pixels are copied into destination image. + * + * Clipping: drawing is clipped to destination image dimensions. + */ +void pbio_image_draw_image(pbio_image_t *image, const pbio_image_t *source, + int x, int y) { + // Clipping. + int ox = x; + int oy = y; + int x2 = x + source->width; + int y2 = y + source->height; + clip_or_return(x, x2, image->width); + clip_or_return(y, y2, image->height); + + // Copy pixels. + uint8_t *src = source->pixels + (y - oy) * source->stride + (x - ox); + uint8_t *dst = image->pixels + y * image->stride + x; + int w = x2 - x; + for (int h = y2 - y; h; h--) { + memcpy(dst, src, w); + dst += image->stride; + src += source->stride; + } +} + +/** + * Draw an image inside another image with transparency. + * @param [in,out] image Destination image to draw into. + * @param [in] source Source image. + * @param [in] x X coordinate of the top-left point in destination + * image. + * @param [in] y Y coordinate of the top-left point in destination + * image. + * @param [in] value Pixel value in source image considered transparent. + * + * Source image pixels are copied into destination image. When a source pixel + * matches the transparent value, the corresponding destination pixel is left + * untouched. + * + * Clipping: drawing is clipped to destination image dimensions. + */ +void pbio_image_draw_image_transparent(pbio_image_t *image, + const pbio_image_t *source, int x, int y, uint8_t value) { + // Clipping. + int ox = x; + int oy = y; + int x2 = x + source->width; + int y2 = y + source->height; + clip_or_return(x, x2, image->width); + clip_or_return(y, y2, image->height); + + // Draw pixels. + uint8_t *src = source->pixels + (y - oy) * source->stride + (x - ox); + uint8_t *dst = image->pixels + y * image->stride + x; + int w = x2 - x; + for (int h = y2 - y; h; h--) { + for (int i = w; i; i--) { + uint8_t c = *src; + if (c != value) { + *dst = c; + } + src++; + dst++; + } + dst += image->stride - w; + src += source->stride - w; + } +} + +/** + * Draw a single pixel. + * @param [in,out] image Image to draw into. + * @param [in] x X coordinate of the pixel. + * @param [in] y Y coordinate of the pixel. + * @param [in] value New pixel value. + * + * Clipping: pixel is not drawn if coordinate is outside of the image. + */ +void pbio_image_draw_pixel(pbio_image_t *image, int x, int y, uint8_t value) { + // Clipping. + if (x < 0 || x >= image->width || y < 0 || y >= image->height) { + return; + } + + // Draw pixel. + uint8_t *p = image->pixels + y * image->stride + x; + *p = value; +} + +/** + * Draw a horizontal line. + * @param [in,out] image Image to draw into. + * @param [in] x X coordinate of the leftmost pixel. + * @param [in] y Y coordinate of the line. + * @param [in] l Length of the line. + * @param [in] value Pixel value. + * + * Clipping: drawing is clipped to image dimensions. + */ +void pbio_image_draw_hline(pbio_image_t *image, int x, int y, int l, + uint8_t value) { + // Eliminate weird cases. + if (l <= 0) { + return; + } + + // Clipping. + int x2 = x + l; + clip_or_return(x, x2, image->width); + if (y < 0 || y >= image->height) { + return; + } + + // Draw line. + uint8_t *p = image->pixels + y * image->stride + x; + memset(p, value, x2 - x); +} + +/** + * Draw a vertical line. + * @param [in,out] image Image to draw into. + * @param [in] x X coordinate of the line. + * @param [in] y Y coordinate of the topmost pixel. + * @param [in] l Length of the line. + * @param [in] value Pixel value. + * + * Clipping: drawing is clipped to image dimensions. + */ +void pbio_image_draw_vline(pbio_image_t *image, int x, int y, int l, + uint8_t value) { + // Eliminate weird cases. + if (l <= 0) { + return; + } + + // Clipping. + int y2 = y + l; + if (x < 0 || x >= image->width) { + return; + } + clip_or_return(y, y2, image->height); + + // Draw line. + uint8_t *p = image->pixels + y * image->stride + x; + for (int h = y2 - y; h; h--) { + *p = value; + p += image->stride; + } +} + +/** + * Draw a line. + * @param [in,out] image Image to draw into. + * @param [in] x1 X coordinate of the first end. + * @param [in] y1 Y coordinate of the first end. + * @param [in] x2 X coordinate of the second end. + * @param [in] y2 Y coordinate of the second end. + * @param [in] value Pixel value. + * + * Clipping: drawing is clipped to image dimensions. + */ +void pbio_image_draw_line(pbio_image_t *image, int x1, int y1, int x2, int y2, + uint8_t value) { + // TODO +} + +/** + * Draw a thick line. + * @param [in,out] image Image to draw into. + * @param [in] x1 X coordinate of the first end. + * @param [in] y1 Y coordinate of the first end. + * @param [in] x2 X coordinate of the second end. + * @param [in] y2 Y coordinate of the second end. + * @param [in] thickness Line thickness. + * @param [in] value Pixel value. + * + * When line thickness is odd, pixels are centered on the line. When even, + * line is thicker on one side. + * + * Clipping: drawing is clipped to image dimensions. + */ +void pbio_image_draw_thick_line(pbio_image_t *image, int x1, int y1, int x2, + int y2, int thickness, uint8_t value) { + // Eliminate weird cases. + if (thickness <= 0) { + return; + } + // TODO +} + +/** + * Draw a rectangle. + * @param [in,out] image Image to draw into. + * @param [in] x X coordinate of the top-left corner. + * @param [in] y Y coordinate of the top-left corner. + * @param [in] width Rectangle width. + * @param [in] height Rectangle height. + * @param [in] value Pixel value. + * + * Clipping: drawing is clipped to image dimensions. + */ +void pbio_image_draw_rect(pbio_image_t *image, int x, int y, int width, + int height, uint8_t value) { + // Draw. + pbio_image_draw_hline(image, x, y, width, value); + if (height > 1) { + pbio_image_draw_hline(image, x, y + height - 1, width, value); + } + pbio_image_draw_vline(image, x, y + 1, height - 2, value); + if (width > 1) { + pbio_image_draw_vline(image, x + width - 1, y + 1, height - 2, value); + } +} + +/** + * Draw a filled rectangle. + * @param [in,out] image Image to draw into. + * @param [in] x X coordinate of the top-left corner. + * @param [in] y Y coordinate of the top-left corner. + * @param [in] width Rectangle width. + * @param [in] height Rectangle height. + * @param [in] value Pixel value. + * + * Clipping: drawing is clipped to image dimensions. + */ +void pbio_image_fill_rect(pbio_image_t *image, int x, int y, int width, + int height, uint8_t value) { + // Eliminate weird cases. + if (width <= 0 || height <= 0) { + return; + } + + // Clipping. + int x2 = x + width; + int y2 = y + height; + clip_or_return(x, x2, image->width); + clip_or_return(y, y2, image->height); + + // Draw. + uint8_t *p = image->pixels + y * image->stride + x; + for (int h = y2 - y; h; h--) { + memset(p, value, x2 - x); + p += image->stride; + } +} + +/** + * Draw a rounded rectangle. + * @param [in,out] image Image to draw into. + * @param [in] x X coordinate of the top-left corner. + * @param [in] y Y coordinate of the top-left corner. + * @param [in] width Rectangle width. + * @param [in] height Rectangle height. + * @param [in] r Corner radius. + * @param [in] value Pixel value. + * + * Clipping: drawing is clipped to image dimensions. + */ +void pbio_image_draw_rounded_rect(pbio_image_t *image, int x, int y, + int width, int height, int r, uint8_t value) { + // Eliminate weird cases. + if (width <= 0 || height <= 0) { + return; + } + if (r > (width - 1) / 2) { + r = (width - 1) / 2; + } + if (r > (height - 1) / 2) { + r = (height - 1) / 2; + } + + // Fall back to regular rectangle. + if (r < 1) { + pbio_image_draw_rect(image, x, y, width, height, value); + return; + } + + // Draw corners. + int dx = 0, dy = r; + int r2 = r * r; + int xr1 = x + r; + int yr1 = y + r; + int xr2 = x + width - 1 - r; + int yr2 = y + height - 1 - r; + while (dx < dy) { + dx++; + if (dx * dx + dy * dy > r2) { + dy--; + } + pbio_image_draw_pixel(image, xr1 - dx, yr1 - dy, value); + pbio_image_draw_pixel(image, xr1 - dy, yr1 - dx, value); + pbio_image_draw_pixel(image, xr2 + dx, yr1 - dy, value); + pbio_image_draw_pixel(image, xr2 + dy, yr1 - dx, value); + pbio_image_draw_pixel(image, xr1 - dx, yr2 + dy, value); + pbio_image_draw_pixel(image, xr1 - dy, yr2 + dx, value); + pbio_image_draw_pixel(image, xr2 + dx, yr2 + dy, value); + pbio_image_draw_pixel(image, xr2 + dy, yr2 + dx, value); + } + + // Draw sides. + int wr = width - 2 * r; + int hr = height - 2 * r; + pbio_image_draw_hline(image, xr1, y, wr, value); + pbio_image_draw_hline(image, xr1, y + height - 1, wr, value); + pbio_image_draw_vline(image, x, yr1, hr, value); + pbio_image_draw_vline(image, x + width - 1, yr1, hr, value); +} + +/** + * Draw a filled rounded rectangle. + * @param [in,out] image Image to draw into. + * @param [in] x X coordinate of the top-left corner. + * @param [in] y Y coordinate of the top-left corner. + * @param [in] width Rectangle width. + * @param [in] height Rectangle height. + * @param [in] r Corner radius. + * @param [in] value Pixel value. + * + * Clipping: drawing is clipped to image dimensions. + */ +void pbio_image_fill_rounded_rect(pbio_image_t *image, int x, int y, + int width, int height, int r, uint8_t value) { + // Eliminate weird cases. + if (width <= 0 || height <= 0) { + return; + } + if (r > (width - 1) / 2) { + r = (width - 1) / 2; + } + if (r > (height - 1) / 2) { + r = (height - 1) / 2; + } + + // Fall back to regular rectangle. + if (r < 1) { + pbio_image_fill_rect(image, x, y, width, height, value); + return; + } + + // Draw top and bottom. + int dx = 0, dy = r; + int r2 = r * r; + int xr1 = x + r; + int yr1 = y + r; + int yr2 = y + height - 1 - r; + int wr = width - 2 * r; + while (dx <= dy) { + // Optimization opportunity: when dy is not moving, the same pixels + // are drawn again and again. + pbio_image_draw_hline(image, xr1 - dx, yr1 - dy, wr + 2 * dx, value); + pbio_image_draw_hline(image, xr1 - dx, yr2 + dy, wr + 2 * dx, value); + if (dx != dy) { + pbio_image_draw_hline(image, xr1 - dy, yr1 - dx, wr + 2 * dy, + value); + pbio_image_draw_hline(image, xr1 - dy, yr2 + dx, wr + 2 * dy, + value); + } + dx++; + if (dx * dx + dy * dy > r2) { + dy--; + } + } + + // Draw middle section. + pbio_image_fill_rect(image, x, yr1 + 1, width, height - 2 - 2 * r, value); +} + +/** + * Draw a circle. + * @param [in,out] image Image to draw into. + * @param [in] x X coordinate of the circle center. + * @param [in] y Y coordinate of the circle center. + * @param [in] r Circle radius. + * @param [in] value Pixel value. + * + * Clipping: drawing is clipped to image dimensions. + */ +void pbio_image_draw_circle(pbio_image_t *image, int x, int y, int r, + uint8_t value) { + // Eliminate weird cases. + if (r <= 0) { + return; + } + + // Draw. + int dx = 0, dy = r; + int r2 = r * r; + while (dx <= dy) { + pbio_image_draw_pixel(image, x + dx, y + dy, value); + pbio_image_draw_pixel(image, x - dx, y + dy, value); + pbio_image_draw_pixel(image, x + dx, y - dy, value); + pbio_image_draw_pixel(image, x - dx, y - dy, value); + pbio_image_draw_pixel(image, x + dy, y + dx, value); + pbio_image_draw_pixel(image, x - dy, y + dx, value); + pbio_image_draw_pixel(image, x + dy, y - dx, value); + pbio_image_draw_pixel(image, x - dy, y - dx, value); + dx++; + if (dx * dx + dy * dy > r2) { + dy--; + } + } +} + +/** + * Draw a disc. + * @param [in,out] image Image to draw into. + * @param [in] x X coordinate of the disc center. + * @param [in] y Y coordinate of the disc center. + * @param [in] r Disc radius. + * @param [in] value Pixel value. + * + * Clipping: drawing is clipped to image dimensions. + */ +void pbio_image_fill_circle(pbio_image_t *image, int x, int y, int r, + uint8_t value) { + // Eliminate weird cases. + if (r <= 0) { + return; + } + + // Draw. + int dx = 0, dy = r; + int r2 = r * r; + while (dx <= dy) { + // Optimization opportunity: when dy is not moving, the same pixels + // are drawn again and again. + pbio_image_draw_hline(image, x - dx, y + dy, 1 + 2 * dx, value); + pbio_image_draw_hline(image, x - dx, y - dy, 1 + 2 * dx, value); + if (dx != dy) { + pbio_image_draw_hline(image, x - dy, y + dx, 1 + 2 * dy, value); + pbio_image_draw_hline(image, x - dy, y - dx, 1 + 2 * dy, value); + } + dx++; + if (dx * dx + dy * dy > r2) { + dy--; + } + } +} + +#endif // PBIO_CONFIG_IMAGE diff --git a/pybricks/hubs/pb_type_ev3brick.c b/pybricks/hubs/pb_type_ev3brick.c index 9d18dbc09..33feeb4d8 100644 --- a/pybricks/hubs/pb_type_ev3brick.c +++ b/pybricks/hubs/pb_type_ev3brick.c @@ -10,6 +10,7 @@ #include #include +#include #include #include @@ -18,6 +19,7 @@ typedef struct _hubs_EV3Brick_obj_t { mp_obj_t battery; mp_obj_t buttons; mp_obj_t light; + mp_obj_t screen; mp_obj_t system; } hubs_EV3Brick_obj_t; @@ -50,6 +52,7 @@ static mp_obj_t hubs_EV3Brick_make_new(const mp_obj_type_t *type, size_t n_args, self->battery = MP_OBJ_FROM_PTR(&pb_module_battery); self->buttons = pb_type_Keypad_obj_new(pb_type_ev3brick_button_pressed); self->light = common_ColorLight_internal_obj_new(pbsys_status_light_main); + self->screen = pb_type_Image_display_obj_new(); self->system = MP_OBJ_FROM_PTR(&pb_type_System); return MP_OBJ_FROM_PTR(self); @@ -59,6 +62,7 @@ static const pb_attr_dict_entry_t hubs_EV3Brick_attr_dict[] = { PB_DEFINE_CONST_ATTR_RO(MP_QSTR_battery, hubs_EV3Brick_obj_t, battery), PB_DEFINE_CONST_ATTR_RO(MP_QSTR_buttons, hubs_EV3Brick_obj_t, buttons), PB_DEFINE_CONST_ATTR_RO(MP_QSTR_light, hubs_EV3Brick_obj_t, light), + PB_DEFINE_CONST_ATTR_RO(MP_QSTR_screen, hubs_EV3Brick_obj_t, screen), PB_DEFINE_CONST_ATTR_RO(MP_QSTR_system, hubs_EV3Brick_obj_t, system), PB_ATTR_DICT_SENTINEL }; diff --git a/pybricks/media.h b/pybricks/media.h new file mode 100644 index 000000000..d4de16801 --- /dev/null +++ b/pybricks/media.h @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2025 The Pybricks Authors + +#ifndef PYBRICKS_INCLUDED_PYBRICKS_MEDIA_H +#define PYBRICKS_INCLUDED_PYBRICKS_MEDIA_H + +#include "py/mpconfig.h" + +#if PYBRICKS_PY_MEDIA + +#include "py/obj.h" + +#if PYBRICKS_PY_MEDIA_IMAGE + +extern const mp_obj_type_t pb_type_Image; + +mp_obj_t pb_type_Image_display_obj_new(void); + +#endif // PYBRICKS_PY_MEDIA_IMAGE + +#endif // PYBRICKS_PY_MEDIA + +#endif // PYBRICKS_INCLUDED_PYBRICKS_MEDIA_H diff --git a/pybricks/media/pb_module_media.c b/pybricks/media/pb_module_media.c index ee7656cdd..6d62ebdc0 100644 --- a/pybricks/media/pb_module_media.c +++ b/pybricks/media/pb_module_media.c @@ -8,10 +8,14 @@ #include "py/mphal.h" #include "py/runtime.h" +#include #include static const mp_rom_map_elem_t media_globals_table[] = { { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_media) }, + #if PYBRICKS_PY_MEDIA_IMAGE + { MP_ROM_QSTR(MP_QSTR_Image), MP_ROM_PTR(&pb_type_Image) }, + #endif }; static MP_DEFINE_CONST_DICT(pb_module_media_globals, media_globals_table); diff --git a/pybricks/media/pb_type_image.c b/pybricks/media/pb_type_image.c new file mode 100644 index 000000000..7473d3678 --- /dev/null +++ b/pybricks/media/pb_type_image.c @@ -0,0 +1,350 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2025 The Pybricks Authors + +#include "py/mpconfig.h" + +#if PYBRICKS_PY_MEDIA && PYBRICKS_PY_MEDIA_IMAGE + +#include "py/mphal.h" +#include "py/obj.h" + +#include +#include +#include +#include + +#include +#include + +#include + +extern const mp_obj_type_t pb_type_Image; + +// pybricks.media.Image class object +typedef struct _pb_type_Image_obj_t { + mp_obj_base_t base; + // TODO: avoid using MicroPython heap for large allocation, use a separate + // memory allocator and use a python object to handle memory release. + uint8_t *buffer; // m_malloc'd buffer, may be shared, NULL for display. + pbio_image_t image; + bool is_display; +} pb_type_Image_obj_t; + +static int get_color(mp_obj_t obj) { + uint8_t max = pbdrv_display_get_max_value(); + if (obj == mp_const_none) { + return max; + } + if (mp_obj_is_int(obj)) { + return mp_obj_get_int(obj); + } + const pbio_color_hsv_t *hsv = pb_type_Color_get_hsv(obj); + int32_t v = pbio_int_math_bind(hsv->v, 0, 100); + return max - v * max / 100; +} + +mp_obj_t pb_type_Image_display_obj_new(void) { + pb_type_Image_obj_t *self = mp_obj_malloc(pb_type_Image_obj_t, &pb_type_Image); + self->buffer = NULL; + self->is_display = true; + self->image = *pbdrv_display_get_image(); + + return MP_OBJ_FROM_PTR(self); +} + +static mp_obj_t pb_type_Image_make_new(const mp_obj_type_t *type, + size_t n_args, size_t n_kw, const mp_obj_t *args) { + PB_PARSE_ARGS_CLASS(n_args, n_kw, args, + PB_ARG_REQUIRED(source), + PB_ARG_DEFAULT_FALSE(sub), + PB_ARG_DEFAULT_INT(x1, 0), + PB_ARG_DEFAULT_INT(y1, 0), + PB_ARG_DEFAULT_NONE(x2), + PB_ARG_DEFAULT_NONE(y2)); + + pb_type_Image_obj_t *self = mp_obj_malloc(pb_type_Image_obj_t, type); + + if (mp_obj_is_type(source_in, &pb_type_Image)) { + pb_type_Image_obj_t *source = MP_OBJ_TO_PTR(source_in); + if (!mp_obj_is_true(sub_in)) { + // Copy. + int width = source->image.width; + int height = source->image.height; + // TODO: avoid using MicroPython heap for large allocation. + self->buffer = m_malloc(width * height * sizeof(uint8_t)); + self->is_display = false; + pbio_image_init(&self->image, self->buffer, width, height, width); + pbio_image_draw_image(&self->image, &source->image, 0, 0); + } else { + // Sub-image. + mp_int_t x1 = pb_obj_get_int(x1_in); + mp_int_t y1 = pb_obj_get_int(y1_in); + mp_int_t x2 = x2_in == mp_const_none ? source->image.width - 1 : pb_obj_get_int(x2_in); + mp_int_t y2 = y2_in == mp_const_none ? source->image.height - 1 : pb_obj_get_int(y2_in); + self->buffer = source->buffer; + self->is_display = false; + int width = x2 - x1 + 1; + int height = y2 - y1 + 1; + pbio_image_init_sub(&self->image, &source->image, x1, y1, width, height); + } + } else if (mp_obj_equal(source_in, MP_OBJ_NEW_QSTR(MP_QSTR__screen_))) { + // Screen. + self->buffer = NULL; + self->is_display = true; + self->image = *pbdrv_display_get_image(); + } else if (mp_obj_is_str(source_in)) { + // TODO: load from image "files". + mp_raise_NotImplementedError(MP_ERROR_TEXT("'_screen_' is the only supported source string")); + } else { + mp_raise_TypeError(MP_ERROR_TEXT("source must be Image or str")); + } + + return MP_OBJ_FROM_PTR(self); +} + +static mp_obj_t pb_type_Image_empty(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + PB_PARSE_ARGS_FUNCTION(n_args, pos_args, kw_args, + PB_ARG_DEFAULT_NONE(width), + PB_ARG_DEFAULT_NONE(height)); + + pbio_image_t *display = pbdrv_display_get_image(); + + mp_int_t width = width_in == mp_const_none ? display->width : mp_obj_get_int(width_in); + mp_int_t height = height_in == mp_const_none ? display->height : mp_obj_get_int(height_in); + + if (width < 1 || height < 1) { + mp_raise_ValueError(MP_ERROR_TEXT("Image width or height is less than 1")); + } + + pb_type_Image_obj_t *self = mp_obj_malloc(pb_type_Image_obj_t, &pb_type_Image); + + // TODO: avoid using MicroPython heap for large allocation. + self->buffer = m_malloc0(width * height * sizeof(uint8_t)); + self->is_display = false; + pbio_image_init(&self->image, self->buffer, width, height, width); + + return MP_OBJ_FROM_PTR(self); +} +static MP_DEFINE_CONST_FUN_OBJ_KW(pb_type_Image_empty_fun_obj, 0, pb_type_Image_empty); +static MP_DEFINE_CONST_STATICMETHOD_OBJ(pb_type_Image_empty_obj, MP_ROM_PTR(&pb_type_Image_empty_fun_obj)); + +static void pb_type_Image_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) { + // Read only + if (dest[0] == MP_OBJ_NULL) { + pb_type_Image_obj_t *self = MP_OBJ_TO_PTR(self_in); + if (attr == MP_QSTR_width) { + dest[0] = mp_obj_new_int(self->image.width); + return; + } + if (attr == MP_QSTR_height) { + dest[0] = mp_obj_new_int(self->image.height); + return; + } + } + // Attribute not found, continue lookup in locals dict. + dest[1] = MP_OBJ_SENTINEL; +} + +static mp_obj_t pb_type_Image_clear(mp_obj_t self_in) { + pb_type_Image_obj_t *self = MP_OBJ_TO_PTR(self_in); + + pbio_image_fill(&self->image, 0); + + if (self->is_display) { + pbdrv_display_update(); + } + + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_1(pb_type_Image_clear_obj, pb_type_Image_clear); + +static mp_obj_t pb_type_Image_load_image(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + PB_PARSE_ARGS_METHOD(n_args, pos_args, kw_args, + pb_type_Image_obj_t, self, + PB_ARG_REQUIRED(source)); + + pb_assert_type(source_in, &pb_type_Image); + pb_type_Image_obj_t *source = MP_OBJ_TO_PTR(source_in); + + int x = (self->image.width - source->image.width) / 2; + int y = (self->image.height - source->image.height) / 2; + + pbio_image_fill(&self->image, 0); + pbio_image_draw_image(&self->image, &source->image, x, y); + + if (self->is_display) { + pbdrv_display_update(); + } + + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_KW(pb_type_Image_load_image_obj, 1, pb_type_Image_load_image); + +static mp_obj_t pb_type_Image_draw_image(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + PB_PARSE_ARGS_METHOD(n_args, pos_args, kw_args, + pb_type_Image_obj_t, self, + PB_ARG_REQUIRED(x), + PB_ARG_REQUIRED(y), + PB_ARG_REQUIRED(source), + PB_ARG_DEFAULT_NONE(transparent)); + + mp_int_t x = mp_obj_get_int(x_in); + mp_int_t y = mp_obj_get_int(y_in); + pb_assert_type(source_in, &pb_type_Image); + pb_type_Image_obj_t *source = MP_OBJ_TO_PTR(source_in); + + if (transparent_in == mp_const_none) { + pbio_image_draw_image(&self->image, &source->image, x, y); + } else { + int transparent_value = get_color(transparent_in); + + pbio_image_draw_image_transparent(&self->image, &source->image, x, y, transparent_value); + } + + if (self->is_display) { + pbdrv_display_update(); + } + + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_KW(pb_type_Image_draw_image_obj, 1, pb_type_Image_draw_image); + +static mp_obj_t pb_type_Image_draw_pixel(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + PB_PARSE_ARGS_METHOD(n_args, pos_args, kw_args, + pb_type_Image_obj_t, self, + PB_ARG_REQUIRED(x), + PB_ARG_REQUIRED(y), + PB_ARG_DEFAULT_NONE(color)); + + mp_int_t x = pb_obj_get_int(x_in); + mp_int_t y = pb_obj_get_int(y_in); + int color = get_color(color_in); + + pbio_image_draw_pixel(&self->image, x, y, color); + + if (self->is_display) { + pbdrv_display_update(); + } + + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_KW(pb_type_Image_draw_pixel_obj, 1, pb_type_Image_draw_pixel); + +static mp_obj_t pb_type_Image_draw_line(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + PB_PARSE_ARGS_METHOD(n_args, pos_args, kw_args, + pb_type_Image_obj_t, self, + PB_ARG_REQUIRED(x1), + PB_ARG_REQUIRED(y1), + PB_ARG_REQUIRED(x2), + PB_ARG_REQUIRED(y2), + PB_ARG_DEFAULT_INT(width, 1), + PB_ARG_DEFAULT_NONE(color)); + + mp_int_t x1 = pb_obj_get_int(x1_in); + mp_int_t y1 = pb_obj_get_int(y1_in); + mp_int_t x2 = pb_obj_get_int(x2_in); + mp_int_t y2 = pb_obj_get_int(y2_in); + mp_int_t width = pb_obj_get_int(width_in); + int color = get_color(color_in); + + mp_raise_NotImplementedError(MP_ERROR_TEXT("draw_line is not implemented yet")); + if (width <= 1) { + pbio_image_draw_line(&self->image, x1, y1, x2, y2, color); + } else { + pbio_image_draw_thick_line(&self->image, x1, y1, x2, y2, width, color); + } + + if (self->is_display) { + pbdrv_display_update(); + } + + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_KW(pb_type_Image_draw_line_obj, 1, pb_type_Image_draw_line); + +static mp_obj_t pb_type_Image_draw_box(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + PB_PARSE_ARGS_METHOD(n_args, pos_args, kw_args, + pb_type_Image_obj_t, self, + PB_ARG_REQUIRED(x1), + PB_ARG_REQUIRED(y1), + PB_ARG_REQUIRED(x2), + PB_ARG_REQUIRED(y2), + PB_ARG_DEFAULT_INT(r, 0), + PB_ARG_DEFAULT_FALSE(fill), + PB_ARG_DEFAULT_NONE(color)); + + mp_int_t x1 = pb_obj_get_int(x1_in); + mp_int_t y1 = pb_obj_get_int(y1_in); + mp_int_t x2 = pb_obj_get_int(x2_in); + mp_int_t y2 = pb_obj_get_int(y2_in); + mp_int_t r = pb_obj_get_int(r_in); + bool fill = mp_obj_is_true(fill_in); + int color = get_color(color_in); + + int width = x2 - x1 + 1; + int height = y2 - y1 + 1; + if (fill) { + pbio_image_fill_rounded_rect(&self->image, x1, y1, width, height, r, color); + } else { + pbio_image_draw_rounded_rect(&self->image, x1, y1, width, height, r, color); + } + + if (self->is_display) { + pbdrv_display_update(); + } + + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_KW(pb_type_Image_draw_box_obj, 1, pb_type_Image_draw_box); + +static mp_obj_t pb_type_Image_draw_circle(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + PB_PARSE_ARGS_METHOD(n_args, pos_args, kw_args, + pb_type_Image_obj_t, self, + PB_ARG_REQUIRED(x), + PB_ARG_REQUIRED(y), + PB_ARG_REQUIRED(r), + PB_ARG_DEFAULT_FALSE(fill), + PB_ARG_DEFAULT_NONE(color)); + + mp_int_t x = pb_obj_get_int(x_in); + mp_int_t y = pb_obj_get_int(y_in); + mp_int_t r = pb_obj_get_int(r_in); + bool fill = mp_obj_is_true(fill_in); + int color = get_color(color_in); + + if (fill) { + pbio_image_fill_circle(&self->image, x, y, r, color); + } else { + pbio_image_draw_circle(&self->image, x, y, r, color); + } + + if (self->is_display) { + pbdrv_display_update(); + } + + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_KW(pb_type_Image_draw_circle_obj, 1, pb_type_Image_draw_circle); + +// dir(pybricks.media.Image) +static const mp_rom_map_elem_t pb_type_Image_locals_dict_table[] = { + { MP_ROM_QSTR(MP_QSTR_empty), MP_ROM_PTR(&pb_type_Image_empty_obj) }, + { MP_ROM_QSTR(MP_QSTR_clear), MP_ROM_PTR(&pb_type_Image_clear_obj) }, + { MP_ROM_QSTR(MP_QSTR_load_image), MP_ROM_PTR(&pb_type_Image_load_image_obj) }, + { MP_ROM_QSTR(MP_QSTR_draw_image), MP_ROM_PTR(&pb_type_Image_draw_image_obj) }, + { MP_ROM_QSTR(MP_QSTR_draw_pixel), MP_ROM_PTR(&pb_type_Image_draw_pixel_obj) }, + { MP_ROM_QSTR(MP_QSTR_draw_line), MP_ROM_PTR(&pb_type_Image_draw_line_obj) }, + { MP_ROM_QSTR(MP_QSTR_draw_box), MP_ROM_PTR(&pb_type_Image_draw_box_obj) }, + { MP_ROM_QSTR(MP_QSTR_draw_circle), MP_ROM_PTR(&pb_type_Image_draw_circle_obj) }, +}; +static MP_DEFINE_CONST_DICT(pb_type_Image_locals_dict, pb_type_Image_locals_dict_table); + +// type(pybricks.media.Image) +MP_DEFINE_CONST_OBJ_TYPE(pb_type_Image, + MP_QSTR_Image, + MP_TYPE_FLAG_NONE, + make_new, pb_type_Image_make_new, + attr, pb_type_Image_attr, + locals_dict, &pb_type_Image_locals_dict); + +#endif // PYBRICKS_PY_MEDIA && PYBRICKS_PY_MEDIA_IMAGE