Article·· 14 min read· AI-assisted

FlyBuds 3 firmware reverse engineering: 26 false starts and corrections

A technical postmortem of the wrong assumptions, tooling traps, protocol mistakes and safety gates behind a successful earbud telemetry patch.

The final result of the FlyBuds 3 battery investigation looks clean: a 96-byte extension exported raw battery voltage, the bilateral OTA completed, and the isolated left earbud collapsed from 3.98 V to 3.53 V before shutting down in roughly 105 seconds.

The route to that result was not clean.

This postmortem records the incorrect assumptions, dead ends and host-side bugs that had to be corrected. The purpose is not to celebrate persistence for its own sake. It is to show which evidence changed the model and how the same mistakes can be avoided in future embedded reverse-engineering work.

[!WARNING] The experiment included an authorized firmware downgrade with a real risk of disabling both earbuds. This article explains the reasoning and safeguards, but deliberately does not provide a ready-to-flash proprietary image or a copy-and-paste flashing procedure.

The final answer, before the mistakes

The investigation ultimately established:

The safety and authorization timeline

The scope changed during the investigation, so statements about “no writes” need a timestamp.

  1. Initial work was passive and offline: Windows interface inventory, APK extraction, firmware decoding, source comparison and read-only protocol reconstruction.
  2. Stock 0x4E requests were sent only after authorization to read the connected earbuds.
  3. Left-only OTA probes stopped at AA41; they sent no AA42 firmware payload and no AA44 apply command.
  4. Exact TWS reconstruction disproved the premise that the right could remain a non-writing relay.
  5. The user then explicitly accepted rewriting both earbuds and accepted permanent loss of either one.
  6. Only then did the guarded bilateral client transmit 447,942 firmware bytes and apply the image.

This distinction is important. A later authorized write does not retroactively make earlier risky assumptions safe.

The 26 corrections

