Back to all projects

Jan 2024 – May 2024

SWE · EE

Last edited

MediCare: Temperature, Time, Tracking

Built for the Physical Medicine outpatient clinic at St. Olavs Hospital, where storage of local-anaesthesia vials (lidocaine) was being lost to manual processes: handwritten dates on opened vials, once-daily fridge temperature checks, and easy-to-miss expiration on unopened stock. The three sub-systems each target one of those failure modes.

The temperature sub-system uses a BME680 sensor on a Raspberry Pi sampling every 10 seconds over i2c, writing to MySQL and serving a React + TypeScript dashboard via a NodeJS backend. A Python SMS service pages staff when the fridge drifts outside the 2–6 °C window, and the dashboard offers a downloadable weekly CSV.

The open-vial holder starts a 3-day countdown when a vial is placed and switches the slot's LED from green to red when usability expires, removing the need for handwritten dates entirely. The rotating rack reads expiration dates from QR codes on unopened vials and rotates the next-to-expire vial to the front, with per-slot LEDs flagging anything already expired.

Affiliation

NTNU

Partners

Report

  • Project report

Keywords

  • IoT
  • Electromechanical Systems
  • Embedded Systems
  • C
  • Python
  • Raspberry Pi
  • BME680
  • MySQL
  • Node.js
  • React
  • TypeScript

Deepdive

Introduction

MediCare is a hospital-logistics system built for the Fysikalsk medisinsk poliklinikk at St. Olavs universitetssykehus as part of NTNU’s TTT4270 Elektronisk systemdesign in spring 2024. The clinic stores 15–20 vials of lidocaine (lokalbedøvelse) in a single refrigerator at any time, each vial a 20 ml dose that, once opened, is reusable for three days and must otherwise be discarded. Two failure modes drive measurable drug waste: the fridge temperature drifting outside the 2–6 °C band without anyone noticing in time, and staff losing track of how long an opened vial has been in use. The system replaces the existing pen-and-paper workflow with three integrated subsystems, a continuous temperature monitor with a web dashboard, a time-tracking rack for opened vials, and a rotating rack for unopened vials, that together eliminate the manual bookkeeping and the silent failures it permits.

Problem Definition

The clinic stores a set of vials V=VoVuV = V_o \cup V_u, where VoV_o are opened vials with a per-vial timer and VuV_u are unopened vials with a printed expiry date. A vial vVv \in V is usable iff three conditions hold simultaneously:

usable(v)  =  1 ⁣[Tfridge(t)[2,6]°C    t[t0,now]]    1 ⁣[τv<τmax]    1 ⁣[dv>now],\mathrm{usable}(v) \;=\; \mathbf{1}\!\left[\, T_{\text{fridge}}(t) \in [2,\, 6]\,°\text{C}\;\; \forall\, t \in [t_0,\, \text{now}] \,\right] \;\wedge\; \mathbf{1}\!\left[\, \tau_v < \tau_{\max} \,\right] \;\wedge\; \mathbf{1}\!\left[\, d_v > \text{now} \,\right],

where Tfridge(t)T_{\text{fridge}}(t) is the continuous fridge temperature, τv\tau_v is the elapsed time since vial vv was opened with shelf-life cutoff τmax=3days\tau_{\max} = 3\,\text{days}, and dvd_v is the unopened expiry date stamped by the manufacturer. The clinic’s existing process resolves the first predicate via a single manual reading per day and the second via a handwritten date on the glass, both of which are dominated by human error in a busy clinical environment. The system’s job is to convert these three predicates from manual judgments into continuously-evaluated machine state, alert when any of them flips, and surface the data in a form the clinic’s secretaries can act on.

The need was distilled into ten user requirements (A–J), of which the most load-bearing are: warn staff on temperature excursions (G), log temperature for compliance auditing (I), indicate when an opened vial has expired (E), and surface the unopened vial with the shortest remaining shelf life (C).

Approach

High-level architecture of MediCare, with three subsystems sharing the clinic refrigerator as their physical context: a BME680 → Raspberry Pi → MySQL → NodeJS/React dashboard with SMS alerts, an ESP-32-driven opened-vial rack running a three-state FSM per slot with WS2812B LED feedback, and a Raspberry Pi 4-driven rotating rack that rotates the shortest-expiry unopened vial into front position via an SG90 servo.
High-level architecture. Three subsystems share the clinic refrigerator as their physical context and run independent compute paths: a continuous temperature monitor feeding a web dashboard and SMS alerts, an ESP-32 rack that times opened vials against a three-day shelf-life, and a rotating rack that surfaces the unopened vial closest to expiry. Dashed edges denote staff-facing outputs.

