I Hacked an ECG Machine

Audio, Data Analysis, Medical, Projects

Abstract

This project explores a method to extract and decode high-quality ECG data from the KardiaMobile device without relying on proprietary APIs. By leveraging the original device’s frequency-modulated (FM) audio output, I developed a pipeline for demodulating, calibrating, and denoising the signal using quadrature demodulation and adaptive filtering techniques. The approach successfully removes noise, including mains hum and harmonics, and reconstructs the ECG signal with high fidelity. This method successfully produced high-quality ECG signals, clearly showing key features such as the P-waves, the QRS-complex, and T-waves. This accessible and cost-effective solution enables incidental ECG data collection for research purposes, using the KardiaMobile’s affordable hardware.

Introduction

I’ve been interested in electrocardiograms (ECG or EKG) for several years. Back in 2017, I attempted to build my own ECG machine. While the project didn’t progress very far (resulting in a few damaged operational amplifiers), it sparked a lasting fascination with ECG technology.

This interest resurfaced when I discovered the KardiaMobile ECG devices. These pocket-sized, battery-powered machines connect to a smartphone and enable basic ECG recordings. They are approved by both the British National Institute for Health and Care Excellence (NICE) and the U.S. Food and Drug Administration (FDA). The KardiaMobile 6L even records three channels, allowing for six-lead ECGs to be synthesized. This elegant, pre-packaged solution immediately reminded me of my earlier ECG project—only someone else had done all the hard work.

The KardiaMobile is a compact device, approximately 80 mm long and 30 mm wide, with two metal electrodes for recording ECGs. It outputs the ECG signal which is then picked up by the smartphone app that seems to work on almost any phone with recordings limited to 5 minutes by the Kardia app. To record an ECG you start up the app and set it to record an ECG then place your left index finger on the left electrode and your right index finger on the right electrode. The app then detects that an ECG is now being generated and starts recording it, plotting it live on the screen. After the ECG has been acquired you can then save the ECG in the app along with tags and notes and you can also generate PDF reports that plot the ECG on a page along with basic information.

The app can even detect a few heart conditions – although it’s no substitute for a cardiologist.

While KardiaMobile devices work seamlessly with the official Kardia app to generate ECG plots and reports, my interest lay elsewhere: I wanted access to the raw ECG data to create custom visualizations and analyses. My goal wasn’t related to health monitoring or diagnosis; I simply wanted to explore the data and create neat charts.

The Problem: How to access the data?

The KardiaMobile 6L transmits ECG data to a smartphone via Bluetooth, a secure two-way protocol with encryption and numerous parameters. Extracting raw data directly seemed like a dead end. I briefly considered tracing ECG plots from the app’s reports, but having tried similar methods before, I knew how error-prone they could be. Kardia does offer an API, but it appears to be intended for clinical use, not individual hobbyists—they never responded to my inquiries.

Chance solution: The older version uses sound waves to transmit the data

By chance, I watched a cardiologist’s review of the KardiaMobile on YouTube. He mentioned that the device uses sound waves to communicate with the app. While this seemed preposterous, I decided to investigate. To my surprise, I discovered that the original KardiaMobile (single-lead) indeed transmits its signal using a frequency-modulated (FM) sound wave in the 18–20 kHz range, as specified in its documentation. The key details include:

  • A carrier frequency of 19 kHz
  • Frequency modulation with a scale of 200 Hz/mV
  • A 10 mV peak-to-peak range
  • A frequency response from 0.1 Hz to 40 Hz

Armed with this information, I mocked up a test: I generated a synthetic heartbeat, frequency-modulated it, and played it through my computer. To my amazement, the Kardia app detected it as a healthy heartbeat with a BPM of 75—precisely what I had set. Confident in my understanding of the encoding process, I purchased a KardiaMobile device to explore further.

I actually found out all of this information before purchasing an ECG machine. So I mocked up an example heartbeat, frequency modulated it and played it on my computer. The Kardia app detected it as a healthy heartbeat with a BPM of 75, exactly what I had set it as. At this point, I was pretty sure that I understood the encoding process since I had effectively reverse-engineered the protocol and had a successful test on the Kardia app. The output is one-way, analogue, and not encrypted. With this newfound confidence in my plan, I bought a KardiaMobile ECG.

Optimising the signal acquisition

While initially concerned about microphone selection, I found that the microphones on my Samsung Galaxy S20 Ultra worked well—FM modulation is forgiving. I also experimented with beamforming algorithms to improve signal-to-noise ratio (SNR), but they failed due to the directional nature of the high-frequency signal. A simpler method, selecting the channel with the most power in the carrier band, proved effective. This shouldn’t have been surprising as the app also works by using the built-in microphone on my phone, however initially I wasn’t sure how much signal processing they were doing so I wanted to get the best initial signal.

