Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions src/Config.h
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,25 @@
#ifndef BUZZER_KIND
#define BUZZER_KIND BUZZER_KIND_PWM
#endif
// Whether the sounder starts switched on, which is decided by what it costs.
// A piezo is a PWM channel and a timer — free, so it ships on and the node
// keeps chirping as it always did. A speaker behind an I2S amplifier is a task
// and a DMA ring, about 5.4 KB of internal RAM, which on the board that has one
// is a quarter of what it has spare and enough to stop the portal serving. That
// ships off, and turning it on is a choice made knowing the trade.
#ifndef SOUND_ENABLED_DEFAULT
#if BUZZER_KIND == BUZZER_KIND_I2S
#define SOUND_ENABLED_DEFAULT 0
#else
#define SOUND_ENABLED_DEFAULT 1
#endif
#endif
// Percent. Only the speaker can act on it: a piezo is driven at the one
// amplitude its PWM channel produces, so the setting is offered where it means
// something and refused where it does not.
Comment on lines +608 to +610
#ifndef SOUND_VOLUME_DEFAULT
#define SOUND_VOLUME_DEFAULT 40
#endif
// The I2S speaker's pins, meaningless on a PWM board.
#ifndef PIN_I2S_BCLK
#define PIN_I2S_BCLK -1
Expand Down
123 changes: 120 additions & 3 deletions src/sys/Buzzer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@

#include <Arduino.h>
#include <esp_timer.h>
#include "Settings.h"

