Bring a sensor reading into the record
Sensor, microcontroller, short-range link, and the NRTR client. A stored reading uses source device. It is not a lab result, and this hardware is not a cleared medical device.
The device does not write the database. It emits samples. The client turns a finished window into an observation and submits it through the same rules as a typed entry: code, number, unit, time. A packet that fails those rules is dropped with a reason. It is not coerced. Those rules are in Record a health observation.
Path
Sensor → microcontroller → sample, do not smooth over gaps → Bluetooth or serial → Flutter client → one observation per finished window → same validation as a manual entry → local database
Packet
{
"device_id": "proto-01",
"stream": "ppg",
"t0": "2026-09-24T14:10:00-06:00",
"period_ms": 20,
"samples": [1024, 1030, 1028]
}
| Field | Rule |
|---|---|
device_id | Stable for the physical unit on the bench. Not a person id. The phone binds the device to the local profile once, in the client, not in the packet. |
stream | A name the client already maps to an observation code and unit. Unknown streams are rejected. Do not add the mapping inside the widget that draws the chart. Add it in one table. |
t0 | Time of sample zero, ISO-8601 with an offset. The phone does not invent times from arrival order. |
period_ms | Interval between samples, in milliseconds. Required. A packet without it is dropped. |
samples | Integers in order. A gap is an explicit gap, not a repeated last value. Repeating the last value invents a measurement. |
Connection
- Idle. The device advertises, or the serial port is closed. The phone stores nothing.
- Connected. The client subscribes to the sample characteristic, or opens the serial port named in the bench notes for this unit.
- Streaming. Packets append to a window. The window closes on a time limit or on a gap.
- Disconnected. The client shows disconnected. Finished windows already closed may still be submitted. A half window is discarded. Do not flush a half window “so we don’t lose data.” A half window is not a measurement.
Errors
| What you see | Cause | What you do | What you do not do |
|---|---|---|---|
| The observation time is hours off the phone clock. | t0 and the phone disagree. | Store the device time and flag the observation. | Rewrite t0 to “now.” |
| The chart stalls and the UI thread is busy. | Samples arrive faster than the client consumes them. | Drop the newest unfinished packet and count the drop. | Block the UI thread until you catch up. |
Validation returns unit or an unknown stream. | The stream has no mapping to a code and unit. | Add the mapping in the one table, then resend a new window. | Hard-code a unit in the chart. |
| Two observations for one window. | The client retried a submit after a success it did not see. | The observation contract returns duplicate_observation when code, time, value, and unit match. Treat that as the first write. | Change the time so the retry looks new. |
Bench check
Use one prototype unit. Do these in order. Write the results down. A check you did not write down did not happen.
- Connect. Confirm the client shows connected, and that no observation exists yet for this window.
- Record a short stream. Close the window on purpose.
- Find one new observation in the local database. Source is
device. The time matchest0plus the window, not the moment you looked at the phone. The unit matches the stream table. - Disconnect in the middle of the next window.
- Confirm that partial window is absent. The observation from step 3 is still present.
- Reconnect and complete a new window. Confirm a second observation, not a duplicate of the first.
If step 3 fails, do not continue to step 4. Fix the mapping or the clock flag first. Stacking failures makes the log useless.
Out of scope for this bench
- Battery life, sealing, skin contact, and regulatory claims.
- More than one person bound to one device. The bench binds one device to one local profile.
- Uploading the packet to a vendor. The observation, if it leaves the machine at all, leaves later, through the internal boundary, as an observation. The raw packet does not.