Amount of signal in carrier band received by each microphone on my phone

After I did a quick experiment where I held the Kardiamobile near my phone in different places I was able to work out where the best place to hold it was. I ended up using the bottom of the phone for all of my tests as it yielded high signal and was convenient.

Decoding the ECG Signal

The decoding process follows these steps:

  1. Input and Preprocessing:
    • Read the audio file and determine its sample rate.
    • If the signal has multiple channels, select the one with the highest power in the carrier band.
  2. FM Demodulation:
    • I tested several demodulation methods, with quadrature demodulation yielding acceptable results with a simple algorithm. This calculates the frequency deviation from the carrier frequency.
  3. Calibration and Downsampling:
    • Convert the frequency deviations to voltage using the scale of 200 Hz/mV.
    • Downsample the signal to 600 samples/second, as the 19 kHz carrier signal has been removed so high temporal resolution isn’t needed.
  4. Denoising and Filtering:
    • Identify and filter out mains hum (e.g., 50 Hz in the UK, including harmonics). I implemented an adaptive Fourier-based filter to detect and remove the specific mains frequency and its harmonics.
    • Remove low-frequency noise (<0.52 Hz) and high-frequency noise (>40 Hz) using a zero-phase filter to avoid adding in phase distortion to the signal.
  5. Post-Processing:
    • Apply wavelet denoising using DB4 wavelets to remove noise without reducing the sharp features in the signal.
    • Trim extraneous noise at the start and end of the recording.

Removing mains hum

The main source of noise was mains hum picked up by the Kardia ECG itself. An example of the filtering process is shown below, where the sharp peak near 50 Hz. I used an adaptive filtering approach that ensures precise removal even when the mains frequency slightly deviates (e.g., 49.92 Hz instead of 50 Hz) removing the minimum additional signal.

Detected Mains Frequency: 49.92 Hz
Filtering 49.92 Hz: Range [49.42, 50.42]
Filtering 99.84 Hz: Range [98.85, 100.84]
Filtering 149.77 Hz: Range [148.27, 151.26]
Filtering 199.69 Hz: Range [197.69, 201.69]
Filtering 249.61 Hz: Range [247.11, 252.11]
Effect of filtering out mains hum in the frequency domain and the time domain. The bottom plots are zoomed-in sections.

Since the mains frequency component is so strong the filtering process also needs to be very strong. A second-order filter with a cut-off frequency of 40Hz still left a large amount of 50Hz noise in the signal. Interestingly this 50Hz hum is not hum picked up by the microphone it’s actually hum picked up inside the Kardia unit. I used second-order zero-phase filters that avoid adding phase artifacts to the signal. Traditional filters like Butterworth filters add a phase shift that varies by frequency which can be particularly harmful to these kinds of signals.

Following this additional denoising was applied using wavelet transforms and Daubechies 4 (DB4) wavelets. This transforms the time domain signal into a signal that is made up of short bursts of short packets of waves. These efficiently model bursty signals like heartbeats and poorly model totally random signals like noise. In the wavelet domain, you can filter out small wavelet coefficients that are more likely to correspond to noise and keep coefficients of greater magnitude that are more likely to correspond to my heartbeat signal.

I then added some basic signal processing was used to estimate where the real signal started and finished (i.e., to remove the initial and final bits of noise from when Kardia unit was off).

Results

After all of these steps, I was able to produce high-quality ECG results that can be exported in any output format (.csv, .xls, .npy, .mat etc) and plotted in any way desired. Kardia likely adds lots of useful signal processing that I’m not able to reproduce at this time however I was able to produce ECG results that clearly show the main features of the expected heartbeat signal including R-waves, T-waves, S-waves, and the QRS complex.

Full signal after decoding and denoising
Signal section showing: P, QRS, and T waves

Conclusion

The result of all this is a high-quality method of recording and exporting ECG data from the Kardiamobile device without using their API which is not available to the general public. Since the Kardia device is very inexpensive – I paid around £50 (60USD or 60EURO) for mine – and this method is easy to reproduce this may open up new avenues for incidental ECG usage in a research setting. This may enable researchers to extract ECG signals from inexpensive hardware, facilitating incidental ECG usage in various research settings.

This method is obviously not appropriate for monitoring or diagnosing any kind of health condition.

