Files
L'électron rare f55093d6fe
ESP-IDF CI / Host Tests (Unity) (push) Successful in 1m8s
CI / firmware-native (push) Successful in 2m57s
Rust Protection Tests / Cargo test (host) (push) Failing after 3m21s
ESP-IDF CI / ESP-IDF Build (v5.4) (push) Failing after 6m55s
ESP-IDF CI / Memory Budget Gate (push) Has been skipped
qa-cicd-environments / qa-kxkm-s3-build (push) Successful in 8m53s
qa-cicd-environments / qa-sim-host (push) Successful in 2m2s
qa-cicd-environments / qa-kxkm-s3-memory-budget (push) Successful in 11m17s
chore: import KXKM Batterie Parallelator
Context: the project archive (KXKM_Batterie_Parallelator-main) had
no git history locally; a fresh repository is needed to host it on
git.saillant.cc (electron/KXKM_Batterie_Parallelator).

Approach: initialize a new repo on branch main, stage the archive
content, and harden .gitignore before the first commit.

Changes:
- Import the full project tree: firmware/, firmware-idf/,
  firmware-rs/, iosApp/, kxkm-bmu-app/, kxkm-api/, hardware/,
  docs/, specs/, scripts/, models/, tests/
- Keep project dotfiles tracked despite the trailing '.*' ignore
  rule: .github/, .claude/, .superpowers/, .gitattributes,
  .markdownlint.json
- Extend .gitignore: firmware/src/credentials.h (local secrets,
  template kept), kxkm-bmu-app/**/build/ (66 MB compiled iOS
  framework), .remember/ (session data)

Impact: the project can now be maintained on the self-hosted Gitea
forge with a clean, secret-free initial history.
2026-07-04 12:32:28 +02:00

204 lines
6.8 KiB
C++

