diff --git a/.github/workflows/micropython-badger2040.yml b/.github/workflows/micropython-badger2040.yml index b71fa1e3..81e71d25 100644 --- a/.github/workflows/micropython-badger2040.yml +++ b/.github/workflows/micropython-badger2040.yml @@ -7,7 +7,7 @@ on: types: [created] env: - MICROPYTHON_VERSION: 67fac4ebc53db6337008ba06df7932faec80f57c + MICROPYTHON_VERSION: 35524a6fda1e44692ad599a39c802c168c897de9 BOARD_TYPE: PIMORONI_BADGER2040 # MicroPython version will be contained in github.event.release.tag_name for releases RELEASE_FILE: pimoroni-badger2040-${{github.event.release.tag_name || github.sha}}-micropython diff --git a/.github/workflows/micropython-badger2040w.yml b/.github/workflows/micropython-badger2040w.yml new file mode 100644 index 00000000..50f8080c --- /dev/null +++ b/.github/workflows/micropython-badger2040w.yml @@ -0,0 +1,153 @@ +name: MicroPython for Badger2040W + +on: + push: + pull_request: + release: + types: [created] + +env: + MICROPYTHON_VERSION: 67fac4ebc53db6337008ba06df7932faec80f57c + BOARD_TYPE: PIMORONI_BADGER2040W + # MicroPython version will be contained in github.event.release.tag_name for releases + RELEASE_FILE: pimoroni-badger2040w-${{github.event.release.tag_name || github.sha}}-micropython + +jobs: + deps: + runs-on: ubuntu-20.04 + name: Dependencies + steps: + - name: Workspace Cache + id: cache + uses: actions/cache@v3 + with: + path: ${{runner.workspace}} + key: workspace-micropython-${{env.MICROPYTHON_VERSION}} + restore-keys: | + workspace-micropython-${{env.MICROPYTHON_VERSION}} + + # Check out MicroPython + - name: Checkout MicroPython + if: steps.cache.outputs.cache-hit != 'true' + uses: actions/checkout@v3 + with: + repository: micropython/micropython + ref: ${{env.MICROPYTHON_VERSION}} + submodules: false # MicroPython submodules are hideously broken + path: micropython + + - name: Fetch base MicroPython submodules + if: steps.cache.outputs.cache-hit != 'true' + shell: bash + working-directory: micropython + run: git submodule update --init + + - name: Fetch Pico SDK submodules + if: steps.cache.outputs.cache-hit != 'true' + shell: bash + working-directory: micropython/lib/pico-sdk + run: git submodule update --init + + - name: Build mpy-cross + if: steps.cache.outputs.cache-hit != 'true' + shell: bash + working-directory: micropython/mpy-cross + run: make + + build: + needs: deps + name: Build Badger 2040W + runs-on: ubuntu-20.04 + + steps: + - name: Compiler Cache + uses: actions/cache@v3 + with: + path: /home/runner/.ccache + key: ccache-micropython-badger2040w-${{github.ref}}-${{github.sha}} + restore-keys: | + ccache-micropython-badger2040w-${{github.ref}} + ccache-micropython-badger2040w- + + - name: Workspace Cache + uses: actions/cache@v3 + with: + path: ${{runner.workspace}} + key: workspace-micropython-${{env.MICROPYTHON_VERSION}} + restore-keys: | + workspace-micropython-${{env.MICROPYTHON_VERSION}} + + - uses: actions/checkout@v3 + with: + submodules: true + path: pimoroni-pico-${{ github.sha }} + + # Check out dir2u2f + - uses: actions/checkout@v3 + with: + repository: gadgetoid/dir2uf2 + ref: v0.0.1 + path: dir2uf2 + + - name: "HACK: MicroPython Board Fixups" + shell: bash + working-directory: micropython/ports/rp2 + run: | + ../../../pimoroni-pico-${GITHUB_SHA}/micropython/_board/board-fixup.sh badger2040w ${{env.BOARD_TYPE}} ../../../pimoroni-pico-${GITHUB_SHA}/micropython/_board + + # Linux deps + - name: Install Compiler & CCache + if: runner.os == 'Linux' + run: | + sudo apt update && sudo apt install ccache gcc-arm-none-eabi + python3 -m pip install pillow + + # Build firmware + - name: Configure MicroPython + shell: bash + working-directory: micropython/ports/rp2 + run: | + cmake -S . -B build-${{env.BOARD_TYPE}} -DPICO_BUILD_DOCS=0 -DUSER_C_MODULES=../../../pimoroni-pico-${GITHUB_SHA}/micropython/modules/micropython-badger2040w.cmake -DMICROPY_BOARD=${{env.BOARD_TYPE}} -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache + + - name: Build MicroPython + shell: bash + working-directory: micropython/ports/rp2 + run: | + ccache --zero-stats || true + cmake --build build-${{env.BOARD_TYPE}} -j 2 + ccache --show-stats || true + + - name: Rename .uf2 for artifact + shell: bash + working-directory: micropython/ports/rp2/build-${{env.BOARD_TYPE}} + run: | + cp firmware.uf2 ${{env.RELEASE_FILE}}.uf2 + + - name: Append Filesystem + shell: bash + run: | + python3 -m pip install littlefs-python + ./dir2uf2/dir2uf2 --append-to micropython/ports/rp2/build-${{env.BOARD_TYPE}}/${{env.RELEASE_FILE}}.uf2 --manifest pimoroni-pico-${{ github.sha }}/micropython/examples/badger2040w/uf2-manifest.txt --filename with-examples.uf2 pimoroni-pico-${{ github.sha }}/micropython/examples/badger2040w/ + + - name: Store .uf2 as artifact + uses: actions/upload-artifact@v3 + with: + name: ${{env.RELEASE_FILE}}.uf2 + path: micropython/ports/rp2/build-${{env.BOARD_TYPE}}/${{env.RELEASE_FILE}}.uf2 + + - name: Store .uf2 + examples as artifact + uses: actions/upload-artifact@v3 + with: + name: ${{env.RELEASE_FILE}}-with-examples.uf2 + path: ${{env.RELEASE_FILE}}-with-examples.uf2 + + - name: Upload .uf2 + if: github.event_name == 'release' + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} + with: + asset_path: micropython/ports/rp2/build-${{env.BOARD_TYPE}}/${{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/drivers/icp10125/icp10125.cpp b/drivers/icp10125/icp10125.cpp index 6ed4ce6e..bd78089e 100644 --- a/drivers/icp10125/icp10125.cpp +++ b/drivers/icp10125/icp10125.cpp @@ -39,13 +39,13 @@ namespace pimoroni { } void ICP10125::reset() { - uint16_t command = __bswap16(SOFT_RESET); + uint16_t command = __builtin_bswap16(SOFT_RESET); i2c->write_blocking(address, (uint8_t *)&command, 2, false); sleep_ms(10); // Soft reset time is 170us but you can never be too sure... } ICP10125::reading ICP10125::measure(meas_command cmd) { - uint16_t command = __bswap16(cmd); + uint16_t command = __builtin_bswap16(cmd); reading result = {0.0f, 0.0f, OK}; uint16_result results[3]; @@ -74,9 +74,9 @@ namespace pimoroni { if(results[1].crc8 != crc8((uint8_t *)&results[1].data, 2)) {result.status = CRC_FAIL; return result;}; if(results[2].crc8 != crc8((uint8_t *)&results[2].data, 2)) {result.status = CRC_FAIL; return result;}; - int temperature = __bswap16(results[0].data); + int temperature = __builtin_bswap16(results[0].data); // Due to all the byte swapping nonsense I'm not sure if I've discarded the LLSB or LMSB here... - int pressure = ((int32_t)__bswap16(results[1].data) << 8) | (__bswap16(results[2].data >> 8)); // LLSB is discarded + int pressure = ((int32_t)__builtin_bswap16(results[1].data) << 8) | (__builtin_bswap16(results[2].data >> 8)); // LLSB is discarded process_data(pressure, temperature, &result.pressure, &result.temperature); return result; @@ -84,7 +84,7 @@ namespace pimoroni { int ICP10125::chip_id() { uint16_result result; - uint16_t command = __bswap16(READ_ID); + uint16_t command = __builtin_bswap16(READ_ID); i2c->write_blocking(address, (uint8_t *)&command, 2, true); i2c->read_blocking(address, (uint8_t *)&result, 3, false); @@ -93,12 +93,12 @@ namespace pimoroni { return -1; } - return __bswap16(result.data) & 0x3f; + return __builtin_bswap16(result.data) & 0x3f; } bool ICP10125::read_otp() { uint16_result result[4]; - uint16_t command = __bswap16(READ_OTP); + uint16_t command = __builtin_bswap16(READ_OTP); uint8_t move_address_ptr[] = { MOVE_ADDRESS_PTR >> 8, MOVE_ADDRESS_PTR & 0xff, 0x00, @@ -114,7 +114,7 @@ namespace pimoroni { if(result[x].crc8 != crc8((uint8_t *)&result[x].data, 2)) { return false; } - sensor_constants[x] = (float)__bswap16(result[x].data); + sensor_constants[x] = (float)__builtin_bswap16(result[x].data); } return true; diff --git a/drivers/vl53l1x/vl53l1x.cpp b/drivers/vl53l1x/vl53l1x.cpp index 059b406c..3eb2812a 100644 --- a/drivers/vl53l1x/vl53l1x.cpp +++ b/drivers/vl53l1x/vl53l1x.cpp @@ -205,7 +205,7 @@ namespace pimoroni { i2c->read_blocking(address, (uint8_t *)&value, 2, false); // TODO do we need to bswap this return value? - return __bswap16(value); + return __builtin_bswap16(value); } // Read a 32-bit register diff --git a/libraries/pico_graphics/pico_graphics.cpp b/libraries/pico_graphics/pico_graphics.cpp index 1ace1d3f..6541ae53 100644 --- a/libraries/pico_graphics/pico_graphics.cpp +++ b/libraries/pico_graphics/pico_graphics.cpp @@ -164,9 +164,15 @@ namespace pimoroni { } if (hershey_font) { - hershey::text(hershey_font, [this](int32_t x1, int32_t y1, int32_t x2, int32_t y2) { - line(Point(x1, y1), Point(x2, y2)); - }, t, p.x, p.y, s, a); + if(thickness == 1) { + hershey::text(hershey_font, [this](int32_t x1, int32_t y1, int32_t x2, int32_t y2) { + line(Point(x1, y1), Point(x2, y2)); + }, t, p.x, p.y, s, a); + } else { + hershey::text(hershey_font, [this](int32_t x1, int32_t y1, int32_t x2, int32_t y2) { + thick_line(Point(x1, y1), Point(x2, y2), thickness); + }, t, p.x, p.y, s, a); + } return; } } @@ -292,6 +298,42 @@ namespace pimoroni { } } + void PicoGraphics::thick_line(Point p1, Point p2, uint thickness) { + // general purpose line + // lines are either "shallow" or "steep" based on whether the x delta + // is greater than the y delta + int32_t dx = p2.x - p1.x; + int32_t dy = p2.y - p1.y; + bool shallow = std::abs(dx) > std::abs(dy); + if(shallow) { + // shallow version + int32_t s = std::abs(dx); // number of steps + int32_t sx = dx < 0 ? -1 : 1; // x step value + int32_t sy = (dy << 16) / s; // y step value in fixed 16:16 + int32_t x = p1.x; + int32_t y = p1.y << 16; + while(s--) { + int32_t ht = thickness / 2; + rectangle({x - ht, (y >> 16) - ht, ht * 2, ht * 2}); + y += sy; + x += sx; + } + }else{ + // steep version + int32_t s = std::abs(dy); // number of steps + int32_t sy = dy < 0 ? -1 : 1; // y step value + int32_t sx = (dx << 16) / s; // x step value in fixed 16:16 + int32_t y = p1.y; + int32_t x = p1.x << 16; + while(s--) { + int32_t ht = thickness / 2; + rectangle({(x >> 16) - ht, y - ht, ht * 2, ht * 2}); + y += sy; + x += sx; + } + } + } + void PicoGraphics::line(Point p1, Point p2) { // fast horizontal line if(p1.y == p2.y) { diff --git a/libraries/pico_graphics/pico_graphics.hpp b/libraries/pico_graphics/pico_graphics.hpp index d9f421ca..88474e23 100644 --- a/libraries/pico_graphics/pico_graphics.hpp +++ b/libraries/pico_graphics/pico_graphics.hpp @@ -175,6 +175,7 @@ namespace pimoroni { PenType pen_type; Rect bounds; Rect clip; + uint thickness = 1; @@ -228,6 +229,7 @@ namespace pimoroni { virtual void set_pen(uint8_t r, uint8_t g, uint8_t b) = 0; virtual void set_pixel(const Point &p) = 0; virtual void set_pixel_span(const Point &p, uint l) = 0; + virtual void set_thickness(uint t) = 0; virtual int create_pen(uint8_t r, uint8_t g, uint8_t b); virtual int create_pen_hsv(float h, float s, float v); @@ -264,6 +266,7 @@ namespace pimoroni { void triangle(Point p1, Point p2, Point p3); void line(Point p1, Point p2); void from_hsv(float h, float s, float v, uint8_t &r, uint8_t &g, uint8_t &b); + void thick_line(Point p1, Point p2, uint thickness); protected: void frame_convert_rgb565(conversion_callback_func callback, next_pixel_func get_next_pixel); @@ -276,6 +279,7 @@ namespace pimoroni { PicoGraphics_Pen1Bit(uint16_t width, uint16_t height, void *frame_buffer); void set_pen(uint c) override; void set_pen(uint8_t r, uint8_t g, uint8_t b) override; + void set_thickness(uint t) override; void set_pixel(const Point &p) override; void set_pixel_span(const Point &p, uint l) override; @@ -292,6 +296,7 @@ namespace pimoroni { PicoGraphics_Pen1BitY(uint16_t width, uint16_t height, void *frame_buffer); void set_pen(uint c) override; void set_pen(uint8_t r, uint8_t g, uint8_t b) override; + void set_thickness(uint t) override; void set_pixel(const Point &p) override; void set_pixel_span(const Point &p, uint l) override; @@ -334,6 +339,7 @@ namespace pimoroni { void set_pen(uint c) override; void set_pen(uint8_t r, uint8_t g, uint8_t b) override; + void set_thickness(uint t) override {}; void set_pixel(const Point &p) override; void set_pixel_span(const Point &p, uint l) override; @@ -360,6 +366,7 @@ namespace pimoroni { PicoGraphics_PenP4(uint16_t width, uint16_t height, void *frame_buffer); void set_pen(uint c) override; void set_pen(uint8_t r, uint8_t g, uint8_t b) override; + void set_thickness(uint t) override {}; int update_pen(uint8_t i, uint8_t r, uint8_t g, uint8_t b) override; int create_pen(uint8_t r, uint8_t g, uint8_t b) override; int reset_pen(uint8_t i) override; @@ -389,6 +396,7 @@ namespace pimoroni { PicoGraphics_PenP8(uint16_t width, uint16_t height, void *frame_buffer); void set_pen(uint c) override; void set_pen(uint8_t r, uint8_t g, uint8_t b) override; + void set_thickness(uint t) override {}; int update_pen(uint8_t i, uint8_t r, uint8_t g, uint8_t b) override; int create_pen(uint8_t r, uint8_t g, uint8_t b) override; int reset_pen(uint8_t i) override; @@ -410,6 +418,7 @@ namespace pimoroni { PicoGraphics_PenRGB332(uint16_t width, uint16_t height, void *frame_buffer); void set_pen(uint c) override; void set_pen(uint8_t r, uint8_t g, uint8_t b) override; + void set_thickness(uint t) override {}; int create_pen(uint8_t r, uint8_t g, uint8_t b) override; void set_pixel(const Point &p) override; void set_pixel_span(const Point &p, uint l) override; @@ -431,6 +440,7 @@ namespace pimoroni { PicoGraphics_PenRGB565(uint16_t width, uint16_t height, void *frame_buffer); void set_pen(uint c) override; void set_pen(uint8_t r, uint8_t g, uint8_t b) override; + void set_thickness(uint t) override {}; int create_pen(uint8_t r, uint8_t g, uint8_t b) override; int create_pen_hsv(float h, float s, float v) override; void set_pixel(const Point &p) override; @@ -447,6 +457,7 @@ namespace pimoroni { PicoGraphics_PenRGB888(uint16_t width, uint16_t height, void *frame_buffer); void set_pen(uint c) override; void set_pen(uint8_t r, uint8_t g, uint8_t b) override; + void set_thickness(uint t) override {}; int create_pen(uint8_t r, uint8_t g, uint8_t b) override; int create_pen_hsv(float h, float s, float v) override; void set_pixel(const Point &p) override; diff --git a/libraries/pico_graphics/pico_graphics_pen_1bit.cpp b/libraries/pico_graphics/pico_graphics_pen_1bit.cpp index 30fb1e53..59f2a4af 100644 --- a/libraries/pico_graphics/pico_graphics_pen_1bit.cpp +++ b/libraries/pico_graphics/pico_graphics_pen_1bit.cpp @@ -18,6 +18,10 @@ namespace pimoroni { color = std::max(r, std::max(g, b)) >> 4; } + void PicoGraphics_Pen1Bit::set_thickness(uint t) { + thickness = t; + } + void PicoGraphics_Pen1Bit::set_pixel(const Point &p) { // pointer to byte in framebuffer that contains this pixel uint8_t *buf = (uint8_t *)frame_buffer; diff --git a/libraries/pico_graphics/pico_graphics_pen_1bitY.cpp b/libraries/pico_graphics/pico_graphics_pen_1bitY.cpp index ea4afe99..b391022d 100644 --- a/libraries/pico_graphics/pico_graphics_pen_1bitY.cpp +++ b/libraries/pico_graphics/pico_graphics_pen_1bitY.cpp @@ -18,6 +18,10 @@ namespace pimoroni { color = std::max(r, std::max(g, b)); } + void PicoGraphics_Pen1BitY::set_thickness(uint t) { + thickness = t; + } + void PicoGraphics_Pen1BitY::set_pixel(const Point &p) { // pointer to byte in framebuffer that contains this pixel uint8_t *buf = (uint8_t *)frame_buffer; diff --git a/micropython/_board/badger2040w/PIMORONI_BADGER2040W/board.json b/micropython/_board/badger2040w/PIMORONI_BADGER2040W/board.json new file mode 100644 index 00000000..d5d4332e --- /dev/null +++ b/micropython/_board/badger2040w/PIMORONI_BADGER2040W/board.json @@ -0,0 +1,17 @@ +{ + "deploy": [ + "../deploy.md" + ], + "docs": "", + "features": [ + "296*128 e-Ink", + "Buttons" + ], + "images": [ + ], + "mcu": "rp2040", + "product": "Badger2040 W (2MiB)", + "thumbnail": "", + "url": "https://shop.pimoroni.com/products/badger-2040w", + "vendor": "Pimoroni" +} diff --git a/micropython/_board/badger2040w/PIMORONI_BADGER2040W/manifest.py b/micropython/_board/badger2040w/PIMORONI_BADGER2040W/manifest.py new file mode 100644 index 00000000..b446b0a0 --- /dev/null +++ b/micropython/_board/badger2040w/PIMORONI_BADGER2040W/manifest.py @@ -0,0 +1,7 @@ +include("../manifest.py") + +require("mip") +require("ntptime") +require("urequests") +require("urllib.urequest") +require("umqtt.simple") \ No newline at end of file diff --git a/micropython/_board/badger2040w/PIMORONI_BADGER2040W/mpconfigboard.cmake b/micropython/_board/badger2040w/PIMORONI_BADGER2040W/mpconfigboard.cmake new file mode 100644 index 00000000..d0b32b91 --- /dev/null +++ b/micropython/_board/badger2040w/PIMORONI_BADGER2040W/mpconfigboard.cmake @@ -0,0 +1,8 @@ +# cmake file for Pimoroni Badger 2040W +set(MICROPY_BOARD PICO_W) + +set(MICROPY_PY_LWIP ON) +set(MICROPY_PY_NETWORK_CYW43 ON) + +# Board specific version of the frozen manifest +set(MICROPY_FROZEN_MANIFEST ${CMAKE_SOURCE_DIR}/boards/PIMORONI_BADGER2040W/manifest.py) \ No newline at end of file diff --git a/micropython/_board/badger2040w/PIMORONI_BADGER2040W/mpconfigboard.h b/micropython/_board/badger2040w/PIMORONI_BADGER2040W/mpconfigboard.h new file mode 100644 index 00000000..89594311 --- /dev/null +++ b/micropython/_board/badger2040w/PIMORONI_BADGER2040W/mpconfigboard.h @@ -0,0 +1,20 @@ +// This is a hack! Need to replace with upstream board definition. +#define MICROPY_HW_BOARD_NAME "Pimoroni Badger2040W 2MB" +#define MICROPY_HW_FLASH_STORAGE_BYTES (848 * 1024) + +// Enable networking. +#define MICROPY_PY_NETWORK 1 + +// CYW43 driver configuration. +#define CYW43_USE_SPI (1) +#define CYW43_LWIP (1) +#define CYW43_GPIO (1) +#define CYW43_SPI_PIO (1) + +// For debugging mbedtls - also set +// Debug level (0-4) 1=warning, 2=info, 3=debug, 4=verbose +// #define MODUSSL_MBEDTLS_DEBUG_LEVEL 1 + +#define MICROPY_HW_PIN_EXT_COUNT CYW43_WL_GPIO_COUNT + +#define MICROPY_HW_PIN_RESERVED(i) ((i) == CYW43_PIN_WL_HOST_WAKE || (i) == CYW43_PIN_WL_REG_ON) \ No newline at end of file diff --git a/micropython/_board/badger2040w/PIMORONI_BADGER2040W/pins.csv b/micropython/_board/badger2040w/PIMORONI_BADGER2040W/pins.csv new file mode 100644 index 00000000..012bc7c7 --- /dev/null +++ b/micropython/_board/badger2040w/PIMORONI_BADGER2040W/pins.csv @@ -0,0 +1,30 @@ +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 +GP26,GPIO26 +GP27,GPIO27 +GP28,GPIO28 +WL_GPIO0,EXT_GPIO0 +WL_GPIO1,EXT_GPIO1 +WL_GPIO2,EXT_GPIO2 +LED,EXT_GPIO0 \ No newline at end of file diff --git a/micropython/_board/badger2040w/fixup.sh b/micropython/_board/badger2040w/fixup.sh new file mode 100644 index 00000000..6acb551d --- /dev/null +++ b/micropython/_board/badger2040w/fixup.sh @@ -0,0 +1,6 @@ +SRC_DIR=$1 +DST_DIR=$2 + +echo "Applying wakeup_gpio.patch" +cd "$DST_DIR/../../lib/pico-sdk" +git apply "$SRC_DIR/wakeup_gpio.patch" \ No newline at end of file diff --git a/micropython/_board/badger2040w/pimoroni_badger2040w.h b/micropython/_board/badger2040w/pimoroni_badger2040w.h new file mode 100644 index 00000000..88610e7b --- /dev/null +++ b/micropython/_board/badger2040w/pimoroni_badger2040w.h @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2020 Raspberry Pi (Trading) Ltd. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +// ----------------------------------------------------- +// NOTE: THIS HEADER IS ALSO INCLUDED BY ASSEMBLER SO +// SHOULD ONLY CONSIST OF PREPROCESSOR DIRECTIVES +// ----------------------------------------------------- + +#ifndef _BOARDS_PIMORONI_BADGER2040_H +#define _BOARDS_PIMORONI_BADGER2040_H + +// For board detection +#define RASPBERRYPI_PICO_W +#define PIMORONI_BADGER2040 +#define PIMORONI_BADGER2040W + +// --- BOARD SPECIFIC --- +#define BADGER2040_UART 0 +#define BADGER2040_TX_PIN 0 +#define BADGER2040_RX_PIN 1 + +#define BADGER2040_I2C 0 +#define BADGER2040_INT_PIN 3 +#define BADGER2040_SDA_PIN 4 +#define BADGER2040_SCL_PIN 5 + +#define BADGER2040_3V3_EN_PIN 10 + +#define BADGER2040_SW_DOWN_PIN 11 +#define BADGER2040_SW_A_PIN 12 +#define BADGER2040_SW_B_PIN 13 +#define BADGER2040_SW_C_PIN 14 +#define BADGER2040_SW_UP_PIN 15 + +#define BADGER2040_INKY_SPI 0 +#define BADGER2040_INKY_MISO_PIN 16 +#define BADGER2040_INKY_CSN_PIN 17 +#define BADGER2040_INKY_SCK_PIN 18 +#define BADGER2040_INKY_MOSI_PIN 19 +#define BADGER2040_INKY_DC_PIN 20 +#define BADGER2040_INKY_RESET_PIN 21 +#define BADGER2040_INKY_BUSY_PIN 26 + +#define BADGER2040_USER_SW_PIN 23 +#define BADGER2040_USER_LED_PIN 25 + +#define BADGER2040_VBUS_DETECT_PIN 24 +#define BADGER2040_VREF_POWER_PIN 27 +#define BADGER2040_1V2_REF_PIN 28 +#define BADGER2040_BAT_SENSE_PIN 29 + +// --- UART --- +#ifndef PICO_DEFAULT_UART +#define PICO_DEFAULT_UART BADGER2040_UART +#endif + +#ifndef PICO_DEFAULT_UART_TX_PIN +#define PICO_DEFAULT_UART_TX_PIN BADGER2040_TX_PIN +#endif + +#ifndef PICO_DEFAULT_UART_RX_PIN +#define PICO_DEFAULT_UART_RX_PIN BADGER2040_RX_PIN +#endif + +// --- LED --- +#ifndef PICO_DEFAULT_LED_PIN +#define PICO_DEFAULT_LED_PIN BADGER2040_USER_LED_PIN +#endif +// no PICO_DEFAULT_WS2812_PIN + +// --- I2C --- +#ifndef PICO_DEFAULT_I2C +#define PICO_DEFAULT_I2C BADGER2040_I2C +#endif +#ifndef PICO_DEFAULT_I2C_SDA_PIN +#define PICO_DEFAULT_I2C_SDA_PIN BADGER2040_SDA_PIN +#endif +#ifndef PICO_DEFAULT_I2C_SCL_PIN +#define PICO_DEFAULT_I2C_SCL_PIN BADGER2040_SCL_PIN +#endif + +// --- SPI --- +#ifndef PICO_DEFAULT_SPI +#define PICO_DEFAULT_SPI BADGER2040_INKY_SPI +#endif +#ifndef PICO_DEFAULT_SPI_SCK_PIN +#define PICO_DEFAULT_SPI_SCK_PIN BADGER2040_INKY_SCK_PIN +#endif +#ifndef PICO_DEFAULT_SPI_TX_PIN +#define PICO_DEFAULT_SPI_TX_PIN BADGER2040_INKY_MOSI_PIN +#endif +#ifndef PICO_DEFAULT_SPI_RX_PIN +#define PICO_DEFAULT_SPI_RX_PIN BADGER2040_INKY_MISO_PIN +#endif +#ifndef PICO_DEFAULT_SPI_CSN_PIN +#define PICO_DEFAULT_SPI_CSN_PIN BADGER2040_INKY_CSN_PIN +#endif + +// --- FLASH --- +#define PICO_BOOT_STAGE2_CHOOSE_W25Q080 1 + +#ifndef PICO_FLASH_SPI_CLKDIV +#define PICO_FLASH_SPI_CLKDIV 2 +#endif + +#ifndef PICO_FLASH_SIZE_BYTES +#define PICO_FLASH_SIZE_BYTES (2 * 1024 * 1024) +#endif + +#ifndef PICO_RP2040_B0_SUPPORTED +#define PICO_RP2040_B0_SUPPORTED 0 +#endif + +#ifndef PICO_RP2040_B1_SUPPORTED +#define PICO_RP2040_B1_SUPPORTED 0 +#endif + +#ifndef CYW43_PIN_WL_HOST_WAKE +#define CYW43_PIN_WL_HOST_WAKE 24 +#endif + +#ifndef CYW43_PIN_WL_REG_ON +#define CYW43_PIN_WL_REG_ON 23 +#endif + +#ifndef CYW43_WL_GPIO_COUNT +#define CYW43_WL_GPIO_COUNT 3 +#endif + +#ifndef CYW43_WL_GPIO_LED_PIN +#define CYW43_WL_GPIO_LED_PIN 0 +#endif + +#endif \ No newline at end of file diff --git a/micropython/_board/badger2040w/wakeup_gpio.patch b/micropython/_board/badger2040w/wakeup_gpio.patch new file mode 100644 index 00000000..ec1c9931 --- /dev/null +++ b/micropython/_board/badger2040w/wakeup_gpio.patch @@ -0,0 +1,143 @@ +diff --git a/src/rp2_common/pico_runtime/runtime.c b/src/rp2_common/pico_runtime/runtime.c +index 70dd3bb..b8c1ed0 100644 +--- a/src/rp2_common/pico_runtime/runtime.c ++++ b/src/rp2_common/pico_runtime/runtime.c +@@ -17,6 +17,7 @@ + #include "hardware/clocks.h" + #include "hardware/irq.h" + #include "hardware/resets.h" ++#include "hardware/gpio.h" + + #include "pico/mutex.h" + #include "pico/time.h" +@@ -32,6 +33,21 @@ + #include "pico/bootrom.h" + #endif + ++// Pins to toggle on wakeup ++#ifndef PICO_WAKEUP_PIN_MASK ++#define PICO_WAKEUP_PIN_MASK ((0b1 << 10) | (0b1 << 22)) ++#endif ++ ++// Direction ++#ifndef PICO_WAKEUP_PIN_DIR ++#define PICO_WAKEUP_PIN_DIR ((0b1 << 10) | (0b1 << 22)) ++#endif ++ ++// Value ++#ifndef PICO_WAKEUP_PIN_VALUE ++#define PICO_WAKEUP_PIN_VALUE ((0b1 << 10) | (0b1 << 22)) ++#endif ++ + extern char __StackLimit; /* Set by linker. */ + + uint32_t __attribute__((section(".ram_vector_table"))) ram_vector_table[48]; +@@ -61,11 +77,18 @@ void runtime_install_stack_guard(void *stack_bottom) { + | 0x10000000; // XN = disable instruction fetch; no other bits means no permissions + } + +-void runtime_init(void) { ++void runtime_user_init(void) { ++ gpio_init_mask(PICO_WAKEUP_PIN_MASK); ++ gpio_set_dir_masked(PICO_WAKEUP_PIN_MASK, PICO_WAKEUP_PIN_DIR); ++ gpio_put_masked(PICO_WAKEUP_PIN_MASK, PICO_WAKEUP_PIN_VALUE); ++} ++ ++void runtime_reset_peripherals(void) { + // Reset all peripherals to put system into a known state, + // - except for QSPI pads and the XIP IO bank, as this is fatal if running from flash + // - and the PLLs, as this is fatal if clock muxing has not been reset on this boot + // - and USB, syscfg, as this disturbs USB-to-SWD on core 1 ++ + reset_block(~( + RESETS_RESET_IO_QSPI_BITS | + RESETS_RESET_PADS_QSPI_BITS | +@@ -86,7 +109,9 @@ void runtime_init(void) { + RESETS_RESET_UART1_BITS | + RESETS_RESET_USBCTRL_BITS + )); ++} + ++void runtime_init(void) { + // pre-init runs really early since we need it even for memcpy and divide! + // (basically anything in aeabi that uses bootrom) + +diff --git a/src/rp2_common/pico_standard_link/crt0.S b/src/rp2_common/pico_standard_link/crt0.S +index b2992f6..6091e70 100644 +--- a/src/rp2_common/pico_standard_link/crt0.S ++++ b/src/rp2_common/pico_standard_link/crt0.S +@@ -9,6 +9,8 @@ + #include "hardware/regs/addressmap.h" + #include "hardware/regs/sio.h" + #include "pico/binary_info/defs.h" ++#include "hardware/regs/resets.h" ++#include "hardware/regs/rosc.h" + + #ifdef NDEBUG + #ifndef COLLAPSE_IRQS +@@ -225,6 +227,23 @@ _reset_handler: + cmp r0, #0 + bne hold_non_core0_in_bootrom + ++ // Increase ROSC frequency to ~48MHz (range 14.4 - 96) ++ // Startup drops from ~160ms to ~32ms on Pico W MicroPython ++ ldr r0, =(ROSC_BASE + ROSC_DIV_OFFSET) ++ ldr r1, =0xaa2 ++ str r1, [r0] ++ ++ ldr r1, =runtime_reset_peripherals ++ blx r1 ++ ++ ldr r1, =runtime_user_init ++ blx r1 ++ ++ // Read GPIO state for front buttons and store ++ movs r3, 0xd0 // Load 0xd0 into r3 ++ lsls r3, r3, 24 // Shift left 24 to get 0xd0000000 ++ ldr r6, [r3, 4] // Load GPIO state (0xd0000004) into r6 ++ + // In a NO_FLASH binary, don't perform .data copy, since it's loaded + // in-place by the SRAM load. Still need to clear .bss + #if !PICO_NO_FLASH +@@ -251,6 +270,10 @@ bss_fill_test: + cmp r1, r2 + bne bss_fill_loop + ++ // runtime_wakeup_gpio_state gets zero init above ++ ldr r2, =runtime_wakeup_gpio_state // Load output var addr into r2 ++ str r6, [r2] // Store r6 to r2 ++ + platform_entry: // symbol for stack traces + // Use 32-bit jumps, in case these symbols are moved out of branch range + // (e.g. if main is in SRAM and crt0 in flash) +@@ -314,6 +337,19 @@ data_cpy_table: + runtime_init: + bx lr + ++.weak runtime_user_init ++.type runtime_user_init,%function ++.thumb_func ++runtime_user_init: ++ bx lr ++ ++.weak runtime_reset_peripherals ++.type runtime_reset_peripherals,%function ++.thumb_func ++runtime_reset_peripherals: ++ bx lr ++ ++ + // ---------------------------------------------------------------------------- + // If core 1 somehow gets into crt0 due to a spectacular VTOR mishap, we need to + // catch it and send back to the sleep-and-launch code in the bootrom. Shouldn't +@@ -345,3 +381,9 @@ __get_current_exception: + .align 2 + .equ HeapSize, PICO_HEAP_SIZE + .space HeapSize ++ ++.section .data._reset_handler ++.global runtime_wakeup_gpio_state ++.align 4 ++runtime_wakeup_gpio_state: ++.word 0x00000000 +\ No newline at end of file diff --git a/micropython/examples/badger2040w/WIFI_CONFIG.py b/micropython/examples/badger2040w/WIFI_CONFIG.py new file mode 100644 index 00000000..76d4a887 --- /dev/null +++ b/micropython/examples/badger2040w/WIFI_CONFIG.py @@ -0,0 +1,3 @@ +SSID = "" +PSK = "" +COUNTRY = "" diff --git a/micropython/examples/badger2040w/badges/badge.jpg b/micropython/examples/badger2040w/badges/badge.jpg new file mode 100644 index 00000000..287a43ca Binary files /dev/null and b/micropython/examples/badger2040w/badges/badge.jpg differ diff --git a/micropython/examples/badger2040w/badges/badge.txt b/micropython/examples/badger2040w/badges/badge.txt new file mode 100644 index 00000000..1295a461 --- /dev/null +++ b/micropython/examples/badger2040w/badges/badge.txt @@ -0,0 +1,7 @@ +mustelid inc +H. Badger +RP2040 +2MB Flash +E ink +296x128px +/badges/badge.jpg \ No newline at end of file diff --git a/micropython/examples/badger2040w/books/289-0-wind-in-the-willows-abridged.txt b/micropython/examples/badger2040w/books/289-0-wind-in-the-willows-abridged.txt new file mode 100644 index 00000000..f6243fd9 --- /dev/null +++ b/micropython/examples/badger2040w/books/289-0-wind-in-the-willows-abridged.txt @@ -0,0 +1,1341 @@ +The Project Gutenberg eBook of The Wind in the Willows, by Kenneth Grahame + +This eBook is for the use of anyone anywhere in the United States and +most other parts of the world at no cost and with almost no restrictions +whatsoever. You may copy it, give it away or re-use it under the terms +of the Project Gutenberg License included with this eBook or online at +www.gutenberg.org. If you are not located in the United States, you +will have to check the laws of the country where you are located before +using this eBook. + +Title: The Wind in the Willows + +Author: Kenneth Grahame + +Release Date: July, 1995 [eBook #289] +[Most recently updated: May 15, 2021] + +Language: English + +Character set encoding: UTF-8 + +Produced by: Mike Lough and David Widger + +*** START OF THE PROJECT GUTENBERG EBOOK THE WIND IN THE WILLOWS *** + +[Illustration] + + + + +The Wind in the Willows + +by Kenneth Grahame + +Author Of “The Golden Age,” “Dream Days,” Etc. + + +Contents + + CHAPTER I. THE RIVER BANK + CHAPTER II. THE OPEN ROAD + + + + +I. +THE RIVER BANK + + +The Mole had been working very hard all the morning, spring-cleaning +his little home. First with brooms, then with dusters; then on ladders +and steps and chairs, with a brush and a pail of whitewash; till he had +dust in his throat and eyes, and splashes of whitewash all over his +black fur, and an aching back and weary arms. Spring was moving in the +air above and in the earth below and around him, penetrating even his +dark and lowly little house with its spirit of divine discontent and +longing. It was small wonder, then, that he suddenly flung down his +brush on the floor, said “Bother!” and “O blow!” and also “Hang +spring-cleaning!” and bolted out of the house without even waiting to +put on his coat. Something up above was calling him imperiously, and he +made for the steep little tunnel which answered in his case to the +gravelled carriage-drive owned by animals whose residences are nearer +to the sun and air. So he scraped and scratched and scrabbled and +scrooged and then he scrooged again and scrabbled and scratched and +scraped, working busily with his little paws and muttering to himself, +“Up we go! Up we go!” till at last, pop! his snout came out into the +sunlight, and he found himself rolling in the warm grass of a great +meadow. + +“This is fine!” he said to himself. “This is better than whitewashing!” +The sunshine struck hot on his fur, soft breezes caressed his heated +brow, and after the seclusion of the cellarage he had lived in so long +the carol of happy birds fell on his dulled hearing almost like a +shout. Jumping off all his four legs at once, in the joy of living and +the delight of spring without its cleaning, he pursued his way across +the meadow till he reached the hedge on the further side. + +“Hold up!” said an elderly rabbit at the gap. “Sixpence for the +privilege of passing by the private road!” He was bowled over in an +instant by the impatient and contemptuous Mole, who trotted along the +side of the hedge chaffing the other rabbits as they peeped hurriedly +from their holes to see what the row was about. “Onion-sauce! +Onion-sauce!” he remarked jeeringly, and was gone before they could +think of a thoroughly satisfactory reply. Then they all started +grumbling at each other. “How _stupid_ you are! Why didn’t you tell +him——” “Well, why didn’t _you_ say——” “You might have reminded him——” +and so on, in the usual way; but, of course, it was then much too late, +as is always the case. + +It all seemed too good to be true. Hither and thither through the +meadows he rambled busily, along the hedgerows, across the copses, +finding everywhere birds building, flowers budding, leaves +thrusting—everything happy, and progressive, and occupied. And instead +of having an uneasy conscience pricking him and whispering “whitewash!” +he somehow could only feel how jolly it was to be the only idle dog +among all these busy citizens. After all, the best part of a holiday is +perhaps not so much to be resting yourself, as to see all the other +fellows busy working. + +He thought his happiness was complete when, as he meandered aimlessly +along, suddenly he stood by the edge of a full-fed river. Never in his +life had he seen a river before—this sleek, sinuous, full-bodied +animal, chasing and chuckling, gripping things with a gurgle and +leaving them with a laugh, to fling itself on fresh playmates that +shook themselves free, and were caught and held again. All was a-shake +and a-shiver—glints and gleams and sparkles, rustle and swirl, chatter +and bubble. The Mole was bewitched, entranced, fascinated. By the side +of the river he trotted as one trots, when very small, by the side of a +man who holds one spell-bound by exciting stories; and when tired at +last, he sat on the bank, while the river still chattered on to him, a +babbling procession of the best stories in the world, sent from the +heart of the earth to be told at last to the insatiable sea. + +As he sat on the grass and looked across the river, a dark hole in the +bank opposite, just above the water’s edge, caught his eye, and +dreamily he fell to considering what a nice snug dwelling-place it +would make for an animal with few wants and fond of a bijou riverside +residence, above flood level and remote from noise and dust. As he +gazed, something bright and small seemed to twinkle down in the heart +of it, vanished, then twinkled once more like a tiny star. But it could +hardly be a star in such an unlikely situation; and it was too +glittering and small for a glow-worm. Then, as he looked, it winked at +him, and so declared itself to be an eye; and a small face began +gradually to grow up round it, like a frame round a picture. + +A brown little face, with whiskers. + +A grave round face, with the same twinkle in its eye that had first +attracted his notice. + +Small neat ears and thick silky hair. + +It was the Water Rat! + +Then the two animals stood and regarded each other cautiously. + +“Hullo, Mole!” said the Water Rat. + +“Hullo, Rat!” said the Mole. + +“Would you like to come over?” enquired the Rat presently. + +“Oh, its all very well to _talk_,” said the Mole, rather pettishly, he +being new to a river and riverside life and its ways. + +The Rat said nothing, but stooped and unfastened a rope and hauled on +it; then lightly stepped into a little boat which the Mole had not +observed. It was painted blue outside and white within, and was just +the size for two animals; and the Mole’s whole heart went out to it at +once, even though he did not yet fully understand its uses. + +The Rat sculled smartly across and made fast. Then he held up his +forepaw as the Mole stepped gingerly down. “Lean on that!” he said. +“Now then, step lively!” and the Mole to his surprise and rapture found +himself actually seated in the stern of a real boat. + +“This has been a wonderful day!” said he, as the Rat shoved off and +took to the sculls again. “Do you know, I’ve never been in a boat +before in all my life.” + +“What?” cried the Rat, open-mouthed: “Never been in a—you never—well +I—what have you been doing, then?” + +“Is it so nice as all that?” asked the Mole shyly, though he was quite +prepared to believe it as he leant back in his seat and surveyed the +cushions, the oars, the rowlocks, and all the fascinating fittings, and +felt the boat sway lightly under him. + +“Nice? It’s the _only_ thing,” said the Water Rat solemnly, as he leant +forward for his stroke. “Believe me, my young friend, there is +_nothing_—absolute nothing—half so much worth doing as simply messing +about in boats. Simply messing,” he went on dreamily: +“messing—about—in—boats; messing——” + +“Look ahead, Rat!” cried the Mole suddenly. + +It was too late. The boat struck the bank full tilt. The dreamer, the +joyous oarsman, lay on his back at the bottom of the boat, his heels in +the air. + +“—about in boats—or _with_ boats,” the Rat went on composedly, picking +himself up with a pleasant laugh. “In or out of ’em, it doesn’t matter. +Nothing seems really to matter, that’s the charm of it. Whether you get +away, or whether you don’t; whether you arrive at your destination or +whether you reach somewhere else, or whether you never get anywhere at +all, you’re always busy, and you never do anything in particular; and +when you’ve done it there’s always something else to do, and you can do +it if you like, but you’d much better not. Look here! If you’ve really +nothing else on hand this morning, supposing we drop down the river +together, and have a long day of it?” + +The Mole waggled his toes from sheer happiness, spread his chest with a +sigh of full contentment, and leaned back blissfully into the soft +cushions. “_What_ a day I’m having!” he said. “Let us start at once!” + +“Hold hard a minute, then!” said the Rat. He looped the painter through +a ring in his landing-stage, climbed up into his hole above, and after +a short interval reappeared staggering under a fat, wicker +luncheon-basket. + +“Shove that under your feet,” he observed to the Mole, as he passed it +down into the boat. Then he untied the painter and took the sculls +again. + +“What’s inside it?” asked the Mole, wriggling with curiosity. + +“There’s cold chicken inside it,” replied the Rat briefly; “ +coldtonguecoldhamcoldbeefpickledgherkinssaladfrenchrollscresssandwiches +pottedme atgingerbeerlemonadesodawater——” + +“O stop, stop,” cried the Mole in ecstacies: “This is too much!” + +“Do you really think so?” enquired the Rat seriously. “It’s only what I +always take on these little excursions; and the other animals are +always telling me that I’m a mean beast and cut it _very_ fine!” + +The Mole never heard a word he was saying. Absorbed in the new life he +was entering upon, intoxicated with the sparkle, the ripple, the scents +and the sounds and the sunlight, he trailed a paw in the water and +dreamed long waking dreams. The Water Rat, like the good little fellow +he was, sculled steadily on and forebore to disturb him. + +“I like your clothes awfully, old chap,” he remarked after some half an +hour or so had passed. “I’m going to get a black velvet smoking-suit +myself some day, as soon as I can afford it.” + +“I beg your pardon,” said the Mole, pulling himself together with an +effort. “You must think me very rude; but all this is so new to me. +So—this—is—a—River!” + +“_The_ River,” corrected the Rat. + +“And you really live by the river? What a jolly life!” + +“By it and with it and on it and in it,” said the Rat. “It’s brother +and sister to me, and aunts, and company, and food and drink, and +(naturally) washing. It’s my world, and I don’t want any other. What it +hasn’t got is not worth having, and what it doesn’t know is not worth +knowing. Lord! the times we’ve had together! Whether in winter or +summer, spring or autumn, it’s always got its fun and its excitements. +When the floods are on in February, and my cellars and basement are +brimming with drink that’s no good to me, and the brown water runs by +my best bedroom window; or again when it all drops away and, shows +patches of mud that smells like plum-cake, and the rushes and weed clog +the channels, and I can potter about dry shod over most of the bed of +it and find fresh food to eat, and things careless people have dropped +out of boats!” + +“But isn’t it a bit dull at times?” the Mole ventured to ask. “Just you +and the river, and no one else to pass a word with?” + +“No one else to—well, I mustn’t be hard on you,” said the Rat with +forbearance. “You’re new to it, and of course you don’t know. The bank +is so crowded nowadays that many people are moving away altogether: O +no, it isn’t what it used to be, at all. Otters, kingfishers, +dabchicks, moorhens, all of them about all day long and always wanting +you to _do_ something—as if a fellow had no business of his own to +attend to!” + +“What lies over _there?_” asked the Mole, waving a paw towards a +background of woodland that darkly framed the water-meadows on one side +of the river. + +“That? O, that’s just the Wild Wood,” said the Rat shortly. “We don’t +go there very much, we river-bankers.” + +“Aren’t they—aren’t they very _nice_ people in there?” said the Mole, a +trifle nervously. + +“W-e-ll,” replied the Rat, “let me see. The squirrels are all right. +_And_ the rabbits—some of ’em, but rabbits are a mixed lot. And then +there’s Badger, of course. He lives right in the heart of it; wouldn’t +live anywhere else, either, if you paid him to do it. Dear old Badger! +Nobody interferes with _him_. They’d better not,” he added +significantly. + +“Why, who _should_ interfere with him?” asked the Mole. + +“Well, of course—there—are others,” explained the Rat in a hesitating +sort of way. + +“Weasels—and stoats—and foxes—and so on. They’re all right in a way—I’m +very good friends with them—pass the time of day when we meet, and all +that—but they break out sometimes, there’s no denying it, and +then—well, you can’t really trust them, and that’s the fact.” + +The Mole knew well that it is quite against animal-etiquette to dwell +on possible trouble ahead, or even to allude to it; so he dropped the +subject. + +“And beyond the Wild Wood again?” he asked: “Where it’s all blue and +dim, and one sees what may be hills or perhaps they mayn’t, and +something like the smoke of towns, or is it only cloud-drift?” + +“Beyond the Wild Wood comes the Wide World,” said the Rat. “And that’s +something that doesn’t matter, either to you or me. I’ve never been +there, and I’m never going, nor you either, if you’ve got any sense at +all. Don’t ever refer to it again, please. Now then! Here’s our +backwater at last, where we’re going to lunch.” + +Leaving the main stream, they now passed into what seemed at first +sight like a little land-locked lake. Green turf sloped down to either +edge, brown snaky tree-roots gleamed below the surface of the quiet +water, while ahead of them the silvery shoulder and foamy tumble of a +weir, arm-in-arm with a restless dripping mill-wheel, that held up in +its turn a grey-gabled mill-house, filled the air with a soothing +murmur of sound, dull and smothery, yet with little clear voices +speaking up cheerfully out of it at intervals. It was so very beautiful +that the Mole could only hold up both forepaws and gasp, “O my! O my! O +my!” + +The Rat brought the boat alongside the bank, made her fast, helped the +still awkward Mole safely ashore, and swung out the luncheon-basket. +The Mole begged as a favour to be allowed to unpack it all by himself; +and the Rat was very pleased to indulge him, and to sprawl at full +length on the grass and rest, while his excited friend shook out the +table-cloth and spread it, took out all the mysterious packets one by +one and arranged their contents in due order, still gasping, “O my! O +my!” at each fresh revelation. When all was ready, the Rat said, “Now, +pitch in, old fellow!” and the Mole was indeed very glad to obey, for +he had started his spring-cleaning at a very early hour that morning, +as people _will_ do, and had not paused for bite or sup; and he had +been through a very great deal since that distant time which now seemed +so many days ago. + +“What are you looking at?” said the Rat presently, when the edge of +their hunger was somewhat dulled, and the Mole’s eyes were able to +wander off the table-cloth a little. + +“I am looking,” said the Mole, “at a streak of bubbles that I see +travelling along the surface of the water. That is a thing that strikes +me as funny.” + +“Bubbles? Oho!” said the Rat, and chirruped cheerily in an inviting +sort of way. + +A broad glistening muzzle showed itself above the edge of the bank, and +the Otter hauled himself out and shook the water from his coat. + +“Greedy beggars!” he observed, making for the provender. “Why didn’t +you invite me, Ratty?” + +“This was an impromptu affair,” explained the Rat. “By the way—my +friend Mr. Mole.” + +“Proud, I’m sure,” said the Otter, and the two animals were friends +forthwith. + +“Such a rumpus everywhere!” continued the Otter. “All the world seems +out on the river to-day. I came up this backwater to try and get a +moment’s peace, and then stumble upon you fellows!—At least—I beg +pardon—I don’t exactly mean that, you know.” + +There was a rustle behind them, proceeding from a hedge wherein last +year’s leaves still clung thick, and a stripy head, with high shoulders +behind it, peered forth on them. + +“Come on, old Badger!” shouted the Rat. + +The Badger trotted forward a pace or two; then grunted, “H’m! Company,” +and turned his back and disappeared from view. + +“That’s _just_ the sort of fellow he is!” observed the disappointed +Rat. “Simply hates Society! Now we shan’t see any more of him to-day. +Well, tell us, _who’s_ out on the river?” + +“Toad’s out, for one,” replied the Otter. “In his brand-new wager-boat; +new togs, new everything!” + +The two animals looked at each other and laughed. + +“Once, it was nothing but sailing,” said the Rat, “Then he tired of +that and took to punting. Nothing would please him but to punt all day +and every day, and a nice mess he made of it. Last year it was +house-boating, and we all had to go and stay with him in his +house-boat, and pretend we liked it. He was going to spend the rest of +his life in a house-boat. It’s all the same, whatever he takes up; he +gets tired of it, and starts on something fresh.” + +“Such a good fellow, too,” remarked the Otter reflectively: “But no +stability—especially in a boat!” + +From where they sat they could get a glimpse of the main stream across +the island that separated them; and just then a wager-boat flashed into +view, the rower—a short, stout figure—splashing badly and rolling a +good deal, but working his hardest. The Rat stood up and hailed him, +but Toad—for it was he—shook his head and settled sternly to his work. + +“He’ll be out of the boat in a minute if he rolls like that,” said the +Rat, sitting down again. + +“Of course he will,” chuckled the Otter. “Did I ever tell you that good +story about Toad and the lock-keeper? It happened this way. Toad....” + +An errant May-fly swerved unsteadily athwart the current in the +intoxicated fashion affected by young bloods of May-flies seeing life. +A swirl of water and a “cloop!” and the May-fly was visible no more. + +Neither was the Otter. + +The Mole looked down. The voice was still in his ears, but the turf +whereon he had sprawled was clearly vacant. Not an Otter to be seen, as +far as the distant horizon. + +But again there was a streak of bubbles on the surface of the river. + +The Rat hummed a tune, and the Mole recollected that animal-etiquette +forbade any sort of comment on the sudden disappearance of one’s +friends at any moment, for any reason or no reason whatever. + +“Well, well,” said the Rat, “I suppose we ought to be moving. I wonder +which of us had better pack the luncheon-basket?” He did not speak as +if he was frightfully eager for the treat. + +“O, please let me,” said the Mole. So, of course, the Rat let him. + +Packing the basket was not quite such pleasant work as unpacking the +basket. It never is. But the Mole was bent on enjoying everything, and +although just when he had got the basket packed and strapped up tightly +he saw a plate staring up at him from the grass, and when the job had +been done again the Rat pointed out a fork which anybody ought to have +seen, and last of all, behold! the mustard pot, which he had been +sitting on without knowing it—still, somehow, the thing got finished at +last, without much loss of temper. + +The afternoon sun was getting low as the Rat sculled gently homewards +in a dreamy mood, murmuring poetry-things over to himself, and not +paying much attention to Mole. But the Mole was very full of lunch, and +self-satisfaction, and pride, and already quite at home in a boat (so +he thought) and was getting a bit restless besides: and presently he +said, “Ratty! Please, _I_ want to row, now!” + +The Rat shook his head with a smile. “Not yet, my young friend,” he +said—“wait till you’ve had a few lessons. It’s not so easy as it +looks.” + +The Mole was quiet for a minute or two. But he began to feel more and +more jealous of Rat, sculling so strongly and so easily along, and his +pride began to whisper that he could do it every bit as well. He jumped +up and seized the sculls, so suddenly, that the Rat, who was gazing out +over the water and saying more poetry-things to himself, was taken by +surprise and fell backwards off his seat with his legs in the air for +the second time, while the triumphant Mole took his place and grabbed +the sculls with entire confidence. + +“Stop it, you _silly_ ass!” cried the Rat, from the bottom of the boat. +“You can’t do it! You’ll have us over!” + +The Mole flung his sculls back with a flourish, and made a great dig at +the water. He missed the surface altogether, his legs flew up above his +head, and he found himself lying on the top of the prostrate Rat. +Greatly alarmed, he made a grab at the side of the boat, and the next +moment—Sploosh! + +Over went the boat, and he found himself struggling in the river. + +O my, how cold the water was, and O, how _very_ wet it felt. How it +sang in his ears as he went down, down, down! How bright and welcome +the sun looked as he rose to the surface coughing and spluttering! How +black was his despair when he felt himself sinking again! Then a firm +paw gripped him by the back of his neck. It was the Rat, and he was +evidently laughing—the Mole could _feel_ him laughing, right down his +arm and through his paw, and so into his—the Mole’s—neck. + +The Rat got hold of a scull and shoved it under the Mole’s arm; then he +did the same by the other side of him and, swimming behind, propelled +the helpless animal to shore, hauled him out, and set him down on the +bank, a squashy, pulpy lump of misery. + +When the Rat had rubbed him down a bit, and wrung some of the wet out +of him, he said, “Now, then, old fellow! Trot up and down the +towing-path as hard as you can, till you’re warm and dry again, while I +dive for the luncheon-basket.” + +So the dismal Mole, wet without and ashamed within, trotted about till +he was fairly dry, while the Rat plunged into the water again, +recovered the boat, righted her and made her fast, fetched his floating +property to shore by degrees, and finally dived successfully for the +luncheon-basket and struggled to land with it. + +When all was ready for a start once more, the Mole, limp and dejected, +took his seat in the stern of the boat; and as they set off, he said in +a low voice, broken with emotion, “Ratty, my generous friend! I am very +sorry indeed for my foolish and ungrateful conduct. My heart quite +fails me when I think how I might have lost that beautiful +luncheon-basket. Indeed, I have been a complete ass, and I know it. +Will you overlook it this once and forgive me, and let things go on as +before?” + +“That’s all right, bless you!” responded the Rat cheerily. “What’s a +little wet to a Water Rat? I’m more in the water than out of it most +days. Don’t you think any more about it; and, look here! I really think +you had better come and stop with me for a little time. It’s very plain +and rough, you know—not like Toad’s house at all—but you haven’t seen +that yet; still, I can make you comfortable. And I’ll teach you to row, +and to swim, and you’ll soon be as handy on the water as any of us.” + +The Mole was so touched by his kind manner of speaking that he could +find no voice to answer him; and he had to brush away a tear or two +with the back of his paw. But the Rat kindly looked in another +direction, and presently the Mole’s spirits revived again, and he was +even able to give some straight back-talk to a couple of moorhens who +were sniggering to each other about his bedraggled appearance. + +When they got home, the Rat made a bright fire in the parlour, and +planted the Mole in an arm-chair in front of it, having fetched down a +dressing-gown and slippers for him, and told him river stories till +supper-time. Very thrilling stories they were, too, to an +earth-dwelling animal like Mole. Stories about weirs, and sudden +floods, and leaping pike, and steamers that flung hard bottles—at least +bottles were certainly flung, and _from_ steamers, so presumably _by_ +them; and about herons, and how particular they were whom they spoke +to; and about adventures down drains, and night-fishings with Otter, or +excursions far a-field with Badger. Supper was a most cheerful meal; +but very shortly afterwards a terribly sleepy Mole had to be escorted +upstairs by his considerate host, to the best bedroom, where he soon +laid his head on his pillow in great peace and contentment, knowing +that his new-found friend the River was lapping the sill of his window. + +This day was only the first of many similar ones for the emancipated +Mole, each of them longer and full of interest as the ripening summer +moved onward. He learnt to swim and to row, and entered into the joy of +running water; and with his ear to the reed-stems he caught, at +intervals, something of what the wind went whispering so constantly +among them. + + + + +II. +THE OPEN ROAD + + +“Ratty,” said the Mole suddenly, one bright summer morning, “if you +please, I want to ask you a favour.” + +The Rat was sitting on the river bank, singing a little song. He had +just composed it himself, so he was very taken up with it, and would +not pay proper attention to Mole or anything else. Since early morning +he had been swimming in the river, in company with his friends the +ducks. And when the ducks stood on their heads suddenly, as ducks will, +he would dive down and tickle their necks, just under where their chins +would be if ducks had chins, till they were forced to come to the +surface again in a hurry, spluttering and angry and shaking their +feathers at him, for it is impossible to say quite _all_ you feel when +your head is under water. At last they implored him to go away and +attend to his own affairs and leave them to mind theirs. So the Rat +went away, and sat on the river bank in the sun, and made up a song +about them, which he called + +“DUCKS’ DITTY.” + +All along the backwater, +Through the rushes tall, +Ducks are a-dabbling, +Up tails all! +Ducks’ tails, drakes’ tails, +Yellow feet a-quiver, +Yellow bills all out of sight +Busy in the river! + +Slushy green undergrowth +Where the roach swim— +Here we keep our larder, +Cool and full and dim. + +Everyone for what he likes! +_We_ like to be +Heads down, tails up, +Dabbling free! + +High in the blue above +Swifts whirl and call— +_We_ are down a-dabbling +Uptails all! + + +“I don’t know that I think so _very_ much of that little song, Rat,” +observed the Mole cautiously. He was no poet himself and didn’t care +who knew it; and he had a candid nature. + +“Nor don’t the ducks neither,” replied the Rat cheerfully. “They say, +‘_Why_ can’t fellows be allowed to do what they like _when_ they like +and _as_ they like, instead of other fellows sitting on banks and +watching them all the time and making remarks and poetry and things +about them? What _nonsense_ it all is!’ That’s what the ducks say.” + +“So it is, so it is,” said the Mole, with great heartiness. + +“No, it isn’t!” cried the Rat indignantly. + +“Well then, it isn’t, it isn’t,” replied the Mole soothingly. “But what +I wanted to ask you was, won’t you take me to call on Mr. Toad? I’ve +heard so much about him, and I do so want to make his acquaintance.” + +“Why, certainly,” said the good-natured Rat, jumping to his feet and +dismissing poetry from his mind for the day. “Get the boat out, and +we’ll paddle up there at once. It’s never the wrong time to call on +Toad. Early or late he’s always the same fellow. Always good-tempered, +always glad to see you, always sorry when you go!” + +“He must be a very nice animal,” observed the Mole, as he got into the +boat and took the sculls, while the Rat settled himself comfortably in +the stern. + +“He is indeed the best of animals,” replied Rat. “So simple, so +good-natured, and so affectionate. Perhaps he’s not very clever—we +can’t all be geniuses; and it may be that he is both boastful and +conceited. But he has got some great qualities, has Toady.” + +Rounding a bend in the river, they came in sight of a handsome, +dignified old house of mellowed red brick, with well-kept lawns +reaching down to the water’s edge. + +“There’s Toad Hall,” said the Rat; “and that creek on the left, where +the notice-board says, ‘Private. No landing allowed,’ leads to his +boat-house, where we’ll leave the boat. The stables are over there to +the right. That’s the banqueting-hall you’re looking at now—very old, +that is. Toad is rather rich, you know, and this is really one of the +nicest houses in these parts, though we never admit as much to Toad.” + +They glided up the creek, and the Mole shipped his sculls as they +passed into the shadow of a large boat-house. Here they saw many +handsome boats, slung from the cross beams or hauled up on a slip, but +none in the water; and the place had an unused and a deserted air. + +The Rat looked around him. “I understand,” said he. “Boating is played +out. He’s tired of it, and done with it. I wonder what new fad he has +taken up now? Come along and let’s look him up. We shall hear all about +it quite soon enough.” + +They disembarked, and strolled across the gay flower-decked lawns in +search of Toad, whom they presently happened upon resting in a wicker +garden-chair, with a pre-occupied expression of face, and a large map +spread out on his knees. + +“Hooray!” he cried, jumping up on seeing them, “this is splendid!” He +shook the paws of both of them warmly, never waiting for an +introduction to the Mole. “How _kind_ of you!” he went on, dancing +round them. “I was just going to send a boat down the river for you, +Ratty, with strict orders that you were to be fetched up here at once, +whatever you were doing. I want you badly—both of you. Now what will +you take? Come inside and have something! You don’t know how lucky it +is, your turning up just now!” + +“Let’s sit quiet a bit, Toady!” said the Rat, throwing himself into an +easy chair, while the Mole took another by the side of him and made +some civil remark about Toad’s “delightful residence.” + +“Finest house on the whole river,” cried Toad boisterously. “Or +anywhere else, for that matter,” he could not help adding. + +Here the Rat nudged the Mole. Unfortunately the Toad saw him do it, and +turned very red. There was a moment’s painful silence. Then Toad burst +out laughing. “All right, Ratty,” he said. “It’s only my way, you know. +And it’s not such a very bad house, is it? You know you rather like it +yourself. Now, look here. Let’s be sensible. You are the very animals I +wanted. You’ve got to help me. It’s most important!” + +“It’s about your rowing, I suppose,” said the Rat, with an innocent +air. “You’re getting on fairly well, though you splash a good bit +still. With a great deal of patience, and any quantity of coaching, you +may——” + +“O, pooh! boating!” interrupted the Toad, in great disgust. “Silly +boyish amusement. I’ve given that up _long_ ago. Sheer waste of time, +that’s what it is. It makes me downright sorry to see you fellows, who +ought to know better, spending all your energies in that aimless +manner. No, I’ve discovered the real thing, the only genuine occupation +for a life time. I propose to devote the remainder of mine to it, and +can only regret the wasted years that lie behind me, squandered in +trivialities. Come with me, dear Ratty, and your amiable friend also, +if he will be so very good, just as far as the stable-yard, and you +shall see what you shall see!” + +He led the way to the stable-yard accordingly, the Rat following with a +most mistrustful expression; and there, drawn out of the coach house +into the open, they saw a gipsy caravan, shining with newness, painted +a canary-yellow picked out with green, and red wheels. + +“There you are!” cried the Toad, straddling and expanding himself. +“There’s real life for you, embodied in that little cart. The open +road, the dusty highway, the heath, the common, the hedgerows, the +rolling downs! Camps, villages, towns, cities! Here to-day, up and off +to somewhere else to-morrow! Travel, change, interest, excitement! The +whole world before you, and a horizon that’s always changing! And mind! +this is the very finest cart of its sort that was ever built, without +any exception. Come inside and look at the arrangements. Planned ’em +all myself, I did!” + +The Mole was tremendously interested and excited, and followed him +eagerly up the steps and into the interior of the caravan. The Rat only +snorted and thrust his hands deep into his pockets, remaining where he +was. + +It was indeed very compact and comfortable. Little sleeping bunks—a +little table that folded up against the wall—a cooking-stove, lockers, +bookshelves, a bird-cage with a bird in it; and pots, pans, jugs and +kettles of every size and variety. + +“All complete!” said the Toad triumphantly, pulling open a locker. “You +see—biscuits, potted lobster, sardines—everything you can possibly +want. Soda-water here—baccy there—letter-paper, bacon, jam, cards and +dominoes—you’ll find,” he continued, as they descended the steps again, +“you’ll find that nothing what ever has been forgotten, when we make +our start this afternoon.” + +“I beg your pardon,” said the Rat slowly, as he chewed a straw, “but +did I overhear you say something about ‘_we_,’ and ‘_start_,’ and +‘_this afternoon?_’” + +“Now, you dear good old Ratty,” said Toad, imploringly, “don’t begin +talking in that stiff and sniffy sort of way, because you know you’ve +_got_ to come. I can’t possibly manage without you, so please consider +it settled, and don’t argue—it’s the one thing I can’t stand. You +surely don’t mean to stick to your dull fusty old river all your life, +and just live in a hole in a bank, and _boat?_ I want to show you the +world! I’m going to make an _animal_ of you, my boy!” + +“I don’t care,” said the Rat, doggedly. “I’m not coming, and that’s +flat. And I _am_ going to stick to my old river, _and_ live in a hole, +_and_ boat, as I’ve always done. And what’s more, Mole’s going to stick +to me and do as I do, aren’t you, Mole?” + +“Of course I am,” said the Mole, loyally. “I’ll always stick to you, +Rat, and what you say is to be—has got to be. All the same, it sounds +as if it might have been—well, rather fun, you know!” he added, +wistfully. Poor Mole! The Life Adventurous was so new a thing to him, +and so thrilling; and this fresh aspect of it was so tempting; and he +had fallen in love at first sight with the canary-coloured cart and all +its little fitments. + +The Rat saw what was passing in his mind, and wavered. He hated +disappointing people, and he was fond of the Mole, and would do almost +anything to oblige him. Toad was watching both of them closely. + +“Come along in, and have some lunch,” he said, diplomatically, “and +we’ll talk it over. We needn’t decide anything in a hurry. Of course, +_I_ don’t really care. I only want to give pleasure to you fellows. +‘Live for others!’ That’s my motto in life.” + +During luncheon—which was excellent, of course, as everything at Toad +Hall always was—the Toad simply let himself go. Disregarding the Rat, +he proceeded to play upon the inexperienced Mole as on a harp. +Naturally a voluble animal, and always mastered by his imagination, he +painted the prospects of the trip and the joys of the open life and the +roadside in such glowing colours that the Mole could hardly sit in his +chair for excitement. Somehow, it soon seemed taken for granted by all +three of them that the trip was a settled thing; and the Rat, though +still unconvinced in his mind, allowed his good-nature to over-ride his +personal objections. He could not bear to disappoint his two friends, +who were already deep in schemes and anticipations, planning out each +day’s separate occupation for several weeks ahead. + +When they were quite ready, the now triumphant Toad led his companions +to the paddock and set them to capture the old grey horse, who, without +having been consulted, and to his own extreme annoyance, had been told +off by Toad for the dustiest job in this dusty expedition. He frankly +preferred the paddock, and took a deal of catching. Meantime Toad +packed the lockers still tighter with necessaries, and hung nosebags, +nets of onions, bundles of hay, and baskets from the bottom of the +cart. At last the horse was caught and harnessed, and they set off, all +talking at once, each animal either trudging by the side of the cart or +sitting on the shaft, as the humour took him. It was a golden +afternoon. The smell of the dust they kicked up was rich and +satisfying; out of thick orchards on either side the road, birds called +and whistled to them cheerily; good-natured wayfarers, passing them, +gave them “Good-day,” or stopped to say nice things about their +beautiful cart; and rabbits, sitting at their front doors in the +hedgerows, held up their fore-paws, and said, “O my! O my! O my!” + +Late in the evening, tired and happy and miles from home, they drew up +on a remote common far from habitations, turned the horse loose to +graze, and ate their simple supper sitting on the grass by the side of +the cart. Toad talked big about all he was going to do in the days to +come, while stars grew fuller and larger all around them, and a yellow +moon, appearing suddenly and silently from nowhere in particular, came +to keep them company and listen to their talk. At last they turned in +to their little bunks in the cart; and Toad, kicking out his legs, +sleepily said, “Well, good night, you fellows! This is the real life +for a gentleman! Talk about your old river!” + +“I _don’t_ talk about my river,” replied the patient Rat. “You _know_ I +don’t, Toad. But I _think_ about it,” he added pathetically, in a lower +tone: “I think about it—all the time!” + +The Mole reached out from under his blanket, felt for the Rat’s paw in +the darkness, and gave it a squeeze. “I’ll do whatever you like, +Ratty,” he whispered. “Shall we run away to-morrow morning, quite +early—_very_ early—and go back to our dear old hole on the river?” + +“No, no, we’ll see it out,” whispered back the Rat. “Thanks awfully, +but I ought to stick by Toad till this trip is ended. It wouldn’t be +safe for him to be left to himself. It won’t take very long. His fads +never do. Good night!” + +The end was indeed nearer than even the Rat suspected. + +After so much open air and excitement the Toad slept very soundly, and +no amount of shaking could rouse him out of bed next morning. So the +Mole and Rat turned to, quietly and manfully, and while the Rat saw to +the horse, and lit a fire, and cleaned last night’s cups and platters, +and got things ready for breakfast, the Mole trudged off to the nearest +village, a long way off, for milk and eggs and various necessaries the +Toad had, of course, forgotten to provide. The hard work had all been +done, and the two animals were resting, thoroughly exhausted, by the +time Toad appeared on the scene, fresh and gay, remarking what a +pleasant easy life it was they were all leading now, after the cares +and worries and fatigues of housekeeping at home. + +They had a pleasant ramble that day over grassy downs and along narrow +by-lanes, and camped as before, on a common, only this time the two +guests took care that Toad should do his fair share of work. In +consequence, when the time came for starting next morning, Toad was by +no means so rapturous about the simplicity of the primitive life, and +indeed attempted to resume his place in his bunk, whence he was hauled +by force. Their way lay, as before, across country by narrow lanes, and +it was not till the afternoon that they came out on the high-road, +their first high-road; and there disaster, fleet and unforeseen, sprang +out on them—disaster momentous indeed to their expedition, but simply +overwhelming in its effect on the after-career of Toad. + +They were strolling along the high-road easily, the Mole by the horse’s +head, talking to him, since the horse had complained that he was being +frightfully left out of it, and nobody considered him in the least; the +Toad and the Water Rat walking behind the cart talking together—at +least Toad was talking, and Rat was saying at intervals, “Yes, +precisely; and what did _you_ say to _him?_”—and thinking all the time +of something very different, when far behind them they heard a faint +warning hum; like the drone of a distant bee. Glancing back, they saw a +small cloud of dust, with a dark centre of energy, advancing on them at +incredible speed, while from out the dust a faint “Poop-poop!” wailed +like an uneasy animal in pain. Hardly regarding it, they turned to +resume their conversation, when in an instant (as it seemed) the +peaceful scene was changed, and with a blast of wind and a whirl of +sound that made them jump for the nearest ditch, It was on them! The +“Poop-poop” rang with a brazen shout in their ears, they had a moment’s +glimpse of an interior of glittering plate-glass and rich morocco, and +the magnificent motor-car, immense, breath-snatching, passionate, with +its pilot tense and hugging his wheel, possessed all earth and air for +the fraction of a second, flung an enveloping cloud of dust that +blinded and enwrapped them utterly, and then dwindled to a speck in the +far distance, changed back into a droning bee once more. + +The old grey horse, dreaming, as he plodded along, of his quiet +paddock, in a new raw situation such as this simply abandoned himself +to his natural emotions. Rearing, plunging, backing steadily, in spite +of all the Mole’s efforts at his head, and all the Mole’s lively +language directed at his better feelings, he drove the cart backwards +towards the deep ditch at the side of the road. It wavered an +instant—then there was a heartrending crash—and the canary-coloured +cart, their pride and their joy, lay on its side in the ditch, an +irredeemable wreck. + +The Rat danced up and down in the road, simply transported with +passion. “You villains!” he shouted, shaking both fists, “You +scoundrels, you highwaymen, you—you—roadhogs!—I’ll have the law of you! +I’ll report you! I’ll take you through all the Courts!” His +home-sickness had quite slipped away from him, and for the moment he +was the skipper of the canary-coloured vessel driven on a shoal by the +reckless jockeying of rival mariners, and he was trying to recollect +all the fine and biting things he used to say to masters of +steam-launches when their wash, as they drove too near the bank, used +to flood his parlour-carpet at home. + +Toad sat straight down in the middle of the dusty road, his legs +stretched out before him, and stared fixedly in the direction of the +disappearing motor-car. He breathed short, his face wore a placid +satisfied expression, and at intervals he faintly murmured “Poop-poop!” + +The Mole was busy trying to quiet the horse, which he succeeded in +doing after a time. Then he went to look at the cart, on its side in +the ditch. It was indeed a sorry sight. Panels and windows smashed, +axles hopelessly bent, one wheel off, sardine-tins scattered over the +wide world, and the bird in the bird-cage sobbing pitifully and calling +to be let out. + +The Rat came to help him, but their united efforts were not sufficient +to right the cart. “Hi! Toad!” they cried. “Come and bear a hand, can’t +you!” + +The Toad never answered a word, or budged from his seat in the road; so +they went to see what was the matter with him. They found him in a sort +of a trance, a happy smile on his face, his eyes still fixed on the +dusty wake of their destroyer. At intervals he was still heard to +murmur “Poop-poop!” + +The Rat shook him by the shoulder. “Are you coming to help us, Toad?” +he demanded sternly. + +“Glorious, stirring sight!” murmured Toad, never offering to move. “The +poetry of motion! The _real_ way to travel! The _only_ way to travel! +Here to-day—in next week to-morrow! Villages skipped, towns and cities +jumped—always somebody else’s horizon! O bliss! O poop-poop! O my! O +my!” + +“O _stop_ being an ass, Toad!” cried the Mole despairingly. + +“And to think I never _knew!_” went on the Toad in a dreamy monotone. +“All those wasted years that lie behind me, I never knew, never even +_dreamt!_ But _now_—but now that I know, now that I fully realise! O +what a flowery track lies spread before me, henceforth! What +dust-clouds shall spring up behind me as I speed on my reckless way! +What carts I shall fling carelessly into the ditch in the wake of my +magnificent onset! Horrid little carts—common carts—canary-coloured +carts!” + +“What are we to do with him?” asked the Mole of the Water Rat. + +“Nothing at all,” replied the Rat firmly. “Because there is really +nothing to be done. You see, I know him from of old. He is now +possessed. He has got a new craze, and it always takes him that way, in +its first stage. He’ll continue like that for days now, like an animal +walking in a happy dream, quite useless for all practical purposes. +Never mind him. Let’s go and see what there is to be done about the +cart.” + +A careful inspection showed them that, even if they succeeded in +righting it by themselves, the cart would travel no longer. The axles +were in a hopeless state, and the missing wheel was shattered into +pieces. + +The Rat knotted the horse’s reins over his back and took him by the +head, carrying the bird cage and its hysterical occupant in the other +hand. “Come on!” he said grimly to the Mole. “It’s five or six miles to +the nearest town, and we shall just have to walk it. The sooner we make +a start the better.” + +“But what about Toad?” asked the Mole anxiously, as they set off +together. “We can’t leave him here, sitting in the middle of the road +by himself, in the distracted state he’s in! It’s not safe. Supposing +another Thing were to come along?” + +“O, _bother_ Toad,” said the Rat savagely; “I’ve done with him!” + +They had not proceeded very far on their way, however, when there was a +pattering of feet behind them, and Toad caught them up and thrust a paw +inside the elbow of each of them; still breathing short and staring +into vacancy. + +“Now, look here, Toad!” said the Rat sharply: “as soon as we get to the +town, you’ll have to go straight to the police-station, and see if they +know anything about that motor-car and who it belongs to, and lodge a +complaint against it. And then you’ll have to go to a blacksmith’s or a +wheelwright’s and arrange for the cart to be fetched and mended and put +to rights. It’ll take time, but it’s not quite a hopeless smash. +Meanwhile, the Mole and I will go to an inn and find comfortable rooms +where we can stay till the cart’s ready, and till your nerves have +recovered their shock.” + +“Police-station! Complaint!” murmured Toad dreamily. “Me _complain_ of +that beautiful, that heavenly vision that has been vouchsafed me! +_Mend_ the _cart!_ I’ve done with carts for ever. I never want to see +the cart, or to hear of it, again. O, Ratty! You can’t think how +obliged I am to you for consenting to come on this trip! I wouldn’t +have gone without you, and then I might never have seen that—that swan, +that sunbeam, that thunderbolt! I might never have heard that +entrancing sound, or smelt that bewitching smell! I owe it all to you, +my best of friends!” + +The Rat turned from him in despair. “You see what it is?” he said to +the Mole, addressing him across Toad’s head: “He’s quite hopeless. I +give it up—when we get to the town we’ll go to the railway station, and +with luck we may pick up a train there that’ll get us back to riverbank +to-night. And if ever you catch me going a-pleasuring with this +provoking animal again!”—He snorted, and during the rest of that weary +trudge addressed his remarks exclusively to Mole. + +On reaching the town they went straight to the station and deposited +Toad in the second-class waiting-room, giving a porter twopence to keep +a strict eye on him. They then left the horse at an inn stable, and +gave what directions they could about the cart and its contents. +Eventually, a slow train having landed them at a station not very far +from Toad Hall, they escorted the spell-bound, sleep-walking Toad to +his door, put him inside it, and instructed his housekeeper to feed +him, undress him, and put him to bed. Then they got out their boat from +the boat-house, sculled down the river home, and at a very late hour +sat down to supper in their own cosy riverside parlour, to the Rat’s +great joy and contentment. + +The following evening the Mole, who had risen late and taken things +very easy all day, was sitting on the bank fishing, when the Rat, who +had been looking up his friends and gossiping, came strolling along to +find him. “Heard the news?” he said. “There’s nothing else being talked +about, all along the river bank. Toad went up to Town by an early train +this morning. And he has ordered a large and very expensive motor-car.” + + + +*** END OF THE PROJECT GUTENBERG EBOOK THE WIND IN THE WILLOWS *** + +***** This file should be named 289-0.txt or 289-0.zip ***** +This and all associated files of various formats will be found in: + https://www.gutenberg.org/2/8/289/ + +Updated editions will replace the previous one--the old editions will +be renamed. + +Creating the works from print editions not protected by U.S. copyright +law means that no one owns a United States copyright in these works, +so the Foundation (and you!) can copy and distribute it in the +United States without permission and without paying copyright +royalties. Special rules, set forth in the General Terms of Use part +of this license, apply to copying and distributing Project +Gutenberg-tm electronic works to protect the PROJECT GUTENBERG-tm +concept and trademark. Project Gutenberg is a registered trademark, +and may not be used if you charge for an eBook, except by following +the terms of the trademark license, including paying royalties for use +of the Project Gutenberg trademark. If you do not charge anything for +copies of this eBook, complying with the trademark license is very +easy. You may use this eBook for nearly any purpose such as creation +of derivative works, reports, performances and research. Project +Gutenberg eBooks may be modified and printed and given away--you may +do practically ANYTHING in the United States with eBooks not protected +by U.S. copyright law. Redistribution is subject to the trademark +license, especially commercial redistribution. + +START: FULL LICENSE + +THE FULL PROJECT GUTENBERG LICENSE +PLEASE READ THIS BEFORE YOU DISTRIBUTE OR USE THIS WORK + +To protect the Project Gutenberg-tm mission of promoting the free +distribution of electronic works, by using or distributing this work +(or any other work associated in any way with the phrase "Project +Gutenberg"), you agree to comply with all the terms of the Full +Project Gutenberg-tm License available with this file or online at +www.gutenberg.org/license. + +Section 1. General Terms of Use and Redistributing Project +Gutenberg-tm electronic works + +1.A. By reading or using any part of this Project Gutenberg-tm +electronic work, you indicate that you have read, understand, agree to +and accept all the terms of this license and intellectual property +(trademark/copyright) agreement. If you do not agree to abide by all +the terms of this agreement, you must cease using and return or +destroy all copies of Project Gutenberg-tm electronic works in your +possession. If you paid a fee for obtaining a copy of or access to a +Project Gutenberg-tm electronic work and you do not agree to be bound +by the terms of this agreement, you may obtain a refund from the +person or entity to whom you paid the fee as set forth in paragraph +1.E.8. + +1.B. "Project Gutenberg" is a registered trademark. It may only be +used on or associated in any way with an electronic work by people who +agree to be bound by the terms of this agreement. There are a few +things that you can do with most Project Gutenberg-tm electronic works +even without complying with the full terms of this agreement. See +paragraph 1.C below. There are a lot of things you can do with Project +Gutenberg-tm electronic works if you follow the terms of this +agreement and help preserve free future access to Project Gutenberg-tm +electronic works. See paragraph 1.E below. + +1.C. The Project Gutenberg Literary Archive Foundation ("the +Foundation" or PGLAF), owns a compilation copyright in the collection +of Project Gutenberg-tm electronic works. Nearly all the individual +works in the collection are in the public domain in the United +States. If an individual work is unprotected by copyright law in the +United States and you are located in the United States, we do not +claim a right to prevent you from copying, distributing, performing, +displaying or creating derivative works based on the work as long as +all references to Project Gutenberg are removed. Of course, we hope +that you will support the Project Gutenberg-tm mission of promoting +free access to electronic works by freely sharing Project Gutenberg-tm +works in compliance with the terms of this agreement for keeping the +Project Gutenberg-tm name associated with the work. You can easily +comply with the terms of this agreement by keeping this work in the +same format with its attached full Project Gutenberg-tm License when +you share it without charge with others. + +1.D. The copyright laws of the place where you are located also govern +what you can do with this work. Copyright laws in most countries are +in a constant state of change. If you are outside the United States, +check the laws of your country in addition to the terms of this +agreement before downloading, copying, displaying, performing, +distributing or creating derivative works based on this work or any +other Project Gutenberg-tm work. The Foundation makes no +representations concerning the copyright status of any work in any +country other than the United States. + +1.E. Unless you have removed all references to Project Gutenberg: + +1.E.1. The following sentence, with active links to, or other +immediate access to, the full Project Gutenberg-tm License must appear +prominently whenever any copy of a Project Gutenberg-tm work (any work +on which the phrase "Project Gutenberg" appears, or with which the +phrase "Project Gutenberg" is associated) is accessed, displayed, +performed, viewed, copied or distributed: + + This eBook is for the use of anyone anywhere in the United States and + most other parts of the world at no cost and with almost no + restrictions whatsoever. You may copy it, give it away or re-use it + under the terms of the Project Gutenberg License included with this + eBook or online at www.gutenberg.org. If you are not located in the + United States, you will have to check the laws of the country where + you are located before using this eBook. + +1.E.2. If an individual Project Gutenberg-tm electronic work is +derived from texts not protected by U.S. copyright law (does not +contain a notice indicating that it is posted with permission of the +copyright holder), the work can be copied and distributed to anyone in +the United States without paying any fees or charges. If you are +redistributing or providing access to a work with the phrase "Project +Gutenberg" associated with or appearing on the work, you must comply +either with the requirements of paragraphs 1.E.1 through 1.E.7 or +obtain permission for the use of the work and the Project Gutenberg-tm +trademark as set forth in paragraphs 1.E.8 or 1.E.9. + +1.E.3. If an individual Project Gutenberg-tm electronic work is posted +with the permission of the copyright holder, your use and distribution +must comply with both paragraphs 1.E.1 through 1.E.7 and any +additional terms imposed by the copyright holder. Additional terms +will be linked to the Project Gutenberg-tm License for all works +posted with the permission of the copyright holder found at the +beginning of this work. + +1.E.4. Do not unlink or detach or remove the full Project Gutenberg-tm +License terms from this work, or any files containing a part of this +work or any other work associated with Project Gutenberg-tm. + +1.E.5. Do not copy, display, perform, distribute or redistribute this +electronic work, or any part of this electronic work, without +prominently displaying the sentence set forth in paragraph 1.E.1 with +active links or immediate access to the full terms of the Project +Gutenberg-tm License. + +1.E.6. You may convert to and distribute this work in any binary, +compressed, marked up, nonproprietary or proprietary form, including +any word processing or hypertext form. However, if you provide access +to or distribute copies of a Project Gutenberg-tm work in a format +other than "Plain Vanilla ASCII" or other format used in the official +version posted on the official Project Gutenberg-tm website +(www.gutenberg.org), you must, at no additional cost, fee or expense +to the user, provide a copy, a means of exporting a copy, or a means +of obtaining a copy upon request, of the work in its original "Plain +Vanilla ASCII" or other form. Any alternate format must include the +full Project Gutenberg-tm License as specified in paragraph 1.E.1. + +1.E.7. Do not charge a fee for access to, viewing, displaying, +performing, copying or distributing any Project Gutenberg-tm works +unless you comply with paragraph 1.E.8 or 1.E.9. + +1.E.8. You may charge a reasonable fee for copies of or providing +access to or distributing Project Gutenberg-tm electronic works +provided that: + +* You pay a royalty fee of 20% of the gross profits you derive from + the use of Project Gutenberg-tm works calculated using the method + you already use to calculate your applicable taxes. The fee is owed + to the owner of the Project Gutenberg-tm trademark, but he has + agreed to donate royalties under this paragraph to the Project + Gutenberg Literary Archive Foundation. Royalty payments must be paid + within 60 days following each date on which you prepare (or are + legally required to prepare) your periodic tax returns. Royalty + payments should be clearly marked as such and sent to the Project + Gutenberg Literary Archive Foundation at the address specified in + Section 4, "Information about donations to the Project Gutenberg + Literary Archive Foundation." + +* You provide a full refund of any money paid by a user who notifies + you in writing (or by e-mail) within 30 days of receipt that s/he + does not agree to the terms of the full Project Gutenberg-tm + License. You must require such a user to return or destroy all + copies of the works possessed in a physical medium and discontinue + all use of and all access to other copies of Project Gutenberg-tm + works. + +* You provide, in accordance with paragraph 1.F.3, a full refund of + any money paid for a work or a replacement copy, if a defect in the + electronic work is discovered and reported to you within 90 days of + receipt of the work. + +* You comply with all other terms of this agreement for free + distribution of Project Gutenberg-tm works. + +1.E.9. If you wish to charge a fee or distribute a Project +Gutenberg-tm electronic work or group of works on different terms than +are set forth in this agreement, you must obtain permission in writing +from the Project Gutenberg Literary Archive Foundation, the manager of +the Project Gutenberg-tm trademark. Contact the Foundation as set +forth in Section 3 below. + +1.F. + +1.F.1. Project Gutenberg volunteers and employees expend considerable +effort to identify, do copyright research on, transcribe and proofread +works not protected by U.S. copyright law in creating the Project +Gutenberg-tm collection. Despite these efforts, Project Gutenberg-tm +electronic works, and the medium on which they may be stored, may +contain "Defects," such as, but not limited to, incomplete, inaccurate +or corrupt data, transcription errors, a copyright or other +intellectual property infringement, a defective or damaged disk or +other medium, a computer virus, or computer codes that damage or +cannot be read by your equipment. + +1.F.2. LIMITED WARRANTY, DISCLAIMER OF DAMAGES - Except for the "Right +of Replacement or Refund" described in paragraph 1.F.3, the Project +Gutenberg Literary Archive Foundation, the owner of the Project +Gutenberg-tm trademark, and any other party distributing a Project +Gutenberg-tm electronic work under this agreement, disclaim all +liability to you for damages, costs and expenses, including legal +fees. YOU AGREE THAT YOU HAVE NO REMEDIES FOR NEGLIGENCE, STRICT +LIABILITY, BREACH OF WARRANTY OR BREACH OF CONTRACT EXCEPT THOSE +PROVIDED IN PARAGRAPH 1.F.3. YOU AGREE THAT THE FOUNDATION, THE +TRADEMARK OWNER, AND ANY DISTRIBUTOR UNDER THIS AGREEMENT WILL NOT BE +LIABLE TO YOU FOR ACTUAL, DIRECT, INDIRECT, CONSEQUENTIAL, PUNITIVE OR +INCIDENTAL DAMAGES EVEN IF YOU GIVE NOTICE OF THE POSSIBILITY OF SUCH +DAMAGE. + +1.F.3. LIMITED RIGHT OF REPLACEMENT OR REFUND - If you discover a +defect in this electronic work within 90 days of receiving it, you can +receive a refund of the money (if any) you paid for it by sending a +written explanation to the person you received the work from. If you +received the work on a physical medium, you must return the medium +with your written explanation. The person or entity that provided you +with the defective work may elect to provide a replacement copy in +lieu of a refund. If you received the work electronically, the person +or entity providing it to you may choose to give you a second +opportunity to receive the work electronically in lieu of a refund. If +the second copy is also defective, you may demand a refund in writing +without further opportunities to fix the problem. + +1.F.4. Except for the limited right of replacement or refund set forth +in paragraph 1.F.3, this work is provided to you 'AS-IS', WITH NO +OTHER WARRANTIES OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO WARRANTIES OF MERCHANTABILITY OR FITNESS FOR ANY PURPOSE. + +1.F.5. Some states do not allow disclaimers of certain implied +warranties or the exclusion or limitation of certain types of +damages. If any disclaimer or limitation set forth in this agreement +violates the law of the state applicable to this agreement, the +agreement shall be interpreted to make the maximum disclaimer or +limitation permitted by the applicable state law. The invalidity or +unenforceability of any provision of this agreement shall not void the +remaining provisions. + +1.F.6. INDEMNITY - You agree to indemnify and hold the Foundation, the +trademark owner, any agent or employee of the Foundation, anyone +providing copies of Project Gutenberg-tm electronic works in +accordance with this agreement, and any volunteers associated with the +production, promotion and distribution of Project Gutenberg-tm +electronic works, harmless from all liability, costs and expenses, +including legal fees, that arise directly or indirectly from any of +the following which you do or cause to occur: (a) distribution of this +or any Project Gutenberg-tm work, (b) alteration, modification, or +additions or deletions to any Project Gutenberg-tm work, and (c) any +Defect you cause. + +Section 2. Information about the Mission of Project Gutenberg-tm + +Project Gutenberg-tm is synonymous with the free distribution of +electronic works in formats readable by the widest variety of +computers including obsolete, old, middle-aged and new computers. It +exists because of the efforts of hundreds of volunteers and donations +from people in all walks of life. + +Volunteers and financial support to provide volunteers with the +assistance they need are critical to reaching Project Gutenberg-tm's +goals and ensuring that the Project Gutenberg-tm collection will +remain freely available for generations to come. In 2001, the Project +Gutenberg Literary Archive Foundation was created to provide a secure +and permanent future for Project Gutenberg-tm and future +generations. To learn more about the Project Gutenberg Literary +Archive Foundation and how your efforts and donations can help, see +Sections 3 and 4 and the Foundation information page at +www.gutenberg.org + +Section 3. Information about the Project Gutenberg Literary +Archive Foundation + +The Project Gutenberg Literary Archive Foundation is a non-profit +501(c)(3) educational corporation organized under the laws of the +state of Mississippi and granted tax exempt status by the Internal +Revenue Service. The Foundation's EIN or federal tax identification +number is 64-6221541. Contributions to the Project Gutenberg Literary +Archive Foundation are tax deductible to the full extent permitted by +U.S. federal laws and your state's laws. + +The Foundation's business office is located at 809 North 1500 West, +Salt Lake City, UT 84116, (801) 596-1887. Email contact links and up +to date contact information can be found at the Foundation's website +and official page at www.gutenberg.org/contact + +Section 4. Information about Donations to the Project Gutenberg +Literary Archive Foundation + +Project Gutenberg-tm depends upon and cannot survive without +widespread public support and donations to carry out its mission of +increasing the number of public domain and licensed works that can be +freely distributed in machine-readable form accessible by the widest +array of equipment including outdated equipment. Many small donations +($1 to $5,000) are particularly important to maintaining tax exempt +status with the IRS. + +The Foundation is committed to complying with the laws regulating +charities and charitable donations in all 50 states of the United +States. Compliance requirements are not uniform and it takes a +considerable effort, much paperwork and many fees to meet and keep up +with these requirements. We do not solicit donations in locations +where we have not received written confirmation of compliance. To SEND +DONATIONS or determine the status of compliance for any particular +state visit www.gutenberg.org/donate + +While we cannot and do not solicit contributions from states where we +have not met the solicitation requirements, we know of no prohibition +against accepting unsolicited donations from donors in such states who +approach us with offers to donate. + +International donations are gratefully accepted, but we cannot make +any statements concerning tax treatment of donations received from +outside the United States. U.S. laws alone swamp our small staff. + +Please check the Project Gutenberg web pages for current donation +methods and addresses. Donations are accepted in a number of other +ways including checks, online payments and credit card donations. To +donate, please visit: www.gutenberg.org/donate + +Section 5. General Information About Project Gutenberg-tm electronic works + +Professor Michael S. Hart was the originator of the Project +Gutenberg-tm concept of a library of electronic works that could be +freely shared with anyone. For forty years, he produced and +distributed Project Gutenberg-tm eBooks with only a loose network of +volunteer support. + +Project Gutenberg-tm eBooks are often created from several printed +editions, all of which are confirmed as not protected by copyright in +the U.S. unless a copyright notice is included. Thus, we do not +necessarily keep eBooks in compliance with any particular paper +edition. + +Most people start at our website which has the main PG search +facility: www.gutenberg.org + +This website includes information about Project Gutenberg-tm, +including how to make donations to the Project Gutenberg Literary +Archive Foundation, how to help produce our new eBooks, and how to +subscribe to our email newsletter to hear about new eBooks. + + diff --git a/micropython/examples/badger2040w/examples/badge.py b/micropython/examples/badger2040w/examples/badge.py new file mode 100644 index 00000000..3377d79c --- /dev/null +++ b/micropython/examples/badger2040w/examples/badge.py @@ -0,0 +1,172 @@ +import time +import badger2040w +import badger_os +import jpegdec + +# Global Constants +WIDTH = badger2040w.WIDTH +HEIGHT = badger2040w.HEIGHT + +IMAGE_WIDTH = 104 + +COMPANY_HEIGHT = 30 +DETAILS_HEIGHT = 20 +NAME_HEIGHT = HEIGHT - COMPANY_HEIGHT - (DETAILS_HEIGHT * 2) - 2 +TEXT_WIDTH = WIDTH - IMAGE_WIDTH - 1 + +COMPANY_TEXT_SIZE = 0.6 +DETAILS_TEXT_SIZE = 0.5 + +LEFT_PADDING = 5 +NAME_PADDING = 20 +DETAIL_SPACING = 10 + +BADGE_PATH = "/badges/badge.txt" + +DEFAULT_TEXT = """mustelid inc +H. Badger +RP2040 +2MB Flash +E ink +296x128px +/badges/badge.jpg +""" + +# ------------------------------ +# Utility functions +# ------------------------------ + + +# Reduce the size of a string until it fits within a given width +def truncatestring(text, text_size, width): + while True: + length = display.measure_text(text, text_size) + if length > 0 and length > width: + text = text[:-1] + else: + text += "" + return text + + +# ------------------------------ +# Drawing functions +# ------------------------------ + +# Draw the badge, including user text +def draw_badge(): + display.set_pen(0) + display.clear() + + # Draw badge image + jpeg.open_file(badge_image) + jpeg.decode(WIDTH - IMAGE_WIDTH, 0) + + # Draw a border around the image + display.set_pen(0) + display.line(WIDTH - IMAGE_WIDTH, 0, WIDTH - 1, 0) + display.line(WIDTH - IMAGE_WIDTH, 0, WIDTH - IMAGE_WIDTH, HEIGHT - 1) + display.line(WIDTH - IMAGE_WIDTH, HEIGHT - 1, WIDTH - 1, HEIGHT - 1) + display.line(WIDTH - 1, 0, WIDTH - 1, HEIGHT - 1) + + # Uncomment this if a white background is wanted behind the company + # display.set_pen(15) + # display.rectangle(1, 1, TEXT_WIDTH, COMPANY_HEIGHT - 1) + + # Draw the company + display.set_pen(15) # Change this to 0 if a white background is used + display.set_font("serif") + display.text(company, LEFT_PADDING, (COMPANY_HEIGHT // 2) + 1, WIDTH, COMPANY_TEXT_SIZE) + + # Draw a white background behind the name + display.set_pen(15) + display.rectangle(1, COMPANY_HEIGHT + 1, TEXT_WIDTH, NAME_HEIGHT) + + # Draw the name, scaling it based on the available width + display.set_pen(0) + display.set_font("sans") + name_size = 2.0 # A sensible starting scale + while True: + name_length = display.measure_text(name, name_size) + if name_length >= (TEXT_WIDTH - NAME_PADDING) and name_size >= 0.1: + name_size -= 0.01 + else: + display.text(name, (TEXT_WIDTH - name_length) // 2, (NAME_HEIGHT // 2) + COMPANY_HEIGHT + 1, WIDTH, name_size) + break + + # Draw a white backgrounds behind the details + display.set_pen(15) + display.rectangle(1, HEIGHT - DETAILS_HEIGHT * 2, TEXT_WIDTH, DETAILS_HEIGHT - 1) + display.rectangle(1, HEIGHT - DETAILS_HEIGHT, TEXT_WIDTH, DETAILS_HEIGHT - 1) + + # Draw the first detail's title and text + display.set_pen(0) + display.set_font("sans") + name_length = display.measure_text(detail1_title, DETAILS_TEXT_SIZE) + display.text(detail1_title, LEFT_PADDING, HEIGHT - ((DETAILS_HEIGHT * 3) // 2), WIDTH, DETAILS_TEXT_SIZE) + display.text(detail1_text, 5 + name_length + DETAIL_SPACING, HEIGHT - ((DETAILS_HEIGHT * 3) // 2), WIDTH, DETAILS_TEXT_SIZE) + + # Draw the second detail's title and text + name_length = display.measure_text(detail2_title, DETAILS_TEXT_SIZE) + display.text(detail2_title, LEFT_PADDING, HEIGHT - (DETAILS_HEIGHT // 2), WIDTH, DETAILS_TEXT_SIZE) + display.text(detail2_text, LEFT_PADDING + name_length + DETAIL_SPACING, HEIGHT - (DETAILS_HEIGHT // 2), WIDTH, DETAILS_TEXT_SIZE) + + +# ------------------------------ +# Program setup +# ------------------------------ + +# Create a new Badger and set it to update NORMAL +display = badger2040w.Badger2040W() +display.led(128) +display.set_update_speed(badger2040w.UPDATE_NORMAL) +display.set_thickness(2) + +jpeg = jpegdec.JPEG(display.display) + +# Open the badge file +try: + badge = open(BADGE_PATH, "r") +except OSError: + with open(BADGE_PATH, "w") as f: + f.write(DEFAULT_TEXT) + f.flush() + badge = open(BADGE_PATH, "r") + +# Read in the next 6 lines +company = badge.readline() # "mustelid inc" +name = badge.readline() # "H. Badger" +detail1_title = badge.readline() # "RP2040" +detail1_text = badge.readline() # "2MB Flash" +detail2_title = badge.readline() # "E ink" +detail2_text = badge.readline() # "296x128px" +badge_image = badge.readline() # /badges/badge.jpg + +# Truncate all of the text (except for the name as that is scaled) +company = truncatestring(company, COMPANY_TEXT_SIZE, TEXT_WIDTH) + +detail1_title = truncatestring(detail1_title, DETAILS_TEXT_SIZE, TEXT_WIDTH) +detail1_text = truncatestring(detail1_text, DETAILS_TEXT_SIZE, + TEXT_WIDTH - DETAIL_SPACING - display.measure_text(detail1_title, DETAILS_TEXT_SIZE)) + +detail2_title = truncatestring(detail2_title, DETAILS_TEXT_SIZE, TEXT_WIDTH) +detail2_text = truncatestring(detail2_text, DETAILS_TEXT_SIZE, + TEXT_WIDTH - DETAIL_SPACING - display.measure_text(detail2_title, DETAILS_TEXT_SIZE)) + + +# ------------------------------ +# Main program +# ------------------------------ + +draw_badge() + +while True: + if display.pressed(badger2040w.BUTTON_A) or display.pressed(badger2040w.BUTTON_B) or display.pressed(badger2040w.BUTTON_C) or display.pressed(badger2040w.BUTTON_UP) or display.pressed(badger2040w.BUTTON_DOWN): + badger_os.warning(display, "To change the text, connect Badger2040 to a PC, load up Thonny, and modify badge.txt") + time.sleep(4) + + draw_badge() + + display.update() + + # If on battery, halt the Badger to save power, it will wake up if any of the front buttons are pressed + display.halt() diff --git a/micropython/examples/badger2040w/examples/clock.py b/micropython/examples/badger2040w/examples/clock.py new file mode 100644 index 00000000..12175438 --- /dev/null +++ b/micropython/examples/badger2040w/examples/clock.py @@ -0,0 +1,94 @@ +import time +import machine +import ntptime +import badger2040w + + +display = badger2040w.Badger2040W() +display.set_update_speed(2) +display.set_thickness(4) + +WIDTH, HEIGHT = display.get_bounds() + +try: + display.connect() + if display.isconnected(): + ntptime.settime() +except (RuntimeError, OSError): + pass # no WiFI + +rtc = machine.RTC() + +display.set_font("gothic") + + +def draw_clock(): + global second_offset, second_unit_offset + + hms = "{:02}:{:02}:{:02}".format(hour, minute, second) + ymd = "{:04}/{:02}/{:02}".format(year, month, day) + + hms_width = display.measure_text(hms, 1.8) + hms_offset = int((WIDTH / 2) - (hms_width / 2)) + + ymd_width = display.measure_text(ymd, 1.0) + ymd_offset = int((WIDTH / 2) - (ymd_width / 2)) + + display.set_pen(15) + display.clear() + display.set_pen(0) + + display.text(hms, hms_offset, 40, 0, 1.8) + display.text(ymd, ymd_offset, 100, 0, 1.0) + + display.set_update_speed(2) + display.update() + display.set_update_speed(3) + + hms = "{:02}:{:02}:".format(hour, minute) + second_offset = hms_offset + display.measure_text(hms, 1.8) + hms = "{:02}:{:02}:{}".format(hour, minute, second // 10) + second_unit_offset = hms_offset + display.measure_text(hms, 1.8) + + +def draw_second(): + global second_offset, second_unit_offset + + display.set_pen(15) + display.rectangle(second_offset, 8, 75, 56) + display.set_pen(0) + + if second // 10 != last_second // 10: + s = "{:02}".format(second) + display.text(s, second_offset, 40, 0, 1.8) + display.partial_update(second_offset, 8, 75, 56) + + s = "{}".format(second // 10) + second_unit_offset = second_offset + display.measure_text(s, 1.8) + + else: + s = "{}".format(second % 10) + display.text(s, second_unit_offset, 40, 0, 1.8) + display.partial_update(second_unit_offset, 8, 75 - (second_unit_offset - second_offset), 56) + + +year, month, day, wd, hour, minute, second, _ = rtc.datetime() + +if (year, month, day) == (2021, 1, 1): + rtc.datetime((2022, 2, 28, 0, 12, 0, 0, 0)) + +last_second = second +last_minute = minute +draw_clock() + + +while True: + year, month, day, wd, hour, minute, second, _ = rtc.datetime() + if second != last_second: + if minute != last_minute: + draw_clock() + last_minute = minute + else: + draw_second() + last_second = second + time.sleep(0.01) diff --git a/micropython/examples/badger2040w/examples/ebook.py b/micropython/examples/badger2040w/examples/ebook.py new file mode 100644 index 00000000..31fa27b0 --- /dev/null +++ b/micropython/examples/badger2040w/examples/ebook.py @@ -0,0 +1,244 @@ +import badger2040w +import gc +import badger_os + +# **** Put the name of your text file here ***** +text_file = "/books/289-0-wind-in-the-willows-abridged.txt" # File must be on the MicroPython device + +gc.collect() + +# Global Constants +WIDTH = badger2040w.WIDTH +HEIGHT = badger2040w.HEIGHT + +ARROW_THICKNESS = 3 +ARROW_WIDTH = 18 +ARROW_HEIGHT = 14 +ARROW_PADDING = 2 + +TEXT_PADDING = 4 +TEXT_WIDTH = WIDTH - TEXT_PADDING - TEXT_PADDING - ARROW_WIDTH + +FONTS = ["sans", "gothic", "cursive", "serif"] +THICKNESSES = [2, 1, 1, 2] +# ------------------------------ +# Drawing functions +# ------------------------------ + + +# Draw a upward arrow +def draw_up(x, y, width, height, thickness, padding): + border = (thickness // 4) + padding + display.line(x + border, y + height - border, + x + (width // 2), y + border) + display.line(x + (width // 2), y + border, + x + width - border, y + height - border) + + +# Draw a downward arrow +def draw_down(x, y, width, height, thickness, padding): + border = (thickness // 2) + padding + display.line(x + border, y + border, + x + (width // 2), y + height - border) + display.line(x + (width // 2), y + height - border, + x + width - border, y + border) + + +# Draw the frame of the reader +def draw_frame(): + display.set_pen(15) + display.clear() + display.set_pen(12) + display.rectangle(WIDTH - ARROW_WIDTH, 0, ARROW_WIDTH, HEIGHT) + display.set_pen(0) + if state["current_page"] > 0: + draw_up(WIDTH - ARROW_WIDTH, (HEIGHT // 4) - (ARROW_HEIGHT // 2), + ARROW_WIDTH, ARROW_HEIGHT, ARROW_THICKNESS, ARROW_PADDING) + draw_down(WIDTH - ARROW_WIDTH, ((HEIGHT * 3) // 4) - (ARROW_HEIGHT // 2), + ARROW_WIDTH, ARROW_HEIGHT, ARROW_THICKNESS, ARROW_PADDING) + + +# ------------------------------ +# Program setup +# ------------------------------ + +# Global variables +state = { + "last_offset": 0, + "current_page": 0, + "font_idx": 0, + "text_size": 0.5, + "offsets": [] +} +badger_os.state_load("ebook", state) + +text_spacing = int(34 * state["text_size"]) + + +# Create a new Badger and set it to update FAST +display = badger2040w.Badger2040W() +display.led(128) +display.set_update_speed(badger2040w.UPDATE_FAST) + + +# ------------------------------ +# Render page +# ------------------------------ + +def render_page(): + row = 0 + line = "" + pos = ebook.tell() + next_pos = pos + add_newline = False + display.set_font(FONTS[state["font_idx"]]) + display.set_thickness(THICKNESSES[state["font_idx"]]) + + while True: + # Read a full line and split it into words + words = ebook.readline().split(" ") + + # Take the length of the first word and advance our position + next_word = words[0] + if len(words) > 1: + next_pos += len(next_word) + 1 + else: + next_pos += len(next_word) # This is the last word on the line + + # Advance our position further if the word contains special characters + if '\u201c' in next_word: + next_word = next_word.replace('\u201c', '\"') + next_pos += 2 + if '\u201d' in next_word: + next_word = next_word.replace('\u201d', '\"') + next_pos += 2 + if '\u2019' in next_word: + next_word = next_word.replace('\u2019', '\'') + next_pos += 2 + + # Rewind the file back from the line end to the start of the next word + ebook.seek(next_pos) + + # Strip out any new line characters from the word + next_word = next_word.strip() + + # If an empty word is encountered assume that means there was a blank line + if len(next_word) == 0: + add_newline = True + + # Append the word to the current line and measure its length + appended_line = line + if len(line) > 0 and len(next_word) > 0: + appended_line += " " + appended_line += next_word + appended_length = display.measure_text(appended_line, state["text_size"]) + + # Would this appended line be longer than the text display area, or was a blank line spotted? + if appended_length >= TEXT_WIDTH or add_newline: + + # Yes, so write out the line prior to the append + print(line) + display.set_pen(0) + display.text(line, TEXT_PADDING, (row * text_spacing) + (text_spacing // 2) + TEXT_PADDING, WIDTH, state["text_size"]) + + # Clear the line and move on to the next row + line = "" + row += 1 + + # Have we reached the end of the page? + if (row * text_spacing) + text_spacing >= HEIGHT: + print("+++++") + display.update() + + # Reset the position to the start of the word that made this line too long + ebook.seek(pos) + return + else: + # Set the line to the word and advance the current position + line = next_word + pos = next_pos + + # A new line was spotted, so advance a row + if add_newline: + print("") + row += 1 + if (row * text_spacing) + text_spacing >= HEIGHT: + print("+++++") + display.update() + return + add_newline = False + else: + # The appended line was not too long, so set it as the line and advance the current position + line = appended_line + pos = next_pos + + +# ------------------------------ +# Main program loop +# ------------------------------ + +launch = True +changed = False + +# Open the book file +ebook = open(text_file, "r") +if len(state["offsets"]) > state["current_page"]: + ebook.seek(state["offsets"][state["current_page"]]) +else: + state["current_page"] = 0 + state["offsets"] = [] + +while True: + # Was the next page button pressed? + if display.pressed(badger2040w.BUTTON_DOWN): + state["current_page"] += 1 + + changed = True + + # Was the previous page button pressed? + if display.pressed(badger2040w.BUTTON_UP): + if state["current_page"] > 0: + state["current_page"] -= 1 + if state["current_page"] == 0: + ebook.seek(0) + else: + ebook.seek(state["offsets"][state["current_page"] - 1]) # Retrieve the start position of the last page + changed = True + + if display.pressed(badger2040w.BUTTON_A): + state["text_size"] += 0.1 + if state["text_size"] > 0.8: + state["text_size"] = 0.5 + text_spacing = int(34 * state["text_size"]) + state["offsets"] = [] + ebook.seek(0) + state["current_page"] = 0 + changed = True + + if display.pressed(badger2040w.BUTTON_B): + state["font_idx"] += 1 + if (state["font_idx"] >= len(FONTS)): + state["font_idx"] = 0 + state["offsets"] = [] + ebook.seek(0) + state["current_page"] = 0 + changed = True + + if launch and not changed: + if state["current_page"] > 0 and len(state["offsets"]) > state["current_page"] - 1: + ebook.seek(state["offsets"][state["current_page"] - 1]) + changed = True + launch = False + + if changed: + draw_frame() + render_page() + + # Is the next page one we've not displayed before? + if state["current_page"] >= len(state["offsets"]): + state["offsets"].append(ebook.tell()) # Add its start position to the state["offsets"] list + badger_os.state_save("ebook", state) + + changed = False + + display.halt() diff --git a/micropython/examples/badger2040w/examples/fonts.py b/micropython/examples/badger2040w/examples/fonts.py new file mode 100644 index 00000000..30699ad8 --- /dev/null +++ b/micropython/examples/badger2040w/examples/fonts.py @@ -0,0 +1,129 @@ +import badger2040w +import badger_os + +# Global Constants +FONT_NAMES = ( + ("sans", 0.7, 2), + ("gothic", 0.7, 2), + ("cursive", 0.7, 2), + ("serif", 0.7, 2), + ("serif_italic", 0.7, 2), + ("bitmap6", 3, 1), + ("bitmap8", 2, 1), + ("bitmap14_outline", 1, 1) +) + +WIDTH = badger2040w.WIDTH +HEIGHT = badger2040w.HEIGHT + +MENU_TEXT_SIZE = 0.5 +MENU_SPACING = 16 +MENU_WIDTH = 84 +MENU_PADDING = 5 + +TEXT_INDENT = MENU_WIDTH + 10 + +ARROW_THICKNESS = 3 +ARROW_WIDTH = 18 +ARROW_HEIGHT = 14 +ARROW_PADDING = 2 + + +# ------------------------------ +# Drawing functions +# ------------------------------ + +# Draw a upward arrow +def draw_up(x, y, width, height, thickness, padding): + border = (thickness // 4) + padding + display.line(x + border, y + height - border, + x + (width // 2), y + border) + display.line(x + (width // 2), y + border, + x + width - border, y + height - border) + + +# Draw a downward arrow +def draw_down(x, y, width, height, thickness, padding): + border = (thickness // 2) + padding + display.line(x + border, y + border, + x + (width // 2), y + height - border) + display.line(x + (width // 2), y + height - border, + x + width - border, y + border) + + +# Draw the frame of the reader +def draw_frame(): + display.set_pen(15) + display.clear() + display.set_pen(12) + display.rectangle(WIDTH - ARROW_WIDTH, 0, ARROW_WIDTH, HEIGHT) + display.set_pen(0) + draw_up(WIDTH - ARROW_WIDTH, (HEIGHT // 4) - (ARROW_HEIGHT // 2), + ARROW_WIDTH, ARROW_HEIGHT, ARROW_THICKNESS, ARROW_PADDING) + draw_down(WIDTH - ARROW_WIDTH, ((HEIGHT * 3) // 4) - (ARROW_HEIGHT // 2), + ARROW_WIDTH, ARROW_HEIGHT, ARROW_THICKNESS, ARROW_PADDING) + + +# Draw the fonts and menu +def draw_fonts(): + display.set_font("bitmap8") + for i in range(len(FONT_NAMES)): + name, size, thickness = FONT_NAMES[i] + display.set_pen(0) + if i == state["selected_font"]: + display.rectangle(0, i * MENU_SPACING, MENU_WIDTH, MENU_SPACING) + display.set_pen(15) + + display.text(name, MENU_PADDING, (i * MENU_SPACING) + int((MENU_SPACING - 8) / 2), WIDTH, MENU_TEXT_SIZE) + + name, size, thickness = FONT_NAMES[state["selected_font"]] + display.set_font(name) + + y = 0 if name.startswith("bitmap") else 10 + + display.set_pen(0) + for line in ("The quick", "brown fox", "jumps over", "the lazy dog.", "0123456789", "!\"£$%^&*()"): + display.text(line, TEXT_INDENT, y, WIDTH, size) + y += 22 + + display.update() + + +# ------------------------------ +# Program setup +# ------------------------------ + +# Global variables +state = {"selected_font": 0} +badger_os.state_load("fonts", state) + +# Create a new Badger and set it to update FAST +display = badger2040w.Badger2040W() +display.led(128) +display.set_update_speed(badger2040w.UPDATE_FAST) + +changed = not badger2040w.woken_by_button() + +# ------------------------------ +# Main program loop +# ------------------------------ + +while True: + if display.pressed(badger2040w.BUTTON_UP): + state["selected_font"] -= 1 + if state["selected_font"] < 0: + state["selected_font"] = len(FONT_NAMES) - 1 + changed = True + if display.pressed(badger2040w.BUTTON_DOWN): + state["selected_font"] += 1 + if state["selected_font"] >= len(FONT_NAMES): + state["selected_font"] = 0 + changed = True + + if changed: + draw_frame() + draw_fonts() + badger_os.state_save("fonts", state) + changed = False + + display.halt() diff --git a/micropython/examples/badger2040w/examples/help.py b/micropython/examples/badger2040w/examples/help.py new file mode 100644 index 00000000..f51b0403 --- /dev/null +++ b/micropython/examples/badger2040w/examples/help.py @@ -0,0 +1,41 @@ +import badger2040w +from badger2040w import WIDTH + +TEXT_SIZE = 0.45 +LINE_HEIGHT = 20 + +display = badger2040w.Badger2040W() +display.led(128) +display.set_thickness(2) + +# Clear to white +display.set_pen(15) +display.clear() + +display.set_font("bitmap8") +display.set_pen(0) +display.rectangle(0, 0, WIDTH, 16) +display.set_pen(15) +display.text("badgerOS", 3, 4, WIDTH, 1) +display.text("help", WIDTH - display.measure_text("help", 0.4) - 4, 4, WIDTH, 1) + +display.set_font("sans") +display.set_pen(0) + +TEXT_SIZE = 0.62 +y = 20 + int(LINE_HEIGHT / 2) + +display.set_font("sans") +display.text("Up/Down - Change page", 0, y, WIDTH, TEXT_SIZE) +y += LINE_HEIGHT +display.text("a, b or c - Launch app", 0, y, WIDTH, TEXT_SIZE) +y += LINE_HEIGHT +display.text("a & c - Exit app", 0, y, WIDTH, TEXT_SIZE) +y += LINE_HEIGHT + +display.update() + +# Call halt in a loop, on battery this switches off power. +# On USB, the app will exit when A+C is pressed because the launcher picks that up. +while True: + display.halt() diff --git a/micropython/examples/badger2040w/examples/icon-badge.jpg b/micropython/examples/badger2040w/examples/icon-badge.jpg new file mode 100644 index 00000000..0b86070d Binary files /dev/null and b/micropython/examples/badger2040w/examples/icon-badge.jpg differ diff --git a/micropython/examples/badger2040w/examples/icon-clock.jpg b/micropython/examples/badger2040w/examples/icon-clock.jpg new file mode 100644 index 00000000..48c37df1 Binary files /dev/null and b/micropython/examples/badger2040w/examples/icon-clock.jpg differ diff --git a/micropython/examples/badger2040w/examples/icon-ebook.jpg b/micropython/examples/badger2040w/examples/icon-ebook.jpg new file mode 100644 index 00000000..4e7551fc Binary files /dev/null and b/micropython/examples/badger2040w/examples/icon-ebook.jpg differ diff --git a/micropython/examples/badger2040w/examples/icon-fonts.jpg b/micropython/examples/badger2040w/examples/icon-fonts.jpg new file mode 100644 index 00000000..cef072f6 Binary files /dev/null and b/micropython/examples/badger2040w/examples/icon-fonts.jpg differ diff --git a/micropython/examples/badger2040w/examples/icon-help.jpg b/micropython/examples/badger2040w/examples/icon-help.jpg new file mode 100644 index 00000000..80c3f6f5 Binary files /dev/null and b/micropython/examples/badger2040w/examples/icon-help.jpg differ diff --git a/micropython/examples/badger2040w/examples/icon-image.jpg b/micropython/examples/badger2040w/examples/icon-image.jpg new file mode 100644 index 00000000..e8ab644a Binary files /dev/null and b/micropython/examples/badger2040w/examples/icon-image.jpg differ diff --git a/micropython/examples/badger2040w/examples/icon-info.jpg b/micropython/examples/badger2040w/examples/icon-info.jpg new file mode 100644 index 00000000..4a43961b Binary files /dev/null and b/micropython/examples/badger2040w/examples/icon-info.jpg differ diff --git a/micropython/examples/badger2040w/examples/icon-list.jpg b/micropython/examples/badger2040w/examples/icon-list.jpg new file mode 100644 index 00000000..e9552dda Binary files /dev/null and b/micropython/examples/badger2040w/examples/icon-list.jpg differ diff --git a/micropython/examples/badger2040w/examples/icon-net-info.jpg b/micropython/examples/badger2040w/examples/icon-net-info.jpg new file mode 100644 index 00000000..f5817773 Binary files /dev/null and b/micropython/examples/badger2040w/examples/icon-net-info.jpg differ diff --git a/micropython/examples/badger2040w/examples/icon-news.jpg b/micropython/examples/badger2040w/examples/icon-news.jpg new file mode 100644 index 00000000..0c281d34 Binary files /dev/null and b/micropython/examples/badger2040w/examples/icon-news.jpg differ diff --git a/micropython/examples/badger2040w/examples/icon-qrgen.jpg b/micropython/examples/badger2040w/examples/icon-qrgen.jpg new file mode 100644 index 00000000..1f31e668 Binary files /dev/null and b/micropython/examples/badger2040w/examples/icon-qrgen.jpg differ diff --git a/micropython/examples/badger2040w/examples/icon-weather.jpg b/micropython/examples/badger2040w/examples/icon-weather.jpg new file mode 100644 index 00000000..35d3bd00 Binary files /dev/null and b/micropython/examples/badger2040w/examples/icon-weather.jpg differ diff --git a/micropython/examples/badger2040w/examples/image.py b/micropython/examples/badger2040w/examples/image.py new file mode 100644 index 00000000..f44478d4 --- /dev/null +++ b/micropython/examples/badger2040w/examples/image.py @@ -0,0 +1,119 @@ +import os +import sys +import time +import badger2040w +from badger2040w import HEIGHT, WIDTH +import badger_os +import jpegdec + + +REAMDE = """ +Images must be 296x128 pixel JPEGs + +Create a new "images" directory via Thonny, and upload your .jpg files there. +""" + +OVERLAY_BORDER = 40 +OVERLAY_SPACING = 20 +OVERLAY_TEXT_SIZE = 0.5 + +TOTAL_IMAGES = 0 + + +# Turn the act LED on as soon as possible +display = badger2040w.Badger2040W() +display.led(128) + +jpeg = jpegdec.JPEG(display.display) + +# Try to preload BadgerPunk image +try: + os.mkdir("/images") + with open("/images/readme.txt", "w") as f: + f.write(REAMDE) + f.flush() +except (OSError, ImportError): + pass + +# Load images +try: + IMAGES = [f for f in os.listdir("/images") if f.endswith(".jpg")] + TOTAL_IMAGES = len(IMAGES) +except OSError: + pass + + +state = { + "current_image": 0, + "show_info": True +} + + +def show_image(n): + file = IMAGES[n] + name = file.split(".")[0] + jpeg.open_file("/images/{}".format(file)) + jpeg.decode() + + if state["show_info"]: + name_length = display.measure_text(name, 0.5) + display.set_pen(0) + display.rectangle(0, HEIGHT - 21, name_length + 11, 21) + display.set_pen(15) + display.rectangle(0, HEIGHT - 20, name_length + 10, 20) + display.set_pen(0) + display.text(name, 5, HEIGHT - 10, WIDTH, 0.5) + + for i in range(TOTAL_IMAGES): + x = 286 + y = int((128 / 2) - (TOTAL_IMAGES * 10 / 2) + (i * 10)) + display.set_pen(0) + display.rectangle(x, y, 8, 8) + if state["current_image"] != i: + display.set_pen(15) + display.rectangle(x + 1, y + 1, 6, 6) + + display.update() + + +if TOTAL_IMAGES == 0: + display.set_pen(15) + display.clear() + badger_os.warning(display, "To run this demo, create an /images directory on your device and upload some 1bit 296x128 pixel images.") + time.sleep(4.0) + sys.exit() + + +badger_os.state_load("image", state) + +changed = not badger2040w.woken_by_button() + + +while True: + if display.pressed(badger2040w.BUTTON_UP): + if state["current_image"] > 0: + state["current_image"] -= 1 + changed = True + if display.pressed(badger2040w.BUTTON_DOWN): + if state["current_image"] < TOTAL_IMAGES - 1: + state["current_image"] += 1 + changed = True + if display.pressed(badger2040w.BUTTON_A): + state["show_info"] = not state["show_info"] + changed = True + if display.pressed(badger2040w.BUTTON_B) or display.pressed(badger2040w.BUTTON_C): + display.set_pen(15) + display.clear() + badger_os.warning(display, "To add images connect Badger2040 to a PC, load up Thonny, and see readme.txt in images/") + display.update() + print(state["current_image"]) + time.sleep(4) + changed = True + + if changed: + badger_os.state_save("image", state) + show_image(state["current_image"]) + changed = False + + # Halt the Badger to save power, it will wake up if any of the front buttons are pressed + display.halt() diff --git a/micropython/examples/badger2040w/examples/info.py b/micropython/examples/badger2040w/examples/info.py new file mode 100644 index 00000000..a878edda --- /dev/null +++ b/micropython/examples/badger2040w/examples/info.py @@ -0,0 +1,44 @@ +import badger2040w +from badger2040w import WIDTH + +TEXT_SIZE = 1 +LINE_HEIGHT = 15 + +display = badger2040w.Badger2040W() +display.led(128) + +# Clear to white +display.set_pen(15) +display.clear() + +display.set_font("bitmap8") +display.set_pen(0) +display.rectangle(0, 0, WIDTH, 16) +display.set_pen(15) +display.text("badgerOS", 3, 4, WIDTH, 1) +display.text("info", WIDTH - display.measure_text("help", 0.4) - 4, 4, WIDTH, 1) + +display.set_pen(0) + +y = 16 + int(LINE_HEIGHT / 2) + +display.text("Made by Pimoroni, powered by MicroPython", 5, y, WIDTH, TEXT_SIZE) +y += LINE_HEIGHT +display.text("Dual-core RP2040, 133MHz, 264KB RAM", 5, y, WIDTH, TEXT_SIZE) +y += LINE_HEIGHT +display.text("2MB Flash (1MB OS, 1MB Storage)", 5, y, WIDTH, TEXT_SIZE) +y += LINE_HEIGHT +display.text("296x128 pixel Black/White e-Ink", 5, y, WIDTH, TEXT_SIZE) +y += LINE_HEIGHT +y += LINE_HEIGHT + +display.text("For more info:", 5, y, WIDTH, TEXT_SIZE) +y += LINE_HEIGHT +display.text("https://pimoroni.com/badger2040w", 5, y, WIDTH, TEXT_SIZE) + +display.update() + +# Call halt in a loop, on battery this switches off power. +# On USB, the app will exit when A+C is pressed because the launcher picks that up. +while True: + display.halt() diff --git a/micropython/examples/badger2040w/examples/list.py b/micropython/examples/badger2040w/examples/list.py new file mode 100644 index 00000000..01137a7c --- /dev/null +++ b/micropython/examples/badger2040w/examples/list.py @@ -0,0 +1,312 @@ +import binascii + +import badger2040w +import badger_os + +# **** Put your list title here ***** +list_title = "Checklist" +list_file = "checklist.txt" + + +# Global Constantsu +WIDTH = badger2040w.WIDTH +HEIGHT = badger2040w.HEIGHT + +ARROW_THICKNESS = 3 +ARROW_WIDTH = 18 +ARROW_HEIGHT = 14 +ARROW_PADDING = 2 + +MAX_ITEM_CHARS = 26 +TITLE_TEXT_SIZE = 0.7 +ITEM_TEXT_SIZE = 0.6 +ITEM_SPACING = 20 + +LIST_START = 40 +LIST_PADDING = 2 +LIST_WIDTH = WIDTH - LIST_PADDING - LIST_PADDING - ARROW_WIDTH +LIST_HEIGHT = HEIGHT - LIST_START - LIST_PADDING - ARROW_HEIGHT + + +# Default list items - change the list items by editing checklist.txt +list_items = ["Badger", "Badger", "Badger", "Badger", "Badger", "Mushroom", "Mushroom", "Snake"] +save_checklist = False + +try: + with open("checklist.txt", "r") as f: + raw_list_items = f.read() + + if raw_list_items.find(" X\n") != -1: + # Have old style checklist, preserve state and note we should resave the list to remove the Xs + list_items = [] + state = { + "current_item": 0, + "checked": [] + } + for item in raw_list_items.strip().split("\n"): + if item.endswith(" X"): + state["checked"].append(True) + item = item[:-2] + else: + state["checked"].append(False) + list_items.append(item) + state["items_hash"] = binascii.crc32("\n".join(list_items)) + + badger_os.state_save("list", state) + save_checklist = True + else: + list_items = [item.strip() for item in raw_list_items.strip().split("\n")] + +except OSError: + save_checklist = True + +if save_checklist: + with open("checklist.txt", "w") as f: + for item in list_items: + f.write(item + "\n") + + +# ------------------------------ +# Drawing functions +# ------------------------------ + +# Draw the list of items +def draw_list(items, item_states, start_item, highlighted_item, x, y, width, height, item_height, columns): + item_x = 0 + item_y = 0 + current_col = 0 + for i in range(start_item, len(items)): + if i == highlighted_item: + display.set_pen(12) + display.rectangle(item_x, item_y + y - (item_height // 2), width // columns, item_height) + display.set_pen(0) + display.text(items[i], item_x + x + item_height, item_y + y, WIDTH, ITEM_TEXT_SIZE) + draw_checkbox(item_x, item_y + y - (item_height // 2), item_height, 15, 0, 2, item_states[i], 2) + item_y += item_height + if item_y >= height - (item_height // 2): + item_x += width // columns + item_y = 0 + current_col += 1 + if current_col >= columns: + return + + +# Draw a upward arrow +def draw_up(x, y, width, height, thickness, padding): + border = (thickness // 4) + padding + display.line(x + border, y + height - border, + x + (width // 2), y + border) + display.line(x + (width // 2), y + border, + x + width - border, y + height - border) + + +# Draw a downward arrow +def draw_down(x, y, width, height, thickness, padding): + border = (thickness // 2) + padding + display.line(x + border, y + border, + x + (width // 2), y + height - border) + display.line(x + (width // 2), y + height - border, + x + width - border, y + border) + + +# Draw a left arrow +def draw_left(x, y, width, height, thickness, padding): + border = (thickness // 2) + padding + display.line(x + width - border, y + border, + x + border, y + (height // 2)) + display.line(x + border, y + (height // 2), + x + width - border, y + height - border) + + +# Draw a right arrow +def draw_right(x, y, width, height, thickness, padding): + border = (thickness // 2) + padding + display.line(x + border, y + border, + x + width - border, y + (height // 2)) + display.line(x + width - border, y + (height // 2), + x + border, y + height - border) + + +# Draw a tick +def draw_tick(x, y, width, height, thickness, padding): + border = (thickness // 2) + padding + display.line(x + border, y + ((height * 2) // 3), + x + (width // 2), y + height - border) + display.line(x + (width // 2), y + height - border, + x + width - border, y + border) + + +# Draw a cross +def draw_cross(x, y, width, height, thickness, padding): + border = (thickness // 2) + padding + display.line(x + border, y + border, x + width - border, y + height - border) + display.line(x + width - border, y + border, x + border, y + height - border) + + +# Draw a checkbox with or without a tick +def draw_checkbox(x, y, size, background, foreground, thickness, tick, padding): + border = (thickness // 2) + padding + display.set_pen(background) + display.rectangle(x + border, y + border, size - (border * 2), size - (border * 2)) + display.set_pen(foreground) + display.line(x + border, y + border, x + size - border, y + border) + display.line(x + border, y + border, x + border, y + size - border) + display.line(x + size - border, y + border, x + size - border, y + size - border) + display.line(x + border, y + size - border, x + size - border, y + size - border) + if tick: + draw_tick(x, y, size, size, thickness, 2 + border) + + +# ------------------------------ +# Program setup +# ------------------------------ + +changed = not badger2040w.woken_by_button() +state = { + "current_item": 0, +} +badger_os.state_load("list", state) +items_hash = binascii.crc32("\n".join(list_items)) +if "items_hash" not in state or state["items_hash"] != items_hash: + # Item list changed, or not yet written reset the list + state["current_item"] = 0 + state["items_hash"] = items_hash + state["checked"] = [False] * len(list_items) + changed = True + +# Global variables +items_per_page = 0 + +# Create a new Badger and set it to update FAST +display = badger2040w.Badger2040W() +display.led(128) +display.set_font("sans") +display.set_thickness(2) +if changed: + display.set_update_speed(badger2040w.UPDATE_FAST) +else: + display.set_update_speed(badger2040w.UPDATE_TURBO) + +# Find out what the longest item is +longest_item = 0 +for i in range(len(list_items)): + while True: + item = list_items[i] + item_length = display.measure_text(item, ITEM_TEXT_SIZE) + if item_length > 0 and item_length > LIST_WIDTH - ITEM_SPACING: + list_items[i] = item[:-1] + else: + break + longest_item = max(longest_item, display.measure_text(list_items[i], ITEM_TEXT_SIZE)) + + +# And use that to calculate the number of columns we can fit onscreen and how many items that would give +list_columns = 1 +while longest_item + ITEM_SPACING < (LIST_WIDTH // (list_columns + 1)): + list_columns += 1 + +items_per_page = ((LIST_HEIGHT // ITEM_SPACING) + 1) * list_columns + + +# ------------------------------ +# Main program loop +# ------------------------------ + +while True: + if len(list_items) > 0: + if display.pressed(badger2040w.BUTTON_A): + if state["current_item"] > 0: + page = state["current_item"] // items_per_page + state["current_item"] = max(state["current_item"] - (items_per_page) // list_columns, 0) + if page != state["current_item"] // items_per_page: + display.update_speed(badger2040w.UPDATE_FAST) + changed = True + if display.pressed(badger2040w.BUTTON_B): + state["checked"][state["current_item"]] = not state["checked"][state["current_item"]] + changed = True + if display.pressed(badger2040w.BUTTON_C): + if state["current_item"] < len(list_items) - 1: + page = state["current_item"] // items_per_page + state["current_item"] = min(state["current_item"] + (items_per_page) // list_columns, len(list_items) - 1) + if page != state["current_item"] // items_per_page: + display.update_speed(badger2040w.UPDATE_FAST) + changed = True + if display.pressed(badger2040w.BUTTON_UP): + if state["current_item"] > 0: + state["current_item"] -= 1 + changed = True + if display.pressed(badger2040w.BUTTON_DOWN): + if state["current_item"] < len(list_items) - 1: + state["current_item"] += 1 + changed = True + + if changed: + badger_os.state_save("list", state) + + display.set_pen(15) + display.clear() + + display.set_pen(12) + display.rectangle(WIDTH - ARROW_WIDTH, 0, ARROW_WIDTH, HEIGHT) + display.rectangle(0, HEIGHT - ARROW_HEIGHT, WIDTH, ARROW_HEIGHT) + + y = LIST_PADDING + 12 + display.set_pen(0) + display.text(list_title, LIST_PADDING, y, WIDTH, TITLE_TEXT_SIZE) + + y += 12 + display.set_pen(0) + display.line(LIST_PADDING, y, WIDTH - LIST_PADDING - ARROW_WIDTH, y) + + if len(list_items) > 0: + page_item = 0 + if items_per_page > 0: + page_item = (state["current_item"] // items_per_page) * items_per_page + + # Draw the list + display.set_pen(0) + draw_list(list_items, state["checked"], page_item, state["current_item"], LIST_PADDING, LIST_START, + LIST_WIDTH, LIST_HEIGHT, ITEM_SPACING, list_columns) + + # Draw the interaction button icons + display.set_pen(0) + + # Previous item + if state["current_item"] > 0: + draw_up(WIDTH - ARROW_WIDTH, (HEIGHT // 4) - (ARROW_HEIGHT // 2), + ARROW_WIDTH, ARROW_HEIGHT, ARROW_THICKNESS, ARROW_PADDING) + + # Next item + if state["current_item"] < (len(list_items) - 1): + draw_down(WIDTH - ARROW_WIDTH, ((HEIGHT * 3) // 4) - (ARROW_HEIGHT // 2), + ARROW_WIDTH, ARROW_HEIGHT, ARROW_THICKNESS, ARROW_PADDING) + + # Previous column + if state["current_item"] > 0: + draw_left((WIDTH // 7) - (ARROW_WIDTH // 2), HEIGHT - ARROW_HEIGHT, + ARROW_WIDTH, ARROW_HEIGHT, ARROW_THICKNESS, ARROW_PADDING) + + # Next column + if state["current_item"] < (len(list_items) - 1): + draw_right(((WIDTH * 6) // 7) - (ARROW_WIDTH // 2), HEIGHT - ARROW_HEIGHT, + ARROW_WIDTH, ARROW_HEIGHT, ARROW_THICKNESS, ARROW_PADDING) + + if state["checked"][state["current_item"]]: + # Tick off item + draw_cross((WIDTH // 2) - (ARROW_WIDTH // 2), HEIGHT - ARROW_HEIGHT, + ARROW_HEIGHT, ARROW_HEIGHT, ARROW_THICKNESS, ARROW_PADDING) + else: + # Untick item + draw_tick((WIDTH // 2) - (ARROW_WIDTH // 2), HEIGHT - ARROW_HEIGHT, + ARROW_HEIGHT, ARROW_HEIGHT, ARROW_THICKNESS, ARROW_PADDING) + else: + # Say that the list is empty + empty_text = "Nothing Here" + text_length = display.measure_text(empty_text, ITEM_TEXT_SIZE) + display.text(empty_text, ((LIST_PADDING + LIST_WIDTH) - text_length) // 2, (LIST_HEIGHT // 2) + LIST_START - (ITEM_SPACING // 4), WIDTH, ITEM_TEXT_SIZE) + + display.update() + display.set_update_speed(badger2040w.UPDATE_TURBO) + changed = False + + display.halt() diff --git a/micropython/examples/badger2040w/examples/net_info.py b/micropython/examples/badger2040w/examples/net_info.py new file mode 100644 index 00000000..083db599 --- /dev/null +++ b/micropython/examples/badger2040w/examples/net_info.py @@ -0,0 +1,48 @@ +import badger2040w as badger2040 +from badger2040w import WIDTH +import network + +TEXT_SIZE = 1 +LINE_HEIGHT = 16 + +# Display Setup +display = badger2040.Badger2040W() +display.led(128) + +# Connects to the wireless network. Ensure you have entered your details in WIFI_CONFIG.py :). +display.connect() +net = network.WLAN(network.STA_IF).ifconfig() + +# Page Header +display.set_pen(15) +display.clear() +display.set_pen(0) + +display.set_pen(0) +display.rectangle(0, 0, WIDTH, 20) +display.set_pen(15) +display.text("badgerOS", 3, 4) +display.text("Network Details", WIDTH - display.measure_text("Network Details") - 4, 4) +display.set_pen(0) + +y = 35 + int(LINE_HEIGHT / 2) + +if net: + display.text("> LOCAL IP: {}".format(net[0]), 0, y, WIDTH) + y += LINE_HEIGHT + display.text("> Subnet: {}".format(net[1]), 0, y, WIDTH) + y += LINE_HEIGHT + display.text("> Gateway: {}".format(net[2]), 0, y, WIDTH) + y += LINE_HEIGHT + display.text("> DNS: {}".format(net[3]), 0, y, WIDTH) +else: + display.text("> No network connection!", 0, y, WIDTH) + y += LINE_HEIGHT + display.text("> Check details in WIFI_CONFIG.py", 0, y, WIDTH) + +display.update() + +# Call halt in a loop, on battery this switches off power. +# On USB, the app will exit when A+C is pressed because the launcher picks that up. +while True: + display.halt() diff --git a/micropython/examples/badger2040w/examples/news.py b/micropython/examples/badger2040w/examples/news.py new file mode 100644 index 00000000..4f280c08 --- /dev/null +++ b/micropython/examples/badger2040w/examples/news.py @@ -0,0 +1,215 @@ +import badger2040w as badger2040 +from badger2040w import WIDTH +import machine +from urllib import urequest +import gc +import qrcode +import badger_os + +# URLS to use (Entertainment, Science and Technology) +URL = ["http://feeds.bbci.co.uk/news/entertainment_and_arts/rss.xml", + "http://feeds.bbci.co.uk/news/science_and_environment/rss.xml", + "http://feeds.bbci.co.uk/news/technology/rss.xml"] + +code = qrcode.QRCode() + +state = { + "current_page": 0, + "feed": 2 +} + +badger_os.state_load("news", state) + +# Display Setup +display = badger2040.Badger2040W() +display.led(128) +display.set_update_speed(2) + +# Setup buttons +button_a = machine.Pin(badger2040.BUTTON_A, machine.Pin.IN, machine.Pin.PULL_DOWN) +button_b = machine.Pin(badger2040.BUTTON_B, machine.Pin.IN, machine.Pin.PULL_DOWN) +button_c = machine.Pin(badger2040.BUTTON_C, machine.Pin.IN, machine.Pin.PULL_DOWN) +button_down = machine.Pin(badger2040.BUTTON_DOWN, machine.Pin.IN, machine.Pin.PULL_DOWN) +button_up = machine.Pin(badger2040.BUTTON_UP, machine.Pin.IN, machine.Pin.PULL_DOWN) + + +def read_until(stream, char): + result = b"" + while True: + c = stream.read(1) + if c == char: + return result + result += c + + +def discard_until(stream, c): + while stream.read(1) != c: + pass + + +def parse_xml_stream(s, accept_tags, group_by, max_items=3): + tag = [] + text = b"" + count = 0 + current = {} + while True: + char = s.read(1) + if len(char) == 0: + break + + if char == b"<": + next_char = s.read(1) + + # Discard stuff like ") + continue + + # Detect ") # Discard ]> + gc.collect() + + elif next_char == b"/": + current_tag = read_until(s, b">") + top_tag = tag[-1] + + # Populate our result dict + if top_tag in accept_tags: + current[top_tag.decode("utf-8")] = text.decode("utf-8") + + # If we've found a group of items, yield the dict + elif top_tag == group_by: + yield current + current = {} + count += 1 + if count == max_items: + return + tag.pop() + text = b"" + gc.collect() + continue + + else: + current_tag = read_until(s, b">") + tag += [next_char + current_tag.split(b" ")[0]] + text = b"" + gc.collect() + + else: + text += char + + +def measure_qr_code(size, code): + w, h = code.get_size() + module_size = int(size / w) + return module_size * w, module_size + + +def draw_qr_code(ox, oy, size, code): + size, module_size = measure_qr_code(size, code) + display.set_pen(15) + display.rectangle(ox, oy, size, size) + display.set_pen(0) + for x in range(size): + for y in range(size): + if code.get_module(x, y): + display.rectangle(ox + x * module_size, oy + y * module_size, module_size, module_size) + + +# A function to get the data from an RSS Feed, this in case BBC News. +def get_rss(url): + try: + stream = urequest.urlopen(url) + output = list(parse_xml_stream(stream, [b"title", b"description", b"guid", b"pubDate"], b"item")) + return output + + except OSError as e: + print(e) + return False + + +# Connects to the wireless network. Ensure you have entered your details in WIFI_CONFIG.py :). +display.connect() + +print(state["feed"]) +feed = get_rss(URL[state["feed"]]) + + +def draw_page(): + + # Clear the display + display.set_pen(15) + display.clear() + display.set_pen(0) + + # Draw the page header + display.set_font("bitmap6") + display.set_pen(0) + display.rectangle(0, 0, WIDTH, 20) + display.set_pen(15) + display.text("News", 3, 4) + display.text("Page: " + str(state["current_page"] + 1), WIDTH - display.measure_text("Page: ") - 4, 4) + display.set_pen(0) + + display.set_font("bitmap8") + + # Draw articles from the feed if they're available. + if feed: + page = state["current_page"] + display.set_pen(0) + display.text(feed[page]["title"], 2, 30, WIDTH - 130, 2) + code.set_text(feed[page]["guid"]) + draw_qr_code(WIDTH - 100, 25, 100, code) + + else: + display.set_pen(0) + display.rectangle(0, 60, WIDTH, 25) + display.set_pen(15) + display.text("Unable to display news! Check your network settings in WIFI_CONFIG.py", 5, 65, WIDTH, 1) + + display.update() + + +draw_page() + +while 1: + + changed = False + + if button_down.value(): + if state["current_page"] < 2: + state["current_page"] += 1 + changed = True + + if button_up.value(): + if state["current_page"] > 0: + state["current_page"] -= 1 + changed = True + + if button_a.value(): + state["feed"] = 0 + state["current_page"] = 0 + feed = get_rss(URL[state["feed"]]) + badger_os.state_save("news", state) + changed = True + + if button_b.value(): + state["feed"] = 1 + state["current_page"] = 0 + feed = get_rss(URL[state["feed"]]) + badger_os.state_save("news", state) + changed = True + + if button_c.value(): + state["feed"] = 2 + state["current_page"] = 0 + feed = get_rss(URL[state["feed"]]) + badger_os.state_save("news", state) + changed = True + + if changed: + draw_page() diff --git a/micropython/examples/badger2040w/examples/qrgen.py b/micropython/examples/badger2040w/examples/qrgen.py new file mode 100644 index 00000000..d4617989 --- /dev/null +++ b/micropython/examples/badger2040w/examples/qrgen.py @@ -0,0 +1,139 @@ +import badger2040w +import qrcode +import time +import os +import badger_os + +# Check that the qrcodes directory exists, if not, make it +try: + os.mkdir("/qrcodes") +except OSError: + pass + +# Check that there is a qrcode.txt, if not preload +try: + text = open("/qrcodes/qrcode.txt", "r") +except OSError: + text = open("/qrcodes/qrcode.txt", "w") + text.write("""https://pimoroni.com/badger2040w +Badger 2040 W +* 296x128 1-bit e-ink +* 2.4GHz wireless +* five user buttons +* user LED +* 2MB QSPI flash + +Scan this code to learn +more about Badger 2040 W. +""") + text.flush() + text.seek(0) + +# Load all available QR Code Files +try: + CODES = [f for f in os.listdir("/qrcodes") if f.endswith(".txt")] + TOTAL_CODES = len(CODES) +except OSError: + pass + + +print(f'There are {TOTAL_CODES} QR Codes available:') +for codename in CODES: + print(f'File: {codename}') + +display = badger2040w.Badger2040W() + +code = qrcode.QRCode() + +state = { + "current_qr": 0 +} + + +def measure_qr_code(size, code): + w, h = code.get_size() + module_size = int(size / w) + return module_size * w, module_size + + +def draw_qr_code(ox, oy, size, code): + size, module_size = measure_qr_code(size, code) + display.set_pen(15) + display.rectangle(ox, oy, size, size) + display.set_pen(0) + for x in range(size): + for y in range(size): + if code.get_module(x, y): + display.rectangle(ox + x * module_size, oy + y * module_size, module_size, module_size) + + +def draw_qr_file(n): + display.led(128) + file = CODES[n] + codetext = open("/qrcodes/{}".format(file), "r") + + lines = codetext.read().strip().split("\n") + code_text = lines.pop(0) + title_text = lines.pop(0) + detail_text = lines + + # Clear the Display + display.set_pen(15) # Change this to 0 if a white background is used + display.clear() + display.set_pen(0) + + code.set_text(code_text) + size, _ = measure_qr_code(128, code) + left = top = int((badger2040w.HEIGHT / 2) - (size / 2)) + draw_qr_code(left, top, 128, code) + + left = 128 + 5 + + display.text(title_text, left, 20, badger2040w.WIDTH, 2) + + top = 40 + for line in detail_text: + display.text(line, left, top, badger2040w.WIDTH, 1) + top += 10 + + if TOTAL_CODES > 1: + for i in range(TOTAL_CODES): + x = 286 + y = int((128 / 2) - (TOTAL_CODES * 10 / 2) + (i * 10)) + display.set_pen(0) + display.rectangle(x, y, 8, 8) + if state["current_qr"] != i: + display.set_pen(15) + display.rectangle(x + 1, y + 1, 6, 6) + display.update() + + +badger_os.state_load("qrcodes", state) +changed = not badger2040w.woken_by_button() + +while True: + if TOTAL_CODES > 1: + if display.pressed(badger2040w.BUTTON_UP): + if state["current_qr"] > 0: + state["current_qr"] -= 1 + changed = True + + if display.pressed(badger2040w.BUTTON_DOWN): + if state["current_qr"] < TOTAL_CODES - 1: + state["current_qr"] += 1 + changed = True + + if display.pressed(badger2040w.BUTTON_B) or display.pressed(badger2040w.BUTTON_C): + display.set_pen(15) + display.clear() + badger_os.warning(display, "To add QR codes, connect Badger 2040 W to a PC, load up Thonny, and add files to /qrcodes directory.") + time.sleep(4) + changed = True + + if changed: + draw_qr_file(state["current_qr"]) + badger_os.state_save("qrcodes", state) + changed = False + + # Halt the Badger to save power, it will wake up if any of the front buttons are pressed + display.halt() diff --git a/micropython/examples/badger2040w/examples/weather.py b/micropython/examples/badger2040w/examples/weather.py new file mode 100644 index 00000000..aa35bede --- /dev/null +++ b/micropython/examples/badger2040w/examples/weather.py @@ -0,0 +1,106 @@ +# This example grabs current weather details from Open Meteo and displays them on Badger 2040 W. +# Find out more about the Open Meteo API at https://open-meteo.com + +import badger2040w as badger2040 +from badger2040w import WIDTH +import urequests +import jpegdec + +# Set your latitude/longitude here (find yours by right clicking in Google Maps!) +LAT = 53.38609085276884 +LNG = -1.4239983439328177 +TIMEZONE = "auto" # determines time zone from lat/long + +URL = "http://api.open-meteo.com/v1/forecast?latitude=" + str(LAT) + "&longitude=" + str(LNG) + "¤t_weather=true&timezone=" + TIMEZONE + +# Display Setup +display = badger2040.Badger2040W() +display.led(128) +display.set_update_speed(2) + +jpeg = jpegdec.JPEG(display.display) + +# Connects to the wireless network. Ensure you have entered your details in WIFI_CONFIG.py :). +display.connect() + + +def get_data(): + global weathercode, temperature, windspeed, winddirection, date, time + print(f"Requesting URL: {URL}") + r = urequests.get(URL) + # open the json data + j = r.json() + print("Data obtained!") + print(j) + + # parse relevant data from JSON + current = j["current_weather"] + temperature = current["temperature"] + windspeed = current["windspeed"] + winddirection = calculate_bearing(current["winddirection"]) + weathercode = current["weathercode"] + date, time = current["time"].split("T") + + r.close() + + +def calculate_bearing(d): + # calculates a compass direction from the wind direction in degrees + dirs = ['N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW'] + ix = round(d / (360. / len(dirs))) + return dirs[ix % len(dirs)] + + +def draw_page(): + # Clear the display + display.set_pen(15) + display.clear() + display.set_pen(0) + + # Draw the page header + display.set_font("bitmap6") + display.set_pen(0) + display.rectangle(0, 0, WIDTH, 20) + display.set_pen(15) + display.text("Weather", 3, 4) + display.set_pen(0) + + display.set_font("bitmap8") + + if temperature is not None: + # Choose an appropriate icon based on the weather code + # Weather codes from https://open-meteo.com/en/docs + # Weather icons from https://fontawesome.com/ + if weathercode in [71, 73, 75, 77, 85, 86]: # codes for snow + jpeg.open_file("/icons/icon-snow.jpg") + elif weathercode in [51, 53, 55, 56, 57, 61, 63, 65, 66, 67, 80, 81, 82]: # codes for rain + jpeg.open_file("/icons/icon-rain.jpg") + elif weathercode in [1, 2, 3, 45, 48]: # codes for cloud + jpeg.open_file("/icons/icon-cloud.jpg") + elif weathercode in [0]: # codes for sun + jpeg.open_file("/icons/icon-sun.jpg") + elif weathercode in [95, 96, 99]: # codes for storm + jpeg.open_file("/icons/icon-storm.jpg") + jpeg.decode(13, 40, jpegdec.JPEG_SCALE_FULL) + display.set_pen(0) + display.text(f"Temperature: {temperature}°C", int(WIDTH / 3), 28, WIDTH - 105, 2) + display.text(f"Wind Speed: {windspeed}kmph", int(WIDTH / 3), 48, WIDTH - 105, 2) + display.text(f"Wind Direction: {winddirection}", int(WIDTH / 3), 68, WIDTH - 105, 2) + display.text(f"Last update: {date}, {time}", int(WIDTH / 3), 88, WIDTH - 105, 2) + + else: + display.set_pen(0) + display.rectangle(0, 60, WIDTH, 25) + display.set_pen(15) + display.text("Unable to display weather! Check your network settings in WIFI_CONFIG.py", 5, 65, WIDTH, 1) + + display.update() + + +get_data() +draw_page() + +# Call halt in a loop, on battery this switches off power. +# On USB, the app will exit when A+C is pressed because the launcher picks that up. +while True: + display.halt() diff --git a/micropython/examples/badger2040w/icons/icon-cloud.jpg b/micropython/examples/badger2040w/icons/icon-cloud.jpg new file mode 100644 index 00000000..b1b1e706 Binary files /dev/null and b/micropython/examples/badger2040w/icons/icon-cloud.jpg differ diff --git a/micropython/examples/badger2040w/icons/icon-rain.jpg b/micropython/examples/badger2040w/icons/icon-rain.jpg new file mode 100644 index 00000000..e0d536d5 Binary files /dev/null and b/micropython/examples/badger2040w/icons/icon-rain.jpg differ diff --git a/micropython/examples/badger2040w/icons/icon-snow.jpg b/micropython/examples/badger2040w/icons/icon-snow.jpg new file mode 100644 index 00000000..db18190d Binary files /dev/null and b/micropython/examples/badger2040w/icons/icon-snow.jpg differ diff --git a/micropython/examples/badger2040w/icons/icon-storm.jpg b/micropython/examples/badger2040w/icons/icon-storm.jpg new file mode 100644 index 00000000..b01a9371 Binary files /dev/null and b/micropython/examples/badger2040w/icons/icon-storm.jpg differ diff --git a/micropython/examples/badger2040w/icons/icon-sun.jpg b/micropython/examples/badger2040w/icons/icon-sun.jpg new file mode 100644 index 00000000..6c3703de Binary files /dev/null and b/micropython/examples/badger2040w/icons/icon-sun.jpg differ diff --git a/micropython/examples/badger2040w/images/README.txt b/micropython/examples/badger2040w/images/README.txt new file mode 100644 index 00000000..c9a6e267 --- /dev/null +++ b/micropython/examples/badger2040w/images/README.txt @@ -0,0 +1,3 @@ +Images must be 296x128 pixel JPEGs + +Create a new "images" directory via Thonny, and upload your .jpg files there. diff --git a/micropython/examples/badger2040w/images/badgerpunk.jpg b/micropython/examples/badger2040w/images/badgerpunk.jpg new file mode 100644 index 00000000..73f32be9 Binary files /dev/null and b/micropython/examples/badger2040w/images/badgerpunk.jpg differ diff --git a/micropython/examples/badger2040w/launcher.py b/micropython/examples/badger2040w/launcher.py new file mode 100644 index 00000000..941e21a8 --- /dev/null +++ b/micropython/examples/badger2040w/launcher.py @@ -0,0 +1,183 @@ +import gc +import os +import time +import math +import badger2040w as badger2040 +import badger_os +import jpegdec + +APP_DIR = "/examples" +FONT_SIZE = 2 + +changed = False +exited_to_launcher = False +woken_by_button = badger2040.woken_by_button() # Must be done before we clear_pressed_to_wake + +if badger2040.pressed_to_wake(badger2040.BUTTON_A) and badger2040.pressed_to_wake(badger2040.BUTTON_C): + # Pressing A and C together at start quits app + exited_to_launcher = badger_os.state_clear_running() + badger2040.reset_pressed_to_wake() +else: + # Otherwise restore previously running app + badger_os.state_launch() + + +display = badger2040.Badger2040W() +display.set_font("bitmap8") +display.led(128) + +jpeg = jpegdec.JPEG(display.display) + +state = { + "page": 0, + "running": "launcher" +} + +badger_os.state_load("launcher", state) + +examples = [x[:-3] for x in os.listdir("/examples") if x.endswith(".py")] + +# Approximate center lines for buttons A, B and C +centers = (41, 147, 253) + +MAX_PAGE = math.ceil(len(examples) / 3) + +WIDTH = 296 + + +def map_value(input, in_min, in_max, out_min, out_max): + return (((input - in_min) * (out_max - out_min)) / (in_max - in_min)) + out_min + + +def draw_disk_usage(x): + _, f_used, _ = badger_os.get_disk_usage() + + display.set_pen(15) + display.image( + bytearray( + ( + 0b00000000, + 0b00111100, + 0b00111100, + 0b00111100, + 0b00111000, + 0b00000000, + 0b00000000, + 0b00000001, + ) + ), + 8, + 8, + x, + 4, + ) + display.rectangle(x + 10, 3, 80, 10) + display.set_pen(0) + display.rectangle(x + 11, 4, 78, 8) + display.set_pen(15) + display.rectangle(x + 12, 5, int(76 / 100.0 * f_used), 6) + display.text("{:.2f}%".format(f_used), x + 91, 4, WIDTH, 1.0) + + +def render(): + display.set_pen(15) + display.clear() + display.set_pen(0) + + max_icons = min(3, len(examples[(state["page"] * 3):])) + + for i in range(max_icons): + x = centers[i] + label = examples[i + (state["page"] * 3)] + icon_label = label.replace("_", "-") + icon = f"{APP_DIR}/icon-{icon_label}.jpg" + label = label.replace("_", " ") + jpeg.open_file(icon) + jpeg.decode(x - 26, 30) + display.set_pen(0) + w = display.measure_text(label, FONT_SIZE) + display.text(label, int(x - (w / 2)), 16 + 80, WIDTH, FONT_SIZE) + + for i in range(MAX_PAGE): + x = 286 + y = int((128 / 2) - (MAX_PAGE * 10 / 2) + (i * 10)) + display.set_pen(0) + display.rectangle(x, y, 8, 8) + if state["page"] != i: + display.set_pen(15) + display.rectangle(x + 1, y + 1, 6, 6) + + display.set_pen(0) + display.rectangle(0, 0, WIDTH, 16) + draw_disk_usage(90) + display.set_pen(15) + display.text("badgerOS", 4, 4, WIDTH, 1.0) + + display.update() + + +def wait_for_user_to_release_buttons(): + while display.pressed_any(): + time.sleep(0.01) + + +def launch_example(index): + wait_for_user_to_release_buttons() + + file = examples[(state["page"] * 3) + index] + file = f"{APP_DIR}/{file}" + + for k in locals().keys(): + if k not in ("gc", "file", "badger_os"): + del locals()[k] + + gc.collect() + + badger_os.launch(file) + + +def button(pin): + global changed + changed = True + + if pin == badger2040.BUTTON_A: + launch_example(0) + if pin == badger2040.BUTTON_B: + launch_example(1) + if pin == badger2040.BUTTON_C: + launch_example(2) + if pin == badger2040.BUTTON_UP: + if state["page"] > 0: + state["page"] -= 1 + render() + if pin == badger2040.BUTTON_DOWN: + if state["page"] < MAX_PAGE - 1: + state["page"] += 1 + render() + + +if exited_to_launcher or not woken_by_button: + wait_for_user_to_release_buttons() + display.set_update_speed(badger2040.UPDATE_MEDIUM) + render() + +display.set_update_speed(badger2040.UPDATE_FAST) + +while True: + if display.pressed(badger2040.BUTTON_A): + button(badger2040.BUTTON_A) + if display.pressed(badger2040.BUTTON_B): + button(badger2040.BUTTON_B) + if display.pressed(badger2040.BUTTON_C): + button(badger2040.BUTTON_C) + + if display.pressed(badger2040.BUTTON_UP): + button(badger2040.BUTTON_UP) + if display.pressed(badger2040.BUTTON_DOWN): + button(badger2040.BUTTON_DOWN) + + if changed: + badger_os.state_save("launcher", state) + changed = False + + display.halt() diff --git a/micropython/examples/badger2040w/lib/badger2040w.py b/micropython/examples/badger2040w/lib/badger2040w.py new file mode 100644 index 00000000..8b733503 --- /dev/null +++ b/micropython/examples/badger2040w/lib/badger2040w.py @@ -0,0 +1,181 @@ +import machine +import micropython +from picographics import PicoGraphics, DISPLAY_INKY_PACK +import network +from network_manager import NetworkManager +import WIFI_CONFIG +import uasyncio +import time +import gc +import wakeup + + +BUTTON_DOWN = 11 +BUTTON_A = 12 +BUTTON_B = 13 +BUTTON_C = 14 +BUTTON_UP = 15 +BUTTON_USER = None # User button not available on W + +BUTTON_MASK = 0b11111 << 11 + +SYSTEM_VERY_SLOW = 0 +SYSTEM_SLOW = 1 +SYSTEM_NORMAL = 2 +SYSTEM_FAST = 3 +SYSTEM_TURBO = 4 + +UPDATE_NORMAL = 0 +UPDATE_MEDIUM = 1 +UPDATE_FAST = 2 +UPDATE_TURBO = 3 + +LED = 22 +ENABLE_3V3 = 10 +BUSY = 26 + +WIDTH = 296 +HEIGHT = 128 + +SYSTEM_FREQS = [ + 4000000, + 12000000, + 48000000, + 133000000, + 250000000 +] + +BUTTONS = { + BUTTON_DOWN: machine.Pin(BUTTON_DOWN, machine.Pin.IN, machine.Pin.PULL_DOWN), + BUTTON_A: machine.Pin(BUTTON_A, machine.Pin.IN, machine.Pin.PULL_DOWN), + BUTTON_B: machine.Pin(BUTTON_B, machine.Pin.IN, machine.Pin.PULL_DOWN), + BUTTON_C: machine.Pin(BUTTON_C, machine.Pin.IN, machine.Pin.PULL_DOWN), + BUTTON_UP: machine.Pin(BUTTON_UP, machine.Pin.IN, machine.Pin.PULL_DOWN), +} + +WAKEUP_MASK = 0 + + +def woken_by_button(): + return wakeup.get_gpio_state() & BUTTON_MASK > 0 + + +def pressed_to_wake(button): + return wakeup.get_gpio_state() & (1 << button) > 0 + + +def reset_pressed_to_wake(): + wakeup.reset_gpio_state() + + +def pressed_to_wake_get_once(button): + global WAKEUP_MASK + result = (wakeup.get_gpio_state() & ~WAKEUP_MASK & (1 << button)) > 0 + WAKEUP_MASK |= (1 << button) + return result + + +def system_speed(speed): + try: + machine.freq(SYSTEM_FREQS[speed]) + except IndexError: + pass + + +class Badger2040W(): + def __init__(self): + self.display = PicoGraphics(DISPLAY_INKY_PACK) + self._led = machine.PWM(machine.Pin(LED)) + self._led.freq(1000) + self._led.duty_u16(0) + self._update_speed = 0 + + def __getattr__(self, item): + # Glue to redirect calls to PicoGraphics + return getattr(self.display, item) + + def update(self): + t_start = time.ticks_ms() + self.display.update() + t_elapsed = time.ticks_ms() - t_start + + delay_ms = [4700, 2600, 900, 250][self._update_speed] + + if t_elapsed < delay_ms: + time.sleep((delay_ms - t_elapsed) / 1000) + + def set_update_speed(self, speed): + self.display.set_update_speed(speed) + self._update_speed = speed + + def led(self, brightness): + brightness = max(0, min(255, brightness)) + self._led.duty_u16(int(brightness * 256)) + + def invert(self, invert): + raise RuntimeError("Display invert not supported in PicoGraphics.") + + def thickness(self, thickness): + raise RuntimeError("Thickness not supported in PicoGraphics.") + + def halt(self): + time.sleep(0.05) + enable = machine.Pin(ENABLE_3V3, machine.Pin.OUT) + enable.off() + while not self.pressed_any(): + pass + + def pressed(self, button): + return BUTTONS[button].value() == 1 or pressed_to_wake_get_once(button) + + def pressed_any(self): + for button in BUTTONS.values(): + if button.value(): + return True + return False + + @micropython.native + def icon(self, data, index, data_w, icon_size, x, y): + s_x = (index * icon_size) % data_w + s_y = int((index * icon_size) / data_w) + + for o_y in range(icon_size): + for o_x in range(icon_size): + o = ((o_y + s_y) * data_w) + (o_x + s_x) + bm = 0b10000000 >> (o & 0b111) + if data[o >> 3] & bm: + self.display.pixel(x + o_x, y + o_y) + + def image(self, data, w, h, x, y): + for oy in range(h): + row = data[oy] + for ox in range(w): + if row & 0b1 == 0: + self.display.pixel(x + ox, y + oy) + row >>= 1 + + def status_handler(self, mode, status, ip): + print(mode, status, ip) + self.display.set_pen(15) + self.display.clear() + self.display.set_pen(0) + if status: + self.display.text("Connected!", 10, 10, 300, 0.5) + self.display.text(ip, 10, 30, 300, 0.5) + else: + self.display.text("Connecting...", 10, 10, 300, 0.5) + self.update() + + def isconnected(self): + return network.WLAN(network.STA_IF).isconnected() + + def ip_address(self): + return network.WLAN(network.STA_IF).ifconfig()[0] + + def connect(self): + if WIFI_CONFIG.COUNTRY == "": + raise RuntimeError("You must populate WIFI_CONFIG.py for networking.") + self.display.set_update_speed(2) + network_manager = NetworkManager(WIFI_CONFIG.COUNTRY, status_handler=self.status_handler) + uasyncio.get_event_loop().run_until_complete(network_manager.client(WIFI_CONFIG.SSID, WIFI_CONFIG.PSK)) + gc.collect() diff --git a/micropython/examples/badger2040w/lib/badger_os.py b/micropython/examples/badger2040w/lib/badger_os.py new file mode 100644 index 00000000..3eaaec63 --- /dev/null +++ b/micropython/examples/badger2040w/lib/badger_os.py @@ -0,0 +1,188 @@ +"""Keep track of app state in persistent flash storage.""" + +import os +import gc +import time +import json +import machine +import badger2040w as badger2040 + + +def get_battery_level(): + return 0 + # Battery measurement + vbat_adc = machine.ADC(badger2040.PIN_BATTERY) + vref_adc = machine.ADC(badger2040.PIN_1V2_REF) + vref_en = machine.Pin(badger2040.PIN_VREF_POWER) + vref_en.init(machine.Pin.OUT) + vref_en.value(0) + + # Enable the onboard voltage reference + vref_en.value(1) + + # Calculate the logic supply voltage, as will be lower that the usual 3.3V when running off low batteries + vdd = 1.24 * (65535 / vref_adc.read_u16()) + vbat = ( + (vbat_adc.read_u16() / 65535) * 3 * vdd + ) # 3 in this is a gain, not rounding of 3.3V + + # Disable the onboard voltage reference + vref_en.value(0) + + # Convert the voltage to a level to display onscreen + return vbat + + +def get_disk_usage(): + # f_bfree and f_bavail should be the same? + # f_files, f_ffree, f_favail and f_flag are unsupported. + f_bsize, f_frsize, f_blocks, f_bfree, _, _, _, _, _, f_namemax = os.statvfs("/") + + f_total_size = f_frsize * f_blocks + f_total_free = f_bsize * f_bfree + f_total_used = f_total_size - f_total_free + + f_used = 100 / f_total_size * f_total_used + f_free = 100 / f_total_size * f_total_free + + return f_total_size, f_used, f_free + + +def state_running(): + state = {"running": "launcher"} + state_load("launcher", state) + return state["running"] + + +def state_clear_running(): + running = state_running() + state_modify("launcher", {"running": "launcher"}) + return running != "launcher" + + +def state_set_running(app): + state_modify("launcher", {"running": app}) + + +def state_launch(): + app = state_running() + if app is not None and app != "launcher": + launch(app) + + +def state_delete(app): + try: + os.remove("/state/{}.json".format(app)) + except OSError: + pass + + +def state_save(app, data): + try: + with open("/state/{}.json".format(app), "w") as f: + f.write(json.dumps(data)) + f.flush() + except OSError: + import os + try: + os.stat("/state") + except OSError: + os.mkdir("/state") + state_save(app, data) + + +def state_modify(app, data): + state = {} + state_load(app, state) + state.update(data) + state_save(app, state) + + +def state_load(app, defaults): + try: + data = json.loads(open("/state/{}.json".format(app), "r").read()) + if type(data) is dict: + defaults.update(data) + return True + except (OSError, ValueError): + pass + + state_save(app, defaults) + return False + + +def launch(file): + state_set_running(file) + + gc.collect() + + button_a = machine.Pin(badger2040.BUTTON_A, machine.Pin.IN, machine.Pin.PULL_DOWN) + button_c = machine.Pin(badger2040.BUTTON_C, machine.Pin.IN, machine.Pin.PULL_DOWN) + + def quit_to_launcher(pin): + if button_a.value() and button_c.value(): + machine.reset() + + button_a.irq(trigger=machine.Pin.IRQ_RISING, handler=quit_to_launcher) + button_c.irq(trigger=machine.Pin.IRQ_RISING, handler=quit_to_launcher) + + try: + __import__(file) + + except ImportError: + # If the app doesn't exist, notify the user + warning(None, f"Could not launch: {file}") + time.sleep(4.0) + except Exception as e: + # If the app throws an error, catch it and display! + print(e) + warning(None, str(e)) + time.sleep(4.0) + + # If the app exits or errors, do not relaunch! + state_clear_running() + machine.reset() # Exit back to launcher + + +# Draw an overlay box with a given message within it +def warning(display, message, width=badger2040.WIDTH - 20, height=badger2040.HEIGHT - 20, line_spacing=20, text_size=0.6): + print(message) + + if display is None: + display = badger2040.Badger2040W() + display.led(128) + + # Draw a light grey background + display.set_pen(12) + display.rectangle((badger2040.WIDTH - width) // 2, (badger2040.HEIGHT - height) // 2, width, height) + + width -= 20 + height -= 20 + + display.set_pen(15) + display.rectangle((badger2040.WIDTH - width) // 2, (badger2040.HEIGHT - height) // 2, width, height) + + # Take the provided message and split it up into + # lines that fit within the specified width + words = message.split(" ") + + lines = [] + current_line = "" + for word in words: + if display.measure_text(current_line + word + " ", text_size) < width: + current_line += word + " " + else: + lines.append(current_line.strip()) + current_line = word + " " + lines.append(current_line.strip()) + + display.set_pen(0) + + # Display each line of text from the message, centre-aligned + num_lines = len(lines) + for i in range(num_lines): + length = display.measure_text(lines[i], text_size) + current_line = (i * line_spacing) - ((num_lines - 1) * line_spacing) // 2 + display.text(lines[i], (badger2040.WIDTH - length) // 2, (badger2040.HEIGHT // 2) + current_line, badger2040.WIDTH, text_size) + + display.update() diff --git a/micropython/examples/badger2040w/lib/network_manager.py b/micropython/examples/badger2040w/lib/network_manager.py new file mode 100644 index 00000000..78b53f56 --- /dev/null +++ b/micropython/examples/badger2040w/lib/network_manager.py @@ -0,0 +1,108 @@ +import rp2 +import network +import machine +import uasyncio + + +class NetworkManager: + _ifname = ("Client", "Access Point") + + def __init__(self, country="GB", client_timeout=30, access_point_timeout=5, status_handler=None, error_handler=None): + rp2.country(country) + self._ap_if = network.WLAN(network.AP_IF) + self._sta_if = network.WLAN(network.STA_IF) + + self._mode = network.STA_IF + self._client_timeout = client_timeout + self._access_point_timeout = access_point_timeout + self._status_handler = status_handler + self._error_handler = error_handler + self.UID = ("{:02X}" * 8).format(*machine.unique_id()) + + def isconnected(self): + return self._sta_if.isconnected() or self._ap_if.isconnected() + + def config(self, var): + if self._sta_if.active(): + return self._sta_if.config(var) + else: + if var == "password": + return self.UID + return self._ap_if.config(var) + + def mode(self): + if self._sta_if.isconnected(): + return self._ifname[0] + if self._ap_if.isconnected(): + return self._ifname[1] + return None + + def ifaddress(self): + if self._sta_if.isconnected(): + return self._sta_if.ifconfig()[0] + if self._ap_if.isconnected(): + return self._ap_if.ifconfig()[0] + return '0.0.0.0' + + def disconnect(self): + if self._sta_if.isconnected(): + self._sta_if.disconnect() + if self._ap_if.isconnected(): + self._ap_if.disconnect() + + async def wait(self, mode): + while not self.isconnected(): + self._handle_status(mode, None) + await uasyncio.sleep_ms(1000) + + def _handle_status(self, mode, status): + if callable(self._status_handler): + self._status_handler(self._ifname[mode], status, self.ifaddress()) + + def _handle_error(self, mode, msg): + if callable(self._error_handler): + if self._error_handler(self._ifname[mode], msg): + return + raise RuntimeError(msg) + + async def client(self, ssid, psk): + if self._sta_if.isconnected(): + self._handle_status(network.STA_IF, True) + return + + self._ap_if.disconnect() + self._ap_if.active(False) + + self._sta_if.active(True) + self._sta_if.connect(ssid, psk) + self._sta_if.config(pm=0xa11140) + + try: + await uasyncio.wait_for(self.wait(network.STA_IF), self._client_timeout) + self._handle_status(network.STA_IF, True) + + except uasyncio.TimeoutError: + self._sta_if.active(False) + self._handle_status(network.STA_IF, False) + self._handle_error(network.STA_IF, "WIFI Client Failed") + + async def access_point(self): + if self._ap_if.isconnected(): + self._handle_status(network.AP_IF, True) + return + + self._sta_if.disconnect() + self._sta_if.active(False) + + self._ap_if.ifconfig(("10.10.1.1", "255.255.255.0", "10.10.1.1", "10.10.1.1")) + self._ap_if.config(password=self.UID) + self._ap_if.active(True) + + try: + await uasyncio.wait_for(self.wait(network.AP_IF), self._access_point_timeout) + self._handle_status(network.AP_IF, True) + + except uasyncio.TimeoutError: + self._sta_if.active(False) + self._handle_status(network.AP_IF, False) + self._handle_error(network.AP_IF, "WIFI Client Failed") diff --git a/micropython/examples/badger2040w/main.py b/micropython/examples/badger2040w/main.py new file mode 100644 index 00000000..967457fd --- /dev/null +++ b/micropython/examples/badger2040w/main.py @@ -0,0 +1 @@ +import launcher # noqa F401 diff --git a/micropython/examples/badger2040w/uf2-manifest.txt b/micropython/examples/badger2040w/uf2-manifest.txt new file mode 100644 index 00000000..715515cd --- /dev/null +++ b/micropython/examples/badger2040w/uf2-manifest.txt @@ -0,0 +1,10 @@ +*.py +lib/*.py +examples/*.jpg +examples/*.py +images/*.jpg +images/*.txt +badges/*.txt +badges/*.jpg +books/*.txt +icons/*.jpg \ No newline at end of file diff --git a/micropython/modules/badger2040/badger2040.cpp b/micropython/modules/badger2040/badger2040.cpp index b6577e45..de59c6d2 100644 --- a/micropython/modules/badger2040/badger2040.cpp +++ b/micropython/modules/badger2040/badger2040.cpp @@ -67,7 +67,7 @@ mp_obj_t Badger2040___del__(mp_obj_t self_in) { _Badger2040_obj_t *self = MP_OBJ_TO_PTR2(self_in, _Badger2040_obj_t); // Try to ensure power is cut off when soft reset (IE: "Stop" in Thonny) self->badger2040->power_off(); - delete self->badger2040; + //delete self->badger2040; return mp_const_none; } @@ -101,7 +101,7 @@ mp_obj_t Badger2040_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_ badger2040_obj = m_new_obj_with_finaliser(_Badger2040_obj_t); badger2040_obj->base.type = &Badger2040_type; badger2040_obj->buf = buffer; - badger2040_obj->badger2040 = new pimoroni::Badger2040(buffer); + badger2040_obj->badger2040 = m_new_class(pimoroni::Badger2040, buffer); badger2040_obj->badger2040->init(); return MP_OBJ_FROM_PTR(badger2040_obj); diff --git a/micropython/modules/badger2040w/README.md b/micropython/modules/badger2040w/README.md new file mode 100644 index 00000000..fd399cd1 --- /dev/null +++ b/micropython/modules/badger2040w/README.md @@ -0,0 +1,260 @@ +# Badger 2040 W + +Badger 2040 W is a Raspberry Pi Pico W powered E Ink badge. + +- [Summary](#summary) + - [Differences between Badger 2040 W and Badger 2040](#differences-between-badger-2040-w-and-badger-2040) + - [Getting Started](#getting-started) + - [Constants](#constants) + - [Screen Size](#screen-size) + - [E Ink Pins](#e-ink-pins) + - [Power Pins](#power-pins) + - [Activity LED Pin](#activity-led-pin) +- [Function Reference](#function-reference) + - [Basic Drawing Settings](#basic-drawing-settings) + - [Pen Colour](#pen-colour) + - [Pen Thickness](#pen-thickness) + - [Displaying Images](#displaying-images) + - [Updating The Display](#updating-the-display) + - [Update](#update) + - [Clear](#clear) + - [Partial Update](#partial-update) + - [Update Speed](#update-speed) + - [LED](#led) + - [Buttons](#buttons) + - [Waking From Sleep](#waking-from-sleep) + - [Button Presses](#button-presses) + - [Real-time Clock](#real-time-clock) + - [Update Speed](#update-speed-1) + - [System speed](#system-speed) + +# Summary + +## Differences between Badger 2040 W and Badger 2040 + +Badger 2040 W switches from the Badger-specific drawing library of Badger 2040, to our generic PicoGraphics library. + +PicoGraphics brings some great improvements, such as JPEG support with dithering and cross-compatibility between all of our other display products. + +We've tried to make the transition as simple as possible, but there are a few breaking changes you'll need to be aware of: + +* `pen()` is now `set_pen()` +* `update_speed()` is now `set_update_speed()` +* `thickness()` is now `set_thickness()` and *only* applies to Hershey fonts +* `image()` and `icon()` are deprecated, use JPEGs instead. +* `invert()` is not supported. + +See the [PicoGraphics function reference](../picographics/README.md) for more information on how to draw to the display. + +Additionally Badger 2040 W does not have a "user" button since the BOOTSEL button (which originally doubled as "user") is now aboard the attached Pico W. + +## Getting Started + +:warning: If you're using the examples-included firmware you're good to go, otherwise you'll need to copy `examples/badger2040w/lib/badger2040w.py` and `examples/badger2040w/lib/network_manager.py` over to your Badger 2040 W. + +To start coding your Badger 2040 W, you will need to add the following lines of code to the start of your code file. + +```python +import badger2040w +badger = badger2040w.Badger2040W() +``` + +This will create a `Badger2040W` class called `badger` that will be used in the rest of the examples going forward. + +## Constants + +Below is a list of other constants that have been made available, to help with the creation of more advanced programs. + +### Screen Size + +* `WIDTH` = `296` +* `HEIGHT` = `128` + +### E Ink Pins + +* `BUSY` = `26` + +### Power Pins + +* `ENABLE_3V3` = `10` + +### Activity LED Pin + +* `LED` = `22` + +# Function Reference + +## Basic Drawing Settings + +Since Badger 2040 W is based upon PicoGraphics you should read the [PicoGraphics function reference](../picographics/README.md) for more information about how to draw to the display. + +### Pen Colour + +There are 16 pen colours - or "shades of grey" - to choose, from 0 (black) to 15 (white). + +Since Badger 2040 W cannot display colours other than black and white, any value from 1 to 14 will apply dithering when drawn, to simulate a shade of grey. + +```python +pen( + colour # int: colour from 0 to 15 +) +``` + +### Pen Thickness + +:warning: Applies to Hershey fonts only. + +Thickness affects Hershey text and governs how thick the component lines should be, making it appear bolder: + +```python +set_thickness( + value # int: thickness in pixels +) +``` + +## Displaying Images + +Badger 2040 W can display basic JPEG images. They must not be progressive. It will attempt to dither them to the black/white display. + +To display a JPEG, import and set up the `jpegdec` module like so: + +```python +import badger2040w +import jpegdec + +badger = badger2040w.Badger2040W() +jpeg = jpegdec.JPEG(badger.display) +``` + +`badger.display` points to the PicoGraphics instance that the Badger2040W class manages for you. + +You can open and display a JPEG file like so: + +```python +jpeg.open_file("/image.jpg") +jpeg.decode(x, y) +``` + +Where `x, y` is the position at which you want to display the JPEG. + +## Updating The Display + +### Update + +Starts a full update of the screen. Will block until the update has finished. + +Update takes no parameters, but the update time will vary depending on which update speed you've selected. + +```python +badger.update() +``` + +### Clear + +Before drawing again it can be useful to `clear` your display. + +`clear` fills the drawing buffer with the pen colour, giving you a clean slate: + +```python +badger.clear() +``` + +### Partial Update + +Starts a partial update of the screen. Will block until the update has finished. + +A partial update allows you to update a portion of the screen rather than the whole thing. + +That portion *must* be a multiple of 8 pixels tall, but can be any number of pixels wide. + +```python +partial_update( + x, # int: x coordinate of the update region + y, # int: y coordinate of the update region (must be a multiple of 8) + w, # int: width of the update region + h # int: height of the update region (must be a multiple of 8) +) +``` + +### Update Speed + +Badger 2040 W is capable of updating the display at multiple different speeds. + +These offer a tradeoff between the quality of the final image and the speed of the update. + +There are currently four constants naming the different update speeds from 0 to 3: + +* `UPDATE_NORMAL` - a normal update, great for display the first screen of your application and ensuring good contrast and no ghosting +* `UPDATE_MEDIUM` - a good balance of speed and clarity, you probably want this most of the time +* `UPDATE_FAST` - a fast update, good for stepping through screens such as the pages of a book or the launcher +* `UPDATE_TURBO` - a super fast update, prone to ghosting, great for making minor changes such as moving a cursor through a menu + +```python +update_speed( + speed # int: one of the update constants +) +``` + +## LED + +The white indicator LED can be controlled, with brightness ranging from 0 (off) to 255: + +```python +led( + brightness # int: 0 (off) to 255 (full) +) +``` + +## Buttons + +Badger 2040 W features five buttons on its front, labelled A, B, C, ↑ (up) and ↓ (down). These can be read using the `pressed(button)` method, which accepts the button's pin number. For convenience, each button can be referred to using these constants: + +* `BUTTON_A` = `12` +* `BUTTON_B` = `13` +* `BUTTON_C` = `14` +* `BUTTON_UP` = `15` +* `BUTTON_DOWN` = `11` + +Additionally you can use `pressed_any()` to see if _any_ button has been pressed. + +## Waking From Sleep + +### Button Presses + +When running on battery, pressing a button on Badger 2040 W will power the unit on. It will automatically be latched on and `main.py` will be executed. + +There are some useful functions to determine if Badger 2040 W has been woken by a button, and figure out which one: + +* `badger2040w.woken_by_button()` - determine if any button was pressed during power-on. +* `badger2040w.pressed_to_wake(button)` - determine if the given button was pressed during power-on. +* `badger2040w.reset_pressed_to_wake()` - clear the wakeup GPIO state. +* `badger2040w.pressed_to_wake_get_once(button)` - returns `True` if the given button was pressed to wake Badger, and then clears the state of that pin. + +### Real-time Clock + +Badger 2040 W includes a PCF85063a RTC which continues to run from battery when the Badger is off. It can be used to wake the Badger on a schedule. + +## Update Speed + +The E Ink display on Badger 2040 W supports several update speeds. These can be set using `set_update_speed(speed)` where `speed` is a value from `0` to `3`. For convenience these speeds have been given the following constants: + +* `UPDATE_NORMAL` = `0` +* `UPDATE_MEDIUM` = `1` +* `UPDATE_FAST` = `2` +* `UPDATE_TURBO` = `3` + +## System speed + +The system clock speed of the RP2040 can be controlled, allowing power to be saved if on battery, or faster computations to be performed. Use `badger2040w.system_speed(speed)` where `speed` is one of the following constants: + +* `SYSTEM_VERY_SLOW` = `0` _4 MHz if on battery, 48 MHz if connected to USB_ +* `SYSTEM_SLOW` = `1` _12 MHz if on battery, 48 MHz if connected to USB_ +* `SYSTEM_NORMAL` = `2` _48 MHz_ +* `SYSTEM_FAST` = `3` _133 MHz_ +* `SYSTEM_TURBO` = `4` _250 MHz_ + +On USB, the system will not run slower than 48MHz, as that is the minimum clock speed required to keep the USB connection stable. + +It is best to set the clock speed as the first thing in your program, and you must not change it after initializing any drivers for any I2C hardware connected to the Qwiic port. To allow you to set the speed at the top of your program, this method is on the `badger2040w` module, rather than the `badger` instance, although we have made sure that it is safe to call it after creating a `badger` instance. + +Note that `SYSTEM_TURBO` overclocks the RP2040 to 250MHz, and applies a small over voltage to ensure this is stable. We've found that every RP2040 we've tested is happy to run at this speed without any issues. diff --git a/micropython/modules/jpegdec/jpegdec.cpp b/micropython/modules/jpegdec/jpegdec.cpp index 6153368e..fb0ba23c 100644 --- a/micropython/modules/jpegdec/jpegdec.cpp +++ b/micropython/modules/jpegdec/jpegdec.cpp @@ -95,9 +95,10 @@ MICROPY_EVENT_POLL_HOOK uint8_t *pixel = (uint8_t *)pDraw->pPixels; for(int y = 0; y < pDraw->iHeight; y++) { for(int x = 0; x < pDraw->iWidth; x++) { - if(x >= pDraw->iWidthUsed) break; // Clip to the used width - current_graphics->set_pen((uint8_t)(*pixel >> 4)); - current_graphics->set_pixel({pDraw->x + x, pDraw->y + y}); + if(x < pDraw->iWidthUsed) { // Clip to the used width + current_graphics->set_pen((uint8_t)(*pixel >> 4)); + current_graphics->pixel({pDraw->x + x, pDraw->y + y}); + } pixel++; } } diff --git a/micropython/modules/micropython-badger2040w.cmake b/micropython/modules/micropython-badger2040w.cmake new file mode 100644 index 00000000..b3fcf252 --- /dev/null +++ b/micropython/modules/micropython-badger2040w.cmake @@ -0,0 +1,59 @@ +include_directories(${CMAKE_CURRENT_LIST_DIR}/../../) + +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}") +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/../") +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/../../") + +# Essential +include(pimoroni_i2c/micropython) +include(pimoroni_bus/micropython) + +# Pico Graphics Essential +include(hershey_fonts/micropython) +include(bitmap_fonts/micropython) +include(picographics/micropython) + +# Pico Graphics Extra +include(jpegdec/micropython) +include(qrcode/micropython/micropython) + +# Sensors & Breakouts +include(breakout_dotmatrix/micropython) +include(breakout_encoder/micropython) +include(breakout_ioexpander/micropython) +include(breakout_ltr559/micropython) +include(breakout_as7262/micropython) +include(breakout_rgbmatrix5x5/micropython) +include(breakout_matrix11x7/micropython) +include(breakout_msa301/micropython) +include(breakout_pmw3901/micropython) +include(breakout_mics6814/micropython) +include(breakout_potentiometer/micropython) +include(breakout_rtc/micropython) +include(breakout_trackball/micropython) +include(breakout_sgp30/micropython) +include(breakout_bh1745/micropython) +include(breakout_bme68x/micropython) +include(breakout_bme280/micropython) +include(breakout_bmp280/micropython) +include(breakout_icp10125/micropython) +include(breakout_scd41/micropython) +include(breakout_vl53l5cx/micropython) +include(pcf85063a/micropython) + +# Utility +include(adcfft/micropython) +include(wakeup/micropython) + +# LEDs & Matrices +include(plasma/micropython) + +# Servos & Motors +include(pwm/micropython) +include(servo/micropython) +include(encoder/micropython) +include(motor/micropython) + +# include(micropython-common) + +include(modules_py/modules_py) \ No newline at end of file diff --git a/micropython/modules/picographics/picographics.c b/micropython/modules/picographics/picographics.c index bf01cdc5..6d801fb6 100644 --- a/micropython/modules/picographics/picographics.c +++ b/micropython/modules/picographics/picographics.c @@ -22,6 +22,7 @@ MP_DEFINE_CONST_FUN_OBJ_KW(ModPicoGraphics_set_palette_obj, 2, ModPicoGraphics_s 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); // Primitives MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(ModPicoGraphics_set_clip_obj, 5, 5, ModPicoGraphics_set_clip); @@ -56,6 +57,7 @@ MP_DEFINE_CONST_FUN_OBJ_1(ModPicoGraphics__del__obj, ModPicoGraphics__del__); 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_update), MP_ROM_PTR(&ModPicoGraphics_update_obj) }, diff --git a/micropython/modules/picographics/picographics.cpp b/micropython/modules/picographics/picographics.cpp index 20487930..b394bcaf 100644 --- a/micropython/modules/picographics/picographics.cpp +++ b/micropython/modules/picographics/picographics.cpp @@ -711,6 +711,18 @@ mp_obj_t ModPicoGraphics_create_pen_hsv(size_t n_args, const mp_obj_t *args) { 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); + + if(self->graphics->pen_type != PicoGraphics::PEN_1BIT) { + mp_raise_msg(&mp_type_RuntimeError, MP_ERROR_TEXT("Thickness not supported!")); + } + + 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; diff --git a/micropython/modules/picographics/picographics.h b/micropython/modules/picographics/picographics.h index e46ba904..d07ad4fd 100644 --- a/micropython/modules/picographics/picographics.h +++ b/micropython/modules/picographics/picographics.h @@ -72,6 +72,7 @@ extern mp_obj_t ModPicoGraphics_hsv_to_rgb(size_t n_args, const mp_obj_t *args); 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); // Primitives extern mp_obj_t ModPicoGraphics_set_clip(size_t n_args, const mp_obj_t *args); diff --git a/micropython/modules/wakeup/wakeup.c b/micropython/modules/wakeup/wakeup.c index f6284f71..e5204112 100644 --- a/micropython/modules/wakeup/wakeup.c +++ b/micropython/modules/wakeup/wakeup.c @@ -1,12 +1,14 @@ #include "wakeup.h" STATIC MP_DEFINE_CONST_FUN_OBJ_0(Wakeup_get_gpio_state_obj, Wakeup_get_gpio_state); +STATIC MP_DEFINE_CONST_FUN_OBJ_0(Wakeup_reset_gpio_state_obj, Wakeup_reset_gpio_state); STATIC MP_DEFINE_CONST_FUN_OBJ_0(Wakeup_get_shift_state_obj, Wakeup_get_shift_state); STATIC MP_DEFINE_CONST_FUN_OBJ_0(Wakeup_reset_shift_state_obj, Wakeup_reset_shift_state); STATIC const mp_map_elem_t wakeup_globals_table[] = { { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_wakeup) }, { MP_ROM_QSTR(MP_QSTR_get_gpio_state), MP_ROM_PTR(&Wakeup_get_gpio_state_obj) }, + { MP_ROM_QSTR(MP_QSTR_reset_gpio_state), MP_ROM_PTR(&Wakeup_reset_gpio_state_obj) }, { MP_ROM_QSTR(MP_QSTR_get_shift_state), MP_ROM_PTR(&Wakeup_get_shift_state_obj) }, { MP_ROM_QSTR(MP_QSTR_reset_shift_state), MP_ROM_PTR(&Wakeup_reset_shift_state_obj) } }; diff --git a/micropython/modules/wakeup/wakeup.cpp b/micropython/modules/wakeup/wakeup.cpp index afe5c8da..e5127526 100644 --- a/micropython/modules/wakeup/wakeup.cpp +++ b/micropython/modules/wakeup/wakeup.cpp @@ -62,6 +62,11 @@ mp_obj_t Wakeup_get_gpio_state() { return mp_obj_new_int(runtime_wakeup_gpio_state); } +mp_obj_t Wakeup_reset_gpio_state() { + runtime_wakeup_gpio_state = 0; + return mp_const_none; +} + void err_no_sr() { mp_raise_msg(&mp_type_RuntimeError, "Wakeup_get_shift_state: board does not have a shift register."); } diff --git a/micropython/modules/wakeup/wakeup.h b/micropython/modules/wakeup/wakeup.h index 7330b739..d14cff23 100644 --- a/micropython/modules/wakeup/wakeup.h +++ b/micropython/modules/wakeup/wakeup.h @@ -2,5 +2,6 @@ #include "py/objstr.h" extern mp_obj_t Wakeup_get_gpio_state(); +extern mp_obj_t Wakeup_reset_gpio_state(); extern mp_obj_t Wakeup_get_shift_state(); extern mp_obj_t Wakeup_reset_shift_state(); \ No newline at end of file