# Wrong turn or obstacle Correction Durable lesson
1 Treating the symptom as a simple “bad percentage” problem Compared runtime, terminal audio loss and later raw VBAT instead of trusting the UI Begin with competing physical and software hypotheses
2 Expecting the charging-case USB port to expose diagnostics or firmware Windows enumerated no useful USB data device; the retail case behaved as power/control hardware Enumerate interfaces before designing around them
3 Expecting COM7/JL_SPP to stream telemetry spontaneously Reconstructed framed request/response commands from the APK and firmware A silent serial channel may be a protocol endpoint, not a console
4 Assuming the vendor Android app had to be installed Analysed APK/DEX files offline and rebuilt the required packet semantics independently Installation and execution are separate from static analysis
5 Assuming V1.3.3 must be downloadable because the earbuds reported it Public and authorized retained history yielded 13 unique packages through V1.3.2, none containing V1.3.3 Running firmware may be absent from public OTA history
6 Alternately describing the package as a case image, a main image or an L/R bundle Container contents and product markers showed one common role-aware earbud image Classify the artifact from its contents, not filenames or UI labels
7 Using generic disassemblers as if the CPU were ARM or MIPS Used JieLi pi32v2 tools as the primary decoder and Ghidra support only as a cross-check Correct ISA identification comes before decompilation
8 Letting file associations and vendor utilities blur the trust boundary Used explicit tool paths; no vendor executable was run after endpoint security flagged one during source checkout “Downloaded” is not “safe to execute”
9 Reading two version triplets in 0x4E as independent left/right versions Exact writer code showed both triplets came from the local responder; each physical side was queried separately Wire-field names do not prove independent provenance
10 Planning to replace percentages with voltage and call the diagnosis complete Established that percentage already came from VBAT thresholds and that current was the missing variable Export new information, not a differently formatted old value
11 Treating a 62-byte region as free code space Revalidation identified it as SHA-256-related data; code was appended instead A run of apparently unused bytes is not a code cave until every reference is checked
12 Treating visible DUT+TST strings as a Bluetooth JL_SPP command set Relocation and public-source matches placed that parser in the chargestore/test-box UART path Strings reveal vocabulary, not transport reachability
13 Treating PROGI/PROGF selectors as current telemetry They were PMU/regulator control channels, not a calibrated battery-current sensor A register name containing “current” may configure rather than measure
14 Mapping FlyBuds 3 to the wrong OTA manager in the APK Exact startDfu() switch reconstruction selected headset.OtaManager, not the role-switch utility handler Follow the product-specific call graph before importing generic behaviour
15 Assuming the SPP-connected left was the only flash target TWS callback reconstruction showed local and peer execution; isolated AA41 stalled without the peer Model fan-out below the host transport before claiming target isolation
16 Treating AA43 as a usable abort or recovery command Exact V1.3.2 routing returned immediately and never delivered it to the updater A host method name is not proof of device-side implementation
17 Treating the retail case reset as firmware rollback The manual and firmware event family matched pairing/TWS address deletion Consumer reset, bootloader entry and firmware restoration are different operations
18 Treating the modified downgrade as safe because it fit in flash Kept loader acceptance, post-switch rollback and missing V1.3.3 recovery as explicit risks Structural validity is necessary, not sufficient, for safe flashing
19 Interpreting AA41 status 1 as “keep waiting for status 2” DEX data-flow showed the branch compared against a preloaded constant 1 and immediately called the first write Track register values across branches; do not infer semantics from nearby cases
20 Repeating gates while the defective left continued to discharge Added fresh battery gates and stopped attempts early when the left fell below threshold Safety checks can consume the resource they are checking
21 Blaming repeated 10060/10050 failures on firmware or COM7 alone Native Winsock RFCOMM separated virtual-port problems from the actual peer-dependent OTA start Replace one transport layer at a time and preserve exact failure stages
22 Expecting a short transfer and risking premature timeout Timed all 896 acknowledgements; the successful run took 211.979 seconds Bound long operations by protocol progress, not intuition
23 Rejecting the first custom response because its sequence byte did not echo the request APK parsing showed the byte was device-owned; it was recorded rather than gated Validate only fields whose semantics are evidenced
24 Decoding 88 01 as 1.36 V PowerShell shifted a System.Byte and overflowed; casting to int produced the correct 3.92 V Preserve raw frames and test multi-byte arithmetic at type boundaries
25 Calling the first 3.90–3.92 V custom samples “left telemetry” Stock fields made the right the likely responder; the conclusion was withheld until the left was isolated Logical role and physical identity require independent proof
26 Leaving “collect the isolated-left curve” in final-state documents after completing it Final audit linked the completed 70-sample series and marked pre-flash statements as historical Documentation state is part of experimental correctness

The corrections that changed the architecture

1. From “serial console” to private request/response protocol

The first mental model was wrong at the interface boundary. JL_SPP looked like a serial endpoint, but an open channel with zero bytes did not imply that the device had no telemetry. The APK showed framed commands; the firmware showed the matching dispatcher and response builders.

The general lesson is to distinguish three layers:

Testing only the first layer cannot answer questions about the other two.

2. From “find a hidden command” to “prove the telemetry boundary”

Searches found VBAT, LDO5V, DTEMP, charger comparators and programmed current stages. They did not find a measured discharge-current path, remaining-capacity accumulator or fuel-gauge state.

That negative result was productive. It prevented the extension from promising current or mAh that the hardware could not supply. The final command exported raw states with explicit units and limits instead of assigning convenient but false meanings to PROGI, PROGF or charge-stage selectors.

The durable pattern is:

  1. identify the physical signal;
  2. identify the ADC or register consumer;
  3. follow it into RAM and packet builders;
  4. distinguish setpoint from measurement;
  5. label anything without calibration as raw.

3. From “left-connected means left-only” to exact TWS fan-out

The most consequential error was assuming that the host-visible SPP target defined the flash target. It did not.

The host sent one stream to one earbud, but the device router forwarded OTA commands through a TWS function. The controller called the registered handler once for local transmission and once for remote reception. Because the handler ignored the rx argument, both earbuds executed their own update stages.

This explains both live observations:

No amount of host-side “select left” checking could constrain that lower-layer callback fan-out. The experiment did not proceed until the authorized target set matched the proven topology.

4. From state labels to data-flow proof

