Default fresh settings to sound off and vibration on, and keep existing saved choices. Apply changes immediately across all three UIs and make custom notification patterns respect the same mute controls. Keep the Wio Mini Companion within its existing flash boundary by storing its LED switch in a reserved preference byte and shortening CLI help without removing commands. Add coverage for live output changes, persistence, and storage failures. Adapted from https://github.com/meshcore-dev/MeshCore/pull/3603. Co-authored-by: R0ck <develop@hectospark.co.uk>
859 lines
40 KiB
Python
859 lines
40 KiB
Python
"""Run production spoken-button mappings, courtesy audio, and UI transactions natively."""
|
|
from pathlib import Path
|
|
import unittest
|
|
|
|
import test_gps_voice_buttons as gps_tests
|
|
from test_radio_receive_contract import method
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
PROMPTS = r'''
|
|
#include <cassert>
|
|
#include <initializer_list>
|
|
#define PIN_BUZZER 25
|
|
#define PIN_BUZZER_EN 37
|
|
#define MESH_BUTTON_AUDIO_HIL 1
|
|
#define HAS_DRV2605 1
|
|
#define MESH_GPS_VOICE 1
|
|
#include <helpers/ui/buzzer.cpp>
|
|
uint32_t now_ms=0;
|
|
int main(){
|
|
using namespace mesh::audio;
|
|
struct Mapping {ButtonVoicePrompt prompt;VoiceClip clip;const char* tone;};
|
|
const Mapping mappings[]={
|
|
{ButtonVoicePrompt::Ready,readyClip,nullptr},
|
|
{ButtonVoicePrompt::NotificationCleared,notificationClearedClip,nullptr},
|
|
{ButtonVoicePrompt::AdvertQueued,advertQueuedClip,nullptr},
|
|
{ButtonVoicePrompt::AdvertFailed,advertFailedClip,nullptr},
|
|
{ButtonVoicePrompt::SoundOn,soundOnClip,nullptr},
|
|
{ButtonVoicePrompt::SoundOff,soundOffClip,nullptr},
|
|
{ButtonVoicePrompt::GpsOn,gpsOnClip,"Startup:d=4,o=5,b=160:16c6,16e6,8g6"},
|
|
{ButtonVoicePrompt::GpsOff,gpsOffClip,"Shutdown:d=4,o=5,b=100:8g5,16e5,16c5"},
|
|
{ButtonVoicePrompt::ActionFailed,actionFailedClip,nullptr},
|
|
{ButtonVoicePrompt::UsbSetup,usbSetupClip,nullptr},
|
|
{ButtonVoicePrompt::ShuttingDown,shuttingDownClip,"Shutdown:d=4,o=5,b=100:8g5,16e5,16c5"},
|
|
{ButtonVoicePrompt::Restarting,restartingClip,"Shutdown:d=4,o=5,b=100:8g5,16e5,16c5"},
|
|
#ifdef HAS_DRV2605
|
|
{ButtonVoicePrompt::AlertsSoundVibration,alertsSoundVibrationClip,nullptr},
|
|
{ButtonVoicePrompt::AlertsSoundOnly,alertsSoundOnlyClip,nullptr},
|
|
{ButtonVoicePrompt::AlertsVibrationOnly,alertsVibrationOnlyClip,nullptr},
|
|
{ButtonVoicePrompt::AlertsSilent,alertsSilentClip,nullptr},
|
|
#endif
|
|
};
|
|
genericBuzzer buzzer;buzzer.begin();buzzer.quiet(false);
|
|
auto finish=[&]{
|
|
assert(!registers.ENABLE);
|
|
rtttl::finish();buzzer.loop();
|
|
now_ms+=99;buzzer.loop();assert(!registers.ENABLE);
|
|
++now_ms;buzzer.loop();
|
|
assert(HwPWM3.owned&&HwPWM3.pin&&HwPWM3.bound_pin==PIN_BUZZER);
|
|
unsigned sequence=0,events=0;
|
|
while(registers.ENABLE){
|
|
assert(++events<2000);
|
|
registers.EVENTS_SEQEND[sequence]=1;PWM3_IRQHandler();sequence^=1;
|
|
}
|
|
buzzer.loop();assert(!buzzer.isPlaying());
|
|
assert(!HwPWM3.owned&&!HwPWM3.pin&&!irq_enabled);
|
|
assert(pin_states[PIN_BUZZER]==LOW);
|
|
};
|
|
for(const auto& entry:mappings){
|
|
auto actual=buttonVoiceClip(entry.prompt);
|
|
assert(actual.data==entry.clip.data&&actual.bytes==entry.clip.bytes);
|
|
assert(actual.prefix==entry.clip.prefix&&actual.prefix_bytes==entry.clip.prefix_bytes);
|
|
assert(actual.samples==entry.clip.samples&&actual.sample_rate==entry.clip.sample_rate);
|
|
assert(actual.valid()&&actual.bytes+actual.prefix_bytes==(actual.samples+1)/2);
|
|
assert(actual.sample_rate==8000||actual.sample_rate==16000);
|
|
VoiceDecoder decoder;decoder.begin(actual);
|
|
unsigned count=0,nonzero=0;
|
|
while(!decoder.done()){nonzero+=decoder.next()!=0;++count;}
|
|
assert(count==actual.samples&&nonzero>count/2);
|
|
auto started=gps_voice.started(),completed=gps_voice.completed(),tones=rtttl::begins;
|
|
assert(buzzer.speakButton(entry.prompt)&&buzzer.isPlaying());
|
|
if(entry.tone){assert(rtttl::melody==entry.tone&&rtttl::begins==tones+1);}
|
|
else {assert(rtttl::begins==tones&&!rtttl::isPlaying());}
|
|
finish();
|
|
assert(gps_voice.started()==started+1&&gps_voice.completed()==completed+1);
|
|
}
|
|
// Invalid values must reject without preempting a valid tone or active voice.
|
|
const auto invalid=static_cast<ButtonVoicePrompt>(255);
|
|
assert(!buttonVoiceClip(invalid).data);
|
|
assert(buzzer.speakGps(true));
|
|
auto tones=rtttl::begins,started=gps_voice.started();
|
|
assert(!buzzer.speakButton(invalid));
|
|
assert(buzzer.isPlaying()&&rtttl::isPlaying()&&rtttl::begins==tones);
|
|
#ifndef HAS_DRV2605
|
|
for(auto prompt:{ButtonVoicePrompt::AlertsSoundVibration,ButtonVoicePrompt::AlertsSoundOnly,
|
|
ButtonVoicePrompt::AlertsVibrationOnly,ButtonVoicePrompt::AlertsSilent}){
|
|
assert(!buttonVoiceClip(prompt).data&&!buzzer.speakButton(prompt,true));
|
|
assert(buzzer.isPlaying()&&rtttl::isPlaying()&&rtttl::begins==tones);
|
|
}
|
|
#endif
|
|
finish();
|
|
assert(gps_voice.started()==started+1);
|
|
assert(buzzer.speakButton(ButtonVoicePrompt::Ready));
|
|
buzzer.loop();now_ms+=100;buzzer.loop();
|
|
assert(registers.ENABLE&&buzzer.isPlaying());
|
|
started=gps_voice.started();
|
|
assert(!buzzer.speakButton(invalid,true));
|
|
assert(registers.ENABLE&&gps_voice.started()==started);
|
|
#ifndef HAS_DRV2605
|
|
for(auto prompt:{ButtonVoicePrompt::AlertsSoundVibration,ButtonVoicePrompt::AlertsSoundOnly,
|
|
ButtonVoicePrompt::AlertsVibrationOnly,ButtonVoicePrompt::AlertsSilent}){
|
|
assert(!buzzer.speakButton(prompt,true));
|
|
assert(registers.ENABLE&&gps_voice.started()==started);
|
|
}
|
|
#endif
|
|
unsigned sequence=0,events=0;
|
|
while(registers.ENABLE){
|
|
assert(++events<2000);registers.EVENTS_SEQEND[sequence]=1;
|
|
PWM3_IRQHandler();sequence^=1;
|
|
}
|
|
buzzer.loop();assert(!buzzer.isPlaying());
|
|
// A busy PWM fails the deferred voice start, without leaking ownership.
|
|
HwPWM3.reject=true;
|
|
assert(buzzer.speakButton(ButtonVoicePrompt::Ready));
|
|
buzzer.loop();now_ms+=100;buzzer.loop();
|
|
assert(!buzzer.isPlaying()&&!registers.ENABLE&&!HwPWM3.owned&&!irq_enabled);
|
|
}
|
|
'''
|
|
|
|
COURTESY = r'''
|
|
#include <cassert>
|
|
#define PIN_BUZZER 25
|
|
#define PIN_BUZZER_EN 37
|
|
#define MESH_BUTTON_AUDIO_HIL 1
|
|
#define MESH_GPS_VOICE 1
|
|
#include <helpers/ui/buzzer.cpp>
|
|
uint32_t now_ms=0;
|
|
int main(){
|
|
genericBuzzer buzzer;buzzer.begin();buzzer.quiet(true);
|
|
assert(buzzer.isQuiet()&&pin_states[PIN_BUZZER_EN]==LOW);
|
|
auto before=gps_voice.started();
|
|
assert(buzzer.speakButton(ButtonVoicePrompt::Ready));
|
|
assert(!buzzer.isPlaying()&&gps_voice.started()==before);
|
|
assert(pin_states[PIN_BUZZER_EN]==LOW);
|
|
// The explicit sound-off confirmation is one-shot: it never unmutes alerts.
|
|
assert(buzzer.speakButton(ButtonVoicePrompt::SoundOff,true));
|
|
assert(buzzer.isQuiet()&&buzzer.isPlaying()&&pin_states[PIN_BUZZER_EN]==HIGH);
|
|
buzzer.loop();now_ms+=100;buzzer.loop();
|
|
assert(registers.ENABLE&&gps_voice.started()==before+1);
|
|
unsigned sequence=0,events=0;
|
|
while(registers.ENABLE){
|
|
assert(++events<2000);registers.EVENTS_SEQEND[sequence]=1;
|
|
PWM3_IRQHandler();sequence^=1;
|
|
}
|
|
assert(buzzer.isPlaying()); // IRQ cleanup is deferred to the foreground.
|
|
buzzer.loop();
|
|
assert(buzzer.isQuiet()&&!buzzer.isPlaying()&&pin_states[PIN_BUZZER_EN]==LOW);
|
|
assert(pin_states[PIN_BUZZER]==LOW&&!HwPWM3.owned&&!HwPWM3.pin&&!irq_enabled);
|
|
auto tones=rtttl::begins;
|
|
buzzer.play("normal:d=4,o=5,b=120:c");
|
|
assert(!buzzer.isPlaying()&&rtttl::begins==tones&&pin_states[PIN_BUZZER_EN]==LOW);
|
|
// Every cancellation path releases the courtesy output without changing mute.
|
|
assert(buzzer.speakButton(ButtonVoicePrompt::SoundOff,true));
|
|
buzzer.stop();assert(buzzer.isQuiet()&&!buzzer.isPlaying());
|
|
assert(pin_states[PIN_BUZZER_EN]==LOW&&pin_states[PIN_BUZZER]==LOW);
|
|
assert(buzzer.speakButton(ButtonVoicePrompt::SoundOff,true));
|
|
buzzer.quiet(true);assert(!buzzer.isPlaying()&&pin_states[PIN_BUZZER_EN]==LOW);
|
|
// Muted ordinary notifications must preserve queued and active courtesy voice.
|
|
assert(buzzer.speakButton(ButtonVoicePrompt::SoundOff,true));
|
|
before=gps_voice.started();auto cancelled=gps_voice.cancelled();tones=rtttl::begins;
|
|
buzzer.play("normal:d=4,o=5,b=120:c");
|
|
assert(buzzer.isQuiet()&&buzzer.isPlaying()&&pin_states[PIN_BUZZER_EN]==HIGH);
|
|
assert(!registers.ENABLE&&gps_voice.started()==before&&rtttl::begins==tones);
|
|
buzzer.loop();now_ms+=100;buzzer.loop();
|
|
assert(registers.ENABLE&&gps_voice.started()==before+1);
|
|
buzzer.play("normal:d=4,o=5,b=120:c");
|
|
assert(buzzer.isQuiet()&&buzzer.isPlaying()&®isters.ENABLE);
|
|
assert(gps_voice.cancelled()==cancelled&&rtttl::begins==tones&&pin_states[PIN_BUZZER_EN]==HIGH);
|
|
// Ordinary sound still preempts voice as soon as the user actually unmutes.
|
|
buzzer.quiet(false);buzzer.play("normal:d=4,o=5,b=120:c");
|
|
assert(!registers.ENABLE&&gps_voice.cancelled()==cancelled+1);
|
|
assert(rtttl::isPlaying()&&rtttl::begins==tones+1&&!HwPWM3.owned&&!irq_enabled);
|
|
rtttl::finish();buzzer.loop();assert(!buzzer.isPlaying());buzzer.quiet(true);
|
|
// Starting a courtesy direction tone also respects cancellation in its gap.
|
|
assert(buzzer.speakButton(ButtonVoicePrompt::GpsOff,true));
|
|
assert(buzzer.isQuiet()&&rtttl::isPlaying()&&pin_states[PIN_BUZZER_EN]==HIGH);
|
|
rtttl::finish();buzzer.loop();buzzer.stop();
|
|
now_ms+=1000;buzzer.loop();
|
|
assert(!buzzer.isPlaying()&&pin_states[PIN_BUZZER_EN]==LOW&&!registers.ENABLE);
|
|
// An unavailable player must restore the sleeping amplifier immediately.
|
|
HwPWM3.reject=true;
|
|
assert(buzzer.speakButton(ButtonVoicePrompt::SoundOff,true));
|
|
buzzer.loop();now_ms+=100;buzzer.loop();
|
|
assert(buzzer.isQuiet()&&!buzzer.isPlaying()&&pin_states[PIN_BUZZER_EN]==LOW);
|
|
}
|
|
'''
|
|
|
|
LEGACY_ORDINARY_PLAY = r'''
|
|
#include <cassert>
|
|
#define PIN_BUZZER 25
|
|
#define PIN_BUZZER_EN 37
|
|
#define MESH_GPS_VOICE 0
|
|
#include <helpers/ui/buzzer.cpp>
|
|
uint32_t now_ms=0;
|
|
int main(){
|
|
genericBuzzer buzzer;buzzer.begin();buzzer.quiet(true);
|
|
assert(buzzer.speakButton(ButtonVoicePrompt::GpsOff,true)&&rtttl::isPlaying());
|
|
auto tones=rtttl::begins;
|
|
// Non-voice builds retain the original cancel-before-quiet behavior.
|
|
buzzer.play("normal:d=4,o=5,b=120:c");
|
|
assert(buzzer.isQuiet()&&!buzzer.isPlaying()&&rtttl::begins==tones);
|
|
assert(pin_states[PIN_BUZZER_EN]==LOW);
|
|
buzzer.quiet(false);assert(buzzer.speakGps(true));tones=rtttl::begins;
|
|
buzzer.play("normal:d=4,o=5,b=120:c");
|
|
assert(!buzzer.isQuiet()&&buzzer.isPlaying()&&rtttl::begins==tones+1);
|
|
assert(rtttl::melody=="normal:d=4,o=5,b=120:c");
|
|
}
|
|
'''
|
|
|
|
UI_ACTIONS = r'''
|
|
#include <cassert>
|
|
#include <algorithm>
|
|
#include <string>
|
|
#include <vector>
|
|
#include <helpers/ui/ButtonVoice.h>
|
|
#include "examples/companion_radio/ui-orig/Button.h"
|
|
#define PIN_BUZZER 25
|
|
#define MESH_GPS_VOICE 1
|
|
#define ENV_INCLUDE_GPS 1
|
|
#define MESH_DEBUG_PRINTLN(...) ((void)0)
|
|
uint32_t now_ms=0;
|
|
static std::vector<std::string> events;
|
|
enum class UIEventType{contactMessage,channelMessage,ack,roomMessage,newContactMessage,none};
|
|
namespace mesh {
|
|
namespace notify {enum {Sound=1};}
|
|
namespace ui {
|
|
enum class DisplayWake {Button};
|
|
inline uint8_t radioProfileDisplayPageCount(bool,uint8_t){return 4;}
|
|
}
|
|
}
|
|
struct Display {
|
|
bool on=true;unsigned wakes=0;
|
|
bool isOn(){return on;}
|
|
void wake(mesh::ui::DisplayWake){++wakes;on=true;}
|
|
};
|
|
static uint8_t radioProfileStatusPageCount(Display&,bool){return 2;}
|
|
struct CompanionNodePrefs {uint8_t gps_enabled=0,buzzer_quiet=0;};
|
|
struct SensorManager {
|
|
bool enabled=false,available=true,reject=false;
|
|
bool setSettingValue(const char*,const char* value){
|
|
events.push_back("gps");
|
|
if(!available||reject)return false;
|
|
enabled=value[0]=='1';return true;
|
|
}
|
|
const char* getSettingByKey(const char*){return available?(enabled?"1":"0"):nullptr;}
|
|
};
|
|
static SensorManager sensors;
|
|
struct MyMesh {
|
|
CompanionNodePrefs _prefs;
|
|
bool save_ok=true,advert_ok=true,notification_active=false,output_muted=false,dual_radio=false;
|
|
unsigned saves=0,adverts=0,notification_buttons=0,mute_calls=0,cli_entries=0;
|
|
std::vector<bool> saved_quiet;
|
|
bool savePrefs(){++saves;events.push_back("save");saved_quiet.push_back(_prefs.buzzer_quiet);return save_ok;}
|
|
template<class T> bool savePreference(T& p,T v){T old=p;p=v;if(savePrefs())return true;p=old;return false;}
|
|
void applyGpsPrefs(){sensors.setSettingValue("gps",_prefs.gps_enabled?"1":"0");}
|
|
bool setGpsEnabled(bool);
|
|
bool advert(){++adverts;events.push_back("advert");return advert_ok;}
|
|
bool notificationButton(){++notification_buttons;bool cleared=notification_active;notification_active=false;return cleared;}
|
|
void setNotificationOutputMute(int,bool muted){++mute_calls;output_muted=muted;events.push_back("mute");}
|
|
bool isDualRadioActive(){return dual_radio;}
|
|
void enterCLIRescue(){++cli_entries;events.push_back("cli");}
|
|
};
|
|
static MyMesh the_mesh;
|
|
struct Buzzer {
|
|
bool muted=false,playing=false,accept=true;
|
|
unsigned plays=0,notification_plays=0,stops=0,notification_stops=0,shutdowns=0;
|
|
std::vector<ButtonVoicePrompt> prompts;
|
|
std::vector<bool> courtesy;
|
|
bool speakButton(ButtonVoicePrompt prompt,bool allowQuiet=false){
|
|
events.push_back("speak");prompts.push_back(prompt);courtesy.push_back(allowQuiet);
|
|
playing=accept&&(!muted||allowQuiet);return accept;
|
|
}
|
|
bool speakGps(bool value){return speakButton(value?ButtonVoicePrompt::GpsOn:ButtonVoicePrompt::GpsOff);}
|
|
void quiet(bool q){muted=q;events.push_back("quiet");}
|
|
bool isQuiet(){return muted;}
|
|
bool isPlaying(){return playing;}
|
|
void stop(){++stops;playing=false;events.push_back("stop");}
|
|
void stopNotification(){++notification_stops;playing=false;events.push_back("notification_stop");}
|
|
void shutdown(){++shutdowns;playing=!muted;}
|
|
void play(const char*){++plays;}
|
|
void playNotification(const char*){++notification_plays;}
|
|
void loop(){}
|
|
};
|
|
struct Board {
|
|
unsigned offs=0,reboots=0;
|
|
void powerOff(){++offs;events.push_back("off");}
|
|
void reboot(){++reboots;events.push_back("reboot");}
|
|
};
|
|
struct UITask {
|
|
Display* _display=nullptr;SensorManager* _sensors=&sensors;
|
|
CompanionNodePrefs* _node_prefs=&the_mesh._prefs;
|
|
Buzzer buzzer;Board board;Board* _board=&board;
|
|
char _alert[80]={},_origin[80]={},_msg[80]={};
|
|
bool _need_refresh=false,_displayWasOn=false,_button_notification_cleared=false;
|
|
bool _shutdown_pending=false,_shutdown_restart=false;
|
|
bool _startup_complete=false;
|
|
uint32_t _shutdown_started_at=0,ui_started_at=0;
|
|
uint8_t _radio_profile_display_page=0,_notification_outputs=mesh::notify::Sound;
|
|
bool guard_ok=true,guard_reject_drain=false;unsigned guards=0,preview_clears=0;
|
|
bool prepareForShutdown(){++guards;events.push_back("guard");return guard_ok&&(!guard_reject_drain||guards==1);}
|
|
bool shouldPlayMessageTone()const{return true;}
|
|
bool isPairingScreenActive()const{return false;}
|
|
void resetRadioProfileDisplayPage(){_radio_profile_display_page=0;}
|
|
void clearMsgPreview(){++preview_clears;_origin[0]=_msg[0]=0;}
|
|
void notify(UIEventType);void notificationMelody(const char*);
|
|
void handleButtonAnyPress();void handleButtonShortPress();
|
|
void handleButtonDoublePress();void handleButtonTriplePress();
|
|
void handleButtonQuadruplePress();void handleButtonLongPress();
|
|
void shutdown(bool restart=false);void servicePendingShutdown();
|
|
};
|
|
@METHODS@
|
|
int main(){
|
|
auto reset=[](){the_mesh=MyMesh{};sensors=SensorManager{};events.clear();now_ms=10000;};
|
|
auto last=[](const UITask& ui,ButtonVoicePrompt expected){assert(!ui.buzzer.prompts.empty()&&ui.buzzer.prompts.back()==expected);};
|
|
reset();UITask single;
|
|
single.handleButtonAnyPress();single.handleButtonShortPress();
|
|
last(single,ButtonVoicePrompt::Ready);
|
|
the_mesh.notification_active=true;
|
|
single.handleButtonAnyPress();single.handleButtonAnyPress(); // The first click really acknowledged it.
|
|
single.handleButtonShortPress();last(single,ButtonVoicePrompt::NotificationCleared);
|
|
single.handleButtonAnyPress();single.handleButtonShortPress();last(single,ButtonVoicePrompt::Ready);
|
|
assert(single.buzzer.prompts.size()==3);
|
|
// Multi-click actions consume the acknowledgement latch instead of leaking it to a later single.
|
|
the_mesh.notification_active=true;
|
|
single.handleButtonAnyPress();single.handleButtonDoublePress();last(single,ButtonVoicePrompt::AdvertQueued);
|
|
single.handleButtonAnyPress();single.handleButtonShortPress();last(single,ButtonVoicePrompt::Ready);
|
|
// An attached display retains its existing preview/wake behavior, with no Ready chatter.
|
|
Display display;single._display=&display;single._displayWasOn=true;
|
|
strcpy(single._origin,"sender");strcpy(single._msg,"message");
|
|
auto prompt_count=single.buzzer.prompts.size();
|
|
single.handleButtonAnyPress();single.handleButtonShortPress();
|
|
assert(single.preview_clears==1&&single.buzzer.prompts.size()==prompt_count&&display.wakes==1);
|
|
|
|
reset();UITask advert;
|
|
advert.handleButtonDoublePress();last(advert,ButtonVoicePrompt::AdvertQueued);
|
|
assert((events==std::vector<std::string>{"advert","speak"}));
|
|
the_mesh.advert_ok=false;events.clear();
|
|
advert.handleButtonDoublePress();last(advert,ButtonVoicePrompt::AdvertFailed);
|
|
assert(the_mesh.adverts==2&&strstr(advert._alert,"failed"));
|
|
assert((events==std::vector<std::string>{"advert","speak"}));
|
|
|
|
reset();UITask mute;
|
|
mute.handleButtonTriplePress();last(mute,ButtonVoicePrompt::SoundOff);
|
|
assert(mute.buzzer.muted&&the_mesh._prefs.buzzer_quiet&&the_mesh.output_muted);
|
|
assert(mute.buzzer.courtesy.back()&&mute.buzzer.playing);
|
|
assert((events==std::vector<std::string>{"save","quiet","mute","speak"}));
|
|
events.clear();mute.handleButtonTriplePress();last(mute,ButtonVoicePrompt::SoundOn);
|
|
assert(!mute.buzzer.muted&&!the_mesh._prefs.buzzer_quiet&&!the_mesh.output_muted);
|
|
assert(mute.buzzer.courtesy.back()&&the_mesh.saves==2&&the_mesh.mute_calls==2);
|
|
// Failed commits preserve runtime quiet, stored preference, and output routing separately.
|
|
for(bool old_quiet:{false,true}){
|
|
mute.buzzer.muted=old_quiet;the_mesh._prefs.buzzer_quiet=old_quiet;
|
|
the_mesh.output_muted=old_quiet;the_mesh.save_ok=false;events.clear();
|
|
auto mutes=the_mesh.mute_calls;
|
|
mute.handleButtonTriplePress();last(mute,ButtonVoicePrompt::ActionFailed);
|
|
assert(mute.buzzer.muted==old_quiet&&the_mesh._prefs.buzzer_quiet==old_quiet);
|
|
assert(the_mesh.output_muted==old_quiet&&the_mesh.mute_calls==mutes);
|
|
assert(mute.buzzer.courtesy.back()&&mute.buzzer.playing&&strstr(mute._alert,"failed"));
|
|
assert(std::find(events.begin(),events.end(),"mute")==events.end());
|
|
}
|
|
// Even an initially inconsistent runtime/preference pair is not rewritten on failure.
|
|
mute.buzzer.muted=false;the_mesh._prefs.buzzer_quiet=1;
|
|
mute.handleButtonTriplePress();assert(!mute.buzzer.muted&&the_mesh._prefs.buzzer_quiet==1);
|
|
|
|
reset();UITask gps;
|
|
#if ENV_INCLUDE_GPS == 1
|
|
gps.handleButtonQuadruplePress();last(gps,ButtonVoicePrompt::GpsOn);
|
|
assert(sensors.enabled&&the_mesh._prefs.gps_enabled&&the_mesh.saves==1);
|
|
assert((events==std::vector<std::string>{"gps","save","speak"}));
|
|
events.clear();gps.handleButtonQuadruplePress();last(gps,ButtonVoicePrompt::GpsOff);
|
|
assert(!sensors.enabled&&!the_mesh._prefs.gps_enabled&&the_mesh.saves==2);
|
|
the_mesh.save_ok=false;events.clear();
|
|
gps.handleButtonQuadruplePress();last(gps,ButtonVoicePrompt::ActionFailed);
|
|
assert(!sensors.enabled&&!the_mesh._prefs.gps_enabled&&strstr(gps._alert,"failed"));
|
|
assert((events==std::vector<std::string>{"gps","save","gps","speak"}));
|
|
the_mesh.save_ok=true;sensors.reject=true;
|
|
auto saves=the_mesh.saves;gps.handleButtonQuadruplePress();last(gps,ButtonVoicePrompt::ActionFailed);
|
|
assert(the_mesh.saves==saves&&!sensors.enabled&&!the_mesh._prefs.gps_enabled);
|
|
sensors.reject=false;sensors.available=false;
|
|
gps.handleButtonQuadruplePress();last(gps,ButtonVoicePrompt::ActionFailed);
|
|
assert(the_mesh.saves==saves);
|
|
#else
|
|
gps.handleButtonQuadruplePress();last(gps,ButtonVoicePrompt::ActionFailed);
|
|
assert(the_mesh.saves==0&&!sensors.enabled&&!the_mesh._prefs.gps_enabled);
|
|
#endif
|
|
gps._sensors=nullptr;gps.handleButtonQuadruplePress();last(gps,ButtonVoicePrompt::ActionFailed);
|
|
|
|
reset();UITask early;
|
|
now_ms=7999;early.handleButtonLongPress();last(early,ButtonVoicePrompt::UsbSetup);
|
|
assert(the_mesh.cli_entries==1&&early.guards==0&&!early._shutdown_pending);
|
|
assert((events==std::vector<std::string>{"cli","speak"}));
|
|
// Uptime arithmetic is unsigned even when boot began just before clock wrap.
|
|
early.ui_started_at=0xfffffff0;now_ms=7982;
|
|
early.handleButtonLongPress();last(early,ButtonVoicePrompt::UsbSetup);
|
|
assert(the_mesh.cli_entries==2);
|
|
|
|
reset();UITask rejected;rejected.guard_ok=false;
|
|
rejected.shutdown();last(rejected,ButtonVoicePrompt::ActionFailed);
|
|
assert(!rejected._shutdown_pending&&!rejected.board.offs&&!rejected.board.reboots);
|
|
rejected.servicePendingShutdown();assert(rejected.guards==1&&!rejected.board.offs);
|
|
|
|
reset();UITask shutdown;
|
|
now_ms=8000;shutdown.handleButtonLongPress();last(shutdown,ButtonVoicePrompt::ShuttingDown);
|
|
assert(shutdown._shutdown_pending&&shutdown.guards==1&&!shutdown.board.offs);
|
|
assert((events==std::vector<std::string>{"guard","speak"}));
|
|
// Shutdown callbacks return immediately; draining is foreground service, not a busy wait.
|
|
assert(now_ms==8000);
|
|
auto before=shutdown.buzzer.prompts.size();
|
|
auto adverts=the_mesh.adverts,shutdown_saves=the_mesh.saves;
|
|
shutdown.handleButtonAnyPress();shutdown.handleButtonShortPress();shutdown.handleButtonDoublePress();
|
|
shutdown.handleButtonTriplePress();shutdown.handleButtonQuadruplePress();shutdown.handleButtonLongPress();
|
|
shutdown.shutdown(true);shutdown.notify(UIEventType::ack);shutdown.notificationMelody("notification");
|
|
assert(shutdown.buzzer.prompts.size()==before&&the_mesh.adverts==adverts&&the_mesh.saves==shutdown_saves);
|
|
assert(!shutdown.buzzer.plays&&!shutdown.buzzer.notification_plays&&shutdown.guards==1);
|
|
now_ms+=4999;shutdown.servicePendingShutdown();assert(!shutdown.board.offs&&shutdown._shutdown_pending);
|
|
shutdown.buzzer.playing=false;shutdown.servicePendingShutdown();
|
|
assert(shutdown.board.offs==1&&!shutdown._shutdown_pending&&shutdown.guards==2&&shutdown.buzzer.stops==1);
|
|
shutdown.servicePendingShutdown();assert(shutdown.board.offs==1);
|
|
|
|
reset();UITask restart;
|
|
now_ms=0xfffffff0;restart.shutdown(true);last(restart,ButtonVoicePrompt::Restarting);
|
|
now_ms+=4999;restart.servicePendingShutdown();assert(!restart.board.reboots&&restart._shutdown_pending);
|
|
++now_ms;restart.servicePendingShutdown();
|
|
assert(restart.board.reboots==1&&!restart.board.offs&&!restart._shutdown_pending&&!restart.buzzer.playing);
|
|
|
|
reset();UITask drain_rejected;drain_rejected.guard_reject_drain=true;
|
|
drain_rejected.shutdown();drain_rejected.buzzer.playing=false;drain_rejected.servicePendingShutdown();
|
|
last(drain_rejected,ButtonVoicePrompt::ActionFailed);
|
|
assert(!drain_rejected._shutdown_pending&&!drain_rejected.board.offs&&drain_rejected.guards==2);
|
|
|
|
// Use the actual click decoder to establish that holding does not turn into a Ready/advert.
|
|
reset();UITask held;now_ms=100;
|
|
Button button(6,HIGH);button.begin();
|
|
static_assert(BUTTON_LONG_PRESS_TIME_MS==3000,"three-second hold");
|
|
button.onAnyPress([&]{held.handleButtonAnyPress();});
|
|
button.onShortPress([&]{held.handleButtonShortPress();});
|
|
button.onDoublePress([&]{held.handleButtonDoublePress();});
|
|
button.onLongPress([&]{held.handleButtonLongPress();});
|
|
assert(button.injectPresses(1,3500));
|
|
for(unsigned i=0;i<500;++i){now_ms+=10;button.update();}
|
|
assert(held.buzzer.prompts.size()==1&&the_mesh.adverts==0&&the_mesh.cli_entries==1);
|
|
last(held,ButtonVoicePrompt::UsbSetup);
|
|
}
|
|
'''
|
|
|
|
NOTIFICATION_ACK = r'''
|
|
#include <cassert>
|
|
#include <initializer_list>
|
|
#pragma GCC diagnostic push
|
|
#pragma GCC diagnostic ignored "-Wmisleading-indentation"
|
|
#include <helpers/CompanionNotificationPolicy.h>
|
|
#pragma GCC diagnostic pop
|
|
uint32_t now_ms=0;
|
|
using namespace mesh::notify;
|
|
struct Probe : Sink,Store {
|
|
unsigned stops=0;
|
|
uint8_t capabilities()const override{return Sound;}
|
|
bool gpioAvailable(uint8_t)const override{return false;}
|
|
void pulse(Output,bool,int8_t)override{}
|
|
void melody(const char* value)override{if(!value)++stops;}
|
|
void screen(int8_t)override{}
|
|
bool save(const Settings&)override{return true;}
|
|
};
|
|
struct MyMesh {
|
|
Controller _notifications;
|
|
explicit MyMesh(Probe& probe):_notifications(probe,probe){}
|
|
@WRAPPER@
|
|
};
|
|
int main(){
|
|
Probe probe;MyMesh mesh(probe);
|
|
assert(!mesh.notificationButton()&&probe.stops==0);
|
|
auto& settings=mesh._notifications.settings();settings.enabled=1;settings.outputs=Sound;
|
|
auto& rule=settings.rules[0];rule.used=1;
|
|
assert(setField(rule,FSound,"test:d=4,o=5,b=120:c"));
|
|
assert(setField(rule,FStop,"button"));
|
|
assert(mesh._notifications.message(nullptr,nullptr,false,100));
|
|
assert(mesh.notificationButton()&&!mesh._notifications.active()&&probe.stops==1);
|
|
assert(!mesh.notificationButton()&&probe.stops==1);
|
|
// An active alert with another stop policy was not actually cleared by input.
|
|
for(const char* policy:{"connected","never"}){
|
|
assert(setField(rule,FStop,policy));
|
|
assert(mesh._notifications.message(nullptr,nullptr,false,200));
|
|
auto stops=probe.stops;
|
|
assert(!mesh.notificationButton()&&mesh._notifications.active()&&probe.stops==stops);
|
|
mesh._notifications.stop();
|
|
}
|
|
}
|
|
'''
|
|
|
|
NOTIFICATION_MELODY = r'''
|
|
#include <cassert>
|
|
#include <cstdint>
|
|
#define PIN_BUZZER 25
|
|
#define MESH_GPS_VOICE @VOICE@
|
|
uint32_t now_ms=0;
|
|
struct Buzzer {
|
|
bool muted=true,playing=true,control_voice=true;unsigned melodies=0,stops=0,notification_stops=0;
|
|
bool isQuiet(){return muted;}
|
|
void playNotification(const char*){++melodies;playing=true;control_voice=false;}
|
|
void stop(){++stops;playing=false;}
|
|
void stopNotification(){++notification_stops;if(!MESH_GPS_VOICE||!control_voice)playing=false;}
|
|
};
|
|
struct UITask {
|
|
Buzzer buzzer;bool _shutdown_pending=false;
|
|
void notificationMelody(const char*);
|
|
};
|
|
@METHOD@
|
|
int main(){
|
|
UITask ui;
|
|
// A muted programmable alert must not preempt the final courtesy confirmation.
|
|
ui.notificationMelody("ordinary");
|
|
#if MESH_GPS_VOICE
|
|
assert(ui.buzzer.melodies==0&&ui.buzzer.playing&&ui.buzzer.stops==0);
|
|
#else
|
|
assert(ui.buzzer.melodies==1&&ui.buzzer.playing&&ui.buzzer.stops==0);
|
|
#endif
|
|
// Background stop requests retain control speech while quiet.
|
|
ui.notificationMelody(nullptr);
|
|
assert(ui.buzzer.stops==0&&ui.buzzer.notification_stops==1);
|
|
#if MESH_GPS_VOICE
|
|
assert(ui.buzzer.playing);
|
|
#else
|
|
assert(!ui.buzzer.playing);
|
|
#endif
|
|
ui.buzzer.muted=false;ui.notificationMelody("ordinary");
|
|
#if MESH_GPS_VOICE
|
|
assert(ui.buzzer.melodies==1&&ui.buzzer.playing);
|
|
#else
|
|
assert(ui.buzzer.melodies==2&&ui.buzzer.playing);
|
|
#endif
|
|
ui._shutdown_pending=true;
|
|
ui.notificationMelody("ordinary");ui.notificationMelody(nullptr);
|
|
#if MESH_GPS_VOICE
|
|
assert(ui.buzzer.melodies==1&&ui.buzzer.stops==0&&ui.buzzer.notification_stops==1&&ui.buzzer.playing);
|
|
#else
|
|
assert(ui.buzzer.melodies==3&&ui.buzzer.stops==0&&ui.buzzer.notification_stops==2&&!ui.buzzer.playing);
|
|
#endif
|
|
}
|
|
'''
|
|
|
|
NOTIFICATION_STOPS = r'''
|
|
#include <cassert>
|
|
#include <initializer_list>
|
|
#define PIN_BUZZER 25
|
|
#define PIN_BUZZER_EN 37
|
|
#define MESH_BUTTON_AUDIO_HIL 1
|
|
#define MESH_GPS_VOICE @VOICE@
|
|
#include <helpers/ui/buzzer.cpp>
|
|
uint32_t now_ms=0;
|
|
int main(){
|
|
genericBuzzer buzzer;buzzer.begin();buzzer.quiet(false);
|
|
buzzer.playNotification("ordinary:d=4,o=5,b=120:c");
|
|
assert(buzzer.isPlaying()&&rtttl::isPlaying());
|
|
buzzer.stopNotification();assert(!buzzer.isPlaying()&&!rtttl::isPlaying());
|
|
assert(buzzer.speakGps(true)&&rtttl::isPlaying());
|
|
buzzer.stopNotification();
|
|
#if MESH_GPS_VOICE
|
|
// A stale notification stop cannot cut off the direction tone or its gap.
|
|
assert(buzzer.isPlaying()&&rtttl::isPlaying()&&!registers.ENABLE);
|
|
auto starts=gps_voice.started(),cancelled=gps_voice.cancelled();
|
|
rtttl::finish();buzzer.loop();
|
|
assert(buzzer.isPlaying()&&!rtttl::isPlaying()&&!registers.ENABLE);
|
|
buzzer.stopNotification();assert(buzzer.isPlaying());
|
|
now_ms+=99;buzzer.loop();buzzer.stopNotification();
|
|
assert(buzzer.isPlaying()&&!registers.ENABLE);
|
|
++now_ms;buzzer.loop();assert(registers.ENABLE&&gps_voice.started()==starts+1);
|
|
buzzer.stopNotification();
|
|
assert(registers.ENABLE&&HwPWM3.owned&&buzzer.isPlaying()&&gps_voice.cancelled()==cancelled);
|
|
auto drain=[&]{
|
|
unsigned sequence=0,events=0;
|
|
while(registers.ENABLE){
|
|
assert(++events<2000);buzzer.stopNotification();
|
|
assert(registers.ENABLE&&HwPWM3.owned&&gps_voice.cancelled()==cancelled);
|
|
registers.EVENTS_SEQEND[sequence]=1;PWM3_IRQHandler();sequence^=1;
|
|
}
|
|
// Completed DMA still owns its hardware until foreground cleanup.
|
|
assert(buzzer.isPlaying()&&HwPWM3.owned);
|
|
buzzer.stopNotification();assert(HwPWM3.owned&&gps_voice.cancelled()==cancelled);
|
|
buzzer.loop();assert(!buzzer.isPlaying()&&!HwPWM3.owned&&!HwPWM3.pin&&!irq_enabled);
|
|
};
|
|
drain();
|
|
// The same protection covers a courtesy confirmation while alerts are muted.
|
|
buzzer.quiet(true);assert(buzzer.speakButton(ButtonVoicePrompt::ActionFailed,true));
|
|
buzzer.stopNotification();assert(buzzer.isPlaying()&&buzzer.isQuiet());
|
|
buzzer.loop();now_ms+=100;buzzer.loop();assert(registers.ENABLE);
|
|
drain();
|
|
assert(pin_states[PIN_BUZZER_EN]==LOW&&pin_states[PIN_BUZZER]==LOW);
|
|
#else
|
|
// Without voice, this API remains an ordinary tone stop.
|
|
assert(!buzzer.isPlaying()&&!rtttl::isPlaying());
|
|
buzzer.quiet(true);
|
|
#endif
|
|
buzzer.playNotification("ordinary:d=4,o=5,b=120:c");
|
|
assert(!buzzer.isPlaying()&&buzzer.isQuiet()&&pin_states[PIN_BUZZER_EN]==LOW);
|
|
buzzer.quiet(false);
|
|
buzzer.playNotification("ordinary:d=4,o=5,b=120:c");
|
|
assert(buzzer.isPlaying()&&pin_states[PIN_BUZZER_EN]==HIGH);
|
|
buzzer.stopNotification();
|
|
assert(!buzzer.isPlaying()&&!rtttl::isPlaying()&&!buzzer.isQuiet());
|
|
}
|
|
'''
|
|
|
|
X1_ALERT_MODES = r'''
|
|
#include <cassert>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <string>
|
|
#include <vector>
|
|
#include <helpers/ui/ButtonVoice.h>
|
|
#define PIN_BUZZER 25
|
|
#define HAS_DRV2605 1
|
|
#define MESH_GPS_VOICE @VOICE@
|
|
#define MESH_DEBUG_PRINTLN(...) ((void)0)
|
|
uint32_t now_ms=0;
|
|
static std::vector<std::string> mode_events;
|
|
enum class UIEventType{ack};
|
|
namespace mesh{namespace notify{enum{Sound=1,Vibration=2};}}
|
|
struct CompanionNodePrefs{uint8_t buzzer_quiet=0,vibe_quiet=0;};
|
|
struct MyMesh {
|
|
CompanionNodePrefs _prefs;
|
|
bool save_ok=true,mask_save_ok=true;
|
|
unsigned saves=0,mutes=0;
|
|
bool sound_muted=false,vibration_muted=false;
|
|
std::vector<unsigned> saved_modes;
|
|
bool savePrefs(){
|
|
mode_events.push_back("save");
|
|
++saves;saved_modes.push_back((_prefs.buzzer_quiet?2:0)|(_prefs.vibe_quiet?1:0));
|
|
return save_ok;
|
|
}
|
|
void setNotificationOutputMute(int output,bool quiet){
|
|
++mutes;
|
|
mode_events.push_back("mask");
|
|
if(!mask_save_ok)return;
|
|
if(output==mesh::notify::Sound)sound_muted=quiet;
|
|
else if(output==mesh::notify::Vibration)vibration_muted=quiet;
|
|
else assert(false);
|
|
}
|
|
};
|
|
static MyMesh the_mesh;
|
|
struct Buzzer {
|
|
bool muted=false;
|
|
std::vector<ButtonVoicePrompt> prompts;
|
|
std::vector<bool> courtesy;
|
|
void quiet(bool value){muted=value;}
|
|
bool isQuiet(){return muted;}
|
|
bool speakButton(ButtonVoicePrompt prompt,bool allowQuiet=false){
|
|
mode_events.push_back("voice");
|
|
prompts.push_back(prompt);courtesy.push_back(allowQuiet);return true;
|
|
}
|
|
};
|
|
struct Vibration {
|
|
bool muted=false,active=false;unsigned triggers=0,stops=0,pulses=0;
|
|
void quiet(bool value){muted=value;}
|
|
bool isQuiet(){return muted;}
|
|
void trigger(bool confirmation=false){assert(confirmation);++triggers;}
|
|
void stop(){++stops;active=false;mode_events.push_back("stop");}
|
|
void pulse(bool on){++pulses;active=on;}
|
|
};
|
|
struct UITask {
|
|
CompanionNodePrefs* _node_prefs=&the_mesh._prefs;
|
|
Buzzer buzzer;Vibration vibration;
|
|
bool _shutdown_pending=false,_button_notification_cleared=false,_need_refresh=false;
|
|
char _alert[80]={};unsigned acknowledgements=0;
|
|
void notify(UIEventType){++acknowledgements;}
|
|
void handleButtonTriplePress();
|
|
void notificationVibration(bool);
|
|
};
|
|
@METHOD@
|
|
@VIBRATION_METHOD@
|
|
int main(){
|
|
UITask ui;
|
|
const ButtonVoicePrompt expected[]={
|
|
ButtonVoicePrompt::AlertsSoundVibration,ButtonVoicePrompt::AlertsSoundOnly,
|
|
ButtonVoicePrompt::AlertsVibrationOnly,ButtonVoicePrompt::AlertsSilent,
|
|
};
|
|
unsigned vibration_triggers=0,vibration_stops=0;
|
|
for(unsigned step=1;step<=8;++step){
|
|
const auto mode=step&3;
|
|
ui._button_notification_cleared=true;
|
|
auto saves=the_mesh.saves;
|
|
mode_events.clear();
|
|
ui.vibration.active=true;
|
|
ui.handleButtonTriplePress();
|
|
assert(the_mesh.saves==saves+1&&the_mesh.saved_modes.back()==mode);
|
|
assert(the_mesh._prefs.buzzer_quiet==!!(mode&2));
|
|
assert(the_mesh._prefs.vibe_quiet==!!(mode&1));
|
|
assert(ui.buzzer.muted==!!(mode&2)&&ui.vibration.muted==!!(mode&1));
|
|
assert(the_mesh.sound_muted==!!(mode&2)&&the_mesh.vibration_muted==!!(mode&1));
|
|
if(!(mode&1))++vibration_triggers;
|
|
assert(ui.vibration.triggers==vibration_triggers&&ui._need_refresh);
|
|
#if MESH_GPS_VOICE
|
|
if(mode&1){
|
|
++vibration_stops;assert(!ui.vibration.active);
|
|
assert(mode_events.size()>=2&&mode_events[0]=="save"&&mode_events[1]=="stop");
|
|
}
|
|
assert(ui.vibration.stops==vibration_stops);
|
|
assert(ui.buzzer.prompts.size()==step&&ui.buzzer.prompts.back()==expected[mode]);
|
|
// Selecting silent or vibration-only gets a single final confirmation,
|
|
// without unmuting saved alerts or notification routing.
|
|
assert(ui.buzzer.courtesy.back()&&!ui._button_notification_cleared);
|
|
assert(ui.acknowledgements==0);
|
|
#else
|
|
assert(ui.buzzer.prompts.empty());
|
|
#endif
|
|
}
|
|
#if MESH_GPS_VOICE
|
|
// Each possible persisted mode is restored on failure. Deliberately divergent
|
|
// runtime and routing states are preserved too; a failed save is not a toggle.
|
|
the_mesh.save_ok=false;
|
|
for(unsigned mode=0;mode<4;++mode){
|
|
the_mesh._prefs.buzzer_quiet=!!(mode&2);the_mesh._prefs.vibe_quiet=!!(mode&1);
|
|
ui.buzzer.muted=!(mode&2);ui.vibration.muted=!(mode&1);
|
|
the_mesh.sound_muted=!(mode&2);the_mesh.vibration_muted=!(mode&1);
|
|
const auto old_buzzer=ui.buzzer.muted,old_vibration=ui.vibration.muted;
|
|
const auto old_sound=the_mesh.sound_muted,old_vibe=the_mesh.vibration_muted;
|
|
const auto mutes=the_mesh.mutes,triggers=ui.vibration.triggers,saves=the_mesh.saves;
|
|
const auto stops=ui.vibration.stops;
|
|
mode_events.clear();
|
|
ui.handleButtonTriplePress();
|
|
assert(the_mesh.saves==saves+1&&the_mesh.saved_modes.back()==((mode+1)&3));
|
|
assert(the_mesh._prefs.buzzer_quiet==!!(mode&2)&&the_mesh._prefs.vibe_quiet==!!(mode&1));
|
|
assert(ui.buzzer.muted==old_buzzer&&ui.vibration.muted==old_vibration);
|
|
assert(the_mesh.sound_muted==old_sound&&the_mesh.vibration_muted==old_vibe);
|
|
assert(the_mesh.mutes==mutes&&ui.vibration.triggers==triggers);
|
|
assert(ui.vibration.stops==stops);
|
|
assert((mode_events==std::vector<std::string>{"save","voice"}));
|
|
assert(ui.buzzer.prompts.back()==ButtonVoicePrompt::ActionFailed&&strstr(ui._alert,"failed"));
|
|
assert(ui.buzzer.courtesy.back());
|
|
}
|
|
// A notification-mask write failure cannot undo the committed physical mode.
|
|
the_mesh.save_ok=true;the_mesh.mask_save_ok=false;
|
|
the_mesh._prefs.buzzer_quiet=the_mesh._prefs.vibe_quiet=0;
|
|
the_mesh.sound_muted=the_mesh.vibration_muted=false;
|
|
ui.vibration.active=true;
|
|
ui.handleButtonTriplePress();
|
|
assert(the_mesh._prefs.vibe_quiet==1&&!the_mesh.vibration_muted&&!ui.vibration.active);
|
|
auto pulses=ui.vibration.pulses,stops=ui.vibration.stops;
|
|
ui.notificationVibration(true);
|
|
assert(ui.vibration.pulses==pulses&&ui.vibration.stops==stops+1&&!ui.vibration.active);
|
|
ui.notificationVibration(false);
|
|
assert(ui.vibration.pulses==pulses+1&&!ui.vibration.active);
|
|
the_mesh._prefs.vibe_quiet=0;ui.notificationVibration(true);
|
|
assert(ui.vibration.pulses==pulses+2&&ui.vibration.active);
|
|
ui.notificationVibration(false);assert(!ui.vibration.active);
|
|
ui._node_prefs=nullptr;ui.notificationVibration(true);assert(ui.vibration.active);
|
|
ui._node_prefs=&the_mesh._prefs;
|
|
// Shutdown quiesces multi-click changes instead of preempting final speech.
|
|
ui._shutdown_pending=true;
|
|
const auto saves=the_mesh.saves;
|
|
const auto prompts=ui.buzzer.prompts.size();
|
|
ui.handleButtonTriplePress();
|
|
assert(the_mesh.saves==saves&&ui.buzzer.prompts.size()==prompts);
|
|
#else
|
|
(void)expected;
|
|
(void)vibration_stops;
|
|
assert(ui.acknowledgements==4);
|
|
// The opt-out retains its historical driver forwarding despite mute prefs.
|
|
ui._node_prefs->vibe_quiet=1;
|
|
ui.notificationVibration(true);assert(ui.vibration.active);
|
|
ui.notificationVibration(false);assert(!ui.vibration.active);
|
|
#endif
|
|
}
|
|
'''
|
|
|
|
|
|
class Tests(unittest.TestCase):
|
|
build = gps_tests.Tests.build
|
|
|
|
def test_every_prompt_uses_its_own_clip_and_preserves_direction_tones(self):
|
|
self.build(PROMPTS, rtttl=True)
|
|
|
|
def test_each_new_board_default_uses_its_actual_buzzer_pin_and_releases_pwm(self):
|
|
for board, pin, enable in (
|
|
('RAK_WISMESH_TAG', 21, None),
|
|
('MESH_TRACKER_X1', 25, None),
|
|
('R1Neo', 3, None),
|
|
('THINKNODE_M3', 23, 36),
|
|
):
|
|
with self.subTest(board=board):
|
|
source = PROMPTS.replace('#define PIN_BUZZER 25', f'#define PIN_BUZZER {pin}')
|
|
source = source.replace('#define PIN_BUZZER_EN 37',
|
|
'' if enable is None else f'#define PIN_BUZZER_EN {enable}')
|
|
source = source.replace('#define MESH_GPS_VOICE 1',
|
|
'#define NRF52_PLATFORM\n#define COMPANION_RADIO_FULL 1\n'
|
|
+f'#define {board}\n')
|
|
if board != 'MESH_TRACKER_X1':
|
|
source = source.replace('#define HAS_DRV2605 1', '')
|
|
self.build(source, rtttl=True)
|
|
|
|
def test_quiet_courtesy_completion_and_cancellation_release_both_pins(self):
|
|
self.build(COURTESY, rtttl=True)
|
|
|
|
def test_muted_x1_mode_announcements_never_unmute_notifications_and_release_pwm(self):
|
|
for prompt in ('AlertsSoundVibration', 'AlertsSoundOnly',
|
|
'AlertsVibrationOnly', 'AlertsSilent'):
|
|
with self.subTest(prompt=prompt):
|
|
self.build(COURTESY.replace('ButtonVoicePrompt::SoundOff',
|
|
'ButtonVoicePrompt::'+prompt)
|
|
.replace('#define MESH_GPS_VOICE 1',
|
|
'#define HAS_DRV2605 1\n#define MESH_GPS_VOICE 1'), rtttl=True)
|
|
|
|
def test_x1_alert_modes_report_committed_result_and_legacy_tones_remain(self):
|
|
ui = (ROOT / 'examples/companion_radio/ui-orig/UITask.cpp').read_text()
|
|
triple = method(ui, 'void UITask::handleButtonTriplePress()')
|
|
vibration = method(ui, 'void UITask::notificationVibration(bool on)')
|
|
for voice in (0, 1):
|
|
with self.subTest(voice=voice):
|
|
self.build(X1_ALERT_MODES.replace('@METHOD@', triple)
|
|
.replace('@VIBRATION_METHOD@', vibration)
|
|
.replace('@VOICE@', str(voice)))
|
|
|
|
def test_voice_disabled_ordinary_play_keeps_its_legacy_preemption(self):
|
|
self.build(LEGACY_ORDINARY_PLAY, rtttl=True)
|
|
|
|
def test_notification_cleared_requires_an_actual_button_acknowledgement(self):
|
|
mesh = (ROOT / 'examples/companion_radio/MyMesh.h').read_text()
|
|
wrapper = method(mesh, 'bool notificationButton()')
|
|
self.build(NOTIFICATION_ACK.replace('@WRAPPER@', wrapper))
|
|
|
|
def test_muted_programmable_notifications_do_not_cancel_courtesy_audio(self):
|
|
ui = (ROOT / 'examples/companion_radio/ui-orig/UITask.cpp').read_text()
|
|
melody = method(ui, 'void UITask::notificationMelody(const char* text)')
|
|
for voice in (0, 1):
|
|
with self.subTest(voice=voice):
|
|
self.build(NOTIFICATION_MELODY.replace('@METHOD@', melody)
|
|
.replace('@VOICE@', str(voice)))
|
|
|
|
def test_notification_stop_preserves_control_tone_gap_and_dma_but_cancels_alerts(self):
|
|
for voice in (0, 1):
|
|
with self.subTest(voice=voice):
|
|
self.build(NOTIFICATION_STOPS.replace('@VOICE@', str(voice)), rtttl=True)
|
|
|
|
def test_button_actions_report_committed_results_and_shutdown_without_blocking(self):
|
|
mesh = (ROOT / 'examples/companion_radio/MyMesh.cpp').read_text()
|
|
ui = (ROOT / 'examples/companion_radio/ui-orig/UITask.cpp').read_text()
|
|
methods = method(mesh, 'bool MyMesh::setGpsEnabled(bool enabled)')
|
|
for signature in (
|
|
'void UITask::notify(UIEventType t)',
|
|
'void UITask::notificationMelody(const char* text)',
|
|
'void UITask::handleButtonAnyPress()',
|
|
'void UITask::handleButtonShortPress()',
|
|
'void UITask::handleButtonDoublePress()',
|
|
'void UITask::handleButtonTriplePress()',
|
|
'void UITask::handleButtonQuadruplePress()',
|
|
'void UITask::handleButtonLongPress()',
|
|
'void UITask::shutdown(bool restart)',
|
|
'void UITask::servicePendingShutdown()',
|
|
):
|
|
methods += '\n' + method(ui, signature)
|
|
self.build(UI_ACTIONS.replace('@METHODS@', methods), button=True)
|
|
self.build(UI_ACTIONS.replace('@METHODS@', methods)
|
|
.replace('#define ENV_INCLUDE_GPS 1', '#define ENV_INCLUDE_GPS 0'), button=True)
|
|
loop = method(ui, 'void UITask::loop()')
|
|
self.assertLess(loop.index('buzzer.loop()'), loop.index('servicePendingShutdown()'))
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|