Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion firmware/patternflow/console/home.html
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@
</div>
<div class="now" id="now">&mdash;</div>
<div class="now-sub" id="nowsub"></div>
<div class="now-sub" id="nowperf" hidden></div>

<div class="pwr">
<span class="pwr-k">Panel</span>
Expand Down Expand Up @@ -334,6 +335,18 @@
if(d)return d+'d '+h+'h';if(h)return h+'h '+m+'m';
if(m)return m+'m';return s+'s';
}
// Optional status chips from feature appendStatus fields. Labels stay
// feature-agnostic so this core page never names a feature (R5).
function paintPerf(s){
var el=$('nowperf');if(!el)return;
var t=[];
if(s.mqttRole==='publisher')t.push('Pub');
else if(s.mqttRole==='subscriber')t.push('Sub');
if(typeof s.sequenceMode==='boolean')
t.push(s.sequenceMode?'Playlist':'Browse');
if(t.length){el.hidden=false;el.textContent=t.join(' · ')}
else{el.hidden=true;el.textContent=''}
}
function tick(){
if(document.hidden)return;
fetch('/api/status',{cache:'no-store'}).then(function(r){return r.json()}).then(function(s){
Expand All @@ -356,6 +369,7 @@
$('nowsub').textContent=(s.frameUs?Math.round(1e6/s.frameUs)+' fps · ':'')+
(s.activeIsModule?'module':'built in');
}
paintPerf(s);
paintPower(s.sleep);
paintBrightness(s.brightness);
$('s-pat').textContent=s.patterns+' ('+s.presets+' built in + '+s.modules+' modules)';
Expand Down Expand Up @@ -388,7 +402,7 @@
}
if(s.params)updateSliders(s.params);
checkUpdate(s.version,s.variant);
}).catch(function(){$('nowsub').textContent='cannot reach device'});
}).catch(function(){$('nowsub').textContent='cannot reach device';var p=$('nowperf');if(p){p.hidden=true;p.textContent=''}});
}

// ── Panel switch ──────────────────────────────────────────────
Expand Down
15 changes: 15 additions & 0 deletions firmware/patternflow/features/pf_feature.h
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,21 @@ struct PFFeature {
// a full pass over the frame is ~8 k pixels and must stay well under a
// millisecond.
const uint8_t* (*composeFrame)(const uint8_t* frame, int w, int h);

// SELECT screen (pattern browser). Optional — null on every feature leaves
// the core's default SELECT untouched. A feature that owns an alternate
// SELECT mode (Sequences playlist) implements these so the sketch never
// has to name that feature.
//
// handleSelectInput: consume SELECT knobs; return true to suppress the
// core's default K4 pattern browse for this frame.
// drawSelect: return true if the feature drew the entire SELECT UI
// (sketch skips drawSelectingMode).
// decorateSelect: called after the default SELECT draw when drawSelect
// returned false — add a chrome hint without replacing the browser.
bool (*handleSelectInput)(InputFrame&);
bool (*drawSelect)();
void (*decorateSelect)();
};

// ── Legacy name shim (2026-08-30) ───────────────────────────────────────
Expand Down
28 changes: 28 additions & 0 deletions firmware/patternflow/features/pf_features.h
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,34 @@ inline const char* capAt(size_t i) {
}
inline size_t count() { return PF_FEATURE_COUNT; }

// SELECT mode. First feature that returns true from drawSelect owns the
// whole SELECT frame. handleSelectInput runs for every feature; any true
// suppresses default K4 browse. decorateSelect runs after the default
// draw when nobody claimed the frame.
inline bool handleSelectInput(InputFrame& input) {
bool suppress = false;
for (size_t i = 0; i < PF_FEATURE_COUNT; i++) {
if (PF_FEATURES[i]->handleSelectInput &&
PF_FEATURES[i]->handleSelectInput(input)) {
suppress = true;
}
}
return suppress;
}

inline bool drawSelect() {
for (size_t i = 0; i < PF_FEATURE_COUNT; i++) {
if (PF_FEATURES[i]->drawSelect && PF_FEATURES[i]->drawSelect()) return true;
}
return false;
}

inline void decorateSelect() {
for (size_t i = 0; i < PF_FEATURE_COUNT; i++) {
if (PF_FEATURES[i]->decorateSelect) PF_FEATURES[i]->decorateSelect();
}
}

} // namespace PFFeatures

// ── Legacy name shim (2026-08-30) ───────────────────────────────────────
Expand Down
68 changes: 68 additions & 0 deletions firmware/patternflow/features/show/core_show_select.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// ═══════════════════════════════════════════════════════════
// PatternFlow - SELECT chrome for the Sequences feature
//
// The sketch must not name this feature. These helpers implement the
// SELECT hooks so Performance can toggle Normal ↔ Sequence playlist
// from K3 without putting PatternflowShow:: into patternflow.ino.
//
// License: MIT
// ═══════════════════════════════════════════════════════════
#pragma once

#include "core_show.h"
#include "../../src/core_ui_text.h"

// dma_display is declared in the sketch / core_display; same pattern as
// the weather clock overlay — features draw without including the HUB75 init.

