Harden HFP call controls and add runtime peer MAC configuration #3

Merged
electron-rare merged 1 commits from codex/set-up-environment-for-bluetooth-hfp into main 2026-02-13 13:42:47 +00:00
3 changed files with 411 additions and 159 deletions
+27 -20
View File
@@ -1,31 +1,38 @@
# RTC_BL_PHONE
Projet PlatformIO ESP32 pour recycler un téléphone RTC ancien (combiné, clavier, hook).
Projet PlatformIO ESP32 pour recycler un téléphone RTC ancien (combiné, clavier, hook), avec intégration Bluetooth HFP pour les appels (émission/réception).
## Démarrage rapide
1. Ouvrir le dossier dans PlatformIO.
2. Compiler et flasher l'environnement `esp32-s3-devkitc-1` (par défaut).
3. Ouvrir le moniteur série à 115200 bauds.
4. Utiliser les commandes série (`h`, `r`, `o`, `d`, `c`) pour piloter la machine d'états.
2. Option A: renseigner l'adresse MAC dans `src/main.cpp` (`DEFAULT_PEER_ADDR`).
3. Option B: la définir au runtime avec la commande série `p <mac>`.
4. Compiler et flasher l'environnement `esp32dev` (par défaut).
5. Ouvrir le moniteur série à 115200 bauds.
6. Connecter puis piloter les appels via commandes série.
## Commandes série
- `h` : aide
- `s` : statut runtime (hook, HFP, audio, call)
- `p <mac>` : configure la MAC du téléphone (`AA:BB:CC:DD:EE:FF`)
- `b` : connexion HFP vers le téléphone (Audio Gateway)
- `x` : déconnexion HFP
- `m <numero>` : émission d'appel
- `a` : décrocher un appel entrant
- `e` : raccrocher / rejeter
- `v <0..15>` : volume speaker HFP
## Cibles matérielles
- **ESP32 (Classic BT)** : support HFP complet (`esp32dev`).
- **ESP32-S3** : Bluetooth Classic non supporté par le silicium, HFP indisponible (le firmware reste compilable avec messages de fallback).
## Comportement hook/ring
- Si combiné **raccroché** (`ON_HOOK`) : ligne coupée.
- Si appel entrant : `pinRingCmd` activé, sonnerie pilotable côté AG1171S.
- Si décroché pendant sonnerie : `answer` automatique.
- Si raccroché pendant appel : `end/reject` automatique.
## Choix de cartes ESP32
Voir `docs/solutions_rtc_phone_esp32.md` pour la shortlist des DevKit utilisables (ESP32-DevKitC, ESP32-S3-DevKitC-1, NodeMCU-32S, LOLIN32), les liens de référence web, et les solutions dinterface (direct combiné/clavier, SLIC/FXS, ATA externe), dont une variante AG1171S (Silvertel).
## Plan projet (chef de projet)
Voir `docs/plan_chef_projet_esp32s3_ag1171s.md` pour le planning en phases, les risques, les critères d'acceptation et les livrables de la version ESP32-S3 + AG1171S.
## Contenu
- `platformio.ini`: configuration multi-env (ESP32-S3 par défaut + ESP32 legacy).
- `src/main.cpp`: squelette firmware machine d'états pour intégration AG1171S.
- `docs/solutions_rtc_phone_esp32.md`: comparaison des architectures et recommandations.
- `docs/plan_chef_projet_esp32s3_ag1171s.md`: plan d'exécution projet version V0.1.
2. Compiler et flasher l'environnement `esp32dev`.
3. Ouvrir le moniteur série à 115200 bauds.
## Choix de cartes ESP32
Voir `docs/solutions_rtc_phone_esp32.md` pour la shortlist des DevKit utilisables (ESP32-DevKitC, ESP32-S3-DevKitC-1, NodeMCU-32S, LOLIN32) et les liens de référence web.
## Contenu
- `platformio.ini`: configuration initiale du projet.
- `src/main.cpp`: prototype minimal (détection décroché/raccroché).
- `docs/solutions_rtc_phone_esp32.md`: comparaison des meilleures architectures + sélection de DevKit ESP32 recommandés.
+4 -9
View File
@@ -1,22 +1,17 @@
[platformio]
default_envs = esp32-s3-devkitc-1
default_envs = esp32dev
[env]
platform = espressif32
default_envs = esp32dev
[env:esp32dev]
platform = espressif32
board = esp32dev
framework = arduino
monitor_speed = 115200
build_flags =
-DCORE_DEBUG_LEVEL=1
[env:esp32-s3-devkitc-1]
board = esp32-s3-devkitc-1
[env:esp32dev]
board = esp32dev
lib_deps =
bblanchon/ArduinoJson@^7.0.4
[env:esp32-s3-devkitc-1]
board = esp32-s3-devkitc-1
+380 -130
View File
@@ -1,217 +1,467 @@
#include <Arduino.h>
#include <cstdio>
#include "esp_bt.h"
#include "esp_bt_device.h"
#include "esp_bt_main.h"
#include "esp_err.h"
#include "esp_hf_client_api.h"
#include "soc/soc_caps.h"
/*
RTC_BL_PHONE - ESP32-S3 + AG1171S control skeleton
RTC_BL_PHONE - ESP32 + AG1171S + Bluetooth HFP
Goal:
- Provide a practical firmware base for a private analog line project.
- Keep telephony analog front-end on AG1171S side.
- Let ESP32 manage call-state logic and debug observability.
Livrable:
- Appel sortant / entrant via HFP
- Gestion hook (décroché / raccroché)
- Pilotage ring/line enable pour combiné RTC
Notes:
- Pin mapping must be validated against your AG1171S application schematic.
- Never connect to PSTN without compliant isolation/protection design.
Note matériel:
- HFP nécessite Bluetooth Classic => ESP32 (pas ESP32-S3)
*/
enum class PhoneState : uint8_t {
ON_HOOK,
OFF_HOOK,
IDLE,
RINGING,
DIALING,
IN_CALL,
RINGING
};
struct PhonePins {
uint8_t pinHookSense;
uint8_t pinRingCmd;
uint8_t pinLineEnable;
uint8_t pinLed;
uint8_t hookSense;
uint8_t ringCmd;
uint8_t lineEnable;
uint8_t led;
};
#if CONFIG_IDF_TARGET_ESP32S3
constexpr PhonePins PINS{
.pinHookSense = 4, // AG1171S hook/off-hook indication input to ESP32-S3
.pinRingCmd = 5, // ESP32-S3 output: request ring cadence generator/enable
.pinLineEnable = 6, // ESP32-S3 output: line feed enable (through safe driver)
.pinLed = 48 // ESP32-S3 DevKitC-1 RGB/Status-compatible GPIO
};
constexpr PhonePins PINS{.hookSense = 4, .ringCmd = 5, .lineEnable = 6, .led = 48};
#else
constexpr PhonePins PINS{
.pinHookSense = 27,
.pinRingCmd = 26,
.pinLineEnable = 25,
.pinLed = 2
};
constexpr PhonePins PINS{.hookSense = 27, .ringCmd = 26, .lineEnable = 25, .led = 2};
#endif
constexpr uint32_t SERIAL_BAUD = 115200;
constexpr uint32_t DEBOUNCE_MS = 25;
constexpr char DEVICE_NAME[] = "RTC_BL_PHONE";
constexpr char DEFAULT_PEER_ADDR[] = "00:00:00:00:00:00"; // A remplacer ou via commande "p <mac>".
PhoneState g_state = PhoneState::ON_HOOK;
bool g_hookOffHook = false;
bool g_hfpReady = false;
bool g_hfpConnected = false;
bool g_audioConnected = false;
bool g_callActive = false;
bool g_callIncoming = false;
bool g_callSetupOutgoing = false;
uint32_t g_lastHookEdgeMs = 0;
String g_serialLine;
String g_peerAddrString = DEFAULT_PEER_ADDR;
esp_bd_addr_t g_peerAddr = {0};
const char* stateToString(PhoneState state) {
switch (state) {
case PhoneState::ON_HOOK: return "ON_HOOK";
case PhoneState::OFF_HOOK: return "OFF_HOOK";
case PhoneState::IDLE: return "IDLE";
case PhoneState::RINGING: return "RINGING";
case PhoneState::DIALING: return "DIALING";
case PhoneState::IN_CALL: return "IN_CALL";
case PhoneState::RINGING: return "RINGING";
}
return "UNKNOWN";
}
void setState(PhoneState newState) {
if (newState == g_state) {
return;
bool parseBdAddr(const char* mac, esp_bd_addr_t out) {
unsigned int v[6] = {0};
if (sscanf(mac, "%x:%x:%x:%x:%x:%x", &v[0], &v[1], &v[2], &v[3], &v[4], &v[5]) != 6) {
return false;
}
g_state = newState;
Serial.printf("[RTC_PHONE] state=%s\n", stateToString(g_state));
for (int i = 0; i < 6; ++i) {
if (v[i] > 0xFF) {
return false;
}
out[i] = static_cast<uint8_t>(v[i]);
}
return true;
}
bool updatePeerAddr(const String& addr) {
esp_bd_addr_t parsed = {0};
if (!parseBdAddr(addr.c_str(), parsed)) {
return false;
}
memcpy(g_peerAddr, parsed, sizeof(g_peerAddr));
g_peerAddrString = addr;
return true;
}
bool isPeerAddrConfigured() {
return g_peerAddrString != DEFAULT_PEER_ADDR;
}
void applyOutputsForState() {
switch (g_state) {
case PhoneState::ON_HOOK:
digitalWrite(PINS.pinLineEnable, LOW);
digitalWrite(PINS.pinRingCmd, LOW);
digitalWrite(PINS.pinLed, LOW);
digitalWrite(PINS.lineEnable, LOW);
digitalWrite(PINS.ringCmd, LOW);
digitalWrite(PINS.led, LOW);
break;
case PhoneState::OFF_HOOK:
digitalWrite(PINS.pinLineEnable, HIGH);
digitalWrite(PINS.pinRingCmd, LOW);
digitalWrite(PINS.pinLed, HIGH);
case PhoneState::IDLE:
digitalWrite(PINS.lineEnable, HIGH);
digitalWrite(PINS.ringCmd, LOW);
digitalWrite(PINS.led, HIGH);
break;
case PhoneState::DIALING:
digitalWrite(PINS.pinLineEnable, HIGH);
digitalWrite(PINS.pinRingCmd, LOW);
digitalWrite(PINS.pinLed, HIGH);
break;
case PhoneState::IN_CALL:
digitalWrite(PINS.pinLineEnable, HIGH);
digitalWrite(PINS.pinRingCmd, LOW);
digitalWrite(PINS.pinLed, HIGH);
break;
case PhoneState::RINGING:
digitalWrite(PINS.pinLineEnable, HIGH);
digitalWrite(PINS.pinRingCmd, HIGH);
digitalWrite(PINS.pinLed, HIGH);
digitalWrite(PINS.lineEnable, HIGH);
digitalWrite(PINS.ringCmd, HIGH);
digitalWrite(PINS.led, HIGH);
break;
case PhoneState::DIALING:
case PhoneState::IN_CALL:
digitalWrite(PINS.lineEnable, HIGH);
digitalWrite(PINS.ringCmd, LOW);
digitalWrite(PINS.led, HIGH);
break;
}
}
void setState(PhoneState newState) {
if (g_state == newState) {
return;
}
g_state = newState;
applyOutputsForState();
Serial.printf("[RTC_PHONE] state=%s\n", stateToString(g_state));
}
void refreshPhoneState() {
if (!g_hookOffHook) {
setState(PhoneState::ON_HOOK);
} else if (g_callIncoming && !g_callActive) {
setState(PhoneState::RINGING);
} else if (g_callSetupOutgoing && !g_callActive) {
setState(PhoneState::DIALING);
} else if (g_callActive) {
setState(PhoneState::IN_CALL);
} else {
setState(PhoneState::IDLE);
}
}
void printHelp() {
Serial.println("[RTC_PHONE] Commands:");
Serial.println(" h -> help");
Serial.println(" r -> enter RINGING");
Serial.println(" o -> force ON_HOOK");
Serial.println(" d -> force DIALING");
Serial.println(" c -> force IN_CALL");
Serial.println("[RTC_PHONE] Commandes:");
Serial.println(" h -> help");
Serial.println(" s -> status");
Serial.println(" p <mac> -> set peer MAC HFP (AA:BB:CC:DD:EE:FF)");
Serial.println(" b -> connect HFP AG");
Serial.println(" x -> disconnect HFP AG");
Serial.println(" a -> answer incoming");
Serial.println(" e -> end/reject call");
Serial.println(" m <number> -> dial number");
Serial.println(" v <0..15> -> set speaker volume");
}
void printStatus() {
Serial.printf("[RTC_PHONE] hook=%s state=%s ready=%s hfp=%s audio=%s incoming=%s outgoing=%s active=%s peer=%s\n",
g_hookOffHook ? "OFF_HOOK" : "ON_HOOK", stateToString(g_state), g_hfpReady ? "YES" : "NO",
g_hfpConnected ? "YES" : "NO", g_audioConnected ? "YES" : "NO", g_callIncoming ? "YES" : "NO",
g_callSetupOutgoing ? "YES" : "NO", g_callActive ? "YES" : "NO", g_peerAddrString.c_str());
}
#if SOC_BT_CLASSIC_SUPPORTED
void onCallTerminatedByState() {
if (!g_callActive && !g_callSetupOutgoing) {
g_audioConnected = false;
}
}
void hfpCallback(esp_hf_client_cb_event_t event, esp_hf_client_cb_param_t* param) {
switch (event) {
case ESP_HF_CLIENT_CONNECTION_STATE_EVT:
g_hfpConnected = (param->conn_stat.state == ESP_HF_CLIENT_CONNECTION_STATE_SLC_CONNECTED);
if (!g_hfpConnected) {
g_audioConnected = false;
}
Serial.printf("[HFP] conn_state=%d\n", param->conn_stat.state);
break;
case ESP_HF_CLIENT_AUDIO_STATE_EVT:
g_audioConnected = (param->audio_stat.state == ESP_HF_CLIENT_AUDIO_STATE_CONNECTED ||
param->audio_stat.state == ESP_HF_CLIENT_AUDIO_STATE_CONNECTED_MSBC);
Serial.printf("[HFP] audio_state=%d\n", param->audio_stat.state);
break;
case ESP_HF_CLIENT_RING_IND_EVT:
g_callIncoming = true;
Serial.println("[HFP] incoming ring");
break;
case ESP_HF_CLIENT_CALL_IND_EVT:
g_callActive = (param->call.ind != 0);
Serial.printf("[HFP] call=%d\n", param->call.ind);
onCallTerminatedByState();
break;
case ESP_HF_CLIENT_CALL_SETUP_IND_EVT:
g_callSetupOutgoing = (param->call_setup.status == ESP_HF_CALL_SETUP_STATUS_OUTGOING_DIALING ||
param->call_setup.status == ESP_HF_CALL_SETUP_STATUS_OUTGOING_ALERTING);
if (param->call_setup.status == ESP_HF_CALL_SETUP_STATUS_INCOMING) {
g_callIncoming = true;
} else if (param->call_setup.status == ESP_HF_CALL_SETUP_STATUS_IDLE) {
g_callIncoming = false;
}
Serial.printf("[HFP] call_setup=%d\n", param->call_setup.status);
onCallTerminatedByState();
break;
case ESP_HF_CLIENT_CLIP_EVT:
if (param->clip.number) {
Serial.printf("[HFP] caller=%s\n", param->clip.number);
}
break;
default:
break;
}
refreshPhoneState();
}
bool initHfp() {
if (!updatePeerAddr(g_peerAddrString)) {
Serial.println("[HFP] MAC invalide. Utilisez: p AA:BB:CC:DD:EE:FF");
return false;
}
esp_err_t err = esp_bt_controller_mem_release(ESP_BT_MODE_BLE);
if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) {
Serial.printf("[HFP] mem_release failed: %s\n", esp_err_to_name(err));
return false;
}
const esp_bt_controller_config_t btCfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
err = esp_bt_controller_init(&btCfg);
if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) {
Serial.printf("[HFP] bt_controller_init failed: %s\n", esp_err_to_name(err));
return false;
}
err = esp_bt_controller_enable(ESP_BT_MODE_CLASSIC_BT);
if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) {
Serial.printf("[HFP] bt_controller_enable failed: %s\n", esp_err_to_name(err));
return false;
}
err = esp_bluedroid_init();
if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) {
Serial.printf("[HFP] bluedroid_init failed: %s\n", esp_err_to_name(err));
return false;
}
err = esp_bluedroid_enable();
if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) {
Serial.printf("[HFP] bluedroid_enable failed: %s\n", esp_err_to_name(err));
return false;
}
err = esp_bt_dev_set_device_name(DEVICE_NAME);
if (err != ESP_OK) {
Serial.printf("[HFP] set_device_name failed: %s\n", esp_err_to_name(err));
return false;
}
err = esp_hf_client_register_callback(hfpCallback);
if (err != ESP_OK) {
Serial.printf("[HFP] register_callback failed: %s\n", esp_err_to_name(err));
return false;
}
err = esp_hf_client_init();
if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) {
Serial.printf("[HFP] init failed: %s\n", esp_err_to_name(err));
return false;
}
g_hfpReady = true;
Serial.println("[HFP] stack ready");
if (!isPeerAddrConfigured()) {
Serial.println("[HFP] ATTENTION: configurez le peer avec p <mac> avant b");
}
return true;
}
void connectHfp() {
if (!g_hfpReady) {
Serial.println("[HFP] stack non initialisee");
return;
}
if (!isPeerAddrConfigured()) {
Serial.println("[HFP] peer MAC non configure. Utilisez: p <mac>");
return;
}
const esp_err_t err = esp_hf_client_connect(g_peerAddr);
Serial.printf("[HFP] connect -> %s\n", esp_err_to_name(err));
}
void disconnectHfp() {
if (!g_hfpReady) {
Serial.println("[HFP] stack non initialisee");
return;
}
const esp_err_t err = esp_hf_client_disconnect(g_peerAddr);
Serial.printf("[HFP] disconnect -> %s\n", esp_err_to_name(err));
}
void answerCall() {
if (!g_hfpConnected || !g_callIncoming) {
Serial.println("[HFP] aucun appel entrant a decrocher");
return;
}
const esp_err_t err = esp_hf_client_answer_call();
Serial.printf("[HFP] answer -> %s\n", esp_err_to_name(err));
}
void endCall() {
if (!g_hfpConnected) {
Serial.println("[HFP] non connecte");
return;
}
esp_err_t err = ESP_FAIL;
if (g_callIncoming && !g_callActive) {
err = esp_hf_client_reject_call();
Serial.printf("[HFP] reject -> %s\n", esp_err_to_name(err));
} else {
err = esp_hf_client_terminate_call();
Serial.printf("[HFP] terminate -> %s\n", esp_err_to_name(err));
}
}
void dialNumber(const String& number) {
if (!g_hfpConnected) {
Serial.println("[HFP] non connecte");
return;
}
if (number.length() == 0) {
Serial.println("[HFP] numero vide");
return;
}
const esp_err_t err = esp_hf_client_dial(number.c_str());
Serial.printf("[HFP] dial(%s) -> %s\n", number.c_str(), esp_err_to_name(err));
}
void setSpeakerVolume(int value) {
if (!g_hfpConnected) {
Serial.println("[HFP] non connecte");
return;
}
const int clipped = constrain(value, 0, 15);
const esp_err_t err = esp_hf_client_volume_update(ESP_HF_VOLUME_CONTROL_TARGET_SPK, clipped);
Serial.printf("[HFP] volume=%d -> %s\n", clipped, esp_err_to_name(err));
}
#else
bool initHfp() {
Serial.println("[HFP] indisponible: cible sans Bluetooth Classic (ex: ESP32-S3)");
return false;
}
void connectHfp() { Serial.println("[HFP] non supporte sur cette cible"); }
void disconnectHfp() { Serial.println("[HFP] non supporte sur cette cible"); }
void answerCall() { Serial.println("[HFP] non supporte sur cette cible"); }
void endCall() { Serial.println("[HFP] non supporte sur cette cible"); }
void dialNumber(const String&) { Serial.println("[HFP] non supporte sur cette cible"); }
void setSpeakerVolume(int) { Serial.println("[HFP] non supporte sur cette cible"); }
#endif
void executeCommand(const String& line) {
if (line == "h") {
printHelp();
} else if (line == "s") {
printStatus();
} else if (line == "b") {
connectHfp();
} else if (line == "x") {
disconnectHfp();
} else if (line == "a") {
answerCall();
} else if (line == "e") {
endCall();
} else if (line.startsWith("m ")) {
dialNumber(line.substring(2));
} else if (line.startsWith("v ")) {
setSpeakerVolume(line.substring(2).toInt());
} else if (line.startsWith("p ")) {
const String mac = line.substring(2);
if (updatePeerAddr(mac)) {
Serial.printf("[HFP] peer configure: %s\n", g_peerAddrString.c_str());
} else {
Serial.println("[HFP] format MAC invalide. Ex: AA:BB:CC:DD:EE:FF");
}
} else if (!line.isEmpty()) {
Serial.printf("[RTC_PHONE] commande inconnue: %s\n", line.c_str());
}
}
void handleSerialCommands() {
while (Serial.available() > 0) {
const char cmd = static_cast<char>(Serial.read());
switch (cmd) {
case 'h':
printHelp();
break;
case 'r':
setState(PhoneState::RINGING);
break;
case 'o':
setState(PhoneState::ON_HOOK);
break;
case 'd':
setState(PhoneState::DIALING);
break;
case 'c':
setState(PhoneState::IN_CALL);
break;
default:
break;
const char c = static_cast<char>(Serial.read());
if (c == '\n' || c == '\r') {
executeCommand(g_serialLine);
g_serialLine = "";
} else {
g_serialLine += c;
}
}
}
void updateHookState() {
const bool rawOffHook = (digitalRead(PINS.pinHookSense) == LOW);
const bool rawOffHook = (digitalRead(PINS.hookSense) == LOW);
const uint32_t nowMs = millis();
if (rawOffHook != g_hookOffHook && (nowMs - g_lastHookEdgeMs) > DEBOUNCE_MS) {
g_lastHookEdgeMs = nowMs;
g_hookOffHook = rawOffHook;
Serial.printf("[RTC_PHONE] hook=%s\n", g_hookOffHook ? "OFF_HOOK" : "ON_HOOK");
if (g_hookOffHook) {
setState(PhoneState::OFF_HOOK);
} else {
setState(PhoneState::ON_HOOK);
if (!g_hookOffHook && (g_callActive || g_callSetupOutgoing || g_callIncoming)) {
endCall();
} else if (g_hookOffHook && g_callIncoming && !g_callActive) {
answerCall();
}
refreshPhoneState();
}
}
void setup() {
Serial.begin(SERIAL_BAUD);
pinMode(PINS.pinHookSense, INPUT_PULLUP);
pinMode(PINS.pinRingCmd, OUTPUT);
pinMode(PINS.pinLineEnable, OUTPUT);
pinMode(PINS.pinLed, OUTPUT);
pinMode(PINS.hookSense, INPUT_PULLUP);
pinMode(PINS.ringCmd, OUTPUT);
pinMode(PINS.lineEnable, OUTPUT);
pinMode(PINS.led, OUTPUT);
digitalWrite(PINS.pinRingCmd, LOW);
digitalWrite(PINS.pinLineEnable, LOW);
digitalWrite(PINS.pinLed, LOW);
digitalWrite(PINS.ringCmd, LOW);
digitalWrite(PINS.lineEnable, LOW);
digitalWrite(PINS.led, LOW);
Serial.println("\n[RTC_PHONE] Boot OK");
#if CONFIG_IDF_TARGET_ESP32S3
Serial.println("[RTC_PHONE] Target: ESP32-S3");
#else
Serial.println("[RTC_PHONE] Target: ESP32 (legacy mapping)");
Serial.println("[RTC_PHONE] Target: ESP32");
#endif
Serial.println("[RTC_PHONE] Profile: AG1171S control skeleton");
printHelp();
Serial.println("[RTC_PHONE] Profile: AG1171S + Bluetooth HFP");
setState(PhoneState::ON_HOOK);
printHelp();
g_hookOffHook = (digitalRead(PINS.hookSense) == LOW);
initHfp();
refreshPhoneState();
}
void loop() {
updateHookState();
handleSerialCommands();
delay(10);
// Prototype minimal pour valider le câblage d'un téléphone RTC recyclé.
// - Hook switch: détection décroché/raccroché
// - Clavier: lecture en matrice (adaptateur nécessaire selon le modèle)
// - Audio: à implémenter ensuite via codec I2S / interface analogique dédiée
constexpr uint8_t PIN_HOOK = 27; // Adapter selon le câblage réel
constexpr uint8_t PIN_LED = 2; // LED status carte
bool offHook = false;
void setup() {
Serial.begin(115200);
pinMode(PIN_HOOK, INPUT_PULLUP);
pinMode(PIN_LED, OUTPUT);
digitalWrite(PIN_LED, LOW);
Serial.println("\n[RTC_PHONE] Boot OK");
Serial.println("[RTC_PHONE] MVP: hook + logique d'etat");
}
void loop() {
bool hookState = digitalRead(PIN_HOOK) == LOW; // LOW = décroché (exemple)
if (hookState != offHook) {
offHook = hookState;
digitalWrite(PIN_LED, offHook ? HIGH : LOW);
Serial.printf("[RTC_PHONE] Etat combine: %s\n", offHook ? "DECROCHE" : "RACCROCHE");
}
delay(20);
}