Inspiration
2,300+ Americans died from heat in the summer of 2023, a record (AP analysis of death certificates). 471 active-duty US service members had heat stroke in 2024, along with 2,380 cases of heat exhaustion (MSMR, 2025). At work, heat caused 7,100 injuries in 2023–2024 that were bad enough to mean time off or restricted duty (NSC).
The frustrating part is that exertional heat stroke is close to 100% survivable if core temperature is brought below 40 °C (104 °F) within 30 minutes (Korey Stringer Institute). The problem is knowing when you're getting there. Core temperature can only be measured with a swallowed pill or a rectal probe, so on a field or a job site people go by feel, and by the time you feel it, you're already in trouble.
There is a standard workaround. A US Army research group published ECTemp, a tool that estimates core temperature from heart rate, and companies sell products built on it. An independent benchmark re-tested it and got a negative $R^2$ on one dataset, meaning it did worse than just guessing the average. We tested it ourselves on 14,041 minutes of real heat-chamber data. Its "90% confident" range held the true temperature only 34% of the time. In 34 of 73 trials where someone's real core temperature passed 38.5 °C, its 38.5 °C alarm never went off.
That's when the project stopped being "build a temperature wearable" and became EMBR: a wearable that's honest about what it knows, shows a range it has actually earned, and says "I can't tell right now" instead of making something up.
What it does
EMBR is a chest strap that estimates your core body temperature from your heart rate and shows it with a calibrated range, like "37.9 °C ± 0.5". It also checks whether the rest of your body agrees with your heart. If your heart is racing but you're sitting still with cool skin, that's probably coffee or nerves, not heat, so EMBR tells you it isn't sure instead of guessing. It never hides the number from someone who's already hot.
Everything streams over Bluetooth to a web app with a simple status: OK, Watch at 38.0 °C (slow down, take your break now) and Act now at 38.5 °C (stop, get to shade). The app also shows when to take breaks and drink water, has coach and supervisor views for a team or crew, and has an emergency panel with 911 and first-aid steps always on screen.
How we built it
We split the work into hardware, firmware, data and the app. The device is built around an ESP32-S3. It has a pulse sensor, two thermistors in the floor of the case touching your skin, a third thermistor hanging outside the strap to read the air, a motion sensor, and a USB power bank in its own pod on the strap. We designed the enclosure in Onshape with a curved back so it sits on your breastbone comfortably, and printed it in PETG. The firmware reads everything 100 times a second, pulls heart rate out of the pulse signal (ignoring "pulses" that are really the sensor bouncing in step with your stride), runs the estimate once a minute, and sends a small Bluetooth packet to the app every second. The app is plain JavaScript, with Web Bluetooth, a USB fallback and a replay mode.
We used Kiro to keep the whole thing from falling apart. We wrote the project as a spec, built it in phases (interface, data, firmware, Bluetooth, then real hardware), and kept a steering file with our hard rules, like "never hide the temperature from someone who's already at 38 °C". With firmware, Python and JavaScript all changing at the same time, that file saved us more than once.
The data side is where EMBR earns the word honest. Our main dataset is PROSPIE V3, an open dataset from Loughborough University where people walked in a heat chamber wearing rectal probes: 37 people, 128 trials, and a little over 14,000 minutes of real measurements. We rebuilt ECTemp from the published equations, where heart rate is modelled as a curve of core temperature $C_t$:
$$\text{HR}_t = \beta_2 C_t^2 + \beta_1 C_t + \beta_0 + v_t$$
Then we added a small learned correction that uses each person's own resting heart rate, since the original filter assumes everyone's heart responds the same way. And we threw out the filter's confidence range completely. Instead we used conformal prediction: we measure how wrong the model actually is on people it has never seen, and build the range from those real errors, so that
$$P\big(C \in [\hat C - \hat q,\ \hat C + \hat q]\big) \ge 0.9$$
without assuming anything about the shape of the errors. We tested everything by leaving one person out at a time, so every score comes from someone the model never trained on. The same calibration table is generated once and shared by the firmware and the app, so they can never disagree.
Challenges we ran into
Our first plan was to show the Kalman ± on screen, until we measured it and realized it was the problem. Then we found that the estimate lags exactly when people are hottest: above 38.5 °C, our range only holds 71% of the time. We couldn't fix that in a weekend, so we built the alerts around it, with Watch at 38.0 °C doing the real work, and put the 71% on our results page instead of burying it.
Some of our safety logic looked smart on paper and was wrong in practice. The "racing heart while still" check fired during recovery breaks, when a racing heart really is heat. Our filter for stride bounce threw out real heart rates from people running hard. Halfway through, we lost our humidity sensor and one of our two pulse sensors, and had to rework the firmware, Bluetooth format, app and wiring around it. The pulse sensor was weak on the chest. An unconnected pin nearly made the board think it had a dying battery and put itself to sleep. Our wires turned out too long for the lid, so we reprinted the case 12 mm taller and widened the screw holes, because PETG prints them tight. And keeping Python, C++ and JavaScript giving exactly the same numbers took a lot of testing.
Accomplishments that we're proud of
The old filter's "90%" range held 34% of the time; ours holds 89%. Our learned correction cut the average error from 0.41 °C to 0.32 °C and shrank the ± from 0.67 to 0.49 °C, and it was better for 31 of the 37 people. By the time someone's real core temperature hit 38.5 °C, our early Watch alert was already on in 65 of 73 trials, up from 47, with no extra false alarms.
We ran a check for differences between men and women that the original validations never did, and found no clear one (−0.06 °C, 95% CI [−0.16, +0.05]). We're also proud that we reported our weak spots, like the 71% in the danger zone, instead of hiding them. Under all of it are 191 unit tests, including ones that prove the C++ on the device matches our Python exactly. All of the results, code and figures are in analysis/JUDGES_SUMMARY.md.
What we learned
The biggest thing we learned is that being honest has to be designed in; you can't tack it on at the end with a disclaimer. A device that says "I don't know" at the right moment is more useful than one that always has an answer, as long as it never says it to someone who's already overheating. We learned that a good overall average can hide exactly where a model fails, which is why we report the danger zone separately. We learned to check fairness with bias instead of overall error: when we injected a fake offset into one group as a test, bias caught it and overall error missed it completely. And we learned that hardware will humble any plan, down to sensor placement and screw holes.
What's next for EMBR
Next, we want to test EMBR on a completely different dataset (there's an open one from firefighters), add calibration that learns each wearer over multiple sessions, and switch to a medical-grade heart-rate sensor. EMBR isn't a medical device yet, but it's the kind of device we'd want to wear ourselves. Everything is open at github.com/XBFBolt/ember.
Built With
- anthropicclaudeapi
- bleak
- bluetoothapi
- c++
- esp32
- freertos
- git
- imu
- ip-api.com-geolocation
- kiro
- matplotlib
- node.js
- numpy
- onshape
- open-meteo-forecast
- openpyxl
- pandas
- python
- r
- scikit-learn
- scipy
- thermistors
Log in or sign up for Devpost to join the conversation.