QRPickle

A lightweight, opinionated, field-friendly HAM Clock

The QRPickle manual on one page. The same manual as a PDF is in each release's ZIP on the releases page.

Introduction

QRPickle is a simple, lightweight dashboard designed for portable, QRP, and field-day amateur radio operations. Built to run on a standard ESP32 Cheap Yellow Display (CYD) with no external PSRAM, it consolidates real-time tracking data and tools into a clean, easy-to-use touchscreen interface.

This project is inspired by cburns42/HamClockCYD and SP3KON/ESP32-HAM-CLOCK, which resulted in a group build at BARC with VU3GWN forking it to (coffeedev/BARC-HAM-CLOCK).

TL;DR: install QRPickle from your browser with the one-click web installer (Chrome or Edge, and a USB data cable).


⚠ DEVELOPED & GENERATED BY A LARGE LANGUAGE MODEL (LLM) ⚠

This codebase, firmware architecture, network parsing engine, memory management schema, and layout optimization layers were designed, audited, and developed using a Large Language Model (LLM).


Device Interface

Boot & Splash Main Dashboard App Menu
Boot & Splash Main Dashboard App Menu
DX Cluster POTA Spots SOTA Spots
DX Cluster POTA Spots SOTA Spots
Band Conditions Solar Indicators Reading the Indicators
Band Conditions Solar Indicators Reading the Indicators
Satellite Passes A Pass SSTV & School Contacts
Satellite Passes A Satellite Pass SSTV Events and School Contacts
APRS Traffic APRS Radar APRS Messages
APRS Traffic APRS Radar APRS Messages
APRS Quick Messages APRS Beacon HamAlert
APRS Quick Messages APRS Beacon HamAlert
Local Sensor OpenWeather Forecast
Local Sensor OpenWeather Forecast
Network WiFi Scan Cloud Updates
Network WiFi Scan Cloud Updates
On-Device Settings On-Screen Keyboard
On-Device Settings On-Screen Keyboard

Screenshots are taken on the device (tools/device_screens.py, Classic theme); network names, addresses and positions are replaced or hidden in them. Smart sleep: after the screen timeout the backlight turns off; a touch wakes it (the first touch only wakes).

Web Management Console

When on the same network, navigate to the device’s IP address to access the integrated Web UI. The UI interacts with the C++ backend via asynchronous REST API endpoints.

Basic Network & Station Config Advanced Settings
Web UI Basic Advanced Settings
Profiles, Backup & Restore Editing a Profile in the Settings Form
Profiles Editing a profile
Live Hardware Telemetry & Manual Update Cloud OTA
System Info Cloud OTA
APRS Messaging
APRS Messaging

Web Console Login

The web console and the QRPickle-Setup hotspot are password-protected.

Saved secrets (WiFi password, API keys, passcodes) are never shown again in the web console. Leave those fields blank to keep the stored values.

Note: saved profiles store their WiFi credentials on the device’s filesystem, so they can switch networks. The flash is not encrypted. A profile without a stored password or key keeps the device’s current one when applied. Backups never contain secrets.

Features and Data Sources

What each screen shows and where its data comes from. Accounts and keys are entered in the web console and stay on the device.

