Merge cursor/igiRadio-ios-app into main (xtal EEPROM persistence)
This commit is contained in:
@@ -23,6 +23,24 @@
|
|||||||
|
|
||||||
namespace eeprom24aa {
|
namespace eeprom24aa {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief XtalCalibration — Si4684 crystal reference trim, EEPROM-backed.
|
||||||
|
*
|
||||||
|
* @dname XtalCalibration
|
||||||
|
* @return n/a (type)
|
||||||
|
* @pubstate Plain DTO mirroring POWER_UP ARG3/ARG8/ARG4-7 (AN649 Command
|
||||||
|
* 0x01); persisted as a set (all three or none) since a partial
|
||||||
|
* trim is meaningless.
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-08-24
|
||||||
|
*/
|
||||||
|
struct XtalCalibration {
|
||||||
|
std::uint8_t ibias; ///< POWER_UP ARG3 IBIAS (0-127).
|
||||||
|
std::uint8_t ctun; ///< POWER_UP ARG8 CTUN (0-63).
|
||||||
|
std::uint32_t xtalFreqHz; ///< POWER_UP ARG4-7 XTAL_FREQ in Hz.
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Eeprom24aa — reads the factory EUI-48 from Microchip 24AA025E48;
|
* @brief Eeprom24aa — reads the factory EUI-48 from Microchip 24AA025E48;
|
||||||
* also stores one board-specific calibration byte in the chip's
|
* also stores one board-specific calibration byte in the chip's
|
||||||
@@ -50,6 +68,12 @@ public:
|
|||||||
/** Same range as kFmAntCapMax; kept as a separate name for the DAB
|
/** Same range as kFmAntCapMax; kept as a separate name for the DAB
|
||||||
* calibration byte's own doc comments below. */
|
* calibration byte's own doc comments below. */
|
||||||
static constexpr std::uint8_t kDabAntCapMax = 128U;
|
static constexpr std::uint8_t kDabAntCapMax = 128U;
|
||||||
|
/** POWER_UP ARG3 IBIAS valid range (AN649 Command 0x01); 0xFF (blank
|
||||||
|
* EEPROM) reads back as "never calibrated". */
|
||||||
|
static constexpr std::uint8_t kXtalIbiasMax = 127U;
|
||||||
|
/** POWER_UP ARG8 CTUN valid range (AN649 Command 0x01); 0xFF (blank
|
||||||
|
* EEPROM) reads back as "never calibrated". */
|
||||||
|
static constexpr std::uint8_t kXtalCtunMax = 63U;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Eeprom24aa — bind to a running I2C master bus and 7-bit addr.
|
* @brief Eeprom24aa — bind to a running I2C master bus and 7-bit addr.
|
||||||
@@ -140,6 +164,40 @@ public:
|
|||||||
[[nodiscard]] std::expected<void, core::IdentityError>
|
[[nodiscard]] std::expected<void, core::IdentityError>
|
||||||
writeDabAntCap(std::uint8_t value);
|
writeDabAntCap(std::uint8_t value);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief readXtalCalibration — read the stored Si4684 crystal trim.
|
||||||
|
*
|
||||||
|
* @dname readXtalCalibration
|
||||||
|
* @return Calibrated ibias/ctun/xtalFreqHz if all three were saved via
|
||||||
|
* writeXtalCalibration(), nullopt if never calibrated (any of
|
||||||
|
* the three bytes still blank/out of range), or IdentityError
|
||||||
|
* on an I2C failure.
|
||||||
|
* @pubstate performs one I2C read of six bytes starting at word address
|
||||||
|
* 0x02.
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-08-24
|
||||||
|
*/
|
||||||
|
[[nodiscard]] std::expected<std::optional<XtalCalibration>, core::IdentityError>
|
||||||
|
readXtalCalibration();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief writeXtalCalibration — persist the Si4684 crystal trim.
|
||||||
|
*
|
||||||
|
* @dname writeXtalCalibration
|
||||||
|
* @param calibration ibias (0-127), ctun (0-63), xtalFreqHz found by
|
||||||
|
* POST /api/tuner/xtal-calibrate, not computed.
|
||||||
|
* @return Ok on success, or IdentityError::I2cFailed.
|
||||||
|
* @pubstate performs one I2C write of six bytes starting at word address
|
||||||
|
* 0x02 (ibias, ctun, xtalFreqHz big-endian), then blocks for
|
||||||
|
* the chip's write-cycle time before returning.
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-08-24
|
||||||
|
*/
|
||||||
|
[[nodiscard]] std::expected<void, core::IdentityError>
|
||||||
|
writeXtalCalibration(const XtalCalibration& calibration);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
i2c_master_bus_handle_t bus_;
|
i2c_master_bus_handle_t bus_;
|
||||||
std::uint8_t addr7_;
|
std::uint8_t addr7_;
|
||||||
|
|||||||
@@ -32,6 +32,11 @@ constexpr std::uint8_t kEui48WordAddress = 0xFAU;
|
|||||||
constexpr std::uint8_t kFmAntCapWordAddress = 0x00U;
|
constexpr std::uint8_t kFmAntCapWordAddress = 0x00U;
|
||||||
/** DAB ANTCAP calibration byte, next word address after the FM byte. */
|
/** DAB ANTCAP calibration byte, next word address after the FM byte. */
|
||||||
constexpr std::uint8_t kDabAntCapWordAddress = 0x01U;
|
constexpr std::uint8_t kDabAntCapWordAddress = 0x01U;
|
||||||
|
/** Si4684 crystal trim: IBIAS byte, CTUN byte, then XTAL_FREQ as 4 bytes
|
||||||
|
* big-endian -- 6 bytes total starting right after the DAB ANTCAP byte. */
|
||||||
|
constexpr std::uint8_t kXtalIbiasWordAddress = 0x02U;
|
||||||
|
constexpr std::uint8_t kXtalCtunWordAddress = 0x03U;
|
||||||
|
constexpr std::uint8_t kXtalFreqHzWordAddress = 0x04U;
|
||||||
constexpr int kI2cTimeoutMs = 100;
|
constexpr int kI2cTimeoutMs = 100;
|
||||||
/** DS20001191 §"Page Write"/"Byte Write": max write cycle time after STOP
|
/** DS20001191 §"Page Write"/"Byte Write": max write cycle time after STOP
|
||||||
* before the chip acknowledges further I2C traffic. */
|
* before the chip acknowledges further I2C traffic. */
|
||||||
@@ -221,4 +226,93 @@ Eeprom24aa::writeDabAntCap(std::uint8_t value)
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::expected<std::optional<XtalCalibration>, core::IdentityError>
|
||||||
|
Eeprom24aa::readXtalCalibration()
|
||||||
|
{
|
||||||
|
if (bus_ == nullptr) {
|
||||||
|
return std::unexpected(core::IdentityError::I2cFailed);
|
||||||
|
}
|
||||||
|
|
||||||
|
i2c_device_config_t devCfg = {};
|
||||||
|
devCfg.dev_addr_length = I2C_ADDR_BIT_LEN_7;
|
||||||
|
devCfg.device_address = addr7_;
|
||||||
|
devCfg.scl_speed_hz = 100000;
|
||||||
|
|
||||||
|
i2c_master_dev_handle_t dev = nullptr;
|
||||||
|
if (i2c_master_bus_add_device(bus_, &devCfg, &dev) != ESP_OK) {
|
||||||
|
ESP_LOGW(kTag, "i2c_master_bus_add_device failed");
|
||||||
|
return std::unexpected(core::IdentityError::I2cFailed);
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::uint8_t wordAddress = kXtalIbiasWordAddress;
|
||||||
|
std::array<std::uint8_t, 6> payload = {};
|
||||||
|
const esp_err_t err = i2c_master_transmit_receive(
|
||||||
|
dev, &wordAddress, 1U, payload.data(), payload.size(), kI2cTimeoutMs);
|
||||||
|
|
||||||
|
i2c_master_bus_rm_device(dev);
|
||||||
|
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGW(kTag, "Xtal calibration read failed (err=0x%x)",
|
||||||
|
static_cast<unsigned>(err));
|
||||||
|
return std::unexpected(core::IdentityError::ReadFailed);
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::uint8_t ibias = payload[0];
|
||||||
|
const std::uint8_t ctun = payload[1];
|
||||||
|
const std::uint32_t xtalFreqHz =
|
||||||
|
(static_cast<std::uint32_t>(payload[2]) << 24)
|
||||||
|
| (static_cast<std::uint32_t>(payload[3]) << 16)
|
||||||
|
| (static_cast<std::uint32_t>(payload[4]) << 8)
|
||||||
|
| static_cast<std::uint32_t>(payload[5]);
|
||||||
|
|
||||||
|
if (ibias > kXtalIbiasMax || ctun > kXtalCtunMax
|
||||||
|
|| xtalFreqHz == 0xFFFFFFFFU) {
|
||||||
|
return std::optional<XtalCalibration>{};
|
||||||
|
}
|
||||||
|
return std::optional<XtalCalibration>{
|
||||||
|
XtalCalibration{.ibias = ibias, .ctun = ctun,
|
||||||
|
.xtalFreqHz = xtalFreqHz}};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::expected<void, core::IdentityError>
|
||||||
|
Eeprom24aa::writeXtalCalibration(const XtalCalibration& calibration)
|
||||||
|
{
|
||||||
|
if (bus_ == nullptr) {
|
||||||
|
return std::unexpected(core::IdentityError::I2cFailed);
|
||||||
|
}
|
||||||
|
|
||||||
|
i2c_device_config_t devCfg = {};
|
||||||
|
devCfg.dev_addr_length = I2C_ADDR_BIT_LEN_7;
|
||||||
|
devCfg.device_address = addr7_;
|
||||||
|
devCfg.scl_speed_hz = 100000;
|
||||||
|
|
||||||
|
i2c_master_dev_handle_t dev = nullptr;
|
||||||
|
if (i2c_master_bus_add_device(bus_, &devCfg, &dev) != ESP_OK) {
|
||||||
|
ESP_LOGW(kTag, "i2c_master_bus_add_device failed");
|
||||||
|
return std::unexpected(core::IdentityError::I2cFailed);
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::array<std::uint8_t, 7> payload = {
|
||||||
|
kXtalIbiasWordAddress,
|
||||||
|
calibration.ibias,
|
||||||
|
calibration.ctun,
|
||||||
|
static_cast<std::uint8_t>(calibration.xtalFreqHz >> 24),
|
||||||
|
static_cast<std::uint8_t>(calibration.xtalFreqHz >> 16),
|
||||||
|
static_cast<std::uint8_t>(calibration.xtalFreqHz >> 8),
|
||||||
|
static_cast<std::uint8_t>(calibration.xtalFreqHz)};
|
||||||
|
const esp_err_t err =
|
||||||
|
i2c_master_transmit(dev, payload.data(), payload.size(), kI2cTimeoutMs);
|
||||||
|
|
||||||
|
i2c_master_bus_rm_device(dev);
|
||||||
|
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGW(kTag, "Xtal calibration write failed (err=0x%x)",
|
||||||
|
static_cast<unsigned>(err));
|
||||||
|
return std::unexpected(core::IdentityError::I2cFailed);
|
||||||
|
}
|
||||||
|
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(kI2cWriteCycleMs));
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace eeprom24aa
|
} // namespace eeprom24aa
|
||||||
|
|||||||
@@ -112,6 +112,11 @@ struct AntennaCalibration {
|
|||||||
* @return false if the chip was never booted or the reboot failed. */
|
* @return false if the chip was never booted or the reboot failed. */
|
||||||
bool (*recalibrateXtal)(std::uint8_t ibias, std::uint8_t ctun,
|
bool (*recalibrateXtal)(std::uint8_t ibias, std::uint8_t ctun,
|
||||||
std::uint32_t xtalFreqHz);
|
std::uint32_t xtalFreqHz);
|
||||||
|
/** Persist a new Si4684 crystal trim (ibias/ctun/xtalFreqHz) to EEPROM,
|
||||||
|
* so it survives a reboot instead of only lasting until the next power
|
||||||
|
* cycle. @return false on an I2C failure. */
|
||||||
|
bool (*saveXtal)(std::uint8_t ibias, std::uint8_t ctun,
|
||||||
|
std::uint32_t xtalFreqHz);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -741,11 +741,24 @@ esp_err_t tunerXtalCalibratePostHandler(httpd_req_t* req)
|
|||||||
return httpd_resp_send(req, json.c_str(), json.size());
|
return httpd_resp_send(req, json.c_str(), json.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Persist every successful live recalibration (2026-08-24): each board
|
||||||
|
// may need its own crystal trim, so the value found by a calibration
|
||||||
|
// sweep must survive a reboot, not just last until the next power
|
||||||
|
// cycle. The live apply above already succeeded regardless of whether
|
||||||
|
// this write does -- report it separately rather than failing the
|
||||||
|
// whole request on an EEPROM hiccup.
|
||||||
|
bool persisted = false;
|
||||||
|
if (ctx->antennaCalibration->saveXtal != nullptr) {
|
||||||
|
persisted = ctx->antennaCalibration->saveXtal(
|
||||||
|
parsed->ibias, parsed->ctun, parsed->xtalFreqHz);
|
||||||
|
}
|
||||||
|
|
||||||
const std::string json =
|
const std::string json =
|
||||||
std::string("{\"status\":\"recalibrated\",\"ibias\":")
|
std::string("{\"status\":\"recalibrated\",\"ibias\":")
|
||||||
+ std::to_string(parsed->ibias) + ",\"ctun\":"
|
+ std::to_string(parsed->ibias) + ",\"ctun\":"
|
||||||
+ std::to_string(parsed->ctun) + ",\"xtal_freq_hz\":"
|
+ std::to_string(parsed->ctun) + ",\"xtal_freq_hz\":"
|
||||||
+ std::to_string(parsed->xtalFreqHz) + "}";
|
+ std::to_string(parsed->xtalFreqHz) + ",\"persisted\":"
|
||||||
|
+ (persisted ? "true" : "false") + "}";
|
||||||
httpd_resp_set_type(req, "application/json");
|
httpd_resp_set_type(req, "application/json");
|
||||||
return httpd_resp_send(req, json.c_str(), json.size());
|
return httpd_resp_send(req, json.c_str(), json.size());
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-5
@@ -204,7 +204,7 @@ Done in fw 0.8.5 unless noted:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## TODO — calibration functions need to become permanent, in-firmware, on-demand tools (2026-08-23)
|
## DONE (2026-08-24) — calibration functions are now permanent, in-firmware, on-demand tools
|
||||||
|
|
||||||
Both ANTCAP calibration (`tools/si4684_antenna_calibration.py`) and Si4684
|
Both ANTCAP calibration (`tools/si4684_antenna_calibration.py`) and Si4684
|
||||||
crystal calibration (`tools/si4684_xtal_calibration.py`,
|
crystal calibration (`tools/si4684_xtal_calibration.py`,
|
||||||
@@ -236,10 +236,24 @@ convergence-loop logic (currently in `tools/si4684_xtal_calibration.py`)
|
|||||||
moves into firmware, or stays host-side with just an EEPROM-persist step
|
moves into firmware, or stays host-side with just an EEPROM-persist step
|
||||||
added at the end of the existing HTTP flow.
|
added at the end of the existing HTTP flow.
|
||||||
|
|
||||||
Not started — explicitly deferred to a future session, noted here only so
|
**Resolved 2026-08-24.** `Eeprom24aa` gained `readXtalCalibration()` /
|
||||||
it isn't lost. See `docs/si4684-rf-investigation-report.md`'s 2026-08-23
|
`writeXtalCalibration()` (word addresses 0x02 ibias, 0x03 ctun, 0x04-0x07
|
||||||
entry for full context on why this calibration was needed and how it
|
xtalFreqHz big-endian, right after the existing FM/DAB ANTCAP bytes at
|
||||||
currently works.
|
0x00/0x01). `HardwareBootstrap::boot()` now reads the ADAU1701 boot
|
||||||
|
earlier (moved before Si4684's, since the EEPROM read needs ADAU1701's
|
||||||
|
I2C bus) and loads the saved crystal trim before calling
|
||||||
|
`gSi4684.boot(...)`, falling back to the compiled-in defaults
|
||||||
|
(ibias=72, ctun=0, xtalFreqHz=19199750) when the EEPROM has never been
|
||||||
|
calibrated. `POST /api/tuner/xtal-calibrate` now persists every
|
||||||
|
successful live recalibration automatically (`"persisted":true/false` in
|
||||||
|
the response) via a new `saveXtalCalibration()` /
|
||||||
|
`net::AntennaCalibration::saveXtal` bridge, mirroring the existing
|
||||||
|
ANTCAP save pattern. The convergence-loop logic
|
||||||
|
(`tools/si4684_xtal_calibration.py`) stays host-side, unchanged — only
|
||||||
|
the persistence gap was closed.
|
||||||
|
|
||||||
|
See `docs/si4684-rf-investigation-report.md`'s 2026-08-23 entry for full
|
||||||
|
context on why this calibration was needed and how it currently works.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
# Note per l'app companion (igiRadio) — sync 2026-08-24
|
||||||
|
|
||||||
|
Riepilogo dei cambi firmware di oggi rilevanti per l'app iOS. Incollare in Cursor come contesto.
|
||||||
|
|
||||||
|
## 1. Nuovo endpoint: codec A2DP Bluetooth
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/bluetooth/a2dp-codec
|
||||||
|
POST /api/bluetooth/a2dp-codec body: {"codec_mask": <0-63>}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `GET` risponde `{"codec":"sbc"}` (valori possibili: `sbc`, `aptx`, `aptx-hd`, `aptx-ll`, `aptx-adaptive`; se il modulo BT1035 negozia AAC la risposta può non essere interpretabile — il datasheet Feasycom non documenta un codice di ritorno per AAC, quindi in quel caso l'app deve gestire un valore/errore sconosciuto senza andare in crash).
|
||||||
|
- `POST` con `codec_mask` è una bitmask: `BIT0`=AAC, `BIT1`=aptX, `BIT2`=aptX-LL, `BIT3`=aptX-HD, `BIT4`=aptX-Adaptive, `BIT5`=LDAC. `0` forza solo SBC (baseline obbligatorio). Risposta `{"status":"saved"}` o `{"status":"error","reason":"..."}`.
|
||||||
|
- **Importante**: il cambio vale solo per la prossima negoziazione — se un dispositivo Bluetooth è già connesso, serve chiamare `POST /api/bluetooth/disconnect` e far riconnettere il device (dal lato telefono/speaker) perché il nuovo codec venga effettivamente usato. Se l'app espone questa funzione in UI, considerare di mostrare un messaggio tipo "riconnetti il dispositivo Bluetooth per applicare".
|
||||||
|
|
||||||
|
## 2. Fix: salvataggio profilo audio (EQ/mixer) ora persiste davvero
|
||||||
|
|
||||||
|
Prima di oggi, `PUT /api/audio/profile` applicava le modifiche dal vivo ma **falliva sempre** silenziosamente nel salvataggio permanente (bug: nome chiave NVS troppo lungo). L'app probabilmente vedeva errori intermittenti o impostazioni che sparivano al riavvio della scheda. Ora è risolto e verificato: le modifiche sopravvivono a un riavvio. Nessun cambio di formato richiesto lato app — stesso schema JSON di sempre.
|
||||||
|
|
||||||
|
## 3. Fix: il parser JSON del firmware ora tollera JSON non-compatto
|
||||||
|
|
||||||
|
Prima di oggi il parser lato firmware richiedeva JSON strettamente compatto (`{"key":"value"}`, **nessuno spazio** dopo i due punti) — un `JSONEncoder` Swift in modalità non-compatta (es. con `.prettyPrinted`, o formattazione di default in alcune configurazioni) poteva produrre `"key": "value"` e far fallire il parsing lato server con `invalid_json` o `missing_field`.
|
||||||
|
|
||||||
|
Questo è ora risolto per tutti gli endpoint (tuner, audio profile, stazioni, bluetooth, wifi, streaming, DSP param). **Se nell'app c'era un workaround per forzare JSON compatto** (es. `JSONEncoder().outputFormatting` impostato esplicitamente senza spazi, o costruzione manuale di stringhe JSON), non è più necessario ma può restare — è comunque compatibile.
|
||||||
|
|
||||||
|
## 4. Nomi campi corretti (promemoria, riscontrati oggi durante i test manuali)
|
||||||
|
|
||||||
|
Attenzione a questi nomi campo esatti attesi dal firmware — un nome sbagliato produce `missing_field`, non necessariamente un errore chiaro:
|
||||||
|
|
||||||
|
- `POST /api/tuner/tune` per DAB: `{"band":"dab","freq_index":<0-37>}` — **non** `"frequency"`.
|
||||||
|
- `POST /api/tuner/tune` per FM: `{"band":"fm","frequency_khz":<khz>}`.
|
||||||
|
- `POST /api/stations` per una stazione FM: `{"name":"...","band":"fm","fm_frequency_khz":<khz>}` — **non** `"frequency_khz"` a livello radice.
|
||||||
|
- `POST /api/stations` per una stazione DAB: `{"name":"...","band":"dab","dab_freq_index":<0-37>}` (opzionali `dab_service_id`, `dab_component_id`).
|
||||||
|
- `POST /api/stations/remove`: `{"index":<n>}` — **non** `{"name":"..."}`.
|
||||||
|
|
||||||
|
Se l'app usa nomi diversi da questi in qualche punto, verificare contro `components/core/src/StationListJson.cpp` e `components/core/src/TunerJson.cpp` nel repo firmware (fonte di verità).
|
||||||
|
|
||||||
|
## 5. Nessun cambio di schema per mixer/EQ
|
||||||
|
|
||||||
|
Lo schema di `GET|PUT /api/audio/profile` è invariato:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mixer": {
|
||||||
|
"si4684_left_db": 0, "si4684_right_db": 0,
|
||||||
|
"esp32_left_db": -96, "esp32_right_db": -96,
|
||||||
|
"mix_left_db": 0, "mix_right_db": -96
|
||||||
|
},
|
||||||
|
"master": {"left_db": 0, "right_db": 0},
|
||||||
|
"eq": [
|
||||||
|
{"gain_db": 0, "center_hz": 20, "q": 1.414},
|
||||||
|
{"gain_db": 0, "center_hz": 100, "q": 1},
|
||||||
|
{"gain_db": 0, "center_hz": 400, "q": 1},
|
||||||
|
{"gain_db": 0, "center_hz": 1000, "q": 1},
|
||||||
|
{"gain_db": 0, "center_hz": 3000, "q": 1},
|
||||||
|
{"gain_db": 0, "center_hz": 8000, "q": 1}
|
||||||
|
],
|
||||||
|
"enhancements": {"stereo_level": 0, "bass_level": 0}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Nota: `esp32_left_db`/`esp32_right_db`/`mix_right_db` a `-96` = leg ESP32 (streaming web radio) mutato, `si4684_*`/`mix_left_db` a `0` = leg radio (FM/DAB) aperto — è il default "radio-first" di fabbrica. Se l'app vuole passare a streaming web-radio senza sentire anche la radio in sovrapposizione, deve invertire questi gain (radio a `-96`, esp32 a `0`) — non è automatico solo abilitando `POST /api/streaming`.
|
||||||
|
|
||||||
|
## 6. Bug noto, non ancora risolto
|
||||||
|
|
||||||
|
- `PUT /api/audio/profile` con `enhancements` (stereo/bass) diverso da zero **sovrascrive** eventuali modifiche manuali dell'EQ — bug già tracciato, non toccato oggi.
|
||||||
|
- La banda EQ indice 0 (20 Hz) è in realtà un filtro passa-alto fisso non modificabile dal DSP — il valore `gain_db` mostrato/inviato per quella banda è cosmetico, non ha effetto reale sul suono.
|
||||||
@@ -25,6 +25,13 @@ bool recalibrateXtal(std::uint8_t ibias, std::uint8_t ctun,
|
|||||||
.recalibrateXtal(ibias, ctun, xtalFreqHz));
|
.recalibrateXtal(ibias, ctun, xtalFreqHz));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Persist to EEPROM, 2026-08-24: see net::AntennaCalibration::saveXtal. */
|
||||||
|
bool saveXtal(std::uint8_t ibias, std::uint8_t ctun, std::uint32_t xtalFreqHz)
|
||||||
|
{
|
||||||
|
return hardware::HardwareBootstrap::saveXtalCalibration(ibias, ctun,
|
||||||
|
xtalFreqHz);
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
net::AntennaCalibration& bridge() noexcept
|
net::AntennaCalibration& bridge() noexcept
|
||||||
@@ -33,6 +40,7 @@ net::AntennaCalibration& bridge() noexcept
|
|||||||
.save = &hardware::HardwareBootstrap::saveFmAntCapCalibration,
|
.save = &hardware::HardwareBootstrap::saveFmAntCapCalibration,
|
||||||
.saveDab = &hardware::HardwareBootstrap::saveDabAntCapCalibration,
|
.saveDab = &hardware::HardwareBootstrap::saveDabAntCapCalibration,
|
||||||
.recalibrateXtal = &recalibrateXtal,
|
.recalibrateXtal = &recalibrateXtal,
|
||||||
|
.saveXtal = &saveXtal,
|
||||||
};
|
};
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -160,25 +160,9 @@ std::expected<void, HardwareBootError> HardwareBootstrap::boot()
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
// xtalCtun=0, xtalFreqHz=19199750 (2026-08-23): the compiled-in
|
// ADAU1701 boots first (independent I2C/SPI chips, no cross-dependency)
|
||||||
// defaults (ctun=31, xtal=19200000 nominal) were never measured
|
// so its I2C bus is available for the EEPROM read below, needed to load
|
||||||
// against this board's actual crystal (Abracon ABM8-19.200MHZ-10-1-U-T,
|
// the Si4684 crystal trim before Si4684 itself boots.
|
||||||
// CL=10pF per part number, plus two external 15pF load caps per the
|
|
||||||
// schematic). CTUN=0 was found audibly best via A/B listening (0/5/31),
|
|
||||||
// then xtalFreqHz was trimmed properly using the chip's own FM_RSQ
|
|
||||||
// FREQOFF measurement (tools/si4684_xtal_calibration.py) against two
|
|
||||||
// real, GPS-locked broadcast carriers 87.6/105.1 MHz -- converged to
|
|
||||||
// -3 to -4 ppm residual on both (cross-check confirms it's the
|
|
||||||
// crystal, not something frequency-dependent), down from +70 ppm
|
|
||||||
// uncorrected. See POST /api/tuner/xtal-calibrate to re-trim live if
|
|
||||||
// this ever needs revisiting (e.g. after a board/crystal change).
|
|
||||||
if (auto tunerResult =
|
|
||||||
gSi4684.boot(si4684::Si4684Band::Dab, 72U, 0U, 19199750U);
|
|
||||||
!tunerResult) {
|
|
||||||
ESP_LOGE(kTag, "Si4684 boot failed: error %d", static_cast<int>(tunerResult.error()));
|
|
||||||
return std::unexpected(HardwareBootError::Si4684BootFailed);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!gAdau1701.isBooted()) {
|
if (!gAdau1701.isBooted()) {
|
||||||
auto dspResult = gAdau1701.boot();
|
auto dspResult = gAdau1701.boot();
|
||||||
if (!dspResult) {
|
if (!dspResult) {
|
||||||
@@ -188,6 +172,51 @@ std::expected<void, HardwareBootError> HardwareBootstrap::boot()
|
|||||||
}
|
}
|
||||||
|
|
||||||
eeprom24aa::Eeprom24aa eeprom = makeEeprom();
|
eeprom24aa::Eeprom24aa eeprom = makeEeprom();
|
||||||
|
|
||||||
|
// Fallback defaults (2026-08-23): ctun=0, xtalFreqHz=19199750 -- the
|
||||||
|
// compiled-in defaults (ctun=31, xtal=19200000 nominal) were never
|
||||||
|
// measured against this board's actual crystal (Abracon
|
||||||
|
// ABM8-19.200MHZ-10-1-U-T, CL=10pF per part number, plus two external
|
||||||
|
// 15pF load caps per the schematic). CTUN=0 was found audibly best via
|
||||||
|
// A/B listening (0/5/31), then xtalFreqHz was trimmed properly using
|
||||||
|
// the chip's own FM_RSQ FREQOFF measurement
|
||||||
|
// (tools/si4684_xtal_calibration.py) against two real, GPS-locked
|
||||||
|
// broadcast carriers 87.6/105.1 MHz -- converged to -3 to -4 ppm
|
||||||
|
// residual on both, down from +70 ppm uncorrected. Used only when the
|
||||||
|
// EEPROM has never been calibrated (or every board would need the same
|
||||||
|
// physical crystal tolerance, which isn't guaranteed). See POST
|
||||||
|
// /api/tuner/xtal-calibrate to re-trim live, and POST it again to
|
||||||
|
// persist -- see saveXtalCalibration() below.
|
||||||
|
std::uint8_t xtalIbias = 72U;
|
||||||
|
std::uint8_t xtalCtun = 0U;
|
||||||
|
std::uint32_t xtalFreqHz = 19199750U;
|
||||||
|
if (auto xtal = eeprom.readXtalCalibration(); xtal) {
|
||||||
|
if (*xtal) {
|
||||||
|
xtalIbias = (*xtal)->ibias;
|
||||||
|
xtalCtun = (*xtal)->ctun;
|
||||||
|
xtalFreqHz = (*xtal)->xtalFreqHz;
|
||||||
|
ESP_LOGI(kTag,
|
||||||
|
"Xtal calibration loaded: ibias=%u ctun=%u "
|
||||||
|
"xtal_freq_hz=%lu",
|
||||||
|
static_cast<unsigned>(xtalIbias),
|
||||||
|
static_cast<unsigned>(xtalCtun),
|
||||||
|
static_cast<unsigned long>(xtalFreqHz));
|
||||||
|
} else {
|
||||||
|
ESP_LOGI(kTag,
|
||||||
|
"Xtal not calibrated — using compiled-in defaults");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ESP_LOGW(kTag, "Xtal calibration read failed — using compiled-in "
|
||||||
|
"defaults");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auto tunerResult = gSi4684.boot(si4684::Si4684Band::Dab, xtalIbias,
|
||||||
|
xtalCtun, xtalFreqHz);
|
||||||
|
!tunerResult) {
|
||||||
|
ESP_LOGE(kTag, "Si4684 boot failed: error %d", static_cast<int>(tunerResult.error()));
|
||||||
|
return std::unexpected(HardwareBootError::Si4684BootFailed);
|
||||||
|
}
|
||||||
|
|
||||||
if (auto identity = eeprom.readDeviceIdentity(); identity) {
|
if (auto identity = eeprom.readDeviceIdentity(); identity) {
|
||||||
gDeviceIdentity = std::move(*identity);
|
gDeviceIdentity = std::move(*identity);
|
||||||
ESP_LOGI(kTag, "unit serial %.*s",
|
ESP_LOGI(kTag, "unit serial %.*s",
|
||||||
@@ -315,4 +344,22 @@ bool HardwareBootstrap::saveDabAntCapCalibration(std::uint8_t antCap)
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool HardwareBootstrap::saveXtalCalibration(std::uint8_t ibias,
|
||||||
|
std::uint8_t ctun,
|
||||||
|
std::uint32_t xtalFreqHz)
|
||||||
|
{
|
||||||
|
eeprom24aa::Eeprom24aa eeprom = makeEeprom();
|
||||||
|
const eeprom24aa::XtalCalibration calibration{
|
||||||
|
.ibias = ibias, .ctun = ctun, .xtalFreqHz = xtalFreqHz};
|
||||||
|
if (auto written = eeprom.writeXtalCalibration(calibration); !written) {
|
||||||
|
ESP_LOGW(kTag, "Xtal calibration write failed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
ESP_LOGI(kTag,
|
||||||
|
"Xtal calibration saved: ibias=%u ctun=%u xtal_freq_hz=%lu",
|
||||||
|
static_cast<unsigned>(ibias), static_cast<unsigned>(ctun),
|
||||||
|
static_cast<unsigned long>(xtalFreqHz));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace hardware
|
} // namespace hardware
|
||||||
|
|||||||
@@ -197,6 +197,26 @@ public:
|
|||||||
* @date 2026-08-20
|
* @date 2026-08-20
|
||||||
*/
|
*/
|
||||||
[[nodiscard]] static bool saveDabAntCapCalibration(std::uint8_t antCap);
|
[[nodiscard]] static bool saveDabAntCapCalibration(std::uint8_t antCap);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief saveXtalCalibration — persist the Si4684 crystal trim.
|
||||||
|
*
|
||||||
|
* @dname saveXtalCalibration
|
||||||
|
* @param ibias Value found via POST /api/tuner/xtal-calibrate.
|
||||||
|
* @param ctun Value found via POST /api/tuner/xtal-calibrate.
|
||||||
|
* @param xtalFreqHz Value found via POST /api/tuner/xtal-calibrate.
|
||||||
|
* @return true on success, false on an I2C failure.
|
||||||
|
* @pubstate writes the 24AA025E48 user region. Does not itself reboot
|
||||||
|
* the Si4684 — callers apply the values live via
|
||||||
|
* Si4684Tuner::recalibrateXtal() first; boot() reads this back
|
||||||
|
* on the next power-up so the trim survives a reboot.
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-08-24
|
||||||
|
*/
|
||||||
|
[[nodiscard]] static bool saveXtalCalibration(std::uint8_t ibias,
|
||||||
|
std::uint8_t ctun,
|
||||||
|
std::uint32_t xtalFreqHz);
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace hardware
|
} // namespace hardware
|
||||||
|
|||||||
Reference in New Issue
Block a user