Use Your AI Agent to Talk to Your Products with Notehub IQ

Blues Developers
What’s New
Resources
API Reference
Notecard and Notehub API documentation
Blog
Technical articles for developers
Connected Product Guidebook
In-depth guides for connected product development
Developer Certification
Get certified on wireless connectivity with Blues
Newsletter
The monthly Blues developer newsletter
Terminal
Connect to a Notecard in your browser
Webinars
Listing of Blues technical webinars
Blues.comNotehub
Shop
Docs
Help
Support DocsNotehub StatusSecurity AdvisoriesVisit our Forum
Sign In
Docs Home
What’s New
Resources
API Reference
Notecard and Notehub API documentation
Blog
Technical articles for developers
Connected Product Guidebook
In-depth guides for connected product development
Developer Certification
Get certified on wireless connectivity with Blues
Newsletter
The monthly Blues developer newsletter
Terminal
Connect to a Notecard in your browser
Webinars
Listing of Blues technical webinars
Blues.comNotehub
Shop
Docs
Example Apps
Samples
Building a Simple Asset Tracker
Building Edge AI Applications
Continuous Asset Tracking with External GPS and Immediate Location Sync
Controlling Device State and Receiving Acknowledgment
Managing a Notecard's WiFi Network Remotely
Putting a Host to Sleep Between Sensor Readings
Routing Data from Notehub to a Custom Cloud Endpoint
Using LEDs and NeoPixels to Monitor Notecard Status
Accelerators
Asset Location Tracking
Heavy Equipment Hours-of-Use & Utilization Tracker
Returnable Container / Tote Pool Tracker
Untethered Trailer and Chassis Fleet Tracker
Asset Performance Optimization
Legacy Diesel Generator Fleet Performance Uplift
Solar Array String-Level Performance Dashboard
Battery Management Systems
Aerial Lift / Rental Equipment Battery Health Monitor
Off-Grid Solar Battery Site Controller
Remote Cabinet Backup Battery Sentinel
Downtime Prevention
Municipal Wastewater Lift Station Monitor
Rooftop HVAC Predictive Maintenance
VFD Pump Predictive Maintenance
Energy Monitoring Solutions
Commercial Tenant Sub-Metering Bridge
EV Charger Session & Utilization Monitor
Utility Distribution Transformer Load Monitor
Energy Savings
Commercial Plug-Load & After-Hours Waste Dashboard
Demand-Response Solar + Battery Dispatcher
Multi-Site Walk-In Cooler Energy & Setpoint Monitor
Industrial Equipment Monitoring
CNC Machine Spindle Load and Cycle Time Tracker
Injection Molding Shot-to-Shot Process Monitor
Trailer Manufacturer Connected Trailer Platform
Loss Prevention
Construction Equipment Anti-Theft Tracker with Immobilizer
Reefer Trailer Cold-Chain & Door-Event Monitor
Remote Patient Monitoring
Cellular Medication Adherence Pillbox
Post-Discharge Vitals Relay Hub
Remotely Monitored Assets
Off-Grid Livestock Water Tank Monitor
Remote Apiary Hive Health Monitor
Safety Assurance
Construction Site Environmental & Noise Exposure Monitor
Lone Worker Panic and Fall Detection Beacon
Regulatory-Grade Pharmacy and Lab Cold Storage Audit Monitor
Supply Chain Tracking
Pallet-Attached Cold Chain Logger
Rail Car Condition & Interchange Tracker
Truck Roll Reduction
Commercial Grease Interceptor Level Monitor
Propane / LPG Tank Fill Telemetry
homechevron_rightExample Appschevron_rightAcceleratorschevron_rightCellular Medication Adherence Pillbox

Cellular Medication Adherence Pillbox

Cellular Medication Adherence Pillbox banner
note

This reference application is here to give you ideas and help you get started quickly. It's built on specific hardware that may not match yours, so focus on the sections that fit your use case. If you'd like to talk through your project and whether it's a good fit for Blues, reach out.

Building with an AI assistant? Point it at this accelerator's firmware in 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 turn into clinical events. A Notecarrier CX with a Blues Notecard Cell+WiFi wakes every 30 seconds and reads seven snap-action micro-switches inside a standard weekly pillbox. Each time a scheduled 30-second poll detects an open compartment lid, the device uploads a cellular event to the Blues Notehub cloud service. The patient has nothing to set up, and the device needs neither WiFi configuration nor a smartphone.

1. Project Overview

The problem. Medication non-adherence is one of the most consequential and under-reported causes of poor outcomes in chronic disease. Patients with diabetes, heart failure, hypertension, or a transplant history who skip or mistime doses cost the healthcare system tens of billions of dollars a year 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 pick up the phone and intervene. One who only finds out about 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 of them fail in the same place, which is connectivity. A WiFi-dependent device works fine in a young, tech-fluent household. It fails quietly in the home of an elderly patient, and elderly patients are statistically the highest-risk population. Those homes have locked WiFi routers, forgotten passwords, carrier-grade NAT, and nobody around to troubleshoot a dropped connection at 7 AM. A Bluetooth-to-smartphone bridge works until the phone's battery runs low, the phone goes out of range, or the patient ignores the pairing prompt, and then the connected pillbox isn't connected anymore.

Why cellular-first. Notecard Cell+WiFi comes with cellular connectivity already provisioned, so nobody needs router credentials, a smartphone, or an IT ticket. The box works as soon as it's on the patient's nightstand. WiFi is still there as an optional fallback at sites that can offer a nearby access point, but the device never depends on it. Many of the chronic-disease patients this device is meant for are elderly, live alone, or don't have consistent technical support. For them, cellular-first is the design decision that makes the device credible as a monitoring tool instead of a gadget. An RPM (remote patient monitoring) device that makes the patient administer a home network turns remote monitoring into 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, because the Notecard's ProductUID is flashed at the pharmacy or the care coordinator's office. All the patient has to do is plug in a USB charger (or replace the LiPo once a year). Adherence events show up for the clinician in Notehub, and real-time alerts reach them through a downstream route. Nobody asks the patient to install an app, go through WiFi onboarding, or keep engaging with the device.

