The dashboard said I had taken a fasting blood glucose reading at 12:12 in the afternoon.
That is not a thing anyone does. Fasting readings happen when you wake up, before coffee, before food, which in my house is a little after six in the morning. But the reading was there, tagged fasting by the meter itself, timestamped midday. And it was not one bad row. Every reading in the list, all 800 of them, was sitting exactly six hours later in the day than it should have been.
Six hours. I live in Mountain time. So this was not a mystery so much as a confession.
The meter is a database
Some background on what I was building. I have been working on a health app for my household: weight, blood pressure, labs, meds, and whatever a wearable will hand over. The visible part is charts and a chat panel. The part that has actually eaten the time is ingestion, because every source of health data arrives in its own dialect and none of them are yours.
The newest source is a Contour Next One, an ordinary drugstore blood glucose meter. What I did not know until I went looking is that meters like this speak a published Bluetooth SIG standard called the Glucose Profile. It is not a vendor protocol, not something anybody had to reverse engineer. Service 0x1808. Two notify characteristics for the measurement and its context. And the interesting one, 0x2A52, the Record Access Control Point.
RACP is what turns the meter from a gadget into a queryable store. The meter keeps roughly 800 records, each one stamped with a monotonic sequence number, and you talk to it by writing a command to that characteristic. Write 0x01 0x01 and it means "report all stored records." Write 0x01 0x03 0x01 followed by a little-endian sequence number and it means "report every record with a sequence greater than or equal to this one." The readings then arrive as a burst of notifications, and when the burst is done, RACP itself indicates back to say the query is finished.
That is an incremental sync. On my side I ask the database for MAX(device_seq) for that meter, add one, and ask for everything past it. First sync pulls 800 readings. Every sync after that pulls two or three, in about a second.
The client is a React component. All of it runs in a browser tab over Web Bluetooth, which means the whole pipeline from the meter to Postgres has no native app anywhere in it. There is no meter app to install, no vendor cloud account, no export CSV button to find and click monthly. You press Sync and the tab talks to the device.
Binary parsing at the top of a component
The catch to skipping the vendor app is that you inherit the wire format. Each measurement notification arrives as a DataView, and you walk it by hand: a flags byte, a uint16 sequence, then year, month, day, hour, minute, second as raw fields. After that the layout is conditional. Bit 0 of the flags says whether a time offset follows. Bit 1 says whether there is an actual glucose concentration in the record. Bit 2 tells you the units. If a field is absent, the ones after it slide down, so the parser advances an offset by hand as it goes.
The value itself is an IEEE-11073 SFLOAT: a signed 12-bit mantissa, a signed 4-bit exponent, packed into two bytes, with a few reserved bit patterns that mean NaN rather than a number. Decode that and you get a concentration in kilograms per liter, which is a strange unit to see attached to a finger stick, and you multiply by 100,000 to get the milligrams per deciliter that the meter's own screen showed you.
None of that was the bug. All of it worked on the first try, because the specification is good and it says exactly what each bit means.
The line I got wrong
Here is what I wrote for the timestamp, roughly:
const measured = new Date(year, month - 1, day, hour, minute, second);
That builds a local time. Then, separately, I read the Time Offset field and did nothing much with it.
In the Glucose Profile, the base time in the record is UTC, and the Time Offset is the meter's local timezone at the moment of the reading. The offset is informational. It is there so a clinician can tell that a reading was taken at 6 AM in Denver rather than 6 AM somewhere else, but the instant itself is already absolute.
So a reading taken at 6:12 AM Mountain is stored by the meter as base time 12:12, offset -360. I took the 12:12, declared it local, and got noon. Every reading in my database was shifted by exactly my own UTC offset, which is why the error was so uniform and so easy to misread as a timezone setting somewhere in the app rather than a decode error at the edge.
The fix is one constructor:
const measured = new Date(Date.UTC(year, month - 1, day, hour, minute, second));
What saved the afternoon
The reason this cost an hour instead of a day is a decision I made before there was a bug to find.
Every stored row keeps a raw column holding the parsed fields exactly as the meter sent them: flags, sequence, the six base-time integers, the time offset, the type and location byte. I did not know what I would need it for. I kept it because throwing away the source form of ingested data is the kind of thing you only regret once.
With raw in the table, correcting 800 already-stored readings was an UPDATE that recomputed measured_at from fields I already had. No reconnecting to the meter, no hoping its ring buffer still held the oldest records, no gap. Had I stored only the derived timestamp, those readings would have been unrecoverable, because a wrong local time and a right UTC time are the same six digits and nothing in the row would have told me which one I was looking at.
The other piece was making the write idempotent from the start: an upsert keyed on member, source, and device sequence. A sequence number is the meter's own identity for a reading, so re-syncing the same records can never duplicate them. That turned "sync" into an operation I could run a hundred times while debugging without ever polluting the data I was debugging.
I have a soft rule from doing this kind of work for a living, and this week it earned its keep again. Decoding is where data goes wrong, and it goes wrong quietly, because a bad decode produces a value with the right type, the right shape, and a plausible face. A crash would have been a gift. Instead I got 800 perfectly valid timestamps, all wrong in the same direction, and the only thing that flagged it was knowing that nobody in this house fasts at lunch.