#include "bmu_i2c.h"
#include "bmu_types.h"
#include "esp_log.h"
#include "driver/i2c_master.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "esp_task_wdt.h"
#include <atomic>
static const char *TAG = "I2C";
static SemaphoreHandle_t s_i2c_mutex = NULL;
static i2c_master_bus_handle_t s_bus_handle = NULL;
static std::atomic<int> s_consecutive_failures{0};
#define BMU_I2C_RECOVERY_THRESHOLD 5
esp_err_t bmu_i2c_init(i2c_master_bus_handle_t *bus_handle)
{
/* Le bus DOCK BSP (I2C_NUM_1, GPIO40/41) est deja cree par bsp_i2c_init().
* On recupere le handle existant plutot que de recreer le bus. */
esp_err_t ret = i2c_master_get_bus_handle(BMU_I2C_PORT, bus_handle);
if (ret == ESP_OK) {
s_bus_handle = *bus_handle;
ESP_LOGI(TAG, "BMU I2C bus OK — SDA=%d SCL=%d %dHz port=%d (BSP DOCK)",
BMU_I2C_SDA_GPIO, BMU_I2C_SCL_GPIO, BMU_I2C_FREQ_HZ, BMU_I2C_PORT);
} else {
ESP_LOGE(TAG, "BMU I2C get bus handle FAILED: %s", esp_err_to_name(ret));
return ret;
}
/* Create I2C bus mutex for multi-register atomic operations (audit H-01) */
// Priority inheritance: if prio-3 task holds this mutex and prio-8 protection
// task blocks on it, FreeRTOS boosts prio-3 to prio-8 until release.
s_i2c_mutex = xSemaphoreCreateMutex();
configASSERT(s_i2c_mutex != NULL);
return ret;
}
esp_err_t bmu_i2c_add_device(i2c_master_bus_handle_t bus, uint8_t addr,
i2c_master_dev_handle_t *dev)
{
i2c_device_config_t dev_config = {};
dev_config.dev_addr_length = I2C_ADDR_BIT_LEN_7;
dev_config.device_address = addr;
dev_config.scl_speed_hz = BMU_I2C_FREQ_HZ;
return i2c_master_bus_add_device(bus, &dev_config, dev);
}
esp_err_t bmu_i2c_probe(i2c_master_bus_handle_t bus, uint8_t addr, TickType_t timeout_ticks)
{
if (bmu_i2c_lock() != ESP_OK) {
return ESP_ERR_TIMEOUT;
}
esp_err_t ret = i2c_master_probe(bus, addr, timeout_ticks);
bmu_i2c_unlock();
return ret;
}
int bmu_i2c_scan(i2c_master_bus_handle_t bus)
{
int count = 0;
int consecutive_fails = 0;
const int MAX_CONSECUTIVE_FAILS = 8;
ESP_LOGI(TAG, "Scanning BMU I2C bus (0x20-0x4F)...");
/* Scan complet : add_device + read registres + rm_device.
* Sert aussi de warm-up pour les esclaves derriere l'ISO1540.
* Bail-out apres MAX_CONSECUTIVE_FAILS echecs consecutifs : si le bus
* est dead (SCL stuck), chaque transmit_receive spin >50ms (bug ESP-IDF
* 5.4 i2c_ll_is_bus_busy), 24 adresses → 30s+ → WDT trigger. */
const uint8_t ranges[][2] = { {0x20, 0x27}, {0x40, 0x4F} };
for (int r = 0; r < 2; r++) {
bool bail = false;
for (uint8_t addr = ranges[r][0]; addr <= ranges[r][1]; addr++) {
/* Feed WDT si main task est registered — ESP_ERR_INVALID_STATE
* signifie que la tache n'est pas enregistree, ce n'est pas une
* erreur dans ce contexte. */
esp_task_wdt_reset();
i2c_master_dev_handle_t dev = NULL;
i2c_device_config_t cfg = {};
cfg.dev_addr_length = I2C_ADDR_BIT_LEN_7;
cfg.device_address = addr;
cfg.scl_speed_hz = BMU_I2C_FREQ_HZ;
if (i2c_master_bus_add_device(bus, &cfg, &dev) != ESP_OK) {
consecutive_fails++;
if (consecutive_fails >= MAX_CONSECUTIVE_FAILS) {
ESP_LOGW(TAG, "Bus dead suspecte apres %d echecs consecutifs — bail", consecutive_fails);
bail = true;
break;
}
continue;
}
/* Try a read to verify presence */
uint8_t reg = (addr <= 0x27) ? 0x00 : 0x3E;
uint8_t rx[2] = {0};
esp_err_t ret = i2c_master_transmit_receive(dev, &reg, 1, rx,
(addr <= 0x27) ? 1 : 2, pdMS_TO_TICKS(50));
if (ret == ESP_OK) {
consecutive_fails = 0; /* reset sur succes */
if (addr <= 0x27) {
ESP_LOGI(TAG, " Found 0x%02X — TCA9535 (Port0=0x%02X)", addr, rx[0]);
} else {
uint16_t mfr = ((uint16_t)rx[0] << 8) | rx[1];
ESP_LOGI(TAG, " Found 0x%02X — INA237 (MFR=0x%04X)", addr, mfr);
}
count++;
} else {
consecutive_fails++;
}
i2c_master_bus_rm_device(dev);
if (consecutive_fails >= MAX_CONSECUTIVE_FAILS) {
ESP_LOGW(TAG, "Bus dead suspecte apres %d echecs consecutifs — bail", consecutive_fails);
bail = true;
break;
}
}
if (bail) break;
}
ESP_LOGI(TAG, "Scan complete: %d device(s)", count);
return count;
}
esp_err_t bmu_i2c_lock(void)
{
if (s_i2c_mutex == NULL) return ESP_ERR_INVALID_STATE;
return (xSemaphoreTake(s_i2c_mutex, pdMS_TO_TICKS(100)) == pdTRUE)
? ESP_OK : ESP_ERR_TIMEOUT;
}
void bmu_i2c_unlock(void)
{
if (s_i2c_mutex != NULL) xSemaphoreGive(s_i2c_mutex);
}
void bmu_i2c_record_success(void)
{
s_consecutive_failures.store(0);
}
void bmu_i2c_record_failure(void)
{
int count = ++s_consecutive_failures;
if (count >= BMU_I2C_RECOVERY_THRESHOLD) {
ESP_LOGW(TAG, "I2C: %d echecs consecutifs — recovery bus", count);
bmu_i2c_bus_recover();
s_consecutive_failures.store(0);
}
}
esp_err_t bmu_i2c_bus_recover(void)
{
/* Utiliser uniquement i2c_master_bus_reset() du driver ESP-IDF.
* L'ancien fallback bit-bang (gpio_config) detachait les pins du
* peripherique I2C, rendant le bus inutilisable apres recovery. */
if (s_bus_handle == NULL) return ESP_ERR_INVALID_STATE;
esp_err_t ret = i2c_master_bus_reset(s_bus_handle);
if (ret == ESP_OK) {
ESP_LOGI(TAG, "I2C bus reset OK");
} else {
ESP_LOGE(TAG, "I2C bus reset FAILED: %s", esp_err_to_name(ret));
}
return ret;
}
// ── Per-device health score tracking ──
void bmu_i2c_health_record_success(bmu_device_health_t *health) {
if (health->score <= BMU_HEALTH_SCORE_MAX - BMU_HEALTH_OK_INCR)
health->score += BMU_HEALTH_OK_INCR;
else
health->score = BMU_HEALTH_SCORE_MAX;
health->consec_fails = 0;
}
void bmu_i2c_health_record_failure(bmu_device_health_t *health) {
if (health->score >= BMU_HEALTH_FAIL_DECR)
health->score -= BMU_HEALTH_FAIL_DECR;
else
health->score = 0;
health->consec_fails++;
}
bool bmu_i2c_health_is_warn(const bmu_device_health_t *health) {
return health->score < BMU_HEALTH_THRESH_WARN;
}
bool bmu_i2c_health_is_critical(const bmu_device_health_t *health) {
return health->score < BMU_HEALTH_THRESH_CRIT;
}
bool bmu_i2c_health_can_reconnect(const bmu_device_health_t *health) {
return health->score >= BMU_HEALTH_THRESH_RECONNECT;
}