Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 4 additions & 8 deletions components/bq25629/include/bq25629.h
Original file line number Diff line number Diff line change
Expand Up @@ -421,15 +421,11 @@ class BQ25629 {
esp_err_t estimate_battery_percent(uint8_t &percent);

/**
* @brief Configure JEITA temperature profile
* @brief Configure battery temperature protection
*
* Sets temperature thresholds and charge current limits:
* - TH1 = 0°C (cold threshold)
* - TH2 = 10°C (cool threshold)
* - TH5 = 45°C (warm threshold)
* - TH6 = 60°C (hot threshold)
* - COOL/WARM zones: 20% charge current
* - NORMAL zone: 100% charge current
* Charging is suspended below 0°C and above 45°C. The COOL zone from
* 0°C to 10°C uses 20% charge current. OTG is suspended outside the
* -10°C to 60°C range. Written values are read back and verified.
*
* @return ESP_OK on success
*/
Expand Down
83 changes: 49 additions & 34 deletions components/bq25629/src/bq25629.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,12 @@ constexpr uint8_t CHG_STAT_SHIFT = 3;
constexpr uint8_t VBUS_STAT_MASK = 0x07;
} // namespace BIT_MASK

namespace NTC_PROFILE {
constexpr uint8_t CONTROL_0 = 0x31;
constexpr uint8_t CONTROL_1 = 0x25;
constexpr uint8_t CONTROL_2 = 0x3F;
} // namespace NTC_PROFILE

esp_err_t BQ25629::enable_auto_ibat_discharge(bool enable) {
esp_err_t ret = modify_register(BQ25629_REG::CHARGER_CONTROL_0, BIT_MASK::EN_AUTO_IBATDIS,
enable ? BIT_MASK::EN_AUTO_IBATDIS : 0);
Expand Down Expand Up @@ -184,6 +190,20 @@ esp_err_t BQ25629::init(const BQ25629_Config &config) {

ESP_LOGI(TAG, "%s found, Part Info: 0x%02X", part_name, part_info);

// EN_CHG defaults to 1 after reset. Hold charging off until the battery
// temperature profile and remaining charger settings are established.
ret = enable_charging(false);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to disable charging during initialization");
return ret;
}

ret = configure_jeita_profile();
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to configure battery temperature profile");
return ret;
}

// Best-effort: enable EN_AUTO_IBATDIS (CHARGER_CONTROL_0 bit7).
// Keep init running even if this fails.
(void)enable_auto_ibat_discharge(true);
Expand Down Expand Up @@ -971,45 +991,40 @@ esp_err_t BQ25629::system_power_reset() {
}

esp_err_t BQ25629::configure_jeita_profile() {
ESP_LOGI(TAG, "Configuring JEITA temperature profile");
esp_err_t ret;
struct RegisterSetting {
uint8_t address;
uint8_t value;
const char *name;
};

// REG0x1A (NTC_Control_0): Set COOL/WARM charge current to 20%
// Bits [7:6] TS_ISET_WARM = 01 (20%)
// Bits [5:4] TS_ISET_COOL = 01 (20%)
// Value: 0x25 = 0b00100101
ret = write_register(BQ25629_REG::NTC_CONTROL_0, 0x25);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to write NTC_CONTROL_0: %s", esp_err_to_name(ret));
return ret;
}
ESP_LOGD(TAG, "NTC_CONTROL_0 = 0x25 (WARM/COOL = 20%%)");
static constexpr RegisterSetting PROFILE[] = {
{BQ25629_REG::NTC_CONTROL_0, NTC_PROFILE::CONTROL_0, "NTC_CONTROL_0"},
{BQ25629_REG::NTC_CONTROL_1, NTC_PROFILE::CONTROL_1, "NTC_CONTROL_1"},
{BQ25629_REG::NTC_CONTROL_2, NTC_PROFILE::CONTROL_2, "NTC_CONTROL_2"},
};

// REG0x1B (NTC_Control_1): Set temperature thresholds
// TS_TH6 [7:6] = 00 (60°C hot threshold)
// TS_TH5 [5:4] = 01 (45°C warm threshold)
// TS_TH2 [3:2] = 01 (10°C cool threshold)
// TS_TH1 [1:0] = 11 (0°C cold threshold)
// Value: 0x27 = 0b00100111
ret = write_register(BQ25629_REG::NTC_CONTROL_1, 0x27);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to write NTC_CONTROL_1: %s", esp_err_to_name(ret));
return ret;
}
ESP_LOGD(TAG, "NTC_CONTROL_1 = 0x27 (TH1=0°C, TH2=10°C, TH5=45°C, TH6=60°C)");
ESP_LOGI(TAG, "Configuring battery temperature profile");
for (const RegisterSetting &setting : PROFILE) {
esp_err_t ret = write_register(setting.address, setting.value);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to write %s: %s", setting.name, esp_err_to_name(ret));
return ret;
}

