Full T-echo card support + Compact UI (#11342)

* T-echo card

* Update NRF52I2SOutput.cpp

* Update NRF52I2SOutput.h

* cleanup

* Update buzz.cpp

* use consistent runtime compact-panel check instead of mixing with compile-time macro

* Update NodeDB.cpp

* Update ExternalNotificationModule.cpp

* switched to Throttle::isWithinTimespanMs

* Update SharedUIDisplay.h

* trunk fix

* last cleanup

* ClockRenderer.cpp for OLED_COMPACT_UI and setup Unit C6L for new UI.

* Fixed regressions in standard OLED and TFT

---------

Co-authored-by: Jason P <applewiz@mac.com>
This commit is contained in:
HarukiToreda
2026-08-04 14:27:35 +00:00
committed by GitHub
co-authored by Jason P
parent d89f3eb8fc
commit 39e7aa6a6c
26 changed files with 1231 additions and 186 deletions
+84
View File
@@ -0,0 +1,84 @@
#include "NRF52I2SOutput.h"
#if defined(HAS_I2S_SPEAKER_NRF52)
#include <nrf_i2s.h>
NRF52I2SOutput nrf52I2SOutput;
bool NRF52I2SOutput::begin(uint8_t sckPin, uint8_t wsPin, uint8_t sdPin)
{
nrf_i2s_pins_set(NRF_I2S, sckPin, wsPin, NRF_I2S_PIN_NOT_CONNECTED, sdPin, NRF_I2S_PIN_NOT_CONNECTED);
bool ok = nrf_i2s_configure(NRF_I2S, NRF_I2S_MODE_MASTER, NRF_I2S_FORMAT_I2S, NRF_I2S_ALIGN_LEFT, NRF_I2S_SWIDTH_16BIT,
NRF_I2S_CHANNELS_STEREO, NRF_I2S_MCK_32MDIV8, NRF_I2S_RATIO_256X);
started = ok;
return ok;
}
void NRF52I2SOutput::fillBuffer(uint32_t frequencyHz)
{
size_t samplesPerCycle = kSampleRateHz / frequencyHz;
if (samplesPerCycle < 4)
samplesPerCycle = 4;
if (samplesPerCycle > kMaxSamples)
samplesPerCycle = kMaxSamples;
// Square wave, same value in both stereo halves (works regardless of SD_MODE strap).
const int16_t amplitude = 12000; // headroom below full-scale
const size_t half = samplesPerCycle / 2;
for (size_t i = 0; i < samplesPerCycle; i++) {
int16_t sample = (i < half) ? amplitude : (int16_t)-amplitude;
buffer[i] = ((uint32_t)(uint16_t)sample << 16) | (uint16_t)sample;
}
// Buffer pointer is never updated after START, so EasyDMA just keeps replaying it.
nrf_i2s_transfer_set(NRF_I2S, (uint16_t)samplesPerCycle, NULL, buffer);
}
void NRF52I2SOutput::playTone(uint32_t frequencyHz, uint32_t durationMs)
{
if (!started || durationMs == 0)
return;
if (frequencyHz <= 1) {
// Rest note.
delay(durationMs);
return;
}
fillBuffer(frequencyHz);
nrf_i2s_enable(NRF_I2S);
nrf_i2s_task_trigger(NRF_I2S, NRF_I2S_TASK_START);
delay(durationMs);
nrf_i2s_task_trigger(NRF_I2S, NRF_I2S_TASK_STOP);
nrf_i2s_disable(NRF_I2S);
}
void NRF52I2SOutput::startTone(uint32_t frequencyHz)
{
if (!started)
return;
stopTone();
if (frequencyHz <= 1)
return;
fillBuffer(frequencyHz);
nrf_i2s_enable(NRF_I2S);
nrf_i2s_task_trigger(NRF_I2S, NRF_I2S_TASK_START);
}
void NRF52I2SOutput::stopTone()
{
nrf_i2s_task_trigger(NRF_I2S, NRF_I2S_TASK_STOP);
nrf_i2s_disable(NRF_I2S);
}
void NRF52I2SOutput::end()
{
stopTone();
started = false;
}
#endif // HAS_I2S_SPEAKER_NRF52
+33
View File
@@ -0,0 +1,33 @@
#pragma once
// nRF52840 I2S tone driver. Drives NRF_I2S registers directly via nrf_i2s.h
// (the framework has no nrfx_i2s.c driver, only the register HAL).
// Plays repeating square-wave tones only - no RTTTL text parsing.
#include <Arduino.h>
class NRF52I2SOutput
{
public:
bool begin(uint8_t sckPin, uint8_t wsPin, uint8_t sdPin);
// frequencyHz <= 1 = silent rest.
void playTone(uint32_t frequencyHz, uint32_t durationMs);
// Non-blocking variants for callers that advance timing themselves (e.g. RTTTL player).
void startTone(uint32_t frequencyHz);
void stopTone();
void end();
private:
void fillBuffer(uint32_t frequencyHz);
static constexpr uint32_t kSampleRateHz = 15625; // 32MHz/8/256
static constexpr size_t kMaxSamples = 512;
uint32_t buffer[kMaxSamples] = {};
bool started = false;
};
extern NRF52I2SOutput nrf52I2SOutput;
+184
View File
@@ -0,0 +1,184 @@
#include "NRF52RtttlPlayer.h"
#if defined(HAS_I2S_SPEAKER_NRF52)
#include "NRF52I2SOutput.h"
#include "mesh/Throttle.h"
#include <cctype>
NRF52RtttlPlayer nrf52RtttlPlayer;
namespace
{
// Index 0 unused, then C4..B4, C5..B5, C6..B6, C7..B7 (48 notes) - same layout as
// the NonBlockingRTTTL library's table, indexed as [(octave - 4) * 12 + note].
const int kNoteFreq[] = {
0, 262, 277, 294, 311, 330, 349, 370, 392, 415, 440, 466, 494, 523, 554, 587, 622,
659, 698, 740, 784, 831, 880, 932, 988, 1047, 1109, 1175, 1245, 1319, 1397, 1480, 1568, 1661,
1760, 1865, 1976, 2093, 2217, 2349, 2489, 2637, 2794, 2960, 3136, 3322, 3520, 3729, 3951,
};
} // namespace
void NRF52RtttlPlayer::begin(const char *songBuffer, uint8_t loopCountIn, unsigned long loopGapMs)
{
buffer = songBuffer;
defaultDuration = 4;
defaultOctave = 6;
bpm = 63;
playing = true;
noteStartAt = millis();
noteDurationMs = 0;
loopCount = loopCountIn;
loopGap = loopGapMs;
pinMode(SPEAKER_EN, OUTPUT);
digitalWrite(SPEAKER_EN, HIGH);
#if defined(SPEAKER_EN_2)
pinMode(SPEAKER_EN_2, OUTPUT);
digitalWrite(SPEAKER_EN_2, HIGH);
#endif
nrf52I2SOutput.begin(SPEAKER_BCLK, SPEAKER_WS_LRCK, SPEAKER_DATA);
nrf52I2SOutput.stopTone();
// format: name:d=N,o=N,b=NNN:notes...
while (*buffer != ':' && *buffer != '\0')
buffer++;
if (*buffer == ':')
buffer++;
int num;
if (*buffer == 'd') {
buffer += 2; // skip "d="
num = 0;
while (isdigit((unsigned char)*buffer))
num = (num * 10) + (*buffer++ - '0');
if (num > 0)
defaultDuration = num;
buffer++; // skip comma
}
if (*buffer == 'o') {
buffer += 2; // skip "o="
num = *buffer++ - '0';
if (num >= 3 && num <= 7)
defaultOctave = num;
buffer++; // skip comma
}
if (*buffer == 'b') {
buffer += 2; // skip "b="
num = 0;
while (isdigit((unsigned char)*buffer))
num = (num * 10) + (*buffer++ - '0');
bpm = num;
buffer++; // skip colon
}
if (bpm <= 0)
bpm = 63;
wholeNoteMs = (60 * 1000L / bpm) * 4;
firstNote = buffer;
}
void NRF52RtttlPlayer::nextNote()
{
nrf52I2SOutput.stopTone();
int num = 0;
while (isdigit((unsigned char)*buffer))
num = (num * 10) + (*buffer++ - '0');
long duration = num ? (wholeNoteMs / num) : (wholeNoteMs / defaultDuration);
int note = 0;
switch (*buffer) {
case 'c':
note = 1;
break;
case 'd':
note = 3;
break;
case 'e':
note = 5;
break;
case 'f':
note = 6;
break;
case 'g':
note = 8;
break;
case 'a':
note = 10;
break;
case 'b':
note = 12;
break;
case 'p':
default:
note = 0;
break;
}
buffer++;
if (*buffer == '#') {
note++;
buffer++;
}
if (*buffer == '.') {
duration += duration / 2;
buffer++;
}
int scale;
if (isdigit((unsigned char)*buffer)) {
scale = *buffer - '0';
buffer++;
} else {
scale = defaultOctave;
}
if (*buffer == '.') {
duration += duration / 2;
buffer++;
}
if (*buffer == ',')
buffer++;
if (note && scale >= 4 && scale <= 7) {
nrf52I2SOutput.startTone(kNoteFreq[(scale - 4) * 12 + note]);
}
noteStartAt = millis();
noteDurationMs = (uint32_t)duration;
}
void NRF52RtttlPlayer::play()
{
if (!playing)
return;
if (Throttle::isWithinTimespanMs(noteStartAt, noteDurationMs))
return;
if (*buffer == '\0') {
if (--loopCount) {
noteStartAt = millis();
noteDurationMs = (uint32_t)loopGap;
buffer = firstNote;
} else {
stop();
}
return;
}
nextNote();
}
void NRF52RtttlPlayer::stop()
{
if (playing) {
nrf52I2SOutput.stopTone();
digitalWrite(SPEAKER_EN, LOW);
#if defined(SPEAKER_EN_2)
digitalWrite(SPEAKER_EN_2, LOW);
#endif
playing = false;
}
}
#endif // HAS_I2S_SPEAKER_NRF52
+33
View File
@@ -0,0 +1,33 @@
#pragma once
// Non-blocking RTTTL player for the nRF52 I2S speaker path (NRF52I2SOutput.h).
// Same parse algorithm as the NonBlockingRTTTL library, but drives
// NRF52I2SOutput::startTone/stopTone instead of tone()/noTone().
#include <Arduino.h>
class NRF52RtttlPlayer
{
public:
void begin(const char *songBuffer, uint8_t loopCount = 1, unsigned long loopGapMs = 1000);
void play(); // call every tick
void stop();
bool isPlaying() const { return playing; }
private:
void nextNote();
const char *buffer = "";
const char *firstNote = "";
uint8_t defaultDuration = 4;
uint8_t defaultOctave = 6;
int bpm = 63;
long wholeNoteMs = 0;
uint32_t noteStartAt = 0;
uint32_t noteDurationMs = 0;
unsigned long loopGap = 1000;
uint8_t loopCount = 1;
bool playing = false;
};
extern NRF52RtttlPlayer nrf52RtttlPlayer;