Screen What it shows Data from
Dashboard Local and UTC time, indoor sensor and weather, band conditions for your chosen band groups the screens below
DX Cluster Live DX spots, filtered by band and mode DX cluster over telnet (default dxspider.co.uk:7300, fallback dxc.w6bgr.com:7373; both configurable)
xOTA POTA and SOTA activators on air, filtered by band, mode and QRP POTA (api.pota.app); SOTA cluster (cluster.sota.org.uk, telnet)
HamAlert Spots matching your own triggers HamAlert (your account, telnet)
APRS Stations heard (list and radar), messages, your position beacon APRS-IS (rotate.aprs.net; your callsign and passcode)
Band Conditions 11 bands (160 m to 6 m) rated for day and night at your location, greyline, next sunrise/sunset, solar indicators explained (how bands are rated) HAMQSL.com solar data, courtesy of N0NBH (every 3 hours, hourly in storms), or your own source
Satellites The next passes over your station of popular amateur satellites (FM, linear, SSTV, voice beacons, optionally APRS) with times, directions and frequencies; ISS SSTV events and ARISS school contacts A feed built every 6 hours from SatNOGS DB (CC BY-SA 4.0), CelesTrak orbits and ARISS (about the feed); passes computed on the device, your location is never sent
Weather Indoor temperature, humidity and pressure; current conditions and a 24-hour forecast BME280 sensor (optional, wiring); OpenWeather (your free API key)
Network, Settings, Cloud OTA WiFi, station and display settings on the device; updates GitHub Pages for updates; time from pool.ntp.org

On the device

Web console and updates

How it fits on an ESP32 without PSRAM


Time Zone Note

QRPickle uses a fixed UTC offset (in half-hour steps), with no automatic daylight saving. In regions that observe DST, change the offset in Settings twice a year. Most ham-radio activity is logged in UTC, which is always shown correctly.

Downloading Pre-Compiled Releases

You do not need to install VS Code or PlatformIO to use QRPickle. Pre-compiled binaries for the CYD are available in the repository releases.

Download the latest release binaries (every release also lists firmware.bin.sha256 for integrity checks)

The release folder contains the following files:


Easy Web Installation (No Software Required)

One-click installer (from v0.2.0): open quixotic.bharathpalavalli.com/QRPickle/install in Chrome or Edge, pick a version (the latest is preselected; the last five releases are listed), connect the CYD by USB and click Install. It writes everything, including the filesystem. If you’re updating a device that already has profiles, download a backup first (web console → Profiles → Download Backup).

Manual alternative: flash the release files with the Espressif Web Flasher.

  1. Connect your CYD to your computer via USB.
  2. Open the Espressif Web Flasher Tool.
  3. Set your baud rate to 921600 and click Connect.
  4. Add the release files to the flash tool using their exact partition offsets:
    • 0x1000 ➔ bootloader.bin
    • 0x8000 ➔ partitions.bin
    • 0x10000 ➔ firmware.bin
    • 0x390000 ➔ littlefs.bin
  5. Click Program and wait for the process to finish. The device will automatically reboot into QRPickle!
  6. And the device should boot up, and create a WiFi access-point called QRPickle-Setup protected with the admin password shown on the device’s Network screen (see Web Console Login).

Hardware and Wiring

QRPickle runs on the ESP32-2432S028R “Cheap Yellow Display” (CYD), the 2.8” ILI9341 version with a resistive touch screen. Nothing needs soldering. The only optional extra is a BME280 sensor for indoor temperature, humidity and pressure.

Board pinout

ESP32-2432S028 (Cheap Yellow Display) pinout by Renzo Mischianti

“ESP32-2432S028 (Cheap Yellow Display) pinout” by Renzo Mischianti, from ESP32-2432S028 (Cheap Yellow Display): high-resolution pinout, datasheet, schema and specs on mischianti.org. Licensed under CC BY-NC-ND 4.0; reproduced unmodified. This image is not covered by QRPickle’s MIT licence (see third-party images).

The BME280 connector is the one labelled “Temp humidity interface” (CN1) at the bottom of the image.

Not the 3.2” board. The ESP32-2432S032 (“CYD v2”) uses a different display and pin map, so QRPickle doesn’t support it.

The board

The reference CYD (checked with esptool flash_id and the boot log):

Item Value
Chip ESP32-D0WD-V3 (revision v3.1), dual core, 240 MHz, WiFi + Bluetooth
Flash 4 MB
PSRAM none
Display 2.8” ILI9341, 320 x 240
Touch XPT2046, resistive

Pins QRPickle uses

