return to projects

observations from the llizard ecosystem

a development log

2025.01.28

The handshake that kept dropping

Spotify OAuth on Janus was a three-bug puzzle. First: two activities claimed the same redirect URI, so Android asked the user to choose between two identical "Janus" options - one that worked and one that didn't. Second: singleTask launch mode broke ActivityResult delivery, so a successful login killed the app. Third and strangest: Coil's crossfade animation on the profile picture corrupted Compose's internal layout tree during the activity resume after the browser redirect. The fix was an AsyncImage inside a stable Box with crossfade disabled - the green placeholder circle shows first, then the profile photo paints over it without Compose ever needing to swap nodes.

~ OAuth redirect disambiguation - removed duplicate janus://spotify-callback intent-filter from MainActivity. Only AppAuth's RedirectUriReceiverActivity handles the redirect now. No more "Open with" dialog.
~ Activity launch mode - changed MainActivity from singleTask to singleTop. ActivityResult delivery works across the OAuth browser round-trip without collapsing the activity stack.
~ Compose LayoutNode crash - Coil's crossfade(true) on AsyncImage triggered an ArrayIndexOutOfBoundsException in MutableVector.add during node insertion. Fixed by wrapping the image in a stable Box container and disabling the Coil crossfade. The Compose tree never changes shape now - only the pixels inside the Box change.
~ State update batching - fetchLibraryStats and checkSavedTracksStatus now collect all API results into local variables and emit a single StateFlow update instead of one per network response. Fewer recompositions, no more mid-layout mutations.
+ Crossfade transition - login, logged-in, and setup states now crossfade between each other in an isolated sub-composition, keeping the parent Column's node count stable.

The real lesson: Compose's layout tree is a living thing. You can't swap a dozen nodes into a Column while the Choreographer is mid-frame and expect the internal arrays to agree on their own length. Give the tree one stable shape and change what's painted inside it.

janus spotify oauth compose coil robustness
2025.01.27

The orb sharpens, the music card learns context

Mercury sheds its prototype skin. The orb tightens, the buttons grow to fill the screen properly, and "Janus" replaces "Mercury" where the user looks - because the service is Mercury, but the companion it connects to is Janus. Meanwhile, Now Playing learns to think about what it's playing. Long-press the select button and two new doors appear: play this album, play this artist. The Albums and Artists plugins learn patience too - art loads one image at a time instead of stampeding the filesystem.

~ Mercury UI overhaul - buttons enlarged to proper touch targets, orb geometry cleaned up with tighter layering, status text reworded to reference Janus (the Android app the user actually interacts with).
+ Play Album / Play Artist actions - Now Playing's context menu gains two new entries. Long-press select, choose "Play Album" or "Play Artist" to start the full album or artist radio via spotify: URI. Only appears when Spotify IDs are present in media state.
+ spotifyAlbumId / spotifyArtistId - two new fields in LlzMediaState, fetched from Redis. The SDK now knows not just what's playing, but where it lives in the Spotify graph.
~ Defensive lazy loading - Albums and Artists plugins gain a LoadState machine (INIT → REQUESTING → LOADED → EMPTY → ERROR). Art loads one image at a time with a 0.5s init delay. Bounds checking everywhere. No more crashes from racing the filesystem.

The staggered art loading matters on the Cortex-A7. Loading 20 album covers simultaneously would starve the render loop. One at a time keeps the carousel smooth while art fills in behind the user's gaze.

mercury nowplaying spotify albums artists robustness
2025.01.26

The artists take the stage

The Spotify library grows a second wing. Where albums scroll past as rectangles, artists arrive as circles - profile photos cropped round, names centered beneath. The carousel shares its physics with Albums but the aesthetic is its own. Select an artist and their top tracks shuffle into life. Deeper in the stack, the SDK learns to speak Spotify's language of shuffle, repeat, and like.

