Welcome to our site!

Electro Tech is an online community (with over 170,000 members) who enjoy talking about and building electronic circuits, projects and gadgets. To participate you need to register. Registration is free. Click here to register now.

  • Welcome to our site! Electro Tech is an online community (with over 170,000 members) who enjoy talking about and building electronic circuits, projects and gadgets. To participate you need to register. Registration is free. Click here to register now.

Making a Bluetooth adapter for a Car Phone from the 90's

Call waiting can be confusing, even when it's working perfectly with a high quality display of a modern touch-screen phone that presents multiple options with descriptive labels, and can clearly show the status of multiple calls.


Try to implement support for call waiting on a vintage car phone with physical buttons never intended to intuitively support this functionality, and a display that can only show 2 lines of 7 characters each, while working with limited information about call state from the Bluetooth Hands-Free profile, and it gets way more confusing.

First, I'll describe the possible call statuses that can be reported by the Bluetooth module:
  • IDLE: Not in a call, nothing happening.
  • VOICE_DIAL: Not quite sure, because I'm not using this. I think when the phone's voice assistant is activated? (e.g., Siri for iPhone)
  • INCOMING: Someone is calling you. You can answer the call, or reject it.
  • OUTGOING: You initiated a call, and the recipient has not answered yet.
  • ACTIVE: You're actively in a call.
  • ACTIVE_WITH_CALL_WAITING: You're actively in a call, and someone else is calling you. This is when you see the terrifying options pictured above.
  • ACTIVE_WITH_HOLD: You're actively in a call, and another call is on hold.
The Bluetooth module sends an event over UART to the MCU whenever this status changes.

There are also many commands supported by Bluetooth HFP that affect the call status. There are raw AT commands for each of these, and the Bluetooth module exposes these as supported UART commands:
  • Place an outgoing call to a specified number.
  • Answer an incoming call.
  • Reject an incoming call.
  • End all calls.
  • End the active call and accept the call waiting.
  • Reject the call waiting call (send it to voicemail).
  • Place the active call on hold and accept the call waiting.
  • Swap the active call and on-hold call
    • If only one call in progress, then it toggles that call between active and on hold.
    • NOTE: There is no call status that corresponds to this idea of "only a single call, and it's on hold". There's no way detect this status or transition in status via the call status event. It's always simply "ACTIVE" regardless of whether the call is on hold or not.
  • End all calls except for the active call.
  • End the active call and swap to the on-hold call.
  • Plus more that I'm not worrying about.

The only buttons on the car phone that can reasonably relate to any of these actions are:
  • END
  • SEND
  • FCN (for modified/alternate action)

I started with evaluating the original behavior of these buttons according to the car phone's owner's manual:
  • END:
    • Rejects an incoming call.
    • Ends an active call.
  • SEND:
    • Answers an incoming call.
    • Sends a "flash" request while in an active call.
      • NOTE: Equivalent to to quickly pressing the "hook" button on older analog land-line phones, which is how call-waiting calls were answered/swapped when call waiting was introduced to land-lines.
  • FCN + SEND:
    • While in an active call, sends the current digits on the screen as DTMF tones.
And I tried to come up with a way to make use of these buttons to provide all the necessary call waiting functionality while retaining consistency with the original basic functionality of these buttons. I decided on the following basic core rules:
  • END always ends the currently active call.
  • SEND answers incoming calls, and can swap between multiple calls (but never ends any calls).
  • FCN + SEND is off limits for call status management because it is already used for a specific different purpose.
  • Avoid making use of other arbitrary buttons on the handset, because it would never be intuitive that RCL, CLR, or STO (for example) should be pressed when dealing with answering/ending/swapping calls.
But following these simple rules leaves a gap in functionality:
  • When there's a call waiting, SEND obviously should answer it (placing the active call on hold), END should end the active call (while accepting the call waiting), but how do we reject the call waiting and stay on the active call?
  • When in an active call with another call on hold, SEND should swap the calls, END should end the active call and swap the on-hold call to active, but how do we end the on-hold call and stay on the active call?
Conveniently, the gap in both cases has a consistency: it involves getting rid of the "other" call that is not the active call. I decided that FCN + END was a reasonable button sequence for an "alternate" end/reject functionality (affects the "other" call instead of the active call).

