VersaMCU/src/CButton.cpp
2026-03-29 14:47:13 +02:00

281 lines
10 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 2050ms 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 <Arduino.h>
#include "CButton.h"
#include "hal/ws2812.h"
// ── Hue → RGB (vollständig gesättigte Farbe, S=255, V=255) ───────────────────
//
// Hue 0255 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); // 0252, Annäherung an 0255
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<uint8_t>(m_led_index), c.r, c.g, c.b);
m_dirty = false;
return true;
}