1
0
mirror of https://github.com/danbee/unicorn synced 2026-06-20 22:52:22 +00:00

Replace with build derived from inky-frame.

This commit is contained in:
Phil Howard 2024-12-04 12:19:05 +00:00
parent 849e8a6a64
commit 4c111cbc97
223 changed files with 8710 additions and 19275 deletions

View File

@ -1,4 +1,4 @@
name: MicroPython Firmware
name: MicroPython
on:
push:
@ -6,149 +6,93 @@ on:
release:
types: [created]
env:
MICROPYTHON_VERSION: v1.20.0
PIMORONI_PICO_VERSION: 302d6ae0ebccbec8b676a5eb5e1ec9b273f4eecd
jobs:
deps:
runs-on: ubuntu-20.04
name: Dependencies
steps:
- name: Workspace Cache
id: cache
uses: actions/cache@v3
with:
path: ${{runner.workspace}}
key: workspace-micropython-${{env.MICROPYTHON_VERSION}}
restore-keys: |
workspace-micropython-${{env.MICROPYTHON_VERSION}}
# Check out MicroPython
- name: Checkout MicroPython
if: steps.cache.outputs.cache-hit != 'true'
uses: actions/checkout@v3
with:
repository: micropython/micropython
ref: ${{env.MICROPYTHON_VERSION}}
submodules: false # MicroPython submodules are hideously broken
path: micropython
- name: Fetch base MicroPython submodules
if: steps.cache.outputs.cache-hit != 'true'
shell: bash
working-directory: micropython
run: git submodule update --init
- name: Fetch Pico SDK submodules
if: steps.cache.outputs.cache-hit != 'true'
shell: bash
working-directory: micropython/lib/pico-sdk
run: git submodule update --init
- name: Build mpy-cross
if: steps.cache.outputs.cache-hit != 'true'
shell: bash
working-directory: micropython/mpy-cross
run: make
build:
needs: deps
name: Build ${{matrix.name}}
runs-on: ubuntu-20.04
name: MicroPython ${{ matrix.name }}
runs-on: ubuntu-24.04
continue-on-error: true
strategy:
matrix:
include:
- name: Cosmic Unicorn
shortname: cosmic_unicorn
board: PIMORONI_COSMIC_UNICORN
- name: Galactic Unicorn
shortname: galactic_unicorn
board: PIMORONI_GALACTIC_UNICORN
- name: pico_w_unicorn
- name: pico2_w_unicorn
env:
RELEASE_FILE: pimoroni-${{matrix.shortname}}-${{github.event.release.tag_name || github.sha}}-micropython.uf2
FIRMWARE_DIR: "$GITHUB_WORKSPACE/unicorn/firmware"
BOARD_DIR: "$GITHUB_WORKSPACE/unicorn/firmware/${{matrix.board}}"
# MicroPython version will be contained in github.event.release.tag_name for releases
RELEASE_FILE: ${{ matrix.name }}-${{ github.event.release.tag_name || github.sha }}-micropython
CI_PROJECT_ROOT: ${{ github.workspace }}/src-${{ github.sha }}
CI_BUILD_ROOT: ${{ github.workspace }}
steps:
- name: Compiler Cache
uses: actions/cache@v3
- name: Compiler Cache Fixup
run: |
mkdir -p /home/runner/.ccache
- name: "CCache: Restore saved cache"
uses: actions/cache@v4
with:
path: /home/runner/.ccache
key: ccache-micropython-${{matrix.shortname}}-${{github.ref}}-${{github.sha}}
key: ccache-micropython-${{ matrix.name }}-${{ github.ref }}-${{ github.sha }}
restore-keys: |
ccache-micropython-${{matrix.shortname}}-${{github.ref}}
ccache-micropython-${{matrix.shortname}}-
ccache-micropython-${{ matrix.name }}-${{ github.ref }}
ccache-micropython-${{ matrix.name }}-
- name: Workspace Cache
uses: actions/cache@v3
with:
path: ${{runner.workspace}}
key: workspace-micropython-${{env.MICROPYTHON_VERSION}}
restore-keys: |
workspace-micropython-${{env.MICROPYTHON_VERSION}}
- uses: actions/checkout@v3
- name: "Checkout Project"
uses: actions/checkout@v4
with:
submodules: true
path: unicorn
path: ${{ env.CI_PROJECT_ROOT }}
# Check out Pimoroni Pico
- uses: actions/checkout@v3
- name: "Install Arm GNU Toolchain (arm-none-eabi-gcc)"
uses: carlosperate/arm-none-eabi-gcc-action@v1
with:
repository: pimoroni/pimoroni-pico
ref: ${{env.PIMORONI_PICO_VERSION}}
submodules: true
path: pimoroni-pico
release: '13.3.Rel1'
# HACK: Patch startup overclock into Pico SDK
- name: "HACK: Startup Overclock Patch"
- name: "Prepare tools & dependencies"
shell: bash
working-directory: micropython/lib/pico-sdk
run: |
git apply "${{env.FIRMWARE_DIR}}/startup_overclock.patch"
source $CI_PROJECT_ROOT/ci/micropython.sh && ci_debug
mkdir -p $CI_BUILD_ROOT
ci_apt_install_build_deps
ci_prepare_all
# Install apt packages
- name: Install CCache & Compiler
- name: "MicroPython: Configure"
shell: bash
run:
sudo apt update && sudo apt install ccache gcc-arm-none-eabi
# Build firmware
- name: Configure MicroPython
shell: bash
working-directory: micropython/ports/rp2
run: |
cmake -S . -B build-${{matrix.shortname}} -DPICO_BUILD_DOCS=0 -DUSER_C_MODULES=${{env.BOARD_DIR}}/micropython.cmake -DMICROPY_BOARD_DIR=${{env.BOARD_DIR}} -DMICROPY_BOARD=${{env.BOARD}} -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache
source $CI_PROJECT_ROOT/ci/micropython.sh && ci_debug
micropython_version
ci_cmake_configure ${{ matrix.name }}
- name: Build MicroPython
- name: "MicroPython: Build"
shell: bash
working-directory: micropython/ports/rp2
run: |
ccache --zero-stats || true
cmake --build build-${{matrix.shortname}} -j 1
ccache --show-stats || true
source $CI_PROJECT_ROOT/ci/micropython.sh && ci_debug
python3 -m venv "$CI_BUILD_ROOT/.dir2uf2"
source "$CI_BUILD_ROOT/.dir2uf2/bin/activate"
ci_cmake_build ${{ matrix.name }}
mv "$CI_BUILD_ROOT/${{ matrix.name }}.uf2" "$CI_BUILD_ROOT/$RELEASE_FILE.uf2"
mv "$CI_BUILD_ROOT/${{ matrix.name }}-with-filesystem.uf2" "$CI_BUILD_ROOT/$RELEASE_FILE-with-filesystem.uf2"
- name: Rename .uf2 for artifact
shell: bash
working-directory: micropython/ports/rp2/build-${{matrix.shortname}}
run: |
cp firmware.uf2 ${{env.RELEASE_FILE}}
- name: Store .uf2 as artifact
uses: actions/upload-artifact@v3
- name: "Artifacts: Upload .uf2"
uses: actions/upload-artifact@v4
with:
name: ${{env.RELEASE_FILE}}
path: micropython/ports/rp2/build-${{matrix.shortname}}/${{env.RELEASE_FILE}}
name: ${{ env.RELEASE_FILE }}.uf2
path: ${{ env.CI_BUILD_ROOT }}/${{ env.RELEASE_FILE }}.uf2
- name: "Artifacts: Upload .uf2 (With Filesystem)"
uses: actions/upload-artifact@v4
with:
name: ${{ env.RELEASE_FILE }}-with-filesystem.uf2
path: ${{ env.CI_BUILD_ROOT }}/${{ env.RELEASE_FILE }}-with-filesystem.uf2
- name: Upload .uf2
- name: "Release: Upload .uf2"
if: github.event_name == 'release'
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
uses: softprops/action-gh-release@v1
with:
asset_path: micropython/ports/rp2/build-${{matrix.shortname}}/${{env.RELEASE_FILE}}
upload_url: ${{github.event.release.upload_url}}
asset_name: ${{env.RELEASE_FILE}}
asset_content_type: application/octet-stream
files: ${{ env.CI_BUILD_ROOT }}/${{ env.RELEASE_FILE }}.uf2
- name: "Release: Upload .uf2 (With Filesystem)"
if: github.event_name == 'release'
uses: softprops/action-gh-release@v1
with:
files: ${{ env.CI_BUILD_ROOT }}/${{ env.RELEASE_FILE }}-with-filesystem.uf2