I think this results in a fairly easy-to-remember pattern of what the buttons do in all cases (even when call waiting is involved):
  • SEND:
    • If there's an incoming call of any kind, it will accept that call.
      • If a call was already active, then that call is placed on hold.
    • If there is no incoming call of any kind, then it will swap the "active" and "on-hold" call.
      • If there is only one call, it toggles it between "active" and "on-hold".
  • END:
    • Ends the active call.
      • If there's an on-hold or waiting call, then that call will become active.
  • FCN + END:
    • Ends the "other" call (on-hold or waiting).
    • Irrelevant and does nothing if there is no "other" call.
The only ambiguity is that it might be intuitive to think that END would reject an incoming call waiting while already on an active call. There's two competing and incompatible possible assumptions about what the END button should do in this case, because there is both an incoming call that can be rejected and an active call that can be ended. I had to pick one for the call waiting situation, and I think my decision is the more consistent of the two (because it allows FCN + END to have a consistent meaning).

So that's one tough job done: simply understanding the possible call states and actions that can be taken in each state, and deciding how to use the buttons to perform those actions.

To be continued...
Last edited:
Next up on the Call Waiting journey is deciding how to present information to the user about the status of the call.

Simple incoming call (status: INCOMING)


An incoming call interrupts anything the user is doing and does not allow the user to do anything aside from answer (SEND) or reject (END) the call, and adjust the ringer volume (up/down arrow buttons on side of handset).

In a normal single call (status: ACTIVE)​

While in a normal single active call, we have a "talk time" timer by default and the "IN USE" indicator is on. The user is also free to perform most operations on the phone (adjust settings, recall numbers from the contact list, etc.). But any time they return to the "home" screen, and have no numeric input entered on the screen, the talk timer displays again:


The call can be ended (END), or put on/off hold (SEND).

Incoming call waiting (status: ACTIVE_WITH_CALL_WAITING)​

When in an active call, and another call comes in (call waiting), I decided I should remain consistent: interrupt anything the user is doing and show the "incoming call" screen. The differences in this call state are that the "IN USE" indicator will be on due to the current active call, the current active call's audio will still be active (instead of a ringer), and the volume level of the call audio is adjustable (instead of ringer volume):


The audio from the paired cell phone conveniently already includes the beep notifications that there's another call incoming. In this state, the user is again limited to making a decision on how to handle this incoming call waiting: Place the active call on hold and answer the call waiting (SEND), end the active call and answer the call waiting (END), or remain on the active call and reject the call waiting (FCN + END).

In a call with another call on hold (status: ACTIVE_WITH_HOLD)​

When in an active call with another call on hold, it is much like being in a normal single active call, but I make the "IN USE" indicator flash on/off as a reminder that there is another call on hold.:


The behavior of the SEND/END buttons is also adjusted for this call state: you can swap the active/hold calls (SEND), end the active call and activate the hold call (END), or end the hold call while remaining in the active call (FCN + END).

Seems pretty easy so far? It's about to get messy...
While testing all the different Call Waiting stuff, I've encountered some awkward situations, and I don't think there's anything I can do about it. It all comes back to the concept of being in a single call, but it is on hold: there is no distinct call status for this situation, and no event from the Bluetooth module to indicate changes between hold and active, because it's all the same call status.

For example:
  1. Get into a call. Status = ACTIVE.
  2. Answer a call waiting. Status = ACTIVE_WITH_HOLD
  3. The other person on the active call hangs up. Status = ACTIVE, but the other call remains on hold. User must press SEND to take the call off hold.


In this situation, I have no easy way to detect that the "ACTIVE" call is actually on hold. If I could, I would display something on the screen to indicate that a call is on hold.

The only event or status change that is accompanied with this situation is that the SCO connection (voice audio connection) for the first call disconnects when it ends, but the SCO connection does not reconnect for the on-hold call. I can't make many assumptions based on this, because the SCO connection can also be disconnected when you transfer audio from the Bluetooth device back to the cell phone. But I may be able to use this SCO connect/disconnect event as a trigger to query the call list (AT+CLCC command I used to get Caller ID info), then I can parse the responses to determine if there's only one call, and if it is on hold. Unfortunately, the AT+CLCC command does not immediately report correct status if executed right after the SCO connect/disconnect. It takes some time. I have to add an arbitrary delay to get correct results, which is unreliable. I really wish there was a distinct "ON_HOLD" call status.