These come from src/hw/User_Setup.h, src/hw/touch.cpp, src/hw/sensor.cpp, src/hw/led_rgb.cpp and src/services/display_manager.cpp.

Function GPIO Notes
Display SPI MISO 12, MOSI 13, SCLK 14, CS 15, DC 2 Reset is wired to 3.3V on the board
Display backlight 21 PWM; also appears on connector P3, so don’t use it there
Touch SPI MOSI 32, MISO 39, CLK 25, CS 33 Separate SPI bus
Light sensor (LDR) 34 (ADC1 channel 6) Auto-brightness
RGB status LED R 4, G 16, B 17 Active low; colours: LEDColours.md
BME280 (I2C) SDA 27, SCL 22 Optional, on connector CN1

The micro-SD slot and the speaker connector are not used.

Optional BME280 sensor

The dashboard and weather screen show indoor temperature, humidity and pressure from a BME280 connected to CN1, the small 4-pin socket on the board’s edge. Without a sensor, QRPickle shows -- for these values.

Wiring

CN1 signal (printed on the board) BME280 pin
GND GND
IO22 (GPIO22) SCL
IO27 (GPIO27) SDA
3.3V VIN / VCC
   CYD connector CN1               BME280 module
  +--------+                      +-----------+
  |  GND   |----------------------| GND       |
  |  IO22  |----------------------| SCL       |
  |  IO27  |----------------------| SDA       |
  |  3.3V  |----------------------| VIN / VCC |
  +--------+                      +-----------+

Match the wires by the labels printed next to CN1, not by position or colour. The pin order looks reversed depending on which side you view the board from (the pinout image shows 3.3V, 27, 22, GND from left to right), and cable colours vary.

Notes

Checking it works

Power

Power the CYD from a 5V USB supply through the micro-USB socket. The firmware limits WiFi transmit power to avoid brown-outs on the board’s 3.3V regulator. If the board resets during WiFi activity, try a better cable or supply first.

Appendix A: LED Colours

The Cheap Yellow Display (CYD) features a rear-mounted SMD RGB LED. The QRPickle firmware utilizes this hardware to provide immediate, unobtrusive background telemetry without requiring the operator to wake the LCD screen during field operations.

1. Boot Sequence (Initialization Phase)

During a cold boot or hardware reset, the LED acts as a progressive loading indicator.

2. Network Status

3. Live Data Traffic

4. Hardware Faults


Hardware Note: The CYD RGB LED (R = GPIO4, G = GPIO16, B = GPIO17) is common anode, so the PWM duty is inverted: 0 is full brightness and 255 is off. An idle channel is driven at duty 255 (off).

Tests: the colours and timings above live in src/hw/led_pattern.cpp and are checked by the host unit tests in test/test_led (run in CI). test/test_hw_led/ is an optional on-device sketch that cycles through every state with the same code, for checking by eye.

Appendix B: How Band Conditions Are Rated

The Band Conditions screen and the dashboard tile rate each band GOOD, FAIR or POOR for day and for night (6 m: Es? or closed). The ratings are an estimate for a typical one-hop path of about 3000 km at mid latitudes, worked out on the device from the solar data. They are not a prediction for a particular path: use them to decide where to listen first.

The code is src/services/band_rating.cpp; every number below is in its MODEL table, and the unit tests (test/test_parsers) check the model.

Inputs

Input Source Used for
Solar flux (SFI) HAMQSL.com (N0NBH) or your own source, every 3 hours the sunspot number, so the MUF; daytime absorption
K index same geomagnetic storms (MUF), noise on the low bands
A index same longer storms (MUF)
Station latitude and longitude web console (or the grid square’s centre) day, night or greyline now; the sporadic-E season
Date the clock (NTP) the sporadic-E season

Without solar data the screen shows the bands as unknown.