An extremely rough version of this code is available at my github: https://github.com/joshuamcateer/kardiadecode

Quick Peek: Demodulating FM Sound Signals in a Noisy Environment

Audio, Data Analysis, Quick Peeks

Premature optimisation is a trap you can easily fall into whenever you are making or designing something. In some ways, it feels wrong to do something badly when you know you could do it better with a little more time. However, it is almost always best to get something working (even if it’s not perfect) rather than striving for an elegant solution that doesn’t yet work. You can always implement the faster, more accurate, or more elegant method later—and often you don’t need to. It is also far easier to improve something that already works than to build something from scratch.

Over the last few days, I have been working on a project that involves decoding a high-frequency, frequency-modulated (FM) audio signal. I recorded this signal on my phone, which has stereo microphones, and spent some time writing a clever beam-forming algorithm that adjusts the amplitude and phase of the signals received by the two microphones to increase the FM signal strength while rejecting background noise. The algorithm used simulated annealing to optimise the amplitude and phase adjustments, and it worked very well in a set of simulated examples that I used for testing.

However, it did not work well on the actual data I collected. I spent quite a while fixing and tuning parameters but could not get it to perform properly. Eventually, I did what I should have done from the beginning: I conducted a real experiment and carried out some simple data analysis to understand what was happening.

I moved the source around different parts of my phone and plotted the power in the FM carrier band. After looking at the plot, it became immediately obvious why the beam-forming algorithm never worked: the sound was far too directional. (This makes complete sense, given that it was a high-frequency sound.) There was never a point where the sound was picked up strongly by both microphones; it was only ever picked up by one at a time. I should have used a much simpler approach: just take the output from whichever channel had the highest power in the carrier band—a single line of code—rather than hundreds.

In fact, the plot of the ratio of signal power to total power implies that in this example, the signal typically makes up the vast majority of the received power. However, the carrier band power is only a proxy for the actual signal, since it also includes FM-encoded noise that is not truly signal. Therefore, the true ratio of signal power to total power is somewhat lower.

DIY wall-reflecting surround sound speakers

Audio, Projects

Surround sound is cool. Hearing sound effects and music coming from every angle is much more immersive than just having the sound come from your TV. However, it isn’t easy to fit so many speakers in to every type of room. People who are into hi-fi will tell you that you need to have your speakers a specific distance from the wall, and rotate them at a specific angle to get the best sonic experience in the seating position. And god forbid if you tell them that you have your sofa against the back wall… “The rear wall will enhance the bass at random frequencies, it will sound boomy and horrible”.

What do you do then, if your room isn’t the right size and shape for an ideal home cinema? What if you only have space to put a speaker on one side of the seats? Well, one thing you probably shouldn’t do is build the speakers yourself (but maybe you still should).

The room problem. A small side table on one side and the doorway on the other. No room for a speaker on the right hand side.

I wanted to install a 5.1 surround sound system, starting with a 3.0 system. If you didn’t know, the first number is the number of ‘surround speaker’, and the second number is the number of subwoofers. Typically in cinemas sounds bellow around 80Hz are played by subwoofers, and higher frequencies are played by the other speakers. 3.1 systems have a left and right speaker, and a centre channel as well as a subwoofer (the 0.1). 5.1 systems add two surround speakers what are approximately in line with the seats. 7.1 systems have another two speakers behind the seats and point towards the TV. In 2014 a new standard was created, Dolby Atmos, that adds hight channels to the sound on films. The best way to do this is to cut holes in the ceiling and place speakers pointing at the seats from the ceiling at specific locations. However, for people who can’t do this, there is also an option to have speakers pointed at the ceiling (typically on top of other speakers at ear level) and to bounce the sound off of them to the seats.

Klipsch tower speaker with ceiling-firing speaker built into the top of it.

This is all very interesting, and probably sounds great. However, I didn’t want to cut holes in the ceiling, or to buy a new receiver that can decode Dolby Atmos, and I still couldn’t fit in even a 5.0 system. I then had the idea of using the surface bouncing idea to reflect the sound off of a wall and back to the seats so that the speaker near the door would have room.

With this idea in mind, I took some measurements of the distances and hight of the seating position, location of sofa, etc and put this geometry into Fusion 360 – a CAD software. This allowed for easy determination of the correct height and angle for the speaker driver to be located, such that a reflection off of the wall would arrive at approximately ear level.

Diagram of sound reflection off of the wall. Red line shows the approximate reflection path.