Artists plugin - horizontal carousel of followed Spotify artists with circular profile cards. Displays name, primary genre, and follower count. Spring-based scrolling physics shared with Albums. Select to shuffle an artist's top tracks.
+ Cursor-based pagination - Spotify's followed artists endpoint uses cursor pagination instead of offset. The SDK and BLE bridge handle the cursor handoff so the plugin just sees a flat list.
Spotify playback state API - the SDK gains first-class shuffle, repeat, and like controls. LlzMediaToggleShuffle(), LlzMediaCycleRepeat(), LlzMediaLikeTrack(). State flows back through BLE into LlzMediaState fields.
+ SDK artist library API - LlzMediaRequestLibraryArtists and LlzMediaGetLibraryArtists join the library family. Artist data stored in Redis with the same chunked BLE transfer pattern as albums.

The circular artist cards use DrawCircle masking rather than texture manipulation - the source images are square, cropped visually at draw time. Cheaper than generating circular textures, and the spring physics keep the carousel feeling alive rather than mechanical.

artists spotify sdk playback ble library
2025.01.27

Mercury rises, the image remembers

The BLE connection gets its own home. A new Mercury plugin surfaces as the face of the Bluetooth bridge - a pulsing orb that breathes when scanning, glows green when connected, and dims red when lost. Two buttons let you reconnect or restart the service without leaving the GUI. Meanwhile, the firmware learns to remember. Menu layout, plugin visibility, and sort order are now baked into the llizardOS image so new installs boot into a curated home screen instead of an alphabetical list of everything.

Mercury plugin - BLE connection status with a liquid metal aesthetic. Status orb pulses during scanning, holds steady when connected. Connect and Restart Service buttons with tap feedback and cooldown timers.
+ Firmware config defaults - plugin_visibility.ini, menu_sort_order.ini, and config.ini are now copied from the live CarThing and baked into the llizardOS build. New installs start with a curated menu layout.
~ Settings becomes the control panel - Plugin Manager and Menu Sorter are hidden from the main menu by default. They now live as launch buttons at the bottom of Settings, reached through LlzRequestOpenPlugin navigation.
~ 24 plugins rebuilt - full ARM binary refresh with 5 new .so files: albums, artists, mercury, queue, and spotify. The image build pipeline copies everything in one pass.

The Mercury orb draws with layered alpha circles - a soft outer glow, a solid core, and a clean inner ring for depth. No textures, just math and transparency. The config bake reads three .ini files from the device over SCP and installs them into /var/llizard/ during the rootfs build stage.

mercury ble llizardos firmware settings
2025.01.27

The music card opens its doors

Tap a song and the world behind it unfolds. Two new buttons appear beneath Now Playing when Spotify is the source - one for the album, one for the artist. Bottom sheets slide up with track listings, follower counts, genres, and top tracks. The data flows from Spotify's API through the SDK, through the controller, into modal surfaces that feel like opening a liner note.

+ View Album sheet - album art, name, artists, release year, track count, label, and a full track listing with durations. The Spotify API's album endpoint finally has a home in the SDK.
+ View Artist sheet - circular artist photo, follower count, genres, and top tracks with album art thumbnails. Two API calls combined into one coherent view.
+ spotSDK endpoints - getAlbum, getArtist, getArtistTopTracks added to the API service. AlbumTracksWrapper model for the album's embedded track paging object.
+ SpotifyPlaybackController learns fetchAlbumDetails and fetchArtistDetails - reusing the existing token infrastructure to serve the new sheets.
~ MainViewModel extended with reactive detail state. Six new flows combined via chained combine operators, loading state properly reactive through the full chain.

The buttons only appear when spotifyAlbumId or spotifyArtistId are present in MediaState - invisible for non-Spotify sources. The bottom sheets use Coil for async image loading and Material 3's ModalBottomSheet with skip-partial-expand for full-height content.

janus spotsdk spotify bottom-sheets compose
2025.01.26

The library comes to the llizard

Your Spotify library now lives on the CarThing. A new Albums plugin presents your saved albums in a smooth carousel, album art floating past like vinyl in a crate. Select one and the music starts - the plugin bows out, Now Playing takes the stage. The whole stack learned new tricks to make it happen.

