Cellular Medication Adherence Pillbox

This reference application is intended to provide inspiration and help you get started quickly. It uses specific hardware choices that may not match your own implementation. Focus on the sections most relevant to your use case. If you'd like to discuss your project and whether it's a good fit for Blues, feel free to reach out.
Building with an AI assistant? Point it at this accelerator's firmware — firmware/cellular_medication_adherence_pillbox/ — so it can learn how the device works and help you adapt it to your hardware.
This project is a remote patient monitoring device that catches missed doses before they become clinical events. A Blues Notecard Cell+WiFi riding on a Notecarrier CX wakes every 30 seconds, reads seven snap-action micro-switches inside a standard weekly pillbox, and uploads a cellular event to the Blues Notehub cloud service each time a compartment lid is detected open during a scheduled 30-second poll: no WiFi configuration, no smartphone, and nothing for the patient to set up.
1. Project Overview
The problem. Medication non-adherence is among the most consequential and under-reported drivers of poor outcomes in chronic disease. Patients with diabetes, heart failure, hypertension, or transplant histories who skip or mistime doses cost the healthcare system tens of billions of dollars annually in preventable hospitalizations, and far more in years of life. A clinician or care coordinator who knows a patient hasn't opened their Tuesday compartment by 10 PM can intervene with a phone call; one who only discovers the missed doses at the next clinic visit, six weeks later, is always playing catch-up.
Existing IoT pillboxes have tried to close this gap, and most fail in the same place: the connectivity model. WiFi-dependent devices work fine in young, tech-fluent households. They fail quietly in the homes of elderly patients — who are statistically the highest-risk population — because those homes have locked WiFi routers, forgotten passwords, carrier-grade NAT, and no one around to troubleshoot a dropped connection at 7 AM. Bluetooth-to-smartphone bridges work until the phone is low on battery, out of range, or the patient ignores the pairing prompt. The result is a connected pillbox that isn't connected.
Why cellular-first. The Notecard Cell+WiFi changes the deployment calculus. Cellular connectivity is already provisioned on the device — no router credentials, no smartphone, no IT ticket. The box works the moment it is placed on the patient's nightstand. WiFi remains available as an optional fallback at sites that can offer a nearby access point, but it is never a dependency. For the actual target demographic of chronic-disease patients — many of whom are elderly, live alone, or lack consistent technical support — this is the design decision that makes the device credible as a monitoring tool rather than a gadget. An RPM (remote patient monitoring) device that requires patient-side network administration isn't really a remote monitoring device; it's homework.
New to Blues?
Learn what we do in this 3-minute video that walks you through how we simplify wireless connectivity.
Learn what we do in this 3-minute video that walks you through how we simplify wireless connectivity.
Deployment scenario. The device ships pre-provisioned: the Notecard's ProductUID is flashed at the pharmacy or care coordinator's office, and the patient only needs to plug in a USB charger (or replace the LiPo annually). The clinician views adherence events in Notehub and receives real-time alerts through a downstream route. No app installation, no WiFi onboarding, and no ongoing patient engagement with the device is required.
An important signal limitation. The device reports compartment lid opens,
not confirmed ingestion, and weekly tray-loading sessions by a caregiver or
pharmacist produce events that are operationally indistinguishable from patient
dose-taking opens. A full refill of the seven-day tray can set all seven bits in
daily_opens in a single polling interval. Downstream workflow must account for
this; see §10 for operational mitigations.
2. System Architecture
Device-side responsibilities. On the patient's nightstand the box looks
asleep, and almost always is. Every 30 seconds the Notecarrier CX's STM32 host
wakes for a fraction of a second, reads seven compartment GPIO pins, and
compares them to the snapshot from the previous wake. A rising edge — a lid that
was closed and is now open — turns into a pill_open.qo
Note marked sync:true, telling the Notecard
to push it out the door right now instead of holding it for the next scheduled
outbound window. Between polls the host sleeps in STM32 STOP2 (a few µA) until
the Notecard's ATTN pin wakes it via
card.attn; because the
host resumes in place with its RAM intact, the previous pin values, the daily
bitmask, and the current UTC day all survive each sleep cycle with no external
EEPROM or flash chip needed.
Notecard responsibilities. The Notecard Cell+WiFi (NOTE-MBGLW) handles
everything network-related the patient should never have to think about. It
queues Notes locally, manages its cellular (or opportunistic WiFi) session on
the configured hub.set
periodic outbound cadence, and flushes any sync:true Note immediately the
moment it sees one. On the inbound side it pulls environment
variables
down from Notehub, so a care coordinator can retune the poll interval, the daily
summary hour, or either sync cadence without sending new firmware. Between
sessions it idles at roughly 8–18 µA, effectively invisible on the LiPo budget.
Notehub responsibilities. Every event that leaves the box lands in
Notehub, which ingests, stores, and applies project
routes. pill_open.qo events arrive in near-real time for the clinician's
queue; pill_summary.qo Notes accumulate for adherence trends and reporting.
Fleets group devices by care
coordinator, practice, or patient tier, so per-fleet environment variables apply
across a whole cohort at once.
Routing to the cloud (high level). Notehub supports HTTP, MQTT, AWS, Azure, GCP, Snowflake, and several other destinations; route setup is project-specific. See the Notehub routing docs; this project ships no specific downstream endpoint.
3. Technical Summary
- Notehub: create a Notehub project, copy its ProductUID.
- Wire the bench rig: Notecarrier CX + Notecard MBGLW + 7 snap-action micro-switches on D5, D6, D9–D13, plus a jumper from the Notecarrier CX's ATTN pin to A0 so the Notecard can wake the host. Full pinout in §5.
- Edit one line of
firmware/cellular_medication_adherence_pillbox/cellular_medication_adherence_pillbox.ino: search for#define PRODUCT_UIDand set it to your project's value. - Flash: build for Blues boards → Cygnet with USB support set to None and upload. Full instructions in §7.1.
- Watch — open Notehub → your project → Events tab. You should see a
_session.qowithin a minute and apill_open.qoeach time you open a compartment.
Here is a sample Note this device emits:
{
"compartment": 4,
"label": "WED",
"day_opens_mask": 15,
"opened_this_poll": 8
}4. Hardware Requirements
Compartment sensing — snap-action micro-switches. This design uses subminiature snap-action micro-switches for compartment lid detection. Each switch mounts in the pillbox base with its roller lever positioned to engage the closed lid; when the lid opens the lever releases and the circuit opens. Micro-switches produce a clean mechanical signal that works directly with the STM32 host's internal pull-up resistor, require no magnets, bias supply, or per-compartment conditioning resistors, and tolerate the tens of thousands of actuation cycles typical of a multi-year patient deployment. The Adafruit #819 roller-lever micro-switch is a convenient off-the-shelf choice, with its SPDT body and pre-installed roller lever making lid-contact alignment straightforward on a variety of pill tray geometries.
| Part | Qty | Rationale |
|---|---|---|
| Notecarrier CX | 1 | Integrated carrier with an onboard STM32L433 host; no separate MCU needed. Exposes 7 digital I/O pins (D5, D6, D9–D13) exactly matching the 7-day compartment count; the host sleeps in STOP2 between polls and the Notecard's ATTN pin (jumpered to A0) wakes it. |
| Notecard Cell+WiFi (NOTE-MBGLW) | 1 | Cellular with WiFi fallback. See the NOTE-MBGLW datasheet for radio and power specifications. Cellular removes all patient-side network configuration; cellular-first is the deployment model that serves the target demographic. |
| Blues Mojo | 1 | Coulomb counter for bench-validation of sleep/wake power budget before patient deployment. Not required in production. See §9. |
| Adafruit #819 Micro Switch w/Roller Lever, ×7 | 7 | One switch per compartment lid. Mounts in the pillbox base with the roller lever engaging the closed lid. Wire C to the digital pin and NO to GND; leave NC unconnected. The host's internal pull-up holds the pin HIGH when the lid is open; the closed lid depresses the lever and pulls the pin LOW. |
| Cellular and WiFi u.FL antenna leads (included with NOTE-MBGLW) | 1 set | Both the cellular and WiFi u.FL flex-antenna leads ship inside the NOTE-MBGLW retail box; no separate antenna purchase is required for an indoor pillbox installation. Attach each lead to the matching u.FL connector on the Notecard face and route flat inside the enclosure away from the LiPo. See §4. |
| Adafruit #328 Lithium Ion Polymer Battery, 3.7 V 2500 mAh, JST-PH connector | 1 | Direct plug-in to the Notecarrier CX LIPO JST connector. At the expected power budget (~5–15 mAh/day), a 2500 mAh cell provides multi-month autonomy between charges. |
| EZY DOSE Contoured Weekly Pill Planner (Apothecary Products, part #67790) | 1 | Patient-facing medication organizer: seven compartments with clear lids, one per day of the week. Available at pharmacies and medical supply retailers under the EZY DOSE brand. |
| Hammond 1591SSBK ABS project enclosure, 110 × 82 × 44 mm | 1 | Electronics housing for the Notecarrier CX and LiPo. Mounts as a sidecar alongside or beneath the pill organizer; only the micro-switch wiring leads penetrate the pill tray body. |
All Blues hardware ships with an active SIM including 500 MB of data and 10 years of service: no monthly commitment.
5. Wiring and Assembly
The Notecard Cell+WiFi (NOTE-MBGLW) seats into the Notecarrier CX's M.2 connector and is powered from the same VBAT rail. All host I/O lands on the Notecarrier CX dual 16-pin header.
Micro-switch wiring — all 7 compartments are wired identically:
Each micro-switch is an SPDT device with three terminals: Common (C), Normally Open (NO), and Normally Closed (NC). Connect C to the assigned digital pin and NO to any GND pin on the header; leave NC unconnected. Mount the switch so the closed lid depresses the roller lever, connecting C to NO and pulling the pin LOW. When the lid opens, the lever releases, the C–NO circuit opens, and the host's internal pull-up holds the pin HIGH. No external resistors are needed.
| Compartment | Day Label | Notecarrier CX Pin |
|---|---|---|
| 1 | SUN | D5 |
| 2 | MON | D6 |
| 3 | TUE | D9 |
| 4 | WED | D10 |
| 5 | THU | D11 |
| 6 | FRI | D12 |
| 7 | SAT | D13 |
Pin-by-pin:
- D5, D6, D9, D10, D11, D12, D13 (the seven available GPIOs) → micro-switch C (Common) terminal of each switch. Note that D7 and D8 are not present on the Notecarrier CX header; the usable GPIO range is D5, D6, then D9–D13.
- GND (any GND pin on the header) → micro-switch NO (Normally Open) terminal of each switch.
- NC terminal of each switch → leave unconnected.
- No external resistors required — all pull-ups are internal to the STM32L433 host.
ATTN-to-A0 jumper (host sleep):
Fit a short jumper between the ATTN and A0 pins on the Notecarrier CX
header — both are on the same dual 16-pin header, on opposite rows. Every one of
the host's digital header pins (D5, D6, D9–D13) is already a compartment input,
so this design uses A0 as an ordinary digital input for the wake line
instead of the D5 the Blues sample uses; the firmware sets CX_ATTN_PIN to
A0 before including cx_sleep.h. The Notecard pulls ATTN low while the host
sleeps and raises it when the next poll is due; the rising edge on A0 wakes
the host from STOP2. Leave the Notecarrier CX EN pin unconnected: on the CX
it enables the shared 3.3 V VIO rail (host, Qwiic, 3V3_OUT, and Notecard
I/O), so driving it from ATTN would brown out the whole board rather than
sleep the host. See Putting a Host to Sleep Between Sensor
Readings
for the full pattern.
Switch and actuator placement:
Mount each switch flush in the pill tray base directly beneath the path of the lid, with the roller lever pointing upward toward the lid's travel path. Position the switch so the lid rim or a small actuator nub depresses the roller 0.5–1 mm when the lid is fully closed — enough to produce a clean snap-action transition but not so much that the lid cannot close completely. With the lid fully closed the pin should read ~0 V (LOW); with the lid open it should read ~3.3 V (HIGH from the internal pull-up). Verify with a multimeter or the serial monitor before final assembly.
LiPo battery:
Connect the LiPo's JST-PH plug directly into the Notecarrier CX's onboard LIPO connector. For bench bring-up with Mojo, splice the Mojo inline between the LiPo and the +VBAT header pin instead of using the LIPO JST connector; this lets Mojo measure the entire current draw of the Notecarrier, Notecard, and sensors together.
Antennas:
Attach the cellular and WiFi u.FL antenna leads — both ship in the NOTE-MBGLW retail box (see BOM) — to the corresponding u.FL connectors on the Notecard face. No separate antenna purchase is required. Position the leads flat inside the enclosure away from the LiPo cell.
Physical assembly — enclosure and switch mounting:
Mount the Hammond 1591SSBK enclosure as a sidecar alongside or beneath the pill organizer tray using two-sided foam tape or a 3D-printed bracket; the enclosure and tray travel together as a unit with only the micro-switch wiring leads crossing the enclosure wall.
Drill or punch seven 4–5 mm cable pass-through holes in the enclosure wall facing the pill tray — one per compartment lead pair. Thread each pair through its own hole with a rubber grommet, or apply a bead of hot-melt adhesive around the wire as a strain-relief bead before the lead enters the enclosure. Strain relief prevents the header-pin connection from pulling out if a lead is tugged from the pill-tray side.
Mount each micro-switch body inside the tray base using the switch's mounting holes with two M2 self-tapping screws, or secure it with an adhesive patch or a 3D-printed bracket. Position the switch so the roller lever aligns with the inner face of the lid rim — the lever should be depressed 0.5–1 mm when the lid is fully closed and return freely to its neutral position when the lid is open. Verify with a multimeter before final assembly: the pin should read LOW (≈0 V) with the lid closed and HIGH (≈3.3 V) with the lid open.
Retain the LiPo inside the enclosure with a strip of hook-and-loop tape (Velcro) or a foam-padded retaining clip so the JST connector is not under mechanical strain. Orient the battery's flat face against the enclosure base and route the lead so it cannot be pinched when the enclosure lid is closed.
6. Notehub Setup
-
Create a project. Sign up at notehub.io and create a project. Copy the ProductUID; it looks like
com.your-company.your-name:pillbox. -
Set the ProductUID in firmware. Open
cellular_medication_adherence_pillbox.inoand replace the empty string on the#define PRODUCT_UID ""line with your value. -
Claim the Notecard. Power the assembled unit. On first cellular connection the Notecard associates itself with your Notehub project automatically; no manual claim step required. The device will appear in your project's Devices tab within a minute or two.
-
Create a Fleet per care group. Fleets and Smart Fleets are how Notehub groups devices for shared configuration. A natural grouping is one fleet per care coordinator or prescribing physician, so that environment variables (alert windows, sync cadence) can be tuned once and applied to all their patients' devices simultaneously.
warning
Privacy Note. For an RPM deployment, patient identifiers should never appear in Note body payloads; they travel in plaintext through any downstream route and may be stored in third-party systems. They also should not be placed in Notehub device metadata (tags, serial-number field): Note bodies and device metadata are not designed as stores for protected health information, and any PHI placed there would be outside a controlled covered system. Instead, assign only an opaque, non-PHI deployment identifier (for example, a random UUID or a clinic-assigned device code) to the Notehub device tag or serial-number field, and maintain the patient-to-device mapping exclusively in your downstream covered system, keyed on the Notecard's device UID. If your organization's downstream architecture requires routing PHI through any cloud component, review Blues' contractual and compliance posture with qualified counsel before proceeding.
-
Set environment variables. Navigate to Fleet → Environment in Notehub. All variables below are optional; firmware defaults apply if omitted. The device pulls updates on its next inbound sync (default every 2 hours) — no reflash required.
Variable Default Purpose poll_interval_sec30Seconds between host wakes for compartment polling. Lower values reduce open-detection latency; minimum enforced at 15. summary_hour_utc0UTC hour (0–23) on the day following each UTC day's end at or after which the previous day's adherence summary is queued for transmission. 0means the summary is queued as soon as midnight UTC passes; set to match the care coordinator's morning review time (e.g.,7for 7 AM UTC).outbound_min720Minutes between Notecard outbound syncs. Immediate open events are never delayed by this value; they use sync:true. Changing this value triggers an automatichub.setreapply.inbound_min120Minutes between Notecard inbound syncs (environment variable fetch cadence). Changing this value triggers an automatic hub.setreapply. -
Configure routes. Add one route for
pill_open.qo(to a real-time alert endpoint, SMS, email, nurse-call system, or care-coordination platform) and a second forpill_summary.qo(to a long-term analytics store or dashboard). Separating the two Notefiles at the source means the real-time alert path has no dependency on the analytics path.
What you should see in Notehub
Within a minute of first power-on, the Events tab should start populating. Three event types matter for this project:
-
_session.qo— automatic Notecard housekeeping on each cellular session. The presence of these events is the fastest way to confirm the radio is reaching Notehub. If you see no_session.qoevents in the first 2–3 minutes, check thatPRODUCT_UIDmatches your Notehub project exactly and that cellular coverage is available. -
pill_open.qo— one Note per detected lid-open event. Detection latency is up to 30 seconds (next poll wake); Notecard cellular session establishment then adds roughly 15–60 seconds, for a typical end-to-end window of under two minutes from physical open to Notehub receipt. For this to be reliable, the lid must remain open (or be re-opened) until the next poll fires — a lid opened and re-closed within the same 30-second interval is not detected. Multiple opens of the same compartment lid in one day each produce a separate Note. The body looks like:{ "compartment": 4, "label": "WED", "day_opens_mask": 15, "opened_this_poll": 8 }day_opens_maskis a 7-bit bitmask of every compartment opened so far today (including this one), so each event body is self-contained and the full picture can be reconstructed from a single Note. Bit positions map directly to compartment numbers (bit 0 = compartment 1 = SUN, bit 6 = compartment 7 = SAT).opened_this_pollis the bitmask of compartments detected open in this specific 30-second polling wake — when multiple bits are set, it indicates that several compartments were opened simultaneously (or at least within the same polling interval), which is a strong signal of a weekly tray-refill session rather than a routine single-dose open. Downstream route logic can threshold on the bit-count ofopened_this_pollto distinguish refill clusters from individual dose events; see §10 for the full discussion. -
pill_summary.qo— one Note per UTC day, queued at thesummary_hour_utcthreshold (default midnight UTC) on the day following each data-collection period and then delivered on the next Notecard sync session — either the scheduled outbound sync or any earlier sync triggered by apill_open.qoevent. The body looks like:{ "opens_mask": 31, "opens_count": 5 }opens_maskuses the same bitmask convention aspill_open.qo. Bit 0 = Sunday (compartment 1), bit 6 = Saturday (compartment 7).opens_count = 0means no compartments were opened that day — the patient missed all doses — which thefull:trueflag onnote.addensures is preserved through the template's omitempty suppression. -
pill_diag.qo— an exceptional diagnostic Note emitted on pending-event queue overflow:error: "pending_overflow"and adroppedcount when the 28-entry retry ring buffer fills and the oldest queued open event is evicted. A matchingerror: "pending_overflow_cleared"Note — with the totaldroppedcount for the episode — is emitted when the queue subsequently drains. These Notes appear in both bench and production modes and indicate that adherence data was lost while the Notecard was unable to acceptnote.addrequests. Routepill_diag.qoalongsidepill_open.qoif data-integrity alerting is required.A host that never sleeps (the
ATTN→A0jumper is missing, or the firmware was built with USB support enabled) is not reported through this Notefile: the firmware simply stays awake, waits out the poll interval, and keeps polling, so adherence data is unaffected and Notehub sees a normal_session.qocadence. Detect this fault instead via the Mojo trace (§9) and the[sleep] ATTN sleep failedline on the debug serial output noted in §7.1.
7. Firmware Design
The firmware is split across a main sketch, two helper files, and a host-sleep
header, all living in
firmware/cellular_medication_adherence_pillbox/:
cellular_medication_adherence_pillbox.ino:setup()(one-time bring-up) andloop()(one poll cycle per wake) — the main application logic.cellular_medication_adherence_pillbox_helpers.h: shared constants, struct definitions, and helper function declarations.cellular_medication_adherence_pillbox_helpers.cpp: helper function implementations (sensor reading, Notecard configuration, event emission, host sleep).cx_sleep.h: host sleep — thecard.attnrequest, STOP2 entry, and ATTN wake.
7.1 Installing and flashing
Dependencies:
- Arduino core for STM32 —
stm32duino/Arduino_Core_STM32. Install via the Arduino Boards Manager by adding the index URLhttps://github.com/stm32duino/BoardManagerFiles/raw/main/package_stmicroelectronics_index.jsonunder File → Preferences → Additional Boards Manager URLs, then search for "STM32 MCU based boards" and install. The Notecarrier CX's host uses the Cygnet board definition (see build settings below). Blues Wireless Notecardlibrary (note-arduino). Install via the Arduino Library Manager (search "Blues Wireless Notecard") or runarduino-cli lib install "Blues Wireless Notecard". See note-arduino releases for changelog and any newer stable versions.STM32duino Low Powerlibrary (and its dependencySTM32duino RTC) —stm32duino/STM32LowPower. Provides the STOP2 entry and ATTN-pin wake used incx_sleep.h.
Build settings: the Notecarrier CX's host is the same STM32L433 as the Blues
Cygnet, so the board is Blues boards → Cygnet (pnum=CYGNET), with USB
support (if available) set to None (usb=none). With the USB CDC stack
enabled and no USB host attached (battery power, or the Notecarrier CX USB-C
switch in NC), the STM32's USB wakeup interrupt fires continuously and the
host never stays in STOP2. Debug output goes to the LPUART on the Notecarrier CX
debug jack instead — an ST-LINK V3 exposes it as a virtual COM port; see Serial
Logging With STLINK.
Flashing — Arduino IDE: open cellular_medication_adherence_pillbox.ino,
apply the build settings above, hit Upload. With the Notecarrier CX's USB-C
DIP switch in HST, the host's DFU bootloader is reachable over the USB-C port
(hold BOOT, tap RST); alternatively upload over SWD with an ST-LINK on the
debug jack.
Flashing — arduino-cli: from the firmware directory,
# Install the libraries once (Low Power + RTC provide the STOP2 sleep).
arduino-cli lib install "Blues Wireless Notecard" "STM32duino Low Power" "STM32duino RTC"
# Find the USB port the Notecarrier enumerates as (typically /dev/cu.usbmodem*
# on macOS, COMx on Windows, or /dev/ttyACM* on Linux).
# On macOS: ls -la /dev/cu.* | grep usb
# On Linux: ls -la /dev/ttyACM*
# On Windows: check Device Manager > Ports.
# Then compile + upload (replace the port below with your value). usb=none
# disables the USB CDC stack, which must be off for the host to sleep.
arduino-cli compile -b STMicroelectronics:stm32:Blues:pnum=CYGNET,usb=none cellular_medication_adherence_pillbox.ino
arduino-cli upload -b STMicroelectronics:stm32:Blues:pnum=CYGNET,usb=none -p /dev/cu.usbmodem14201 cellular_medication_adherence_pillbox.inosketch.yaml in the firmware directory sets the same board options, so running
arduino-cli compile there without -b picks them up too.
After upload, open a serial monitor on the ST-LINK virtual COM port at 115200
baud (USB CDC is disabled, so nothing appears on the Notecarrier CX's own
USB-C port). On first boot you should see the Notecard request/response exchange
for hub.set and note.template echoed by the note-arduino debug stream (the
firmware prints [init] ... lines only when one of those steps fails, for
example [init] hub.set failed — will retry next wake), then the device goes
silent as it enters its first 30-second sleep cycle. Once the Notecard has valid
time you will also see one [day] first time-sync utc_day=N line. Opening a
compartment lid on the next wake prints
[open] compartment=N (DAY) day_mask=0bXXXXXXX.
How the host sleeps. sleepHost() calls cxSleepUntilAttn() (in
cx_sleep.h), which asks the Notecard to hold ATTN low for poll_sec
seconds, confirms the pin has gone low, and puts the host into STM32 STOP2; the
rising edge on A0 wakes it when the Notecard raises ATTN. Because the host
stays alive, the card.attn call is a checked request rather than a
fire-and-forget command. If the Notecard does not accept it (for example, it is
still booting after a cold start) or ATTN never goes low (the jumper is
missing), the firmware logs
[sleep] ATTN sleep failed — waiting out the poll interval awake, waits out one
poll interval with delay(), and tries again on the next cycle — the pillbox
keeps working, but the host is drawing active current the whole time.
7.1a Host sleep critical Note
Before deploying to a battery-powered Notecarrier CX, verify that the host is
actually sleeping: the ATTN→A0 jumper is fitted
(§5) and the firmware was built with USB support
set to None. If either is missing, the host stays awake between polls — the
firmware falls back to a software delay, or the STM32's USB wakeup interrupt
pulls it out of STOP2 within a millisecond — and drains the LiPo continuously.
You'll detect this on the bench as a continuous 10–80 mA baseline on the Mojo
trace instead of a near-zero idle floor, and as the [sleep] ATTN sleep failed
line on the debug serial output; Notehub itself looks normal, because the
Notecard keeps syncing on schedule. Never place a unit with a patient until the
Mojo trace shows the host sleeping.
7.2 Modules
| Responsibility | Function |
|---|---|
Notecard configuration (hub.set, accelerometer disable) | initNotecard() |
Note template registration for pill_summary.qo | defineTemplates() |
| Environment variable fetch and clamp | fetchEnvOverrides() |
| Compartment GPIO pin sampling | sampleCompartments() |
| Immediate open-event emission | emitOpenEvent() |
| Daily adherence summary emission | emitDailySummary() |
| UTC time query for day-rollover detection | utcDayAndHour() |
Host sleep (STOP2 until the Notecard raises ATTN) | sleepHost(), cx_sleep.h: cxSleepBegin(), cxSleepUntilAttn() |
| Application state (held in RAM across sleep cycles) | PillboxState |
7.3 Sensor reading strategy
On each 30-second wake, sampleCompartments() reads all seven digital pins into
a single byte bitmask. The firmware XORs this against the prev_pin_mask stored
in PillboxState to compute a newly_opened byte — bits set in newly_opened
represent pins that transitioned from LOW (closed) to HIGH (open) since the last
wake. Every detected rising edge generates a pill_open.qo event — multiple
opens of the same compartment lid in a day each produce a separate Note,
allowing downstream systems to correlate opening patterns against the patient's
dosing schedule. The daily_opens bitmask tracks which compartments were opened
at all today and feeds the end-of-day summary; the day_opens_mask field in
each pill_open.qo body is a running snapshot of that bitmask at the moment of
the event, making each event payload self-contained. A second field,
opened_this_poll, records which compartments were detected open in this
specific wake — when multiple bits are set it is a direct downstream signal that
multiple lids were opened simultaneously, consistent with a weekly tray-refill
session rather than a single dose open.
Important polling limitation. Because the firmware compares pin state at
discrete 30-second boundaries, a lid that is opened and fully re-closed within a
single 30-second interval between wakes produces no rising edge and generates no
event. That open is silently missed and is not counted in daily_opens. For the
typical adherence use case this is not a concern: a patient opening a
compartment to take a pill holds it open for several seconds to several minutes,
well beyond the polling resolution. However, any brief mechanical disturbance
that resolves before the next wake will go undetected. The minimum configurable
poll interval is 15 seconds (poll_interval_sec environment variable,
firmware-enforced floor). If guaranteed sub-second open detection is required,
the design would need interrupt-driven GPIO or a latch-based circuit that
captures and holds the open state until the next poll.
The maximum detection latency from "lid physically opened" to "event queued for transmission" is equal to the configured poll interval (default 30 seconds). Real-world latency from event queuing to Notehub receipt adds the Notecard's cellular session-establishment time (typically 15–60 seconds), giving an end-to-end window of typically under two minutes.
7.4 Event payload design
pill_open.qo is left untemplated (free-form JSON). Open events are
low-frequency even in the every-open model (a typical patient opens each lid
once or twice per day, for a total well under 20 Notes/day), making wire
overhead negligible. The untemplated format keeps each event easy to inspect in
Notehub without adding unnecessary template management overhead. The
opened_this_poll field carries the bitmask of all compartments detected open
in the same polling wake as this event; when multiple bits are set it provides a
downstream signal distinguishing a weekly refill session (multiple lids opened
simultaneously) from a routine single-compartment dose open.
pill_summary.qo is templated with TUINT8 (1-byte unsigned integer) for
both fields. The template registration tells the Notecard to store each daily
summary as a 2-byte fixed-length record rather than a variable-length JSON
string, keeping the on-device queue compact across a full month of data if
cellular connectivity is temporarily unavailable.
// pill_open.qo — untemplated, sync:true, queued on detected open (30-second poll boundary)
{
"compartment": 4,
"label": "WED",
"day_opens_mask": 15,
"opened_this_poll": 8
}
// pill_summary.qo — templated, queued at summary_hour_utc, delivered on next outbound sync
{
"opens_mask": 63,
"opens_count": 6
}The full:true flag on the daily summary's note.add request preserves
opens_count: 0 even though the Note template would normally suppress
zero-valued fields. Knowing that a patient opened zero compartments in a day is
exactly the signal a care coordinator needs.
7.5 Low-power strategy
The Notecarrier CX's STM32L433 host sleeps in STOP2 between wakes, drawing a few microamps with its RAM intact. This means every 30-second cycle consists of ~100–200 milliseconds of active host execution followed by ~29.8 seconds at ~1–2 µA of host draw.
setup() runs once at power-up; each pass through loop() is one poll cycle.
After each cycle, sleepHost() calls cxSleepUntilAttn() (in cx_sleep.h),
which sends a
card.attn sleep
request for poll_sec seconds, waits for the Notecard to pull ATTN low, and
enters STOP2. When the interval elapses the Notecard raises ATTN; the rising
edge on A0 wakes the host, and execution resumes on the next line of loop().
Because the host resumes in place rather than cold-booting, the PillboxState
struct — previous pin mask, daily bitmask, UTC day, pending-event queue — simply
lives in RAM; nothing is serialized to the Notecard. From the firmware author's
perspective, the sleep call is a single function invocation; from the patient's
perspective, the device is simply always on.
The Notecard itself remains powered continuously and idles at approximately 8–18
µA between cellular sessions. Cellular sessions — triggered by sync:true open
events or the scheduled outbound timer for queued summary Notes — draw several
hundred milliamps for the duration of the transmission (typically under 30
seconds) and then return to idle.
7.6 Retry and error handling
- The first
hub.setcall on cold boot usessendRequestWithRetry(req, 5)to handle the known cold-boot I2C race condition where the host comes up before the Notecard is ready to receive transactions. fetchEnvOverrides()usesrequestAndResponse()and guards against a NULL response; a failed env fetch leaves the current state values unchanged rather than crashing or zeroing thresholds.- If
utcDayAndHour()returns 0 (Notecard hasn't yet synced to get a valid time), the day-rollover branch is skipped entirely. Any opens that occurred before time-sync remain indaily_opensand are associated with the first valid UTC day once time becomes available; they are only moved intoprev_day_opensat the first actual day rollover, at which point they feed the subsequent end-of-day summary. - Because the host resumes in place after each sleep, there is no state-restore
step that can fail. If the LiPo dies, the host cold-boots into
setup(), which zeroesPillboxState, re-reads the initial pin state, and reconfigures the Notecard — the same first-boot path as a fresh power-on. - If
cxSleepUntilAttn()cannot put the host to sleep (the Notecard is still booting after a cold start, orATTNnever goes low because the jumper is missing),sleepHost()waits out the poll interval awake and tries again on the next cycle, so the polling cadence is preserved either way. emitOpenEvent()failure and retry queue. When anote.addfails after all three attempts,enqueuePendingEvent()stores the event (compartment index, day mask, poll mask) in a 28-entry ring buffer held insidePillboxState. On each subsequent wake,replayPendingOpenEvents()retries every buffered event before sampling new opens; successfully replayed records are removed and the queue is compacted. If the queue fills before Notecard connectivity is restored, the oldest entry is evicted and apill_diag.qoNote is immediately sent to Notehub witherror: "pending_overflow"and adropped: 1count — giving cloud-visible data-loss visibility even while the primary note-add path is degraded. When the queue fully drains, a secondpill_diag.qowitherror: "pending_overflow_cleared"and the cumulative drop count closes the episode and confirms how manypill_open.qoevents are missing. The 28-slot capacity covers four consecutive worst-case 7-compartment wakes; a sustained Notecard failure beyond that window causes adherence event loss. See §10.
7.7 Key code snippet 1 — template definition
TUINT8 is defined in note-c as the integer constant 21, which tells the
Notecard to encode each templated field as a 1-byte unsigned integer (0–255).
The template registers this encoding with the Notecard's on-device compression
layer, so every pill_summary.qo Note is stored as a fixed 2-byte binary record
rather than variable-length JSON. This compression keeps the on-device queue
compact across a full month of data if cellular connectivity is temporarily
unavailable. For downstream integrations, the Notecard automatically
decompresses these records back to JSON when they reach Notehub.
J *req = notecard.newRequest("note.template");
JAddStringToObject(req, "file", "pill_summary.qo");
JAddNumberToObject(req, "port", 50);
J *body = JAddObjectToObject(req, "body");
JAddNumberToObject(body, "opens_mask", TUINT8); // 7-bit bitmask, 0–127
JAddNumberToObject(body, "opens_count", TUINT8); // 0–7 count
notecard.sendRequest(req);7.8 Key code snippet 2 — immediate open event
sync:true tells the Notecard not to wait for the next outbound window; this
Note jumps the queue and the radio wakes immediately.
J *req = notecard.newRequest("note.add");
JAddStringToObject(req, "file", "pill_open.qo");
JAddBoolToObject (req, "sync", true); // immediate transmission
J *body = JAddObjectToObject(req, "body");
JAddNumberToObject(body, "compartment", 4); // 1–7
JAddStringToObject(body, "label", "WED");
JAddNumberToObject(body, "day_opens_mask", 15); // running daily bitmask
JAddNumberToObject(body, "opened_this_poll", 8); // per-poll multi-open bitmask
notecard.sendRequest(req);7.9 Key code snippet 3 — sleeping until the Notecard raises ATTN
cxSleepUntilAttn() asks the Notecard to hold ATTN low for poll_sec
seconds, confirms the pin has gone low, and puts the STM32 into STOP2. The host
stays alive, so the card.attn call is a request whose result is checked (with
retries, to cover the Notecard still booting after a cold start) rather than a
fire-and-forget command. The Notecard's ATTN pin brings the host back on
schedule and loop() continues with the next poll; PillboxState is still in
RAM.
// In the .ino — all seven D pins are compartment inputs, so ATTN is on A0
#define CX_ATTN_PIN A0
#include "cx_sleep.h"
// In setup() — arm the ATTN wake once
cxSleepBegin();
// In sleepHost(), called at the end of every loop() pass
if (!cxSleepUntilAttn(notecard, s.poll_sec, NULL, &usbSerial)) {
// Notecard not ready, or ATTN never went low (check the ATTN -> A0 jumper).
// Keep the poll cadence and try again next cycle.
delay(s.poll_sec * 1000UL);
}8. Data Flow
Collected. Every 30 seconds: a 7-bit bitmask of compartment lid states, compared against the previous sample. The firmware does not transmit on every wake, only on a state transition.
Transmitted.
pill_open.qo— one Note per detected lid-open event (lid must be open when the 30-second poll fires), immediate (sync:true). Contains the compartment number (1–7), its day label, the running daily bitmask of all compartments opened so far today (day_opens_mask), and a per-poll bitmask of all compartments detected open in this specific wake (opened_this_poll). Multiple bits set inopened_this_pollindicate a likely refill session. Multiple opens of the same compartment each generate a separate Note, provided each open is present at a poll boundary.pill_summary.qo— one Note per UTC day, template-encoded. Queued at the configuredsummary_hour_utcon the day following each data-collection period (default: midnight UTC) and then delivered on the next Notecard sync session — either the scheduled outbound sync or any earlier sync triggered by apill_open.qoevent. Containsopens_mask(which compartments opened) andopens_count(how many). Usesfull:trueto preserve a zero count when the patient opened no compartments.
Routed. Both Notefiles go to Notehub and from there to whatever downstream
the project's routes specify. The two filenames are deliberately separate so
pill_open.qo can fan out to a real-time alert channel (SMS, pager, care
platform webhook) while pill_summary.qo goes to a long-term store at a
different cadence.
Alerts trigger on.
- Any
pill_open.qoevent — a compartment was opened. Route this to whatever real-time channel the care coordinator uses. Note that weekly tray-refill sessions produce events identical to patient dose-taking opens; see §10 for how to distinguish them operationally. - Absence of expected
pill_open.qoby a configurable time window: the patient hasn't opened their morning compartment. This logic lives in the downstream route or dashboard, not in firmware. pill_summary.qowithopens_count: 0: the patient missed all doses for the day.
9. Validation and Testing
Expected cadence after deployment. In steady state, a patient who takes
medication once per day generates one pill_open.qo event per day and one
pill_summary.qo per day. A correctly behaving device with no lids opened
should still send a daily pill_summary.qo with opens_count: 0.
Bench functional test. The firmware fires an open event on a LOW→HIGH rising edge — when the pin transitions from LOW (actuator pressed, switch closed, lid closed) to HIGH (actuator released, switch open, lid open). A common mistake is holding the switch actuator depressed and expecting an open event; that simulates a closed lid, not an open one. The correct procedure:
- Establish a closed baseline. Press the switch actuator (or install the
switch so the lid depresses it) so the pin reads LOW. Let at least one
30-second poll fire while the actuator is held closed — this records the LOW
state in
prev_pin_maskfor that compartment. A faster shortcut: power the device on with the actuator already pressed. The first-boot code snapshots the current pin state as the baseline, so the LOW is captured immediately without waiting for a second poll. - Simulate a lid open. Release the actuator (or open the lid) and leave it
released until the next poll fires. The pin rises to HIGH, creating the
LOW→HIGH transition the firmware detects as a lid-open event. The serial
monitor should print
[open] compartment=N (DAY) day_mask=0bXXXXXXX. - Verify in Notehub. Confirm the corresponding
pill_open.qoevent appears in the Events tab within roughly two minutes (up to 30-second poll latency + cellular session establishment).
If the actuator is pressed and released before the next poll fires, no event is generated; both transitions resolve within the same interval and the firmware observes no net change. This is expected behavior, not a fault.
Quick continuity check (no Notehub required). Use a multimeter on the pin header or watch serial output while manually pressing and releasing the switch actuator: pressing it should pull the pin to ~0 V; releasing it should let the pin float to ~3.3 V (internal pull-up active). If the result is reversed, check for these common causes: wrong header pin, NO and NC terminals swapped, missing ground connection, actuator not engaging the lid properly, or internal pull-up not active — verify against the wiring table in §4.
The daily summary is queued on the first wake after UTC midnight (or after the
configured summary_hour_utc has passed on the new day) and arrives in Notehub
on the next Notecard sync session — either the next scheduled outbound sync
(default every 12 hours) or sooner if a pill_open.qo event triggers an
immediate sync that day.
Power validation with Mojo. Splice the Mojo inline between the LiPo and the Notecarrier CX +VBAT pin and run the device for 24 hours on the bench.
Notecard-datasheet figures (from the NOTE-MBGLW datasheet and the low-power design guide):
| Phase | Published figure |
|---|---|
| Notecard idle (radio off, between sessions) | ~8–18 µA @ 5V |
| Cellular session (modem active) | ~250 mA average, brief peaks up to ~2 A |
Modeled example budget (estimated; validate with Mojo on your specific bench assembly before committing to a deployment charge schedule):
| Phase | Estimated draw |
|---|---|
| Host MCU active (~150 milliseconds per 30 seconds wake) | ~10 mA for ~150 milliseconds every 30 seconds (~50 µA average contribution) |
Cellular session — open-event sync (sync:true, ~2–3 per day) | ~250 mA average, ~20–30 seconds per session |
| Cellular session — scheduled outbound sync (every 12 h) | ~250 mA average, ~15–20 seconds per session |
At 30-second polling with the default 12-hour outbound sync and roughly 2–3 open events per day triggering immediate syncs, the modeled daily energy total is on the order of 5–15 mAh/day; treat this as an estimated starting point until confirmed with a bench trace.
Quick runway estimate: With a 2500 mAh LiPo and 5–15 mAh/day, expect 5–15 months of continuous operation between charges (165–500 days). In a real patient deployment with variable cellular signal and temperature swings, always validate with a 24-hour Mojo trace before setting a charge schedule. Poor cellular signal or frequent refill sessions will increase daily consumption.
A good Mojo trace on this device looks like: an essentially flat near-zero
baseline punctuated by brief millisecond blips every 30 seconds (host wake),
with occasional 15–30-second bursts at hundreds of milliamps (cellular session).
If the baseline is continuously 10–80 mA, the host is not entering STOP2 — check
the ATTN→A0 jumper and confirm the firmware was built with USB support set
to None (usb=none); between wakes the trace should sit at the ~10 µA idle
floor (Notecard ~8 µA plus the host's ~1–2 µA in STOP2). If cellular bursts
occur much more frequently than expected, verify sync:true is only set on open
events and that the outbound cadence is not set to a very low value.
With a 2500 mAh LiPo and an estimated 5–15 mAh/day, modeled runtime is roughly 165–500 days between charges. Actual runtime will vary with cellular signal strength, session frequency, and temperature; confirm with bench data before committing to a deployment charge schedule.
Mojo is a bench and commissioning tool for this project; it is not deployed to the patient's home. Once a firmware revision passes the trace check, the deployed units run on LiPo without it.
Troubleshooting
| Symptom | Likely cause | What to check |
|---|---|---|
| Device never appears in Notehub Devices tab. | PRODUCT_UID is empty or wrong, or the cellular antenna is disconnected. | Verify PRODUCT_UID matches the Notehub project exactly. Check that antennas are attached and not coiled tightly against the LiPo. Move the device near a window if testing indoors. |
Device appears but no pill_open.qo events show up after opening a lid. | Micro-switch mounting or wiring issue; pin wired to wrong header position. | Open the serial monitor at 115200 baud and watch for [open] lines. If none appear, confirm pin continuity with a multimeter: pin should read ~3.3 V with the lid open and ~0 V with the lid closed. |
pill_open.qo fires on startup without any lid being opened. | Likely a wiring fault floating the pin high, or a lid that physically moved between the power-on pin snapshot and the following 30-second wake. | Check for loose wiring or wrong terminal (NC connected instead of NO) on the affected compartment pin. If the problem recurs after verifying wiring, force a clean start by disconnecting the LiPo for ~30 seconds. On the next power-on setup() runs again, zeroes PillboxState, and re-snapshots the current pin state as the baseline. |
Daily summary (pill_summary.qo) never appears. | UTC time not yet available (utcDayAndHour() returning 0), or summary_hour_utc hasn't been reached yet on the new day. | Confirm the device has had at least one successful cellular session (_session.qo present in Notehub). The summary is queued on the first wake after UTC midnight at or after summary_hour_utc, then delivered on the next Notecard sync session — the scheduled outbound sync (default every 12 hours) or sooner if a pill_open.qo event triggers an earlier sync. |
opens_count in the daily summary is missing (field absent from the Notehub event). | full:true was not applied. | Verify the firmware calls note.add with JAddBoolToObject(req, "full", true) before sending the summary. Without full:true, the template's omitempty behavior suppresses the zero value. |
| Environment variable changes don't take effect. | Inbound sync hasn't occurred yet (default every 2 hours). | Use Notehub's Sync Now (inbound) button on the device page to trigger an immediate inbound sync. Alternatively, lower inbound_min in the fleet environment to 15; the device re-applies hub.set automatically once it picks up the change, tightening the fetch cadence for subsequent updates. |
Mojo trace shows a continuous 10–80 mA baseline instead of near-zero idle, and the debug serial prints [sleep] ATTN sleep failed — waiting out the poll interval awake every cycle. | The ATTN→A0 jumper is missing or on the wrong pin, so ATTN never reaches the host; sleepHost() falls back to a software delay, which keeps the host awake. | Fit the jumper between the Notecarrier CX's ATTN and A0 header pins (see §5) and leave EN unconnected. If adapting this design to a different carrier, wire ATTN to any interrupt-capable host GPIO and change CX_ATTN_PIN in the .ino. |
Mojo trace shows a continuous 10–80 mA baseline, but the debug serial shows no [sleep] warnings — each cycle looks normal. | The firmware was built with USB support enabled: with the USB CDC stack running and no USB host attached, the STM32's USB wakeup interrupt pulls the host out of STOP2 within a millisecond. | Rebuild with USB support (if available) set to None (usb=none, see §7.1) and reflash. Notehub looks normal in this state — the Notecard keeps syncing on schedule — so the Mojo trace is the only reliable indicator. |
pill_diag.qo appears with error: "pending_overflow" or "pending_overflow_cleared". | The 28-entry retry ring buffer filled while the Notecard was unable to accept note.add requests; the oldest buffered adherence events were evicted and lost. | Check for gaps in _session.qo events indicating a connectivity outage. The dropped field on the pending_overflow Note shows how many pill_open.qo events are missing. Once the Notecard recovers, the queue drains automatically and a pending_overflow_cleared Note confirms the episode is closed. Mark the affected interval in your downstream adherence record as incomplete. |
If a problem isn't on this list, the Blues community forum is the fastest place to get a second pair of eyes on a Notecard and sensor setup.
10. Limitations and Next Steps
The design deliberately stops at "did a lid open?", the most reliable signal that survives the realities of an elderly patient's home network, a caregiver's weekly refill routine, and a Bluetooth-fatigued smartphone. Confirmed ingestion, AM/PM trays, and local reminder UX are real product features, but they belong in a follow-on design rather than diluting the cellular-first sensor this POC is proving out.
Simplified for the POC
The simplifications below are deliberate scope choices: each is a place where a production deployment will add a sensor, a downstream workflow, or a tighter data-handling control once a real RPM program starts running it. This is a proof-of-concept reference design, not a medical device.
Cannot confirm a dose was actually taken. The device reports each detected compartment lid open, but it cannot distinguish "opened and took the pill" from "opened and closed without taking it." Confirming a dose requires a weight sensor on the tray or patient self-report; both are straightforward extensions but add hardware or UX complexity outside the scope of this POC.
Caregiver and pharmacist refill sessions are indistinguishable from patient
dose-taking opens. When the weekly tray is loaded — whether by the patient, a
caregiver, or a pharmacist — opening each compartment lid to place pills
generates pill_open.qo events and sets bitmask bits that are identical to
genuine dose-taking opens. A single loading session can set all seven bits in
daily_opens and produce up to seven events within one or two polling
intervals, creating a false picture of perfect adherence for that day. This
is a more active distortion than the dose-confirmation limitation above: it
inflates the adherence record rather than leaving it ambiguous. Downstream
systems and clinical workflows must account for this in at least one of the
following ways:
- Fill outside the monitored window. If the tray is loaded at a predictable time (e.g., Sunday evening before the monitoring week begins), configure downstream routes to flag or suppress clustered multi-lid opens during that window.
- Use a clinician-side refill flag. The care coordinator logs refill sessions in the downstream covered system; the patient-record view excludes or tags those event clusters from adherence calculations.
- Filter simultaneous multi-lid opens downstream. Multiple bits set in a
single 30-second poll (
newly_openedcontaining ≥ 3 compartments at once) is a strong operational signal of a refill rather than individual dose-taking. A downstream route rule or dashboard filter can flag clusters above a configurable threshold for manual review rather than counting them as adherence events.
Brief opens between polls are missed. The firmware detects lid-open events
by comparing pin state at each poll boundary (default every 30 seconds). A lid
that is opened and fully re-closed within a single poll interval generates no
event and is not counted in the daily summary. For the intended use case — a
patient opening a compartment to take a pill — the lid will naturally remain
open long enough to be detected. Caregiver testing, brief accidental knocks, or
other sub-30-second interactions will not be recorded. Reducing
poll_interval_sec to the firmware-enforced minimum of 15 seconds halves the
exposure window; interrupt-driven or latch-based hardware would eliminate it
entirely.
Pending retry queue has a finite depth. If the Notecard is unable to accept
note.add requests across more than four consecutive worst-case polling wakes
(28 buffered events), the oldest queued dose-open records are evicted and
permanently lost. The device emits a pill_diag.qo Note with
error: "pending_overflow" and a dropped count when overflow first occurs,
and a matching error: "pending_overflow_cleared" Note when the queue drains,
providing cloud-visible evidence of data loss. A Notecard outage long enough to
exhaust the buffer is uncommon on a battery-backed Notecard Cell+WiFi, but in
the worst case downstream adherence calculations will undercount dose-opens for
the affected interval without an explicit correction signal. Route
pill_diag.qo to your alert channel alongside pill_open.qo so these episodes
are not missed.
7-compartment design only. The Notecarrier CX exposes exactly seven digital I/O pins (D5, D6, D9–D13), which maps cleanly to a standard 7-day tray. A 14-compartment tray (AM/PM per day) would require an I2C GPIO expander such as the MCP23017, adding one part and a library dependency.
No on-device time zone awareness. The daily summary rolls over at UTC
midnight. In practice, a care coordinator should set summary_hour_utc to match
the patient's local midnight (e.g., 5 for Eastern Standard Time), which is
tunable via environment variable without a reflash.
Micro-switch placement is manual. Mounting the micro-switches inside a commercial pillbox requires drilling, adhesive, or 3D-printed brackets. A production design would integrate the switches into a purpose-built tray PCB.
No tamper or battery-low reporting. The device does not currently alert if
someone removes the Notecard, cuts power, or if the LiPo voltage drops below a
safe threshold. Battery voltage monitoring via card.voltage and a sync:true
low-battery event would be a straightforward addition.
No local visual or audible reminder. The device is purely a reporting platform; it does not buzz or light up to remind the patient to take their medication. Adding a piezo buzzer on a PWM-capable pin would require a scheduled inbound event from Notehub to trigger it.
Pre-time-sync opens carry no calendar date. On first boot, the device may
accumulate opens before the Notecard acquires valid UTC time. Those opens remain
in daily_opens and are associated with the first valid UTC day once time
becomes available, then move into prev_day_opens at the next actual day
rollover to feed the subsequent summary, with the correct bitmask. However, the
summary's timestamp reflects when it was sent, not the (unknown) calendar day
when the lids were opened. If accurate calendar-day attribution of pre-sync
opens is required, wait for a _session.qo event in Notehub (confirming
time-sync) before placing the device with the patient.
Firmware state holds only one pending day's summary data. The PillboxState
struct contains a single prev_day_opens slot. If two consecutive UTC-day
boundaries pass while a pending summary has not yet been emitted (for example,
because the device is powered off spanning an entire day, or because a total
power loss resets the host and clears the RAM-resident PillboxState), the slot
is overwritten and the earlier day's adherence data is unrecoverable. Note that
a lack of cellular connectivity alone does not cause this: the Notecard stores
queued Notes locally in its on-device flash and delivers them automatically once
connectivity is restored. The risk is power loss before emitDailySummary()
executes. Size the LiPo for the intended deployment duration to minimize
exposure.
Patient identity must stay out of Note payloads and Notehub metadata. Note bodies are stored and routed as plaintext through Notehub and any downstream systems. Notehub device metadata (tags, serial-number field) is not designed as a covered system for protected health information. For an RPM deployment, never embed a patient name, MRN, date of birth, or any other PHI in a Note body or in Notehub device metadata. Assign only an opaque, non-PHI deployment identifier (for example, a random UUID or clinic-assigned device code) to the Notehub device tag or serial-number field. Maintain the mapping from device UID to patient record exclusively in your downstream covered system. If your deployment architecture requires routing PHI through any cloud component, engage qualified counsel to review the complete data path before go-live.
Production Next Steps
Once a real RPM program is running the basic monitor, the following extensions are the natural progression toward a fieldable product.
14-slot tray support via an MCP23017 I2C GPIO expander (adds AM/PM per day); the firmware's bitmask and event structure extend naturally to 16 bits.
Opaque deployment-ID assignment (a random UUID or clinic-assigned device code) in the Notehub device tag or serial-number field so downstream routes can correlate events to the correct caregiver via a patient-record lookup in the downstream covered system, keyed on device UID.
Voltage-variable sync behavior via hub.set voutbound, automatically
extending the outbound cadence as the LiPo voltage drops, maximizing runtime
without manual intervention.
Notecard Outboard DFU for over-the-air host firmware updates so new features (buzzer support, new alert rules) can be pushed to deployed devices without a pharmacy recall.
A scheduled inbound Notefile (pill_reminder.qi) that the Notecard can
trigger a local buzzer or LED on, turning the device from a passive sensor into
an active reminder system.
11. Summary
The care coordinator now has the one signal that matters: a pill_open.qo event
when a lid opens, and a clean opens_count: 0 at the end of any day the patient
missed entirely. The box arrives on the patient's nightstand pre-provisioned,
asks nothing of them, and reports over cellular whether the household's WiFi
exists, works, or has been forgotten years ago. For the elderly chronic-disease
patients who need this monitoring most, that's the difference between a
connected device and a device that stays connected.
On This Page
- 1. Project Overview
- 2. System Architecture
- 3. Technical Summary
- 4. Hardware Requirements
- 5. Wiring and Assembly
- 6. Notehub Setup
- 7. Firmware Design
- 7.1 Installing and flashing
- 7.1a Host sleep critical Note
- 7.2 Modules
- 7.3 Sensor reading strategy
- 7.4 Event payload design
- 7.5 Low-power strategy
- 7.6 Retry and error handling
- 7.7 Key code snippet 1 — template definition
- 7.8 Key code snippet 2 — immediate open event
- 7.9 Key code snippet 3 — sleeping until the Notecard raises ATTN
- 8. Data Flow
- 9. Validation and Testing
- 10. Limitations and Next Steps
- 11. Summary