A speaker driver was selected, meeting the criteria of being full range (not including frequencies that might be covered by a future subwoofer), relatively small, high sensitivity, 8ohm, and relatively inexpensive. Since I don’t have any test equipment (oscilloscope or calibrated microphone) I thought it would be best not to try for a two way speaker, since designing and testing the a crossover without being able to measure anything really isn’t engineering, it’s just guesswork. The sensitivity requirement was set at about 90dB/1m/1W since one of the speakers has a long and inefficient path including a wall reflection. Small drivers were required so that the speakers can remain compact. (In hindsight, a coaxial speaker might have been a good choice, as a I could probably find a sufficient crossover network that someone else has calculated.)

FaitalPro 3FE22 full-range driver

The speaker driver selected was the FaitalPro 3FE22. These are 8ohm, full-range speakers (~100-20k Hz) that have an RMS power handling of 20W and a sensitivity of 91dB/1m/1W. These would then be expected to play at up to 104dB/1m, which isn’t as loud as the front stage, but sure is enough to damage your hearing with extended play. Further, rear channels really don’t have much happening most of the time and so if slightly more power is pushed through them during the final blockbuster-explosion, then they probably won’t catch fire. Interestingly, in Dolby Pro Logic, the first one in the ’70s, the surround channels were mono and limited to 7kHz. Modern movie mixes use the rear channels more, but still most of the sound will always come from the front speakers, and lower quality rear speakers are a reasonable cost saving endeavour.

To design an enclosure you need to know how the speaker will perform. In the ’60s and ’70s Thiele and Small worked out a simple model of how speakers react in various boxes. Thiele-Small parameters are used to model speakers, although they only apply for low frequencies. Some speaker manufactures ‘fudge’ their numbers a little bit, so before I ordered the drivers I wrote a small C++ programme that checks the Thiele-Small parameters against one another (the parameters are not completely independent). The driver checked out and so I trusted the rest of their measurements.

In order to ensure that a solid 100Hz was playable through the speakers a ported box was designed in WinISD (a free speaker modelling software). WinISD takes the Thiele-Small parameters and plots the frequency response of the speaker in different enclosures. The TL;DR answer to speaker boxes is that larger boxes allow deeper bass notes to be played as the air behind the speaker is more easily compressed (just like a long piece of rubber band is more stretchy than a short piece). Ported boxes resonate, exactly like a mass-spring (or pendulum) system. Frequencies that are near to the resonate frequency of the port excite air in the port and cause it to oscillate, this boosts the volume at the port resonant frequency. If you align the ports resonant frequency to be about the same frequency where the speaker driver starts to loose efficiency (and so play quieter) then boosting the volumes extends the linear frequency range.

There are some disadvantages to ports, in that you are basically always listening to the driver as well as the delayed response from the port, these two pressure waves can interfere with one another, and can also extend the length of a note while the port is oscillating. Ports with a small area produce a chuffing sound as air rushes through them, and larger ports need to be very long to achieve the same resonant frequency, they also have a large mass of air that can cause them to be improperly damped. This being said, perfectly good speakers can be designed with ports – like anything in engineering there is a trade-off between whatever compromises you choose to make.

The compromise that I came up with was to have a large long port that slightly boosted the bass. Due to the method of construction, this very wide port was easier to make.

Comparison of a ported and sealed enclosure response in WinISD. The ported response has a higher -3dB point, but the response drops of more rapidly. The sealed box has a volume of 0.7L. The ported box has a volume of 2L and a 4″x1″ port that is 40.6cm long.

The final parameters for the ported box was a volume of 2L, a rectangular port of 4″x 1″ and 40.6cm long. With these parameters I then went back to Fusion 360 to design the boxes. I designed two completely different boxes that both have the same port length and volume. The first of which was a traditional bookshelf speaker form, with a front facing port. The driver height was set to be just above ear level. The second box was designed with the speaker driver angled at 5.7˚ above horizontal, and a plate such that it can be stabilised under a sofa. The second box was also much taller, just under the height of the arm of the sofa.

Designing a box of the correct volume is easy enough, working out the correct port length was a little more challenging, but for a folded port it is easy to use some algebra to work out how many turns to use. To make sure the speaker was stiff enough everything was made from 12mm MDF (although I understand that plywood would have been a better choice as it is stiffer for the same thickness), and the front baffle was made double thickness. The top of the angled speaker was also made twice as thick to reduce the radiated sound. The sharp angles in the port will cause turbulence and reduce the efficiency of the port – I assume it was also reduce the Q of the port resonance (which should increase the tolerance in port length).