Albums plugin - carousel UI for browsing saved Spotify albums. Spring-based physics scrolling, album art from dual cache directories, auto-navigation to Now Playing after selection.
+ SDK library API - new functions for fetching and displaying Spotify library data. LlzMediaRequestLibraryAlbums, LlzMediaGetLibraryAlbums, LlzMediaPlaySpotifyUri. The host speaks Spotify now.
+ BLE response types 7-11 - the Go bridge learns five new languages. Spotify state, library overview, track lists, album lists, playlist lists. Each reassembled from chunks, stored in Redis.
+ Separate preview cache - library album art (150×150) lives in /var/mediadash/album_art_previews. Now playing art (250×250) stays in album_art_cache. The Albums plugin checks both.
~ Auto token refresh - Janus no longer forgets its Spotify credentials. SpotifyLibraryManager refreshes expired tokens automatically. Play commands work without visiting the app first.
~ play_uri command - BLE can now start any Spotify content. Albums, playlists, individual tracks - send the URI, music plays. The CarThing becomes a remote.

The full vertical: CarThing requests albums → Go bridge sends BLE command → Janus fetches from Spotify API → chunks flow back → Redis stores → C plugin displays → user selects → play_uri command → music. Each layer trusts the next.

albums spotify sdk mercury janus ble
2025.01.25

The queue learns to skip gracefully

When you skip through a queue of ten songs to reach the eleventh, you don't want to see nine flashing album covers along the way. The BLE bridge learns discretion - intermediate tracks pass in silence, only the destination announces itself.

+ Track change detection - the Queue plugin now polls media state every 500ms. When the track title changes, a fresh queue is requested automatically.
+ "Now Playing" stays current - the queue's header item syncs directly from Redis media state, independent of queue API responses. No stale data.
~ Suppressed intermediate updates - during multi-skip queue jumps, MediaControllerManager silences metadata broadcasts. Only the final destination track triggers BLE notifications.
~ Skip callback system - skipToPositionLocal() now accepts an onBeforeSkip callback with (skipNumber, totalSkips, isFinalSkip). Callers control suppression timing.

The pattern: suppress before intermediate skips, enable before the final one, always re-enable after completion (success or error). The user sees one smooth transition, not a slideshow.

queue janus ble polish
2025.01.24

Janus learns to speak Spotify

The Android companion app grows a new voice. A full Spotify integration page emerges - OAuth flows, library statistics, playback controls, and a queue viewer that lets you skip through your upcoming tracks. The spotSDK learns new endpoints, and the BLE bridge prepares to carry richer data.

Spotify Auth Page - complete OAuth 2.0 PKCE flow with configurable Client ID. Paste your credentials, login with Spotify, see your profile with avatar, premium status, and follower count.
+ Library statistics - saved tracks, albums, playlists, and followed artists displayed with refresh button. Currently playing and recently played tracks shown in real-time.
+ Playback controls - shuffle toggle (off/on) and repeat cycle (off/all/one). State syncs with Spotify and persists across app restarts.
+ Queue viewer - shows up to 10 upcoming tracks with album art, artist, and duration. Tap any track to skip forward through the queue (calls skip-to-next repeatedly to preserve queue context).
~ spotSDK endpoints - added playback state (GET /me/player), shuffle/repeat toggles, play endpoint, skip-to-next, and queue retrieval. user-follow-read scope for artist following.

The queue skip uses Spotify's skip-to-next API repeatedly rather than playing tracks directly. This preserves the queue context - after the skipped-to track finishes, playback continues naturally.

janus spotsdk spotify oauth playback
2025.01.24

The arena learns who you are

LLZ Survivors grows a soul. Five warrior classes step into the arena, each with their own philosophy of survival. The upgrade screen becomes a shop where choices linger. Champions emerge from the horde with golden halos and dangerous gifts. The spatial grid learns to see, and the engine breathes easier.

