// CButton.cpp // Implementierung der CButton-Klasse. // // Animations-Arithmetik: // Alle Berechnungen in Integer (kein float) – Cortex-M0+ hat keine FPU. // Helligkeits-Skalierung: (Kanalwert * scale) / 255 // PULSE nutzt ein lineares Dreieck (0→255→0) statt Sinus – einfacher, trotzdem // weich genug für den visuellen Eindruck bei 20–50ms Loop-Rate. // // COLOR_CYCLE – Hue-Sweep: // Teilt den Farbraum in 6 Segmente (R→Y→G→C→B→M→R), je 43 Hue-Einheiten. // Innerhalb jedes Segments steigt/fällt ein Kanal linear – kein HSV-Float nötig. // // COLOR_FADE – Crossfade: // Lineare RGB-Interpolation zwischen m_anim_from und m_anim_to. // t = elapsed * 255 / period (0→255); Ergebnis: from + (to-from)*t/255. #include #include "CButton.h" #include "hal/ws2812.h" // ── Hue → RGB (vollständig gesättigte Farbe, S=255, V=255) ─────────────────── // // Hue 0–255 wird auf 6 Segmente à 43 Einheiten aufgeteilt: // Seg 0: R=255, G steigt, B=0 (Rot → Gelb) // Seg 1: R fällt, G=255, B=0 (Gelb → Grün) // Seg 2: R=0, G=255, B steigt (Grün → Cyan) // Seg 3: R=0, G fällt, B=255 (Cyan → Blau) // Seg 4: R steigt, G=0, B=255 (Blau → Magenta) // Seg 5: R=255, G=0, B fällt (Magenta → Rot) static RGB hue_to_rgb(uint8_t h) { uint8_t seg = h / 43; uint8_t frac = (uint8_t)((h % 43) * 6); // 0–252, Annäherung an 0–255 switch (seg) { case 0: return RGB(255, frac, 0); case 1: return RGB(255 - frac, 255, 0); case 2: return RGB(0, 255, frac); case 3: return RGB(0, 255 - frac, 255); case 4: return RGB(frac, 0, 255); default: return RGB(255, 0, 255 - frac); } } // ── Konstruktor ─────────────────────────────────────────────────────────────── CButton::CButton() : m_key_id(0) , m_led_index(-1) , m_action{ActionType::NONE, 0} , m_base() , m_override() , m_override_active(false) , m_dirty(false) , m_anim(LEDAnim::STATIC) , m_anim_period_ms(500) , m_anim_start_ms(0) , m_anim_from() , m_anim_to() {} // ── Initialisierung ─────────────────────────────────────────────────────────── void CButton::init(uint8_t key_id, int8_t led_index, SAction action, RGB base) { m_key_id = key_id; m_led_index = led_index; m_action = action; m_base = base; m_override_active = false; m_anim = LEDAnim::STATIC; m_dirty = true; // Initialen Zustand beim ersten render_led() schreiben } // ── Tastendruck-Hooks ───────────────────────────────────────────────────────── void CButton::on_press() { // Reserviert für zukünftige Button-Logik (Hold, Toggle, Doppelklick …) } void CButton::on_release() { // Reserviert für zukünftige Button-Logik } // ── LED Layer 1: base ───────────────────────────────────────────────────────── void CButton::set_base(RGB color) { m_base = color; m_dirty = true; } // ── LED Layer 2: override ───────────────────────────────────────────────────── void CButton::set_override(RGB color) { m_override = color; m_override_active = true; m_dirty = true; } void CButton::clear_override() { m_override_active = false; m_dirty = true; // Base-Farbe muss neu in WS2812-Buffer geschrieben werden } // ── LED-Animation ───────────────────────────────────────────────────────────── void CButton::set_anim(LEDAnim anim, uint16_t period_ms, uint16_t phase_offset_ms) { m_anim = anim; m_anim_period_ms = (period_ms > 0) ? period_ms : 1; // Division durch 0 vermeiden // phase_offset_ms in die Vergangenheit zurücksetzen → verschobener Startpunkt m_anim_start_ms = millis() - phase_offset_ms; m_dirty = true; } void CButton::set_color_fade(RGB to, uint16_t period_ms) { // Startfarbe = Snapshot der aktuell aktiven Farbe (override > base) m_anim_from = m_override_active ? m_override : m_base; m_anim_to = to; m_anim = LEDAnim::COLOR_FADE; m_anim_period_ms = (period_ms > 0) ? period_ms : 1; m_anim_start_ms = millis(); m_dirty = true; } void CButton::clear_anim() { m_anim = LEDAnim::STATIC; m_dirty = true; // Sofort wieder volle Helligkeit rendern } // ── Animations-Hilfsmethoden ────────────────────────────────────────────────── // Gibt true zurück solange die Animation noch läuft. // BLINK, PULSE, COLOR_CYCLE laufen endlos; FADE_*/COLOR_FADE enden nach period_ms. bool CButton::is_animating() const { if (m_anim == LEDAnim::STATIC) return false; if (m_anim == LEDAnim::BLINK || m_anim == LEDAnim::PULSE || m_anim == LEDAnim::COLOR_CYCLE) return true; // FADE_IN / FADE_OUT / COLOR_FADE: läuft bis elapsed >= period return (millis() - m_anim_start_ms) < (uint32_t)m_anim_period_ms; } // Berechnet den aktuellen Helligkeits-Skalierungsfaktor (0 = aus, 255 = voll). // Darf m_anim und m_dirty verändern (Abschluss von FADE_IN/FADE_OUT). uint8_t CButton::compute_scale() { if (m_anim == LEDAnim::STATIC) return 255; uint32_t elapsed = millis() - m_anim_start_ms; uint32_t period = (uint32_t)m_anim_period_ms; switch (m_anim) { case LEDAnim::BLINK: { // An für erste Halbperiode, aus für zweite uint32_t t = elapsed % period; return (t < period / 2) ? 255 : 0; } case LEDAnim::PULSE: { // Lineares Dreieck: 0 → 255 → 0 in einer Periode uint32_t t = elapsed % period; uint32_t half = period / 2; if (t < half) return (uint8_t)((t * 255) / half); else return (uint8_t)(((period - t) * 255) / half); } case LEDAnim::FADE_IN: { // Einmalig: schwarz → volle Helligkeit if (elapsed >= period) { // Animation abgeschlossen – ab jetzt STATIC bei voller Helligkeit m_anim = LEDAnim::STATIC; m_dirty = true; return 255; } return (uint8_t)((elapsed * 255) / period); } case LEDAnim::FADE_OUT: { // Einmalig: volle Helligkeit → schwarz if (elapsed >= period) { // Animation abgeschlossen – LED bleibt aus. // set_base() oder set_override() bringt sie zurück. m_anim = LEDAnim::STATIC; m_base = RGB(0, 0, 0); // Base auf schwarz setzen damit LED dunkel bleibt m_dirty = true; return 0; } return (uint8_t)(((period - elapsed) * 255) / period); } default: return 255; } } // Berechnet die finale RGB-Farbe für alle Animationstypen. // Helligkeits-Animationen (STATIC/BLINK/PULSE/FADE_*): // Wendet compute_scale() auf die aktive Farbe (override > base) an. // Farb-Animationen (COLOR_CYCLE/COLOR_FADE): // Berechnet die Farbe direkt; base/override werden nicht verändert. RGB CButton::compute_rgb() { switch (m_anim) { case LEDAnim::COLOR_CYCLE: { // Hue 0→255 über eine Periode, dann Wiederholung; 40% Helligkeit (102/255) uint32_t elapsed = millis() - m_anim_start_ms; uint32_t period = (uint32_t)m_anim_period_ms; uint8_t hue = (uint8_t)((elapsed % period) * 255 / period); RGB c = hue_to_rgb(hue); return RGB( (uint8_t)(((uint16_t)c.r * 102) / 255), (uint8_t)(((uint16_t)c.g * 102) / 255), (uint8_t)(((uint16_t)c.b * 102) / 255) ); } case LEDAnim::COLOR_FADE: { // Linearer Crossfade von m_anim_from nach m_anim_to uint32_t elapsed = millis() - m_anim_start_ms; uint32_t period = (uint32_t)m_anim_period_ms; if (elapsed >= period) { // Abgeschlossen – base auf Zielfarbe setzen, ab jetzt STATIC m_anim = LEDAnim::STATIC; m_base = m_anim_to; m_dirty = true; return m_anim_to; } uint8_t t = (uint8_t)((elapsed * 255) / period); // 0→255 int16_t dr = (int16_t)m_anim_to.r - (int16_t)m_anim_from.r; int16_t dg = (int16_t)m_anim_to.g - (int16_t)m_anim_from.g; int16_t db = (int16_t)m_anim_to.b - (int16_t)m_anim_from.b; return RGB( (uint8_t)(m_anim_from.r + (dr * t) / 255), (uint8_t)(m_anim_from.g + (dg * t) / 255), (uint8_t)(m_anim_from.b + (db * t) / 255) ); } default: { // Helligkeits-Animation: compute_scale() × aktive Farbe const RGB& color = m_override_active ? m_override : m_base; uint8_t scale = compute_scale(); return RGB( (uint8_t)(((uint16_t)color.r * scale) / 255), (uint8_t)(((uint16_t)color.g * scale) / 255), (uint8_t)(((uint16_t)color.b * scale) / 255) ); } } } // ── Rendering ───────────────────────────────────────────────────────────────── bool CButton::render_led() { if (!has_led()) return false; bool animating = is_animating(); if (!m_dirty && !animating) return false; // Nichts zu tun RGB c = compute_rgb(); ws2812_set(static_cast(m_led_index), c.r, c.g, c.b); m_dirty = false; return true; }