Then there's a super obnoxious bug that I think is an iPhone-specific bug:
  1. Get into a call. Status = ACTIVE.
  2. Answer a call waiting. Status = ACTIVE_WITH_HOLD
  3. Press END to end the active call and swap to the hold call.
    1. Expected: status = ACTIVE, and the call that was on hold is now active.
    2. Actual: status = ACTIVE, but the cell phone shows that the ended call is still active for several seconds. Then the active call finally ends, but the on-hold call remains on hold.
If between steps 2 and 3, I press SEND to swap calls, then press SEND to swap calls again, I'm right back to the same situation (second call is active, first call is on hold). Pressing END behaves exactly as expected now.

I initially suspected a bug in the Bluetooth module's implementation of its "end call and swap hold call" UART command, so I found the raw AT command to do the same and tried sending the raw AT command through the BT module to the cell phone. Same behavior.

I hate Call Waiting...
Now it's time for my adventures in battery level detection/reporting.

The original behavior of the car phone is that you display the current battery level in the form of 1-5 dashes:


There's also low battery indication:

My original attempt at re-implementing this behavior was to use an ADC input on my MCU to measure the voltage of the power supply to the handset. I was able to roughly convert this voltage reading to a battery level similar to the original phone, but I could never quite get it right. The biggest problem was that I could not accurately determine when the phone was entering into "low battery mode". I want to be able to accurately detect this so I can produce the low battery indication and warn the user that the phone may turn off soon (remember: I'm using the original transceiver as my power supply for both the handset and my circuit, and its decision to power down is beyond my control).

So I took a different approach now. I have plenty of UART modules on my MCU, so why not use another to communicate directly with the transceiver using UART commands?

Here's my rough ASCII art sketch of all the UART communications on my circuit now:

|M| <==UART1==> [Handset]
|C| <==UART2==> [Bluetooth Module]
|U| ---UART3--> [Console logging to PC]
| | <==UART4==> [Transceiver]

Fetching the Battery Level​

To read the battery level from the transceiver, I can simulate the button press sequence from the handset to the transceiver to cause the transceiver to display the battery level, then monitor the resulting UART commands that the transceiver tries to send the handset and count how many dashes (-) it wants to print to the display. I know when the transceiver is done printing the battery level when I have received at least one dash, and then receive the command to enable the text display. NOTE: none of these commands from the transceiver actually make it to the handset. Only my MCU is communicating with the transceiver.

My initial implementation was to do this on-demand when the user tries to view the battery level on the real handset (same FCN / * / 5 button sequence). However, there was quite a delay because of the slow 800 baud data speed of the handset/transceiver UART communication. This on-demand fetching also won't be sufficient for sending the battery level to the paired phone for display of the Bluetooth device's battery level.

So I reworked it to periodically fetch the battery level from the transceiver always (every minute). So I always have a battery level value ready to display immediately. I can also send the battery level over Bluetooth any time the new battery level is different from the previous level. There's some additional details about the timing of the first battery level fetch to ensure the transceiver is ready to respond to button presses, timing out if I don't get a battery level response from the transceiver, simulating the CLR button press to try to recover (return the transceiver's state back to being on the "home" screen ready to handle the button sequence again).


I still trigger a fetch of battery level to happen when the user attempts to view the battery level. This "hybrid" approach allows me to immediately display a battery level that is correct within the last minute, but then also possibly update the display to be more correct value as of right now when the transceiver finishes responding in about 1.5 seconds.

Detecting "Low Battery" Condition​

When the battery is low, the transceiver does a few things:
  • Starts flashing the PWR indicator on/off about once per second.
  • Produces a beep about once every 20 seconds.
  • Displays "LOW BATTERY" on the screen when the user is at the "home" screen with no digits typed.
When the low battery condition ends (due to being plugged into an external power supply), the PWR indicator stops flashing (remains on), and the "LOW BATTERY" message is cleared from the screen.

The flashing PWR indicator is the the simplest thing for me to detect. The transceiver flashes the indicator by turning it on and off repeatedly. I can detect the PWR indicator/on off commands. When I receive a PWR "off" command, I know that the battery is low. If some amount of time passes after receiving the PWR "on" command, and I have not yet received another PWR "off" command, then I know the battery is no longer low.

With this information, I can now maintain a "low battery" status in code and handle transitions in this status to reimplement all the original low battery indication behavior.

I also trigger an immediate battery level fetch any time the low battery status changes to guarantee that the battery level value I have in memory is in sync.

Coming next: communicating battery levels over Bluetooth...
Last edited:
The Bluetooth module supports a UART command to send a battery level to the paired phone, which the phone can display in various ways



I was already making use of this from back when I was trying to calculate a battery level value from the voltage of the power supply. But now that I have an accurate reading of the exact battery level from the transceiver, I started paying more attention to the reported battery level on the paired cell phone and noticed it didn't seem to correlate very well with the battery level I was presenting on the car phone's handset.

The UART command for reporting battery level uses a percentage value ranging from 0-100. The battery level I receive from the transceiver is a value ranging from 1-5 (number of dashes displayed on the screen). So a simple multiplication by 20 should produce battery percentages of 20, 40, 60, 80, or 100. But I sometimes saw a value of 10% or 70% presented on my cell phone.

So I temporarily adjusted some code to send every battery level from 0-100 (2 second pause between each command) while also logging to the console. I watched the console output and the battery voltage presented on my cell phone to discover the following relationship between reported battery level and presented battery percentage:

Reporting 0-29 presents as 10%.
Reporting 30-59 presents as 40%.
Reporting 60-89 presents as 70%.
Reporting 90-100 presents as 100%.

Not very good granularity of possible battery levels. As far as I can tell, this is a problem with my phone's handling of the standard command for reporting battery level over Bluetooth HFP (I tried sending the raw AT command for this and saw the same results).

After spending some time Googling, I found that there are some common proprietary AT commands for reporting battery level that existed before an official/standard command was added in HFP version 1.7. One of these commands is was introduced by Apple, originally for iPhone accessories, but is also supposedly handled by Android phones for compatibility: "+IPHONEACCEV"

I have an iPhone, so I decided to use the iPhone-specific command. It accepts a battery level value in the range of 0-9, which maps to presented percentages ranging from 10-100%, in 10% increments. So I convert my transceiver battery level (1-5) to a value for this command as follows:

[IPHONEACCEV value] = ([transceiver batt level] x 2) - 1

This produces the originally intended result of 20%, 40%, 60%, 80%, or 100% displayed on the cell phone.

If the battery is "low", the transceiver still reports back a battery level of 1, so I added a special case to send the battery level as 0 (10%) when the battery is "low" to create a distinction in displayed battery percentage on the cell phone.
The final piece of the battery-level reporting puzzle is to view the paired cell phone's battery level from the car phone's handset.

The Bluetooth module sends 2 UART events to the MCU related to the cell phone's battery level:
  1. Max battery level: specifies the integer value that represents 100% battery level.
    • Event sent after connecting to the cell phone.
  2. Current battery level: an integer from 0 to [max battery level]
    • Event sent after sending [max battery level] event, then any time the battery level changes.
Practically, the max battery level will probably always be 5, because the Bluetooth HFP specification says that the battery level indicator value has a valid range of 0-5. But I still wrote my code to account for any possible max battery level just in case:

[display level] = ([reported level] x 5) / [max level]

However, this would always round down due to integer math. To achieve "normal" rounding (round to nearest integer), I added a bit more complexity:

[display level] = ((([reported level] x (5 x 2)) / [max level]) + 1) / 2

But it doesn't matter in practice, because the max is always 5, which is exactly the max of the range I can present.

NOTE: Cell phone signal strength is reported with the same approach, and I do a similar calculation to map the reported signal strength to the range of up to 6 bars supported on the car phone handset display. The rounding actually does matter here, because the practical consistent max signal strength reported over Bluetooth is 5.

I enhanced my battery level display feature on the car phone handset so that you can toggle between viewing the car phone battery level and the paired cell phone battery level by pressing the FCN button.

Car phone battery level:


Paired cell phone battery level:


I'm not entirely happy with the label "CELLBAT" for the paired cell phone battery level, but it's hard to come up with a good clear label that is 7 characters or less. I'm open to suggestions :).
Last edited:

Latest threads