Class selection system - five starting classes with unique stats. Warrior (130 HP, 15% armor), Scout (fast, +25% XP), Tank (180 HP, slow, 25% armor), Mage (60 HP, +40% XP, magic bonus), Balanced (classic mode). Carousel UI with stat previews.
Multi-upgrade shop - level-up screen transformed into a point-spending session. Buy multiple upgrades, see "SOLD OUT" on purchased cards, confirm when done. No more forced single choices.
+ Champion enemies - 5% of enemies after wave 3 spawn as Champions with random affixes. Swift (+50% speed), Vampiric (regenerates), Armored (50% DR), Splitter (spawns copies on death). Golden glow, 2.5x XP reward.
~ Spatial grid optimization - FindNearestEnemyGrid() replaces O(n) loops with grid cell queries. Melee spin and chain lightning now target through the grid. The engine runs lighter.
~ Enemy AI refactor - 200-line switch statement dissolved into function pointer dispatch. Each enemy type gets its own AI function. Adding new creatures is now a single function and one table entry.

~900 lines added. The class system flows: Menu → Class Select → Weapon Select → Arena. Each class's preferred weapon gets bonus damage.

llzsurvivors classes champions optimization architecture
2025.01.24

The arena rewards the bold

New systems emerge that reward aggression and punish complacency. Kill milestones mark your journey with permanent buffs. Danger zones bloom across the battlefield - step inside for bonus XP, pay with your health. The combo system learns to count higher, and each tier brings new power.

+ Kill milestone rewards - eight thresholds (50 to 2000 kills) with unique rewards. FIRST BLOOD heals, CENTURION grants upgrade points, EXTERMINATOR nukes the screen. Progress bar shows kills to next milestone.
+ Danger zones - hazardous areas spawn every 25 seconds after wave 2. Fire (2x XP, burns), Electric (1.75x XP, shocks), Slow (1.5x XP, drags). 3-second warning, 15-second duration, visible on minimap.
+ Combo tier system - seven tiers from Nice (5 kills) to GODLIKE (100+). Each tier adds XP bonus (up to 2x) and damage bonus (up to 30%). 2.5-second timeout keeps the pressure on.
~ Draw call optimizations - danger glow uses gradient rectangles (140 → 4 calls), enemy angles cached (no redundant atan2f), shielder arcs halved, minimap uses squared distance, particle system early-exits when empty.

~850 more lines. Total survivors codebase now exceeds 7,000 lines. The CarThing's Cortex-A7 breathes easier with every optimization.

llzsurvivors milestones danger-zones combo performance
2025.01.23

The llizard emerges

Today marks the public release of llizardOS. The Fukai Sake Edition goes live - "deeply functional and beautiful, not quite perfect." The firmware, Android app, and all 18 plugins are now available on GitHub. A subreddit springs into existence.

LlizardOS open beta - full system image released with Void Linux, raylib DRM graphics, Redis state management, and the complete plugin ecosystem.
Janus APK - Android BLE companion app released. Reads media state from any app, broadcasts over Bluetooth, receives control commands.
+ r/llizardOS - community subreddit created. Setup guides, discussions, and feature requests welcome.
~ spotSDK begins development - a collection of service-specific SDKs for Spotify, Apple Music, YouTube Music. Unified APIs for media control across platforms.

iOS support is planned. BLE implementation was designed with Apple requirements in mind - the slower BLE protocol allows for plug-and-play on iPhone once development begins.

release llizardos janus spotsdk community
2025.01.23

The arena learns to fight back

Six new creatures enter the arena, each with their own philosophy of combat. The Hornet keeps its distance and draws lines of fire. The Spinner hides its eye until the storm passes. The Mirror asks: which one is real? The bullet hell has arrived.

+ Hornet enemy (Wave 8) - approaches to 250 units, stops, charges a laser for 1.5s with visible warning line, then fires a powerful beam that stays on course. Wasp-shaped with wings and stripes.
+ Spinner enemy (Wave 12) - fires spiral bullet patterns from rotating spokes. Only vulnerable when its eye opens between barrages. Classic shmup design.
+ Mirror enemy (Wave 13) - creates decoy copies of itself. Only the real one takes damage. Briefly reveals its true form (golden glow) after splitting.
+ Shielder enemy (Wave 14) - carries a rotating energy shield that blocks frontal attacks. Must be hit from behind. Performs charge attacks when close.
+ Bomber enemy (Wave 16) - drops clusters of delayed explosive mines. Stunned and vulnerable briefly after each drop. Mines blink faster as detonation approaches.
+ Phaser enemy (Wave 18) - phases in and out of reality. Only damageable when visible. Teleports near player and fires bullet bursts when phasing in. Ghost-like translucent form.
+ Enemy bullet system - new projectile pool for bullet hell patterns. Enemy bullets have glowing cores and damage player on contact.
+ Mine system - timed explosives with visual warning radius. Spiky appearance, blinking indicator accelerates before boom.

