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:
- both physical earbuds ran V1.3.3 before the experiment;
- the available public and retained update history ended at V1.3.2;
- stock battery percentage was a voltage-threshold result, not measured capacity;
- the sealed hardware exposed VBAT and charger state but no calibrated discharge current or Coulomb counter;
- stock Bluetooth OTA could not target only the left: it either stalled without the TWS peer or updated both with the peer present;
- an explicitly authorized bilateral downgrade installed a 96-byte diagnostic extension;
- the isolated left measured 3.98 V at rest, 3.53 V at 98.963 seconds of audio and no connection by 105.069 seconds;
- the left could not restart without charging;
- very low usable capacity and/or high effective resistance is the leading diagnosis, while abnormal PCB current remains unresolved without physical current measurement.
The safety and authorization timeline
The scope changed during the investigation, so statements about “no writes” need a timestamp.
- Initial work was passive and offline: Windows interface inventory, APK extraction, firmware decoding, source comparison and read-only protocol reconstruction.
- Stock
0x4Erequests were sent only after authorization to read the connected earbuds. - Left-only OTA probes stopped at
AA41; they sent noAA42firmware payload and noAA44apply command. - Exact TWS reconstruction disproved the premise that the right could remain a non-writing relay.
- The user then explicitly accepted rewriting both earbuds and accepted permanent loss of either one.
- 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:
- transport availability;
- protocol framing;
- command semantics.
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:
- identify the physical signal;
- identify the ADC or register consumer;
- follow it into RAM and packet builders;
- distinguish setpoint from measurement;
- 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:
- right absent:
AA41had no peer synchronization and remained silent; - right present: the stream could progress, but both devices became targets.
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:
- register data flow in DEX;
- exact firmware templates;
- timing of live repeated responses.
5. From decoded values back to raw bytes
Two host bugs appeared only after the firmware worked:
- an unevidenced requirement that a response sequence echo the request;
- eight-bit overflow while reconstructing little-endian 16-bit values in PowerShell.
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:
- the complete raw frame;
- parser version;
- interpreted fields;
- failure stage;
- elapsed time;
- target-attribution evidence.
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:
- place the right in the case;
- connect only the charged left;
- prove left 90%, right 0% in the stock response;
- collect a rest baseline;
- apply audio;
- continue until terminal loss;
- 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
- Androguard: DEX methods, product routing and OTA packet/state reconstruction.
- JieLi
pi32v2objdump: authoritative instruction decoding for the custom CPU. - Custom container scripts: JLFS/SFC parsing, encryption round trips, CRCs, MD5 and layout validation.
- Small analyzers with tests: dispatch tables, direct references, TWS registrations and updater transitions.
- Native Winsock RFCOMM: removed the Windows virtual-COM layer from critical OTA exchanges.
- Raw JSON evidence: separated observation from interpretation and allowed later corrections.
Useful only with caveats
ghidra-jieli: valuable for processor semantics and cross-reference checks, but its own README describespi32v2support as early and incomplete.- Generic firmware carvers: useful for entropy and boundaries, but unable to replace product-aware JLFS/SFC parsing.
- Public JieLi SDKs: strong semantic references, not proof that Tribit used an identical commit or configuration.
Rejected approaches
- installing the vendor Android app;
- executing opaque vendor Windows tools;
- treating the case USB port as a debug path;
- sending undocumented factory strings over the wrong transport;
- attaching a normal USB-UART adapter directly to charging contacts;
- publishing or blindly flashing an unverified binary.
A more efficient workflow for the next device
If starting again, the investigation can be shortened substantially:
- Reproduce the symptom with a control. Record exact volume, audio source, query cadence and terminal behaviour for both sides.
- Inventory interfaces. Separate USB power, Bluetooth profiles, SPP, BLE and physical fixture contacts before opening any endpoint.
- Recover the stock status command. Preserve raw frames and prove which fields are local, peer-derived or duplicated.
- Establish artifact provenance. Validate embedded version, hashes, CRCs and container structure before trusting server metadata.
- Identify the ISA early. Use the vendor architecture toolchain before generic decompilers.
- Map the complete command router. Prove whether an unused command exists and whether hidden telemetry is truly absent.
- Audit hardware capability. Distinguish voltage, current setpoints, measured current, accumulated charge and health estimates.
- Map OTA fan-out before writing a host. Include TWS callbacks, local/remote flags, peer synchronization and recovery behaviour.
- Design the smallest extension. Preserve normal commands, existing addresses, flash footprint and packet builders.
- Build mechanical gates. Pin hashes, versions, battery thresholds, offsets, terminal statuses and authorization text.
- Test the decoder with synthetic edge values. Include multi-byte values above 255 and device-owned fields.
- Capture complete live evidence directly to a file. Do not reconstruct a final dataset from console history if it can be avoided.
- 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:
- the exact V1.3.3 binary and its source provenance;
- calibrated left-board discharge current;
- delivered mAh under the test load;
- direct cell or path resistance;
- whether incomplete charging contributes to the failure;
- the exact commercial chip designation behind the indirect marking;
- the exact factory-loader handshake and accepted recovery container for this product;
- whether the retail case contains undocumented firmware features beyond its evidenced pairing/reset role.
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:
- Bluetooth addresses and personal identifiers;
- synthetic guest credentials or request identifiers;
- local filesystem paths;
- proprietary APK or firmware binaries;
- a ready-to-run destructive flasher;
- unsupported claims that the modification repairs the battery.
The modified package hashes are retained so that the experiment remains internally identifiable without redistributing copyrighted firmware.
Primary references
- Main FlyBuds 3 investigation and final measurements
- Official FlyBuds 3 manual
- FCC filing and exhibits for BTH92SC
- JieLi AC63 public SDK
ghidra-jieliprocessor module- JieLi wired production-testbox update procedure
- JieLi fixture wiring
Closing lesson
The useful outcome was not merely that a firmware patch worked. It was that each major conclusion survived a stricter replacement model:
- percentages became raw voltage;
- local SPP targeting became TWS callback fan-out;
- guessed status semantics became bytecode data flow;
- inferred physical identity became isolated-side proof;
- a Bluetooth timeout became confirmed no-charge failure to restart.
Reverse engineering becomes dependable when every convenient story remains provisional until a lower layer, a control experiment or raw evidence can confirm it.