Width of the baffle for both speakers was 4” (~102mm), I don’t have a table saw, and cutting many metres of straight 4” wood was never going to happen. Luckily my local hardware store was able to cut the wood for me. I planned the design around this so that my manual sawing would have the fewest opportunities to ruin the build. I only had to cut strips to length, and cut out the side panels.

CAD model of router jig.

I 3D printed a circle cutting jig for my router in order to cut the 3” hole for the driver; an M3 machine screw holds the jig to the wood and sets the radius. For small holes the order of operations is a little awkward as the screw head is under the body of the router. Rather disconcertingly when in use the last part of the circle is quite hard to cut as the wood is only joined by a thin section. I drilled the centre holes for the speaker cut outs with the two piece of wood taped together, however, the router bit was not long enough to cut through both piece of wood, so they were separated for the final hole. I cut the hole undersized for the driver, and, after gluing together both components, I widened the hole with a rotary tool to make room for the basket and terminals.

The second speaker was much like the first. However, there were a few unusual challenges such as the angled joining points at the top of the speaker.

Test fitting the driver revealed the ideal location for the holes to fasten it to the baffle. I used M4 T nuts and machine screws to attach the driver as MDF disintegrates if screws are driven in and out of the wood. The drivers would have been difficult to flush mount, and already had gaskets on them, so this process was easier than it could have been. The T nuts were hammered in to the back, and later on in the build I used a clamp to seat them deeper as the first test fitting pushed them out (from the screw threading into the wood).

Each of the components were glued on to one of the side panels. 3D printed spacer cubes were used to hold the components of the port the correct distance away from one another. These were strong enough to clamp over and were printed very slowly so as to ensure they were dimensionally accurate. Clamps and weights (including a blender full of water) were used to hold everything together. Only one or two parts of the speaker were glued at each step, other parts were dry fitted in place to ensure everything stayed in position. Aluminium foil was used to separate surfaces that shouldn’t be glued.

The last part to be glued on was the side panel. The binding posts on both speakers were on the side panel as they were both designed to sit right up against a rear surface. Heavy gauge wire was soldered to speaker terminals and the binding post rings. The 3” hole for the driver was just large enough for my hand to tighten the nuts after the side was glued on.

I lightly sanded various parts of the speakers for fitting. The 5.7˚ angle for the angled speaker baffle was sanded into the front panel. And other parts were sanded flat to remove saw marks before gluing. The dust from this was collected and mixed with glue to make a wood filler that was used to fill the gaps made by my imperfect joinery. This filler was used inside and out of both speakers before the side panel was finally attached.

The two speakers were then installed in the living room, connected to the receiver. I manually set the distances and levels by playing white noise thought the each of the speaker in turn and using an app on my phone to measure the level– although at some point I should try the auto adjustment. The two speakers sound a little thin at the moment, but otherwise are fine. I have them set up as ‘small’ on the receiver, it then sends all of the bass to the front three speakers that have much larger drivers (the main speakers play down to 44Hz). I don’t know if this is the best compromise. Maybe I’ll update this when I’ve lived with them for a little while.

Surround music sound really good on the system with Dolby Prologic II music. This algorithm takes normal stereo music and sends the common signal to the centre channel and the difference between the left and right to the rear channels with some filtering. You get some interesting ambient effects and it really feels like the music is surrounding you. I’m sure better rear speakers would make it sound even better, but I’m quite happy for the moment with these. (You can do a similar thing without any fancy processing with a spare speaker and any normal stereo. Just connect the spare speaker to two positive terminals of your amplifier i.e. the positive from the left speaker and the positive from the right speaker. Then run that speaker behind you. The stereo sound should be about the same, but ambient effects will play through the rear one (you can do the same with two rear speakers). This set up is called a Hafler circuit, the ambient sounds that you hear are sounds that are out of phase between the L/R speakers.)

Movies also sound really good too. The first film we watched with the speakers installed was Jurassic Park, incidentally the first film with a DTS sound track. I was particularly struck with the echoes in the entrance hall of the Jurassic park building. In a darkened room, you really feel like you are in a room the size of the one depicted, not the size of your own room. The fire work effects in Coco were also very involving as you can hear them all going off around you.

First on my list of things to finish off with them is to get some primer on. The MDF won’t last long without something to protect it. However, that will involve quite a bit more sanding and a few clear days when I can paint outside, and then the final colour can go on. After that, I’d like to measure the output of the speakers and see if I can improve the sound. I have some DSP capability on the amplifier, but I might also try to implement a baffle step correction circuit.

In short, if you don’t have room to put a speaker for a surround sound system. You really can bounce the sound off of a wall, and it sounds pretty good.