An important signal limitation. The device reports compartment lid opens, not confirmed ingestion. Weekly tray loading by a caregiver or pharmacist also produces events that are operationally indistinguishable from a patient opening a lid to take a dose. A full refill of the seven-day tray can set all seven bits in daily_opens in a single polling interval. Your downstream workflow must account for this, and §10 covers the operational mitigations.

2. System Architecture

System architecture

Device-side responsibilities. On the patient's nightstand the box looks asleep, and it 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 with the snapshot from its previous wake. A rising edge means a lid that was closed is now open. The host turns it into a pill_open.qo Note marked sync:true, which tells the Notecard to send it right away 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 through card.attn. Since 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 without an external EEPROM or flash chip.

Notecard responsibilities. The Notecard Cell+WiFi (NOTE-MBGLW) handles all the networking the patient should never have to think about. It queues Notes locally and manages its cellular (or opportunistic WiFi) session on the periodic outbound cadence configured with hub.set. Any sync:true Note goes out the moment the Notecard sees it. 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, which barely registers on the LiPo budget.

Notehub responsibilities. Every event that leaves the box lands in Notehub, which ingests and stores it and applies the project's routes. pill_open.qo events reach the clinician's queue in near-real time, while pill_summary.qo Notes build up for adherence trends and reporting. Fleets group devices by care coordinator, practice, or patient tier, so a per-fleet environment variable applies to a whole cohort at once.

Routing to the cloud (high level). Notehub can route to HTTP, MQTT, AWS, Azure, GCP, Snowflake, and several other destinations. Route setup depends on your project, and this one doesn't ship a specific downstream endpoint. See the Notehub routing docs for details.

3. Technical Summary

  1. Notehub: create a Notehub project and copy its ProductUID.
  2. Wire the bench rig: a Notecarrier CX, a Notecard MBGLW, and 7 snap-action micro-switches on D5, D6, and D9–D13, plus a jumper from the Notecarrier CX's ATTN pin to A0 so the Notecard can wake the host. The full pinout is in §5.
  3. Edit one line of firmware/cellular_medication_adherence_pillbox/cellular_medication_adherence_pillbox.ino. Search for #define PRODUCT_UID and set it to your project's value.
  4. Flash: build for Blues boards → Cygnet with USB support set to None, then upload. Full instructions are in §7.1.
  5. Watch: open Notehub → your project → Events tab. You should see a _session.qo within a minute, and a pill_open.qo each 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 with snap-action micro-switches. This design detects compartment lids with subminiature snap-action micro-switches. 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. A micro-switch gives a clean mechanical signal that works directly with the STM32 host's internal pull-up resistor. It doesn't need magnets, a bias supply, or conditioning resistors on each compartment, and it holds up to the tens of thousands of actuation cycles a multi-year patient deployment typically puts on it. The Adafruit #819 roller-lever micro-switch is a convenient off-the-shelf choice. Its SPDT body and pre-installed roller lever make it easy to line up with the lid on a variety of pill tray geometries.