#if BUZZER_KIND == BUZZER_KIND_I2S
#include <ESP_I2S.h>
Expand All @@ -60,7 +61,12 @@ constexpr uint32_t kRateHz = 8000;
// A quarter of full scale. The MAX98357A on the board this was written for
// straps its gain pin to ground, which is 12 dB, and full scale through that
// into a small speaker is startling rather than informative.
constexpr int16_t kAmplitude = 8000;
constexpr int16_t kAmplitudeFull = 8000;
// The amplitude actually used, from the volume setting. A quarter of full scale
// is the ceiling rather than the level: the MAX98357A straps its gain to ground,
// which is 12 dB, and full scale through that into a small speaker is startling
// rather than informative.
int16_t sAmplitude = kAmplitudeFull;
// Square rather than sine, deliberately: it is what the piezo this stands in
// for produces, it needs no floating point in the task that generates it, and
// the difference is inaudible in a 120 ms beep.
Expand Down Expand Up @@ -91,7 +97,7 @@ void play(uint32_t hz, uint32_t ms) {
while (left) {
const size_t n = left < kChunk ? left : kChunk;
for (size_t i = 0; i < n; i++) {
buf[i] = (halfPeriod && (phase / halfPeriod) % 2) ? kAmplitude : (int16_t)-kAmplitude;
buf[i] = (halfPeriod && (phase / halfPeriod) % 2) ? sAmplitude : (int16_t)-sAmplitude;
phase++;
}
sI2s.write((const uint8_t*)buf, n * sizeof(int16_t));
Expand All @@ -103,10 +109,22 @@ void play(uint32_t hz, uint32_t ms) {
sI2s.write((const uint8_t*)quiet, sizeof(quiet));
}

// Asked to stand down, and the acknowledgement. The task deletes itself rather
// than being deleted: it can be inside an I2S write when the switch is thrown,
// and killing a task mid-driver leaves the driver's state to chance. A sentinel
// note reaches it only between notes, which is exactly when it is safe to go.
volatile bool sStopping = false;
volatile bool sStopped = false;

void audioTask(void*) {
Note n;
for (;;) {
if (xQueueReceive(sQueue, &n, portMAX_DELAY) != pdTRUE) continue;
if (!n.hz && !n.ms) { // the sentinel: nothing to play, time to go
sStopped = true;
vTaskDelete(nullptr);
return;
}
play(n.hz, n.ms);
if (n.nextHz) play(n.nextHz, n.ms);
// Only now. Clearing it when the note was taken off the queue would open
Expand Down Expand Up @@ -194,7 +212,50 @@ bool start(uint32_t hz, uint32_t ms, uint32_t nextHz) {

namespace Buzzer {

namespace {

// Volume as an amplitude, clamped. Zero is silence rather than a click: a note
// of zero amplitude still costs the time it takes to play.
void setVolume(uint8_t percent) {
#if BUZZER_KIND == BUZZER_KIND_I2S
if (percent > 100) percent = 100;
sAmplitude = (int16_t)((int32_t)kAmplitudeFull * percent / 100);
#else
// A piezo on a PWM channel has one loudness: the pin swings rail to rail and
// the element is as loud as it is. Duty cycle changes the timbre rather than
// the level, and driving it quieter would mean driving it wrong. The setting
// is accepted and ignored here rather than hidden, so a fleet can carry one
// configuration across boards that answer it differently.
(void)percent;
#endif
}

bool sUp = false; // the hardware is taken up

void takeUp();
void release();

} // namespace

void begin() {
setVolume(settings.sound().volume);
apply();
}

// Match the hardware to the setting. Idempotent, so the settings layer can call
// it after every save without knowing whether anything changed.
void apply() {
setVolume(settings.sound().volume);
const bool want = settings.sound().enabled;
if (want == sUp) return;
if (want) takeUp(); else release();
}

bool present() { return sUp; }

namespace {

void takeUp() {
#if BUZZER_KIND == BUZZER_KIND_I2S
sI2s.setPins(PIN_I2S_BCLK, PIN_I2S_LRCLK, PIN_I2S_DOUT);
if (!sI2s.begin(I2S_MODE_STD, kRateHz, I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO)) {
Expand Down Expand Up @@ -229,10 +290,66 @@ void begin() {
.callback = step, .arg = nullptr,
.dispatch_method = ESP_TIMER_TASK, .name = "buzzer", .skip_unhandled_events = true,
};
if (esp_timer_create(&args, &sTimer) != ESP_OK) sTimer = nullptr;
if (esp_timer_create(&args, &sTimer) != ESP_OK) {
// Without the timer a note starts and never ends, so start() refuses and
// the sounder is silent. Give the pin back and stay down rather than
// report a sounder that cannot make a sound: sUp is what apply() compares
// the setting against, so claiming success here would also mean it never
// tried again — a transient failure would become a permanent one.
sTimer = nullptr;
ledcDetach(PIN_BUZZER);
log_w("buzzer: no timer for the sounder; it stays off and will be retried");
return;
}
#endif
sUp = true;
}

// Give the hardware back. On the speaker this is the whole reason the switch
// exists — the task's stack and the DMA ring are the cost, and a switch that
// only silenced them would save nothing.
void release() {
#if BUZZER_KIND == BUZZER_KIND_I2S
sReady = false; // refuse new notes before anything goes
if (sQueue) {
sStopping = true;
sStopped = false;
const Note bye{0, 0, 0};
// Room for it: a note in flight is at most a couple of hundred
// milliseconds, and the queue is one deep.
if (xQueueSend(sQueue, &bye, pdMS_TO_TICKS(500)) == pdTRUE) {
for (int i = 0; i < 100 && !sStopped; i++) vTaskDelay(pdMS_TO_TICKS(10));
}
if (!sStopped) {
// It did not go. Leave the queue and the driver alone rather than pull
// them out from under a task that is still running in them: the sounder
// stays up and the memory stays spent, which is the safe half of a bad
// pair.
log_w("buzzer: the audio task did not stand down; leaving the speaker up");
sStopping = false;
sReady = true;
return;
}
vQueueDelete(sQueue);
sQueue = nullptr;
}
sStopping = false;
sI2s.end();
portENTER_CRITICAL(&sBusyMux);
sBusy = false; // whatever was playing is not any more
portEXIT_CRITICAL(&sBusyMux);
log_i("buzzer: speaker released; its task and DMA are back");
#else
if (sTimer) { esp_timer_stop(sTimer); esp_timer_delete(sTimer); sTimer = nullptr; }
ledcWriteTone(PIN_BUZZER, 0);
ledcDetach(PIN_BUZZER);
sBusy = false;
#endif
sUp = false;
}

} // namespace

// Near a small piezo's resonance, short enough to be polite. Worth saying:
// the first bench V4 produced no sound at any drive or frequency while every
// other function worked, so its sounder is likely simply not fitted — the pin
Expand Down
15 changes: 14 additions & 1 deletion src/sys/Buzzer.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,21 @@

namespace Buzzer {

void begin(); // claims the pin; silent until asked
void begin(); // claims what the board needs, if the sounder is switched on
void boot(); // two short notes up: running
void message(); // one note: something arrived for you

// Take up or release the hardware to match the setting, without a restart.
// That is the point of the switch on the speaker boards: the task and the DMA
// ring are about 5.4 KB of internal RAM, a quarter of what one of these boards
// has spare and enough to stop the portal serving — so turning the sounder off
// has to give that back rather than merely stay quiet.
void apply();

// Whether the sounder is up and can be heard. False when the board has none,
// when it is switched off, and when it would not start.
bool present();

} // namespace Buzzer

#else
Expand All @@ -54,6 +65,8 @@ namespace Buzzer {
inline void begin() {}
inline void boot() {}
inline void message() {}
inline void apply() {}
inline bool present() { return false; }
} // namespace Buzzer

#endif // HAS_BUZZER
10 changes: 10 additions & 0 deletions src/sys/Settings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ void Settings::load() {
if (_prefs.isKey("w_stap")) _prefs.getString("w_stap", _wifi.staPassword, sizeof(_wifi.staPassword));

if (_prefs.isKey("a_pass")) _prefs.getString("a_pass", _admin.password, sizeof(_admin.password));
LOAD(_sound.enabled, "snd_en", getBool ("snd_en"));
LOAD(_sound.volume, "snd_vol", getUChar("snd_vol"));
LOAD(_display.touchWake, "d_twake", getBool ("d_twake"));
LOAD(_display.brightness, "d_bri", getUChar("d_bri"));
LOAD(_display.daylight, "d_day", getBool ("d_day"));
Expand Down Expand Up @@ -192,6 +194,14 @@ bool Settings::saveAdminPassword(const char* password) {
return ok;
}

bool Settings::saveSound(const SoundSettings& s) {
_sound = s;
bool ok = _prefs.putBool ("snd_en", s.enabled) > 0
&& _prefs.putUChar("snd_vol", s.volume) > 0;
if (!ok) log_e("NVS write failed (sound)");
return ok;
}

bool Settings::saveDisplay(const DisplaySettings& d) {
_display = d;
bool ok = _prefs.putBool("d_twake", d.touchWake) > 0;
Expand Down
10 changes: 10 additions & 0 deletions src/sys/Settings.h
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,13 @@ struct LinkSettings {
// What the glass does, on boards that have one worth configuring. Its own
// section because it will grow — brightness, sleep timing — and because a
// UI behaviour is neither radio nor maintenance.
// The sounder, which on one board is the difference between a portal that
// serves and one that cannot allocate (Buzzer.h).
struct SoundSettings {
bool enabled = SOUND_ENABLED_DEFAULT != 0;
uint8_t volume = SOUND_VOLUME_DEFAULT; // percent; the piezo cannot act on it
};

struct DisplaySettings {
// Whether a tap wakes a blanked panel. On by the phone convention; off for
// a pocketed device whose every accidental touch would light the glass.
Expand Down Expand Up @@ -205,6 +212,7 @@ class Settings {
const LinkSettings& links() const { return _links; }
const MaintenanceSettings& maintenance() const { return _maintenance; }
const DisplaySettings& display() const { return _display; }
const SoundSettings& sound() const { return _sound; }

bool saveRadio(const RadioSettings& r);
bool saveLinks(const LinkSettings& l);
Expand All @@ -213,6 +221,7 @@ class Settings {
bool saveAdminPassword(const char* password);
bool saveTransport(const TransportSettings& t);
bool saveDisplay(const DisplaySettings& d);
bool saveSound(const SoundSettings& s);
void factoryReset(); // wipes the namespace, restores defaults

// Valid LoRa bandwidths shared by SX126x and SX127x, in kHz.
Expand All @@ -229,6 +238,7 @@ class Settings {
LinkSettings _links;
MaintenanceSettings _maintenance;
DisplaySettings _display;
SoundSettings _sound;
};

extern Settings settings;
23 changes: 23 additions & 0 deletions src/sys/SettingsFields.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
// section the same way its HTTP handler does.
#include "SettingsFields.h"
#include "RnsTransport.h"
#include "Buzzer.h"

#include <Arduino.h>
#include <stdlib.h>
Expand Down Expand Up @@ -590,6 +591,28 @@ const Entry kFields[] = {
if (!Power::profileFromName(v, pp)) { snprintf(e, n, "power_profile must be performance|balanced|battery"); return Result::BadValue; }
TransportSettings t = settings.transport(); t.powerProfile = (uint8_t)pp; return commitTransport(t, e, n); } },

// --- sound ---------------------------------------------------------------
// The switch gives the hardware back rather than merely silencing it, which
// is why it is worth having at runtime: on a board whose sounder is a speaker
// the task and its DMA are about 5.4 KB of internal RAM, and that is the
// difference between a portal that serves and one that cannot allocate.
{ "sound.enabled",
[](char* o, size_t n) { snprintf(o, n, "%s", settings.sound().enabled ? "on" : "off"); },
[](const char* v, char* e, size_t n) { bool b;
if (!parseBool(v, b)) { snprintf(e, n, "expected on or off"); return Result::BadValue; }
SoundSettings s = settings.sound(); s.enabled = b;
if (!settings.saveSound(s)) { snprintf(e, n, "could not be saved"); return Result::NvsFailed; }
Buzzer::apply(); // takes the hardware up or gives it back, now
return Result::Ok; } },
{ "sound.volume",
[](char* o, size_t n) { snprintf(o, n, "%u", (unsigned)settings.sound().volume); },
[](const char* v, char* e, size_t n) { uint32_t u;
if (!parseU32Max(v, 100, u, e, n)) return Result::BadValue;
SoundSettings s = settings.sound(); s.volume = (uint8_t)u;
if (!settings.saveSound(s)) { snprintf(e, n, "could not be saved"); return Result::NvsFailed; }
Buzzer::apply();
return Result::Ok; } },

// --- admin -------------------------------------------------------------
{ "admin.password",
[](char* o, size_t n) { renderSecret(o, n, settings.admin().password); },
Expand Down
2 changes: 2 additions & 0 deletions src/ui/lvgl/UiSettings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ enum class Kind : uint8_t { Text, Secret, Number, Switch, Region, Words, Slider

Kind kindFor(const char* key, const char* value, bool quoted) {
if (strcmp(key, "display.brightness") == 0) return Kind::Slider;
// A level, not a figure to type: the useful gesture is "a bit louder".
if (strcmp(key, "sound.volume") == 0) return Kind::Slider;
if (strcmp(key, "radio.region") == 0) return Kind::Region;
if (strcmp(key, "wifi.security") == 0) return Kind::Words;
if (strcmp(key, "display.theme") == 0) return Kind::Words;
Expand Down