File DrumDisplay.cpp
File List > DrumDisplay > DrumDisplay.cpp
Go to the documentation of this file
#ifdef ARDUINO_ARCH_STM32
#include "DrumDisplay.h"
#include <math.h>
#include <string.h>
namespace OpenSkyhawk {
// Tuning constants (mirror the prototype: 0.30 ease, ~60 fps gate).
static const float EASE = 0.30f; // per-frame ease factor (0..1); lower = slower roll
static const uint32_t FRAME_MS = 16; // ~60 fps render gate
static const float SETTLE_EPS = 0.02f; // |target-pos| below which a place is "settled"
static const float SNAP_LANDING = 1.5f; // on a big jump, land this many digits shy of target
static const float PX_PER_MM = 4.35f; // nominal scale (bench value); auto-shrink corrects it
// Cell kinds stored in _cellKind[].
static const uint8_t KIND_DIGIT = 0;
static const uint8_t KIND_GLYPH = 1;
static const uint8_t KIND_FLAG = 2;
static long pow10l(uint8_t n) {
long r = 1;
while (n--) r *= 10;
return r;
}
// ── construction ──────────────────────────────────────────────────────────────
DrumDisplay::DrumDisplay(U8G2& oled, const DrumReadout& readout,
DrumFont font, float xOffsetMm, float yOffsetMm)
: _oled(&oled), _r(&readout), _mux(nullptr), _channel(0), _font(font),
_xOffMm(xOffsetMm), _yOffMm(yOffsetMm),
_target(0), _flagTarget(0), _dirty(false), _hasState(false),
_flagPos(0.0f),
_geomDirty(false), _colW(0), _cellH(0), _gap(0), _flagW(0), _cy(0),
_nCells(0), _lastFrameMs(0) {
for (uint8_t i = 0; i < 6; i++) _pos[i] = 0.0f;
}
DrumDisplay::DrumDisplay(U8G2& oled, const DrumReadout& readout,
I2cMux& mux, uint8_t channel,
DrumFont font, float xOffsetMm, float yOffsetMm)
: DrumDisplay(oled, readout, font, xOffsetMm, yOffsetMm) {
_mux = &mux;
_channel = channel;
}
// ── decode helpers ────────────────────────────────────────────────────────────
long DrumDisplay::decodeDigits(uint16_t value, uint16_t mask, uint8_t nDigits) {
uint16_t m = mask ? mask : 0xFFFF;
uint32_t masked = static_cast<uint32_t>(value & m);
long span = pow10l(nDigits) - 1; // 1 digit → 9, 2 digits → 99
// Whole-word sources (m == 0xFFFF) scale 0..65535 → 0..span. A low-justified bit field
// scales 0..m → 0..span. Non-contiguous packed fields would need a right-shift first —
// none shipped; see the TODO(bench) notes on the sketch-defined descriptors.
return lroundf(static_cast<float>(masked) / static_cast<float>(m) * static_cast<float>(span));
}
// ── onControlPacket — decode + splice + mark dirty; NEVER draws ────────────────
void DrumDisplay::onControlPacket(uint16_t controlId, uint16_t value) {
// Flag source? (A source may be BOTH a digit and the flag — NAV hemisphere dual-role —
// so this does not early-return; the digit loop below still runs for the same address.)
if (_r->flag.enabled && controlId == _r->flag.address) {
uint16_t m = _r->flag.mask ? _r->flag.mask : 0xFFFF;
uint32_t masked = static_cast<uint32_t>(value & m);
int nFaces = static_cast<int>(strlen(_r->flag.faces));
if (nFaces < 1) nFaces = 1;
long face = (m == 0xFFFF)
? lroundf(static_cast<float>(masked) / 65535.0f * static_cast<float>(nFaces - 1))
: (masked ? (nFaces - 1) : 0);
if (!_hasState || face != _flagTarget) {
_flagTarget = face;
_dirty = true;
_hasState = true;
}
}
// Digit source(s)? Scan ALL sources, not just the first match — two sources may share one
// address with different masks (e.g. ARC-51 10 MHz + 1 MHz both at 0x853a, mask-separated).
for (uint8_t i = 0; i < _r->nSources; i++) {
const DrumSource& s = _r->sources[i];
if (controlId != s.address) continue;
long part = decodeDigits(value, s.mask, s.nDigits);
long lo = pow10l(s.place); // weight of the low column of this field
long hi = pow10l(s.place + s.nDigits); // weight just above the field
long keepHigh = (_target / hi) * hi; // digits above the field
long keepLow = _target % lo; // digits below the field
long spliced = keepHigh + part * lo + keepLow;
if (!_hasState || spliced != _target) {
_target = spliced;
_dirty = true;
_hasState = true;
}
}
}
// ── i2cProbe / oledAddr — I2cHealth reachability contract ──────────────────────
bool DrumDisplay::i2cProbe() {
#ifdef DRUMDISPLAY_TEST
_probeCount++;
if (_probeOverride >= 0) { _fault = _probeOverride ? Fault::None : Fault::Device; return _probeOverride != 0; }
#endif
if (!_mux) { // direct-bus: probe the OLED on the default bus (Wire)
Wire.beginTransmission(oledAddr()); // NOTE: assumes Wire; a direct OLED on Wire1 isn't covered yet
const bool ok = (Wire.endTransmission() == 0);
_fault = ok ? Fault::None : Fault::Device;
return ok;
}
// Muxed: FORCE-write the channel (uncached) so a mux reset / power-glitch is re-routed; a NAK on
// that write means the mux itself is gone. Then probe the OLED on the now-selected branch.
if (!_mux->select(_channel, /*force=*/true)) { _fault = Fault::Mux; return false; }
if (!_mux->deviceAcks(oledAddr())) { _fault = Fault::Device; return false; }
_fault = Fault::None;
return true;
}
uint8_t DrumDisplay::oledAddr() const {
return static_cast<uint8_t>(_oled->getU8x8()->i2c_address >> 1); // U8g2 stores the 8-bit (shifted) addr
}
// ── configure — auto-fit geometry, blank ───────────────────────────────────────
void DrumDisplay::configure() {
_oled->setFont(fontPtr());
_oled->setFontPosCenter();
fitGeometry(); // geometry from the U8G2 buffer dims — no I2C
_oled->clearBuffer();
if (i2cReachable()) { // blank the panel; skip + trip the breaker if it's absent at boot
_oled->sendBuffer();
#ifdef DRUMDISPLAY_TEST
_renderCount++;
#endif
}
}
// ── fitGeometry — descriptor mm + font + offset → px (replaces hardcoded consts) ─
void DrumDisplay::fitGeometry() {
const int W = _oled->getDisplayWidth();
const int H = _oled->getDisplayHeight();
int colW = lroundf(_r->digitWidthMm * PX_PER_MM);
int gap = lroundf(_r->interDigitGapMm * PX_PER_MM);
int grpG = lroundf(_r->groupGapMm * PX_PER_MM);
int cellH = lroundf(_r->digitHeightMm * PX_PER_MM);
int flagW = _r->flag.enabled ? lroundf(_r->flag.widthMm * PX_PER_MM) : 0;
// Build the ordered visual-cell list left→right: digits (leftmost = highest place),
// glyphs at their afterCol, the flag at its atVisualCol. extraGap[] carries group gaps.
uint8_t kinds[MAX_CELLS];
int16_t datas[MAX_CELLS];
int16_t widths[MAX_CELLS];
int16_t extraGap[MAX_CELLS];
uint8_t n = 0;
for (uint8_t c = 0; c <= _r->nDigits && n < MAX_CELLS; c++) {
for (uint8_t g = 0; g < _r->nGlyphs && n < MAX_CELLS; g++) {
if (_r->glyphs[g].afterCol == c) {
kinds[n] = KIND_GLYPH;
datas[n] = static_cast<int16_t>(g);
widths[n] = lroundf(_r->glyphs[g].widthMm * PX_PER_MM);
extraGap[n] = 0;
n++;
}
}
if (_r->flag.enabled && _r->flag.atVisualCol == c && n < MAX_CELLS) {
kinds[n] = KIND_FLAG;
datas[n] = 0;
widths[n] = static_cast<int16_t>(flagW);
extraGap[n] = 0;
n++;
}
if (c < _r->nDigits && n < MAX_CELLS) {
kinds[n] = KIND_DIGIT;
datas[n] = static_cast<int16_t>(_r->nDigits - 1 - c); // visual L→R, leftmost = top place
widths[n] = static_cast<int16_t>(colW);
bool grpBoundary = (_r->groupSize > 0 && c > 0 && (c % _r->groupSize) == 0);
extraGap[n] = grpBoundary ? static_cast<int16_t>(grpG) : 0;
n++;
}
}
// Total laid-out width: cells + inter-cell gaps + group gaps.
int totalW = 0;
for (uint8_t i = 0; i < n; i++) {
totalW += widths[i];
if (i > 0) totalW += gap;
totalW += extraGap[i];
}
// Auto-shrink if the row is wider than the panel (the prototype's "won't fit" note, real).
if (totalW > W && totalW > 0) {
float k = static_cast<float>(W) / static_cast<float>(totalW);
colW = static_cast<int>(floorf(colW * k));
gap = static_cast<int>(floorf(gap * k));
flagW = static_cast<int>(floorf(flagW * k));
for (uint8_t i = 0; i < n; i++) {
widths[i] = static_cast<int16_t>(floorf(widths[i] * k));
extraGap[i] = static_cast<int16_t>(floorf(extraGap[i] * k));
}
totalW = 0;
for (uint8_t i = 0; i < n; i++) {
totalW += widths[i];
if (i > 0) totalW += gap;
totalW += extraGap[i];
}
}
if (cellH > H) cellH = H; // clamp roll window to short panels (128x32)
int x0 = (W - totalW) / 2 + lroundf(_xOffMm * PX_PER_MM); // centre the row, then apply the mm offset
_cy = static_cast<int16_t>(H / 2 + lroundf(_yOffMm * PX_PER_MM));
int x = x0;
for (uint8_t i = 0; i < n; i++) {
if (i > 0) x += gap;
x += extraGap[i];
_cellX[i] = static_cast<int16_t>(x);
_cellW[i] = widths[i];
_cellKind[i] = kinds[i];
_cellData[i] = datas[i];
x += widths[i];
}
_nCells = n;
_colW = static_cast<int16_t>(colW);
_cellH = static_cast<int16_t>(cellH);
_gap = static_cast<int16_t>(gap);
_flagW = static_cast<int16_t>(flagW);
_geomDirty = false;
}
// ── ported renderers (cell width passed in, not a global) ──────────────────────
void DrumDisplay::drawTape(int16_t cx, float p, int16_t w) {
long c = lroundf(p);
for (long k = c - 1; k <= c + 1; k++) {
int glyph = static_cast<int>(((k % 10) + 10) % 10);
char s[2] = { static_cast<char>('0' + glyph), 0 };
int gx = cx + (w - static_cast<int>(_oled->getStrWidth(s))) / 2;
int y = _cy + static_cast<int>(lroundf((static_cast<float>(k) - p) * _cellH));
_oled->drawStr(gx, y, s);
}
}
void DrumDisplay::drawFlag(int16_t cx, float p, int16_t w) {
int nF = static_cast<int>(strlen(_r->flag.faces));
if (nF < 1) return;
long c = lroundf(p);
for (long k = c - 1; k <= c + 1; k++) {
int idx = static_cast<int>(((k % nF) + nF) % nF);
char s[2] = { _r->flag.faces[idx], 0 };
int gx = cx + (w - static_cast<int>(_oled->getStrWidth(s))) / 2;
int y = _cy + static_cast<int>(lroundf((static_cast<float>(k) - p) * _cellH));
_oled->drawStr(gx, y, s);
}
}
// ── update — frame gate + idle skip + ease/snap + render ───────────────────────
void DrumDisplay::update() {
if (!_hasState) return; // nothing received yet → stay blank
uint32_t now = millis();
if (now - _lastFrameMs < FRAME_MS) return; // ~60 fps gate
if (settled() && !_dirty && !_geomDirty) return; // idle skip: no I2C when nothing moves
// (_geomDirty forces a frame after setOffset/setFontSize)
_lastFrameMs = now;
// Skip the render if the panel/mux is unreachable — keeps _dirty + tape positions, so the next
// reachable frame catches up to the live value (no stale freeze). Probes ~every 2 s while dead.
if (!i2cReachable()) return;
if (_geomDirty) {
_oled->setFont(fontPtr());
_oled->setFontPosCenter();
fitGeometry();
}
// Ease each place toward target/10^place, with SNAP_SETTLE jump handling.
long place = 1;
for (uint8_t i = 0; i < _r->nDigits; i++) {
float step = static_cast<float>(_target / place);
if (_r->scroll == DrumScroll::SNAP_SETTLE && fabsf(step - _pos[i]) > _r->snapThreshold) {
_pos[i] = step - copysignf(SNAP_LANDING, step - _pos[i]); // land just shy, same direction
}
_pos[i] += (step - _pos[i]) * EASE;
place *= 10;
}
if (_r->flag.enabled) {
float ft = static_cast<float>(_flagTarget);
if (_r->scroll == DrumScroll::SNAP_SETTLE && fabsf(ft - _flagPos) > 1.0f) {
_flagPos = ft - copysignf(0.9f, ft - _flagPos);
}
_flagPos += (ft - _flagPos) * EASE;
}
_dirty = false;
// Render (select mux first; full-buffer; per-cell clip; one sendBuffer).
if (_mux) _mux->select(_channel);
_oled->clearBuffer();
_oled->setFont(fontPtr());
_oled->setFontPosCenter();
for (uint8_t ci = 0; ci < _nCells; ci++) {
int16_t cx = _cellX[ci];
int16_t w = _cellW[ci];
_oled->setClipWindow(cx, _cy - _cellH / 2, cx + w, _cy + _cellH / 2);
if (_cellKind[ci] == KIND_DIGIT) {
drawTape(cx, _pos[_cellData[ci]], w);
} else if (_cellKind[ci] == KIND_GLYPH) {
const DrumGlyph& g = _r->glyphs[_cellData[ci]];
char s[2] = { g.ch, 0 };
int gx = cx + (w - static_cast<int>(_oled->getStrWidth(s))) / 2;
_oled->drawStr(gx, _cy, s); // static glyph: centred, no tape
} else { // KIND_FLAG
drawFlag(cx, _flagPos, w);
}
}
_oled->setMaxClipWindow();
#ifdef DRUMDISPLAY_TEST
_renderCount++;
#endif
_oled->sendBuffer(); // the one expensive I2C op
}
// ── runtime setters ────────────────────────────────────────────────────────────
void DrumDisplay::setFontSize(DrumFont font) {
_font = font;
_geomDirty = true;
}
void DrumDisplay::setOffset(float xOffsetMm, float yOffsetMm) {
_xOffMm = xOffsetMm;
_yOffMm = yOffsetMm;
_geomDirty = true;
}
// ── small helpers ──────────────────────────────────────────────────────────────
bool DrumDisplay::settled() const {
long place = 1;
for (uint8_t i = 0; i < _r->nDigits; i++) {
if (fabsf(static_cast<float>(_target / place) - _pos[i]) > SETTLE_EPS) return false;
place *= 10;
}
if (_r->flag.enabled && fabsf(static_cast<float>(_flagTarget) - _flagPos) > SETTLE_EPS) return false;
return true;
}
const uint8_t* DrumDisplay::fontPtr() const {
return _font == DrumFont::LARGE ? u8g2_font_profont29_mr : u8g2_font_profont22_mr;
}
#ifdef DRUMDISPLAY_TEST
int16_t DrumDisplay::debugRowWidth() const {
if (_nCells == 0) return 0;
return static_cast<int16_t>(_cellX[_nCells - 1] + _cellW[_nCells - 1] - _cellX[0]);
}
#endif
} // namespace OpenSkyhawk
#endif // ARDUINO_ARCH_STM32