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.
132 lines
6.2 KiB
Markdown
132 lines
6.2 KiB
Markdown
# 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) et `tests/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](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é sur
|
||
`type + 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) :
|
||
|
||
```bash
|
||
# 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
|
||
```
|