Product Thinker

©2026

05 / 07 · Case study

Spiiro

Role

Product Engineer

Discipline

  • Engineering

Context

Healthtech

Year

2026

Two rounds of clinical interviews reframed the whole build: the hard problem wasn't measuring a breath, it was replacing the supervisor who normally catches a bad one.

As part of a 4-person biomedical engineering capstone team at the University of Waterloo, I led stakeholder interviews with respirologists and clinical researchers, then built the companion app, its Bluetooth integration with the custom hardware, and the test-taking flow itself.

8 physicians and researchers consulted to shape the design and validation approach

90% accuracy separating a valid test from an invalid one, in the shipped hierarchical model the app coaches against

50–80ms latency from breath to on-screen coaching cue

$279 CAD target price vs. $350+ CAD for commercial spirometers, at a projected 32.7% IRR

01

The Problem

A test that only works when someone is watching

One in five Canadians lives with a serious respiratory disease like COPD, and the 18% who live in rural communities face the worst of it: appointments months apart, hours of travel, and a disease that can progress unnoticed in between. Spirometry, the standard test for tracking lung function, is well understood clinically, but it depends on a clinician watching the patient breathe. A cough, a hesitation, a half-effort breath, all of it quietly invalidates a result, and normally a technician catches it in the moment and asks the patient to redo it.

Take the supervisor out of the room and that quality check disappears with them. The commercial at-home spirometers we looked at didn't replace it, they just assumed perfect technique and handed back a number regardless. Our team's premise was that an at-home device only earns the word "clinical-grade" if it can catch what the supervisor would have caught.

02

Listening First

What the clinic floor actually looks like

Before any of us wrote code, I led the stakeholder research: interviews with respirologists and clinical researchers, and an in-person visit to an airway clinic to watch spirometry technicians run real tests. The goal was narrow, understand exactly what a human supervisor is doing that a device would need to do instead.

Two things came out of those conversations that shaped everything downstream. First, the errors that invalidate a test are a short, specific list, poor effort, a short blow, coughing mid-test, a premature stop or restart, hesitation, which meant the app didn't need to understand every possible way a breath could look wrong, just those five. Second, clinics calibrate their spirometers daily against a 3-litre syringe at slow, medium, and high flow speeds before trusting a single reading, a step that has no equivalent at all in a device that only ever gets used by one untrained person on their kitchen counter.

Five common test-technique errors
ErrorRegular
The five error signatures the app was built to catch: each error trace (solid) bends away from the shared regular-effort trace (dashed) in its own distinct way.
From clinic floor to app decision
01
Physician interviews
Talked to respirologists and researchers about what makes a spirometry result untrustworthy.
02
Airway clinic visit
Watched technicians run and calibrate real tests, in person.
03
Finding: 5 known failure modes
Poor effort, short blow, cough, restart, hesitation: a finite list, not an open-ended one.
04
Shipped: real-time coaching + ambient calibration
The app replaces the missing technician with live feedback and a per-test calibration step.
Interviews and a clinic visit narrowed an open-ended problem, detecting bad technique, into a finite, buildable one.

That reframe is what let the rest of the team scope a two-layer AI model against exactly five error types instead of an unbounded one, and it's what told me the app needed a calibration step of its own, not just a results screen.

03

Building the App and the Integration

Flutter, until the platforms stopped being equal

I built the companion app in Flutter, chosen so the same codebase could target both iOS and Android rather than maintaining two native builds for a four-person team. That held until we tried to actually deploy to an iPhone: Apple's additional device-testing requirements made iteration slow enough that we made the call to develop against Android exclusively and treat iOS as a later port. Cross-platform was still the right starting bet, it just meant staying honest about when the "write once" promise stops being free.

The harder integration problem was making the Bluetooth link trustworthy enough for a clinical reading. The device streams flow data at 100Hz, but sending every sample individually would have congested the BLE stack, so I worked within the team's batching scheme, 10 samples per 20-byte packet, sent at roughly 10Hz, and reconstructed true per-sample timing on the app side from total test duration rather than trusting packet arrival time, since BLE transmission jitter would otherwise have thrown off the flow-volume curve.

