Merge pull request #1032 from pimoroni/inky-patches
Patches for Inky on SDK 2.1.0 + MicroPython edgepull/1033/head
|
@ -23,8 +23,12 @@ jobs:
|
|||
|
||||
env:
|
||||
PICO_SDK_PATH: $GITHUB_WORKSPACE/pico-sdk
|
||||
|
||||
|
||||
steps:
|
||||
- name: Compiler Cache Fixup
|
||||
run: |
|
||||
mkdir -p /home/runner/.ccache
|
||||
|
||||
- name: Compiler Cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
|
|
|
@ -42,8 +42,6 @@ jobs:
|
|||
board: RPI_PICO_W
|
||||
- name: stellar_unicorn
|
||||
board: RPI_PICO_W
|
||||
- name: inky_frame
|
||||
board: PICO_W_INKY
|
||||
|
||||
env:
|
||||
# MicroPython version will be contained in github.event.release.tag_name for releases
|
||||
|
|
|
@ -29,7 +29,7 @@ function micropython_clone {
|
|||
function micropython_build_mpy_cross {
|
||||
cd micropython/mpy-cross
|
||||
ccache --zero-stats || true
|
||||
CROSS_COMPILE="ccache " make
|
||||
CROSS_COMPILE="ccache " USER_C_MODULES= make
|
||||
ccache --show-stats || true
|
||||
cd ../../
|
||||
}
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
{
|
||||
"deploy": [
|
||||
"../deploy.md"
|
||||
],
|
||||
"docs": "",
|
||||
"features": [
|
||||
"Breadboard friendly",
|
||||
"Castellated Pads",
|
||||
"Micro USB",
|
||||
"WiFi",
|
||||
"Bluetooth"
|
||||
],
|
||||
"id": "rp2-pico-w",
|
||||
"images": [
|
||||
"rp2-pico-w.jpg"
|
||||
],
|
||||
"mcu": "rp2040",
|
||||
"product": "Pico W",
|
||||
"thumbnail": "",
|
||||
"url": "https://www.raspberrypi.com/products/raspberry-pi-pico/",
|
||||
"vendor": "Raspberry Pi"
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
include("$(PORT_DIR)/boards/manifest.py")
|
||||
|
||||
# https://github.com/micropython/micropython-lib/blob/master/micropython/bundles/bundle-networking/manifest.py
|
||||
require("bundle-networking")
|
||||
require("urllib.urequest")
|
||||
require("umqtt.simple")
|
||||
|
||||
# SD Card
|
||||
require("sdcard")
|
||||
|
||||
# Bluetooth
|
||||
require("aioble")
|
||||
|
||||
freeze("../../modules_py", "pimoroni.py")
|
||||
freeze("../../modules_py", "boot.py")
|
|
@ -1,17 +0,0 @@
|
|||
# cmake file for Pimoroni Inky with Raspberry Pi Pico W
|
||||
set(MICROPY_BOARD RPI_PICO_W)
|
||||
set(PICO_BOARD "pico_w")
|
||||
|
||||
# The C malloc is needed by cyw43-driver Bluetooth and Pimoroni Pico modules
|
||||
set(MICROPY_C_HEAP_SIZE 4096)
|
||||
|
||||
set(MICROPY_PY_LWIP ON)
|
||||
set(MICROPY_PY_NETWORK_CYW43 ON)
|
||||
|
||||
# Bluetooth
|
||||
set(MICROPY_PY_BLUETOOTH ON)
|
||||
set(MICROPY_BLUETOOTH_BTSTACK ON)
|
||||
set(MICROPY_PY_BLUETOOTH_CYW43 ON)
|
||||
|
||||
# Board specific version of the frozen manifest
|
||||
set(MICROPY_FROZEN_MANIFEST ${MICROPY_BOARD_DIR}/manifest.py)
|
|
@ -1,23 +0,0 @@
|
|||
// Board and hardware specific configuration
|
||||
#define MICROPY_HW_BOARD_NAME "Raspberry Pi Pico W"
|
||||
|
||||
// todo: We need something to check our binary size
|
||||
#define MICROPY_HW_FLASH_STORAGE_BYTES (848 * 1024)
|
||||
|
||||
// Enable networking.
|
||||
#define MICROPY_PY_NETWORK 1
|
||||
#define MICROPY_PY_NETWORK_HOSTNAME_DEFAULT "InkyW"
|
||||
|
||||
// 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)
|
|
@ -1,30 +0,0 @@
|
|||
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
|
|
|
@ -1,110 +1,3 @@
|
|||
# Pico Inky Frame MicroPython Examples <!-- omit in toc -->
|
||||
|
||||
- [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
|
||||
|
||||
|
||||
Moved to: [https://github.com/pimoroni/inky-frame/tree/main/examples](https://github.com/pimoroni/inky-frame/tree/main/examples)
|
||||
|
|
|
@ -1,86 +0,0 @@
|
|||
# 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()
|
|
@ -1,59 +0,0 @@
|
|||
# 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
|
|
@ -1,149 +0,0 @@
|
|||
"""
|
||||
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.
|
||||
"""
|
|
@ -1,36 +0,0 @@
|
|||
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()
|
Przed Szerokość: | Wysokość: | Rozmiar: 98 KiB |
Przed Szerokość: | Wysokość: | Rozmiar: 70 KiB |
Przed Szerokość: | Wysokość: | Rozmiar: 78 KiB |
Przed Szerokość: | Wysokość: | Rozmiar: 105 KiB |
Przed Szerokość: | Wysokość: | Rozmiar: 92 KiB |
Przed Szerokość: | Wysokość: | Rozmiar: 90 KiB |
Przed Szerokość: | Wysokość: | Rozmiar: 62 KiB |
Przed Szerokość: | Wysokość: | Rozmiar: 71 KiB |
Przed Szerokość: | Wysokość: | Rozmiar: 95 KiB |
Przed Szerokość: | Wysokość: | Rozmiar: 83 KiB |
Przed Szerokość: | Wysokość: | Rozmiar: 126 KiB |
Przed Szerokość: | Wysokość: | Rozmiar: 83 KiB |
Przed Szerokość: | Wysokość: | Rozmiar: 103 KiB |
Przed Szerokość: | Wysokość: | Rozmiar: 149 KiB |
Przed Szerokość: | Wysokość: | Rozmiar: 142 KiB |
|
@ -1,45 +0,0 @@
|
|||
# Image Gallery <!-- omit in toc -->
|
||||
|
||||
- [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
|
|
@ -1,75 +0,0 @@
|
|||
"""
|
||||
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()
|
|
@ -1,84 +0,0 @@
|
|||
"""
|
||||
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()
|
|
@ -1,67 +0,0 @@
|
|||
"""
|
||||
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)
|
|
@ -1,143 +0,0 @@
|
|||
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)
|
|
@ -1,68 +0,0 @@
|
|||
# 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()
|
|
@ -1,192 +0,0 @@
|
|||
# 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 <?xml vers...
|
||||
if next_char == b"?":
|
||||
discard_until(s, b">")
|
||||
continue
|
||||
|
||||
# Detect <![CDATA
|
||||
elif next_char == b"!":
|
||||
s.read(1) # Discard [
|
||||
discard_until(s, b"[") # Discard CDATA[
|
||||
text = read_until(s, b"]")
|
||||
discard_until(s, b">") # 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)
|
|
@ -1,70 +0,0 @@
|
|||
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()
|
|
@ -1,171 +0,0 @@
|
|||
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)
|
|
@ -1,90 +0,0 @@
|
|||
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()
|
|
@ -1,88 +0,0 @@
|
|||
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)
|
|
@ -1,78 +0,0 @@
|
|||
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()
|
|
@ -1,127 +0,0 @@
|
|||
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()
|
|
@ -1,154 +0,0 @@
|
|||
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)
|
|
@ -1,662 +0,0 @@
|
|||
"""
|
||||
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('<html><h1>Hello, world!</h1></html>')
|
||||
"""
|
||||
self.add_header('Content-Type', 'text/html')
|
||||
await self._send_headers()
|
||||
|
||||
async def send_file(self, filename, content_type=None, content_encoding=None, max_age=2592000, buf_size=128):
|
||||
"""Send local file as HTTP response.
|
||||
This function is generator.
|
||||
Arguments:
|
||||
filename - Name of file which exists in local filesystem
|
||||
Keyword arguments:
|
||||
content_type - Filetype. By default - None means auto-detect.
|
||||
max_age - Cache control. How long browser can keep this file on disk.
|
||||
By default - 30 days
|
||||
Set to 0 - to disable caching.
|
||||
Example 1: Default use case:
|
||||
await resp.send_file('images/cat.jpg')
|
||||
Example 2: Disable caching:
|
||||
await resp.send_file('static/index.html', max_age=0)
|
||||
Example 3: Override content type:
|
||||
await resp.send_file('static/file.bin', content_type='application/octet-stream')
|
||||
"""
|
||||
try:
|
||||
# Get file size
|
||||
stat = os.stat(filename)
|
||||
slen = str(stat[6])
|
||||
self.add_header('Content-Length', slen)
|
||||
# Find content type
|
||||
if content_type:
|
||||
self.add_header('Content-Type', content_type)
|
||||
# Add content-encoding, if any
|
||||
if content_encoding:
|
||||
self.add_header('Content-Encoding', content_encoding)
|
||||
# Since this is static content is totally make sense
|
||||
# to tell browser to cache it, however, you can always
|
||||
# override it by setting max_age to zero
|
||||
self.add_header('Cache-Control', 'max-age={}, public'.format(max_age))
|
||||
with open(filename) as f:
|
||||
await self._send_headers()
|
||||
gc.collect()
|
||||
buf = bytearray(min(stat[6], buf_size))
|
||||
while True:
|
||||
size = f.readinto(buf)
|
||||
if size == 0:
|
||||
break
|
||||
await self.send(buf, sz=size)
|
||||
except OSError as e:
|
||||
# special handling for ENOENT / EACCESS
|
||||
if e.args[0] in (errno.ENOENT, errno.EACCES):
|
||||
raise HTTPException(404)
|
||||
else:
|
||||
raise
|
||||
|
||||
|
||||
async def restful_resource_handler(req, resp, param=None):
|
||||
"""Handler for RESTful API endpoins"""
|
||||
# Gather data - query string, JSON in request body...
|
||||
data = await req.read_parse_form_data()
|
||||
# Add parameters from URI query string as well
|
||||
# This one is actually for simply development of RestAPI
|
||||
if req.query_string != b'':
|
||||
data.update(parse_query_string(req.query_string.decode()))
|
||||
# Call actual handler
|
||||
_handler, _kwargs = req.params['_callmap'][req.method]
|
||||
# Collect garbage before / after handler execution
|
||||
gc.collect()
|
||||
if param:
|
||||
res = _handler(data, param, **_kwargs)
|
||||
else:
|
||||
res = _handler(data, **_kwargs)
|
||||
gc.collect()
|
||||
# Handler result could be:
|
||||
# 1. generator - in case of large payload
|
||||
# 2. string - just string :)
|
||||
# 2. dict - meaning client what tinyweb to convert it to JSON
|
||||
# it can also return error code together with str / dict
|
||||
# res = {'blah': 'blah'}
|
||||
# res = {'blah': 'blah'}, 201
|
||||
if isinstance(res, type_gen):
|
||||
# Result is generator, use chunked response
|
||||
# NOTICE: HTTP 1.0 by itself does not support chunked responses, so, making workaround:
|
||||
# Response is HTTP/1.1 with Connection: close
|
||||
resp.version = '1.1'
|
||||
resp.add_header('Connection', 'close')
|
||||
resp.add_header('Content-Type', 'application/json')
|
||||
resp.add_header('Transfer-Encoding', 'chunked')
|
||||
resp.add_access_control_headers()
|
||||
await resp._send_headers()
|
||||
# Drain generator
|
||||
for chunk in res:
|
||||
chunk_len = len(chunk.encode('utf-8'))
|
||||
await resp.send('{:x}\r\n'.format(chunk_len))
|
||||
await resp.send(chunk)
|
||||
await resp.send('\r\n')
|
||||
gc.collect()
|
||||
await resp.send('0\r\n\r\n')
|
||||
else:
|
||||
if type(res) is tuple:
|
||||
resp.code = res[1]
|
||||
res = res[0]
|
||||
elif res is None:
|
||||
raise Exception('Result expected')
|
||||
# Send response
|
||||
if type(res) is dict:
|
||||
res_str = json.dumps(res)
|
||||
else:
|
||||
res_str = res
|
||||
resp.add_header('Content-Type', 'application/json')
|
||||
resp.add_header('Content-Length', str(len(res_str)))
|
||||
resp.add_access_control_headers()
|
||||
await resp._send_headers()
|
||||
await resp.send(res_str)
|
||||
|
||||
|
||||
class webserver:
|
||||
|
||||
def __init__(self, request_timeout=3, max_concurrency=3, backlog=16, debug=False):
|
||||
"""Tiny Web Server class.
|
||||
Keyword arguments:
|
||||
request_timeout - Time for client to send complete request
|
||||
after that connection will be closed.
|
||||
max_concurrency - How many connections can be processed concurrently.
|
||||
It is very important to limit this number because of
|
||||
memory constrain.
|
||||
Default value depends on platform
|
||||
backlog - Parameter to socket.listen() function. Defines size of
|
||||
pending to be accepted connections queue.
|
||||
Must be greater than max_concurrency
|
||||
debug - Whether send exception info (text + backtrace)
|
||||
to client together with HTTP 500 or not.
|
||||
"""
|
||||
self.loop = asyncio.get_event_loop()
|
||||
self.request_timeout = request_timeout
|
||||
self.max_concurrency = max_concurrency
|
||||
self.backlog = backlog
|
||||
self.debug = debug
|
||||
self.explicit_url_map = {}
|
||||
self.catch_all_handler = None
|
||||
self.parameterized_url_map = {}
|
||||
# Currently opened connections
|
||||
self.conns = {}
|
||||
# Statistics
|
||||
self.processed_connections = 0
|
||||
|
||||
def _find_url_handler(self, req):
|
||||
"""Helper to find URL handler.
|
||||
Returns tuple of (function, opts, param) or (None, None) if not found.
|
||||
"""
|
||||
# First try - lookup in explicit (non parameterized URLs)
|
||||
if req.path in self.explicit_url_map:
|
||||
return self.explicit_url_map[req.path]
|
||||
# Second try - strip last path segment and lookup in another map
|
||||
idx = req.path.rfind(b'/') + 1
|
||||
path2 = req.path[:idx]
|
||||
if len(path2) > 0 and path2 in self.parameterized_url_map:
|
||||
# Save parameter into request
|
||||
req._param = req.path[idx:].decode()
|
||||
return self.parameterized_url_map[path2]
|
||||
|
||||
if self.catch_all_handler:
|
||||
return self.catch_all_handler
|
||||
|
||||
# No handler found
|
||||
return (None, None)
|
||||
|
||||
async def _handle_request(self, req, resp):
|
||||
await req.read_request_line()
|
||||
# Find URL handler
|
||||
req.handler, req.params = self._find_url_handler(req)
|
||||
if not req.handler:
|
||||
# No URL handler found - read response and issue HTTP 404
|
||||
await req.read_headers()
|
||||
raise HTTPException(404)
|
||||
# req.params = params
|
||||
# req.handler = han
|
||||
resp.params = req.params
|
||||
# Read / parse headers
|
||||
await req.read_headers(req.params['save_headers'])
|
||||
|
||||
async def _handler(self, reader, writer):
|
||||
"""Handler for TCP connection with
|
||||
HTTP/1.0 protocol implementation
|
||||
"""
|
||||
gc.collect()
|
||||
|
||||
try:
|
||||
req = request(reader)
|
||||
resp = response(writer)
|
||||
# Read HTTP Request with timeout
|
||||
await asyncio.wait_for(self._handle_request(req, resp),
|
||||
self.request_timeout)
|
||||
|
||||
# OPTIONS method is handled automatically
|
||||
if req.method == b'OPTIONS':
|
||||
resp.add_access_control_headers()
|
||||
# Since we support only HTTP 1.0 - it is important
|
||||
# to tell browser that there is no payload expected
|
||||
# otherwise some webkit based browsers (Chrome)
|
||||
# treat this behavior as an error
|
||||
resp.add_header('Content-Length', '0')
|
||||
await resp._send_headers()
|
||||
return
|
||||
|
||||
# Ensure that HTTP method is allowed for this path
|
||||
if req.method not in req.params['methods']:
|
||||
raise HTTPException(405)
|
||||
|
||||
# Handle URL
|
||||
gc.collect()
|
||||
if hasattr(req, '_param'):
|
||||
await req.handler(req, resp, req._param)
|
||||
else:
|
||||
await req.handler(req, resp)
|
||||
# Done here
|
||||
except (asyncio.CancelledError, asyncio.TimeoutError):
|
||||
pass
|
||||
except OSError as e:
|
||||
# Do not send response for connection related errors - too late :)
|
||||
# P.S. code 32 - is possible BROKEN PIPE error (TODO: is it true?)
|
||||
if e.args[0] not in (errno.ECONNABORTED, errno.ECONNRESET, 32):
|
||||
try:
|
||||
await resp.error(500)
|
||||
except Exception as e:
|
||||
log.exc(e, "")
|
||||
except HTTPException as e:
|
||||
try:
|
||||
await resp.error(e.code)
|
||||
except Exception as e:
|
||||
log.exc(e)
|
||||
except Exception as e:
|
||||
# Unhandled expection in user's method
|
||||
log.error(req.path.decode())
|
||||
log.exc(e, "")
|
||||
try:
|
||||
await resp.error(500)
|
||||
# Send exception info if desired
|
||||
if self.debug:
|
||||
sys.print_exception(e, resp.writer.s)
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
await writer.aclose()
|
||||
# Max concurrency support -
|
||||
# if queue is full schedule resume of TCP server task
|
||||
if len(self.conns) == self.max_concurrency:
|
||||
self.loop.create_task(self._server_coro)
|
||||
# Delete connection, using socket as a key
|
||||
del self.conns[id(writer.s)]
|
||||
|
||||
def add_route(self, url, f, **kwargs):
|
||||
"""Add URL to function mapping.
|
||||
Arguments:
|
||||
url - url to map function with
|
||||
f - function to map
|
||||
Keyword arguments:
|
||||
methods - list of allowed methods. Defaults to ['GET', 'POST']
|
||||
save_headers - contains list of HTTP headers to be saved. Case sensitive. Default - empty.
|
||||
max_body_size - Max HTTP body size (e.g. POST form data). Defaults to 1024
|
||||
allowed_access_control_headers - Default value for the same name header. Defaults to *
|
||||
allowed_access_control_origins - Default value for the same name header. Defaults to *
|
||||
"""
|
||||
if url == '' or '?' in url:
|
||||
raise ValueError('Invalid URL')
|
||||
# Initial params for route
|
||||
params = {'methods': ['GET'],
|
||||
'save_headers': [],
|
||||
'max_body_size': 1024,
|
||||
'allowed_access_control_headers': '*',
|
||||
'allowed_access_control_origins': '*',
|
||||
}
|
||||
params.update(kwargs)
|
||||
params['allowed_access_control_methods'] = ', '.join(params['methods'])
|
||||
# Convert methods/headers to bytestring
|
||||
params['methods'] = [x.encode() for x in params['methods']]
|
||||
params['save_headers'] = [x.encode() for x in params['save_headers']]
|
||||
# If URL has a parameter
|
||||
if url.endswith('>'):
|
||||
idx = url.rfind('<')
|
||||
path = url[:idx]
|
||||
idx += 1
|
||||
param = url[idx:-1]
|
||||
if path.encode() in self.parameterized_url_map:
|
||||
raise ValueError('URL exists')
|
||||
params['_param_name'] = param
|
||||
self.parameterized_url_map[path.encode()] = (f, params)
|
||||
|
||||
if url.encode() in self.explicit_url_map:
|
||||
raise ValueError('URL exists')
|
||||
self.explicit_url_map[url.encode()] = (f, params)
|
||||
|
||||
def add_resource(self, cls, url, **kwargs):
|
||||
"""Map resource (RestAPI) to URL
|
||||
Arguments:
|
||||
cls - Resource class to map to
|
||||
url - url to map to class
|
||||
kwargs - User defined key args to pass to the handler.
|
||||
Example:
|
||||
class myres():
|
||||
def get(self, data):
|
||||
return {'hello': 'world'}
|
||||
app.add_resource(myres, '/api/myres')
|
||||
"""
|
||||
methods = []
|
||||
callmap = {}
|
||||
# Create instance of resource handler, if passed as just class (not instance)
|
||||
try:
|
||||
obj = cls()
|
||||
except TypeError:
|
||||
obj = cls
|
||||
# Get all implemented HTTP methods and make callmap
|
||||
for m in ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']:
|
||||
fn = m.lower()
|
||||
if hasattr(obj, fn):
|
||||
methods.append(m)
|
||||
callmap[m.encode()] = (getattr(obj, fn), kwargs)
|
||||
self.add_route(url, restful_resource_handler,
|
||||
methods=methods,
|
||||
save_headers=['Content-Length', 'Content-Type'],
|
||||
_callmap=callmap)
|
||||
|
||||
def catchall(self):
|
||||
"""Decorator for catchall()
|
||||
Example:
|
||||
@app.catchall()
|
||||
def catchall_handler(req, resp):
|
||||
response.code = 404
|
||||
await response.start_html()
|
||||
await response.send('<html><body><h1>My custom 404!</h1></html>\n')
|
||||
"""
|
||||
params = {'methods': [b'GET'], 'save_headers': [], 'max_body_size': 1024, 'allowed_access_control_headers': '*', 'allowed_access_control_origins': '*'}
|
||||
|
||||
def _route(f):
|
||||
self.catch_all_handler = (f, params)
|
||||
return f
|
||||
return _route
|
||||
|
||||
def route(self, url, **kwargs):
|
||||
"""Decorator for add_route()
|
||||
Example:
|
||||
@app.route('/')
|
||||
def index(req, resp):
|
||||
await resp.start_html()
|
||||
await resp.send('<html><body><h1>Hello, world!</h1></html>\n')
|
||||
"""
|
||||
def _route(f):
|
||||
self.add_route(url, f, **kwargs)
|
||||
return f
|
||||
return _route
|
||||
|
||||
def resource(self, url, method='GET', **kwargs):
|
||||
"""Decorator for add_resource() method
|
||||
Examples:
|
||||
@app.resource('/users')
|
||||
def users(data):
|
||||
return {'a': 1}
|
||||
@app.resource('/messages/<topic_id>')
|
||||
async def index(data, topic_id):
|
||||
yield '{'
|
||||
yield '"topic_id": "{}",'.format(topic_id)
|
||||
yield '"message": "test",'
|
||||
yield '}'
|
||||
"""
|
||||
def _resource(f):
|
||||
self.add_route(url, restful_resource_handler,
|
||||
methods=[method],
|
||||
save_headers=['Content-Length', 'Content-Type'],
|
||||
_callmap={method.encode(): (f, kwargs)})
|
||||
return f
|
||||
return _resource
|
||||
|
||||
async def _tcp_server(self, host, port, backlog):
|
||||
"""TCP Server implementation.
|
||||
Opens socket for accepting connection and
|
||||
creates task for every new accepted connection
|
||||
"""
|
||||
addr = socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM)[0][-1]
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.setblocking(False)
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
sock.bind(addr)
|
||||
sock.listen(backlog)
|
||||
try:
|
||||
while True:
|
||||
if IS_UASYNCIO_V3:
|
||||
yield uasyncio.core._io_queue.queue_read(sock)
|
||||
else:
|
||||
yield asyncio.IORead(sock)
|
||||
csock, caddr = sock.accept()
|
||||
csock.setblocking(False)
|
||||
# Start handler / keep it in the map - to be able to
|
||||
# shutdown gracefully - by close all connections
|
||||
self.processed_connections += 1
|
||||
hid = id(csock)
|
||||
handler = self._handler(asyncio.StreamReader(csock),
|
||||
asyncio.StreamWriter(csock, {}))
|
||||
self.conns[hid] = handler
|
||||
self.loop.create_task(handler)
|
||||
# In case of max concurrency reached - temporary pause server:
|
||||
# 1. backlog must be greater than max_concurrency, otherwise
|
||||
# client will got "Connection Reset"
|
||||
# 2. Server task will be resumed whenever one active connection finished
|
||||
if len(self.conns) == self.max_concurrency:
|
||||
# Pause
|
||||
yield False
|
||||
except asyncio.CancelledError:
|
||||
return
|
||||
finally:
|
||||
sock.close()
|
||||
|
||||
def run(self, host="127.0.0.1", port=8081, loop_forever=True):
|
||||
"""Run Web Server. By default it runs forever.
|
||||
Keyword arguments:
|
||||
host - host to listen on. By default - localhost (127.0.0.1)
|
||||
port - port to listen on. By default - 8081
|
||||
loop_forever - run loo.loop_forever(), otherwise caller must run it by itself.
|
||||
"""
|
||||
self._server_coro = self._tcp_server(host, port, self.backlog)
|
||||
self.loop.create_task(self._server_coro)
|
||||
if loop_forever:
|
||||
self.loop.run_forever()
|
||||
|
||||
def shutdown(self):
|
||||
"""Gracefully shutdown Web Server"""
|
||||
asyncio.cancel(self._server_coro)
|
||||
for hid, coro in self.conns.items():
|
||||
asyncio.cancel(coro)
|
|
@ -1,168 +0,0 @@
|
|||
import gc
|
||||
import time
|
||||
from machine import reset
|
||||
import inky_helper as ih
|
||||
|
||||
# Uncomment the line for your Inky Frame display size
|
||||
# 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"
|
||||
|
||||
# Create a secrets.py with your Wifi details to be able to get the time
|
||||
#
|
||||
# secrets.py should contain:
|
||||
# WIFI_SSID = "Your WiFi SSID"
|
||||
# WIFI_PASSWORD = "Your WiFi password"
|
||||
|
||||
# A short delay to give USB chance to initialise
|
||||
time.sleep(0.5)
|
||||
|
||||
# Setup for the display.
|
||||
graphics = PicoGraphics(DISPLAY)
|
||||
WIDTH, HEIGHT = graphics.get_bounds()
|
||||
graphics.set_font("bitmap8")
|
||||
|
||||
|
||||
def launcher():
|
||||
|
||||
# Apply an offset for the Inky Frame 5.7".
|
||||
if HEIGHT == 448:
|
||||
y_offset = 20
|
||||
# Inky Frame 7.3"
|
||||
elif HEIGHT == 480:
|
||||
y_offset = 35
|
||||
# Inky Frame 4"
|
||||
else:
|
||||
y_offset = 0
|
||||
|
||||
# Draws the menu
|
||||
graphics.set_pen(1)
|
||||
graphics.clear()
|
||||
graphics.set_pen(0)
|
||||
|
||||
graphics.set_pen(graphics.create_pen(255, 215, 0))
|
||||
graphics.rectangle(0, 0, WIDTH, 50)
|
||||
graphics.set_pen(0)
|
||||
title = "Launcher"
|
||||
title_len = graphics.measure_text(title, 4) // 2
|
||||
graphics.text(title, (WIDTH // 2 - title_len), 10, WIDTH, 4)
|
||||
|
||||
graphics.set_pen(4)
|
||||
graphics.rectangle(30, HEIGHT - (340 + y_offset), WIDTH - 100, 50)
|
||||
graphics.set_pen(1)
|
||||
graphics.text("A. NASA Picture of the Day", 35, HEIGHT - (325 + y_offset), 600, 3)
|
||||
|
||||
graphics.set_pen(6)
|
||||
graphics.rectangle(30, HEIGHT - (280 + y_offset), WIDTH - 150, 50)
|
||||
graphics.set_pen(1)
|
||||
graphics.text("B. Word Clock", 35, HEIGHT - (265 + y_offset), 600, 3)
|
||||
|
||||
graphics.set_pen(2)
|
||||
graphics.rectangle(30, HEIGHT - (220 + y_offset), WIDTH - 200, 50)
|
||||
graphics.set_pen(1)
|
||||
graphics.text("C. Daily Activity", 35, HEIGHT - (205 + y_offset), 600, 3)
|
||||
|
||||
graphics.set_pen(3)
|
||||
graphics.rectangle(30, HEIGHT - (160 + y_offset), WIDTH - 250, 50)
|
||||
graphics.set_pen(1)
|
||||
graphics.text("D. Headlines", 35, HEIGHT - (145 + y_offset), 600, 3)
|
||||
|
||||
graphics.set_pen(0)
|
||||
graphics.rectangle(30, HEIGHT - (100 + y_offset), WIDTH - 300, 50)
|
||||
graphics.set_pen(1)
|
||||
graphics.text("E. Random Joke", 35, HEIGHT - (85 + y_offset), 600, 3)
|
||||
|
||||
graphics.set_pen(graphics.create_pen(220, 220, 220))
|
||||
graphics.rectangle(WIDTH - 100, HEIGHT - (340 + y_offset), 70, 50)
|
||||
graphics.rectangle(WIDTH - 150, HEIGHT - (280 + y_offset), 120, 50)
|
||||
graphics.rectangle(WIDTH - 200, HEIGHT - (220 + y_offset), 170, 50)
|
||||
graphics.rectangle(WIDTH - 250, HEIGHT - (160 + y_offset), 220, 50)
|
||||
graphics.rectangle(WIDTH - 300, HEIGHT - (100 + y_offset), 270, 50)
|
||||
|
||||
graphics.set_pen(0)
|
||||
note = "Hold A + E, then press Reset, to return to the Launcher"
|
||||
note_len = graphics.measure_text(note, 2) // 2
|
||||
graphics.text(note, (WIDTH // 2 - note_len), HEIGHT - 30, 600, 2)
|
||||
|
||||
ih.led_warn.on()
|
||||
graphics.update()
|
||||
ih.led_warn.off()
|
||||
|
||||
# Now we've drawn the menu to the screen, we wait here for the user to select an app.
|
||||
# Then once an app is selected, we set that as the current app and reset the device and load into it.
|
||||
|
||||
# You can replace any of the included examples with one of your own,
|
||||
# just replace the name of the app in the line "ih.update_last_app("nasa_apod")"
|
||||
|
||||
while True:
|
||||
if ih.inky_frame.button_a.read():
|
||||
ih.inky_frame.button_a.led_on()
|
||||
ih.update_state("nasa_apod")
|
||||
time.sleep(0.5)
|
||||
reset()
|
||||
if ih.inky_frame.button_b.read():
|
||||
ih.inky_frame.button_b.led_on()
|
||||
ih.update_state("word_clock")
|
||||
time.sleep(0.5)
|
||||
reset()
|
||||
if ih.inky_frame.button_c.read():
|
||||
ih.inky_frame.button_c.led_on()
|
||||
ih.update_state("daily_activity")
|
||||
time.sleep(0.5)
|
||||
reset()
|
||||
if ih.inky_frame.button_d.read():
|
||||
ih.inky_frame.button_d.led_on()
|
||||
ih.update_state("news_headlines")
|
||||
time.sleep(0.5)
|
||||
reset()
|
||||
if ih.inky_frame.button_e.read():
|
||||
ih.inky_frame.button_e.led_on()
|
||||
ih.update_state("random_joke")
|
||||
time.sleep(0.5)
|
||||
reset()
|
||||
|
||||
|
||||
# Turn any LEDs off that may still be on from last run.
|
||||
ih.clear_button_leds()
|
||||
ih.led_warn.off()
|
||||
|
||||
if ih.inky_frame.button_a.read() and ih.inky_frame.button_e.read():
|
||||
launcher()
|
||||
|
||||
ih.clear_button_leds()
|
||||
|
||||
if ih.file_exists("state.json"):
|
||||
# Loads the JSON and launches the app
|
||||
ih.load_state()
|
||||
ih.launch_app(ih.state['run'])
|
||||
|
||||
# Passes the the graphics object from the launcher to the app
|
||||
ih.app.graphics = graphics
|
||||
ih.app.WIDTH = WIDTH
|
||||
ih.app.HEIGHT = HEIGHT
|
||||
|
||||
else:
|
||||
launcher()
|
||||
|
||||
try:
|
||||
from secrets import WIFI_SSID, WIFI_PASSWORD
|
||||
ih.network_connect(WIFI_SSID, WIFI_PASSWORD)
|
||||
except ImportError:
|
||||
print("Create secrets.py with your WiFi credentials")
|
||||
|
||||
# Get some memory back, we really need it!
|
||||
gc.collect()
|
||||
|
||||
# The main loop executes the update and draw function from the imported app,
|
||||
# and then goes to sleep ZzzzZZz
|
||||
|
||||
file = ih.file_exists("state.json")
|
||||
|
||||
print(file)
|
||||
|
||||
while True:
|
||||
ih.app.update()
|
||||
ih.led_warn.on()
|
||||
ih.app.draw()
|
||||
ih.led_warn.off()
|
||||
ih.sleep(ih.app.UPDATE_INTERVAL)
|
|
@ -1,101 +0,0 @@
|
|||
import gc
|
||||
import jpegdec
|
||||
from urllib import urequest
|
||||
from ujson import load
|
||||
|
||||
gc.collect()
|
||||
|
||||
graphics = None
|
||||
WIDTH = None
|
||||
HEIGHT = None
|
||||
|
||||
FILENAME = "nasa-apod-daily"
|
||||
|
||||
# A Demo Key is used in this example and is IP rate limited. You can get your own API Key from https://api.nasa.gov/
|
||||
API_URL = "https://api.nasa.gov/planetary/apod?api_key=DEMO_KEY"
|
||||
|
||||
# Length of time between updates in minutes.
|
||||
# Frequent updates will reduce battery life!
|
||||
UPDATE_INTERVAL = 240
|
||||
|
||||
# Variable for storing the NASA APOD Title
|
||||
apod_title = None
|
||||
|
||||
|
||||
def show_error(text):
|
||||
graphics.set_pen(4)
|
||||
graphics.rectangle(0, 10, WIDTH, 35)
|
||||
graphics.set_pen(1)
|
||||
graphics.text(text, 5, 16, 400, 2)
|
||||
|
||||
|
||||
def update():
|
||||
global apod_title
|
||||
|
||||
if HEIGHT == 448:
|
||||
# Image for Inky Frame 5.7
|
||||
IMG_URL = "https://pimoroni.github.io/feed2image/nasa-apod-daily.jpg"
|
||||
elif HEIGHT == 400:
|
||||
# Image for Inky Frame 4.0
|
||||
IMG_URL = "https://pimoroni.github.io/feed2image/nasa-apod-640x400-daily.jpg"
|
||||
elif HEIGHT == 480:
|
||||
# Image for Inky Frame 7.3
|
||||
IMG_URL = "https://pimoroni.github.io/feed2image/nasa-apod-800x480-daily.jpg"
|
||||
|
||||
try:
|
||||
# Grab the data
|
||||
socket = urequest.urlopen(API_URL)
|
||||
gc.collect()
|
||||
j = load(socket)
|
||||
socket.close()
|
||||
apod_title = j['title']
|
||||
gc.collect()
|
||||
except OSError as e:
|
||||
print(e)
|
||||
apod_title = "Image Title Unavailable"
|
||||
|
||||
try:
|
||||
# Grab the image
|
||||
socket = urequest.urlopen(IMG_URL)
|
||||
|
||||
gc.collect()
|
||||
|
||||
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()
|
||||
except OSError as e:
|
||||
print(e)
|
||||
show_error("Unable to download image")
|
||||
|
||||
|
||||
def draw():
|
||||
jpeg = jpegdec.JPEG(graphics)
|
||||
gc.collect() # For good measure...
|
||||
|
||||
graphics.set_pen(1)
|
||||
graphics.clear()
|
||||
|
||||
try:
|
||||
jpeg.open_file(FILENAME)
|
||||
jpeg.decode()
|
||||
except OSError:
|
||||
graphics.set_pen(4)
|
||||
graphics.rectangle(0, (HEIGHT // 2) - 20, WIDTH, 40)
|
||||
graphics.set_pen(1)
|
||||
graphics.text("Unable to display image!", 5, (HEIGHT // 2) - 15, WIDTH, 2)
|
||||
graphics.text("Check your network settings in secrets.py", 5, (HEIGHT // 2) + 2, WIDTH, 2)
|
||||
|
||||
graphics.set_pen(0)
|
||||
graphics.rectangle(0, HEIGHT - 25, WIDTH, 25)
|
||||
graphics.set_pen(1)
|
||||
graphics.text(apod_title, 5, HEIGHT - 20, WIDTH, 2)
|
||||
|
||||
gc.collect()
|
||||
|
||||
graphics.update()
|
|
@ -1,170 +0,0 @@
|
|||
from urllib import urequest
|
||||
import gc
|
||||
import qrcode
|
||||
|
||||
# Uncomment one URL to use (Top Stories, World News and technology)
|
||||
# URL = "https://feeds.bbci.co.uk/news/rss.xml"
|
||||
# URL = "https://feeds.bbci.co.uk/news/world/rss.xml"
|
||||
URL = "https://feeds.bbci.co.uk/news/technology/rss.xml"
|
||||
|
||||
# Length of time between updates in minutes.
|
||||
# Frequent updates will reduce battery life!
|
||||
UPDATE_INTERVAL = 90
|
||||
|
||||
graphics = None
|
||||
WIDTH = None
|
||||
HEIGHT = None
|
||||
code = qrcode.QRCode()
|
||||
|
||||
|
||||
def read_until(stream, find):
|
||||
result = b""
|
||||
while len(c := stream.read(1)) > 0:
|
||||
if c == find:
|
||||
return result
|
||||
result += c
|
||||
|
||||
|
||||
def discard_until(stream, find):
|
||||
_ = read_until(stream, find)
|
||||
|
||||
|
||||
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 <?xml vers...
|
||||
if next_char == b"?":
|
||||
discard_until(s, b">")
|
||||
continue
|
||||
|
||||
# Detect <![CDATA
|
||||
elif next_char == b"!":
|
||||
s.read(1) # Discard [
|
||||
discard_until(s, b"[") # Discard CDATA[
|
||||
text = read_until(s, b"]")
|
||||
discard_until(s, b">") # 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">")
|
||||
if not current_tag.endswith(b"/"):
|
||||
tag += [next_char + current_tag.split(b" ")[0]]
|
||||
text = b""
|
||||
|
||||
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)
|
||||
|
||||
|
||||
# A function to get the data from an RSS Feed, this in case BBC News.
|
||||
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 []
|
||||
|
||||
|
||||
feed = None
|
||||
|
||||
|
||||
def update():
|
||||
global feed
|
||||
# Gets Feed Data
|
||||
feed = get_rss()
|
||||
|
||||
|
||||
def draw():
|
||||
global feed
|
||||
graphics.set_font("bitmap8")
|
||||
|
||||
# Clear the screen
|
||||
graphics.set_pen(1)
|
||||
graphics.clear()
|
||||
graphics.set_pen(0)
|
||||
|
||||
# Draws 2 articles from the feed if they're available.
|
||||
if len(feed) > 0:
|
||||
|
||||
# Title
|
||||
graphics.set_pen(graphics.create_pen(200, 0, 0))
|
||||
graphics.rectangle(0, 0, WIDTH, 40)
|
||||
graphics.set_pen(1)
|
||||
graphics.text("Headlines from BBC News:", 10, 10, 320, 3)
|
||||
|
||||
graphics.set_pen(4)
|
||||
graphics.text(feed[0]["title"], 10, 70, WIDTH - 150, 3 if graphics.measure_text(feed[0]["title"]) < WIDTH else 2)
|
||||
graphics.text(feed[1]["title"], 130, 260, WIDTH - 140, 3 if graphics.measure_text(feed[1]["title"]) < WIDTH else 2)
|
||||
|
||||
graphics.set_pen(3)
|
||||
graphics.text(feed[0]["description"], 10, 135 if graphics.measure_text(feed[0]["title"]) < 650 else 90, WIDTH - 150, 2)
|
||||
graphics.text(feed[1]["description"], 130, 320 if graphics.measure_text(feed[1]["title"]) < 650 else 230, WIDTH - 145, 2)
|
||||
|
||||
graphics.line(10, 215, WIDTH - 10, 215)
|
||||
|
||||
code.set_text(feed[0]["guid"])
|
||||
draw_qr_code(WIDTH - 110, 65, 100, code)
|
||||
code.set_text(feed[1]["guid"])
|
||||
draw_qr_code(10, 265, 100, code)
|
||||
|
||||
graphics.set_pen(graphics.create_pen(200, 0, 0))
|
||||
graphics.rectangle(0, HEIGHT - 20, WIDTH, 20)
|
||||
|
||||
else:
|
||||
graphics.set_pen(4)
|
||||
graphics.rectangle(0, (HEIGHT // 2) - 20, WIDTH, 40)
|
||||
graphics.set_pen(1)
|
||||
graphics.text("Unable to display news feed!", 5, (HEIGHT // 2) - 15, WIDTH, 2)
|
||||
graphics.text("Check your network settings in secrets.py", 5, (HEIGHT // 2) + 2, WIDTH, 2)
|
||||
|
||||
graphics.update()
|
|
@ -1,121 +0,0 @@
|
|||
import gc
|
||||
import random
|
||||
from urllib import urequest
|
||||
from ujson import load
|
||||
import qrcode
|
||||
|
||||
gc.collect() # We're really gonna need that RAM!
|
||||
|
||||
graphics = None
|
||||
|
||||
WIDTH = 0
|
||||
HEIGHT = 0
|
||||
|
||||
FILENAME = "random-joke.jpg"
|
||||
|
||||
JOKE_IDS = "https://pimoroni.github.io/feed2image/jokeapi-ids.txt"
|
||||
JOKE_IMG = "https://pimoroni.github.io/feed2image/jokeapi-{}.json"
|
||||
|
||||
UPDATE_INTERVAL = 60
|
||||
|
||||
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
|
||||
|
||||
joke = []
|
||||
|
||||
|
||||
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 update():
|
||||
global joke
|
||||
|
||||
try:
|
||||
socket = urequest.urlopen(JOKE_IDS)
|
||||
except OSError:
|
||||
return
|
||||
|
||||
# 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)
|
||||
|
||||
gc.collect()
|
||||
|
||||
# Grab the data
|
||||
try:
|
||||
socket = urequest.urlopen(url)
|
||||
gc.collect()
|
||||
j = load(socket)
|
||||
socket.close()
|
||||
joke = j
|
||||
del j
|
||||
gc.collect()
|
||||
except OSError as e:
|
||||
print(e)
|
||||
|
||||
|
||||
def draw():
|
||||
|
||||
code = qrcode.QRCode()
|
||||
graphics.set_pen(1)
|
||||
graphics.clear()
|
||||
|
||||
if joke:
|
||||
if joke['type'] == "single":
|
||||
graphics.set_pen(4)
|
||||
graphics.text(joke['joke'], 10, 10, WIDTH - 75, 5)
|
||||
if joke['type'] == "twopart":
|
||||
graphics.set_pen(4)
|
||||
graphics.text(joke['setup'], 10, 10, WIDTH - 75, 5)
|
||||
graphics.set_pen(3)
|
||||
graphics.text(joke['delivery'], 10, 250, WIDTH - 75, 5)
|
||||
|
||||
graphics.set_pen(0)
|
||||
# Donate link QR
|
||||
code.set_text("https://github.com/sponsors/Sv443")
|
||||
draw_qr_code(WIDTH - 75, HEIGHT - 75, 75, code)
|
||||
|
||||
graphics.text("curated by jokeapi.dev", 10, HEIGHT - 15, WIDTH, 1)
|
||||
graphics.text("donate <3", WIDTH - 65, HEIGHT - 12, WIDTH, 1)
|
||||
|
||||
else:
|
||||
graphics.set_pen(4)
|
||||
graphics.rectangle(0, (HEIGHT // 2) - 20, WIDTH, 40)
|
||||
graphics.set_pen(1)
|
||||
graphics.text("Unable to display random joke!", 5, (HEIGHT // 2) - 15, WIDTH, 2)
|
||||
graphics.text("Check your network settings in secrets.py", 5, (HEIGHT // 2) + 2, WIDTH, 2)
|
||||
|
||||
graphics.update()
|
|
@ -1,3 +0,0 @@
|
|||
# secrets.py should contain:
|
||||
WIFI_SSID = ""
|
||||
WIFI_PASSWORD = ""
|
|
@ -1,101 +0,0 @@
|
|||
import machine
|
||||
import ntptime
|
||||
|
||||
# Length of time between updates in minutes.
|
||||
UPDATE_INTERVAL = 15
|
||||
graphics = None
|
||||
|
||||
rtc = machine.RTC()
|
||||
time_string = None
|
||||
words = ["it", "d", "is", "m", "about", "l", "half", "c", "quarter", "b", "to", "u", "past", "n", "one",
|
||||
"two", "three", "four", "five", "six", "eleven", "ten", "nine", "eight", "seven", "rm", "twelve", "rt", "O'Clock", "q"]
|
||||
|
||||
|
||||
def approx_time(hours, minutes):
|
||||
nums = {0: "twelve", 1: "one", 2: "two",
|
||||
3: "three", 4: "four", 5: "five", 6: "six",
|
||||
7: "seven", 8: "eight", 9: "nine", 10: "ten",
|
||||
11: "eleven", 12: "twelve"}
|
||||
|
||||
if hours == 12:
|
||||
hours = 0
|
||||
if minutes > 0 and minutes < 8:
|
||||
return "it is about " + nums[hours] + " O'Clock"
|
||||
elif minutes >= 8 and minutes < 23:
|
||||
return "it is about quarter past " + nums[hours]
|
||||
elif minutes >= 23 and minutes < 38:
|
||||
return "it is about half past " + nums[hours]
|
||||
elif minutes >= 38 and minutes < 53:
|
||||
return "it is about quarter to " + nums[hours + 1]
|
||||
else:
|
||||
return "it is about " + nums[hours + 1] + " O'Clock"
|
||||
|
||||
|
||||
def update():
|
||||
global time_string
|
||||
# grab the current time from the ntp server and update the Pico RTC
|
||||
try:
|
||||
ntptime.settime()
|
||||
except OSError:
|
||||
print("Unable to contact NTP server")
|
||||
|
||||
current_t = rtc.datetime()
|
||||
time_string = approx_time(current_t[4] - 12 if current_t[4] > 12 else current_t[4], current_t[5])
|
||||
|
||||
# Splits the string into an array of words for displaying later
|
||||
time_string = time_string.split()
|
||||
|
||||
print(time_string)
|
||||
|
||||
|
||||
def draw():
|
||||
global time_string
|
||||
graphics.set_font("bitmap8")
|
||||
|
||||
WIDTH, HEIGHT = graphics.get_bounds()
|
||||
|
||||
# Clear the screen
|
||||
graphics.set_pen(1)
|
||||
graphics.clear()
|
||||
graphics.set_pen(6)
|
||||
|
||||
# Values for the layout and spacing
|
||||
if WIDTH == 640: # Inky Frame 4.0"
|
||||
default_x = 5
|
||||
x = default_x
|
||||
y = 10
|
||||
line_space = 70
|
||||
letter_space = 40
|
||||
elif WIDTH == 800:
|
||||
default_x = 5
|
||||
x = default_x
|
||||
y = 70
|
||||
line_space = 60
|
||||
letter_space = 50
|
||||
else: # Inky Frame 5.7"
|
||||
default_x = 20
|
||||
x = default_x
|
||||
y = 40
|
||||
line_space = 65
|
||||
letter_space = 35
|
||||
|
||||
scale = 5
|
||||
spacing = 2
|
||||
|
||||
for word in words:
|
||||
|
||||
if word in time_string:
|
||||
graphics.set_pen(0)
|
||||
else:
|
||||
graphics.set_pen(graphics.create_pen(220, 220, 220))
|
||||
|
||||
for letter in word:
|
||||
text_length = graphics.measure_text(letter, scale, spacing)
|
||||
if not x + text_length <= WIDTH:
|
||||
y += line_space
|
||||
x = default_x
|
||||
|
||||
graphics.text(letter.upper(), x, y, 640, scale=scale, spacing=spacing)
|
||||
x += letter_space
|
||||
|
||||
graphics.update()
|
|
@ -1,33 +0,0 @@
|
|||
# Control the brightness of Inky Frame's LEDs using PWM
|
||||
# More about PWM / frequency / duty cycle here:
|
||||
# https://projects.raspberrypi.org/en/projects/getting-started-with-the-pico/7
|
||||
|
||||
from machine import Pin, PWM
|
||||
from time import sleep
|
||||
|
||||
led_activity = PWM(Pin(6))
|
||||
|
||||
# we're just PWMing the activity LED in this demo
|
||||
# but here are the pins for the other LEDs for reference
|
||||
led_connect = PWM(Pin(7))
|
||||
led_a = PWM(Pin(11))
|
||||
led_b = PWM(Pin(12))
|
||||
led_c = PWM(Pin(13))
|
||||
led_d = PWM(Pin(14))
|
||||
led_e = PWM(Pin(15))
|
||||
|
||||
led_activity.freq(1000)
|
||||
|
||||
leds = [led_activity, led_connect, led_a, led_b, led_c, led_d, led_e]
|
||||
n = 0
|
||||
|
||||
while True:
|
||||
for _ in range(2):
|
||||
for duty in range(65025, 2):
|
||||
leds[n].duty_u16(duty)
|
||||
sleep(0.0001)
|
||||
for duty in range(65025, 0, -2):
|
||||
leds[n].duty_u16(duty)
|
||||
sleep(0.0001)
|
||||
n += 1
|
||||
n %= len(leds)
|
Przed Szerokość: | Wysokość: | Rozmiar: 5.4 KiB |
|
@ -1,26 +0,0 @@
|
|||
# This simple example shows how to read and write from the SD card on Inky Frame.
|
||||
from machine import Pin, SPI
|
||||
import sdcard
|
||||
import os
|
||||
|
||||
# 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 file and add some text
|
||||
# if this file already exists it will be overwritten
|
||||
with open("/sd/inkytest.txt", "w") as f:
|
||||
f.write("Hello Inky!\r\n")
|
||||
f.close()
|
||||
|
||||
# open the file and append some text (useful for logging!)
|
||||
with open("/sd/inkytest.txt", "a") as f:
|
||||
f.write("We're appending some text\r\n")
|
||||
f.close()
|
||||
|
||||
# read the file and print the contents to the console
|
||||
with open("/sd/inkytest.txt", "r") as f:
|
||||
data = f.read()
|
||||
print(data)
|
||||
f.close()
|
|
@ -1,58 +0,0 @@
|
|||
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}/../../")
|
||||
|
||||
set(CMAKE_C_STANDARD 11)
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
|
||||
# Essential
|
||||
include(pimoroni_i2c/micropython)
|
||||
include(pimoroni_bus/micropython)
|
||||
|
||||
# Pico Graphics Essential
|
||||
include(hershey_fonts/micropython)
|
||||
include(bitmap_fonts/micropython)
|
||||
include(picographics/micropython)
|
||||
|
||||
# Pico Graphics Extra
|
||||
include(pngdec/micropython)
|
||||
include(jpegdec/micropython)
|
||||
include(qrcode/micropython/micropython)
|
||||
|
||||
# Sensors & Breakouts
|
||||
include(micropython-common-breakouts)
|
||||
include(pcf85063a/micropython)
|
||||
|
||||
# Utility
|
||||
include(adcfft/micropython)
|
||||
include(wakeup/micropython)
|
||||
|
||||
# Configure wakeup for Inky Frame
|
||||
target_compile_definitions(usermod_wakeup INTERFACE
|
||||
-DWAKEUP_HAS_RTC=1
|
||||
-DWAKEUP_HAS_SHIFT_REGISTER=1
|
||||
-DWAKEUP_PIN_MASK=0b01000100
|
||||
-DWAKEUP_PIN_DIR=0b01000100
|
||||
-DWAKEUP_PIN_VALUE=0b01000100
|
||||
)
|
||||
|
||||
# LEDs & Matrices
|
||||
include(plasma/micropython)
|
||||
|
||||
# Servos & Motors
|
||||
include(pwm/micropython)
|
||||
include(servo/micropython)
|
||||
include(encoder/micropython)
|
||||
include(motor/micropython)
|
||||
|
||||
include(modules_py/modules_py)
|
||||
|
||||
copy_module(inky_frame.py)
|
||||
|
||||
# C++ Magic Memory
|
||||
include(cppmem/micropython)
|
||||
|
||||
# Disable build-busting C++ exceptions
|
||||
include(micropython-disable-exceptions)
|
|
@ -14,6 +14,7 @@ target_include_directories(usermod_${MOD_NAME} INTERFACE
|
|||
|
||||
target_compile_definitions(usermod_${MOD_NAME} INTERFACE
|
||||
-DMODULE_${MOD_NAME_UPPER}_ENABLED=1
|
||||
-DPICO_INCLUDE_RTC_DATETIME=1
|
||||
)
|
||||
|
||||
target_link_libraries(usermod INTERFACE usermod_${MOD_NAME})
|
|
@ -40,21 +40,4 @@ const mp_obj_module_t wakeup_user_cmodule = {
|
|||
.globals = (mp_obj_dict_t*)&mp_module_wakeup_globals,
|
||||
};
|
||||
|
||||
#if MICROPY_VERSION <= 70144
|
||||
MP_REGISTER_MODULE(MP_QSTR_wakeup, wakeup_user_cmodule, MODULE_WAKEUP_ENABLED);
|
||||
#else
|
||||
MP_REGISTER_MODULE(MP_QSTR_wakeup, wakeup_user_cmodule);
|
||||
#endif
|
||||
|
||||
void runtime_init_latch(void) {
|
||||
runtime_wakeup_gpio_state = gpio_get_all();
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
// After runtime_init_early_resets, PICO_RUNTIME_INIT_EARLY_RESETS ?
|
||||
PICO_RUNTIME_INIT_FUNC_HW(runtime_init_latch, "00101");
|
||||
// Too early?
|
||||
// PICO_RUNTIME_INIT_FUNC_HW(runtime_init_latch, PICO_RUNTIME_INIT_EARLIEST);
|
||||
|
|
|
@ -1,164 +1,3 @@
|
|||
# Inky Frame (MicroPython) <!-- omit in toc -->
|
||||
|
||||
Most of your interaction with Inky Frame will be via the PicoGraphics library, but we have created an `inky_frame` module to help you read the onboard A, B, C, D and E buttons and control the LEDs.
|
||||
|
||||
- [Pico Graphics](#pico-graphics)
|
||||
- [Colour \& Dithering](#colour--dithering)
|
||||
- [Images \& JPEGs](#images--jpegs)
|
||||
- [Buttons \& LEDs](#buttons--leds)
|
||||
- [Status LEDs](#status-leds)
|
||||
- [Battery Power \& RTC](#battery-power--rtc)
|
||||
- [Function Reference](#function-reference)
|
||||
- [Wakeup States](#wakeup-states)
|
||||
- [RTC](#rtc)
|
||||
- [Other](#other)
|
||||
- [RAM Usage](#ram-usage)
|
||||
- [Accessing The Framebuffer](#accessing-the-framebuffer)
|
||||
|
||||
|
||||
## Pico Graphics
|
||||
|
||||
You can draw on Inky Frame using our PicoGraphics display library.
|
||||
- [PicoGraphics MicroPython function reference](../modules/picographics)
|
||||
|
||||
### Colour & Dithering
|
||||
|
||||
Inky Frame supports eight native colours, those that it's physically capable of displaying. These are:
|
||||
|
||||
* `BLACK` = 0
|
||||
* `WHITE` = 1
|
||||
* `GREEN` = 2
|
||||
* `BLUE` = 3
|
||||
* `RED` = 4
|
||||
* `YELLOW` = 5
|
||||
* `ORANGE` = 6
|
||||
* `TAUPE` = 7 (only used for dither on 4.0" and 5.7" displays †)
|
||||
|
||||
:info: † - the "taupe" colour (also known as clear) is the colour used to reset the display to a default, clear state. We misuse this as an extra colour on 4.0" and 5.7", but the 7.3" display clear colour is a sort of muddy, greenish gradient that's not consistent enough. You can stil use it, by all means, but it wont be considered for dithering.
|
||||
|
||||
These colours are available as constants in the `inky_frame` module so you don't have to remember which number corresponds to which, eg:
|
||||
|
||||
```python
|
||||
display.set_pen(inky_frame.YELLOW)
|
||||
```
|
||||
|
||||
You can use colours outside of these eight by using `create_pen(r, g, b)` or `create_pen_hsv(h, s, v)`. eg:
|
||||
|
||||
```python
|
||||
my_colour = display.create_pen(255, 128, 0)
|
||||
display.set_pen(my_colour)
|
||||
```
|
||||
|
||||
PicoGraphics will do its best to match your colour choice by mixing the available colours with a dither pattern. This works best on large areas of colour such as backgrounds, big UI elements or chunky text. See the `inky_frame_dithering.py` example for a demonstration.
|
||||
|
||||
:info: Due to the lack of light blue or pink native colours, Inky Frame isn't very good at reproducing pink, purple, violet or light blue, cyan and teal.
|
||||
|
||||
|
||||
### Images & JPEGs
|
||||
|
||||
You can use the `jpegdec` (JPEG decoding) module to display JPEG files on Inky, but you should be aware of some caveats:
|
||||
|
||||
1. JPEGs are compressed and lossy, a small JPEG displayed on Inky might show random specks of colour where you don't expect them as PicoGraphics tries its best to dither noise to the nearest available colours.
|
||||
2. JPEGs are, by default, dithered to the 7 (or 8 on 4.0 and 5.7) available colours. This uses "ordered dither," which is fast, but not very pretty.
|
||||
3. You can turn off dithering with `jpeg.decode(dither=False)` for a posterised effect.
|
||||
|
||||
## Buttons & LEDs
|
||||
|
||||
Inky Frame has five user buttons labelled A to E, with LEDs. The buttons are connected via a shift-register and available in the `inky_frame` module as `button_a`, `button_b`, `button_c`, `button_d` and `button_e`.
|
||||
|
||||
Each button has some convenient methods for checking if it's pressed:
|
||||
|
||||
* `raw()` - get the current state of the button with no debounce (returns `True` the first time it's called if the corresponding button)
|
||||
* `read()` - read the current button state, observing debounce (50ms)
|
||||
|
||||
Additionally each button has some methods for controlling its associated LED:
|
||||
|
||||
* `led_on()` - turn the LED on
|
||||
* `led_off()` - turn the LED off
|
||||
* `led_toggle()` - toggle the LED
|
||||
* `led_brightness(0.5)` - set the LED brightness (from 0.0 to 1.0)
|
||||
|
||||
### Status LEDs
|
||||
|
||||
In addition to the button LEDs there are two status LEDs, `busy` and `wifi`, which are available as:
|
||||
|
||||
* `led_busy` - connected to pin `LED_BUSY`
|
||||
* `led_wifi` - connected to pin `LED_WIFI`
|
||||
|
||||
These are instances of the `pimoroni.PWMLED` class, and have the following methods:
|
||||
|
||||
* `on()` - turn the LED on
|
||||
* `off()` - turn the LED off
|
||||
* `toggle()` - toggle the LED
|
||||
* `brightness(0.5)` - set the LED brightness (from 0.0 to 1.0)
|
||||
|
||||
:info: `toggle()` is provided for compatibility with the `Pin()` class. It's a little odd, since it changes the LED brightness to `1.0 - current_brightness`.
|
||||
|
||||
## Battery Power & RTC
|
||||
|
||||
When running on battery power, Inky Frame's buttons cause it to wake from a power-off state and start running your code. Additionally an onboard PCF85063A real-time clock runs continuously from battery and can wake up your Frame automatically.
|
||||
|
||||
The Inky Frame library includes a number of convenience functions to set the clock and sleep your device. If you want accurate time you must check if the clock is set - usually a simple check against the year works - connect to a network, and set via NTP (Network Time Protocol).
|
||||
|
||||
Inky Frame technically has *two* real-time clocks, the external RTC we've added - which remains continuously powered by battery - and the internal RTC of the Pico W. For convenience and compatibility with other code examples we recommend using the external RTC only to set the time on the internal one which makes functions like `time.localtime()` work as you'd expect.
|
||||
|
||||
```python
|
||||
import time
|
||||
import machine
|
||||
import inky_frame
|
||||
|
||||
inky_frame.pcf_to_pico_rtc() # Sync Inky RTC time to Pico's RTC
|
||||
|
||||
year, month, day, dow, hour, minute, second, _ = machine.RTC().datetime()
|
||||
|
||||
if year < 2023:
|
||||
# Connect to network
|
||||
inky_frame.set_time() # Sets both the Inky and Pico RTC
|
||||
|
||||
print(time.localtime())
|
||||
```
|
||||
|
||||
### Function Reference
|
||||
|
||||
#### Wakeup States
|
||||
|
||||
For your convenience these wakeup state functions also check the *current* state of their associated event, this allows you to run code from Thonny with a button held down - for example - to test how your deployed code will behave on battery-
|
||||
|
||||
* `inky_frame.woken_by_rtc()` - Returns `True` if the RTC caused a wakeup, or if the RTC ALARM is currently raised.
|
||||
* `inky_frame.woken_by_button()` - Returns `True` if a button caused a wakeup, or if a button is currently pressed.
|
||||
* `inky_frame.woken_by_ext_trigger()` Returns `True` if the external trigger caused a wakeup, or if the trigger is currently asserted.
|
||||
|
||||
#### RTC
|
||||
|
||||
* `inky_frame.set_time()` - Attempt to run `ntptime.settime()` and set the time on both RTCs
|
||||
* `inky_frame.pcf_to_pico_rtc()` - Sync from Inky's RTC to the Pico W's RTC
|
||||
* `inky_frame.pico_rtc_to_pcf()` - Sync from Pico W's RTC to Inky's RTC (sometime useful since Thonny sets the Pico W RTC automatically)
|
||||
* `inky_frame.sleep_for(minutes)` - Set the RTC alarm for a number of minutes and cut the power. This will completely power off the Pico W, but leave the Inky RTC running to wake it back up.
|
||||
|
||||
:info: You can access all Inky RTC (PCF85063A) functions via `inky_frame.rtc`
|
||||
|
||||
#### Other
|
||||
|
||||
* `inky_frame.turn_off()` - Cut the power to the Pico W (on battery only), only an alarm event or a button press can wake it back up.
|
||||
|
||||
## RAM Usage
|
||||
|
||||
Both Inky 4.0" and 5.7" use only the Pico's onboard RAM. It's quite cozy. The frame buffers are 3-bits-per-pixel, making Inky 4.0"
|
||||
|
||||
On 7.3" we had to add a PSRAM chip to act as the display's framebuffer. Right now it acts exclusively as a framebuffer, but that frees up some Pico RAM to work with so you can do more with that 7.3" panel.
|
||||
|
||||
The Inky frame buffer sizes are as follows:
|
||||
|
||||
* 4.0" - 640x400 - 96,000 bytes.
|
||||
* 5.7" - 600x448 - 100,800 bytes.
|
||||
* 7.3" - 800x480 - 144,000 bytes.
|
||||
|
||||
Since MicroPython on a Pico W has only 166k that would have left *just* 22k on 7.3", instead you get (almost) all 166k to play with since the PicoGraphics instance itself uses only 8544 bytes!
|
||||
|
||||
### Accessing The Framebuffer
|
||||
|
||||
PicoGraphics has undocumented support for accessing its raw framebuffer using `memoryview(graphics)`.
|
||||
|
||||
This is useful for copying raw binary images (effectively valid Inky frame buffers saved to a file) avoiding JPEG compression and so forth. For some dicussion about why and how you might do this, see: https://github.com/pimoroni/pimoroni-pico/issues/681
|
||||
|
||||
:warning: This *does not work* for Inky 7.3, since there is no framebuffer in memory. PicoGraphics will raise a `ValueError: No local framebuffer.` if you try. We aim to fix this with some hardfault handling sorcery.
|
||||
Moved to: [https://github.com/pimoroni/inky-frame/blob/main/docs/inky_frame.md](https://github.com/pimoroni/inky-frame/blob/main/docs/inky_frame.md)
|
|
@ -1,181 +0,0 @@
|
|||
from pimoroni import ShiftRegister, PWMLED
|
||||
from machine import Pin, I2C, RTC
|
||||
from wakeup import get_shift_state, reset_shift_state
|
||||
from micropython import const
|
||||
import pcf85063a
|
||||
import ntptime
|
||||
import time
|
||||
|
||||
BLACK = const(0)
|
||||
WHITE = const(1)
|
||||
|
||||
GREEN = const(2)
|
||||
BLUE = const(3)
|
||||
RED = const(4)
|
||||
YELLOW = const(5)
|
||||
ORANGE = const(6)
|
||||
TAUPE = const(7)
|
||||
|
||||
SR_CLOCK = const(8)
|
||||
SR_LATCH = const(9)
|
||||
SR_OUT = const(10)
|
||||
|
||||
LED_A = const(11)
|
||||
LED_B = const(12)
|
||||
LED_C = const(13)
|
||||
LED_D = const(14)
|
||||
LED_E = const(15)
|
||||
|
||||
LED_BUSY = const(6)
|
||||
LED_WIFI = const(7)
|
||||
|
||||
HOLD_VSYS_EN = const(2)
|
||||
|
||||
RTC_ALARM = const(2)
|
||||
EXTERNAL_TRIGGER = const(1)
|
||||
EINK_BUSY = const(0)
|
||||
|
||||
SHIFT_STATE = get_shift_state()
|
||||
|
||||
reset_shift_state()
|
||||
|
||||
i2c = I2C(0)
|
||||
rtc = pcf85063a.PCF85063A(i2c)
|
||||
i2c.writeto_mem(0x51, 0x00, b'\x00') # ensure rtc is running (this should be default?)
|
||||
rtc.enable_timer_interrupt(False)
|
||||
|
||||
vsys = Pin(HOLD_VSYS_EN)
|
||||
vsys.on()
|
||||
|
||||
|
||||
def woken_by_rtc():
|
||||
mask = (1 << RTC_ALARM)
|
||||
return bool(sr.read() & mask) or bool(SHIFT_STATE & mask)
|
||||
|
||||
|
||||
def woken_by_ext_trigger():
|
||||
mask = (1 << EXTERNAL_TRIGGER)
|
||||
return bool(sr.read() & mask) or bool(SHIFT_STATE & mask)
|
||||
|
||||
|
||||
def woken_by_button():
|
||||
return bool(sr.read() & 0b11111000) or bool(SHIFT_STATE & 0b11111000)
|
||||
|
||||
|
||||
def pico_rtc_to_pcf():
|
||||
# Set the PCF85063A to the time stored by Pico W's RTC
|
||||
year, month, day, dow, hour, minute, second, _ = RTC().datetime()
|
||||
rtc.datetime((year, month, day, hour, minute, second, dow))
|
||||
|
||||
|
||||
def pcf_to_pico_rtc():
|
||||
# Set Pico W's RTC to the time stored by the PCF85063A
|
||||
t = rtc.datetime()
|
||||
# BUG ERRNO 22, EINVAL, when date read from RTC is invalid for the Pico's RTC.
|
||||
try:
|
||||
RTC().datetime((t[0], t[1], t[2], t[6], t[3], t[4], t[5], 0))
|
||||
return True
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
|
||||
def sleep_for(minutes):
|
||||
year, month, day, hour, minute, second, dow = rtc.datetime()
|
||||
|
||||
# if the time is very close to the end of the minute, advance to the next minute
|
||||
# this aims to fix the edge case where the board goes to sleep right as the RTC triggers, thus never waking up
|
||||
if second >= 55:
|
||||
minute += 1
|
||||
|
||||
# Can't sleep beyond a month, so clamp the sleep to a 28 day maximum
|
||||
minutes = min(minutes, 40320)
|
||||
|
||||
# Calculate the future alarm date; first, turn the current time into seconds since epoch
|
||||
sec_since_epoch = time.mktime((year, month, day, hour, minute, second, dow, 0))
|
||||
|
||||
# Add the required minutes to this
|
||||
sec_since_epoch += minutes * 60
|
||||
|
||||
# And convert it back into a more useful tuple
|
||||
(ayear, amonth, aday, ahour, aminute, asecond, adow, adoy) = time.localtime(sec_since_epoch)
|
||||
|
||||
# And now set the alarm as before, now including the day
|
||||
rtc.clear_alarm_flag()
|
||||
rtc.set_alarm(0, aminute, ahour, aday)
|
||||
rtc.enable_alarm_interrupt(True)
|
||||
|
||||
turn_off()
|
||||
|
||||
# Simulate sleep while on USB power
|
||||
while minutes > 0:
|
||||
time.sleep(60)
|
||||
minutes -= 1
|
||||
|
||||
|
||||
def turn_off():
|
||||
time.sleep(0.1)
|
||||
vsys.off()
|
||||
|
||||
|
||||
def set_time():
|
||||
# Set both Pico W's RTC and PCF85063A to NTP time
|
||||
ntptime.settime()
|
||||
pico_rtc_to_pcf()
|
||||
|
||||
|
||||
class Button:
|
||||
def __init__(self, sr, idx, led, debounce=50):
|
||||
self.sr = sr
|
||||
self.startup_state = bool(SHIFT_STATE & (1 << idx))
|
||||
self.led = PWMLED(led)
|
||||
self.led.off()
|
||||
self._idx = idx
|
||||
self._debounce_time = debounce
|
||||
self._changed = time.ticks_ms()
|
||||
self._last_value = None
|
||||
|
||||
def led_on(self):
|
||||
self.led.on()
|
||||
|
||||
def led_off(self):
|
||||
self.led.off()
|
||||
|
||||
def led_brightness(self, brightness):
|
||||
self.led.brightness(brightness)
|
||||
|
||||
def led_toggle(self):
|
||||
self.led.toggle()
|
||||
|
||||
def read(self):
|
||||
if self.startup_state:
|
||||
self.startup_state = False
|
||||
return True
|
||||
|
||||
value = self.raw()
|
||||
if value != self._last_value and time.ticks_ms() - self._changed > self._debounce_time:
|
||||
self._last_value = value
|
||||
self._changed = time.ticks_ms()
|
||||
return value
|
||||
return False
|
||||
|
||||
def raw(self):
|
||||
if self.startup_state:
|
||||
self.startup_state = False
|
||||
return True
|
||||
return self.sr[self._idx] == 1
|
||||
|
||||
@property
|
||||
def is_pressed(self):
|
||||
return self.raw()
|
||||
|
||||
|
||||
sr = ShiftRegister(SR_CLOCK, SR_LATCH, SR_OUT)
|
||||
|
||||
button_a = Button(sr, 7, LED_A)
|
||||
button_b = Button(sr, 6, LED_B)
|
||||
button_c = Button(sr, 5, LED_C)
|
||||
button_d = Button(sr, 4, LED_D)
|
||||
button_e = Button(sr, 3, LED_E)
|
||||
|
||||
led_busy = PWMLED(LED_BUSY)
|
||||
led_wifi = PWMLED(LED_WIFI)
|
|
@ -26,7 +26,7 @@ On the releases page you'll find a bunch of different .uf2 files for use on diff
|
|||
| Tiny 2040 8MB | **tiny2040_8mb-vx.x.x-pimoroni-micropython.uf2** | Includes support for the increased flash memory on this board |
|
||||
| Enviro Urban, Indoor, Weather & Grow | **enviro-vx.x.x-pimoroni-micropython.uf2** | For a .uf2 with examples built in, go to the [Enviro releases page](https://github.com/pimoroni/enviro/releases)! |
|
||||
| Galactic Unicorn | **galactic_unicorn-vx.x.x-pimoroni-micropython.uf2** | |
|
||||
| Inky Frame | **inky_frame-vx.x.x-pimoroni-micropython.uf2** | |
|
||||
| Inky Frame | | See: [https://github.com/pimoroni/inky-frame/releases/latest](https://github.com/pimoroni/inky-frame/releases/latest) |
|
||||
| Badger 2040 W | **badger2040w-vx.x.x-pimoroni-micropython.uf2** or **pimoroni-badger2040w-v0.0.1-micropython-with-badger-os.uf2** | :warning: Badger OS will overwrite your files!
|
||||
| Badger 2040 | **badger2040-vx.x.x-pimoroni-micropython.uf2** or **pimoroni-badger2040-v0.0.1-micropython-with-badger-os.uf2** | :warning: Badger OS will overwrite your files!
|
||||
| Cosmic Unicorn | **cosmic_unicorn-vx.x.x-pimoroni-micropython.uf2** | |
|
||||
|
|