
On a casual Monday evening in San Francisco, I met with Nik from Omi to talk about our vision and interest in life. Twenty minutes in, he asked me to visit his office in District 3 to tackle an interesting challenge: reverse-engineer the new and shiny Friend pendant. The goal: retrieve the audio stream and the button state from the device.
This article lays out my process, from how I approached the problem to how I figured out the inner workings of friend.
AI pendants are not new. In fact, Omi was one of the original AI pendants that was released all the way back in Q1 of 2025.
Using an nRF chipset connected to a microphone and a button, Omi is an always-on pendant that listens to all your conversations and helps you talk with your memories. Even better: Omi is entirely open-source, from the backend, to the app, to the firmware and schematics of each device.
Since both devices share similar functionality, one might expect Friend to use a similar communication protocol. That’s why prior to the challenge, Nik walked me through the documentation of Omi and pinpointed its communication protocol.
To support audio transmission while keeping low energy usage, Omi uses Bluetooth Low Energy (BLE). It has three services:
The custom BLE service for audio uses notifications to push values from Omi to your phone every ~10 ms. For a 16 kHz sampling rate, this means 160 samples per audio frame. Depending on the MTU negotiated, the audio frame may be fragmented over multiple packets. There’s an additional 3-byte header that keeps ordering of packets and fragments.
Using this information, we can now begin reverse-engineering Friend with an understanding of how a similar device is implemented.
Friend can be set up using an app in the App Store (which shares the name “Friend”). Prior to reverse-engineering the device, I walked through the onboarding process of the pendant, which gives insight into what protocol might be used. As with most smart wearables, Friend uses BLE for communication — this can be inferred from how the pendant is set up in the app.

Using a BLE Scanner on my phone, I confirmed this observation and began looking into services and characteristics of the device.
From the BLE scanner output, Friend reveals:
I then wrote a small Python script that connects to the device and logs the UUIDs of each service and characteristic.

By checking the value updates of each characteristic, I found two interesting behaviors:
Through online searches, I discovered that Friend not only records when you tap on the device, but actually always listens. Therefore, characteristic 01000000 is very likely the audio stream. The 03000000 characteristic handles the button state.
Finding the characteristic is only half the battle. The hardest part is figuring out whether Friend uses encryption, and if not, what codec it uses and what the parameters are.
I have a few educated guesses:
With those assumptions, I narrowed down codec candidates based on common usage in open-source audio/BLE projects:
I then wrote a Python script to connect to Friend and record all updates from the audio characteristic into a binary file (.bin). The script also recorded timestamps and lengths of each audio-update into a CSV file.

Using NumPy on the CSV data, I found that Friend sends a fixed-length packet (95 bytes) every ~30 ms. I also checked that the negotiated MTU between my computer and Friend was 247 bytes.
Given a 16 kHz audio stream and using 16-bit samples, a 30ms PCM frame would require:
But here we see only 95 bytes every ~30ms, so uncompressed PCM is almost certainly ruled out. Also, since the packet size is constant (95 bytes) and not variable, this strongly disfavors Opus (which typically uses variable packet sizes depending on complexity/compression).
After checking packet length and frequency, I look into the content of each packet. Here, a pattern emerge: the last 2 bytes of each packet is used for counting packet number, as you can see from the image below.

Further analysis shows that there was not a repeated pattern at the beginning of the packet nor the end (excluding the counter and padding). This means Opus is probably not our candidate since Opus frames comes with a TOC header that contains data on how to decode the audio. If each audio packet is of same length, this TOC should stay relatively the same across packets.
This leaves us with LC3 as the codec used by friend.
From the evidence, we now have:
By stripping out the packet counter bytes and any trailing padding, one can concatenate the data packets to form a continuous LC3 audio stream. Using an LC3 decoder (open source or custom build) we can decode this stream and export as a WAV file, enabling a spectrogram view of the audio and further analysis.

Now we can easily hoist the audio and button input of friend for any application.
Sample code and formal description can be found here.