Steps

  1. Effective sunspot number R from the flux, by inverting the usual fit SFI = 63.7 + 0.728 R + 0.00089 R². R is 0 at SFI 64 or less.
  2. Maximum usable frequency (MUF) for a 3000 km hop: day MUF = 18 + 0.14 R MHz; night MUF = 10.5 + 0.08 R MHz.
  3. Storm factor: the MUF is multiplied by the stronger reduction of K (4: 0.95, 5: 0.85, 6: 0.75, 7 or more: 0.65) and A (20 or more: 0.95, 30: 0.85, 50: 0.75).
  4. Skywave rating: a band at or below 85 % of the MUF (the optimum working frequency) is GOOD, up to the MUF FAIR, above it POOR.
  5. Daytime absorption (D layer): the lowest usable frequency is LUF = 4 + 0.02 (SFI − 70) MHz, at least 3 MHz. In the day a band below the LUF is POOR, below 1.5 × LUF at best FAIR. At night there is no D layer.
  6. Noise on the low bands (below 7 MHz): K 3-4 makes them at best FAIR, K 5 or more POOR.
  7. 6 m: rated like the other bands when the MUF reaches it (solar maximum). Otherwise it shows Es? in the daytime in the sporadic-E season (May to August in the northern hemisphere, November to February in the southern), and closed the rest of the time.
  8. Groups (the dashboard tile) show the best band in the group: “something in this group works”.

Day, night and greyline: the sun more than 6° above the horizon is day, more than 6° below it night; in between is greyline (about 30-60 minutes around sunrise and sunset), when both columns are marked because paths along the terminator favour the low bands.

Examples (quiet conditions unless stated)

Conditions Day: GOOD Day: FAIR Night: GOOD Night: FAIR
SFI 70, K 1 (solar minimum): MUF 19 MHz day, 11 at night 40-20 m 60 m, 17 m 160-40 m 30 m
SFI 120, K 2: MUF 28 / 16 MHz 30-15 m 60-40 m, 12 m 160-30 m 20 m
SFI 180, K 2 (solar maximum): MUF 37 / 21 MHz 30-10 m 40 m 160-17 m 15 m
SFI 150, K 5, A 30 (storm): MUF 28 / 16 MHz 30-15 m 40 m, 12 m 40-30 m 20 m

In the storm the low bands are POOR at night because of the noise, although the MUF alone would allow them.

Limits

Sources

The SFI-sunspot fit is the commonly used one, and 85 % of the MUF is the classic optimum working frequency. The MUF, LUF and storm constants are rough estimates for a mid-latitude 3000 km path, not fitted to measurements. Corrections from operators are welcome (an issue on GitHub).

Appendix C: Flash Memory Map

This project utilizes a heavily customized partition table (partitions.csv) to fit a complex FreeRTOS application, LVGL graphics, a Web UI, and an Over-The-Air (OTA) update system into a standard 4MB ESP32 flash chip.

The full flash map, from the start of the chip. The bootloader and the partition table sit at fixed addresses before the first partition; the rest comes from partitions.csv:

Address Region Size Contents Written at install
0x001000 Bootloader ESP32 second-stage bootloader bootloader.bin
0x008000 Partition table The table below partitions.bin
0x009000 nvs 0x005000 (20 KB) Settings: WiFi, callsign, keys, admin password Kept (cleared only by “Erase device”)
0x00E000 otadata 0x002000 (8 KB) Which app slot boots (app0 or app1) boot_app0.bin (boot app0)
0x010000 app0 0x1C0000 (1.75 MB) Firmware slot 1 firmware.bin
0x1D0000 app1 0x1C0000 (1.75 MB) Firmware slot 2: updates (web upload or Cloud OTA) are written to whichever slot isn’t running, so a failed update can roll back Not written
0x390000 spiffs 0x070000 (448 KB) LittleFS: web console (www/), images, about text, saved profiles littlefs.bin

The installer and the manual flashing steps below use exactly these addresses for every version. Changing partitions.csv breaks OTA for devices already in use.


Appendix D: Building from Source