PartQtyRationale
Notecarrier CX1Integrated carrier with an onboard STM32L433 host, so you don't need a separate MCU. Its 7 digital I/O pins (D5, D6, D9–D13) match the 7-day compartment count exactly. The host sleeps in STOP2 between polls, and the Notecard's ATTN pin (jumpered to A0) wakes it.
Notecard Cell+WiFi (NOTE-MBGLW)1Cellular with WiFi fallback. See the NOTE-MBGLW datasheet for radio and power specifications. With cellular, the patient has no network to configure, which is why cellular-first is the deployment model that fits the people this device is for.
Blues Mojo1Coulomb counter for validating the sleep/wake power budget on the bench before patient deployment. You don't need it in production. See §9.
Adafruit #819 Micro Switch w/Roller Lever, ×77One switch per compartment lid, mounted in the pillbox base so the roller lever engages the closed lid. Wire C to the digital pin and NO to GND, and leave NC unconnected. When the lid is open, the host's internal pull-up holds the pin HIGH. The closed lid depresses the lever and pulls the pin LOW.
Cellular and WiFi u.FL antenna leads (included with NOTE-MBGLW)1 setThe cellular and WiFi u.FL flex-antenna leads both ship inside the NOTE-MBGLW retail box, so an indoor pillbox installation doesn't need a separate antenna. Attach each lead to the matching u.FL connector on the Notecard face and lay it flat inside the enclosure, away from the LiPo. See §5.
Adafruit #328 Lithium Ion Polymer Battery, 3.7 V 2500 mAh, JST-PH connector1Plugs straight into the Notecarrier CX LIPO JST connector. At the expected power budget (~5–15 mAh/day), a 2500 mAh cell runs for months between charges.
EZY DOSE Contoured Weekly Pill Planner (Apothecary Products, part #67790)1The patient-facing medication organizer, with seven clear-lidded compartments, one for each day of the week. Pharmacies and medical supply retailers sell it under the EZY DOSE brand.
Hammond 1591SSBK ABS project enclosure, 110 × 82 × 44 mm1Houses the Notecarrier CX and LiPo. It mounts as a sidecar beside or under the pill organizer, and only the micro-switch wiring leads pass into the pill tray body.

All Blues hardware ships with an active SIM that includes 500 MB of data and 10 years of service, with no activation fees and no monthly commitment.

5. Wiring and Assembly

Wiring and assembly: seven micro-switches on D5, D6, D9–D13 with NO terminals to GND; ATTN → A0 jumper so the Notecard can wake the host from STOP2; LiPo on JST; cellular + WiFi u.FL leads

The Notecard Cell+WiFi (NOTE-MBGLW) seats in the Notecarrier CX's M.2 connector and runs off the same VBAT rail. All of the host I/O comes out on the Notecarrier CX dual 16-pin header.

Micro-switch wiring (all 7 compartments are wired the same way):

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, and leave NC unconnected. Mount the switch so the closed lid presses the roller lever down, which connects C to NO and pulls the pin LOW. When the lid opens, the lever releases and the C–NO circuit opens, so the host's internal pull-up holds the pin HIGH. You don't need any external resistors.

CompartmentDay LabelNotecarrier CX Pin
1SUND5
2MOND6
3TUED9
4WEDD10
5THUD11
6FRID12
7SATD13

Pin-by-pin:

  • D5, D6, D9, D10, D11, D12, D13 (the seven available GPIOs) → the C (Common) terminal of each micro-switch. The Notecarrier CX header has no D7 or D8, so the usable GPIOs are D5, D6, and then D9–D13.
  • GND (any GND pin on the header) → the NO (Normally Open) terminal of each micro-switch.
  • NC terminal of each switch → leave unconnected.
  • No external resistors are required, because all the 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. 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 that the Blues sample uses. The firmware sets CX_ATTN_PIN to A0 before it includes cx_sleep.h. The Notecard pulls ATTN low while the host sleeps and raises it when the next poll is due, and that 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 instead of putting the host to sleep. Putting a Host to Sleep Between Sensor Readings walks through the full pattern.

Switch and actuator placement:

Mount each switch flush in the pill tray base, directly under the lid's path, with the roller lever pointing up toward where the lid travels. Position it so the lid rim or a small actuator nub presses the roller down 0.5–1 mm when the lid is fully closed. That's enough for a clean snap-action transition without keeping the lid from closing all the way. With the lid fully closed the pin should read ~0 V (LOW), and with the lid open it should read ~3.3 V (HIGH from the internal pull-up). Check it with a multimeter or the serial monitor before final assembly.

LiPo battery:

Plug the LiPo's JST-PH connector straight 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. That way Mojo measures the total current draw of the Notecarrier, Notecard, and sensors together.

Antennas:

The cellular and WiFi u.FL antenna leads both ship in the NOTE-MBGLW retail box (see BOM), so you don't need to buy antennas separately. Attach each one to its matching u.FL connector on the Notecard face, and lay 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 beside or under the pill organizer tray with two-sided foam tape or a 3D-printed bracket. The enclosure and tray move together as one unit, and only the micro-switch wiring leads cross the enclosure wall.

Drill or punch seven 4–5 mm pass-through holes in the enclosure wall that faces the pill tray, one for each compartment's lead pair. Thread each pair through its own hole with a rubber grommet, or run a bead of hot-melt adhesive around the wire for strain relief before the lead enters the enclosure. Without strain relief, a tug on a lead from the pill-tray side can pull the header-pin connection loose.

Mount each micro-switch body inside the tray base with two M2 self-tapping screws through the switch's mounting holes, or hold it in place with an adhesive patch or a 3D-printed bracket. Line the roller lever up with the inner face of the lid rim. The lever should be pressed down 0.5–1 mm when the lid is fully closed and spring back freely to its neutral position when the lid opens. Check it 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.

Hold the LiPo in place inside the enclosure with a strip of hook-and-loop tape (Velcro) or a foam-padded retaining clip, so the JST connector isn't under mechanical strain. Lay the battery's flat face against the enclosure base, and route the lead where the enclosure lid can't pinch it when it closes.

6. Notehub Setup

  1. Create a project. Sign up at notehub.io and create a project. Copy the ProductUID, which looks like com.your-company.your-name:pillbox.

  2. Set the ProductUID in firmware. Open cellular_medication_adherence_pillbox.ino and replace the empty string on the #define PRODUCT_UID "" line with your value.

  3. Claim the Notecard. Power up the assembled unit. The Notecard associates itself with your Notehub project on its first cellular connection, so there's no manual claim step. The device shows up in your project's Devices tab within a minute or two.

  4. Create a Fleet per care group. Fleets and Smart Fleets are how Notehub groups devices that share configuration. One fleet per care coordinator or prescribing physician is a natural fit, because then you can tune environment variables (alert windows, sync cadence) once and have them apply to all of that clinician's patients' devices at the same time.

    warning

    Privacy Note. For an RPM deployment, patient identifiers should never appear in Note body payloads. Note bodies travel in plaintext through any downstream route and may be stored in third-party systems. Patient identifiers also shouldn't go in Notehub device metadata (tags or the serial-number field). Neither Note bodies nor device metadata are designed to store protected health information, and any PHI placed there would sit outside a controlled covered system. Instead, give the Notehub device tag or serial-number field only an opaque, non-PHI deployment identifier (for example, a random UUID or a clinic-assigned device code). Keep 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 you proceed.

  5. Set environment variables. In Notehub, go to Fleet → Environment. Every variable below is optional, and the firmware defaults apply to any you leave out. The device pulls updates on its next inbound sync (every 2 hours by default), so you don't need to reflash.

    VariableDefaultPurpose
    poll_interval_sec30Seconds between host wakes for compartment polling. Lower values cut open-detection latency. The firmware ignores values below 15 or above 3600 and keeps the current interval.
    summary_hour_utc0The UTC hour (0–23) on the following day at or after which the firmware queues the previous UTC day's adherence summary for transmission. 0 queues the summary as soon as midnight UTC passes. Set it to match the care coordinator's morning review time (for example, 7 for 7 AM UTC).
    outbound_min720Minutes between Notecard outbound syncs. This value never delays immediate open events, because they use sync:true. Changing it automatically reapplies hub.set.
    inbound_min120Minutes between Notecard inbound syncs, which sets how often environment variables are fetched. Changing it automatically reapplies hub.set.
  6. 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 for pill_summary.qo (to a long-term analytics store or dashboard). Because the two Notefiles are split at the source, the real-time alert path doesn't depend on the analytics path at all.

What you should see in Notehub

The Events tab should start filling in within a minute of first power-on. Four event types matter for this project:

  • _session.qo: automatic Notecard housekeeping on each cellular session. These events are the fastest way to confirm the radio is reaching Notehub. If you don't see any _session.qo events in the first 2–3 minutes, check that PRODUCT_UID matches your Notehub project exactly and that you have cellular coverage.

  • pill_open.qo: one Note per detected lid-open event. Detection takes up to 30 seconds (until the next poll wake), and establishing a Notecard cellular session then adds roughly 15–60 seconds. From the physical open to Notehub receiving it, the end-to-end window is typically under two minutes. For this to be reliable, the lid must stay open (or be re-opened) until the next poll fires. A lid that's opened and re-closed within the same 30-second interval isn't detected. Each open of the same compartment lid in one day produces its own Note. The body looks like:

    {
      "compartment": 4,
      "label": "WED",
      "day_opens_mask": 15,
      "opened_this_poll": 8
    }

    day_opens_mask is a 7-bit bitmask of every compartment opened so far today (including this one). That makes each event body self-contained, so you can rebuild the full picture from a single Note. Bit positions map directly to compartment numbers (bit 0 = compartment 1 = SUN, bit 6 = compartment 7 = SAT). opened_this_poll is the bitmask of compartments detected open in this particular 30-second polling wake. When it has several bits set, several compartments were opened at the same time (or at least within the same polling interval). That's a strong sign of a weekly tray refill rather than a routine single-dose open. Downstream route logic can threshold on the bit count of opened_this_poll to tell refill clusters apart from individual dose events, and §10 has the full discussion.

  • pill_summary.qo: one Note per UTC day. It's queued at the summary_hour_utc threshold (midnight UTC by default) on the day after each data-collection period, then delivered on the next Notecard sync session. That can be the scheduled outbound sync or any earlier sync triggered by a pill_open.qo event. The body looks like:

    {
      "opens_mask": 31,
      "opens_count": 5
    }

    opens_mask uses the same bitmask convention as pill_open.qo: bit 0 = Sunday (compartment 1), bit 6 = Saturday (compartment 7). opens_count = 0 means the device detected no compartment opens that day. Either the patient missed all doses, or every open was too brief for a poll to catch (see §7.3). The full:true flag on note.add makes sure that zero survives the template's omitempty suppression.

  • pill_diag.qo: a diagnostic Note that only shows up on pending-event queue overflow. When the 28-entry retry ring buffer fills and the oldest queued open event gets evicted, the device sends error: "pending_overflow" with a dropped count. Once the queue drains, a matching error: "pending_overflow_cleared" Note follows with the total dropped count for the episode. The firmware retries that second Note until the Notecard accepts it. The first one is a single attempt made while the Notecard is already failing, so it may never arrive. These Notes appear in both bench and production modes, and they mean adherence data was lost while the Notecard couldn't accept note.add requests. If you need data-integrity alerting, route pill_diag.qo alongside pill_open.qo.

    A host that never sleeps (because the ATTN→A0 jumper is missing, or the firmware was built with USB support enabled) is not reported through this Notefile. The firmware just stays awake, waits out the poll interval, and keeps polling. Adherence data isn't affected, and Notehub sees a normal _session.qo cadence. To catch this fault, use the Mojo trace (§9) and watch for the [sleep] ATTN sleep failed line on the debug serial output described in §7.1.

7. Firmware Design

The firmware is split into a main sketch, two helper files, and a host-sleep header, all in firmware/cellular_medication_adherence_pillbox/:

  • cellular_medication_adherence_pillbox.ino: setup() (one-time bring-up) and loop() (one poll cycle per wake), which hold 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, covering the card.attn request, STOP2 entry, and ATTN wake.

7.1 Installing and flashing

Dependencies:

  • Arduino core for STM32, from stm32duino/Arduino_Core_STM32. Install it through the Arduino Boards Manager. Add the index URL https://github.com/stm32duino/BoardManagerFiles/raw/main/package_stmicroelectronics_index.json under File → Preferences → Additional Boards Manager URLs, then search for "STM32 MCU based boards" and install it. The Notecarrier CX's host uses the Cygnet board definition (see the build settings below).
  • Blues Wireless Notecard library (note-arduino). Install it from the Arduino Library Manager (search for "Blues Wireless Notecard") or run arduino-cli lib install "Blues Wireless Notecard". The note-arduino releases page has the changelog and any newer stable versions.
  • STM32duino Low Power library (and its dependency STM32duino RTC), from stm32duino/STM32LowPower. It provides the STOP2 entry and ATTN-pin wake that cx_sleep.h uses.

Build settings: The Notecarrier CX's host is the same STM32L433 as the Blues Cygnet, so select Blues boards → Cygnet (pnum=CYGNET) and set USB support (if available) to None (usb=none). If the USB CDC stack is enabled and no USB host is 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, which an ST-LINK V3 exposes as a virtual COM port (see Serial Logging With STLINK).

Flashing with the Arduino IDE: open cellular_medication_adherence_pillbox.ino, apply the build settings above, and hit Upload. With the Notecarrier CX's USB-C DIP switch in HST, you can reach the host's DFU bootloader over the USB-C port (hold BOOT, tap RST). You can also upload over SWD with an ST-LINK on the debug jack.

Flashing with arduino-cli: run these 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.ino

The sketch.yaml in the firmware directory sets the same board options, so arduino-cli compile picks them up there even without -b.

After uploading, open a serial monitor on the ST-LINK virtual COM port at 115200 baud. USB CDC is disabled, so nothing shows up on the Notecarrier CX's own USB-C port. On first boot, the note-arduino debug stream echoes the Notecard request/response exchange for hub.set and note.template. The firmware only prints [init] ... lines when one of those steps fails (for example, [init] hub.set failed — will retry next wake). After that the device goes quiet as it enters its first 30-second sleep cycle. Once the Notecard has valid time, you'll also see one [day] first time-sync utc_day=N line. Open a compartment lid, and the next wake prints [open] compartment=N (DAY) day_mask=0bXXXXXXX.

How the host sleeps. sleepHost() calls cxSleepUntilAttn() (in cx_sleep.h). That function asks the Notecard to hold ATTN low for poll_sec seconds, confirms the pin has gone low, and puts the host into STM32 STOP2. When the Notecard raises ATTN, the rising edge on A0 wakes the host. Since the host stays alive through all of this, the card.attn call is a checked request instead of a fire-and-forget command. If the Notecard doesn't accept it, or ATTN never goes low because the jumper is missing, the firmware logs [sleep] ATTN sleep failed — waiting out the poll interval awake. It then waits out one poll interval with delay() and tries again on the next cycle. The pillbox keeps working, but the host draws active current the whole time.

7.1a Host sleep critical Note

Before you deploy to a battery-powered Notecarrier CX, verify that the host is actually sleeping. That means the ATTN→A0 jumper is fitted (§5) and the firmware was built with USB support set to None. If either one is missing, the host stays awake between polls and drains the LiPo continuously. Either the firmware falls back to a software delay, or the STM32's USB wakeup interrupt pulls the host out of STOP2 within a millisecond. On the bench, you'll see this 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

ResponsibilityFunction
Notecard configuration (hub.set; the accelerometer is disabled once in setup())initNotecard()
Note template registration for pill_summary.qodefineTemplates()
Environment variable fetch and range checkfetchEnvOverrides()
Compartment GPIO pin samplingsampleCompartments()
Immediate open-event emissionemitOpenEvent()
Daily adherence summary emissionemitDailySummary()
UTC time query for day-rollover detectionutcDayAndHour()
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 masks that with the inverse of the prev_pin_mask stored in PillboxState (cur_mask & ~prev_pin_mask) to compute a newly_opened byte. Each bit set in newly_opened is a pin that went from LOW (closed) to HIGH (open) since the last wake. Every detected rising edge generates a pill_open.qo event. If the same compartment lid opens several times in a day, each open gets its own Note, so downstream systems can line up 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. Each pill_open.qo body carries a day_opens_mask field, a running snapshot of that bitmask at the moment of the event, which makes every event payload self-contained. A second field, opened_this_poll, records which compartments were detected open in this particular wake. When it has several bits set, downstream systems get a direct signal that several lids were opened at the same time, which is consistent with a weekly tray refill rather than a single dose open.

Important polling limitation. The firmware compares pin state at discrete 30-second boundaries. A lid that's opened and fully re-closed within a single 30-second interval between wakes produces no rising edge and no event. That open is silently missed, and it isn't counted in daily_opens. This matters for adherence monitoring. A patient who opens a compartment, tips out a pill, and closes the lid may be done in a few seconds, and an open that short is caught only if a poll happens to land while the lid is up. The odds are roughly the open time divided by the poll interval, so a 5-second open has about a 1-in-6 chance of being seen at the default 30 seconds (1 in 3 at the 15-second minimum). A missed open looks exactly like a missed dose, in both pill_open.qo and the daily summary. The minimum configurable poll interval is 15 seconds (poll_interval_sec environment variable, with a firmware-enforced floor). If you need guaranteed sub-second open detection, the design would need interrupt-driven GPIO or a latch-based circuit that captures the open state and holds it until the next poll.

The maximum detection latency, from "lid physically opened" to "event queued for transmission," equals the configured poll interval (30 seconds by default). Getting from the queue to Notehub then adds the Notecard's cellular session-establishment time, usually 15–60 seconds, so the real-world end-to-end window is typically under two minutes.

7.4 Event payload design

pill_open.qo is left untemplated (free-form JSON). Even with a Note for every open, open events are low-frequency: a typical patient opens each lid once or twice per day, for a total well under 20 Notes/day, so the wire overhead is negligible. Leaving it untemplated keeps each event easy to read in Notehub and skips template management it doesn't need. The opened_this_poll field carries the bitmask of every compartment detected open in the same polling wake as this event. When several bits are set, downstream systems can use it to tell a weekly refill session (several lids opened at once) apart from a routine single-compartment dose open.

pill_summary.qo is templated, with TUINT8 (1-byte unsigned integer) for both fields. Registering the template tells the Notecard to store each daily summary as a 2-byte fixed-length record instead of a variable-length JSON string. If cellular connectivity drops out for a while, that keeps the on-device queue compact, even across a full month of data.

// 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 keeps opens_count: 0 in the Note, even though the template would normally suppress zero-valued fields. A day when the patient opened zero compartments is exactly the signal a care coordinator needs.

7.5 Low-power strategy

The Notecarrier CX's STM32L433 host sleeps in STOP2 between wakes and draws a few microamps with its RAM intact. So each 30-second cycle is ~100–200 milliseconds of active host execution, followed by ~29.8 seconds with the host drawing ~1–2 µA.

setup() runs once at power-up, and each pass through loop() is one poll cycle. At the end of a cycle, sleepHost() calls cxSleepUntilAttn() (in cx_sleep.h). That sends a card.attn sleep request for poll_sec seconds, waits for the Notecard to pull ATTN low, and enters STOP2. When the interval is up, the Notecard raises ATTN. The rising edge on A0 wakes the host, and execution picks up on the next line of loop(). The host resumes in place instead of cold-booting, so the PillboxState struct (previous pin mask, daily bitmask, UTC day, and pending-event queue) just lives in RAM, and nothing gets serialized to the Notecard. To the firmware author, sleeping is a single function call, and to the patient the device simply looks like it's always on.

The Notecard itself stays powered the whole time and idles at about 8–18 µA between cellular sessions. A session starts on a sync:true open event or when the scheduled outbound timer fires for queued summary Notes. It draws several hundred milliamps while it transmits (usually for under 30 seconds) and then drops back to idle.

7.6 Retry and error handling

  • The firmware re-applies hub.set and note.template on every wake, so a failure on one wake can't leave the device unassociated with Notehub or sending untemplated Notes.
  • fetchEnvOverrides() uses requestAndResponse() and guards against a NULL response. If an env fetch fails, the current state values stay as they are, instead of the firmware crashing or zeroing thresholds.
  • If utcDayAndHour() returns 0 (the Notecard hasn't synced yet to get a valid time), the firmware skips the day-rollover branch entirely. Any opens from before time-sync stay in daily_opens and get associated with the first valid UTC day once time is available. They only move into prev_day_opens at the first actual day rollover, and from there they feed the next end-of-day summary.
  • The host resumes in place after each sleep, so there's no state-restore step that can fail. If the LiPo dies, the host cold-boots into setup(), which zeroes PillboxState, re-reads the initial pin state, and reconfigures the Notecard. That's the same first-boot path as a fresh power-on.
  • If cxSleepUntilAttn() can't put the host to sleep (the Notecard doesn't accept the request, or ATTN never goes low because the jumper is missing), sleepHost() waits out the poll interval awake and tries again on the next cycle. Either way, the polling cadence holds.
  • emitOpenEvent() failure and retry queue. When a note.add fails after all three attempts, enqueuePendingEvent() stores the event (compartment index, day mask, poll mask) in a 28-entry ring buffer inside PillboxState. On every wake after that, replayPendingOpenEvents() retries each buffered event before sampling new opens. Records that replay successfully are removed, and the queue is compacted. If the queue fills before the Notecard starts accepting note.add again, the oldest entry is evicted and the firmware makes one attempt to add a pill_diag.qo Note (sync:true) with error: "pending_overflow" and a dropped: 1 count. That attempt isn't retried, and it goes to the same Notecard that's already failing note.add, so it may be lost too. Once the queue fully drains, a second pill_diag.qo with error: "pending_overflow_cleared" and the cumulative drop count closes the episode and confirms how many pill_open.qo events are missing. The firmware keeps the drop count in RAM and retries this second Note on every wake until the Notecard accepts it, so it's the one to rely on. A reset or a dead LiPo before then clears the count, and the episode goes unreported. The 28 slots cover four consecutive worst-case 7-compartment wakes, and a sustained Notecard failure longer than that loses adherence events. See §10.

7.7 Key code snippet 1 — template definition

TUINT8 is defined in note-c as the integer constant 21, and it tells the Notecard to encode each templated field as a 1-byte unsigned integer (0–255). The template registers that encoding with the Notecard's on-device compression layer, so every pill_summary.qo Note is stored as a fixed 2-byte binary record instead of variable-length JSON. If cellular connectivity is unavailable for a while, the compression keeps the on-device queue compact across a full month of data. Downstream integrations still get JSON, because Notehub decodes these records back to JSON when they arrive.

NOTEFILE_SUMMARY is "pill_summary.qo", defined in the helpers header. defineTemplates() runs on every wake and checks the response, so a template that fails to register is simply tried again on the next wake.

void defineTemplates() {
    // ...
    J *req = notecard.newRequest("note.template");
    if (!req) return;
    JAddStringToObject(req, "file", NOTEFILE_SUMMARY);
    JAddNumberToObject(req, "port", 50);
    J *body = JAddObjectToObject(req, "body");
    JAddNumberToObject(body, "opens_mask",  TUINT8); // 7-bit bitmask (bits 0–6)
    JAddNumberToObject(body, "opens_count", TUINT8); // 0–7 integer count
    J *rsp = notecard.requestAndResponse(req);
    if (rsp) {
        if (notecard.responseError(rsp)) {
            // ...
        }
        notecard.deleteResponse(rsp);
    }
    // ...

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 up right away. emitOpenEvent() adds it to NOTEFILE_OPEN ("pill_open.qo") with the 1-based compartment number, the day label, the running daily bitmask (day_mask), and the bitmask of compartments opened in this wake (poll_mask). It makes up to three checked attempts, and a false return sends the event to the retry queue described in §7.6.

bool emitOpenEvent(uint8_t idx, uint8_t day_mask, uint8_t poll_mask) {
    const uint8_t MAX_ATTEMPTS = 3;
    for (uint8_t attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
        J *req = notecard.newRequest("note.add");
        // ...
        JAddStringToObject(req, "file", NOTEFILE_OPEN);
        JAddBoolToObject  (req, "sync", true);
        J *body = JAddObjectToObject(req, "body");
        JAddNumberToObject(body, "compartment",      (int)(idx + 1)); // 1–7
        JAddStringToObject(body, "label",            kDayLabel[idx]);
        JAddNumberToObject(body, "day_opens_mask",   (int)day_mask);
        JAddNumberToObject(body, "opened_this_poll", (int)poll_mask);

        J *rsp = notecard.requestAndResponse(req);
        // ...
        bool ok = !notecard.responseError(rsp);
        notecard.deleteResponse(rsp);
        if (ok) {
            // ...
            return true;
        }
        // ...
    }
    // ...
    return false;
}

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. Because the host stays alive, the firmware checks the result of the card.attn request instead of treating it as fire-and-forget. The Notecard's ATTN pin brings the host back on schedule, and loop() carries on with the next poll with PillboxState still in RAM.

The rest of the setup lives in the .ino. It defines CX_ATTN_PIN as A0 before it includes cx_sleep.h, because all seven D pins are compartment inputs, and setup() calls cxSleepBegin() once to arm the ATTN wake. After that, loop() ends every pass by calling sleepHost(), shown here. log is the debug serial stream, or NULL when debug output is compiled out.

void sleepHost(PillboxState &s) {
    // ...
    if (!cxSleepUntilAttn(notecard, s.poll_sec, NULL, log)) {
        // The Notecard didn't take the sleep request, or ATTN never went low
        // (check the ATTN -> A0 jumper). Keep the poll cadence and try again.
        // ...
        delay(s.poll_sec * 1000UL);
    }
}

8. Data Flow

Data flow

Collected. Every 30 seconds: a 7-bit bitmask of compartment lid states, compared against the previous sample. The firmware only transmits on a state transition, not on every wake.

Transmitted.

  • pill_open.qo: one Note per detected lid-open event (the lid must be open when the 30-second poll fires), sent immediately (sync:true). It contains the compartment number (1–7), its day label, the running daily bitmask of every compartment opened so far today (day_opens_mask), and a per-poll bitmask of every compartment detected open in this wake (opened_this_poll). Several bits set in opened_this_poll point to a likely refill session. Each open of the same compartment generates its own Note, as long as the lid is open at a poll boundary each time.
  • pill_summary.qo: one template-encoded Note per UTC day. It's queued at the configured summary_hour_utc (midnight UTC by default) on the day after each data-collection period, then delivered on the next Notecard sync session, whether that's the scheduled outbound sync or an earlier one triggered by a pill_open.qo event. It contains opens_mask (which compartments opened) and opens_count (how many), and it uses full:true to keep a zero count when the patient opened no compartments.

Routed. Both Notefiles go to Notehub, and from there to whatever downstream systems the project's routes specify. The two filenames are kept separate on purpose, so pill_open.qo can go out to a real-time alert channel (SMS, pager, care platform webhook) while pill_summary.qo goes to a long-term store on a different cadence.

Alerts trigger on.

  • Any pill_open.qo event, meaning a compartment was opened. Route this to whatever real-time channel the care coordinator uses. Keep in mind that weekly tray-refill sessions produce events identical to patient dose-taking opens. §10 covers how to tell them apart operationally.
  • An expected pill_open.qo that hasn't arrived within a configurable time window, meaning the patient hasn't opened their morning compartment. This logic lives in the downstream route or dashboard, not in the firmware.
  • pill_summary.qo with opens_count: 0: the patient missed all doses for the day, or every open that day was too brief for a poll to catch.

9. Validation and Testing

Expected cadence after deployment. In steady state, a patient who takes medication once a day generates one pill_open.qo event and one pill_summary.qo each day. A device that's working correctly should still send a daily pill_summary.qo with opens_count: 0 on a day when no lids are opened.

Bench functional test. The firmware fires an open event on a LOW→HIGH rising edge, when the pin goes from LOW (actuator pressed, switch closed, lid closed) to HIGH (actuator released, switch open, lid open). A common mistake is holding the switch actuator down and expecting an open event. That simulates a closed lid, not an open one. Here's the correct procedure:

  1. Establish a closed baseline. Press the switch actuator (or install the switch so the lid presses it) so the pin reads LOW. Let at least one 30-second poll fire while you hold the actuator closed, which records the LOW state in prev_pin_mask for that compartment. For 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 it captures the LOW right away without waiting for a second poll.
  2. 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, which is the LOW→HIGH transition the firmware detects as a lid-open event. The serial monitor should print [open] compartment=N (DAY) day_mask=0bXXXXXXX.
  3. Verify in Notehub. Confirm that the matching pill_open.qo event shows up in the Events tab within roughly two minutes (up to 30 seconds of poll latency, plus cellular session establishment).

If you press and release the actuator before the next poll fires, no event is generated. Both transitions happen within the same interval, so the firmware sees no net change. That's expected behavior, not a fault.

Quick continuity check (no Notehub required). Put a multimeter on the pin header, or watch the serial output, while you press and release the switch actuator by hand. Pressing it should pull the pin to ~0 V, and releasing it should let the pin float to ~3.3 V (internal pull-up active). If it's reversed, the usual causes are the wrong header pin, swapped NO and NC terminals, a missing ground connection, an actuator that isn't engaging the lid properly, or an internal pull-up that isn't active. Check against the wiring table in §5.

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). It reaches Notehub on the next Notecard sync session. That's either the next scheduled outbound sync (every 12 hours by default) 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 on the bench for 24 hours.

Notecard-datasheet figures (from the NOTE-MBGLW datasheet and the low-power design guide):

PhasePublished 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, so validate it with Mojo on your own bench assembly before you commit to a deployment charge schedule):

