From 67152e32e5933a3fb861cdc825df19dc5e40f8ad Mon Sep 17 00:00:00 2001 From: ZodiusInfuser Date: Tue, 30 May 2023 12:42:27 +0100 Subject: [PATCH] Copied Cosmic MPy examples to Stellar --- .../cosmic_unicorn/feature_test_with_audio.py | 4 +- .../examples/stellar_unicorn/README.md | 178 ++ .../stellar_unicorn/cheerlights_history.py | 128 ++ micropython/examples/stellar_unicorn/clock.py | 229 +++ .../eighties_super_computer.py | 79 + .../stellar_unicorn/exchange_ticker.py | 148 ++ .../examples/stellar_unicorn/feature_test.py | 123 ++ .../feature_test_with_audio.py | 347 ++++ .../examples/stellar_unicorn/fire_effect.py | 102 ++ .../stellar_unicorn/http_text/html_text.py | 190 ++ .../stellar_unicorn/http_text/index.html | 102 ++ .../examples/stellar_unicorn/launch/fire.py | 80 + .../examples/stellar_unicorn/launch/main.py | 113 ++ .../stellar_unicorn/launch/rainbow.py | 59 + .../stellar_unicorn/launch/supercomputer.py | 40 + .../examples/stellar_unicorn/launch/today.py | 100 ++ .../examples/stellar_unicorn/lava_lamp.py | 151 ++ .../examples/stellar_unicorn/melody_maker.py | 131 ++ .../stellar_unicorn/nostalgia_prompt.py | 158 ++ .../numpy/eighties_super_computer.py | 115 ++ .../stellar_unicorn/numpy/fire_effect.py | 126 ++ .../stellar_unicorn/numpy/lava_lamp.py | 118 ++ .../examples/stellar_unicorn/numpy/life.py | 145 ++ .../stellar_unicorn/numpy/rgb_channels.py | 95 + .../stellar_unicorn/numpy/the_matrix.py | 89 + .../stellar_unicorn/numpy/this_is_fine.py | 131 ++ .../examples/stellar_unicorn/numpy/trippy.py | 97 + .../examples/stellar_unicorn/rainbow.py | 115 ++ .../stellar_unicorn/scrolling_text.py | 96 + .../stellar_unicorn/stellar_paint/README.md | 11 + .../stellar_paint/stellar_paint.py | 113 ++ .../stellar_paint/stellar_paint/index.html | 54 + .../stellar_paint/static/paint.css | 131 ++ .../stellar_paint/static/paint.js | 216 +++ .../stellar_paint/static/tinycolor.js | 1193 +++++++++++++ micropython/examples/stellar_unicorn/today.py | 128 ++ .../stellar_unicorn/weather/icons/cloud1.jpg | Bin 0 -> 1151 bytes .../stellar_unicorn/weather/icons/cloud2.jpg | Bin 0 -> 1579 bytes .../stellar_unicorn/weather/icons/cloud3.jpg | Bin 0 -> 1656 bytes .../stellar_unicorn/weather/icons/cloud4.jpg | Bin 0 -> 1656 bytes .../stellar_unicorn/weather/icons/rain1.jpg | Bin 0 -> 2053 bytes .../stellar_unicorn/weather/icons/rain2.jpg | Bin 0 -> 2100 bytes .../stellar_unicorn/weather/icons/rain3.jpg | Bin 0 -> 2091 bytes .../stellar_unicorn/weather/icons/rain4.jpg | Bin 0 -> 2053 bytes .../stellar_unicorn/weather/icons/snow1.jpg | Bin 0 -> 2128 bytes .../stellar_unicorn/weather/icons/snow2.jpg | Bin 0 -> 2196 bytes .../stellar_unicorn/weather/icons/snow3.jpg | Bin 0 -> 2223 bytes .../stellar_unicorn/weather/icons/snow4.jpg | Bin 0 -> 2169 bytes .../stellar_unicorn/weather/icons/storm1.jpg | Bin 0 -> 1360 bytes .../stellar_unicorn/weather/icons/storm2.jpg | Bin 0 -> 1692 bytes .../stellar_unicorn/weather/icons/storm3.jpg | Bin 0 -> 1835 bytes .../stellar_unicorn/weather/icons/storm4.jpg | Bin 0 -> 1703 bytes .../stellar_unicorn/weather/icons/sun1.jpg | Bin 0 -> 2273 bytes .../stellar_unicorn/weather/icons/sun2.jpg | Bin 0 -> 2193 bytes .../stellar_unicorn/weather/icons/sun3.jpg | Bin 0 -> 2269 bytes .../stellar_unicorn/weather/icons/sun4.jpg | Bin 0 -> 2194 bytes ...icons_sourcefile(doesn't need copying).svg | 1577 +++++++++++++++++ .../stellar_unicorn/weather/weather.py | 153 ++ 58 files changed, 7163 insertions(+), 2 deletions(-) create mode 100644 micropython/examples/stellar_unicorn/README.md create mode 100644 micropython/examples/stellar_unicorn/cheerlights_history.py create mode 100644 micropython/examples/stellar_unicorn/clock.py create mode 100644 micropython/examples/stellar_unicorn/eighties_super_computer.py create mode 100644 micropython/examples/stellar_unicorn/exchange_ticker.py create mode 100644 micropython/examples/stellar_unicorn/feature_test.py create mode 100644 micropython/examples/stellar_unicorn/feature_test_with_audio.py create mode 100644 micropython/examples/stellar_unicorn/fire_effect.py create mode 100644 micropython/examples/stellar_unicorn/http_text/html_text.py create mode 100644 micropython/examples/stellar_unicorn/http_text/index.html create mode 100644 micropython/examples/stellar_unicorn/launch/fire.py create mode 100644 micropython/examples/stellar_unicorn/launch/main.py create mode 100644 micropython/examples/stellar_unicorn/launch/rainbow.py create mode 100644 micropython/examples/stellar_unicorn/launch/supercomputer.py create mode 100644 micropython/examples/stellar_unicorn/launch/today.py create mode 100644 micropython/examples/stellar_unicorn/lava_lamp.py create mode 100644 micropython/examples/stellar_unicorn/melody_maker.py create mode 100644 micropython/examples/stellar_unicorn/nostalgia_prompt.py create mode 100644 micropython/examples/stellar_unicorn/numpy/eighties_super_computer.py create mode 100644 micropython/examples/stellar_unicorn/numpy/fire_effect.py create mode 100644 micropython/examples/stellar_unicorn/numpy/lava_lamp.py create mode 100644 micropython/examples/stellar_unicorn/numpy/life.py create mode 100644 micropython/examples/stellar_unicorn/numpy/rgb_channels.py create mode 100644 micropython/examples/stellar_unicorn/numpy/the_matrix.py create mode 100644 micropython/examples/stellar_unicorn/numpy/this_is_fine.py create mode 100644 micropython/examples/stellar_unicorn/numpy/trippy.py create mode 100644 micropython/examples/stellar_unicorn/rainbow.py create mode 100644 micropython/examples/stellar_unicorn/scrolling_text.py create mode 100644 micropython/examples/stellar_unicorn/stellar_paint/README.md create mode 100644 micropython/examples/stellar_unicorn/stellar_paint/stellar_paint.py create mode 100644 micropython/examples/stellar_unicorn/stellar_paint/stellar_paint/index.html create mode 100644 micropython/examples/stellar_unicorn/stellar_paint/stellar_paint/static/paint.css create mode 100644 micropython/examples/stellar_unicorn/stellar_paint/stellar_paint/static/paint.js create mode 100644 micropython/examples/stellar_unicorn/stellar_paint/stellar_paint/static/tinycolor.js create mode 100644 micropython/examples/stellar_unicorn/today.py create mode 100644 micropython/examples/stellar_unicorn/weather/icons/cloud1.jpg create mode 100644 micropython/examples/stellar_unicorn/weather/icons/cloud2.jpg create mode 100644 micropython/examples/stellar_unicorn/weather/icons/cloud3.jpg create mode 100644 micropython/examples/stellar_unicorn/weather/icons/cloud4.jpg create mode 100644 micropython/examples/stellar_unicorn/weather/icons/rain1.jpg create mode 100644 micropython/examples/stellar_unicorn/weather/icons/rain2.jpg create mode 100644 micropython/examples/stellar_unicorn/weather/icons/rain3.jpg create mode 100644 micropython/examples/stellar_unicorn/weather/icons/rain4.jpg create mode 100644 micropython/examples/stellar_unicorn/weather/icons/snow1.jpg create mode 100644 micropython/examples/stellar_unicorn/weather/icons/snow2.jpg create mode 100644 micropython/examples/stellar_unicorn/weather/icons/snow3.jpg create mode 100644 micropython/examples/stellar_unicorn/weather/icons/snow4.jpg create mode 100644 micropython/examples/stellar_unicorn/weather/icons/storm1.jpg create mode 100644 micropython/examples/stellar_unicorn/weather/icons/storm2.jpg create mode 100644 micropython/examples/stellar_unicorn/weather/icons/storm3.jpg create mode 100644 micropython/examples/stellar_unicorn/weather/icons/storm4.jpg create mode 100644 micropython/examples/stellar_unicorn/weather/icons/sun1.jpg create mode 100644 micropython/examples/stellar_unicorn/weather/icons/sun2.jpg create mode 100644 micropython/examples/stellar_unicorn/weather/icons/sun3.jpg create mode 100644 micropython/examples/stellar_unicorn/weather/icons/sun4.jpg create mode 100644 micropython/examples/stellar_unicorn/weather/icons_sourcefile(doesn't need copying).svg create mode 100644 micropython/examples/stellar_unicorn/weather/weather.py diff --git a/micropython/examples/cosmic_unicorn/feature_test_with_audio.py b/micropython/examples/cosmic_unicorn/feature_test_with_audio.py index fdb89c6f..70fb2c1a 100644 --- a/micropython/examples/cosmic_unicorn/feature_test_with_audio.py +++ b/micropython/examples/cosmic_unicorn/feature_test_with_audio.py @@ -152,8 +152,6 @@ tone_b = 0 # The current synth beat beat = 0 -text = "" - def next_beat(): global beat @@ -312,6 +310,8 @@ while True: # print("white gradient") gradient(255, 255, 255) + text = "" + if cu.is_pressed(CosmicUnicorn.SWITCH_A): text = "PlaySyn" diff --git a/micropython/examples/stellar_unicorn/README.md b/micropython/examples/stellar_unicorn/README.md new file mode 100644 index 00000000..68cc1277 --- /dev/null +++ b/micropython/examples/stellar_unicorn/README.md @@ -0,0 +1,178 @@ +# Stellar Unicorn MicroPython Examples + +- [About Stellar Unicorn](#about-stellar-unicorn) +- [Stellar Unicorn and PicoGraphics](#stellar-unicorn-and-picographics) +- [Examples](#examples) + - [Clock](#clock) + - [Eighties Super Computer](#eighties-super-computer) + - [Feature Test](#feature-test) + - [Feature Test With Audio](#feature-test-with-audio) + - [Fire Effect](#fire-effect) + - [Lava Lamp](#lava-lamp) + - [Nostalgia Prompt](#nostalgia-prompt) + - [Rainbow](#rainbow) + - [Scrolling Text](#scrolling-text) + - [Today](#today) +- [Wireless Examples](#wireless-examples) + - [Cheerlights History](#cheerlights-history) + - [Stellar Paint](#stellar-paint) + - [Exchange Ticker](#exchange-ticker) + - [HTTP Text](#http-text) + - [Weather](#weather) +- [NumPy examples](#numpy-examples) +- [Other Examples](#other-examples) + - [Launch (Demo Reel)](#launch-demo-reel) +- [Other Resources](#other-resources) + +## About Stellar Unicorn + +Stellar Unicorn offers 16x16 bright RGB LEDs driven by Pico W's PIO in addition to a 1W amplifier + speaker, a collection of system and user buttons, and two Qw/ST connectors for adding external sensors and devices. Woha! + +- :link: [Stellar Unicorn store page](https://shop.pimoroni.com/products/stellar-unicorn) + +Stellar Unicorn ships with MicroPython firmware pre-loaded, but you can download the most recent version at the link below (you'll want the `stellar-unicorn` image). + +- [MicroPython releases](https://github.com/pimoroni/pimoroni-pico/releases) +- [Installing MicroPython](../../../setting-up-micropython.md) + +## Stellar Unicorn and PicoGraphics + +The easiest way to start displaying cool stuff on Stellar Unicorn is using our Stellar Unicorn module (which contains a bunch of helpful functions for interacting with the buttons, adjusting brightness and suchlike) and our PicoGraphics library, which is chock full of useful functions for drawing on the LED matrix. + +- [Stellar Unicorn function reference](../../modules/stellar_unicorn/README.md) +- [PicoGraphics function reference](../../modules/picographics/README.md) + +## Examples + +### Clock + +[clock.py](clock.py) + +Clock example with (optional) NTP synchronization. You can adjust the brightness with LUX + and -, and resync the time by pressing A. + +### Eighties Super Computer + +[eighties_super_computer.py](eighties_super_computer.py) + +Random LEDs blink on and off mimicing the look of a movie super computer doing its work in the eighties. You can adjust the brightness with LUX + and -. + +### Feature Test + +[feature_test.py](feature_test.py) + +Displays some text, gradients and colours and demonstrates button use. You can adjust the brightness with LUX + and -. + +### Feature Test With Audio + +[feature_test_with_audio.py](feature_test_with_audio.py) + +Displays some text, gradients and colours and demonstrates button use. Also demonstrates some of the audio / synth features. +- Button A plays a synth tune +- Button B plays a solo channel of the synth tune +- Button C plays a sinewave (it's frequency can be adjusted with VOL + and -) +- Button D plays a second sinewave (it's frequency can be adjusted with LUX + and -) +- Sleep button stops the sounds + +### Fire Effect + +[fire_effect.py](fire_effect.py) + +A pretty, procedural fire effect. Switch between landscape fire and vertical fire using the A and B buttons! You can adjust the brightness with LUX + and -. + +### Lava Lamp + +[lava_lamp.py](lava_lamp.py) + +A 70s-tastic, procedural rainbow lava lamp. You can adjust the brightness with LUX + and -. + +### Nostalgia Prompt + +[nostalgia_prompt.py](nostalgia_prompt.py) + +A collection of copies of classic terminal styles including C64, MS-DOS, Spectrum, and more. Images and text are drawn pixel by pixel from a pattern of Os and Xs. You can adjust the brightness with LUX + and -. + +### Rainbow + +[rainbow.py](rainbow.py) + +Some good old fashioned rainbows! You can adjust the cycling speed with A and B, stripe width with C and D, hue with VOL + and -, and the brightness with LUX + and -. The sleep button stops the animation (can be started again with A or B). + +### Scrolling Text + +[scrolling_text.py](scrolling_text.py) + +Display scrolling wisdom, quotes or greetz. You can adjust the brightness with LUX + and -. + +### Today + +[today.py](today.py) + +Calendar example with (optional) NTP synchronization. You can adjust the brightness with LUX + and -, and resync the date by pressing C. + +## Wireless Examples + +These examples need `WIFI_CONFIG.py` and `network_manager.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). + +- [micropython/examples/common](../../examples/common) + +### Cheerlights History + +[cheerlights_history.py](cheerlights_history.py) + +Updates one pixel every two minutes to display the most recent #Cheerlights colour. Discover the most popular colours over time, or use it as an avant garde (but colourful) 16 hour clock! Find out more about the Cheerlights API at https://cheerlights.com/ + +You can adjust the brightness with LUX + and -. + +### Stellar Paint + +[stellar_paint](stellar_paint) + +Draw on your Stellar Unicorn from another device in real time, over wifi! + +This example needs the `micropython-phew` and `microdot` libraries (you can install these using Thonny's 'Tools > Manage Packages'). + +### Exchange Ticker + +[exchange_ticker.py](exchange_ticker.py) + +This example uses the Coinbase open API to collect the current exchange rates of various cryptocurrencies. + +Press A to change to a different base exchange currency. + +### HTTP Text + +[http_text](http_text) + +Display scrolling wisdom, quotes or greetz... from another computer or device! + +You can adjust the brightness with LUX + and -. + +Requires `logging.mpy` and `tinyweb` from [micropython/examples/common](../../examples/common) - copy these into the `lib` folder on your Pico W. You'll also need `index.html` to be saved alongside `html_text.py`. + +### Weather + +[weather](weather) + +Display current weather data from the [Open-Meteo](https://open-meteo.com/) weather API. + +## NumPy examples + +[numpy](numpy) + +The examples in the folder use `numpy`-like array functions contained in the `ulab` library for super fast graphical effects. + +## Other Examples + +### Launch (Demo Reel) + +[launch](launch) + +If you want to get the demo reel that Stellar Unicorn ships with back, copy the contents of this `launch` folder to your Pico W. + +## Other Resources + +Here are some cool Stellar Unicorn community projects and resources that you might find useful / inspirational! Note that code at the links below has not been tested by us and we're not able to offer support with it. + +- :link: [Green Energy Display with Stellar Unicorn](https://www.hackster.io/andreas-motzek/clock-and-green-energy-display-with-stellar-unicorn-641dcb) +- :link: [stellar-emoji-react - paint emojis from a computer, phone or tablet](https://github.com/chriscareycode/stellar-unicorn/tree/main/stellar-emoji-react) +- :link: [stellar-paste - paste images from the clipboard to Stellar Unicorn](https://github.com/chriscareycode/stellar-unicorn/tree/main/stellar-paste) diff --git a/micropython/examples/stellar_unicorn/cheerlights_history.py b/micropython/examples/stellar_unicorn/cheerlights_history.py new file mode 100644 index 00000000..daf8c628 --- /dev/null +++ b/micropython/examples/stellar_unicorn/cheerlights_history.py @@ -0,0 +1,128 @@ +# This Stellar Unicorn example updates a pixel every two(ish) minutes +# to display the most recent #cheerlights colour. Discover the most popular +# colours over time, or use it as an avant garde (but colourful) 16 hour clock! +# Find out more about the Cheerlights API at https://cheerlights.com/ +# +# To run this example you'll need WIFI_CONFIG.py and network_manager.py from +# the pimoroni-pico micropython/examples/common folder + +import WIFI_CONFIG +from network_manager import NetworkManager +import uasyncio +import urequests +import time +from machine import Timer, Pin +from stellar import StellarUnicorn +from picographics import PicoGraphics, DISPLAY_STELLAR_UNICORN as DISPLAY + +URL = 'http://api.thingspeak.com/channels/1417/field/2/last.json' + +UPDATE_INTERVAL = 113 # refresh interval in secs. Be nice to free APIs! +# this esoteric number is used so that a column of LEDs equates (approximately) to an hour + + +def status_handler(mode, status, ip): + # reports wifi connection status + print(mode, status, ip) + print('Connecting to wifi...') + if status is not None: + if status: + print('Wifi connection successful!') + else: + print('Wifi connection failed!') + + +def hex_to_rgb(hex): + # converts a hex colour code into RGB + h = hex.lstrip('#') + r, g, b = (int(h[i:i + 2], 16) for i in (0, 2, 4)) + return r, g, b + + +def get_data(): + # open the json file + print(f'Requesting URL: {URL}') + r = urequests.get(URL) + # open the json data + j = r.json() + print('Data obtained!') + r.close() + + # flash the onboard LED after getting data + pico_led.value(True) + time.sleep(0.2) + pico_led.value(False) + + # extract hex colour from the json data + hex = j['field2'] + + # add the new hex colour to the end of the array + colour_array.append(hex) + print(f'Colour added to array: {hex}') + # remove the oldest colour in the array + colour_array.pop(0) + update_leds() + + +def update_leds(): + # light up the LEDs + # this step takes a second, it's doing a lot of hex_to_rgb calculations! + print("Updating LEDs...") + i = 0 + for x in range(width): + for y in range(height): + r, g, b = hex_to_rgb(colour_array[i]) + + current_colour = graphics.create_pen(r, g, b) + graphics.set_pen(current_colour) + graphics.pixel(x, y) + i = i + 1 + su.update(graphics) + print("LEDs updated!") + + +su = StellarUnicorn() +graphics = PicoGraphics(DISPLAY) + +width = StellarUnicorn.WIDTH +height = StellarUnicorn.HEIGHT + +su.set_brightness(0.5) + +# set up the Pico W's onboard LED +pico_led = Pin('LED', Pin.OUT) + +current_colour = graphics.create_pen(0, 0, 0) + +# set up an list to store the colours +colour_array = ["#000000"] * 1024 + +# set up wifi +try: + 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)) +except Exception as e: + print(f'Wifi connection failed! {e}') + +# get the first lot of data +get_data() + +# start timer (the timer will call the function to update our data every UPDATE_INTERVAL) +timer = Timer(-1) +timer.init(period=UPDATE_INTERVAL * 1000, mode=Timer.PERIODIC, callback=lambda t: get_data()) + +while True: + # adjust brightness with LUX + and - + # LEDs take a couple of secs to update, so adjust in big (10%) steps + if su.is_pressed(StellarUnicorn.SWITCH_BRIGHTNESS_UP): + su.adjust_brightness(+0.1) + update_leds() + print(f"Brightness set to {su.get_brightness()}") + + if su.is_pressed(StellarUnicorn.SWITCH_BRIGHTNESS_DOWN): + su.adjust_brightness(-0.1) + update_leds() + print(f"Brightness set to {su.get_brightness()}") + + # pause for a moment (important or the USB serial device will fail) + time.sleep(0.001) diff --git a/micropython/examples/stellar_unicorn/clock.py b/micropython/examples/stellar_unicorn/clock.py new file mode 100644 index 00000000..20339f1d --- /dev/null +++ b/micropython/examples/stellar_unicorn/clock.py @@ -0,0 +1,229 @@ +# Clock example with NTP synchronization +# +# Create a secrets.py with your Wifi details to be able to get the time +# when the Stellar Unicorn isn't connected to Thonny. +# +# secrets.py should contain: +# WIFI_SSID = "Your WiFi SSID" +# WIFI_PASSWORD = "Your WiFi password" +# +# Clock synchronizes time on start, and resynchronizes if you press the A button + +import time +import math +import machine +import network +import ntptime +from stellar import StellarUnicorn +from picographics import PicoGraphics, DISPLAY_STELLAR_UNICORN as DISPLAY + +try: + from secrets import WIFI_SSID, WIFI_PASSWORD + wifi_available = True +except ImportError: + print("Create secrets.py with your WiFi credentials to get time from NTP") + wifi_available = False + + +# constants for controlling the background colour throughout the day +MIDDAY_HUE = 1.1 +MIDNIGHT_HUE = 0.8 +HUE_OFFSET = -0.1 + +MIDDAY_SATURATION = 1.0 +MIDNIGHT_SATURATION = 1.0 + +MIDDAY_VALUE = 0.8 +MIDNIGHT_VALUE = 0.3 + + +# create stellar object and graphics surface for drawing +su = StellarUnicorn() +graphics = PicoGraphics(DISPLAY) + +# create the rtc object +rtc = machine.RTC() + +width = StellarUnicorn.WIDTH +height = StellarUnicorn.HEIGHT + +# set up some pens to use later +WHITE = graphics.create_pen(255, 255, 255) +BLACK = graphics.create_pen(0, 0, 0) + + +@micropython.native # noqa: F821 +def from_hsv(h, s, v): + i = math.floor(h * 6.0) + f = h * 6.0 - i + v *= 255.0 + p = v * (1.0 - s) + q = v * (1.0 - f * s) + t = v * (1.0 - (1.0 - f) * s) + + i = int(i) % 6 + if i == 0: + return int(v), int(t), int(p) + if i == 1: + return int(q), int(v), int(p) + if i == 2: + return int(p), int(v), int(t) + if i == 3: + return int(p), int(q), int(v) + if i == 4: + return int(t), int(p), int(v) + if i == 5: + return int(v), int(p), int(q) + + +# function for drawing a gradient background +def gradient_background(start_hue, start_sat, start_val, end_hue, end_sat, end_val): + half_width = width // 2 + for x in range(0, half_width): + hue = ((end_hue - start_hue) * (x / half_width)) + start_hue + sat = ((end_sat - start_sat) * (x / half_width)) + start_sat + val = ((end_val - start_val) * (x / half_width)) + start_val + colour = from_hsv(hue, sat, val) + graphics.set_pen(graphics.create_pen(int(colour[0]), int(colour[1]), int(colour[2]))) + for y in range(0, height): + graphics.pixel(x, y) + graphics.pixel(width - x - 1, y) + + colour = from_hsv(end_hue, end_sat, end_val) + graphics.set_pen(graphics.create_pen(int(colour[0]), int(colour[1]), int(colour[2]))) + for y in range(0, height): + graphics.pixel(half_width, y) + + +# function for drawing outlined text +def outline_text(text, x, y): + graphics.set_pen(BLACK) + graphics.text(text, x - 1, y - 1, -1, 1) + graphics.text(text, x, y - 1, -1, 1) + graphics.text(text, x + 1, y - 1, -1, 1) + graphics.text(text, x - 1, y, -1, 1) + graphics.text(text, x + 1, y, -1, 1) + graphics.text(text, x - 1, y + 1, -1, 1) + graphics.text(text, x, y + 1, -1, 1) + graphics.text(text, x + 1, y + 1, -1, 1) + + graphics.set_pen(WHITE) + graphics.text(text, x, y, -1, 1) + + +# Connect to wifi and synchronize the RTC time from NTP +def sync_time(): + if not wifi_available: + return + + # Start connection + wlan = network.WLAN(network.STA_IF) + wlan.active(True) + wlan.connect(WIFI_SSID, WIFI_PASSWORD) + + # Wait for connect success or failure + max_wait = 100 + while max_wait > 0: + if wlan.status() < 0 or wlan.status() >= 3: + break + max_wait -= 1 + print('waiting for connection...') + time.sleep(0.2) + + redraw_display_if_reqd() + su.update(graphics) + + if max_wait > 0: + print("Connected") + + try: + ntptime.settime() + print("Time set") + except OSError: + pass + + wlan.disconnect() + wlan.active(False) + + +# NTP synchronizes the time to UTC, this allows you to adjust the displayed time +# by one hour increments from UTC by pressing the volume up/down buttons +# +# We use the IRQ method to detect the button presses to avoid incrementing/decrementing +# multiple times when the button is held. +utc_offset = 0 + +up_button = machine.Pin(StellarUnicorn.SWITCH_VOLUME_UP, machine.Pin.IN, machine.Pin.PULL_UP) +down_button = machine.Pin(StellarUnicorn.SWITCH_VOLUME_DOWN, machine.Pin.IN, machine.Pin.PULL_UP) + + +def adjust_utc_offset(pin): + global utc_offset + if pin == up_button: + utc_offset += 1 + if pin == down_button: + utc_offset -= 1 + + +up_button.irq(trigger=machine.Pin.IRQ_FALLING, handler=adjust_utc_offset) +down_button.irq(trigger=machine.Pin.IRQ_FALLING, handler=adjust_utc_offset) + + +year, month, day, wd, hour, minute, second, _ = rtc.datetime() + +last_second = second + + +# Check whether the RTC time has changed and if so redraw the display +def redraw_display_if_reqd(): + global year, month, day, wd, hour, minute, second, last_second + + year, month, day, wd, hour, minute, second, _ = rtc.datetime() + if second != last_second: + hour = (hour + utc_offset) % 24 + time_through_day = (((hour * 60) + minute) * 60) + second + percent_through_day = time_through_day / 86400 + percent_to_midday = 1.0 - ((math.cos(percent_through_day * math.pi * 2) + 1) / 2) + print(percent_to_midday) + + hue = ((MIDDAY_HUE - MIDNIGHT_HUE) * percent_to_midday) + MIDNIGHT_HUE + sat = ((MIDDAY_SATURATION - MIDNIGHT_SATURATION) * percent_to_midday) + MIDNIGHT_SATURATION + val = ((MIDDAY_VALUE - MIDNIGHT_VALUE) * percent_to_midday) + MIDNIGHT_VALUE + + gradient_background(hue, sat, val, + hue + HUE_OFFSET, sat, val) + + clock = "{:02}:{:02}:{:02}".format(hour, minute, second) + + # calculate text position so that it is centred + w = graphics.measure_text(clock, 1) + x = int(width / 2 - w / 2 + 1) + y = 5 + + outline_text(clock, x, y) + + last_second = second + + +# set the font +graphics.set_font("bitmap6") +su.set_brightness(0.5) + +sync_time() + +while True: + if su.is_pressed(StellarUnicorn.SWITCH_BRIGHTNESS_UP): + su.adjust_brightness(+0.01) + + if su.is_pressed(StellarUnicorn.SWITCH_BRIGHTNESS_DOWN): + su.adjust_brightness(-0.01) + + if su.is_pressed(StellarUnicorn.SWITCH_A): + sync_time() + + redraw_display_if_reqd() + + # update the display + su.update(graphics) + + time.sleep(0.01) diff --git a/micropython/examples/stellar_unicorn/eighties_super_computer.py b/micropython/examples/stellar_unicorn/eighties_super_computer.py new file mode 100644 index 00000000..2036eef2 --- /dev/null +++ b/micropython/examples/stellar_unicorn/eighties_super_computer.py @@ -0,0 +1,79 @@ +import time +import random +from stellar import StellarUnicorn +from picographics import PicoGraphics, DISPLAY_STELLAR_UNICORN as DISPLAY + +''' +Random LEDs blink on and off mimicing the look of a movie +super computer doing its work in the eighties. + +You can adjust the brightness with LUX + and -. +''' + +su = StellarUnicorn() +graphics = PicoGraphics(DISPLAY) + +colour = (230, 150, 0) + + +@micropython.native # noqa: F821 +def setup(): + global width, height, lifetime, age + width = StellarUnicorn.WIDTH + height = StellarUnicorn.HEIGHT + lifetime = [[0.0 for y in range(height)] for x in range(width)] + age = [[0.0 for y in range(height)] for x in range(width)] + for y in range(height): + for x in range(width): + lifetime[x][y] = 1.0 + random.uniform(0.0, 0.1) + age[x][y] = random.uniform(0.0, 1.0) * lifetime[x][y] + + +@micropython.native # noqa: F821 +def draw(): + for y in range(height): + for x in range(width): + if age[x][y] < lifetime[x][y] * 0.3: + graphics.set_pen(graphics.create_pen(colour[0], colour[1], colour[2])) + elif age[x][y] < lifetime[x][y] * 0.5: + decay = (lifetime[x][y] * 0.5 - age[x][y]) * 5.0 + graphics.set_pen(graphics.create_pen(int(decay * colour[0]), int(decay * colour[1]), int(decay * colour[2]))) + else: + graphics.set_pen(0) + graphics.pixel(x, y) + + su.update(graphics) + + +@micropython.native # noqa: F821 +def update(): + for y in range(height): + for x in range(width): + if age[x][y] >= lifetime[x][y]: + age[x][y] = 0.0 + lifetime[x][y] = 1.0 + random.uniform(0.0, 0.1) + + age[x][y] += 0.025 + + +setup() + +su.set_brightness(0.5) + +while True: + + if su.is_pressed(StellarUnicorn.SWITCH_BRIGHTNESS_UP): + su.adjust_brightness(+0.01) + + if su.is_pressed(StellarUnicorn.SWITCH_BRIGHTNESS_DOWN): + su.adjust_brightness(-0.01) + + start = time.ticks_ms() + + draw() + update() + + # pause for a moment (important or the USB serial device will fail) + time.sleep(0.001) + + print("total took: {} ms".format(time.ticks_ms() - start)) diff --git a/micropython/examples/stellar_unicorn/exchange_ticker.py b/micropython/examples/stellar_unicorn/exchange_ticker.py new file mode 100644 index 00000000..7448baa1 --- /dev/null +++ b/micropython/examples/stellar_unicorn/exchange_ticker.py @@ -0,0 +1,148 @@ +""" +This example uses the Coinbase open API to collect the current exchange rates. +Use Switch A to change to a different base exchange currency. +""" + +import WIFI_CONFIG +from network_manager import NetworkManager +import uasyncio +import urequests +import time +import math +from stellar import StellarUnicorn +from picographics import PicoGraphics, DISPLAY_STELLAR_UNICORN as DISPLAY +import gc + +URL = 'https://api.coinbase.com/v2/exchange-rates?currency={0}' + +currencies = {"Bitcoin": "BTC", "Ethereun": "ETH", "Pound": "GBP", "Dollar": "USD", "Dogecoin": "DOGE"} +currency_keys = list(currencies.keys()) + +ref_currency_name = "" +currency_name = "" +currency_symbol = "" +currency_rate = "" +rate_keys = [] + +# diplay options +line_1_line = -2 +line_2_line = 9 +line_3_line = 20 + +ref_currency_index = 0 + +cycles_per_sequence = 120 + +su = StellarUnicorn() +graphics = PicoGraphics(DISPLAY) + + +# for Handling the wifi connection +def status_handler(mode, status, ip): + # reports wifi connection status + print(mode, status, ip) + print('Connecting to wifi...') + if status is not None: + if status: + print('Wifi connection successful!') + else: + print('Wifi connection failed!') + + +try: + 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)) +except Exception as e: + print(f'Wifi connection failed! {e}') + + +def get_data(currency_selected): + + graphics.set_pen(graphics.create_pen(20, 20, 20)) + graphics.clear() + graphics.set_pen(graphics.create_pen(100, 100, 100)) + graphics.text("Get", 0, 10, scale=1, spacing=1) + graphics.text("data", 8, 16, scale=1, spacing=1) + su.update(graphics) + gc.collect() + # open the json file + print('Requesting URL:') + print(URL.format(currencies[currency_selected])) + r = urequests.get(URL.format(currencies[currency_selected])) + gc.collect() + # open the json data + data_obj = r.json() + print('Data obtained!') + r.close() + return data_obj + + +def calculate_xpos(length, cycle): + cycle_phase = math.cos(math.pi * cycle / (cycles_per_sequence / 2)) + pos_x = int((-(length / 2) * 10) - (length / 2) * 10 * cycle_phase) + return pos_x + + +def update_display(cycle): + + graphics.set_pen(graphics.create_pen(20, 20, 20)) + graphics.clear() + graphics.set_pen(graphics.create_pen(100, 0, 0)) + graphics.text(ref_currency_name, calculate_xpos((len(ref_currency_name)), cycle), line_1_line, scale=2, spacing=1) + graphics.set_pen(graphics.create_pen(100, 100, 0)) + if len(currency_symbol) > 3: + graphics.text(currency_symbol, calculate_xpos((len(currency_symbol)), cycle), line_2_line, scale=2, spacing=1) + else: + graphics.text(currency_symbol, 0, line_2_line, scale=2, spacing=1) + graphics.set_pen(graphics.create_pen(0, 100, 100)) + graphics.text(currency_rate, calculate_xpos((len(currency_rate)), cycle), line_3_line, scale=2, spacing=1) + + +def update_base_currency(index): + fetched_data = 0 + global rates, rate_keys, currency_symbol, currency_rate, ref_currency_name + fetched_data = get_data(currency_keys[index]) + rates = fetched_data['data']['rates'] + rate_keys = list(rates.keys()) + currency_symbol = rate_keys[index] + currency_rate = str(rates[rate_keys[index]]) + ref_currency_name = "{0}-{1}".format(currency_keys[index], currencies[currency_keys[index]]) + gc.collect() + + +update_base_currency(ref_currency_index) +update_display(0) +su.update(graphics) +cycle_count = 0 +symbol_index = 0 +print("Display {0} {1}".format(currency_symbol, currency_rate)) + +while 1: + if cycle_count > 4 * cycles_per_sequence: + cycle_count = 0 + symbol_index += 1 + if symbol_index > len(currency_keys): + symbol_index = 0 + print("Display {0} {1}".format(currency_symbol, currency_rate)) + currency_symbol = rate_keys[symbol_index] + currency_rate = rates[rate_keys[symbol_index]] + + if (su.is_pressed(StellarUnicorn.SWITCH_A)): + ref_currency_index += 1 + if (ref_currency_index > len(currency_keys)): + ref_currency_index = 0 + update_base_currency(ref_currency_index) + + if (su.is_pressed(StellarUnicorn.SWITCH_B)): + cycle_count = 0 + symbol_index += 1 + + if symbol_index > len(rate_keys): + symbol_index = 0 + currency_symbol = rate_keys[symbol_index] + currency_rate = rates[rate_keys[symbol_index]] + + update_display(cycle_count) + su.update(graphics) + cycle_count += 1 + time.sleep(0.1) diff --git a/micropython/examples/stellar_unicorn/feature_test.py b/micropython/examples/stellar_unicorn/feature_test.py new file mode 100644 index 00000000..868d832b --- /dev/null +++ b/micropython/examples/stellar_unicorn/feature_test.py @@ -0,0 +1,123 @@ +import time +import math +from stellar import StellarUnicorn +from picographics import PicoGraphics, DISPLAY_STELLAR_UNICORN as DISPLAY + +''' +Displays some text, gradients and colours and demonstrates button use. + +You can adjust the brightness with LUX + and -. +''' + +su = StellarUnicorn() +graphics = PicoGraphics(DISPLAY) + +width = StellarUnicorn.WIDTH +height = StellarUnicorn.HEIGHT + + +def gradient(r, g, b): + for y in range(0, height): + for x in range(0, width): + graphics.set_pen(graphics.create_pen(int((r * x) / 16), int((g * x) / 16), int((b * x) / 16))) + graphics.pixel(x, y) + + +def grid(r, g, b): + for y in range(0, height): + for x in range(0, width): + if (x + y) % 2 == 0: + graphics.set_pen(graphics.create_pen(r, g, b)) + else: + graphics.set_pen(0) + graphics.pixel(x, y) + + +def outline_text(text): + ms = time.ticks_ms() + + graphics.set_font("bitmap8") + v = int((math.sin(ms / 100.0) + 1.0) * 127.0) + w = graphics.measure_text(text, 1) + + x = int(16 / 2 - w / 2 + 1) + y = 12 + + graphics.set_pen(0) + graphics.text(text, x - 1, y - 1, -1, 1) + graphics.text(text, x, y - 1, -1, 1) + graphics.text(text, x + 1, y - 1, -1, 1) + graphics.text(text, x - 1, y, -1, 1) + graphics.text(text, x + 1, y, -1, 1) + graphics.text(text, x - 1, y + 1, -1, 1) + graphics.text(text, x, y + 1, -1, 1) + graphics.text(text, x + 1, y + 1, -1, 1) + + graphics.set_pen(graphics.create_pen(v, v, v)) + graphics.text(text, x, y, -1, 1) + + +su.set_brightness(0.5) + +while True: + + time_ms = time.ticks_ms() + test = (time_ms // 1000) % 5 + + if su.is_pressed(StellarUnicorn.SWITCH_BRIGHTNESS_UP): + su.adjust_brightness(+0.01) + + if su.is_pressed(StellarUnicorn.SWITCH_BRIGHTNESS_DOWN): + su.adjust_brightness(-0.01) + + graphics.set_pen(graphics.create_pen(0, 0, 0)) + graphics.clear() + + if test == 0: + print("grid pattern") + grid(255, 255, 255) + elif test == 1: + print("red gradient") + gradient(255, 0, 0) + elif test == 2: + print("green gradient") + gradient(0, 255, 0) + elif test == 3: + print("blue gradient") + gradient(0, 0, 255) + elif test == 4: + print("white gradient") + gradient(255, 255, 255) + + text = "" + + if su.is_pressed(StellarUnicorn.SWITCH_A): + text = "Button A" + + if su.is_pressed(StellarUnicorn.SWITCH_B): + text = "Button B" + + if su.is_pressed(StellarUnicorn.SWITCH_C): + text = "Button C" + + if su.is_pressed(StellarUnicorn.SWITCH_D): + text = "Button D" + + if su.is_pressed(StellarUnicorn.SWITCH_VOLUME_UP): + text = "Louder!" + + if su.is_pressed(StellarUnicorn.SWITCH_VOLUME_DOWN): + text = "Quieter" + + if su.is_pressed(StellarUnicorn.SWITCH_BRIGHTNESS_UP): + text = "Brighter!" + + if su.is_pressed(StellarUnicorn.SWITCH_BRIGHTNESS_DOWN): + text = "Darker" + + if su.is_pressed(StellarUnicorn.SWITCH_SLEEP): + text = "Zzz... zzz..." + + outline_text(text) + + su.update(graphics) diff --git a/micropython/examples/stellar_unicorn/feature_test_with_audio.py b/micropython/examples/stellar_unicorn/feature_test_with_audio.py new file mode 100644 index 00000000..1c67de43 --- /dev/null +++ b/micropython/examples/stellar_unicorn/feature_test_with_audio.py @@ -0,0 +1,347 @@ +import gc +import time +import math +from machine import Timer +from stellar import StellarUnicorn, Channel +from picographics import PicoGraphics, DISPLAY_STELLAR_UNICORN as DISPLAY + +''' +Displays some text, gradients and colours and demonstrates button use. +Also demonstrates some of the audio / synth features. + +- Button A plays a synth tune +- Button B plays a solo channel of the synth tune +- Button C plays a sinewave (it's frequency can be adjusted with VOL + and -) +- Button D plays a second sinewave (it's frequency can be adjusted with LUX + and -) +- Sleep button stops the sounds +''' + +gc.collect() + +su = StellarUnicorn() +graphics = PicoGraphics(DISPLAY) + +width = StellarUnicorn.WIDTH +height = StellarUnicorn.HEIGHT + +SONG_LENGTH = 384 +HAT = 20000 +BASS = 500 +SNARE = 6000 +SUB = 50 + +melody_notes = ( + 147, 0, 0, 0, 0, 0, 0, 0, 175, 0, 196, 0, 220, 0, 262, 0, 247, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 175, 0, 0, 0, 0, 0, 0, 0, 175, 0, 196, 0, 220, 0, 262, 0, 330, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 349, 0, 0, 0, 0, 0, 0, 0, 349, 0, 330, 0, 294, 0, 220, 0, 262, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 247, 0, 0, 0, 0, 0, 0, 0, 247, 0, 220, 0, 196, 0, 147, 0, 175, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, + 147, 0, 0, 0, 0, 0, 0, 0, 175, 0, 196, 0, 220, 0, 262, 0, 247, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 175, 0, 0, 0, 0, 0, 0, 0, 175, 0, 196, 0, 220, 0, 262, 0, 330, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 349, 0, 0, 0, 0, 0, 0, 0, 349, 0, 330, 0, 294, 0, 220, 0, 262, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 247, 0, 0, 0, 0, 0, 0, 0, 247, 0, 220, 0, 196, 0, 147, 0, 175, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, + 147, 0, 0, 0, 0, 0, 0, 0, 175, 0, 196, 0, 220, 0, 262, 0, 247, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 175, 0, 0, 0, 0, 0, 0, 0, 175, 0, 196, 0, 220, 0, 262, 0, 330, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 349, 0, 0, 0, 0, 0, 0, 0, 349, 0, 330, 0, 294, 0, 220, 0, 262, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 247, 0, 0, 0, 0, 0, 0, 0, 247, 0, 262, 0, 294, 0, 392, 0, 440, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) + +rhythm_notes = ( + 294, 0, 440, 0, 587, 0, 440, 0, 294, 0, 440, 0, 587, 0, 440, 0, 294, 0, 440, 0, 587, 0, 440, 0, 294, 0, 440, 0, 587, 0, 440, 0, 294, 0, 440, 0, 587, 0, 440, 0, 294, 0, 440, 0, 587, 0, 440, 0, 392, 0, 523, 0, 659, 0, 523, 0, 392, 0, 523, 0, 659, 0, 523, 0, 698, 0, 587, 0, 440, 0, 587, 0, 698, 0, 587, 0, 440, 0, 587, 0, 523, 0, 440, 0, 330, 0, 440, 0, 523, 0, 440, 0, 330, 0, 440, 0, 349, 0, 294, 0, 220, 0, 294, 0, 349, 0, 294, 0, 220, 0, 294, 0, 262, 0, 247, 0, 220, 0, 175, 0, 165, 0, 147, 0, 131, 0, 98, 0, + 294, 0, 440, 0, 587, 0, 440, 0, 294, 0, 440, 0, 587, 0, 440, 0, 294, 0, 440, 0, 587, 0, 440, 0, 294, 0, 440, 0, 587, 0, 440, 0, 294, 0, 440, 0, 587, 0, 440, 0, 294, 0, 440, 0, 587, 0, 440, 0, 392, 0, 523, 0, 659, 0, 523, 0, 392, 0, 523, 0, 659, 0, 523, 0, 698, 0, 587, 0, 440, 0, 587, 0, 698, 0, 587, 0, 440, 0, 587, 0, 523, 0, 440, 0, 330, 0, 440, 0, 523, 0, 440, 0, 330, 0, 440, 0, 349, 0, 294, 0, 220, 0, 294, 0, 349, 0, 294, 0, 220, 0, 294, 0, 262, 0, 247, 0, 220, 0, 175, 0, 165, 0, 147, 0, 131, 0, 98, 0, + 294, 0, 440, 0, 587, 0, 440, 0, 294, 0, 440, 0, 587, 0, 440, 0, 294, 0, 440, 0, 587, 0, 440, 0, 294, 0, 440, 0, 587, 0, 440, 0, 294, 0, 440, 0, 587, 0, 440, 0, 294, 0, 440, 0, 587, 0, 440, 0, 392, 0, 523, 0, 659, 0, 523, 0, 392, 0, 523, 0, 659, 0, 523, 0, 698, 0, 587, 0, 440, 0, 587, 0, 698, 0, 587, 0, 440, 0, 587, 0, 523, 0, 440, 0, 330, 0, 440, 0, 523, 0, 440, 0, 330, 0, 440, 0, 349, 0, 294, 0, 220, 0, 294, 0, 349, 0, 294, 0, 220, 0, 294, 0, 262, 0, 247, 0, 220, 0, 175, 0, 165, 0, 147, 0, 131, 0, 98, 0) + +drum_beats = ( + BASS, -1, 0, 0, 0, 0, 0, 0, SNARE, 0, -1, 0, 0, 0, BASS, -1, BASS, -1, 0, 0, 0, 0, 0, 0, SNARE, 0, -1, 0, 0, 0, 0, 0, BASS, -1, 0, 0, 0, 0, 0, 0, SNARE, 0, -1, 0, 0, 0, BASS, -1, BASS, -1, 0, 0, 0, 0, 0, 0, SNARE, 0, -1, 0, 0, 0, 0, 0, BASS, -1, 0, 0, 0, 0, 0, 0, SNARE, 0, -1, 0, 0, 0, BASS, -1, BASS, -1, 0, 0, 0, 0, 0, 0, SNARE, 0, -1, 0, 0, 0, 0, 0, BASS, -1, 0, 0, 0, 0, 0, 0, SNARE, 0, -1, 0, 0, 0, BASS, -1, BASS, -1, 0, 0, 0, 0, 0, 0, SNARE, 0, -1, 0, 0, 0, 0, 0, + BASS, -1, 0, 0, 0, 0, 0, 0, SNARE, 0, -1, 0, 0, 0, BASS, -1, BASS, -1, 0, 0, 0, 0, 0, 0, SNARE, 0, -1, 0, 0, 0, 0, 0, BASS, -1, 0, 0, 0, 0, 0, 0, SNARE, 0, -1, 0, 0, 0, BASS, -1, BASS, -1, 0, 0, 0, 0, 0, 0, SNARE, 0, -1, 0, 0, 0, 0, 0, BASS, -1, 0, 0, 0, 0, 0, 0, SNARE, 0, -1, 0, 0, 0, BASS, -1, BASS, -1, 0, 0, 0, 0, 0, 0, SNARE, 0, -1, 0, 0, 0, 0, 0, BASS, -1, 0, 0, 0, 0, 0, 0, SNARE, 0, -1, 0, 0, 0, BASS, -1, BASS, -1, 0, 0, 0, 0, 0, 0, SNARE, 0, -1, 0, 0, 0, 0, 0, + BASS, -1, 0, 0, 0, 0, 0, 0, SNARE, 0, -1, 0, 0, 0, BASS, -1, BASS, -1, 0, 0, 0, 0, 0, 0, SNARE, 0, -1, 0, 0, 0, 0, 0, BASS, -1, 0, 0, 0, 0, 0, 0, SNARE, 0, -1, 0, 0, 0, BASS, -1, BASS, -1, 0, 0, 0, 0, 0, 0, SNARE, 0, -1, 0, 0, 0, 0, 0, BASS, -1, 0, 0, 0, 0, 0, 0, SNARE, 0, -1, 0, 0, 0, BASS, -1, BASS, -1, 0, 0, 0, 0, 0, 0, SNARE, 0, -1, 0, 0, 0, 0, 0, BASS, -1, 0, 0, 0, 0, 0, 0, SNARE, 0, -1, 0, 0, 0, BASS, -1, BASS, -1, 0, 0, 0, 0, 0, 0, SNARE, 0, -1, 0, 0, 0, 0, 0) + +hi_hat = ( + HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, + HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, + HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1) + +bass_notes = ( + SUB, -1, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, SUB, -1, SUB, -1, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, SUB, -1, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, SUB, -1, SUB, -1, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, SUB, -1, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, SUB, -1, SUB, -1, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, SUB, -1, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, SUB, -1, SUB, -1, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, + SUB, -1, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, SUB, -1, SUB, -1, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, SUB, -1, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, SUB, -1, SUB, -1, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, SUB, -1, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, SUB, -1, SUB, -1, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, SUB, -1, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, SUB, -1, SUB, -1, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, + SUB, -1, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, SUB, -1, SUB, -1, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, SUB, -1, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, SUB, -1, SUB, -1, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, SUB, -1, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, SUB, -1, SUB, -1, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, SUB, -1, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, SUB, -1, SUB, -1, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0) + +notes = [melody_notes, rhythm_notes, drum_beats, hi_hat, bass_notes] +channels = [su.synth_channel(i) for i in range(len(notes) + 1)] # Extra channel for tones + +# Configure the synth to play our notes +channels[0].configure(waveforms=Channel.TRIANGLE + Channel.SQUARE, + attack=0.016, + decay=0.168, + sustain=0xafff / 65535, + release=0.168, + volume=10000 / 65535) + +channels[1].configure(waveforms=Channel.SINE + Channel.SQUARE, + attack=0.038, + decay=0.300, + sustain=0, + release=0, + volume=12000 / 65535) + +channels[2].configure(waveforms=Channel.NOISE, + attack=0.005, + decay=0.010, + sustain=16000 / 65535, + release=0.100, + volume=18000 / 65535) + +channels[3].configure(waveforms=Channel.NOISE, + attack=0.005, + decay=0.005, + sustain=8000 / 65535, + release=0.040, + volume=8000 / 65535) + +channels[4].configure(waveforms=Channel.SQUARE, + attack=0.010, + decay=0.100, + sustain=0, + release=0.500, + volume=12000 / 65535) + + +def gradient(r, g, b): + for y in range(0, height): + for x in range(0, width): + graphics.set_pen(graphics.create_pen(int((r * x) / 52), int((g * x) / 52), int((b * x) / 52))) + graphics.pixel(x, y) + + +def grid(r, g, b): + for y in range(0, height): + for x in range(0, width): + if (x + y) % 2 == 0: + graphics.set_pen(graphics.create_pen(r, g, b)) + else: + graphics.set_pen(0) + graphics.pixel(x, y) + + +def outline_text(text): + ms = time.ticks_ms() + + graphics.set_font("bitmap6") + v = int((math.sin(ms / 100.0) + 1.0) * 127.0) + w = graphics.measure_text(text, 1) + + x = int(16 / 2 - w / 2 + 1) + y = 5 + + graphics.set_pen(0) + graphics.text(text, x - 1, y - 1, -1, 1) + graphics.text(text, x, y - 1, -1, 1) + graphics.text(text, x + 1, y - 1, -1, 1) + graphics.text(text, x - 1, y, -1, 1) + graphics.text(text, x + 1, y, -1, 1) + graphics.text(text, x - 1, y + 1, -1, 1) + graphics.text(text, x, y + 1, -1, 1) + graphics.text(text, x + 1, y + 1, -1, 1) + + graphics.set_pen(graphics.create_pen(v, v, v)) + graphics.text(text, x, y, -1, 1) + + +su.set_brightness(0.5) + +# Vars for storing button state +was_a_pressed = False +was_b_pressed = False +was_c_pressed = False +was_d_pressed = False +was_z_pressed = False + +# The two frequencies to play +tone_a = 0 +tone_b = 0 + +# The current synth beat +beat = 0 + + +def next_beat(): + global beat + for i in range(5): + if notes[i][beat] > 0: + channels[i].frequency(notes[i][beat]) + channels[i].trigger_attack() + elif notes[i][beat] == -1: + channels[i].trigger_release() + + beat = (beat + 1) % SONG_LENGTH + + +def tick(timer): + next_beat() + + +timer = Timer(-1) + +synthing = False + + +while True: + + time_ms = time.ticks_ms() + test = (time_ms // 1000) % 5 + + if su.is_pressed(StellarUnicorn.SWITCH_A): + if not was_a_pressed: + channels[0].volume(10000 / 65535) + channels[1].volume(12000 / 65535) + channels[2].volume(18000 / 65535) + channels[3].volume(8000 / 65535) + channels[4].volume(12000 / 65535) + channels[5].volume(0) + + # If the synth is not already playing, init the first beat + if not synthing: + beat = 0 + next_beat() + + su.play_synth() + synthing = True + timer.init(freq=10, mode=Timer.PERIODIC, callback=tick) + + was_a_pressed = True + else: + was_a_pressed = False + + if su.is_pressed(StellarUnicorn.SWITCH_B): + if not was_b_pressed: + channels[0].volume(0) + channels[1].volume(12000 / 65535) + channels[2].volume(0) + channels[3].volume(0) + channels[4].volume(0) + channels[5].volume(0) + + # If the synth is not already playing, init the first beat + if not synthing: + beat = 0 + next_beat() + + su.play_synth() + synthing = True + timer.init(freq=10, mode=Timer.PERIODIC, callback=tick) + + was_b_pressed = True + else: + was_b_pressed = False + + if su.is_pressed(StellarUnicorn.SWITCH_C): + if not was_c_pressed: + # Stop synth (if running) and play Tone A + timer.deinit() + tone_a = 400 + channels[5].play_tone(tone_a, 0.06) + channels[5].volume(12000 / 65535) + + su.play_synth() + synthing = False + + was_c_pressed = True + else: + was_c_pressed = False + + if su.is_pressed(StellarUnicorn.SWITCH_D): + if not was_d_pressed: + # Stop synth (if running) and play Tone B + timer.deinit() + tone_b = 600 + + channels[5].play_tone(tone_b, 0.06, attack=0.5) + channels[5].volume(12000 / 65535) + + su.play_synth() + synthing = False + + was_d_pressed = True + else: + was_d_pressed = False + + if su.is_pressed(StellarUnicorn.SWITCH_BRIGHTNESS_UP): + if tone_b > 0: # Zero means tone not playing + # Increase Tone B + tone_b = min(tone_b + 10, 20000) + channels[5].frequency(tone_b) + + if su.is_pressed(StellarUnicorn.SWITCH_BRIGHTNESS_DOWN): + if tone_b > 0: # Zero means tone not playing + # Decrease Tone B + tone_b = max(tone_b - 10, 10) + channels[5].frequency(max(tone_b, 10)) + + if su.is_pressed(StellarUnicorn.SWITCH_VOLUME_UP): + if tone_a > 0: # Zero means tone not playing + # Increase Tone A + tone_a = min(tone_a + 10, 20000) + channels[5].frequency(tone_a) + + if su.is_pressed(StellarUnicorn.SWITCH_VOLUME_DOWN): + if tone_a > 0: # Zero means tone not playing + # Decrease Tone A + tone_a = max(tone_a - 10, 10) + channels[5].frequency(tone_a) + + if su.is_pressed(StellarUnicorn.SWITCH_SLEEP): + if not was_z_pressed: + # Stop synth and both tones + tone_a = 0 + tone_b = 0 + su.stop_playing() + timer.deinit() + synthing = False + + was_z_pressed = True + else: + was_z_pressed = False + + graphics.set_pen(graphics.create_pen(0, 0, 0)) + graphics.clear() + + if test == 0: + # print("grid pattern") + grid(255, 255, 255) + elif test == 1: + # print("red gradient") + gradient(255, 0, 0) + elif test == 2: + # print("green gradient") + gradient(0, 255, 0) + elif test == 3: + # print("blue gradient") + gradient(0, 0, 255) + elif test == 4: + # print("white gradient") + gradient(255, 255, 255) + + text = "" + + if su.is_pressed(StellarUnicorn.SWITCH_A): + text = "PlaySyn" + + if su.is_pressed(StellarUnicorn.SWITCH_B): + text = "SoloSyn" + + if su.is_pressed(StellarUnicorn.SWITCH_C): + text = "Tone A" + + if su.is_pressed(StellarUnicorn.SWITCH_D): + text = "Tone B" + + if su.is_pressed(StellarUnicorn.SWITCH_VOLUME_UP): + text = "RaiseA" + + if su.is_pressed(StellarUnicorn.SWITCH_VOLUME_DOWN): + text = "LowerA" + + if su.is_pressed(StellarUnicorn.SWITCH_BRIGHTNESS_UP): + text = "RaiseB" + + if su.is_pressed(StellarUnicorn.SWITCH_BRIGHTNESS_DOWN): + text = "LowerB" + + if su.is_pressed(StellarUnicorn.SWITCH_SLEEP): + text = "Stop" + + outline_text(text) + + su.update(graphics) + + # pause for a moment (important or the USB serial device will fail + time.sleep(0.001) diff --git a/micropython/examples/stellar_unicorn/fire_effect.py b/micropython/examples/stellar_unicorn/fire_effect.py new file mode 100644 index 00000000..d32d9c51 --- /dev/null +++ b/micropython/examples/stellar_unicorn/fire_effect.py @@ -0,0 +1,102 @@ +import time +import random +from stellar import StellarUnicorn +from picographics import PicoGraphics, DISPLAY_STELLAR_UNICORN as DISPLAY + +''' +A pretty, procedural fire effect. + +You can adjust the brightness with LUX + and -. +''' + +su = StellarUnicorn() +graphics = PicoGraphics(DISPLAY) + +fire_colours = [graphics.create_pen(0, 0, 0), + graphics.create_pen(20, 20, 20), + graphics.create_pen(180, 30, 0), + graphics.create_pen(220, 160, 0), + graphics.create_pen(255, 255, 180)] + + +@micropython.native # noqa: F821 +def update(): + # take local references as it's quicker than accessing the global + # and we access it a lot in this method + _heat = heat + + # clear the bottom row and then add a new fire seed to it + for x in range(width): + _heat[x][height - 1] = 0.0 + _heat[x][height - 2] = 0.0 + + for c in range(fire_spawns): + x = random.randint(0, width - 4) + 2 + _heat[x + 0][height - 1] = 1.0 + _heat[x + 1][height - 1] = 1.0 + _heat[x - 1][height - 1] = 1.0 + _heat[x + 0][height - 2] = 1.0 + _heat[x + 1][height - 2] = 1.0 + _heat[x - 1][height - 2] = 1.0 + + factor = damping_factor / 5.0 + for y in range(0, height - 2): + for x in range(1, width - 1): + _heat[x][y] += _heat[x][y + 1] + _heat[x][y + 2] + _heat[x - 1][y + 1] + _heat[x + 1][y + 1] + _heat[x][y] *= factor + + +@micropython.native # noqa: F821 +def draw(): + # take local references as it's quicker than accessing the global + # and we access it a lot in this method + _graphics = graphics + _heat = heat + _set_pen = graphics.set_pen + _pixel = graphics.pixel + _fire_colours = fire_colours + + for y in range(StellarUnicorn.HEIGHT): + for x in range(StellarUnicorn.WIDTH): + value = _heat[x + 1][y] + if value < 0.15: + _set_pen(_fire_colours[0]) + elif value < 0.25: + _set_pen(_fire_colours[1]) + elif value < 0.35: + _set_pen(_fire_colours[2]) + elif value < 0.45: + _set_pen(_fire_colours[3]) + else: + _set_pen(_fire_colours[4]) + _pixel(x, y) + + su.update(_graphics) + + +width = StellarUnicorn.WIDTH + 2 +height = StellarUnicorn.HEIGHT + 4 +heat = [[0.0 for y in range(height)] for x in range(width)] +fire_spawns = 5 +damping_factor = 0.97 + + +su.set_brightness(0.5) + +while True: + + if su.is_pressed(StellarUnicorn.SWITCH_BRIGHTNESS_UP): + su.adjust_brightness(+0.01) + + if su.is_pressed(StellarUnicorn.SWITCH_BRIGHTNESS_DOWN): + su.adjust_brightness(-0.01) + + start = time.ticks_ms() + + update() + draw() + + print("total took: {} ms".format(time.ticks_ms() - start)) + + # pause for a moment (important or the USB serial device will fail) + time.sleep(0.001) diff --git a/micropython/examples/stellar_unicorn/http_text/html_text.py b/micropython/examples/stellar_unicorn/http_text/html_text.py new file mode 100644 index 00000000..dfd186af --- /dev/null +++ b/micropython/examples/stellar_unicorn/http_text/html_text.py @@ -0,0 +1,190 @@ +import time +from stellar import StellarUnicorn +from picographics import PicoGraphics, DISPLAY_STELLAR_UNICORN as DISPLAY +import WIFI_CONFIG +from network_manager import NetworkManager +import uasyncio as asyncio +import uasyncio.core +from tinyweb.server import webserver + +''' +Display scrolling wisdom, quotes or greetz... from the internetz! + +You can adjust the brightness with LUX + and -. + +Requires network_manager.py , WIFI_CONFIG.py, logging.mpy and tinyweb from micropython/examples/common +You'll also need index.html to be saved alongside this file. +''' + +# Server Settings +host = "0.0.0.0" +port = 80 + + +def convert_colour(colour_str): + colour_str = colour_str.split(',') + print(colour_str) + return colour_str[0], colour_str[1], colour_str[2] + + +class text: + + def get(self, data): + global MESSAGE, MESSAGE_COLOUR, BACKGROUND_COLOUR + print(data) + if 'text' in data.keys(): + MESSAGE = data['text'] + if 'colourfg' in data.keys(): + MESSAGE_COLOUR = convert_colour(data['colourfg']) + if 'colourbg' in data.keys(): + BACKGROUND_COLOUR = convert_colour(data['colourbg']) + return {'message': 'text updated'}, 201 + + def post(self, data): + + return {'message': 'text updated'}, 201 + + +def status_handler(mode, status, ip): + global MESSAGE + print("Network: {}".format(WIFI_CONFIG.SSID)) + status_text = "Connecting..." + if status is not None: + if status: + status_text = "Connection successful!" + else: + status_text = "Connection failed!" + + print(status_text) + print("IP: {}".format(ip)) + MESSAGE = "{}".format(ip) + + +# Create web server application +app = webserver() + + +# Index page +@app.route('/') +async def index(request, response): + # Send actual HTML page + await response.send_file('index.html', content_type='text/html') + + +# HTTP redirection +@app.route('/redirect') +async def redirect(request, response): + # Start HTTP response with content-type text/html + await response.redirect('/') + +# constants for controlling scrolling text +PADDING = 5 +MESSAGE_COLOUR = (255, 255, 255) +OUTLINE_COLOUR = (0, 0, 0) +BACKGROUND_COLOUR = (10, 0, 96) +MESSAGE = "Connecting" +HOLD_TIME = 2.0 +STEP_TIME = 0.075 + +# create galactic object and graphics surface for drawing +su = StellarUnicorn() +graphics = PicoGraphics(DISPLAY) + +width = StellarUnicorn.WIDTH +height = StellarUnicorn.HEIGHT + + +# function for drawing outlined text +def outline_text(text, x, y): + graphics.set_pen(graphics.create_pen(int(OUTLINE_COLOUR[0]), int(OUTLINE_COLOUR[1]), int(OUTLINE_COLOUR[2]))) + graphics.text(text, x - 1, y - 1, -1, 1) + graphics.text(text, x, y - 1, -1, 1) + graphics.text(text, x + 1, y - 1, -1, 1) + graphics.text(text, x - 1, y, -1, 1) + graphics.text(text, x + 1, y, -1, 1) + graphics.text(text, x - 1, y + 1, -1, 1) + graphics.text(text, x, y + 1, -1, 1) + graphics.text(text, x + 1, y + 1, -1, 1) + + graphics.set_pen(graphics.create_pen(int(MESSAGE_COLOUR[0]), int(MESSAGE_COLOUR[1]), int(MESSAGE_COLOUR[2]))) + graphics.text(text, x, y, -1, 1) + + +def run(): + # Setup wifi + network_manager = NetworkManager(WIFI_CONFIG.COUNTRY, status_handler=status_handler) + + app.add_resource(text, '/update') + + # Connect to Wifi network + asyncio.get_event_loop().run_until_complete(network_manager.client(WIFI_CONFIG.SSID, WIFI_CONFIG.PSK)) + while (not network_manager.isconnected()): + time.sleep(0.1) + + +su.set_brightness(0.5) + +# Start wifi connection +run() + + +async def message_update(): + global MESSAGE + last_time = time.ticks_ms() + STATE_PRE_SCROLL = 0 + STATE_SCROLLING = 1 + STATE_POST_SCROLL = 2 + + shift = 0 + state = STATE_PRE_SCROLL + + # set the font + graphics.set_font("bitmap8") + + # calculate the message width so scrolling can happen + msg_width = graphics.measure_text(MESSAGE, 1) + while 1: + + msg_width = graphics.measure_text(MESSAGE, 1) + time_ms = time.ticks_ms() + + if su.is_pressed(StellarUnicorn.SWITCH_BRIGHTNESS_UP): + su.adjust_brightness(+0.01) + + if su.is_pressed(StellarUnicorn.SWITCH_BRIGHTNESS_DOWN): + su.adjust_brightness(-0.01) + + if state == STATE_PRE_SCROLL and time_ms - last_time > HOLD_TIME * 1000: + if msg_width + PADDING * 2 >= width: + state = STATE_SCROLLING + last_time = time_ms + + if state == STATE_SCROLLING and time_ms - last_time > STEP_TIME * 1000: + shift += 1 + if shift >= (msg_width + PADDING * 2) - width - 1: + state = STATE_POST_SCROLL + last_time = time_ms + + if state == STATE_POST_SCROLL and time_ms - last_time > HOLD_TIME * 1000: + state = STATE_PRE_SCROLL + shift = 0 + last_time = time_ms + + graphics.set_pen(graphics.create_pen(int(BACKGROUND_COLOUR[0]), int(BACKGROUND_COLOUR[1]), int(BACKGROUND_COLOUR[2]))) + graphics.clear() + + outline_text(MESSAGE, x=PADDING - shift, y=11) + + # update the display + su.update(graphics) + + # pause for a moment (important or the USB serial device will fail) + await asyncio.sleep(0.001) + + +# The following is required to run both the web server and the scrolling text coherently +app._server_coro = app._tcp_server(host, port, app.backlog) +loop = asyncio.get_event_loop() +t1 = loop.create_task(message_update()) +t2 = loop.create_task(app._server_coro) +loop.run_forever() diff --git a/micropython/examples/stellar_unicorn/http_text/index.html b/micropython/examples/stellar_unicorn/http_text/index.html new file mode 100644 index 00000000..890148ef --- /dev/null +++ b/micropython/examples/stellar_unicorn/http_text/index.html @@ -0,0 +1,102 @@ + + + + + + +