The system decomposes into three subsystems that share only the fridge as a physical context. Each was scoped to its own user requirements and built around a microcontroller appropriate to its compute and I/O profile.

Temperature Monitoring

A Bosch BME680 reads the fridge temperature over I²C and is polled by a Raspberry Pi at 10s10\,\text{s} intervals (system requirement 1.5). Each sample is written to a MySQL table with a timestamp via an INSERT INTO temperatur (id, temperatur, tid, dato) VALUES (...) query issued from a C client that wraps the Bosch BME680 SensorAPI. The library exposes bme680_get_sensor_data against a struct bme680_dev whose I²C read/write/delay function pointers are filled in with platform-specific implementations, on the Pi these wrap /dev/i2c-1 via open, read, and write, with a sleep(period / 1000) delay shim.

The samples feed a NodeJS back-end that exposes four APIs to a React/TypeScript front-end: a rolling average over the last hour, a downloadable weekly CSV, a graph series, and a Python-script trigger that sends SMS alerts when the last-10-minute average drifts outside the [2,6]°C[2,\,6]\,°\text{C} band. The front-end re-fetches every 10s10\,\text{s} so the dashboard tracks the database in near-real-time.

Sensor accuracy was characterised by treating each reading as a uniform draw on [T1,T+1][T-1,\, T+1]. The mean and variance of one sample are then

μ  =  (T+1)+(T1)2  =  T,σ2  =  (2)212  =  13,\mu \;=\; \frac{(T+1)+(T-1)}{2} \;=\; T, \qquad \sigma^2 \;=\; \frac{(2)^2}{12} \;=\; \frac{1}{3},

and averaging n=3000n = 3000 samples per displayed datapoint, the central limit theorem gives

Z  =  Xˉ,μσ2/n    N(0,1),Z \;=\; \frac{\bar{X}, \mu}{\sqrt{\sigma^2/n}} \;\sim\; \mathcal{N}(0, 1),

which puts P(Xˉ,T>0.025°C)P(|\bar{X}, T| > 0.025\,°\text{C}) at a negligible level, comfortably inside the ±0.5°C\pm 0.5\,°\text{C} tolerance demanded by system requirement 1.6.

Probability density of the temperature deviation after averaging 3000 BME680 samples, sharply concentrated within ±0.025 °C of the true value.
Probability density of the averaged temperature deviation. After averaging 3000 samples, the displayed value sits within ±0.025 °C of the true temperature with overwhelming probability, well inside the ±0.5 °C tolerance required by system requirement 1.6.

Time-Tracking Rack for Opened Vials

Each slot in the opened-vial rack is a three-state machine that tracks how long a vial has been in residence:

S    {S0(empty),    S1(counting),    S2(expired)}.S \;\in\; \{\, S_0\,\text{(empty)},\;\; S_1\,\text{(counting)},\;\; S_2\,\text{(expired)} \,\}.

Transitions are driven by a Zippy DF-series micro-switch signal KK (vial present), a reset push-button, and an elapsed-time variable τ\tau derived from the ESP-32’s millis():

S0K=1S1,S1ττmaxS2,S1,2resetS0,S2K=0ττmaxS0.S_0 \xrightarrow{K=1} S_1, \qquad S_1 \xrightarrow{\tau \,\geq\, \tau_{\max}} S_2, \qquad S_{1,2} \xrightarrow{\text{reset}} S_0, \qquad S_2 \xrightarrow{K=0\,\wedge\,\tau\,\geq\,\tau_{\max}} S_0.

A WS2812B addressable LED under each slot encodes SS: off in S0S_0, a green-to-blue gradient through S1S_1 proportional to τ/τmax\tau / \tau_{\max}, and red in S2S_2. The gradient is computed every iteration of the main loop as

b  =  255ττmax,g  =  255,b,b \;=\; \left\lfloor 255 \cdot \frac{\tau}{\tau_{\max}} \right\rceil, \qquad g \;=\; 255, b,