PhaseEstimated draw
Host MCU active (~150 milliseconds per 30-second wake)~10 mA for ~150 milliseconds every 30 seconds (~50 µA average contribution)
Cellular session for an open-event sync (sync:true, ~2–3 per day)~250 mA average, ~20–30 seconds per session
Cellular session for a scheduled outbound sync (every 12 h)~250 mA average, ~15–20 seconds per session

With 30-second polling, 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 that as an estimated starting point until a bench trace confirms it.

Quick runway estimate: With a 2500 mAh LiPo at 5–15 mAh/day, expect about 5–16 months of continuous operation between charges (165–500 days). A real patient deployment sees variable cellular signal and temperature swings, so always validate with a 24-hour Mojo trace before you set a charge schedule. Poor cellular signal or frequent refill sessions will push daily consumption up.

A good Mojo trace on this device has a nearly flat, near-zero baseline with brief millisecond blips every 30 seconds (host wake) and occasional 15–30-second bursts at hundreds of milliamps (cellular session). If the baseline sits at 10–80 mA continuously, the host isn't 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 happen much more often than you expect, verify that sync:true is only set on open events and that the outbound cadence isn't set to a very low value.

For a 2500 mAh LiPo at an estimated 5–15 mAh/day, the modeled runtime is roughly 165–500 days between charges. Actual runtime varies with cellular signal strength, session frequency, and temperature, so confirm it with bench data before committing to a deployment charge schedule.

