Improvement batch on top of the validated system. STATE grows 27 -> 28 bytes (click_pos): both firmwares must be reflashed together, a version mix shows the disconnected banner. - Box-3 'Effets' tab: delay level/feedback/length sliders wired to three new CMD opcodes (0x10-0x12) calling the ml_delay setters that were only reachable over MIDI before. - visual metronome: STATE carries the click bar position; the click button flashes on each beat, brighter on beat 1. - input clip indicator: near-full-scale input turns the input VU red for 2 s (peak hold). - fix a regression from the VT100 removal: status messages could Serial.printf from the audio core (MIDI callbacks), busting the 1.088 ms budget. They are now staged and printed by core 0. - flush the link RX buffer after an SD operation so commands touched during the blocking save/load no longer replay in a burst afterwards. - static_assert on sizeof(link_state_t) in the shared header and TRACK_CNT == LINK_TRACK_CNT in the looper. - protocol.md updated: 28-byte layout, delay opcodes, real liveness rules (any valid frame, 8 s during SD operations). Verified: pio run SUCCESS, idf.py build OK, C host test OK, 5/5 pytest.
6.2 KiB
Protocole de liaison UART « looper link »
Liaison série entre l'ESP32 Audio Kit (moteur looper) et l'ESP32-S3-BOX-3 (afficheur + surface tactile).
- Source de vérité :
shared/looper_link.h(header C partagé par les deux firmwares) ettests/link_sim.py(implémentation Python miroir). - Physique : UART 3 fils (TX/RX croisés + GND), 460 800 bauds, 8N1, niveaux 3,3 V. Brochage : voir wiring.md.
Trame
[0xA5] [type:1] [len:1] [payload:len] [crc8:1]
0xA5: octet de synchronisation (SOF).type: type de message (voir ci-dessous).len: taille du payload (≤ 64 octets).crc8: CRC8 Dallas/Maxim (polynôme réfléchi 0x8C) calculé surtype + len + payload.
Une trame corrompue est ignorée silencieusement ; le parseur se resynchronise
sur le prochain 0xA5. La perte d'une trame est sans conséquence : l'état
complet est réémis 20 fois par seconde.
Messages
STATE (0x01) — Audio Kit → Box-3, périodique à 20 Hz
Payload : link_state_t, 28 octets, little-endian, packé.
| Champ | Type | Description |
|---|---|---|
flags[4] |
u8×4 | par piste : bit0 DATA, bit1 ACTIVE, bit2 REC, bit3 ERASE |
gain_out[4] |
u8×4 | gain de sortie par piste (0–255) |
pan[4] |
u8×4 | panoramique par piste (0 = gauche, 128 = centre, 255 = droite) |
pos |
u16 | position de lecture relative (0–65535 sur MAX_LOOP) |
len |
u16 | longueur de boucle relative (0–65535 sur MAX_LOOP) |
tempo_bpm |
u16 | tempo du métronome en BPM |
click_on |
u8 | métronome audible (0/1) |
click_pos |
u8 | position dans la mesure de 4 temps (0–255) — métronome visuel |
input_src |
u8 | source d'entrée : 0 = line, 1 = mic |
gain_in |
u8 | gain d'entrée (0–255) |
vu[6] |
u8×6 | VU-mètres : pistes 1–4, entrée, sortie |
La taille est verrouillée par un static_assert dans le header partagé et
par les tests hôte (C et Python) — toute dérive de layout casse la
compilation ou les tests.
EVT (0x02) — Audio Kit → Box-3, ponctuel
Payload : link_evt_t, 2 octets : code, arg.
| Code | Nom | Argument |
|---|---|---|
| 0x01 | SD_PROGRESS |
avancement 0–100 % |
| 0x02 | SD_DONE |
numéro de session |
| 0x03 | SD_ERROR |
1 = mount, 2 = open, 3 = format, 4 = write |
| 0x04 | RESET_DONE |
— |
CMD (0x10) — Box-3 → Audio Kit
Payload : link_cmd_t, 4 octets : op (u8), arg0 (u8), arg1 (u16 LE).
Chaque opcode appelle une fonction existante du looper — la Box-3 ne porte
aucune logique métier.
| Op | Nom | Arguments | Fonction appelée |
|---|---|---|---|
| 0x01 | SELECT_TRACK |
arg0 = piste (0–3) | Loop_SelectTrack |
| 0x02 | STOP_TRACK |
arg0 = piste | Loop_StopChannel |
| 0x03 | ERASE_TRACK |
arg0 = piste | Loop_EraseTrack |
| 0x04 | SET_LENGTH |
— | Loop_SetLength |
| 0x05 | START_ALL |
— | Loop_StartAll |
| 0x06 | STOP_ALL |
— | Loop_Stop |
| 0x07 | GAIN_OUT |
arg0 = piste, arg1 = 0–65535 | Loop_SetChannelGainOut |
| 0x08 | PAN |
arg0 = piste, arg1 = 0–65535 | Loop_SetChannelPan |
| 0x09 | GAIN_IN |
arg1 = 0–65535 | Loop_SetChannelGainIn |
| 0x0A | CLICK_TOGGLE |
— | Click_ToggleOnOff |
| 0x0B | SET_TEMPO |
arg1 = BPM | Click_SetTempoBPM |
| 0x0C | TOGGLE_SRC |
— | MTLooper_ToggleSource |
| 0x0D | RESET_ALL |
— | Loop_CompleteReset (+ EVT RESET_DONE) |
| 0x0E | SD_SAVE |
arg0 = session (0–7 côté UI) | Sd_RequestSave |
| 0x0F | SD_LOAD |
arg0 = session | Sd_RequestLoad |
| 0x10 | DELAY_FB |
arg1 = 0–65535 | Delay_SetFeedback |
| 0x11 | DELAY_LEN |
arg1 = 0–65535 | Delay_SetLength |
| 0x12 | DELAY_LEVEL |
arg1 = 0–65535 | Delay_SetOutputLevel |
Pas d'acquittement sur CMD : l'effet est visible dans le STATE suivant
(≤ 50 ms). Les réglages de delay ne sont pas réémis dans STATE
(fire-and-forget, comme les sliders MIDI).
Les commandes reçues pendant une opération SD sont ignorées : le looper purge son tampon de réception à la fin de l'opération.
Détection de déconnexion
Toute trame CRC-valide (STATE ou EVT) rafraîchit la vivacité du lien.
La Box-3 affiche un bandeau « Looper déconnecté » après 500 ms sans
trame valide (LINK_TIMEOUT_MS) — délai élargi à 8 s
(LINK_SD_TIMEOUT_MS) entre l'envoi d'une commande SD et son événement
terminal, car le looper n'émet plus de STATE pendant l'accès carte
(seuls les SD_PROGRESS circulent, à intervalle irrégulier).
Vecteur de test figé
CMD SELECT_TRACK piste 2 :
A5 10 04 01 02 00 00 84
Ce vecteur est vérifié à l'identique par tests/test_link_protocol.c (C)
et tests/test_link_sim.py (Python) — il garantit la parité des deux
implémentations. Ne pas le modifier sans changer les deux tests.
Tester sans matériel complet
tests/link_sim.py (nécessite pyserial pour le CLI) :
# décoder ce que l'Audio Kit émet (via l'adaptateur USB-série de ton choix)
python3 tests/link_sim.py --port /dev/cu.usbserial-XXXX monitor
# envoyer une commande au looper
python3 tests/link_sim.py --port ... cmd SELECT_TRACK 2
python3 tests/link_sim.py --port ... cmd SET_TEMPO 0 90
# simuler un Audio Kit pour tester la Box-3 seule (STATE de démo à 20 Hz)
python3 tests/link_sim.py --port ... replay-state