Keep RTTTL tones and queued speech running across timer rollover

This commit is contained in:
mikecarper committed 2026-10-16 16:08:33 -07:00
1 parent 9b4d3350df
commit d01b63f0c3
20 files changed
+1070

No files matched your search

+2
View File
@@ -780,6 +780,8 @@ jobs:
- name: Verify Companion Bluetooth settings and WebConfig
run: |
python3 -B test/test_rtttl_rollover.py -v
python3 -B test/test_rtttl_rollover_recipes.py -v
python3 -B test/test_gps_voice_buttons.py -v
python3 -B test/test_gps_voice_generator.py -v
python3 -B test/test_button_voice_actions.py -v
+4
View File
@@ -85,6 +85,7 @@ monitor_filters = esp32_exception_decoder
; it needs a custom bootloader; RP2040/STM32 have no A/B path yet.)
extra_scripts =
pre:scripts/client_acl_role.py
pre:scripts/rtttl_rollover_fix.py
pre:scripts/generate_webconfig_html.py
pre:scripts/meshcore_image_identity.py
pre:scripts/ota_device_deflate_role.py
@@ -162,6 +163,7 @@ platform_packages =
platformio/toolchain-gccarmnoneeabi@^1.140201.0
extra_scripts = create-uf2.py
pre:scripts/client_acl_role.py
pre:scripts/rtttl_rollover_fix.py
pre:scripts/ota_device_deflate_role.py
pre:scripts/nrf52_usb_power_fix.py
pre:scripts/nrf52_internal_flash_fix.py
@@ -339,6 +341,7 @@ lib_deps = ${nrf52_lora_ota.lib_deps}
[rp2040_base]
extends = arduino_base
extra_scripts = pre:scripts/client_acl_role.py
pre:scripts/rtttl_rollover_fix.py
post:scripts/check_firmware_ram.py
upload_protocol = picotool
board_build.core = earlephilhower
@@ -359,6 +362,7 @@ extends = arduino_base
platform = ststm32@19.5.0
extra_scripts = post:arch/stm32/build_hex.py
pre:scripts/client_acl_role.py
pre:scripts/rtttl_rollover_fix.py
post:scripts/check_firmware_ram.py
build_flags = ${arduino_base.build_flags}
-D STM32_PLATFORM
+96
View File
@@ -0,0 +1,96 @@
"""Fix reviewed RTTTL timers in private build copies, never in libdeps.
An absolute millis() comparison can truncate a tone at rollover or keep it
busy until the next wrap, blocking queued speech. Use a signed 32-bit delta
and make the initial deadline due now, including in the upper counter half.
As with other Arduino deadline comparisons, waits must remain below 2^31 ms;
the firmware's note durations and repeat gaps are far below that limit.
"""
import hashlib
from pathlib import Path
# Each entry binds the actual source, its matching header, and the result.
# The recipes allow both releases; do not silently upgrade cached 1.3.0.
REVIEWED = {
"b50c159c8806d177398972bf8f94ca762532a855b60cb1d3aa9c3ebf021423ca": (
"abea0a4870e58222fdefd1e88d291136518912e83880af30d1c85d39d7bd5cc1",
"29204341d99b8adeef501c0d4cb47a4137534ebe6a30f2e6b3032d558aeef802",
),
"d4fddfa663c8adeacea3b1daf56ec5fbe63ecaaf68e5477ac96dc4fb2e28d363": (
"5a33eec77e2ba53ed9b83a8242633b8ae11873d3b4f8cfd257abbb2eb61b6220",
"e7710c4e3309b25517f5bace697c0c2574a5ce219f3cb156d352f739c178d75f",
),
}
ORIGINAL_BEGIN = " noteDelay = 0;\n"
PATCHED_BEGIN = " noteDelay = millis();\n"
ORIGINAL_COMPARE = " if (m < noteDelay)\n"
PATCHED_COMPARE = " if (static_cast<int32_t>(static_cast<uint32_t>(m - noteDelay)) < 0)\n"
def digest(source):
return hashlib.sha256(source.encode("utf-8")).hexdigest()
def reviewed_pair(source):
source_hash = digest(source.replace("\r\n", "\n"))
if source_hash in REVIEWED:
return REVIEWED[source_hash]
for header_hash, patched_hash in REVIEWED.values():
if source_hash == patched_hash:
return header_hash, patched_hash
raise RuntimeError("RTTTL timing fix: changed reviewed source; review dependency update")
def patched_source(source):
source = source.replace("\r\n", "\n")
_, patched_hash = reviewed_pair(source)
if digest(source) == patched_hash:
return source
if source.count(ORIGINAL_BEGIN) != 1 or source.count(ORIGINAL_COMPARE) != 1:
raise RuntimeError("RTTTL timing fix: changed reviewed timing statements")
source = source.replace(ORIGINAL_BEGIN, PATCHED_BEGIN, 1)
source = source.replace(ORIGINAL_COMPARE, PATCHED_COMPARE, 1)
# Version 1.4.0 has a degree symbol only in a disabled debug string.
# Keep generated project-private source ASCII without changing playback.
source = source.replace("\u00b0", "")
if digest(source) != patched_hash:
raise RuntimeError("RTTTL timing fix: unexpected transform result")
source.encode("ascii")
return source
def write_if_changed(path, content):
if not path.exists() or path.read_text(encoding="ascii") != content:
path.write_text(content, encoding="ascii")
def replace_rtttl_source(build_env, node):
source = Path(node.srcnode().get_abspath())
if source.name != "NonBlockingRtttl.cpp" or source.parent.name != "src":
return node
original = source.read_text(encoding="utf-8")
header_hash, _ = reviewed_pair(original)
try:
header = source.with_suffix(".h").read_text(encoding="ascii").replace("\r\n", "\n")
except (OSError, UnicodeError) as error:
raise RuntimeError("RTTTL timing fix: missing or changed reviewed header") from error
if digest(header) != header_hash:
raise RuntimeError("RTTTL timing fix: changed reviewed header; review dependency update")
patched = patched_source(original)
destination = Path(build_env.subst("$BUILD_DIR")) / "patched-rtttl" / source.name
destination.parent.mkdir(parents=True, exist_ok=True)
# A quoted include must resolve to the reviewed matching header, not a
# same-named library earlier in the build clone's include path.
write_if_changed(destination.with_suffix(".h"), header)
write_if_changed(destination, patched)
return build_env.File(str(destination))
def install(build_env):
build_env.AddBuildMiddleware(replace_rtttl_source, "*NonBlockingRtttl.cpp")
if "Import" in globals():
Import("env")
install(env)
+9
View File
@@ -0,0 +1,9 @@
# This file contains a list of people who've made contribution to
# the project. People who commit code are encouraged to add
# their names here. Please keep the list sorted by first names.
Antoine Beauchamp <end2endzone@gmail.com>
Chris Dennis <https://github.com/StarsoftAnalysis>
James Brown <https://github.com/jbrown123>
per1234 <https://github.com/per1234>
Erwan d'Orgeville <https://github.com/edorgeville>
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 Antoine Beauchamp
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+22
View File
@@ -0,0 +1,22 @@
NonBlockingRTTTL 1.3.0 regression fixture
Source: https://github.com/end2endzone/NonBlockingRTTTL/tree/1.3.0
The official 1.3.0 tag's source is vendored as ASCII JSON for offline tests.
Decode the "source" field in src/NonBlockingRtttl.cpp.json to recover the
exact reviewed upstream CPP. The header is unchanged. LICENSE and AUTHORS
retain the original MIT attribution.
No firmware code is linked from this directory.
Canonical normalization is LF line endings and ASCII-only text. The upstream
tag already has both, so no bytes or executable statements are changed.
Unpatched CPP SHA256:
b50c159c8806d177398972bf8f94ca762532a855b60cb1d3aa9c3ebf021423ca
Patched private CPP SHA256:
29204341d99b8adeef501c0d4cb47a4137534ebe6a30f2e6b3032d558aeef802
Unchanged header SHA256:
abea0a4870e58222fdefd1e88d291136518912e83880af30d1c85d39d7bd5cc1
LICENSE SHA256:
44bbc2541c595d5d8790f9b0be9a56fc88c9a500ab6bd2a88b4e00f8e28f0585
Original AUTHORS SHA256 (trailing whitespace trimmed in copy):
35b555001d2fd35c64002cb517bd1abce2d502b88540aff61d6ae5387b697c45
@@ -0,0 +1,3 @@
{
"source": "// ---------------------------------------------------------------------------\n// Created by Antoine Beauchamp based on the code from the ToneLibrary\n//\n// See \"NonBlockingRtttl.h\" for purpose, syntax, version history, links, and more.\n// ---------------------------------------------------------------------------\n\n#include \"Arduino.h\"\n#include \"NonBlockingRtttl.h\"\n\n/*********************************************************\n * RTTTL Library data\n *********************************************************/\n\nnamespace rtttl\n{\n\nconst int notes[] = { 0,\nNOTE_C4, NOTE_CS4, NOTE_D4, NOTE_DS4, NOTE_E4, NOTE_F4, NOTE_FS4, NOTE_G4, NOTE_GS4, NOTE_A4, NOTE_AS4, NOTE_B4,\nNOTE_C5, NOTE_CS5, NOTE_D5, NOTE_DS5, NOTE_E5, NOTE_F5, NOTE_FS5, NOTE_G5, NOTE_GS5, NOTE_A5, NOTE_AS5, NOTE_B5,\nNOTE_C6, NOTE_CS6, NOTE_D6, NOTE_DS6, NOTE_E6, NOTE_F6, NOTE_FS6, NOTE_G6, NOTE_GS6, NOTE_A6, NOTE_AS6, NOTE_B6,\nNOTE_C7, NOTE_CS7, NOTE_D7, NOTE_DS7, NOTE_E7, NOTE_F7, NOTE_FS7, NOTE_G7, NOTE_GS7, NOTE_A7, NOTE_AS7, NOTE_B7\n};\n\n//#define isdigit(n) (n >= '0' && n <= '9')\n#define OCTAVE_OFFSET 0\n\nconst char * buffer = \"\";\nint bufferIndex = -32760;\nbyte default_dur = 4;\nbyte default_oct = 5;\nint bpm = 63;\nlong wholenote;\nbyte pin = -1;\nunsigned long noteDelay = 0; //m will always be after which means the last note is done playing\nbool playing = false;\n\n//pre-declaration\nvoid nextnote();\n\n// tone() and noTone() are not implemented for Arduino core for the ESP32\n// See https://github.com/espressif/arduino-esp32/issues/980\n// and https://github.com/espressif/arduino-esp32/issues/1720\n#if defined(ESP32)\nvoid noTone(){\n ledcWrite(0, 0); // channel, volume\n}\n\nvoid noTone(int pin){\n noTone();\n}\n\nvoid tone(int frq) {\n ledcWriteTone(0, frq); // channel, freq\n ledcWrite(0, 255); // channel, volume\n}\n\nvoid tone(int pin, int frq, int duration){\n tone(frq);\n}\n#endif\n\nvoid begin(byte iPin, const char * iSongBuffer)\n{\n #ifdef RTTTL_NONBLOCKING_DEBUG\n Serial.print(\"playing: \");\n Serial.println(iSongBuffer);\n #endif\n \n //init values\n pin = iPin;\n #if defined(ESP32)\n ledcSetup(0, 1000, 10); // resolution always seems to be 10bit, no matter what is given\n ledcAttachPin(pin, 0);\n #endif\n buffer = iSongBuffer;\n bufferIndex = 0;\n default_dur = 4;\n default_oct = 6;\n bpm=63;\n playing = true;\n noteDelay = 0;\n #ifdef RTTTL_NONBLOCKING_DEBUG\n Serial.print(\"noteDelay=\");\n Serial.println(noteDelay);\n #endif\n \n //stop current note\n noTone(pin);\n\n //read buffer until first note\n int num;\n\n // format: d=N,o=N,b=NNN:\n // find the start (skip name, etc)\n\n while(*buffer != ':') buffer++; // ignore name\n buffer++; // skip ':'\n\n // get default duration\n if(*buffer == 'd')\n {\n buffer++; buffer++; // skip \"d=\"\n num = 0;\n while(isdigit(*buffer))\n {\n num = (num * 10) + (*buffer++ - '0');\n }\n if(num > 0) default_dur = num;\n buffer++; // skip comma\n }\n\n #ifdef RTTTL_NONBLOCKING_INFO\n Serial.print(\"ddur: \"); Serial.println(default_dur, 10);\n #endif\n \n // get default octave\n if(*buffer == 'o')\n {\n buffer++; buffer++; // skip \"o=\"\n num = *buffer++ - '0';\n if(num >= 3 && num <=7) default_oct = num;\n buffer++; // skip comma\n }\n\n #ifdef RTTTL_NONBLOCKING_INFO\n Serial.print(\"doct: \"); Serial.println(default_oct, 10);\n #endif\n \n // get BPM\n if(*buffer == 'b')\n {\n buffer++; buffer++; // skip \"b=\"\n num = 0;\n while(isdigit(*buffer))\n {\n num = (num * 10) + (*buffer++ - '0');\n }\n bpm = num;\n buffer++; // skip colon\n }\n\n #ifdef RTTTL_NONBLOCKING_INFO\n Serial.print(\"bpm: \"); Serial.println(bpm, 10);\n #endif\n\n // BPM usually expresses the number of quarter notes per minute\n wholenote = (60 * 1000L / bpm) * 4; // this is the time for whole note (in milliseconds)\n\n #ifdef RTTTL_NONBLOCKING_INFO\n Serial.print(\"wn: \"); Serial.println(wholenote, 10);\n #endif\n}\n\nvoid nextnote()\n{\n long duration;\n byte note;\n byte scale;\n\n //stop current note\n noTone(pin);\n\n // first, get note duration, if available\n int num = 0;\n while(isdigit(*buffer))\n {\n num = (num * 10) + (*buffer++ - '0');\n }\n \n if(num) duration = wholenote / num;\n else duration = wholenote / default_dur; // we will need to check if we are a dotted note after\n\n // now get the note\n note = 0;\n\n switch(*buffer)\n {\n case 'c':\n note = 1;\n break;\n case 'd':\n note = 3;\n break;\n case 'e':\n note = 5;\n break;\n case 'f':\n note = 6;\n break;\n case 'g':\n note = 8;\n break;\n case 'a':\n note = 10;\n break;\n case 'b':\n note = 12;\n break;\n case 'p':\n default:\n note = 0;\n Line truncated
}
@@ -0,0 +1,161 @@
// ---------------------------------------------------------------------------
// AUTHOR/LICENSE:
// The following code was written by Antoine Beauchamp. For other authors, see AUTHORS file.
// The code & updates for the library can be found at http://github.com/end2endzone/NonBlockingRTTTL
// Original source code for the RTTL player: https://code.google.com/archive/p/rogue-code/wikis/ToneLibraryDocumentation.wiki
// MIT License: http://www.opensource.org/licenses/mit-license.php
// ---------------------------------------------------------------------------
#ifndef NonBlockingRtttl_h
#define NonBlockingRtttl_h
#define NONBLOCKINGRTTTL_VERSION 1.3.0
#include "Arduino.h"
//if notes are not already defined
#ifndef NOTE_B0
#define NOTE_H 0
#define NOTE_B0 31
#define NOTE_C1 33
#define NOTE_CS1 35
#define NOTE_D1 37
#define NOTE_DS1 39
#define NOTE_E1 41
#define NOTE_F1 44
#define NOTE_FS1 46
#define NOTE_G1 49
#define NOTE_GS1 52
#define NOTE_A1 55
#define NOTE_AS1 58
#define NOTE_B1 62
#define NOTE_C2 65
#define NOTE_CS2 69
#define NOTE_D2 73
#define NOTE_DS2 78
#define NOTE_E2 82
#define NOTE_F2 87
#define NOTE_FS2 93
#define NOTE_G2 98
#define NOTE_GS2 104
#define NOTE_A2 110
#define NOTE_AS2 117
#define NOTE_B2 123
#define NOTE_C3 131
#define NOTE_CS3 139
#define NOTE_D3 147
#define NOTE_DS3 156
#define NOTE_E3 165
#define NOTE_F3 175
#define NOTE_FS3 185
#define NOTE_G3 196
#define NOTE_GS3 208
#define NOTE_A3 220
#define NOTE_AS3 233
#define NOTE_B3 247
#define NOTE_C4 262
#define NOTE_CS4 277
#define NOTE_D4 294
#define NOTE_DS4 311
#define NOTE_E4 330
#define NOTE_F4 349
#define NOTE_FS4 370
#define NOTE_G4 392
#define NOTE_GS4 415
#define NOTE_A4 440
#define NOTE_AS4 466
#define NOTE_B4 494
#define NOTE_C5 523
#define NOTE_CS5 554
#define NOTE_D5 587
#define NOTE_DS5 622
#define NOTE_E5 659
#define NOTE_F5 698
#define NOTE_FS5 740
#define NOTE_G5 784
#define NOTE_GS5 831
#define NOTE_A5 880
#define NOTE_AS5 932
#define NOTE_B5 988
#define NOTE_C6 1047
#define NOTE_CS6 1109
#define NOTE_D6 1175
#define NOTE_DS6 1245
#define NOTE_E6 1319
#define NOTE_F6 1397
#define NOTE_FS6 1480
#define NOTE_G6 1568
#define NOTE_GS6 1661
#define NOTE_A6 1760
#define NOTE_AS6 1865
#define NOTE_B6 1976
#define NOTE_C7 2093
#define NOTE_CS7 2217
#define NOTE_D7 2349
#define NOTE_DS7 2489
#define NOTE_E7 2637
#define NOTE_F7 2794
#define NOTE_FS7 2960
#define NOTE_G7 3136
#define NOTE_GS7 3322
#define NOTE_A7 3520
#define NOTE_AS7 3729
#define NOTE_B7 3951
#define NOTE_C8 4186
#define NOTE_CS8 4435
#define NOTE_D8 4699
#define NOTE_DS8 4978
//if notes are not already defined
#endif
//#define RTTTL_NONBLOCKING_DEBUG
//#define RTTTL_NONBLOCKING_INFO
namespace rtttl
{
/****************************************************************************
* Description:
* begin() setups the NON-BLOCKING RTTTL library for
* decoding a new RTTTL song.
* Parameters:
* iPin: The pin which is connected to the piezo buffer.
* iSongBuffer: The string buffer of the RTTTL song.
****************************************************************************/
void begin(byte iPin, const char * iSongBuffer);
/****************************************************************************
* Description:
* play() automatically plays a new note when required.
* This function must constantly be called within the loop() function.
* Warning: inserting too long delays within the loop function may
* disrupt the NON-BLOCKING RTTTL library from playing properly.
****************************************************************************/
void play();
/****************************************************************************
* Description:
* stop() stops playing the current song.
****************************************************************************/
void stop();
/****************************************************************************
* Description:
* isPlaying() return true when the library is playing
* the given RTTTL song.
****************************************************************************/
bool isPlaying();
/****************************************************************************
* Description:
* done() return true when the library is done playing
* the given RTTTL song.
****************************************************************************/
bool done();
}; //namespace
//NonBlockingRtttl_h
#endif
+10
View File
@@ -0,0 +1,10 @@
# This file contains a list of people who've made contribution to
# the project. People who commit code are encouraged to add
# their names here. Please keep the list sorted by first names.
Antoine Beauchamp <end2endzone@gmail.com>
Chris Dennis <https://github.com/StarsoftAnalysis>
James Brown <https://github.com/jbrown123>
per1234 <https://github.com/per1234>
Erwan d'Orgeville <https://github.com/edorgeville>
Emanuele Palombo <https://github.com/elbowz>
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 Antoine Beauchamp
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+26
View File
@@ -0,0 +1,26 @@
NonBlockingRTTTL 1.4.0 regression fixture
Source: PlatformIO end2endzone/NonBlockingRTTTL 1.4.0
Upstream: https://github.com/end2endzone/NonBlockingRTTTL
The installed library's source is vendored as ASCII JSON for offline tests.
Decode the "source" field in src/NonBlockingRtttl.cpp.json to recover the
exact reviewed upstream CPP, including its degree symbol. The header is
unchanged. LICENSE and AUTHORS retain the original MIT attribution.
No firmware code is linked from this directory.
Canonical source normalization for hash comparison is LF line endings.
The fixture file uses a JSON escape for the single U+00B0 degree symbol in
the disabled diagnostic string "Loop n". The production private-build patch
removes that diagnostic-only symbol to make generated copies ASCII as well.
No executable statement in the unpatched fixture is changed.
Original installed CPP SHA256:
d4fddfa663c8adeacea3b1daf56ec5fbe63ecaaf68e5477ac96dc4fb2e28d363
Patched private CPP SHA256:
e7710c4e3309b25517f5bace697c0c2574a5ce219f3cb156d352f739c178d75f
Unchanged header SHA256:
5a33eec77e2ba53ed9b83a8242633b8ae11873d3b4f8cfd257abbb2eb61b6220
LICENSE SHA256:
44bbc2541c595d5d8790f9b0be9a56fc88c9a500ab6bd2a88b4e00f8e28f0585
Original AUTHORS SHA256 (trailing whitespace trimmed in copy):
5ee7d6fd1d7730b5d9184accadc45ec5ee67e57bca50ed1729d0043acf8ff303
@@ -0,0 +1,3 @@
{
"source": "// ---------------------------------------------------------------------------\n// Created by Antoine Beauchamp based on the code from the ToneLibrary\n//\n// See \"NonBlockingRtttl.h\" for purpose, syntax, version history, links, and more.\n// ---------------------------------------------------------------------------\n\n#include \"Arduino.h\"\n#include \"NonBlockingRtttl.h\"\n\n/*********************************************************\n * RTTTL Library data\n *********************************************************/\n\nnamespace rtttl\n{\n\nconst int notes[] = { 0,\nNOTE_C4, NOTE_CS4, NOTE_D4, NOTE_DS4, NOTE_E4, NOTE_F4, NOTE_FS4, NOTE_G4, NOTE_GS4, NOTE_A4, NOTE_AS4, NOTE_B4,\nNOTE_C5, NOTE_CS5, NOTE_D5, NOTE_DS5, NOTE_E5, NOTE_F5, NOTE_FS5, NOTE_G5, NOTE_GS5, NOTE_A5, NOTE_AS5, NOTE_B5,\nNOTE_C6, NOTE_CS6, NOTE_D6, NOTE_DS6, NOTE_E6, NOTE_F6, NOTE_FS6, NOTE_G6, NOTE_GS6, NOTE_A6, NOTE_AS6, NOTE_B6,\nNOTE_C7, NOTE_CS7, NOTE_D7, NOTE_DS7, NOTE_E7, NOTE_F7, NOTE_FS7, NOTE_G7, NOTE_GS7, NOTE_A7, NOTE_AS7, NOTE_B7\n};\n\n//#define isdigit(n) (n >= '0' && n <= '9')\n#define OCTAVE_OFFSET 0\n\nconst char * buffer = \"\";\nconst char * firstNote = \"\";\nint bufferIndex = -32760;\nbyte default_dur = 4;\nbyte default_oct = 5;\nint bpm = 63;\nlong wholenote;\nbyte pin = -1;\nunsigned long noteDelay = 0; //m will always be after which means the last note is done playing\nunsigned long loopGap;\nbyte loopCount;\nbool playing = false;\n\n//pre-declaration\nvoid nextnote();\n\n#ifdef ESP32\n#ifdef ESP_ARDUINO_VERSION\n #if ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(3, 0, 0)\n // Code specific to ESP32 core 3.x\n // Core 3.x manages channels automatically:\n // In all esp32 functions, input parameter 'channel' has been changed to 'pin'.\n // See official documentation for migrating from core 2.x to 3.x:\n // https://docs.espressif.com/projects/arduino-esp32/en/latest/migration_guides/2.x_to_3.0.html\n\n #define LEDC_RESOLUTION 10\n\n // Function noTone() stop the PWM signal for the given pin.\n // It does not detach the pin from it's assigned channel.\n // You can have sequential calls to tone() and noTone()\n // without having to call toneSetup() between calls.\n void noTone(uint8_t pin) {\n ledcWriteTone(pin, 0);\n }\n\n // Function tone() set a pin to output a PWM signal that matches the given frequency.\n // The duration argument is ignored. The function signature \n // matches arduino's tone() function for compatibility reasons.\n // Arduino tone() function:\n // https://docs.arduino.cc/language-reference/en/functions/advanced-io/tone/\n // ESP32 ledcWriteTone() function:\n // https://github.com/espressif/arduino-esp32/blob/2.0.17/cores/esp32/esp32-hal-ledc.c#L118\n void tone(uint8_t pin, unsigned int frq, unsigned long duration) {\n // don't care about the given duration\n ledcWriteTone(pin, frq);\n }\n\n // Function toneSetup() setup the given pin to output a PWM signal for generating tones with a piezo buzzer.\n void toneSetup(uint8_t pin) {\n ledcAttach(pin, 1000, LEDC_RESOLUTION);\n }\n \n #elif ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(2, 0, 0)\n // Code specific to ESP32 core 2.x\n // Core 2.x uses channels instead of pins:\n // In all esp32 functions, input parameter is a 'channel' instead of a given pin.\n // This library uses the function getEsp32ChannelForPin() to map a pin to a channel number.\n // This channel is then used with all esp32 apis functions.\n // See official documentation for migrating from core 2.x to 3.x:\n // https://docs.espressif.com/projects/arduino-esp32/en/latest/migration_guides/2.x_to_3.0.html\n\n #define ESP32_INVALID_CHANNEL 0xFF\n\n // Function getEsp32ChannelForPin() maps a given pin to a esp32 channel.\n // Returns a value between 0 and n where n is the maximum of channel for your board.\n // Returns ESP32_INVALID_CHANNEL if there is no assigned channel for the given pin number.\n // See your board documentation for details.\n // See https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/peripherals/ledc.html#led-control-ledc\n uint8_t getEsp32ChannelForPin(uint8_t pin) {\n // This library only supports 1 pin at a time.\n // Always returns channel 0\n return 0;\n }\n\n // Function noTone() stop the PWM signal for the given pin.\n // It does not detach the pin from it's assigned channel.\n // You can have sequential calls to tone() and noTone()\n // without having to call toneSetup() between calls.\n void noTone(uint8_t pin) {\n uint8_t channel = getEsp32ChannelForPin(pin);\n ledcWriteTone(channel, 0); // Silence the buzzer without detaching\n }\n\n // Function tone() set a pin to output a PWM signal that matches the given frequency.\n // The duration argument is ignored. The function signature \n // matches arduino's tone() function foLine truncated
}
@@ -0,0 +1,163 @@
// ---------------------------------------------------------------------------
// AUTHOR/LICENSE:
// The following code was written by Antoine Beauchamp. For other authors, see AUTHORS file.
// The code & updates for the library can be found at http://github.com/end2endzone/NonBlockingRTTTL
// Original source code for the RTTL player: https://code.google.com/archive/p/rogue-code/wikis/ToneLibraryDocumentation.wiki
// MIT License: http://www.opensource.org/licenses/mit-license.php
// ---------------------------------------------------------------------------
#ifndef NonBlockingRtttl_h
#define NonBlockingRtttl_h
#define NONBLOCKINGRTTTL_VERSION 1.4.0
#include "Arduino.h"
//if notes are not already defined
#ifndef NOTE_B0
#define NOTE_H 0
#define NOTE_B0 31
#define NOTE_C1 33
#define NOTE_CS1 35
#define NOTE_D1 37
#define NOTE_DS1 39
#define NOTE_E1 41
#define NOTE_F1 44
#define NOTE_FS1 46
#define NOTE_G1 49
#define NOTE_GS1 52
#define NOTE_A1 55
#define NOTE_AS1 58
#define NOTE_B1 62
#define NOTE_C2 65
#define NOTE_CS2 69
#define NOTE_D2 73
#define NOTE_DS2 78
#define NOTE_E2 82
#define NOTE_F2 87
#define NOTE_FS2 93
#define NOTE_G2 98
#define NOTE_GS2 104
#define NOTE_A2 110
#define NOTE_AS2 117
#define NOTE_B2 123
#define NOTE_C3 131
#define NOTE_CS3 139
#define NOTE_D3 147
#define NOTE_DS3 156
#define NOTE_E3 165
#define NOTE_F3 175
#define NOTE_FS3 185
#define NOTE_G3 196
#define NOTE_GS3 208
#define NOTE_A3 220
#define NOTE_AS3 233
#define NOTE_B3 247
#define NOTE_C4 262
#define NOTE_CS4 277
#define NOTE_D4 294
#define NOTE_DS4 311
#define NOTE_E4 330
#define NOTE_F4 349
#define NOTE_FS4 370
#define NOTE_G4 392
#define NOTE_GS4 415
#define NOTE_A4 440
#define NOTE_AS4 466
#define NOTE_B4 494
#define NOTE_C5 523
#define NOTE_CS5 554
#define NOTE_D5 587
#define NOTE_DS5 622
#define NOTE_E5 659
#define NOTE_F5 698
#define NOTE_FS5 740
#define NOTE_G5 784
#define NOTE_GS5 831
#define NOTE_A5 880
#define NOTE_AS5 932
#define NOTE_B5 988
#define NOTE_C6 1047
#define NOTE_CS6 1109
#define NOTE_D6 1175
#define NOTE_DS6 1245
#define NOTE_E6 1319
#define NOTE_F6 1397
#define NOTE_FS6 1480
#define NOTE_G6 1568
#define NOTE_GS6 1661
#define NOTE_A6 1760
#define NOTE_AS6 1865
#define NOTE_B6 1976
#define NOTE_C7 2093
#define NOTE_CS7 2217
#define NOTE_D7 2349
#define NOTE_DS7 2489
#define NOTE_E7 2637
#define NOTE_F7 2794
#define NOTE_FS7 2960
#define NOTE_G7 3136
#define NOTE_GS7 3322
#define NOTE_A7 3520
#define NOTE_AS7 3729
#define NOTE_B7 3951
#define NOTE_C8 4186
#define NOTE_CS8 4435
#define NOTE_D8 4699
#define NOTE_DS8 4978
//if notes are not already defined
#endif
//#define RTTTL_NONBLOCKING_DEBUG
//#define RTTTL_NONBLOCKING_INFO
namespace rtttl
{
/****************************************************************************
* Description:
* begin() setups the NON-BLOCKING RTTTL library for
* decoding a new RTTTL song.
* Parameters:
* iPin: The pin which is connected to the piezo buffer.
* iSongBuffer: The string buffer of the RTTTL song.
* iLoopCount: Number of times the RTTTL song will be playback (default = 1).
* iLoopGap: Pause of milliseconds between the song repetition (default = 1000ms).
****************************************************************************/
void begin(byte iPin, const char * iSongBuffer, byte iLoopCount = 1, unsigned long iLoopGap = 1000);
/****************************************************************************
* Description:
* play() automatically plays a new note when required.
* This function must constantly be called within the loop() function.
* Warning: inserting too long delays within the loop function may
* disrupt the NON-BLOCKING RTTTL library from playing properly.
****************************************************************************/
void play();
/****************************************************************************
* Description:
* stop() stops playing the current song.
****************************************************************************/
void stop();
/****************************************************************************
* Description:
* isPlaying() return true when the library is playing
* the given RTTTL song.
****************************************************************************/
bool isPlaying();
/****************************************************************************
* Description:
* done() return true when the library is done playing
* the given RTTTL song.
****************************************************************************/
bool done();
}; //namespace
//NonBlockingRtttl_h
#endif
+22
View File
@@ -0,0 +1,22 @@
#pragma once
typedef unsigned char byte;
typedef unsigned int uint32_t;
typedef int int32_t;
typedef unsigned int size_t;
typedef unsigned char uint8_t;
typedef unsigned short uint16_t;
typedef short int16_t;
typedef signed char int8_t;
typedef unsigned int uintptr_t;
#define LOW 0
#define HIGH 1
#define OUTPUT 1
extern uint32_t now_ms;
extern unsigned tone_calls;
extern unsigned silence_calls;
inline uint32_t millis() { return now_ms; }
inline int isdigit(int c) { return c >= '0' && c <= '9'; }
inline void tone(byte, unsigned int, unsigned long) { ++tone_calls; }
inline void noTone(byte) { ++silence_calls; }
inline void pinMode(int, int) {}
inline void digitalWrite(int, int) {}
+29
View File
@@ -0,0 +1,29 @@
#pragma once
struct Sequence { uintptr_t PTR; uint32_t CNT, REFRESH, ENDDELAY; };
struct Registers {
uint32_t INTENCLR, SHORTS, ENABLE, MODE, COUNTERTOP, PRESCALER,
DECODER, LOOP, EVENTS_STOPPED, EVENTS_LOOPSDONE, INTENSET;
Sequence SEQ[2];
uint32_t EVENTS_SEQEND[2], EVENTS_SEQSTARTED[2], TASKS_SEQSTART[2];
};
static Registers registers;
#define NRF_PWM3 (&registers)
#define PWM_MODE_UPDOWN_Up 0
#define PWM_PRESCALER_PRESCALER_DIV_1 0
#define PWM_DECODER_LOAD_Common 0
#define PWM_SHORTS_LOOPSDONE_SEQSTART0_Msk 4
#define PWM_INTENSET_SEQEND0_Msk 1
#define PWM_INTENSET_SEQEND1_Msk 2
#define PWM3_IRQn 45
inline void NVIC_DisableIRQ(int) {}
inline void NVIC_EnableIRQ(int) {}
inline void NVIC_ClearPendingIRQ(int) {}
inline void NVIC_SetPriority(int, int) {}
struct HardwarePWM {
bool owned;
bool takeOwnership(uint32_t) { if (owned) return false; return owned = true; }
bool addPin(int) { return true; }
void removePin(int) {}
bool releaseOwnership(uint32_t) { owned = false; return true; }
};
static HardwarePWM HwPWM3;
+46
View File
@@ -0,0 +1,46 @@
#define PIN_BUZZER 25
#define MESH_GPS_VOICE 1
#include <helpers/ui/buzzer.cpp>
static_assert(sizeof(unsigned long) == 4, "Use device-width unsigned long");
uint32_t now_ms = 0;
unsigned tone_calls = 0;
unsigned silence_calls = 0;
extern "C" void* memset(void* dst, int value, unsigned length) {
unsigned char* bytes = static_cast<unsigned char*>(dst);
for (unsigned i = 0; i < length; ++i) bytes[i] = static_cast<unsigned char>(value);
return dst;
}
static void quit(unsigned code) {
__asm__ volatile("int $0x80" : : "a"(1), "b"(code));
__builtin_unreachable();
}
static void check(bool okay, unsigned line) {
if (!okay) quit(line);
}
#define CHECK(expression) check((expression), __LINE__)
extern "C" void _start() {
genericBuzzer buzzer;
buzzer.begin();
now_ms = 0xffffffffU - 100;
CHECK(buzzer.speakGps(true));
buzzer.loop();
CHECK(tone_calls == 1 && buzzer.isPlaying() && !registers.ENABLE);
now_ms = 0xffffffffU - 10;
buzzer.loop();
CHECK(tone_calls == 1 && buzzer.isPlaying());
now_ms = 20;
buzzer.loop();
CHECK(tone_calls == 2 && buzzer.isPlaying() && !registers.ENABLE);
now_ms += 94; buzzer.loop();
CHECK(tone_calls == 3 && !registers.ENABLE);
now_ms += 188; buzzer.loop();
CHECK(rtttl::done() && !registers.ENABLE);
buzzer.loop(); // Start the intentional speech gap.
now_ms += 99; buzzer.loop();
CHECK(!registers.ENABLE);
now_ms += 1; buzzer.loop();
CHECK(registers.ENABLE && buzzer.isPlaying() && HwPWM3.owned);
buzzer.stop();
CHECK(!registers.ENABLE && !buzzer.isPlaying() && !HwPWM3.owned);
quit(0);
}
+110
View File
@@ -0,0 +1,110 @@
#include <NonBlockingRtttl.h>
static_assert(sizeof(unsigned long) == 4, "Use device-width unsigned long");
uint32_t now_ms = 0;
unsigned tone_calls = 0;
unsigned silence_calls = 0;
extern unsigned long noteDelay;
static const char rising[] = "Startup:d=4,o=5,b=160:16c6,16e6,8g6";
static const char empty[] = "Empty:d=4,o=5,b=160:";
static void quit(unsigned code) {
__asm__ volatile("int $0x80" : : "a"(1), "b"(code));
__builtin_unreachable();
}
static void check(bool okay, unsigned line) {
if (!okay) quit(line);
}
#define CHECK(expression) check((expression), __LINE__)
static void advance(uint32_t amount) {
now_ms += amount;
rtttl::play();
}
static void checkStartup(uint32_t origin) {
now_ms = origin;
tone_calls = silence_calls = 0;
rtttl::begin(25, rising);
CHECK(rtttl::isPlaying() && tone_calls == 0);
rtttl::play();
CHECK(tone_calls == 1 && rtttl::isPlaying());
for (unsigned i = 0; i < 4; ++i) rtttl::play();
CHECK(tone_calls == 1 && rtttl::isPlaying());
advance(93);
CHECK(tone_calls == 1 && rtttl::isPlaying());
advance(1);
CHECK(tone_calls == 2 && rtttl::isPlaying());
advance(93);
CHECK(tone_calls == 2 && rtttl::isPlaying());
advance(1);
CHECK(tone_calls == 3 && rtttl::isPlaying());
advance(187);
CHECK(tone_calls == 3 && rtttl::isPlaying());
advance(1);
CHECK(tone_calls == 3 && rtttl::done());
}
extern "C" void _start() {
// Identical note timing, including first note in the upper counter half.
for (unsigned i = 0; i < 5; ++i) {
const uint32_t origins[] = {0, 1000, 0x80000000U, 0xfffffff0U, 0xffffffffU - 100};
checkStartup(origins[i]);
}
// A foreground loop which crosses an expired pre-wrap deadline must
// continue the melody and allow the pending control clip to start.
now_ms = 0xffffffffU - 100;
tone_calls = 0;
rtttl::begin(25, rising);
rtttl::play();
CHECK(tone_calls == 1);
advance(90);
CHECK(tone_calls == 1);
now_ms = 20;
rtttl::play();
CHECK(tone_calls == 2 && rtttl::isPlaying());
advance(94);
CHECK(tone_calls == 3);
advance(188);
CHECK(rtttl::done());
#ifndef RTTTL_NO_LOOPS
// Repeating gap also wraps safely; no new note during the gap.
now_ms = 0xffffffffU - 500;
tone_calls = 0;
rtttl::begin(25, rising, 2, 1000);
rtttl::play(); advance(94); advance(94); advance(188);
CHECK(rtttl::isPlaying() && tone_calls == 3);
advance(999);
CHECK(tone_calls == 3);
advance(1);
CHECK(tone_calls == 4);
advance(94); advance(94); advance(188);
CHECK(rtttl::done() && tone_calls == 6);
#endif
// A rest uses the duration directly, without the audible note's extra ms.
now_ms = 0xfffffff0U;
tone_calls = 0;
rtttl::begin(25, "Rest:d=4,o=5,b=160:16p,16c6");
rtttl::play();
CHECK(tone_calls == 0 && rtttl::isPlaying());
advance(92);
CHECK(tone_calls == 0 && rtttl::isPlaying());
advance(1);
CHECK(tone_calls == 1 && rtttl::isPlaying());
advance(93);
CHECK(rtttl::isPlaying());
advance(1);
CHECK(rtttl::done());
// Empty melody, stop, and replacement do not remain artificially busy.
now_ms = 0xfffffff0U;
tone_calls = 0;
rtttl::begin(25, empty);
rtttl::play(); CHECK(rtttl::done() && tone_calls == 0);
rtttl::begin(25, rising); rtttl::play();
CHECK(tone_calls == 1);
rtttl::stop(); CHECK(rtttl::done());
advance(500); CHECK(tone_calls == 1);
rtttl::begin(25, rising); rtttl::play();
CHECK(tone_calls == 2 && rtttl::isPlaying());
rtttl::stop();
quit(0);
}
+2
View File
@@ -0,0 +1,2 @@
#pragma once
#include "Arduino.h"
+232
View File
@@ -0,0 +1,232 @@
"""Exercise reviewed real RTTTL dependencies with actual 32-bit clock widths.
The freestanding ELF32 binaries do not require 32-bit libc/libstdc++ packages.
Only the clock, pins, IRQ controller and PWM registers are mocked. The queue
test compiles the real buzzer, decoder, clip catalog and voice-player sources.
"""
from pathlib import Path
import importlib.util
import json
import platform
import shutil
import subprocess
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
FIXTURES = Path(__file__).resolve().parent / "fixtures/rtttl_rollover"
SPEC = importlib.util.spec_from_file_location("rtttl_rollover_fix", ROOT / "scripts/rtttl_rollover_fix.py")
FIX = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(FIX)
CXX = shutil.which("g++")
VERSIONS = ("1.3.0", "1.4.0")
PINS = {
"1.3.0": (
"b50c159c8806d177398972bf8f94ca762532a855b60cb1d3aa9c3ebf021423ca",
"abea0a4870e58222fdefd1e88d291136518912e83880af30d1c85d39d7bd5cc1",
"29204341d99b8adeef501c0d4cb47a4137534ebe6a30f2e6b3032d558aeef802",
),
"1.4.0": (
"d4fddfa663c8adeacea3b1daf56ec5fbe63ecaaf68e5477ac96dc4fb2e28d363",
"5a33eec77e2ba53ed9b83a8242633b8ae11873d3b4f8cfd257abbb2eb61b6220",
"e7710c4e3309b25517f5bace697c0c2574a5ce219f3cb156d352f739c178d75f",
),
}
def source_for(version):
path = FIXTURES / version / "src/NonBlockingRtttl.cpp.json"
return json.loads(path.read_text(encoding="ascii"))["source"]
def header_for(version):
return (FIXTURES / version / "src/NonBlockingRtttl.h").read_text(encoding="ascii")
class Node:
def __init__(self, path):
self.path = Path(path)
def srcnode(self):
return self
def get_abspath(self):
return str(self.path)
class BuildEnv:
def __init__(self, build):
self.build = Path(build)
self.registrations = []
def subst(self, value):
assert value == "$BUILD_DIR"
return str(self.build)
def File(self, value):
return Path(value)
def AddBuildMiddleware(self, callback, pattern):
self.registrations.append((callback, pattern))
class TransformTests(unittest.TestCase):
def test_fixtures_are_ascii_licensed_and_exact_reviewed_bytes_after_json_decode(self):
for path in FIXTURES.rglob("*"):
if path.is_file():
path.read_text(encoding="ascii")
for version in VERSIONS:
with self.subTest(version=version):
source, header = source_for(version), header_for(version)
expected_source, expected_header, _ = PINS[version]
self.assertEqual(FIX.digest(source), expected_source)
self.assertEqual(FIX.digest(header), expected_header)
self.assertIn("#define NONBLOCKINGRTTTL_VERSION " + version, header)
license_text = (FIXTURES / version / "LICENSE").read_text(encoding="ascii")
self.assertIn("MIT License", license_text)
self.assertIn("Copyright (c) 2018 Antoine Beauchamp", license_text)
self.assertIn("Permission is hereby granted, free of charge", license_text)
self.assertIn("THE SOFTWARE IS PROVIDED", license_text)
self.assertIn("Antoine Beauchamp", (FIXTURES / version / "AUTHORS").read_text())
def test_both_reviewed_versions_raw_crlf_idempotence_and_expected_patch_digest(self):
for version in VERSIONS:
with self.subTest(version=version):
original = source_for(version)
patched = FIX.patched_source(original)
self.assertEqual(FIX.digest(patched), PINS[version][2])
patched.encode("ascii")
self.assertEqual(FIX.patched_source(original.replace("\n", "\r\n")), patched)
self.assertEqual(FIX.patched_source(patched), patched)
self.assertEqual(FIX.patched_source(patched.replace("\n", "\r\n")), patched)
self.assertEqual(patched.count(FIX.PATCHED_BEGIN), 1)
self.assertEqual(patched.count(FIX.PATCHED_COMPARE), 1)
def test_unknown_changed_or_half_patched_sources_fail_closed(self):
for version in VERSIONS:
original = source_for(version)
for changed in (
original + "\n", original.replace("bpm=63", "bpm=64", 1),
original.replace(FIX.ORIGINAL_BEGIN, FIX.PATCHED_BEGIN, 1),
original.replace(FIX.ORIGINAL_COMPARE, FIX.PATCHED_COMPARE, 1),
):
with self.subTest(version=version), self.assertRaisesRegex(RuntimeError, "changed reviewed source"):
FIX.patched_source(changed)
def test_middleware_registers_only_expected_source_filter(self):
env = BuildEnv("unused")
FIX.install(env)
self.assertEqual(env.registrations, [(FIX.replace_rtttl_source, "*NonBlockingRtttl.cpp")])
def test_unrelated_source_is_unchanged_and_not_read(self):
with tempfile.TemporaryDirectory() as temporary:
env = BuildEnv(Path(temporary) / "build")
for path in ("src/not_rtttl.cpp", "other/NonBlockingRtttl.cpp"):
node = Node(Path(temporary) / path)
self.assertIs(FIX.replace_rtttl_source(env, node), node)
self.assertFalse(env.build.exists())
def test_private_source_and_header_are_paired_idempotent_and_leave_shared_files_unchanged(self):
for version in VERSIONS:
for crlf in (False, True):
with self.subTest(version=version, crlf=crlf), tempfile.TemporaryDirectory() as temporary:
directory = Path(temporary)
source = directory / "libdeps/NonBlockingRTTTL/src/NonBlockingRtttl.cpp"
source.parent.mkdir(parents=True)
original, header = source_for(version), header_for(version)
if crlf:
original, header = original.replace("\n", "\r\n"), header.replace("\n", "\r\n")
source.write_bytes(original.encode("utf-8"))
source.with_suffix(".h").write_bytes(header.encode("ascii"))
before = (source.read_bytes(), source.with_suffix(".h").read_bytes())
env = BuildEnv(directory / "build")
destination = FIX.replace_rtttl_source(env, Node(source))
self.assertEqual(destination, env.build / "patched-rtttl/NonBlockingRtttl.cpp")
self.assertEqual(FIX.digest(destination.read_text(encoding="ascii")), PINS[version][2])
self.assertEqual(destination.with_suffix(".h").read_text(encoding="ascii"), header_for(version))
times = (destination.stat().st_mtime_ns, destination.with_suffix(".h").stat().st_mtime_ns)
self.assertEqual(FIX.replace_rtttl_source(env, Node(source)), destination)
self.assertEqual(times, (destination.stat().st_mtime_ns, destination.with_suffix(".h").stat().st_mtime_ns))
self.assertEqual(before, (source.read_bytes(), source.with_suffix(".h").read_bytes()))
def test_missing_unknown_or_wrong_version_headers_fail_before_creating_private_output(self):
for version in VERSIONS:
other = "1.4.0" if version == "1.3.0" else "1.3.0"
for header in (None, header_for(version) + "\n", header_for(other), "// unknown\n", "\u00b0"):
with self.subTest(version=version, header=header is None), tempfile.TemporaryDirectory() as temporary:
directory = Path(temporary)
source = directory / "libdeps/NonBlockingRTTTL/src/NonBlockingRtttl.cpp"
source.parent.mkdir(parents=True)
source.write_text(source_for(version), encoding="utf-8")
if header is not None:
source.with_suffix(".h").write_text(header, encoding="utf-8")
env = BuildEnv(directory / "build")
with self.assertRaisesRegex(RuntimeError, "reviewed header"):
FIX.replace_rtttl_source(env, Node(source))
self.assertFalse(env.build.exists())
@unittest.skipUnless(CXX and platform.system() == "Linux" and platform.machine() in ("x86_64", "i386", "i686"),
"Freestanding ELF32 tests require Linux x86 and g++")
class RuntimeTests(unittest.TestCase):
def build_run(self, dependency, version, fixture):
with tempfile.TemporaryDirectory() as temporary:
directory = Path(temporary)
source = directory / "NonBlockingRtttl.cpp"
source.write_text(dependency, encoding="utf-8")
source.with_suffix(".h").write_text(header_for(version), encoding="ascii")
binary = directory / "case"
flags = ["-DRTTTL_NO_LOOPS=1"] if version == "1.3.0" else []
subprocess.run([
CXX, "-m32", "-std=c++17", "-Wall", "-Wextra", "-Werror", *flags,
"-fno-exceptions", "-fno-rtti", "-fno-stack-protector", "-nostdlib", "-no-pie",
"-I" + str(FIXTURES), "-I" + str(directory), "-I" + str(ROOT / "src"),
str(FIXTURES / fixture), str(source), "-o", str(binary),
], check=True, capture_output=True, timeout=30)
self.assertEqual(binary.read_bytes()[:5], b"\x7fELF\x01")
return subprocess.run([str(binary)], capture_output=True, timeout=10).returncode
def test_actual_unpatched_versions_reproduce_both_semantic_rollover_failures(self):
for version in VERSIONS:
with self.subTest(version=version):
self.assertEqual(self.build_run(source_for(version), version, "regression.cpp"), 30)
self.assertEqual(self.build_run(source_for(version), version, "queue.cpp"), 33)
def test_patched_versions_keep_tempo_empty_stop_replacement_and_real_voice_queue(self):
for version in VERSIONS:
with self.subTest(version=version):
patched = FIX.patched_source(source_for(version))
self.assertEqual(self.build_run(patched, version, "regression.cpp"), 0)
self.assertEqual(self.build_run(patched, version, "queue.cpp"), 0)
def test_each_timing_fix_is_required_not_merely_accepted_by_source_checks(self):
for version in VERSIONS:
with self.subTest(version=version):
original = source_for(version)
initial_only = original.replace(FIX.ORIGINAL_BEGIN, FIX.PATCHED_BEGIN, 1)
signed_only = original.replace(FIX.ORIGINAL_COMPARE, FIX.PATCHED_COMPARE, 1)
self.assertEqual(self.build_run(initial_only, version, "regression.cpp"), 30)
self.assertEqual(self.build_run(signed_only, version, "regression.cpp"), 28)
def test_private_cpp_quotes_its_paired_header_even_with_poison_include_path_first(self):
for version in VERSIONS:
with self.subTest(version=version), tempfile.TemporaryDirectory() as temporary:
directory = Path(temporary)
original = directory / "libdeps/NonBlockingRTTTL/src/NonBlockingRtttl.cpp"
original.parent.mkdir(parents=True)
original.write_text(source_for(version), encoding="utf-8")
original.with_suffix(".h").write_text(header_for(version), encoding="ascii")
destination = FIX.replace_rtttl_source(BuildEnv(directory / "build"), Node(original))
poison = directory / "poison"
poison.mkdir()
(poison / "NonBlockingRtttl.h").write_text('#error "wrong header selected"\n', encoding="ascii")
subprocess.run([
CXX, "-m32", "-std=c++17", "-Wall", "-Wextra", "-Werror",
"-fno-exceptions", "-fno-rtti", "-fno-stack-protector",
"-I" + str(poison), "-I" + str(FIXTURES),
"-c", str(destination), "-o", str(directory / "dependency.o"),
], check=True, capture_output=True, timeout=30)
if __name__ == "__main__":
unittest.main(verbosity=2)
+88
View File
@@ -0,0 +1,88 @@
"""Resolve tracked RTTTL build hooks, including synthesized Full Companions."""
import json
from pathlib import Path
import subprocess
import tempfile
import unittest
from platformio.project.config import ProjectConfig
ROOT = Path(__file__).resolve().parents[1]
HOOK = "pre:scripts/rtttl_rollover_fix.py"
class RtttlRolloverRecipeTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
# Ignore developer-local overlays when checking the tracked matrix.
cls.config = ProjectConfig(str(ROOT / "platformio.ini"), parse_extra=False)
for path in sorted((ROOT / "variants").glob("*/platformio.ini")):
cls.config.read(str(path), parse_extra=False)
def has_rtttl(self, section):
return any("NonBlockingRTTTL" in dep
for dep in self.config.get(section, "lib_deps", []))
def test_every_resolved_rtttl_recipe_loads_one_fix(self):
tested = []
for section in self.config.sections():
if not section.startswith("env:") or not self.has_rtttl(section):
continue
with self.subTest(target=section):
self.assertEqual(self.config.get(section, "extra_scripts", []).count(HOOK), 1)
tested.append(section)
self.assertGreater(len(tested), 70)
self.assertIn("env:t1000e_companion_radio_usb", tested)
self.assertIn("env:heltec_v4_r8_tft_companion_radio_ble", tested)
def test_all_arduino_platform_bases_propagate_fix(self):
for base in ("esp32_base", "nrf52_base", "rp2040_base", "stm32_base"):
with self.subTest(base=base):
self.assertEqual(self.config.get(base, "extra_scripts", []).count(HOOK), 1)
def test_actual_builder_synthesized_full_targets_retain_their_base_hook(self):
# Feed resolved tracked options into the real builder's target mapper.
# No PlatformIO command, dependency download, compilation or flashing.
options = ["build_flags", "lib_deps", "build_src_filter", "board", "extra_scripts"]
records = [[section, [[key, self.config.get(section, key, [])] for key in options]]
for section in self.config.sections() if section.startswith("env:")]
shell = r'''
source "$1/build_legacy.sh"
pio() { printf 'FORBIDDEN_PLATFORMIO\n' >&2; return 99; }
ALL_PIO_ENVS=(fixture)
PIO_CONFIG_JSON=$(<"$2")
init_project_context
for target in "${SUPPORTED_PIO_ENVS[@]}"; do
if is_companion_radio_full_target "$target"; then
printf '%s\t%s\n' "$target" "${PIO_ENV_BUILD_BASE_BY_NAME[$target]:-$target}"
fi
done
'''
with tempfile.TemporaryDirectory(prefix="rtttl-recipes-") as temporary:
path = Path(temporary) / "config.json"
path.write_text(json.dumps(records, ensure_ascii=True), encoding="ascii")
result = subprocess.run(["bash", "-c", shell, "rtttl-recipes", str(ROOT), str(path)],
cwd=ROOT, capture_output=True, text=True, timeout=30)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertNotIn("FORBIDDEN_PLATFORMIO", result.stdout + result.stderr)
pairs = dict(line.split("\t") for line in result.stdout.splitlines())
self.assertIn("t1000e_companion_radio_full", pairs)
self.assertIn("heltec_v4_r8_tft_companion_radio_full", pairs)
tested = []
for target, base in pairs.items():
section = "env:" + base
if not self.has_rtttl(section):
continue
with self.subTest(target=target, base=base):
self.assertEqual(self.config.get(section, "extra_scripts", []).count(HOOK), 1)
tested.append(target)
self.assertGreater(len(tested), 20)
def test_regression_suites_are_required_in_ci(self):
workflow = (ROOT / ".github/workflows/run-unit-tests.yml").read_text(encoding="ascii")
self.assertIn("python3 -B test/test_rtttl_rollover.py -v", workflow)
self.assertIn("python3 -B test/test_rtttl_rollover_recipes.py -v", workflow)
if __name__ == "__main__":
unittest.main()