Mojo is a bench and commissioning tool for this project, and it doesn't go to the patient's home. Once a firmware revision passes the trace check, the deployed units run on the LiPo without it.

Troubleshooting

SymptomLikely causeWhat to check
Device never appears in the Notehub Devices tab.PRODUCT_UID is empty or wrong, or the cellular antenna is disconnected.Make sure PRODUCT_UID matches the Notehub project exactly. Check that the antennas are attached and not coiled tightly against the LiPo. If you're testing indoors, move the device near a window.
Device appears but no pill_open.qo events show up after opening a lid.A micro-switch mounting or wiring problem, or a pin wired to the wrong header position.Open the serial monitor at 115200 baud and watch for [open] lines. If none appear, check pin continuity with a multimeter. The 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.Probably a wiring fault that leaves the pin floating high, or a lid that physically moved between the power-on pin snapshot and the next 30-second wake.Check the affected compartment pin for loose wiring or the wrong terminal (NC connected instead of NO). If it keeps happening after you've checked the 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 isn't available yet (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. That's the scheduled outbound sync (every 12 hours by default), 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 wasn't applied.Check that 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 happened yet (every 2 hours by default).Click Notehub's Sync Now (inbound) button on the device page to trigger an immediate inbound sync. Or lower inbound_min in the fleet environment to 15. Once the device picks up that change, it re-applies hub.set automatically, which tightens the fetch cadence for later 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 you're 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 and 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 because 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 couldn't accept note.add requests, and the oldest buffered adherence events were evicted and lost.Look for gaps in _session.qo events, which point to the Notecard itself being down. A cellular outage alone doesn't fill this buffer, because note.add works offline. The dropped field on the pending_overflow_cleared Note shows how many pill_open.qo events are missing in total. Once the Notecard recovers, the queue drains on its own, 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

This design stops at "did a lid open?" on purpose. That's 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. They belong in a follow-on design, though, where they won't dilute the cellular-first sensor this POC is proving out.

Simplified for the POC

Each item below is a deliberate scope choice. Once a real RPM program starts running this device, a production deployment will answer each one with a new sensor, a downstream workflow, or a tighter data-handling control. This is a proof-of-concept reference design, not a medical device.

Cannot confirm a dose was actually taken. The device reports each compartment lid open it detects, but it can't tell "opened and took the pill" from "opened and closed without taking it." Confirming a dose takes a weight sensor on the tray or patient self-report. Both are straightforward extensions, but they add hardware or UX complexity that's outside the scope of this POC.

Caregiver and pharmacist refill sessions are indistinguishable from patient dose-taking opens. Whoever loads the weekly tray (the patient, a caregiver, or a pharmacist) opens each compartment lid to put pills in. Each of those opens generates pill_open.qo events and sets bitmask bits exactly as a genuine dose-taking open would. 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. That's a more active distortion than the dose-confirmation limitation above, because it inflates the adherence record instead of 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 gets loaded at a predictable time (for example, 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, and the patient-record view excludes those event clusters from adherence calculations or tags them.
  • Filter simultaneous multi-lid opens downstream. Several bits set in a single 30-second poll (opened_this_poll containing ≥ 3 compartments at once) are a strong operational sign 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 instead of 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 (every 30 seconds by default). A lid that's opened and fully re-closed within a single poll interval generates no event and isn't counted in the daily summary. This applies to the intended use case too. A patient can open a compartment, take the pill, and close the lid in a few seconds, and an open shorter than the poll interval is caught only if a poll happens to land during it. Each missed open undercounts adherence, and a day where every open was missed reports opens_count: 0, which reads as a day of missed doses. Dropping poll_interval_sec to the firmware-enforced minimum of 15 seconds halves the exposure window, and interrupt-driven or latch-based hardware would eliminate it entirely.

Pending retry queue has a finite depth. If the Notecard can't 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. When overflow first happens, the device makes one attempt to send a pill_diag.qo Note with error: "pending_overflow" and a dropped count, which may be lost because the Notecard is already failing. A matching error: "pending_overflow_cleared" Note follows when the queue drains, and the firmware retries it until the Notecard accepts it, so the data loss leaves visible evidence in the cloud. A Notecard outage long enough to exhaust the buffer is uncommon on a battery-backed Notecard Cell+WiFi. In the worst case, though, 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 nobody misses these episodes.

7-compartment design only. The Notecarrier CX exposes exactly seven digital I/O pins (D5, D6, D9–D13), which map neatly onto a standard 7-day tray. A 14-compartment tray (AM/PM for each day) would need an I2C GPIO expander like the MCP23017, which adds one part and a library dependency.

No on-device time zone awareness. The adherence day rolls over at UTC midnight, which is 7 PM local time for a patient on Eastern Standard Time, so a dose taken after 7 PM lands in the next day's summary. Setting summary_hour_utc to 5 doesn't move that boundary. It only holds the previous UTC day's summary until 5 AM UTC (midnight EST) before queuing it. Counting days from the patient's local midnight would take a firmware change, such as a UTC-offset environment variable applied to the day calculation in utcDayAndHour(). Each pill_open.qo event still carries its own timestamp, so downstream systems can regroup opens by local day.

Micro-switch placement is manual. Mounting the micro-switches inside a commercial pillbox takes drilling, adhesive, or 3D-printed brackets. A production design would build the switches into a purpose-built tray PCB.

No tamper or battery-low reporting. The device doesn't currently send an alert if someone removes the Notecard or cuts power, or if the LiPo voltage drops below a safe threshold. Monitoring battery voltage with card.voltage and sending a sync:true low-battery event would be a straightforward addition.

No local visual or audible reminder. The device only reports. It doesn't buzz or light up to remind the patient to take their medication. You could add a piezo buzzer on a PWM-capable pin, but it would need a scheduled inbound event from Notehub to trigger it.

Pre-time-sync opens carry no calendar date. On first boot, the device may pick up opens before the Notecard has valid UTC time. Those opens stay in daily_opens and get associated with the first valid UTC day once time is available. At the next actual day rollover they move into prev_day_opens and feed the following summary with the correct bitmask. The summary's timestamp, however, reflects when it was sent, not the (unknown) calendar day the lids were opened. If you need accurate calendar-day attribution for pre-sync opens, wait for a _session.qo event in Notehub (which confirms time-sync) before placing the device with the patient.

Firmware state holds only one pending day's summary data. The PillboxState struct has a single prev_day_opens slot. If two consecutive UTC-day boundaries pass before a pending summary goes out, the slot is overwritten and the earlier day's adherence data is unrecoverable. That can happen if the device is powered off for an entire day, or if a total power loss resets the host and clears the RAM-resident PillboxState. A lack of cellular connectivity alone does not cause this, because the Notecard stores queued Notes locally in its on-device flash and delivers them automatically once connectivity comes back. The risk is power loss before emitDailySummary() runs, so size the LiPo for the intended deployment duration to keep that exposure small.

Patient identity must stay out of Note payloads and Notehub metadata. Notehub and any downstream systems store and route Note bodies as plaintext. Notehub device metadata (tags, serial-number field) isn't 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. Give the Notehub device tag or serial-number field only an opaque, non-PHI deployment identifier (for example, a random UUID or a clinic-assigned device code). Keep 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, have qualified counsel review the complete data path before go-live.

Production Next Steps

Once a real RPM program has the basic monitor running, these extensions move it toward a product you can put in the field.

14-slot tray support through an MCP23017 I2C GPIO expander adds AM/PM slots for each 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 lets downstream routes match events to the correct caregiver. They do it with a patient-record lookup in the downstream covered system, keyed on device UID.

Voltage-variable sync behavior with hub.set voutbound stretches the outbound cadence automatically as the LiPo voltage drops, so the device gets the most runtime out of each charge without anyone stepping in.

Notecard Outboard DFU adds over-the-air host firmware updates, so you can push new features (buzzer support, new alert rules) to deployed devices without a pharmacy recall.

A scheduled inbound Notefile (pill_reminder.qi) would give the Notecard something to 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 shows up on the patient's nightstand pre-provisioned and asks nothing of them. It reports over cellular whether the household's WiFi exists, works, or was forgotten about years ago. For the elderly chronic-disease patients who need this monitoring most, that's the difference between a connected device and one 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
    • What you should see in Notehub
  • 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
    • Troubleshooting
  • 10. Limitations and Next Steps
    • Simplified for the POC
    • Production Next Steps
  • 11. Summary

Resources

  • GitHub
  • Request More Info
Can we improve this page? Send us feedback
© 2026 Blues Inc.
© 2026 Blues Inc.
AboutDocsAPI ReferenceTermsPrivacy