Victory condition added: reach level 20 to win. Lifesteal nerfed with exponential dropoff - raw 50% becomes ~15% effective. The endgame now has teeth.

llzsurvivors bullet-hell enemies balance
2025.01.22

New creatures emerge, old ones grow wiser

LLZ Survivors hatched today - a Vampire Survivors-style arena game with skill trees and branching weapon paths. Meanwhile, Bejeweled learned to count its levels properly. The build pipeline grew a new helper script that knows where all the binaries live.

+ LLZ Survivors plugin - arena survival game with 5 starting weapons, 3 skill branches each, potions, XP collection, and a scrolling world larger than the screen.
~ Bejeweled level progression fix - levels now actually increment based on score thresholds (was stuck at 1). Progress bar syncs with real level boundaries.
+ copy_bins_before_commit_push.sh - new script in llizardOS that copies llizardGUI, mercury, 18 plugins, and question banks in one command.
Memory audit - verified both Bejeweled and Survivors use only static arrays, no leaks. SDK fonts are cached and properly cleaned.

18 plugins now live in the llizardOS resources - five newcomers: bejeweled, llzsurvivors, media_channels, menu_sorter, plugin_manager.

llzsurvivors bejeweled build-tools llizardos memory
2025.01.22

The back button learns patience

Today the SDK learned to distinguish between a quick tap and a thoughtful hold. The back button now understands intent - a brief press means "go back," while a longer hold opens the media channel selector without triggering navigation on release.

+ Back button hold detection - new SDK fields: backHold, backClick, backDown, backHoldTime. Long press (0.5s) triggers backHold, quick release triggers backClick.
+ Media channels popup in Now Playing - long-press back to open a floating panel for switching between Spotify, YouTube Music, Podcasts, and other active media sources.
~ Input event semantics - backReleased no longer fires after a long press. The hold was the action; the release is silence.

Documentation updated across all repositories - SDK, Mercury, Janus, and LlizardOS now have comprehensive READMEs.

sdk input nowplaying media-channels documentation
2025.01.21

Channels and courtesy

The media channel system grows more polite. When switching from Spotify to YouTube Music, the old app now gracefully pauses before the new one takes the stage. No more two voices speaking at once.

+ Media Channels plugin - a dedicated screen for browsing and selecting active media sources, accessible from Settings.
+ Refresh button in media channels - pull the latest list of active apps from your phone on demand.
+ Podcasts as a channel - Janus now reports its internal podcast player alongside external apps like Spotify.
~ Auto-pause on switch - changing channels pauses the currently playing app first. Courteous transitions.
media-channels janus podcasts settings
2025.01.20

Five ways to see a menu

The main menu now speaks five visual languages. List view for the minimalists, Carousel for those who like to swipe, Cards for the tactile, CarThing for nostalgia, and Grid for the Apple-hearted. Each style persists across reboots.

+ Grid menu style - a clean 3x2 icon grid with Apple-inspired white aesthetic and iBrand font.
+ CarThing menu style - the original Spotify aesthetic returns, complete with Omicron and Tracklister fonts.
~ Menu style persistence - your preferred style survives power cycles via SDK config.
menu ui fonts config
2025.01.19

The blur follows the music

Album art now bleeds into the background. The SDK tracks album art changes in Redis and automatically generates blurred backdrops. Nine animated background styles can now inherit colors from whatever's playing.

+ Auto-blur album art - SDK background system tracks Redis album art keys and generates blur textures automatically.
+ Color extraction - dominant colors pulled from album art to tint UI elements and background animations.
~ Crossfade transitions - smooth alpha blending when album art changes. No jarring switches.
sdk album-art backgrounds redis