diff --git a/.github/workflows/micropython.yml b/.github/workflows/micropython.yml new file mode 100644 index 0000000..833f8f4 --- /dev/null +++ b/.github/workflows/micropython.yml @@ -0,0 +1,129 @@ +name: MicroPython + +on: + push: + pull_request: + release: + types: [created] + +env: + MICROPYTHON_VERSION: feature/psram + MICROPYTHON_FLAVOUR: MichaelBell + PIMORONI_PICO_VERSION: feature/sdk-2.0.0 + +jobs: + build: + name: ${{ matrix.name }} (${{ matrix.board }} ${{ matrix.variant }}) + runs-on: ubuntu-20.04 + continue-on-error: true + strategy: + matrix: + include: + - name: pico_plus2_rp2350_psram + board: PIMORONI_PICO_PLUS2 + variant: PSRAM + + env: + # MicroPython version will be contained in github.event.release.tag_name for releases + RELEASE_FILE: ${{ matrix.name }}-${{ github.event.release.tag_name || github.sha }}-micropython + PIMORONI_PICO_DIR: "${{ github.workspace }}/pimoroni-pico" + USER_C_MODULES: "${{ github.workspace }}/src-${{ github.sha }}/modules/dvhstx.cmake" + TAG_OR_SHA: ${{ github.event.release.tag_name || github.sha }} + MICROPY_BOARD: ${{ matrix.board }} + MICROPY_BOARD_VARIANT: ${{ matrix.variant }} + MICROPY_BOARD_DIR: "${{ github.workspace }}/src-${{ github.sha }}/micropython/board/${{ matrix.BOARD }}" + BOARD_NAME: ${{ matrix.name }} + BUILD_TOOLS: src-${{ github.sha }}/ci/micropython.sh + + steps: + - name: "CCache: Restore saved cache" + uses: actions/cache@v4 + with: + path: /home/runner/.ccache + key: ccache-micropython-${{ matrix.name }}-${{ github.ref }}-${{ github.sha }} + restore-keys: | + ccache-micropython-${{ matrix.name }}-${{ github.ref }} + ccache-micropython-${{ matrix.name }}- + + - name: "Src: Checkout" + uses: actions/checkout@v4 + with: + submodules: true + path: src-${{ github.sha }} + + - name: "Pimoroni Pico: Checkout" + uses: actions/checkout@v4 + with: + repository: pimoroni/pimoroni-pico + ref: ${{env.PIMORONI_PICO_VERSION}} + submodules: true + path: pimoroni-pico + + - name: Install Arm GNU Toolchain (arm-none-eabi-gcc) + uses: carlosperate/arm-none-eabi-gcc-action@v1 + with: + release: '9-2020-q2' + + - name: "CCache: Install" + run: | + source $BUILD_TOOLS + apt_install_build_deps + + - name: "MicroPython: Checkout" + run: | + source $BUILD_TOOLS + micropython_clone + + - name: "Py_Decl: Checkout" + uses: actions/checkout@v4 + with: + repository: gadgetoid/py_decl + ref: v0.0.2 + path: py_decl + + - name: "dir2uf2: Checkout" + uses: actions/checkout@v4 + with: + repository: gadgetoid/dir2uf2 + ref: v0.0.7 + path: dir2uf2 + + - name: "MicroPython: Build MPY Cross" + run: | + source $BUILD_TOOLS + micropython_build_mpy_cross + + - name: "MicroPython: Configure" + shell: bash + run: | + source $BUILD_TOOLS + micropython_version + cmake_configure + + - name: "MicroPython: Build" + shell: bash + run: | + source $BUILD_TOOLS + cmake_build + + - name: "Py_Decl: Verify .uf2" + shell: bash + run: | + python3 py_decl/py_decl.py --to-json --verify build-${{ matrix.name }}/${{ env.RELEASE_FILE }}.uf2 + + - name: "Artifacts: Upload .uf2" + uses: actions/upload-artifact@v4 + with: + name: ${{ env.RELEASE_FILE }}.uf2 + path: build-${{ matrix.name }}/${{ env.RELEASE_FILE }}.uf2 + + - name: "Release: Upload .uf2" + if: github.event_name == 'release' + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + asset_path: build-${{ matrix.name }}/${{ env.RELEASE_FILE }}.uf2 + upload_url: ${{ github.event.release.upload_url }} + asset_name: ${{ env.RELEASE_FILE }}.uf2 + asset_content_type: application/octet-stream diff --git a/ci/micropython.sh b/ci/micropython.sh new file mode 100644 index 0000000..0a4b53a --- /dev/null +++ b/ci/micropython.sh @@ -0,0 +1,65 @@ +export TERM=${TERM:="xterm-256color"} + +function log_success { + echo -e "$(tput setaf 2)$1$(tput sgr0)" +} + +function log_inform { + echo -e "$(tput setaf 6)$1$(tput sgr0)" +} + +function log_warning { + echo -e "$(tput setaf 1)$1$(tput sgr0)" +} + +function micropython_clone { + log_inform "Using MicroPython $MICROPYTHON_VERSION" + git clone https://github.com/$MICROPYTHON_FLAVOUR/micropython + cd micropython + git checkout $MICROPYTHON_VERSION + git submodule update --init lib/pico-sdk + git submodule update --init lib/cyw43-driver + git submodule update --init lib/lwip + git submodule update --init lib/mbedtls + git submodule update --init lib/micropython-lib + git submodule update --init lib/tinyusb + git submodule update --init lib/btstack + cd ../ +} + +function micropython_build_mpy_cross { + cd micropython/mpy-cross + ccache --zero-stats || true + CROSS_COMPILE="ccache " make + ccache --show-stats || true + cd ../../ +} + +function apt_install_build_deps { + sudo apt update && sudo apt install ccache +} + +function micropython_version { + echo "MICROPY_GIT_TAG=$MICROPYTHON_VERSION, $BOARD_NAME $TAG_OR_SHA" >> $GITHUB_ENV + echo "MICROPY_GIT_HASH=$MICROPYTHON_VERSION-$TAG_OR_SHA" >> $GITHUB_ENV +} + +function cmake_configure { + cmake -S micropython/ports/rp2 -B build-$BOARD_NAME \ + -DPICO_BUILD_DOCS=0 \ + -DPICO_NO_COPRO_DIS=1 \ + -DUSER_C_MODULES=$USER_C_MODULES \ + -DMICROPY_BOARD_DIR=$MICROPY_BOARD_DIR \ + -DMICROPY_BOARD=$MICROPY_BOARD \ + -DMICROPY_BOARD_VARIANT=$MICROPY_BOARD_VARIANT \ + -DCMAKE_C_COMPILER_LAUNCHER=ccache \ + -DCMAKE_CXX_COMPILER_LAUNCHER=ccache +} + +function cmake_build { + ccache --zero-stats || true + cmake --build build-$BOARD_NAME -j 2 + ccache --show-stats || true + cd build-$BOARD_NAME + cp firmware.uf2 $RELEASE_FILE.uf2 +} \ No newline at end of file diff --git a/drivers/dvhstx/dvhstx.cpp b/drivers/dvhstx/dvhstx.cpp index d84fa79..6121673 100644 --- a/drivers/dvhstx/dvhstx.cpp +++ b/drivers/dvhstx/dvhstx.cpp @@ -25,11 +25,25 @@ extern "C" { using namespace pimoroni; +#ifdef MICROPY_BUILD_TYPE +#define FRAME_BUFFER_SIZE (640*360) +__attribute__((section(".uninitialized_data"))) static uint8_t frame_buffer_a[FRAME_BUFFER_SIZE]; +__attribute__((section(".uninitialized_data"))) static uint8_t frame_buffer_b[FRAME_BUFFER_SIZE]; +#endif + #include "font.h" // If changing the font, note this code will not handle glyphs wider than 13 pixels #define FONT (&intel_one_mono) +#ifdef MICROPY_BUILD_TYPE +extern "C" { +void dvhstx_debug(const char *fmt, ...); +} +#else +#define dvhstx_debug printf +#endif + static inline __attribute__((always_inline)) uint32_t render_char_line(int c, int y) { if (c < 0x20 || c > 0x7e) return 0; const lv_font_fmt_txt_glyph_dsc_t* g = &FONT->dsc->glyph_dsc[c - 0x20 + 1]; @@ -373,6 +387,7 @@ void __scratch_x("display") DVHSTX::text_dma_handler() { // ---------------------------------------------------------------------------- // Experimental clock config +#ifndef MICROPY_BUILD_TYPE static void __no_inline_not_in_flash_func(set_qmi_timing)() { // Make sure flash is deselected - QMI doesn't appear to have a busy flag(!) while ((ioqspi_hw->io[1].status & IO_QSPI_GPIO_QSPI_SS_STATUS_OUTTOPAD_BITS) != IO_QSPI_GPIO_QSPI_SS_STATUS_OUTTOPAD_BITS) @@ -384,8 +399,11 @@ static void __no_inline_not_in_flash_func(set_qmi_timing)() { volatile uint32_t* ptr = (volatile uint32_t*)0x14000000; (void) *ptr; } +#endif + +extern "C" void __no_inline_not_in_flash_func(display_setup_clock_preinit)() { + uint32_t intr_stash = save_and_disable_interrupts(); -void DVHSTX::display_setup_clock() { // Before messing with clock speeds ensure QSPI clock is nice and slow hw_write_masked(&qmi_hw->m[0].timing, 6, QMI_M0_TIMING_CLKDIV_BITS); @@ -396,12 +414,6 @@ void DVHSTX::display_setup_clock() { volatile uint32_t* ptr = (volatile uint32_t*)0x14000000; (void) *ptr; - const uint32_t dvi_clock_khz = timing_mode->bit_clk_khz >> 1; - uint vco_freq, post_div1, post_div2; - if (!check_sys_clock_khz(dvi_clock_khz, &vco_freq, &post_div1, &post_div2)) - panic("System clock of %u kHz cannot be exactly achieved", dvi_clock_khz); - const uint32_t freq = vco_freq / (post_div1 * post_div2); - // Before we touch PLLs, switch sys and ref cleanly away from their aux sources. hw_clear_bits(&clocks_hw->clk[clk_sys].ctrl, CLOCKS_CLK_SYS_CTRL_SRC_BITS); while (clocks_hw->clk[clk_sys].selected != 0x1) @@ -416,8 +428,7 @@ void DVHSTX::display_setup_clock() { clock_stop(clk_peri); clock_stop(clk_hstx); - // Set the sys PLL to the requested freq, set USB PLL to 528MHz - pll_init(pll_sys, PLL_COMMON_REFDIV, vco_freq, post_div1, post_div2); + // Set USB PLL to 528MHz pll_init(pll_usb, PLL_COMMON_REFDIV, 1584 * MHZ, 3, 1); const uint32_t usb_pll_freq = 528 * MHZ; @@ -448,14 +459,43 @@ void DVHSTX::display_setup_clock() { usb_pll_freq, USB_CLK_KHZ * KHZ); + // Now we are running fast set fast QSPI clock and read delay + // On MicroPython this is setup by main. +#ifndef MICROPY_BUILD_TYPE + set_qmi_timing(); +#endif + + restore_interrupts(intr_stash); +} + +#ifndef MICROPY_BUILD_TYPE +// Trigger clock setup early - on MicroPython this is done by a hook in main. +namespace { + class DV_preinit { + public: + DV_preinit() { + display_setup_clock_preinit(); + } + }; + DV_preinit dv_preinit __attribute__ ((init_priority (101))) ; +} +#endif + +void DVHSTX::display_setup_clock() { + const uint32_t dvi_clock_khz = timing_mode->bit_clk_khz >> 1; + uint vco_freq, post_div1, post_div2; + if (!check_sys_clock_khz(dvi_clock_khz, &vco_freq, &post_div1, &post_div2)) + panic("System clock of %u kHz cannot be exactly achieved", dvi_clock_khz); + const uint32_t freq = vco_freq / (post_div1 * post_div2); + + // Set the sys PLL to the requested freq + pll_init(pll_sys, PLL_COMMON_REFDIV, vco_freq, post_div1, post_div2); + // CLK HSTX = Requested freq clock_configure(clk_hstx, 0, CLOCKS_CLK_HSTX_CTRL_AUXSRC_VALUE_CLKSRC_PLL_SYS, freq, freq); - - // Now we are running fast set fast QSPI clock and read delay - set_qmi_timing(); } void DVHSTX::write_pixel(const Point &p, uint16_t colour) @@ -613,9 +653,13 @@ bool DVHSTX::init(uint16_t width, uint16_t height, Mode mode_) display = this; display_palette = get_palette(); + dvhstx_debug("Setup clock\n"); display_setup_clock(); +#ifndef MICROPY_BUILD_TYPE stdio_init_all(); +#endif + dvhstx_debug("Clock setup done\n"); v_inactive_total = timing_mode->v_front_porch + timing_mode->v_sync_width + timing_mode->v_back_porch; v_total_active_lines = v_inactive_total + timing_mode->v_active_lines; @@ -666,14 +710,24 @@ bool DVHSTX::init(uint16_t width, uint16_t height, Mode mode_) return false; } +#ifdef MICROPY_BUILD_TYPE + if (frame_width * frame_height * frame_bytes_per_pixel > sizeof(frame_buffer_a)) { + panic("Frame buffer too large"); + } + + frame_buffer_display = frame_buffer_a; + frame_buffer_back = frame_buffer_b; +#else frame_buffer_display = (uint8_t*)malloc(frame_width * frame_height * frame_bytes_per_pixel); frame_buffer_back = (uint8_t*)malloc(frame_width * frame_height * frame_bytes_per_pixel); +#endif memset(frame_buffer_display, 0, frame_width * frame_height * frame_bytes_per_pixel); memset(frame_buffer_back, 0, frame_width * frame_height * frame_bytes_per_pixel); memset(palette, 0, PALETTE_SIZE * sizeof(palette[0])); frame_buffer_display = frame_buffer_display; + dvhstx_debug("Frame buffers inited\n"); const bool is_text_mode = (mode == MODE_TEXT_MONO || mode == MODE_TEXT_RGB111); const int frame_pixel_words = (frame_width * h_repeat * line_bytes_per_pixel + 3) >> 2; @@ -815,6 +869,8 @@ bool DVHSTX::init(uint16_t width, uint16_t height, Mode mode_) gpio_set_drive_strength(i, GPIO_DRIVE_STRENGTH_4MA); } + dvhstx_debug("GPIO configured\n"); + // Always use the bottom channels dma_claim_mask((1 << NUM_CHANS) - 1); @@ -860,18 +916,24 @@ bool DVHSTX::init(uint16_t width, uint16_t height, Mode mode_) ); } - dma_hw->ints0 = (1 << NUM_CHANS) - 1; - dma_hw->inte0 = (1 << NUM_CHANS) - 1; - if (is_text_mode) irq_set_exclusive_handler(DMA_IRQ_0, dma_irq_handler_text); - else irq_set_exclusive_handler(DMA_IRQ_0, dma_irq_handler); - irq_set_enabled(DMA_IRQ_0, true); + dvhstx_debug("DMA channels claimed\n"); + + dma_hw->ints2 = (1 << NUM_CHANS) - 1; + dma_hw->inte2 = (1 << NUM_CHANS) - 1; + if (is_text_mode) irq_set_exclusive_handler(DMA_IRQ_2, dma_irq_handler_text); + else irq_set_exclusive_handler(DMA_IRQ_2, dma_irq_handler); + irq_set_enabled(DMA_IRQ_2, true); dma_channel_start(0); + dvhstx_debug("DVHSTX started\n"); + for (int i = 0; i < frame_height; ++i) { memset(&frame_buffer_display[i * frame_width * frame_bytes_per_pixel], i, frame_width * frame_bytes_per_pixel); } + dvhstx_debug("Frame buffer filled\n"); + return true; } diff --git a/micropython/board/PIMORONI_PICO_PLUS2/board.json b/micropython/board/PIMORONI_PICO_PLUS2/board.json new file mode 100644 index 0000000..2b9cbc9 --- /dev/null +++ b/micropython/board/PIMORONI_PICO_PLUS2/board.json @@ -0,0 +1,19 @@ +{ + "deploy": [ + "../deploy.md" + ], + "docs": "", + "features": [ + "Dual-core", + "External Flash", + "USB" + ], + "images": [ + "rp2-picos.jpg" + ], + "mcu": "rp2350", + "product": "Pico2", + "thumbnail": "", + "url": "https://shop.pimoroni.com/products/raspberry-pi-pico-plus2/", + "vendor": "Raspberry Pi" +} diff --git a/micropython/board/PIMORONI_PICO_PLUS2/manifest.py b/micropython/board/PIMORONI_PICO_PLUS2/manifest.py new file mode 100644 index 0000000..fb2a59b --- /dev/null +++ b/micropython/board/PIMORONI_PICO_PLUS2/manifest.py @@ -0,0 +1,3 @@ +include("$(PORT_DIR)/boards/manifest.py") + +include("../manifest_pico2.py") \ No newline at end of file diff --git a/micropython/board/PIMORONI_PICO_PLUS2/mpconfigboard.cmake b/micropython/board/PIMORONI_PICO_PLUS2/mpconfigboard.cmake new file mode 100644 index 0000000..1d8acc7 --- /dev/null +++ b/micropython/board/PIMORONI_PICO_PLUS2/mpconfigboard.cmake @@ -0,0 +1,9 @@ +# cmake file for Raspberry Pi Pico +set(PICO_BOARD "pimoroni_pico_plus2_rp2350") +set(PICO_PLATFORM "rp2350") +set(PICO_NUM_GPIOS 48) + +# Board specific version of the frozen manifest +set(MICROPY_FROZEN_MANIFEST ${MICROPY_BOARD_DIR}/manifest.py) + +set(MICROPY_C_HEAP_SIZE 32768) diff --git a/micropython/board/PIMORONI_PICO_PLUS2/mpconfigboard.h b/micropython/board/PIMORONI_PICO_PLUS2/mpconfigboard.h new file mode 100644 index 0000000..9a7b9c1 --- /dev/null +++ b/micropython/board/PIMORONI_PICO_PLUS2/mpconfigboard.h @@ -0,0 +1,11 @@ +// Board and hardware specific configuration +#ifndef MICROPY_HW_BOARD_NAME +// Might be defined by mpconfigvariant.cmake +#define MICROPY_HW_BOARD_NAME "Pimoroni Pico Plus 2" +#endif +#define MICROPY_HW_FLASH_STORAGE_BYTES (PICO_FLASH_SIZE_BYTES - (2 * 1024 * 1024)) + +#define MICROPY_HW_PSRAM_CS_PIN PIMORONI_PICO_PLUS2_PSRAM_CS_PIN + +extern void display_setup_clock_preinit(); +#define MICROPY_BOARD_STARTUP() display_setup_clock_preinit() \ No newline at end of file diff --git a/micropython/board/PIMORONI_PICO_PLUS2/mpconfigvariant.cmake b/micropython/board/PIMORONI_PICO_PLUS2/mpconfigvariant.cmake new file mode 100644 index 0000000..e69de29 diff --git a/micropython/board/PIMORONI_PICO_PLUS2/mpconfigvariant_PSRAM.cmake b/micropython/board/PIMORONI_PICO_PLUS2/mpconfigvariant_PSRAM.cmake new file mode 100644 index 0000000..9de86c6 --- /dev/null +++ b/micropython/board/PIMORONI_PICO_PLUS2/mpconfigvariant_PSRAM.cmake @@ -0,0 +1,6 @@ +# Override the MicroPython board name +list(APPEND MICROPY_DEF_BOARD + "MICROPY_HW_ENABLE_PSRAM=1" + "MICROPY_GC_SPLIT_HEAP=0" + "MICROPY_HW_BOARD_NAME=\"Pimoroni Pico Plus 2 (PSRAM)\"" +) diff --git a/micropython/board/PIMORONI_PICO_PLUS2/pins.csv b/micropython/board/PIMORONI_PICO_PLUS2/pins.csv new file mode 100644 index 0000000..9d58ee8 --- /dev/null +++ b/micropython/board/PIMORONI_PICO_PLUS2/pins.csv @@ -0,0 +1,47 @@ +GP0,GPIO0 +GP1,GPIO1 +GP2,GPIO2 +GP3,GPIO3 +GP4,GPIO4 +GP5,GPIO5 +GP6,GPIO6 +GP7,GPIO7 +GP8,GPIO8 +GP9,GPIO9 +GP10,GPIO10 +GP11,GPIO11 +GP12,GPIO12 +GP13,GPIO13 +GP14,GPIO14 +GP15,GPIO15 +GP16,GPIO16 +GP17,GPIO17 +GP18,GPIO18 +GP19,GPIO19 +GP20,GPIO20 +GP21,GPIO21 +GP22,GPIO22 +GP25,GPIO25 +GP26,GPIO26 +GP27,GPIO27 +GP28,GPIO28 +GP29,GPIO29 +GP30,GPIO30 +GP31,GPIO31 +GP32,GPIO32 +GP33,GPIO33 +GP34,GPIO34 +GP35,GPIO35 +GP36,GPIO36 +GP37,GPIO37 +GP38,GPIO38 +GP39,GPIO39 +GP40,GPIO40 +GP41,GPIO41 +GP42,GPIO42 +GP43,GPIO43 +GP44,GPIO44 +GP45,GPIO45 +GP46,GPIO46 +GP47,GPIO47 +LED,GPIO25 \ No newline at end of file diff --git a/micropython/board/manifest_pico2.py b/micropython/board/manifest_pico2.py new file mode 100644 index 0000000..df993ee --- /dev/null +++ b/micropython/board/manifest_pico2.py @@ -0,0 +1,6 @@ +MODULES_PY = "../../../pimoroni-pico/micropython/modules_py" + +freeze(MODULES_PY, "gfx_pack.py") + +freeze(MODULES_PY, "pimoroni.py") +freeze(MODULES_PY, "boot.py") diff --git a/micropython/examples/vector_clock.py b/micropython/examples/vector_clock.py new file mode 100644 index 0000000..aa3276b --- /dev/null +++ b/micropython/examples/vector_clock.py @@ -0,0 +1,120 @@ + +import time +import gc + +from dvhstx import DVHSTX, PEN_P8, PEN_RGB565 +from picovector import PicoVector, Polygon, RegularPolygon, Rectangle, ANTIALIAS_X4, ANTIALIAS_X16 + +display = DVHSTX(PEN_P8, 640, 360) + +vector = PicoVector(display) +vector.set_antialiasing(ANTIALIAS_X4) + +BG = display.create_pen(20, 20, 50) +RED = display.create_pen(200, 0, 0) +BLACK = display.create_pen(0, 0, 0) +GREY = display.create_pen(200, 200, 200) +WHITE = display.create_pen(255, 255, 255) + +""" +# Redefine colours for a Blue clock +RED = display.create_pen(200, 0, 0) +BLACK = display.create_pen(135, 159, 169) +GREY = display.create_pen(10, 40, 50) +WHITE = display.create_pen(14, 60, 76) +""" + +WIDTH, HEIGHT = display.get_bounds() + +hub = RegularPolygon(int(WIDTH / 2), int(HEIGHT / 2), 24, 5) + +face = RegularPolygon(int(WIDTH / 2), int(HEIGHT / 2), 48, int(HEIGHT / 2)) + +print(time.localtime()) + +last_second = None + +while True: + t_start = time.ticks_ms() + year, month, day, hour, minute, second, _, _ = time.localtime() + + if last_second == second: + continue + + last_second = second + + display.set_pen(0) + display.clear() + + display.set_pen(BLACK) + display.circle(int(WIDTH / 2), int(HEIGHT / 2), int(HEIGHT / 2)) + display.set_pen(WHITE) + display.circle(int(WIDTH / 2), int(HEIGHT / 2), int(HEIGHT / 2) - 4) + + display.set_pen(GREY) + + for a in range(60): + tick_mark = Rectangle(int(WIDTH / 2) - 3, 10, 6, int(HEIGHT / 48)) + vector.rotate(tick_mark, 360 / 60.0 * a, int(WIDTH / 2), int(HEIGHT / 2)) + vector.translate(tick_mark, 0, 2) + vector.draw(tick_mark) + + for a in range(12): + hour_mark = Rectangle(int(WIDTH / 2) - 5, 10, 10, int(HEIGHT / 10)) + vector.rotate(hour_mark, 360 / 12.0 * a, int(WIDTH / 2), int(HEIGHT / 2)) + vector.translate(hour_mark, 0, 2) + vector.draw(hour_mark) + + angle_second = second * 6 + second_hand_length = int(HEIGHT / 2) - int(HEIGHT / 8) + second_hand = Polygon((-2, -second_hand_length), (-2, int(HEIGHT / 8)), (2, int(HEIGHT / 8)), (2, -second_hand_length)) + vector.rotate(second_hand, angle_second, 0, 0) + vector.translate(second_hand, int(WIDTH / 2), int(HEIGHT / 2) + 5) + + angle_minute = minute * 6 + angle_minute += second / 10.0 + minute_hand_length = int(HEIGHT / 2) - int(HEIGHT / 24) + minute_hand = Polygon((-5, -minute_hand_length), (-10, int(HEIGHT / 16)), (10, int(HEIGHT / 16)), (5, -minute_hand_length)) + vector.rotate(minute_hand, angle_minute, 0, 0) + vector.translate(minute_hand, int(WIDTH / 2), int(HEIGHT / 2) + 5) + + angle_hour = (hour % 12) * 30 + angle_hour += minute / 2 + hour_hand_length = int(HEIGHT / 2) - int(HEIGHT / 8) + hour_hand = Polygon((-5, -hour_hand_length), (-10, int(HEIGHT / 16)), (10, int(HEIGHT / 16)), (5, -hour_hand_length)) + vector.rotate(hour_hand, angle_hour, 0, 0) + vector.translate(hour_hand, int(WIDTH / 2), int(HEIGHT / 2) + 5) + + display.set_pen(GREY) + + vector.draw(minute_hand) + vector.draw(hour_hand) + vector.draw(second_hand) + + display.set_pen(BLACK) + + for a in range(60): + tick_mark = Rectangle(int(WIDTH / 2) - 3, 10, 6, int(HEIGHT / 48)) + vector.rotate(tick_mark, 360 / 60.0 * a, int(WIDTH / 2), int(HEIGHT / 2)) + vector.draw(tick_mark) + + for a in range(12): + hour_mark = Rectangle(int(WIDTH / 2) - 5, 10, 10, int(HEIGHT / 10)) + vector.rotate(hour_mark, 360 / 12.0 * a, int(WIDTH / 2), int(HEIGHT / 2)) + vector.draw(hour_mark) + + vector.translate(minute_hand, 0, -5) + vector.translate(hour_hand, 0, -5) + vector.draw(minute_hand) + vector.draw(hour_hand) + + display.set_pen(RED) + vector.translate(second_hand, 0, -5) + vector.draw(second_hand) + vector.draw(hub) + + display.update() + gc.collect() + + t_end = time.ticks_ms() + print(f"Took {t_end - t_start}ms") \ No newline at end of file diff --git a/modules/dvhstx.cmake b/modules/dvhstx.cmake new file mode 100644 index 0000000..9286e5f --- /dev/null +++ b/modules/dvhstx.cmake @@ -0,0 +1,44 @@ +if(NOT DEFINED PIMORONI_PICO_PATH) +set(PIMORONI_PICO_PATH ../pimoroni-pico) +endif() +include(${CMAKE_CURRENT_LIST_DIR}/../pimoroni_pico_import.cmake) + +include_directories(${PIMORONI_PICO_PATH}/micropython) + +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/../") +list(APPEND CMAKE_MODULE_PATH "${PIMORONI_PICO_PATH}/micropython") +list(APPEND CMAKE_MODULE_PATH "${PIMORONI_PICO_PATH}/micropython/modules") + +# Allows us to find /pga/modules/c//micropython +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}") + +set(CMAKE_C_STANDARD 11) +set(CMAKE_CXX_STANDARD 17) + +# Essential +include(pimoroni_i2c/micropython) +include(pimoroni_bus/micropython) + +# Pico Graphics Essential +include(hershey_fonts/micropython) +include(bitmap_fonts/micropython) +include(picovector/micropython) +include(modules/picographics/micropython) + +# Pico Graphics Extra +include(jpegdec/micropython) +include(pngdec/micropython) +include(qrcode/micropython/micropython) + +# Sensors & Breakouts +include(micropython-common-breakouts) + +# Utility +include(adcfft/micropython) + +# Note: cppmem is *required* for C++ code to function on MicroPython +# it redirects `malloc` and `free` calls to MicroPython's heap +include(cppmem/micropython) + +# version.py, pimoroni.py and boot.py +include(modules_py/modules_py) diff --git a/modules/picographics/README.md b/modules/picographics/README.md new file mode 100644 index 0000000..608a118 --- /dev/null +++ b/modules/picographics/README.md @@ -0,0 +1,582 @@ +# Pico Graphics + +Pico Graphics is our unified graphics and display library for driving displays from your Pico in MicroPython. + +Pico Graphics replaces the individual drivers for displays- if you're been using breakout_colorlcd, ST7789 then you'll need to update your code! + +- [Setting up Pico Graphics](#setting-up-pico-graphics) + - [Supported Displays](#supported-displays) + - [Interstate75 and Interstate75W Display modes](#interstate75-and-interstate75w-display-modes) + - [Supported Graphics Modes (Pen Type)](#supported-graphics-modes-pen-type) + - [Supported Rotations](#supported-rotations) + - [Custom Pins](#custom-pins) + - [SPI / Parallel](#spi--parallel) + - [I2C](#i2c) +- [Function Reference](#function-reference) + - [General](#general) + - [Creating and Setting Pens](#creating-and-setting-pens) + - [RGB888, RGB565, RGB332, P8 and P4 modes](#rgb888-rgb565-rgb332-p8-and-p4-modes) + - [Monochrome Modes](#monochrome-modes) + - [Inky Frame](#inky-frame) + - [Controlling the Backlight](#controlling-the-backlight) + - [Clipping](#clipping) + - [Clear](#clear) + - [Update](#update) + - [Get Bounds](#get-bounds) + - [Text](#text) + - [Changing The Font](#changing-the-font) + - [Changing The Thickness](#changing-the-thickness) + - [Drawing Text](#drawing-text) + - [Basic Shapes](#basic-shapes) + - [Line](#line) + - [Circle](#circle) + - [Rectangle](#rectangle) + - [Triangle](#triangle) + - [Polygon](#polygon) + - [Pixels](#pixels) + - [Palette Management](#palette-management) + - [Utility Functions](#utility-functions) + - [Sprites](#sprites) + - [Loading Sprites](#loading-sprites) + - [Drawing Sprites](#drawing-sprites) + - [JPEG Files](#jpeg-files) + +## Setting up Pico Graphics + +You must construct an instance of PicoGraphics with your desired display: + +```python +from picographics import PicoGraphics, DISPLAY_LCD_160X80 + +display = PicoGraphics(display=DISPLAY_LCD_160X80) +``` + +Bear in mind that MicroPython has only 192K of RAM available- a 320x240 pixel display in RGB565 mode uses 150K! + +### Supported Displays + +* Pico Display - 240x135 SPI LCD - `DISPLAY_PICO_DISPLAY` +* Pico Display 2 - 320x240 SPI LCD - `DISPLAY_PICO_DISPLAY_2` +* Tufty 2040 - 320x240 Parallel LCD - `DISPLAY_TUFTY_2040` +* Pico Explorer - 240x240 SPI LCD - `DISPLAY_PICO_EXPLORER` +* Enviro Plus - 240x240 SPI LCD - `DISPLAY_ENVIRO_PLUS` +* 240x240 Round SPI LCD Breakout - `DISPLAY_ROUND_LCD_240X240` +* 240x240 Square SPI LCD Breakout - `DISPLAY_LCD_240X240` +* 160x80 SPI LCD Breakout - `DISPLAY_LCD_160X80` +* 128x128 I2C OLED - `DISPLAY_I2C_OLED_128X128` +* Pico Inky Pack / Badger 2040 / Badger 2040 W - 296x128 mono E ink - `DISPLAY_INKY_PACK` +* Inky Frame 5.7" - 600x448 7-colour E ink - `DISPLAY_INKY_FRAME` +* Inky Frame 4.0" - 640x400 7-colour E ink - `DISPLAY_INKY_FRAME_4` +* Inky Frame 7.3" - 800x480 7-colour E ink - `DISPLAY_INKY_FRAME_7` +* Pico GFX Pack - 128x64 mono LCD Matrix - `DISPLAY_GFX_PACK` +* Galactic Unicorn - 53x11 LED Matrix - `DISPLAY_GALACTIC_UNICORN` +* Interstate75 and 75W - HUB75 Matrix driver - `DISPLAY_INTERSTATE75_SIZEOFMATRIX` please read below! +* Cosmic Unicorn - 32x32 LED Matrix - `DISPLAY_COSMIC_UNICORN` + +#### Interstate75 and Interstate75W Display modes + +Both the Interstate75 and Interstate75W support lots of different sizes of HUB75 matrix displays. + +The available display settings are listed here: + +* 32 x 32 Matrix - `DISPLAY_INTERSTATE75_32X32` +* 64 x 32 Matrix - `DISPLAY_INTERSTATE75_64X32` +* 96 x 32 Matrix - `DISPLAY_INTERSTATE75_96X32` +* 128 x 32 Matrix - `DISPLAY_INTERSTATE75_128X32` +* 64 x 64 Matrix - `DISPLAY_INTERSTATE75_64X64` +* 128 x 64 Matrix - `DISPLAY_INTERSTATE75_128X64` +* 192 x 64 Matrix - `DISPLAY_INTERSTATE75_192X64` +* 256 x 64 Matrix - `DISPLAY_INTERSTATE75_256X64` + +### Supported Graphics Modes (Pen Type) + +* 1-bit - `PEN_1BIT` - mono, used for Pico Inky Pack and i2c OLED +* 3-bit - `PEN_3BIT` - 8-colour, used for Inky Frame +* 4-bit - `PEN_P4` - 16-colour palette of your choice +* 8-bit - `PEN_P8` - 256-colour palette of your choice +* 8-bit RGB332 - `PEN_RGB332` - 256 fixed colours (3 bits red, 3 bits green, 2 bits blue) +* 16-bit RGB565 - `PEN_RGB565` - 64K colours at the cost of RAM. (5 bits red, 6 bits green, 5 bits blue) +* 24-bit RGB888 - `PEN_RGB888` - 16M colours at the cost of lots of RAM. (8 bits red, 8 bits green, 8 bits blue) + +These offer a tradeoff between RAM usage and available colours. In most cases you would probably use `RGB332` since it offers the easiest tradeoff. It's also the default for colour LCDs. + +Eg: + +```python +display = PicoGraphics(display=PICO_DISPLAY) +``` + +Is equivalent to: + +```python +display = PicoGraphics(display=PICO_DISPLAY, pen_type=PEN_RGB332) +``` + +### Supported Rotations + +All SPI LCDs support 0, 90, 180 and 270 degree rotations. + +Eg: + +```python +display = PicoGraphics(display=PICO_DISPLAY, rotate=90) +``` + +### Custom Pins + +#### SPI / Parallel + +The `pimoroni_bus` library includes `SPIBus` for SPI LCDs and `ParallelBus` for Parallel LCDs (like Tufty 2040). + +In most cases you'll never need to use these, but they come in useful if you're wiring breakouts to your Pico or using multiple LCDs. + +A custom SPI bus: + +```python +from pimoroni_bus import SPIBus +from picographics import PicoGraphics, DISPLAY_PICO_EXPLORER, PEN_RGB332 + +spibus = SPIBus(cs=17, dc=16, sck=18, mosi=19) + +display = PicoGraphics(display=DISPLAY_PICO_EXPLORER, bus=spibus, pen_type=PEN_RGB332) +``` + +#### I2C + +The `pimoroni_i2c` library includes `PimoroniI2C` which can be used to change the pins used by the mono OLED: + +```python +from pimoroni_i2c import PimoroniI2C +from picographics import PicoGraphics, DISPLAY_I2C_OLED_128X128 + +i2cbus = PimoroniI2C(4, 5) + +display = PicoGraphics(display=DISPLAY_I2C_OLED_128X128, bus=i2cbus) +``` + +## Function Reference + +### General + +#### Creating and Setting Pens + +##### RGB888, RGB565, RGB332, P8 and P4 modes + +Create a pen colour for drawing into a screen: + +```python +my_pen = display.create_pen(r, g, b) +``` + +In RGB565 and RGB332 modes this packs the given RGB into an integer representing a colour in these formats and returns the result. + +In P4 and P8 modes this will consume one palette entry, or return an error if your palette is full. Palette colours are stored as RGB and converted when they are displayed on screen. + +You can also now specify an HSV pen, which allows a pen to be created from HSV (Hue, Saturation, Value) values between 0.0 and 1.0, avoiding the need to calculate the RGB result in Python. + +```python +display.create_pen_hsv(h, s, v) +``` + +To tell PicoGraphics which pen to use: + +```python +display.set_pen(my_pen) +``` + +This will be either an RGB332, RGB565 or RGB888 colour, or a palette index. + +##### Monochrome Modes + +For 1BIT mode - such as for Inky Pack and the Mono OLED - pens are handled a little differently. + +There's no need to create one, since mapping an RGB colour to black/white is meaningless. + +Instead you can pick from 16 shades of grey which are automatically dithered into the PicoGraphics buffer, where: + +* `0` is Black, +* `1 - 14` are shades of grey, +* `15` is white. + +And just call `set_pen` with your desired shade: + +```python +display.set_pen(0) # Black +display.set_pen(15) # White +``` + +Because shades 1 through 14 are created with ordered dither you should avoid using them for text, small details or lines. + +Dithering works by mixing black and white pixels in various patterns and quantities to fake grey shades. + +If you were to try and draw a single "grey" pixel it will end up either black or white depending on where it's drawn and which shade of grey you pick. + +##### Inky Frame + +Inky Frame is a special case- the display itself supports only 7 (8 if you include its cleaning "clear" colour, which we call Taupe) colours. + +These are: + +* `BLACK` = 0 +* `WHITE` = 1 +* `GREEN` = 2 +* `BLUE` = 3 +* `RED` = 4 +* `YELLOW` = 5 +* `ORANGE` = 6 +* `TAUPE` = 7 + +#### Controlling the Backlight + +You can set the display backlight brightness between `0.0` and `1.0`: + +```python +display.set_backlight(0.5) +``` + +#### Clipping + +Set the clipping bounds for drawing: + +```python +display.set_clip(x, y, w, h) +``` + +Remove the clipping bounds: + +```python +display.remove_clip() +``` + +#### Clear + +Clear the display to the current pen colour: + +```python +display.clear() +``` + +This is equivalent to: + +```python +w, h = display.get_bounds() +display.rectangle(0, 0, w, h) +``` + +You can clear portions of the screen with rectangles to save time redrawing things like JPEGs or complex graphics. + +#### Update + +Send the contents of your Pico Graphics buffer to your screen: + +```python +display.update() +``` + +If you are using a Galactic Unicorn, then the process for updating the display is different. Instead of the above, do: + +```python +galactic_unicorn.update(display) +``` + +#### Get Bounds + +You can use `get_bounds()` to get the width and height of the display - useful for writing code that's portable across different displays. + +```python +WIDTH, HEIGHT = display.get_bounds() +``` + +### Text + +#### Changing The Font + +Change the font: + +```python +display.set_font(font) +``` + +Bitmap fonts. +These are aligned from their top-left corner. + +* `bitmap6` +* `bitmap8` +* `bitmap14_outline` + +Vector (Hershey) fonts. +These are aligned horizontally (x) to their left edge, but vertically (y) to their midline excluding descenders [i.e., aligned at top edge of lower case letter m]. At `scale=1`, the top edge of upper case letters is 10 pixels above the specified `y`, text baseline is 10 pixels below the specified `y`, and descenders go down to 20 pixels below the specified `y`. + +* `sans` +* `gothic` +* `cursive` +* `serif_italic` +* `serif` + +#### Changing The Thickness + +Vector (Hershey) fonts are drawn with individual lines. By default these are 1px thick, making for very thin and typically illegible text. + +To change the thickness of lines used for Vector fonts, use the `set_thickness` method: + +```python +display.set_thickness(n) +``` + +Drawing thick text involves setting a lot more pixels and may slow your drawing down considerably. Be careful how and where you use this. + +#### Drawing Text + +Write some text: + +```python +display.text(text, x, y, wordwrap, scale, angle, spacing) +``` + +* `text` - the text string to draw +* `x` - the destination X coordinate +* `y` - the destination Y coordinate +* `wordwrap` - number of pixels width before trying to break text into multiple lines +* `scale` - size +* `angle` - rotation angle (Vector only!) +* `spacing` - letter spacing + +Text scale can be a whole number (integer) for Bitmap fonts, or a decimal (float) for Vector (Hershey) fonts. + +For example: + +```python +display.set_font("bitmap8") +display.text("Hello World", 0, 0, scale=2) +``` +Draws "Hello World" in a 16px tall, 2x scaled version of the `bitmap8` font. + +Sometimes you might want to measure a text string for centering or alignment on screen, you can do this with: + +```python +width = display.measure_text(text, scale, spacing) +``` + +The height of each Bitmap font is explicit in its name. + +Write a single character: + +```python +display.character(char, x, y, scale) +``` + +Specify `char` using a [decimal ASCII code](https://www.ascii-code.com/). Note not all characters are supported. + +For example: +```python +display.set_font("bitmap8") +display.character(38, 0, 0, scale=2) +``` +Draws an ampersand in a 16px tall, 2x scaled version of the 'bitmap8' font. + +### Basic Shapes + +#### Line + +To draw a straight line at any angle between two specified points: + +```python +display.line(x1, y1, x2, y2) +``` + +The X1/Y1 and X2/Y2 coordinates describe the start and end of the line respectively. + +If you need a thicker line, for an outline or UI elements you can supply a fifth parameter - thickness - like so: + +```python +display.line(x1, y1, x2, y2, thickness) +``` + +#### Circle + +To draw a circle: + +```python +display.circle(x, y, r) +``` + +* `x` - the destination X coordinate +* `y` - the destination Y coordinate +* `r` - the radius + +The X/Y coordinates describe the center of your circle. + +#### Rectangle + +```python +display.rectangle(x, y, w, h) +``` + +* `x` - the destination X coordinate +* `y` - the destination Y coordinate +* `w` - the width +* `h` - the height + +#### Triangle + +```python +display.triangle(x1, y1, x2, y2, x3, y3) +``` + +The three pairs of X/Y coordinates describe each point of the triangle. + +#### Polygon + +To draw other shapes, you can provide a list of points to `polygon`: + +```python +display.polygon([ + (0, 10), + (20, 10), + (20, 0), + (30, 20), + (20, 30), + (20, 20), + (0, 20), +]) +``` + +### Pixels + +Setting individual pixels is slow, but you can do it with: + +```python +display.pixel(x, y) +``` + +You can draw a horizontal span of pixels a little faster with: + +```python +display.pixel_span(x, y, length) +``` + +(use `display.line()` instead if you want to draw a straight line at any angle) + +### Palette Management + +Intended for P4 and P8 modes. + +You have a 16-color and 256-color palette respectively. + +Set n elements in the palette from a list of RGB tuples: + +```python +display.set_palette([ + (r, g, b), + (r, g, b), + (r, g, b) +]) +``` + +Update an entry in the P4 or P8 palette with the given colour. + +```python +display.update_pen(index, r, g, b) +``` + +This is stored internally as RGB and converted to whatever format your screen requires when displayed. + +Reset a pen back to its default value (black, marked unused): + +```python +display.reset_pen(index) +``` + +#### Utility Functions + +Sometimes it can be useful to convert between colour formats: + +* `RGB332_to_RGB` +* `RGB_to_RGB332` +* `RGB565_to_RGB` +* `RGB_to_RGB565` + + +### Sprites + +Pico Display has very limited support for sprites in RGB332 mode. + +Sprites must be 8x8 pixels arranged in a 128x128 pixel spritesheet. 1-bit transparency is handled by electing a single colour to skip over. + +We've prepared some RGB332-compatible sprite assets for you, but you can use `spritesheet-to-rgb332.py ` to convert your own. + +#### Loading Sprites + +You'll need to include the [pen_type](#supported-graphics-modes-pen-type) in the import statement, and define the pen_type before using loading the spritesheet: + +``` python +from picographics import PicoGraphics, PEN_RGB565, PEN_RGB332 + +display = PicoGraphics(display=PICO_DISPLAY, pen_type=PEN_RGB332) +``` +Use Thonny to upload your `spritesheet.rgb332` file onto your Pico. Then load it into Pico Graphics: + +```python +display.load_spritesheet("s4m_ur4i-dingbads.rgb332") +``` + +and then update the display, to show the sprite: + +```python +display.update() +``` + +#### Drawing Sprites + +And finally display a sprite: + +```python +display.sprite(0, 0, 0, 0) +``` + +These arguments for `sprite` are as follows: + +1. Sprite X position (from 0-15) - this selects the horizontal location of an 8x8 sprite from your 128x128 pixel spritesheet. +2. Sprite Y position (from 0-15) +3. Destination X - where to draw on your screen horizontally +4. Destination Y = where to draw on your screen vertically +5. Scale (optional) - an integer scale value, 1 = 8x8, 2 = 16x16 etc. +6. Transparent (optional) - specify a colour to treat as transparent + +### JPEG Files + +We've included BitBank's JPEGDEC - https://github.com/bitbank2/JPEGDEC - so you can display JPEG files on your LCDs. + +Eg: + +```python +import picographics +import jpegdec + +display = picographics.PicoGraphics(display=picographics.DISPLAY_PICO_EXPLORER) + +# Create a new JPEG decoder for our PicoGraphics +j = jpegdec.JPEG(display) + +# Open the JPEG file +j.open_file("filename.jpeg") + +# Decode the JPEG +j.decode(0, 0, jpegdec.JPEG_SCALE_FULL, dither=True) + +# Display the result +display.update() +``` + +JPEG files must be small enough to load into RAM for decoding, and must *not* be progressive. + +JPEG files will be automatically dithered in RGB332 mode. + +In P4 and P8 modes JPEGs are dithered to your custom colour palette. Their appearance of an image will vary based on the colours you choose. + +The arguments for `decode` are as follows: + +1. Decode X - where to place the decoded JPEG on screen +2. Decode Y +3. Flags - one of `JPEG_SCALE_FULL`, `JPEG_SCALE_HALF`, `JPEG_SCALE_QUARTER` or `JPEG_SCALE_EIGHTH` +4. If you want to turn off dither altogether, try `dither=False`. This is useful if you want to [pre-dither your images](https://ditherit.com/) or for artsy posterization effects. diff --git a/modules/picographics/micropython.cmake b/modules/picographics/micropython.cmake new file mode 100644 index 0000000..b67b2db --- /dev/null +++ b/modules/picographics/micropython.cmake @@ -0,0 +1,48 @@ +set(MOD_NAME picographics) +string(TOUPPER ${MOD_NAME} MOD_NAME_UPPER) +add_library(usermod_${MOD_NAME} INTERFACE) + +get_filename_component(PICOVISION_PATH ${CMAKE_CURRENT_LIST_DIR}/../.. ABSOLUTE) + +target_sources(usermod_${MOD_NAME} INTERFACE + ${CMAKE_CURRENT_LIST_DIR}/${MOD_NAME}.c + ${CMAKE_CURRENT_LIST_DIR}/${MOD_NAME}.cpp + ${PICOVISION_PATH}/drivers/dvhstx/dvhstx.cpp + ${PICOVISION_PATH}/drivers/dvhstx/dvi.cpp + ${PICOVISION_PATH}/drivers/dvhstx/intel_one_mono_2bpp.c + ${PIMORONI_PICO_PATH}/libraries/pico_graphics/pico_graphics.cpp + ${PICOVISION_PATH}/libraries/pico_graphics/pico_graphics_pen_dvhstx_rgb565.cpp + ${PICOVISION_PATH}/libraries/pico_graphics/pico_graphics_pen_dvhstx_p8.cpp + ${PIMORONI_PICO_PATH}/libraries/pico_graphics/types.cpp +) + +# MicroPython compiles with -Os by default, these functions are critical path enough that -O2 is worth it (note -O3 is slower in this case) +set_source_files_properties(${PICOVISION_PATH}/drivers/dvhstx/dvhstx.cpp PROPERTIES COMPILE_OPTIONS "-O2") + + +target_include_directories(usermod_${MOD_NAME} INTERFACE + ${CMAKE_CURRENT_LIST_DIR} + ${PICOVISION_PATH} + ${PICOVISION_PATH}/drivers/dvhstx + ${PICOVISION_PATH}/libraries/pico_graphics # for pico_graphics_dv.hpp + ${PIMORONI_PICO_PATH}/libraries/pico_graphics # for pico_graphics.hpp +# ${PIMORONI_PICO_PATH}/libraries/pngdec +) + +target_compile_definitions(usermod_${MOD_NAME} INTERFACE + -DMODULE_${MOD_NAME_UPPER}_ENABLED=1 +) + +if (SUPPORT_WIDE_MODES) +target_compile_definitions(usermod_${MOD_NAME} INTERFACE + -DSUPPORT_WIDE_MODES=1 +) +endif() + +target_link_libraries(usermod INTERFACE usermod_${MOD_NAME} hardware_vreg) + +set_source_files_properties( + ${CMAKE_CURRENT_LIST_DIR}/${MOD_NAME}.c + PROPERTIES COMPILE_FLAGS + "-Wno-discarded-qualifiers" +) diff --git a/modules/picographics/picographics.c b/modules/picographics/picographics.c new file mode 100644 index 0000000..2fcfb69 --- /dev/null +++ b/modules/picographics/picographics.c @@ -0,0 +1,147 @@ +#include "picographics.h" + +// Class Methods +MP_DEFINE_CONST_FUN_OBJ_1(ModPicoGraphics_update_obj, ModPicoGraphics_update); + +// Palette management +MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(ModPicoGraphics_update_pen_obj, 5, 5, ModPicoGraphics_update_pen); +MP_DEFINE_CONST_FUN_OBJ_2(ModPicoGraphics_reset_pen_obj, ModPicoGraphics_reset_pen); +MP_DEFINE_CONST_FUN_OBJ_KW(ModPicoGraphics_set_palette_obj, 2, ModPicoGraphics_set_palette); + +// Pen +MP_DEFINE_CONST_FUN_OBJ_2(ModPicoGraphics_set_pen_obj, ModPicoGraphics_set_pen); +MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(ModPicoGraphics_create_pen_obj, 4, 4, ModPicoGraphics_create_pen); +MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(ModPicoGraphics_create_pen_hsv_obj, 4, 4, ModPicoGraphics_create_pen_hsv); +MP_DEFINE_CONST_FUN_OBJ_2(ModPicoGraphics_set_thickness_obj, ModPicoGraphics_set_thickness); + +// Alpha Blending +MP_DEFINE_CONST_FUN_OBJ_2(ModPicoGraphics_set_bg_obj, ModPicoGraphics_set_bg); +MP_DEFINE_CONST_FUN_OBJ_2(ModPicoGraphics_set_blend_mode_obj, ModPicoGraphics_set_blend_mode); + +// Depth +MP_DEFINE_CONST_FUN_OBJ_2(ModPicoGraphics_set_depth_obj, ModPicoGraphics_set_depth); + +// Primitives +MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(ModPicoGraphics_set_clip_obj, 5, 5, ModPicoGraphics_set_clip); +MP_DEFINE_CONST_FUN_OBJ_1(ModPicoGraphics_remove_clip_obj, ModPicoGraphics_remove_clip); +MP_DEFINE_CONST_FUN_OBJ_1(ModPicoGraphics_clear_obj, ModPicoGraphics_clear); +MP_DEFINE_CONST_FUN_OBJ_3(ModPicoGraphics_pixel_obj, ModPicoGraphics_pixel); +MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(ModPicoGraphics_pixel_span_obj, 4, 4, ModPicoGraphics_pixel_span); +MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(ModPicoGraphics_rectangle_obj, 5, 5, ModPicoGraphics_rectangle); +MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(ModPicoGraphics_circle_obj, 4, 4, ModPicoGraphics_circle); +MP_DEFINE_CONST_FUN_OBJ_KW(ModPicoGraphics_character_obj, 1, ModPicoGraphics_character); +MP_DEFINE_CONST_FUN_OBJ_KW(ModPicoGraphics_text_obj, 1, ModPicoGraphics_text); +MP_DEFINE_CONST_FUN_OBJ_KW(ModPicoGraphics_measure_text_obj, 1, ModPicoGraphics_measure_text); +MP_DEFINE_CONST_FUN_OBJ_KW(ModPicoGraphics_polygon_obj, 2, ModPicoGraphics_polygon); +MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(ModPicoGraphics_triangle_obj, 7, 7, ModPicoGraphics_triangle); +MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(ModPicoGraphics_line_obj, 5, 6, ModPicoGraphics_line); + +// Sprites +MP_DEFINE_CONST_FUN_OBJ_KW(ModPicoGraphics_load_sprite_obj, 2, ModPicoGraphics_load_sprite); +MP_DEFINE_CONST_FUN_OBJ_KW(ModPicoGraphics_display_sprite_obj, 5, ModPicoGraphics_display_sprite); +MP_DEFINE_CONST_FUN_OBJ_2(ModPicoGraphics_clear_sprite_obj, ModPicoGraphics_clear_sprite); + +// Utility +MP_DEFINE_CONST_FUN_OBJ_1(ModPicoGraphics_get_bounds_obj, ModPicoGraphics_get_bounds); +MP_DEFINE_CONST_FUN_OBJ_2(ModPicoGraphics_set_font_obj, ModPicoGraphics_set_font); + +MP_DEFINE_CONST_FUN_OBJ_1(ModPicoGraphics__del__obj, ModPicoGraphics__del__); + +// Loop +MP_DEFINE_CONST_FUN_OBJ_3(ModPicoGraphics_loop_obj, ModPicoGraphics_loop); + + +static const mp_rom_map_elem_t ModPicoGraphics_locals_dict_table[] = { + { MP_ROM_QSTR(MP_QSTR_pixel), MP_ROM_PTR(&ModPicoGraphics_pixel_obj) }, + { MP_ROM_QSTR(MP_QSTR_set_pen), MP_ROM_PTR(&ModPicoGraphics_set_pen_obj) }, + { MP_ROM_QSTR(MP_QSTR_set_thickness), MP_ROM_PTR(&ModPicoGraphics_set_thickness_obj) }, + { MP_ROM_QSTR(MP_QSTR_clear), MP_ROM_PTR(&ModPicoGraphics_clear_obj) }, + + { MP_ROM_QSTR(MP_QSTR_set_bg), MP_ROM_PTR(&ModPicoGraphics_set_bg_obj) }, + { MP_ROM_QSTR(MP_QSTR_set_blend_mode), MP_ROM_PTR(&ModPicoGraphics_set_blend_mode_obj) }, + + { MP_ROM_QSTR(MP_QSTR_set_depth), MP_ROM_PTR(&ModPicoGraphics_set_depth_obj) }, + + { MP_ROM_QSTR(MP_QSTR_update), MP_ROM_PTR(&ModPicoGraphics_update_obj) }, + { MP_ROM_QSTR(MP_QSTR_set_clip), MP_ROM_PTR(&ModPicoGraphics_set_clip_obj) }, + { MP_ROM_QSTR(MP_QSTR_remove_clip), MP_ROM_PTR(&ModPicoGraphics_remove_clip_obj) }, + { MP_ROM_QSTR(MP_QSTR_pixel_span), MP_ROM_PTR(&ModPicoGraphics_pixel_span_obj) }, + { MP_ROM_QSTR(MP_QSTR_rectangle), MP_ROM_PTR(&ModPicoGraphics_rectangle_obj) }, + { MP_ROM_QSTR(MP_QSTR_circle), MP_ROM_PTR(&ModPicoGraphics_circle_obj) }, + { MP_ROM_QSTR(MP_QSTR_character), MP_ROM_PTR(&ModPicoGraphics_character_obj) }, + { MP_ROM_QSTR(MP_QSTR_text), MP_ROM_PTR(&ModPicoGraphics_text_obj) }, + { MP_ROM_QSTR(MP_QSTR_measure_text), MP_ROM_PTR(&ModPicoGraphics_measure_text_obj) }, + { MP_ROM_QSTR(MP_QSTR_polygon), MP_ROM_PTR(&ModPicoGraphics_polygon_obj) }, + { MP_ROM_QSTR(MP_QSTR_triangle), MP_ROM_PTR(&ModPicoGraphics_triangle_obj) }, + { MP_ROM_QSTR(MP_QSTR_line), MP_ROM_PTR(&ModPicoGraphics_line_obj) }, + + { MP_ROM_QSTR(MP_QSTR_create_pen), MP_ROM_PTR(&ModPicoGraphics_create_pen_obj) }, + { MP_ROM_QSTR(MP_QSTR_create_pen_hsv), MP_ROM_PTR(&ModPicoGraphics_create_pen_hsv_obj) }, + { MP_ROM_QSTR(MP_QSTR_update_pen), MP_ROM_PTR(&ModPicoGraphics_update_pen_obj) }, + { MP_ROM_QSTR(MP_QSTR_reset_pen), MP_ROM_PTR(&ModPicoGraphics_reset_pen_obj) }, + { MP_ROM_QSTR(MP_QSTR_set_palette), MP_ROM_PTR(&ModPicoGraphics_set_palette_obj) }, + + { MP_ROM_QSTR(MP_QSTR_get_bounds), MP_ROM_PTR(&ModPicoGraphics_get_bounds_obj) }, + { MP_ROM_QSTR(MP_QSTR_set_font), MP_ROM_PTR(&ModPicoGraphics_set_font_obj) }, + +// { MP_ROM_QSTR(MP_QSTR_loop), MP_ROM_PTR(&ModPicoGraphics_loop_obj) }, + + { MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&ModPicoGraphics__del__obj) }, +}; +static MP_DEFINE_CONST_DICT(ModPicoGraphics_locals_dict, ModPicoGraphics_locals_dict_table); + +/***** Class Definition *****/ +#ifdef MP_DEFINE_CONST_OBJ_TYPE +MP_DEFINE_CONST_OBJ_TYPE( + ModPicoGraphics_type, + MP_QSTR_dvhstx, + MP_TYPE_FLAG_NONE, + make_new, ModPicoGraphics_make_new, + locals_dict, (mp_obj_dict_t*)&ModPicoGraphics_locals_dict +); +#else +const mp_obj_type_t ModPicoGraphics_type = { + { &mp_type_type }, + .name = MP_QSTR_dvhstx, + .make_new = ModPicoGraphics_make_new, + .locals_dict = (mp_obj_dict_t*)&ModPicoGraphics_locals_dict, +}; +#endif + +/***** Module Globals *****/ +static const mp_map_elem_t picographics_globals_table[] = { + { MP_ROM_QSTR(MP_QSTR___name__), MP_OBJ_NEW_QSTR(MP_QSTR_dvhstx) }, + { MP_ROM_QSTR(MP_QSTR_DVHSTX), (mp_obj_t)&ModPicoGraphics_type }, + + { MP_ROM_QSTR(MP_QSTR_PEN_RGB888), MP_ROM_INT(PEN_RGB888) }, + { MP_ROM_QSTR(MP_QSTR_PEN_RGB565), MP_ROM_INT(PEN_RGB565) }, + { MP_ROM_QSTR(MP_QSTR_PEN_P8), MP_ROM_INT(PEN_P8) }, + + { MP_ROM_QSTR(MP_QSTR_BLEND_TARGET), MP_ROM_INT(0) }, + { MP_ROM_QSTR(MP_QSTR_BLEND_FIXED), MP_ROM_INT(1) }, + + { MP_ROM_QSTR(MP_QSTR_SPRITE_OVERWRITE), MP_ROM_INT(0) }, + { MP_ROM_QSTR(MP_QSTR_SPRITE_UNDER), MP_ROM_INT(1) }, + { MP_ROM_QSTR(MP_QSTR_SPRITE_OVER), MP_ROM_INT(2) }, + { MP_ROM_QSTR(MP_QSTR_SPRITE_BLEND_UNDER), MP_ROM_INT(3) }, + { MP_ROM_QSTR(MP_QSTR_SPRITE_BLEND_OVER), MP_ROM_INT(4) }, + +#if SUPPORT_WIDE_MODES + { MP_ROM_QSTR(MP_QSTR_WIDESCREEN), MP_ROM_TRUE }, +#else + { MP_ROM_QSTR(MP_QSTR_WIDESCREEN), MP_ROM_FALSE }, +#endif +}; +static MP_DEFINE_CONST_DICT(mp_module_picographics_globals, picographics_globals_table); + +/***** Module Definition *****/ +const mp_obj_module_t picographics_user_cmodule = { + .base = { &mp_type_module }, + .globals = (mp_obj_dict_t*)&mp_module_picographics_globals, +}; + +#if MICROPY_VERSION <= 70144 +MP_REGISTER_MODULE(MP_QSTR_dvhstx, picographics_user_cmodule, MODULE_PICOGRAPHICS_ENABLED); +#else +MP_REGISTER_MODULE(MP_QSTR_dvhstx, picographics_user_cmodule); +#endif diff --git a/modules/picographics/picographics.cpp b/modules/picographics/picographics.cpp new file mode 100644 index 0000000..0bc9ece --- /dev/null +++ b/modules/picographics/picographics.cpp @@ -0,0 +1,614 @@ +#include "drivers/dvhstx/dvhstx.hpp" +#include "libraries/pico_graphics/pico_graphics_dvhstx.hpp" +#include "common/pimoroni_common.hpp" + +#include "micropython/modules/util.hpp" + +using namespace pimoroni; + +extern "C" { +#include "picographics.h" +#include "pimoroni_i2c.h" +#include "py/stream.h" +#include "py/reader.h" +#include "py/objarray.h" +#include "extmod/vfs.h" + +const std::string_view mp_obj_to_string_r(const mp_obj_t &obj) { + if(mp_obj_is_str_or_bytes(obj)) { + GET_STR_DATA_LEN(obj, str, str_len); + return std::string_view((const char*)str, str_len); + } + mp_raise_TypeError("can't convert object to str implicitly"); +} + +void __printf_debug_flush() { + for(auto i = 0u; i < 10; i++) { + sleep_ms(2); + mp_event_handle_nowait(); + } +} + +int mp_vprintf(const mp_print_t *print, const char *fmt, va_list args); + +void dvhstx_debug(const char *fmt, ...) { + va_list ap; + va_start(ap, fmt); + int ret = mp_vprintf(&mp_plat_print, fmt, ap); + va_end(ap); + __printf_debug_flush(); + (void)ret; +} + +static DVHSTX dv_display; + +typedef struct _ModPicoGraphics_obj_t { + mp_obj_base_t base; + PicoGraphicsDVHSTX *graphics; + DVHSTX *display; +} ModPicoGraphics_obj_t; + +size_t get_required_buffer_size(PicoGraphicsPenType pen_type, uint width, uint height) { + switch(pen_type) { + //case PEN_RGB888: + //return PicoGraphics_PenDVHSTX_RGB888::buffer_size(width, height); + case PEN_RGB565: + return PicoGraphics_PenDVHSTX_RGB565::buffer_size(width, height); + case PEN_P8: + return PicoGraphics_PenDVHSTX_P8::buffer_size(width, height); + default: + return 0; + } +} + +mp_obj_t ModPicoGraphics_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) { + ModPicoGraphics_obj_t *self = nullptr; + + enum { ARG_pen_type, ARG_width, ARG_height }; + static const mp_arg_t allowed_args[] = { + { MP_QSTR_pen_type, MP_ARG_INT, { .u_int = PEN_P8 } }, + { MP_QSTR_width, MP_ARG_INT, { .u_int = 320 } }, + { MP_QSTR_height, MP_ARG_INT, { .u_int = 240 } } + }; + + // Parse args. + mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; + mp_arg_parse_all_kw_array(n_args, n_kw, all_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + + self = mp_obj_malloc_with_finaliser(ModPicoGraphics_obj_t, &ModPicoGraphics_type); + self->base.type = &ModPicoGraphics_type; + + bool status = false; + int width = args[ARG_width].u_int; + int height = args[ARG_height].u_int; + int pen_type = args[ARG_pen_type].u_int; + + dvhstx_debug("DVHSTX create display\n"); + + // Create an instance of the graphics library and DV display driver + switch((PicoGraphicsPenType)pen_type) { + case PEN_RGB888: + //self->graphics = m_new_class(PicoGraphics_PenDVHSTX_RGB888, width, height, dv_display); + //status = dv_display.init(width, height, DVHSTX::MODE_RGB888); + break; + case PEN_RGB565: + self->graphics = m_new_class(PicoGraphics_PenDVHSTX_RGB565, width, height, dv_display); + status = dv_display.init(width, height, DVHSTX::MODE_RGB565); + break; + case PEN_P8: + self->graphics = m_new_class(PicoGraphics_PenDVHSTX_P8, width, height, dv_display); + status = dv_display.init(width, height, DVHSTX::MODE_PALETTE); + break; + default: + break; + } + + if (!status) { + mp_raise_msg(&mp_type_RuntimeError, "PicoVision: Unsupported Mode!"); + } + + dvhstx_debug("DVHSTX created\n"); + + self->display = &dv_display; + + // Clear each buffer + for(auto x = 0u; x < 2u; x++){ + self->graphics->set_pen(0); + self->graphics->clear(); + dv_display.flip_now(); + } + + return MP_OBJ_FROM_PTR(self); + +} + +mp_obj_t ModPicoGraphics__del__(mp_obj_t self_in) { + //dv_display.reset(); + return mp_const_none; +} + +mp_obj_t ModPicoGraphics_set_font(mp_obj_t self_in, mp_obj_t font) { + ModPicoGraphics_obj_t *self = MP_OBJ_TO_PTR2(self_in, ModPicoGraphics_obj_t); + self->graphics->set_font(mp_obj_to_string_r(font)); + return mp_const_none; +} + +mp_obj_t ModPicoGraphics_get_bounds(mp_obj_t self_in) { + ModPicoGraphics_obj_t *self = MP_OBJ_TO_PTR2(self_in, ModPicoGraphics_obj_t); + mp_obj_t tuple[2] = { + mp_obj_new_int(self->graphics->bounds.w), + mp_obj_new_int(self->graphics->bounds.h) + }; + return mp_obj_new_tuple(2, tuple); +} + +mp_obj_t ModPicoGraphics_update(mp_obj_t self_in) { + (void)self_in; + dv_display.flip_blocking(); + return mp_const_none; +} + +mp_obj_t ModPicoGraphics_module_RGB332_to_RGB(mp_obj_t rgb332) { + RGB c((RGB332)mp_obj_get_int(rgb332)); + mp_obj_t t[] = { + mp_obj_new_int(c.r), + mp_obj_new_int(c.g), + mp_obj_new_int(c.b), + }; + return mp_obj_new_tuple(3, t); +} + +mp_obj_t ModPicoGraphics_module_RGB565_to_RGB(mp_obj_t rgb565) { + RGB c((RGB565)mp_obj_get_int(rgb565)); + mp_obj_t t[] = { + mp_obj_new_int(c.r), + mp_obj_new_int(c.g), + mp_obj_new_int(c.b), + }; + return mp_obj_new_tuple(3, t); +} + +mp_obj_t ModPicoGraphics_module_RGB_to_RGB332(mp_obj_t r, mp_obj_t g, mp_obj_t b) { + return mp_obj_new_int(RGB( + mp_obj_get_int(r), + mp_obj_get_int(g), + mp_obj_get_int(b) + ).to_rgb332()); +} + +mp_obj_t ModPicoGraphics_module_RGB_to_RGB565(mp_obj_t r, mp_obj_t g, mp_obj_t b) { + return mp_obj_new_int(RGB( + mp_obj_get_int(r), + mp_obj_get_int(g), + mp_obj_get_int(b) + ).to_rgb565()); +} + +mp_obj_t ModPicoGraphics_set_pen(mp_obj_t self_in, mp_obj_t pen) { + ModPicoGraphics_obj_t *self = MP_OBJ_TO_PTR2(self_in, ModPicoGraphics_obj_t); + + self->graphics->set_pen(mp_obj_get_int(pen)); + + return mp_const_none; +} + +mp_obj_t ModPicoGraphics_set_bg(mp_obj_t self_in, mp_obj_t pen) { + ModPicoGraphics_obj_t *self = MP_OBJ_TO_PTR2(self_in, ModPicoGraphics_obj_t); + + self->graphics->set_bg(mp_obj_get_int(pen)); + + return mp_const_none; +} + +mp_obj_t ModPicoGraphics_set_blend_mode(mp_obj_t self_in, mp_obj_t pen) { + ModPicoGraphics_obj_t *self = MP_OBJ_TO_PTR2(self_in, ModPicoGraphics_obj_t); + + self->graphics->set_blend_mode((BlendMode)mp_obj_get_int(pen)); + + return mp_const_none; +} + +mp_obj_t ModPicoGraphics_set_depth(mp_obj_t self_in, mp_obj_t depth) { + ModPicoGraphics_obj_t *self = MP_OBJ_TO_PTR2(self_in, ModPicoGraphics_obj_t); + + self->graphics->set_depth(mp_obj_get_int(depth)); + + return mp_const_none; +} + +mp_obj_t ModPicoGraphics_reset_pen(mp_obj_t self_in, mp_obj_t pen) { + ModPicoGraphics_obj_t *self = MP_OBJ_TO_PTR2(self_in, ModPicoGraphics_obj_t); + + self->graphics->reset_pen(mp_obj_get_int(pen)); + + return mp_const_none; +} + +mp_obj_t ModPicoGraphics_update_pen(size_t n_args, const mp_obj_t *args) { + enum { ARG_self, ARG_i, ARG_r, ARG_g, ARG_b }; + + ModPicoGraphics_obj_t *self = MP_OBJ_TO_PTR2(args[ARG_self], ModPicoGraphics_obj_t); + + self->graphics->update_pen( + mp_obj_get_int(args[ARG_i]) & 0xff, + mp_obj_get_int(args[ARG_r]) & 0xff, + mp_obj_get_int(args[ARG_g]) & 0xff, + mp_obj_get_int(args[ARG_b]) & 0xff + ); + + return mp_const_none; +} + +mp_obj_t ModPicoGraphics_create_pen(size_t n_args, const mp_obj_t *args) { + enum { ARG_self, ARG_r, ARG_g, ARG_b }; + + ModPicoGraphics_obj_t *self = MP_OBJ_TO_PTR2(args[ARG_self], ModPicoGraphics_obj_t); + + int result = self->graphics->create_pen( + mp_obj_get_int(args[ARG_r]) & 0xff, + mp_obj_get_int(args[ARG_g]) & 0xff, + mp_obj_get_int(args[ARG_b]) & 0xff + ); + + if (result == -1) mp_raise_ValueError("create_pen failed. No matching colour or space in palette!"); + + return mp_obj_new_int(result); +} + +mp_obj_t ModPicoGraphics_create_pen_hsv(size_t n_args, const mp_obj_t *args) { + enum { ARG_self, ARG_h, ARG_s, ARG_v }; + + ModPicoGraphics_obj_t *self = MP_OBJ_TO_PTR2(args[ARG_self], ModPicoGraphics_obj_t); + int result = self->graphics->create_pen_hsv( + mp_obj_get_float(args[ARG_h]), + mp_obj_get_float(args[ARG_s]), + mp_obj_get_float(args[ARG_v]) + ); + + if (result == -1) mp_raise_ValueError("create_pen failed. No matching colour or space in palette!"); + + return mp_obj_new_int(result); +} + +mp_obj_t ModPicoGraphics_set_thickness(mp_obj_t self_in, mp_obj_t pen) { + ModPicoGraphics_obj_t *self = MP_OBJ_TO_PTR2(self_in, ModPicoGraphics_obj_t); + + self->graphics->set_thickness(mp_obj_get_int(pen)); + + return mp_const_none; +} + +mp_obj_t ModPicoGraphics_set_palette(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + size_t num_tuples = n_args - 1; + const mp_obj_t *tuples = pos_args + 1; + + ModPicoGraphics_obj_t *self = MP_OBJ_TO_PTR2(pos_args[0], ModPicoGraphics_obj_t); + + // Check if there is only one argument, which might be a list + if(n_args == 2) { + if(mp_obj_is_type(pos_args[1], &mp_type_list)) { + mp_obj_list_t *points = MP_OBJ_TO_PTR2(pos_args[1], mp_obj_list_t); + + if(points->len <= 0) mp_raise_ValueError("set_palette(): cannot provide an empty list"); + + num_tuples = points->len; + tuples = points->items; + } + else { + mp_raise_TypeError("set_palette(): can't convert object to list"); + } + } + + for(size_t i = 0; i < num_tuples; i++) { + mp_obj_t obj = tuples[i]; + if(!mp_obj_is_type(obj, &mp_type_tuple)) mp_raise_ValueError("set_palette(): can't convert object to tuple"); + + mp_obj_tuple_t *tuple = MP_OBJ_TO_PTR2(obj, mp_obj_tuple_t); + + if(tuple->len != 3) mp_raise_ValueError("set_palette(): tuple must contain R, G, B values"); + + self->graphics->update_pen( + i, + mp_obj_get_int(tuple->items[0]), + mp_obj_get_int(tuple->items[1]), + mp_obj_get_int(tuple->items[2]) + ); + } + + return mp_const_none; +} + +mp_obj_t ModPicoGraphics_set_clip(size_t n_args, const mp_obj_t *args) { + enum { ARG_self, ARG_x, ARG_y, ARG_w, ARG_h }; + + ModPicoGraphics_obj_t *self = MP_OBJ_TO_PTR2(args[ARG_self], ModPicoGraphics_obj_t); + + self->graphics->set_clip({ + mp_obj_get_int(args[ARG_x]), + mp_obj_get_int(args[ARG_y]), + mp_obj_get_int(args[ARG_w]), + mp_obj_get_int(args[ARG_h]) + }); + + return mp_const_none; +} + +mp_obj_t ModPicoGraphics_remove_clip(mp_obj_t self_in) { + ModPicoGraphics_obj_t *self = MP_OBJ_TO_PTR2(self_in, ModPicoGraphics_obj_t); + + self->graphics->remove_clip(); + + return mp_const_none; +} + +mp_obj_t ModPicoGraphics_clear(mp_obj_t self_in) { + ModPicoGraphics_obj_t *self = MP_OBJ_TO_PTR2(self_in, ModPicoGraphics_obj_t); + + self->graphics->clear(); + + return mp_const_none; +} + +mp_obj_t ModPicoGraphics_pixel(mp_obj_t self_in, mp_obj_t x, mp_obj_t y) { + ModPicoGraphics_obj_t *self = MP_OBJ_TO_PTR2(self_in, ModPicoGraphics_obj_t); + + self->graphics->pixel({ + mp_obj_get_int(x), + mp_obj_get_int(y) + }); + + return mp_const_none; +} + +mp_obj_t ModPicoGraphics_pixel_span(size_t n_args, const mp_obj_t *args) { + enum { ARG_self, ARG_x, ARG_y, ARG_l }; + + ModPicoGraphics_obj_t *self = MP_OBJ_TO_PTR2(args[ARG_self], ModPicoGraphics_obj_t); + + self->graphics->pixel_span({ + mp_obj_get_int(args[ARG_x]), + mp_obj_get_int(args[ARG_y]) + }, mp_obj_get_int(args[ARG_l])); + + return mp_const_none; +} + +mp_obj_t ModPicoGraphics_rectangle(size_t n_args, const mp_obj_t *args) { + enum { ARG_self, ARG_x, ARG_y, ARG_w, ARG_h }; + + ModPicoGraphics_obj_t *self = MP_OBJ_TO_PTR2(args[ARG_self], ModPicoGraphics_obj_t); + + self->graphics->rectangle({ + mp_obj_get_int(args[ARG_x]), + mp_obj_get_int(args[ARG_y]), + mp_obj_get_int(args[ARG_w]), + mp_obj_get_int(args[ARG_h]) + }); + + return mp_const_none; +} + +mp_obj_t ModPicoGraphics_circle(size_t n_args, const mp_obj_t *args) { + enum { ARG_self, ARG_x, ARG_y, ARG_r }; + + ModPicoGraphics_obj_t *self = MP_OBJ_TO_PTR2(args[ARG_self], ModPicoGraphics_obj_t); + + self->graphics->circle({ + mp_obj_get_int(args[ARG_x]), + mp_obj_get_int(args[ARG_y]) + }, mp_obj_get_int(args[ARG_r])); + + return mp_const_none; +} + +mp_obj_t ModPicoGraphics_character(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + enum { ARG_self, ARG_char, ARG_x, ARG_y, ARG_scale }; + static const mp_arg_t allowed_args[] = { + { MP_QSTR_, MP_ARG_REQUIRED | MP_ARG_OBJ }, + { MP_QSTR_char, MP_ARG_REQUIRED | MP_ARG_OBJ }, + { MP_QSTR_x, MP_ARG_REQUIRED | MP_ARG_INT }, + { MP_QSTR_y, MP_ARG_REQUIRED | MP_ARG_INT }, + { MP_QSTR_scale, MP_ARG_INT, {.u_int = 2} }, + }; + + mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; + mp_arg_parse_all(n_args, pos_args, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + + ModPicoGraphics_obj_t *self = MP_OBJ_TO_PTR2(args[ARG_self].u_obj, ModPicoGraphics_obj_t); + + int c = mp_obj_get_int(args[ARG_char].u_obj); + int x = args[ARG_x].u_int; + int y = args[ARG_y].u_int; + int scale = args[ARG_scale].u_int; + + self->graphics->character((char)c, Point(x, y), scale); + + return mp_const_none; +} + +mp_obj_t ModPicoGraphics_text(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + enum { ARG_self, ARG_text, ARG_x, ARG_y, ARG_wrap, ARG_scale, ARG_angle, ARG_spacing, ARG_fixed_width }; + static const mp_arg_t allowed_args[] = { + { MP_QSTR_, MP_ARG_REQUIRED | MP_ARG_OBJ }, + { MP_QSTR_text, MP_ARG_REQUIRED | MP_ARG_OBJ }, + { MP_QSTR_x1, MP_ARG_REQUIRED | MP_ARG_INT }, + { MP_QSTR_y1, MP_ARG_REQUIRED | MP_ARG_INT }, + { MP_QSTR_wordwrap, MP_ARG_INT, {.u_int = __INT32_MAX__} }, // Sort-of a fudge, but wide-enough to avoid wrapping on any display size + { MP_QSTR_scale, MP_ARG_OBJ, {.u_obj = mp_const_none} }, + { MP_QSTR_angle, MP_ARG_INT, {.u_int = 0} }, + { MP_QSTR_spacing, MP_ARG_INT, {.u_int = 1} }, + { MP_QSTR_fixed_width, MP_ARG_OBJ, {.u_obj = mp_const_false} }, + }; + + mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; + mp_arg_parse_all(n_args, pos_args, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + + ModPicoGraphics_obj_t *self = MP_OBJ_TO_PTR2(args[ARG_self].u_obj, ModPicoGraphics_obj_t); + + mp_obj_t text_obj = args[ARG_text].u_obj; + + if(!mp_obj_is_str_or_bytes(text_obj)) mp_raise_TypeError("text: string required"); + + GET_STR_DATA_LEN(text_obj, str, str_len); + + const std::string_view t((const char*)str, str_len); + + int x = args[ARG_x].u_int; + int y = args[ARG_y].u_int; + int wrap = args[ARG_wrap].u_int; + float scale = args[ARG_scale].u_obj == mp_const_none ? 2.0f : mp_obj_get_float(args[ARG_scale].u_obj); + int angle = args[ARG_angle].u_int; + int letter_spacing = args[ARG_spacing].u_int; + bool fixed_width = args[ARG_fixed_width].u_obj == mp_const_true; + + self->graphics->text(t, Point(x, y), wrap, scale, angle, letter_spacing, fixed_width); + + return mp_const_none; +} + +mp_obj_t ModPicoGraphics_measure_text(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + enum { ARG_self, ARG_text, ARG_scale, ARG_spacing, ARG_fixed_width }; + static const mp_arg_t allowed_args[] = { + { MP_QSTR_, MP_ARG_REQUIRED | MP_ARG_OBJ }, + { MP_QSTR_text, MP_ARG_REQUIRED | MP_ARG_OBJ }, + { MP_QSTR_scale, MP_ARG_OBJ, {.u_obj = mp_const_none} }, + { MP_QSTR_spacing, MP_ARG_INT, {.u_int = 1} }, + { MP_QSTR_fixed_width, MP_ARG_OBJ, {.u_obj = mp_const_false} }, + }; + + mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; + mp_arg_parse_all(n_args, pos_args, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + + ModPicoGraphics_obj_t *self = MP_OBJ_TO_PTR2(args[ARG_self].u_obj, ModPicoGraphics_obj_t); + + mp_obj_t text_obj = args[ARG_text].u_obj; + + if(!mp_obj_is_str_or_bytes(text_obj)) mp_raise_TypeError("text: string required"); + + GET_STR_DATA_LEN(text_obj, str, str_len); + + const std::string_view t((const char*)str, str_len); + + float scale = args[ARG_scale].u_obj == mp_const_none ? 2.0f : mp_obj_get_float(args[ARG_scale].u_obj); + int letter_spacing = args[ARG_spacing].u_int; + bool fixed_width = args[ARG_fixed_width].u_obj == mp_const_true; + + int width = self->graphics->measure_text(t, scale, letter_spacing, fixed_width); + + return mp_obj_new_int(width); +} + +mp_obj_t ModPicoGraphics_polygon(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + size_t num_tuples = n_args - 1; + const mp_obj_t *tuples = pos_args + 1; + + ModPicoGraphics_obj_t *self = MP_OBJ_TO_PTR2(pos_args[0], ModPicoGraphics_obj_t); + + // Check if there is only one argument, which might be a list + if(n_args == 2) { + if(mp_obj_is_type(pos_args[1], &mp_type_list)) { + mp_obj_list_t *points = MP_OBJ_TO_PTR2(pos_args[1], mp_obj_list_t); + + if(points->len <= 0) mp_raise_ValueError("poly(): cannot provide an empty list"); + + num_tuples = points->len; + tuples = points->items; + } + else { + mp_raise_TypeError("poly(): can't convert object to list"); + } + } + + if(num_tuples > 0) { + std::vector points; + for(size_t i = 0; i < num_tuples; i++) { + mp_obj_t obj = tuples[i]; + if(!mp_obj_is_type(obj, &mp_type_tuple)) mp_raise_ValueError("poly(): can't convert object to tuple"); + + mp_obj_tuple_t *tuple = MP_OBJ_TO_PTR2(obj, mp_obj_tuple_t); + + if(tuple->len != 2) mp_raise_ValueError("poly(): tuple must only contain two numbers"); + + points.push_back({ + mp_obj_get_int(tuple->items[0]), + mp_obj_get_int(tuple->items[1]) + }); + } + self->graphics->polygon(points); + } + + return mp_const_none; +} + +mp_obj_t ModPicoGraphics_triangle(size_t n_args, const mp_obj_t *args) { + enum { ARG_self, ARG_x1, ARG_y1, ARG_x2, ARG_y2, ARG_x3, ARG_y3 }; + + ModPicoGraphics_obj_t *self = MP_OBJ_TO_PTR2(args[ARG_self], ModPicoGraphics_obj_t); + + self->graphics->triangle( + {mp_obj_get_int(args[ARG_x1]), + mp_obj_get_int(args[ARG_y1])}, + {mp_obj_get_int(args[ARG_x2]), + mp_obj_get_int(args[ARG_y2])}, + {mp_obj_get_int(args[ARG_x3]), + mp_obj_get_int(args[ARG_y3])} + ); + + return mp_const_none; +} + +mp_obj_t ModPicoGraphics_line(size_t n_args, const mp_obj_t *args) { + enum { ARG_self, ARG_x1, ARG_y1, ARG_x2, ARG_y2, ARG_thickness }; + + ModPicoGraphics_obj_t *self = MP_OBJ_TO_PTR2(args[ARG_self], ModPicoGraphics_obj_t); + + if(n_args == 5) { + self->graphics->line( + {mp_obj_get_int(args[ARG_x1]), + mp_obj_get_int(args[ARG_y1])}, + {mp_obj_get_int(args[ARG_x2]), + mp_obj_get_int(args[ARG_y2])} + ); + } + else if(n_args == 6) { + self->graphics->thick_line( + {mp_obj_get_int(args[ARG_x1]), + mp_obj_get_int(args[ARG_y1])}, + {mp_obj_get_int(args[ARG_x2]), + mp_obj_get_int(args[ARG_y2])}, + mp_obj_get_int(args[ARG_thickness]) + ); + } + + return mp_const_none; +} + +mp_obj_t ModPicoGraphics_loop(mp_obj_t self_in, mp_obj_t update, mp_obj_t render) { + (void)self_in; + /* + TODO: Uh how do we typecheck a function? + if(!mp_obj_is_type(update, &mp_type_function)) { + mp_raise_TypeError("update(ticks_ms) must be a function."); + } + if(!mp_obj_is_type(render, &mp_type_function)) { + mp_raise_TypeError("render(ticks_ms) must be a function."); + }*/ + //ModPicoGraphics_obj_t *self = MP_OBJ_TO_PTR2(self_in, ModPicoGraphics_obj_t); + absolute_time_t t_start = get_absolute_time(); + mp_obj_t result; + while(true) { + uint32_t tick = absolute_time_diff_us(t_start, get_absolute_time()) / 1000; + result = mp_call_function_1(update, mp_obj_new_int(tick)); + if (result == mp_const_false) break; + dv_display.wait_for_flip(); + result = mp_call_function_1(render, mp_obj_new_int(tick)); + if (result == mp_const_false) break; + dv_display.flip_async(); + #ifdef mp_event_handle_nowait + mp_event_handle_nowait(); + #endif + } + return mp_const_none; +} + +} diff --git a/modules/picographics/picographics.h b/modules/picographics/picographics.h new file mode 100644 index 0000000..7361f1c --- /dev/null +++ b/modules/picographics/picographics.h @@ -0,0 +1,103 @@ +#include "py/runtime.h" +#include "py/objstr.h" + +enum PicoGraphicsPenType { + PEN_1BIT = 0, + PEN_3BIT, + PEN_P2, + PEN_P4, + PEN_P8, + PEN_RGB332, + PEN_RGB565, + PEN_RGB888, + PEN_INKY7 +}; + +enum PicoGraphicsBusType { + BUS_I2C, + BUS_SPI, + BUS_PARALLEL, + BUS_PIO +}; + +// Type +extern const mp_obj_type_t ModPicoGraphics_type; + +// Module functions +extern mp_obj_t ModPicoGraphics_get_required_buffer_size(mp_obj_t display_in, mp_obj_t pen_type_in); + +// DV Display specific functions +extern mp_obj_t ModPicoGraphics_set_scroll_group_offset(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args); +extern mp_obj_t ModPicoGraphics_set_scroll_group_for_lines(size_t n_args, const mp_obj_t *args); +extern mp_obj_t ModPicoGraphics_tilemap(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args); +extern mp_obj_t ModPicoGraphics_load_animation(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args); + +// Class methods +extern mp_obj_t ModPicoGraphics_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args); + +extern mp_obj_t ModPicoGraphics_update(mp_obj_t self_in); + +// Palette management +extern mp_obj_t ModPicoGraphics_update_pen(size_t n_args, const mp_obj_t *args); +extern mp_obj_t ModPicoGraphics_reset_pen(mp_obj_t self_in, mp_obj_t pen); +extern mp_obj_t ModPicoGraphics_set_palette(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args); +extern mp_obj_t ModPicoGraphics_hsv_to_rgb(size_t n_args, const mp_obj_t *args); + +// Pen +extern mp_obj_t ModPicoGraphics_set_pen(mp_obj_t self_in, mp_obj_t pen); +extern mp_obj_t ModPicoGraphics_create_pen(size_t n_args, const mp_obj_t *args); +extern mp_obj_t ModPicoGraphics_create_pen_hsv(size_t n_args, const mp_obj_t *args); +extern mp_obj_t ModPicoGraphics_set_thickness(mp_obj_t self_in, mp_obj_t thickness); + +// Alpha Blending +extern mp_obj_t ModPicoGraphics_set_bg(mp_obj_t self_in, mp_obj_t pen); +extern mp_obj_t ModPicoGraphics_set_blend_mode(mp_obj_t self_in, mp_obj_t pen); + +// Depth +extern mp_obj_t ModPicoGraphics_set_depth(mp_obj_t self_in, mp_obj_t depth); + +// Primitives +extern mp_obj_t ModPicoGraphics_set_clip(size_t n_args, const mp_obj_t *args); +extern mp_obj_t ModPicoGraphics_remove_clip(mp_obj_t self_in); +extern mp_obj_t ModPicoGraphics_clear(mp_obj_t self_in); +extern mp_obj_t ModPicoGraphics_pixel(mp_obj_t self_in, mp_obj_t x, mp_obj_t y); +extern mp_obj_t ModPicoGraphics_pixel_span(size_t n_args, const mp_obj_t *args); +extern mp_obj_t ModPicoGraphics_rectangle(size_t n_args, const mp_obj_t *args); +extern mp_obj_t ModPicoGraphics_circle(size_t n_args, const mp_obj_t *args); +extern mp_obj_t ModPicoGraphics_character(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args); +extern mp_obj_t ModPicoGraphics_text(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args); +extern mp_obj_t ModPicoGraphics_measure_text(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args); +extern mp_obj_t ModPicoGraphics_polygon(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args); +extern mp_obj_t ModPicoGraphics_triangle(size_t n_args, const mp_obj_t *args); +extern mp_obj_t ModPicoGraphics_line(size_t n_args, const mp_obj_t *args); + +// Sprites +extern mp_obj_t ModPicoGraphics_load_sprite(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args); +extern mp_obj_t ModPicoGraphics_display_sprite(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args); +extern mp_obj_t ModPicoGraphics_clear_sprite(mp_obj_t self_in, mp_obj_t slot); + +// Utility +extern mp_obj_t ModPicoGraphics_set_font(mp_obj_t self_in, mp_obj_t font); +extern mp_obj_t ModPicoGraphics_get_bounds(mp_obj_t self_in); + +extern mp_obj_t ModPicoGraphics_get_i2c(mp_obj_t self_in); + +extern mp_obj_t ModPicoGraphics__del__(mp_obj_t self_in); + +// IO IO +extern mp_obj_t ModPicoGraphics_is_button_x_pressed(mp_obj_t self_in); +extern mp_obj_t ModPicoGraphics_is_button_a_pressed(mp_obj_t self_in); + +extern mp_obj_t ModPicoGraphics_get_gpu_io_value(mp_obj_t self_in, mp_obj_t pin); +extern mp_obj_t ModPicoGraphics_set_gpu_io_value(mp_obj_t self_in, mp_obj_t pin, mp_obj_t value); +extern mp_obj_t ModPicoGraphics_set_gpu_io_output_enable(mp_obj_t self_in, mp_obj_t pin, mp_obj_t enable); +extern mp_obj_t ModPicoGraphics_set_gpu_io_pull_up(mp_obj_t self_in, mp_obj_t pin, mp_obj_t enable); +extern mp_obj_t ModPicoGraphics_set_gpu_io_pull_down(mp_obj_t self_in, mp_obj_t pin, mp_obj_t enable); + +extern mp_obj_t ModPicoGraphics_set_gpu_io_adc_enable(mp_obj_t self_in, mp_obj_t pin, mp_obj_t enable); +extern mp_obj_t ModPicoGraphics_get_gpu_io_adc_voltage(mp_obj_t self_in, mp_obj_t pin); + +extern mp_obj_t ModPicoGraphics_get_gpu_temp(mp_obj_t self_in); + +// Loop +extern mp_obj_t ModPicoGraphics_loop(mp_obj_t self_in, mp_obj_t update, mp_obj_t render); diff --git a/modules/picographics/spritesheet-to-rgb332.py b/modules/picographics/spritesheet-to-rgb332.py new file mode 100644 index 0000000..9eaff38 --- /dev/null +++ b/modules/picographics/spritesheet-to-rgb332.py @@ -0,0 +1,37 @@ +#!/bin/env python3 +from PIL import Image +import numpy +import sys +import pathlib + +# Run with `./filename.py source-image.jpg` +IMAGE_PATH = pathlib.Path(sys.argv[1]) +OUTPUT_PATH = IMAGE_PATH.with_suffix(".rgb332") + + +def image_to_data(image): + """Generator function to convert a PIL image to 16-bit 565 RGB bytes.""" + # NumPy is much faster at doing this. NumPy code provided by: + # Keith (https://www.blogger.com/profile/02555547344016007163) + pb = numpy.array(image.convert('RGBA')).astype('uint16') + + r = (pb[:, :, 0] & 0b11100000) >> 0 + g = (pb[:, :, 1] & 0b11100000) >> 3 + b = (pb[:, :, 2] & 0b11000000) >> 6 + a = pb[:, :, 3] # Discard + + # AAAA RRRR GGGG BBBB + color = r | g | b + return color.astype("uint8").flatten().tobytes() + + +img = Image.open(IMAGE_PATH) +w, h = img.size +data = image_to_data(img) + +print(f"Converted: {w}x{h} {len(data)} bytes") + +with open(OUTPUT_PATH, "wb") as f: + f.write(data) + +print(f"Written to: {OUTPUT_PATH}") \ No newline at end of file