The project is built entirely within PlatformIO (VS Code).

1. Pre-Flight Setup

QRPickle targets the ESP32-2432S028R (2.8” CYD); no soldering is needed. The pins it uses and the wiring for the optional BME280 sensor are in Hardware and Wiring.

2. Uploading the Filesystem (Web UI & Profiles)

Before the firmware will function fully (especially the Web UI), you must write the data/ folder to the LittleFS partition.

pio run --target uploadfs

Warning: writing the filesystem (by USB, web upload or web installer) replaces the whole LittleFS partition, which erases saved profiles. Settings stored in NVS (WiFi, callsign, keys, admin password) are kept, unless you tick “Erase device” in the web installer.

3. Compiling and Flashing the Core Firmware

Compile the C++ source and burn the binary to the ESP32 via USB.

pio run --target upload

4. Over-The-Air (OTA) Updates

Once deployed in the field, further updates can be pushed wirelessly. Compile your firmware in PlatformIO to generate firmware.bin or littlefs.bin, open the System Info tab on the Web UI, and push the payload directly over your local network.

Designed for operational reliability out in the field. Keep the signal clean, the power consumption managed, and the heap layout stable. 73!


Appendix E: Libraries & Frameworks

The complete list, with licences, is in THIRD_PARTY_NOTICES.md.


Appendix F: Screens, Widgets and Themes

How the on-device user interface is put together, and the rules for adding a screen, a dashboard tile or a theme. It is written for contributors; the code is in src/ui/. LVGL 9 draws everything, on a 320 x 240 landscape display with touch.

The band-conditions screen and the dashboard band tile (v0.2.4) follow every rule here; use them as worked examples (src/ui/screens/band_cond.cpp, src/ui/widgets/widget_band.cpp).

Dashboard Band conditions

Screen geometry

All sizes come from src/ui/layout.h; don’t write 320, 240 or 216 in a page.

 0                                                         319
 ┌───────────────────────────────────────────────────────────┐ 0
 │ ☰  Title                              ⊘  wifi  ⚙  HH:MM   │ status bar
 ├───────────────────────────────────────────────────────────┤ 24
 │                                                           │
 │             page content: view_container                  │
 │             SCREEN_W x CONTENT_H = 320 x 216              │
 │                                                           │
 │                                              keep clear   │
 │                                            ┌───────────┐  │
 │                                            │        ⌂  │  │
 └────────────────────────────────────────────┴───────────┴──┘ 239
                                    home button, ~40 x 24 px

The shell and navigation

ui.cpp owns the persistent parts (status bar, sidebar, home button, view_container) and swaps the page content:

ui_navigate_local(PAGE_X)
  └─ lv_obj_clean(view_container)   deletes the old page
  └─ status_bar_set_title("...")
  └─ draw_x_page(view_container)    the new page builds itself

Pages

A page is one function, void draw_x_page(lv_obj_t* parent), in src/ui/screens/x.cpp with a header x.h. It creates all its objects as children of parent.

Lifecycle. The page lives until the next ui_navigate_local() cleans the container. Anything the page keeps outside LVGL’s object tree must be released when its root object is deleted:

s.timer = lv_timer_create(
    [](lv_timer_t*) { refresh(false); }, 2000, nullptr);
lv_obj_add_event_cb(s.root, [](lv_event_t*) {
    if (s.timer) lv_timer_delete(s.timer);  // not a child object
    s = Screen{};  // drop pointers to deleted objects
}, LV_EVENT_DELETE, nullptr);

Keep a page’s state in one static struct (Screen s;), reset at the start of draw_x_page() and on delete. A stale pointer to a deleted label is the classic crash.

Data. Data comes from a manager in src/services/, never from the network directly:

Text and fonts. Use the fonts in fonts.h; their glyphs are ASCII only (0x20-0x7E). The fallback is an icons-only font (font_symbols_10/14, from Font Awesome 5) holding exactly the LV_SYMBOL_* icons in use plus the degree sign and the bullet. A new LV_SYMBOL_ (in our code or a newly enabled LVGL widget) needs its code point added to scripts/build_fonts.sh and the fonts regenerated, or it renders as nothing. Don’t use other Unicode characters (☀, ⚠, ·): use words (“DAY”, “GREYLINE”) or an LV_SYMBOL_.

Font Use
font_jetbrains_10 dense tables and status lines (monospace: columns line up)
font_jetbrains_14 larger table text (the tile with 3 groups or fewer)
font_jetbrains_24 big numbers (clock)
font_atkinson_10/14/18 prose, buttons, titles (font_atkinson_14 is LVGL’s default)

At 10 px a JetBrains Mono character is about 6 px wide: a 320 px line holds about 50 characters. Count before adding text to a crowded row; the band screen’s footer keeps each half under 25.

Touch. Make tap targets at least ~28 x 16 px. Badges, labels and decorations inside a tappable tile must not swallow the tap: lv_obj_set_clickable(obj, false) and lv_obj_set_event_bubble(obj, true) (see band_badge_create()).

LVGL: what is switched on

To save flash, QRPickle builds only the LVGL parts it uses (include/lv_conf.h). scripts/check_lvgl_usage.py runs before every build and stops it when code uses a part that is switched off, or an LV_SYMBOL_* the icons font lacks.

Available Switched off
label, button, image, textarea, keyboard (+ buttonmatrix), dropdown, checkbox, slider (+ bar), msgbox, tabview arc, spinner, switch, list, menu, win, tileview, LED, line, canvas, span, imagebutton, animimg, arclabel, chart, scale, calendar, table, roller, spinbox
flex layout grid layout, observer (subjects)
fonts in fonts.h + icons-only fallback (font_symbols_10/14) LVGL’s Montserrat fonts
RGB565 screen, RGB565A8 images, A8 masks other colour formats (ARGB8888 layers among them)
image cache 16 KB, layer buffer 8 KB larger caches

Before using a switched-off part: set its LV_USE_* to 1 in include/lv_conf.h, run pio run -t clean, then check the flash size (check_size.py) and the LVGL peak on the device (/api/status → mem, docs/MEMORY.md). A widget often brings theme styles and icons with it: the guard tells you which icons to add to scripts/build_fonts.sh. Prefer composing the available parts (a “switch” can be a checkbox; a “list” a flex column of buttons, as the WiFi scan does).

Dashboard and tiles

The dashboard (screens/dashboard.cpp) places tiles from src/ui/widgets/. A tile is lv_obj_t* widget_x_create(lv_obj_t* parent, WidgetSize size):

 ┌────────────────┬────────────────┐
 │ clock          │ band           │  QUARTER    160 x 108 (154 x 104)
 │ QUARTER        │ HALF_VERT      │  HALF_VERT  160 x 216 (154 x 212)
 ├────────────────┤                │  FULL       320 x 216
 │ sensor/weather │                │
 │ QUARTER        │                │  4 px gaps around and between
 └────────────────┴────────────────┘

WidgetSize values are WIDGET_SIZE_QUARTER, WIDGET_SIZE_HALF_VERT and WIDGET_SIZE_FULL; the sizes in brackets are what the tiles draw inside the gaps.

Tile rules:

Screen cycling

src/ui/screen_cycle.cpp shows the screens chosen in the settings (cycle_enabled, cycle_screens, cycle_seconds; default off, all seven screens, 10 s) one after another.

Satellites screen

src/ui/screens/satellites.cpp (menu: Satellites, PAGE_SATELLITES), laid out like Band Conditions: tabs PASSES / EVENTS / GUIDE, a 172 px body and a footer (satellites and passes held, feed age, refresh).

Themes

Every colour comes from the theme: theme_color(TOKEN) (theme.h). Never write lv_color_hex() in a page or tile.

