Synced from upstream — do not edit this file directly
This page is generated from
CARSSCenter/OpenNerve-Implantable-Pulse-Generator at Docs/IPG-Logging-Guide.md as of commit 7ab3cd3.
To change it, edit the file upstream and re-run tools/sync_docs.py.
Use the "Edit this page" link above to go straight to the upstream source.
IPG Logging Guide¶
OpenNerve Gen2 March 2026
Overview¶
The main STM32U585 MCU maintains a persistent event log stored in the external FRAM chip. Logs survive power cycles and sleep, are timestamped via the RTC, and can be read back over BLE. The nRF52810 BLE chip has no persistent logging — its debug output only appears on a live J-Link RTT connection during development.
Storage¶
Logs live in the FRAM (CY15B108QN) at:
| Region | Address | Size |
|---|---|---|
| Log data | 0x00000 |
128 KB |
| Log write pointer | 0x20000 |
4 bytes |
The log is a circular buffer. When it fills, the write pointer wraps to 0x00000 and old entries are silently overwritten. There is no overflow flag or warning.
Log Entry Format¶
Every entry is a human-readable ASCII string terminated by \r\n:
[20YY-MM-DDThh:mm:ssZ(uuu)]<TYPE>DATA\r\n
- Timestamp is UTC from the RTC.
uuuis sub-second resolution (units of 1/255 s, so 000–255). <TYPE>is one of four 4-character tags (including the angle brackets).
Data types¶
| Tag | Meaning | Example data |
|---|---|---|
<EV> |
Event | ER |
<BA> |
Battery voltage | A=3.7V, B=3.6V |
<IM> |
Impedance measurement | 12,500ohm |
<PA> |
Parameter change | (HSPID)Val=00010.0 |
Full example entries¶
[2026-03-15T14:32:05Z(128)]<EV>ER \r\n
[2026-03-15T14:32:06Z(012)]<BA>A=3.7V, B=3.6V\r\n
[2026-03-15T14:32:10Z(200)]<IM>12,500ohm\r\n
[2026-03-15T14:32:11Z(005)]<PA>(HSPID)Val=00010.0\r\n
Event Types (<EV>)¶
Event label strings are exactly 5 bytes (padded with spaces):
| Label | Constant | Trigger |
|---|---|---|
PO |
EVENT_POWER_ON |
MCU powered on and fully initialized (written once per clean boot, not on wakeup from sleep) |
ER |
EVENT_ER |
Battery voltage has been in the Early Replacement range for 3 consecutive checks |
EOS |
EVENT_EOS |
Battery voltage has been at End of Service level for 3 consecutive checks; device transitions to sleep immediately after, then pulls BATT_SW_EN LOW to discharge the battery-disconnect timing capacitor (~1–2 min to full disconnect) |
UF |
EVENT_UNRESPONSIVE_FUNCTION |
Watchdog timer fired — written on the next boot by checking the reset-cause register |
MD |
EVENT_MAGNET_DETECTION |
Magnet was held and removed for the configured duration (triggers sleep↔wake transition) |
SC |
EVENT_SHORT_CIRCUIT |
Short circuit detected during impedance measurement |
HI |
EVENT_HIGH_IMPED |
High impedance detected during impedance measurement |
NI |
EVENT_NORMAL_IMPED |
Normal impedance confirmed during impedance measurement |
LSA |
EVENT_LOWER_STIM_AMP |
Commanded stimulation amplitude exceeded MAX_SAFE_AMPLITUDE; was clamped down automatically |
SS |
EVENT_STIM_START |
Stimulation session started (scheduled or manual) |
SE |
EVENT_STIM_STOP |
Stimulation session stopped (scheduled end, manual BLE command, or forced stop on sleep/shutdown) |
BC |
EVENT_BLE_CONNECT |
BLE client authenticated and connected (auth opcode accepted) |
BD |
EVENT_BLE_DISCONNECT |
BLE client disconnected (idle timeout, explicit disconnect request, or hardware-side link loss) |
SL |
EVENT_SLEEP |
Device entering sleep (low-power STOP) state |
WK |
EVENT_WAKEUP |
Device woken from sleep and peripherals re-initialized |
SD |
EVENT_SHUTDOWN |
Intentional shutdown commanded via BLE (OP_SHUTDOWN_SYSTEM). Does not appear for EOS shutdowns — those are logged as EOS instead. |
Note:
OC(open circuit) is defined in the header but has no call site — it is never written by current firmware.
What is NOT currently logged¶
- OTA firmware update completion
Reading Logs Over BLE¶
Both read and erase commands require an active Admin-authenticated BLE session (opcode 0xF0).
OP_READ_IPG_LOG — 0xAE¶
Returns one log entry per call. The entry returned is the first entry with a timestamp strictly greater than the timestamp you send.
Request payload (7 bytes):
| Byte | Field | Description |
|---|---|---|
| 0 | Year | 2-digit year, e.g. 26 for 2026 |
| 1 | Month | 1–12 |
| 2 | Day | 1–31 |
| 3 | Hour | 0–23 |
| 4 | Minute | 0–59 |
| 5 | Second | 0–59 |
| 6 | Sub-second | 0–255 (units of 1/255 s) |
Response:
- STATUS_SUCCESS + payload containing the raw ASCII log entry bytes (including the timestamp prefix and \r\n)
- STATUS_SUCCESS with payload length 0 means no more entries exist after that timestamp — download is complete
To dump the full log, start with all-zeros timestamp and loop, using the timestamp from each returned entry as the input for the next call:
byte[] timestamp = new byte[7]; // all zeros = "before everything"
while (true) {
var response = SendCommand(0xAE, timestamp);
if (response.Status != STATUS_SUCCESS || response.PayloadLength == 0) break;
string entry = Encoding.ASCII.GetString(response.Payload, 0, response.PayloadLength);
Console.WriteLine(entry);
// Parse the timestamp out of the returned entry for the next request
// Format: [20YY-MM-DDThh:mm:ssZ(uuu)]
timestamp = ParseTimestamp(entry);
}
The firmware remembers the last-read address internally, so repeated calls with the same timestamp will return the same entry — it does not auto-advance. You must always pass the timestamp of the entry you just received to get the next one.
OP_ERASE_IPG_LOG — 0xAF¶
No payload. Erases all log data and resets the write pointer to 0x00000.
SendCommand(0xAF, new byte[0]);
Use with caution — this is irreversible.
Setting the Real-Time Clock¶
Accurate log timestamps require the RTC to be set before logs are collected. Both commands require an active Admin-authenticated BLE session.
OP_WRITE_TIME_AND_DATE — 0xB1¶
Sets the RTC to the specified date and time. All fields are plain binary integers (not BCD).
Request payload (6 bytes):
| Byte | Field | Range | Notes |
|---|---|---|---|
| 0 | Year | 0–99 | 2-digit year offset from 2000, e.g. 26 for 2026 |
| 1 | Month | 1–12 | |
| 2 | Day | 1–31 | |
| 3 | Hour | 0–23 | UTC |
| 4 | Minute | 0–59 | |
| 5 | Second | 0–59 |
The firmware validates all fields before applying. Any out-of-range value returns STATUS_INVALID and the RTC is not changed.
Example — setting 2026-03-24 15:30:00 UTC:
SendCommand(0xB1, new byte[] { 26, 3, 24, 15, 30, 0 });
Practical note: To minimize clock error, capture the target time in your software immediately before sending the command. Round to the nearest second and account for BLE round-trip latency if sub-second accuracy matters.
OP_READ_TIME_AND_DATE — 0xB0¶
Reads the current RTC value. No request payload.
Response payload (6 bytes): Same layout as the write command — Year, Month, Day, Hour, Minute, Second.
Use this to verify the time was set correctly:
var resp = SendCommand(0xB0, new byte[0]);
// resp.Payload: [YY, MM, DD, hh, mm, ss]
Console.WriteLine($"20{resp.Payload[0]:D2}-{resp.Payload[1]:D2}-{resp.Payload[2]:D2}T{resp.Payload[3]:D2}:{resp.Payload[4]:D2}:{resp.Payload[5]:D2}Z");
Timestamp Parsing¶
The timestamp in a log entry looks like:
[2026-03-15T14:32:05Z(128)]
To extract the 7-byte timestamp for the next OP_READ_IPG_LOG request:
// Input: "[2026-03-15T14:32:05Z(128)]..."
// Fields: [20YY-MM-DDThh:mm:ssZ(uuu)]
byte[] ParseTimestamp(string entry) {
// entry[3..4] = YY
// entry[6..7] = MM
// entry[9..10] = DD
// entry[12..13] = hh
// entry[15..16] = mm
// entry[18..19] = ss
// entry[22..24] = uuu
return new byte[] {
byte.Parse(entry.Substring(3, 2)), // YY
byte.Parse(entry.Substring(6, 2)), // MM
byte.Parse(entry.Substring(9, 2)), // DD
byte.Parse(entry.Substring(12, 2)), // hh
byte.Parse(entry.Substring(15, 2)), // mm
byte.Parse(entry.Substring(18, 2)), // ss
byte.Parse(entry.Substring(22, 3)), // uuu
};
}
Relevant Source Files¶
| File | Description |
|---|---|
App/Functions/Src/app_func_logs.c |
All log read/write/erase logic |
App/Functions/Inc/app_func_logs.h |
Event label constants and function prototypes |
App/Bsp/Inc/bsp_fram.h |
FRAM address map (ADDR_LOG_BASE, SIZE_LOG, etc.) |
App/Src/app.c |
Writes PO event on boot |
App/Src/app_state.c |
Writes SL (sleep entry), WK (wakeup), and SE (stim stop on forced power-off) events |
App/Src/app_mode_battery_test.c |
Writes <BA> and ER events; writes EOS during sleep-based battery checks |
App/Functions/Src/app_func_state_machine.c (continued) |
Also writes EOS event via app_func_sm_active_eos_check() — the active-mode EOS path that fires once per minute during BLE, connection, and therapy states |
App/Src/app_mode_impedance_test.c |
Writes <IM>, SC, HI, NI events |
App/Src/app_mode_therapy_session.c |
Writes LSA, SS, SE events |
App/Src/app_mode_ble_active.c |
Writes BC event on successful BLE authentication |
App/Src/app_mode_ble_connection.c |
Handles OP_READ_IPG_LOG and OP_ERASE_IPG_LOG opcodes; writes <PA> entries on parameter changes; writes BD (BLE disconnect) and SD (shutdown) events |
App/Functions/Src/app_func_state_machine.c |
Writes UF (watchdog reset) and MD (magnet) events |
App/Functions/Src/app_func_ble.c |
Also writes MD event on magnet detection |