// REG0x1C (NTC_Control_2): Keep default voltage settings
// VRECHG_TH_PREWARM/PRECOOL unchanged
// Value: 0x3F (default)
ret = write_register(BQ25629_REG::NTC_CONTROL_2, 0x3F);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to write NTC_CONTROL_2: %s", esp_err_to_name(ret));
return ret;
uint8_t readback = 0;
ret = read_register(setting.address, readback);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to verify %s: %s", setting.name, esp_err_to_name(ret));
return ret;
}
if (readback != setting.value) {
ESP_LOGE(TAG, "%s verification failed: wrote 0x%02X, read 0x%02X", setting.name,
setting.value, readback);
return ESP_ERR_INVALID_RESPONSE;
}
}
ESP_LOGD(TAG, "NTC_CONTROL_2 = 0x3F (default voltage settings)");

ESP_LOGI(TAG, "JEITA profile OK (0/10/45/60\u00b0C, COOL/WARM=20%%)");

ESP_LOGI(TAG, "Battery temperature profile configured (charge 0-45°C, OTG -10-60°C)");
return ESP_OK;
}

Expand Down
14 changes: 9 additions & 5 deletions products/go/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -697,8 +697,9 @@ Button 1 (`PIN_BUTTON_POWER`, GPIO5) is wired to **both** the ESP32 GPIO
`enter_ship_mode()` is refused and the path falls back to deep sleep, so
the restart behavior only applies on battery.

Ship mode is also triggered automatically by the EDV and OT safety trips —
see [Power Management](docs/power_management.md) for details.
Ship mode is also triggered automatically by the EDV and high- or
low-battery-temperature safety trips — see
[Power Management](docs/power_management.md) for details.

## Services

