mirror of
https://github.com/alexhopeoconnor/firmware.git
synced 2026-10-17 09:01:30 +10:00
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:
@@ -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
|
||||
@@ -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;
|
||||
@@ -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
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user