Stellar Unicorn Web Text

+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +

+ Please type in what you wish to be displayed on the Stellar Unicorn and whe you are ready hit update to update the display +

+ +
+ +
\ No newline at end of file diff --git a/micropython/examples/stellar_unicorn/launch/fire.py b/micropython/examples/stellar_unicorn/launch/fire.py new file mode 100644 index 00000000..5c0d90f2 --- /dev/null +++ b/micropython/examples/stellar_unicorn/launch/fire.py @@ -0,0 +1,80 @@ +import random +from stellar import StellarUnicorn + +graphics = None +palette = None + +# setup heat value buffer and fire parameters +width = StellarUnicorn.WIDTH + 2 +height = StellarUnicorn.HEIGHT + 4 +heat = [[0.0 for y in range(height)] for x in range(width)] +fire_spawns = 5 +damping_factor = 0.97 + + +def init(): + # a palette of five firey colours (white, yellow, orange, red, smoke) + global palette + palette = [ + graphics.create_pen(0, 0, 0), + graphics.create_pen(20, 20, 20), + graphics.create_pen(180, 30, 0), + graphics.create_pen(220, 160, 0), + graphics.create_pen(255, 255, 180) + ] + + +# returns the palette entry for a given heat value +@micropython.native # noqa: F821 +def pen_from_value(value): + if value < 0.15: + return palette[0] + elif value < 0.25: + return palette[1] + elif value < 0.35: + return palette[2] + elif value < 0.45: + return palette[3] + return palette[4] + + +@micropython.native # noqa: F821 +def draw(): + # clear the the rows off the bottom of the display + for x in range(width): + heat[x][height - 1] = 0.0 + heat[x][height - 2] = 0.0 + + # add new fire spawns + for c in range(fire_spawns): + x = random.randint(0, width - 4) + 2 + heat[x + 0][height - 1] = 1.0 + heat[x + 1][height - 1] = 1.0 + heat[x - 1][height - 1] = 1.0 + heat[x + 0][height - 2] = 1.0 + heat[x + 1][height - 2] = 1.0 + heat[x - 1][height - 2] = 1.0 + + # average and damp out each value to create rising flame effect + for y in range(0, height - 2): + for x in range(1, width - 1): + # update this pixel by averaging the below pixels + average = ( + heat[x][y] + heat[x][y + 1] + heat[x][y + 2] + heat[x - 1][y + 1] + heat[x + 1][y + 1] + ) / 5.0 + + # damping factor to ensure flame tapers out towards the top of the displays + average *= damping_factor + + # update the heat map with our newly averaged value + heat[x][y] = average + + # render the heat values to the graphics buffer + for y in range(StellarUnicorn.HEIGHT): + for x in range(StellarUnicorn.WIDTH): + graphics.set_pen(pen_from_value(heat[x + 1][y])) + graphics.pixel(x, y) + + +def test(): + print("A") diff --git a/micropython/examples/stellar_unicorn/launch/main.py b/micropython/examples/stellar_unicorn/launch/main.py new file mode 100644 index 00000000..f60d20e2 --- /dev/null +++ b/micropython/examples/stellar_unicorn/launch/main.py @@ -0,0 +1,113 @@ +import time +import machine +from stellar import StellarUnicorn +from picographics import PicoGraphics, DISPLAY_STELLAR_UNICORN as DISPLAY + +# overclock to 200Mhz +machine.freq(200000000) + +# create stellar object and graphics surface for drawing +stellar = StellarUnicorn() +graphics = PicoGraphics(DISPLAY) + +brightness = 0.5 + + +# returns the id of the button that is currently pressed or +# None if none are +def pressed(): + if stellar.is_pressed(StellarUnicorn.SWITCH_A): + return StellarUnicorn.SWITCH_A + if stellar.is_pressed(StellarUnicorn.SWITCH_B): + return StellarUnicorn.SWITCH_B + if stellar.is_pressed(StellarUnicorn.SWITCH_C): + return StellarUnicorn.SWITCH_C + if stellar.is_pressed(StellarUnicorn.SWITCH_D): + return StellarUnicorn.SWITCH_D + return None + + +# wait for a button to be pressed and load that effect +while True: + graphics.set_font("bitmap6") + graphics.set_pen(graphics.create_pen(0, 0, 0)) + graphics.clear() + graphics.set_pen(graphics.create_pen(155, 155, 155)) + graphics.text("PRESS", 3, 6, -1, 1) + graphics.text("A B C OR D!", 5, 14, 16, 1, 0) + + # brightness up/down + if stellar.is_pressed(StellarUnicorn.SWITCH_BRIGHTNESS_UP): + brightness += 0.01 + if stellar.is_pressed(StellarUnicorn.SWITCH_BRIGHTNESS_DOWN): + brightness -= 0.01 + brightness = max(min(brightness, 1.0), 0.0) + + stellar.set_brightness(brightness) + stellar.update(graphics) + + if pressed() == StellarUnicorn.SWITCH_A: + import fire as effect + break + if pressed() == StellarUnicorn.SWITCH_B: + import supercomputer as effect # noqa: F811 + break + if pressed() == StellarUnicorn.SWITCH_C: + import rainbow as effect # noqa: F811 + break + if pressed() == StellarUnicorn.SWITCH_D: + import today as effect # noqa: F811 + break + + # pause for a moment + time.sleep(0.01) + +# wait until all buttons are released +while pressed() is not None: + time.sleep(0.1) + +effect.graphics = graphics +effect.init() + +sleep = False +was_sleep_pressed = False + + +# wait +while True: + # if A, B, C, or D are pressed then reset + if pressed() is not None: + machine.reset() + + sleep_pressed = stellar.is_pressed(StellarUnicorn.SWITCH_SLEEP) + if sleep_pressed and not was_sleep_pressed: + sleep = not sleep + + was_sleep_pressed = sleep_pressed + + if sleep: + # fade out if screen not off + stellar.set_brightness(stellar.get_brightness() - 0.01) + + if stellar.get_brightness() > 0.0: + effect.draw() + + # update the display + stellar.update(graphics) + else: + effect.draw() + + # update the display + stellar.update(graphics) + + # brightness up/down + if stellar.is_pressed(StellarUnicorn.SWITCH_BRIGHTNESS_UP): + brightness += 0.01 + if stellar.is_pressed(StellarUnicorn.SWITCH_BRIGHTNESS_DOWN): + brightness -= 0.01 + brightness = max(min(brightness, 1.0), 0.0) + + stellar.set_brightness(brightness) + + # pause for a moment (important or the USB serial device will fail + time.sleep(0.001) diff --git a/micropython/examples/stellar_unicorn/launch/rainbow.py b/micropython/examples/stellar_unicorn/launch/rainbow.py new file mode 100644 index 00000000..637c6ee1 --- /dev/null +++ b/micropython/examples/stellar_unicorn/launch/rainbow.py @@ -0,0 +1,59 @@ +import math +from stellar import StellarUnicorn + +graphics = None +palette = None + +width = StellarUnicorn.WIDTH +height = StellarUnicorn.HEIGHT + + +@micropython.native # noqa: F821 +def from_hsv(h, s, v): + i = math.floor(h * 6.0) + f = h * 6.0 - i + v *= 255.0 + p = v * (1.0 - s) + q = v * (1.0 - f * s) + t = v * (1.0 - (1.0 - f) * s) + + i = int(i) % 6 + if i == 0: + return int(v), int(t), int(p) + if i == 1: + return int(q), int(v), int(p) + if i == 2: + return int(p), int(v), int(t) + if i == 3: + return int(p), int(q), int(v) + if i == 4: + return int(t), int(p), int(v) + if i == 5: + return int(v), int(p), int(q) + + +phase = 0 +hue_map = [from_hsv(x / width, 1.0, 1.0) for x in range(width)] +hue_offset = 0.0 +stripe_width = 3.0 +speed = 5.0 + + +def init(): + pass + + +@micropython.native # noqa: F821 +def draw(): + global hue_offset, phase + + phase += speed + + phase_percent = phase / 15 + for x in range(width): + colour = hue_map[int((x + (hue_offset * width)) % width)] + for y in range(height): + v = ((math.sin((x + y) / stripe_width + phase_percent) + 1.5) / 2.5) + + graphics.set_pen(graphics.create_pen(int(colour[0] * v), int(colour[1] * v), int(colour[2] * v))) + graphics.pixel(x, y) diff --git a/micropython/examples/stellar_unicorn/launch/supercomputer.py b/micropython/examples/stellar_unicorn/launch/supercomputer.py new file mode 100644 index 00000000..0f933483 --- /dev/null +++ b/micropython/examples/stellar_unicorn/launch/supercomputer.py @@ -0,0 +1,40 @@ +import random +from stellar import StellarUnicorn + +graphics = None + +colour = (230, 150, 0) + + +def init(): + global width, height, lifetime, age + width = StellarUnicorn.WIDTH + height = StellarUnicorn.HEIGHT + lifetime = [[0.0 for y in range(height)] for x in range(width)] + age = [[0.0 for y in range(height)] for x in range(width)] + for y in range(height): + for x in range(width): + lifetime[x][y] = 1.0 + random.uniform(0.0, 0.1) + age[x][y] = random.uniform(0.0, 1.0) * lifetime[x][y] + + +@micropython.native # noqa: F821 +def draw(): + for y in range(height): + for x in range(width): + if age[x][y] >= lifetime[x][y]: + age[x][y] = 0.0 + lifetime[x][y] = 1.0 + random.uniform(0.0, 0.1) + + age[x][y] += 0.025 + + for y in range(height): + for x in range(width): + if age[x][y] < lifetime[x][y] * 0.3: + graphics.set_pen(graphics.create_pen(colour[0], colour[1], colour[2])) + elif age[x][y] < lifetime[x][y] * 0.5: + decay = (lifetime[x][y] * 0.5 - age[x][y]) * 5.0 + graphics.set_pen(graphics.create_pen(int(decay * colour[0]), int(decay * colour[1]), int(decay * colour[2]))) + else: + graphics.set_pen(0) + graphics.pixel(x, y) diff --git a/micropython/examples/stellar_unicorn/launch/today.py b/micropython/examples/stellar_unicorn/launch/today.py new file mode 100644 index 00000000..8dfc576f --- /dev/null +++ b/micropython/examples/stellar_unicorn/launch/today.py @@ -0,0 +1,100 @@ +import time +import network +import ntptime +import machine + +# You will need to create or update the file secrets.py with your network credentials using Thonny +# in order for the example to update using the NTP. + +# secrets.py should contain: +# WIFI_SSID = "" +# WIFI_PASSWORD = "" + +try: + from secrets import WIFI_SSID, WIFI_PASSWORD +except ImportError: + print("Create secrets.py with your WiFi credentials") + +graphics = None + +WIDTH = 16 # StellarUnicorn.WIDTH +HEIGHT = 16 # StellarUnicorn.HEIGHT + +rtc = machine.RTC() + +DAYS = ["Mon", "Tue", "Wed", "Thur", "Fri", "Sat", "Sun"] + +# Enable the Wireless +wlan = network.WLAN(network.STA_IF) +wlan.active(True) + + +def network_connect(SSID, PSK): + + # Number of attempts to make before timeout + max_wait = 5 + + # Sets the Wireless LED pulsing and attempts to connect to your local network. + print("connecting...") + 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) + + # Handle connection error. Switches the Warn LED on. + if wlan.status() != 3: + print("Unable to connect. Attempting connection again") + + +# Function to sync the Pico RTC using NTP +def sync_time(): + + try: + network_connect(WIFI_SSID, WIFI_PASSWORD) + except NameError: + print("Create secrets.py with your WiFi credentials") + + if wlan.status() < 0 or wlan.status() >= 3: + try: + ntptime.settime() + except OSError: + print("Unable to sync with NTP server. Check network and try again.") + + +def init(): + + sync_time() + + +def draw(): + + # Pens + RED = graphics.create_pen(120, 0, 0) + WHITE = graphics.create_pen(255, 255, 255) + + current_t = rtc.datetime() + + # Set the pen to Red and clear the screen. + graphics.set_pen(WHITE) + graphics.clear() + + # Measures the length of the text to help us with centring later. + day_length = graphics.measure_text(DAYS[current_t[3]], 1) + date_length = graphics.measure_text(str(current_t[2]), 3) + + graphics.set_font("bitmap6") + graphics.set_pen(RED) + graphics.rectangle(0, 0, WIDTH, 7) + graphics.set_pen(WHITE) + graphics.text(DAYS[current_t[3]], (WIDTH // 2) - (day_length // 2) - 1, 0, 16, 1) + + graphics.set_pen(RED) + graphics.set_font("bitmap8") + graphics.text(str(current_t[2]), (WIDTH // 2) - (date_length // 2) + 1, 9, 16, 3) + + graphics.set_pen(graphics.create_pen(0, 0, 0)) diff --git a/micropython/examples/stellar_unicorn/lava_lamp.py b/micropython/examples/stellar_unicorn/lava_lamp.py new file mode 100644 index 00000000..de8d4454 --- /dev/null +++ b/micropython/examples/stellar_unicorn/lava_lamp.py @@ -0,0 +1,151 @@ +import time +import random +import math +from stellar import StellarUnicorn +from picographics import PicoGraphics, DISPLAY_STELLAR_UNICORN as DISPLAY + +''' +A 70s-tastic, procedural rainbow lava lamp. + +You can adjust the brightness with LUX + and -. +''' + +su = StellarUnicorn() +graphics = PicoGraphics(DISPLAY) + +blob_count = 10 + + +class Blob(): + def __init__(self): + self.x = float(random.randint(0, width - 1)) + self.y = float(random.randint(0, height - 1)) + self.r = (float(random.randint(0, 40)) / 10.0) + 5.0 + self.dx = (float(random.randint(0, 2)) / 10.0) - 0.1 + self.dy = (float(random.randint(0, 2)) / 10.0) - 0.05 # positive bias + + +@micropython.native # noqa: F821 +def setup_portrait(): + global width, height, liquid, blobs + width = StellarUnicorn.HEIGHT + height = StellarUnicorn.WIDTH + liquid = [[0.0 for y in range(height)] for x in range(width)] + blobs = [Blob() for i in range(blob_count)] + + +hue = 0.0 + + +@micropython.native # noqa: F821 +def from_hsv(h, s, v): + i = math.floor(h * 6.0) + f = h * 6.0 - i + v *= 255.0 + p = v * (1.0 - s) + q = v * (1.0 - f * s) + t = v * (1.0 - (1.0 - f) * s) + + i = int(i) % 6 + if i == 0: + return graphics.create_pen(int(v), int(t), int(p)) + if i == 1: + return graphics.create_pen(int(q), int(v), int(p)) + if i == 2: + return graphics.create_pen(int(p), int(v), int(t)) + if i == 3: + return graphics.create_pen(int(p), int(q), int(v)) + if i == 4: + return graphics.create_pen(int(t), int(p), int(v)) + if i == 5: + return graphics.create_pen(int(v), int(p), int(q)) + + +@micropython.native # noqa: F821 +def update_liquid(): + for y in range(height): + for x in range(width): + liquid[x][y] = 0.0 + + for blob in blobs: + r_sq = blob.r * blob.r + blob_y_range = range(max(math.floor(blob.y - blob.r), 0), + min(math.ceil(blob.y + blob.r), height)) + blob_x_range = range(max(math.floor(blob.x - blob.r), 0), + min(math.ceil(blob.x + blob.r), width)) + + for y in blob_y_range: + for x in blob_x_range: + x_diff = x - blob.x + y_diff = y - blob.y + d_sq = x_diff * x_diff + y_diff * y_diff + if d_sq <= r_sq: + liquid[x][y] += 1.0 - (d_sq / r_sq) + + +@micropython.native # noqa: F821 +def move_blobs(): + for blob in blobs: + blob.x += blob.dx + blob.y += blob.dy + + if blob.x < 0.0 or blob.x >= float(width): + blob.dx = 0.0 - blob.dx + + if blob.y < 0.0 or blob.y >= float(height): + blob.dy = 0.0 - blob.dy + + +@micropython.native # noqa: F821 +def draw_portrait(): + global hue + hue += 0.001 + + dark = from_hsv(hue, 1.0, 0.3) + mid = from_hsv(hue, 1.0, 0.6) + bright = from_hsv(hue, 1.0, 1.0) + + for y in range(height): + for x in range(width): + v = liquid[x][y] + + # select a colour for this pixel based on how much + # "blobfluence" there is at this position in the liquid + if v >= 1.5: + graphics.set_pen(bright) + elif v >= 1.25: + graphics.set_pen(mid) + elif v >= 1.0: + graphics.set_pen(dark) + else: + graphics.set_pen(0) + graphics.pixel(y, x) + + su.update(graphics) + + +setup_portrait() + +su.set_brightness(0.5) + +while True: + + if su.is_pressed(StellarUnicorn.SWITCH_BRIGHTNESS_UP): + su.adjust_brightness(+0.01) + + if su.is_pressed(StellarUnicorn.SWITCH_BRIGHTNESS_DOWN): + su.adjust_brightness(-0.01) + + if su.is_pressed(StellarUnicorn.SWITCH_A): + setup_portrait() + + start = time.ticks_ms() + + update_liquid() + move_blobs() + draw_portrait() + + # pause for a moment (important or the USB serial device will fail) + time.sleep(0.001) + + print("total took: {} ms".format(time.ticks_ms() - start)) diff --git a/micropython/examples/stellar_unicorn/melody_maker.py b/micropython/examples/stellar_unicorn/melody_maker.py new file mode 100644 index 00000000..ec808276 --- /dev/null +++ b/micropython/examples/stellar_unicorn/melody_maker.py @@ -0,0 +1,131 @@ +import time +import random +from stellar import StellarUnicorn, Channel +from picographics import PicoGraphics, DISPLAY_STELLAR_UNICORN, PEN_P8 + +""" +A gloriously terrible melody maker. + +Use Vol + and Vol - to move up/down (note pitch) + +Use Lux - and D to move left/right (note position) + +Press A to set a note. + +Press B to delete a note. + +Use Lux + to play/pause. +""" + +NOTE_DURATION = 125 + +su = StellarUnicorn() +su.set_brightness(0.5) +graphics = PicoGraphics(DISPLAY_STELLAR_UNICORN, pen_type=PEN_P8) + +boopety_beepety = su.synth_channel(0) +boopety_beepety.configure( + waveforms=Channel.SQUARE | Channel.SINE, + attack=0.1, + decay=0.5, + sustain=0.0, + release=1.0, + volume=1.0 +) + +su.play_synth() + +black = graphics.create_pen(0, 0, 0) +note = graphics.create_pen(255, 255, 255) +cursor_bg = graphics.create_pen(64, 0, 0) +cursor = graphics.create_pen(255, 0, 0) +playhead = graphics.create_pen(0, 128, 0) + +cursor_position = [0, 0] + +playhead_position = 0 + +width, height = graphics.get_bounds() + +notes = [random.randint(0, height) for _ in range(width)] + +last_note_advance = time.ticks_ms() + +last_action = time.ticks_ms() + +playing = True + + +def debounce(button, duration=100): + global last_action + if su.is_pressed(button) and time.ticks_ms() - last_action > duration: + last_action = time.ticks_ms() + return True + return False + + +def note_to_frequency(note_number): + return int((2 ** ((note_number - 69.0) / 12)) * 440) + + +while True: + if debounce(StellarUnicorn.SWITCH_D): + cursor_position[0] -= 1 + cursor_position[0] %= width + + if debounce(StellarUnicorn.SWITCH_BRIGHTNESS_DOWN): + cursor_position[0] += 1 + cursor_position[0] %= width + + if debounce(StellarUnicorn.SWITCH_VOLUME_DOWN): + cursor_position[1] += 1 + cursor_position[1] %= height + + if debounce(StellarUnicorn.SWITCH_VOLUME_UP): + cursor_position[1] -= 1 + cursor_position[1] %= height + + if debounce(StellarUnicorn.SWITCH_BRIGHTNESS_UP, 500): + playing = not playing + if not playing: + boopety_beepety.trigger_release() + + if su.is_pressed(StellarUnicorn.SWITCH_A): + notes[cursor_position[0]] = cursor_position[1] + + if su.is_pressed(StellarUnicorn.SWITCH_B): + notes[cursor_position[0]] = None + + if time.ticks_ms() - last_note_advance > NOTE_DURATION: + current_note = None + if playing: + playhead_position += 1 + playhead_position %= width + current_note = notes[playhead_position] + if current_note is not None: + current_note = height - current_note # Bottom = Low, Top = High + current_note += 36 # Shift up the scale a couple of octaves + current_freq = note_to_frequency(current_note) + boopety_beepety.frequency(current_freq) + boopety_beepety.trigger_attack() + last_note_advance = time.ticks_ms() + + graphics.set_pen(black) + graphics.clear() + + graphics.set_pen(playhead) + graphics.line(playhead_position, 0, playhead_position, height) + + graphics.set_pen(cursor_bg) + graphics.line(cursor_position[0], 0, cursor_position[0], height) + + graphics.set_pen(note) + for x in range(width): + y = notes[x] + if y is not None: + graphics.pixel(x, y) + + graphics.set_pen(cursor) + graphics.pixel(*cursor_position) + + su.update(graphics) diff --git a/micropython/examples/stellar_unicorn/nostalgia_prompt.py b/micropython/examples/stellar_unicorn/nostalgia_prompt.py new file mode 100644 index 00000000..8484f9ca --- /dev/null +++ b/micropython/examples/stellar_unicorn/nostalgia_prompt.py @@ -0,0 +1,158 @@ +import time +from stellar import StellarUnicorn +from picographics import PicoGraphics, DISPLAY_STELLAR_UNICORN as DISPLAY + +''' +A collection of copies of classic terminal styles including +C64, MS-DOS, Spectrum, and more. Images and text are drawn +pixel by pixel from a pattern of Os and Xs. +You can adjust the brightness with LUX + and -. +''' + +su = StellarUnicorn() +graphics = PicoGraphics(DISPLAY) + +prompt_x = 0 +prompt_y = 4 + +c64 = [ + " ", + " ", + " OOOOO OOOOOO OO OOOO ", + " OO OO OO OOOO OO OO ", + " OO OO OO OO OO OO OO ", + " OOOOO OOOO OOOOOO OO OO ", + " OOOO OO OO OO OO OO ", + " OO OO OO OO OO OO OO ", + " OO OO OOOOOO OO OO OOOO ", + " ", + " ", + " ", + " ", + " OO OO XXXXXXX ", + " OO OO XXXXXXX ", + " OO OO XXXXXXX ", + " OOOO XXXXXXX ", + " OO XXXXXXX ", + " OO OO XXXXXXX ", + " OO OO XXXXXXX ", + " XXXXXXX ", + " " +] +FOREGROUND_C64 = (230, 210, 250) +BACKGROUND_C64 = (20, 20, 120) + +spectrum = [ + " ", + " ", + " O OOOO OOOO OOOOO ", + " O O O O O O O ", + " O O O O O O O ", + " O O O OOOOOO O O ", + " O O O O O O O ", + " OOOOOO OOOO O O OOOOO ", + " ", + " ", + " ", + " ", + " ", + " O O O O XXXXXXXX ", + " O O O O X XXXXXX ", + " X XXXXXX ", + " X XXXXXX ", + " X XXXXXX ", + " X XXXXXX ", + " X X ", + " XXXXXXXX ", + " " +] +FOREGROUND_SPECTRUM = (0, 0, 0) +BACKGROUND_SPECTRUM = (180, 150, 150) + +bbc_micro = [ + " ", + " ", + " OOOOO OO OOOO OOO ", + " O O O O O O O ", + " O O O O O O ", + " OOOOO O O OOOO O ", + " O O OOOOOO O O ", + " O O O O O O O ", + " OOOOO O O OOOO OOO ", + " ", + " ", + " ", + " ", + " OOOO O ", + " O O O ", + " O O ", + " O O ", + " O O ", + " O O O ", + " OOOO O ", + " XXXXXXX ", + " " +] +FOREGROUND_BBC_MICRO = (255, 255, 255) +BACKGROUND_BBC_MICRO = (0, 0, 0) + +PROMPT_C64 = 0 +PROMPT_SPECTRUM = 1 +PROMPT_BBC_MICRO = 2 +prompt = 0 + + +@micropython.native # noqa: F821 +def draw(image, fg, bg, time_ms): + fg_pen = graphics.create_pen(fg[0], fg[1], fg[2]) + bg_pen = graphics.create_pen(bg[0], bg[1], bg[2]) + graphics.set_pen(bg_pen) + graphics.clear() + for y in range(len(image)): + row = image[y] + for x in range(len(row)): + pixel = row[x] + # draw the prompt text + if pixel == 'O': + graphics.set_pen(fg_pen) + + # draw the caret blinking + elif pixel == 'X' and (time_ms // 300) % 2: + graphics.set_pen(fg_pen) + + else: + graphics.set_pen(bg_pen) + + graphics.pixel(x + prompt_x, y + prompt_y) + + su.update(graphics) + + +su.set_brightness(0.5) + +while True: + + time_ms = time.ticks_ms() + prompt = (time_ms // 3000) % 3 + + if su.is_pressed(StellarUnicorn.SWITCH_BRIGHTNESS_UP): + su.adjust_brightness(+0.01) + + if su.is_pressed(StellarUnicorn.SWITCH_BRIGHTNESS_DOWN): + su.adjust_brightness(-0.01) + + start = time.ticks_ms() + + if prompt == PROMPT_C64: + draw(c64, FOREGROUND_C64, BACKGROUND_C64, time_ms) + + elif prompt == PROMPT_SPECTRUM: + draw(spectrum, FOREGROUND_SPECTRUM, BACKGROUND_SPECTRUM, time_ms) + + elif prompt == PROMPT_BBC_MICRO: + draw(bbc_micro, FOREGROUND_BBC_MICRO, BACKGROUND_BBC_MICRO, time_ms) + + # pause for a moment (important or the USB serial device will fail) + time.sleep(0.001) + + print("total took: {} ms".format(time.ticks_ms() - start)) diff --git a/micropython/examples/stellar_unicorn/numpy/eighties_super_computer.py b/micropython/examples/stellar_unicorn/numpy/eighties_super_computer.py new file mode 100644 index 00000000..96ff4b8a --- /dev/null +++ b/micropython/examples/stellar_unicorn/numpy/eighties_super_computer.py @@ -0,0 +1,115 @@ +import gc +import time +import random +from stellar import StellarUnicorn, Channel +from picographics import PicoGraphics, DISPLAY_STELLAR_UNICORN, PEN_P8 +from ulab import numpy + +""" +A random, computer effect. +Experiment with the damping, number of spawns and intensity to change the effect. +""" + +# MAXIMUM OVERKILL +# machine.freq(250_000_000) + +DAMPING_FACTOR = 0.95 +NUMBER_OF_LIGHTS = 10 +INTENSITY = 20 + +volume = 0.5 + +su = StellarUnicorn() +su.set_brightness(0.5) +graphics = PicoGraphics(DISPLAY_STELLAR_UNICORN, pen_type=PEN_P8) + +boopety_beepety = su.synth_channel(0) +boopety_beepety.configure( + waveforms=Channel.SQUARE | Channel.SINE, + attack=0.1, + decay=0.1, + sustain=0.0, + release=0.5, + volume=volume +) + +su.play_synth() + +# Fill palette with a yellow +r, g, b = (230, 150, 0) +PALETTE_ENTRIES = 255 +for x in range(PALETTE_ENTRIES): + _ = graphics.create_pen(r * x // PALETTE_ENTRIES, g * x // PALETTE_ENTRIES, b) + + +def update(): + computer[:] *= DAMPING_FACTOR + + # Spawn random drops + for _ in range(NUMBER_OF_LIGHTS): + x = random.randint(0, width - 1) + y = random.randint(0, height - 1) + computer[y][x] = random.randint(0, INTENSITY) + + +def draw(): + # Copy the effect to the framebuffer + memoryview(graphics)[:] = numpy.ndarray(numpy.clip(computer, 0, 1) * (PALETTE_ENTRIES - 1), dtype=numpy.uint8).tobytes() + su.update(graphics) + + +width = StellarUnicorn.WIDTH +height = StellarUnicorn.HEIGHT +computer = numpy.zeros((height, width)) + +t_count = 0 +t_total = 0 + + +while True: + if su.is_pressed(StellarUnicorn.SWITCH_BRIGHTNESS_UP): + su.adjust_brightness(+0.01) + + if su.is_pressed(StellarUnicorn.SWITCH_BRIGHTNESS_DOWN): + su.adjust_brightness(-0.01) + + if su.is_pressed(StellarUnicorn.SWITCH_VOLUME_DOWN): + volume -= 0.1 + volume = max(0.0, volume) + boopety_beepety.volume(volume) + + if su.is_pressed(StellarUnicorn.SWITCH_VOLUME_UP): + volume += 0.1 + volume = min(1.0, volume) + boopety_beepety.volume(volume) + + tstart = time.ticks_ms() + gc.collect() + update() + draw() + tfinish = time.ticks_ms() + + # Play random notes between 100 and 880Hz for a computery effect + boopety_beepety.frequency(random.randint(100, 880)) + boopety_beepety.trigger_attack() + + total = tfinish - tstart + t_total += total + t_count += 1 + + if t_count == 60: + per_frame_avg = t_total / t_count + print(f"60 frames in {t_total}ms, avg {per_frame_avg:.02f}ms per frame, {1000/per_frame_avg:.02f} FPS") + t_count = 0 + t_total = 0 + + # pause for a moment (important or the USB serial device will fail) + # try to pace at 60fps or 30fps + if total > 1000 / 30: + time.sleep(0.0001) + elif total > 1000 / 60: + t = 1000 / 30 - total + time.sleep(t / 1000) + else: + t = 1000 / 60 - total + time.sleep(t / 1000) diff --git a/micropython/examples/stellar_unicorn/numpy/fire_effect.py b/micropython/examples/stellar_unicorn/numpy/fire_effect.py new file mode 100644 index 00000000..6cbbc6b9 --- /dev/null +++ b/micropython/examples/stellar_unicorn/numpy/fire_effect.py @@ -0,0 +1,126 @@ +import time +import gc +import random +from stellar import StellarUnicorn +from picographics import PicoGraphics, DISPLAY_STELLAR_UNICORN, PEN_P8 +from ulab import numpy + +""" +Classic fire effect. +Play with the number of spawns, heat, damping factor and colour palette to tweak it. +""" + +# MAXIMUM OVERKILL +# machine.freq(250_000_000) + +su = StellarUnicorn() +su.set_brightness(0.5) +graphics = PicoGraphics(DISPLAY_STELLAR_UNICORN, pen_type=PEN_P8) + +# Number of random fire spawns +FIRE_SPAWNS = 5 + +# Fire damping +DAMPING_FACTOR = 0.98 + +# TURN UP THE HEEEAAT +HEAT = 3.0 + +# Create the fire palette +""" +# Raging Gas Inferno +graphics.create_pen(0, 0, 0) +graphics.create_pen(0, 0, 0) +graphics.create_pen(20, 20, 20) +graphics.create_pen(50, 10, 0) +graphics.create_pen(180, 30, 0) +graphics.create_pen(220, 160, 0) +graphics.create_pen(255, 255, 180) +graphics.create_pen(255, 255, 220) +graphics.create_pen(90, 90, 255) +graphics.create_pen(255, 0, 255) +""" +# Original Colours +graphics.create_pen(0, 0, 0) +graphics.create_pen(20, 20, 20) +graphics.create_pen(180, 30, 0) +graphics.create_pen(220, 160, 0) +graphics.create_pen(255, 255, 180) + +PALETTE_SIZE = 5 # Should match the number of colours defined above + + +def update(): + # Clear the bottom two rows (off screen) + heat[height - 1][:] = 0.0 + heat[height - 2][:] = 0.0 + + # Add random fire spawns + for c in range(FIRE_SPAWNS): + x = random.randint(0, width - 4) + 2 + heat[height - 1][x - 1:x + 1] = HEAT / 2.0 + heat[height - 2][x - 1:x + 1] = HEAT + + # Propagate the fire upwards + a = numpy.roll(heat, -1, axis=0) # y + 1, x + b = numpy.roll(heat, -2, axis=0) # y + 2, x + c = numpy.roll(heat, -1, axis=0) # y + 1 + d = numpy.roll(c, 1, axis=1) # y + 1, x + 1 + e = numpy.roll(c, -1, axis=1) # y + 1, x - 1 + + # Average over 5 adjacent pixels and apply damping + heat[:] += a + b + d + e + heat[:] *= DAMPING_FACTOR / 5.0 + + +def draw(): + # Copy the fire effect to the framebuffer + # Clips the fire to 0.0 to 1.0 + # Multiplies it by the number of palette entries (-1) to turn it into a palette index + # Converts to uint8_t datatype to match the framebuffer + memoryview(graphics)[:] = numpy.ndarray(numpy.clip(heat[0:16, 0:16], 0, 1) * (PALETTE_SIZE - 1), dtype=numpy.uint8).tobytes() + su.update(graphics) + + +width = StellarUnicorn.WIDTH +height = StellarUnicorn.HEIGHT + 4 +heat = numpy.zeros((height, width)) + +t_count = 0 +t_total = 0 + +while True: + gc.collect() + + if su.is_pressed(StellarUnicorn.SWITCH_BRIGHTNESS_UP): + su.adjust_brightness(+0.01) + + if su.is_pressed(StellarUnicorn.SWITCH_BRIGHTNESS_DOWN): + su.adjust_brightness(-0.01) + + tstart = time.ticks_ms() + gc.collect() + update() + draw() + tfinish = time.ticks_ms() + + total = tfinish - tstart + t_total += total + t_count += 1 + + if t_count == 60: + per_frame_avg = t_total / t_count + print(f"60 frames in {t_total}ms, avg {per_frame_avg:.02f}ms per frame, {1000/per_frame_avg:.02f} FPS") + t_count = 0 + t_total = 0 + + # pause for a moment (important or the USB serial device will fail) + # try to pace at 60fps or 30fps + if total > 1000 / 30: + time.sleep(0.0001) + elif total > 1000 / 60: + t = 1000 / 30 - total + time.sleep(t / 1000) + else: + t = 1000 / 60 - total + time.sleep(t / 1000) diff --git a/micropython/examples/stellar_unicorn/numpy/lava_lamp.py b/micropython/examples/stellar_unicorn/numpy/lava_lamp.py new file mode 100644 index 00000000..97d7de3f --- /dev/null +++ b/micropython/examples/stellar_unicorn/numpy/lava_lamp.py @@ -0,0 +1,118 @@ +import gc +import time +import math +import random +from stellar import StellarUnicorn +from picographics import PicoGraphics, DISPLAY_STELLAR_UNICORN, PEN_P8 +from ulab import numpy + +""" +A lava lamp effect, created by blurred, moving particles. +""" + +# MAXIMUM OVERKILL +# machine.freq(250_000_000) + +su = StellarUnicorn() +graphics = PicoGraphics(DISPLAY_STELLAR_UNICORN, pen_type=PEN_P8) +su.set_brightness(0.5) + +width = StellarUnicorn.WIDTH +height = StellarUnicorn.HEIGHT +lava = numpy.zeros((height, width)) + + +class Blob(): + def __init__(self): + self.x = float(random.randint(0, width - 1)) + self.y = float(random.randint(0, height - 1)) + self.r = (float(random.randint(0, 40)) / 10.0) + 5.0 + self.dx = (float(random.randint(0, 2)) / 20.0) - 0.05 + self.dy = (float(random.randint(0, 2)) / 20.0) - 0.025 # positive bias + + def move(self): + self.x += self.dx + self.y += self.dy + + if self.x < 0.0 or self.x >= float(width): + self.x = max(0.0, self.x) + self.x = min(float(width - 1), self.x) + self.dx = -self.dx + + if self.y < 0.0 or self.y >= float(height): + self.y = max(0.0, self.y) + self.y = min(float(height - 1), self.y) + self.dy = -self.dy + + +blobs = [Blob() for _ in range(10)] + + +# Fill palette with a steep falloff from bright red to dark blue +PAL_COLS = 9 +for x in range(PAL_COLS): + graphics.create_pen_hsv(0.5 + math.log(x + 1, PAL_COLS + 1) / 2.0, 1.0, math.log(x + 1, PAL_COLS + 1)) + + +def update(): + # Update the blobs and draw them into the effect + for blob in blobs: + blob.move() + lava[int(blob.y)][int(blob.x)] = blob.r + + # Propogate the blobs outwards + a = numpy.roll(lava, 1, axis=0) + b = numpy.roll(lava, -1, axis=0) + d = numpy.roll(lava, 1, axis=1) + e = numpy.roll(lava, -1, axis=1) + + # Average over 5 adjacent pixels and apply damping + lava[:] += a + b + d + e + lava[:] *= 0.97 / 5.0 + + +def draw(): + # Copy the lava effect to the framebuffer + # Clips to 0.0 - 1.0 + # Multiplies by palette entries (-1) to turn it into a palette index + # Converts to uint8_t datatype to match the framebuffer + memoryview(graphics)[:] = numpy.ndarray(numpy.clip(lava, 0.0, 1.0) * (PAL_COLS - 1), dtype=numpy.uint8).tobytes() + su.update(graphics) + + +t_count = 0 +t_total = 0 + +while True: + if su.is_pressed(StellarUnicorn.SWITCH_BRIGHTNESS_UP): + su.adjust_brightness(+0.01) + + if su.is_pressed(StellarUnicorn.SWITCH_BRIGHTNESS_DOWN): + su.adjust_brightness(-0.01) + + tstart = time.ticks_ms() + gc.collect() + update() + draw() + tfinish = time.ticks_ms() + + total = tfinish - tstart + t_total += total + t_count += 1 + + if t_count == 60: + per_frame_avg = t_total / t_count + print(f"60 frames in {t_total}ms, avg {per_frame_avg:.02f}ms per frame, {1000/per_frame_avg:.02f} FPS") + t_count = 0 + t_total = 0 + + # pause for a moment (important or the USB serial device will fail) + # try to pace at 60fps or 30fps + if total > 1000 / 30: + time.sleep(0.0001) + elif total > 1000 / 60: + t = 1000 / 30 - total + time.sleep(t / 1000) + else: + t = 1000 / 60 - total + time.sleep(t / 1000) diff --git a/micropython/examples/stellar_unicorn/numpy/life.py b/micropython/examples/stellar_unicorn/numpy/life.py new file mode 100644 index 00000000..354c5165 --- /dev/null +++ b/micropython/examples/stellar_unicorn/numpy/life.py @@ -0,0 +1,145 @@ +import gc +import time +import random +from stellar import StellarUnicorn +from picographics import PicoGraphics, DISPLAY_STELLAR_UNICORN, PEN_P8 +from ulab import numpy + +""" +A randomly-seeded game-of-life cellular automata effect. +Experiment with the values below to change the effect. + +Press "A" to manually re-seed. +""" + +# MAXIMUM OVERKILL +# machine.freq(250_000_000) + +INITIAL_LIFE = 500 # Number of live cells to seed +GENERATION_TIME_MS = 50 # MS between generations +MINIMUM_LIFE = 10 # Auto reseed when only this many alive cells remain +SMOOTHED = True # Enable for a more organic if somewhat unsettling feel + +DECAY = 0.90 # Rate at which smoothing effect decays, higher number = more persistent, 1.0 = no decay +TENACITY = 32 # Rate at which smoothing effect increases + +su = StellarUnicorn() +su.set_brightness(0.5) +graphics = PicoGraphics(DISPLAY_STELLAR_UNICORN, pen_type=PEN_P8) + +for c in range(256): + graphics.create_pen(c // 2, 0, c) + + +def update(): + global last_gen + + if SMOOTHED: + duration[:] += life * TENACITY + duration[:] *= DECAY + + if time.ticks_ms() - last_gen < GENERATION_TIME_MS: + return + + last_gen = time.ticks_ms() + + if numpy.sum(life) < MINIMUM_LIFE: + seed_life() + return + + # Rollin' rollin' rollin. + _N = numpy.roll(life, -1, axis=0) + _NW = numpy.roll(_N, -1, axis=1) + _NE = numpy.roll(_N, 1, axis=1) + _S = numpy.roll(life, 1, axis=0) + _SW = numpy.roll(_S, -1, axis=1) + _SE = numpy.roll(_S, 1, axis=1) + _W = numpy.roll(life, -1, axis=1) + _E = numpy.roll(life, 1, axis=1) + + # Compute the total neighbours for each cell + neighbours[:] = _N + _NW + _NE + _S + _SW + _SE + _W + _E + + next_generation[:] = life[:] + + # Any cells with exactly three neighbours should always stay alive + next_generation[:] += neighbours[:] == 3 + + # Any alive cells with less than two neighbours should die + next_generation[:] -= (neighbours[:] < 2) * life + + # Any alive cells with more than three neighbours should die + next_generation[:] -= (neighbours[:] > 3) * life + + life[:] = numpy.clip(next_generation, 0, 1) + + +def draw(): + # Copy the effect to the framebuffer + if SMOOTHED: + memoryview(graphics)[:] = numpy.ndarray(numpy.clip(duration, 0, 255), dtype=numpy.uint8).tobytes() + else: + memoryview(graphics)[:] = numpy.ndarray(life * 255, dtype=numpy.uint8).tobytes() + su.update(graphics) + + +def seed_life(): + for _ in range(INITIAL_LIFE): + x = random.randint(0, width - 1) + y = random.randint(0, height - 1) + life[y][x] = int(True) # Avoid: TypeError: 'bool' object isn't iterable + + +width = StellarUnicorn.WIDTH +height = StellarUnicorn.HEIGHT +life = numpy.zeros((height, width), dtype=numpy.bool) +next_generation = numpy.zeros((height, width), dtype=numpy.bool) +neighbours = numpy.zeros((height, width), dtype=numpy.uint8) +duration = numpy.zeros((height, width)) +last_gen = time.ticks_ms() + +t_count = 0 +t_total = 0 + +seed_life() + + +while True: + if su.is_pressed(StellarUnicorn.SWITCH_BRIGHTNESS_UP): + su.adjust_brightness(+0.01) + + if su.is_pressed(StellarUnicorn.SWITCH_BRIGHTNESS_DOWN): + su.adjust_brightness(-0.01) + + if su.is_pressed(StellarUnicorn.SWITCH_A): + life[:] = int(False) + + if su.is_pressed(StellarUnicorn.SWITCH_B): + SMOOTHED = not SMOOTHED + + tstart = time.ticks_ms() + gc.collect() + update() + draw() + tfinish = time.ticks_ms() + + total = tfinish - tstart + t_total += total + t_count += 1 + + if t_count == 60: + per_frame_avg = t_total / t_count + print(f"60 frames in {t_total}ms, avg {per_frame_avg:.02f}ms per frame, {1000/per_frame_avg:.02f} FPS") + t_count = 0 + t_total = 0 + + # pause for a moment (important or the USB serial device will fail) + # try to pace at 60fps or 30fps + if total > 1000 / 30: + time.sleep(0.0001) + elif total > 1000 / 60: + t = 1000 / 30 - total + time.sleep(t / 1000) + else: + t = 1000 / 60 - total + time.sleep(t / 1000) diff --git a/micropython/examples/stellar_unicorn/numpy/rgb_channels.py b/micropython/examples/stellar_unicorn/numpy/rgb_channels.py new file mode 100644 index 00000000..44d6ce72 --- /dev/null +++ b/micropython/examples/stellar_unicorn/numpy/rgb_channels.py @@ -0,0 +1,95 @@ +import gc +import time +from stellar import StellarUnicorn +from picographics import PicoGraphics, DISPLAY_STELLAR_UNICORN, PEN_RGB888 +from ulab import numpy + +""" +This example demonstrates how to work with full RGB888 colour in ulab/numpy. + +Each colour channel is given its own array, and these are combined before +copying them into the PicoGraphics buffer. + +At great cost to performance (about half the speed) this example works in +floating point 0.0 to 1.0 and converts the result to 8bits per channel. +""" + +# MAXIMUM OVERKILL +# machine.freq(250_000_000) + +su = StellarUnicorn() +su.set_brightness(0.5) +graphics = PicoGraphics(DISPLAY_STELLAR_UNICORN, pen_type=PEN_RGB888) + + +def update(): + # Do something basic with the colour channels + # to prove this actually works. + red[:] = numpy.roll(red, 1, axis=1) + green[:] *= 0.999 # Slowly desaturate green + blue[:] *= 1.001 # Slowly saturate blue + + +def draw(): + # Copy the red, green, blue channels into + # their respective places in the RGB_ array + rgb[2::4] = red.flatten() + rgb[1::4] = green.flatten() + rgb[0::4] = blue.flatten() + + # Convert the results to 8bit RGB and copy to the framebuffer + memoryview(graphics)[:] = numpy.ndarray(numpy.clip(rgb, 0, 1) * 255, dtype=numpy.uint8).tobytes() + + # Copy the framebuffer to Stellar + su.update(graphics) + # Whew! + + +width, height = graphics.get_bounds() + +# Individual channels +red = numpy.zeros((height, width)) +green = numpy.zeros((height, width)) +blue = numpy.zeros((height, width)) + +# Reserved for combined channels +rgb = numpy.zeros((width * height * 4),) + +# Stick some gradients in the channels so we have something to look at +red[::] = numpy.linspace(0, 1, width) + +# There has to be a better way!? +for x in range(width): + green[::, x] = numpy.linspace(0, 1, width) + blue[::, x] = numpy.linspace(1, 0, width,) + +t_count = 0 +t_total = 0 + +while True: + tstart = time.ticks_ms() + gc.collect() + update() + draw() + tfinish = time.ticks_ms() + + total = tfinish - tstart + t_total += total + t_count += 1 + + if t_count == 60: + per_frame_avg = t_total / t_count + print(f"60 frames in {t_total}ms, avg {per_frame_avg:.02f}ms per frame, {1000/per_frame_avg:.02f} FPS") + t_count = 0 + t_total = 0 + + # pause for a moment (important or the USB serial device will fail) + # try to pace at 60fps or 30fps + if total > 1000 / 30: + time.sleep(0.0001) + elif total > 1000 / 60: + t = 1000 / 30 - total + time.sleep(t / 1000) + else: + t = 1000 / 60 - total + time.sleep(t / 1000) diff --git a/micropython/examples/stellar_unicorn/numpy/the_matrix.py b/micropython/examples/stellar_unicorn/numpy/the_matrix.py new file mode 100644 index 00000000..b16f3a96 --- /dev/null +++ b/micropython/examples/stellar_unicorn/numpy/the_matrix.py @@ -0,0 +1,89 @@ +import gc +import time +import random +from stellar import StellarUnicorn +from picographics import PicoGraphics, DISPLAY_STELLAR_UNICORN, PEN_P8 +from ulab import numpy + +""" +HELLO NEO. +""" + +# MAXIMUM OVERKILL +# machine.freq(250_000_000) + +su = StellarUnicorn() +su.set_brightness(1.0) +graphics = PicoGraphics(DISPLAY_STELLAR_UNICORN, pen_type=PEN_P8) + + +# Fill half the palette with GREEEN +for g in range(128): + _ = graphics.create_pen(0, g, 0) + +# And half with bright green for white sparkles +for g in range(128): + _ = graphics.create_pen(128, 128 + g, 128) + + +def update(): + trippy[:] *= 0.65 + + for _ in range(2): + x = random.randint(0, width - 1) + y = random.randint(0, height // 2) + trippy[y][x] = random.randint(128, 255) / 255.0 + + # Propagate downwards + old = numpy.ndarray(trippy) * 0.5 + trippy[:] = numpy.roll(trippy, 1, axis=0) + trippy[:] += old + + +def draw(): + # Copy the effect to the framebuffer + memoryview(graphics)[:] = numpy.ndarray(numpy.clip(trippy, 0, 1) * 254, dtype=numpy.uint8).tobytes() + su.update(graphics) + + +width = StellarUnicorn.WIDTH +height = StellarUnicorn.HEIGHT +trippy = numpy.zeros((height, width)) + +t_count = 0 +t_total = 0 + + +while True: + if su.is_pressed(StellarUnicorn.SWITCH_BRIGHTNESS_UP): + su.adjust_brightness(+0.01) + + if su.is_pressed(StellarUnicorn.SWITCH_BRIGHTNESS_DOWN): + su.adjust_brightness(-0.01) + + tstart = time.ticks_ms() + gc.collect() + update() + draw() + tfinish = time.ticks_ms() + + total = tfinish - tstart + t_total += total + t_count += 1 + + if t_count == 60: + per_frame_avg = t_total / t_count + print(f"60 frames in {t_total}ms, avg {per_frame_avg:.02f}ms per frame, {1000/per_frame_avg:.02f} FPS") + t_count = 0 + t_total = 0 + + # pause for a moment (important or the USB serial device will fail) + # try to pace at 60fps or 30fps + if total > 1000 / 30: + time.sleep(0.0001) + elif total > 1000 / 60: + t = 1000 / 30 - total + time.sleep(t / 1000) + else: + t = 1000 / 60 - total + time.sleep(t / 1000) diff --git a/micropython/examples/stellar_unicorn/numpy/this_is_fine.py b/micropython/examples/stellar_unicorn/numpy/this_is_fine.py new file mode 100644 index 00000000..0af42d75 --- /dev/null +++ b/micropython/examples/stellar_unicorn/numpy/this_is_fine.py @@ -0,0 +1,131 @@ +import time +import gc +import random +from stellar import StellarUnicorn +from picographics import PicoGraphics, DISPLAY_STELLAR_UNICORN, PEN_P8 +from ulab import numpy + +""" +THIS IS FINE! +""" + +# MAXIMUM OVERKILL +# machine.freq(250_000_000) + +su = StellarUnicorn() +su.set_brightness(0.5) +graphics = PicoGraphics(DISPLAY_STELLAR_UNICORN, pen_type=PEN_P8) + +# Number of random fire spawns +FIRE_SPAWNS = 5 + +# Fire damping +DAMPING_FACTOR = 0.98 + +# TURN UP THE HEEEAAT +HEAT = 3.0 + +# Create the fire palette +""" +# Raging Gas Inferno +graphics.create_pen(0, 0, 0) +graphics.create_pen(0, 0, 0) +graphics.create_pen(20, 20, 20) +graphics.create_pen(50, 10, 0) +graphics.create_pen(180, 30, 0) +graphics.create_pen(220, 160, 0) +graphics.create_pen(255, 255, 180) +graphics.create_pen(255, 255, 220) +graphics.create_pen(90, 90, 255) +graphics.create_pen(255, 0, 255) +""" +# Original Colours +graphics.create_pen(0, 0, 0) +graphics.create_pen(20, 20, 20) +graphics.create_pen(180, 30, 0) +graphics.create_pen(220, 160, 0) +graphics.create_pen(255, 255, 180) + +PALETTE_SIZE = 5 # Should match the number of colours defined above + + +def update(): + # Clear the bottom two rows (off screen) + heat[height - 1][:] = 0.0 + heat[height - 2][:] = 0.0 + + # Add random fire spawns + for c in range(FIRE_SPAWNS): + x = random.randint(0, width - 4) + 2 + heat[height - 1][x - 1:x + 1] = HEAT / 2.0 + heat[height - 2][x - 1:x + 1] = HEAT + + # Propagate the fire upwards + a = numpy.roll(heat, -1, axis=0) # y + 1, x + b = numpy.roll(heat, -2, axis=0) # y + 2, x + c = numpy.roll(heat, -1, axis=0) # y + 1 + d = numpy.roll(c, 1, axis=1) # y + 1, x + 1 + e = numpy.roll(c, -1, axis=1) # y + 1, x - 1 + + # Average over 5 adjacent pixels and apply damping + heat[:] += a + b + d + e + heat[:] *= DAMPING_FACTOR / 5.0 + + +def draw(): + # Copy the fire effect to the framebuffer + # Clips the fire to 0.0 to 1.0 + # Multiplies it by the number of palette entries (-1) to turn it into a palette index + # Converts to uint8_t datatype to match the framebuffer + memoryview(graphics)[:] = numpy.ndarray(numpy.clip(heat[0:16, 0:16], 0, 1) * (PALETTE_SIZE - 1), dtype=numpy.uint8).tobytes() + + # Draw text over the top + graphics.set_pen(0) + graphics.text("This", 6, 1, 1, 1) + graphics.text("is", 11, 9, 1, 1) + graphics.text("fine", 6, 17, 1, 1) + su.update(graphics) + + +width = StellarUnicorn.WIDTH +height = StellarUnicorn.HEIGHT + 4 +heat = numpy.zeros((height, width)) + +t_count = 0 +t_total = 0 + +while True: + gc.collect() + + if su.is_pressed(StellarUnicorn.SWITCH_BRIGHTNESS_UP): + su.adjust_brightness(+0.01) + + if su.is_pressed(StellarUnicorn.SWITCH_BRIGHTNESS_DOWN): + su.adjust_brightness(-0.01) + + tstart = time.ticks_ms() + gc.collect() + update() + draw() + tfinish = time.ticks_ms() + + total = tfinish - tstart + t_total += total + t_count += 1 + + if t_count == 60: + per_frame_avg = t_total / t_count + print(f"60 frames in {t_total}ms, avg {per_frame_avg:.02f}ms per frame, {1000/per_frame_avg:.02f} FPS") + t_count = 0 + t_total = 0 + + # pause for a moment (important or the USB serial device will fail) + # try to pace at 60fps or 30fps + if total > 1000 / 30: + time.sleep(0.0001) + elif total > 1000 / 60: + t = 1000 / 30 - total + time.sleep(t / 1000) + else: + t = 1000 / 60 - total + time.sleep(t / 1000) diff --git a/micropython/examples/stellar_unicorn/numpy/trippy.py b/micropython/examples/stellar_unicorn/numpy/trippy.py new file mode 100644 index 00000000..d1049b84 --- /dev/null +++ b/micropython/examples/stellar_unicorn/numpy/trippy.py @@ -0,0 +1,97 @@ +import gc +import time +import random +from stellar import StellarUnicorn +from picographics import PicoGraphics, DISPLAY_STELLAR_UNICORN, PEN_P8 +from ulab import numpy + +""" +A random, trippy effect. +Experiment with the damping, number of spawns, intensity and offset to change the effect. +""" + +# MAXIMUM OVERKILL +# machine.freq(250_000_000) + +su = StellarUnicorn() +su.set_brightness(0.5) +graphics = PicoGraphics(DISPLAY_STELLAR_UNICORN, pen_type=PEN_P8) + + +DAMPING_FACTOR = 0.8 +NUMBER_OF_DROPS = 5 +INTENSITY = 10 +OFFSET = 0.0 # Try 0.5 + +# Fill palette with a rainbow sweep +PALETTE_ENTRIES = 255 +for x in range(PALETTE_ENTRIES): + _ = graphics.create_pen_hsv(float(x) / PALETTE_ENTRIES + OFFSET, 1.0, 1.0) + + +def update(): + trippy[:] *= DAMPING_FACTOR + + # Spawn random drops + for _ in range(NUMBER_OF_DROPS): + x = random.randint(0, width - 1) + y = random.randint(0, height - 1) + trippy[y][x] = random.randint(0, INTENSITY) + + a = numpy.roll(trippy, 1, axis=0) + b = numpy.roll(trippy, -1, axis=0) + d = numpy.roll(trippy, 1, axis=1) + e = numpy.roll(trippy, -1, axis=1) + + # Average over 5 adjacent pixels and apply damping + trippy[:] += a + b + d + e + trippy[:] /= 5.0 + + +def draw(): + # Copy the effect to the framebuffer + memoryview(graphics)[:] = numpy.ndarray(numpy.clip(trippy, 0, 1) * (PALETTE_ENTRIES - 1), dtype=numpy.uint8).tobytes() + su.update(graphics) + + +width = StellarUnicorn.WIDTH +height = StellarUnicorn.HEIGHT +trippy = numpy.zeros((height, width)) + +t_count = 0 +t_total = 0 + + +while True: + if su.is_pressed(StellarUnicorn.SWITCH_BRIGHTNESS_UP): + su.adjust_brightness(+0.01) + + if su.is_pressed(StellarUnicorn.SWITCH_BRIGHTNESS_DOWN): + su.adjust_brightness(-0.01) + + tstart = time.ticks_ms() + gc.collect() + update() + draw() + tfinish = time.ticks_ms() + + total = tfinish - tstart + t_total += total + t_count += 1 + + if t_count == 60: + per_frame_avg = t_total / t_count + print(f"60 frames in {t_total}ms, avg {per_frame_avg:.02f}ms per frame, {1000/per_frame_avg:.02f} FPS") + t_count = 0 + t_total = 0 + + # pause for a moment (important or the USB serial device will fail) + # try to pace at 60fps or 30fps + if total > 1000 / 30: + time.sleep(0.0001) + elif total > 1000 / 60: + t = 1000 / 30 - total + time.sleep(t / 1000) + else: + t = 1000 / 60 - total + time.sleep(t / 1000) diff --git a/micropython/examples/stellar_unicorn/rainbow.py b/micropython/examples/stellar_unicorn/rainbow.py new file mode 100644 index 00000000..5619e0ef --- /dev/null +++ b/micropython/examples/stellar_unicorn/rainbow.py @@ -0,0 +1,115 @@ +import time +import math +from stellar import StellarUnicorn +from picographics import PicoGraphics, DISPLAY_STELLAR_UNICORN as DISPLAY + +''' +Some good old fashioned rainbows! + +You can adjust the cycling speed with A and B, +stripe width with C and D, hue with VOL + and -, +and the brightness with LUX + and -. +The sleep button stops the animation (can be started again with A or B). +''' + +su = StellarUnicorn() +graphics = PicoGraphics(DISPLAY) + +width = StellarUnicorn.WIDTH +height = StellarUnicorn.HEIGHT + + +@micropython.native # noqa: F821 +def from_hsv(h, s, v): + i = math.floor(h * 6.0) + f = h * 6.0 - i + v *= 255.0 + p = v * (1.0 - s) + q = v * (1.0 - f * s) + t = v * (1.0 - (1.0 - f) * s) + + i = int(i) % 6 + if i == 0: + return int(v), int(t), int(p) + if i == 1: + return int(q), int(v), int(p) + if i == 2: + return int(p), int(v), int(t) + if i == 3: + return int(p), int(q), int(v) + if i == 4: + return int(t), int(p), int(v) + if i == 5: + return int(v), int(p), int(q) + + +@micropython.native # noqa: F821 +def draw(): + global hue_offset, phase + phase_percent = phase / 15 + for x in range(width): + colour = hue_map[int((x + (hue_offset * width)) % width)] + for y in range(height): + v = ((math.sin((x + y) / stripe_width + phase_percent) + 1.5) / 2.5) + + graphics.set_pen(graphics.create_pen(int(colour[0] * v), int(colour[1] * v), int(colour[2] * v))) + graphics.pixel(x, y) + + su.update(graphics) + + +hue_map = [from_hsv(x / width, 1.0, 1.0) for x in range(width)] +hue_offset = 0.0 + +animate = True +stripe_width = 3.0 +speed = 1.0 + +su.set_brightness(0.5) + +phase = 0 +while True: + + if animate: + phase += speed + + if su.is_pressed(StellarUnicorn.SWITCH_VOLUME_UP): + hue_offset += 0.01 + hue_offset = 1.0 if hue_offset > 1.0 else hue_offset + + if su.is_pressed(StellarUnicorn.SWITCH_VOLUME_DOWN): + hue_offset -= 0.01 + hue_offset = 0.0 if hue_offset < 0.0 else hue_offset + + if su.is_pressed(StellarUnicorn.SWITCH_BRIGHTNESS_UP): + su.adjust_brightness(+0.01) + + if su.is_pressed(StellarUnicorn.SWITCH_BRIGHTNESS_DOWN): + su.adjust_brightness(-0.01) + + if su.is_pressed(StellarUnicorn.SWITCH_SLEEP): + animate = False + + if su.is_pressed(StellarUnicorn.SWITCH_A): + speed += 0.05 + speed = 10.0 if speed > 10.0 else speed + animate = True + + if su.is_pressed(StellarUnicorn.SWITCH_B): + speed -= 0.05 + speed = 0.0 if speed < 0.0 else speed + animate = True + + if su.is_pressed(StellarUnicorn.SWITCH_C): + stripe_width += 0.05 + stripe_width = 10.0 if stripe_width > 10.0 else stripe_width + + if su.is_pressed(StellarUnicorn.SWITCH_D): + stripe_width -= 0.05 + stripe_width = 1.0 if stripe_width < 1.0 else stripe_width + + start = time.ticks_ms() + + draw() + + print("total took: {} ms".format(time.ticks_ms() - start)) diff --git a/micropython/examples/stellar_unicorn/scrolling_text.py b/micropython/examples/stellar_unicorn/scrolling_text.py new file mode 100644 index 00000000..62356709 --- /dev/null +++ b/micropython/examples/stellar_unicorn/scrolling_text.py @@ -0,0 +1,96 @@ +import time +from stellar import StellarUnicorn +from picographics import PicoGraphics, DISPLAY_STELLAR_UNICORN as DISPLAY + +''' +Display scrolling wisdom, quotes or greetz. + +You can adjust the brightness with LUX + and -. +''' + +# constants for controlling scrolling text +PADDING = 5 +MESSAGE_COLOUR = (255, 255, 255) +OUTLINE_COLOUR = (0, 0, 0) +BACKGROUND_COLOUR = (10, 0, 96) +MESSAGE = "\"Space is big. Really big. You just won't believe how vastly hugely mind-bogglingly big it is. I mean, you may think it's a long way down the road to the chemist, but that's just peanuts to space.\" - Douglas Adams" +HOLD_TIME = 2.0 +STEP_TIME = 0.075 + +# create stellar object and graphics surface for drawing +su = StellarUnicorn() +graphics = PicoGraphics(DISPLAY) + +width = StellarUnicorn.WIDTH +height = StellarUnicorn.HEIGHT + + +# function for drawing outlined text +def outline_text(text, x, y): + graphics.set_pen(graphics.create_pen(int(OUTLINE_COLOUR[0]), int(OUTLINE_COLOUR[1]), int(OUTLINE_COLOUR[2]))) + graphics.text(text, x - 1, y - 1, -1, 1) + graphics.text(text, x, y - 1, -1, 1) + graphics.text(text, x + 1, y - 1, -1, 1) + graphics.text(text, x - 1, y, -1, 1) + graphics.text(text, x + 1, y, -1, 1) + graphics.text(text, x - 1, y + 1, -1, 1) + graphics.text(text, x, y + 1, -1, 1) + graphics.text(text, x + 1, y + 1, -1, 1) + + graphics.set_pen(graphics.create_pen(int(MESSAGE_COLOUR[0]), int(MESSAGE_COLOUR[1]), int(MESSAGE_COLOUR[2]))) + graphics.text(text, x, y, -1, 1) + + +su.set_brightness(0.5) + +# state constants +STATE_PRE_SCROLL = 0 +STATE_SCROLLING = 1 +STATE_POST_SCROLL = 2 + +shift = 0 +state = STATE_PRE_SCROLL + +# set the font +graphics.set_font("bitmap8") + +# calculate the message width so scrolling can happen +msg_width = graphics.measure_text(MESSAGE, 1) + +last_time = time.ticks_ms() + +while True: + time_ms = time.ticks_ms() + + if su.is_pressed(StellarUnicorn.SWITCH_BRIGHTNESS_UP): + su.adjust_brightness(+0.01) + + if su.is_pressed(StellarUnicorn.SWITCH_BRIGHTNESS_DOWN): + su.adjust_brightness(-0.01) + + if state == STATE_PRE_SCROLL and time_ms - last_time > HOLD_TIME * 1000: + if msg_width + PADDING * 2 >= width: + state = STATE_SCROLLING + last_time = time_ms + + if state == STATE_SCROLLING and time_ms - last_time > STEP_TIME * 1000: + shift += 1 + if shift >= (msg_width + PADDING * 2) - width - 1: + state = STATE_POST_SCROLL + last_time = time_ms + + if state == STATE_POST_SCROLL and time_ms - last_time > HOLD_TIME * 1000: + state = STATE_PRE_SCROLL + shift = 0 + last_time = time_ms + + graphics.set_pen(graphics.create_pen(int(BACKGROUND_COLOUR[0]), int(BACKGROUND_COLOUR[1]), int(BACKGROUND_COLOUR[2]))) + graphics.clear() + + outline_text(MESSAGE, x=PADDING - shift, y=4) + + # update the display + su.update(graphics) + + # pause for a moment (important or the USB serial device will fail) + time.sleep(0.001) diff --git a/micropython/examples/stellar_unicorn/stellar_paint/README.md b/micropython/examples/stellar_unicorn/stellar_paint/README.md new file mode 100644 index 00000000..53a45d2b --- /dev/null +++ b/micropython/examples/stellar_unicorn/stellar_paint/README.md @@ -0,0 +1,11 @@ +# Stellar Paint + +Stellar Paint lets you paint pixels onto your Stellar Unicorn over WiFi, in realtime! + +## Setting Up + +You'll need `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 will also have to install `micropython-phew` and `microdot` through Thonny's Tools -> Manage Packages. + +Run the example through Thonny and it should get connected and give you a URL to visit. Open that URL in your browser and start painting! \ No newline at end of file diff --git a/micropython/examples/stellar_unicorn/stellar_paint/stellar_paint.py b/micropython/examples/stellar_unicorn/stellar_paint/stellar_paint.py new file mode 100644 index 00000000..15d4422d --- /dev/null +++ b/micropython/examples/stellar_unicorn/stellar_paint/stellar_paint.py @@ -0,0 +1,113 @@ +import os +from microdot_asyncio import Microdot, send_file +from microdot_asyncio_websocket import with_websocket +from phew import connect_to_wifi +from stellar import StellarUnicorn +from picographics import PicoGraphics, DISPLAY_STELLAR_UNICORN as DISPLAY +from WIFI_CONFIG import SSID, PSK + + +su = StellarUnicorn() +graphics = PicoGraphics(DISPLAY) +mv_graphics = memoryview(graphics) +su.set_brightness(0.5) + +WIDTH, HEIGHT = graphics.get_bounds() + +ip = connect_to_wifi(SSID, PSK) + +print(f"Start painting at: http://{ip}") + + +server = Microdot() + + +@server.route("/", methods=["GET"]) +def route_index(request): + return send_file("stellar_paint/index.html") + + +@server.route("/static/", methods=["GET"]) +def route_static(request, path): + return send_file(f"stellar_paint/static/{path}") + + +def get_pixel(x, y): + if x < WIDTH and y < HEIGHT and x >= 0 and y >= 0: + o = (y * WIDTH + x) * 4 + return tuple(mv_graphics[o:o + 3]) + return None + + +def flood_fill(x, y, r, g, b): + todo = [] + + def fill(x, y, c): + if get_pixel(x, y) != c: + return + + graphics.pixel(x, y) + + up = get_pixel(x, y - 1) + dn = get_pixel(x, y + 1) + lf = get_pixel(x - 1, y) + ri = get_pixel(x + 1, y) + + if up == c: + todo.append((x, y - 1)) + if dn == c: + todo.append((x, y + 1)) + if lf == c: + todo.append((x - 1, y)) + if ri == c: + todo.append((x + 1, y)) + + c = get_pixel(x, y) + + if c is None: + return + + fill(x, y, c) + + while len(todo): + x, y = todo.pop(0) + fill(x, y, c) + + +@server.route('/paint') +@with_websocket +async def echo(request, ws): + while True: + data = await ws.receive() + try: + x, y, r, g, b = [int(n) for n in data[0:5]] + graphics.set_pen(graphics.create_pen(r, g, b)) + graphics.pixel(x, y) + + except ValueError: + if data == "show": + su.update(graphics) + + if data == "fill": + data = await ws.receive() + x, y, r, g, b = [int(n) for n in data[0:5]] + graphics.set_pen(graphics.create_pen(r, g, b)) + flood_fill(x, y, r, g, b) + + if data == "clear": + graphics.set_pen(graphics.create_pen(0, 0, 0)) + graphics.clear() + + if data == "save": + filename = await ws.receive() + print(f"Saving to {filename}.bin") + try: + os.mkdir("saves") + except OSError: + pass + with open(f"saves/{filename}.bin", "wb") as f: + f.write(graphics) + await ws.send(f"alert: Saved to saves/{filename}.bin") + + +server.run(host="0.0.0.0", port=80) diff --git a/micropython/examples/stellar_unicorn/stellar_paint/stellar_paint/index.html b/micropython/examples/stellar_unicorn/stellar_paint/stellar_paint/index.html new file mode 100644 index 00000000..f7c60d08 --- /dev/null +++ b/micropython/examples/stellar_unicorn/stellar_paint/stellar_paint/index.html @@ -0,0 +1,54 @@ + + + + + Stellar Paint + + + + + +
+

