Inspiration
Picture a new hire on day two at a Florida solar farm. It's 2 PM in August, the heat index is past 105 °F, and they don't want to be the one who asks for a break. They feel a little off, push through, and keep tightening bolts. By the time they feel dizzy, their core temperature is already dangerously high, and judgment is one of the first things heat takes away.
That scenario isn't rare. Heat is the deadliest weather hazard in the US, and on job sites it's badly undercounted. In one CDC review, 9 of 13 worker heat deaths happened in the first three days on the job, before the body adapts. The workers most at risk are exactly the ones least likely to speak up.
We built this in Gainesville, where Florida crews at solar farms, substations and power plants work through this every summer. Their tools today are a thermometer on the wall, a water cooler, and a supervisor's gut feeling. When we read the PlanIT Control track brief, one question stuck with us: what if the system could see heat illness coming before the worker could feel it?
That became Redline: a band that forecasts each worker's core temperature 30 minutes ahead, buzzes before it gets dangerous, and calls for help if someone goes down anyway.
What we learned
Measure before you believe anything. We had a lot of good-sounding ideas. About half of them didn't survive a fair test.
- Our first attempt at personalization learned 16 numbers per worker from their heart rate. It made forecasts 40% worse (0.30 → 0.42 °C). Instead of tuning it harder, we asked why. Most of a person's error turned out to be a steady offset, and heart rate carries essentially no information about it (correlation −0.02). So we switched to learning the offset from quick thermometer spot checks, with a shrinkage rule that treats one odd reading with suspicion:
$$ \text{offset} = \frac{1}{n + 2} \sum_{i=1}^{n} (m_i - p_i), \quad |\text{offset}| \le 0.6 $$
Here \(m_i\) is a thermometer reading, \(p_i\) is our prediction at that moment, and the offset is capped at 0.6 °C. The +2 acts like two imaginary checks saying "this person is average." That cut 20-minute error from 0.308 to 0.270 °C.
- Validation scores flatter you. Our best model scored 0.230 °C on the people we used to pick it, and 0.370 °C on ten people we'd locked away and scored exactly once. We report 0.370.
AI and safety need a hard line between them. When Claude planned crew schedules on its own, 92% of its plans broke a safety rule. Once a plain-code rule checker had the final say, that number went to 0%. The rule we built everything around: AI predicts and suggests; code makes every safety decision.
Hardware humbles you. A simulation never told us that a buzzer is passive, that I2C pins can be unpowered, or that a flashlight flickers. Real uploads and real drop tests did.
How we built it
The band. An Adafruit ESP32-S3 Feather on a breadboard strapped to the forearm, with an accelerometer (from an Adafruit Prop-Maker FeatherWing), a diode we turned into a skin thermometer, a photoresistor for sun versus shade, a buzzer, a button and a status light. A diode's forward voltage drops about 2 mV per degree, so with a 64-sample average and a two-point calibration in room-temperature and ice water, a 1N4148 becomes a usable thermometer. We wrote the firmware from scratch in Arduino C++. It reads the accelerometer 400 times a second so it never misses the spike of a fall, and it streams to the laptop over Bluetooth Low Energy. The band reports and the laptop decides, so we can improve the logic without reflashing.
Core temperature right now. We implemented the US Army's ECTemp model, a Kalman filter that treats core temperature \(CT\) as a hidden state and heart rate as a noisy measurement of it. Each minute:
$$ CT^- = CT_{t-1}, \quad v^- = v_{t-1} + \gamma^2 $$
$$ K = \frac{v^- c}{c^2 v^- + \sigma^2}, \quad CT_t = CT^- + K \left(HR_t - h(CT^-)\right), \quad v_t = (1 - K c) v^- $$
where \(h(CT) = b_2 CT^2 + b_1 CT + b_0\) maps core temperature to expected heart rate and \(c\) is the slope of \(h\) at the current guess. It needs no training and works from minute one.
The forecaster. A GRU encoder-decoder reads the last hour minute by minute (15 inputs, including heart rate, skin temperature, movement, WBGT and the Kalman estimate) and predicts a correction to the Kalman estimate for the next 30 minutes. It outputs a low, likely and high value each minute, trained with the pinball loss:
$$ L_q(y, \hat{y}) = \max\left(q (y - \hat{y}), (q - 1)(y - \hat{y})\right), \quad q \in \lbrace 0.1, 0.5, 0.9 \rbrace $$
We added an asymmetric penalty that makes under-predicting 3× more costly when someone is really above 38 °C, and a physics penalty for any forecast moving faster than 0.05 °C per minute. We pretrained on 9,000 simulated work shifts from the international heat-strain standard (ISO 7933), then fine-tuned on 65 real people from a published study who swallowed core-temperature pills. Five networks vote on every forecast, and conformal prediction stretches the ranges until "90% sure" really holds 90% of the time.
The planner. A crew of 6 over 16 half-hour slots has about \(10^{107}\) possible schedules. Google's OR-Tools CP-SAT solver finds the most productive one that keeps everyone's predicted temperature under the warning line. Claude explains the plan in plain words and writes a 6 PM briefing, and a code rule checker re-checks every plan before anyone sees it. On top of that, the agent improves itself: it invents its own practice days, writes a playbook of lessons, and evolves its strategy (GEPA). A change only sticks if it wins on days it never practiced on.
Offline AI. Job sites often have no signal, so we distilled Claude into a 1 GB Qwen3-1.7B model with LoRA fine-tuning on a MacBook. Constrained decoding and self-training on its own safe plans raised its rule-passing schedules from 17% to 68%.
Site hazards. We pull live NOAA GOES-19 lightning, river gauges, NASA FIRMS wildfire data and National Weather Service warnings for 79 real Florida power plants. A gradient-boosted model trained on 202,003 site-days predicts tomorrow's risk at each site.
The emergency call. If the band detects a hard fall and the worker doesn't press the button within 20 seconds, an ElevenLabs voice agent calls through Twilio. It states the location first, repeats it at the end, and answers only from the facts it has. Emergency numbers are blocked in code, and in the demo it calls a teammate's phone.
Everything else. A FastAPI backend streaming over WebSockets, a Next.js dashboard for the crew lead, worker and judges, and 729 automated tests, including end-to-end runs of the whole system.
Challenges we ran into
Nothing could be ordered. A hackathon weekend is too short for shipping, so every part came from the hardware lab. There were no Grove cables anywhere, so the Grove accelerometer and light sensor we'd planned around couldn't connect. We redesigned around a Prop-Maker FeatherWing and a plain photoresistor.
The accelerometer that wasn't there. The board couldn't find the accelerometer. The first cause was that the FeatherWing was seated in the breadboard rotated 180°. After we fixed that, it still wasn't found: the ESP32-S3 Feather keeps its I2C pins unpowered until the code switches them on. One line of code fixed it.
Hardware surprises everywhere. The buzzer was passive, so setting the pin high did nothing and every alert had to become a tone() pattern. The LED strip stayed dark because we'd wired the OUT end. Pins A0 to A4 stop reading analog values whenever WiFi is on. Each of these took an hour to find and a minute to fix.
The phantom heartbeat. With no pulse sensor, we tried reading a fingertip pulse with the photoresistor and a phone flashlight. It found a real 53 bpm pulse, and then a perfectly steady 90 bpm appeared out of nowhere. The flashlight's invisible PWM flicker was being sampled too slowly and aliasing into a fake heartbeat. We fixed the sampling, but the signal was still too weak to trust, so the demo uses a clearly labeled heart-rate input.
Our first fall test failed. A hard drop was detected, and then tiny sensor wobble kept cancelling the alarm. Now a real fall locks in, and only the worker's button press can stop the call.
One bug, three failures. Our first real emergency test call failed because the Mac's Python couldn't verify HTTPS certificates. The same bug was silently breaking live storm data and crew texts. Fixing it fixed all three.
A learning loop that learned noise. The solver gave slightly different answers depending on how busy the laptop was, and our self-improvement loop happily "learned" from that noise. We made the solver deterministic and re-ran everything.
Telling the truth about our results. A simple baseline, "temperature stays flat, with an honest range," warned almost as early as our network (16.1 vs 16.9 minutes). It would have been easy to leave that out. We kept it in, because it taught us something real: most of the early-warning benefit comes from forecasting with honest uncertainty at all, and our network's edge is accuracy.
Results: on ten people the models never saw, our 20-minute forecast is off by 0.370 °C, versus 0.481 °C for the published Army method (23% better). It catches 88% of heat warnings early, 16.9 minutes ahead on average.
Built With
- adrafruit-feather
- arduino
- bluetooth
- claude
- esp32
- fastapi
- kalman-filter
- python
- pytorch
- websockets
Log in or sign up for Devpost to join the conversation.