so a staff member glancing at the rack sees not just which vials must be discarded, but which of the remaining vials has the most life left, directly addressing user requirement C without an additional UI. The microswitches were chosen for their light actuation force (a 20 ml vial doesn’t weigh much), and a 3D-printed casing extends the lever arm to increase the moment from the vial’s weight onto the switch.

The implementation has to defend against millis() rollover, which on the ESP-32 occurs after about 49 days when the underlying unsigned long saturates at 23212^{32}-1. The elapsed-time check is therefore written as

Δt  =  {(ULONG_MAX,tstart)+tnowtnow<tstart,tnow,tstartotherwise,\Delta t \;=\; \begin{cases} (\,\texttt{ULONG\_MAX}, t_{\text{start}}\,) + t_{\text{now}} & t_{\text{now}} < t_{\text{start}}, \\[2pt] t_{\text{now}}, t_{\text{start}} & \text{otherwise,} \end{cases}

so a vial inserted near the rollover boundary still ages correctly. The full logic, replicated per slot, lives in a single infinite loop on the ESP-32 with the per-slot variables knapp_Pin, Reset_knapp, start_time, and leds[].

Three-state machine for one slot of the opened-vial rack: Idle (G=0, R=0, t=0), Counting (G=1, R=0), and Discard (G=0, R=1), with transitions on the micro-switch signal K, the elapsed time t, and a manual reset.
State diagram for one slot of the opened-vial rack. Insertion lights the LED green, the elapsed-time variable drives a smooth green-to-blue gradient until the three-day cutoff, after which the slot flips to red and waits for either removal or a manual reset.
3D-printed opened-vial rack holding three lidocaine vials, with two slots glowing green (still usable) and one glowing red (past three-day shelf-life).
Physical realisation of the opened-vial rack. Each slot’s LED encodes the slot’s state directly: the two left vials are still within shelf-life and the rightmost has expired and must be discarded.

Rotating Rack for Unopened Vials

The unopened-vial subsystem is a rotating wheel with twelve slots, driven by an SG90 9G servo from a Raspberry Pi 4 Model B. Each slot carries an expiry date did_i loaded into a Python list at startup, and the wheel is continuously rotated so that the slot with the smallest non-expired did_i sits at angular position θ=0\theta = 0 (closest to the user):

i  =  argmini{0,,11}  {di  :  di>now}.i^\star \;=\; \arg\min_{i \,\in\, \{0,\dots,11\}} \; \bigl\{\, d_i \;:\; d_i > \text{now} \,\bigr\}.

Slots whose vial has been removed are skipped (GPIO reads HIGH from the slot’s micro-switch), expired slots are marked red, and the remaining valid slots are blue, with ii^\star highlighted by being rotated into front position rather than colour-coded. All electronics, Pi, servo, BME680, switch bank, and a WS2812B strip, sit in the upper rotating half of the wheel and are mechanically isolated from the stationary base via a tooth-gear coupling, which dodges the need for a slip ring in the prototype. The custom interface board is a perfboard with one 10kΩ10\,\text{k}\Omega pull-down per switch line, fan-out to the Pi’s GPIO header, and a 330Ω330\,\Omega resistor on the LED data line as recommended by the WS2812B datasheet.

Exploded CAD view of the rotating unopened-vial rack, showing the stationary base, the rotating upper wheel with twelve vial slots, the vial holder ring, and the central shaft.
Exploded CAD view of the rotating rack. The upper wheel rotates relative to the stationary base via a tooth-gear coupling, which avoids the slip-ring that an electrically-connected rotating subsystem would otherwise require.
Underside of the rotating rack with its electronics exposed: Raspberry Pi 4, SG90 servo, BME680 sensor, perfboard with pull-down resistors, and the WS2812B LED strip wired to the slot positions.
Interior of the rotating rack. All electronics, Pi 4, servo, BME680, switch bank, and the LED strip, sit in the upper rotating half, with a 10kΩ10\,\text{k}\Omega pull-down per switch line and a 330Ω330\,\Omega series resistor on the LED data line.

Results

Verification was structured around the 14 system requirements derived from user requirements A–J, each checked against an explicit pass/fail criterion. The temperature subsystem passed every requirement: SMS alerts fire on excursions, the dashboard updates every 10s10\,\text{s}, weekly CSVs are downloadable, the temperature text on the dashboard switches between green/blue/red on the [2,6]°C[2,\,6]\,°\text{C} band, and the central-limit-theorem analysis above bounds the worst-case display deviation well below ±0.5°C\pm 0.5\,°\text{C}.

The opened-vial rack was timed against a stopwatch at 55, 1010, and 3030 second intervals as a proxy for the three-day countdown (the real countdown is impractical to time end-to-end on a verification deadline). Across three trials per interval, the maximum deviation was 0.35s\approx 0.35\,\text{s} and the mean deviation 0.25s\approx 0.25\,\text{s}, dominated by stopwatch reaction time. Extrapolated to τmax=3days\tau_{\max} = 3\,\text{days}, the timing error is well inside any clinically meaningful tolerance.

IntervalTrial 1Trial 2Trial 3
5s5\,\text{s}5.19s5.19\,\text{s}5.35s5.35\,\text{s}5.13s5.13\,\text{s}
10s10\,\text{s}10.28s10.28\,\text{s}10.21s10.21\,\text{s}10.15s10.15\,\text{s}
30s30\,\text{s}30.26s30.26\,\text{s}30.22s30.22\,\text{s}30.27s30.27\,\text{s}

The rotating rack met all of its requirements: diameter 28cm28\,\text{cm} and height 12.5cm12.5\,\text{cm} (both inside the 40×30cm40\times30\,\text{cm} envelope set by the clinic fridge), correct rotation to the shortest-expiry vial across repeated tests, and a red-light indication for already-expired slots.

Validation was conducted by surveying five healthcare-sector respondents (four physical, one nursing student who evaluated the web dashboard only) against the ten user requirements on a 1–100 scale. Mean scores were A=100A=100, B=89.5B=89.5, C=86.75C=86.75, D=94D=94, E=87E=87, F=92F=92, G=100G=100, H=97.8H=97.8, I=78I=78, J=82J=82. The weakest areas were II (the weekly table currently reads back exactly 60,48060{,}480 samples, which slides the window by minutes rather than aligning to calendar days) and JJ (the perfboard wiring is fragile under repeated handling).

Future Work

The largest remaining risk in the temperature subsystem is operational rather than electrical: the SMS alerting pipeline runs as a Python script invoked by the NodeJS back-end, so an alert is only fired if the website happens to be up. A natural fix is to lift the alerting logic out of the front-end stack entirely and run it as a standalone service on a server that is independent of the dashboard’s uptime, with a cron-triggered query against the MySQL table and the existing SMS gateway. The same service should also be hardened against fridge-power loss, which currently puts the ESP-32 rack back into S0S_0 on cold boot and silently loses every active countdown.

The opened-vial rack would benefit from migrating from jumper-wired perfboard onto a custom PCB, which would simultaneously address the JJ-score complaint about wiring fragility and free up the underside of the rack for a per-slot E-paper display showing τ\tau, τmax,τ\tau_{\max}, \tau, and the absolute opening date. Adding a screen would also remove the only colour-only failure mode in the design, a colour-blind user currently cannot distinguish the green/blue gradient from red in poor lighting, which is the single biggest accessibility hole in the current build.

The rotating rack is the subsystem with the most headroom. The current rotation is open-loop on time, so cumulative position drift is the dominant error term; closing the loop with an encoder or a Hall sensor on the base would bring the rotation under a proper controller and eliminate the residual angular drift. The rack should also gain a network link to the temperature dashboard, so that the secretaries who manage stock can see, in one place, which unopened vials are closest to expiry and how many of each lidocaine batch are left. Finally, vendor packaging delivers lidocaine in cassettes of five vials with identical expiry, which means twelve independent slots is wasteful in practice; a redesigned wheel with five-slot cassette holders, sketched in the recommendations section of the original report, is a strictly better fit for the actual SKU shape and would push the achievable capacity from 1212 vials to the 15152020 vials the clinic actually holds.

Proposed redesign of the rotating rack with five-vial cassette holders arranged radially, matching the lidocaine manufacturer's five-vial packaging cassettes.
Proposed redesign of the rotating rack. Five-vial cassette holders replace the twelve independent slots so that the geometry matches the manufacturer’s packaging, raising the achievable capacity from twelve vials to the fifteen-to-twenty the clinic actually holds.