align espnow interop and add terminal dtmf tests
This commit is contained in:
@@ -314,10 +314,14 @@ Le rapport HTML sera disponible dans `coverage/html`.
|
||||
Lancez les tests avec PlatformIO :
|
||||
|
||||
```bash
|
||||
pio test
|
||||
bash scripts/test_terminal.sh
|
||||
```
|
||||
|
||||
Puis générez le rapport de couverture.
|
||||
Ce script enchaîne:
|
||||
- Build des tests embarqués sans upload matériel.
|
||||
- Tests hôte DTMF (génération de tonalités synthétiques) en terminal.
|
||||
|
||||
Puis, si besoin, générez le rapport de couverture.
|
||||
|
||||
### Objectif
|
||||
Ces ajouts permettent de valider la robustesse, la gestion mémoire, la sécurité multithread et les interactions entre modules, tout en mesurant la couverture des tests.
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
# Gates & objectifs — Phase suivante RTC_BL_PHONE
|
||||
|
||||
## [2026-02-20] Alignement multi-repos (RTC + Zacus + Kill_LIFE)
|
||||
- Compatibilité ESP-NOW renforcée côté RTC:
|
||||
- commandes runtime `ESPNOW_ON` / `ESPNOW_OFF`
|
||||
- extraction de commande bridge depuis payload JSON imbriqué (`event/message/payload`)
|
||||
- Correctif mesures ESP-NOW:
|
||||
- suppression du double comptage `tx_ok` (ack + callback)
|
||||
- Base méthodologique importée depuis Kill_LIFE:
|
||||
- gates minimales Spec/Build/Test/Release
|
||||
- evidence pack et standards firmware de référence
|
||||
- documentée dans `docs/CROSS_REPO_INTELLIGENCE.md`
|
||||
|
||||
## Gates prioritaires
|
||||
- Extension endpoints HTTP : audio, batterie, rtos, bluetooth, wifi
|
||||
- Sécurisation endpoints : authentification, validation, gestion des droits
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
# Cross-Repo Intelligence Base
|
||||
|
||||
Date: 2026-02-20
|
||||
|
||||
## Repos de référence
|
||||
|
||||
- `electron-rare/RTC_BL_PHONE`:
|
||||
- firmware téléphonie RTC + Bluetooth + WiFi + MQTT + ESP-NOW.
|
||||
- repo d’exécution pour la stack A252.
|
||||
- `electron-rare/le-mystere-professeur-zacus`:
|
||||
- repo scénario/UI avec bridge WiFi/ESP-NOW orienté événements story.
|
||||
- référence pour formats de payload entrants hétérogènes.
|
||||
- `electron-rare/Kill_LIFE`:
|
||||
- repo base méthodologique (agents, gates, evidence pack, standards firmware).
|
||||
- référence process qualité/traçabilité.
|
||||
|
||||
## Standards repris depuis Kill_LIFE
|
||||
|
||||
- Gates minimales obligatoires sur changements firmware:
|
||||
- `Spec` -> `Build` -> `Test` -> `Release`.
|
||||
- Evidence minimum à conserver:
|
||||
- logs build/test,
|
||||
- résultat CI,
|
||||
- artefacts de validation.
|
||||
- Standards firmware:
|
||||
- PlatformIO + Unity,
|
||||
- wrappers autour des drivers,
|
||||
- timeouts/watchdogs pour IO bloquants.
|
||||
|
||||
## Contrat d’interop RTC <-> Zacus
|
||||
|
||||
- Commandes série ESP-NOW alignées:
|
||||
- `ESPNOW_ON`
|
||||
- `ESPNOW_OFF`
|
||||
- `ESPNOW_STATUS`
|
||||
- `ESPNOW_PEER_ADD <mac>`
|
||||
- `ESPNOW_PEER_DEL <mac>`
|
||||
- `ESPNOW_PEER_LIST`
|
||||
- `ESPNOW_SEND <mac|broadcast> <payload>`
|
||||
- Payload entrant bridge accepté par `RTC_BL_PHONE`:
|
||||
- direct: `cmd`, `raw`, `command`, `action`
|
||||
- imbriqué: `event.*`, `message.*`, `payload.*`
|
||||
|
||||
## Règle opérationnelle
|
||||
|
||||
- Toute évolution protocolaire ESP-NOW doit:
|
||||
- être documentée ici + `docs/props.md`,
|
||||
- être testée au minimum par build terminal + smoke host,
|
||||
- être reflétée dans une PR liée côté repo partenaire si impact croisé.
|
||||
@@ -8,6 +8,12 @@
|
||||
## ESP-NOW
|
||||
- Contrôle local sans broker, même schéma de payload JSON.
|
||||
- Les événements sont broadcastés à tous les pairs ESP-NOW.
|
||||
- Commandes série runtime disponibles : `ESPNOW_ON`, `ESPNOW_OFF`, `ESPNOW_STATUS`, `ESPNOW_PEER_ADD`, `ESPNOW_PEER_DEL`, `ESPNOW_PEER_LIST`, `ESPNOW_SEND`.
|
||||
- Pour la compatibilité inter-repos (`le-mystere-professeur-zacus`), la commande entrante peut être lue depuis :
|
||||
- `cmd`, `raw`, `command`, `action`
|
||||
- `event.<...>` (objet imbriqué)
|
||||
- `message.<...>` (objet imbriqué)
|
||||
- `payload.<...>` (objet ou texte)
|
||||
|
||||
## DTMF logiciel
|
||||
- Détection Goertzel sur frames audio, publication des chiffres détectés dans les événements MQTT/ESP-NOW.
|
||||
|
||||
Executable
+15
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "${ROOT_DIR}"
|
||||
|
||||
echo "[test_terminal] build unit tests (no upload / no hardware)"
|
||||
platformio test --without-uploading --without-testing -e esp32dev
|
||||
|
||||
echo "[test_terminal] run host dtmf tests"
|
||||
mkdir -p .pio/host
|
||||
c++ -std=c++17 -Wall -Wextra -pedantic -Isrc test/host/test_dtmf_host.cpp src/telephony/DtmfDecoder.cpp -o .pio/host/test_dtmf_host
|
||||
.pio/host/test_dtmf_host
|
||||
|
||||
echo "[test_terminal] all checks passed"
|
||||
+52
-5
@@ -109,6 +109,48 @@ bool splitFirstToken(const String& input, String& first, String& rest) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool extractBridgeCommand(JsonVariantConst payload, String& out_cmd, uint8_t depth = 0) {
|
||||
if (depth > 4U) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (payload.is<const char*>()) {
|
||||
out_cmd = payload.as<const char*>();
|
||||
out_cmd.trim();
|
||||
return !out_cmd.isEmpty();
|
||||
}
|
||||
|
||||
if (!payload.is<JsonObjectConst>()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const char* keys[] = {"cmd", "raw", "command", "action"};
|
||||
for (const char* key : keys) {
|
||||
if (!payload[key].is<const char*>()) {
|
||||
continue;
|
||||
}
|
||||
out_cmd = payload[key].as<const char*>();
|
||||
out_cmd.trim();
|
||||
if (!out_cmd.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!payload["event"].isNull() && extractBridgeCommand(payload["event"], out_cmd, static_cast<uint8_t>(depth + 1U))) {
|
||||
return true;
|
||||
}
|
||||
if (!payload["message"].isNull() &&
|
||||
extractBridgeCommand(payload["message"], out_cmd, static_cast<uint8_t>(depth + 1U))) {
|
||||
return true;
|
||||
}
|
||||
if (!payload["payload"].isNull() &&
|
||||
extractBridgeCommand(payload["payload"], out_cmd, static_cast<uint8_t>(depth + 1U))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
AudioConfig buildI2sConfig(const A252PinsConfig& pins_cfg, const A252AudioConfig& audio_cfg) {
|
||||
AudioConfig cfg;
|
||||
cfg.port = I2S_NUM_0;
|
||||
@@ -534,6 +576,14 @@ void registerCommands() {
|
||||
return makeResponse(ok, "MQTT_PUB");
|
||||
});
|
||||
|
||||
g_dispatcher.registerCommand("ESPNOW_ON", [](const String&) {
|
||||
return makeResponse(g_espnow.begin(g_peer_store), "ESPNOW_ON");
|
||||
});
|
||||
|
||||
g_dispatcher.registerCommand("ESPNOW_OFF", [](const String&) {
|
||||
return makeResponse(g_espnow.stop(), "ESPNOW_OFF");
|
||||
});
|
||||
|
||||
g_dispatcher.registerCommand("ESPNOW_PEER_ADD", [](const String& args) {
|
||||
if (args.isEmpty()) {
|
||||
return makeResponse(false, "ESPNOW_PEER_ADD invalid_mac");
|
||||
@@ -683,11 +733,8 @@ void registerCommands() {
|
||||
}
|
||||
|
||||
void processInboundBridgeCommand(const String& source, const JsonVariantConst& payload) {
|
||||
String cmd = payload["cmd"] | "";
|
||||
if (cmd.isEmpty()) {
|
||||
cmd = payload["raw"] | "";
|
||||
}
|
||||
if (cmd.isEmpty()) {
|
||||
String cmd;
|
||||
if (!extractBridgeCommand(payload, cmd)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,10 @@ EspNowBridge::EspNowBridge() {
|
||||
}
|
||||
|
||||
bool EspNowBridge::begin(const EspNowPeerStore& initial_peers) {
|
||||
if (ready_) {
|
||||
return true;
|
||||
}
|
||||
|
||||
store_ = initial_peers;
|
||||
|
||||
WiFi.mode(WIFI_STA);
|
||||
@@ -33,6 +37,16 @@ bool EspNowBridge::begin(const EspNowPeerStore& initial_peers) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool EspNowBridge::stop() {
|
||||
if (!ready_) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const esp_err_t err = esp_now_deinit();
|
||||
ready_ = false;
|
||||
return err == ESP_OK;
|
||||
}
|
||||
|
||||
void EspNowBridge::tick() {
|
||||
// ESP-NOW uses callbacks, no polling required.
|
||||
}
|
||||
@@ -81,6 +95,7 @@ void EspNowBridge::statusToJson(JsonObject obj) const {
|
||||
obj["tx_fail"] = tx_fail_;
|
||||
obj["rx_count"] = rx_count_;
|
||||
obj["last_rx_mac"] = last_rx_mac_;
|
||||
obj["last_rx_payload"] = last_rx_payload_;
|
||||
|
||||
JsonArray peers = obj["peers"].to<JsonArray>();
|
||||
for (const String& peer : store_.peers) {
|
||||
@@ -155,12 +170,11 @@ bool EspNowBridge::sendToMac(const uint8_t mac[6], const String& payload) {
|
||||
}
|
||||
|
||||
const esp_err_t err = esp_now_send(mac, reinterpret_cast<const uint8_t*>(payload.c_str()), payload.length());
|
||||
if (err == ESP_OK) {
|
||||
tx_ok_++;
|
||||
return true;
|
||||
if (err != ESP_OK) {
|
||||
tx_fail_++;
|
||||
return false;
|
||||
}
|
||||
tx_fail_++;
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void EspNowBridge::onDataRecv(const uint8_t* mac_addr, const uint8_t* data, int len) {
|
||||
|
||||
@@ -16,6 +16,7 @@ public:
|
||||
EspNowBridge();
|
||||
|
||||
bool begin(const EspNowPeerStore& initial_peers);
|
||||
bool stop();
|
||||
void tick();
|
||||
|
||||
bool addPeer(const String& mac);
|
||||
|
||||
@@ -1,13 +1,148 @@
|
||||
#include "DtmfDecoder.h"
|
||||
#include <math.h>
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
|
||||
DtmfDecoder::DtmfDecoder() : onDigit(nullptr) {}
|
||||
namespace {
|
||||
constexpr std::array<double, 4> kLowFreq = {{697.0, 770.0, 852.0, 941.0}};
|
||||
constexpr std::array<double, 4> kHighFreq = {{1209.0, 1336.0, 1477.0, 1633.0}};
|
||||
constexpr char kDigitMap[4][4] = {
|
||||
{'1', '2', '3', 'A'},
|
||||
{'4', '5', '6', 'B'},
|
||||
{'7', '8', '9', 'C'},
|
||||
{'*', '0', '#', 'D'},
|
||||
};
|
||||
constexpr double kPi = 3.14159265358979323846;
|
||||
constexpr double kDominanceRatio = 1.8;
|
||||
|
||||
double goertzelPower(const int16_t* samples, size_t count, double freqHz, uint16_t sampleRateHz) {
|
||||
if (samples == nullptr || count == 0 || sampleRateHz == 0U) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
const double omega = 2.0 * kPi * freqHz / static_cast<double>(sampleRateHz);
|
||||
const double coeff = 2.0 * std::cos(omega);
|
||||
double q0 = 0.0;
|
||||
double q1 = 0.0;
|
||||
double q2 = 0.0;
|
||||
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
q0 = coeff * q1 - q2 + static_cast<double>(samples[i]);
|
||||
q2 = q1;
|
||||
q1 = q0;
|
||||
}
|
||||
return q1 * q1 + q2 * q2 - coeff * q1 * q2;
|
||||
}
|
||||
|
||||
template <size_t N>
|
||||
size_t indexOfMax(const std::array<double, N>& values) {
|
||||
size_t idx = 0;
|
||||
for (size_t i = 1; i < N; ++i) {
|
||||
if (values[i] > values[idx]) {
|
||||
idx = i;
|
||||
}
|
||||
}
|
||||
return idx;
|
||||
}
|
||||
|
||||
template <size_t N>
|
||||
double secondBest(const std::array<double, N>& values, size_t bestIndex) {
|
||||
double second = 0.0;
|
||||
for (size_t i = 0; i < N; ++i) {
|
||||
if (i == bestIndex) {
|
||||
continue;
|
||||
}
|
||||
second = std::max(second, values[i]);
|
||||
}
|
||||
return second;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
DtmfDecoder::DtmfDecoder()
|
||||
: DtmfDecoder(8000U, 160U) {}
|
||||
|
||||
DtmfDecoder::DtmfDecoder(uint16_t sampleRateHz, size_t windowSize)
|
||||
: onDigit(nullptr),
|
||||
sampleRateHz_(sampleRateHz == 0U ? 8000U : sampleRateHz),
|
||||
windowSize_(windowSize < 80U ? 80U : windowSize),
|
||||
lastCandidate_('\0'),
|
||||
stableCount_(0U),
|
||||
latchedDigit_('\0') {}
|
||||
|
||||
void DtmfDecoder::setDigitCallback(DigitCallback cb) {
|
||||
onDigit = cb;
|
||||
}
|
||||
|
||||
void DtmfDecoder::feedAudioSamples(const int16_t* samples, size_t count) {
|
||||
// TODO: Implémentation Goertzel pour détecter DTMF
|
||||
// Si détection, appeler onDigit(digit)
|
||||
char DtmfDecoder::detectDigit(const int16_t* samples, size_t count) const {
|
||||
if (samples == nullptr || count < (windowSize_ / 2U)) {
|
||||
return '\0';
|
||||
}
|
||||
|
||||
std::array<double, 4> lowPower = {{0.0, 0.0, 0.0, 0.0}};
|
||||
std::array<double, 4> highPower = {{0.0, 0.0, 0.0, 0.0}};
|
||||
for (size_t i = 0; i < 4; ++i) {
|
||||
lowPower[i] = goertzelPower(samples, count, kLowFreq[i], sampleRateHz_);
|
||||
highPower[i] = goertzelPower(samples, count, kHighFreq[i], sampleRateHz_);
|
||||
}
|
||||
|
||||
const size_t lowIdx = indexOfMax(lowPower);
|
||||
const size_t highIdx = indexOfMax(highPower);
|
||||
const double lowBest = lowPower[lowIdx];
|
||||
const double highBest = highPower[highIdx];
|
||||
const double lowSecond = secondBest(lowPower, lowIdx);
|
||||
const double highSecond = secondBest(highPower, highIdx);
|
||||
const double lowSum = lowPower[0] + lowPower[1] + lowPower[2] + lowPower[3];
|
||||
const double highSum = highPower[0] + highPower[1] + highPower[2] + highPower[3];
|
||||
|
||||
if (lowBest <= 0.0 || highBest <= 0.0) {
|
||||
return '\0';
|
||||
}
|
||||
if (lowSecond > 0.0 && (lowBest / lowSecond) < kDominanceRatio) {
|
||||
return '\0';
|
||||
}
|
||||
if (highSecond > 0.0 && (highBest / highSecond) < kDominanceRatio) {
|
||||
return '\0';
|
||||
}
|
||||
if ((lowBest / (lowSum + 1.0)) < 0.55 || (highBest / (highSum + 1.0)) < 0.55) {
|
||||
return '\0';
|
||||
}
|
||||
|
||||
return kDigitMap[lowIdx][highIdx];
|
||||
}
|
||||
|
||||
void DtmfDecoder::feedAudioSamples(const int16_t* samples, size_t count) {
|
||||
if (samples == nullptr || count == 0U) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (size_t offset = 0; offset < count; offset += windowSize_) {
|
||||
const size_t frameSize = std::min(windowSize_, count - offset);
|
||||
if (frameSize < (windowSize_ / 2U)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const char candidate = detectDigit(samples + offset, frameSize);
|
||||
if (candidate == '\0') {
|
||||
lastCandidate_ = '\0';
|
||||
stableCount_ = 0U;
|
||||
latchedDigit_ = '\0';
|
||||
continue;
|
||||
}
|
||||
|
||||
if (candidate == lastCandidate_) {
|
||||
if (stableCount_ < 255U) {
|
||||
++stableCount_;
|
||||
}
|
||||
} else {
|
||||
lastCandidate_ = candidate;
|
||||
stableCount_ = 1U;
|
||||
}
|
||||
|
||||
if (stableCount_ >= 2U && candidate != latchedDigit_) {
|
||||
latchedDigit_ = candidate;
|
||||
if (onDigit) {
|
||||
onDigit(candidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
#pragma once
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <vector>
|
||||
|
||||
class DtmfDecoder {
|
||||
public:
|
||||
using DigitCallback = std::function<void(char)>;
|
||||
DtmfDecoder();
|
||||
explicit DtmfDecoder(uint16_t sampleRateHz, size_t windowSize = 160);
|
||||
void feedAudioSamples(const int16_t* samples, size_t count);
|
||||
void setDigitCallback(DigitCallback cb);
|
||||
|
||||
private:
|
||||
char detectDigit(const int16_t* samples, size_t count) const;
|
||||
DigitCallback onDigit;
|
||||
// ...internals Goertzel...
|
||||
uint16_t sampleRateHz_;
|
||||
size_t windowSize_;
|
||||
char lastCandidate_;
|
||||
uint8_t stableCount_;
|
||||
char latchedDigit_;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
#include "telephony/DtmfDecoder.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
constexpr double kPi = 3.14159265358979323846;
|
||||
|
||||
std::vector<int16_t> generateDtmfTone(double lowFreqHz,
|
||||
double highFreqHz,
|
||||
int sampleRateHz,
|
||||
double durationSeconds,
|
||||
int16_t amplitude = 12000) {
|
||||
const size_t sampleCount = static_cast<size_t>(durationSeconds * static_cast<double>(sampleRateHz));
|
||||
std::vector<int16_t> samples(sampleCount, 0);
|
||||
for (size_t i = 0; i < sampleCount; ++i) {
|
||||
const double t = static_cast<double>(i) / static_cast<double>(sampleRateHz);
|
||||
const double value = 0.5 * (std::sin(2.0 * kPi * lowFreqHz * t) + std::sin(2.0 * kPi * highFreqHz * t));
|
||||
samples[i] = static_cast<int16_t>(std::round(static_cast<double>(amplitude) * value));
|
||||
}
|
||||
return samples;
|
||||
}
|
||||
|
||||
std::vector<int16_t> generateSingleTone(double freqHz,
|
||||
int sampleRateHz,
|
||||
double durationSeconds,
|
||||
int16_t amplitude = 12000) {
|
||||
const size_t sampleCount = static_cast<size_t>(durationSeconds * static_cast<double>(sampleRateHz));
|
||||
std::vector<int16_t> samples(sampleCount, 0);
|
||||
for (size_t i = 0; i < sampleCount; ++i) {
|
||||
const double t = static_cast<double>(i) / static_cast<double>(sampleRateHz);
|
||||
const double value = std::sin(2.0 * kPi * freqHz * t);
|
||||
samples[i] = static_cast<int16_t>(std::round(static_cast<double>(amplitude) * value));
|
||||
}
|
||||
return samples;
|
||||
}
|
||||
|
||||
bool runScenario(const std::string& name, const std::function<bool()>& scenario) {
|
||||
const bool ok = scenario();
|
||||
std::cout << "[host-dtmf] " << name << ": " << (ok ? "PASS" : "FAIL") << std::endl;
|
||||
return ok;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
bool allOk = true;
|
||||
|
||||
allOk &= runScenario("silence_does_not_emit", []() {
|
||||
DtmfDecoder decoder(8000U, 160U);
|
||||
bool emitted = false;
|
||||
decoder.setDigitCallback([&](char) { emitted = true; });
|
||||
std::vector<int16_t> silence(800, 0);
|
||||
decoder.feedAudioSamples(silence.data(), silence.size());
|
||||
return !emitted;
|
||||
});
|
||||
|
||||
allOk &= runScenario("detects_digit_5", []() {
|
||||
DtmfDecoder decoder(8000U, 160U);
|
||||
int callbackCount = 0;
|
||||
char lastDigit = '\0';
|
||||
decoder.setDigitCallback([&](char digit) {
|
||||
++callbackCount;
|
||||
lastDigit = digit;
|
||||
});
|
||||
const std::vector<int16_t> tone = generateDtmfTone(770.0, 1336.0, 8000, 0.12);
|
||||
decoder.feedAudioSamples(tone.data(), tone.size());
|
||||
return callbackCount == 1 && lastDigit == '5';
|
||||
});
|
||||
|
||||
allOk &= runScenario("single_tone_is_rejected", []() {
|
||||
DtmfDecoder decoder(8000U, 160U);
|
||||
bool emitted = false;
|
||||
decoder.setDigitCallback([&](char) { emitted = true; });
|
||||
const std::vector<int16_t> tone = generateSingleTone(770.0, 8000, 0.12);
|
||||
decoder.feedAudioSamples(tone.data(), tone.size());
|
||||
return !emitted;
|
||||
});
|
||||
|
||||
allOk &= runScenario("digit_can_retrigger_after_pause", []() {
|
||||
DtmfDecoder decoder(8000U, 160U);
|
||||
int callbackCount = 0;
|
||||
decoder.setDigitCallback([&](char digit) {
|
||||
if (digit == '5') {
|
||||
++callbackCount;
|
||||
}
|
||||
});
|
||||
|
||||
const std::vector<int16_t> tone = generateDtmfTone(770.0, 1336.0, 8000, 0.12);
|
||||
std::vector<int16_t> payload;
|
||||
payload.reserve(tone.size() * 2 + 320);
|
||||
payload.insert(payload.end(), tone.begin(), tone.end());
|
||||
payload.insert(payload.end(), 320, 0);
|
||||
payload.insert(payload.end(), tone.begin(), tone.end());
|
||||
decoder.feedAudioSamples(payload.data(), payload.size());
|
||||
return callbackCount == 2;
|
||||
});
|
||||
|
||||
return allOk ? 0 : 1;
|
||||
}
|
||||
Reference in New Issue
Block a user