Expand Down Expand Up @@ -782,9 +783,12 @@ Two tiers of storage:
can exceed 1S cell-protection OCP. Between measurements the SPS30 is
power-managed via its native Sleep command. See
[`docs/power_management.md`](docs/power_management.md#why-pmid-is-session-armed)
- **Cell safety trips:** EDV (over-discharge at 2.9 V, 3-poll debounce)
and OT (charge cutoff at 50 C / resume at 47 C, ship mode at 60 C)
fire `enter_ship_mode()` to protect the battery
- **Cell safety trips:** EDV uses a 2.9 V, three-poll debounce. Battery charging
is permitted from 0 °C through 45 °C and recovers from a temperature or
invalid-NTC block only from 2 °C through 43 °C. An invalid NTC disables
charging without shutdown. Discharge is permitted from -10 °C through 60 °C;
crossing either limit requests the corresponding cold- or hot-temperature
ship-mode shutdown
- **Fuel gauge (V1 only):** `PowerService::set_fuel_gauge()` attaches an
already-initialised `FuelGaugeDevice` for runtime SOC reads. `poll_bms()`
prefers FG-derived SOC and tags the log line with `src=FG|BMS`
Expand Down
8 changes: 6 additions & 2 deletions products/go/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,12 @@ onboarding. Button 2 long press remains factory reset.
- **EDV (over-discharge):** ship mode requested when cell voltage stays
below 2.9 V for 3 consecutive polls while on battery. The orchestrator
shows a warning on `Screen::Info` before entering ship mode.
- **OT (over-temperature):** charge cutoff at 50 C (resume at 47 C);
ship mode requested at 60 C with a warning display before shutdown.
- **Battery-temperature protection:** charging is allowed from 0 °C through
45 °C. After a temperature or invalid-NTC block, charging resumes only from
2 °C through 43 °C. Discharging is allowed from -10 °C through 60 °C;
temperatures outside that range cause distinct cold- or hot-temperature
shutdowns. An invalid NTC reading disables charging only and does not request
shutdown.
- **Full-charge pause:** when the battery is full and USB is present,
charging is disabled to reduce cell stress. Resumes when SOC drops
to 95 %. V1 uses the BQ27427 FC flag; Prototype falls back to
Expand Down
5 changes: 3 additions & 2 deletions products/go/docs/ble_service.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ is NOTIFY-only and does **not** touch the snapshot:
| `update_status()` | No (set-value only) | — | Steady-state polls (BMS, GPS fix, history-delete, on-connect snapshot) |
| `notify_tracking_status()` | Yes, when subscribed | `{tracking, session}` | Urgent tracking transitions: start success, start failure, manual stop |
| `notify_charging_status()` | Yes, when subscribed | `{charging, bat_pct, bat_v}` | Charging transitions: plug in, unplug, charge complete |
| `notify_disconnect()` | Yes, when connected | `{disc}` | Imminent link drop: shutdown (overheat / low_batt / user) or leaving Portable (op_stationary / op_offline) |
| `notify_disconnect()` | Yes, when connected | `{disc}` | Imminent link drop: shutdown (`overheat` for hot or cold battery temperature, `low_batt`, or `user`) or leaving Portable (`op_stationary` or `op_offline`) |

The delta shapes have **disjoint keys** and carry **no** `"type"` discriminator,
so the client merges whichever keys arrive. The Read value stays the full 9-key
Expand Down Expand Up @@ -313,6 +313,7 @@ briefly before teardown so the fire-and-forget notice can drain:
| `change_mode()` leaving Portable → Stationary | `op_stationary` | `BLE_MODE_CHANGE_NOTIFY_SETTLE_MS` (200 ms) |
| `change_mode()` leaving Portable → Offline | `op_offline` | `BLE_MODE_CHANGE_NOTIFY_SETTLE_MS` (200 ms) |
| `shutdown(OverTemperature)` | `overheat` | `SHUTDOWN_POWER_OFF_SETTLE_MS` (500 ms post-paint dwell) |
| `shutdown(UnderTemperature)` | `overheat` | `SHUTDOWN_POWER_OFF_SETTLE_MS` (500 ms post-paint dwell) |
| `shutdown(OverDischarge)` | `low_batt` | `SHUTDOWN_POWER_OFF_SETTLE_MS` (500 ms) |
| `shutdown(None)` — user long-press | `user` | `SHUTDOWN_POWER_OFF_SETTLE_MS` (500 ms) |

Expand Down Expand Up @@ -900,7 +901,7 @@ failed `setup_ble()` is non-fatal (advertise without OTA). See
| `update_status(power, gps, tracking, session_id)` | Encode via `encode_status()`, `set_value()` only. Sole writer of the Status snapshot. Used for steady-state polls (BMS, GPS fix, history-delete reconciliation). |
| `notify_tracking_status(power, gps, tracking, session_id)` | Refreshes the full 9-key snapshot via `update_status()` (Read stays full), then pushes a `{tracking, session}` transition delta via `notify(data, len)`. Used for urgent tracking transitions (start success, start failure, manual stop). Best-effort delivery — Read remains authoritative. |
| `notify_charging_status(power, gps, tracking, session_id)` | Refreshes the full 9-key snapshot via `update_status()` (Read stays full), then pushes a `{charging, bat_pct, bat_v}` power delta via `notify(data, len)`. Used for charging transitions (plug in, unplug, charge complete). Disjoint keys from the tracking delta, no `"type"` discriminator — client merges by key. |
| `notify_disconnect(reason)` | Pushes a NOTIFY-only `{disc}` delta via `notify(data, len)` (snapshot untouched) announcing an imminent link drop and why (`overheat`/`low_batt`/`user`/`op_stationary`/`op_offline`). Called from `change_mode()` (leaving Portable) and `shutdown()`; gated on `is_connected()`; the caller settles before teardown so it can drain. |
| `notify_disconnect(reason)` | Pushes a NOTIFY-only `{disc}` delta via `notify(data, len)` (snapshot untouched) announcing an imminent link drop and why (`overheat`/`low_batt`/`user`/`op_stationary`/`op_offline`). Both `OverTemperature` and `UnderTemperature` use the legacy `overheat` value. Called from `change_mode()` (leaving Portable) and `shutdown()`; gated on `is_connected()`; the caller settles before teardown so it can drain. |
| `update_config(settings)` | Encode the full snapshot via `encode_config()` (16 keys, no `"type"`), `set_value()` only. Sole writer of the Config snapshot; buffer sized to the 512-byte ATT ceiling. |
| `notify_config(prev, cur)` | Refreshes the snapshot via `update_config(cur)`, then sends the changed-fields delta (`encode_config_delta()`: `"type":"config"` + changed keys) via `notify(data, len)`. |
| `notify_command_progress(cmd)` | Inline CBOR encoding (2 keys: type + cmd), `notify(data, len)` (stored value untouched). Sent before long-running commands. |
Expand Down
11 changes: 7 additions & 4 deletions products/go/docs/display_service.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,8 @@ hardware-dependent and excluded from host builds (stubs provided).
| `Settings` / `SettingsChoice` / `TagList` / `About` / `Confirm` | Full-screen lists |
| `ShutdownUser` | Goodbye screen for user long-press shutdown ("Powered off" / "Hold button" / "to turn on") |
| `ShutdownDischarge` | Safety-trip shutdown for OverDischarge ("Battery critically low" / "Connect charger" / "Charge before use") |
| `ShutdownTemperature` | Safety-trip shutdown for OverTemperature ("Battery overheated" / "Let device cool" / "Keep out of sun") |
| `ShutdownTemperature` | High-temperature safety shutdown ("Battery overheated" / "Move device to a" / "cooler location") |
| `ShutdownTemperatureLow` | Low-temperature safety shutdown ("Battery too cold" / "Move device to a" / "warmer location") |
| `PairingPasskey` | Title-as-header + 3 px divider + large 6-digit BLE passkey + hint; no status bar, no snackbar |
| `Info` | Generic single-text presentation surface (cold-boot splash, Stationary bring-up narration); no status bar, no snackbar |
| `Provisioning` | Stationary Wi-Fi provisioning page (QR + status + action rows); no status bar, no snackbar |
Expand Down Expand Up @@ -473,15 +474,17 @@ Screen dispatch:
6-digit passkey (`logisoso32_tr`, baseline y=145), and "Enter on
phone" hint (`helvR12_tr`, baseline y=215). No status bar, no
snackbar.
- **ShutdownUser / ShutdownDischarge / ShutdownTemperature:** Unified
template — `"AirGradient"` brand header (`helvB14_tf`, baseline y=34),
- **ShutdownUser / ShutdownDischarge / ShutdownTemperature /
ShutdownTemperatureLow:** Unified template — `"AirGradient"` brand header
(`helvB14_tf`, baseline y=34),
3 px-thick divider at y=49, reason-specific icon centred at
(`SCREEN_W / 2`, y=94), and a title/action/detail text block
(`helvB14_tf` titles at y=151/169, `helvR12_tr` action at y=198,
`helvR08_tr` detail at y=214). Icons are drawn from u8g2 primitives
(power circle, battery body, thermometer with heat-wave lines). No
status bar, no snackbar. The renderer dispatches on the Screen
variant.
variant. The temperature icons use heat-wave lines for high temperature and
a snowflake for low temperature.

### Fonts

Expand Down
16 changes: 10 additions & 6 deletions products/go/docs/orchestrator.md
Original file line number Diff line number Diff line change
Expand Up @@ -489,11 +489,14 @@ Unified shutdown pipeline for all shutdown paths. Takes an optional

1. If a BLE client is connected, push a `disc` Status notice
(`notify_disconnect()` — `overheat` / `low_batt` / `user`) so the client knows
the link is about to drop. Sent early so it drains before power is cut.
the link is about to drop. Both high- and low-temperature shutdowns use the
legacy `overheat` value for protocol compatibility. Sent early so it drains
before power is cut.
2. Show the reason-specific shutdown screen — all variants share the
same unified template (brand header + icon + title/action/detail):
`Screen::ShutdownDischarge` for `OverDischarge`,
`Screen::ShutdownTemperature` for `OverTemperature`,
`Screen::ShutdownTemperatureLow` for `UnderTemperature`,
`Screen::ShutdownUser` for user-initiated long-press
3. Queue the shutdown frame with `update_display(wait=true)` and
`DisplayService::flush()` so the e-paper paint is complete before continuing
Expand All @@ -503,11 +506,12 @@ Unified shutdown pipeline for all shutdown paths. Takes an optional
painted reason screen remains visible and the `disc` notice can drain
7. `PowerService::shutdown()` — BMS ship mode → deep sleep fallback

Safety trips (EDV/OT) are detected by `poll_bms()` and signalled via
`PowerSnapshot::ship_mode_request`. The orchestrator checks this field
in `on_bms_timer()` and routes to `shutdown(reason)`. The `disc` notice is
the safety/user-shutdown counterpart of the leave-Portable notice in
[`change_mode()`](#change_mode).
EDV and high- or low-battery-temperature safety trips are detected by
`poll_bms()` and signalled via `PowerSnapshot::ship_mode_request`. Invalid NTC
disables charging but does not create a ship-mode request. The orchestrator
checks the request in `on_bms_timer()` and routes to `shutdown(reason)`. The
`disc` notice is the safety/user-shutdown counterpart of the leave-Portable
notice in [`change_mode()`](#change_mode).

## Stationary Networking

Expand Down
33 changes: 20 additions & 13 deletions products/go/docs/power_management.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@ For AGo, the power service also manages:
(`ShipModeRequest::OverDischarge`) when cell voltage stays below 2.9 V
for 3 consecutive polls while on battery. The orchestrator shows a
warning on `Screen::Info` then calls `shutdown()`
- **OT (over-temperature) trip:** two-tier policy — charge cutoff at 50 °C
with 47 °C hysteresis resume; requests ship mode
(`ShipModeRequest::OverTemperature`) at 60 °C. The orchestrator shows a
warning then calls `shutdown()`
- **Battery-temperature protection:** charging is allowed from 0 °C through
45 °C. After a temperature or invalid-NTC block, charging resumes only from
2 °C through 43 °C. Discharging is allowed from -10 °C through 60 °C;
temperatures outside that range request the corresponding
`ShipModeRequest::UnderTemperature` or `ShipModeRequest::OverTemperature`.
An invalid NTC reading disables charging only and does not request shutdown
- **Full-charge pause:** disables charging when the battery is full and
USB is present. Resumes when SOC drops to 95 %. V1 detects full via
BQ27427 FC flag; Prototype falls back to `ChargeTerminationDone` +
Expand Down Expand Up @@ -69,7 +71,7 @@ fields default to invalid sentinels (`BmsInvalid::VOLT` / `-1.0f` / `false`).
| `fg_internal_temperature_c` | `float` | `-273.16` | FG die temperature (C) |
| `fg_flags` | `uint16_t` | `0` | FG flags register (decoded via `FgFlags::FC`, `CHG`, `DSG`, etc.) |
| `full_charge_paused` | `bool` | `false` | True when charging is paused because battery is full + USB present |
| `ship_mode_request` | `ShipModeRequest` | `None` | Non-`None` when a safety trip requires the orchestrator to show a warning and enter ship mode (`OverDischarge` or `OverTemperature`) |
| `ship_mode_request` | `ShipModeRequest` | `None` | Non-`None` when a safety trip requires warning and ship mode: `OverDischarge`, `OverTemperature`, or `UnderTemperature`. Invalid NTC alone never sets this field |

### SOC Source Preference

Expand Down Expand Up @@ -540,10 +542,12 @@ latches. The actual `set_charge_enable()` I2C write is only issued when it
would change the effective state. When either flag wants charging off, the
hardware stays off. The last flag to clear re-enables charging:

- OT resume with full-charge active → `_thermal_charge_disabled` cleared,
no `set_charge_enable(true)` (full-charge pause holds)
- Full-charge resume with thermal active → `_full_charge_paused` cleared,
no `set_charge_enable(true)` (thermal holds)
- Temperature or invalid-NTC recovery with full-charge active →
`_thermal_charge_disabled` clears only for a valid battery temperature from
2 °C through 43 °C; no `set_charge_enable(true)` (full-charge pause holds)
- Full-charge resume with temperature protection active →
`_full_charge_paused` cleared, no `set_charge_enable(true)` (temperature
protection holds)

### Snapshot

Expand Down Expand Up @@ -580,15 +584,18 @@ is refused (the deep-sleep fallback runs instead), so the restart
behavior applies only on battery.

Ship mode is no longer called directly from `poll_bms()`. Instead,
safety trips (EDV and OT) set `PowerSnapshot::ship_mode_request` and the
orchestrator handles the actual shutdown after displaying a warning.
safety trips (EDV and high- or low-battery-temperature) set
`PowerSnapshot::ship_mode_request`, and the orchestrator handles the actual
shutdown after displaying a warning. An invalid NTC reading disables charging
without setting a ship-mode request.

The orchestrator's unified `shutdown(ShipModeRequest reason)` pipeline:

1. Show the reason-specific shutdown screen — all variants share the
same unified template: `Screen::ShutdownDischarge` (EDV),
`Screen::ShutdownTemperature` (OT), or `Screen::ShutdownUser`
(user-initiated long-press)
`Screen::ShutdownTemperature` (high temperature),
`Screen::ShutdownTemperatureLow` (low temperature), or
`Screen::ShutdownUser` (user-initiated long-press)
2. Queue the shutdown frame with `update_display(wait=true)` and
`DisplayService::flush()` so the e-paper paint is complete before continuing
3. Stop tracking if active; backup chart cache
Expand Down
Loading
Loading