The AA41 status bug is a compact example of why decompiled control flow is not enough. A nearby branch used status 2, making it tempting to describe status 1 as intermediate. The exact bytecode loaded constant 1 before the command switch, and the AA41 branch reached its comparison without changing that register. Equality called write() immediately.

The firmware independently contained a status-1 ready template retransmitted while waiting for the first data block. Sixteen repeated status-1 responses were not evidence that the updater wanted more waiting; they were evidence that the host had failed to send the required first AA42.

The correction came from combining:

5. From decoded values back to raw bytes

Two host bugs appeared only after the firmware worked:

The raw response bytes made both bugs recoverable without reflashing. 88 01 is 392 in little-endian units, or 3.92 V at 10 mV per unit. A host that retained only “1.36 V” would have destroyed the most useful diagnostic evidence.

Every live protocol tool should preserve:

6. From “a value” to a physically isolated series

The first custom samples were valid, but their physical source was uncertain. Stock state reported left 0% and right 80%, so 3.90–3.92 V was likely local to the right. Calling it left telemetry would have turned a successful protocol test into invalid battery evidence.

The fix was procedural, not algorithmic:

  1. place the right in the case;
  2. connect only the charged left;
  3. prove left 90%, right 0% in the stock response;
  4. collect a rest baseline;
  5. apply audio;
  6. continue until terminal loss;
  7. attempt a no-charge reconnect.

That series converted a plausible diagnosis into a direct voltage-collapse observation.

Tooling that helped—and tooling that did not

Effective tools

Useful only with caveats

Rejected approaches

A more efficient workflow for the next device

If starting again, the investigation can be shortened substantially:

  1. Reproduce the symptom with a control. Record exact volume, audio source, query cadence and terminal behaviour for both sides.
  2. Inventory interfaces. Separate USB power, Bluetooth profiles, SPP, BLE and physical fixture contacts before opening any endpoint.
  3. Recover the stock status command. Preserve raw frames and prove which fields are local, peer-derived or duplicated.
  4. Establish artifact provenance. Validate embedded version, hashes, CRCs and container structure before trusting server metadata.
  5. Identify the ISA early. Use the vendor architecture toolchain before generic decompilers.
  6. Map the complete command router. Prove whether an unused command exists and whether hidden telemetry is truly absent.
  7. Audit hardware capability. Distinguish voltage, current setpoints, measured current, accumulated charge and health estimates.
  8. Map OTA fan-out before writing a host. Include TWS callbacks, local/remote flags, peer synchronization and recovery behaviour.
  9. Design the smallest extension. Preserve normal commands, existing addresses, flash footprint and packet builders.
  10. Build mechanical gates. Pin hashes, versions, battery thresholds, offsets, terminal statuses and authorization text.
  11. Test the decoder with synthetic edge values. Include multi-byte values above 255 and device-owned fields.
  12. Capture complete live evidence directly to a file. Do not reconstruct a final dataset from console history if it can be avoided.
  13. Perform a final stale-state audit. Search for “pending”, “unflashed”, “unknown” and superseded next steps after the experiment closes.

What remains unknown

The final experiment did not—and with the sealed hardware could not—answer everything:

These are not loose ends that invalidate the conclusion. They define its boundary. Raw VBAT collapse and terminal loss are confirmed; the exact split among capacity, resistance and abnormal current remains unresolved because current was never measured.

Publication and reproducibility choices

The public record includes the result, protocol model, schema, transfer geometry, hashes and the complete 70-sample voltage dataset. It excludes:

The modified package hashes are retained so that the experiment remains internally identifiable without redistributing copyrighted firmware.

Primary references

Closing lesson

The useful outcome was not merely that a firmware patch worked. It was that each major conclusion survived a stricter replacement model:

Reverse engineering becomes dependable when every convenient story remains provisional until a lower layer, a control experiment or raw evidence can confirm it.

Sources

  1. Official FlyBuds 3 manual
  2. FCC filing and exhibits for BTH92SC
  3. JieLi AC63 public SDK
  4. ghidra-jieli processor module
  5. JieLi wired production-testbox update procedure
  6. JieLi production fixture wiring