21
LICENSE
View File

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2023 Pimoroni Ltd
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1 +1,21 @@
# Cosmic Unicorn & Galactic Unicorn
# Pico-powered Unicorn
## Galactic, Cosmic, Stellar!
This repository is home to the MicroPython firmware and examples for
Galactic, Cosmic and Stellar Unicorn - our Pico-powered LED matrix boards.
## Download Firmware
There are two choices, a regular build that just updates the firmware on
your board and a "-with-examples" build which includes a ready-to-go set
of examples (everything in [examples/launcher](examples/launcher))
that you can interact with right on your display.
:warning: If you've changed any of the code on your board then back up before
flashing "-with-examples" - *your files will be erased!*
You can find the latest release at [https://github.com/pimoroni/unicorn/releases/latest](https://github.com/pimoroni/unicorn/releases/latest).
## Guides & Documentation
## Get Unicorn

14
boards/manifest-common.py Normal file
View File

@ -0,0 +1,14 @@
# https://github.com/micropython/micropython-lib/blob/master/micropython/bundles/bundle-networking/manifest.py
require("bundle-networking")
require("urllib.urequest")
require("umqtt.simple")
# SD Card
require("sdcard")
# Bluetooth
require("aioble")
freeze("../../pimoroni-pico/micropython/modules_py", "pimoroni.py")
freeze("../../pimoroni-pico/micropython/modules_py", "boot.py")
freeze("../../pimoroni-pico/micropython/modules_py", "lte.py")

View File

@ -0,0 +1,2 @@
include("$(PORT_DIR)/boards/manifest.py")
include("../manifest-common.py")

View File

@ -0,0 +1,3 @@
*.py
lib/*
lib/*/*

View File

@ -0,0 +1,17 @@
# cmake file for Pimoroni Inky with Raspberry Pi Pico W
set(MICROPY_BOARD RPI_PICO_W)
set(PICO_BOARD "pico2_w")
# The C malloc is needed by cyw43-driver Bluetooth and Pimoroni Pico modules
set(MICROPY_C_HEAP_SIZE 4096)
set(MICROPY_PY_LWIP ON)
set(MICROPY_PY_NETWORK_CYW43 ON)
# Bluetooth
set(MICROPY_PY_BLUETOOTH ON)
set(MICROPY_BLUETOOTH_BTSTACK ON)
set(MICROPY_PY_BLUETOOTH_CYW43 ON)
# Board specific version of the frozen manifest
set(MICROPY_FROZEN_MANIFEST ${MICROPY_BOARD_DIR}/manifest.py)

View File

@ -0,0 +1,14 @@
// Board and hardware specific configuration
#define MICROPY_HW_BOARD_NAME "Raspberry Pi Pico2 W (Unicorn)"
// Leave 50/50 split - 2MB firmware, 2MB user filesystem
#define MICROPY_HW_FLASH_STORAGE_BYTES (PICO_FLASH_SIZE_BYTES - (2 * 1024 * 1024))
// Enable networking.
#define MICROPY_PY_NETWORK_PPP_LWIP (1)
#define MICROPY_PY_NETWORK_HOSTNAME_DEFAULT "Unicorn2W"
#include "enable_cyw43.h"
// For debugging mbedtls - also set
// Debug level (0-4) 1=warning, 2=info, 3=debug, 4=verbose
// #define MODUSSL_MBEDTLS_DEBUG_LEVEL 1

View File

@ -27,4 +27,4 @@ GP28,GPIO28
WL_GPIO0,EXT_GPIO0
WL_GPIO1,EXT_GPIO1
WL_GPIO2,EXT_GPIO2
LED,EXT_GPIO0
LED,EXT_GPIO0
1 GP0 GPIO0
27 WL_GPIO0 EXT_GPIO0
28 WL_GPIO1 EXT_GPIO1
29 WL_GPIO2 EXT_GPIO2
30 LED EXT_GPIO0

View File

@ -0,0 +1,7 @@
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/../")
include(galactic_unicorn/micropython)
include(cosmic_unicorn/micropython)
include(stellar_unicorn/micropython)
include(usermod-common)

View File

@ -0,0 +1,2 @@
include("$(PORT_DIR)/boards/manifest.py")
include("../manifest-common.py")

View File

@ -0,0 +1,3 @@
*.py
lib/*
lib/*/*

View File

@ -0,0 +1,17 @@
# cmake file for Pimoroni Inky with Raspberry Pi Pico W
set(MICROPY_BOARD RPI_PICO_W)
set(PICO_BOARD "pico_w")
# The C malloc is needed by cyw43-driver Bluetooth and Pimoroni Pico modules
set(MICROPY_C_HEAP_SIZE 4096)
set(MICROPY_PY_LWIP ON)
set(MICROPY_PY_NETWORK_CYW43 ON)
# Bluetooth
set(MICROPY_PY_BLUETOOTH ON)
set(MICROPY_BLUETOOTH_BTSTACK ON)
set(MICROPY_PY_BLUETOOTH_CYW43 ON)
# Board specific version of the frozen manifest
set(MICROPY_FROZEN_MANIFEST ${MICROPY_BOARD_DIR}/manifest.py)

View File

@ -0,0 +1,14 @@
// Board and hardware specific configuration
#define MICROPY_HW_BOARD_NAME "Raspberry Pi Pico W (Unicorn)"
// Leave 848k for user filesystem
#define MICROPY_HW_FLASH_STORAGE_BYTES (PICO_FLASH_SIZE_BYTES - (1200 * 1024))
// Enable networking.
#define MICROPY_PY_NETWORK_PPP_LWIP (1)
#define MICROPY_PY_NETWORK_HOSTNAME_DEFAULT "Unicorn"
#include "enable_cyw43.h"
// For debugging mbedtls - also set
// Debug level (0-4) 1=warning, 2=info, 3=debug, 4=verbose
// #define MODUSSL_MBEDTLS_DEBUG_LEVEL 1

View File

@ -27,4 +27,4 @@ GP28,GPIO28
WL_GPIO0,EXT_GPIO0
WL_GPIO1,EXT_GPIO1
WL_GPIO2,EXT_GPIO2
LED,EXT_GPIO0
LED,EXT_GPIO0
1 GP0 GPIO0
27 WL_GPIO0 EXT_GPIO0
28 WL_GPIO1 EXT_GPIO1
29 WL_GPIO2 EXT_GPIO2
30 LED EXT_GPIO0

View File

@ -0,0 +1,3 @@
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/../")
include(usermod-common)

View File

@ -1,10 +1,16 @@
set(PIMORONI_PICO_PATH ../../../../pimoroni-pico)
include(${CMAKE_CURRENT_LIST_DIR}/../pimoroni_pico_import.cmake)
if(NOT DEFINED PIMORONI_PICO_PATH)
set(PIMORONI_PICO_PATH ${CMAKE_CURRENT_LIST_DIR}/../pimoroni-pico)
endif()
include(${PIMORONI_PICO_PATH}/pimoroni_pico_import.cmake)
include_directories(${CMAKE_CURRENT_LIST_DIR}/../../)
include_directories(${PIMORONI_PICO_PATH}/micropython)
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/../../")
# Drivers, etc
list(APPEND CMAKE_MODULE_PATH "${PIMORONI_PICO_PATH}")
# modules_py/modules_py
list(APPEND CMAKE_MODULE_PATH "${PIMORONI_PICO_PATH}/micropython")
# All regular modules
list(APPEND CMAKE_MODULE_PATH "${PIMORONI_PICO_PATH}/micropython/modules")
set(CMAKE_C_STANDARD 11)
@ -20,24 +26,33 @@ include(bitmap_fonts/micropython)
include(picographics/micropython)
# Pico Graphics Extra
include(pngdec/micropython)
include(jpegdec/micropython)
include(qrcode/micropython/micropython)
# Sensors & Breakouts
include(micropython-common-breakouts)
include(pcf85063a/micropython)
# Utility
include(adcfft/micropython)
# LEDs & Matrices
include(plasma/micropython)
include(galactic_unicorn/micropython)
include(cosmic_unicorn/micropython)
include(stellar_unicorn/micropython)
# ULAB
include(micropython-common-ulab)
enable_ulab()
# Servos & Motors
include(pwm/micropython)
include(servo/micropython)
include(encoder/micropython)
include(motor/micropython)
# Still required for version.py
include(modules_py/modules_py)
# C++ Magic Memory
include(cppmem/micropython)
# Disable build-busting C++ exceptions
include(micropython-disable-exceptions)

135
ci/micropython.sh Normal file
View File

@ -0,0 +1,135 @@
export TERM=${TERM:="xterm-256color"}
MICROPYTHON_FLAVOUR="peterharperuk"
MICROPYTHON_VERSION="pico2_w_changes"
PIMORONI_PICO_FLAVOUR="pimoroni"
PIMORONI_PICO_VERSION="feature/picovector2-and-layers"
PY_DECL_VERSION="v0.0.3"
DIR2UF2_VERSION="v0.0.9"
if [ -z ${CI_PROJECT_ROOT+x} ]; then
SCRIPT_PATH="$(dirname $0)"
CI_PROJECT_ROOT=$(realpath "$SCRIPT_PATH/..")
fi
if [ -z ${CI_BUILD_ROOT+x} ]; then
CI_BUILD_ROOT=$(pwd)
fi
function log_success {
echo -e "$(tput setaf 2)$1$(tput sgr0)"
}
function log_inform {
echo -e "$(tput setaf 6)$1$(tput sgr0)"
}
function log_warning {
echo -e "$(tput setaf 1)$1$(tput sgr0)"
}
function ci_pimoroni_pico_clone {
log_inform "Using Pimoroni Pico $PIMORONI_PICO_FLAVOUR/$PIMORONI_PICO_VERSION"
git clone https://github.com/$PIMORONI_PICO_FLAVOUR/pimoroni-pico "$CI_BUILD_ROOT/pimoroni-pico"
cd "$CI_BUILD_ROOT/pimoroni-pico" || return 1
git checkout $PIMORONI_PICO_VERSION
git submodule update --init
cd "$CI_BUILD_ROOT"
}
function ci_micropython_clone {
log_inform "Using MicroPython $MICROPYTHON_FLAVOUR/$MICROPYTHON_VERSION"
git clone https://github.com/$MICROPYTHON_FLAVOUR/micropython "$CI_BUILD_ROOT/micropython"
cd "$CI_BUILD_ROOT/micropython" || return 1
git checkout $MICROPYTHON_VERSION
git submodule update --init lib/pico-sdk
git submodule update --init lib/cyw43-driver
git submodule update --init lib/lwip
git submodule update --init lib/mbedtls
git submodule update --init lib/micropython-lib
git submodule update --init lib/tinyusb
git submodule update --init lib/btstack
cd "$CI_BUILD_ROOT"
}
function ci_tools_clone {
mkdir -p "$CI_BUILD_ROOT/tools"
git clone https://github.com/gadgetoid/py_decl -b "$PY_DECL_VERSION" "$CI_BUILD_ROOT/tools/py_decl"
git clone https://github.com/gadgetoid/dir2uf2 -b "$DIR2UF2_VERSION" "$CI_BUILD_ROOT/tools/dir2uf2"
}
function ci_micropython_build_mpy_cross {
cd "$CI_BUILD_ROOT/micropython/mpy-cross" || return 1
ccache --zero-stats || true
CROSS_COMPILE="ccache " make
ccache --show-stats || true
cd "$CI_BUILD_ROOT"
}
function ci_apt_install_build_deps {
sudo apt update && sudo apt install ccache python3-virtualenvwrapper virtualenvwrapper
}
function ci_prepare_all {
ci_tools_clone
ci_micropython_clone
ci_pimoroni_pico_clone
ci_micropython_build_mpy_cross
}
function ci_debug {
log_inform "Project root: $CI_PROJECT_ROOT"
log_inform "Build root: $CI_BUILD_ROOT"
}
function micropython_version {
BOARD=$1
echo "MICROPY_GIT_TAG=$MICROPYTHON_VERSION, $BOARD $TAG_OR_SHA" >> $GITHUB_ENV
echo "MICROPY_GIT_HASH=$MICROPYTHON_VERSION-$TAG_OR_SHA" >> $GITHUB_ENV
}
function ci_cmake_configure {
BOARD=$1
MICROPY_BOARD_DIR=$CI_PROJECT_ROOT/boards/$BOARD
if [ ! -f "$MICROPY_BOARD_DIR/usermodules.cmake" ]; then
log_warning "Invalid board: $MICROPY_BOARD_DIR"
return 1
fi
BUILD_DIR="$CI_BUILD_ROOT/build-$BOARD"
cmake -S $CI_BUILD_ROOT/micropython/ports/rp2 -B "$BUILD_DIR" \
-DPICOTOOL_FORCE_FETCH_FROM_GIT=1 \
-DPICO_BUILD_DOCS=0 \
-DPICO_NO_COPRO_DIS=1 \
-DPIMORONI_PICO_PATH="$CI_BUILD_ROOT/pimoroni-pico" \
-DUSER_C_MODULES="$MICROPY_BOARD_DIR/usermodules.cmake" \
-DMICROPY_BOARD_DIR="$MICROPY_BOARD_DIR" \
-DMICROPY_BOARD="$BOARD" \
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache
}
function ci_cmake_build {
BOARD=$1
MICROPY_BOARD_DIR=$CI_PROJECT_ROOT/boards/$BOARD
EXAMPLES_DIR=$CI_PROJECT_ROOT/examples/launcher/
TOOLS_DIR=$CI_BUILD_ROOT/tools
BUILD_DIR="$CI_BUILD_ROOT/build-$BOARD"
ccache --zero-stats || true
cmake --build $BUILD_DIR -j 2
ccache --show-stats || true
if [ -d "$TOOLS_DIR/py_decl" ]; then
log_inform "Tools found, verifying .uf2 with py_decl..."
python3 "$TOOLS_DIR/py_decl/py_decl.py" --to-json --verify "$BUILD_DIR/firmware.uf2"
fi
log_inform "Copying .uf2 to $(pwd)/$BOARD.uf2"
cp "$BUILD_DIR/firmware.uf2" $BOARD.uf2
if [ -f "$MICROPY_BOARD_DIR/manifest.txt" ] && [ -d "$TOOLS_DIR/dir2uf2" ]; then
log_inform "Creating $(pwd)/$BOARD-with-filesystem.uf2"
python3 -m pip install littlefs-python==0.12.0
$TOOLS_DIR/dir2uf2/dir2uf2 --fs-compact --sparse --append-to "$(pwd)/$BOARD.uf2" --manifest "$MICROPY_BOARD_DIR/manifest.txt" --filename with-filesystem.uf2 "$EXAMPLES_DIR"
fi
}

164
docs/inky_frame.md Normal file
View File

@ -0,0 +1,164 @@
# Inky Frame (MicroPython) <!-- omit in toc -->
Most of your interaction with Inky Frame will be via the PicoGraphics library, but we have created an `inky_frame` module to help you read the onboard A, B, C, D and E buttons and control the LEDs.
- [Pico Graphics](#pico-graphics)
- [Colour \& Dithering](#colour--dithering)
- [Images \& JPEGs](#images--jpegs)
- [Buttons \& LEDs](#buttons--leds)
- [Status LEDs](#status-leds)
- [Battery Power \& RTC](#battery-power--rtc)
- [Function Reference](#function-reference)
- [Wakeup States](#wakeup-states)
- [RTC](#rtc)
- [Other](#other)
- [RAM Usage](#ram-usage)
- [Accessing The Framebuffer](#accessing-the-framebuffer)
## Pico Graphics
You can draw on Inky Frame using our PicoGraphics display library.
- [PicoGraphics MicroPython function reference](https://github.com/pimoroni/pimoroni-pico/blob/main/micropython/modules/picographics/README.md)
### Colour & Dithering
Inky Frame supports eight native colours, those that it's physically capable of displaying. These are:
* `BLACK` = 0
* `WHITE` = 1
* `GREEN` = 2
* `BLUE` = 3
* `RED` = 4
* `YELLOW` = 5
* `ORANGE` = 6
* `TAUPE` = 7 (only used for dither on 4.0" and 5.7" displays †)
:info: † - the "taupe" colour (also known as clear) is the colour used to reset the display to a default, clear state. We misuse this as an extra colour on 4.0" and 5.7", but the 7.3" display clear colour is a sort of muddy, greenish gradient that's not consistent enough. You can still use it, by all means, but it wont be considered for dithering.
These colours are available as constants in the `inky_frame` module so you don't have to remember which number corresponds to which, eg:
```python
display.set_pen(inky_frame.YELLOW)
```
You can use colours outside of these eight by using `create_pen(r, g, b)` or `create_pen_hsv(h, s, v)`. eg:
```python
my_colour = display.create_pen(255, 128, 0)
display.set_pen(my_colour)
```
PicoGraphics will do its best to match your colour choice by mixing the available colours with a dither pattern. This works best on large areas of colour such as backgrounds, big UI elements or chunky text. See the `inky_frame_dithering.py` example for a demonstration.
:info: Due to the lack of light blue or pink native colours, Inky Frame isn't very good at reproducing pink, purple, violet or light blue, cyan and teal.
### Images & JPEGs
You can use the `jpegdec` (JPEG decoding) module to display JPEG files on Inky, but you should be aware of some caveats:
1. JPEGs are compressed and lossy, a small JPEG displayed on Inky might show random specks of colour where you don't expect them as PicoGraphics tries its best to dither noise to the nearest available colours.
2. JPEGs are, by default, dithered to the 7 (or 8 on 4.0 and 5.7) available colours. This uses "ordered dither," which is fast, but not very pretty.
3. You can turn off dithering with `jpeg.decode(dither=False)` for a posterised effect.
## Buttons & LEDs
Inky Frame has five user buttons labelled A to E, with LEDs. The buttons are connected via a shift-register and available in the `inky_frame` module as `button_a`, `button_b`, `button_c`, `button_d` and `button_e`.
Each button has some convenient methods for checking if it's pressed:
* `raw()` - get the current state of the button with no debounce (returns `True` the first time it's called if the corresponding button)
* `read()` - read the current button state, observing debounce (50ms)
Additionally each button has some methods for controlling its associated LED:
* `led_on()` - turn the LED on
* `led_off()` - turn the LED off
* `led_toggle()` - toggle the LED
* `led_brightness(0.5)` - set the LED brightness (from 0.0 to 1.0)
### Status LEDs
In addition to the button LEDs there are two status LEDs, `busy` and `wifi`, which are available as:
* `led_busy` - connected to pin `LED_BUSY`
* `led_wifi` - connected to pin `LED_WIFI`
These are instances of the `pimoroni.PWMLED` class, and have the following methods:
* `on()` - turn the LED on
* `off()` - turn the LED off
* `toggle()` - toggle the LED
* `brightness(0.5)` - set the LED brightness (from 0.0 to 1.0)
:info: `toggle()` is provided for compatibility with the `Pin()` class. It's a little odd, since it changes the LED brightness to `1.0 - current_brightness`.
## Battery Power & RTC
When running on battery power, Inky Frame's buttons cause it to wake from a power-off state and start running your code. Additionally an onboard PCF85063A real-time clock runs continuously from battery and can wake up your Frame automatically.
The Inky Frame library includes a number of convenience functions to set the clock and sleep your device. If you want accurate time you must check if the clock is set - usually a simple check against the year works - connect to a network, and set via NTP (Network Time Protocol).
Inky Frame technically has *two* real-time clocks, the external RTC we've added - which remains continuously powered by battery - and the internal RTC of the Pico W. For convenience and compatibility with other code examples we recommend using the external RTC only to set the time on the internal one which makes functions like `time.localtime()` work as you'd expect.
```python
import time
import machine
import inky_frame
inky_frame.pcf_to_pico_rtc() # Sync Inky RTC time to Pico's RTC
year, month, day, dow, hour, minute, second, _ = machine.RTC().datetime()
if year < 2023:
# Connect to network
inky_frame.set_time() # Sets both the Inky and Pico RTC
print(time.localtime())
```
### Function Reference
#### Wakeup States
For your convenience these wakeup state functions also check the *current* state of their associated event, this allows you to run code from Thonny with a button held down - for example - to test how your deployed code will behave on battery-
* `inky_frame.woken_by_rtc()` - Returns `True` if the RTC caused a wakeup, or if the RTC ALARM is currently raised.
* `inky_frame.woken_by_button()` - Returns `True` if a button caused a wakeup, or if a button is currently pressed.
* `inky_frame.woken_by_ext_trigger()` Returns `True` if the external trigger caused a wakeup, or if the trigger is currently asserted.
#### RTC
* `inky_frame.set_time()` - Attempt to run `ntptime.settime()` and set the time on both RTCs
* `inky_frame.pcf_to_pico_rtc()` - Sync from Inky's RTC to the Pico W's RTC
* `inky_frame.pico_rtc_to_pcf()` - Sync from Pico W's RTC to Inky's RTC (sometime useful since Thonny sets the Pico W RTC automatically)
* `inky_frame.sleep_for(minutes)` - Set the RTC alarm for a number of minutes and cut the power. This will completely power off the Pico W, but leave the Inky RTC running to wake it back up.
:info: You can access all Inky RTC (PCF85063A) functions via `inky_frame.rtc`
#### Other
* `inky_frame.turn_off()` - Cut the power to the Pico W (on battery only), only an alarm event or a button press can wake it back up.
## RAM Usage
Both Inky 4.0" and 5.7" use only the Pico's onboard RAM. It's quite cozy. The frame buffers are 3-bits-per-pixel, making Inky 4.0"
On 7.3" we had to add a PSRAM chip to act as the display's framebuffer. Right now it acts exclusively as a framebuffer, but that frees up some Pico RAM to work with so you can do more with that 7.3" panel.
The Inky frame buffer sizes are as follows:
* 4.0" - 640x400 - 96,000 bytes.
* 5.7" - 600x448 - 100,800 bytes.
* 7.3" - 800x480 - 144,000 bytes.
Since MicroPython on a Pico W has only 166k that would have left *just* 22k on 7.3", instead you get (almost) all 166k to play with since the PicoGraphics instance itself uses only 8544 bytes!
### Accessing The Framebuffer
PicoGraphics has undocumented support for accessing its raw framebuffer using `memoryview(graphics)`.
This is useful for copying raw binary images (effectively valid Inky frame buffers saved to a file) avoiding JPEG compression and so forth. For some dicussion about why and how you might do this, see: https://github.com/pimoroni/pimoroni-pico/issues/681
:warning: This *does not work* for Inky 7.3, since there is no framebuffer in memory. PicoGraphics will raise a `ValueError: No local framebuffer.` if you try. We aim to fix this with some hardfault handling sorcery.

View File

@ -1,84 +0,0 @@
add_executable(
cosmic_rainbow_text
cosmic_rainbow_text.cpp
)
# Pull in pico libraries that we need
target_link_libraries(cosmic_rainbow_text pico_stdlib hardware_pio hardware_adc hardware_dma pico_graphics cosmic_unicorn)
pico_enable_stdio_usb(cosmic_rainbow_text 1)
# create map/bin/hex file etc.
pico_add_extra_outputs(cosmic_rainbow_text)
add_executable(
cosmic_rainbow
cosmic_rainbow.cpp
)
# Pull in pico libraries that we need
target_link_libraries(cosmic_rainbow pico_stdlib hardware_pio hardware_adc hardware_dma pico_graphics cosmic_unicorn)
pico_enable_stdio_usb(cosmic_rainbow 1)
# create map/bin/hex file etc.
pico_add_extra_outputs(cosmic_rainbow)
add_executable(
cosmic_eighties_super_computer
cosmic_eighties_super_computer.cpp
)
# Pull in pico libraries that we need
target_link_libraries(cosmic_eighties_super_computer pico_stdlib hardware_pio hardware_adc hardware_dma pico_graphics cosmic_unicorn)
pico_enable_stdio_usb(cosmic_eighties_super_computer 1)
# create map/bin/hex file etc.
pico_add_extra_outputs(cosmic_eighties_super_computer)
add_executable(
cosmic_fire_effect
cosmic_fire_effect.cpp
)
# Pull in pico libraries that we need
target_link_libraries(cosmic_fire_effect pico_stdlib hardware_pio hardware_adc hardware_dma pico_graphics cosmic_unicorn)
pico_enable_stdio_usb(cosmic_fire_effect 1)
# create map/bin/hex file etc.
pico_add_extra_outputs(cosmic_fire_effect)
add_executable(
cosmic_scroll_text
cosmic_scroll_text.cpp
)
# Pull in pico libraries that we need
target_link_libraries(cosmic_scroll_text pico_stdlib hardware_pio hardware_adc hardware_dma pico_graphics cosmic_unicorn)
pico_enable_stdio_usb(cosmic_scroll_text 1)
# create map/bin/hex file etc.
pico_add_extra_outputs(cosmic_scroll_text)
add_executable(
cosmic_lava_lamp
cosmic_lava_lamp.cpp
)
# Pull in pico libraries that we need
target_link_libraries(cosmic_lava_lamp pico_stdlib hardware_pio hardware_adc hardware_dma pico_graphics cosmic_unicorn)
pico_enable_stdio_usb(cosmic_lava_lamp 1)
# create map/bin/hex file etc.
pico_add_extra_outputs(cosmic_lava_lamp)

File diff suppressed because it is too large Load Diff

View File

@ -1,67 +0,0 @@
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include "pico/stdlib.h"
#include "libraries/pico_graphics/pico_graphics.hpp"
#include "cosmic_unicorn.hpp"
using namespace pimoroni;
PicoGraphics_PenRGB888 graphics(32, 32, nullptr);
CosmicUnicorn cosmic_unicorn;
float lifetime[32][32];
float age[32][32];
int main() {
stdio_init_all();
for(int y = 0; y < 32; y++) {
for(int x = 0; x < 32; x++) {
lifetime[x][y] = 1.0f + ((rand() % 10) / 100.0f);
age[x][y] = ((rand() % 100) / 100.0f) * lifetime[x][y];
}
}
cosmic_unicorn.init();
while(true) {
if(cosmic_unicorn.is_pressed(cosmic_unicorn.SWITCH_BRIGHTNESS_UP)) {
cosmic_unicorn.adjust_brightness(+0.01);
}
if(cosmic_unicorn.is_pressed(cosmic_unicorn.SWITCH_BRIGHTNESS_DOWN)) {
cosmic_unicorn.adjust_brightness(-0.01);
}
graphics.set_pen(0, 0, 0);
graphics.clear();
for(int y = 0; y < 32; y++) {
for(int x = 0; x < 32; x++) {
if(age[x][y] < lifetime[x][y] * 0.3f) {
graphics.set_pen(230, 150, 0);
graphics.pixel(Point(x, y));
}else if(age[x][y] < lifetime[x][y] * 0.5f) {
float decay = (lifetime[x][y] * 0.5f - age[x][y]) * 5.0f;
graphics.set_pen(decay * 230, decay * 150, 0);
graphics.pixel(Point(x, y));
}
if(age[x][y] >= lifetime[x][y]) {
age[x][y] = 0.0f;
lifetime[x][y] = 1.0f + ((rand() % 10) / 100.0f);
}
age[x][y] += 0.01f;
}
}
cosmic_unicorn.update(&graphics);
sleep_ms(10);
}
return 0;
}

View File

@ -1,115 +0,0 @@
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <string.h>
#include "pico/stdlib.h"
#include "libraries/pico_graphics/pico_graphics.hpp"
#include "cosmic_unicorn.hpp"
using namespace pimoroni;
PicoGraphics_PenRGB888 graphics(32, 32, nullptr);
CosmicUnicorn cosmic_unicorn;
// extra row of pixels for sourcing flames and averaging
int width = 32;
int height = 33;
// a buffer that's at least big enough to store 55 x 15 values (to allow for both orientations)
float heat[2000] = {0.0f};
void set(int x, int y, float v) {
heat[x + y * width] = v;
}
float get(int x, int y) {
/*if(x < 0 || x >= width || y < 0 || y >= height) {
return 0.0f;
}*/
x = x < 0 ? 0 : x;
x = x >= width ? width - 1 : x;
return heat[x + y * width];
}
int main() {
stdio_init_all();
cosmic_unicorn.init();
cosmic_unicorn.set_brightness(0.2);
bool landscape = true;
/*
while(true) {
cosmic_unicorn.set_pixel(0, 0, 255, 0, 0);
cosmic_unicorn.set_pixel(1, 1, 0, 255, 0);
cosmic_unicorn.set_pixel(2, 2, 0, 0, 255);
}*/
while(true) {
if(cosmic_unicorn.is_pressed(cosmic_unicorn.SWITCH_BRIGHTNESS_UP)) {
cosmic_unicorn.adjust_brightness(+0.01);
}
if(cosmic_unicorn.is_pressed(cosmic_unicorn.SWITCH_BRIGHTNESS_DOWN)) {
cosmic_unicorn.adjust_brightness(-0.01);
}
for(int y = 0; y < height; y++) {
for(int x = 0; x < width; x++) {
float value = get(x, y);
graphics.set_pen(0, 0, 0);
if(value > 0.5f) {
graphics.set_pen(255, 255, 180);
}else if(value > 0.4f) {
graphics.set_pen(220, 160, 0);
}else if(value > 0.3f) {
graphics.set_pen(180, 30, 0);
}else if(value > 0.22f) {
graphics.set_pen(20, 20, 20);
}
if(landscape) {
graphics.pixel(Point(x, y));
}else{
graphics.pixel(Point(y, x));
}
// update this pixel by averaging the below pixels
float average = (get(x, y) + get(x, y + 2) + get(x, y + 1) + get(x - 1, y + 1) + get(x + 1, y + 1)) / 5.0f;
// damping factor to ensure flame tapers out towards the top of the displays
average *= 0.98f;
// update the heat map with our newly averaged value
set(x, y, average);
}
}
cosmic_unicorn.update(&graphics);
// clear the bottom row and then add a new fire seed to it
for(int x = 0; x < width; x++) {
set(x, height - 1, 0.0f);
}
// add a new random heat source
int source_count = landscape ? 5 : 1;
for(int c = 0; c < source_count; c++) {
int px = (rand() % (width - 4)) + 2;
set(px , height - 2, 1.0f);
set(px + 1, height - 2, 1.0f);
set(px - 1, height - 2, 1.0f);
set(px , height - 1, 1.0f);
set(px + 1, height - 1, 1.0f);
set(px - 1, height - 1, 1.0f);
}
sleep_ms(20);
}
return 0;
}

View File

@ -1,148 +0,0 @@
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include "pico/stdlib.h"
#include "libraries/pico_graphics/pico_graphics.hpp"
#include "cosmic_unicorn.hpp"
using namespace pimoroni;
PicoGraphics_PenRGB888 graphics(32, 32, nullptr);
CosmicUnicorn cosmic_unicorn;
// HSV Conversion expects float inputs in the range of 0.00-1.00 for each channel
// Outputs are rgb in the range 0-255 for each channel
void from_hsv(float h, float s, float v, uint8_t &r, uint8_t &g, uint8_t &b) {
float i = floor(h * 6.0f);
float f = h * 6.0f - i;
v *= 255.0f;
uint8_t p = v * (1.0f - s);
uint8_t q = v * (1.0f - f * s);
uint8_t t = v * (1.0f - (1.0f - f) * s);
switch (int(i) % 6) {
case 0: r = v; g = t; b = p; break;
case 1: r = q; g = v; b = p; break;
case 2: r = p; g = v; b = t; break;
case 3: r = p; g = q; b = v; break;
case 4: r = t; g = p; b = v; break;
case 5: r = v; g = p; b = q; break;
}
}
struct blob_t {
float x, y;
float r;
float dx, dy;
};
constexpr int blob_count = 20;
int main() {
stdio_init_all();
cosmic_unicorn.init();
cosmic_unicorn.set_brightness(0.5);
// randomise blob start positions, directions, and size
std::array<blob_t, blob_count> blobs;
for(auto &blob : blobs) {
blob.x = rand() % 32;
blob.y = rand() % 32;
blob.r = ((rand() % 40) / 10.0f) + 5.0f;
blob.dx = ((rand() % 2) / 10.0f) - 0.05f;
blob.dy = ((rand() % 3) / 10.0f) - 0.1f;
}
float hue = 0.0f;
while(true) {
// allow user to adjust brightness
if(cosmic_unicorn.is_pressed(cosmic_unicorn.SWITCH_BRIGHTNESS_UP)) {
cosmic_unicorn.adjust_brightness(+0.01);
}
if(cosmic_unicorn.is_pressed(cosmic_unicorn.SWITCH_BRIGHTNESS_DOWN)) {
cosmic_unicorn.adjust_brightness(-0.01);
}
uint start_ms = to_ms_since_boot(get_absolute_time());
// calculate the influence of each blob on the liquid based
// on their distance to each pixel. this causes blobs to
// "merge" into each other when we use fixed thresholds to
// determine which colour to draw with
float liquid[32][32] = {0.0f};
for(auto &blob : blobs) {
float r_sq = blob.r * blob.r;
for(int y = 0; y < 32; y++) {
for(int x = 0; x < 32; x++) {
float d_sq = (x - blob.x) * (x - blob.x) + (y - blob.y) * (y - blob.y);
if(d_sq <= r_sq) {
// add this blobs influence to this pixel
liquid[x][y] += 1.0f - (d_sq / r_sq);
}
}
}
}
// update the blob positions
for(auto &blob : blobs) {
blob.x += blob.dx;
blob.y += blob.dy;
// if we hit the edge then bounce!
if(blob.x < 0.0f || blob.x >= 11.0f) {
blob.dx *= -1.0f;
}
if(blob.y < 0.0f || blob.y >= 53.0f) {
blob.dy *= -1.0f;
}
}
// rotate the hue
hue += 0.001f;
// calculate dark, medium, and bright shades for rendering the
// lava
uint8_t dark_r, dark_g, dark_b;
from_hsv(hue, 1.0f, 0.3f, dark_r, dark_g, dark_b);
uint8_t mid_r, mid_g, mid_b;
from_hsv(hue, 1.0f, 0.6f, mid_r, mid_g, mid_b);
uint8_t bright_r, bright_g, bright_b;
from_hsv(hue, 1.0f, 1.0f, bright_r, bright_g, bright_b);
// clear the canvas
graphics.set_pen(0, 0, 0);
graphics.clear();
// render the lava
for(int y = 0; y < 32; y++) {
for(int x = 0; x < 32; x++) {
float 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.5f) {
graphics.set_pen(bright_r, bright_g, bright_b);
graphics.pixel(Point(y, x));
}else if(v >= 1.25f) {
graphics.set_pen(mid_r, mid_g, mid_b);
graphics.pixel(Point(y, x));
}else if(v >= 1.0f) {
graphics.set_pen(dark_r, dark_g, dark_b);
graphics.pixel(Point(y, x));
}
}
}
uint end_ms = to_ms_since_boot(get_absolute_time());
printf("rendering took %dms\n", end_ms - start_ms);
cosmic_unicorn.update(&graphics);
}
return 0;
}

View File

@ -1,184 +0,0 @@
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include "pico/stdlib.h"
#include "libraries/pico_graphics/pico_graphics.hpp"
#include "cosmic_unicorn.hpp"
using namespace pimoroni;
PicoGraphics_PenRGB888 graphics(32, 32, nullptr);
CosmicUnicorn cosmic_unicorn;
// HSV Conversion expects float inputs in the range of 0.00-1.00 for each channel
// Outputs are rgb in the range 0-255 for each channel
void from_hsv(float h, float s, float v, uint8_t &r, uint8_t &g, uint8_t &b) {
float i = floor(h * 6.0f);
float f = h * 6.0f - i;
v *= 255.0f;
uint8_t p = v * (1.0f - s);
uint8_t q = v * (1.0f - f * s);
uint8_t t = v * (1.0f - (1.0f - f) * s);
switch (int(i) % 6) {
case 0: r = v; g = t; b = p; break;
case 1: r = q; g = v; b = p; break;
case 2: r = p; g = v; b = t; break;
case 3: r = p; g = q; b = v; break;
case 4: r = t; g = p; b = v; break;
case 5: r = v; g = p; b = q; break;
}
}
void text(std::string t, Point p, float s = 1.0f, float a = 1.0f) {
int w = graphics.measure_text(t, s);
p.x += (53 / 2) - (w / 2);
p.y += (11 / 2);
graphics.text(t, Point(p.x, p.y), -1, s, a);
//graphics.text(t, Point(p.x + 1, p.y), -1, s, a);
//graphics.text(t, Point(p.x + 1, p.y + 1), -1, s, a);
//graphics.text(t, Point(p.x, p.y + 1), -1, s, a);
}
struct star_t {
float dx, dy, x, y, a;
uint8_t brightness() {
int b = a / 5;
return b > 15 ? 15 : b;
}
};
void init_star(star_t &s) {
s.x = ((rand() % 100) / 5.0f) - 10.0f;
s.y = ((rand() % 100) / 10.0f) - 5.0f;
s.dx = s.x / 10.0f;
s.dy = s.y / 10.0f;
s.a = 0;
}
void step_star(star_t &s) {
s.x += s.dx;
s.y += s.dy;
s.a++;
if(s.a > 100) {
init_star(s);
}
}
int main() {
stdio_init_all();
uint8_t hue_map[53][3];
for(int i = 0; i < 53; i++) {
from_hsv(i / 53.0f, 1.0f, 1.0f, hue_map[i][0], hue_map[i][1], hue_map[i][2]);
}
star_t stars[100];
for(int i = 0; i < 100; i++) {
init_star(stars[i]);
stars[i].a = i;
}
gpio_set_function(28, GPIO_FUNC_SIO);
gpio_set_dir(28, GPIO_OUT);
for(int i = 0; i < 10; i++) {
gpio_put(28, !gpio_get(28));
sleep_ms(100);
}
sleep_ms(1000);
gpio_put(28,true);
cosmic_unicorn.init();
float i = 0;
float hue_offset = 0.0f;
bool animate = true;
float stripe_width = 3.0f;
float speed = 1.0f;
float curve = 0.0f;
while(true) {
if(animate) {
i += speed;
}
if(cosmic_unicorn.is_pressed(cosmic_unicorn.SWITCH_VOLUME_UP)) {
curve += 0.05;
if(hue_offset > 1.0f) hue_offset = 1.0f;
}
if(cosmic_unicorn.is_pressed(cosmic_unicorn.SWITCH_VOLUME_DOWN)) {
curve -= 0.05;
if(hue_offset < 0.0f) hue_offset = 0.0f;
}
if(cosmic_unicorn.is_pressed(cosmic_unicorn.SWITCH_BRIGHTNESS_UP)) {
cosmic_unicorn.adjust_brightness(+0.01);
}
if(cosmic_unicorn.is_pressed(cosmic_unicorn.SWITCH_BRIGHTNESS_DOWN)) {
cosmic_unicorn.adjust_brightness(-0.01);
}
if(cosmic_unicorn.is_pressed(cosmic_unicorn.SWITCH_SLEEP)) {
animate = false;
}
if(cosmic_unicorn.is_pressed(cosmic_unicorn.SWITCH_A)) {
speed += 0.05f;
speed = speed >= 10.0f ? 10.0f : speed;
animate = true;
}
if(cosmic_unicorn.is_pressed(cosmic_unicorn.SWITCH_B)) {
speed -= 0.05f;
speed = speed <= 0.0f ? 0.0f : speed;
animate = true;
}
if(cosmic_unicorn.is_pressed(cosmic_unicorn.SWITCH_C)) {
stripe_width += 0.05f;
stripe_width = stripe_width >= 10.0f ? 10.0f : stripe_width;
}
if(cosmic_unicorn.is_pressed(cosmic_unicorn.SWITCH_D)) {
stripe_width -= 0.05f;
stripe_width = stripe_width <= 1.0f ? 1.0f : stripe_width;
}
for(int x = 0; x < 32; x++) {
for(int y = 0; y < 32; y++) {
int v = ((sin((x + y) / stripe_width + (sin((y * 3.1415927f * 2.0f) / 11.0f) * curve) + i / 15.0f) + 1.5f) / 2.5f) * 255.0f;
uint8_t r = (hue_map[x][0] * v) / 256;
uint8_t g = (hue_map[x][1] * v) / 256;
uint8_t b = (hue_map[x][2] * v) / 256;
graphics.set_pen(r, g, b);
graphics.pixel(Point(x, y));
}
}
cosmic_unicorn.update(&graphics);
printf("%d\n", cosmic_unicorn.light());
sleep_ms(20);
}
printf("done\n");
return 0;
}

View File

@ -1,102 +0,0 @@
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include "pico/stdlib.h"
#include "libraries/pico_graphics/pico_graphics.hpp"
#include "cosmic_unicorn.hpp"
using namespace pimoroni;
PicoGraphics_PenRGB888 graphics(32, 32, nullptr);
CosmicUnicorn cosmic_unicorn;
// HSV Conversion expects float inputs in the range of 0.00-1.00 for each channel
// Outputs are rgb in the range 0-255 for each channel
void from_hsv(float h, float s, float v, uint8_t &r, uint8_t &g, uint8_t &b) {
float i = floor(h * 6.0f);
float f = h * 6.0f - i;
v *= 255.0f;
uint8_t p = v * (1.0f - s);
uint8_t q = v * (1.0f - f * s);
uint8_t t = v * (1.0f - (1.0f - f) * s);
switch (int(i) % 6) {
case 0: r = v; g = t; b = p; break;
case 1: r = q; g = v; b = p; break;
case 2: r = p; g = v; b = t; break;
case 3: r = p; g = q; b = v; break;
case 4: r = t; g = p; b = v; break;
case 5: r = v; g = p; b = q; break;
}
}
void text(std::string t, Point p, float s = 1.0f, float a = 1.0f) {
int w = graphics.measure_text(t, s);
p.x += (32 / 2) - (w / 2);
p.y += (32 / 2);
graphics.text(t, Point(p.x, p.y), -1, s, a);
graphics.text(t, Point(p.x + 1, p.y), -1, s, a);
graphics.text(t, Point(p.x + 1, p.y + 1), -1, s, a);
graphics.text(t, Point(p.x, p.y + 1), -1, s, a);
}
int main() {
uint8_t hue_map[32][3];
for(int i = 0; i < 32; i++) {
from_hsv(i / 32.0f, 1.0f, 0.5f, hue_map[i][0], hue_map[i][1], hue_map[i][2]);
}
cosmic_unicorn.init();
graphics.set_font("sans");
uint i = 0;
while(true) {
if(cosmic_unicorn.is_pressed(cosmic_unicorn.SWITCH_BRIGHTNESS_UP)) {
cosmic_unicorn.adjust_brightness(+0.01);
}
if(cosmic_unicorn.is_pressed(cosmic_unicorn.SWITCH_BRIGHTNESS_DOWN)) {
cosmic_unicorn.adjust_brightness(-0.01);
}
i++;
graphics.set_pen(0, 0, 0);
graphics.clear();
float s = 0.8f;//0.65f + (sin(i / 25.0f) * 0.15f);
float a = 1.0f;// (sin(i / 25.0f) * 100.0f);
float x = (sin((i) / 50.0f) * 90.0f);
float y = (cos((i) / 40.0f) * 5.0f);
graphics.set_pen(255, 255, 255);
text("Galactic Unicorn", Point(x, y), s, a);
uint8_t *p = (uint8_t *)graphics.frame_buffer;
for(size_t i = 0; i < 32 * 32; i++) {
int x = i % 32;
int y = i / 32;
uint r = *p++;
uint g = *p++;
uint b = *p++;
p++;
if(r > 0) {
r = hue_map[x][0];
g = hue_map[x][1];
b = hue_map[x][2];
}
graphics.set_pen(r, g, b);
graphics.pixel(Point(x, y));
}
cosmic_unicorn.update(&graphics);
}
printf("done\n");
return 0;
}

View File

@ -1,76 +0,0 @@
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include "pico/stdlib.h"
#include "libraries/pico_graphics/pico_graphics.hpp"
#include "cosmic_unicorn.hpp"
using namespace pimoroni;
PicoGraphics_PenRGB888 graphics(32, 32, nullptr);
CosmicUnicorn cosmic_unicorn;
std::string message = "Pirate. Monkey. Robot. Ninja.";
// HSV Conversion expects float inputs in the range of 0.00-1.00 for each channel
// Outputs are rgb in the range 0-255 for each channel
void from_hsv(float h, float s, float v, uint8_t &r, uint8_t &g, uint8_t &b) {
float i = floor(h * 6.0f);
float f = h * 6.0f - i;
v *= 255.0f;
uint8_t p = v * (1.0f - s);
uint8_t q = v * (1.0f - f * s);
uint8_t t = v * (1.0f - (1.0f - f) * s);
switch (int(i) % 6) {
case 0: r = v; g = t; b = p; break;
case 1: r = q; g = v; b = p; break;
case 2: r = p; g = v; b = t; break;
case 3: r = p; g = q; b = v; break;
case 4: r = t; g = p; b = v; break;
case 5: r = v; g = p; b = q; break;
}
}
int main() {
stdio_init_all();
cosmic_unicorn.init();
float scroll = -32.0f;
while(true) {
//uint time_ms = to_ms_since_boot(get_absolute_time());
if(cosmic_unicorn.is_pressed(cosmic_unicorn.SWITCH_BRIGHTNESS_UP)) {
cosmic_unicorn.adjust_brightness(+0.01);
}
if(cosmic_unicorn.is_pressed(cosmic_unicorn.SWITCH_BRIGHTNESS_DOWN)) {
cosmic_unicorn.adjust_brightness(-0.01);
}
int width = graphics.measure_text(message, 1);
scroll += 0.25f;
if(scroll > width) {
scroll = -32.0f;
}
graphics.set_pen(0, 0, 0);
graphics.clear();
uint8_t r = 0, g = 0, b = 0;
from_hsv(scroll / 100.0f, 1.0f, 0.5f, r, g, b);
graphics.set_pen(r, g, b);
graphics.text(message, Point(0 - scroll, 14), -1, 0.55);
cosmic_unicorn.update(&graphics);
sleep_ms(10);
}
return 0;
}

View File

@ -1,125 +0,0 @@
add_executable(
rainbow_text
rainbow_text.cpp
)
# Pull in pico libraries that we need
target_link_libraries(rainbow_text pico_stdlib hardware_pio hardware_adc hardware_dma pico_graphics galactic_unicorn)
pico_enable_stdio_usb(rainbow_text 1)
# create map/bin/hex file etc.
pico_add_extra_outputs(rainbow_text)
add_executable(
rainbow
rainbow.cpp
)
# Pull in pico libraries that we need
target_link_libraries(rainbow pico_stdlib hardware_pio hardware_adc hardware_dma pico_graphics galactic_unicorn)
pico_enable_stdio_usb(rainbow 1)
# create map/bin/hex file etc.
pico_add_extra_outputs(rainbow)
add_executable(
nostalgia_prompt
nostalgia_prompt.cpp
)
# Pull in pico libraries that we need
target_link_libraries(nostalgia_prompt pico_stdlib hardware_pio hardware_adc hardware_dma pico_graphics galactic_unicorn)
pico_enable_stdio_usb(nostalgia_prompt 1)
# create map/bin/hex file etc.
pico_add_extra_outputs(nostalgia_prompt)
add_executable(
eighties_super_computer
eighties_super_computer.cpp
)
# Pull in pico libraries that we need
target_link_libraries(eighties_super_computer pico_stdlib hardware_pio hardware_adc hardware_dma pico_graphics galactic_unicorn)
pico_enable_stdio_usb(eighties_super_computer 1)
# create map/bin/hex file etc.
pico_add_extra_outputs(eighties_super_computer)
add_executable(
fire_effect
fire_effect.cpp
)
# Pull in pico libraries that we need
target_link_libraries(fire_effect pico_stdlib hardware_pio hardware_adc hardware_dma pico_graphics galactic_unicorn)
pico_enable_stdio_usb(fire_effect 1)
# create map/bin/hex file etc.
pico_add_extra_outputs(fire_effect)
add_executable(
scroll_text
scroll_text.cpp
)
# Pull in pico libraries that we need
target_link_libraries(scroll_text pico_stdlib hardware_pio hardware_adc hardware_dma pico_graphics galactic_unicorn)
pico_enable_stdio_usb(scroll_text 1)
# create map/bin/hex file etc.
pico_add_extra_outputs(scroll_text)
add_executable(
balls
balls.cpp
)
# Pull in pico libraries that we need
target_link_libraries(balls pico_stdlib hardware_pio hardware_adc hardware_dma pico_graphics galactic_unicorn)
pico_enable_stdio_usb(balls 1)
# create map/bin/hex file etc.
pico_add_extra_outputs(balls)
add_executable(
lava_lamp
lava_lamp.cpp
)
# Pull in pico libraries that we need
target_link_libraries(lava_lamp pico_stdlib hardware_pio hardware_adc hardware_dma pico_graphics galactic_unicorn)
pico_enable_stdio_usb(lava_lamp 1)
# create map/bin/hex file etc.
pico_add_extra_outputs(lava_lamp)
add_executable(
feature_test
feature_test.cpp
audio_samples.cpp
)
# Pull in pico libraries that we need
target_link_libraries(feature_test pico_stdlib hardware_pio hardware_adc hardware_dma pico_graphics galactic_unicorn)
pico_enable_stdio_usb(feature_test 1)
# create map/bin/hex file etc.
pico_add_extra_outputs(feature_test)

File diff suppressed because it is too large Load Diff

View File

@ -1,87 +0,0 @@
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include "pico/stdlib.h"
#include "libraries/pico_graphics/pico_graphics.hpp"
#include "galactic_unicorn.hpp"
#include "okcolor.hpp"
using namespace pimoroni;
PicoGraphics_PenRGB888 graphics(53, 11, nullptr);
GalacticUnicorn galactic_unicorn;
// extra row of pixels for sourcing flames and averaging
float heat[53][13] = {0.0f};
int main() {
stdio_init_all();
galactic_unicorn.init();
while(true) {
if(galactic_unicorn.is_pressed(galactic_unicorn.SWITCH_BRIGHTNESS_UP)) {
galactic_unicorn.adjust_brightness(+0.01);
}
if(galactic_unicorn.is_pressed(galactic_unicorn.SWITCH_BRIGHTNESS_DOWN)) {
galactic_unicorn.adjust_brightness(-0.01);
}
graphics.set_pen(0, 0, 0);
graphics.clear();
for(int y = 0; y < 12; y++) {
for(int x = 0; x < 53; x++) {
if(heat[x][y] > 0.5f) {
graphics.set_pen(255, 255, 180);
graphics.pixel(Point(x, y));
}else if(heat[x][y] > 0.4f) {
graphics.set_pen(220, 160, 0);
graphics.pixel(Point(x, y));
}else if(heat[x][y] > 0.3f) {
graphics.set_pen(180, 50, 0);
graphics.pixel(Point(x, y));
}else if(heat[x][y] > 0.2f) {
graphics.set_pen(40, 40, 40);
graphics.pixel(Point(x, y));
}
// update this pixel by averaging the below pixels
if(x == 0) {
heat[x][y] = (heat[x][y] + heat[x][y + 2] + heat[x][y + 1] + heat[x + 1][y + 1]) / 4.0f;
} else if(x == 52) {
heat[x][y] = (heat[x][y] + heat[x][y + 2] + heat[x][y + 1] + heat[x - 1][y + 1]) / 4.0f;
} else {
heat[x][y] = (heat[x][y] + heat[x][y + 2] + heat[x][y + 1] + heat[x - 1][y + 1] + heat[x + 1][y + 1]) / 5.0f;
}
heat[x][y] -= 0.01f;
heat[x][y] = heat[x][y] < 0.0f ? 0.0f: heat[x][y];
}
}
galactic_unicorn.update(&graphics);
// clear the bottom row and then add a new fire seed to it
for(int x = 0; x < 53; x++) {
heat[x][11] = 0.0f;
}
// add a new random heat source
for(int c = 0; c < 5; c++) {
int px = (rand() % 51) + 1;
heat[px][11] = 1.0f;
heat[px + 1][11] = 1.0f;
heat[px - 1][11] = 1.0f;
heat[px][12] = 1.0f;
heat[px + 1][12] = 1.0f;
heat[px - 1][12] = 1.0f;
}
sleep_ms(50);
}
return 0;
}

View File

@ -1,68 +0,0 @@
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include "pico/stdlib.h"
#include "libraries/pico_graphics/pico_graphics.hpp"
#include "galactic_unicorn.hpp"
#include "okcolor.hpp"
using namespace pimoroni;
PicoGraphics_PenRGB888 graphics(53, 11, nullptr);
GalacticUnicorn galactic_unicorn;
float lifetime[53][11];
float age[53][11];
int main() {
stdio_init_all();
for(int y = 0; y < 11; y++) {
for(int x = 0; x < 53; x++) {
lifetime[x][y] = 1.0f + ((rand() % 10) / 100.0f);
age[x][y] = ((rand() % 100) / 100.0f) * lifetime[x][y];
}
}
galactic_unicorn.init();
while(true) {
if(galactic_unicorn.is_pressed(galactic_unicorn.SWITCH_BRIGHTNESS_UP)) {
galactic_unicorn.adjust_brightness(+0.01);
}
if(galactic_unicorn.is_pressed(galactic_unicorn.SWITCH_BRIGHTNESS_DOWN)) {
galactic_unicorn.adjust_brightness(-0.01);
}
graphics.set_pen(0, 0, 0);
graphics.clear();
for(int y = 0; y < 11; y++) {
for(int x = 0; x < 53; x++) {
if(age[x][y] < lifetime[x][y] * 0.3f) {
graphics.set_pen(230, 150, 0);
graphics.pixel(Point(x, y));
}else if(age[x][y] < lifetime[x][y] * 0.5f) {
float decay = (lifetime[x][y] * 0.5f - age[x][y]) * 5.0f;
graphics.set_pen(decay * 230, decay * 150, 0);
graphics.pixel(Point(x, y));
}
if(age[x][y] >= lifetime[x][y]) {
age[x][y] = 0.0f;
lifetime[x][y] = 1.0f + ((rand() % 10) / 100.0f);
}
age[x][y] += 0.01f;
}
}
galactic_unicorn.update(&graphics);
sleep_ms(10);
}
return 0;
}

View File

@ -1,164 +0,0 @@
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include "pico/stdlib.h"
#include "libraries/pico_graphics/pico_graphics.hpp"
#include "galactic_unicorn.hpp"
#include "okcolor.hpp"
using namespace pimoroni;
PicoGraphics_PenRGB888 graphics(53, 11, nullptr);
GalacticUnicorn galactic_unicorn;
// left_channel_bin and right_channel_bin are in audio_samples.cpp
extern uint8_t left_channel_bin[];
extern uint32_t left_channel_bin_len;
extern uint8_t right_channel_bin[];
extern uint32_t right_channel_bin_len;
void gradient(uint8_t r, uint8_t g, uint8_t b) {
for(int y = 0; y < 12; y++) {
for(int x = 0; x < 53; x++) {
// graphics.set_pen((r * x) / 52, (g * x) / 52, (b * x) / 52);
graphics.set_pen(x, x, x);
graphics.pixel(Point(x, y));
}
}
}
typedef void (*shader_func_t)(int ms, int x, int y);
void grid(int ms, int x, int y) {
int v = (x + y + (ms / 1000)) % 2 == 0 ? 255 : 0;
graphics.set_pen(v, v, v);
graphics.pixel(Point(x, y));
}
void shader_fill(int ms, shader_func_t f) {
for(int y = 0; y < 12; y++) {
for(int x = 0; x < 53; x++) {
f(ms, x, y);
}
}
}
void outline_text(std::string text) {
uint ms = to_ms_since_boot(get_absolute_time());
graphics.set_font("bitmap8");
uint8_t v = (sin(ms / 100.0f) + 1.0f) * 127.0f;
uint w = graphics.measure_text(text, 1);
int x = 53 / 2 - w / 2 + 1, y = 2;
graphics.set_pen(0, 0, 0);
graphics.text(text, Point(x - 1, y - 1), -1, 1);
graphics.text(text, Point(x , y - 1), -1, 1);
graphics.text(text, Point(x + 1, y - 1), -1, 1);
graphics.text(text, Point(x - 1, y ), -1, 1);
graphics.text(text, Point(x + 1, y ), -1, 1);
graphics.text(text, Point(x - 1, y + 1), -1, 1);
graphics.text(text, Point(x , y + 1), -1, 1);
graphics.text(text, Point(x + 1, y + 1), -1, 1);
graphics.set_pen(v, v, v);
graphics.text(text, Point(x, y), -1, 1);
}
int main() {
stdio_init_all();
galactic_unicorn.init();
//galactic_unicorn.set_brightness(0.5f);
while(true) {
if(galactic_unicorn.is_pressed(galactic_unicorn.SWITCH_BRIGHTNESS_UP)) {
galactic_unicorn.adjust_brightness(+0.01f);
}
if(galactic_unicorn.is_pressed(galactic_unicorn.SWITCH_BRIGHTNESS_DOWN)) {
galactic_unicorn.adjust_brightness(-0.01f);
}
uint ms = to_ms_since_boot(get_absolute_time());
uint8_t test = (ms / 1000) % 4;
graphics.set_pen(0, 0, 0);
graphics.clear();
switch(test) {
case 0: {
printf("grid pattern");
shader_fill(ms, grid);
}break;
case 1: {
printf("green gradient");
gradient(0, 255, 0);
}break;
case 2: {
printf("blue gradient");
gradient(0, 0, 255);
}break;
case 3: {
printf("white gradient");
gradient(255, 255, 255);
}break;
}
printf("%d\n", galactic_unicorn.light());
std::string text = "";
static bool was_a_pressed = false;
if(galactic_unicorn.is_pressed(GalacticUnicorn::SWITCH_A)) {
if(!was_a_pressed) {
galactic_unicorn.play_sample(left_channel_bin, left_channel_bin_len);
}
was_a_pressed = true;
}else{
was_a_pressed = false;
}
if(galactic_unicorn.is_pressed(GalacticUnicorn::SWITCH_A)) {
text = "Button A";
}
if(galactic_unicorn.is_pressed(GalacticUnicorn::SWITCH_B)) {
text = "Button B";
}
if(galactic_unicorn.is_pressed(GalacticUnicorn::SWITCH_C)) {
text = "Button C";
}
if(galactic_unicorn.is_pressed(GalacticUnicorn::SWITCH_D)) {
text = "Button D";
}
if(galactic_unicorn.is_pressed(GalacticUnicorn::SWITCH_VOLUME_UP)) {
text = "Louder!";
}
if(galactic_unicorn.is_pressed(GalacticUnicorn::SWITCH_VOLUME_DOWN)) {
text = "quieter";
}
if(galactic_unicorn.is_pressed(GalacticUnicorn::SWITCH_BRIGHTNESS_UP)) {
text = "Brighter!";
}
if(galactic_unicorn.is_pressed(GalacticUnicorn::SWITCH_BRIGHTNESS_DOWN)) {
text = "Darker";
}
if(galactic_unicorn.is_pressed(GalacticUnicorn::SWITCH_SLEEP)) {
text = "Zzz... zzz...";
}
outline_text(text);
galactic_unicorn.update(&graphics);
sleep_ms(50);
}
return 0;
}

View File

@ -1,122 +0,0 @@
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <string.h>
#include "pico/stdlib.h"
#include "libraries/pico_graphics/pico_graphics.hpp"
#include "galactic_unicorn.hpp"
#include "okcolor.hpp"
using namespace pimoroni;
PicoGraphics_PenRGB888 graphics(53, 11, nullptr);
GalacticUnicorn galactic_unicorn;
// extra row of pixels for sourcing flames and averaging
int width = 53;
int height = 15;
// a buffer that's at least big enough to store 55 x 15 values (to allow for both orientations)
float heat[1000] = {0.0f};
void set(int x, int y, float v) {
heat[x + y * width] = v;
}
float get(int x, int y) {
/*if(x < 0 || x >= width || y < 0 || y >= height) {
return 0.0f;
}*/
x = x < 0 ? 0 : x;
x = x >= width ? width - 1 : x;
return heat[x + y * width];
}
int main() {
stdio_init_all();
galactic_unicorn.init();
galactic_unicorn.set_brightness(0.5);
bool landscape = true;
while(true) {
if(galactic_unicorn.is_pressed(galactic_unicorn.SWITCH_BRIGHTNESS_UP)) {
galactic_unicorn.adjust_brightness(+0.01);
}
if(galactic_unicorn.is_pressed(galactic_unicorn.SWITCH_BRIGHTNESS_DOWN)) {
galactic_unicorn.adjust_brightness(-0.01);
}
if(galactic_unicorn.is_pressed(galactic_unicorn.SWITCH_A)) {
landscape = true;
width = 53;
height = 15;
memset(heat, 0, sizeof(heat));
}
if(galactic_unicorn.is_pressed(galactic_unicorn.SWITCH_B)) {
landscape = false;
width = 11;
height = 55;
memset(heat, 0, sizeof(heat));
}
for(int y = 0; y < height; y++) {
for(int x = 0; x < width; x++) {
float value = get(x, y);
graphics.set_pen(0, 0, 0);
if(value > 0.5f) {
graphics.set_pen(255, 255, 180);
}else if(value > 0.4f) {
graphics.set_pen(220, 160, 0);
}else if(value > 0.3f) {
graphics.set_pen(180, 30, 0);
}else if(value > 0.22f) {
graphics.set_pen(20, 20, 20);
}
if(landscape) {
graphics.pixel(Point(x, y));
}else{
graphics.pixel(Point(y, x));
}
// update this pixel by averaging the below pixels
float average = (get(x, y) + get(x, y + 2) + get(x, y + 1) + get(x - 1, y + 1) + get(x + 1, y + 1)) / 5.0f;
// damping factor to ensure flame tapers out towards the top of the displays
average *= landscape ? 0.95f : 0.99f;
// update the heat map with our newly averaged value
set(x, y, average);
}
}
galactic_unicorn.update(&graphics);
// clear the bottom row and then add a new fire seed to it
for(int x = 0; x < width; x++) {
set(x, height - 1, 0.0f);
}
// add a new random heat source
int source_count = landscape ? 5 : 1;
for(int c = 0; c < source_count; c++) {
int px = (rand() % (width - 4)) + 2;
set(px , height - 2, 1.0f);
set(px + 1, height - 2, 1.0f);
set(px - 1, height - 2, 1.0f);
set(px , height - 1, 1.0f);
set(px + 1, height - 1, 1.0f);
set(px - 1, height - 1, 1.0f);
}
sleep_ms(20);
}
return 0;
}

View File

@ -1,149 +0,0 @@
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include "pico/stdlib.h"
#include "libraries/pico_graphics/pico_graphics.hpp"
#include "galactic_unicorn.hpp"
#include "okcolor.hpp"
using namespace pimoroni;
PicoGraphics_PenRGB888 graphics(53, 11, nullptr);
GalacticUnicorn galactic_unicorn;
// HSV Conversion expects float inputs in the range of 0.00-1.00 for each channel
// Outputs are rgb in the range 0-255 for each channel
void from_hsv(float h, float s, float v, uint8_t &r, uint8_t &g, uint8_t &b) {
float i = floor(h * 6.0f);
float f = h * 6.0f - i;
v *= 255.0f;
uint8_t p = v * (1.0f - s);
uint8_t q = v * (1.0f - f * s);
uint8_t t = v * (1.0f - (1.0f - f) * s);
switch (int(i) % 6) {
case 0: r = v; g = t; b = p; break;
case 1: r = q; g = v; b = p; break;
case 2: r = p; g = v; b = t; break;
case 3: r = p; g = q; b = v; break;
case 4: r = t; g = p; b = v; break;
case 5: r = v; g = p; b = q; break;
}
}
struct blob_t {
float x, y;
float r;
float dx, dy;
};
constexpr int blob_count = 20;
int main() {
stdio_init_all();
galactic_unicorn.init();
galactic_unicorn.set_brightness(0.5);
// randomise blob start positions, directions, and size
std::array<blob_t, blob_count> blobs;
for(auto &blob : blobs) {
blob.x = rand() % 11;
blob.y = rand() % 53;
blob.r = ((rand() % 40) / 10.0f) + 5.0f;
blob.dx = ((rand() % 2) / 10.0f) - 0.05f;
blob.dy = ((rand() % 3) / 10.0f) - 0.1f;
}
float hue = 0.0f;
while(true) {
// allow user to adjust brightness
if(galactic_unicorn.is_pressed(galactic_unicorn.SWITCH_BRIGHTNESS_UP)) {
galactic_unicorn.adjust_brightness(+0.01);
}
if(galactic_unicorn.is_pressed(galactic_unicorn.SWITCH_BRIGHTNESS_DOWN)) {
galactic_unicorn.adjust_brightness(-0.01);
}
uint start_ms = to_ms_since_boot(get_absolute_time());
// calculate the influence of each blob on the liquid based
// on their distance to each pixel. this causes blobs to
// "merge" into each other when we use fixed thresholds to
// determine which colour to draw with
float liquid[11][53] = {0.0f};
for(auto &blob : blobs) {
float r_sq = blob.r * blob.r;
for(int y = 0; y < 53; y++) {
for(int x = 0; x < 11; x++) {
float d_sq = (x - blob.x) * (x - blob.x) + (y - blob.y) * (y - blob.y);
if(d_sq <= r_sq) {
// add this blobs influence to this pixel
liquid[x][y] += 1.0f - (d_sq / r_sq);
}
}
}
}
// update the blob positions
for(auto &blob : blobs) {
blob.x += blob.dx;
blob.y += blob.dy;
// if we hit the edge then bounce!
if(blob.x < 0.0f || blob.x >= 11.0f) {
blob.dx *= -1.0f;
}
if(blob.y < 0.0f || blob.y >= 53.0f) {
blob.dy *= -1.0f;
}
}
// rotate the hue
hue += 0.001f;
// calculate dark, medium, and bright shades for rendering the
// lava
uint8_t dark_r, dark_g, dark_b;
from_hsv(hue, 1.0f, 0.3f, dark_r, dark_g, dark_b);
uint8_t mid_r, mid_g, mid_b;
from_hsv(hue, 1.0f, 0.6f, mid_r, mid_g, mid_b);
uint8_t bright_r, bright_g, bright_b;
from_hsv(hue, 1.0f, 1.0f, bright_r, bright_g, bright_b);
// clear the canvas
graphics.set_pen(0, 0, 0);
graphics.clear();
// render the lava
for(int y = 0; y < 53; y++) {
for(int x = 0; x < 11; x++) {
float 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.5f) {
graphics.set_pen(bright_r, bright_g, bright_b);
graphics.pixel(Point(y, x));
}else if(v >= 1.25f) {
graphics.set_pen(mid_r, mid_g, mid_b);
graphics.pixel(Point(y, x));
}else if(v >= 1.0f) {
graphics.set_pen(dark_r, dark_g, dark_b);
graphics.pixel(Point(y, x));
}
}
}
uint end_ms = to_ms_since_boot(get_absolute_time());
printf("rendering took %dms\n", end_ms - start_ms);
galactic_unicorn.update(&graphics);
}
return 0;
}

View File

@ -1,122 +0,0 @@
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include "pico/stdlib.h"
#include "libraries/pico_graphics/pico_graphics.hpp"
#include "galactic_unicorn.hpp"
#include "okcolor.hpp"
using namespace pimoroni;
PicoGraphics_PenRGB888 graphics(53, 11, nullptr);
GalacticUnicorn galactic_unicorn;
std::array<std::string, 11> c64 = {
"",
" OOOOO OOOOOO OO OOOO OO OO XXXXXXX",
" OO OO OO OOOO OO OO OO OO XXXXXXX",
" OO OO OO OO OO OO OO OO OO XXXXXXX",
" OOOOO OOOO OOOOOO OO OO OOOO XXXXXXX",
" OOOO OO OO OO OO OO OO XXXXXXX",
" OO OO OO OO OO OO OO OO OO XXXXXXX",
" OO OO OOOOOO OO OO OOOO OO OO XXXXXXX",
" XXXXXXX"
};
std::array<std::string, 11> spectrum = {
"",
" O OOOO OOOO OOOOO O O O O XXXXXXXX",
" O O O O O O O O O O O X XXXXXX",
" O O O O O O O X XXXXXX",
" O O O OOOOOO O O X XXXXXX",
" O O O O O O O X XXXXXX",
" OOOOOO OOOO O O OOOOO X XXXXXX",
" X X",
" XXXXXXXX"
};
std::array<std::string, 11> bbc_micro = {
"",
" OOOOO OO OOOO OOO OOOO O ",
" O O O O O O O O O O ",
" O O O O O O O O ",
" OOOOO O O OOOO O O O ",
" O O OOOOOO O O O O ",
" O O O O O O O O O O ",
" OOOOO O O OOOO OOO OOOO O ",
" XXXXXXX"
};
constexpr uint PROMPT_C64 = 0;
constexpr uint PROMPT_SPECTRUM = 1;
constexpr uint PROMPT_BBC_MICRO = 2;
uint prompt = 0;
int main() {
stdio_init_all();
galactic_unicorn.init();
while(true) {
uint time_ms = to_ms_since_boot(get_absolute_time());
prompt = (time_ms / 3000) % 3;
if(galactic_unicorn.is_pressed(galactic_unicorn.SWITCH_BRIGHTNESS_UP)) {
galactic_unicorn.adjust_brightness(+0.01);
}
if(galactic_unicorn.is_pressed(galactic_unicorn.SWITCH_BRIGHTNESS_DOWN)) {
galactic_unicorn.adjust_brightness(-0.01);
}
std::array<std::string, 11> image = c64;
if(prompt == PROMPT_C64) {
image = c64;
}else if(prompt == PROMPT_SPECTRUM) {
image = spectrum;
}else if(prompt == PROMPT_BBC_MICRO) {
image = bbc_micro;
}
if(prompt == PROMPT_C64) {
graphics.set_pen(20, 20, 120);
}else if(prompt == PROMPT_SPECTRUM){
graphics.set_pen(180, 150, 150);
}else if(prompt == PROMPT_BBC_MICRO){
graphics.set_pen(0, 0, 0);
}
graphics.clear();
if(prompt == PROMPT_C64) {
graphics.set_pen(230, 210, 250);
}else if(prompt == PROMPT_SPECTRUM){
graphics.set_pen(0, 0, 0);
}else if(prompt == PROMPT_BBC_MICRO){
graphics.set_pen(255, 255, 255);
}
for(size_t y = 0; y < image.size(); y++) {
for(size_t x = 0; x < image[y].length(); x++) {
// draw the prompt text
if(image[y][x] == 'O') {
graphics.pixel(Point(x, y + 1));
}
// draw the caret blinking
if(image[y][x] == 'X' && (time_ms / 300) % 2) {
graphics.pixel(Point(x, y + 1));
}
}
}
galactic_unicorn.update(&graphics);
sleep_ms(10);
}
return 0;
}

View File

@ -1,684 +0,0 @@
#pragma once
// Copyright(c) 2021 Björn Ottosson
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this softwareand associated documentation files(the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and /or sell copies
// of the Software, and to permit persons to whom the Software is furnished to do
// so, subject to the following conditions :
// The above copyright noticeand this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
#include <cmath>
#include <cfloat>
namespace ok_color
{
struct Lab { float L; float a; float b; };
struct RGB { float r; float g; float b; };
struct HSV { float h; float s; float v; };
struct HSL { float h; float s; float l; };
struct LC { float L; float C; };
// Alternative representation of (L_cusp, C_cusp)
// Encoded so S = C_cusp/L_cusp and T = C_cusp/(1-L_cusp)
// The maximum value for C in the triangle is then found as fmin(S*L, T*(1-L)), for a given L
struct ST { float S; float T; };
constexpr float pi = 3.1415926535897932384626433832795028841971693993751058209749445923078164062f;
float clamp(float x, float min, float max)
{
if (x < min)
return min;
if (x > max)
return max;
return x;
}
float sgn(float x)
{
return (float)(0.f < x) - (float)(x < 0.f);
}
float srgb_transfer_function(float a)
{
return .0031308f >= a ? 12.92f * a : 1.055f * powf(a, .4166666666666667f) - .055f;
}
float srgb_transfer_function_inv(float a)
{
return .04045f < a ? powf((a + .055f) / 1.055f, 2.4f) : a / 12.92f;
}
Lab linear_srgb_to_oklab(RGB c)
{
float l = 0.4122214708f * c.r + 0.5363325363f * c.g + 0.0514459929f * c.b;
float m = 0.2119034982f * c.r + 0.6806995451f * c.g + 0.1073969566f * c.b;
float s = 0.0883024619f * c.r + 0.2817188376f * c.g + 0.6299787005f * c.b;
float l_ = cbrtf(l);
float m_ = cbrtf(m);
float s_ = cbrtf(s);
return {
0.2104542553f * l_ + 0.7936177850f * m_ - 0.0040720468f * s_,
1.9779984951f * l_ - 2.4285922050f * m_ + 0.4505937099f * s_,
0.0259040371f * l_ + 0.7827717662f * m_ - 0.8086757660f * s_,
};
}
RGB oklab_to_linear_srgb(Lab c)
{
float l_ = c.L + 0.3963377774f * c.a + 0.2158037573f * c.b;
float m_ = c.L - 0.1055613458f * c.a - 0.0638541728f * c.b;
float s_ = c.L - 0.0894841775f * c.a - 1.2914855480f * c.b;
float l = l_ * l_ * l_;
float m = m_ * m_ * m_;
float s = s_ * s_ * s_;
return {
+4.0767416621f * l - 3.3077115913f * m + 0.2309699292f * s,
-1.2684380046f * l + 2.6097574011f * m - 0.3413193965f * s,
-0.0041960863f * l - 0.7034186147f * m + 1.7076147010f * s,
};
}
// Finds the maximum saturation possible for a given hue that fits in sRGB
// Saturation here is defined as S = C/L
// a and b must be normalized so a^2 + b^2 == 1
float compute_max_saturation(float a, float b)
{
// Max saturation will be when one of r, g or b goes below zero.
// Select different coefficients depending on which component goes below zero first
float k0, k1, k2, k3, k4, wl, wm, ws;
if (-1.88170328f * a - 0.80936493f * b > 1)
{
// Red component
k0 = +1.19086277f; k1 = +1.76576728f; k2 = +0.59662641f; k3 = +0.75515197f; k4 = +0.56771245f;
wl = +4.0767416621f; wm = -3.3077115913f; ws = +0.2309699292f;
}
else if (1.81444104f * a - 1.19445276f * b > 1)
{
// Green component
k0 = +0.73956515f; k1 = -0.45954404f; k2 = +0.08285427f; k3 = +0.12541070f; k4 = +0.14503204f;
wl = -1.2684380046f; wm = +2.6097574011f; ws = -0.3413193965f;
}
else
{
// Blue component
k0 = +1.35733652f; k1 = -0.00915799f; k2 = -1.15130210f; k3 = -0.50559606f; k4 = +0.00692167f;
wl = -0.0041960863f; wm = -0.7034186147f; ws = +1.7076147010f;
}
// Approximate max saturation using a polynomial:
float S = k0 + k1 * a + k2 * b + k3 * a * a + k4 * a * b;
// Do one step Halley's method to get closer
// this gives an error less than 10e6, except for some blue hues where the dS/dh is close to infinite
// this should be sufficient for most applications, otherwise do two/three steps
float k_l = +0.3963377774f * a + 0.2158037573f * b;
float k_m = -0.1055613458f * a - 0.0638541728f * b;
float k_s = -0.0894841775f * a - 1.2914855480f * b;
{
float l_ = 1.f + S * k_l;
float m_ = 1.f + S * k_m;
float s_ = 1.f + S * k_s;
float l = l_ * l_ * l_;
float m = m_ * m_ * m_;
float s = s_ * s_ * s_;
float l_dS = 3.f * k_l * l_ * l_;
float m_dS = 3.f * k_m * m_ * m_;
float s_dS = 3.f * k_s * s_ * s_;
float l_dS2 = 6.f * k_l * k_l * l_;
float m_dS2 = 6.f * k_m * k_m * m_;
float s_dS2 = 6.f * k_s * k_s * s_;
float f = wl * l + wm * m + ws * s;
float f1 = wl * l_dS + wm * m_dS + ws * s_dS;
float f2 = wl * l_dS2 + wm * m_dS2 + ws * s_dS2;
S = S - f * f1 / (f1 * f1 - 0.5f * f * f2);
}
return S;
}
// finds L_cusp and C_cusp for a given hue
// a and b must be normalized so a^2 + b^2 == 1
LC find_cusp(float a, float b)
{
// First, find the maximum saturation (saturation S = C/L)
float S_cusp = compute_max_saturation(a, b);
// Convert to linear sRGB to find the first point where at least one of r,g or b >= 1:
RGB rgb_at_max = oklab_to_linear_srgb({ 1, S_cusp * a, S_cusp * b });
float L_cusp = cbrtf(1.f / fmax(fmax(rgb_at_max.r, rgb_at_max.g), rgb_at_max.b));
float C_cusp = L_cusp * S_cusp;
return { L_cusp , C_cusp };
}
// Finds intersection of the line defined by
// L = L0 * (1 - t) + t * L1;
// C = t * C1;
// a and b must be normalized so a^2 + b^2 == 1
float find_gamut_intersection(float a, float b, float L1, float C1, float L0, LC cusp)
{
// Find the intersection for upper and lower half seprately
float t;
if (((L1 - L0) * cusp.C - (cusp.L - L0) * C1) <= 0.f)
{
// Lower half
t = cusp.C * L0 / (C1 * cusp.L + cusp.C * (L0 - L1));
}
else
{
// Upper half
// First intersect with triangle
t = cusp.C * (L0 - 1.f) / (C1 * (cusp.L - 1.f) + cusp.C * (L0 - L1));
// Then one step Halley's method
{
float dL = L1 - L0;
float dC = C1;
float k_l = +0.3963377774f * a + 0.2158037573f * b;
float k_m = -0.1055613458f * a - 0.0638541728f * b;
float k_s = -0.0894841775f * a - 1.2914855480f * b;
float l_dt = dL + dC * k_l;
float m_dt = dL + dC * k_m;
float s_dt = dL + dC * k_s;
// If higher accuracy is required, 2 or 3 iterations of the following block can be used:
{
float L = L0 * (1.f - t) + t * L1;
float C = t * C1;
float l_ = L + C * k_l;
float m_ = L + C * k_m;
float s_ = L + C * k_s;
float l = l_ * l_ * l_;
float m = m_ * m_ * m_;
float s = s_ * s_ * s_;
float ldt = 3 * l_dt * l_ * l_;
float mdt = 3 * m_dt * m_ * m_;
float sdt = 3 * s_dt * s_ * s_;
float ldt2 = 6 * l_dt * l_dt * l_;
float mdt2 = 6 * m_dt * m_dt * m_;
float sdt2 = 6 * s_dt * s_dt * s_;
float r = 4.0767416621f * l - 3.3077115913f * m + 0.2309699292f * s - 1;
float r1 = 4.0767416621f * ldt - 3.3077115913f * mdt + 0.2309699292f * sdt;
float r2 = 4.0767416621f * ldt2 - 3.3077115913f * mdt2 + 0.2309699292f * sdt2;
float u_r = r1 / (r1 * r1 - 0.5f * r * r2);
float t_r = -r * u_r;
float g = -1.2684380046f * l + 2.6097574011f * m - 0.3413193965f * s - 1;
float g1 = -1.2684380046f * ldt + 2.6097574011f * mdt - 0.3413193965f * sdt;
float g2 = -1.2684380046f * ldt2 + 2.6097574011f * mdt2 - 0.3413193965f * sdt2;
float u_g = g1 / (g1 * g1 - 0.5f * g * g2);
float t_g = -g * u_g;
float b = -0.0041960863f * l - 0.7034186147f * m + 1.7076147010f * s - 1;
float b1 = -0.0041960863f * ldt - 0.7034186147f * mdt + 1.7076147010f * sdt;
float b2 = -0.0041960863f * ldt2 - 0.7034186147f * mdt2 + 1.7076147010f * sdt2;
float u_b = b1 / (b1 * b1 - 0.5f * b * b2);
float t_b = -b * u_b;
t_r = u_r >= 0.f ? t_r : FLT_MAX;
t_g = u_g >= 0.f ? t_g : FLT_MAX;
t_b = u_b >= 0.f ? t_b : FLT_MAX;
t += fmin(t_r, fmin(t_g, t_b));
}
}
}
return t;
}
float find_gamut_intersection(float a, float b, float L1, float C1, float L0)
{
// Find the cusp of the gamut triangle
LC cusp = find_cusp(a, b);
return find_gamut_intersection(a, b, L1, C1, L0, cusp);
}
RGB gamut_clip_preserve_chroma(RGB rgb)
{
if (rgb.r < 1 && rgb.g < 1 && rgb.b < 1 && rgb.r > 0 && rgb.g > 0 && rgb.b > 0)
return rgb;
Lab lab = linear_srgb_to_oklab(rgb);
float L = lab.L;
float eps = 0.00001f;
float C = fmax(eps, sqrtf(lab.a * lab.a + lab.b * lab.b));
float a_ = lab.a / C;
float b_ = lab.b / C;
float L0 = clamp(L, 0, 1);
float t = find_gamut_intersection(a_, b_, L, C, L0);
float L_clipped = L0 * (1 - t) + t * L;
float C_clipped = t * C;
return oklab_to_linear_srgb({ L_clipped, C_clipped * a_, C_clipped * b_ });
}
RGB gamut_clip_project_to_0_5(RGB rgb)
{
if (rgb.r < 1 && rgb.g < 1 && rgb.b < 1 && rgb.r > 0 && rgb.g > 0 && rgb.b > 0)
return rgb;
Lab lab = linear_srgb_to_oklab(rgb);
float L = lab.L;
float eps = 0.00001f;
float C = fmax(eps, sqrtf(lab.a * lab.a + lab.b * lab.b));
float a_ = lab.a / C;
float b_ = lab.b / C;
float L0 = 0.5;
float t = find_gamut_intersection(a_, b_, L, C, L0);
float L_clipped = L0 * (1 - t) + t * L;
float C_clipped = t * C;
return oklab_to_linear_srgb({ L_clipped, C_clipped * a_, C_clipped * b_ });
}
RGB gamut_clip_project_to_L_cusp(RGB rgb)
{
if (rgb.r < 1 && rgb.g < 1 && rgb.b < 1 && rgb.r > 0 && rgb.g > 0 && rgb.b > 0)
return rgb;
Lab lab = linear_srgb_to_oklab(rgb);
float L = lab.L;
float eps = 0.00001f;
float C = fmax(eps, sqrtf(lab.a * lab.a + lab.b * lab.b));
float a_ = lab.a / C;
float b_ = lab.b / C;
// The cusp is computed here and in find_gamut_intersection, an optimized solution would only compute it once.
LC cusp = find_cusp(a_, b_);
float L0 = cusp.L;
float t = find_gamut_intersection(a_, b_, L, C, L0);
float L_clipped = L0 * (1 - t) + t * L;
float C_clipped = t * C;
return oklab_to_linear_srgb({ L_clipped, C_clipped * a_, C_clipped * b_ });
}
RGB gamut_clip_adaptive_L0_0_5(RGB rgb, float alpha = 0.05f)
{
if (rgb.r < 1 && rgb.g < 1 && rgb.b < 1 && rgb.r > 0 && rgb.g > 0 && rgb.b > 0)
return rgb;
Lab lab = linear_srgb_to_oklab(rgb);
float L = lab.L;
float eps = 0.00001f;
float C = fmax(eps, sqrtf(lab.a * lab.a + lab.b * lab.b));
float a_ = lab.a / C;
float b_ = lab.b / C;
float Ld = L - 0.5f;
float e1 = 0.5f + fabs(Ld) + alpha * C;
float L0 = 0.5f * (1.f + sgn(Ld) * (e1 - sqrtf(e1 * e1 - 2.f * fabs(Ld))));
float t = find_gamut_intersection(a_, b_, L, C, L0);
float L_clipped = L0 * (1.f - t) + t * L;
float C_clipped = t * C;
return oklab_to_linear_srgb({ L_clipped, C_clipped * a_, C_clipped * b_ });
}
RGB gamut_clip_adaptive_L0_L_cusp(RGB rgb, float alpha = 0.05f)
{
if (rgb.r < 1 && rgb.g < 1 && rgb.b < 1 && rgb.r > 0 && rgb.g > 0 && rgb.b > 0)
return rgb;
Lab lab = linear_srgb_to_oklab(rgb);
float L = lab.L;
float eps = 0.00001f;
float C = fmax(eps, sqrtf(lab.a * lab.a + lab.b * lab.b));
float a_ = lab.a / C;
float b_ = lab.b / C;
// The cusp is computed here and in find_gamut_intersection, an optimized solution would only compute it once.
LC cusp = find_cusp(a_, b_);
float Ld = L - cusp.L;
float k = 2.f * (Ld > 0 ? 1.f - cusp.L : cusp.L);
float e1 = 0.5f * k + fabs(Ld) + alpha * C / k;
float L0 = cusp.L + 0.5f * (sgn(Ld) * (e1 - sqrtf(e1 * e1 - 2.f * k * fabs(Ld))));
float t = find_gamut_intersection(a_, b_, L, C, L0);
float L_clipped = L0 * (1.f - t) + t * L;
float C_clipped = t * C;
return oklab_to_linear_srgb({ L_clipped, C_clipped * a_, C_clipped * b_ });
}
float toe(float x)
{
constexpr float k_1 = 0.206f;
constexpr float k_2 = 0.03f;
constexpr float k_3 = (1.f + k_1) / (1.f + k_2);
return 0.5f * (k_3 * x - k_1 + sqrtf((k_3 * x - k_1) * (k_3 * x - k_1) + 4 * k_2 * k_3 * x));
}
float toe_inv(float x)
{
constexpr float k_1 = 0.206f;
constexpr float k_2 = 0.03f;
constexpr float k_3 = (1.f + k_1) / (1.f + k_2);
return (x * x + k_1 * x) / (k_3 * (x + k_2));
}
ST to_ST(LC cusp)
{
float L = cusp.L;
float C = cusp.C;
return { C / L, C / (1 - L) };
}
// Returns a smooth approximation of the location of the cusp
// This polynomial was created by an optimization process
// It has been designed so that S_mid < S_max and T_mid < T_max
ST get_ST_mid(float a_, float b_)
{
float S = 0.11516993f + 1.f / (
+7.44778970f + 4.15901240f * b_
+ a_ * (-2.19557347f + 1.75198401f * b_
+ a_ * (-2.13704948f - 10.02301043f * b_
+ a_ * (-4.24894561f + 5.38770819f * b_ + 4.69891013f * a_
)))
);
float T = 0.11239642f + 1.f / (
+1.61320320f - 0.68124379f * b_
+ a_ * (+0.40370612f + 0.90148123f * b_
+ a_ * (-0.27087943f + 0.61223990f * b_
+ a_ * (+0.00299215f - 0.45399568f * b_ - 0.14661872f * a_
)))
);
return { S, T };
}
struct Cs { float C_0; float C_mid; float C_max; };
Cs get_Cs(float L, float a_, float b_)
{
LC cusp = find_cusp(a_, b_);
float C_max = find_gamut_intersection(a_, b_, L, 1, L, cusp);
ST ST_max = to_ST(cusp);
// Scale factor to compensate for the curved part of gamut shape:
float k = C_max / fmin((L * ST_max.S), (1 - L) * ST_max.T);
float C_mid;
{
ST ST_mid = get_ST_mid(a_, b_);
// Use a soft minimum function, instead of a sharp triangle shape to get a smooth value for chroma.
float C_a = L * ST_mid.S;
float C_b = (1.f - L) * ST_mid.T;
C_mid = 0.9f * k * sqrtf(sqrtf(1.f / (1.f / (C_a * C_a * C_a * C_a) + 1.f / (C_b * C_b * C_b * C_b))));
}
float C_0;
{
// for C_0, the shape is independent of hue, so ST are constant. Values picked to roughly be the average values of ST.
float C_a = L * 0.4f;
float C_b = (1.f - L) * 0.8f;
// Use a soft minimum function, instead of a sharp triangle shape to get a smooth value for chroma.
C_0 = sqrtf(1.f / (1.f / (C_a * C_a) + 1.f / (C_b * C_b)));
}
return { C_0, C_mid, C_max };
}
RGB okhsl_to_srgb(HSL hsl)
{
float h = hsl.h;
float s = hsl.s;
float l = hsl.l;
if (l == 1.0f)
{
return { 1.f, 1.f, 1.f };
}
else if (l == 0.f)
{
return { 0.f, 0.f, 0.f };
}
float a_ = cosf(2.f * pi * h);
float b_ = sinf(2.f * pi * h);
float L = toe_inv(l);
Cs cs = get_Cs(L, a_, b_);
float C_0 = cs.C_0;
float C_mid = cs.C_mid;
float C_max = cs.C_max;
float mid = 0.8f;
float mid_inv = 1.25f;
float C, t, k_0, k_1, k_2;
if (s < mid)
{
t = mid_inv * s;
k_1 = mid * C_0;
k_2 = (1.f - k_1 / C_mid);
C = t * k_1 / (1.f - k_2 * t);
}
else
{
t = (s - mid)/ (1 - mid);
k_0 = C_mid;
k_1 = (1.f - mid) * C_mid * C_mid * mid_inv * mid_inv / C_0;
k_2 = (1.f - (k_1) / (C_max - C_mid));
C = k_0 + t * k_1 / (1.f - k_2 * t);
}
RGB rgb = oklab_to_linear_srgb({ L, C * a_, C * b_ });
return {
srgb_transfer_function(rgb.r),
srgb_transfer_function(rgb.g),
srgb_transfer_function(rgb.b),
};
}
HSL srgb_to_okhsl(RGB rgb)
{
Lab lab = linear_srgb_to_oklab({
srgb_transfer_function_inv(rgb.r),
srgb_transfer_function_inv(rgb.g),
srgb_transfer_function_inv(rgb.b)
});
float C = sqrtf(lab.a * lab.a + lab.b * lab.b);
float a_ = lab.a / C;
float b_ = lab.b / C;
float L = lab.L;
float h = 0.5f + 0.5f * atan2f(-lab.b, -lab.a) / pi;
Cs cs = get_Cs(L, a_, b_);
float C_0 = cs.C_0;
float C_mid = cs.C_mid;
float C_max = cs.C_max;
// Inverse of the interpolation in okhsl_to_srgb:
float mid = 0.8f;
float mid_inv = 1.25f;
float s;
if (C < C_mid)
{
float k_1 = mid * C_0;
float k_2 = (1.f - k_1 / C_mid);
float t = C / (k_1 + k_2 * C);
s = t * mid;
}
else
{
float k_0 = C_mid;
float k_1 = (1.f - mid) * C_mid * C_mid * mid_inv * mid_inv / C_0;
float k_2 = (1.f - (k_1) / (C_max - C_mid));
float t = (C - k_0) / (k_1 + k_2 * (C - k_0));
s = mid + (1.f - mid) * t;
}
float l = toe(L);
return { h, s, l };
}
RGB okhsv_to_srgb(HSV hsv)
{
float h = hsv.h;
float s = hsv.s;
float v = hsv.v;
float a_ = cosf(2.f * pi * h);
float b_ = sinf(2.f * pi * h);
LC cusp = find_cusp(a_, b_);
ST ST_max = to_ST(cusp);
float S_max = ST_max.S;
float T_max = ST_max.T;
float S_0 = 0.5f;
float k = 1 - S_0 / S_max;
// first we compute L and V as if the gamut is a perfect triangle:
// L, C when v==1:
float L_v = 1 - s * S_0 / (S_0 + T_max - T_max * k * s);
float C_v = s * T_max * S_0 / (S_0 + T_max - T_max * k * s);
float L = v * L_v;
float C = v * C_v;
// then we compensate for both toe and the curved top part of the triangle:
float L_vt = toe_inv(L_v);
float C_vt = C_v * L_vt / L_v;
float L_new = toe_inv(L);
C = C * L_new / L;
L = L_new;
RGB rgb_scale = oklab_to_linear_srgb({ L_vt, a_ * C_vt, b_ * C_vt });
float scale_L = cbrtf(1.f / fmax(fmax(rgb_scale.r, rgb_scale.g), fmax(rgb_scale.b, 0.f)));
L = L * scale_L;
C = C * scale_L;
RGB rgb = oklab_to_linear_srgb({ L, C * a_, C * b_ });
return {
srgb_transfer_function(rgb.r),
srgb_transfer_function(rgb.g),
srgb_transfer_function(rgb.b),
};
}
HSV srgb_to_okhsv(RGB rgb)
{
Lab lab = linear_srgb_to_oklab({
srgb_transfer_function_inv(rgb.r),
srgb_transfer_function_inv(rgb.g),
srgb_transfer_function_inv(rgb.b)
});
float C = sqrtf(lab.a * lab.a + lab.b * lab.b);
float a_ = lab.a / C;
float b_ = lab.b / C;
float L = lab.L;
float h = 0.5f + 0.5f * atan2f(-lab.b, -lab.a) / pi;
LC cusp = find_cusp(a_, b_);
ST ST_max = to_ST(cusp);
float S_max = ST_max.S;
float T_max = ST_max.T;
float S_0 = 0.5f;
float k = 1 - S_0 / S_max;
// first we find L_v, C_v, L_vt and C_vt
float t = T_max / (C + L * T_max);
float L_v = t * L;
float C_v = t * C;
float L_vt = toe_inv(L_v);
float C_vt = C_v * L_vt / L_v;
// we can then use these to invert the step that compensates for the toe and the curved top part of the triangle:
RGB rgb_scale = oklab_to_linear_srgb({ L_vt, a_ * C_vt, b_ * C_vt });
float scale_L = cbrtf(1.f / fmax(fmax(rgb_scale.r, rgb_scale.g), fmax(rgb_scale.b, 0.f)));
L = L / scale_L;
C = C / scale_L;
C = C * toe(L) / L;
L = toe(L);
// we can now compute v and s:
float v = L / L_v;
float s = (S_0 + T_max) * C_v / ((T_max * S_0) + T_max * k * C_v);
return { h, s, v };
}
} // namespace ok_color

View File

@ -1,200 +0,0 @@
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include "pico/stdlib.h"
#include "libraries/pico_graphics/pico_graphics.hpp"
#include "galactic_unicorn.hpp"
#include "okcolor.hpp"
using namespace pimoroni;
PicoGraphics_PenRGB888 graphics(53, 11, nullptr);
GalacticUnicorn galactic_unicorn;
// HSV Conversion expects float inputs in the range of 0.00-1.00 for each channel
// Outputs are rgb in the range 0-255 for each channel
void from_hsv(float h, float s, float v, uint8_t &r, uint8_t &g, uint8_t &b) {
float i = floor(h * 6.0f);
float f = h * 6.0f - i;
v *= 255.0f;
uint8_t p = v * (1.0f - s);
uint8_t q = v * (1.0f - f * s);
uint8_t t = v * (1.0f - (1.0f - f) * s);
switch (int(i) % 6) {
case 0: r = v; g = t; b = p; break;
case 1: r = q; g = v; b = p; break;
case 2: r = p; g = v; b = t; break;
case 3: r = p; g = q; b = v; break;
case 4: r = t; g = p; b = v; break;
case 5: r = v; g = p; b = q; break;
}
}
void text(std::string t, Point p, float s = 1.0f, float a = 1.0f) {
int w = graphics.measure_text(t, s);
p.x += (53 / 2) - (w / 2);
p.y += (11 / 2);
graphics.text(t, Point(p.x, p.y), -1, s, a);
//graphics.text(t, Point(p.x + 1, p.y), -1, s, a);
//graphics.text(t, Point(p.x + 1, p.y + 1), -1, s, a);
//graphics.text(t, Point(p.x, p.y + 1), -1, s, a);
}
struct star_t {
float dx, dy, x, y, a;
uint8_t brightness() {
int b = a / 5;
return b > 15 ? 15 : b;
}
};
void init_star(star_t &s) {
s.x = ((rand() % 100) / 5.0f) - 10.0f;
s.y = ((rand() % 100) / 10.0f) - 5.0f;
s.dx = s.x / 10.0f;
s.dy = s.y / 10.0f;
s.a = 0;
}
void step_star(star_t &s) {
s.x += s.dx;
s.y += s.dy;
s.a++;
if(s.a > 100) {
init_star(s);
}
}
int main() {
stdio_init_all();
uint8_t hue_map[53][3];
for(int i = 0; i < 53; i++) {
from_hsv(i / 53.0f, 1.0f, 1.0f, hue_map[i][0], hue_map[i][1], hue_map[i][2]);
}
star_t stars[100];
for(int i = 0; i < 100; i++) {
init_star(stars[i]);
stars[i].a = i;
}
gpio_set_function(28, GPIO_FUNC_SIO);
gpio_set_dir(28, GPIO_OUT);
for(int i = 0; i < 10; i++) {
gpio_put(28, !gpio_get(28));
sleep_ms(100);
}
sleep_ms(1000);
gpio_put(28,true);
galactic_unicorn.init();
/*
bool a_pressed = false;
bool b_pressed = false;
bool x_pressed = false;
bool y_pressed = false;
*/
graphics.set_font("bitmap8");
float i = 0;
float hue_offset = 0.0f;
bool animate = true;
float stripe_width = 3.0f;
float speed = 1.0f;
float curve = 0.0f;
while(true) {
if(animate) {
i += speed;
}
if(galactic_unicorn.is_pressed(galactic_unicorn.SWITCH_VOLUME_UP)) {
curve += 0.05;
if(hue_offset > 1.0f) hue_offset = 1.0f;
}
if(galactic_unicorn.is_pressed(galactic_unicorn.SWITCH_VOLUME_DOWN)) {
curve -= 0.05;
if(hue_offset < 0.0f) hue_offset = 0.0f;
}
if(galactic_unicorn.is_pressed(galactic_unicorn.SWITCH_BRIGHTNESS_UP)) {
galactic_unicorn.adjust_brightness(+0.01);
}
if(galactic_unicorn.is_pressed(galactic_unicorn.SWITCH_BRIGHTNESS_DOWN)) {
galactic_unicorn.adjust_brightness(-0.01);
}
if(galactic_unicorn.is_pressed(galactic_unicorn.SWITCH_SLEEP)) {
animate = false;
}
if(galactic_unicorn.is_pressed(galactic_unicorn.SWITCH_A)) {
speed += 0.05f;
speed = speed >= 10.0f ? 10.0f : speed;
animate = true;
}
if(galactic_unicorn.is_pressed(galactic_unicorn.SWITCH_B)) {
speed -= 0.05f;
speed = speed <= 0.0f ? 0.0f : speed;
animate = true;
}
if(galactic_unicorn.is_pressed(galactic_unicorn.SWITCH_C)) {
stripe_width += 0.05f;
stripe_width = stripe_width >= 10.0f ? 10.0f : stripe_width;
}
if(galactic_unicorn.is_pressed(galactic_unicorn.SWITCH_D)) {
stripe_width -= 0.05f;
stripe_width = stripe_width <= 1.0f ? 1.0f : stripe_width;
}
/*
graphics.set_pen(255, 255, 255);
float s = 0.65f;//0.65f + (sin(i / 25.0f) * 0.15f);
float a = 1.0f;// (sin(i / 25.0f) * 100.0f);
float x = (sin(i / 74.0f) * 80.0f) * s;
float y = (cos(i / 43.0f) * 6.0f) * s;
text("Chester smells!", Point(x, y - 3), s, a);
*/
for(int x = 0; x < 53; x++) {
for(int y = 0; y < 11; y++) {
int v = ((sin((x + y) / stripe_width + (sin((y * 3.1415927f * 2.0f) / 11.0f) * curve) + i / 15.0f) + 1.5f) / 2.5f) * 255.0f;
uint8_t r = (hue_map[x][0] * v) / 256;
uint8_t g = (hue_map[x][1] * v) / 256;
uint8_t b = (hue_map[x][2] * v) / 256;
graphics.set_pen(r, g, b);
graphics.pixel(Point(x, y));
}
}
galactic_unicorn.update(&graphics);
printf("%d\n", galactic_unicorn.light());
sleep_ms(20);
}
printf("done\n");
return 0;
}

View File

@ -1,103 +0,0 @@
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include "pico/stdlib.h"
#include "libraries/pico_graphics/pico_graphics.hpp"
#include "galactic_unicorn.hpp"
#include "okcolor.hpp"
using namespace pimoroni;
PicoGraphics_PenRGB888 graphics(53, 11, nullptr);
GalacticUnicorn galactic_unicorn;
// HSV Conversion expects float inputs in the range of 0.00-1.00 for each channel
// Outputs are rgb in the range 0-255 for each channel
void from_hsv(float h, float s, float v, uint8_t &r, uint8_t &g, uint8_t &b) {
float i = floor(h * 6.0f);
float f = h * 6.0f - i;
v *= 255.0f;
uint8_t p = v * (1.0f - s);
uint8_t q = v * (1.0f - f * s);
uint8_t t = v * (1.0f - (1.0f - f) * s);
switch (int(i) % 6) {
case 0: r = v; g = t; b = p; break;
case 1: r = q; g = v; b = p; break;
case 2: r = p; g = v; b = t; break;
case 3: r = p; g = q; b = v; break;
case 4: r = t; g = p; b = v; break;
case 5: r = v; g = p; b = q; break;
}
}
void text(std::string t, Point p, float s = 1.0f, float a = 1.0f) {
int w = graphics.measure_text(t, s);
p.x += (53 / 2) - (w / 2);
p.y += (11 / 2);
graphics.text(t, Point(p.x, p.y), -1, s, a);
graphics.text(t, Point(p.x + 1, p.y), -1, s, a);
graphics.text(t, Point(p.x + 1, p.y + 1), -1, s, a);
graphics.text(t, Point(p.x, p.y + 1), -1, s, a);
}
int main() {
uint8_t hue_map[53][3];
for(int i = 0; i < 53; i++) {
from_hsv(i / 53.0f, 1.0f, 0.1f, hue_map[i][0], hue_map[i][1], hue_map[i][2]);
}
galactic_unicorn.init();
graphics.set_font("sans");
uint i = 0;
while(true) {
if(galactic_unicorn.is_pressed(galactic_unicorn.SWITCH_BRIGHTNESS_UP)) {
galactic_unicorn.adjust_brightness(+0.01);
}
if(galactic_unicorn.is_pressed(galactic_unicorn.SWITCH_BRIGHTNESS_DOWN)) {
galactic_unicorn.adjust_brightness(-0.01);
}
i++;
graphics.set_pen(0, 0, 0);
graphics.clear();
float s = 0.8f;//0.65f + (sin(i / 25.0f) * 0.15f);
float a = 1.0f;// (sin(i / 25.0f) * 100.0f);
float x = (sin((i) / 50.0f) * 90.0f);
float y = (cos((i) / 40.0f) * 5.0f);
graphics.set_pen(255, 255, 255);
text("Galactic Unicorn", Point(x, y), s, a);
uint8_t *p = (uint8_t *)graphics.frame_buffer;
for(size_t i = 0; i < 53 * 11; i++) {
int x = i % 53;
int y = i / 53;
uint r = *p++;
uint g = *p++;
uint b = *p++;
p++;
if(r > 0) {
r = hue_map[x][0];
g = hue_map[x][1];
b = hue_map[x][2];
}
graphics.set_pen(r, g, b);
graphics.pixel(Point(x, y));
}
}
printf("done\n");
return 0;
}

View File

@ -1,68 +0,0 @@
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include "pico/stdlib.h"
#include "libraries/pico_graphics/pico_graphics.hpp"
#include "galactic_unicorn.hpp"
#include "okcolor.hpp"
using namespace pimoroni;
PicoGraphics_PenRGB888 graphics(53, 11, nullptr);
GalacticUnicorn galactic_unicorn;
std::string message = "Pirate. Monkey. Robot. Ninja.";
int main() {
stdio_init_all();
galactic_unicorn.init();
float scroll = -53.0f;
while(true) {
//uint time_ms = to_ms_since_boot(get_absolute_time());
if(galactic_unicorn.is_pressed(galactic_unicorn.SWITCH_BRIGHTNESS_UP)) {
galactic_unicorn.adjust_brightness(+0.01);
}
if(galactic_unicorn.is_pressed(galactic_unicorn.SWITCH_BRIGHTNESS_DOWN)) {
galactic_unicorn.adjust_brightness(-0.01);
}
int width = graphics.measure_text(message, 1);
scroll += 0.25f;
if(scroll > width) {
scroll = -53.0f;
}
graphics.set_pen(0, 0, 0);
graphics.clear();
ok_color::HSL hsl{scroll / 100.0f, 1.0f, 0.5f};
ok_color::RGB rgb = ok_color::okhsl_to_srgb(hsl);
graphics.set_pen(rgb.r * 255, rgb.g * 255, rgb.b * 255);
graphics.text(message, Point(0 - scroll, 5), -1, 0.55);
/*graphics.text(message, Point(0 - scroll + 1, 5), -1, 0.5);
graphics.text(message, Point(0 - scroll, 5 + 1), -1, 0.5);
graphics.text(message, Point(0 - scroll + 1, 5 + 1), -1, 0.5);*/
/*
for(int x = 0; x < 53; x++) {
for(int y = 0; y < 2; y++) {
float b = sin((x - y * 3 + int(scroll) + 100) / 3.0f);
graphics.set_pen(180 * b, 150 * b, 0);
graphics.pixel(Point(52 - x, y));
graphics.pixel(Point(52 - x, y + 9));
}
}*/
galactic_unicorn.update(&graphics);
sleep_ms(10);
}
return 0;
}

View File

@ -9,6 +9,7 @@
- [Feature Test With Audio](#feature-test-with-audio)
- [Fire Effect](#fire-effect)
- [Lava Lamp](#lava-lamp)
- [Light Sensor](#light-sensor)
- [Nostalgia Prompt](#nostalgia-prompt)
- [Rainbow](#rainbow)
- [Scrolling Text](#scrolling-text)
@ -85,6 +86,14 @@ A pretty, procedural fire effect. Switch between landscape fire and vertical fir
A 70s-tastic, procedural rainbow lava lamp. You can adjust the brightness with LUX + and -.
### Light Sensor
[light_sensor.py](light_sensor.py)
Reads data from the on board light sensor and displays the brightness level of the environment. The display is by default set to auto brightness i.e reacts to the brightness of the environment.
- Button A turns auto brightness off
- Button B turns auto brightness on
### Nostalgia Prompt
[nostalgia_prompt.py](nostalgia_prompt.py)
@ -174,3 +183,6 @@ If you want to get the demo reel that Cosmic Unicorn ships with back, copy the c
Here are some cool Cosmic 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 Cosmic Unicorn](https://www.hackster.io/andreas-motzek/clock-and-green-energy-display-with-cosmic-unicorn-641dcb)
- :link: [cosmic-emoji-react - paint emojis from a computer, phone or tablet](https://github.com/chriscareycode/cosmic-unicorn/tree/main/cosmic-emoji-react)
- :link: [cosmic-paste - paste images from the clipboard to Cosmic Unicorn](https://github.com/chriscareycode/cosmic-unicorn/tree/main/cosmic-paste)
- :link: [Halloweenicorn - PIR enabled scarer](https://github.com/mrglennjones/Halloweenicorn)

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,276 @@
# SPDX-FileCopyrightText: 2023 Christopher Parrott for Pimoroni Ltd
#
# SPDX-License-Identifier: MIT
import os
import math
import struct
from machine import I2S, Pin
"""
A class for playing Wav files out of an I2S audio amp. It can also play pure tones.
This code is based heavily on the work of Mike Teachman, at:
https://github.com/miketeachman/micropython-i2s-examples/blob/master/examples/wavplayer.py
"""
class WavPlayer:
# Internal states
PLAY = 0
PAUSE = 1
FLUSH = 2
STOP = 3
NONE = 4
MODE_WAV = 0
MODE_TONE = 1
# Default buffer length
SILENCE_BUFFER_LENGTH = 1000
WAV_BUFFER_LENGTH = 10000
INTERNAL_BUFFER_LENGTH = 20000
TONE_SAMPLE_RATE = 44_100
TONE_BITS_PER_SAMPLE = 16
TONE_FULL_WAVES = 2
def __init__(self, id, sck_pin, ws_pin, sd_pin, amp_enable=None, ibuf_len=INTERNAL_BUFFER_LENGTH, root="/"):
self.__id = id
self.__sck_pin = sck_pin
self.__ws_pin = ws_pin
self.__sd_pin = sd_pin
self.__ibuf_len = ibuf_len
self.__enable = None
if amp_enable is not None:
self.__enable = Pin(amp_enable, Pin.OUT)
# Set the directory to search for files in
self.set_root(root)
self.__state = WavPlayer.NONE
self.__mode = WavPlayer.MODE_WAV
self.__wav_file = None
self.__loop_wav = False
self.__first_sample_offset = None
self.__flush_count = 0
self.__audio_out = None
# Allocate a small array of blank audio samples used for silence
self.__silence_samples = bytearray(self.SILENCE_BUFFER_LENGTH)
# Allocate a larger array for WAV audio samples, using a memoryview for more efficient access
self.__wav_samples_mv = memoryview(bytearray(self.WAV_BUFFER_LENGTH))
# Reserve a variable for audio samples used for tones
self.__tone_samples = None
self.__queued_samples = None
def set_root(self, root):
self.__root = root.rstrip("/") + "/"
def play_wav(self, wav_file, loop=False):
if os.listdir(self.__root).count(wav_file) == 0:
raise ValueError(f"'{wav_file}' not found")
self.__stop_i2s() # Stop any active playback and terminate the I2S instance
self.__wav_file = open(self.__root + wav_file, "rb") # Open the chosen WAV file in read-only, binary mode
self.__loop_wav = loop # Record if the user wants the file to loop
# Parse the WAV file, returning the necessary parameters to initialise I2S communication
format, sample_rate, bits_per_sample, self.__first_sample_offset, self.sample_size = WavPlayer.__parse_wav(self.__wav_file)
# Keep a track of total bytes read from WAV File
self.total_bytes_read = 0
self.__wav_file.seek(self.__first_sample_offset) # Advance to first byte of sample data
self.__start_i2s(bits=bits_per_sample,
format=format,
rate=sample_rate,
state=WavPlayer.PLAY,
mode=WavPlayer.MODE_WAV)
def play_tone(self, frequency, amplitude):
if frequency < 20.0 or frequency > 20_000:
raise ValueError("frequency out of range. Expected between 20Hz and 20KHz")
if amplitude < 0.0 or amplitude > 1.0:
raise ValueError("amplitude out of range. Expected 0.0 to 1.0")
# Create a buffer containing the pure tone samples
samples_per_cycle = self.TONE_SAMPLE_RATE // frequency
sample_size_in_bytes = self.TONE_BITS_PER_SAMPLE // 8
samples = bytearray(self.TONE_FULL_WAVES * samples_per_cycle * sample_size_in_bytes)
range = pow(2, self.TONE_BITS_PER_SAMPLE) // 2
format = "<h" if self.TONE_BITS_PER_SAMPLE == 16 else "<l"
# Populate the buffer with multiple cycles to avoid it completing too quickly and causing drop outs
for i in range(samples_per_cycle * self.TONE_FULL_WAVES):
sample = int((range - 1) * (math.sin(2 * math.pi * i / samples_per_cycle)) * amplitude)
struct.pack_into(format, samples, i * sample_size_in_bytes, sample)
# Are we not already playing tones?
if not (self.__mode == WavPlayer.MODE_TONE and (self.__state == WavPlayer.PLAY or self.__state == WavPlayer.PAUSE)):
self.__stop_i2s() # Stop any active playback and terminate the I2S instance
self.__tone_samples = samples
self.__start_i2s(bits=self.TONE_BITS_PER_SAMPLE,
format=I2S.MONO,
rate=self.TONE_SAMPLE_RATE,
state=WavPlayer.PLAY,
mode=WavPlayer.MODE_TONE)
else:
self.__queued_samples = samples
self.__state = WavPlayer.PLAY
def pause(self):
if self.__state == WavPlayer.PLAY:
self.__state = WavPlayer.PAUSE # Enter the pause state on the next callback
def resume(self):
if self.__state == WavPlayer.PAUSE:
self.__state = WavPlayer.PLAY # Enter the play state on the next callback
def stop(self):
if self.__state == WavPlayer.PLAY or self.__state == WavPlayer.PAUSE:
if self.__mode == WavPlayer.MODE_WAV:
# Enter the flush state on the next callback and close the file
# It is done in this order to prevent the callback entering the play
# state after we close the file but before we change the state)
self.__state = WavPlayer.FLUSH
self.__wav_file.close()
else:
self.__state = WavPlayer.STOP
def is_playing(self):
return self.__state != WavPlayer.NONE and self.__state != WavPlayer.STOP
def is_paused(self):
return self.__state == WavPlayer.PAUSE
def __start_i2s(self, bits=16, format=I2S.MONO, rate=44_100, state=STOP, mode=MODE_WAV):
import gc
gc.collect()
self.__audio_out = I2S(
self.__id,
sck=self.__sck_pin,
ws=self.__ws_pin,
sd=self.__sd_pin,
mode=I2S.TX,
bits=bits,
format=format,
rate=rate,
ibuf=self.__ibuf_len,
)
self.__state = state
self.__mode = mode
self.__flush_count = self.__ibuf_len // self.SILENCE_BUFFER_LENGTH + 1
self.__audio_out.irq(self.__i2s_callback)
self.__audio_out.write(self.__silence_samples)
if self.__enable is not None:
self.__enable.on()
def __stop_i2s(self):
self.stop() # Stop any active playback
while self.is_playing(): # and wait for it to complete
pass
if self.__enable is not None:
self.__enable.off()
if self.__audio_out is not None:
self.__audio_out.deinit() # Deinit any active I2S comms
self.__state == WavPlayer.NONE # Return to the none state
def __i2s_callback(self, arg):
# PLAY
if self.__state == WavPlayer.PLAY:
if self.__mode == WavPlayer.MODE_WAV:
num_read = self.__wav_file.readinto(self.__wav_samples_mv) # Read the next section of the WAV file
self.total_bytes_read += num_read
# Have we reached the end of the file?
if num_read == 0:
# Do we want to loop the WAV playback?
if self.__loop_wav:
_ = self.__wav_file.seek(self.__first_sample_offset) # Play again, so advance to first byte of sample data
else:
self.__wav_file.close() # Stop playing, so close the file
self.__state = WavPlayer.FLUSH # and enter the flush state on the next callback
self.__audio_out.write(self.__silence_samples) # In both cases play silence to end this callback
else:
if num_read > 0 and num_read < self.WAV_BUFFER_LENGTH:
num_read = num_read - (self.total_bytes_read - self.sample_size)
self.__audio_out.write(self.__wav_samples_mv[: num_read]) # We are within the file, so write out the next audio samples
else:
if self.__queued_samples is not None:
self.__tone_samples = self.__queued_samples
self.__queued_samples = None
self.__audio_out.write(self.__tone_samples)
# PAUSE or STOP
elif self.__state == WavPlayer.PAUSE or self.__state == WavPlayer.STOP:
self.__audio_out.write(self.__silence_samples) # Play silence
# FLUSH
elif self.__state == WavPlayer.FLUSH:
# Flush is used to allow the residual audio samples in the internal buffer to be written
# to the I2S peripheral. This step avoids part of the sound file from being cut off
if self.__flush_count > 0:
self.__flush_count -= 1
else:
self.__state = WavPlayer.STOP # Enter the stop state on the next callback
self.__audio_out.write(self.__silence_samples) # Play silence
# NONE
elif self.__state == WavPlayer.NONE:
pass
@staticmethod
def __parse_wav(wav_file):
chunk_ID = wav_file.read(4)
if chunk_ID != b"RIFF":
raise ValueError("WAV chunk ID invalid")
_ = wav_file.read(4) # chunk_size
format = wav_file.read(4)
if format != b"WAVE":
raise ValueError("WAV format invalid")
sub_chunk1_ID = wav_file.read(4)
if sub_chunk1_ID != b"fmt ":
raise ValueError("WAV sub chunk 1 ID invalid")
_ = wav_file.read(4) # sub_chunk1_size
_ = struct.unpack("<H", wav_file.read(2))[0] # audio_format
num_channels = struct.unpack("<H", wav_file.read(2))[0]
if num_channels == 1:
format = I2S.MONO
else:
format = I2S.STEREO
sample_rate = struct.unpack("<I", wav_file.read(4))[0]
# if sample_rate != 44_100 and sample_rate != 48_000:
# raise ValueError(f"WAV sample rate of {sample_rate} invalid. Only 44.1KHz or 48KHz audio are supported")
_ = struct.unpack("<I", wav_file.read(4))[0] # byte_rate
_ = struct.unpack("<H", wav_file.read(2))[0] # block_align
bits_per_sample = struct.unpack("<H", wav_file.read(2))[0]
# usually the sub chunk2 ID ("data") comes next, but
# some online MP3->WAV converters add
# binary data before "data". So, read a fairly large
# block of bytes and search for "data".
binary_block = wav_file.read(200)
offset = binary_block.find(b"data")
if offset == -1:
raise ValueError("WAV sub chunk 2 ID not found")
wav_file.seek(40)
sub_chunk2_size = struct.unpack("<I", wav_file.read(4))[0]
return (format, sample_rate, bits_per_sample, 44 + offset, sub_chunk2_size)

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,91 @@
from machine import Timer
from audio import WavPlayer
from cosmic import CosmicUnicorn
from picographics import PicoGraphics, DISPLAY_COSMIC_UNICORN as DISPLAY
import time
cu = CosmicUnicorn()
graphics = PicoGraphics(DISPLAY)
graphics.set_font("bitmap6")
WHITE = graphics.create_pen(255, 255, 255)
BLUE = graphics.create_pen(0, 0, 255)
CLEAR = graphics.create_pen(0, 0, 0)
RED = graphics.create_pen(255, 0, 0)
GREEN = graphics.create_pen(0, 255, 0)
cu.set_brightness(0.7)
audio = WavPlayer(0, 10, 11, 9, amp_enable=22)
class Countdown(object):
def __init__(self):
self.timer_running = False
self.total_seconds = 0
self.timer = None
def process_input(self):
if cu.is_pressed(CosmicUnicorn.SWITCH_VOLUME_UP):
self.total_seconds += 1
if cu.is_pressed(CosmicUnicorn.SWITCH_VOLUME_DOWN):
if self.total_seconds > 0:
self.total_seconds -= 1
if cu.is_pressed(CosmicUnicorn.SWITCH_SLEEP):
self.start_timer()
def display_time(self):
seconds = self.total_seconds % (24 * 3600)
seconds %= 3600
minutes = seconds // 60
seconds %= 60
# Add leading zeros to the minutes and seconds
if len(str(minutes)) == 1:
minutes = "0{}".format(minutes)
if len(str(seconds)) == 1:
seconds = "0{}".format(seconds)
return "{}:{}".format(minutes, seconds)
def draw(self):
graphics.set_pen(graphics.create_pen(0, 0, 0))
graphics.clear()
graphics.set_pen(BLUE)
graphics.circle(0, 0, 12)
graphics.set_pen(GREEN)
graphics.circle(25, 30, 5)
graphics.set_pen(RED)
graphics.circle(0, 32, 12)
graphics.set_pen(CLEAR)
graphics.rectangle(0, 11, CosmicUnicorn.WIDTH, 9)
graphics.set_pen(WHITE)
graphics.text(self.display_time(), 4, 12, -1, 1)
cu.update(graphics)
def start_timer(self):
if not self.timer_running:
self.timer = Timer(mode=Timer.PERIODIC, period=1000, callback=self.countdown)
self.timer_running = True
def reset(self):
self.timer.deinit()
self.timer_running = False
def countdown(self, arg):
if self.total_seconds == 0:
audio.play_wav("doorbell.wav", False)
self.reset()
else:
self.total_seconds -= 1
count = Countdown()
while 1:
count.process_input()
count.draw()
time.sleep(0.07)

Binary file not shown.

View File

@ -0,0 +1,74 @@
from audio import WavPlayer
from cosmic import CosmicUnicorn
from picographics import PicoGraphics, DISPLAY_COSMIC_UNICORN as DISPLAY
from time import sleep
cu = CosmicUnicorn()
graphics = PicoGraphics(DISPLAY)
audio = WavPlayer(0, 10, 11, 9, amp_enable=22)
WHITE = graphics.create_pen(255, 255, 255)
RED = graphics.create_pen(255, 0, 0)
class Menu(object):
def __init__(self):
self.items = ["Pew 1", "Pew 2", "Pew 3"]
self.selected = 0
# A function to draw only the menu elements.
# Helps to keep our main function tidy!
def draw_menu(self):
graphics.set_pen(WHITE)
for item in range(len(self.items)):
if self.selected == item:
graphics.set_pen(RED)
graphics.text(self.items[item], 0, 2 + item * 10, 31, 1)
graphics.set_pen(WHITE)
# Make changes based on the currently selected menu item
def process_selected(self):
if self.selected == 0:
audio.play_wav("Pew1.wav", False)
if self.selected == 1:
audio.play_wav("Pew2.wav", False)
if self.selected == 2:
audio.play_wav("Pew3.wav", False)
menu = Menu()
graphics.set_font("bitmap6")
cu.set_brightness(0.7)
while True:
graphics.set_pen(graphics.create_pen(0, 0, 0))
graphics.clear()
if cu.is_pressed(CosmicUnicorn.SWITCH_BRIGHTNESS_DOWN):
audio.play_wav("buttonbeep.wav", False)
if menu.selected + 1 < len(menu.items):
menu.selected += 1
else:
menu.selected = 0
if cu.is_pressed(CosmicUnicorn.SWITCH_BRIGHTNESS_UP):
audio.play_wav("buttonbeep.wav", False)
if menu.selected > 0:
menu.selected -= 1
else:
menu.selected = len(menu.items) - 1
if cu.is_pressed(CosmicUnicorn.SWITCH_SLEEP):
menu.process_selected()
menu.draw_menu()
cu.update(graphics)
sleep(0.2)

View File

@ -0,0 +1,8 @@
from audio import WavPlayer
sound = WavPlayer(0, 10, 11, 9, amp_enable=22)
sound.play_wav("beepboop.wav", False)
while sound.is_playing():
pass

View File

@ -158,11 +158,13 @@ down_button = machine.Pin(CosmicUnicorn.SWITCH_VOLUME_DOWN, machine.Pin.IN, mach
def adjust_utc_offset(pin):
global utc_offset
global utc_offset, last_second
if pin == up_button:
utc_offset += 1
last_second = None
if pin == down_button:
utc_offset -= 1
last_second = None
up_button.irq(trigger=machine.Pin.IRQ_FALLING, handler=adjust_utc_offset)
@ -214,9 +216,11 @@ sync_time()
while True:
if cu.is_pressed(CosmicUnicorn.SWITCH_BRIGHTNESS_UP):
cu.adjust_brightness(+0.01)
last_second = None
if cu.is_pressed(CosmicUnicorn.SWITCH_BRIGHTNESS_DOWN):
cu.adjust_brightness(-0.01)
last_second = None
if cu.is_pressed(CosmicUnicorn.SWITCH_A):
sync_time()

View File

@ -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"

View File

@ -63,20 +63,12 @@ def status_handler(mode, status, ip):
# Create web server application
app = webserver()
# Static page
html_file = open('index.html', 'r')
# WIFI settings
WIFI_COUNTRY = "GB" # Change to your local two-letter ISO 3166-1 country code
# Index page
@app.route('/')
async def index(request, response):
# Start HTTP response with content-type text/html
await response.start_html()
# Send actual HTML page
await response.send(html_file.read())
await response.send_file('index.html', content_type='text/html')
# HTTP redirection
@ -120,7 +112,7 @@ def outline_text(text, x, y):
def run():
# Setup wifi
network_manager = NetworkManager(WIFI_COUNTRY, status_handler=status_handler)
network_manager = NetworkManager(WIFI_CONFIG.COUNTRY, status_handler=status_handler)
app.add_resource(text, '/update')

View File

@ -0,0 +1,165 @@
import time
from cosmic import CosmicUnicorn
from picographics import PicoGraphics, DISPLAY_COSMIC_UNICORN as DISPLAY
"""
Light sensoring feature for the cosmic unicorn
Uses the onboard light sensor to detect the light in the environment
The brightness level is displayed as percentage.
Brightness of the display is auto adjusted to the brightness level of the environment
Press A to turn auto brightness on
Press B to turn auto brightness off
"""
# set up unicorn and drawing variables
cu = CosmicUnicorn()
graphics = PicoGraphics(DISPLAY)
WIDTH, HEIGHT = graphics.get_bounds()
BLACK = graphics.create_pen(0, 0, 0)
WHITE = graphics.create_pen(255, 255, 255)
GREY = graphics.create_pen(20, 20, 20)
HUE_START = 0
HUE_END = 100
graphics.set_font("bitmap8")
# Text display sleep time in ms
TEXT_SLEEP = 500
# the onboard light sensor has a wide range from 0 t0 4095
# It is therefore needed to set a lower max and a higher minimum
MIN_LS_VALUE = 10
MAX_LS_VALUE = 295 # 4095 to use the full range
MIN_RANGE = 0.1
MAX_RANGE = 1
# Rate of display change i.e the lower the value the slower the transition
TRANSITION_RATE = 1.0 / 72.0
# perform linear interpolation to map a range of values to discrete
def map_range(
x,
min_input=MIN_LS_VALUE,
max_input=MAX_LS_VALUE,
min_output=MIN_RANGE,
max_output=MAX_RANGE,
):
return (x - min_input) * (max_output - min_output) / (
max_input - min_input
) + min_output
# gets the light sensor value from onboard sensor and interpolates it
# clamps the brightness value it outside the ranges specified
def calculate_brightness(prev_brightness_val):
current_lsv = cu.light()
current_brightness_val = map_range(current_lsv)
# uses the previous value to smooth out display changes reducing flickering
brightness_diff = current_brightness_val - prev_brightness_val
brightness_val = prev_brightness_val + (brightness_diff * TRANSITION_RATE)
if brightness_val > 1:
brightness_val = 1
elif brightness_val < 0.1:
brightness_val = 0.1
return brightness_val
# sets up a handy function we can call to clear the screen
def clear():
graphics.set_pen(BLACK)
graphics.clear()
def draw_percentage(x, y):
graphics.rectangle(x + 1, y + 1, 2, 2)
graphics.line(x + 1, y + 5, x + 6, y)
graphics.rectangle(x + 4, y + 4, 2, 2)
# draws a sun icon
def draw_sun(x, y, r):
circle_x = x + 3 + r
circle_y = y + 3 + r
graphics.circle(circle_x, circle_y, r)
graphics.line(circle_x, y, circle_x, y + 2)
graphics.line(x, circle_y, x + 2, circle_y)
graphics.line(circle_x, (y + 5 + 2 * r), circle_x, (y + 5 + 2 * r) + 2)
graphics.line((x + 5 + 2 * r), circle_y, (x + 5 + 2 * r) + 2, circle_y)
graphics.line(
circle_x + 1 + r, circle_y - 1 - r, circle_x + 3 + r, circle_y - 3 - r
)
graphics.line(
circle_x + 1 + r, circle_y + 1 + r, circle_x + 3 + r, circle_y + 3 + r
)
graphics.line(
circle_x - 1 - r, circle_y - 1 - r, circle_x - 3 - r, circle_y - 3 - r
)
graphics.line(
circle_x - 1 - r, circle_y + 1 + r, circle_x - 3 - r, circle_y + 3 + r
)
mode = "auto"
last = time.ticks_ms()
brightness_value = MIN_RANGE # set the initial brightness level to the minimum
while True:
current = time.ticks_ms()
# set the display brightness
brightness_value = calculate_brightness(brightness_value)
cu.set_brightness(brightness_value)
bp = (brightness_value / MAX_RANGE) * 100 # gets brightness value in percentage relative to the MAX_LS_VALUE set
# deactivate auto brightness by pressing A
if cu.is_pressed(CosmicUnicorn.SWITCH_A):
print("Auto brightness off")
mode = "off"
# reactivate auto brightness by pressing A
if cu.is_pressed(CosmicUnicorn.SWITCH_B):
print("Auto brightness on")
mode = "auto"
# set brightness to default value if off
if mode == "off":
cu.set_brightness(0.5)
# set text update rate after a certain time to reduce flickering
if current - last >= TEXT_SLEEP:
clear()
# calculate colour from the brightness value
hue = max(0, HUE_START + ((bp - 0) * (HUE_END - HUE_START) / (100 - 0)))
# create pens with this colour (and with the high / low colours)
CURRENT_COLOUR = graphics.create_pen_hsv(hue / 360, 1.0, 0.8)
HIGH_COLOUR = graphics.create_pen_hsv(HUE_END / 360, 1.0, 0.8)
LOW_COLOUR = graphics.create_pen_hsv(HUE_START / 360, 1.0, 0.8)
# draw the text
graphics.set_pen(CURRENT_COLOUR)
graphics.text("BRT: ", 0, 1, scale=1)
graphics.text(f"{bp:.0f}", 7, 23, scale=1)
draw_percentage((WIDTH - 10), 23)
# draw sun icon
draw_sun(0, 10, 2)
# draw a bar for the background
bar_width = WIDTH - 12
graphics.set_pen(GREY)
graphics.rectangle(13, 10, bar_width, 11)
# draw a bar for the current brightness percentage
graphics.set_pen(CURRENT_COLOUR)
graphics.rectangle(13, 10, int((bp / 100) * bar_width), 11)
last = current
# time to update the display
cu.update(graphics)

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Some files were not shown because too many files have changed in this diff Show More