diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index aced739dc4..8a2c5f83dc 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -766,6 +766,7 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no void UITask::showAlert(const char* text, int duration_millis) { strcpy(_alert, text); _alert_expiry = millis() + duration_millis; + if (_alert_expiry == 0) _alert_expiry = 1; // zero means no active alert } void UITask::notify(UIEventType t) { @@ -811,7 +812,7 @@ void UITask::onMessageRecv(mesh::Packet *pkt, const ContactInfo &from, uint8_t t } if (_display->isOn()) { _auto_off = millis() + AUTO_OFF_MILLIS; // extend the auto-off timer - _next_refresh = 100; // trigger refresh + _next_refresh = 0; // trigger refresh } } @@ -831,7 +832,7 @@ void UITask::onChannelMessageRecv(mesh::Packet *pkt, ChannelDetails& channel_det } if (_display->isOn()) { _auto_off = millis() + AUTO_OFF_MILLIS; // extend the auto-off timer - _next_refresh = 100; // trigger refresh + _next_refresh = 0; // trigger refresh } } @@ -884,7 +885,7 @@ void UITask::userLedHandler() { void UITask::setCurrScreen(UIScreen* c) { curr = c; - _next_refresh = 100; + _next_refresh = 0; } /* @@ -900,7 +901,7 @@ void UITask::shutdown(bool restart){ */ buzzer.shutdown(); uint32_t buzzer_timer = millis(); // fail-safe shutdown - while (buzzer.isPlaying() && (millis() - 2500) < buzzer_timer) + while (buzzer.isPlaying() && uint32_t(millis() - buzzer_timer) < 2500) buzzer.loop(); #endif // PIN_BUZZER @@ -1035,7 +1036,7 @@ void UITask::loop() { if (c != 0 && curr) { curr->handleInput(c); _auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer - _next_refresh = 100; // trigger refresh + _next_refresh = 0; // trigger refresh } userLedHandler(); @@ -1047,10 +1048,10 @@ void UITask::loop() { if (curr) curr->poll(); if (_display != NULL && _display->isOn()) { - if (millis() >= _next_refresh && curr) { + if ((_next_refresh == 0 || int32_t(millis() - _next_refresh) >= 0) && curr) { _display->startFrame(); int delay_millis = curr->render(*_display); - if (millis() < _alert_expiry) { // render alert popup + if (_alert_expiry && int32_t(millis() - _alert_expiry) < 0) { // render alert popup _display->setTextSize(1); int y = _display->height() / 3; int p = _display->height() / 32; @@ -1062,6 +1063,7 @@ void UITask::loop() { _next_refresh = _alert_expiry; // will need refresh when alert is dismissed } else { _next_refresh = millis() + delay_millis; + if (_next_refresh == 0) _next_refresh = 1; // reserve zero for forced refresh } _display->endFrame(); } @@ -1075,7 +1077,7 @@ void UITask::loop() { _auto_off = millis() + AUTO_OFF_MILLIS; } #endif - if (millis() > _auto_off) { + if (int32_t(millis() - _auto_off) >= 0) { #ifdef EINK_SCREENOFF_BLANK if (curr != msg_preview) { // don't blank if messages are waiting _display->blankScreen(); diff --git a/test/ui_new_timing/run.py b/test/ui_new_timing/run.py new file mode 100644 index 0000000000..ac95668f16 --- /dev/null +++ b/test/ui_new_timing/run.py @@ -0,0 +1,98 @@ +"""Exercise the actual ui-new timing expressions with a 32-bit clock.""" +import argparse +import os +from pathlib import Path +import re +import subprocess + +root = Path(__file__).resolve().parents[2] +parser = argparse.ArgumentParser(description=__doc__) +parser.add_argument('--sanitize', action='store_true') +parser.add_argument('--source-ref', help='Optional Git revision for a regression check') +args = parser.parse_args() + +def source(name): + if args.source_ref: + return subprocess.check_output(['git', 'show', args.source_ref+':'+name], cwd=root).decode() + return (root/name).read_text() + +def method(text, signature): + start = text.index(signature) + first = text.index('{', start) + end, depth = first+1, 1 + while depth: + depth += (text[end] == '{') - (text[end] == '}') + end += 1 + return text[start:end] + +text = source('examples/companion_radio/ui-new/UITask.cpp') +loop = method(text, 'void UITask::loop()') +refresh = re.search(r'if \((.*_next_refresh.*) && curr\) \{', loop).group(1) +alert = re.search(r'if \((.*)\) \{ // render alert popup', loop).group(1) +off = re.search(r'if \((.*_auto_off.*)\) \{', loop).group(1) +shutdown = re.search(r'while \((.*)\)\s*buzzer.loop\(\)', method(text, 'void UITask::shutdown(')).group(1) +show = method(text, 'void UITask::showAlert(') +copy = method(source('src/helpers/TxtDataHelpers.cpp'), 'void StrHelper::strzcpy(') +forced = re.findall(r'_next_refresh = (\d+);[^\n]*trigger refresh', text) +forced += re.findall(r'_next_refresh = (\d+);', method(text, 'void UITask::setCurrScreen(')) +schedule = re.search(r'_next_refresh = millis\(\) \+ delay_millis;[^}]*', loop).group() +program = '''#include +#include +#include +uint32_t now = 0; +uint32_t millis() { return now; } +class StrHelper { public: static void strzcpy(char*, const char*, size_t); }; +class UITask { public: char _alert[80]; uint32_t _alert_expiry=0; void showAlert(const char*,int); }; +'''+copy+'\n'+show+''' +struct Buzzer { bool isPlaying() { return true; } } buzzer; +bool refresh(uint32_t t,uint32_t _next_refresh) { now=t; return '''+refresh+'''; } +bool alert(uint32_t t,uint32_t _alert_expiry) { now=t; return '''+alert+'''; } +bool off(uint32_t t,uint32_t _auto_off) { now=t; return '''+off+'''; } +bool waiting(uint32_t t,uint32_t buzzer_timer) { now=t; return '''+shutdown+'''; } +uint32_t schedule(uint32_t t,uint32_t delay_millis) { now=t; uint32_t _next_refresh; +'''+schedule+''' +return _next_refresh; } +int main() { + assert(waiting(100,100)); + assert(waiting(2499,0)); + assert(!waiting(2500,0)); + assert(waiting(0x20u,0xFFFFFFF0u)); + assert(!waiting(uint32_t(0xFFFFFFF0u+2500u),0xFFFFFFF0u)); + for(uint32_t deadline : {32u,0x80000020u,0xFFFFFFF0u}) { + assert(!refresh(uint32_t(deadline-1),deadline)); + assert(refresh(deadline,deadline)); + assert(refresh(uint32_t(deadline+1),deadline)); + assert(!off(uint32_t(deadline-1),deadline)); + assert(off(deadline,deadline)); + assert(off(uint32_t(deadline+1),deadline)); + assert(alert(uint32_t(deadline-1),deadline)); + assert(!alert(deadline,deadline)); + assert(!alert(uint32_t(deadline+1),deadline)); + } + assert(!refresh(0xFFFFFFF0u,0x20u)); + assert(!off(0xFFFFFFF0u,0x20u)); + assert(alert(0xFFFFFFF0u,0x20u)); + for(uint32_t t : {0u,100u,0x80000080u,0xFFFFFFF0u}) { + for(uint32_t forced : {'''+','.join(v+'u' for v in forced)+'''}) assert(refresh(t,forced)); + assert(!alert(t,0)); + } + UITask task; + now=0xFFFFFF9Cu; task.showAlert("test",100); + assert(task._alert_expiry != 0); + assert(alert(now,task._alert_expiry)); + assert(alert(0,task._alert_expiry)); + assert(!alert(1,task._alert_expiry)); + assert(schedule(0xFFFFFF9Cu,100)==1); +} +''' +# initializer_list is needed by the harness's case tables, not by the firmware. +program = '#include \n'+program +output = root/'.pio/ui-new-timing-check' +output.mkdir(parents=True, exist_ok=True) +(output/'check.cpp').write_text(program) +binary = output/('check.exe' if os.name == 'nt' else 'check') +flags = ['-fsanitize=address,undefined', '-fno-omit-frame-pointer'] if args.sanitize else [] +subprocess.run([os.environ.get('CXX','g++'), '-std=c++17', '-Wall', '-Wextra', *flags, + str(output/'check.cpp'), '-o', str(binary)], check=True) +subprocess.run([str(binary)], check=True) +print('PASS: actual render, alert, auto-off and shutdown conditions across 32-bit wrap')