namespace PatternflowShowSelect {

inline uint16_t ledC() { return dma_display->color565(232, 85, 46); }
inline uint16_t dimC() { return dma_display->color565(80, 80, 80); }
inline uint16_t greyC() { return dma_display->color565(160, 160, 160); }
inline uint16_t softC() { return dma_display->color565(200, 200, 200); }

inline bool handleSelectInput(InputFrame& input) {
if (input.knobDeltas[2] != 0) {
bool nowSeq = PatternflowShow::toggleSequenceMode();
Serial.printf(">>> SELECT mode → %s\n", nowSeq ? "SEQUENCE" : "NORMAL");
input.knobDeltas[2] = 0;
}
// In Sequence mode the playlist owns the panel — suppress K4 browse.
return PatternflowShow::isSequenceMode();
}

inline bool drawSelect() {
if (!PatternflowShow::isSequenceMode()) return false;
if (!dma_display) return true;

uint16_t screenH = dma_display->height();
PatternflowUiText::drawChromeLine("SEQUENCE", 10,
dma_display->color565(190, 190, 190));

char name[64];
if (PatternflowShow::isPlaying() && PatternflowShow::title()[0]) {
snprintf(name, sizeof(name), "%s", PatternflowShow::title());
} else if (PatternflowShow::storedSize() > 0) {
snprintf(name, sizeof(name), "%u SHOWS",
(unsigned)PatternflowShow::storedSize());
} else {
snprintf(name, sizeof(name), "NO PLAYLIST");
}
PatternflowUiText::drawWrappedName(name, screenH / 2,
dma_display->color565(255, 255, 255));
PatternflowUiText::drawChromeLine("HOLD TO SELECT", screenH - 22, softC());
PatternflowUiText::drawChromeLine("K3=NORMAL", screenH - 12, greyC());
PatternflowUiText::useDefaultFont();
return true;
}

inline void decorateSelect() {
if (PatternflowShow::isSequenceMode()) return;
if (!dma_display) return;
uint16_t screenH = dma_display->height();
PatternflowUiText::drawChromeLine("K3=SEQUENCE", screenH - 12, greyC());
PatternflowUiText::useDefaultFont();
}

} // namespace PatternflowShowSelect
15 changes: 14 additions & 1 deletion firmware/patternflow/features/show/feature_show.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#include "core_show.h"
#include "core_show_http.h"
#include "core_show_schedule.h"
#include "core_show_select.h"
#include "core_library_http.h"

namespace PFFeatureShow {
Expand Down Expand Up @@ -68,6 +69,14 @@ inline void drawOverlay(const PFFeatureFrame& frame) {
}
}

// Lean fields for /api/status (home card). Full show state stays on /api/show.
inline void appendStatus(String& json) {
json += ",\"sequenceMode\":";
json += PatternflowShow::isSequenceMode() ? "true" : "false";
json += ",\"showPlaying\":";
json += PatternflowShow::isPlaying() ? "true" : "false";
}

inline const PFFeature descriptor = {
"show", // name
"shows", // cap reported by /api/status
Expand All @@ -84,12 +93,16 @@ inline const PFFeature descriptor = {
nullptr, // shortName - not listed in the device menu
nullptr, // isRuntimeEnabled
nullptr, // setRuntimeEnabled
nullptr, // appendStatus
appendStatus,
drawOverlay,
"/show", // navPath - the console header link
"Sequences", // navLabel
"Timed shows from .pfs tables — play one, chain a playlist, "
"schedule night and wake.",
nullptr, // composeFrame
PatternflowShowSelect::handleSelectInput,
PatternflowShowSelect::drawSelect,
PatternflowShowSelect::decorateSelect,
};

} // namespace PFFeatureShow
11 changes: 9 additions & 2 deletions firmware/patternflow/patternflow.ino
Original file line number Diff line number Diff line change
Expand Up @@ -1881,12 +1881,16 @@ void loop() {
drawBrightnessNotice();
}
} else {
// Features may own an alternate SELECT mode (e.g. Sequences playlist).
// If any returns true, skip the default K4 pattern browse this frame.
bool selectFeatureOwnsBrowse = PFFeatures::handleSelectInput(input);
int selectSteps = 0;
if (input.knobDeltas[3] != 0) {
if (!selectFeatureOwnsBrowse && input.knobDeltas[3] != 0) {
selectAccum += input.knobDeltas[3];
selectSteps = selectAccum / SELECT_DETENTS_PER_STEP; // truncates toward zero
selectAccum -= selectSteps * SELECT_DETENTS_PER_STEP;
}
if (selectFeatureOwnsBrowse) selectAccum = 0;
if (selectSteps != 0) {
currentPatternIdx += selectSteps;
// Floored modulo: OSC /knob/4/delta can deliver a delta more negative
Expand Down Expand Up @@ -1949,7 +1953,10 @@ void loop() {
}

dma_display->setRotation(1);
drawSelectingMode();
if (!PFFeatures::drawSelect()) {
drawSelectingMode();
PFFeatures::decorateSelect();
}
if (brightnessAdjusting) {
drawBrightnessNotice(); // mode indicator, same as in RUNNING
}
Expand Down
Loading
Loading