Band conditions in Classic, Field Red, Terminal Green and E-Ink Light

There are two kinds of tokens:

Use Token
page background, tile background, borders COLOR_BG_APP, COLOR_BG_PANEL, COLOR_BORDER
text, secondary text COLOR_TEXT_MAIN, COLOR_TEXT_MUTED
highlight (current column, selected tab) COLOR_ACCENT_PRIMARY
good / fair / poor (ratings, value quality) COLOR_BAND_GOOD, COLOR_BAND_FAIR, COLOR_BAND_POOR
text on a coloured badge or button COLOR_TEXT_ON_ACCENT (or the more readable of it and COLOR_TEXT_MAIN, see below)
neutral button, “no data” badge COLOR_BG_BUTTON
status dots COLOR_STATUS_OK/ERROR/WARN/BUSY

Rules that keep all seven themes readable:

Adding a theme: add it to enum ThemeId before THEME_COUNT; raise config::THEME_ID_MAX (a static_assert in theme.cpp reminds you); give it a case with every palette token in theme_color() and a name in theme_get_name(); add the <option> to the theme list in data/www/index.html; add it to THEMES in tools/device_screens.py; capture all pages.

Privacy

Screens are photographed and shared. Never show a secret (admin password, WiFi password, API keys, APRS passcode) in clear: show ****** and reveal it only on a tap, hiding it again after a few seconds. Treat the WiFi name, IP and MAC address the same way on screens that are likely to be shared. (The Network screen gets this in v0.2.4.)

Screenshots

tools/device_screens.py captures pages over WiFi from a cyd-screens build (debug only, never released). Its PAGES list must match enum LocalPage in order.

Capture only new or changed screens, in the themes the change affects (all seven when colours or layout changed), and replace just those images in docs/pics/ and the README. A full capture of every screen is needed only when something shared changes (the shell, fonts, a theme’s palette).

pio run -e cyd-screens -t upload
QRP_ADMIN_PW=... python3 -I tools/device_screens.py \
    --ip <device-ip> --out release/shots/now \
    --themes 0,1,2,3,4,5,6 --pages 0,9 \
    [--compare release/shots/before]

The cyd-screens build has less free heap than a release build: HTTPS requests with certificate checks (Cloud OTA) can fail on it. Judge network behaviour on the cyd build.

Checklists

New page

  1. src/ui/screens/x.{h,cpp} with draw_x_page(parent); state in one static struct; timers deleted on LV_EVENT_DELETE.
  2. Add PAGE_X to enum LocalPage (at the end: the screenshot tool uses the numbers) and a branch with a title in ui_navigate_local().
  3. If it belongs in the menu: DEST_X in sidebar.h, an entry in sidebar.cpp, a case in sidebar_selection_cb().
  4. Data through a manager; no lv_* outside the main loop.
  5. Keep the bottom-right corner free for the home button.
  6. Add it to PAGES in tools/device_screens.py; capture it in all themes; add the screenshot to the README table. Later changes to the page: recapture only this page.

New tile

  1. src/ui/widgets/widget_x.{h,cpp} with widget_x_create(parent, size), panel style as above.
  2. Place it in draw_dashboard_page(); tap opens its page.
  3. Timer and pointers released on LV_EVENT_DELETE.
  4. A user choice of content goes in config::Config + the web console (see config_json.cpp).

New colour need

  1. Use an existing token if one describes the element.
  2. Otherwise add a semantic token (Classic value + the palette token other themes reuse).
  3. Capture all seven themes.

Appendix G: Third-Party Notices

QRPickle’s own code is MIT-licensed (see License). The firmware and documentation also contain the components below, each under its own licence.

In the firmware