The Venturi tube's pressure taps feed the flow calculation; the Bluetooth link they sit behind is what had to stay trustworthy enough to hand that reading to the app.
The Venturi tube's pressure taps feed the flow calculation; the Bluetooth link they sit behind is what had to stay trustworthy enough to hand that reading to the app.Fig. 01
Two integration decisions, and the constraint each answered
Ambient-flow calibration
Constraint
No technician in the room to confirm the device is reading zero before the patient blows.
Decision
A 5-second ambient-flow measurement runs before every test and is subtracted from the result.
Packet batching over BLE
Constraint
100Hz raw samples would congest the BLE stack on a low-power microcontroller.
Decision
Batch 10 samples per 20-byte packet at ~10Hz, and recompute true timing from test duration rather than packet arrival.
Both decisions trade a small amount of app-side complexity for a reading the team could actually trust.
04

Designing for the Person Actually Holding the Device

A static target looked fine in a demo and fell apart with a real user

To keep patients motivated through a 20-second test, the app shows live flow-rate feedback as a simple game, a ball that responds to how hard the user is breathing. My first version gave every user the same static target line to hit. It worked in a quick demo, but it had two problems that only showed up with the elderly, rural users we were actually designing for: it needed per-user calibration to mean anything, and it assumed a flow rate that's only true in the first second of a real exhale. A human breath peaks fast, then tapers off, so a flat target left users watching the ball fall further behind for the second half of every test, which is exactly the kind of discouraging feedback loop that tanks compliance in a population we couldn't afford to lose.

The in-test game: keep a blue ball above a target line by blowing harder
Live feedback during the blow: the ball is the flow rate, the line is the moving target
Motivating a 20-second breath test
Needs to keep an elderly, possibly frustrated user engaged for the full test, every time.
Rejected
Static target
Simple to build, but required per-user calibration and stayed flat while a real exhale naturally decays; users fell behind and lost motivation late in the test.
Shipped
Dynamic target
Starts at 6.0 L/s, snaps to 90% of the detected peak flow rate, then decays over 7 seconds, shaped like an actual human exhale, so the target stays reachable.
The fix came from matching the target curve to the physiology of an exhale, not from adding more instructions to the screen.

That same design instinct, build for the person on the other end of the device, not the happy-path demo, ran through the rest of the flow: large single-purpose screens, plain-language test instructions instead of clinical terminology, and a results view that answers "am I doing better or worse than last time" before it shows a single raw number.

Home check-in screen showing FEV1/FVC and a trend line across past tests
Home: the trend line is the first thing shown, before any single-day number
Plain-language test setup instructions with a simple posture icon
Setup: plain language, one instruction at a time
Results screen showing FEV1/FVC and change from the last test
Results: framed against last test, not a raw number in isolation
05

Where It Stands

The bar we set was clinical trust, not a lower price

The final prototype met the numbers that mattered: 90% accuracy separating a valid test from an invalid one, 88% accuracy identifying which of the five errors occurred, and a 50–80ms round trip from breath to on-screen coaching. At a proposed $279 CAD against commercial spirometers running $350 and up, it's also a financially viable at-home alternative, not just a research prototype.

Non-REG Subclass Classification
True labelPredicted label
Binary Classification (REG vs NON_REG)
True labelPredicted label
Confusion matrices for the two-layer model: the binary regular-vs-non-regular pass (right) and the sub-classification of which error occurred once a test is flagged non-regular (left).

We were equally direct about where it falls short. The AI model was trained on tests performed by team members rather than real COPD patients, so it hasn't been validated against the population it's meant to serve. The device captures exhalation only, not the full breath cycle a clinical reading uses. And BLE encryption during active transmission was deliberately deferred: we tried it, connection reliability dropped enough to make the device frustrating to use, and for a capstone prototype we chose a device that works over one with a security property that would need re-earning before any real deployment anyway. None of these are things I'd want to ship past a prototype, but naming them precisely is what makes the 90% a number worth trusting instead of one worth doubting.