diff --git a/.github/workflows/micropython.yml b/.github/workflows/micropython.yml index c6be5a4..218fbad 100644 --- a/.github/workflows/micropython.yml +++ b/.github/workflows/micropython.yml @@ -63,22 +63,32 @@ jobs: shell: bash run: | source $CI_PROJECT_ROOT/ci/micropython.sh && ci_debug + source $(dpkg -L virtualenvwrapper | grep "virtualenvwrapper.sh") + mkvirtualenv "dir2uf2" ci_cmake_build ${{ matrix.name }} mv "$CI_BUILD_ROOT/${{ matrix.name }}.uf2" "$CI_BUILD_ROOT/$RELEASE_FILE.uf2" + mv "$CI_BUILD_ROOT/${{ matrix.name }}-with-filesystem.uf2" "$CI_BUILD_ROOT/$RELEASE_FILE-with-filesystem.uf2" - name: "Artifacts: Upload .uf2" uses: actions/upload-artifact@v4 with: name: ${{ env.RELEASE_FILE }}.uf2 path: ${{ env.CI_BUILD_ROOT }}/${{ env.RELEASE_FILE }}.uf2 + + - name: "Artifacts: Upload .uf2 (With Filesystem)" + uses: actions/upload-artifact@v4 + with: + name: ${{ env.RELEASE_FILE }}.uf2 + path: ${{ env.CI_BUILD_ROOT }}/${{ env.RELEASE_FILE }}-with-filesystem.uf2 - name: "Release: Upload .uf2" if: github.event_name == 'release' - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: softprops/action-gh-release@v1 + with: + files: ${{ env.CI_BUILD_ROOT }}/${{ env.RELEASE_FILE }}.uf2 + + - name: "Release: Upload .uf2 (With Filesystem)" + if: github.event_name == 'release' + uses: softprops/action-gh-release@v1 with: - asset_path: ${{ env.CI_BUILD_ROOT }}/${{ env.RELEASE_FILE }}.uf2 - upload_url: ${{ github.event.release.upload_url }} - asset_name: ${{ env.RELEASE_FILE }}.uf2 - asset_content_type: application/octet-stream + files: ${{ env.CI_BUILD_ROOT }}/${{ env.RELEASE_FILE }}-with-filesystem.uf2 diff --git a/boards/pico2_w_inky/manifest.txt b/boards/pico2_w_inky/manifest.txt new file mode 100644 index 0000000..05cb090 --- /dev/null +++ b/boards/pico2_w_inky/manifest.txt @@ -0,0 +1,3 @@ +*.py +lib/* +lib/*/* \ No newline at end of file diff --git a/boards/pico_w_inky/manifest.txt b/boards/pico_w_inky/manifest.txt new file mode 100644 index 0000000..05cb090 --- /dev/null +++ b/boards/pico_w_inky/manifest.txt @@ -0,0 +1,3 @@ +*.py +lib/* +lib/*/* \ No newline at end of file diff --git a/ci/micropython.sh b/ci/micropython.sh index 445e06a..6466293 100644 --- a/ci/micropython.sh +++ b/ci/micropython.sh @@ -70,7 +70,7 @@ function ci_micropython_build_mpy_cross { } function ci_apt_install_build_deps { - sudo apt update && sudo apt install ccache + sudo apt update && sudo apt install ccache python3-virtualenvwrapper virtualenvwrapper } function ci_prepare_all { @@ -98,7 +98,8 @@ function ci_cmake_configure { log_warning "Invalid board: $MICROPY_BOARD_DIR" return 1 fi - cmake -S $CI_BUILD_ROOT/micropython/ports/rp2 -B build-$BOARD \ + BUILD_DIR="$CI_BUILD_ROOT/build-$BOARD" + cmake -S $CI_BUILD_ROOT/micropython/ports/rp2 -B "$BUILD_DIR" \ -DPICOTOOL_FORCE_FETCH_FROM_GIT=1 \ -DPICO_BUILD_DOCS=0 \ -DPICO_NO_COPRO_DIS=1 \ @@ -112,13 +113,23 @@ function ci_cmake_configure { function ci_cmake_build { BOARD=$1 + MICROPY_BOARD_DIR=$CI_PROJECT_ROOT/boards/$BOARD + EXAMPLES_DIR=$CI_PROJECT_ROOT/examples/inkylauncher/ + TOOLS_DIR=$CI_BUILD_ROOT/tools + BUILD_DIR="$CI_BUILD_ROOT/build-$BOARD" ccache --zero-stats || true - cmake --build build-$BOARD -j 2 + cmake --build $BUILD_DIR -j 2 ccache --show-stats || true - if [ -d "tools/py_decl" ]; then + if [ -d "$TOOLS_DIR/py_decl" ]; then log_inform "Tools found, verifying .uf2 with py_decl..." - python3 tools/py_decl/py_decl.py --to-json --verify "build-$BOARD/firmware.uf2" + python3 "$TOOLS_DIR/py_decl/py_decl.py" --to-json --verify "$BUILD_DIR/firmware.uf2" fi log_inform "Copying .uf2 to $(pwd)/$BOARD.uf2" - cp build-$BOARD/firmware.uf2 $BOARD.uf2 + cp "$BUILD_DIR/firmware.uf2" $BOARD.uf2 + + if [ -f "$MICROPY_BOARD_DIR/manifest.txt" ] && [ -d "$TOOLS_DIR/dir2uf2" ]; then + log_inform "Creating $(pwd)/$BOARD-with-filesystem.uf2" + python3 -m pip install littlefs-python==0.12.0 + $TOOLS_DIR/dir2uf2/dir2uf2 --fs-compact --sparse --append-to "$(pwd)/$BOARD.uf2" --manifest "$MICROPY_BOARD_DIR/manifest.txt" --filename with-filesystem.uf2 "$EXAMPLES_DIR" + fi } diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..7f4c5be --- /dev/null +++ b/examples/README.md @@ -0,0 +1,110 @@ +# Pico Inky Frame MicroPython Examples + +- [PicoGraphics](#picographics) +- [Examples](#examples) + - [Button Test](#button-test) + - [Carbon Intensity](#carbon-intensity) + - [Daily Activity](#daily-activity) + - [Dithering](#dithering) + - [Image Gallery](#image-gallery) + - [LED PWM](#led-pwm) + - [News](#news) + - [PlaceKitten](#placekitten) + - [Quote of the Day](#quote-of-the-day) + - [Random Joke](#random-joke) + - [RTC Demo](#rtc-demo) + - [SD Card Test](#sd-card-test) + - [XKCD Daily](#xkcd-daily) + +## PicoGraphics + +You can draw on Inky Frame using our tiny PicoGraphics display library. +- [PicoGraphics MicroPython function reference](../../modules/picographics) + +## Examples + +The wireless examples need `network_manager.py` and `WIFI_CONFIG.py` from the `common` directory to be saved to your Pico W. Open up `WIFI_CONFIG.py` in Thonny to add your wifi details (and save it when you're done). + +You'll also need to install the `micropython-urllib.urequest` library using Thonny's 'Tools' > 'Manage Packages' or `common/lib/urllib` which contains a compiled `.mpy` version that uses less RAM. You should place this directory in `lib` on your Pico W. + +### Button Test +[button_test.py](button_test.py) + +Inky Frame's buttons (and the RTC alarm, busy signal from the screen and external trigger from the hack header) are connected to a shift register to help conserve pins, and to allow these inputs to wake the board up from sleep. + +This example demonstrates a simple way of reading when a button has been pushed by reading the shift register and checking if the bit in a specific position is 0 or 1. + +### Carbon Intensity +[carbon_intensity.py](carbon_intensity.py) + +This example connects to the Carbon Intensity API to give you a regional forecast of how your (UK) electricity is being generated and its carbon impact. + +Find out more at https://carbonintensity.org.uk/ + +### Daily Activity +[inky_frame_daily_activity.py](inky_frame_daily_activity.py) + +Generate a random activity from Bored API. + +### Dithering +[inky_frame_dithering.py](inky_frame_dithering.py) + +A basic example showing automatic dithering in action, as PicoGraphics tries to use Inky Frame's limited colour palette to match arbitrary colours. + +### Image Gallery +[/image_gallery](../inky_frame/image_gallery) + +This photo frame example displays local images on Inky Frame and lets you switch between them with the buttons. Use `image_gallery.py` if your images are stored on your Pico, or `image_gallery_sd.py` if the images are on your SD card. + +### LED PWM +[led_pwm.py](led_pwm.py) + +A basic example showing how you can control the brightness of Inky Frame's LEDs using PWM. + +### News +[inky_frame_news.py](inky_frame_news.py) + +Display headlines from BBC News. + +### PlaceKitten +[inky_frame_placekitten.py](inky_frame_placekitten.py) + +Download a random (from a small subset) image from PlaceKitten. + +### Quote of the Day +[inky_frame_quote_of_the_day.py](inky_frame_quote_of_the_day.py) + +Load the WikiQuotes Quote of the Day and display it. + +### Random Joke +[inky_frame_random_joke.py](inky_frame_random_joke.py) + +Load a random joke from JokeAPI.dev and display it. + +Jokes are rendered into images "offline" by our feed2image service for two reasons: + +1. Saves the Pico W having to process them +2. JokeAPI.dev needs TLS1.3 which Pico W does not support! + +For bugs/contributions or to complain about a joke, see: https://github.com/pimoroni/feed2image + +### RTC Demo +[inky_frame_rtc_demo.py](inky_frame_rtc_demo.py) + +A basic example that sets the time/date from an NTP server, syncs the Inky and Pico RTCs and and makes Inky Frame wake up on a timer. + +### SD Card Test +[sd_test.py](sd_test.py) + +This simple example shows how to read and write from the SD card on Inky Frame. + +### XKCD Daily +[inky_frame_xkcd_daily.py](inky_frame_xkcd_daily.py) + +Download and display the daily webcomic from https://xkcd.com/ + +The webcomic is rendered "offline" by our feed2image service since xkcd.com requires TLS1.3! + +For bugs/contributions see: https://github.com/pimoroni/feed2image + + diff --git a/examples/button_demo.py b/examples/button_demo.py new file mode 100644 index 0000000..8feed68 --- /dev/null +++ b/examples/button_demo.py @@ -0,0 +1,86 @@ +# This example shows you a simple, non-interrupt way of reading Inky Frame's buttons with a loop that checks to see if buttons are pressed. + +from pimoroni import ShiftRegister +# from picographics import PicoGraphics, DISPLAY_INKY_FRAME as DISPLAY # 5.7" +# from picographics import PicoGraphics, DISPLAY_INKY_FRAME_4 as DISPLAY # 4.0" +from picographics import PicoGraphics, DISPLAY_INKY_FRAME_7 as DISPLAY # 7.3" +from machine import Pin + +display = PicoGraphics(display=DISPLAY) + +display.set_font("bitmap8") + +# Inky Frame uses a shift register to read the buttons +SR_CLOCK = 8 +SR_LATCH = 9 +SR_OUT = 10 + +sr = ShiftRegister(SR_CLOCK, SR_LATCH, SR_OUT) + +# set up the button LEDs +button_a_led = Pin(11, Pin.OUT) +button_b_led = Pin(12, Pin.OUT) +button_c_led = Pin(13, Pin.OUT) +button_d_led = Pin(14, Pin.OUT) +button_e_led = Pin(15, Pin.OUT) + + +# a handy function we can call to clear the screen +# display.set_pen(1) is white and display.set_pen(0) is black +def clear(): + display.set_pen(1) + display.clear() + + +# set up +clear() +display.set_pen(0) +display.text("Press any button!", 10, 10, scale=4) +display.update() + +while True: + button_a_led.off() + button_b_led.off() + button_c_led.off() + button_d_led.off() + button_e_led.off() + + # read the shift register + # we can tell which button has been pressed by checking if a specific bit is 0 or 1 + result = sr.read() + button_a = sr[7] + button_b = sr[6] + button_c = sr[5] + button_d = sr[4] + button_e = sr[3] + + if button_a == 1: # if a button press is detected then... + button_a_led.on() + clear() # clear to white + display.set_pen(4) # change the pen colour + display.text("Button A pressed", 10, 10, scale=4) # display some text on the screen + display.update() # update the display + elif button_b == 1: + button_b_led.on() + clear() + display.set_pen(6) + display.text("Button B pressed", 10, 50, scale=4) + display.update() + elif button_c == 1: + button_c_led.on() + clear() + display.set_pen(5) + display.text("Button C pressed", 10, 90, scale=4) + display.update() + elif button_d == 1: + button_d_led.on() + clear() + display.set_pen(2) + display.text("Button D pressed", 10, 130, scale=4) + display.update() + elif button_e == 1: + button_e_led.on() + clear() + display.set_pen(3) + display.text("Button E pressed", 10, 170, scale=4) + display.update() diff --git a/examples/button_test.py b/examples/button_test.py new file mode 100644 index 0000000..62c44db --- /dev/null +++ b/examples/button_test.py @@ -0,0 +1,59 @@ +# This example allows you to test Inky Frame's buttons +# It does not update the screen. + +from pimoroni import ShiftRegister +from machine import Pin +import time + + +# Inky Frame uses a shift register to read the buttons +SR_CLOCK = 8 +SR_LATCH = 9 +SR_OUT = 10 + +sr = ShiftRegister(SR_CLOCK, SR_LATCH, SR_OUT) + + +# Simple class to debounce button input and handle LED +class Button: + def __init__(self, idx, led, debounce=50): + self.led = Pin(led, Pin.OUT) # LEDs are just regular IOs + self.led.on() + self._idx = idx + self._debounce_time = debounce + self._changed = time.ticks_ms() + self._last_value = None + + def debounced(self): + return time.ticks_ms() - self._changed > self._debounce_time + + def get(self, sr): + value = sr[self._idx] + if value != self._last_value and self.debounced(): + self._last_value = value + self._changed = time.ticks_ms() + return value + + +button_a = Button(7, 11) +button_b = Button(6, 12) +button_c = Button(5, 13) +button_d = Button(4, 14) +button_e = Button(3, 15) + + +while True: + sr.read() + + if button_a.get(sr): + button_a.led.toggle() + if button_b.get(sr): + button_b.led.toggle() + if button_c.get(sr): + button_c.led.toggle() + if button_d.get(sr): + button_d.led.toggle() + if button_e.get(sr): + button_e.led.toggle() + + time.sleep(1.0 / 60) # Poll 60 times/second diff --git a/examples/carbon_intensity.py b/examples/carbon_intensity.py new file mode 100644 index 0000000..20c4142 --- /dev/null +++ b/examples/carbon_intensity.py @@ -0,0 +1,149 @@ +""" +This example connects to the Carbon Intensity API to give you a regional +forecast of how your (UK) electricity is being generated and its carbon impact. + +Carbon Intensity API only reports data from the UK National Grid. + +Find out more about what the numbers mean at: +https://carbonintensity.org.uk/ + +Make sure to uncomment the correct size for your display! + +""" + +from picographics import PicoGraphics, DISPLAY_INKY_FRAME_4 as DISPLAY # 4.0" +# from picographics import PicoGraphics, DISPLAY_INKY_FRAME as DISPLAY # 5.7" +# from picographics import PicoGraphics, DISPLAY_INKY_FRAME_7 as DISPLAY # 7.3" +import urequests +import inky_frame +import uasyncio +from network_manager import NetworkManager +import WIFI_CONFIG + +# Set (the first half) of your UK postcode here +POSTCODE = "S9" + +URL = "https://api.carbonintensity.org.uk/regional/postcode/" + str(POSTCODE) + + +def get_data(): + global region, forecast, index, power_list, datetime_to, datetime_from + print(f"Requesting URL: {URL}") + r = urequests.get(URL) + # open the json data + j = r.json() + print("Data obtained!") + print(j) + + # parse the json data + region = j["data"][0]["shortname"] + + forecast = j["data"][0]["data"][0]["intensity"]["forecast"] + index = j["data"][0]["data"][0]["intensity"]["index"] + + power_list = [] + for power in j["data"][0]["data"][0]["generationmix"]: + power_list.append(power['perc']) + + datetime_to = j["data"][0]["data"][0]["to"].split("T") + datetime_from = j["data"][0]["data"][0]["from"].split("T") + + # close the socket + r.close() + + +def draw(): + global graphics + # we're setting up our PicoGraphics buffer after we've made our RAM intensive https request + graphics = PicoGraphics(DISPLAY) + w, h = graphics.get_bounds() + graphics.set_pen(inky_frame.WHITE) + graphics.clear() + + # draw lines + graphics.set_pen(inky_frame.BLACK) + graphics.line(0, int((h / 100) * 0), w, int((h / 100) * 0)) + graphics.line(0, int((h / 100) * 50), w, int((h / 100) * 50)) + graphics.set_font("bitmap8") + graphics.text('100%', w - 40, 10, scale=2) + graphics.text('50%', w - 40, int((h / 100) * 50 + 10), scale=2) + + # draw bars + bar_colours = [ + inky_frame.ORANGE, + inky_frame.RED, + inky_frame.ORANGE, + inky_frame.RED, + inky_frame.BLUE, + inky_frame.ORANGE, + inky_frame.GREEN, + inky_frame.GREEN, + inky_frame.GREEN + ] + for p in power_list: + graphics.set_pen(bar_colours[power_list.index(p)]) + graphics.rectangle(int(power_list.index(p) * w / 9), int(h - p * (h / 100)), + int(w / 9), int(h / 100 * p)) + + # draw labels + graphics.set_font('sans') + # once in white for a background + graphics.set_pen(inky_frame.WHITE) + labels = ['biomass', 'coal', 'imports', 'gas', 'nuclear', 'other', 'hydro', 'solar', 'wind'] + graphics.set_thickness(4) + for label in labels: + graphics.text(f'{label}', int((labels.index(label) * w / 9) + (w / 9) / 2), h - 10, angle=270, scale=1) + # again in black + graphics.set_pen(inky_frame.BLACK) + labels = ['biomass', 'coal', 'imports', 'gas', 'nuclear', 'other', 'hydro', 'solar', 'wind'] + graphics.set_thickness(2) + for label in labels: + graphics.text(f'{label}', int((labels.index(label) * w / 9) + (w / 9) / 2), h - 10, angle=270, scale=1) + + # draw header + graphics.set_thickness(3) + graphics.set_pen(inky_frame.GREEN) + if index in ['high', 'very high']: + graphics.set_pen(inky_frame.RED) + if index in ['moderate']: + graphics.set_pen(inky_frame.ORANGE) + graphics.set_font("sans") + graphics.text('Carbon Intensity', 10, 35, scale=1.2, angle=0) + + # draw small text + graphics.set_pen(inky_frame.BLACK) + graphics.set_font("bitmap8") + graphics.text(f'Region: {region}', int((w / 2) + 30), 10, scale=2) + graphics.text(f'{forecast} gCO2/kWh ({index})', int((w / 2) + 30), 30, scale=2) + graphics.text(f'{datetime_from[0]} {datetime_from[1]} to {datetime_to[1]}', int((w / 2) + 30), 50, scale=2) + + graphics.update() + + +def status_handler(mode, status, ip): + print(mode, status, ip) + + +inky_frame.led_busy.on() +network_manager = NetworkManager(WIFI_CONFIG.COUNTRY, status_handler=status_handler) + +# connect to wifi +try: + uasyncio.get_event_loop().run_until_complete(network_manager.client(WIFI_CONFIG.SSID, WIFI_CONFIG.PSK)) +except ImportError: + print("Add WIFI_CONFIG.py with your WiFi credentials") + +get_data() +draw() + +# Go to sleep if on battery power +inky_frame.turn_off() + +# Or comment out the line above and uncomment this one to wake up and update every half hour +# inky_frame.sleep_for(30) + +""" +Pico W RAM seems insufficient to decode a https request whilst having a PicoGraphics instance active. +If you are running off USB and want this to update periodically, you could incorporate a machine.reset() +to reset the Pico and start afresh every time. +""" diff --git a/examples/display_png.py b/examples/display_png.py new file mode 100644 index 0000000..7b3cd58 --- /dev/null +++ b/examples/display_png.py @@ -0,0 +1,36 @@ +from picographics import PicoGraphics, DISPLAY_INKY_FRAME as DISPLAY # 5.7" +# from picographics import PicoGraphics, DISPLAY_INKY_FRAME_4 as DISPLAY # 4.0" +# from picographics import PicoGraphics, DISPLAY_INKY_FRAME_7 as DISPLAY # 7.3" +import pngdec + +# Create a PicoGraphics instance +graphics = PicoGraphics(DISPLAY) +WIDTH, HEIGHT = graphics.get_bounds() + +# Set the font +graphics.set_font("bitmap8") + +# Create an instance of the PNG Decoder +png = pngdec.PNG(graphics) + +# Clear the screen +graphics.set_pen(1) +graphics.clear() +graphics.set_pen(0) + +# Few lines of text. +graphics.text("PNG Pencil", 70, 100, WIDTH, 3) + +# Open our PNG File from flash. In this example we're using a cartoon pencil. +# You can use Thonny to transfer PNG Images to your Inky Frame. +try: + png.open_file("pencil_256x256.png") + + # Decode our PNG file and set the X and Y + png.decode(200, 100) + +except OSError: + graphics.text("Unable to find PNG file! Copy 'pencil_256x256.png' to your Inky Frame using Thonny :)", 10, 70, WIDTH, 3) + +# Start the screen update +graphics.update() diff --git a/examples/image_gallery/600x448/jwst1.jpg b/examples/image_gallery/600x448/jwst1.jpg new file mode 100644 index 0000000..6bfbb68 Binary files /dev/null and b/examples/image_gallery/600x448/jwst1.jpg differ diff --git a/examples/image_gallery/600x448/jwst2.jpg b/examples/image_gallery/600x448/jwst2.jpg new file mode 100644 index 0000000..5d0868d Binary files /dev/null and b/examples/image_gallery/600x448/jwst2.jpg differ diff --git a/examples/image_gallery/600x448/jwst3.jpg b/examples/image_gallery/600x448/jwst3.jpg new file mode 100644 index 0000000..d46e764 Binary files /dev/null and b/examples/image_gallery/600x448/jwst3.jpg differ diff --git a/examples/image_gallery/600x448/jwst4.jpg b/examples/image_gallery/600x448/jwst4.jpg new file mode 100644 index 0000000..0b9c751 Binary files /dev/null and b/examples/image_gallery/600x448/jwst4.jpg differ diff --git a/examples/image_gallery/600x448/jwst5.jpg b/examples/image_gallery/600x448/jwst5.jpg new file mode 100644 index 0000000..05d86f1 Binary files /dev/null and b/examples/image_gallery/600x448/jwst5.jpg differ diff --git a/examples/image_gallery/640x400/jwst1.jpg b/examples/image_gallery/640x400/jwst1.jpg new file mode 100644 index 0000000..202e747 Binary files /dev/null and b/examples/image_gallery/640x400/jwst1.jpg differ diff --git a/examples/image_gallery/640x400/jwst2.jpg b/examples/image_gallery/640x400/jwst2.jpg new file mode 100644 index 0000000..103341e Binary files /dev/null and b/examples/image_gallery/640x400/jwst2.jpg differ diff --git a/examples/image_gallery/640x400/jwst3.jpg b/examples/image_gallery/640x400/jwst3.jpg new file mode 100644 index 0000000..4b66d65 Binary files /dev/null and b/examples/image_gallery/640x400/jwst3.jpg differ diff --git a/examples/image_gallery/640x400/jwst4.jpg b/examples/image_gallery/640x400/jwst4.jpg new file mode 100644 index 0000000..f1a541e Binary files /dev/null and b/examples/image_gallery/640x400/jwst4.jpg differ diff --git a/examples/image_gallery/640x400/jwst5.jpg b/examples/image_gallery/640x400/jwst5.jpg new file mode 100644 index 0000000..21a70f3 Binary files /dev/null and b/examples/image_gallery/640x400/jwst5.jpg differ diff --git a/examples/image_gallery/800x480/jwst1.jpg b/examples/image_gallery/800x480/jwst1.jpg new file mode 100644 index 0000000..c70f67a Binary files /dev/null and b/examples/image_gallery/800x480/jwst1.jpg differ diff --git a/examples/image_gallery/800x480/jwst2.jpg b/examples/image_gallery/800x480/jwst2.jpg new file mode 100644 index 0000000..375775f Binary files /dev/null and b/examples/image_gallery/800x480/jwst2.jpg differ diff --git a/examples/image_gallery/800x480/jwst3.jpg b/examples/image_gallery/800x480/jwst3.jpg new file mode 100644 index 0000000..38876f1 Binary files /dev/null and b/examples/image_gallery/800x480/jwst3.jpg differ diff --git a/examples/image_gallery/800x480/jwst4.jpg b/examples/image_gallery/800x480/jwst4.jpg new file mode 100644 index 0000000..21f0ee7 Binary files /dev/null and b/examples/image_gallery/800x480/jwst4.jpg differ diff --git a/examples/image_gallery/800x480/jwst5.jpg b/examples/image_gallery/800x480/jwst5.jpg new file mode 100644 index 0000000..6d243d8 Binary files /dev/null and b/examples/image_gallery/800x480/jwst5.jpg differ diff --git a/examples/image_gallery/README.md b/examples/image_gallery/README.md new file mode 100644 index 0000000..ae36d84 --- /dev/null +++ b/examples/image_gallery/README.md @@ -0,0 +1,45 @@ +# Image Gallery + +- [Image transfer instructions](#image-transfer-instructions) + - [image\_gallery.py](#image_gallerypy) + - [image\_gallery\_sd.py / image\_gallery\_sd\_random.py](#image_gallery_sdpy--image_gallery_sd_randompy) +- [Image Credits](#image-credits) + +Some example programs to display images on your Inky Frame, plus sample images in different sizes. + +Use: +640x400 for Inky Frame 4.0" +600x448 for Inky Frame 5.7" +800x480 for Inky Frame 7.3" + +If you want to use your own images, they will need to be the correct dimensions for your screen and saved *without progressive encoding*. + +## Image transfer instructions + +In all cases the images will need to be copied to the root of your Pico W or SD card. + +### image_gallery.py + +Copy the images to your Pico W using Thonny. + +### image_gallery_sd.py / image_gallery_sd_random.py + +Pop an SD card into your computer to copy the images across. + +Alternatively, you can transfer them using Thonny, but you will have to mount the SD card using the REPL first: + +```python +import os +import sdcard +from machine import Pin, SPI +sd_spi = SPI(0, sck=Pin(18, Pin.OUT), mosi=Pin(19, Pin.OUT), miso=Pin(16, Pin.OUT)) +sd = sdcard.SDCard(sd_spi, Pin(22)) +os.mount(sd, "/sd") +``` + +## Image Credits + +Sample images from the Webb Space Telescope (credit: NASA, ESA, CSA, and STScI). +Find more gorgeous images and info @ https://webbtelescope.org/ + +... and Raspberry Pi <3 diff --git a/examples/image_gallery/image_gallery.py b/examples/image_gallery/image_gallery.py new file mode 100644 index 0000000..60b31e4 --- /dev/null +++ b/examples/image_gallery/image_gallery.py @@ -0,0 +1,75 @@ +""" +An offline image gallery that switches between five jpg images. + +Copy images into the root of your Pico's flash using Thonny. + +If you want to use your own images they must be the screen dimensions +(or smaller) and saved as *non-progressive* jpgs. + +Make sure to uncomment the correct size for your display! +""" + +# from picographics import PicoGraphics, DISPLAY_INKY_FRAME as DISPLAY # 5.7" +# from picographics import PicoGraphics, DISPLAY_INKY_FRAME_4 as DISPLAY # 4.0" +from picographics import PicoGraphics, DISPLAY_INKY_FRAME_7 as DISPLAY # 7.3" +import inky_frame +import jpegdec + +# you can change your file names here +IMAGE_A = "jwst1.jpg" +IMAGE_B = "jwst2.jpg" +IMAGE_C = "jwst3.jpg" +IMAGE_D = "jwst4.jpg" +IMAGE_E = "jwst5.jpg" + +# set up the display +graphics = PicoGraphics(DISPLAY) + +# Create a new JPEG decoder for our PicoGraphics +j = jpegdec.JPEG(graphics) + + +def display_image(filename): + + # Open the JPEG file + j.open_file(filename) + + # Decode the JPEG + j.decode(0, 0, jpegdec.JPEG_SCALE_FULL) + + # Display the result + graphics.update() + + +print('Press a button to display an image!') + +while True: + inky_frame.button_a.led_off() + inky_frame.button_b.led_off() + inky_frame.button_c.led_off() + inky_frame.button_d.led_off() + inky_frame.button_e.led_off() + + if inky_frame.button_a.read(): + print('Refreshing image A.') + inky_frame.button_a.led_on() + display_image(IMAGE_A) + elif inky_frame.button_b.read(): + print('Refreshing image B.') + inky_frame.button_b.led_on() + display_image(IMAGE_B) + elif inky_frame.button_c.read(): + print('Refreshing image C.') + inky_frame.button_c.led_on() + display_image(IMAGE_C) + elif inky_frame.button_d.read(): + print('Refreshing image D.') + inky_frame.button_d.led_on() + display_image(IMAGE_D) + elif inky_frame.button_e.read(): + print('Refreshing image E.') + inky_frame.button_e.led_on() + display_image(IMAGE_E) + + # Go to sleep if on battery power + inky_frame.turn_off() diff --git a/examples/image_gallery/image_gallery_sd.py b/examples/image_gallery/image_gallery_sd.py new file mode 100644 index 0000000..659ca29 --- /dev/null +++ b/examples/image_gallery/image_gallery_sd.py @@ -0,0 +1,84 @@ +""" +An offline image gallery that switches between five jpg images on your SD card. + +Copy images to the root of your SD card by plugging it into a computer. + +If you want to use your own images they must be the screen dimensions +(or smaller) and saved as *non-progressive* jpgs. + +Make sure to uncomment the correct size for your display! +""" + +# from picographics import PicoGraphics, DISPLAY_INKY_FRAME as DISPLAY # 5.7" +# from picographics import PicoGraphics, DISPLAY_INKY_FRAME_4 as DISPLAY # 4.0" +from picographics import PicoGraphics, DISPLAY_INKY_FRAME_7 as DISPLAY # 7.3" +from machine import Pin, SPI +import jpegdec +import sdcard +import os +import inky_frame + +# you can change your file names here +IMAGE_A = "sd/jwst1.jpg" +IMAGE_B = "sd/jwst2.jpg" +IMAGE_C = "sd/jwst3.jpg" +IMAGE_D = "sd/jwst4.jpg" +IMAGE_E = "sd/jwst5.jpg" + +# set up the display +graphics = PicoGraphics(DISPLAY) + +# set up the SD card +sd_spi = SPI(0, sck=Pin(18, Pin.OUT), mosi=Pin(19, Pin.OUT), miso=Pin(16, Pin.OUT)) +sd = sdcard.SDCard(sd_spi, Pin(22)) +os.mount(sd, "/sd") + +# Create a new JPEG decoder for our PicoGraphics +j = jpegdec.JPEG(graphics) + + +def display_image(filename): + + # Open the JPEG file + j.open_file(filename) + + # Decode the JPEG + j.decode(0, 0, jpegdec.JPEG_SCALE_FULL) + + # Display the result + graphics.update() + + +# setup +print('Press a button to display an image!') + +while True: + inky_frame.button_a.led_off() + inky_frame.button_b.led_off() + inky_frame.button_c.led_off() + inky_frame.button_d.led_off() + inky_frame.button_e.led_off() + + if inky_frame.button_a.read(): + print('Refreshing image A.') + inky_frame.button_a.led_on() + display_image(IMAGE_A) + elif inky_frame.button_b.read(): + print('Refreshing image B.') + inky_frame.button_b.led_on() + display_image(IMAGE_B) + elif inky_frame.button_c.read(): + print('Refreshing image C.') + inky_frame.button_c.led_on() + display_image(IMAGE_C) + elif inky_frame.button_d.read(): + print('Refreshing image D.') + inky_frame.button_d.led_on() + display_image(IMAGE_D) + elif inky_frame.button_e.read(): + print('Refreshing image E.') + inky_frame.button_e.led_on() + display_image(IMAGE_E) + + # Go to sleep if on battery power + inky_frame.turn_off() diff --git a/examples/image_gallery/image_gallery_sd_random.py b/examples/image_gallery/image_gallery_sd_random.py new file mode 100644 index 0000000..965610a --- /dev/null +++ b/examples/image_gallery/image_gallery_sd_random.py @@ -0,0 +1,67 @@ +""" +An offline image gallery that displays a random image from your SD card +and updates on a timer. + +Copy images to the root of your SD card by plugging it into a computer. + +If you want to use your own images they must be the screen dimensions +(or smaller) and saved as *non-progressive* jpgs. + +Make sure to uncomment the correct size for your display! +""" + +# from picographics import PicoGraphics, DISPLAY_INKY_FRAME as DISPLAY # 5.7" +# from picographics import PicoGraphics, DISPLAY_INKY_FRAME_4 as DISPLAY # 4.0" +from picographics import PicoGraphics, DISPLAY_INKY_FRAME_7 as DISPLAY # 7.3" +from machine import Pin, SPI +import jpegdec +import sdcard +import os +import inky_frame +import random + +# how often to change image (in minutes) +UPDATE_INTERVAL = 60 + +# set up the display +graphics = PicoGraphics(DISPLAY) + +# set up the SD card +sd_spi = SPI(0, sck=Pin(18, Pin.OUT), mosi=Pin(19, Pin.OUT), miso=Pin(16, Pin.OUT)) +sd = sdcard.SDCard(sd_spi, Pin(22)) +os.mount(sd, "/sd") + +# Create a new JPEG decoder for our PicoGraphics +j = jpegdec.JPEG(graphics) + + +def display_image(filename): + + # Open the JPEG file + j.open_file(filename) + + # Decode the JPEG + j.decode(0, 0, jpegdec.JPEG_SCALE_FULL) + + # Display the result + graphics.update() + + +inky_frame.led_busy.on() + +# Get a list of files that are in the directory +files = os.listdir("/sd") +# remove files from the list that aren't .jpgs or .jpegs +files = [f for f in files if f.endswith(".jpg") or f.endswith(".jpeg")] + +while True: + # pick a random file + file = files[random.randrange(len(files))] + + # Open the file + print(f"Displaying /sd/{file}") + display_image("/sd/" + file) + + # Sleep or wait for a bit + print(f"Sleeping for {UPDATE_INTERVAL} minutes") + inky_frame.sleep_for(UPDATE_INTERVAL) diff --git a/examples/inky_frame_daily_activity.py b/examples/inky_frame_daily_activity.py new file mode 100644 index 0000000..a21e6b3 --- /dev/null +++ b/examples/inky_frame_daily_activity.py @@ -0,0 +1,143 @@ +import gc +import WIFI_CONFIG +from network_manager import NetworkManager +import uasyncio +import ujson +from urllib import urequest +# from picographics import PicoGraphics, DISPLAY_INKY_FRAME as DISPLAY # 5.7" +# from picographics import PicoGraphics, DISPLAY_INKY_FRAME_4 as DISPLAY # 4.0" +from picographics import PicoGraphics, DISPLAY_INKY_FRAME_7 as DISPLAY # 7.3" +from machine import Pin +from pimoroni_i2c import PimoroniI2C +from pcf85063a import PCF85063A +import time + + +I2C_SDA_PIN = 4 +I2C_SCL_PIN = 5 +HOLD_VSYS_EN_PIN = 2 + +# set up and enable vsys hold so we don't go to sleep +hold_vsys_en_pin = Pin(HOLD_VSYS_EN_PIN, Pin.OUT) +hold_vsys_en_pin.value(True) + +# intialise the pcf85063a real time clock chip +i2c = PimoroniI2C(I2C_SDA_PIN, I2C_SCL_PIN, 100000) +rtc = PCF85063A(i2c) + +# Length of time between updates in Seconds. +# Frequent updates will reduce battery life! +UPDATE_INTERVAL = 60 * 1 + +# API URL +URL = "https://www.boredapi.com/api/activity" + + +def status_handler(mode, status, ip): + print(mode, status, ip) + + +network_manager = NetworkManager(WIFI_CONFIG.COUNTRY, status_handler=status_handler) + +gc.collect() +graphics = PicoGraphics(DISPLAY) +WIDTH, HEIGHT = graphics.get_bounds() +gc.collect() + + +def display_quote(text, ox, oy, scale, wordwrap): + # Processing text is memory intensive + # so we'll do it one char at a time as we draw to the screen + line_height = 8 * scale + html = False + html_tag = "" + word = "" + space_width = graphics.measure_text(" ", scale=scale) + x = ox + y = oy + for char in text: + if char in "[]": + continue + if char == "<": + html = True + html_tag = "" + continue + if char == ">": + html = False + continue + if html: + if char in "/ ": + continue + html_tag += char + continue + if char in (" ", "\n") or html_tag == "br": + w = graphics.measure_text(word, scale=scale) + if x + w > wordwrap or char == "\n" or html_tag == "br": + x = ox + y += line_height + + graphics.text(word, x, y, scale=scale) + word = "" + html_tag = "" + x += w + space_width + continue + + word += char + + # Last word + w = graphics.measure_text(word, scale=scale) + if x + w > wordwrap: + x = ox + y += line_height + + graphics.text(word, x, y, scale=scale) + + +rtc.enable_timer_interrupt(True) + +while True: + # Connect to WiFi + uasyncio.get_event_loop().run_until_complete(network_manager.client(WIFI_CONFIG.SSID, WIFI_CONFIG.PSK)) + + # Clear the screen + graphics.set_pen(1) + graphics.clear() + graphics.set_pen(0) + + # Grab the data + socket = urequest.urlopen(URL) + j = ujson.load(socket) + socket.close() + + text = [j['activity'], j['type'], j['participants']] + + # Page lines! + graphics.set_pen(3) + graphics.line(0, 65, WIDTH, 65) + for i in range(2, 13): + graphics.line(0, i * 35, WIDTH, i * 35) + + # Page margin + graphics.set_pen(4) + graphics.line(50, 0, 50, HEIGHT) + graphics.set_pen(0) + + # Main text + graphics.set_font("cursive") + graphics.set_pen(4) + graphics.set_font("cursive") + graphics.text("Activity Idea", 55, 30, WIDTH - 20, 2) + graphics.set_pen(0) + graphics.set_font("bitmap8") + display_quote(text[0], 55, 170, 5, WIDTH - 20) + + graphics.set_pen(2) + graphics.text("Activity Type: " + text[1], 55, HEIGHT - 45, WIDTH - 20, 2) + graphics.text("Participants: " + str(text[2]), 400, HEIGHT - 45, WIDTH - 20, 2) + + graphics.update() + + # Time to have a little nap until the next update + rtc.set_timer(UPDATE_INTERVAL) + hold_vsys_en_pin.init(Pin.IN) + time.sleep(UPDATE_INTERVAL) diff --git a/examples/inky_frame_dithering.py b/examples/inky_frame_dithering.py new file mode 100644 index 0000000..15c947e --- /dev/null +++ b/examples/inky_frame_dithering.py @@ -0,0 +1,68 @@ +# from picographics import PicoGraphics, DISPLAY_INKY_FRAME as DISPLAY # 5.7" +# from picographics import PicoGraphics, DISPLAY_INKY_FRAME_4 as DISPLAY # 4.0" +from picographics import PicoGraphics, DISPLAY_INKY_FRAME_7 as DISPLAY # 7.3" + +graphics = PicoGraphics(DISPLAY) + +WIDTH, HEIGHT = graphics.get_bounds() + +graphics.set_pen(1) +graphics.clear() + +w = int(WIDTH / 8) + +# Solid Colours + +for p in range(8): + graphics.set_pen(p) + graphics.rectangle(w * p, 0, w, 50) + +# "Greydient" + +for x in range(WIDTH): + g = int(x / float(WIDTH) * 255) + graphics.set_pen(graphics.create_pen(g, g, g)) + for y in range(30): + graphics.pixel(x, 60 + y) + +# Rainbow Gradient + +for x in range(WIDTH): + h = x / float(WIDTH) + graphics.set_pen(graphics.create_pen_hsv(h, 1.0, 1.0)) + for y in range(100): + graphics.pixel(x, 100 + y) + +# Block Colours & Text + +graphics.set_pen(graphics.create_pen(128, 128, 0)) +graphics.rectangle(0, 210, 200, 100) +graphics.set_pen(graphics.create_pen(200, 200, 200)) +graphics.text("Hello", 10, 220) +graphics.text("Hello", 10, 240, scale=4.0) + +graphics.set_pen(graphics.create_pen(0, 128, 128)) +graphics.rectangle(200, 210, 200, 100) +graphics.set_pen(graphics.create_pen(200, 200, 200)) +graphics.text("Hello", 210, 220) +graphics.text("Hello", 210, 240, scale=4.0) + +graphics.set_pen(graphics.create_pen(128, 0, 128)) +graphics.rectangle(400, 210, 200, 100) +graphics.set_pen(graphics.create_pen(200, 200, 200)) +graphics.text("Hello", 410, 220) +graphics.text("Hello", 410, 240, scale=4.0) + +# Red, Green and Blue gradients + +for x in range(WIDTH): + g = int(x / float(WIDTH) * 255) + for y in range(20): + graphics.set_pen(graphics.create_pen(g, 0, 0)) + graphics.pixel(x, 320 + y) + graphics.set_pen(graphics.create_pen(0, g, 0)) + graphics.pixel(x, 350 + y) + graphics.set_pen(graphics.create_pen(0, 0, g)) + graphics.pixel(x, 380 + y) + +graphics.update() diff --git a/examples/inky_frame_news.py b/examples/inky_frame_news.py new file mode 100644 index 0000000..b59df09 --- /dev/null +++ b/examples/inky_frame_news.py @@ -0,0 +1,192 @@ +# from picographics import PicoGraphics, DISPLAY_INKY_FRAME as DISPLAY # 5.7" +# from picographics import PicoGraphics, DISPLAY_INKY_FRAME_4 as DISPLAY # 4.0" +from picographics import PicoGraphics, DISPLAY_INKY_FRAME_7 as DISPLAY # 7.3" +from network_manager import NetworkManager +import uasyncio +from urllib import urequest +import WIFI_CONFIG +import gc +import qrcode +from machine import Pin +from pimoroni_i2c import PimoroniI2C +from pcf85063a import PCF85063A +import time + +I2C_SDA_PIN = 4 +I2C_SCL_PIN = 5 +HOLD_VSYS_EN_PIN = 2 + +# set up and enable vsys hold so we don't go to sleep +hold_vsys_en_pin = Pin(HOLD_VSYS_EN_PIN, Pin.OUT) +hold_vsys_en_pin.value(True) + +# Uncomment one URL to use (Top Stories, World News and technology) +# URL = "http://feeds.bbci.co.uk/news/rss.xml" +# URL = "http://feeds.bbci.co.uk/news/world/rss.xml" +URL = "http://feeds.bbci.co.uk/news/technology/rss.xml" + +# Length of time between updates in Seconds. +# Frequent updates will reduce battery life! +UPDATE_INTERVAL = 60 * 1 + +graphics = PicoGraphics(DISPLAY) +WIDTH, HEIGHT = graphics.get_bounds() +graphics.set_font("bitmap8") +code = qrcode.QRCode() + +# intialise the pcf85063a real time clock chip +i2c = PimoroniI2C(I2C_SDA_PIN, I2C_SCL_PIN, 100000) +rtc = PCF85063A(i2c) + + +def status_handler(mode, status, ip): + print(mode, status, ip) + + +network_manager = NetworkManager(WIFI_CONFIG.COUNTRY, status_handler=status_handler) + + +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) + graphics.set_pen(1) + graphics.rectangle(ox, oy, size, size) + graphics.set_pen(0) + for x in range(size): + for y in range(size): + if code.get_module(x, y): + graphics.rectangle(ox + x * module_size, oy + y * module_size, module_size, module_size) + + +def get_rss(): + 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 + + +rtc.enable_timer_interrupt(True) + +while True: + # Connect to WiFi + uasyncio.get_event_loop().run_until_complete(network_manager.client(WIFI_CONFIG.SSID, WIFI_CONFIG.PSK)) + + # Gets Feed Data + feed = get_rss() + + # Clear the screen + graphics.set_pen(1) + graphics.clear() + graphics.set_pen(0) + + # Title + graphics.text("Headlines from BBC News:", 10, 10, 300, 2) + + # Draws 3 articles from the feed if they're available. + if feed: + graphics.set_pen(4) + graphics.text(feed[0]["title"], 10, 40, WIDTH - 150, 3 if graphics.measure_text(feed[0]["title"]) < 650 else 2) + graphics.text(feed[1]["title"], 130, 180, WIDTH - 140, 3 if graphics.measure_text(feed[1]["title"]) < 650 else 2) + graphics.text(feed[2]["title"], 10, 320, WIDTH - 150, 3 if graphics.measure_text(feed[2]["title"]) < 650 else 2) + + graphics.set_pen(3) + graphics.text(feed[0]["description"], 10, 110 if graphics.measure_text(feed[0]["title"]) < 650 else 90, WIDTH - 150, 2) + graphics.text(feed[1]["description"], 130, 250 if graphics.measure_text(feed[1]["title"]) < 650 else 230, WIDTH - 145, 2) + graphics.text(feed[2]["description"], 10, 395 if graphics.measure_text(feed[2]["title"]) < 650 else 375, WIDTH - 150, 2) + + code.set_text(feed[0]["guid"]) + draw_qr_code(WIDTH - 110, 40, 100, code) + code.set_text(feed[1]["guid"]) + draw_qr_code(10, 180, 100, code) + code.set_text(feed[2]["guid"]) + draw_qr_code(WIDTH - 110, 320, 100, code) + + else: + graphics.set_pen(4) + graphics.text("Error: Unable to get feed :(", 10, 40, WIDTH - 150, 4) + + graphics.update() + + # Time to have a little nap until the next update + rtc.set_timer(UPDATE_INTERVAL) + hold_vsys_en_pin.init(Pin.IN) + time.sleep(UPDATE_INTERVAL) diff --git a/examples/inky_frame_placekitten.py b/examples/inky_frame_placekitten.py new file mode 100644 index 0000000..8f07249 --- /dev/null +++ b/examples/inky_frame_placekitten.py @@ -0,0 +1,70 @@ +import gc +import uos +import random +import machine +import jpegdec +import uasyncio +import sdcard +import WIFI_CONFIG +from urllib import urequest +from network_manager import NetworkManager +# from picographics import PicoGraphics, DISPLAY_INKY_FRAME as DISPLAY # 5.7" +# from picographics import PicoGraphics, DISPLAY_INKY_FRAME_4 as DISPLAY # 4.0" +from picographics import PicoGraphics, DISPLAY_INKY_FRAME_7 as DISPLAY # 7.3" + +""" +random placekitten (from a very small set) + +You *must* insert an SD card into Inky Frame! +We need somewhere to save the jpg for display. +""" + +gc.collect() # We're really gonna need that RAM! + + +def status_handler(mode, status, ip): + print(mode, status, ip) + + +network_manager = NetworkManager(WIFI_CONFIG.COUNTRY, status_handler=status_handler) +uasyncio.get_event_loop().run_until_complete(network_manager.client(WIFI_CONFIG.SSID, WIFI_CONFIG.PSK)) + + +graphics = PicoGraphics(DISPLAY) + +WIDTH, HEIGHT = graphics.get_bounds() +FILENAME = "/sd/placekitten.jpg" +ENDPOINT = "http://placecats.com/{0}/{1}" + + +sd_spi = machine.SPI(0, sck=machine.Pin(18, machine.Pin.OUT), mosi=machine.Pin(19, machine.Pin.OUT), miso=machine.Pin(16, machine.Pin.OUT)) +sd = sdcard.SDCard(sd_spi, machine.Pin(22)) +uos.mount(sd, "/sd") +gc.collect() # Claw back some RAM! + +url = ENDPOINT.format(WIDTH, HEIGHT + random.randint(0, 10)) + +socket = urequest.urlopen(url) + +# Stream the image data from the socket onto disk in 1024 byte chunks +# the 600x448-ish jpeg will be roughly ~24k, we really don't have the RAM! +data = bytearray(1024) +with open(FILENAME, "wb") as f: + while True: + if socket.readinto(data) == 0: + break + f.write(data) +socket.close() +gc.collect() # We really are tight on RAM! + + +jpeg = jpegdec.JPEG(graphics) +gc.collect() # For good measure... + +graphics.set_pen(1) +graphics.clear() + +jpeg.open_file(FILENAME) +jpeg.decode() + +graphics.update() diff --git a/examples/inky_frame_quote_of_the_day.py b/examples/inky_frame_quote_of_the_day.py new file mode 100644 index 0000000..ed07297 --- /dev/null +++ b/examples/inky_frame_quote_of_the_day.py @@ -0,0 +1,171 @@ +import gc +import time +import ujson +import uasyncio +import WIFI_CONFIG +from urllib import urequest +from network_manager import NetworkManager +# from picographics import PicoGraphics, DISPLAY_INKY_FRAME as DISPLAY # 5.7" +# from picographics import PicoGraphics, DISPLAY_INKY_FRAME_4 as DISPLAY # 4.0" +from picographics import PicoGraphics, DISPLAY_INKY_FRAME_7 as DISPLAY # 7.3" + + +ENDPOINT = "https://en.wikiquote.org/w/api.php?format=json&action=expandtemplates&prop=wikitext&text={{{{Wikiquote:Quote%20of%20the%20day/{3}%20{2},%20{0}}}}}" +MONTHNAMES = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"] + + +last_date = "" + + +def parse_qotd(text): + print(text) + text = text.split("\n") + author = text[8].split("|")[2][5:-4] + text = text[6][2:] + gc.collect() + return text, author + + +def status_handler(mode, status, ip): + print(mode, status, ip) + + +network_manager = NetworkManager(WIFI_CONFIG.COUNTRY, status_handler=status_handler) + +gc.collect() +graphics = PicoGraphics(DISPLAY) +WIDTH, HEIGHT = graphics.get_bounds() +graphics.set_font("bitmap8") +gc.collect() + + +BADCHARS = { + "’": "'", + "—": "", + "…": "..." +} + + +def display_quote(text, ox, oy, scale, wordwrap): + # Processing text is memory intensive + # so we'll do it one char at a time as we draw to the screen + line_height = 9 * scale + html_tag = "" + word = "" + extra_text = "" + space_width = graphics.measure_text(" ", scale=scale) + x = ox + y = oy + i = -1 + while True: + if len(extra_text) == 0: + i += 1 + if i >= len(text): + break + + if len(extra_text) > 0: + char = extra_text[0] + extra_text = extra_text[1:] + else: + char = text[i] + + if char in BADCHARS: + word += BADCHARS[char] + continue + + # Unpick stuff like [[word]] and [[disambiguation|word]] + # and [[w:wikipedia_page|word]] + # test cases: July 8th 2022, July 12th 2022 + if char == "[": + if text[i:i + 2] == "[[": + link = False + if text[i + 2:i + 4] == "w:": + link = True + i += 2 + end = text[i:].index("]]") + if "|" in text[i + 2:i + end]: + parts = text[i + 2:i + end].split("|") + word = parts[1] + if not link: + extra_text = " (" + parts[0] + ")" + else: + word = text[i + 2:i + end] + i += end + 1 + continue + + if char == "&": + if text[i:i + 5] == "&": + word += "&" + i += 4 + continue + + if char == "<": + j = i + text[i:].index(">") + html_tag = text[i + 1:j].replace("/", "").strip() + i = j + continue + + if char in (" ", "\n") or html_tag == "br": + w = graphics.measure_text(word, scale=scale) + if x + w > wordwrap or char == "\n" or html_tag == "br": + x = ox + y += line_height + + graphics.text(word, x, y, scale=scale) + word = "" + html_tag = "" + x += w + space_width + continue + + word += char + + # Last word + w = graphics.measure_text(word, scale=scale) + if x + w > wordwrap: + x = ox + y += line_height + + graphics.text(word, x, y, scale=scale) + + +while True: + gc.collect() + + uasyncio.get_event_loop().run_until_complete(network_manager.client(WIFI_CONFIG.SSID, WIFI_CONFIG.PSK)) + + date = list(time.localtime())[:3] + date.append(MONTHNAMES[date[1] - 1]) + + if "{3} {2}, {0}".format(*date) == last_date: + time.sleep(60) + continue + + url = ENDPOINT.format(*date) + print("Requesting URL: {}".format(url)) + socket = urequest.urlopen(url) + j = ujson.load(socket) + socket.close() + + text = j['expandtemplates']['wikitext'] + del j + gc.collect() + + text, author = parse_qotd(text) + + print(text) + + graphics.set_pen(1) + graphics.clear() + graphics.set_pen(0) + graphics.text("QoTD - {2} {3} {0:04d}".format(*date), 10, 10, scale=3) + + display_quote(text, 10, 40, 2, wordwrap=WIDTH - 20) + + graphics.text(author, 10, HEIGHT - 20, scale=2) + + graphics.update() + gc.collect() + + last_date = "{3} {2}, {0}".format(*date) + + time.sleep(60) diff --git a/examples/inky_frame_random_joke.py b/examples/inky_frame_random_joke.py new file mode 100644 index 0000000..f67c778 --- /dev/null +++ b/examples/inky_frame_random_joke.py @@ -0,0 +1,90 @@ +import gc +import uos +import random +import machine +import jpegdec +import WIFI_CONFIG +import uasyncio +from network_manager import NetworkManager +# from picographics import PicoGraphics, DISPLAY_INKY_FRAME as DISPLAY # 5.7" +# from picographics import PicoGraphics, DISPLAY_INKY_FRAME_4 as DISPLAY # 4.0" +from picographics import PicoGraphics, DISPLAY_INKY_FRAME_7 as DISPLAY # 7.3" +from urllib import urequest + + +gc.collect() # We're really gonna need that RAM! + + +def status_handler(mode, status, ip): + print(mode, status, ip) + + +network_manager = NetworkManager(WIFI_CONFIG.COUNTRY, status_handler=status_handler) +uasyncio.get_event_loop().run_until_complete(network_manager.client(WIFI_CONFIG.SSID, WIFI_CONFIG.PSK)) + + +graphics = PicoGraphics(DISPLAY) + +WIDTH, HEIGHT = graphics.get_bounds() +FILENAME = "/sd/random-joke.jpg" + +JOKE_IDS = "https://pimoroni.github.io/feed2image/jokeapi-ids.txt" +JOKE_IMG = "https://pimoroni.github.io/feed2image/jokeapi-{}-{}x{}.jpg" + +import sdcard # noqa: E402 - putting this at the top causes an MBEDTLS OOM error!? +sd_spi = machine.SPI(0, sck=machine.Pin(18, machine.Pin.OUT), mosi=machine.Pin(19, machine.Pin.OUT), miso=machine.Pin(16, machine.Pin.OUT)) +sd = sdcard.SDCard(sd_spi, machine.Pin(22)) +uos.mount(sd, "/sd") +gc.collect() # Claw back some RAM! + +# We don't have the RAM to store the list of Joke IDs in memory. +# the first line of `jokeapi-ids.txt` is a COUNT of IDs. +# Grab it, then pick a random line between 0 and COUNT. +# Seek to that line and ...y'know... there's our totally random joke ID + +socket = urequest.urlopen(JOKE_IDS) + +# Get the first line, which is a count of the joke IDs +number_of_lines = int(socket.readline().decode("ascii")) +print("Total jokes {}".format(number_of_lines)) + +# Pick a random joke (by its line number) +line = random.randint(0, number_of_lines) +print("Getting ID from line {}".format(line)) + +for x in range(line): # Throw away lines to get where we need + socket.readline() + +# Read our chosen joke ID! +random_joke_id = int(socket.readline().decode("ascii")) +socket.close() + +print("Random joke ID: {}".format(random_joke_id)) + +url = JOKE_IMG.format(random_joke_id, WIDTH, HEIGHT) + +socket = urequest.urlopen(url) + +# Stream the image data from the socket onto disk in 1024 byte chunks +# the 600x448-ish jpeg will be roughly ~24k, we really don't have the RAM! +data = bytearray(1024) +with open(FILENAME, "wb") as f: + while True: + if socket.readinto(data) == 0: + break + f.write(data) +socket.close() +del data +gc.collect() # We really are tight on RAM! + + +jpeg = jpegdec.JPEG(graphics) +gc.collect() # For good measure... + +graphics.set_pen(1) +graphics.clear() + +jpeg.open_file(FILENAME) +jpeg.decode() + +graphics.update() diff --git a/examples/inky_frame_rtc_demo.py b/examples/inky_frame_rtc_demo.py new file mode 100644 index 0000000..004d0b7 --- /dev/null +++ b/examples/inky_frame_rtc_demo.py @@ -0,0 +1,88 @@ +import time +import uasyncio +import WIFI_CONFIG +import inky_frame +from network_manager import NetworkManager +# from picographics import PicoGraphics, DISPLAY_INKY_FRAME as DISPLAY # 5.7" +# from picographics import PicoGraphics, DISPLAY_INKY_FRAME_4 as DISPLAY # 4.0" +from picographics import PicoGraphics, DISPLAY_INKY_FRAME_7 as DISPLAY # 7.3" + +# Set tz_offset to be the number of hours off of UTC for your local zone. +# Examples: tz_offset = -7 # Pacific time (PST) +# tz_offset = 1 # CEST (Paris) +tz_offset = 0 +tz_seconds = tz_offset * 3600 + +# Sync the Inky (always on) RTC to the Pico W so that "time.localtime()" works. +inky_frame.pcf_to_pico_rtc() + +# Avoid running code unless we've been triggered by an event +# Keeps this example from locking up Thonny when we want to tweak the code +if inky_frame.woken_by_rtc() or inky_frame.woken_by_button(): + graphics = PicoGraphics(DISPLAY) + WIDTH, HEIGHT = graphics.get_bounds() + + graphics.set_pen(1) + graphics.clear() + + # Look, just because this is an RTC demo, + # doesn't mean we can't make it rainbow. + for x in range(WIDTH): + h = x / WIDTH + p = graphics.create_pen_hsv(h, 1.0, 1.0) + graphics.set_pen(p) + graphics.line(x, 0, x, HEIGHT) + + graphics.set_pen(0) + graphics.rectangle(0, 0, WIDTH, 14) + graphics.set_pen(1) + graphics.text("Inky Frame", 1, 0) + graphics.set_pen(0) + + def status_handler(mode, status, ip): + print(mode, status, ip) + + year, month, day, hour, minute, second, dow, _ = time.localtime(time.time() + tz_seconds) + + # Connect to the network and get the time if it's not set + if year < 2023: + connected = False + network_manager = NetworkManager(WIFI_CONFIG.COUNTRY, status_handler=status_handler, client_timeout=60) + + t_start = time.time() + try: + uasyncio.get_event_loop().run_until_complete(network_manager.client(WIFI_CONFIG.SSID, WIFI_CONFIG.PSK)) + connected = True + except RuntimeError: + pass + t_end = time.time() + + if connected: + inky_frame.set_time() + + graphics.text("Setting time from network...", 0, 40) + graphics.text(f"Connection took: {t_end-t_start}s", 0, 60) + else: + graphics.text("Failed to connect!", 0, 40) + + # Display the date and time + year, month, day, hour, minute, second, dow, _ = time.localtime(time.time() + tz_seconds) + + date_time = f"{year:04}/{month:02}/{day:02} {hour:02}:{minute:02}:{second:02}" + + graphics.set_font("bitmap8") + + text_scale = 8 if WIDTH == 800 else 6 + text_height = 8 * text_scale + + offset_left = (WIDTH - graphics.measure_text(date_time, scale=text_scale)) // 2 + offset_top = (HEIGHT - text_height) // 2 + + graphics.set_pen(graphics.create_pen(50, 50, 50)) + graphics.text(date_time, offset_left + 2, offset_top + 2, scale=text_scale) + graphics.set_pen(1) + graphics.text(date_time, offset_left, offset_top, scale=text_scale) + + graphics.update() + + inky_frame.sleep_for(2) diff --git a/examples/inky_frame_xkcd_daily.py b/examples/inky_frame_xkcd_daily.py new file mode 100644 index 0000000..69e0909 --- /dev/null +++ b/examples/inky_frame_xkcd_daily.py @@ -0,0 +1,78 @@ +import gc +import uos +import machine +import jpegdec +import uasyncio +import sdcard +import WIFI_CONFIG +from urllib import urequest +from network_manager import NetworkManager +# from picographics import PicoGraphics, DISPLAY_INKY_FRAME as DISPLAY # 5.7" +# from picographics import PicoGraphics, DISPLAY_INKY_FRAME_4 as DISPLAY # 4.0" +from picographics import PicoGraphics, DISPLAY_INKY_FRAME_7 as DISPLAY # 7.3" + +""" +xkcd daily + +You *must* insert an SD card into Inky Frame! +We need somewhere to save the jpg for display. + +Fetches a pre-processed XKCD daily image from: +https://pimoroni.github.io/feed2image/xkcd-daily.jpg + +See https://xkcd.com/ for more webcomics! +""" + +gc.collect() # We're really gonna need that RAM! + + +def status_handler(mode, status, ip): + print(mode, status, ip) + + +network_manager = NetworkManager("GB", status_handler=status_handler) +uasyncio.get_event_loop().run_until_complete(network_manager.client(WIFI_CONFIG.SSID, WIFI_CONFIG.PSK)) + + +graphics = PicoGraphics(DISPLAY) + +WIDTH, HEIGHT = graphics.get_bounds() +FILENAME = "/sd/xkcd-daily.jpg" +ENDPOINT = "https://pimoroni.github.io/feed2image/xkcd-daily.jpg" + + +sd_spi = machine.SPI(0, sck=machine.Pin(18, machine.Pin.OUT), mosi=machine.Pin(19, machine.Pin.OUT), miso=machine.Pin(16, machine.Pin.OUT)) +sd = sdcard.SDCard(sd_spi, machine.Pin(22)) +uos.mount(sd, "/sd") +gc.collect() # Claw back some RAM! + + +url = ENDPOINT + +if (WIDTH, HEIGHT) != (600, 448): + url = url.replace("xkcd-", f"xkcd-{WIDTH}x{HEIGHT}-") + +socket = urequest.urlopen(url) + +# Stream the image data from the socket onto disk in 1024 byte chunks +# the 600x448-ish jpeg will be roughly ~24k, we really don't have the RAM! +data = bytearray(1024) +with open(FILENAME, "wb") as f: + while True: + if socket.readinto(data) == 0: + break + f.write(data) +socket.close() +gc.collect() # We really are tight on RAM! + + +jpeg = jpegdec.JPEG(graphics) +gc.collect() # For good measure... + +graphics.set_pen(1) +graphics.clear() + +jpeg.open_file(FILENAME) +jpeg.decode() + +graphics.update() diff --git a/examples/inkylauncher/daily_activity.py b/examples/inkylauncher/daily_activity.py new file mode 100644 index 0000000..265c4d4 --- /dev/null +++ b/examples/inkylauncher/daily_activity.py @@ -0,0 +1,127 @@ +import gc +import ujson +from urllib import urequest + +# Length of time between updates in Seconds. +# Frequent updates will reduce battery life! +UPDATE_INTERVAL = 240 + +# API URL +URL = "https://www.boredapi.com/api/activity" + +graphics = None +text = None + +gc.collect() + + +def display_quote(text, ox, oy, scale, wordwrap): + # Processing text is memory intensive + # so we'll do it one char at a time as we draw to the screen + line_height = 8 * scale + html = False + html_tag = "" + word = "" + space_width = graphics.measure_text(" ", scale=scale) + x = ox + y = oy + for char in text: + if char in "[]": + continue + if char == "<": + html = True + html_tag = "" + continue + if char == ">": + html = False + continue + if html: + if char in "/ ": + continue + html_tag += char + continue + if char in (" ", "\n") or html_tag == "br": + w = graphics.measure_text(word, scale=scale) + if x + w > wordwrap or char == "\n" or html_tag == "br": + x = ox + y += line_height + + graphics.text(word, x, y, scale=scale) + word = "" + html_tag = "" + x += w + space_width + continue + + word += char + + # Last word + w = graphics.measure_text(word, scale=scale) + if x + w > wordwrap: + x = ox + y += line_height + + graphics.text(word, x, y, scale=scale) + + +def update(): + global text + gc.collect() + + try: + # Grab the data + socket = urequest.urlopen(URL) + j = ujson.load(socket) + socket.close() + text = [j['activity'], j['type'], j['participants']] + gc.collect() + except OSError: + pass + + +def draw(): + global text + + WIDTH, HEIGHT = graphics.get_bounds() + + # Clear the screen + graphics.set_pen(1) + graphics.clear() + graphics.set_pen(0) + + # Page lines! + graphics.set_pen(3) + graphics.line(0, 65, WIDTH, 65) + for i in range(2, 13): + graphics.line(0, i * 35, WIDTH, i * 35) + + gc.collect() + + # Page margin + graphics.set_pen(4) + graphics.line(50, 0, 50, HEIGHT) + graphics.set_pen(0) + + # Main text + graphics.set_font("cursive") + graphics.set_pen(4) + graphics.set_font("cursive") + graphics.text("Activity Idea", 55, 30, WIDTH - 20, 2) + graphics.set_pen(0) + graphics.set_font("bitmap8") + + if text: + display_quote(text[0], 55, 170, 5, WIDTH - 20) + gc.collect() + graphics.set_pen(2) + graphics.text("Activity Type: " + text[1], 55, HEIGHT - 45, WIDTH - 20, 2) + graphics.text("Participants: " + str(text[2]), 400, HEIGHT - 45, WIDTH - 20, 2) + else: + graphics.set_pen(4) + graphics.rectangle(0, (HEIGHT // 2) - 20, WIDTH, 40) + graphics.set_pen(1) + graphics.text("Unable to get activity data!", 5, (HEIGHT // 2) - 15, WIDTH, 2) + graphics.text("Check your network settings in secrets.py", 5, (HEIGHT // 2) + 2, WIDTH, 2) + + graphics.update() + + gc.collect() diff --git a/examples/inkylauncher/inky_helper.py b/examples/inkylauncher/inky_helper.py new file mode 100644 index 0000000..2d680ae --- /dev/null +++ b/examples/inkylauncher/inky_helper.py @@ -0,0 +1,154 @@ +from pimoroni_i2c import PimoroniI2C +from pcf85063a import PCF85063A +import math +from machine import Pin, PWM, Timer +import time +import inky_frame +import json +import network +import os + +# Pin setup for VSYS_HOLD needed to sleep and wake. +HOLD_VSYS_EN_PIN = 2 +hold_vsys_en_pin = Pin(HOLD_VSYS_EN_PIN, Pin.OUT) + +# intialise the pcf85063a real time clock chip +I2C_SDA_PIN = 4 +I2C_SCL_PIN = 5 +i2c = PimoroniI2C(I2C_SDA_PIN, I2C_SCL_PIN, 100000) +rtc = PCF85063A(i2c) + +led_warn = Pin(6, Pin.OUT) + +# set up for the network LED +network_led_pwm = PWM(Pin(7)) +network_led_pwm.freq(1000) +network_led_pwm.duty_u16(0) + + +# set the brightness of the network led +def network_led(brightness): + brightness = max(0, min(100, brightness)) # clamp to range + # gamma correct the brightness (gamma 2.8) + value = int(pow(brightness / 100.0, 2.8) * 65535.0 + 0.5) + network_led_pwm.duty_u16(value) + + +network_led_timer = Timer(-1) +network_led_pulse_speed_hz = 1 + + +def network_led_callback(t): + # updates the network led brightness based on a sinusoid seeded by the current time + brightness = (math.sin(time.ticks_ms() * math.pi * 2 / (1000 / network_led_pulse_speed_hz)) * 40) + 60 + value = int(pow(brightness / 100.0, 2.8) * 65535.0 + 0.5) + network_led_pwm.duty_u16(value) + + +# set the network led into pulsing mode +def pulse_network_led(speed_hz=1): + global network_led_timer, network_led_pulse_speed_hz + network_led_pulse_speed_hz = speed_hz + network_led_timer.deinit() + network_led_timer.init(period=50, mode=Timer.PERIODIC, callback=network_led_callback) + + +# turn off the network led and disable any pulsing animation that's running +def stop_network_led(): + global network_led_timer + network_led_timer.deinit() + network_led_pwm.duty_u16(0) + + +def sleep(t): + # Time to have a little nap until the next update + rtc.clear_timer_flag() + rtc.set_timer(t, ttp=rtc.TIMER_TICK_1_OVER_60HZ) + rtc.enable_timer_interrupt(True) + + # Set the HOLD VSYS pin to an input + # this allows the device to go into sleep mode when on battery power. + hold_vsys_en_pin.init(Pin.IN) + + # Regular time.sleep for those powering from USB + time.sleep(60 * t) + + +# Turns off the button LEDs +def clear_button_leds(): + inky_frame.button_a.led_off() + inky_frame.button_b.led_off() + inky_frame.button_c.led_off() + inky_frame.button_d.led_off() + inky_frame.button_e.led_off() + + +def network_connect(SSID, PSK): + # Enable the Wireless + wlan = network.WLAN(network.STA_IF) + wlan.active(True) + + # Number of attempts to make before timeout + max_wait = 10 + + # Sets the Wireless LED pulsing and attempts to connect to your local network. + pulse_network_led() + wlan.config(pm=0xa11140) # Turn WiFi power saving off for some slow APs + wlan.connect(SSID, PSK) + + while max_wait > 0: + if wlan.status() < 0 or wlan.status() >= 3: + break + max_wait -= 1 + print('waiting for connection...') + time.sleep(1) + + stop_network_led() + network_led_pwm.duty_u16(30000) + + # Handle connection error. Switches the Warn LED on. + if wlan.status() != 3: + stop_network_led() + led_warn.on() + + +state = {"run": None} +app = None + + +def file_exists(filename): + try: + return (os.stat(filename)[0] & 0x4000) == 0 + except OSError: + return False + + +def clear_state(): + if file_exists("state.json"): + os.remove("state.json") + + +def save_state(data): + with open("/state.json", "w") as f: + f.write(json.dumps(data)) + f.flush() + + +def load_state(): + global state + data = json.loads(open("/state.json", "r").read()) + if type(data) is dict: + state = data + + +def update_state(running): + global state + state['run'] = running + save_state(state) + + +def launch_app(app_name): + global app + app = __import__(app_name) + print(app) + update_state(app_name) diff --git a/examples/inkylauncher/lib/logging.mpy b/examples/inkylauncher/lib/logging.mpy new file mode 100644 index 0000000..fc00426 Binary files /dev/null and b/examples/inkylauncher/lib/logging.mpy differ diff --git a/examples/inkylauncher/lib/sdcard.mpy b/examples/inkylauncher/lib/sdcard.mpy new file mode 100644 index 0000000..7f3fd6b Binary files /dev/null and b/examples/inkylauncher/lib/sdcard.mpy differ diff --git a/examples/inkylauncher/lib/tinyweb/server.mpy b/examples/inkylauncher/lib/tinyweb/server.mpy new file mode 100644 index 0000000..a841634 Binary files /dev/null and b/examples/inkylauncher/lib/tinyweb/server.mpy differ diff --git a/examples/inkylauncher/lib/tinyweb/server.py b/examples/inkylauncher/lib/tinyweb/server.py new file mode 100644 index 0000000..c61b8b0 --- /dev/null +++ b/examples/inkylauncher/lib/tinyweb/server.py @@ -0,0 +1,662 @@ +""" +Tiny Web - pretty simple and powerful web server for tiny platforms like ESP8266 / ESP32 +MIT license +(C) Konstantin Belyalov 2017-2018 +""" +import logging +import uasyncio as asyncio +import uasyncio.core +import ujson as json +import gc +import uos as os +import sys +import uerrno as errno +import usocket as socket + + +log = logging.getLogger('WEB') + +type_gen = type((lambda: (yield))()) # noqa: E275 + +# uasyncio v3 is shipped with MicroPython 1.13, and contains some subtle +# but breaking changes. See also https://github.com/peterhinch/micropython-async/blob/master/v3/README.md +IS_UASYNCIO_V3 = hasattr(asyncio, "__version__") and asyncio.__version__ >= (3,) + + +def urldecode_plus(s): + """Decode urlencoded string (including '+' char). + Returns decoded string + """ + s = s.replace('+', ' ') + arr = s.split('%') + res = arr[0] + for it in arr[1:]: + if len(it) >= 2: + res += chr(int(it[:2], 16)) + it[2:] + elif len(it) == 0: + res += '%' + else: + res += it + return res + + +def parse_query_string(s): + """Parse urlencoded string into dict. + Returns dict + """ + res = {} + pairs = s.split('&') + for p in pairs: + vals = [urldecode_plus(x) for x in p.split('=', 1)] + if len(vals) == 1: + res[vals[0]] = '' + else: + res[vals[0]] = vals[1] + return res + + +class HTTPException(Exception): + """HTTP protocol exceptions""" + + def __init__(self, code=400): + self.code = code + + +class request: + """HTTP Request class""" + + def __init__(self, _reader): + self.reader = _reader + self.headers = {} + self.method = b'' + self.path = b'' + self.query_string = b'' + + async def read_request_line(self): + """Read and parse first line (AKA HTTP Request Line). + Function is generator. + Request line is something like: + GET /something/script?param1=val1 HTTP/1.1 + """ + while True: + rl = await self.reader.readline() + # skip empty lines + if rl == b'\r\n' or rl == b'\n': + continue + break + rl_frags = rl.split() + if len(rl_frags) != 3: + raise HTTPException(400) + self.method = rl_frags[0] + url_frags = rl_frags[1].split(b'?', 1) + self.path = url_frags[0] + if len(url_frags) > 1: + self.query_string = url_frags[1] + + async def read_headers(self, save_headers=[]): + """Read and parse HTTP headers until \r\n\r\n: + Optional argument 'save_headers' controls which headers to save. + This is done mostly to deal with memory constrains. + Function is generator. + HTTP headers could be like: + Host: google.com + Content-Type: blah + \r\n + """ + while True: + gc.collect() + line = await self.reader.readline() + if line == b'\r\n': + break + frags = line.split(b':', 1) + if len(frags) != 2: + raise HTTPException(400) + if frags[0] in save_headers: + self.headers[frags[0]] = frags[1].strip() + + async def read_parse_form_data(self): + """Read HTTP form data (payload), if any. + Function is generator. + Returns: + - dict of key / value pairs + - None in case of no form data present + """ + # TODO: Probably there is better solution how to handle + # request body, at least for simple urlencoded forms - by processing + # chunks instead of accumulating payload. + gc.collect() + if b'Content-Length' not in self.headers: + return {} + # Parse payload depending on content type + if b'Content-Type' not in self.headers: + # Unknown content type, return unparsed, raw data + return {} + size = int(self.headers[b'Content-Length']) + if size > self.params['max_body_size'] or size < 0: + raise HTTPException(413) + data = await self.reader.readexactly(size) + # Use only string before ';', e.g: + # application/x-www-form-urlencoded; charset=UTF-8 + ct = self.headers[b'Content-Type'].split(b';', 1)[0] + try: + if ct == b'application/json': + return json.loads(data) + elif ct == b'application/x-www-form-urlencoded': + return parse_query_string(data.decode()) + except ValueError: + # Re-generate exception for malformed form data + raise HTTPException(400) + + +class response: + """HTTP Response class""" + + def __init__(self, _writer): + self.writer = _writer + self.send = _writer.awrite + self.code = 200 + self.version = '1.0' + self.headers = {} + + async def _send_headers(self): + """Compose and send: + - HTTP request line + - HTTP headers following by \r\n. + This function is generator. + P.S. + Because of usually we have only a few HTTP headers (2-5) it doesn't make sense + to send them separately - sometimes it could increase latency. + So combining headers together and send them as single "packet". + """ + # Request line + hdrs = 'HTTP/{} {} MSG\r\n'.format(self.version, self.code) + # Headers + for k, v in self.headers.items(): + hdrs += '{}: {}\r\n'.format(k, v) + hdrs += '\r\n' + # Collect garbage after small mallocs + gc.collect() + await self.send(hdrs) + + async def error(self, code, msg=None): + """Generate HTTP error response + This function is generator. + Arguments: + code - HTTP response code + Example: + # Not enough permissions. Send HTTP 403 - Forbidden + await resp.error(403) + """ + self.code = code + if msg: + self.add_header('Content-Length', len(msg)) + await self._send_headers() + if msg: + await self.send(msg) + + async def redirect(self, location, msg=None): + """Generate HTTP redirect response to 'location'. + Basically it will generate HTTP 302 with 'Location' header + Arguments: + location - URL to redirect to + Example: + # Redirect to /something + await resp.redirect('/something') + """ + self.code = 302 + self.add_header('Location', location) + if msg: + self.add_header('Content-Length', len(msg)) + await self._send_headers() + if msg: + await self.send(msg) + + def add_header(self, key, value): + """Add HTTP response header + Arguments: + key - header name + value - header value + Example: + resp.add_header('Content-Encoding', 'gzip') + """ + self.headers[key] = value + + def add_access_control_headers(self): + """Add Access Control related HTTP response headers. + This is required when working with RestApi (JSON requests) + """ + self.add_header('Access-Control-Allow-Origin', self.params['allowed_access_control_origins']) + self.add_header('Access-Control-Allow-Methods', self.params['allowed_access_control_methods']) + self.add_header('Access-Control-Allow-Headers', self.params['allowed_access_control_headers']) + + async def start_html(self): + """Start response with HTML content type. + This function is generator. + Example: + await resp.start_html() + await resp.send('