Stellar Paint

+ + +
+
+
    +
  • +
  • +
  • +
  • +
  • +
  • +
  • +
  • +
  • +
  • +
  • +
  • +
  • +
  • +
  • +
  • +
+ + +
+
    +
  • +
  • +
  • +
  • +
  • +
  • +
  • +
  • +
+
+ + + + + + diff --git a/micropython/examples/stellar_unicorn/stellar_paint/stellar_paint/static/paint.css b/micropython/examples/stellar_unicorn/stellar_paint/stellar_paint/static/paint.css new file mode 100644 index 00000000..46163157 --- /dev/null +++ b/micropython/examples/stellar_unicorn/stellar_paint/stellar_paint/static/paint.css @@ -0,0 +1,131 @@ +body { + background:#333; + padding:20px; + font-family:Arial, Verdana, Sans-Serif; + background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAaUlEQVQYV33Q0Q3AIAgEUBjBFVyBFRzbWVjBEajXBIOVypcJj1NhETG61BiDVJX4Bh211v5hRDiniV+Elx0wQwd0hEatlUop65srMSah23vf8Auz65AWMc8rDHvCCjAQK2KeDcuQDzh+AHEJX8mbbU1BAAAAAElFTkSuQmCC) repeat; +} + +.icons { + position:absolute; + margin:0; + padding:20px; + list-style:none; +} + +.icons li { + margin:20px; + padding:0; + list-style:none; + padding-top:80px; + width:100px; +} + +.icons li span { + background:#FFF; + color:#000; + border:1px solid #000; + line-height:20px; + padding:5px 10px; + text-align:center; + font-size:10px; + line-height:10px; + display:inline-block; +} + +#palette ul, #palette li { + margin:0;padding:0;list-style:none; +} + +#palette { + list-style:none; + position:relative; + height: 122px; + padding:0 8px; +} + +#palette ul { + display:block; + width:456px; + float: left; +} + +#palette li, #palette input { + border: 2px outset; + width:49px; + height:49px; + float:left; + display:block; + margin:2px; +} + +#palette input { + width:110px; + height:110px; +} + +.window { + width: 640px; + position: relative; + background: #0E071A; + box-shadow: 0px 5px 10px rgba(0, 0, 0, 0.5); +} + +.tools { + margin:0;padding:0;list-style:none; + clear:both; + display:block; + position:absolute; + top: 50px; + right: 8px; + width: 98px; + background:#999999; + font-size:0; +} +.tools span { + line-height:30px; +} + +.tools li { + font-size:16px; + width: 45px; + height: 40px; + text-align:center; + margin:0; + padding:0; + display:inline-block; + line-height:40px; + border:2px outset #EEEEEE; + background:#F5F5F5; + cursor:pointer; + color:#000; +} + +.tools li.selected { + background:#000; + color:#FFF; +} + +h1 { + color: #FFF; + background: #6D38BB; + height:40px; + margin:0; + padding:0 8px; + line-height:40px; + font-weight:normal; + font-size:24px; +} + +table { + clear:both; + cursor:pointer; + margin:10px; + border:1px solid #333; + background: #000000; +} + +table td { + width:14px; + height:14px; + border:1px solid #333; +} \ No newline at end of file diff --git a/micropython/examples/stellar_unicorn/stellar_paint/stellar_paint/static/paint.js b/micropython/examples/stellar_unicorn/stellar_paint/stellar_paint/static/paint.js new file mode 100644 index 00000000..1ddcd1d4 --- /dev/null +++ b/micropython/examples/stellar_unicorn/stellar_paint/stellar_paint/static/paint.js @@ -0,0 +1,216 @@ +'use strict'; + +var WIDTH = 16; +var HEIGHT = 16; +var md = false; +var color = tinycolor('#840000'); +var update; + +$(document).ready(function(){ + + var picker = $('#custom'); + var palette = $('#palette'); + + picker.val(color.toHexString()); + + $(document) + .on('mousedown',function(e){md=true;}) + .on('mouseup',function(e){md=false;}); + + $('table').on('dragstart', function(e){ + e.preventDefault(); + return false; + }); + + for (var y = 0; y < HEIGHT; y++) { + var row = $(''); + for (var x = 0; x < WIDTH; x++) { + row.append(''); + } + $('tbody').append(row); + } + + $('.tools li').on('click', function(){ + switch($(this).index()){ + case 6: + clear(); + break; + case 7: + save(); + break; + default: + $('.tools li').removeClass('selected'); + $(this).addClass('selected'); + break; + } + }); + + picker.on('change', function(){ + color = tinycolor($(this).val()); + }) + + palette.find('li').on('click', function(){ + pick(this); + }); + + function handle_tool(obj, is_click){ + switch($('.tools li.selected').index()){ + case 0: //'paint': + paint(obj); + break; + case 1: // Fill + if( is_click ) fill(obj); + break; + case 2: // Erase + update_pixel(obj, tinycolor('#000000')); + break; + case 3: //'pick': + pick(obj); + break; + case 4: //'lighten': + lighten(obj); + break; + case 5: //'darken': + darken(obj); + break; + } + } + + var fill_target = null; + var fill_stack = []; + function fill(obj){ + fill_target = tinycolor($(obj).css('background-color')).toRgbString(); + + if( fill_target == color.toRgbString() ){ + return false; + } + + var x = $(obj).index(); + var y = $(obj).parent().index(); + + socket.send("fill"); + socket.send(new Uint8Array([x, y, color.toRgb().r, color.toRgb().g, color.toRgb().b])); + socket.send('show'); + + do_fill(obj); + + while(fill_stack.length > 0){ + var pixel = fill_stack.pop(); + do_fill(pixel); + } + } + + function is_target_color(obj){ + return ( tinycolor($(obj).css('background-color')).toRgbString() == fill_target); + } + + function do_fill(obj){ + var obj = $(obj); + + if( is_target_color(obj) ){ + + $(obj).css('background-color', color.toRgbString()); + + var r = obj.next('td'); // Right + var l = obj.prev('td'); // Left + var u = obj.parent().prev('tr').find('td:eq(' + obj.index() + ')'); // Above + var d = obj.parent().next('tr').find('td:eq(' + obj.index() + ')'); // Below + + if( r.length && is_target_color(r[0]) ) fill_stack.push(r[0]); + if( l.length && is_target_color(l[0]) ) fill_stack.push(l[0]); + if( u.length && is_target_color(u[0]) ) fill_stack.push(u[0]); + if( d.length && is_target_color(d[0]) ) fill_stack.push(d[0]); + } + } + + function save(){ + var filename = prompt('Please enter a filename', 'mypaint'); + filename = filename.replace(/[^a-z0-9]/gi, '_').toLowerCase(); + socket.send('save'); + socket.send(filename); + } + + function clear(){ + $('td').css('background-color','rgb(0,0,0)').data('changed',false); + socket.send('clear'); + socket.send('show'); + } + + function lighten(obj){ + var c = tinycolor($(obj).css('background-color')); + c.lighten(5); + update_pixel(obj, c); + } + + function darken(obj){ + var c = tinycolor($(obj).css('background-color')); + c.darken(5); + update_pixel(obj, c); + } + + function pick(obj){ + color = tinycolor($(obj).css('background-color')); + picker.val(color.toHexString()); + } + + function update_pixel(obj, col){ + var bgcol = tinycolor($(obj).css('background-color')); + + if(col != bgcol){ + $(obj) + .data('changed', true) + .css('background-color', col.toRgbString()); + } + } + + function update_pixels(){ + var changed = false; + + $('td').each(function( index, obj ){ + if($(obj).data('changed')){ + $(obj).data('changed',false); + changed = true; + + var x = $(this).index(); + var y = $(this).parent().index(); + var col = tinycolor($(obj).css('background-color')).toRgb(); + + if(socket) { + socket.send(new Uint8Array([x, y, col.r, col.g, col.b])); + } + } + }); + if(changed){ + socket.send('show'); + } + } + + function paint(obj){ + update_pixel(obj, color); + } + + $('table td').on('click', function(){ + handle_tool(this, true); + }); + $('table td').on('mousemove', function(){ + if(!md) return false; + handle_tool(this, false); + }) + + const socket = new WebSocket('ws://' + window.location.host + '/paint'); + socket.addEventListener('message', ev => { + console.log('<<< ' + ev.data); + + if(ev.data.substring(0, 6) == "alert:") { + alert(ev.data.substring(6)); + } + }); + socket.addEventListener('close', ev => { + console.log('<<< closed'); + }); + + socket.addEventListener('open', ev => { + clear(); + update = setInterval(update_pixels, 50); + }); +}); diff --git a/micropython/examples/stellar_unicorn/stellar_paint/stellar_paint/static/tinycolor.js b/micropython/examples/stellar_unicorn/stellar_paint/stellar_paint/static/tinycolor.js new file mode 100644 index 00000000..0d8b0e7f --- /dev/null +++ b/micropython/examples/stellar_unicorn/stellar_paint/stellar_paint/static/tinycolor.js @@ -0,0 +1,1193 @@ +// TinyColor v1.4.2 +// https://github.com/bgrins/TinyColor +// Brian Grinstead, MIT License + +(function(Math) { + + var trimLeft = /^\s+/, + trimRight = /\s+$/, + mathRound = Math.round, + mathMin = Math.min, + mathMax = Math.max, + mathRandom = Math.random; + + function tinycolor (color, opts) { + + color = (color) ? color : ''; + opts = opts || { }; + + // If input is already a tinycolor, return itself + if (color instanceof tinycolor) { + return color; + } + // If we are called as a function, call using new instead + if (!(this instanceof tinycolor)) { + return new tinycolor(color, opts); + } + + var rgb = inputToRGB(color); + this._originalInput = color, + this._r = rgb.r, + this._g = rgb.g, + this._b = rgb.b, + this._a = rgb.a, + this._roundA = mathRound(100*this._a) / 100, + this._format = opts.format || rgb.format; + this._gradientType = opts.gradientType; + + // Don't let the range of [0,255] come back in [0,1]. + // Potentially lose a little bit of precision here, but will fix issues where + // .5 gets interpreted as half of the total, instead of half of 1 + // If it was supposed to be 128, this was already taken care of by `inputToRgb` + if (this._r < 1) { this._r = mathRound(this._r); } + if (this._g < 1) { this._g = mathRound(this._g); } + if (this._b < 1) { this._b = mathRound(this._b); } + + this._ok = rgb.ok; + } + + tinycolor.prototype = { + isDark: function() { + return this.getBrightness() < 128; + }, + isLight: function() { + return !this.isDark(); + }, + isValid: function() { + return this._ok; + }, + getOriginalInput: function() { + return this._originalInput; + }, + getFormat: function() { + return this._format; + }, + getAlpha: function() { + return this._a; + }, + getBrightness: function() { + //http://www.w3.org/TR/AERT#color-contrast + var rgb = this.toRgb(); + return (rgb.r * 299 + rgb.g * 587 + rgb.b * 114) / 1000; + }, + getLuminance: function() { + //http://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef + var rgb = this.toRgb(); + var RsRGB, GsRGB, BsRGB, R, G, B; + RsRGB = rgb.r/255; + GsRGB = rgb.g/255; + BsRGB = rgb.b/255; + + if (RsRGB <= 0.03928) {R = RsRGB / 12.92;} else {R = Math.pow(((RsRGB + 0.055) / 1.055), 2.4);} + if (GsRGB <= 0.03928) {G = GsRGB / 12.92;} else {G = Math.pow(((GsRGB + 0.055) / 1.055), 2.4);} + if (BsRGB <= 0.03928) {B = BsRGB / 12.92;} else {B = Math.pow(((BsRGB + 0.055) / 1.055), 2.4);} + return (0.2126 * R) + (0.7152 * G) + (0.0722 * B); + }, + setAlpha: function(value) { + this._a = boundAlpha(value); + this._roundA = mathRound(100*this._a) / 100; + return this; + }, + toHsv: function() { + var hsv = rgbToHsv(this._r, this._g, this._b); + return { h: hsv.h * 360, s: hsv.s, v: hsv.v, a: this._a }; + }, + toHsvString: function() { + var hsv = rgbToHsv(this._r, this._g, this._b); + var h = mathRound(hsv.h * 360), s = mathRound(hsv.s * 100), v = mathRound(hsv.v * 100); + return (this._a == 1) ? + "hsv(" + h + ", " + s + "%, " + v + "%)" : + "hsva(" + h + ", " + s + "%, " + v + "%, "+ this._roundA + ")"; + }, + toHsl: function() { + var hsl = rgbToHsl(this._r, this._g, this._b); + return { h: hsl.h * 360, s: hsl.s, l: hsl.l, a: this._a }; + }, + toHslString: function() { + var hsl = rgbToHsl(this._r, this._g, this._b); + var h = mathRound(hsl.h * 360), s = mathRound(hsl.s * 100), l = mathRound(hsl.l * 100); + return (this._a == 1) ? + "hsl(" + h + ", " + s + "%, " + l + "%)" : + "hsla(" + h + ", " + s + "%, " + l + "%, "+ this._roundA + ")"; + }, + toHex: function(allow3Char) { + return rgbToHex(this._r, this._g, this._b, allow3Char); + }, + toHexString: function(allow3Char) { + return '#' + this.toHex(allow3Char); + }, + toHex8: function(allow4Char) { + return rgbaToHex(this._r, this._g, this._b, this._a, allow4Char); + }, + toHex8String: function(allow4Char) { + return '#' + this.toHex8(allow4Char); + }, + toRgb: function() { + return { r: mathRound(this._r), g: mathRound(this._g), b: mathRound(this._b), a: this._a }; + }, + toRgbString: function() { + return (this._a == 1) ? + "rgb(" + mathRound(this._r) + ", " + mathRound(this._g) + ", " + mathRound(this._b) + ")" : + "rgba(" + mathRound(this._r) + ", " + mathRound(this._g) + ", " + mathRound(this._b) + ", " + this._roundA + ")"; + }, + toPercentageRgb: function() { + return { r: mathRound(bound01(this._r, 255) * 100) + "%", g: mathRound(bound01(this._g, 255) * 100) + "%", b: mathRound(bound01(this._b, 255) * 100) + "%", a: this._a }; + }, + toPercentageRgbString: function() { + return (this._a == 1) ? + "rgb(" + mathRound(bound01(this._r, 255) * 100) + "%, " + mathRound(bound01(this._g, 255) * 100) + "%, " + mathRound(bound01(this._b, 255) * 100) + "%)" : + "rgba(" + mathRound(bound01(this._r, 255) * 100) + "%, " + mathRound(bound01(this._g, 255) * 100) + "%, " + mathRound(bound01(this._b, 255) * 100) + "%, " + this._roundA + ")"; + }, + toName: function() { + if (this._a === 0) { + return "transparent"; + } + + if (this._a < 1) { + return false; + } + + return hexNames[rgbToHex(this._r, this._g, this._b, true)] || false; + }, + toFilter: function(secondColor) { + var hex8String = '#' + rgbaToArgbHex(this._r, this._g, this._b, this._a); + var secondHex8String = hex8String; + var gradientType = this._gradientType ? "GradientType = 1, " : ""; + + if (secondColor) { + var s = tinycolor(secondColor); + secondHex8String = '#' + rgbaToArgbHex(s._r, s._g, s._b, s._a); + } + + return "progid:DXImageTransform.Microsoft.gradient("+gradientType+"startColorstr="+hex8String+",endColorstr="+secondHex8String+")"; + }, + toString: function(format) { + var formatSet = !!format; + format = format || this._format; + + var formattedString = false; + var hasAlpha = this._a < 1 && this._a >= 0; + var needsAlphaFormat = !formatSet && hasAlpha && (format === "hex" || format === "hex6" || format === "hex3" || format === "hex4" || format === "hex8" || format === "name"); + + if (needsAlphaFormat) { + // Special case for "transparent", all other non-alpha formats + // will return rgba when there is transparency. + if (format === "name" && this._a === 0) { + return this.toName(); + } + return this.toRgbString(); + } + if (format === "rgb") { + formattedString = this.toRgbString(); + } + if (format === "prgb") { + formattedString = this.toPercentageRgbString(); + } + if (format === "hex" || format === "hex6") { + formattedString = this.toHexString(); + } + if (format === "hex3") { + formattedString = this.toHexString(true); + } + if (format === "hex4") { + formattedString = this.toHex8String(true); + } + if (format === "hex8") { + formattedString = this.toHex8String(); + } + if (format === "name") { + formattedString = this.toName(); + } + if (format === "hsl") { + formattedString = this.toHslString(); + } + if (format === "hsv") { + formattedString = this.toHsvString(); + } + + return formattedString || this.toHexString(); + }, + clone: function() { + return tinycolor(this.toString()); + }, + + _applyModification: function(fn, args) { + var color = fn.apply(null, [this].concat([].slice.call(args))); + this._r = color._r; + this._g = color._g; + this._b = color._b; + this.setAlpha(color._a); + return this; + }, + lighten: function() { + return this._applyModification(lighten, arguments); + }, + brighten: function() { + return this._applyModification(brighten, arguments); + }, + darken: function() { + return this._applyModification(darken, arguments); + }, + desaturate: function() { + return this._applyModification(desaturate, arguments); + }, + saturate: function() { + return this._applyModification(saturate, arguments); + }, + greyscale: function() { + return this._applyModification(greyscale, arguments); + }, + spin: function() { + return this._applyModification(spin, arguments); + }, + + _applyCombination: function(fn, args) { + return fn.apply(null, [this].concat([].slice.call(args))); + }, + analogous: function() { + return this._applyCombination(analogous, arguments); + }, + complement: function() { + return this._applyCombination(complement, arguments); + }, + monochromatic: function() { + return this._applyCombination(monochromatic, arguments); + }, + splitcomplement: function() { + return this._applyCombination(splitcomplement, arguments); + }, + triad: function() { + return this._applyCombination(triad, arguments); + }, + tetrad: function() { + return this._applyCombination(tetrad, arguments); + } + }; + + // If input is an object, force 1 into "1.0" to handle ratios properly + // String input requires "1.0" as input, so 1 will be treated as 1 + tinycolor.fromRatio = function(color, opts) { + if (typeof color == "object") { + var newColor = {}; + for (var i in color) { + if (color.hasOwnProperty(i)) { + if (i === "a") { + newColor[i] = color[i]; + } + else { + newColor[i] = convertToPercentage(color[i]); + } + } + } + color = newColor; + } + + return tinycolor(color, opts); + }; + + // Given a string or object, convert that input to RGB + // Possible string inputs: + // + // "red" + // "#f00" or "f00" + // "#ff0000" or "ff0000" + // "#ff000000" or "ff000000" + // "rgb 255 0 0" or "rgb (255, 0, 0)" + // "rgb 1.0 0 0" or "rgb (1, 0, 0)" + // "rgba (255, 0, 0, 1)" or "rgba 255, 0, 0, 1" + // "rgba (1.0, 0, 0, 1)" or "rgba 1.0, 0, 0, 1" + // "hsl(0, 100%, 50%)" or "hsl 0 100% 50%" + // "hsla(0, 100%, 50%, 1)" or "hsla 0 100% 50%, 1" + // "hsv(0, 100%, 100%)" or "hsv 0 100% 100%" + // + function inputToRGB(color) { + + var rgb = { r: 0, g: 0, b: 0 }; + var a = 1; + var s = null; + var v = null; + var l = null; + var ok = false; + var format = false; + + if (typeof color == "string") { + color = stringInputToObject(color); + } + + if (typeof color == "object") { + if (isValidCSSUnit(color.r) && isValidCSSUnit(color.g) && isValidCSSUnit(color.b)) { + rgb = rgbToRgb(color.r, color.g, color.b); + ok = true; + format = String(color.r).substr(-1) === "%" ? "prgb" : "rgb"; + } + else if (isValidCSSUnit(color.h) && isValidCSSUnit(color.s) && isValidCSSUnit(color.v)) { + s = convertToPercentage(color.s); + v = convertToPercentage(color.v); + rgb = hsvToRgb(color.h, s, v); + ok = true; + format = "hsv"; + } + else if (isValidCSSUnit(color.h) && isValidCSSUnit(color.s) && isValidCSSUnit(color.l)) { + s = convertToPercentage(color.s); + l = convertToPercentage(color.l); + rgb = hslToRgb(color.h, s, l); + ok = true; + format = "hsl"; + } + + if (color.hasOwnProperty("a")) { + a = color.a; + } + } + + a = boundAlpha(a); + + return { + ok: ok, + format: color.format || format, + r: mathMin(255, mathMax(rgb.r, 0)), + g: mathMin(255, mathMax(rgb.g, 0)), + b: mathMin(255, mathMax(rgb.b, 0)), + a: a + }; + } + + + // Conversion Functions + // -------------------- + + // `rgbToHsl`, `rgbToHsv`, `hslToRgb`, `hsvToRgb` modified from: + // + + // `rgbToRgb` + // Handle bounds / percentage checking to conform to CSS color spec + // + // *Assumes:* r, g, b in [0, 255] or [0, 1] + // *Returns:* { r, g, b } in [0, 255] + function rgbToRgb(r, g, b){ + return { + r: bound01(r, 255) * 255, + g: bound01(g, 255) * 255, + b: bound01(b, 255) * 255 + }; + } + + // `rgbToHsl` + // Converts an RGB color value to HSL. + // *Assumes:* r, g, and b are contained in [0, 255] or [0, 1] + // *Returns:* { h, s, l } in [0,1] + function rgbToHsl(r, g, b) { + + r = bound01(r, 255); + g = bound01(g, 255); + b = bound01(b, 255); + + var max = mathMax(r, g, b), min = mathMin(r, g, b); + var h, s, l = (max + min) / 2; + + if(max == min) { + h = s = 0; // achromatic + } + else { + var d = max - min; + s = l > 0.5 ? d / (2 - max - min) : d / (max + min); + switch(max) { + case r: h = (g - b) / d + (g < b ? 6 : 0); break; + case g: h = (b - r) / d + 2; break; + case b: h = (r - g) / d + 4; break; + } + + h /= 6; + } + + return { h: h, s: s, l: l }; + } + + // `hslToRgb` + // Converts an HSL color value to RGB. + // *Assumes:* h is contained in [0, 1] or [0, 360] and s and l are contained [0, 1] or [0, 100] + // *Returns:* { r, g, b } in the set [0, 255] + function hslToRgb(h, s, l) { + var r, g, b; + + h = bound01(h, 360); + s = bound01(s, 100); + l = bound01(l, 100); + + function hue2rgb(p, q, t) { + if(t < 0) t += 1; + if(t > 1) t -= 1; + if(t < 1/6) return p + (q - p) * 6 * t; + if(t < 1/2) return q; + if(t < 2/3) return p + (q - p) * (2/3 - t) * 6; + return p; + } + + if(s === 0) { + r = g = b = l; // achromatic + } + else { + var q = l < 0.5 ? l * (1 + s) : l + s - l * s; + var p = 2 * l - q; + r = hue2rgb(p, q, h + 1/3); + g = hue2rgb(p, q, h); + b = hue2rgb(p, q, h - 1/3); + } + + return { r: r * 255, g: g * 255, b: b * 255 }; + } + + // `rgbToHsv` + // Converts an RGB color value to HSV + // *Assumes:* r, g, and b are contained in the set [0, 255] or [0, 1] + // *Returns:* { h, s, v } in [0,1] + function rgbToHsv(r, g, b) { + + r = bound01(r, 255); + g = bound01(g, 255); + b = bound01(b, 255); + + var max = mathMax(r, g, b), min = mathMin(r, g, b); + var h, s, v = max; + + var d = max - min; + s = max === 0 ? 0 : d / max; + + if(max == min) { + h = 0; // achromatic + } + else { + switch(max) { + case r: h = (g - b) / d + (g < b ? 6 : 0); break; + case g: h = (b - r) / d + 2; break; + case b: h = (r - g) / d + 4; break; + } + h /= 6; + } + return { h: h, s: s, v: v }; + } + + // `hsvToRgb` + // Converts an HSV color value to RGB. + // *Assumes:* h is contained in [0, 1] or [0, 360] and s and v are contained in [0, 1] or [0, 100] + // *Returns:* { r, g, b } in the set [0, 255] + function hsvToRgb(h, s, v) { + + h = bound01(h, 360) * 6; + s = bound01(s, 100); + v = bound01(v, 100); + + var i = Math.floor(h), + f = h - i, + p = v * (1 - s), + q = v * (1 - f * s), + t = v * (1 - (1 - f) * s), + mod = i % 6, + r = [v, q, p, p, t, v][mod], + g = [t, v, v, q, p, p][mod], + b = [p, p, t, v, v, q][mod]; + + return { r: r * 255, g: g * 255, b: b * 255 }; + } + + // `rgbToHex` + // Converts an RGB color to hex + // Assumes r, g, and b are contained in the set [0, 255] + // Returns a 3 or 6 character hex + function rgbToHex(r, g, b, allow3Char) { + + var hex = [ + pad2(mathRound(r).toString(16)), + pad2(mathRound(g).toString(16)), + pad2(mathRound(b).toString(16)) + ]; + + // Return a 3 character hex if possible + if (allow3Char && hex[0].charAt(0) == hex[0].charAt(1) && hex[1].charAt(0) == hex[1].charAt(1) && hex[2].charAt(0) == hex[2].charAt(1)) { + return hex[0].charAt(0) + hex[1].charAt(0) + hex[2].charAt(0); + } + + return hex.join(""); + } + + // `rgbaToHex` + // Converts an RGBA color plus alpha transparency to hex + // Assumes r, g, b are contained in the set [0, 255] and + // a in [0, 1]. Returns a 4 or 8 character rgba hex + function rgbaToHex(r, g, b, a, allow4Char) { + + var hex = [ + pad2(mathRound(r).toString(16)), + pad2(mathRound(g).toString(16)), + pad2(mathRound(b).toString(16)), + pad2(convertDecimalToHex(a)) + ]; + + // Return a 4 character hex if possible + if (allow4Char && hex[0].charAt(0) == hex[0].charAt(1) && hex[1].charAt(0) == hex[1].charAt(1) && hex[2].charAt(0) == hex[2].charAt(1) && hex[3].charAt(0) == hex[3].charAt(1)) { + return hex[0].charAt(0) + hex[1].charAt(0) + hex[2].charAt(0) + hex[3].charAt(0); + } + + return hex.join(""); + } + + // `rgbaToArgbHex` + // Converts an RGBA color to an ARGB Hex8 string + // Rarely used, but required for "toFilter()" + function rgbaToArgbHex(r, g, b, a) { + + var hex = [ + pad2(convertDecimalToHex(a)), + pad2(mathRound(r).toString(16)), + pad2(mathRound(g).toString(16)), + pad2(mathRound(b).toString(16)) + ]; + + return hex.join(""); + } + + // `equals` + // Can be called with any tinycolor input + tinycolor.equals = function (color1, color2) { + if (!color1 || !color2) { return false; } + return tinycolor(color1).toRgbString() == tinycolor(color2).toRgbString(); + }; + + tinycolor.random = function() { + return tinycolor.fromRatio({ + r: mathRandom(), + g: mathRandom(), + b: mathRandom() + }); + }; + + + // Modification Functions + // ---------------------- + // Thanks to less.js for some of the basics here + // + + function desaturate(color, amount) { + amount = (amount === 0) ? 0 : (amount || 10); + var hsl = tinycolor(color).toHsl(); + hsl.s -= amount / 100; + hsl.s = clamp01(hsl.s); + return tinycolor(hsl); + } + + function saturate(color, amount) { + amount = (amount === 0) ? 0 : (amount || 10); + var hsl = tinycolor(color).toHsl(); + hsl.s += amount / 100; + hsl.s = clamp01(hsl.s); + return tinycolor(hsl); + } + + function greyscale(color) { + return tinycolor(color).desaturate(100); + } + + function lighten (color, amount) { + amount = (amount === 0) ? 0 : (amount || 10); + var hsl = tinycolor(color).toHsl(); + hsl.l += amount / 100; + hsl.l = clamp01(hsl.l); + return tinycolor(hsl); + } + + function brighten(color, amount) { + amount = (amount === 0) ? 0 : (amount || 10); + var rgb = tinycolor(color).toRgb(); + rgb.r = mathMax(0, mathMin(255, rgb.r - mathRound(255 * - (amount / 100)))); + rgb.g = mathMax(0, mathMin(255, rgb.g - mathRound(255 * - (amount / 100)))); + rgb.b = mathMax(0, mathMin(255, rgb.b - mathRound(255 * - (amount / 100)))); + return tinycolor(rgb); + } + + function darken (color, amount) { + amount = (amount === 0) ? 0 : (amount || 10); + var hsl = tinycolor(color).toHsl(); + hsl.l -= amount / 100; + hsl.l = clamp01(hsl.l); + return tinycolor(hsl); + } + + // Spin takes a positive or negative amount within [-360, 360] indicating the change of hue. + // Values outside of this range will be wrapped into this range. + function spin(color, amount) { + var hsl = tinycolor(color).toHsl(); + var hue = (hsl.h + amount) % 360; + hsl.h = hue < 0 ? 360 + hue : hue; + return tinycolor(hsl); + } + + // Combination Functions + // --------------------- + // Thanks to jQuery xColor for some of the ideas behind these + // + + function complement(color) { + var hsl = tinycolor(color).toHsl(); + hsl.h = (hsl.h + 180) % 360; + return tinycolor(hsl); + } + + function triad(color) { + var hsl = tinycolor(color).toHsl(); + var h = hsl.h; + return [ + tinycolor(color), + tinycolor({ h: (h + 120) % 360, s: hsl.s, l: hsl.l }), + tinycolor({ h: (h + 240) % 360, s: hsl.s, l: hsl.l }) + ]; + } + + function tetrad(color) { + var hsl = tinycolor(color).toHsl(); + var h = hsl.h; + return [ + tinycolor(color), + tinycolor({ h: (h + 90) % 360, s: hsl.s, l: hsl.l }), + tinycolor({ h: (h + 180) % 360, s: hsl.s, l: hsl.l }), + tinycolor({ h: (h + 270) % 360, s: hsl.s, l: hsl.l }) + ]; + } + + function splitcomplement(color) { + var hsl = tinycolor(color).toHsl(); + var h = hsl.h; + return [ + tinycolor(color), + tinycolor({ h: (h + 72) % 360, s: hsl.s, l: hsl.l}), + tinycolor({ h: (h + 216) % 360, s: hsl.s, l: hsl.l}) + ]; + } + + function analogous(color, results, slices) { + results = results || 6; + slices = slices || 30; + + var hsl = tinycolor(color).toHsl(); + var part = 360 / slices; + var ret = [tinycolor(color)]; + + for (hsl.h = ((hsl.h - (part * results >> 1)) + 720) % 360; --results; ) { + hsl.h = (hsl.h + part) % 360; + ret.push(tinycolor(hsl)); + } + return ret; + } + + function monochromatic(color, results) { + results = results || 6; + var hsv = tinycolor(color).toHsv(); + var h = hsv.h, s = hsv.s, v = hsv.v; + var ret = []; + var modification = 1 / results; + + while (results--) { + ret.push(tinycolor({ h: h, s: s, v: v})); + v = (v + modification) % 1; + } + + return ret; + } + + // Utility Functions + // --------------------- + + tinycolor.mix = function(color1, color2, amount) { + amount = (amount === 0) ? 0 : (amount || 50); + + var rgb1 = tinycolor(color1).toRgb(); + var rgb2 = tinycolor(color2).toRgb(); + + var p = amount / 100; + + var rgba = { + r: ((rgb2.r - rgb1.r) * p) + rgb1.r, + g: ((rgb2.g - rgb1.g) * p) + rgb1.g, + b: ((rgb2.b - rgb1.b) * p) + rgb1.b, + a: ((rgb2.a - rgb1.a) * p) + rgb1.a + }; + + return tinycolor(rgba); + }; + + + // Readability Functions + // --------------------- + // false + // tinycolor.isReadable("#000", "#111",{level:"AA",size:"large"}) => false + tinycolor.isReadable = function(color1, color2, wcag2) { + var readability = tinycolor.readability(color1, color2); + var wcag2Parms, out; + + out = false; + + wcag2Parms = validateWCAG2Parms(wcag2); + switch (wcag2Parms.level + wcag2Parms.size) { + case "AAsmall": + case "AAAlarge": + out = readability >= 4.5; + break; + case "AAlarge": + out = readability >= 3; + break; + case "AAAsmall": + out = readability >= 7; + break; + } + return out; + + }; + + // `mostReadable` + // Given a base color and a list of possible foreground or background + // colors for that base, returns the most readable color. + // Optionally returns Black or White if the most readable color is unreadable. + // *Example* + // tinycolor.mostReadable(tinycolor.mostReadable("#123", ["#124", "#125"],{includeFallbackColors:false}).toHexString(); // "#112255" + // tinycolor.mostReadable(tinycolor.mostReadable("#123", ["#124", "#125"],{includeFallbackColors:true}).toHexString(); // "#ffffff" + // tinycolor.mostReadable("#a8015a", ["#faf3f3"],{includeFallbackColors:true,level:"AAA",size:"large"}).toHexString(); // "#faf3f3" + // tinycolor.mostReadable("#a8015a", ["#faf3f3"],{includeFallbackColors:true,level:"AAA",size:"small"}).toHexString(); // "#ffffff" + tinycolor.mostReadable = function(baseColor, colorList, args) { + var bestColor = null; + var bestScore = 0; + var readability; + var includeFallbackColors, level, size ; + args = args || {}; + includeFallbackColors = args.includeFallbackColors ; + level = args.level; + size = args.size; + + for (var i= 0; i < colorList.length ; i++) { + readability = tinycolor.readability(baseColor, colorList[i]); + if (readability > bestScore) { + bestScore = readability; + bestColor = tinycolor(colorList[i]); + } + } + + if (tinycolor.isReadable(baseColor, bestColor, {"level":level,"size":size}) || !includeFallbackColors) { + return bestColor; + } + else { + args.includeFallbackColors=false; + return tinycolor.mostReadable(baseColor,["#fff", "#000"],args); + } + }; + + + // Big List of Colors + // ------------------ + // + var names = tinycolor.names = { + aliceblue: "f0f8ff", + antiquewhite: "faebd7", + aqua: "0ff", + aquamarine: "7fffd4", + azure: "f0ffff", + beige: "f5f5dc", + bisque: "ffe4c4", + black: "000", + blanchedalmond: "ffebcd", + blue: "00f", + blueviolet: "8a2be2", + brown: "a52a2a", + burlywood: "deb887", + burntsienna: "ea7e5d", + cadetblue: "5f9ea0", + chartreuse: "7fff00", + chocolate: "d2691e", + coral: "ff7f50", + cornflowerblue: "6495ed", + cornsilk: "fff8dc", + crimson: "dc143c", + cyan: "0ff", + darkblue: "00008b", + darkcyan: "008b8b", + darkgoldenrod: "b8860b", + darkgray: "a9a9a9", + darkgreen: "006400", + darkgrey: "a9a9a9", + darkkhaki: "bdb76b", + darkmagenta: "8b008b", + darkolivegreen: "556b2f", + darkorange: "ff8c00", + darkorchid: "9932cc", + darkred: "8b0000", + darksalmon: "e9967a", + darkseagreen: "8fbc8f", + darkslateblue: "483d8b", + darkslategray: "2f4f4f", + darkslategrey: "2f4f4f", + darkturquoise: "00ced1", + darkviolet: "9400d3", + deeppink: "ff1493", + deepskyblue: "00bfff", + dimgray: "696969", + dimgrey: "696969", + dodgerblue: "1e90ff", + firebrick: "b22222", + floralwhite: "fffaf0", + forestgreen: "228b22", + fuchsia: "f0f", + gainsboro: "dcdcdc", + ghostwhite: "f8f8ff", + gold: "ffd700", + goldenrod: "daa520", + gray: "808080", + green: "008000", + greenyellow: "adff2f", + grey: "808080", + honeydew: "f0fff0", + hotpink: "ff69b4", + indianred: "cd5c5c", + indigo: "4b0082", + ivory: "fffff0", + khaki: "f0e68c", + lavender: "e6e6fa", + lavenderblush: "fff0f5", + lawngreen: "7cfc00", + lemonchiffon: "fffacd", + lightblue: "add8e6", + lightcoral: "f08080", + lightcyan: "e0ffff", + lightgoldenrodyellow: "fafad2", + lightgray: "d3d3d3", + lightgreen: "90ee90", + lightgrey: "d3d3d3", + lightpink: "ffb6c1", + lightsalmon: "ffa07a", + lightseagreen: "20b2aa", + lightskyblue: "87cefa", + lightslategray: "789", + lightslategrey: "789", + lightsteelblue: "b0c4de", + lightyellow: "ffffe0", + lime: "0f0", + limegreen: "32cd32", + linen: "faf0e6", + magenta: "f0f", + maroon: "800000", + mediumaquamarine: "66cdaa", + mediumblue: "0000cd", + mediumorchid: "ba55d3", + mediumpurple: "9370db", + mediumseagreen: "3cb371", + mediumslateblue: "7b68ee", + mediumspringgreen: "00fa9a", + mediumturquoise: "48d1cc", + mediumvioletred: "c71585", + midnightblue: "191970", + mintcream: "f5fffa", + mistyrose: "ffe4e1", + moccasin: "ffe4b5", + navajowhite: "ffdead", + navy: "000080", + oldlace: "fdf5e6", + olive: "808000", + olivedrab: "6b8e23", + orange: "ffa500", + orangered: "ff4500", + orchid: "da70d6", + palegoldenrod: "eee8aa", + palegreen: "98fb98", + paleturquoise: "afeeee", + palevioletred: "db7093", + papayawhip: "ffefd5", + peachpuff: "ffdab9", + peru: "cd853f", + pink: "ffc0cb", + plum: "dda0dd", + powderblue: "b0e0e6", + purple: "800080", + rebeccapurple: "663399", + red: "f00", + rosybrown: "bc8f8f", + royalblue: "4169e1", + saddlebrown: "8b4513", + salmon: "fa8072", + sandybrown: "f4a460", + seagreen: "2e8b57", + seashell: "fff5ee", + sienna: "a0522d", + silver: "c0c0c0", + skyblue: "87ceeb", + slateblue: "6a5acd", + slategray: "708090", + slategrey: "708090", + snow: "fffafa", + springgreen: "00ff7f", + steelblue: "4682b4", + tan: "d2b48c", + teal: "008080", + thistle: "d8bfd8", + tomato: "ff6347", + turquoise: "40e0d0", + violet: "ee82ee", + wheat: "f5deb3", + white: "fff", + whitesmoke: "f5f5f5", + yellow: "ff0", + yellowgreen: "9acd32" + }; + + // Make it easy to access colors via `hexNames[hex]` + var hexNames = tinycolor.hexNames = flip(names); + + + // Utilities + // --------- + + // `{ 'name1': 'val1' }` becomes `{ 'val1': 'name1' }` + function flip(o) { + var flipped = { }; + for (var i in o) { + if (o.hasOwnProperty(i)) { + flipped[o[i]] = i; + } + } + return flipped; + } + + // Return a valid alpha value [0,1] with all invalid values being set to 1 + function boundAlpha(a) { + a = parseFloat(a); + + if (isNaN(a) || a < 0 || a > 1) { + a = 1; + } + + return a; + } + + // Take input from [0, n] and return it as [0, 1] + function bound01(n, max) { + if (isOnePointZero(n)) { n = "100%"; } + + var processPercent = isPercentage(n); + n = mathMin(max, mathMax(0, parseFloat(n))); + + // Automatically convert percentage into number + if (processPercent) { + n = parseInt(n * max, 10) / 100; + } + + // Handle floating point rounding errors + if ((Math.abs(n - max) < 0.000001)) { + return 1; + } + + // Convert into [0, 1] range if it isn't already + return (n % max) / parseFloat(max); + } + + // Force a number between 0 and 1 + function clamp01(val) { + return mathMin(1, mathMax(0, val)); + } + + // Parse a base-16 hex value into a base-10 integer + function parseIntFromHex(val) { + return parseInt(val, 16); + } + + // Need to handle 1.0 as 100%, since once it is a number, there is no difference between it and 1 + // + function isOnePointZero(n) { + return typeof n == "string" && n.indexOf('.') != -1 && parseFloat(n) === 1; + } + + // Check to see if string passed in is a percentage + function isPercentage(n) { + return typeof n === "string" && n.indexOf('%') != -1; + } + + // Force a hex value to have 2 characters + function pad2(c) { + return c.length == 1 ? '0' + c : '' + c; + } + + // Replace a decimal with it's percentage value + function convertToPercentage(n) { + if (n <= 1) { + n = (n * 100) + "%"; + } + + return n; + } + + // Converts a decimal to a hex value + function convertDecimalToHex(d) { + return Math.round(parseFloat(d) * 255).toString(16); + } + // Converts a hex value to a decimal + function convertHexToDecimal(h) { + return (parseIntFromHex(h) / 255); + } + + var matchers = (function() { + + // + var CSS_INTEGER = "[-\\+]?\\d+%?"; + + // + var CSS_NUMBER = "[-\\+]?\\d*\\.\\d+%?"; + + // Allow positive/negative integer/number. Don't capture the either/or, just the entire outcome. + var CSS_UNIT = "(?:" + CSS_NUMBER + ")|(?:" + CSS_INTEGER + ")"; + + // Actual matching. + // Parentheses and commas are optional, but not required. + // Whitespace can take the place of commas or opening paren + var PERMISSIVE_MATCH3 = "[\\s|\\(]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")\\s*\\)?"; + var PERMISSIVE_MATCH4 = "[\\s|\\(]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")\\s*\\)?"; + + return { + CSS_UNIT: new RegExp(CSS_UNIT), + rgb: new RegExp("rgb" + PERMISSIVE_MATCH3), + rgba: new RegExp("rgba" + PERMISSIVE_MATCH4), + hsl: new RegExp("hsl" + PERMISSIVE_MATCH3), + hsla: new RegExp("hsla" + PERMISSIVE_MATCH4), + hsv: new RegExp("hsv" + PERMISSIVE_MATCH3), + hsva: new RegExp("hsva" + PERMISSIVE_MATCH4), + hex3: /^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/, + hex6: /^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/, + hex4: /^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/, + hex8: /^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/ + }; + })(); + + // `isValidCSSUnit` + // Take in a single string / number and check to see if it looks like a CSS unit + // (see `matchers` above for definition). + function isValidCSSUnit(color) { + return !!matchers.CSS_UNIT.exec(color); + } + + // `stringInputToObject` + // Permissive string parsing. Take in a number of formats, and output an object + // based on detected format. Returns `{ r, g, b }` or `{ h, s, l }` or `{ h, s, v}` + function stringInputToObject(color) { + + color = color.replace(trimLeft,'').replace(trimRight, '').toLowerCase(); + var named = false; + if (names[color]) { + color = names[color]; + named = true; + } + else if (color == 'transparent') { + return { r: 0, g: 0, b: 0, a: 0, format: "name" }; + } + + // Try to match string input using regular expressions. + // Keep most of the number bounding out of this function - don't worry about [0,1] or [0,100] or [0,360] + // Just return an object and let the conversion functions handle that. + // This way the result will be the same whether the tinycolor is initialized with string or object. + var match; + if ((match = matchers.rgb.exec(color))) { + return { r: match[1], g: match[2], b: match[3] }; + } + if ((match = matchers.rgba.exec(color))) { + return { r: match[1], g: match[2], b: match[3], a: match[4] }; + } + if ((match = matchers.hsl.exec(color))) { + return { h: match[1], s: match[2], l: match[3] }; + } + if ((match = matchers.hsla.exec(color))) { + return { h: match[1], s: match[2], l: match[3], a: match[4] }; + } + if ((match = matchers.hsv.exec(color))) { + return { h: match[1], s: match[2], v: match[3] }; + } + if ((match = matchers.hsva.exec(color))) { + return { h: match[1], s: match[2], v: match[3], a: match[4] }; + } + if ((match = matchers.hex8.exec(color))) { + return { + r: parseIntFromHex(match[1]), + g: parseIntFromHex(match[2]), + b: parseIntFromHex(match[3]), + a: convertHexToDecimal(match[4]), + format: named ? "name" : "hex8" + }; + } + if ((match = matchers.hex6.exec(color))) { + return { + r: parseIntFromHex(match[1]), + g: parseIntFromHex(match[2]), + b: parseIntFromHex(match[3]), + format: named ? "name" : "hex" + }; + } + if ((match = matchers.hex4.exec(color))) { + return { + r: parseIntFromHex(match[1] + '' + match[1]), + g: parseIntFromHex(match[2] + '' + match[2]), + b: parseIntFromHex(match[3] + '' + match[3]), + a: convertHexToDecimal(match[4] + '' + match[4]), + format: named ? "name" : "hex8" + }; + } + if ((match = matchers.hex3.exec(color))) { + return { + r: parseIntFromHex(match[1] + '' + match[1]), + g: parseIntFromHex(match[2] + '' + match[2]), + b: parseIntFromHex(match[3] + '' + match[3]), + format: named ? "name" : "hex" + }; + } + + return false; + } + + function validateWCAG2Parms(parms) { + // return valid WCAG2 parms for isReadable. + // If input parms are invalid, return {"level":"AA", "size":"small"} + var level, size; + parms = parms || {"level":"AA", "size":"small"}; + level = (parms.level || "AA").toUpperCase(); + size = (parms.size || "small").toLowerCase(); + if (level !== "AA" && level !== "AAA") { + level = "AA"; + } + if (size !== "small" && size !== "large") { + size = "small"; + } + return {"level":level, "size":size}; + } + + // Node: Export function + if (typeof module !== "undefined" && module.exports) { + module.exports = tinycolor; + } + // AMD/requirejs: Define the module + else if (typeof define === 'function' && define.amd) { + define(function () {return tinycolor;}); + } + // Browser: Expose to window + else { + window.tinycolor = tinycolor; + } + + })(Math); \ No newline at end of file diff --git a/micropython/examples/stellar_unicorn/today.py b/micropython/examples/stellar_unicorn/today.py new file mode 100644 index 00000000..436e125a --- /dev/null +++ b/micropython/examples/stellar_unicorn/today.py @@ -0,0 +1,128 @@ +import time +import network +import ntptime +import machine + +from stellar import StellarUnicorn +from picographics import PicoGraphics, DISPLAY_STELLAR_UNICORN as DISPLAY + +su = StellarUnicorn() +graphics = PicoGraphics(DISPLAY) + +# Default Brightness +su.set_brightness(0.4) + +# You will need to create or update the file secrets.py with your network credentials using Thonny +# in order for the example to update using the NTP. + +# secrets.py should contain: +# WIFI_SSID = "" +# WIFI_PASSWORD = "" + +try: + from secrets import WIFI_SSID, WIFI_PASSWORD +except ImportError: + print("Create secrets.py with your WiFi credentials") + + +WIDTH = StellarUnicorn.WIDTH +HEIGHT = StellarUnicorn.HEIGHT + +rtc = machine.RTC() + +DAYS = ["Mon", "Tue", "Wed", "Thur", "Fri", "Sat", "Sun"] + +# Enable the Wireless +wlan = network.WLAN(network.STA_IF) +wlan.active(True) + + +def network_connect(SSID, PSK): + + # Number of attempts to make before timeout + max_wait = 5 + + # Sets the Wireless LED pulsing and attempts to connect to your local network. + print("connecting...") + 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) + + # Handle connection error. Switches the Warn LED on. + if wlan.status() != 3: + print("Unable to connect. Attempting connection again") + + +# Function to sync the Pico RTC using NTP +def sync_time(): + + try: + network_connect(WIFI_SSID, WIFI_PASSWORD) + except NameError: + print("Create secrets.py with your WiFi credentials") + + if wlan.status() < 0 or wlan.status() >= 3: + try: + ntptime.settime() + except OSError: + print("Unable to sync with NTP server. Check network and try again.") + + +def init(): + + sync_time() + + +def draw(): + + # Pens + RED = graphics.create_pen(120, 0, 0) + WHITE = graphics.create_pen(255, 255, 255) + + current_t = rtc.datetime() + + # Set the pen to Red and clear the screen. + graphics.set_pen(WHITE) + graphics.clear() + + # Measures the length of the text to help us with centring later. + day_length = graphics.measure_text(DAYS[current_t[3]], 1) + date_length = graphics.measure_text(str(current_t[2]), 1) + + graphics.set_font("bitmap6") + graphics.set_pen(RED) + graphics.rectangle(0, 0, WIDTH, 7) + graphics.set_pen(WHITE) + graphics.text(DAYS[current_t[3]], (WIDTH // 2) - (day_length // 2) - 1, 0, 16, 1) + + graphics.set_pen(RED) + graphics.set_font("bitmap6") + graphics.text(str(current_t[2]), (WIDTH // 2) - (date_length // 2) + 1, 8, 16, 1) + + graphics.set_pen(graphics.create_pen(0, 0, 0)) + + su.update(graphics) + + +init() + +while 1: + + # Adjust Brightness +/- + if su.is_pressed(StellarUnicorn.SWITCH_BRIGHTNESS_UP): + su.adjust_brightness(+0.01) + + if su.is_pressed(StellarUnicorn.SWITCH_BRIGHTNESS_DOWN): + su.adjust_brightness(-0.01) + + # Connect to the network and sync with the NTP server + if su.is_pressed(StellarUnicorn.SWITCH_C): + sync_time() + + draw() diff --git a/micropython/examples/stellar_unicorn/weather/icons/cloud1.jpg b/micropython/examples/stellar_unicorn/weather/icons/cloud1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7c400e81f4e89910c388c9fa5cc751ed95ab3e0c GIT binary patch literal 1151 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 literal 0 HcmV?d00001 diff --git a/micropython/examples/stellar_unicorn/weather/icons/rain2.jpg b/micropython/examples/stellar_unicorn/weather/icons/rain2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5fe470c72d496ef8a7ce8d4dc7537932a3fe3cf2 GIT binary patch literal 2100 zcmbu-c{H2p9tZF@5=$&K5rlM6RW;U%W%Md?RLj^p)7k|o2DR0ihN)$$n4;yhjjaw^ zu~zN7$xum9rPHyLqL##{itSCSyols-=bpLe+`sO*^Lx(o$8(^9& z#?smnfWctEwSxf&rU8q?)!(YakiQjhcv$cPK#2h-fs=69DL@DXgQH*qCZGfWFyTM5 zfqxfF2#ydI5fwv;BX-XTv!-^IOvW)*ar|OVHstdxd{4-x9F(|S-sm2 zo{FhlXy}oec;MK_J*xT`mcau{r_QD^FAC^8YbGV;AZ+_X@hIV!jT`&Ot*OmPV`!5&j zz$Ju0z!9Q{E|?JcV8T%dVP%|%jQJH&?+ElMz1w247alxq=s~LJUj^lSB8SE0PaCjQ zxrelW$^JK3(*Gs<3-%w^6d(zQ9XuW!1>ga`@5(lYkBkxkhtE+$_``Gh=&f}dSxS+>5Wnq=S1Ytc} z3uQR(m3XL)EfvUV&y!~Z71Z(kQ{%}!c+u!?u{Q!h%&@C0wzMLhG>3f;r5Owu2CwO{ zCmi|o;We6Ol`T2Io9w5(w*MjL(M*BDhE`Z&ol;67((G6q3M3UX1VD-zdX$Eqe{HtT z-yLL3*?Ja85Na;7YBv^dhw0oXy*J}&k}Fg4QbphT^lgimi`_;1LcHB1L^`(7lPJmE z;oGzQ#Pg**_R@9R?NqhoZ7Ke-aZ$00J6^`EXIx({<S=_PD z3Ud-)imm40zRZyq5fgIWpH|7@W0~HPzo^%^H?J&k+wE+7tk4+@`okxmb`6((RaQ0F zPBw2pnvXFC#RWh-kDD+j06uGMY0c!6R4cwOnGEQJTs5k{AY~ zQvf8>Y%9#Gp{QQ0P8Dk=(YixVBGlpH_-eo?$z!k}5D@m8vkZ;A8 zJ-KsnY_+98Y`t`ZH_)7Bu;*#YA3tkm^igH=_aP_?*8>@{*U8K{`(8JG#9a?m?>UYE zZs7Y&f{CX`mgx|`N%FTAw+XtBhF{(*$S#D^AVX(2G}m+KeOVow$>3RJj(0G(U*wwJ zQ1#b?^^UXg6z?U6Bm2}|4c-1li0G%KrHmz2S;<`}3_?B&YQf4>t{4|nx*O}(Lb(}R z=b;QWW>_+|sNnec(~82Q%q}cfiZWH466Z$UStgw*t_|GUzKMx#?YyrOLXy(|ezb+4 zX!35Y?&0|Q?(E8Ttu&-9x~3LU$5R8J>{EE(6sd4A$vO#IG2M1Vnb$^Jb97m7FNQCT zjybY!e`9>ws^j24lBrOxbCHs)DKBYDLcymoHshT~BJQzaP0dzK`J@-e{l@(OQhab2 zsX7?9BC~89N^3f0f2*pWwU2k2_@SZo!pO{-ta2hJRJ^^hxxF3NH=Ccx{!W;a#x-(j z1@Vk5=|p;Hwd*n;4KguvaymMEIk$mY!pi8%?2(T4F_J0Xapc-)^CV5Vs%IgT$oz4E z5Ih)5z1|f{%N-}a_tK3&9(S7Sa5p?(0LVdM*-JY-Jx-oR$7a1^dX2h|X^{g(00h2$ z)n1Uw@y`n>QJ{GRx=1^p8?rZ%cf-1dB)Fu-`e`N3d!X52is7jQg3bU$#pL>p^jgv) z7t`pURBa26-rIeS%XhwBpB>RiwL*&5N2}X(X4TnQrgL>sv6FxK+=%BE)&>t@=spdvYnt#D?8PL*^e7(BR!8RPN;k^I_h3mMZOpEU0$pd7Z(Ae5jL&3OOa@|Q zE$C>dGagQ82l17G^aVE(1{s>izzT7SYN?38Z%w3)d1(%XInxwo7)Bz_+;Q YSxqSm)MYQa8C{=LePx#^00fi&1lYIJH2?qr literal 0 HcmV?d00001 diff --git a/micropython/examples/stellar_unicorn/weather/icons/rain3.jpg b/micropython/examples/stellar_unicorn/weather/icons/rain3.jpg new file mode 100644 index 0000000000000000000000000000000000000000..91be10a9dc810584d65b3f337a5c77a81cd9891b GIT binary patch literal 2091 zcmbu+do-=@r+0VP)Ki>5`?{}@w`$|WpQ^0O_ z7dICG27>|TwiX~A2aax^{!(p+{H1{H%hEOgr2y;&_QGKXfD8%-N5P~#Ko0<5a(`q4 z|1OveTvl#}yaGZ|X=|WjToT!_3#jEajV{Y*|symBLznwEYuBQxh-?k{=y1@{Xdl|C+eQckaUT3h$LzM=6& zQ!|IlYwzfM)zv*PIP`XSWc1w_UoicTnUAw`pM>Db>gO+OU!nDlZ5IrH|M_Hp!2ZjH z+H%Rr%ED#kw_Pw9^45lmJf|pHL$v-u*doC!x}ci(Arz17IwK`QGK5c z-w52M{Y&=0!LI*bvOi(}ag71WaM;%8!BGGXkc7{FMN1HIQb4v^#9Sm!ZTd~;cOqkW z_GGRJKIJPn1SO(TpFJ4(LO=^K?m-Q8mBPZIwMJ$+bh$$?MLav)e91DZFt03z{xS-K zZzxs~zSk3@me&lSGvDxHcc_RdzL1toX5-}JTNQexfPyWnEb(!9CTSA$9?G!kvW;G} z5{&ptIQ@$)CKVp!$WU^`!NtvwdG|gPX)K$@P?&mD3c^u42_+)YxKe=Xs5;o9I`zhJ zNwU$y9rM5!DFrdS_|KMSuEm)B?eXmocx=APqgRI3Zu_nseKpfsA}PjszJo~G%WR4= zxGp&%h)}$b4E~mB@xs%{RNW)@LSj-};>>!8eZzPDuVxEc)J=@(U_lbg*P;L$agh%W zt!hjveWAHd!ozt3ZLuS2p(_hy#i2W)$}hiX1U{di248r3uw7MeaXC2;RyJ%ug&Qsy z3(mN|opT!E_9#k$WHFdBDFs9>I4TawLO?y!kqiBFi0L+zqB%#NFaZgFPx7eJLw&OE zO^x@*tn$zs>Sb{;x;0OtF=qF5CTdL4jm^b*<&hD#Wi4mXDZ%-AZ`3Gdb{W_sj}o}t zW+{-`;!*BY3B|Qz%qsXFC~i$wNwI34N zSazMd6NJSFXFI%>)G7a7A27lRGma?ehCDCj)~IjUHITo@(ETz_!vv}eS%uRo&`8=AAsE|HMdrv~eT zG_j+L%$(t+jfwfMgI-eLdcTMO~xDshOYqmrJK2%bpGXUuJk@SH-Y zTB#x&@8r$={7Ge&X0eM_G($!x5eV1jI|_ALxp*y4=xkw3hWIFjxbqr0c8{(m>&FGN zc=JUHhIL1RPN?7=!Y$BH#ERa1uZW4eC&A>uF0=&-Q8_U{VY0q z5tsr`Z_aS%Nu~6j&>rtBOnn^Y7vsSwHB0}Pai$7|O_EqcSKD-VgC{!MdS$qbgjrsR z$q?3|oWRUv#23-1g{FOTSG*c^pOow^@nBr}*51G}8i-Lbxwyax6D{OaDcfohRaf9^ z9h)tVhb7~Ik{fif&x^iz%1+2FMxKgAOwF6TIl{F+-tj=+W7GRIU5I&B@if}6dOXO< zK~hS8qm4S&Pt0qMN24v?OyM-JghvT4+p8&@{X{yEr6g)h#~z;QRrusLVFc=BhSd%x z;mX)ME3ZD8fM{K;+8U5>YpZcb&#ivoGt!EC3Ov3MkU(*9jn-(XeJS3zVGSMRcbJ<` zJ8FwM5Ac`FaxF)1n_2I!(P$Yl;C!s(*$0s9GGE7G&30L^XA%k`{KaPp-ad>20=_mr z_y>bv@%u;tQzzCz+spMn7-DDD<-;9dINvLCl`lBM?#L-I4(UNs83ZH&?e72DLEF=p PSnlF=+j!6mCw=!%5gW-L literal 0 HcmV?d00001 diff --git a/micropython/examples/stellar_unicorn/weather/icons/rain4.jpg b/micropython/examples/stellar_unicorn/weather/icons/rain4.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9e676daf20e4affa57ba670afc9274356ec35947 GIT binary patch literal 2053 zcmbu+c{JPU8VB%SB$g;`V@sQA71h?#+OL^3$(^4|n4eC_g49B|msbeUzS&`8X?|R#5niUQ}H2yy^wBy5?nV z9h=kK(%RPE@usJ@uYZ8|esE}lKl!hzkJB@=bHbI?PiyNN;O5qz3j#p@e6l}a|KmdJ zx+GvQC`@|K1(6`{ZYTmKsctBxV0l@Z5UQwQ6epu}AuX@639f1EEKv5l(JiZT!el~A zxJUc9?Eiz^`Af1tVgGdv0di2t?&m=ffCV7(pWi@=;9+6_Rv}<42958yPiC~qhcu(f z99{hF4UP{&keg8cv}es1Ift^xRaceHW%aGsFzDcoR{nU9+d$nlLsC|HVMx&{5(;0P zqcHbDZ5y$?t_fcGjNi5gvl-g~lo)c8g>*PehAReSOdATL3h2oZW2g^cl1YbY@S+i) z=PF`%FV^c8W66O8GV#pf&d2nJBiVXtXI+1DH3KzH@F&PVu)8Kgng@r z#`dk{+Kxf}n|m5VdCG*91+r}4eS+L8?J}L;x-X0SJ0vn423;6~(*f|-v&|L&F?D}pWo*|Fw3Vr_>jr~;(F^lHL zhkKw_n~_^8g<&DbDxU?T&OSamTB(Pusmn>H2NF#S>)nvIy))Fhlw%9ck}&7;BRQNp zF%VymrCXMQVJ)b$#SBJ z!;VY$aTvUJ>iM^#D!Jd7o;4acmo76W( zZ%a>iR=AGFPzlp6`*&z9ItFc1pf6ERPaPFeY^!`BhXgMnF;NOd^XObE>p5fbx-ez^ z9GG&dIV2wSB>U(<9zEwsa|22!PaVoli1wsy&PE)|Ee~4XxQUFauDf^kT7Gi$_=wlp4Za5ddmU*!!4xz~-R2A0n~y!yk@(PH{=FnXlnm!X!{ z)WVBoIWJ1F)Xy1i`XycTAq+1xsN+@uk1IQTroWcz@FXp2^2FXOpi7IJ=RqvjqX#EL8MbO_a#kX(Fr#~m+mB&L56nBH1 z%(BguuD`^W#aOn8RKN8U0|#yO>2YN_{OkW@uqGz=%$uol;N+eprOTxIts$D6+Kv4DO)FT^yGXVuH(9vo;vpucK;s*c@pzFahiDlaYxcCQ%5S)Lcsin>%0*4;trb>T>)LD#BT z^BFg;vrx6V&aEnle@2>Y;DJ7q-LTvqj6+K*x&>Geofo7UQT zSxjkgO$d{D-D_*gfyo&dMa7`%_Y0Oy8ig4$U?6AnfFhf6(gV|@wCV+Zs5y{&CBK-U zp2Hm;H@w{FzD3bs^;LNOG;9A_&N-#j=NDRXz00KNB-_sjGo4(e$I+(Flz?_vX(x(K i`H{gIbWfx0AU_D>Xz{BHeQ|z*+K;9(I-IcL!QTOuyT7gg literal 0 HcmV?d00001 diff --git a/micropython/examples/stellar_unicorn/weather/icons/snow1.jpg b/micropython/examples/stellar_unicorn/weather/icons/snow1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..de769f956703ff6b8be4465b1e67b3676373c1c0 GIT binary patch literal 2128 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 RBfAN6M(dlIxhzpK{Vyb-`Op9W literal 0 HcmV?d00001 diff --git a/micropython/examples/stellar_unicorn/weather/icons/snow3.jpg b/micropython/examples/stellar_unicorn/weather/icons/snow3.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b85dc3b2662348326d4ee724492ab1b30e9baf00 GIT binary patch literal 2223 zcmbu-do+~m8UXNb3=I)YF5}jWyLPrjE|ECh2uaAEkfuq+j3lDQbrxmDB{@ZeOo^e1 z5ke(TIa8`&VJtY{_(8$d7rg@@AJyXr%p#i5{T#H6E0jNB_>_EoJpZ(W#?QkD7;Zre6ytVZe~&@4>@3p=TNHdUydb^RUgPI0*G${w0B#EO=@47UflL`fYS0d$ z<}jcqdlZLG349=w0~3xi=Z7FUKa?%gY>i|#Z?xK7G%~|YQpyk>GLx_vv3M6@@dlx4 zHL*!?(0a~V4wTu9h4R?4SMCN~XL{19+oiP(a^j?I z1q&tl>8{STxDcV6@l$a5R&iV2>_iIr#2{VvvSlPD{_W1Nv&E7UNJFZF6&ZoQfyJWY z+!U-GiNkJl%AGse@4AD)3+g}Pz}Rxv*NH2bo{TwMwhOW-H@`JgD?YnuTt^TdFY@uA z%nx|U0fZ#&^VEJf<6yr`H1gc(~`i$R`?KYswXKB%v0;^wWev+KEmJg6MNSw&;y`4{f zSkw8WSq@};6}#;W=iPEsqk83Y-l;&o)pYVYQEkNZjvd@#d$Pv6NRIE2gO@#1jTGhi zm?DxS*KV4x%E3JAkv(-MV8qrMvmB>qF*>0s?%*SChaQQ_^UO>Mo5raSWai+$&zbF( zDWk3D#bp1X7VG4I$Gv~2FZOnFkG}L{C**nAMruaWOP=%L zBxOO^%=kXkSiU-`zymTMi1h2ekAMW0Q5brDR!rs=E#R$$ZU)0?y>)_qAAe<0RT2uO=30+CD#F+Y%eB z#Te{#?Jd@zcHt*o4&8-x)xwhGfGWQ$@LBWr`rrq^C-Ip+)iVF0h+ETkV{d4o1-wSh zs)iZWP*v|6%Kb03KI4R=jDXC8gdQTzByT%frwL40keZRfn^BUHbdlfM859d8>jIZq z9m_3zgV>m5>K$T7OGCu$DmBjPzB`rJ-818$yik{>PV%1tOfj#t5uD#RaAPKQCHoO zir>+WRbzsF)OHkq9pVu9p}2t*!84g!`SXnsIk){(btFa|+etG3W3pQE0@(wCrbtu> zz5A-vlNl^ zI?6??W}zLxbnj`ndAcF4e%OIpuI;0l&8`ayS=uz*jW^ADaOJL`r;u@+@1kXhcQv%c zZe@GWELtMAdtQTb!lIJu>C+f0%|e_FZJQ=@=n$Iz{z<|sjDKUQ1{;gIhMQp-6?Bf2 z_GK^iDNf*UUfT2RqSD+DuedjpLX>;tKR3s{+qyDd3O!sS!(lm!n{=3l20xBeB-e}6 zrZ^Wbu*{$KpQ(5Ct9&~i8Wkc8BeXd_F&C9r3tuI~Skw|?v}eYGOZEmeCYaF>gYiC7 zO45T-y?tmB#=jZrx-lIA9ZOHR6GGGcDx-O+5q_gKHPBvt>%wDJL)KfG zCoy{|Q~QP-zI8n9j~7JOZ<)0pkDjYvnE&i~{a3^Lx;a5$A;Smb%DSP(E`CH)U6)RJ znc(WH8xD=nqwC=|Ohb-A{#7fz^aq9bd#A)-c(&r6h8y~0Dy*YN8L!I|U*RjW%MaKu tmp;HEXSaT(JL4OEr0zcvyOl)Lz34$Pp*b=S9Naj_Idr*Dz(dN%{{>EY2KE2| literal 0 HcmV?d00001 diff --git a/micropython/examples/stellar_unicorn/weather/icons/snow4.jpg b/micropython/examples/stellar_unicorn/weather/icons/snow4.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8ae9bd6af3d2affd9c44eb265b5ed77fc179d1f8 GIT binary patch literal 2169 zcmbu+e>Bto9tZHxY<^psF^uR-S$^eWzA9^ODatM@KSm>3Qi!yWeKkaE61l0!M0fcy znjiW4l^*=Kb%3p@=Q)q}TlrEs z4XpR}-0ulMAQ0fl>H?ILfX7<*r)n+aPX(-9Rz3g_8o)+iBNSo*s30Iv1VkwS%mDzR z`g=C;?}Dg6VXA8C8k$<#s{^g;0Tl=osse+ms={Ea)2XZH01Tn3_mkr;HRM5xx&>9= zDdP%T!*X|1uYuo)Y?JfRGntxNC_^J-^kyr}7Hb=<%g?Ucw(s!Rv)9vW-+piZfIw1E z@F8-`jE;O8%kOUqy7-@dQ8AOQ4_C;J`t zUoOO|O9cjl!qnGX5S8TB4Mo6Ie{xjQ+jUT#LPc6QWoYQ{zQS(m)wFc>lNlU6Gopps zgq3Ut*J!`V{x?|W|0Vka_8*rB(1Ai$KM#rk+<}#obbCp>9&JjH;*fkIdP#IQUe2iH$WzPw}mmla<6bdeH}`8sGR^ySXU zGvOk)X(r=T501tSAMIzyNng3~)3#mZi;-)BQuk@N0ra`B4Ui1~rG4scyO@ekjuGsh#EkS(nq8OP zW6P6rvZANCeB&^Det6aQZINP=BlxcV(8ybFXm(QdOTx$wQc!uo1Igzl1}zruUDB!< z!KRa(3~XQUF-)$(c@D;A@r~Vav(pNO63FB!fnJ=8IYaDzOUiryNp@z|SMHD`(P%Bm zMbMnaeTi_IxsW6(Te|ZGL9BzhyS48%J?O1<=d-E0o-1{ttp&EX-03)tj`;|YaAOD@bt4OKR9PNz&eocpN)18 zsp0xoYtcvV-pd*A^>(TCtB&q5Y2wKWa&bi0OWiJ2Q}W~kvRC=V7h2UvA6`@hp?Z?2 z$u+byZig)vyv{km;ZBC*rKHit+6tc#A2gkuRp?`izZ92HK}%RMxRt4(g0_dj#$KPJ zv-{+1sktiM8dPJej%O#;$5>Y)I^6RIwl|WX(3X-M>w4&SAIb@|v3`+MgPvk#pUwLpdfhxDOXiVwwbcbJ;Qn6qOT}JDU(;->4g-P@JhqS)n_h$AEeKs|nd&agI$=4Vx zN3!Uxn-7j%$MqL?2++ls%AbR(&t?(VJ^bk~O6MdM=Kv7~y$hla&R9~HZ%%e)R=ULf+?%h)3dVl*Ye zrEWoYqVf&SU(#j14#OZn6^hXy1=P*P4(Wwq7*oM#Wv$EXor}&iFM+%0yJsO%(CH`g z!rx{c8pTo#me|dspN#%_cL~kOjsV+p=?R9t4!Vjx;|hob?kq5KY;FVfNh!QsoU81M z7j4sv?p-tEvNzpi`5=uDrmm45M*@5R}iLT!~_3XEJcCjFZ5K!K1NCG8WNq6_8x zIE&PM^17%t58=>JksWH8D|rhVE@#MICu@kVr8vAS_Aeqx+%}wFE>aC+Grzh$J|T~ldGzM vbJ@|vK-JBApK7oyD#P_5&vShlX5|=7zc#~tfq7!R{Lr0lgUz*Mj&kB(gd)(H literal 0 HcmV?d00001 diff --git a/micropython/examples/stellar_unicorn/weather/icons/storm1.jpg b/micropython/examples/stellar_unicorn/weather/icons/storm1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d7ac6251071627f7786f880888b74fc460426512 GIT binary patch literal 1360 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 literal 0 HcmV?d00001 diff --git a/micropython/examples/stellar_unicorn/weather/icons/storm3.jpg b/micropython/examples/stellar_unicorn/weather/icons/storm3.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6081f046654ef0891ff4a29e6c9958699875cbd1 GIT binary patch literal 1835 zcmbu+eKga190%~Bc8 zKp+rc|J(vp{eb&?_d_)w@0V4o_AiiY-e-{J_ zgCo?C>L?A(xq+&M02Bg)LE$h20uG;>J~wv`z%dA&m3Eud76pVOjbj$uCtkUwZnC+& zK{xOfXu3AyWD-h4Pk)I4*38^um8F$~qm#4Cy7lf`wt8&a?&%e@GdLu47k+o-frE#l zqKSu3#gb0Pk>k&tzd)lWGg4ABuU^Z#o}F`pSx{KSF6P|6Q&PdNtg5c5z5k%GsrgAu z>#tASI)z=`J+FKFL=vg&-O%vJ`wye?iOJ7XUw&8oF+J~s0I=^*_6_zg7iP`{g~MTR z>l2PEBW105Uvgk+FTE`r^%3Zk0EnOx6a1x)CQ|Y3P|cNX+E(w7+Ek z4VLu3WZz-`xJ1AL7-a7AU>IN{FdI2G~O1@)5!ixP^7e4>sLCyaI7pc&FBscy-RNBl<95siYm zMEs|5(b+=#=cbME*r+&#KPS60lA+WPzv0ts*C3f9n&@Xj~Cd*d3C)^cF& z&EV8$qY$Y;;8%?r7R zv@K+t30ZK>ef|UffpoI%E#|6sgvm$pk8Zlv@{UV3O4KP*yf`gEkDO|xHV~GA7);3$xNCX zG};K;FB@l&(gr_0-y@EDoWE4L13cd7?R2xMJ|)ad7~9_KPzX0^r;j&To;$u->GIq#5$$!kg-Ix{Wjab!1cPrCdVs)*gQE;ct zpRNKR6xUK}^TtYxPs_6NK_*$&Xy^i>n=XK~?7RtzCD_U!X)gEsDy?UTbKCV}6q!f- z6``|BGZ7Cegu7J$p)Svjt;=qqfp4ia3TmeCGdhWYs6VpNU@wts)J^}5nvQeV8-3>nn5*HbOeC+PM4>*`yGpHnnpIQwpJIUjvJoz2poO-$z;_4To4 T2TP)@9m0bHZdyAnRrUP|Es;5$ literal 0 HcmV?d00001 diff --git a/micropython/examples/stellar_unicorn/weather/icons/storm4.jpg b/micropython/examples/stellar_unicorn/weather/icons/storm4.jpg new file mode 100644 index 0000000000000000000000000000000000000000..98cdc59c01e66aff906414d311da0631845bc5ec GIT binary patch literal 1703 zcmbu+cTm%37zglQ7GVe&ro>W|Eee)EKqTrR%HGpBhzx<)T1AKuWgI9Ff`S7?gF+>Q zAS*&xD#{cTWQJu>2#E|q$}b|CB9KDu*}MMLtIvDyAJ4td`@Q?TuXtF@12#I?*xLXQ z2n0C3wgB-6V7=abSFMM9SHSvZaRY#o1hxQMU=U3Jih{sU5OE8j0RRxh*KFYLfjMXEZ0+nFJiWYqd=KFd2L_!84hbcm z438k4i;ND1A{}uuejrU!P|++sp)q!!Ve1{7e9R#{js#}f&j2@Pxck|FBfXf1%<<5aOAoR z0*zeTFcciI)mTEo${pz+rl^UDmsHw+?Ou7Kl$MDHs2p&%S6W5ebX-TcPWwyt-(U&< zOZE-+kBbXzfI-$i4~7Ev0jq(ti>|9unPLDwS-JACs$2c%9CKk(Ij=skUhar3XAvK5 ztdg^8Ak?5>hv$t9BT9dq@~-MPeWE+!QeJW4ji=`m4%SRIyxlEV(h-{+5GL92rxktvQZ@juSi;Va^QGq)HW)tyq z^V|z`Os94e866TSa%E=q1yYu!d9T??wL6hC-mb^#XGB$*VWkF}h!b^|*4x`A(v6vp zvk}HhVfa>a7=>jm@9cTGVu|FR?m_aFJGM9dN9BpwEmOBjID1$ZTi(95^hs?EGP&zb zeK`{VidaQmv8{}4eGNS=o=lJYJG7Q|g=W*=W(cBpVjv+~3^bbWi1U9u4;2+R_!$o_ zhk$H8Vw9Q3HK)=!$<#V5-`QYvyMTEzi*_*QHe==!tvXkG==`aboXmn3itm;Z8E>vr z=8RZzdb2B=V=+;dys^vC2-Zk6sRxHFjmA~Urf&<7E0BU4s# zvu8(bx_mZ&!D6Da$Wh8x-X<+Piw9PMqT1a@MOSYo#`r7+xDTSa3a~=OJHy0@^pM9buTnj z13e;`r;t*|<~on?A{%lyFS~)Kn;gyVR@JBY>2M-ChE3^kt&XJmWc?Vo zuFv4O85*0EOO~MDcMJg+V;a98qK|;N?*+n0{7U{ZUc2Ucv<7ING6kAnGGsB>xqM_x zJ(}nnD+{(ImR)%)l?N`Hp=&5Ocm5u$fznf^W3qJ;mQQTHIB~G@v+}AFEpFgQYDkkw ztC3#XG33>8P)V@Ehe*Q(tNCNlr2O%fTsh6cL2F9aR-eS9H{@>(bWZbs8aU&|autqB zN39D~nTq5Gor(7T-U%8O=$F$u1-m=4XHKu3grf70GEl}!d2MNM`0JNRZd zeGrH3)m$(o@2%RL66^$3wuhHi%Xd;pbEgS8-Oe255H`l~P_}Dt37r#hL5*bjf)&K> ztDLr{Hal07U#gh7E2@_k9fYcvQ=hYXLm#$hT6i(g*b^=a#SSLkS}t^IctMeVqnBGI uzksf+osr>jj$n*cwkUM&lJ;=RPEF15Jc$YW^=O$xXY2gUPmf=VU;PQ#^YlRg literal 0 HcmV?d00001 diff --git a/micropython/examples/stellar_unicorn/weather/icons/sun1.jpg b/micropython/examples/stellar_unicorn/weather/icons/sun1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b989b43259ddb2b4df2b6b3f4064e081146d76e4 GIT binary patch literal 2273 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 MFrZ=KSdDV}pF0fxb^rhX literal 0 HcmV?d00001 diff --git a/micropython/examples/stellar_unicorn/weather/icons_sourcefile(doesn't need copying).svg b/micropython/examples/stellar_unicorn/weather/icons_sourcefile(doesn't need copying).svg new file mode 100644 index 00000000..8eedbcb0 --- /dev/null +++ b/micropython/examples/stellar_unicorn/weather/icons_sourcefile(doesn't need copying).svg @@ -0,0 +1,1577 @@ + + + + diff --git a/micropython/examples/stellar_unicorn/weather/weather.py b/micropython/examples/stellar_unicorn/weather/weather.py new file mode 100644 index 00000000..7cc00674 --- /dev/null +++ b/micropython/examples/stellar_unicorn/weather/weather.py @@ -0,0 +1,153 @@ +import time +from stellar import StellarUnicorn +from picographics import PicoGraphics, DISPLAY_STELLAR_UNICORN as DISPLAY +import WIFI_CONFIG +from network_manager import NetworkManager +import uasyncio as asyncio +import urequests +import jpegdec + +# Set your latitude/longitude here (find yours by right clicking in Google Maps!) +LAT = 53.38609085276884 +LNG = -1.4239983439328177 +TIMEZONE = "auto" # determines time zone from lat/long + +URL = "http://api.open-meteo.com/v1/forecast?latitude=" + str(LAT) + "&longitude=" + str(LNG) + "¤t_weather=true&timezone=" + TIMEZONE +WEATHER_TEXT = '' +user_icon = None + + +def get_data(): + global WEATHER_TEXT, temperature, weathercode + print(f"Requesting URL: {URL}") + r = urequests.get(URL) + # open the json data + j = r.json() + print("Data obtained!") + print(j) + + # parse relevant data from JSON + current = j["current_weather"] + temperature = current["temperature"] + windspeed = current["windspeed"] + winddirection = calculate_bearing(current["winddirection"]) + weathercode = current["weathercode"] + date, now = current["time"].split("T") + WEATHER_TEXT = f"Temp: {temperature}°C Wind Speed: {windspeed}kmph Wind Direction: {winddirection} As of: {date}, {now}" + print(WEATHER_TEXT) + r.close() + + +def calculate_bearing(d): + # calculates a compass direction from the wind direction in degrees + dirs = ['N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW'] + ix = round(d / (360. / len(dirs))) + return dirs[ix % len(dirs)] + + +def status_handler(mode, status, ip): + global MESSAGE + print("Network: {}".format(WIFI_CONFIG.SSID)) + + +# create galactic object and graphics surface for drawing +su = StellarUnicorn() +display = PicoGraphics(DISPLAY) + +WIDTH = StellarUnicorn.WIDTH +HEIGHT = StellarUnicorn.HEIGHT + +jpeg = jpegdec.JPEG(display) +TEXT_COLOUR = display.create_pen(200, 0, 200) +BLACK = display.create_pen(0, 0, 0) +WHITE = display.create_pen(255, 255, 255) + + +def run(): + # Setup wifi + network_manager = NetworkManager(WIFI_CONFIG.COUNTRY, status_handler=status_handler) + + # Connect to Wifi network + asyncio.get_event_loop().run_until_complete(network_manager.client(WIFI_CONFIG.SSID, WIFI_CONFIG.PSK)) + while (not network_manager.isconnected()): + time.sleep(0.1) + + +su.set_brightness(1) +run() # Sets up Wifi connection + + +def outline_text(text, x, y): + display.set_pen(BLACK) + display.text(text, x - 1, y - 1, -1, 1) + display.text(text, x, y - 1, -1, 1) + display.text(text, x + 1, y - 1, -1, 1) + display.text(text, x - 1, y, -1, 1) + display.text(text, x + 1, y, -1, 1) + display.text(text, x - 1, y + 1, -1, 1) + display.text(text, x, y + 1, -1, 1) + display.text(text, x + 1, y + 1, -1, 1) + + display.set_pen(WHITE) + display.text(text, x, y, -1, 1) + + +def draw_page(cycle): + global user_icon + text_cycle = cycle % 1000 + cycle = cycle % 4 + # Clear the display + display.set_pen(15) + display.clear() + + # Draw the page header + display.set_font("bitmap6") + + if temperature is not None: + # Choose an appropriate icon based on the weather code + # Weather codes from https://open-meteo.com/en/docs + if user_icon is not None: + icons = ["icons/snow{0}.jpg".format(cycle + 1), "icons/rain{0}.jpg".format(cycle + 1), "icons/cloud{0}.jpg".format(cycle + 1), "icons/sun{0}.jpg".format(cycle + 1), "icons/storm{0}.jpg".format(cycle + 1)] + jpeg.open_file(icons[user_icon]) + else: + if weathercode in [71, 73, 75, 77, 85, 86]: # codes for snow + jpeg.open_file("icons/snow{0}.jpg".format(cycle + 1)) + elif weathercode in [51, 53, 55, 56, 57, 61, 63, 65, 66, 67, 80, 81, 82]: # codes for rain + jpeg.open_file("icons/rain{0}.jpg".format(cycle + 1)) + elif weathercode in [1, 2, 3, 45, 48]: # codes for cloud + jpeg.open_file("icons/cloud{0}.jpg".format(cycle + 1)) + elif weathercode in [0]: # codes for sun + jpeg.open_file("icons/sun{0}.jpg".format(cycle + 1)) + elif weathercode in [95, 96, 99]: # codes for storm + jpeg.open_file("icons/storm{0}.jpg".format(cycle + 1)) + jpeg.decode(0, 0, jpegdec.JPEG_SCALE_FULL) + display.set_pen(TEXT_COLOUR) + outline_text(WEATHER_TEXT, 16 - text_cycle, 0) + + else: + display.set_pen(0) + display.set_pen(15) + display.text("Unable to display weather! Check your network settings in WIFI_CONFIG.py", 5, 65, WIDTH, 1) + + su.update(display) + + +while 1: + + get_data() + for count in range(500): + + if su.is_pressed(StellarUnicorn.SWITCH_A): + user_icon = 0 + if su.is_pressed(StellarUnicorn.SWITCH_B): + user_icon = 1 + if su.is_pressed(StellarUnicorn.SWITCH_C): + user_icon = 2 + if su.is_pressed(StellarUnicorn.SWITCH_D): + user_icon = 3 + if su.is_pressed(StellarUnicorn.SWITCH_BRIGHTNESS_UP): + user_icon = 4 + if su.is_pressed(StellarUnicorn.SWITCH_BRIGHTNESS_DOWN): + user_icon = None + draw_page(count) + time.sleep(0.2)