Component Licence Copyright / source Licence text
Atkinson Hyperlegible font (AtkinsonHyperlegible-Regular.ttf, converted to src/ui/fonts/font_atkinson_*_raw.c) SIL OFL 1.1 © 2020 Braille Institute of America, Inc. assets/fonts/OFL-AtkinsonHyperlegible.txt
JetBrains Mono font (JetBrainsMono-Bold.ttf, converted to src/ui/fonts/font_jetbrains_*_raw.c) SIL OFL 1.1 © 2020 The JetBrains Mono Project Authors assets/fonts/OFL-JetBrainsMono.txt
Font Awesome 5 Free symbols: 18 icons in src/ui/fonts/font_symbols_*_raw.c, converted from assets/fonts/FontAwesome5-Solid+Brands+Regular.woff (from LVGL v8.3.11 scripts/built_in_font/) SIL OFL 1.1 © Fonticons, Inc. assets/fonts/OFL-LVGL-builtin.txt
Font Awesome 7 Free icons “globe” and “microchip” (assets/img/icon_*), recoloured CC BY 4.0 © Fonticons, Inc. creativecommons.org/licenses/by/4.0
Solar-terrestrial data (fetched at run time, not redistributed) Free use; credit requested HAMQSL.com, Paul L Herrman, N0NBH hamqsl.com/solar.html; credited in About and the band screen’s guide
Weather condition icons (assets/img/[0-9]*) No published icon licence; attributed © OpenWeather, openweathermap.org assets/img/SOURCES.md
International amateur radio symbol (splash screen; web console favicon data/www/favicon.svg) Public domain Denelson83, Wikimedia Commons assets/img/SOURCES.md
Bangalore Amateur Radio Club (VU2ARC) logo (splash screen) Not MIT; used with the club’s permission © Bangalore Amateur Radio Club, barc.in assets/img/SOURCES.md
LVGL 9.6 MIT © LVGL Kft, lvgl/lvgl in the library
TFT_eSPI 2.5 MIT (per library.json) Bodmer, Bodmer/TFT_eSPI in the library
ArduinoJson 7 MIT © Benoît Blanchon, arduinojson.org in the library
ESPAsyncWebServer 3.12, AsyncTCP 3.5 LGPL-3.0 ESP32Async in the library; QRPickle’s full source is public, so the firmware can be relinked with modified versions
Adafruit BME280 Library BSD © Adafruit Industries in the library
Adafruit Unified Sensor Apache-2.0 © Adafruit Industries in the library
Adafruit BusIO MIT © Adafruit Industries in the library
XPT2046_Touchscreen MIT-style © 2015 Paul Stoffregen in the library
Sgp4 (Hopperpop) 1.0.3, satellite passes MIT © 2016 Hopperpop; SGP4 code from “Fundamentals of Astrodynamics and Applications” by David Vallado (freely distributed) in the library
Space feed data (fetched at run time) CC BY-SA 4.0 (SatNOGS DB); CelesTrak and ARISS credited SatNOGS, CelesTrak, ARISS built by tools/space_feed/build_feed.py; credited in About
Arduino-ESP32 core 2.0.17 (platform espressif32 @ 6.13.0) LGPL-2.1 © Espressif Systems and contributors espressif/arduino-esp32
ESP-IDF 4.4 (incl. FreeRTOS, lwIP, mbedTLS) Apache-2.0 (FreeRTOS: MIT, lwIP: BSD) © Espressif Systems and the respective authors espressif/esp-idf

The libraries are fetched by PlatformIO at the versions pinned in platformio.ini; their licence files come with them. Image assets and their sources are listed in assets/img/SOURCES.md.

scripts/check_licenses.py runs on every build and in CI. It fails when a library, font, platform version, image or file with a foreign copyright line is added without being recorded here (or in the image manifests).

In the documentation only

Component Licence Copyright / source
CYD pinout image (docs/pics/third-party/) CC BY-NC-ND 4.0 (not MIT) © Renzo Mischianti, mischianti.org. Unmodified; see docs/pics/third-party/README.md.

Web installer page

The GitHub Pages installer loads ESP Web Tools (Apache-2.0) from a CDN; it is not bundled.